feat: works

This commit is contained in:
2022-07-17 10:11:20 +08:00
parent 4ba63b4c2e
commit 74a202f1ed
458 changed files with 125067 additions and 8 deletions

View File

@@ -0,0 +1,75 @@
[package]
name = "boa_engine"
version = "0.15.0"
edition = "2021"
rust-version = "1.60"
authors = ["boa-dev"]
description = "Boa is a Javascript lexer, parser and Just-in-Time compiler written in Rust. Currently, it has support for some of the language."
repository = "https://github.com/boa-dev/boa"
keywords = ["javascript", "js", "compiler", "lexer", "parser"]
categories = ["parser-implementations", "compilers"]
license = "Unlicense/MIT"
readme = "../README.md"
[features]
profiler = ["boa_profiler/profiler"]
deser = ["boa_interner/serde"]
intl = [
"dep:icu_locale_canonicalizer",
"dep:icu_locid",
"dep:icu_datetime",
"dep:icu_plurals",
"dep:icu_provider",
"dep:icu_testdata",
"dep:sys-locale"
]
# Enable Boa's WHATWG console object implementation.
console = []
[dependencies]
boa_unicode = { path = "../boa_unicode", version = "0.15.0" }
boa_interner = { path = "../boa_interner", version = "0.15.0" }
boa_gc = { path = "../boa_gc", version = "0.15.0" }
gc = "0.4.1"
boa_profiler = { path = "../boa_profiler", version = "0.15.0" }
serde = { version = "1.0.139", features = ["derive", "rc"] }
serde_json = "1.0.82"
rand = "0.8.5"
num-traits = "0.2.15"
regress = "0.4.1"
rustc-hash = "1.1.0"
num-bigint = { version = "0.4.3", features = ["serde"] }
num-integer = "0.1.45"
bitflags = "1.3.2"
indexmap = "1.9.1"
ryu-js = "0.2.2"
chrono = "0.4.19"
fast-float = "0.2.0"
unicode-normalization = "0.1.21"
dyn-clone = "1.0.7"
once_cell = "1.13.0"
tap = "1.0.1"
icu_locale_canonicalizer = { version = "0.6.0", features = ["serde"], optional = true }
icu_locid = { version = "0.6.0", features = ["serde"], optional = true }
icu_datetime = { version = "0.6.0", features = ["serde"], optional = true }
icu_plurals = { version = "0.6.0", features = ["serde"], optional = true }
icu_provider = { version = "0.6.0", optional = true }
icu_testdata = { version = "0.6.0", optional = true }
sys-locale = { version = "0.2.1", optional = true }
[dev-dependencies]
criterion = "0.3.5"
float-cmp = "0.9.0"
[target.x86_64-unknown-linux-gnu.dev-dependencies]
jemallocator = "0.5.0"
[lib]
crate-type = ["cdylib", "lib"]
name = "boa_engine"
bench = false
[[bench]]
name = "full"
harness = false

View File

@@ -0,0 +1,10 @@
# Boa Benchmarks
For each js script in the `bench_scripts` folder, we create three benchmarks:
- Parser => lexing and parsing of the source code
- Compiler => compilation of the parsed statement list into bytecode
- Execution => execution of the bytecode in the vm
The idea is to check the performance of Boa in different scenarios.
Different parts of Boa are benchmarked separately to make the impact of local changes visible.

View File

@@ -0,0 +1 @@
((2 + 2) ** 3 / 100 - 5 ** 3 * -1000) ** 2 + 100 - 8;

View File

@@ -0,0 +1,7 @@
(function () {
let testArr = [1, 2, 3, 4, 5];
let res = testArr[2];
return res;
})();

View File

@@ -0,0 +1,8 @@
(function () {
let testArr = [];
for (let a = 0; a <= 500; a++) {
testArr[a] = "p" + a;
}
return testArr;
})();

View File

@@ -0,0 +1,22 @@
(function () {
let testArray = [
83, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83,
62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93,
17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77,
32, 45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32,
56, 67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234,
23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29,
2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28,
93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83, 62,
99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17,
28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32,
45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56,
67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28,
];
while (testArray.length > 0) {
testArray.pop();
}
return testArray;
})();

View File

@@ -0,0 +1,7 @@
new Boolean(
!new Boolean(
new Boolean(
!new Boolean(false).valueOf() && new Boolean(true).valueOf()
).valueOf()
).valueOf()
).valueOf();

View File

@@ -0,0 +1,15 @@
!function () {
var M = new Array();
for (i = 0; i < 100; i++) {
M.push(Math.floor(Math.random() * 100));
}
var test = [];
for (i = 0; i < 100; i++) {
if (M[i] > 50) {
test.push(M[i]);
}
}
test.forEach(elem => {
0
});
}();

View File

@@ -0,0 +1,10 @@
(function () {
let num = 12;
function fib(n) {
if (n <= 1) return 1;
return fib(n - 1) + fib(n - 2);
}
return fib(num);
})();

View File

@@ -0,0 +1,10 @@
(function () {
let b = "hello";
for (let a = 10; a < 100; a += 5) {
if (a < 50) {
b += "world";
}
}
return b;
})();

View File

@@ -0,0 +1 @@
!function(){var r=new Array();for(i=0;i<100;i++)r.push(Math.floor(100*Math.random()));var a=[];for(i=0;i<100;i++)r[i]>50&&a.push(r[i]);a.forEach(i=>{0})}();

View File

@@ -0,0 +1,5 @@
new Number(
new Number(
new Number(new Number(100).valueOf() - 10.5).valueOf() + 100
).valueOf() * 1.6
);

View File

@@ -0,0 +1,8 @@
(function () {
let test = {
my_prop: "hello",
another: 65,
};
return test;
})();

View File

@@ -0,0 +1,8 @@
(function () {
let test = {
my_prop: "hello",
another: 65,
};
return test.my_prop;
})();

View File

@@ -0,0 +1,8 @@
(function () {
let test = {
my_prop: "hello",
another: 65,
};
return test["my" + "_prop"];
})();

View File

@@ -0,0 +1,5 @@
(function () {
let regExp = new RegExp("hello", "i");
return regExp.test("Hello World");
})();

View File

@@ -0,0 +1,5 @@
(function () {
let regExp = new RegExp("hello", "i");
return regExp;
})();

View File

@@ -0,0 +1,5 @@
(function () {
let regExp = /hello/i;
return regExp.test("Hello World");
})();

View File

@@ -0,0 +1,5 @@
(function () {
let regExp = /hello/i;
return regExp;
})();

View File

@@ -0,0 +1,9 @@
(function () {
var a = "hello";
var b = "world";
var c = a == b;
var d = b;
var e = d == b;
})();

View File

@@ -0,0 +1,6 @@
(function () {
var a = "hello";
var b = "world";
var c = a + b;
})();

View File

@@ -0,0 +1,4 @@
(function () {
var a = "hello";
var b = a;
})();

View File

@@ -0,0 +1,7 @@
new String(
new String(
new String(
new String("Hello").valueOf() + new String(", world").valueOf()
).valueOf() + "!"
).valueOf()
).valueOf();

View File

@@ -0,0 +1,3 @@
(function () {
return Symbol();
})();

View File

@@ -0,0 +1,94 @@
//! Benchmarks of the whole execution engine in Boa.
use boa_engine::{realm::Realm, Context};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))]
#[cfg_attr(
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
global_allocator
)]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
fn create_realm(c: &mut Criterion) {
c.bench_function("Create Realm", move |b| b.iter(Realm::create));
}
macro_rules! full_benchmarks {
($({$id:literal, $name:ident}),*) => {
fn bench_parser(c: &mut Criterion) {
$(
{
static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js"));
let mut context = Context::default();
c.bench_function(concat!($id, " (Parser)"), move |b| {
b.iter(|| context.parse(black_box(CODE)))
});
}
)*
}
fn bench_compile(c: &mut Criterion) {
$(
{
static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js"));
let mut context = Context::default();
let statement_list = context.parse(CODE).expect("parsing failed");
c.bench_function(concat!($id, " (Compiler)"), move |b| {
b.iter(|| {
context.compile(black_box(&statement_list))
})
});
}
)*
}
fn bench_execution(c: &mut Criterion) {
$(
{
static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js"));
let mut context = Context::default();
let statement_list = context.parse(CODE).expect("parsing failed");
let code_block = context.compile(&statement_list).unwrap();
c.bench_function(concat!($id, " (Execution)"), move |b| {
b.iter(|| {
context.execute(black_box(code_block.clone())).unwrap()
})
});
}
)*
}
};
}
full_benchmarks!(
{"Symbols", symbol_creation},
{"For loop", for_loop},
{"Fibonacci", fibonacci},
{"Object Creation", object_creation},
{"Static Object Property Access", object_prop_access_const},
{"Dynamic Object Property Access", object_prop_access_dyn},
{"RegExp Literal Creation", regexp_literal_creation},
{"RegExp Creation", regexp_creation},
{"RegExp Literal", regexp_literal},
{"RegExp", regexp},
{"Array access", array_access},
{"Array creation", array_create},
{"Array pop", array_pop},
{"String concatenation", string_concat},
{"String comparison", string_compare},
{"String copy", string_copy},
{"Number Object Access", number_object_access},
{"Boolean Object Access", boolean_object_access},
{"String Object Access", string_object_access},
{"Arithmetic operations", arithmetic_operations},
{"Clean js", clean_js},
{"Mini js", mini_js}
);
criterion_group!(
benches,
create_realm,
bench_parser,
bench_compile,
bench_execution,
);
criterion_main!(benches);

View File

@@ -0,0 +1,453 @@
//! This module implements the JavaScript bigint primitive rust type.
use crate::{builtins::Number, Context, JsValue};
use boa_gc::{unsafe_empty_trace, Finalize, Trace};
use num_integer::Integer;
use num_traits::{pow::Pow, FromPrimitive, One, ToPrimitive, Zero};
use std::{
fmt::{self, Display},
ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Rem, Shl, Shr, Sub},
rc::Rc,
};
/// The raw bigint type.
pub type RawBigInt = num_bigint::BigInt;
#[cfg(feature = "deser")]
use serde::{Deserialize, Serialize};
/// JavaScript bigint primitive rust type.
#[cfg_attr(feature = "deser", derive(Serialize, Deserialize))]
#[derive(Debug, Finalize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JsBigInt {
inner: Rc<RawBigInt>,
}
// Safety: BigInt does not contain any objects which needs to be traced,
// so this is safe.
unsafe impl Trace for JsBigInt {
unsafe_empty_trace!();
}
impl JsBigInt {
/// Create a new [`JsBigInt`].
#[inline]
pub fn new<T: Into<Self>>(value: T) -> Self {
value.into()
}
/// Create a [`JsBigInt`] with value `0`.
#[inline]
pub fn zero() -> Self {
Self {
inner: Rc::new(RawBigInt::zero()),
}
}
/// Check if is zero.
#[inline]
pub fn is_zero(&self) -> bool {
self.inner.is_zero()
}
/// Create a [`JsBigInt`] with value `1`.
#[inline]
pub fn one() -> Self {
Self {
inner: Rc::new(RawBigInt::one()),
}
}
/// Check if is one.
#[inline]
pub fn is_one(&self) -> bool {
self.inner.is_one()
}
/// Convert bigint to string with radix.
#[inline]
pub fn to_string_radix(&self, radix: u32) -> String {
self.inner.to_str_radix(radix)
}
/// Converts the `BigInt` to a f64 type.
///
/// Returns `f64::INFINITY` if the `BigInt` is too big.
#[inline]
pub fn to_f64(&self) -> f64 {
self.inner.to_f64().unwrap_or(f64::INFINITY)
}
/// Converts a string to a `BigInt` with the specified radix.
#[inline]
pub fn from_string_radix(buf: &str, radix: u32) -> Option<Self> {
Some(Self {
inner: Rc::new(RawBigInt::parse_bytes(buf.as_bytes(), radix)?),
})
}
/// This function takes a string and converts it to `BigInt` type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-stringtobigint
#[inline]
pub fn from_string(mut string: &str) -> Option<Self> {
string = string.trim();
if string.is_empty() {
return Some(Self::zero());
}
let mut radix = 10;
if string.starts_with("0b") || string.starts_with("0B") {
radix = 2;
string = &string[2..];
} else if string.starts_with("0x") || string.starts_with("0X") {
radix = 16;
string = &string[2..];
} else if string.starts_with("0o") || string.starts_with("0O") {
radix = 8;
string = &string[2..];
}
Self::from_string_radix(string, radix)
}
/// Checks for `SameValueZero` equality.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-numeric-types-bigint-equal
#[inline]
pub fn same_value_zero(x: &Self, y: &Self) -> bool {
// Return BigInt::equal(x, y)
Self::equal(x, y)
}
/// Checks for `SameValue` equality.
///
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-numeric-types-bigint-sameValue
#[inline]
pub fn same_value(x: &Self, y: &Self) -> bool {
// Return BigInt::equal(x, y)
Self::equal(x, y)
}
/// Checks for mathematical equality.
///
/// The abstract operation `BigInt::equal` takes arguments x (a `BigInt`) and y (a `BigInt`).
/// It returns `true` if x and y have the same mathematical integer value and false otherwise.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-numeric-types-bigint-sameValueZero
#[inline]
pub fn equal(x: &Self, y: &Self) -> bool {
x == y
}
#[inline]
pub fn pow(x: &Self, y: &Self, context: &mut Context) -> Result<Self, JsValue> {
let y = if let Some(y) = y.inner.to_biguint() {
y
} else {
return context.throw_range_error("BigInt negative exponent");
};
let num_bits = (x.inner.bits() as f64
* y.to_f64().expect("Unable to convert from BigUInt to f64"))
.floor()
+ 1f64;
if num_bits > 1_000_000_000f64 {
return context.throw_range_error("Maximum BigInt size exceeded");
}
Ok(Self::new(x.inner.as_ref().clone().pow(y)))
}
#[inline]
pub fn shift_right(x: &Self, y: &Self, context: &mut Context) -> Result<Self, JsValue> {
if let Some(n) = y.inner.to_i32() {
let inner = if n > 0 {
x.inner.as_ref().clone().shr(n as usize)
} else {
x.inner.as_ref().clone().shl(n.unsigned_abs())
};
Ok(Self::new(inner))
} else {
context.throw_range_error("Maximum BigInt size exceeded")
}
}
#[inline]
pub fn shift_left(x: &Self, y: &Self, context: &mut Context) -> Result<Self, JsValue> {
if let Some(n) = y.inner.to_i32() {
let inner = if n > 0 {
x.inner.as_ref().clone().shl(n as usize)
} else {
x.inner.as_ref().clone().shr(n.unsigned_abs())
};
Ok(Self::new(inner))
} else {
context.throw_range_error("Maximum BigInt size exceeded")
}
}
/// Floored integer modulo.
///
/// # Examples
/// ```
/// # use num_integer::Integer;
/// assert_eq!((8).mod_floor(&3), 2);
/// assert_eq!((8).mod_floor(&-3), -1);
/// ```
#[inline]
pub fn mod_floor(x: &Self, y: &Self) -> Self {
Self::new(x.inner.mod_floor(&y.inner))
}
#[inline]
pub fn add(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().add(y.inner.as_ref()))
}
#[inline]
pub fn sub(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().sub(y.inner.as_ref()))
}
#[inline]
pub fn mul(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().mul(y.inner.as_ref()))
}
#[inline]
pub fn div(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().div(y.inner.as_ref()))
}
#[inline]
pub fn rem(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().rem(y.inner.as_ref()))
}
#[inline]
pub fn bitand(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().bitand(y.inner.as_ref()))
}
#[inline]
pub fn bitor(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().bitor(y.inner.as_ref()))
}
#[inline]
pub fn bitxor(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().bitxor(y.inner.as_ref()))
}
#[inline]
pub fn neg(x: &Self) -> Self {
Self::new(x.as_inner().neg())
}
#[inline]
pub fn not(x: &Self) -> Self {
Self::new(!x.as_inner())
}
#[inline]
pub(crate) fn as_inner(&self) -> &RawBigInt {
&self.inner
}
}
impl Display for JsBigInt {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.inner, f)
}
}
impl From<RawBigInt> for JsBigInt {
#[inline]
fn from(value: RawBigInt) -> Self {
Self {
inner: Rc::new(value),
}
}
}
impl From<Box<RawBigInt>> for JsBigInt {
#[inline]
fn from(value: Box<RawBigInt>) -> Self {
Self {
inner: value.into(),
}
}
}
impl From<i8> for JsBigInt {
#[inline]
fn from(value: i8) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<u8> for JsBigInt {
#[inline]
fn from(value: u8) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<i16> for JsBigInt {
#[inline]
fn from(value: i16) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<u16> for JsBigInt {
#[inline]
fn from(value: u16) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<i32> for JsBigInt {
#[inline]
fn from(value: i32) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<u32> for JsBigInt {
#[inline]
fn from(value: u32) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<i64> for JsBigInt {
#[inline]
fn from(value: i64) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<u64> for JsBigInt {
#[inline]
fn from(value: u64) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<isize> for JsBigInt {
#[inline]
fn from(value: isize) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
impl From<usize> for JsBigInt {
#[inline]
fn from(value: usize) -> Self {
Self {
inner: Rc::new(RawBigInt::from(value)),
}
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct TryFromF64Error;
impl Display for TryFromF64Error {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Could not convert f64 value to a BigInt type")
}
}
impl TryFrom<f64> for JsBigInt {
type Error = TryFromF64Error;
#[inline]
fn try_from(n: f64) -> Result<Self, Self::Error> {
// If the truncated version of the number is not the
// same as the non-truncated version then the floating-point
// number conains a fractional part.
if !Number::equal(n.trunc(), n) {
return Err(TryFromF64Error);
}
match RawBigInt::from_f64(n) {
Some(bigint) => Ok(Self::new(bigint)),
None => Err(TryFromF64Error),
}
}
}
impl PartialEq<i32> for JsBigInt {
#[inline]
fn eq(&self, other: &i32) -> bool {
self.inner.as_ref() == &RawBigInt::from(*other)
}
}
impl PartialEq<JsBigInt> for i32 {
#[inline]
fn eq(&self, other: &JsBigInt) -> bool {
&RawBigInt::from(*self) == other.inner.as_ref()
}
}
impl PartialEq<f64> for JsBigInt {
#[inline]
fn eq(&self, other: &f64) -> bool {
if other.fract() != 0.0 {
return false;
}
self.inner.as_ref() == &RawBigInt::from(*other as i64)
}
}
impl PartialEq<JsBigInt> for f64 {
#[inline]
fn eq(&self, other: &JsBigInt) -> bool {
if self.fract() != 0.0 {
return false;
}
&RawBigInt::from(*self as i64) == other.inner.as_ref()
}
}

View File

@@ -0,0 +1,145 @@
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue},
object::{JsObject, ObjectData},
property::{PropertyDescriptor, PropertyNameKind},
symbol::WellKnownSymbols,
Context, JsResult,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
/// The Array Iterator object represents an iteration over an array. It implements the iterator protocol.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-array-iterator-objects
#[derive(Debug, Clone, Finalize, Trace)]
pub struct ArrayIterator {
array: JsObject,
next_index: usize,
kind: PropertyNameKind,
done: bool,
}
impl ArrayIterator {
pub(crate) const NAME: &'static str = "ArrayIterator";
fn new(array: JsObject, kind: PropertyNameKind) -> Self {
Self {
array,
kind,
next_index: 0,
done: false,
}
}
/// `CreateArrayIterator( array, kind )`
///
/// Creates a new iterator over the given array.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createarrayiterator
pub(crate) fn create_array_iterator(
array: JsObject,
kind: PropertyNameKind,
context: &Context,
) -> JsValue {
let array_iterator = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.array_iterator(),
ObjectData::array_iterator(Self::new(array, kind)),
);
array_iterator.into()
}
/// %ArrayIteratorPrototype%.next( )
///
/// Gets the next result in the array.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%.next
pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let mut array_iterator = this.as_object().map(JsObject::borrow_mut);
let array_iterator = array_iterator
.as_mut()
.and_then(|obj| obj.as_array_iterator_mut())
.ok_or_else(|| context.construct_type_error("`this` is not an ArrayIterator"))?;
let index = array_iterator.next_index;
if array_iterator.done {
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
}
let len = if let Some(f) = array_iterator.array.borrow().as_typed_array() {
if f.is_detached() {
return context.throw_type_error(
"Cannot get value from typed array that has a detached array buffer",
);
}
f.array_length()
} else {
array_iterator.array.length_of_array_like(context)?
};
if index >= len {
array_iterator.done = true;
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
}
array_iterator.next_index = index + 1;
match array_iterator.kind {
PropertyNameKind::Key => Ok(create_iter_result_object(index.into(), false, context)),
PropertyNameKind::Value => {
let element_value = array_iterator.array.get(index, context)?;
Ok(create_iter_result_object(element_value, false, context))
}
PropertyNameKind::KeyAndValue => {
let element_value = array_iterator.array.get(index, context)?;
let result = Array::create_array_from_list([index.into(), element_value], context);
Ok(create_iter_result_object(result.into(), false, context))
}
}
}
/// Create the `%ArrayIteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object
pub(crate) fn create_prototype(
iterator_prototype: JsObject,
context: &mut Context,
) -> JsObject {
let _timer = Profiler::global().start_event(Self::NAME, "init");
// Create prototype
let array_iterator =
JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary());
make_builtin_fn(Self::next, "next", &array_iterator, 0, context);
let to_string_tag = WellKnownSymbols::to_string_tag();
let to_string_tag_property = PropertyDescriptor::builder()
.value("Array Iterator")
.writable(false)
.enumerable(false)
.configurable(true);
array_iterator.insert(to_string_tag, to_string_tag_property);
array_iterator
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,818 @@
#[cfg(test)]
mod tests;
use crate::{
builtins::{typed_array::TypedArrayKind, BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder,
JsObject, ObjectData,
},
property::Attribute,
symbol::WellKnownSymbols,
value::{IntegerOrInfinity, Numeric},
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use num_traits::{Signed, ToPrimitive};
use tap::{Conv, Pipe};
#[derive(Debug, Clone, Trace, Finalize)]
pub struct ArrayBuffer {
pub array_buffer_data: Option<Vec<u8>>,
pub array_buffer_byte_length: usize,
pub array_buffer_detach_key: JsValue,
}
impl ArrayBuffer {
pub(crate) fn array_buffer_byte_length(&self) -> usize {
self.array_buffer_byte_length
}
}
impl BuiltIn for ArrayBuffer {
const NAME: &'static str = "ArrayBuffer";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE;
let get_species = FunctionBuilder::native(context, Self::get_species)
.name("get [Symbol.species]")
.constructor(false)
.build();
let get_byte_length = FunctionBuilder::native(context, Self::get_byte_length)
.name("get byteLength")
.build();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().array_buffer().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.accessor("byteLength", Some(get_byte_length), None, flag_attributes)
.static_accessor(
WellKnownSymbols::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.static_method(Self::is_view, "isView", 1)
.method(Self::slice, "slice", 2)
.property(
WellKnownSymbols::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl ArrayBuffer {
const LENGTH: usize = 1;
/// `25.1.3.1 ArrayBuffer ( length )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-arraybuffer-length
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return context
.throw_type_error("ArrayBuffer.constructor called with undefined new target");
}
// 2. Let byteLength be ? ToIndex(length).
let byte_length = args.get_or_undefined(0).to_index(context)?;
// 3. Return ? AllocateArrayBuffer(NewTarget, byteLength).
Ok(Self::allocate(new_target, byte_length, context)?.into())
}
/// `25.1.4.3 get ArrayBuffer [ @@species ]`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer-@@species
#[allow(clippy::unnecessary_wraps)]
fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// 1. Return the this value.
Ok(this.clone())
}
/// `25.1.4.1 ArrayBuffer.isView ( arg )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.isview
#[allow(clippy::unnecessary_wraps)]
fn is_view(_: &JsValue, args: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
// 1. If Type(arg) is not Object, return false.
// 2. If arg has a [[ViewedArrayBuffer]] internal slot, return true.
// 3. Return false.
Ok(args
.get_or_undefined(0)
.as_object()
.map(|obj| obj.borrow().has_viewed_array_buffer())
.unwrap_or_default()
.into())
}
/// `25.1.5.1 get ArrayBuffer.prototype.byteLength`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.bytelength
pub(crate) fn get_byte_length(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
let obj = if let Some(obj) = this.as_object() {
obj
} else {
return context.throw_type_error("ArrayBuffer.byteLength called with non-object value");
};
let obj = obj.borrow();
let o = if let Some(o) = obj.as_array_buffer() {
o
} else {
return context.throw_type_error("ArrayBuffer.byteLength called with invalid object");
};
// TODO: Shared Array Buffer
// 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
// 4. If IsDetachedBuffer(O) is true, return +0𝔽.
if Self::is_detached_buffer(o) {
return Ok(0.into());
}
// 5. Let length be O.[[ArrayBufferByteLength]].
// 6. Return 𝔽(length).
Ok(o.array_buffer_byte_length.into())
}
/// `25.1.5.3 ArrayBuffer.prototype.slice ( start, end )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.prototype.slice
fn slice(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
let obj = if let Some(obj) = this.as_object() {
obj
} else {
return context.throw_type_error("ArrayBuffer.slice called with non-object value");
};
let obj_borrow = obj.borrow();
let o = if let Some(o) = obj_borrow.as_array_buffer() {
o
} else {
return context.throw_type_error("ArrayBuffer.slice called with invalid object");
};
// TODO: Shared Array Buffer
// 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
// 4. If IsDetachedBuffer(O) is true, throw a TypeError exception.
if Self::is_detached_buffer(o) {
return context.throw_type_error("ArrayBuffer.slice called with detached buffer");
}
// 5. Let len be O.[[ArrayBufferByteLength]].
let len = o.array_buffer_byte_length as i64;
// 6. Let relativeStart be ? ToIntegerOrInfinity(start).
let relative_start = args.get_or_undefined(0).to_integer_or_infinity(context)?;
let first = match relative_start {
// 7. If relativeStart is -∞, let first be 0.
IntegerOrInfinity::NegativeInfinity => 0,
// 8. Else if relativeStart < 0, let first be max(len + relativeStart, 0).
IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0),
// 9. Else, let first be min(relativeStart, len).
IntegerOrInfinity::Integer(i) => std::cmp::min(i, len),
IntegerOrInfinity::PositiveInfinity => len,
};
// 10. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
let end = args.get_or_undefined(1);
let relative_end = if end.is_undefined() {
IntegerOrInfinity::Integer(len)
} else {
end.to_integer_or_infinity(context)?
};
let r#final = match relative_end {
// 11. If relativeEnd is -∞, let final be 0.
IntegerOrInfinity::NegativeInfinity => 0,
// 12. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0),
// 13. Else, let final be min(relativeEnd, len).
IntegerOrInfinity::Integer(i) => std::cmp::min(i, len),
IntegerOrInfinity::PositiveInfinity => len,
};
// 14. Let newLen be max(final - first, 0).
let new_len = std::cmp::max(r#final - first, 0) as usize;
// 15. Let ctor be ? SpeciesConstructor(O, %ArrayBuffer%).
let ctor = obj.species_constructor(StandardConstructors::array_buffer, context)?;
// 16. Let new be ? Construct(ctor, « 𝔽(newLen) »).
let new = ctor.construct(&[new_len.into()], Some(&ctor), context)?;
{
let new_obj = new.borrow();
// 17. Perform ? RequireInternalSlot(new, [[ArrayBufferData]]).
let new_array_buffer = new_obj.as_array_buffer().ok_or_else(|| {
context.construct_type_error("ArrayBuffer constructor returned invalid object")
})?;
// TODO: Shared Array Buffer
// 18. If IsSharedArrayBuffer(new) is true, throw a TypeError exception.
// 19. If IsDetachedBuffer(new) is true, throw a TypeError exception.
if new_array_buffer.is_detached_buffer() {
return context
.throw_type_error("ArrayBuffer constructor returned detached ArrayBuffer");
}
}
// 20. If SameValue(new, O) is true, throw a TypeError exception.
if this
.as_object()
.map(|obj| JsObject::equals(obj, &new))
.unwrap_or_default()
{
return context.throw_type_error("New ArrayBuffer is the same as this ArrayBuffer");
}
{
let mut new_obj_borrow = new.borrow_mut();
let new_array_buffer = new_obj_borrow
.as_array_buffer_mut()
.expect("Already checked that `new_obj` was an `ArrayBuffer`");
// 21. If new.[[ArrayBufferByteLength]] < newLen, throw a TypeError exception.
if new_array_buffer.array_buffer_byte_length < new_len {
return context.throw_type_error("New ArrayBuffer length too small");
}
// 22. NOTE: Side-effects of the above steps may have detached O.
// 23. If IsDetachedBuffer(O) is true, throw a TypeError exception.
if Self::is_detached_buffer(o) {
return context
.throw_type_error("ArrayBuffer detached while ArrayBuffer.slice was running");
}
// 24. Let fromBuf be O.[[ArrayBufferData]].
let from_buf = o
.array_buffer_data
.as_ref()
.expect("ArrayBuffer cannot be detached here");
// 25. Let toBuf be new.[[ArrayBufferData]].
let to_buf = new_array_buffer
.array_buffer_data
.as_mut()
.expect("ArrayBuffer cannot be detached here");
// 26. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen).
copy_data_block_bytes(to_buf, 0, from_buf, first as usize, new_len);
}
// 27. Return new.
Ok(new.into())
}
/// `25.1.2.1 AllocateArrayBuffer ( constructor, byteLength )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-allocatearraybuffer
pub(crate) fn allocate(
constructor: &JsValue,
byte_length: usize,
context: &mut Context,
) -> JsResult<JsObject> {
// 1. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] »).
let prototype = get_prototype_from_constructor(
constructor,
StandardConstructors::array_buffer,
context,
)?;
let obj = context.construct_object();
obj.set_prototype(prototype.into());
// 2. Let block be ? CreateByteDataBlock(byteLength).
let block = create_byte_data_block(byte_length, context)?;
// 3. Set obj.[[ArrayBufferData]] to block.
// 4. Set obj.[[ArrayBufferByteLength]] to byteLength.
obj.borrow_mut().data = ObjectData::array_buffer(Self {
array_buffer_data: Some(block),
array_buffer_byte_length: byte_length,
array_buffer_detach_key: JsValue::Undefined,
});
// 5. Return obj.
Ok(obj)
}
/// `25.1.2.2 IsDetachedBuffer ( arrayBuffer )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-isdetachedbuffer
pub(crate) fn is_detached_buffer(&self) -> bool {
// 1. If arrayBuffer.[[ArrayBufferData]] is null, return true.
// 2. Return false.
self.array_buffer_data.is_none()
}
/// `25.1.2.4 CloneArrayBuffer ( srcBuffer, srcByteOffset, srcLength, cloneConstructor )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-clonearraybuffer
pub(crate) fn clone_array_buffer(
&self,
src_byte_offset: usize,
src_length: usize,
clone_constructor: &JsValue,
context: &mut Context,
) -> JsResult<JsObject> {
// 1. Let targetBuffer be ? AllocateArrayBuffer(cloneConstructor, srcLength).
let target_buffer = Self::allocate(clone_constructor, src_length, context)?;
// 2. If IsDetachedBuffer(srcBuffer) is true, throw a TypeError exception.
// 3. Let srcBlock be srcBuffer.[[ArrayBufferData]].
let src_block = if let Some(b) = &self.array_buffer_data {
b
} else {
return context.throw_syntax_error("Cannot clone detached array buffer");
};
{
// 4. Let targetBlock be targetBuffer.[[ArrayBufferData]].
let mut target_buffer_mut = target_buffer.borrow_mut();
let target_block = target_buffer_mut
.as_array_buffer_mut()
.expect("This must be an ArrayBuffer");
// 5. Perform CopyDataBlockBytes(targetBlock, 0, srcBlock, srcByteOffset, srcLength).
copy_data_block_bytes(
target_block
.array_buffer_data
.as_mut()
.expect("ArrayBuffer cannot me detached here"),
0,
src_block,
src_byte_offset,
src_length,
);
}
// 6. Return targetBuffer.
Ok(target_buffer)
}
/// `25.1.2.6 IsUnclampedIntegerElementType ( type )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-isunclampedintegerelementtype
fn is_unclamped_integer_element_type(t: TypedArrayKind) -> bool {
// 1. If type is Int8, Uint8, Int16, Uint16, Int32, or Uint32, return true.
// 2. Return false.
matches!(
t,
TypedArrayKind::Int8
| TypedArrayKind::Uint8
| TypedArrayKind::Int16
| TypedArrayKind::Uint16
| TypedArrayKind::Int32
| TypedArrayKind::Uint32
)
}
/// `25.1.2.7 IsBigIntElementType ( type )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-isbigintelementtype
fn is_big_int_element_type(t: TypedArrayKind) -> bool {
// 1. If type is BigUint64 or BigInt64, return true.
// 2. Return false.
matches!(t, TypedArrayKind::BigUint64 | TypedArrayKind::BigInt64)
}
/// `25.1.2.8 IsNoTearConfiguration ( type, order )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-isnotearconfiguration
// TODO: Allow unused function until shared array buffers are implemented.
#[allow(dead_code)]
fn is_no_tear_configuration(t: TypedArrayKind, order: SharedMemoryOrder) -> bool {
// 1. If ! IsUnclampedIntegerElementType(type) is true, return true.
if Self::is_unclamped_integer_element_type(t) {
return true;
}
// 2. If ! IsBigIntElementType(type) is true and order is not Init or Unordered, return true.
if Self::is_big_int_element_type(t)
&& !matches!(
order,
SharedMemoryOrder::Init | SharedMemoryOrder::Unordered
)
{
return true;
}
// 3. Return false.
false
}
/// `25.1.2.9 RawBytesToNumeric ( type, rawBytes, isLittleEndian )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-rawbytestonumeric
fn raw_bytes_to_numeric(t: TypedArrayKind, bytes: &[u8], is_little_endian: bool) -> JsValue {
let n: Numeric = match t {
TypedArrayKind::Int8 => {
if is_little_endian {
i8::from_le_bytes(bytes.try_into().expect("slice with incorrect length")).into()
} else {
i8::from_be_bytes(bytes.try_into().expect("slice with incorrect length")).into()
}
}
TypedArrayKind::Uint8 | TypedArrayKind::Uint8Clamped => {
if is_little_endian {
u8::from_le_bytes(bytes.try_into().expect("slice with incorrect length")).into()
} else {
u8::from_be_bytes(bytes.try_into().expect("slice with incorrect length")).into()
}
}
TypedArrayKind::Int16 => {
if is_little_endian {
i16::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
i16::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
TypedArrayKind::Uint16 => {
if is_little_endian {
u16::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
u16::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
TypedArrayKind::Int32 => {
if is_little_endian {
i32::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
i32::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
TypedArrayKind::Uint32 => {
if is_little_endian {
u32::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
u32::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
TypedArrayKind::BigInt64 => {
if is_little_endian {
i64::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
i64::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
TypedArrayKind::BigUint64 => {
if is_little_endian {
u64::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
u64::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
TypedArrayKind::Float32 => {
if is_little_endian {
f32::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
f32::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
TypedArrayKind::Float64 => {
if is_little_endian {
f64::from_le_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
} else {
f64::from_be_bytes(bytes.try_into().expect("slice with incorrect length"))
.into()
}
}
};
n.into()
}
/// `25.1.2.10 GetValueFromBuffer ( arrayBuffer, byteIndex, type, isTypedArray, order [ , isLittleEndian ] )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-getvaluefrombuffer
pub(crate) fn get_value_from_buffer(
&self,
byte_index: usize,
t: TypedArrayKind,
_is_typed_array: bool,
_order: SharedMemoryOrder,
is_little_endian: Option<bool>,
) -> JsValue {
// 1. Assert: IsDetachedBuffer(arrayBuffer) is false.
// 2. Assert: There are sufficient bytes in arrayBuffer starting at byteIndex to represent a value of type.
// 3. Let block be arrayBuffer.[[ArrayBufferData]].
let block = self
.array_buffer_data
.as_ref()
.expect("ArrayBuffer cannot be detached here");
// 4. Let elementSize be the Element Size value specified in Table 73 for Element Type type.
let element_size = t.element_size();
// TODO: Shared Array Buffer
// 5. If IsSharedArrayBuffer(arrayBuffer) is true, then
// 6. Else, let rawValue be a List whose elements are bytes from block at indices byteIndex (inclusive) through byteIndex + elementSize (exclusive).
// 7. Assert: The number of elements in rawValue is elementSize.
let raw_value = &block[byte_index..byte_index + element_size];
// TODO: Agent Record [[LittleEndian]] filed
// 8. If isLittleEndian is not present, set isLittleEndian to the value of the [[LittleEndian]] field of the surrounding agent's Agent Record.
let is_little_endian = is_little_endian.unwrap_or(true);
// 9. Return RawBytesToNumeric(type, rawValue, isLittleEndian).
Self::raw_bytes_to_numeric(t, raw_value, is_little_endian)
}
/// `25.1.2.11 NumericToRawBytes ( type, value, isLittleEndian )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-numerictorawbytes
fn numeric_to_raw_bytes(
t: TypedArrayKind,
value: &JsValue,
is_little_endian: bool,
context: &mut Context,
) -> JsResult<Vec<u8>> {
Ok(match t {
TypedArrayKind::Int8 if is_little_endian => {
value.to_int8(context)?.to_le_bytes().to_vec()
}
TypedArrayKind::Int8 => value.to_int8(context)?.to_be_bytes().to_vec(),
TypedArrayKind::Uint8 if is_little_endian => {
value.to_uint8(context)?.to_le_bytes().to_vec()
}
TypedArrayKind::Uint8 => value.to_uint8(context)?.to_be_bytes().to_vec(),
TypedArrayKind::Uint8Clamped if is_little_endian => {
value.to_uint8_clamp(context)?.to_le_bytes().to_vec()
}
TypedArrayKind::Uint8Clamped => value.to_uint8_clamp(context)?.to_be_bytes().to_vec(),
TypedArrayKind::Int16 if is_little_endian => {
value.to_int16(context)?.to_le_bytes().to_vec()
}
TypedArrayKind::Int16 => value.to_int16(context)?.to_be_bytes().to_vec(),
TypedArrayKind::Uint16 if is_little_endian => {
value.to_uint16(context)?.to_le_bytes().to_vec()
}
TypedArrayKind::Uint16 => value.to_uint16(context)?.to_be_bytes().to_vec(),
TypedArrayKind::Int32 if is_little_endian => {
value.to_i32(context)?.to_le_bytes().to_vec()
}
TypedArrayKind::Int32 => value.to_i32(context)?.to_be_bytes().to_vec(),
TypedArrayKind::Uint32 if is_little_endian => {
value.to_u32(context)?.to_le_bytes().to_vec()
}
TypedArrayKind::Uint32 => value.to_u32(context)?.to_be_bytes().to_vec(),
TypedArrayKind::BigInt64 if is_little_endian => {
let big_int = value.to_big_int64(context)?;
big_int
.to_i64()
.unwrap_or_else(|| {
if big_int.is_positive() {
i64::MAX
} else {
i64::MIN
}
})
.to_le_bytes()
.to_vec()
}
TypedArrayKind::BigInt64 => {
let big_int = value.to_big_int64(context)?;
big_int
.to_i64()
.unwrap_or_else(|| {
if big_int.is_positive() {
i64::MAX
} else {
i64::MIN
}
})
.to_be_bytes()
.to_vec()
}
TypedArrayKind::BigUint64 if is_little_endian => value
.to_big_uint64(context)?
.to_u64()
.unwrap_or(u64::MAX)
.to_le_bytes()
.to_vec(),
TypedArrayKind::BigUint64 => value
.to_big_uint64(context)?
.to_u64()
.unwrap_or(u64::MAX)
.to_be_bytes()
.to_vec(),
TypedArrayKind::Float32 => match value.to_number(context)? {
f if is_little_endian => (f as f32).to_le_bytes().to_vec(),
f => (f as f32).to_be_bytes().to_vec(),
},
TypedArrayKind::Float64 => match value.to_number(context)? {
f if is_little_endian => f.to_le_bytes().to_vec(),
f => f.to_be_bytes().to_vec(),
},
})
}
/// `25.1.2.12 SetValueInBuffer ( arrayBuffer, byteIndex, type, value, isTypedArray, order [ , isLittleEndian ] )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-setvalueinbuffer
pub(crate) fn set_value_in_buffer(
&mut self,
byte_index: usize,
t: TypedArrayKind,
value: &JsValue,
_order: SharedMemoryOrder,
is_little_endian: Option<bool>,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Assert: IsDetachedBuffer(arrayBuffer) is false.
// 2. Assert: There are sufficient bytes in arrayBuffer starting at byteIndex to represent a value of type.
// 3. Assert: Type(value) is BigInt if ! IsBigIntElementType(type) is true; otherwise, Type(value) is Number.
// 4. Let block be arrayBuffer.[[ArrayBufferData]].
let block = self
.array_buffer_data
.as_mut()
.expect("ArrayBuffer cannot be detached here");
// 5. Let elementSize be the Element Size value specified in Table 73 for Element Type type.
// TODO: Agent Record [[LittleEndian]] filed
// 6. If isLittleEndian is not present, set isLittleEndian to the value of the [[LittleEndian]] field of the surrounding agent's Agent Record.
let is_little_endian = is_little_endian.unwrap_or(true);
// 7. Let rawBytes be NumericToRawBytes(type, value, isLittleEndian).
let raw_bytes = Self::numeric_to_raw_bytes(t, value, is_little_endian, context)?;
// TODO: Shared Array Buffer
// 8. If IsSharedArrayBuffer(arrayBuffer) is true, then
// 9. Else, store the individual bytes of rawBytes into block, starting at block[byteIndex].
for (i, raw_byte) in raw_bytes.iter().enumerate() {
block[byte_index + i] = *raw_byte;
}
// 10. Return NormalCompletion(undefined).
Ok(JsValue::undefined())
}
}
/// `CreateByteDataBlock ( size )` abstract operation.
///
/// The abstract operation `CreateByteDataBlock` takes argument `size` (a non-negative
/// integer). For more information, check the [spec][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-createbytedatablock
pub fn create_byte_data_block(size: usize, context: &mut Context) -> JsResult<Vec<u8>> {
// 1. Let db be a new Data Block value consisting of size bytes. If it is impossible to
// create such a Data Block, throw a RangeError exception.
let mut data_block = Vec::new();
data_block.try_reserve(size).map_err(|e| {
context.construct_range_error(format!("couldn't allocate the data block: {e}"))
})?;
// 2. Set all of the bytes of db to 0.
data_block.resize(size, 0);
// 3. Return db.
Ok(data_block)
}
/// `6.2.8.3 CopyDataBlockBytes ( toBlock, toIndex, fromBlock, fromIndex, count )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-copydatablockbytes
fn copy_data_block_bytes(
to_block: &mut [u8],
mut to_index: usize,
from_block: &[u8],
mut from_index: usize,
mut count: usize,
) {
// 1. Assert: fromBlock and toBlock are distinct values.
// 2. Let fromSize be the number of bytes in fromBlock.
let from_size = from_block.len();
// 3. Assert: fromIndex + count ≤ fromSize.
assert!(from_index + count <= from_size);
// 4. Let toSize be the number of bytes in toBlock.
let to_size = to_block.len();
// 5. Assert: toIndex + count ≤ toSize.
assert!(to_index + count <= to_size);
// 6. Repeat, while count > 0,
while count > 0 {
// a. If fromBlock is a Shared Data Block, then
// TODO: Shared Data Block
// b. Else,
// i. Assert: toBlock is not a Shared Data Block.
// ii. Set toBlock[toIndex] to fromBlock[fromIndex].
to_block[to_index] = from_block[from_index];
// c. Set toIndex to toIndex + 1.
to_index += 1;
// d. Set fromIndex to fromIndex + 1.
from_index += 1;
// e. Set count to count - 1.
count -= 1;
}
// 7. Return NormalCompletion(empty).
}
// TODO: Allow unused variants until shared array buffers are implemented.
#[allow(dead_code)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub(crate) enum SharedMemoryOrder {
Init,
SeqCst,
Unordered,
}

View File

@@ -0,0 +1,15 @@
use super::*;
#[test]
fn ut_sunny_day_create_byte_data_block() {
let mut context = Context::default();
assert!(create_byte_data_block(100, &mut context).is_ok());
}
#[test]
fn ut_rainy_day_create_byte_data_block() {
let mut context = Context::default();
assert!(create_byte_data_block(usize::MAX, &mut context).is_err());
}

View File

@@ -0,0 +1,107 @@
//! This module implements the global `AsyncFunction` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-async-function-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
use crate::{
builtins::{
function::{ConstructorKind, Function},
BuiltIn,
},
object::ObjectData,
property::PropertyDescriptor,
symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
#[derive(Debug, Clone, Copy)]
pub struct AsyncFunction;
impl BuiltIn for AsyncFunction {
const NAME: &'static str = "AsyncFunction";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let prototype = &context
.intrinsics()
.constructors()
.async_function()
.prototype;
let constructor = &context
.intrinsics()
.constructors()
.async_function()
.constructor;
constructor.set_prototype(Some(
context.intrinsics().constructors().function().constructor(),
));
let property = PropertyDescriptor::builder()
.value(1)
.writable(false)
.enumerable(false)
.configurable(true);
constructor.borrow_mut().insert("length", property);
let property = PropertyDescriptor::builder()
.value(Self::NAME)
.writable(false)
.enumerable(false)
.configurable(true);
constructor.borrow_mut().insert("name", property);
let property = PropertyDescriptor::builder()
.value(prototype.clone())
.writable(false)
.enumerable(false)
.configurable(false);
constructor.borrow_mut().insert("prototype", property);
constructor.borrow_mut().data = ObjectData::function(Function::Native {
function: Self::constructor,
constructor: Some(ConstructorKind::Base),
});
prototype.set_prototype(Some(
context.intrinsics().constructors().function().prototype(),
));
let property = PropertyDescriptor::builder()
.value(constructor.clone())
.writable(false)
.enumerable(false)
.configurable(true);
prototype.borrow_mut().insert("constructor", property);
let property = PropertyDescriptor::builder()
.value(Self::NAME)
.writable(false)
.enumerable(false)
.configurable(true);
prototype
.borrow_mut()
.insert(WellKnownSymbols::to_string_tag(), property);
None
}
}
impl AsyncFunction {
/// `AsyncFunction ( p1, p2, … , pn, body )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-async-function-constructor-arguments
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
crate::builtins::function::BuiltInFunctionObject::create_dynamic_function(
new_target, args, true, false, context,
)
.map(Into::into)
}
}

View File

@@ -0,0 +1,290 @@
//! This module implements the global `BigInt` object.
//!
//! `BigInt` is a built-in object that provides a way to represent whole numbers larger
//! than the largest number JavaScript can reliably represent with the Number primitive
//! and represented by the `Number.MAX_SAFE_INTEGER` constant.
//! `BigInt` can be used for arbitrarily large integers.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-bigint-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
use crate::{
builtins::{BuiltIn, JsArgs},
object::ConstructorBuilder,
property::Attribute,
symbol::WellKnownSymbols,
value::{IntegerOrInfinity, PreferredType},
Context, JsBigInt, JsResult, JsValue,
};
use boa_profiler::Profiler;
use num_bigint::ToBigInt;
use tap::{Conv, Pipe};
#[cfg(test)]
mod tests;
/// `BigInt` implementation.
#[derive(Debug, Clone, Copy)]
pub struct BigInt;
impl BuiltIn for BigInt {
const NAME: &'static str = "BigInt";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let to_string_tag = WellKnownSymbols::to_string_tag();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().bigint_object().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.callable(true)
.constructor(true)
.method(Self::to_string, "toString", 0)
.method(Self::value_of, "valueOf", 0)
.static_method(Self::as_int_n, "asIntN", 2)
.static_method(Self::as_uint_n, "asUintN", 2)
.property(
to_string_tag,
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl BigInt {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// `BigInt()`
///
/// The `BigInt()` constructor is used to create `BigInt` objects.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-bigint-objects
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/BigInt
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is not undefined, throw a TypeError exception.
if !new_target.is_undefined() {
return context.throw_type_error("BigInt is not a constructor");
}
let value = args.get_or_undefined(0);
// 2. Let prim be ? ToPrimitive(value, number).
let prim = value.to_primitive(context, PreferredType::Number)?;
// 3. If Type(prim) is Number, return ? NumberToBigInt(prim).
if let Some(number) = prim.as_number() {
return Self::number_to_bigint(number, context);
}
// 4. Otherwise, return ? ToBigInt(value).
Ok(value.to_bigint(context)?.into())
}
/// `NumberToBigInt ( number )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-numbertobigint
#[inline]
fn number_to_bigint(number: f64, context: &mut Context) -> JsResult<JsValue> {
// 1. If IsIntegralNumber(number) is false, throw a RangeError exception.
if number.is_nan() || number.is_infinite() || number.fract() != 0.0 {
return context.throw_range_error(format!("Cannot convert {number} to BigInt"));
}
// 2. Return the BigInt value that represents (number).
Ok(JsBigInt::from(number.to_bigint().expect("This conversion must be safe")).into())
}
/// The abstract operation `thisBigIntValue` takes argument value.
///
/// The phrase “this `BigInt` value” within the specification of a method refers to the
/// result returned by calling the abstract operation `thisBigIntValue` with the `this` value
/// of the method invocation passed as the argument.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-thisbigintvalue
#[inline]
fn this_bigint_value(value: &JsValue, context: &mut Context) -> JsResult<JsBigInt> {
value
// 1. If Type(value) is BigInt, return value.
.as_bigint()
.cloned()
// 2. If Type(value) is Object and value has a [[BigIntData]] internal slot, then
// a. Assert: Type(value.[[BigIntData]]) is BigInt.
// b. Return value.[[BigIntData]].
.or_else(|| {
value
.as_object()
.and_then(|obj| obj.borrow().as_bigint().cloned())
})
// 3. Throw a TypeError exception.
.ok_or_else(|| context.construct_type_error("'this' is not a BigInt"))
}
/// `BigInt.prototype.toString( [radix] )`
///
/// The `toString()` method returns a string representing the specified `BigInt` object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.tostring
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_string(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let x be ? thisBigIntValue(this value).
let x = Self::this_bigint_value(this, context)?;
let radix = args.get_or_undefined(0);
// 2. If radix is undefined, let radixMV be 10.
let radix_mv = if radix.is_undefined() {
// 5. If radixMV = 10, return ! ToString(x).
// Note: early return optimization.
return Ok(x.to_string().into());
// 3. Else, let radixMV be ? ToIntegerOrInfinity(radix).
} else {
radix.to_integer_or_infinity(context)?
};
// 4. If radixMV < 2 or radixMV > 36, throw a RangeError exception.
let radix_mv = match radix_mv {
IntegerOrInfinity::Integer(i) if (2..=36).contains(&i) => i,
_ => {
return context.throw_range_error(
"radix must be an integer at least 2 and no greater than 36",
)
}
};
// 5. If radixMV = 10, return ! ToString(x).
if radix_mv == 10 {
return Ok(x.to_string().into());
}
// 1. Let x be ? thisBigIntValue(this value).
// 6. Return the String representation of this Number value using the radix specified by radixMV.
// Letters a-z are used for digits with values 10 through 35.
// The precise algorithm is implementation-defined, however the algorithm should be a generalization of that specified in 6.1.6.2.23.
Ok(JsValue::new(x.to_string_radix(radix_mv as u32)))
}
/// `BigInt.prototype.valueOf()`
///
/// The `valueOf()` method returns the wrapped primitive value of a Number object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.valueof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/valueOf
pub(crate) fn value_of(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
Ok(JsValue::new(Self::this_bigint_value(this, context)?))
}
/// `BigInt.asIntN()`
///
/// The `BigInt.asIntN()` method wraps the value of a `BigInt` to a signed integer between `-2**(width - 1)` and `2**(width-1) - 1`.
///
/// [spec]: https://tc39.es/ecma262/#sec-bigint.asintn
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asIntN
#[allow(clippy::wrong_self_convention)]
pub(crate) fn as_int_n(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let (modulo, bits) = Self::calculate_as_uint_n(args, context)?;
if bits > 0
&& modulo
>= JsBigInt::pow(
&JsBigInt::new(2),
&JsBigInt::new(i64::from(bits) - 1),
context,
)?
{
Ok(JsValue::new(JsBigInt::sub(
&modulo,
&JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits)), context)?,
)))
} else {
Ok(JsValue::new(modulo))
}
}
/// `BigInt.asUintN()`
///
/// The `BigInt.asUintN()` method wraps the value of a `BigInt` to an unsigned integer between `0` and `2**(width) - 1`.
///
/// [spec]: https://tc39.es/ecma262/#sec-bigint.asuintn
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asUintN
#[allow(clippy::wrong_self_convention)]
pub(crate) fn as_uint_n(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let (modulo, _) = Self::calculate_as_uint_n(args, context)?;
Ok(JsValue::new(modulo))
}
/// Helper function to wrap the value of a `BigInt` to an unsigned integer.
///
/// This function expects the same arguments as `as_uint_n` and wraps the value of a `BigInt`.
/// Additionally to the wrapped unsigned value it returns the converted `bits` argument, so it
/// can be reused from the `as_int_n` method.
fn calculate_as_uint_n(args: &[JsValue], context: &mut Context) -> JsResult<(JsBigInt, u32)> {
let bits_arg = args.get_or_undefined(0);
let bigint_arg = args.get_or_undefined(1);
let bits = bits_arg.to_index(context)?;
let bits = u32::try_from(bits).unwrap_or(u32::MAX);
let bigint = bigint_arg.to_bigint(context)?;
Ok((
JsBigInt::mod_floor(
&bigint,
&JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits)), context)?,
),
bits,
))
}
}

View File

@@ -0,0 +1,413 @@
use crate::{forward, Context};
#[test]
fn equality() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "0n == 0n"), "true");
assert_eq!(forward(&mut context, "1n == 0n"), "false");
assert_eq!(
forward(
&mut context,
"1000000000000000000000000000000000n == 1000000000000000000000000000000000n"
),
"true"
);
assert_eq!(forward(&mut context, "0n == ''"), "true");
assert_eq!(forward(&mut context, "100n == '100'"), "true");
assert_eq!(forward(&mut context, "100n == '100.5'"), "false");
assert_eq!(
forward(&mut context, "10000000000000000n == '10000000000000000'"),
"true"
);
assert_eq!(forward(&mut context, "'' == 0n"), "true");
assert_eq!(forward(&mut context, "'100' == 100n"), "true");
assert_eq!(forward(&mut context, "'100.5' == 100n"), "false");
assert_eq!(
forward(&mut context, "'10000000000000000' == 10000000000000000n"),
"true"
);
assert_eq!(forward(&mut context, "0n == 0"), "true");
assert_eq!(forward(&mut context, "0n == 0.0"), "true");
assert_eq!(forward(&mut context, "100n == 100"), "true");
assert_eq!(forward(&mut context, "100n == 100.0"), "true");
assert_eq!(forward(&mut context, "100n == '100.5'"), "false");
assert_eq!(forward(&mut context, "100n == '1005'"), "false");
assert_eq!(
forward(&mut context, "10000000000000000n == 10000000000000000"),
"true"
);
assert_eq!(forward(&mut context, "0 == 0n"), "true");
assert_eq!(forward(&mut context, "0.0 == 0n"), "true");
assert_eq!(forward(&mut context, "100 == 100n"), "true");
assert_eq!(forward(&mut context, "100.0 == 100n"), "true");
assert_eq!(forward(&mut context, "100.5 == 100n"), "false");
assert_eq!(forward(&mut context, "1005 == 100n"), "false");
assert_eq!(
forward(&mut context, "10000000000000000 == 10000000000000000n"),
"true"
);
}
#[test]
fn bigint_function_conversion_from_integer() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "BigInt(1000)"), "1000n");
assert_eq!(
forward(&mut context, "BigInt(20000000000000000)"),
"20000000000000000n"
);
assert_eq!(
forward(&mut context, "BigInt(1000000000000000000000000000000000)"),
"999999999999999945575230987042816n"
);
}
#[test]
fn bigint_function_conversion_from_rational() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "BigInt(0.0)"), "0n");
assert_eq!(forward(&mut context, "BigInt(1.0)"), "1n");
assert_eq!(forward(&mut context, "BigInt(10000.0)"), "10000n");
}
#[test]
fn bigint_function_conversion_from_rational_with_fractional_part() {
let mut context = Context::default();
let scenario = r#"
try {
BigInt(0.1);
} catch (e) {
e.toString();
}
"#;
assert_eq!(
forward(&mut context, scenario),
"\"RangeError: Cannot convert 0.1 to BigInt\""
);
}
#[test]
fn bigint_function_conversion_from_null() {
let mut context = Context::default();
let scenario = r#"
try {
BigInt(null);
} catch (e) {
e.toString();
}
"#;
assert_eq!(
forward(&mut context, scenario),
"\"TypeError: cannot convert null to a BigInt\""
);
}
#[test]
fn bigint_function_conversion_from_undefined() {
let mut context = Context::default();
let scenario = r#"
try {
BigInt(undefined);
} catch (e) {
e.toString();
}
"#;
assert_eq!(
forward(&mut context, scenario),
"\"TypeError: cannot convert undefined to a BigInt\""
);
}
#[test]
fn bigint_function_conversion_from_string() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "BigInt('')"), "0n");
assert_eq!(forward(&mut context, "BigInt(' ')"), "0n");
assert_eq!(
forward(&mut context, "BigInt('200000000000000000')"),
"200000000000000000n"
);
assert_eq!(
forward(&mut context, "BigInt('1000000000000000000000000000000000')"),
"1000000000000000000000000000000000n"
);
assert_eq!(forward(&mut context, "BigInt('0b1111')"), "15n");
assert_eq!(forward(&mut context, "BigInt('0o70')"), "56n");
assert_eq!(forward(&mut context, "BigInt('0xFF')"), "255n");
}
#[test]
fn add() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "10000n + 1000n"), "11000n");
}
#[test]
fn sub() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "10000n - 1000n"), "9000n");
}
#[test]
fn mul() {
let mut context = Context::default();
assert_eq!(
forward(&mut context, "123456789n * 102030n"),
"12596296181670n"
);
}
#[test]
fn div() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "15000n / 50n"), "300n");
}
#[test]
fn div_with_truncation() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "15001n / 50n"), "300n");
}
#[test]
fn r#mod() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "15007n % 10n"), "7n");
}
#[test]
fn pow() {
let mut context = Context::default();
assert_eq!(
forward(&mut context, "100n ** 10n"),
"100000000000000000000n"
);
}
#[test]
fn pow_negative_exponent() {
let mut context = Context::default();
assert_throws(&mut context, "10n ** (-10n)", "RangeError");
}
#[test]
fn shl() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "8n << 2n"), "32n");
}
#[test]
fn shl_out_of_range() {
let mut context = Context::default();
assert_throws(&mut context, "1000n << 1000000000000000n", "RangeError");
}
#[test]
fn shr() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "8n >> 2n"), "2n");
}
#[test]
fn shr_out_of_range() {
let mut context = Context::default();
assert_throws(&mut context, "1000n >> 1000000000000000n", "RangeError");
}
#[test]
fn to_string() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "1000n.toString()"), "\"1000\"");
assert_eq!(forward(&mut context, "1000n.toString(2)"), "\"1111101000\"");
assert_eq!(forward(&mut context, "255n.toString(16)"), "\"ff\"");
assert_eq!(forward(&mut context, "1000n.toString(36)"), "\"rs\"");
}
#[test]
fn to_string_invalid_radix() {
let mut context = Context::default();
assert_throws(&mut context, "10n.toString(null)", "RangeError");
assert_throws(&mut context, "10n.toString(-1)", "RangeError");
assert_throws(&mut context, "10n.toString(37)", "RangeError");
}
#[test]
fn as_int_n() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "BigInt.asIntN(0, 1n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asIntN(1, 1n)"), "-1n");
assert_eq!(forward(&mut context, "BigInt.asIntN(3, 10n)"), "2n");
assert_eq!(forward(&mut context, "BigInt.asIntN({}, 1n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asIntN(2, 0n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asIntN(2, -0n)"), "0n");
assert_eq!(
forward(&mut context, "BigInt.asIntN(2, -123456789012345678901n)"),
"-1n"
);
assert_eq!(
forward(&mut context, "BigInt.asIntN(2, -123456789012345678900n)"),
"0n"
);
assert_eq!(
forward(&mut context, "BigInt.asIntN(2, 123456789012345678900n)"),
"0n"
);
assert_eq!(
forward(&mut context, "BigInt.asIntN(2, 123456789012345678901n)"),
"1n"
);
assert_eq!(
forward(
&mut context,
"BigInt.asIntN(200, 0xcffffffffffffffffffffffffffffffffffffffffffffffffffn)"
),
"-1n"
);
assert_eq!(
forward(
&mut context,
"BigInt.asIntN(201, 0xcffffffffffffffffffffffffffffffffffffffffffffffffffn)"
),
"1606938044258990275541962092341162602522202993782792835301375n"
);
assert_eq!(
forward(
&mut context,
"BigInt.asIntN(200, 0xc89e081df68b65fedb32cffea660e55df9605650a603ad5fc54n)"
),
"-741470203160010616172516490008037905920749803227695190508460n"
);
assert_eq!(
forward(
&mut context,
"BigInt.asIntN(201, 0xc89e081df68b65fedb32cffea660e55df9605650a603ad5fc54n)"
),
"865467841098979659369445602333124696601453190555097644792916n"
);
}
#[test]
fn as_int_n_errors() {
let mut context = Context::default();
assert_throws(&mut context, "BigInt.asIntN(-1, 0n)", "RangeError");
assert_throws(&mut context, "BigInt.asIntN(-2.5, 0n)", "RangeError");
assert_throws(
&mut context,
"BigInt.asIntN(9007199254740992, 0n)",
"RangeError",
);
assert_throws(&mut context, "BigInt.asIntN(0n, 0n)", "TypeError");
}
#[test]
fn as_uint_n() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "BigInt.asUintN(0, -2n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(0, -1n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(0, 0n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(0, 1n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(0, 2n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(1, -3n)"), "1n");
assert_eq!(forward(&mut context, "BigInt.asUintN(1, -2n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(1, -1n)"), "1n");
assert_eq!(forward(&mut context, "BigInt.asUintN(1, 0n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(1, 1n)"), "1n");
assert_eq!(forward(&mut context, "BigInt.asUintN(1, 2n)"), "0n");
assert_eq!(forward(&mut context, "BigInt.asUintN(1, 3n)"), "1n");
assert_eq!(
forward(&mut context, "BigInt.asUintN(1, -123456789012345678901n)"),
"1n"
);
assert_eq!(
forward(&mut context, "BigInt.asUintN(1, -123456789012345678900n)"),
"0n"
);
assert_eq!(
forward(&mut context, "BigInt.asUintN(1, 123456789012345678900n)"),
"0n"
);
assert_eq!(
forward(&mut context, "BigInt.asUintN(1, 123456789012345678901n)"),
"1n"
);
assert_eq!(
forward(
&mut context,
"BigInt.asUintN(200, 0xbffffffffffffffffffffffffffffffffffffffffffffffffffn)"
),
"1606938044258990275541962092341162602522202993782792835301375n"
);
assert_eq!(
forward(
&mut context,
"BigInt.asUintN(201, 0xbffffffffffffffffffffffffffffffffffffffffffffffffffn)"
),
"3213876088517980551083924184682325205044405987565585670602751n"
);
}
#[test]
fn as_uint_n_errors() {
let mut context = Context::default();
assert_throws(&mut context, "BigInt.asUintN(-1, 0n)", "RangeError");
assert_throws(&mut context, "BigInt.asUintN(-2.5, 0n)", "RangeError");
assert_throws(
&mut context,
"BigInt.asUintN(9007199254740992, 0n)",
"RangeError",
);
assert_throws(&mut context, "BigInt.asUintN(0n, 0n)", "TypeError");
}
fn assert_throws(context: &mut Context, src: &str, error_type: &str) {
let result = forward(context, src);
assert!(result.contains(error_type));
}
#[test]
fn division_by_zero() {
let mut context = Context::default();
assert_throws(&mut context, "1n/0n", "RangeError");
}
#[test]
fn remainder_by_zero() {
let mut context = Context::default();
assert_throws(&mut context, "1n % 0n", "RangeError");
}

View File

@@ -0,0 +1,123 @@
//! This module implements the global `Boolean` object.
//!
//! The `Boolean` object is an object wrapper for a boolean value.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-boolean-object
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
#[cfg(test)]
mod tests;
use crate::{
builtins::BuiltIn,
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
/// Boolean implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Boolean;
impl BuiltIn for Boolean {
/// The name of the object.
const NAME: &'static str = "Boolean";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().boolean().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.method(Self::to_string, "toString", 0)
.method(Self::value_of, "valueOf", 0)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Boolean {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// `[[Construct]]` Create a new boolean object
///
/// `[[Call]]` Creates a new boolean primitive
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// Get the argument, if any
let data = args.get(0).map_or(false, JsValue::to_boolean);
if new_target.is_undefined() {
return Ok(JsValue::new(data));
}
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::boolean, context)?;
let boolean = JsObject::from_proto_and_data(prototype, ObjectData::boolean(data));
Ok(boolean.into())
}
/// An Utility function used to get the internal `[[BooleanData]]`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-thisbooleanvalue
fn this_boolean_value(value: &JsValue, context: &mut Context) -> JsResult<bool> {
value
.as_boolean()
.or_else(|| value.as_object().and_then(|obj| obj.borrow().as_boolean()))
.ok_or_else(|| context.construct_type_error("'this' is not a boolean"))
}
/// The `toString()` method returns a string representing the specified `Boolean` object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-boolean-object
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/toString
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_string(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let boolean = Self::this_boolean_value(this, context)?;
Ok(JsValue::new(boolean.to_string()))
}
/// The valueOf() method returns the primitive value of a `Boolean` object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-boolean.prototype.valueof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/valueOf
#[inline]
pub(crate) fn value_of(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
Ok(JsValue::new(Self::this_boolean_value(this, context)?))
}
}

View File

@@ -0,0 +1,65 @@
use crate::{forward, forward_val, Context};
/// Test the correct type is returned from call and construct
#[allow(clippy::unwrap_used)]
#[test]
fn construct_and_call() {
let mut context = Context::default();
let init = r#"
var one = new Boolean(1);
var zero = Boolean(0);
"#;
eprintln!("{}", forward(&mut context, init));
let one = forward_val(&mut context, "one").unwrap();
let zero = forward_val(&mut context, "zero").unwrap();
assert!(one.is_object());
assert!(zero.is_boolean());
}
#[test]
fn constructor_gives_true_instance() {
let mut context = Context::default();
let init = r#"
var trueVal = new Boolean(true);
var trueNum = new Boolean(1);
var trueString = new Boolean("true");
var trueBool = new Boolean(trueVal);
"#;
eprintln!("{}", forward(&mut context, init));
let true_val = forward_val(&mut context, "trueVal").expect("value expected");
let true_num = forward_val(&mut context, "trueNum").expect("value expected");
let true_string = forward_val(&mut context, "trueString").expect("value expected");
let true_bool = forward_val(&mut context, "trueBool").expect("value expected");
// Values should all be objects
assert!(true_val.is_object());
assert!(true_num.is_object());
assert!(true_string.is_object());
assert!(true_bool.is_object());
// Values should all be truthy
assert!(true_val.to_boolean());
assert!(true_num.to_boolean());
assert!(true_string.to_boolean());
assert!(true_bool.to_boolean());
}
#[test]
fn instances_have_correct_proto_set() {
let mut context = Context::default();
let init = r#"
var boolInstance = new Boolean(true);
var boolProto = Boolean.prototype;
"#;
eprintln!("{}", forward(&mut context, init));
let bool_instance = forward_val(&mut context, "boolInstance").expect("value expected");
let bool_prototype = forward_val(&mut context, "boolProto").expect("value expected");
assert_eq!(
&*bool_instance.as_object().unwrap().prototype(),
&bool_prototype.as_object().cloned()
);
}

View File

@@ -0,0 +1,577 @@
//! This module implements the global `console` object.
//!
//! The `console` object can be accessed from any global object.
//!
//! The specifics of how it works varies from browser to browser, but there is a de facto set of features that are typically provided.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [WHATWG `console` specification][spec]
//!
//! [spec]: https://console.spec.whatwg.org/
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Console
#![allow(clippy::print_stdout)]
#[cfg(test)]
mod tests;
use crate::{
builtins::{BuiltIn, JsArgs},
object::ObjectInitializer,
value::{display::display_obj, JsValue, Numeric},
Context, JsResult, JsString,
};
use boa_profiler::Profiler;
use rustc_hash::FxHashMap;
use std::time::SystemTime;
use tap::{Conv, Pipe};
/// This represents the different types of log messages.
#[derive(Debug)]
pub enum LogMessage {
Log(String),
Info(String),
Warn(String),
Error(String),
}
/// Helper function for logging messages.
pub(crate) fn logger(msg: LogMessage, console_state: &Console) {
let indent = 2 * console_state.groups.len();
match msg {
LogMessage::Error(msg) => {
eprintln!("{msg:>indent$}");
}
LogMessage::Log(msg) | LogMessage::Info(msg) | LogMessage::Warn(msg) => {
println!("{msg:>indent$}");
}
}
}
/// This represents the `console` formatter.
pub fn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {
let target = data
.get(0)
.cloned()
.unwrap_or_default()
.to_string(context)?;
match data.len() {
0 => Ok(String::new()),
1 => Ok(target.to_string()),
_ => {
let mut formatted = String::new();
let mut arg_index = 1;
let mut chars = target.chars();
while let Some(c) = chars.next() {
if c == '%' {
let fmt = chars.next().unwrap_or('%');
match fmt {
/* integer */
'd' | 'i' => {
let arg = match data.get_or_undefined(arg_index).to_numeric(context)? {
Numeric::Number(r) => (r.floor() + 0.0).to_string(),
Numeric::BigInt(int) => int.to_string(),
};
formatted.push_str(&arg);
arg_index += 1;
}
/* float */
'f' => {
let arg = data.get_or_undefined(arg_index).to_number(context)?;
formatted.push_str(&format!("{arg:.6}"));
arg_index += 1;
}
/* object, FIXME: how to render this properly? */
'o' | 'O' => {
let arg = data.get_or_undefined(arg_index);
formatted.push_str(&arg.display().to_string());
arg_index += 1;
}
/* string */
's' => {
let arg = data
.get(arg_index)
.cloned()
.unwrap_or_default()
.to_string(context)?;
formatted.push_str(&arg);
arg_index += 1;
}
'%' => formatted.push('%'),
/* TODO: %c is not implemented */
c => {
formatted.push('%');
formatted.push(c);
}
}
} else {
formatted.push(c);
};
}
/* unformatted data */
for rest in data.iter().skip(arg_index) {
formatted.push_str(&format!(" {}", rest.to_string(context)?));
}
Ok(formatted)
}
}
}
/// This is the internal console object state.
#[derive(Debug, Default)]
pub(crate) struct Console {
count_map: FxHashMap<JsString, u32>,
timer_map: FxHashMap<JsString, u128>,
groups: Vec<String>,
}
impl BuiltIn for Console {
const NAME: &'static str = "console";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
ObjectInitializer::new(context)
.function(Self::assert, "assert", 0)
.function(Self::clear, "clear", 0)
.function(Self::debug, "debug", 0)
.function(Self::error, "error", 0)
.function(Self::info, "info", 0)
.function(Self::log, "log", 0)
.function(Self::trace, "trace", 0)
.function(Self::warn, "warn", 0)
.function(Self::error, "exception", 0)
.function(Self::count, "count", 0)
.function(Self::count_reset, "countReset", 0)
.function(Self::group, "group", 0)
.function(Self::group, "groupCollapsed", 0)
.function(Self::group_end, "groupEnd", 0)
.function(Self::time, "time", 0)
.function(Self::time_log, "timeLog", 0)
.function(Self::time_end, "timeEnd", 0)
.function(Self::dir, "dir", 0)
.function(Self::dir, "dirxml", 0)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Console {
/// The name of the object.
pub(crate) const NAME: &'static str = "console";
/// `console.assert(condition, ...data)`
///
/// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there
/// were no arguments.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#assert
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/assert
pub(crate) fn assert(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let assertion = args.get(0).map_or(false, JsValue::to_boolean);
if !assertion {
let mut args: Vec<JsValue> = args.iter().skip(1).cloned().collect();
let message = "Assertion failed".to_string();
if args.is_empty() {
args.push(JsValue::new(message));
} else if !args[0].is_string() {
args.insert(0, JsValue::new(message));
} else {
let concat = format!("{message}: {}", args[0].display());
args[0] = JsValue::new(concat);
}
logger(
LogMessage::Error(formatter(&args, context)?),
context.console(),
);
}
Ok(JsValue::undefined())
}
/// `console.clear()`
///
/// Removes all groups and clears console if possible.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#clear
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn clear(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
context.console_mut().groups.clear();
Ok(JsValue::undefined())
}
/// `console.debug(...data)`
///
/// Prints a JavaScript values with "debug" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#debug
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/debug
pub(crate) fn debug(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Log(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.error(...data)`
///
/// Prints a JavaScript values with "error" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#error
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/error
pub(crate) fn error(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Error(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.info(...data)`
///
/// Prints a JavaScript values with "info" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#info
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/info
pub(crate) fn info(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Info(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.log(...data)`
///
/// Prints a JavaScript values with "log" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#log
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/log
pub(crate) fn log(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Log(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
fn get_stack_trace(context: &mut Context) -> Vec<String> {
let mut stack_trace: Vec<String> = vec![];
for frame in context.vm.frames.iter().rev() {
stack_trace.push(
context
.interner()
.resolve_expect(frame.code.name)
.to_owned(),
);
}
stack_trace
}
/// `console.trace(...data)`
///
/// Prints a stack trace with "trace" logLevel, optionally labelled by data.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#trace
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/trace
pub(crate) fn trace(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if !args.is_empty() {
logger(
LogMessage::Log(formatter(args, context)?),
context.console(),
);
let stack_trace_dump = Self::get_stack_trace(context).join("\n");
logger(LogMessage::Log(stack_trace_dump), context.console());
}
Ok(JsValue::undefined())
}
/// `console.warn(...data)`
///
/// Prints a JavaScript values with "warn" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#warn
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/warn
pub(crate) fn warn(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Warn(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.count(label)`
///
/// Prints number of times the function was called with that particular label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#count
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/count
pub(crate) fn count(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
let msg = format!("count {label}:");
let c = context.console_mut().count_map.entry(label).or_insert(0);
*c += 1;
logger(LogMessage::Info(format!("{msg} {c}")), context.console());
Ok(JsValue::undefined())
}
/// `console.countReset(label)`
///
/// Resets the counter for label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#countreset
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/countReset
pub(crate) fn count_reset(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
context.console_mut().count_map.remove(&label);
logger(
LogMessage::Warn(format!("countReset {label}")),
context.console(),
);
Ok(JsValue::undefined())
}
/// Returns current system time in ms.
fn system_time_in_ms() -> u128 {
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH)
.expect("negative duration")
.as_millis()
}
/// `console.time(label)`
///
/// Starts the timer for given label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#time
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/time
pub(crate) fn time(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if context.console().timer_map.get(&label).is_some() {
logger(
LogMessage::Warn(format!("Timer '{label}' already exist")),
context.console(),
);
} else {
let time = Self::system_time_in_ms();
context.console_mut().timer_map.insert(label, time);
}
Ok(JsValue::undefined())
}
/// `console.timeLog(label, ...data)`
///
/// Prints elapsed time for timer with given label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#timelog
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog
pub(crate) fn time_log(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if let Some(t) = context.console().timer_map.get(&label) {
let time = Self::system_time_in_ms();
let mut concat = format!("{label}: {} ms", time - t);
for msg in args.iter().skip(1) {
concat = concat + " " + &msg.display().to_string();
}
logger(LogMessage::Log(concat), context.console());
} else {
logger(
LogMessage::Warn(format!("Timer '{label}' doesn't exist")),
context.console(),
);
}
Ok(JsValue::undefined())
}
/// `console.timeEnd(label)`
///
/// Removes the timer with given label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#timeend
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd
pub(crate) fn time_end(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if let Some(t) = context.console_mut().timer_map.remove(label.as_str()) {
let time = Self::system_time_in_ms();
logger(
LogMessage::Info(format!("{label}: {} ms - timer removed", time - t)),
context.console(),
);
} else {
logger(
LogMessage::Warn(format!("Timer '{label}' doesn't exist")),
context.console(),
);
}
Ok(JsValue::undefined())
}
/// `console.group(...data)`
///
/// Adds new group with name from formatted data to stack.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#group
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/group
pub(crate) fn group(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let group_label = formatter(args, context)?;
logger(
LogMessage::Info(format!("group: {group_label}")),
context.console(),
);
context.console_mut().groups.push(group_label);
Ok(JsValue::undefined())
}
/// `console.groupEnd(label)`
///
/// Removes the last group from the stack.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#groupend
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupEnd
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn group_end(
_: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
context.console_mut().groups.pop();
Ok(JsValue::undefined())
}
/// `console.dir(item, options)`
///
/// Prints info about item
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#dir
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn dir(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Info(display_obj(args.get_or_undefined(0), true)),
context.console(),
);
Ok(JsValue::undefined())
}
}

View File

@@ -0,0 +1,68 @@
use crate::{builtins::console::formatter, Context, JsValue};
#[test]
fn formatter_no_args_is_empty_string() {
let mut context = Context::default();
assert_eq!(formatter(&[], &mut context).unwrap(), "");
}
#[test]
fn formatter_empty_format_string_is_empty_string() {
let mut context = Context::default();
let val = JsValue::new("");
assert_eq!(formatter(&[val], &mut context).unwrap(), "");
}
#[test]
fn formatter_format_without_args_renders_verbatim() {
let mut context = Context::default();
let val = [JsValue::new("%d %s %% %f")];
let res = formatter(&val, &mut context).unwrap();
assert_eq!(res, "%d %s %% %f");
}
#[test]
fn formatter_empty_format_string_concatenates_rest_of_args() {
let mut context = Context::default();
let val = [
JsValue::new(""),
JsValue::new("to powinno zostać"),
JsValue::new("połączone"),
];
let res = formatter(&val, &mut context).unwrap();
assert_eq!(res, " to powinno zostać połączone");
}
#[test]
fn formatter_utf_8_checks() {
let mut context = Context::default();
let val = [
JsValue::new("Są takie chwile %dą %są tu%sów %привет%ź".to_string()),
JsValue::new(123),
JsValue::new(1.23),
JsValue::new("ł"),
];
let res = formatter(&val, &mut context).unwrap();
assert_eq!(res, "Są takie chwile 123ą 1.23ą tułów %привет%ź");
}
#[test]
fn formatter_trailing_format_leader_renders() {
let mut context = Context::default();
let val = [JsValue::new("%%%%%"), JsValue::new("|")];
let res = formatter(&val, &mut context).unwrap();
assert_eq!(res, "%%% |");
}
#[test]
#[allow(clippy::approx_constant)]
fn formatter_float_format_works() {
let mut context = Context::default();
let val = [JsValue::new("%f"), JsValue::new(3.1415)];
let res = formatter(&val, &mut context).unwrap();
assert_eq!(res, "3.141500");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
//! This module implements the global `AggregateError` object.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-aggregate-error
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
use crate::{
builtins::{iterable::iterable_to_list, Array, BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::{Attribute, PropertyDescriptorBuilder},
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use super::Error;
#[derive(Debug, Clone, Copy)]
pub(crate) struct AggregateError;
impl BuiltIn for AggregateError {
const NAME: &'static str = "AggregateError";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let error_constructor = context.intrinsics().constructors().error().constructor();
let error_prototype = context.intrinsics().constructors().error().prototype();
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context
.intrinsics()
.constructors()
.aggregate_error()
.clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.inherit(error_prototype)
.custom_prototype(error_constructor)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl AggregateError {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 2;
/// Create a new aggregate error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%AggregateError.prototype%", « [[ErrorData]] »).
let prototype = get_prototype_from_constructor(
new_target,
StandardConstructors::aggregate_error,
context,
)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(1);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Error::install_error_cause(&o, args.get_or_undefined(2), context)?;
// 5. Let errorsList be ? IterableToList(errors).
let errors = args.get_or_undefined(0);
let errors_list = iterable_to_list(context, errors, None)?;
// 6. Perform ! DefinePropertyOrThrow(O, "errors",
// PropertyDescriptor {
// [[Configurable]]: true,
// [[Enumerable]]: false,
// [[Writable]]: true,
// [[Value]]: CreateArrayFromList(errorsList)
// }).
o.define_property_or_throw(
"errors",
PropertyDescriptorBuilder::new()
.configurable(true)
.enumerable(false)
.writable(true)
.value(Array::create_array_from_list(errors_list, context))
.build(),
context,
)
.expect("should not fail according to spec");
// 5. Return O.
Ok(o.into())
}
}

View File

@@ -0,0 +1,91 @@
//! This module implements the global `EvalError` object.
//!
//! Indicates an error regarding the global `eval()` function.
//! This exception is not thrown by JavaScript anymore, however
//! the `EvalError` object remains for compatibility.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-evalerror
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError
use crate::{
builtins::{BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use super::Error;
/// JavaScript `EvalError` implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct EvalError;
impl BuiltIn for EvalError {
const NAME: &'static str = "EvalError";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let error_constructor = context.intrinsics().constructors().error().constructor();
let error_prototype = context.intrinsics().constructors().error().prototype();
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().eval_error().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.inherit(error_prototype)
.custom_prototype(error_constructor)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl EvalError {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// Create a new error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::eval_error, context)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(0);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Error::install_error_cause(&o, args.get_or_undefined(1), context)?;
// 5. Return O.
Ok(o.into())
}
}

View File

@@ -0,0 +1,187 @@
//! This module implements the global `Error` object.
//!
//! Error objects are thrown when runtime errors occur.
//! The Error object can also be used as a base object for user-defined exceptions.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-error-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
use crate::{
builtins::BuiltIn,
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
Context, JsResult, JsString, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
pub(crate) mod aggregate;
pub(crate) mod eval;
pub(crate) mod range;
pub(crate) mod reference;
pub(crate) mod syntax;
pub(crate) mod r#type;
pub(crate) mod uri;
#[cfg(test)]
mod tests;
pub(crate) use self::aggregate::AggregateError;
pub(crate) use self::eval::EvalError;
pub(crate) use self::r#type::TypeError;
pub(crate) use self::range::RangeError;
pub(crate) use self::reference::ReferenceError;
pub(crate) use self::syntax::SyntaxError;
pub(crate) use self::uri::UriError;
use super::JsArgs;
/// Built-in `Error` object.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Error;
impl BuiltIn for Error {
const NAME: &'static str = "Error";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().error().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.method(Self::to_string, "toString", 0)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Error {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
pub(crate) fn install_error_cause(
o: &JsObject,
options: &JsValue,
context: &mut Context,
) -> JsResult<()> {
// 1. If Type(options) is Object and ? HasProperty(options, "cause") is true, then
if let Some(options) = options.as_object() {
if options.has_property("cause", context)? {
// a. Let cause be ? Get(options, "cause").
let cause = options.get("cause", context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "cause", cause).
o.create_non_enumerable_data_property_or_throw("cause", cause, context);
}
}
// 2. Return unused.
Ok(())
}
/// `Error( message [ , options ] )`
///
/// Create a new error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%Error.prototype%", « [[ErrorData]] »).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::error, context)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(0);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Self::install_error_cause(&o, args.get_or_undefined(1), context)?;
// 5. Return O.
Ok(o.into())
}
/// `Error.prototype.toString()`
///
/// The toString() method returns a string representing the specified Error object.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-error.prototype.tostring
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_string(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
let o = if let Some(o) = this.as_object() {
o
// 2. If Type(O) is not Object, throw a TypeError exception.
} else {
return context.throw_type_error("'this' is not an Object");
};
// 3. Let name be ? Get(O, "name").
let name = o.get("name", context)?;
// 4. If name is undefined, set name to "Error"; otherwise set name to ? ToString(name).
let name = if name.is_undefined() {
JsString::new("Error")
} else {
name.to_string(context)?
};
// 5. Let msg be ? Get(O, "message").
let msg = o.get("message", context)?;
// 6. If msg is undefined, set msg to the empty String; otherwise set msg to ? ToString(msg).
let msg = if msg.is_undefined() {
JsString::empty()
} else {
msg.to_string(context)?
};
// 7. If name is the empty String, return msg.
if name.is_empty() {
return Ok(msg.into());
}
// 8. If msg is the empty String, return name.
if msg.is_empty() {
return Ok(name.into());
}
// 9. Return the string-concatenation of name, the code unit 0x003A (COLON),
// the code unit 0x0020 (SPACE), and msg.
Ok(format!("{name}: {msg}").into())
}
}

View File

@@ -0,0 +1,89 @@
//! This module implements the global `RangeError` object.
//!
//! Indicates a value that is not in the set or range of allowable values.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError
use crate::{
builtins::{BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use super::Error;
/// JavaScript `RangeError` implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct RangeError;
impl BuiltIn for RangeError {
const NAME: &'static str = "RangeError";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let error_constructor = context.intrinsics().constructors().error().constructor();
let error_prototype = context.intrinsics().constructors().error().prototype();
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().range_error().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.inherit(error_prototype)
.custom_prototype(error_constructor)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl RangeError {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// Create a new error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::range_error, context)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(0);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Error::install_error_cause(&o, args.get_or_undefined(1), context)?;
// 5. Return O.
Ok(o.into())
}
}

View File

@@ -0,0 +1,95 @@
//! This module implements the global `ReferenceError` object.
//!
//! Indicates an error that occurs when de-referencing an invalid reference
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-referenceerror
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError
use crate::{
builtins::{BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use super::Error;
#[derive(Debug, Clone, Copy)]
pub(crate) struct ReferenceError;
impl BuiltIn for ReferenceError {
const NAME: &'static str = "ReferenceError";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let error_constructor = context.intrinsics().constructors().error().constructor();
let error_prototype = context.intrinsics().constructors().error().prototype();
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context
.intrinsics()
.constructors()
.reference_error()
.clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.inherit(error_prototype)
.custom_prototype(error_constructor)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl ReferenceError {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// Create a new error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »).
let prototype = get_prototype_from_constructor(
new_target,
StandardConstructors::reference_error,
context,
)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(0);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Error::install_error_cause(&o, args.get_or_undefined(1), context)?;
// 5. Return O.
Ok(o.into())
}
}

View File

@@ -0,0 +1,94 @@
//! This module implements the global `SyntaxError` object.
//!
//! The `SyntaxError` object represents an error when trying to interpret syntactically invalid code.
//! It is thrown when the JavaScript context encounters tokens or token order that does not conform
//! to the syntax of the language when parsing code.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-syntaxerror
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError
use crate::{
builtins::{BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use super::Error;
/// JavaScript `SyntaxError` implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct SyntaxError;
impl BuiltIn for SyntaxError {
const NAME: &'static str = "SyntaxError";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let error_constructor = context.intrinsics().constructors().error().constructor();
let error_prototype = context.intrinsics().constructors().error().prototype();
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().syntax_error().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.inherit(error_prototype)
.custom_prototype(error_constructor)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl SyntaxError {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// Create a new error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »).
let prototype = get_prototype_from_constructor(
new_target,
StandardConstructors::syntax_error,
context,
)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(0);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Error::install_error_cause(&o, args.get_or_undefined(1), context)?;
// 5. Return O.
Ok(o.into())
}
}

View File

@@ -0,0 +1,86 @@
use crate::{forward, Context};
#[test]
fn error_to_string() {
let mut context = Context::default();
let init = r#"
let e = new Error('1');
let name = new Error();
let message = new Error('message');
message.name = '';
let range_e = new RangeError('2');
let ref_e = new ReferenceError('3');
let syntax_e = new SyntaxError('4');
let type_e = new TypeError('5');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "e.toString()"), "\"Error: 1\"");
assert_eq!(forward(&mut context, "name.toString()"), "\"Error\"");
assert_eq!(forward(&mut context, "message.toString()"), "\"message\"");
assert_eq!(
forward(&mut context, "range_e.toString()"),
"\"RangeError: 2\""
);
assert_eq!(
forward(&mut context, "ref_e.toString()"),
"\"ReferenceError: 3\""
);
assert_eq!(
forward(&mut context, "syntax_e.toString()"),
"\"SyntaxError: 4\""
);
assert_eq!(
forward(&mut context, "type_e.toString()"),
"\"TypeError: 5\""
);
}
#[test]
fn eval_error_name() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "EvalError.name"), "\"EvalError\"");
}
#[test]
fn eval_error_length() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "EvalError.length"), "1");
}
#[test]
fn eval_error_to_string() {
let mut context = Context::default();
assert_eq!(
forward(&mut context, "new EvalError('hello').toString()"),
"\"EvalError: hello\""
);
assert_eq!(
forward(&mut context, "new EvalError().toString()"),
"\"EvalError\""
);
}
#[test]
fn uri_error_name() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "URIError.name"), "\"URIError\"");
}
#[test]
fn uri_error_length() {
let mut context = Context::default();
assert_eq!(forward(&mut context, "URIError.length"), "1");
}
#[test]
fn uri_error_to_string() {
let mut context = Context::default();
assert_eq!(
forward(&mut context, "new URIError('hello').toString()"),
"\"URIError: hello\""
);
assert_eq!(
forward(&mut context, "new URIError().toString()"),
"\"URIError\""
);
}

View File

@@ -0,0 +1,118 @@
//! This module implements the global `TypeError` object.
//!
//! The `TypeError` object represents an error when an operation could not be performed,
//! typically (but not exclusively) when a value is not of the expected type.
//!
//! A `TypeError` may be thrown when:
//! - an operand or argument passed to a function is incompatible with the type expected by that operator or function.
//! - when attempting to modify a value that cannot be changed.
//! - when attempting to use a value in an inappropriate way.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-typeerror
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
use crate::{
builtins::{function::Function, BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::{Attribute, PropertyDescriptor},
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use super::Error;
/// JavaScript `TypeError` implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct TypeError;
impl BuiltIn for TypeError {
const NAME: &'static str = "TypeError";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let error_constructor = context.intrinsics().constructors().error().constructor();
let error_prototype = context.intrinsics().constructors().error().prototype();
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().type_error().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.inherit(error_prototype)
.custom_prototype(error_constructor)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl TypeError {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// Create a new error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::type_error, context)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(0);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Error::install_error_cause(&o, args.get_or_undefined(1), context)?;
// 5. Return O.
Ok(o.into())
}
}
pub(crate) fn create_throw_type_error(context: &mut Context) -> JsObject {
fn throw_type_error(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
context.throw_type_error("'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them")
}
let function = JsObject::from_proto_and_data(
context.intrinsics().constructors().function().prototype(),
ObjectData::function(Function::Native {
function: throw_type_error,
constructor: None,
}),
);
let property = PropertyDescriptor::builder()
.writable(false)
.enumerable(false)
.configurable(false);
function.insert_property("name", property.clone().value("ThrowTypeError"));
function.insert_property("length", property.value(0));
function
}

View File

@@ -0,0 +1,90 @@
//! This module implements the global `URIError` object.
//!
//! The `URIError` object represents an error when a global URI handling
//! function was used in a wrong way.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-urierror
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
use crate::{
builtins::{BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use super::Error;
/// JavaScript `URIError` implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct UriError;
impl BuiltIn for UriError {
const NAME: &'static str = "URIError";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let error_constructor = context.intrinsics().constructors().error().constructor();
let error_prototype = context.intrinsics().constructors().error().prototype();
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().uri_error().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.inherit(error_prototype)
.custom_prototype(error_constructor)
.property("name", Self::NAME, attribute)
.property("message", "", attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl UriError {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 1;
/// Create a new error object.
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
// 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::uri_error, context)?;
let o = JsObject::from_proto_and_data(prototype, ObjectData::error());
// 3. If message is not undefined, then
let message = args.get_or_undefined(0);
if !message.is_undefined() {
// a. Let msg be ? ToString(message).
let msg = message.to_string(context)?;
// b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg).
o.create_non_enumerable_data_property_or_throw("message", msg, context);
}
// 4. Perform ? InstallErrorCause(O, options).
Error::install_error_cause(&o, args.get_or_undefined(1), context)?;
// 5. Return O.
Ok(o.into())
}
}

View File

@@ -0,0 +1,149 @@
//! This module implements the global `eval` function.
//!
//! The `eval()` function evaluates JavaScript code represented as a string.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-eval-x
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
use crate::{
builtins::{BuiltIn, JsArgs},
object::FunctionBuilder,
property::Attribute,
Context, JsValue,
};
use boa_profiler::Profiler;
use rustc_hash::FxHashSet;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Eval;
impl BuiltIn for Eval {
const NAME: &'static str = "eval";
const ATTRIBUTE: Attribute = Attribute::CONFIGURABLE
.union(Attribute::NON_ENUMERABLE)
.union(Attribute::WRITABLE);
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let object = FunctionBuilder::native(context, Self::eval)
.name("eval")
.length(1)
.constructor(false)
.build();
Some(object.into())
}
}
impl Eval {
/// `19.2.1 eval ( x )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-eval-x
fn eval(_: &JsValue, args: &[JsValue], context: &mut Context) -> Result<JsValue, JsValue> {
// 1. Return ? PerformEval(x, false, false).
Self::perform_eval(args.get_or_undefined(0), false, false, context)
}
/// `19.2.1.1 PerformEval ( x, strictCaller, direct )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-performeval
pub(crate) fn perform_eval(
x: &JsValue,
direct: bool,
strict: bool,
context: &mut Context,
) -> Result<JsValue, JsValue> {
// 1. Assert: If direct is false, then strictCaller is also false.
if !direct {
debug_assert!(!strict);
}
// 2. If Type(x) is not String, return x.
let x = if let Some(x) = x.as_string() {
x.clone()
} else {
return Ok(x.clone());
};
// Because of implementation details the following code differs from the spec.
// Parse the script body and handle early errors (6 - 11)
let body = match context.parse_eval(x.as_bytes(), direct, strict) {
Ok(body) => body,
Err(e) => return context.throw_syntax_error(e.to_string()),
};
// 12 - 13 are implicit in the call of `Context::compile_with_new_declarative`.
// Because our environment model does not map directly to the spec this section looks very different.
// 14 - 33 are in the following section, together with EvalDeclarationInstantiation.
if direct {
// If the call to eval is direct, the code is executed in the current environment.
// Poison the current environment, because it may contain new declarations after/during eval.
context.realm.environments.poison_current();
// Set the compile time environment to the current running environment and save the number of current environments.
context.realm.compile_env = context.realm.environments.current_compile_environment();
let environments_len = context.realm.environments.len();
// Error if any var declaration in the eval code already exists as a let/const declaration in the current running environment.
let mut vars = FxHashSet::default();
body.var_declared_names_new(&mut vars);
if let Some(name) = context
.realm
.environments
.has_lex_binding_until_function_environment(&vars)
{
let name = context.interner().resolve_expect(name);
let msg = format!("variable declaration {name} in eval function already exists as lexically declaration");
return context.throw_syntax_error(msg);
}
// Compile and execute the eval statement list.
let code_block = context.compile_with_new_declarative(&body, strict)?;
context
.realm
.environments
.extend_outer_function_environment();
let result = context.execute(code_block);
// Pop any added runtime environments that where not removed during the eval execution.
context.realm.environments.truncate(environments_len);
result
} else {
// If the call to eval is indirect, the code is executed in the global environment.
// Poison all environments, because the global environment may contain new declarations after/during eval.
context.realm.environments.poison_all();
// Pop all environments before the eval execution.
let environments = context.realm.environments.pop_to_global();
let environments_len = context.realm.environments.len();
context.realm.compile_env = context.realm.environments.current_compile_environment();
// Compile and execute the eval statement list.
let code_block = context.compile_with_new_declarative(&body, false)?;
let result = context.execute(code_block);
// Restore all environments to the state from before the eval execution.
context.realm.environments.truncate(environments_len);
context.realm.environments.extend(environments);
result
}
}
}

View File

@@ -0,0 +1,293 @@
use crate::{
builtins::Array,
environments::DeclarativeEnvironment,
object::{JsObject, ObjectData},
property::PropertyDescriptor,
symbol::{self, WellKnownSymbols},
syntax::ast::node::FormalParameterList,
Context, JsValue,
};
use boa_gc::{Finalize, Gc, Trace};
use rustc_hash::FxHashMap;
/// `ParameterMap` represents the `[[ParameterMap]]` internal slot on a Arguments exotic object.
///
/// This struct stores all the data to access mapped function parameters in their environment.
#[derive(Debug, Clone, Trace, Finalize)]
pub struct ParameterMap {
binding_indices: Vec<Option<usize>>,
environment: Gc<DeclarativeEnvironment>,
}
impl ParameterMap {
/// Deletes the binding with the given index from the parameter map.
pub(crate) fn delete(&mut self, index: usize) {
if let Some(binding) = self.binding_indices.get_mut(index) {
*binding = None;
}
}
/// Get the value of the binding at the given index from the function environment.
///
/// Note: This function is the abstract getter closure described in 10.4.4.7.1 `MakeArgGetter ( name, env )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-makearggetter
pub(crate) fn get(&self, index: usize) -> Option<JsValue> {
if let Some(Some(binding_index)) = self.binding_indices.get(index) {
return Some(self.environment.get(*binding_index));
}
None
}
/// Set the value of the binding at the given index in the function environment.
///
/// Note: This function is the abstract setter closure described in 10.4.4.7.2 `MakeArgSetter ( name, env )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-makeargsetter
pub(crate) fn set(&self, index: usize, value: &JsValue) {
if let Some(Some(binding_index)) = self.binding_indices.get(index) {
self.environment.set(*binding_index, value.clone());
}
}
}
#[derive(Debug, Clone, Trace, Finalize)]
pub enum Arguments {
Unmapped,
Mapped(ParameterMap),
}
impl Arguments {
/// Creates a new unmapped Arguments ordinary object.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createunmappedargumentsobject
pub(crate) fn create_unmapped_arguments_object(
arguments_list: &[JsValue],
context: &mut Context,
) -> JsObject {
// 1. Let len be the number of elements in argumentsList.
let len = arguments_list.len();
// 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%, « [[ParameterMap]] »).
let obj = context.construct_object();
// 3. Set obj.[[ParameterMap]] to undefined.
// skipped because the `Arguments` enum ensures ordinary argument objects don't have a `[[ParameterMap]]`
obj.borrow_mut().data = ObjectData::arguments(Self::Unmapped);
// 4. Perform DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len),
// [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }).
obj.define_property_or_throw(
"length",
PropertyDescriptor::builder()
.value(len)
.writable(true)
.enumerable(false)
.configurable(true),
context,
)
.expect("Defining new own properties for a new ordinary object cannot fail");
// 5. Let index be 0.
// 6. Repeat, while index < len,
for (index, value) in arguments_list.iter().cloned().enumerate() {
// a. Let val be argumentsList[index].
// b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val).
obj.create_data_property_or_throw(index, value, context)
.expect("Defining new own properties for a new ordinary object cannot fail");
// c. Set index to index + 1.
}
// 7. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor {
// [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false,
// [[Configurable]]: true }).
obj.define_property_or_throw(
symbol::WellKnownSymbols::iterator(),
PropertyDescriptor::builder()
.value(Array::values_intrinsic(context))
.writable(true)
.enumerable(false)
.configurable(true),
context,
)
.expect("Defining new own properties for a new ordinary object cannot fail");
let throw_type_error = context.intrinsics().objects().throw_type_error();
// 8. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor {
// [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false,
// [[Configurable]]: false }).
obj.define_property_or_throw(
"callee",
PropertyDescriptor::builder()
.get(throw_type_error.clone())
.set(throw_type_error)
.enumerable(false)
.configurable(false),
context,
)
.expect("Defining new own properties for a new ordinary object cannot fail");
// 9. Return obj.
obj
}
/// Creates a new mapped Arguments exotic object.
///
/// <https://tc39.es/ecma262/#sec-createmappedargumentsobject>
pub(crate) fn create_mapped_arguments_object(
func: &JsObject,
formals: &FormalParameterList,
arguments_list: &[JsValue],
env: &Gc<DeclarativeEnvironment>,
context: &mut Context,
) -> JsObject {
// 1. Assert: formals does not contain a rest parameter, any binding patterns, or any initializers.
// It may contain duplicate identifiers.
// 2. Let len be the number of elements in argumentsList.
let len = arguments_list.len();
// 3. Let obj be ! MakeBasicObject(« [[Prototype]], [[Extensible]], [[ParameterMap]] »).
// 4. Set obj.[[GetOwnProperty]] as specified in 10.4.4.1.
// 5. Set obj.[[DefineOwnProperty]] as specified in 10.4.4.2.
// 6. Set obj.[[Get]] as specified in 10.4.4.3.
// 7. Set obj.[[Set]] as specified in 10.4.4.4.
// 8. Set obj.[[Delete]] as specified in 10.4.4.5.
// 9. Set obj.[[Prototype]] to %Object.prototype%.
// Section 17-19 are done first, for easier object creation in 11.
//
// The section 17-19 differs from the spec, due to the way the runtime environments work.
//
// This section creates getters and setters for all mapped arguments.
// Getting and setting values on the `arguments` object will actually access the bindings in the environment:
// ```
// function f(a) {console.log(a); arguments[0] = 1; console.log(a)};
// f(0) // 0, 1
// ```
//
// The spec assumes, that identifiers are used at runtime to reference bindings in the environment.
// We use indices to access environment bindings at runtime.
// To map to function parameters to binding indices, we use the fact, that bindings in a
// function environment start with all of the arguments in order:
// `function f (a,b,c)`
// | binding index | `arguments` property key | identifier |
// | 0 | 0 | a |
// | 1 | 1 | b |
// | 2 | 2 | c |
//
// Notice that the binding index does not correspond to the argument index:
// `function f (a,a,b)` => binding indices 0 (a), 1 (b), 2 (c)
// | binding index | `arguments` property key | identifier |
// | - | 0 | - |
// | 0 | 1 | a |
// | 1 | 2 | b |
// While the `arguments` object contains all arguments, they must not be all bound.
// In the case of duplicate parameter names, the last one is bound as the environment binding.
//
// The following logic implements the steps 17-19 adjusted for our environment structure.
let mut bindings = FxHashMap::default();
let mut property_index = 0;
'outer: for formal in formals.parameters.iter() {
for name in formal.names() {
if property_index >= len {
break 'outer;
}
let binding_index = bindings.len() + 1;
let entry = bindings
.entry(name)
.or_insert((binding_index, property_index));
entry.1 = property_index;
property_index += 1;
}
}
let mut map = ParameterMap {
binding_indices: vec![None; property_index],
environment: env.clone(),
};
for (binding_index, property_index) in bindings.values() {
map.binding_indices[*property_index] = Some(*binding_index);
}
// 11. Set obj.[[ParameterMap]] to map.
let obj = JsObject::from_proto_and_data(
context.intrinsics().constructors().object().prototype(),
ObjectData::arguments(Self::Mapped(map)),
);
// 14. Let index be 0.
// 15. Repeat, while index < len,
for (index, val) in arguments_list.iter().cloned().enumerate() {
// a. Let val be argumentsList[index].
// b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val).
// Note: Insert is used here because `CreateDataPropertyOrThrow` would cause a panic while executing
// exotic argument object set methods before the variables in the environment are initialized.
obj.insert(
index,
PropertyDescriptor::builder()
.value(val)
.writable(true)
.enumerable(true)
.configurable(true)
.build(),
);
// c. Set index to index + 1.
}
// 16. Perform ! DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len),
// [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }).
obj.define_property_or_throw(
"length",
PropertyDescriptor::builder()
.value(len)
.writable(true)
.enumerable(false)
.configurable(true),
context,
)
.expect("Defining new own properties for a new ordinary object cannot fail");
// 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor {
// [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false,
// [[Configurable]]: true }).
obj.define_property_or_throw(
WellKnownSymbols::iterator(),
PropertyDescriptor::builder()
.value(Array::values_intrinsic(context))
.writable(true)
.enumerable(false)
.configurable(true),
context,
)
.expect("Defining new own properties for a new ordinary object cannot fail");
// 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor {
// [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }).
obj.define_property_or_throw(
"callee",
PropertyDescriptor::builder()
.value(func.clone())
.writable(true)
.enumerable(false)
.configurable(true),
context,
)
.expect("Defining new own properties for a new ordinary object cannot fail");
// 22. Return obj.
obj
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
use crate::{
forward, forward_val,
object::FunctionBuilder,
property::{Attribute, PropertyDescriptor},
Context, JsString,
};
#[allow(clippy::float_cmp)]
#[test]
fn arguments_object() {
let mut context = Context::default();
let init = r#"
function jason(a, b) {
return arguments[0];
}
var val = jason(100, 6);
"#;
eprintln!("{}", forward(&mut context, init));
let return_val = forward_val(&mut context, "val").expect("value expected");
assert!(return_val.is_integer());
assert_eq!(
return_val
.to_i32(&mut context)
.expect("Could not convert value to i32"),
100
);
}
#[test]
fn self_mutating_function_when_calling() {
let mut context = Context::default();
let func = r#"
function x() {
x.y = 3;
}
x();
"#;
eprintln!("{}", forward(&mut context, func));
let y = forward_val(&mut context, "x.y").expect("value expected");
assert!(y.is_integer());
assert_eq!(
y.to_i32(&mut context)
.expect("Could not convert value to i32"),
3
);
}
#[test]
fn self_mutating_function_when_constructing() {
let mut context = Context::default();
let func = r#"
function x() {
x.y = 3;
}
new x();
"#;
eprintln!("{}", forward(&mut context, func));
let y = forward_val(&mut context, "x.y").expect("value expected");
assert!(y.is_integer());
assert_eq!(
y.to_i32(&mut context)
.expect("Could not convert value to i32"),
3
);
}
#[test]
fn call_function_prototype() {
let mut context = Context::default();
let func = r#"
Function.prototype()
"#;
let value = forward_val(&mut context, func).unwrap();
assert!(value.is_undefined());
}
#[test]
fn call_function_prototype_with_arguments() {
let mut context = Context::default();
let func = r#"
Function.prototype(1, "", new String(""))
"#;
let value = forward_val(&mut context, func).unwrap();
assert!(value.is_undefined());
}
#[test]
fn call_function_prototype_with_new() {
let mut context = Context::default();
let func = r#"
new Function.prototype()
"#;
let value = forward_val(&mut context, func);
assert!(value.is_err());
}
#[test]
fn function_prototype_name() {
let mut context = Context::default();
let func = r#"
Function.prototype.name
"#;
let value = forward_val(&mut context, func).unwrap();
assert!(value.is_string());
assert!(value.as_string().unwrap().is_empty());
}
#[test]
#[allow(clippy::float_cmp)]
fn function_prototype_length() {
let mut context = Context::default();
let func = r#"
Function.prototype.length
"#;
let value = forward_val(&mut context, func).unwrap();
assert!(value.is_number());
assert_eq!(value.as_number().unwrap(), 0.0);
}
#[test]
fn function_prototype_call() {
let mut context = Context::default();
let func = r#"
let e = new Error()
Object.prototype.toString.call(e)
"#;
let value = forward_val(&mut context, func).unwrap();
assert!(value.is_string());
assert_eq!(value.as_string().unwrap(), "[object Error]");
}
#[test]
fn function_prototype_call_throw() {
let mut context = Context::default();
let throw = r#"
let call = Function.prototype.call;
call(call)
"#;
let value = forward_val(&mut context, throw).unwrap_err();
assert!(value.is_object());
let string = value.to_string(&mut context).unwrap();
assert!(string.starts_with("TypeError"));
}
#[test]
fn function_prototype_call_multiple_args() {
let mut context = Context::default();
let init = r#"
function f(a, b) {
this.a = a;
this.b = b;
}
let o = {a: 0, b: 0};
f.call(o, 1, 2);
"#;
forward_val(&mut context, init).unwrap();
let boolean = forward_val(&mut context, "o.a == 1")
.unwrap()
.as_boolean()
.unwrap();
assert!(boolean);
let boolean = forward_val(&mut context, "o.b == 2")
.unwrap()
.as_boolean()
.unwrap();
assert!(boolean);
}
#[test]
fn function_prototype_apply() {
let mut context = Context::default();
let init = r#"
const numbers = [6, 7, 3, 4, 2];
const max = Math.max.apply(null, numbers);
const min = Math.min.apply(null, numbers);
"#;
forward_val(&mut context, init).unwrap();
let boolean = forward_val(&mut context, "max == 7")
.unwrap()
.as_boolean()
.unwrap();
assert!(boolean);
let boolean = forward_val(&mut context, "min == 2")
.unwrap()
.as_boolean()
.unwrap();
assert!(boolean);
}
#[test]
fn function_prototype_apply_on_object() {
let mut context = Context::default();
let init = r#"
function f(a, b) {
this.a = a;
this.b = b;
}
let o = {a: 0, b: 0};
f.apply(o, [1, 2]);
"#;
forward_val(&mut context, init).unwrap();
let boolean = forward_val(&mut context, "o.a == 1")
.unwrap()
.as_boolean()
.unwrap();
assert!(boolean);
let boolean = forward_val(&mut context, "o.b == 2")
.unwrap()
.as_boolean()
.unwrap();
assert!(boolean);
}
#[test]
fn closure_capture_clone() {
let mut context = Context::default();
let string = JsString::from("Hello");
let object = context.construct_object();
object
.define_property_or_throw(
"key",
PropertyDescriptor::builder()
.value(" world!")
.writable(false)
.enumerable(false)
.configurable(false),
&mut context,
)
.unwrap();
let func = FunctionBuilder::closure_with_captures(
&mut context,
|_, _, captures, context| {
let (string, object) = &captures;
let hw = JsString::concat(
string,
object
.__get_own_property__(&"key".into(), context)?
.and_then(|prop| prop.value().cloned())
.and_then(|val| val.as_string().cloned())
.ok_or_else(|| context.construct_type_error("invalid `key` property"))?,
);
Ok(hw.into())
},
(string, object),
)
.name("closure")
.build();
context.register_global_property("closure", func, Attribute::default());
assert_eq!(forward(&mut context, "closure()"), "\"Hello world!\"");
}

View File

@@ -0,0 +1,419 @@
//! This module implements the global `Generator` object.
//!
//! A Generator is an instance of a generator function and conforms to both the Iterator and Iterable interfaces.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-generator-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
use crate::{
builtins::{iterable::create_iter_result_object, BuiltIn, JsArgs},
environments::DeclarativeEnvironmentStack,
object::{ConstructorBuilder, JsObject, ObjectData},
property::{Attribute, PropertyDescriptor},
symbol::WellKnownSymbols,
value::JsValue,
vm::{CallFrame, GeneratorResumeKind, ReturnType},
Context, JsResult,
};
use boa_gc::{Cell, Finalize, Gc, Trace};
use boa_profiler::Profiler;
/// Indicates the state of a generator.
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum GeneratorState {
Undefined,
SuspendedStart,
SuspendedYield,
Executing,
Completed,
}
/// Holds all information that a generator needs to continue it's execution.
///
/// All of the fields must be changed with those that are currently present in the
/// context/vm before the generator execution starts/resumes and after it has ended/yielded.
#[derive(Debug, Clone, Finalize, Trace)]
pub(crate) struct GeneratorContext {
pub(crate) environments: DeclarativeEnvironmentStack,
pub(crate) call_frame: CallFrame,
pub(crate) stack: Vec<JsValue>,
}
/// The internal representation on a `Generator` object.
#[derive(Debug, Clone, Finalize, Trace)]
pub struct Generator {
/// The `[[GeneratorState]]` internal slot.
#[unsafe_ignore_trace]
pub(crate) state: GeneratorState,
/// The `[[GeneratorContext]]` internal slot.
pub(crate) context: Option<Gc<Cell<GeneratorContext>>>,
}
impl BuiltIn for Generator {
const NAME: &'static str = "Generator";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let iterator_prototype = context
.intrinsics()
.objects()
.iterator_prototypes()
.iterator_prototype();
let generator_function_prototype = context
.intrinsics()
.constructors()
.generator_function()
.prototype();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().generator().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.property(
WellKnownSymbols::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.method(Self::next, "next", 1)
.method(Self::r#return, "return", 1)
.method(Self::throw, "throw", 1)
.inherit(iterator_prototype)
.build();
context
.intrinsics()
.constructors()
.generator()
.prototype
.insert_property(
"constructor",
PropertyDescriptor::builder()
.value(generator_function_prototype)
.writable(false)
.enumerable(false)
.configurable(true),
);
None
}
}
impl Generator {
pub(crate) const LENGTH: usize = 0;
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn constructor(
_: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let prototype = context.intrinsics().constructors().generator().prototype();
let this = JsObject::from_proto_and_data(
prototype,
ObjectData::generator(Self {
state: GeneratorState::Undefined,
context: None,
}),
);
Ok(this.into())
}
/// `Generator.prototype.next ( value )`
///
/// The `next()` method returns an object with two properties done and value.
/// You can also provide a parameter to the next method to send a value to the generator.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.next
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next
pub(crate) fn next(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Return ? GeneratorResume(this value, value, empty).
match this.as_object() {
Some(obj) if obj.is_generator() => {
Self::generator_resume(obj, args.get_or_undefined(0), context)
}
_ => context.throw_type_error("Generator.prototype.next called on non generator"),
}
}
/// `Generator.prototype.return ( value )`
///
/// The `return()` method returns the given value and finishes the generator.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.return
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return
pub(crate) fn r#return(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let g be the this value.
// 2. Let C be Completion { [[Type]]: return, [[Value]]: value, [[Target]]: empty }.
// 3. Return ? GeneratorResumeAbrupt(g, C, empty).
Self::generator_resume_abrupt(this, Ok(args.get_or_undefined(0).clone()), context)
}
/// `Generator.prototype.throw ( exception )`
///
/// The `throw()` method resumes the execution of a generator by throwing an error into it
/// and returns an object with two properties done and value.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.throw
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw
pub(crate) fn throw(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let g be the this value.
// 2. Let C be ThrowCompletion(exception).
// 3. Return ? GeneratorResumeAbrupt(g, C, empty).
Self::generator_resume_abrupt(this, Err(args.get_or_undefined(0).clone()), context)
}
/// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-generatorresume
pub(crate) fn generator_resume(
generator_obj: &JsObject,
value: &JsValue,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let state be ? GeneratorValidate(generator, generatorBrand).
let mut generator_obj_mut = generator_obj.borrow_mut();
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| {
context.construct_type_error("generator resumed on non generator object")
})?;
let state = generator.state;
if state == GeneratorState::Executing {
return Err(context.construct_type_error("Generator should not be executing"));
}
// 2. If state is completed, return CreateIterResultObject(undefined, true).
if state == GeneratorState::Completed {
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
}
// 3. Assert: state is either suspendedStart or suspendedYield.
assert!(matches!(
state,
GeneratorState::SuspendedStart | GeneratorState::SuspendedYield
));
// 4. Let genContext be generator.[[GeneratorContext]].
// 5. Let methodContext be the running execution context.
// 6. Suspend methodContext.
// 7. Set generator.[[GeneratorState]] to executing.
generator.state = GeneratorState::Executing;
let first_execution = matches!(state, GeneratorState::SuspendedStart);
let generator_context_cell = generator
.context
.take()
.expect("generator context cannot be empty here");
let mut generator_context = generator_context_cell.borrow_mut();
drop(generator_obj_mut);
std::mem::swap(
&mut context.realm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
context.vm.push_frame(generator_context.call_frame.clone());
if !first_execution {
context.vm.push(value);
}
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal;
let result = context.run();
generator_context.call_frame = context
.vm
.pop_frame()
.expect("generator call frame must exist");
std::mem::swap(
&mut context.realm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
let mut generator_obj_mut = generator_obj.borrow_mut();
let generator = generator_obj_mut
.as_generator_mut()
.expect("already checked this object type");
match result {
Ok((value, ReturnType::Yield)) => {
generator.state = GeneratorState::SuspendedYield;
drop(generator_context);
generator.context = Some(generator_context_cell);
Ok(create_iter_result_object(value, false, context))
}
Ok((value, _)) => {
generator.state = GeneratorState::Completed;
Ok(create_iter_result_object(value, true, context))
}
Err(value) => {
generator.state = GeneratorState::Completed;
Err(value)
}
}
// 8. Push genContext onto the execution context stack; genContext is now the running execution context.
// 9. Resume the suspended evaluation of genContext using NormalCompletion(value) as the result of the operation that suspended it. Let result be the value returned by the resumed computation.
// 10. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context.
// 11. Return Completion(result).
}
/// `27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-generatorresumeabrupt
pub(crate) fn generator_resume_abrupt(
this: &JsValue,
abrupt_completion: JsResult<JsValue>,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let state be ? GeneratorValidate(generator, generatorBrand).
let generator_obj = this.as_object().ok_or_else(|| {
context.construct_type_error("generator resumed on non generator object")
})?;
let mut generator_obj_mut = generator_obj.borrow_mut();
let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| {
context.construct_type_error("generator resumed on non generator object")
})?;
let mut state = generator.state;
if state == GeneratorState::Executing {
return Err(context.construct_type_error("Generator should not be executing"));
}
// 2. If state is suspendedStart, then
if state == GeneratorState::SuspendedStart {
// a. Set generator.[[GeneratorState]] to completed.
generator.state = GeneratorState::Completed;
// b. Once a generator enters the completed state it never leaves it and its associated execution context is never resumed. Any execution state associated with generator can be discarded at this point.
generator.context = None;
// c. Set state to completed.
state = GeneratorState::Completed;
}
// 3. If state is completed, then
if state == GeneratorState::Completed {
// a. If abruptCompletion.[[Type]] is return, then
if let Ok(value) = abrupt_completion {
// i. Return CreateIterResultObject(abruptCompletion.[[Value]], true).
return Ok(create_iter_result_object(value, true, context));
}
// b. Return Completion(abruptCompletion).
return abrupt_completion;
}
// 4. Assert: state is suspendedYield.
// 5. Let genContext be generator.[[GeneratorContext]].
// 6. Let methodContext be the running execution context.
// 7. Suspend methodContext.
// 8. Set generator.[[GeneratorState]] to executing.
// 9. Push genContext onto the execution context stack; genContext is now the running execution context.
// 10. Resume the suspended evaluation of genContext using abruptCompletion as the result of the operation that suspended it. Let result be the completion record returned by the resumed computation.
// 11. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context.
// 12. Return Completion(result).
let generator_context_cell = generator
.context
.take()
.expect("generator context cannot be empty here");
let mut generator_context = generator_context_cell.borrow_mut();
generator.state = GeneratorState::Executing;
drop(generator_obj_mut);
std::mem::swap(
&mut context.realm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
context.vm.push_frame(generator_context.call_frame.clone());
let result = match abrupt_completion {
Ok(value) => {
context.vm.push(value);
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Return;
context.run()
}
Err(value) => {
context.vm.push(value);
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw;
context.run()
}
};
generator_context.call_frame = context
.vm
.pop_frame()
.expect("generator call frame must exist");
std::mem::swap(
&mut context.realm.environments,
&mut generator_context.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context.stack);
let mut generator_obj_mut = generator_obj.borrow_mut();
let generator = generator_obj_mut
.as_generator_mut()
.expect("already checked this object type");
match result {
Ok((value, ReturnType::Yield)) => {
generator.state = GeneratorState::SuspendedYield;
drop(generator_context);
generator.context = Some(generator_context_cell);
Ok(create_iter_result_object(value, false, context))
}
Ok((value, _)) => {
generator.state = GeneratorState::Completed;
Ok(create_iter_result_object(value, true, context))
}
Err(value) => {
generator.state = GeneratorState::Completed;
Err(value)
}
}
}
}

View File

@@ -0,0 +1,128 @@
//! This module implements the global `GeneratorFunction` object.
//!
//! The `GeneratorFunction` constructor creates a new generator function object.
//! In JavaScript, every generator function is actually a `GeneratorFunction` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-generatorfunction-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/GeneratorFunction
use crate::{
builtins::{
function::{BuiltInFunctionObject, ConstructorKind, Function},
BuiltIn,
},
object::ObjectData,
property::PropertyDescriptor,
symbol::WellKnownSymbols,
value::JsValue,
Context, JsResult,
};
use boa_profiler::Profiler;
/// The internal representation on a `Generator` object.
#[derive(Debug, Clone, Copy)]
pub struct GeneratorFunction;
impl BuiltIn for GeneratorFunction {
const NAME: &'static str = "GeneratorFunction";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let prototype = &context
.intrinsics()
.constructors()
.generator_function()
.prototype;
let constructor = &context
.intrinsics()
.constructors()
.generator_function()
.constructor;
constructor.set_prototype(Some(
context.intrinsics().constructors().function().constructor(),
));
let property = PropertyDescriptor::builder()
.value(1)
.writable(false)
.enumerable(false)
.configurable(true);
constructor.borrow_mut().insert("length", property);
let property = PropertyDescriptor::builder()
.value("GeneratorFunction")
.writable(false)
.enumerable(false)
.configurable(true);
constructor.borrow_mut().insert("name", property);
let property = PropertyDescriptor::builder()
.value(
context
.intrinsics()
.constructors()
.generator_function()
.prototype(),
)
.writable(false)
.enumerable(false)
.configurable(false);
constructor.borrow_mut().insert("prototype", property);
constructor.borrow_mut().data = ObjectData::function(Function::Native {
function: Self::constructor,
constructor: Some(ConstructorKind::Base),
});
prototype.set_prototype(Some(
context.intrinsics().constructors().function().prototype(),
));
let property = PropertyDescriptor::builder()
.value(
context
.intrinsics()
.constructors()
.generator_function()
.constructor(),
)
.writable(false)
.enumerable(false)
.configurable(true);
prototype.borrow_mut().insert("constructor", property);
let property = PropertyDescriptor::builder()
.value(context.intrinsics().constructors().generator().prototype())
.writable(false)
.enumerable(false)
.configurable(true);
prototype.borrow_mut().insert("prototype", property);
let property = PropertyDescriptor::builder()
.value("GeneratorFunction")
.writable(false)
.enumerable(false)
.configurable(true);
prototype
.borrow_mut()
.insert(WellKnownSymbols::to_string_tag(), property);
None
}
}
impl GeneratorFunction {
/// `GeneratorFunction ( p1, p2, … , pn, body )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-generatorfunction
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
BuiltInFunctionObject::create_dynamic_function(new_target, args, false, true, context)
.map(Into::into)
}
}

View File

@@ -0,0 +1,30 @@
//! This module implements the global `globalThis` property.
//!
//! The global globalThis property contains the global this value,
//! which is akin to the global object.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-globalthis
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis
use crate::{builtins::BuiltIn, Context, JsValue};
use boa_profiler::Profiler;
#[cfg(test)]
mod tests;
/// The JavaScript `globalThis`.
pub(crate) struct GlobalThis;
impl BuiltIn for GlobalThis {
const NAME: &'static str = "globalThis";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
Some(context.global_object().clone().into())
}
}

View File

@@ -0,0 +1,10 @@
use crate::exec;
#[test]
fn global_this_exists_on_global_object_and_evaluates_to_an_object() {
let scenario = r#"
typeof globalThis;
"#;
assert_eq!(&exec(scenario), "\"object\"");
}

View File

@@ -0,0 +1,34 @@
//! This module implements the global `Infinity` property.
//!
//! The global property `Infinity` is a numeric value representing infinity.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-value-properties-of-the-global-object-infinity
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity
#[cfg(test)]
mod tests;
use crate::{builtins::BuiltIn, property::Attribute, Context, JsValue};
use boa_profiler::Profiler;
/// JavaScript global `Infinity` property.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Infinity;
impl BuiltIn for Infinity {
const NAME: &'static str = "Infinity";
const ATTRIBUTE: Attribute = Attribute::READONLY
.union(Attribute::NON_ENUMERABLE)
.union(Attribute::PERMANENT);
fn init(_: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
Some(f64::INFINITY.into())
}
}

View File

@@ -0,0 +1,19 @@
use crate::exec;
#[test]
fn infinity_exists_on_global_object_and_evaluates_to_infinity_value() {
let scenario = r#"
Infinity;
"#;
assert_eq!(&exec(scenario), "Infinity");
}
#[test]
fn infinity_exists_and_equals_to_number_positive_infinity_value() {
let scenario = r#"
Number.POSITIVE_INFINITY === Infinity;
"#;
assert_eq!(&exec(scenario), "true");
}

View File

@@ -0,0 +1,236 @@
//! This module implements the global `Intl.DateTimeFormat` object.
//!
//! `Intl.DateTimeFormat` is a built-in object that has properties and methods for date and time i18n.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma402/#datetimeformat-objects
use crate::{
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsFunction, JsObject,
ObjectData,
},
Context, JsResult, JsString, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
/// JavaScript `Intl.DateTimeFormat` object.
#[derive(Debug, Clone, Trace, Finalize)]
pub struct DateTimeFormat {
initialized_date_time_format: bool,
locale: JsString,
calendar: JsString,
numbering_system: JsString,
time_zone: JsString,
weekday: JsString,
era: JsString,
year: JsString,
month: JsString,
day: JsString,
day_period: JsString,
hour: JsString,
minute: JsString,
second: JsString,
fractional_second_digits: JsString,
time_zone_name: JsString,
hour_cycle: JsString,
pattern: JsString,
bound_format: JsString,
}
impl DateTimeFormat {
const NAME: &'static str = "DateTimeFormat";
pub(super) fn init(context: &mut Context) -> JsFunction {
let _timer = Profiler::global().start_event(Self::NAME, "init");
ConstructorBuilder::new(context, Self::constructor)
.name(Self::NAME)
.length(0)
.build()
}
}
impl DateTimeFormat {
/// The `Intl.DateTimeFormat` constructor is the `%DateTimeFormat%` intrinsic object and a standard built-in property of the `Intl` object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#datetimeformat-objects
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
pub(crate) fn constructor(
new_target: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
let prototype = get_prototype_from_constructor(
new_target,
StandardConstructors::date_time_format,
context,
)?;
// 2. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%DateTimeFormat.prototype%",
// « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[Weekday]],
// [[Era]], [[Year]], [[Month]], [[Day]], [[DayPeriod]], [[Hour]], [[Minute]], [[Second]],
// [[FractionalSecondDigits]], [[TimeZoneName]], [[HourCycle]], [[Pattern]], [[BoundFormat]] »).
let date_time_format = JsObject::from_proto_and_data(
prototype,
ObjectData::date_time_format(Box::new(Self {
initialized_date_time_format: true,
locale: JsString::from("en-US"),
calendar: JsString::from("gregory"),
numbering_system: JsString::from("arab"),
time_zone: JsString::from("UTC"),
weekday: JsString::from("narrow"),
era: JsString::from("narrow"),
year: JsString::from("numeric"),
month: JsString::from("narrow"),
day: JsString::from("numeric"),
day_period: JsString::from("narrow"),
hour: JsString::from("numeric"),
minute: JsString::from("numeric"),
second: JsString::from("numeric"),
fractional_second_digits: JsString::from(""),
time_zone_name: JsString::from(""),
hour_cycle: JsString::from("h24"),
pattern: JsString::from("{hour}:{minute}"),
bound_format: JsString::from("undefined"),
})),
);
// TODO 3. Perform ? InitializeDateTimeFormat(dateTimeFormat, locales, options).
// TODO 4. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
// TODO a. Let this be the this value.
// TODO b. Return ? ChainDateTimeFormat(dateTimeFormat, NewTarget, this).
// 5. Return dateTimeFormat.
Ok(date_time_format.into())
}
}
/// Represents the `required` and `defaults` arguments in the abstract operation
/// `toDateTimeOptions`.
///
/// Since `required` and `defaults` differ only in the `any` and `all` variants,
/// we combine both in a single variant `AnyAll`.
#[allow(unused)]
#[derive(Debug, PartialEq)]
pub(crate) enum DateTimeReqs {
Date,
Time,
AnyAll,
}
/// The abstract operation `toDateTimeOptions` is called with arguments `options`, `required` and
/// `defaults`.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-todatetimeoptions
#[allow(unused)]
pub(crate) fn to_date_time_options(
options: &JsValue,
required: &DateTimeReqs,
defaults: &DateTimeReqs,
context: &mut Context,
) -> JsResult<JsObject> {
// 1. If options is undefined, let options be null;
// otherwise let options be ? ToObject(options).
// 2. Let options be ! OrdinaryObjectCreate(options).
let options = if options.is_undefined() {
None
} else {
Some(options.to_object(context)?)
};
let options = JsObject::from_proto_and_data(options, ObjectData::ordinary());
// 3. Let needDefaults be true.
let mut need_defaults = true;
// 4. If required is "date" or "any", then
if [DateTimeReqs::Date, DateTimeReqs::AnyAll].contains(required) {
// a. For each property name prop of « "weekday", "year", "month", "day" », do
for property in ["weekday", "year", "month", "day"] {
// i. Let value be ? Get(options, prop).
let value = options.get(property, context)?;
// ii. If value is not undefined, let needDefaults be false.
if !value.is_undefined() {
need_defaults = false;
}
}
}
// 5. If required is "time" or "any", then
if [DateTimeReqs::Time, DateTimeReqs::AnyAll].contains(required) {
// a. For each property name prop of « "dayPeriod", "hour", "minute", "second",
// "fractionalSecondDigits" », do
for property in [
"dayPeriod",
"hour",
"minute",
"second",
"fractionalSecondDigits",
] {
// i. Let value be ? Get(options, prop).
let value = options.get(property, context)?;
// ii. If value is not undefined, let needDefaults be false.
if !value.is_undefined() {
need_defaults = false;
}
}
}
// 6. Let dateStyle be ? Get(options, "dateStyle").
let date_style = options.get("dateStyle", context)?;
// 7. Let timeStyle be ? Get(options, "timeStyle").
let time_style = options.get("timeStyle", context)?;
// 8. If dateStyle is not undefined or timeStyle is not undefined, let needDefaults be false.
if !date_style.is_undefined() || !time_style.is_undefined() {
need_defaults = false;
}
// 9. If required is "date" and timeStyle is not undefined, then
if required == &DateTimeReqs::Date && !time_style.is_undefined() {
// a. Throw a TypeError exception.
return context.throw_type_error("'date' is required, but timeStyle was defined");
}
// 10. If required is "time" and dateStyle is not undefined, then
if required == &DateTimeReqs::Time && !date_style.is_undefined() {
// a. Throw a TypeError exception.
return context.throw_type_error("'time' is required, but dateStyle was defined");
}
// 11. If needDefaults is true and defaults is either "date" or "all", then
if need_defaults && [DateTimeReqs::Date, DateTimeReqs::AnyAll].contains(defaults) {
// a. For each property name prop of « "year", "month", "day" », do
for property in ["year", "month", "day"] {
// i. Perform ? CreateDataPropertyOrThrow(options, prop, "numeric").
options.create_data_property_or_throw(property, "numeric", context)?;
}
}
// 12. If needDefaults is true and defaults is either "time" or "all", then
if need_defaults && [DateTimeReqs::Time, DateTimeReqs::AnyAll].contains(defaults) {
// a. For each property name prop of « "hour", "minute", "second" », do
for property in ["hour", "minute", "second"] {
// i. Perform ? CreateDataPropertyOrThrow(options, prop, "numeric").
options.create_data_property_or_throw(property, "numeric", context)?;
}
}
// 13. Return options.
Ok(options)
}

View File

@@ -0,0 +1,836 @@
//! This module implements the global `Intl` object.
//!
//! `Intl` is a built-in object that has properties and methods for i18n. It's not a function object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma402/#intl-object
use crate::{
builtins::intl::date_time_format::DateTimeFormat,
builtins::{Array, BuiltIn, JsArgs},
object::{JsObject, ObjectInitializer},
property::Attribute,
symbol::WellKnownSymbols,
Context, JsResult, JsString, JsValue,
};
pub mod date_time_format;
#[cfg(test)]
mod tests;
use boa_profiler::Profiler;
use icu_locale_canonicalizer::LocaleCanonicalizer;
use icu_locid::{locale, Locale};
use indexmap::IndexSet;
use rustc_hash::FxHashMap;
use tap::{Conv, Pipe, TapOptional};
/// JavaScript `Intl` object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Intl;
impl BuiltIn for Intl {
const NAME: &'static str = "Intl";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let string_tag = WellKnownSymbols::to_string_tag();
let date_time_format = DateTimeFormat::init(context);
ObjectInitializer::new(context)
.function(Self::get_canonical_locales, "getCanonicalLocales", 1)
.property(
string_tag,
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"DateTimeFormat",
date_time_format,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Intl {
/// `Intl.getCanonicalLocales ( locales )`
///
/// Returns an array containing the canonical locale names.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN docs][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.getcanonicallocales
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/getCanonicalLocales
pub(crate) fn get_canonical_locales(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let ll be ? CanonicalizeLocaleList(locales).
let ll = canonicalize_locale_list(args, context)?;
// 2. Return CreateArrayFromList(ll).
Ok(JsValue::Object(Array::create_array_from_list(
ll.into_iter().map(|loc| loc.to_string().into()),
context,
)))
}
}
/// `MatcherRecord` type aggregates unicode `locale` string and unicode locale `extension`.
///
/// This is a return value for `lookup_matcher` and `best_fit_matcher` subroutines.
#[derive(Debug)]
struct MatcherRecord {
locale: JsString,
extension: JsString,
}
/// Abstract operation `DefaultLocale ( )`
///
/// Returns a String value representing the structurally valid and canonicalized
/// Unicode BCP 47 locale identifier for the host environment's current locale.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-defaultlocale
fn default_locale(canonicalizer: &LocaleCanonicalizer) -> Locale {
sys_locale::get_locale()
.and_then(|loc| loc.parse::<Locale>().ok())
.tap_some_mut(|loc| canonicalize_unicode_locale_id(loc, canonicalizer))
.unwrap_or(locale!("en-US"))
}
/// Abstract operation `BestAvailableLocale ( availableLocales, locale )`
///
/// Compares the provided argument `locale`, which must be a String value with a
/// structurally valid and canonicalized Unicode BCP 47 locale identifier, against
/// the locales in `availableLocales` and returns either the longest non-empty prefix
/// of `locale` that is an element of `availableLocales`, or undefined if there is no
/// such element.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-bestavailablelocale
fn best_available_locale(available_locales: &[JsString], locale: &JsString) -> Option<JsString> {
// 1. Let candidate be locale.
let mut candidate = locale.clone();
// 2. Repeat
loop {
// a. If availableLocales contains an element equal to candidate, return candidate.
if available_locales.contains(&candidate) {
return Some(candidate);
}
// b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined.
let pos = candidate.rfind('-');
match pos {
Some(ind) => {
// c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2.
let tmp_candidate = candidate[..ind].to_string();
let prev_dash = tmp_candidate.rfind('-').unwrap_or(ind);
let trim_ind = if ind >= 2 && prev_dash == ind - 2 {
ind - 2
} else {
ind
};
// d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
candidate = JsString::new(&candidate[..trim_ind]);
}
None => return None,
}
}
}
/// Abstract operation `LookupMatcher ( availableLocales, requestedLocales )`
///
/// Compares `requestedLocales`, which must be a `List` as returned by `CanonicalizeLocaleList`,
/// against the locales in `availableLocales` and determines the best available language to
/// meet the request.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-lookupmatcher
fn lookup_matcher(
available_locales: &[JsString],
requested_locales: &[JsString],
canonicalizer: &LocaleCanonicalizer,
) -> MatcherRecord {
// 1. Let result be a new Record.
// 2. For each element locale of requestedLocales, do
for locale_str in requested_locales {
// a. Let noExtensionsLocale be the String value that is locale with any Unicode locale
// extension sequences removed.
let parsed_locale =
Locale::from_bytes(locale_str.as_bytes()).expect("Locale parsing failed");
let no_extensions_locale = JsString::new(parsed_locale.id.to_string());
// b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
let available_locale = best_available_locale(available_locales, &no_extensions_locale);
// c. If availableLocale is not undefined, then
if let Some(available_locale) = available_locale {
// i. Set result.[[locale]] to availableLocale.
// Assignment deferred. See return statement below.
// ii. If locale and noExtensionsLocale are not the same String value, then
let maybe_ext = if locale_str.eq(&no_extensions_locale) {
JsString::empty()
} else {
// 1. Let extension be the String value consisting of the substring of the Unicode
// locale extension sequence within locale.
// 2. Set result.[[extension]] to extension.
JsString::new(parsed_locale.extensions.to_string())
};
// iii. Return result.
return MatcherRecord {
locale: available_locale,
extension: maybe_ext,
};
}
}
// 3. Let defLocale be ! DefaultLocale().
// 4. Set result.[[locale]] to defLocale.
// 5. Return result.
MatcherRecord {
locale: default_locale(canonicalizer).to_string().into(),
extension: JsString::empty(),
}
}
/// Abstract operation `BestFitMatcher ( availableLocales, requestedLocales )`
///
/// Compares `requestedLocales`, which must be a `List` as returned by `CanonicalizeLocaleList`,
/// against the locales in `availableLocales` and determines the best available language to
/// meet the request. The algorithm is implementation dependent, but should produce results
/// that a typical user of the requested locales would perceive as at least as good as those
/// produced by the `LookupMatcher` abstract operation.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-bestfitmatcher
fn best_fit_matcher(
available_locales: &[JsString],
requested_locales: &[JsString],
canonicalizer: &LocaleCanonicalizer,
) -> MatcherRecord {
lookup_matcher(available_locales, requested_locales, canonicalizer)
}
/// `Keyword` structure is a pair of keyword key and keyword value.
#[derive(Debug)]
struct Keyword {
key: JsString,
value: JsString,
}
/// `UniExtRecord` structure represents unicode extension records.
///
/// It contains the list of unicode `extension` attributes and the list of `keywords`.
///
/// For example:
///
/// - `-u-nu-thai` has no attributes and the list of keywords contains `(nu:thai)` pair.
#[allow(dead_code)]
#[derive(Debug)]
struct UniExtRecord {
attributes: Vec<JsString>, // never read at this point
keywords: Vec<Keyword>,
}
/// Abstract operation `UnicodeExtensionComponents ( extension )`
///
/// Returns the attributes and keywords from `extension`, which must be a String
/// value whose contents are a `Unicode locale extension` sequence.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-unicode-extension-components
fn unicode_extension_components(extension: &JsString) -> UniExtRecord {
// 1. Let attributes be a new empty List.
let mut attributes = Vec::<JsString>::new();
// 2. Let keywords be a new empty List.
let mut keywords = Vec::<Keyword>::new();
// 3. Let keyword be undefined.
let mut keyword: Option<Keyword> = None;
// 4. Let size be the length of extension.
let size = extension.len();
// 5. Let k be 3.
let mut k = 3;
// 6. Repeat, while k < size,
while k < size {
// a. Let e be ! StringIndexOf(extension, "-", k).
let e = extension.index_of(&JsString::new("-"), k);
// b. If e = -1, let len be size - k; else let len be e - k.
let len = match e {
Some(pos) => pos - k,
None => size - k,
};
// c. Let subtag be the String value equal to the substring of extension consisting of the
// code units at indices k (inclusive) through k + len (exclusive).
let subtag = JsString::new(&extension[k..k + len]);
// d. If keyword is undefined and len ≠ 2, then
if keyword.is_none() && len != 2 {
// i. If subtag is not an element of attributes, then
if !attributes.contains(&subtag) {
// 1. Append subtag to attributes.
attributes.push(subtag);
}
// e. Else if len = 2, then
} else if len == 2 {
// i. If keyword is not undefined and keywords does not contain an element
// whose [[Key]] is the same as keyword.[[Key]], then
// 1. Append keyword to keywords.
if let Some(keyword_val) = keyword {
let has_key = keywords.iter().any(|elem| elem.key == keyword_val.key);
if !has_key {
keywords.push(keyword_val);
}
};
// ii. Set keyword to the Record { [[Key]]: subtag, [[Value]]: "" }.
keyword = Some(Keyword {
key: subtag,
value: JsString::empty(),
});
// f. Else,
} else {
// i. If keyword.[[Value]] is the empty String, then
// 1. Set keyword.[[Value]] to subtag.
// ii. Else,
// 1. Set keyword.[[Value]] to the string-concatenation of keyword.[[Value]], "-", and subtag.
if let Some(keyword_val) = keyword {
let new_keyword_val = if keyword_val.value.is_empty() {
subtag
} else {
JsString::new(format!("{}-{subtag}", keyword_val.value))
};
keyword = Some(Keyword {
key: keyword_val.key,
value: new_keyword_val,
});
};
}
// g. Let k be k + len + 1.
k = k + len + 1;
}
// 7. If keyword is not undefined and keywords does not contain an element whose [[Key]] is
// the same as keyword.[[Key]], then
// a. Append keyword to keywords.
if let Some(keyword_val) = keyword {
let has_key = keywords.iter().any(|elem| elem.key == keyword_val.key);
if !has_key {
keywords.push(keyword_val);
}
};
// 8. Return the Record { [[Attributes]]: attributes, [[Keywords]]: keywords }.
UniExtRecord {
attributes,
keywords,
}
}
/// Abstract operation `InsertUnicodeExtensionAndCanonicalize ( locale, extension )`
///
/// Inserts `extension`, which must be a Unicode locale extension sequence, into
/// `locale`, which must be a String value with a structurally valid and canonicalized
/// Unicode BCP 47 locale identifier.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-insert-unicode-extension-and-canonicalize
fn insert_unicode_extension_and_canonicalize(
locale: &str,
extension: &str,
canonicalizer: &LocaleCanonicalizer,
) -> JsString {
// TODO 1. Assert: locale does not contain a substring that is a Unicode locale extension sequence.
// TODO 2. Assert: extension is a Unicode locale extension sequence.
// TODO 3. Assert: tag matches the unicode_locale_id production.
// 4. Let privateIndex be ! StringIndexOf(locale, "-x-", 0).
let private_index = locale.find("-x-");
let new_locale = match private_index {
// 5. If privateIndex = -1, then
None => {
// a. Let locale be the string-concatenation of locale and extension.
locale.to_owned() + extension
}
// 6. Else,
Some(idx) => {
// a. Let preExtension be the substring of locale from position 0, inclusive,
// to position privateIndex, exclusive.
let pre_extension = &locale[0..idx];
// b. Let postExtension be the substring of locale from position privateIndex to
// the end of the string.
let post_extension = &locale[idx..];
// c. Let locale be the string-concatenation of preExtension, extension,
// and postExtension.
pre_extension.to_owned() + extension + post_extension
}
};
// 7. Assert: ! IsStructurallyValidLanguageTag(locale) is true.
let mut new_locale = new_locale
.parse()
.expect("Assert: ! IsStructurallyValidLanguageTag(locale) is true.");
// 8. Return ! CanonicalizeUnicodeLocaleId(locale).
canonicalize_unicode_locale_id(&mut new_locale, canonicalizer);
new_locale.to_string().into()
}
/// Abstract operation `CanonicalizeLocaleList ( locales )`
///
/// Converts an array of [`JsValue`]s containing structurally valid
/// [Unicode BCP 47 locale identifiers][bcp-47] into their [canonical form][canon].
///
/// For efficiency, this returns a [`Vec`] of [`Locale`]s instead of a [`Vec`] of
/// [`String`]s, since [`Locale`] allows us to modify individual parts of the locale
/// without scanning the whole string again.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-canonicalizelocalelist
/// [bcp-47]: https://unicode.org/reports/tr35/#Unicode_locale_identifier
/// [canon]: https://unicode.org/reports/tr35/#LocaleId_Canonicalization
fn canonicalize_locale_list(args: &[JsValue], context: &mut Context) -> JsResult<Vec<Locale>> {
// 1. If locales is undefined, then
let locales = args.get_or_undefined(0);
if locales.is_undefined() {
// a. Return a new empty List.
return Ok(Vec::new());
}
// 2. Let seen be a new empty List.
let mut seen = IndexSet::new();
// 3. If Type(locales) is String or Type(locales) is Object and locales has an [[InitializedLocale]] internal slot, then
// TODO: check if Type(locales) is object and handle the internal slots
let o = if locales.is_string() {
// a. Let O be CreateArrayFromList(« locales »).
Array::create_array_from_list([locales.clone()], context)
} else {
// 4. Else,
// a. Let O be ? ToObject(locales).
locales.to_object(context)?
};
// 5. Let len be ? ToLength(? Get(O, "length")).
let len = o.length_of_array_like(context)?;
// 6 Let k be 0.
// 7. Repeat, while k < len,
for k in 0..len {
// a. Let Pk be ToString(k).
// b. Let kPresent be ? HasProperty(O, Pk).
let k_present = o.has_property(k, context)?;
// c. If kPresent is true, then
if k_present {
// i. Let kValue be ? Get(O, Pk).
let k_value = o.get(k, context)?;
// ii. If Type(kValue) is not String or Object, throw a TypeError exception.
if !(k_value.is_object() || k_value.is_string()) {
return context.throw_type_error("locale should be a String or Object");
}
// iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then
// TODO: handle checks for InitializedLocale internal slot (there should be an if statement here)
// 1. Let tag be kValue.[[Locale]].
// iv. Else,
// 1. Let tag be ? ToString(kValue).
let tag = k_value.to_string(context)?;
// v. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
let mut tag = tag.parse().map_err(|_| {
context.construct_range_error("locale is not a structurally valid language tag")
})?;
// vi. Let canonicalizedTag be CanonicalizeUnicodeLocaleId(tag).
canonicalize_unicode_locale_id(&mut tag, &*context.icu().locale_canonicalizer());
seen.insert(tag);
// vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
}
// d. Increase k by 1.
}
// 8. Return seen.
Ok(seen.into_iter().collect())
}
/// `LocaleDataRecord` is the type of `locale_data` argument in `resolve_locale` subroutine.
///
/// It is an alias for a map where key is a string and value is another map.
///
/// Value of that inner map is a vector of strings representing locale parameters.
type LocaleDataRecord = FxHashMap<JsString, FxHashMap<JsString, Vec<JsString>>>;
/// `DateTimeFormatRecord` type aggregates `locale_matcher` selector and `properties` map.
///
/// It is used as a type of `options` parameter in `resolve_locale` subroutine.
#[derive(Debug)]
struct DateTimeFormatRecord {
pub(crate) locale_matcher: JsString,
pub(crate) properties: FxHashMap<JsString, JsValue>,
}
/// `ResolveLocaleRecord` type consists of unicode `locale` string, `data_locale` string and `properties` map.
///
/// This is a return value for `resolve_locale` subroutine.
#[derive(Debug)]
struct ResolveLocaleRecord {
pub(crate) locale: JsString,
pub(crate) properties: FxHashMap<JsString, JsValue>,
pub(crate) data_locale: JsString,
}
/// Abstract operation `ResolveLocale ( availableLocales, requestedLocales, options, relevantExtensionKeys, localeData )`
///
/// Compares a BCP 47 language priority list `requestedLocales` against the locales
/// in `availableLocales` and determines the best available language to meet the request.
/// `availableLocales`, `requestedLocales`, and `relevantExtensionKeys` must be provided as
/// `List` values, options and `localeData` as Records.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-resolvelocale
#[allow(dead_code)]
fn resolve_locale(
available_locales: &[JsString],
requested_locales: &[JsString],
options: &DateTimeFormatRecord,
relevant_extension_keys: &[JsString],
locale_data: &LocaleDataRecord,
context: &mut Context,
) -> ResolveLocaleRecord {
// 1. Let matcher be options.[[localeMatcher]].
let matcher = &options.locale_matcher;
// 2. If matcher is "lookup", then
// a. Let r be ! LookupMatcher(availableLocales, requestedLocales).
// 3. Else,
// a. Let r be ! BestFitMatcher(availableLocales, requestedLocales).
let r = if matcher.eq(&JsString::new("lookup")) {
lookup_matcher(
available_locales,
requested_locales,
context.icu().locale_canonicalizer(),
)
} else {
best_fit_matcher(
available_locales,
requested_locales,
context.icu().locale_canonicalizer(),
)
};
// 4. Let foundLocale be r.[[locale]].
let mut found_locale = r.locale;
// 5. Let result be a new Record.
let mut result = ResolveLocaleRecord {
locale: JsString::empty(),
properties: FxHashMap::default(),
data_locale: JsString::empty(),
};
// 6. Set result.[[dataLocale]] to foundLocale.
result.data_locale = found_locale.clone();
// 7. If r has an [[extension]] field, then
let keywords = if r.extension.is_empty() {
Vec::<Keyword>::new()
} else {
// a. Let components be ! UnicodeExtensionComponents(r.[[extension]]).
let components = unicode_extension_components(&r.extension);
// b. Let keywords be components.[[Keywords]].
components.keywords
};
// 8. Let supportedExtension be "-u".
let mut supported_extension = JsString::new("-u");
// 9. For each element key of relevantExtensionKeys, do
for key in relevant_extension_keys {
// a. Let foundLocaleData be localeData.[[<foundLocale>]].
// TODO b. Assert: Type(foundLocaleData) is Record.
let found_locale_data = match locale_data.get(&found_locale) {
Some(locale_value) => locale_value.clone(),
None => FxHashMap::default(),
};
// c. Let keyLocaleData be foundLocaleData.[[<key>]].
// TODO d. Assert: Type(keyLocaleData) is List.
let key_locale_data = match found_locale_data.get(key) {
Some(locale_vec) => locale_vec.clone(),
None => Vec::new(),
};
// e. Let value be keyLocaleData[0].
// TODO f. Assert: Type(value) is either String or Null.
let mut value = match key_locale_data.get(0) {
Some(first_elt) => JsValue::String(first_elt.clone()),
None => JsValue::null(),
};
// g. Let supportedExtensionAddition be "".
let mut supported_extension_addition = JsString::empty();
// h. If r has an [[extension]] field, then
if !r.extension.is_empty() {
// i. If keywords contains an element whose [[Key]] is the same as key, then
// 1. Let entry be the element of keywords whose [[Key]] is the same as key.
let maybe_entry = keywords.iter().find(|elem| key.eq(&elem.key));
if let Some(entry) = maybe_entry {
// 2. Let requestedValue be entry.[[Value]].
let requested_value = &entry.value;
// 3. If requestedValue is not the empty String, then
if !requested_value.is_empty() {
// a. If keyLocaleData contains requestedValue, then
if key_locale_data.contains(requested_value) {
// i. Let value be requestedValue.
value = JsValue::String(JsString::new(requested_value));
// ii. Let supportedExtensionAddition be the string-concatenation
// of "-", key, "-", and value.
supported_extension_addition =
JsString::concat_array(&["-", key, "-", requested_value]);
}
// 4. Else if keyLocaleData contains "true", then
} else if key_locale_data.contains(&JsString::new("true")) {
// a. Let value be "true".
value = JsValue::String(JsString::new("true"));
// b. Let supportedExtensionAddition be the string-concatenation of "-" and key.
supported_extension_addition = JsString::concat_array(&["-", key]);
}
}
}
// i. If options has a field [[<key>]], then
if options.properties.contains_key(key) {
// i. Let optionsValue be options.[[<key>]].
// TODO ii. Assert: Type(optionsValue) is either String, Undefined, or Null.
let mut options_value = options
.properties
.get(key)
.unwrap_or(&JsValue::undefined())
.clone();
// iii. If Type(optionsValue) is String, then
if options_value.is_string() {
// TODO 1. Let optionsValue be the string optionsValue after performing the
// algorithm steps to transform Unicode extension values to canonical syntax
// per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale
// Identifiers, treating key as ukey and optionsValue as uvalue productions.
// TODO 2. Let optionsValue be the string optionsValue after performing the
// algorithm steps to replace Unicode extension values with their canonical
// form per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode
// Locale Identifiers, treating key as ukey and optionsValue as uvalue
// productions.
// 3. If optionsValue is the empty String, then
if let Some(options_val_str) = options_value.as_string() {
if options_val_str.is_empty() {
// a. Let optionsValue be "true".
options_value = JsValue::String(JsString::new("true"));
}
}
}
// iv. If keyLocaleData contains optionsValue, then
let options_val_str = options_value
.to_string(context)
.unwrap_or_else(|_| JsString::empty());
if key_locale_data.contains(&options_val_str) {
// 1. If SameValue(optionsValue, value) is false, then
if !options_value.eq(&value) {
// a. Let value be optionsValue.
value = options_value;
// b. Let supportedExtensionAddition be "".
supported_extension_addition = JsString::empty();
}
}
}
// j. Set result.[[<key>]] to value.
result.properties.insert(key.clone(), value);
// k. Append supportedExtensionAddition to supportedExtension.
supported_extension = JsString::concat(supported_extension, &supported_extension_addition);
}
// 10. If the number of elements in supportedExtension is greater than 2, then
if supported_extension.len() > 2 {
// a. Let foundLocale be InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedExtension).
found_locale = insert_unicode_extension_and_canonicalize(
&found_locale,
&supported_extension,
context.icu().locale_canonicalizer(),
);
}
// 11. Set result.[[locale]] to foundLocale.
result.locale = found_locale;
// 12. Return result.
result
}
#[allow(unused)]
pub(crate) enum GetOptionType {
String,
Boolean,
}
/// Abstract operation `GetOption ( options, property, type, values, fallback )`
///
/// Extracts the value of the property named `property` from the provided `options` object,
/// converts it to the required `type`, checks whether it is one of a `List` of allowed
/// `values`, and fills in a `fallback` value if necessary. If `values` is
/// undefined, there is no fixed set of values and any is permitted.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-getoption
#[allow(unused)]
pub(crate) fn get_option(
options: &JsObject,
property: &str,
r#type: &GetOptionType,
values: &[JsString],
fallback: &JsValue,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Assert: Type(options) is Object.
// 2. Let value be ? Get(options, property).
let mut value = options.get(property, context)?;
// 3. If value is undefined, return fallback.
if value.is_undefined() {
return Ok(fallback.clone());
}
// 4. Assert: type is "boolean" or "string".
// 5. If type is "boolean", then
// a. Set value to ! ToBoolean(value).
// 6. If type is "string", then
// a. Set value to ? ToString(value).
// 7. If values is not undefined and values does not contain an element equal to value,
// throw a RangeError exception.
value = match r#type {
GetOptionType::Boolean => JsValue::Boolean(value.to_boolean()),
GetOptionType::String => {
let string_value = value.to_string(context)?;
if !values.is_empty() && !values.contains(&string_value) {
return context.throw_range_error("GetOption: values array does not contain value");
}
JsValue::String(string_value)
}
};
// 8. Return value.
Ok(value)
}
/// Abstract operation `GetNumberOption ( options, property, minimum, maximum, fallback )`
///
/// Extracts the value of the property named `property` from the provided `options`
/// object, converts it to a `Number value`, checks whether it is in the allowed range,
/// and fills in a `fallback` value if necessary.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-getnumberoption
#[allow(unused)]
pub(crate) fn get_number_option(
options: &JsObject,
property: &str,
minimum: f64,
maximum: f64,
fallback: Option<f64>,
context: &mut Context,
) -> JsResult<Option<f64>> {
// 1. Assert: Type(options) is Object.
// 2. Let value be ? Get(options, property).
let value = options.get(property, context)?;
// 3. Return ? DefaultNumberOption(value, minimum, maximum, fallback).
default_number_option(&value, minimum, maximum, fallback, context)
}
/// Abstract operation `DefaultNumberOption ( value, minimum, maximum, fallback )`
///
/// Converts `value` to a `Number value`, checks whether it is in the allowed range,
/// and fills in a `fallback` value if necessary.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-defaultnumberoption
#[allow(unused)]
pub(crate) fn default_number_option(
value: &JsValue,
minimum: f64,
maximum: f64,
fallback: Option<f64>,
context: &mut Context,
) -> JsResult<Option<f64>> {
// 1. If value is undefined, return fallback.
if value.is_undefined() {
return Ok(fallback);
}
// 2. Set value to ? ToNumber(value).
let value = value.to_number(context)?;
// 3. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception.
if value.is_nan() || value < minimum || value > maximum {
return context.throw_range_error("DefaultNumberOption: value is out of range.");
}
// 4. Return floor(value).
Ok(Some(value.floor()))
}
/// Abstract operation `CanonicalizeUnicodeLocaleId ( locale )`.
///
/// This function differs slightly from the specification by modifying in-place
/// the provided [`Locale`] instead of creating a new canonicalized copy.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid
fn canonicalize_unicode_locale_id(locale: &mut Locale, canonicalizer: &LocaleCanonicalizer) {
canonicalizer.canonicalize(locale);
}

View File

@@ -0,0 +1,545 @@
use crate::{
builtins::intl::date_time_format::{to_date_time_options, DateTimeReqs},
builtins::intl::{
best_available_locale, best_fit_matcher, default_locale, default_number_option,
get_number_option, get_option, insert_unicode_extension_and_canonicalize, lookup_matcher,
resolve_locale, unicode_extension_components, DateTimeFormatRecord, GetOptionType,
},
object::JsObject,
Context, JsString, JsValue,
};
use icu_locale_canonicalizer::LocaleCanonicalizer;
use rustc_hash::FxHashMap;
#[test]
fn best_avail_loc() {
let no_extensions_locale = JsString::new("en-US");
let available_locales = Vec::<JsString>::new();
assert_eq!(
best_available_locale(&available_locales, &no_extensions_locale,),
None
);
let no_extensions_locale = JsString::new("de-DE");
let available_locales = vec![no_extensions_locale.clone()];
assert_eq!(
best_available_locale(&available_locales, &no_extensions_locale,),
Some(no_extensions_locale)
);
let locale_part = "fr".to_string();
let no_extensions_locale = JsString::new(locale_part.clone() + "-CA");
let available_locales = vec![JsString::new(locale_part.clone())];
assert_eq!(
best_available_locale(&available_locales, &no_extensions_locale,),
Some(JsString::new(locale_part))
);
let ja_kana_t = JsString::new("ja-Kana-JP-t");
let ja_kana = JsString::new("ja-Kana-JP");
let no_extensions_locale = JsString::new("ja-Kana-JP-t-it-latn-it");
let available_locales = vec![ja_kana_t, ja_kana.clone()];
assert_eq!(
best_available_locale(&available_locales, &no_extensions_locale,),
Some(ja_kana)
);
}
#[test]
fn lookup_match() {
let provider = icu_testdata::get_provider();
let canonicalizer =
LocaleCanonicalizer::new(&provider).expect("Could not create canonicalizer");
// available: [], requested: []
let available_locales = Vec::<JsString>::new();
let requested_locales = Vec::<JsString>::new();
let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer);
assert_eq!(
matcher.locale,
default_locale(&canonicalizer).to_string().as_str()
);
assert_eq!(matcher.extension, "");
// available: [de-DE], requested: []
let available_locales = vec![JsString::new("de-DE")];
let requested_locales = Vec::<JsString>::new();
let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer);
assert_eq!(
matcher.locale,
default_locale(&canonicalizer).to_string().as_str()
);
assert_eq!(matcher.extension, "");
// available: [fr-FR], requested: [fr-FR-u-hc-h12]
let available_locales = vec![JsString::new("fr-FR")];
let requested_locales = vec![JsString::new("fr-FR-u-hc-h12")];
let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer);
assert_eq!(matcher.locale, "fr-FR");
assert_eq!(matcher.extension, "u-hc-h12");
// available: [es-ES], requested: [es-ES]
let available_locales = vec![JsString::new("es-ES")];
let requested_locales = vec![JsString::new("es-ES")];
let matcher = best_fit_matcher(&available_locales, &requested_locales, &canonicalizer);
assert_eq!(matcher.locale, "es-ES");
assert_eq!(matcher.extension, "");
}
#[test]
fn insert_unicode_ext() {
let provider = icu_testdata::get_provider();
let canonicalizer =
LocaleCanonicalizer::new(&provider).expect("Could not create canonicalizer");
let locale = JsString::new("hu-HU");
let ext = JsString::empty();
assert_eq!(
insert_unicode_extension_and_canonicalize(&locale, &ext, &canonicalizer),
locale
);
let locale = JsString::new("hu-HU");
let ext = JsString::new("-u-hc-h12");
assert_eq!(
insert_unicode_extension_and_canonicalize(&locale, &ext, &canonicalizer),
JsString::new("hu-HU-u-hc-h12")
);
let locale = JsString::new("hu-HU-x-PRIVATE");
let ext = JsString::new("-u-hc-h12");
assert_eq!(
insert_unicode_extension_and_canonicalize(&locale, &ext, &canonicalizer),
JsString::new("hu-HU-u-hc-h12-x-private")
);
}
#[test]
fn uni_ext_comp() {
let ext = JsString::new("-u-ca-japanese-hc-h12");
let components = unicode_extension_components(&ext);
assert!(components.attributes.is_empty());
assert_eq!(components.keywords.len(), 2);
assert_eq!(components.keywords[0].key, "ca");
assert_eq!(components.keywords[0].value, "japanese");
assert_eq!(components.keywords[1].key, "hc");
assert_eq!(components.keywords[1].value, "h12");
let ext = JsString::new("-u-alias-co-phonebk-ka-shifted");
let components = unicode_extension_components(&ext);
assert_eq!(components.attributes, vec![JsString::new("alias")]);
assert_eq!(components.keywords.len(), 2);
assert_eq!(components.keywords[0].key, "co");
assert_eq!(components.keywords[0].value, "phonebk");
assert_eq!(components.keywords[1].key, "ka");
assert_eq!(components.keywords[1].value, "shifted");
let ext = JsString::new("-u-ca-buddhist-kk-nu-thai");
let components = unicode_extension_components(&ext);
assert!(components.attributes.is_empty());
assert_eq!(components.keywords.len(), 3);
assert_eq!(components.keywords[0].key, "ca");
assert_eq!(components.keywords[0].value, "buddhist");
assert_eq!(components.keywords[1].key, "kk");
assert_eq!(components.keywords[1].value, "");
assert_eq!(components.keywords[2].key, "nu");
assert_eq!(components.keywords[2].value, "thai");
let ext = JsString::new("-u-ca-islamic-civil");
let components = unicode_extension_components(&ext);
assert!(components.attributes.is_empty());
assert_eq!(components.keywords.len(), 1);
assert_eq!(components.keywords[0].key, "ca");
assert_eq!(components.keywords[0].value, "islamic-civil");
}
#[test]
fn locale_resolution() {
let mut context = Context::default();
// test lookup
let available_locales = Vec::<JsString>::new();
let requested_locales = Vec::<JsString>::new();
let relevant_extension_keys = Vec::<JsString>::new();
let locale_data = FxHashMap::default();
let options = DateTimeFormatRecord {
locale_matcher: JsString::new("lookup"),
properties: FxHashMap::default(),
};
let locale_record = resolve_locale(
&available_locales,
&requested_locales,
&options,
&relevant_extension_keys,
&locale_data,
&mut context,
);
assert_eq!(
locale_record.locale,
default_locale(context.icu().locale_canonicalizer())
.to_string()
.as_str()
);
assert_eq!(
locale_record.data_locale,
default_locale(context.icu().locale_canonicalizer())
.to_string()
.as_str()
);
assert!(locale_record.properties.is_empty());
// test best fit
let available_locales = Vec::<JsString>::new();
let requested_locales = Vec::<JsString>::new();
let relevant_extension_keys = Vec::<JsString>::new();
let locale_data = FxHashMap::default();
let options = DateTimeFormatRecord {
locale_matcher: JsString::new("best-fit"),
properties: FxHashMap::default(),
};
let locale_record = resolve_locale(
&available_locales,
&requested_locales,
&options,
&relevant_extension_keys,
&locale_data,
&mut context,
);
assert_eq!(
locale_record.locale,
default_locale(context.icu().locale_canonicalizer())
.to_string()
.as_str()
);
assert_eq!(
locale_record.data_locale,
default_locale(context.icu().locale_canonicalizer())
.to_string()
.as_str()
);
assert!(locale_record.properties.is_empty());
// available: [es-ES], requested: [es-ES]
let available_locales = vec![JsString::new("es-ES")];
let requested_locales = vec![JsString::new("es-ES")];
let relevant_extension_keys = Vec::<JsString>::new();
let locale_data = FxHashMap::default();
let options = DateTimeFormatRecord {
locale_matcher: JsString::new("lookup"),
properties: FxHashMap::default(),
};
let locale_record = resolve_locale(
&available_locales,
&requested_locales,
&options,
&relevant_extension_keys,
&locale_data,
&mut context,
);
assert_eq!(locale_record.locale, "es-ES");
assert_eq!(locale_record.data_locale, "es-ES");
assert!(locale_record.properties.is_empty());
// available: [zh-CN], requested: []
let available_locales = vec![JsString::new("zh-CN")];
let requested_locales = Vec::<JsString>::new();
let relevant_extension_keys = Vec::<JsString>::new();
let locale_data = FxHashMap::default();
let options = DateTimeFormatRecord {
locale_matcher: JsString::new("lookup"),
properties: FxHashMap::default(),
};
let locale_record = resolve_locale(
&available_locales,
&requested_locales,
&options,
&relevant_extension_keys,
&locale_data,
&mut context,
);
assert_eq!(
locale_record.locale,
default_locale(context.icu().locale_canonicalizer())
.to_string()
.as_str()
);
assert_eq!(
locale_record.data_locale,
default_locale(context.icu().locale_canonicalizer())
.to_string()
.as_str()
);
assert!(locale_record.properties.is_empty());
}
#[test]
fn get_opt() {
let mut context = Context::default();
let values = Vec::<JsString>::new();
let fallback = JsValue::String(JsString::new("fallback"));
let options_obj = JsObject::empty();
let option_type = GetOptionType::String;
let get_option_result = get_option(
&options_obj,
"",
&option_type,
&values,
&fallback,
&mut context,
)
.expect("GetOption should not fail on fallback test");
assert_eq!(get_option_result, fallback);
let values = Vec::<JsString>::new();
let fallback = JsValue::String(JsString::new("fallback"));
let options_obj = JsObject::empty();
let locale_value = JsValue::String(JsString::new("en-US"));
options_obj
.set("Locale", locale_value.clone(), true, &mut context)
.expect("Setting a property should not fail");
let option_type = GetOptionType::String;
let get_option_result = get_option(
&options_obj,
"Locale",
&option_type,
&values,
&fallback,
&mut context,
)
.expect("GetOption should not fail on string test");
assert_eq!(get_option_result, locale_value);
let fallback = JsValue::String(JsString::new("fallback"));
let options_obj = JsObject::empty();
let locale_string = JsString::new("en-US");
let locale_value = JsValue::String(locale_string.clone());
let values = vec![locale_string];
options_obj
.set("Locale", locale_value.clone(), true, &mut context)
.expect("Setting a property should not fail");
let option_type = GetOptionType::String;
let get_option_result = get_option(
&options_obj,
"Locale",
&option_type,
&values,
&fallback,
&mut context,
)
.expect("GetOption should not fail on values test");
assert_eq!(get_option_result, locale_value);
let fallback = JsValue::new(false);
let options_obj = JsObject::empty();
let boolean_value = JsValue::new(true);
let values = Vec::<JsString>::new();
options_obj
.set("boolean_val", boolean_value.clone(), true, &mut context)
.expect("Setting a property should not fail");
let option_type = GetOptionType::Boolean;
let get_option_result = get_option(
&options_obj,
"boolean_val",
&option_type,
&values,
&fallback,
&mut context,
)
.expect("GetOption should not fail on boolean test");
assert_eq!(get_option_result, boolean_value);
let fallback = JsValue::String(JsString::new("fallback"));
let options_obj = JsObject::empty();
let locale_value = JsValue::String(JsString::new("en-US"));
let other_locale_str = JsString::new("de-DE");
let values = vec![other_locale_str];
options_obj
.set("Locale", locale_value, true, &mut context)
.expect("Setting a property should not fail");
let option_type = GetOptionType::String;
let get_option_result = get_option(
&options_obj,
"Locale",
&option_type,
&values,
&fallback,
&mut context,
);
assert!(get_option_result.is_err());
let value = JsValue::undefined();
let minimum = 1.0;
let maximum = 10.0;
let fallback_val = 5.0;
let fallback = Some(fallback_val);
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context);
assert_eq!(get_option_result, Ok(fallback));
let value = JsValue::nan();
let minimum = 1.0;
let maximum = 10.0;
let fallback = Some(5.0);
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context);
assert!(get_option_result.is_err());
let value = JsValue::new(0);
let minimum = 1.0;
let maximum = 10.0;
let fallback = Some(5.0);
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context);
assert!(get_option_result.is_err());
let value = JsValue::new(11);
let minimum = 1.0;
let maximum = 10.0;
let fallback = Some(5.0);
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context);
assert!(get_option_result.is_err());
let value_f64 = 7.0;
let value = JsValue::new(value_f64);
let minimum = 1.0;
let maximum = 10.0;
let fallback = Some(5.0);
let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context);
assert_eq!(get_option_result, Ok(Some(value_f64)));
let options = JsObject::empty();
let property = "fractionalSecondDigits";
let minimum = 1.0;
let maximum = 10.0;
let fallback_val = 5.0;
let fallback = Some(fallback_val);
let get_option_result =
get_number_option(&options, property, minimum, maximum, fallback, &mut context);
assert_eq!(get_option_result, Ok(fallback));
let options = JsObject::empty();
let value_f64 = 8.0;
let value = JsValue::new(value_f64);
let property = "fractionalSecondDigits";
options
.set(property, value, true, &mut context)
.expect("Setting a property should not fail");
let minimum = 1.0;
let maximum = 10.0;
let fallback = Some(5.0);
let get_option_result =
get_number_option(&options, property, minimum, maximum, fallback, &mut context);
assert_eq!(get_option_result, Ok(Some(value_f64)));
}
#[test]
fn to_date_time_opts() {
let mut context = Context::default();
let options_obj = JsObject::empty();
options_obj
.set("timeStyle", JsObject::empty(), true, &mut context)
.expect("Setting a property should not fail");
let date_time_opts = to_date_time_options(
&JsValue::new(options_obj),
&DateTimeReqs::Date,
&DateTimeReqs::Date,
&mut context,
);
assert!(date_time_opts.is_err());
let options_obj = JsObject::empty();
options_obj
.set("dateStyle", JsObject::empty(), true, &mut context)
.expect("Setting a property should not fail");
let date_time_opts = to_date_time_options(
&JsValue::new(options_obj),
&DateTimeReqs::Time,
&DateTimeReqs::Time,
&mut context,
);
assert!(date_time_opts.is_err());
let date_time_opts = to_date_time_options(
&JsValue::undefined(),
&DateTimeReqs::Date,
&DateTimeReqs::Date,
&mut context,
)
.expect("toDateTimeOptions should not fail in date test");
let numeric_jsstring = JsValue::String(JsString::new("numeric"));
assert_eq!(
date_time_opts.get("year", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("month", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("day", &mut context),
Ok(numeric_jsstring)
);
let date_time_opts = to_date_time_options(
&JsValue::undefined(),
&DateTimeReqs::Time,
&DateTimeReqs::Time,
&mut context,
)
.expect("toDateTimeOptions should not fail in time test");
let numeric_jsstring = JsValue::String(JsString::new("numeric"));
assert_eq!(
date_time_opts.get("hour", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("minute", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("second", &mut context),
Ok(numeric_jsstring)
);
let date_time_opts = to_date_time_options(
&JsValue::undefined(),
&DateTimeReqs::AnyAll,
&DateTimeReqs::AnyAll,
&mut context,
)
.expect("toDateTimeOptions should not fail when testing required = 'any'");
let numeric_jsstring = JsValue::String(JsString::new("numeric"));
assert_eq!(
date_time_opts.get("year", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("month", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("day", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("hour", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("minute", &mut context),
Ok(numeric_jsstring.clone())
);
assert_eq!(
date_time_opts.get("second", &mut context),
Ok(numeric_jsstring)
);
}

View File

@@ -0,0 +1,508 @@
use crate::{
builtins::{
regexp::regexp_string_iterator::RegExpStringIterator,
string::string_iterator::StringIterator, ArrayIterator, ForInIterator, MapIterator,
SetIterator,
},
object::{JsObject, ObjectInitializer},
symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
#[derive(Debug, Default)]
pub struct IteratorPrototypes {
/// %IteratorPrototype%
iterator_prototype: JsObject,
/// %MapIteratorPrototype%
array_iterator: JsObject,
/// %SetIteratorPrototype%
set_iterator: JsObject,
/// %StringIteratorPrototype%
string_iterator: JsObject,
/// %RegExpStringIteratorPrototype%
regexp_string_iterator: JsObject,
/// %MapIteratorPrototype%
map_iterator: JsObject,
/// %ForInIteratorPrototype%
for_in_iterator: JsObject,
}
impl IteratorPrototypes {
pub(crate) fn init(context: &mut Context) -> Self {
let _timer = Profiler::global().start_event("IteratorPrototypes::init", "init");
let iterator_prototype = create_iterator_prototype(context);
Self {
array_iterator: ArrayIterator::create_prototype(iterator_prototype.clone(), context),
set_iterator: SetIterator::create_prototype(iterator_prototype.clone(), context),
string_iterator: StringIterator::create_prototype(iterator_prototype.clone(), context),
regexp_string_iterator: RegExpStringIterator::create_prototype(
iterator_prototype.clone(),
context,
),
map_iterator: MapIterator::create_prototype(iterator_prototype.clone(), context),
for_in_iterator: ForInIterator::create_prototype(iterator_prototype.clone(), context),
iterator_prototype,
}
}
#[inline]
pub fn array_iterator(&self) -> JsObject {
self.array_iterator.clone()
}
#[inline]
pub fn iterator_prototype(&self) -> JsObject {
self.iterator_prototype.clone()
}
#[inline]
pub fn set_iterator(&self) -> JsObject {
self.set_iterator.clone()
}
#[inline]
pub fn string_iterator(&self) -> JsObject {
self.string_iterator.clone()
}
#[inline]
pub fn regexp_string_iterator(&self) -> JsObject {
self.regexp_string_iterator.clone()
}
#[inline]
pub fn map_iterator(&self) -> JsObject {
self.map_iterator.clone()
}
#[inline]
pub fn for_in_iterator(&self) -> JsObject {
self.for_in_iterator.clone()
}
}
/// `CreateIterResultObject( value, done )`
///
/// Generates an object supporting the `IteratorResult` interface.
#[inline]
pub fn create_iter_result_object(value: JsValue, done: bool, context: &mut Context) -> JsValue {
let _timer = Profiler::global().start_event("create_iter_result_object", "init");
// 1. Assert: Type(done) is Boolean.
// 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%).
let obj = context.construct_object();
// 3. Perform ! CreateDataPropertyOrThrow(obj, "value", value).
obj.create_data_property_or_throw("value", value, context)
.expect("this CreateDataPropertyOrThrow call must not fail");
// 4. Perform ! CreateDataPropertyOrThrow(obj, "done", done).
obj.create_data_property_or_throw("done", done, context)
.expect("this CreateDataPropertyOrThrow call must not fail");
// 5. Return obj.
obj.into()
}
/// Iterator hint for `GetIterator`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IteratorHint {
Sync,
Async,
}
impl JsValue {
/// `GetIterator ( obj [ , hint [ , method ] ] )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-getiterator
#[inline]
pub fn get_iterator(
&self,
context: &mut Context,
hint: Option<IteratorHint>,
method: Option<Self>,
) -> JsResult<IteratorRecord> {
// 1. If hint is not present, set hint to sync.
let hint = hint.unwrap_or(IteratorHint::Sync);
// 2. If method is not present, then
let method = if let Some(method) = method {
method
} else {
// a. If hint is async, then
if hint == IteratorHint::Async {
// i. Set method to ? GetMethod(obj, @@asyncIterator).
if let Some(method) =
self.get_method(WellKnownSymbols::async_iterator(), context)?
{
method.into()
} else {
// ii. If method is undefined, then
// 1. Let syncMethod be ? GetMethod(obj, @@iterator).
let sync_method = self
.get_method(WellKnownSymbols::iterator(), context)?
.map_or(Self::Undefined, Self::from);
// 2. Let syncIteratorRecord be ? GetIterator(obj, sync, syncMethod).
let _sync_iterator_record =
self.get_iterator(context, Some(IteratorHint::Sync), Some(sync_method));
// 3. Return ! CreateAsyncFromSyncIterator(syncIteratorRecord).
todo!("CreateAsyncFromSyncIterator");
}
} else {
// b. Otherwise, set method to ? GetMethod(obj, @@iterator).
self.get_method(WellKnownSymbols::iterator(), context)?
.map_or(Self::Undefined, Self::from)
}
};
// 3. Let iterator be ? Call(method, obj).
let iterator = context.call(&method, self, &[])?;
// 4. If Type(iterator) is not Object, throw a TypeError exception.
let iterator_obj = iterator
.as_object()
.ok_or_else(|| context.construct_type_error("the iterator is not an object"))?;
// 5. Let nextMethod be ? GetV(iterator, "next").
let next_method = iterator.get_v("next", context)?;
// 6. Let iteratorRecord be the Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }.
// 7. Return iteratorRecord.
Ok(IteratorRecord::new(
iterator_obj.clone(),
next_method,
false,
))
}
}
/// Create the `%IteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%iteratorprototype%-object
#[inline]
fn create_iterator_prototype(context: &mut Context) -> JsObject {
let _timer = Profiler::global().start_event("Iterator Prototype", "init");
let symbol_iterator = WellKnownSymbols::iterator();
let iterator_prototype = ObjectInitializer::new(context)
.function(
|v, _, _| Ok(v.clone()),
(symbol_iterator, "[Symbol.iterator]"),
0,
)
.build();
iterator_prototype
}
/// The result of the iteration process.
#[derive(Debug)]
pub struct IteratorResult {
object: JsObject,
}
impl IteratorResult {
/// `IteratorComplete ( iterResult )`
///
/// The abstract operation `IteratorComplete` takes argument `iterResult` (an `Object`) and
/// returns either a normal completion containing a `Boolean` or a throw completion.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-iteratorcomplete
#[inline]
pub fn complete(&self, context: &mut Context) -> JsResult<bool> {
// 1. Return ToBoolean(? Get(iterResult, "done")).
Ok(self.object.get("done", context)?.to_boolean())
}
/// `IteratorValue ( iterResult )`
///
/// The abstract operation `IteratorValue` takes argument `iterResult` (an `Object`) and
/// returns either a normal completion containing an ECMAScript language value or a throw
/// completion.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-iteratorvalue
#[inline]
pub fn value(&self, context: &mut Context) -> JsResult<JsValue> {
// 1. Return ? Get(iterResult, "value").
self.object.get("value", context)
}
}
/// Iterator Record
///
/// An Iterator Record is a Record value used to encapsulate an
/// `Iterator` or `AsyncIterator` along with the `next` method.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-iterator-records
#[derive(Debug)]
pub struct IteratorRecord {
/// `[[Iterator]]`
///
/// An object that conforms to the `Iterator` or `AsyncIterator` interface.
iterator: JsObject,
/// `[[NextMethod]]`
///
/// The `next` method of the `[[Iterator]]` object.
next_method: JsValue,
/// `[[Done]]`
///
/// Whether the iterator has been closed.
done: bool,
}
impl IteratorRecord {
/// Creates a new `IteratorRecord` with the given iterator object, next method and `done` flag.
#[inline]
pub fn new(iterator: JsObject, next_method: JsValue, done: bool) -> Self {
Self {
iterator,
next_method,
done,
}
}
/// Get the `[[Iterator]]` field of the `IteratorRecord`.
#[inline]
pub(crate) fn iterator(&self) -> &JsObject {
&self.iterator
}
/// Get the `[[NextMethod]]` field of the `IteratorRecord`.
#[inline]
pub(crate) fn next_method(&self) -> &JsValue {
&self.next_method
}
/// Get the `[[Done]]` field of the `IteratorRecord`.
#[inline]
pub(crate) fn done(&self) -> bool {
self.done
}
/// Sets the `[[Done]]` field of the `IteratorRecord`.
#[inline]
pub(crate) fn set_done(&mut self, done: bool) {
self.done = done;
}
/// `IteratorNext ( iteratorRecord [ , value ] )`
///
/// The abstract operation `IteratorNext` takes argument `iteratorRecord` (an `Iterator`
/// Record) and optional argument `value` (an ECMAScript language value) and returns either a
/// normal completion containing an `Object` or a throw completion.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-iteratornext
#[inline]
pub(crate) fn next(
&self,
value: Option<JsValue>,
context: &mut Context,
) -> JsResult<IteratorResult> {
let _timer = Profiler::global().start_event("IteratorRecord::next", "iterator");
// Note: We check if iteratorRecord.[[NextMethod]] is callable here.
// This check would happen in `Call` according to the spec, but we do not implement call for `JsValue`.
let next_method = if let Some(next_method) = self.next_method.as_callable() {
next_method
} else {
return context.throw_type_error("iterable next method not a function");
};
let result = if let Some(value) = value {
// 2. Else,
// a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]], « value »).
next_method.call(&self.iterator.clone().into(), &[value], context)?
} else {
// 1. If value is not present, then
// a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]).
next_method.call(&self.iterator.clone().into(), &[], context)?
};
// 3. If Type(result) is not Object, throw a TypeError exception.
// 4. Return result.
if let Some(o) = result.as_object() {
Ok(IteratorResult { object: o.clone() })
} else {
context.throw_type_error("next value should be an object")
}
}
/// `IteratorStep ( iteratorRecord )`
///
/// The abstract operation `IteratorStep` takes argument `iteratorRecord` (an `Iterator`
/// Record) and returns either a normal completion containing either an `Object` or `false`, or
/// a throw completion. It requests the next value from `iteratorRecord.[[Iterator]]` by
/// calling `iteratorRecord.[[NextMethod]]` and returns either `false` indicating that the
/// iterator has reached its end or the `IteratorResult` object if a next value is available.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-iteratorstep
pub(crate) fn step(&self, context: &mut Context) -> JsResult<Option<IteratorResult>> {
let _timer = Profiler::global().start_event("IteratorRecord::step", "iterator");
// 1. Let result be ? IteratorNext(iteratorRecord).
let result = self.next(None, context)?;
// 2. Let done be ? IteratorComplete(result).
let done = result.complete(context)?;
// 3. If done is true, return false.
if done {
return Ok(None);
}
// 4. Return result.
Ok(Some(result))
}
/// `IteratorClose ( iteratorRecord, completion )`
///
/// The abstract operation `IteratorClose` takes arguments `iteratorRecord` (an
/// [Iterator Record][Self]) and `completion` (a `Completion` Record) and returns a
/// `Completion` Record. It is used to notify an iterator that it should perform any actions it
/// would normally perform when it has reached its completed state.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-iteratorclose
#[inline]
pub(crate) fn close(
&self,
completion: JsResult<JsValue>,
context: &mut Context,
) -> JsResult<JsValue> {
let _timer = Profiler::global().start_event("IteratorRecord::close", "iterator");
// 1. Assert: Type(iteratorRecord.[[Iterator]]) is Object.
// 2. Let iterator be iteratorRecord.[[Iterator]].
let iterator = &self.iterator;
// 3. Let innerResult be Completion(GetMethod(iterator, "return")).
let inner_result = iterator.get_method("return", context);
// 4. If innerResult.[[Type]] is normal, then
let inner_result = match inner_result {
Ok(inner_result) => {
// a. Let return be innerResult.[[Value]].
let r#return = inner_result;
if let Some(r#return) = r#return {
// c. Set innerResult to Completion(Call(return, iterator)).
r#return.call(&iterator.clone().into(), &[], context)
} else {
// b. If return is undefined, return ? completion.
return completion;
}
}
Err(inner_result) => {
// 5. If completion.[[Type]] is throw, return ? completion.
completion?;
// 6. If innerResult.[[Type]] is throw, return ? innerResult.
return Err(inner_result);
}
};
// 5. If completion.[[Type]] is throw, return ? completion.
let completion = completion?;
// 6. If innerResult.[[Type]] is throw, return ? innerResult.
let inner_result = inner_result?;
if inner_result.is_object() {
// 8. Return ? completion.
Ok(completion)
} else {
// 7. If Type(innerResult.[[Value]]) is not Object, throw a TypeError exception.
context.throw_type_error("inner result was not an object")
}
}
}
/// `IterableToList ( items [ , method ] )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-iterabletolist
pub(crate) fn iterable_to_list(
context: &mut Context,
items: &JsValue,
method: Option<JsValue>,
) -> JsResult<Vec<JsValue>> {
let _timer = Profiler::global().start_event("iterable_to_list", "iterator");
// 1. If method is present, then
let iterator_record = if let Some(method) = method {
// a. Let iteratorRecord be ? GetIterator(items, sync, method).
items.get_iterator(context, Some(IteratorHint::Sync), Some(method))?
} else {
// 2. Else,
// a. Let iteratorRecord be ? GetIterator(items, sync).
items.get_iterator(context, Some(IteratorHint::Sync), None)?
};
// 3. Let values be a new empty List.
let mut values = Vec::new();
// 4. Let next be true.
// 5. Repeat, while next is not false,
// a. Set next to ? IteratorStep(iteratorRecord).
// b. If next is not false, then
// i. Let nextValue be ? IteratorValue(next).
// ii. Append nextValue to the end of the List values.
while let Some(next) = iterator_record.step(context)? {
let next_value = next.value(context)?;
values.push(next_value);
}
// 6. Return values.
Ok(values)
}
/// `IfAbruptCloseIterator ( value, iteratorRecord )`
///
/// `IfAbruptCloseIterator` is a shorthand for a sequence of algorithm steps that use an `Iterator`
/// Record.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-ifabruptcloseiterator
macro_rules! if_abrupt_close_iterator {
($value:expr, $iterator_record:expr, $context:expr) => {
match $value {
// 1. If value is an abrupt completion, return ? IteratorClose(iteratorRecord, value).
Err(err) => return $iterator_record.close(Err(err), $context),
// 2. Else if value is a Completion Record, set value to value.
Ok(value) => value,
}
};
}
// Export macro to crate level
pub(crate) use if_abrupt_close_iterator;

View File

@@ -0,0 +1,739 @@
//! This module implements the global `JSON` object.
//!
//! The `JSON` object contains methods for parsing [JavaScript Object Notation (JSON)][spec]
//! and converting values to JSON. It can't be called or constructed, and aside from its
//! two method properties, it has no interesting functionality of its own.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//! - [JSON specification][json]
//!
//! [spec]: https://tc39.es/ecma262/#sec-json
//! [json]: https://www.json.org/json-en.html
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
use super::JsArgs;
use crate::{
builtins::{
string::{is_leading_surrogate, is_trailing_surrogate},
BuiltIn,
},
object::{JsObject, ObjectInitializer, RecursionLimiter},
property::{Attribute, PropertyNameKind},
symbol::WellKnownSymbols,
value::IntegerOrInfinity,
Context, JsResult, JsString, JsValue,
};
use boa_profiler::Profiler;
use serde_json::{self, Value as JSONValue};
use tap::{Conv, Pipe};
#[cfg(test)]
mod tests;
/// JavaScript `JSON` global object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Json;
impl BuiltIn for Json {
const NAME: &'static str = "JSON";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let to_string_tag = WellKnownSymbols::to_string_tag();
let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
ObjectInitializer::new(context)
.function(Self::parse, "parse", 2)
.function(Self::stringify, "stringify", 3)
.property(to_string_tag, Self::NAME, attribute)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Json {
/// `JSON.parse( text[, reviver] )`
///
/// This `JSON` method parses a JSON string, constructing the JavaScript value or object described by the string.
///
/// An optional `reviver` function can be provided to perform a transformation on the resulting object before it is returned.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-json.parse
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let jsonString be ? ToString(text).
let json_string = args
.get(0)
.cloned()
.unwrap_or_default()
.to_string(context)?;
// 2. Parse ! StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404.
// Throw a SyntaxError exception if it is not a valid JSON text as defined in that specification.
if let Err(e) = serde_json::from_str::<JSONValue>(&json_string) {
return context.throw_syntax_error(e.to_string());
}
// 3. Let scriptString be the string-concatenation of "(", jsonString, and ");".
let script_string = JsString::concat_array(&["(", json_string.as_str(), ");"]);
// 4. Let script be ParseText(! StringToCodePoints(scriptString), Script).
// 5. NOTE: The early error rules defined in 13.2.5.1 have special handling for the above invocation of ParseText.
// 6. Assert: script is a Parse Node.
// 7. Let completion be the result of evaluating script.
// 8. NOTE: The PropertyDefinitionEvaluation semantics defined in 13.2.5.5 have special handling for the above evaluation.
// 9. Let unfiltered be completion.[[Value]].
// 10. Assert: unfiltered is either a String, Number, Boolean, Null, or an Object that is defined by either an ArrayLiteral or an ObjectLiteral.
let unfiltered = context.eval(script_string.as_bytes())?;
// 11. If IsCallable(reviver) is true, then
if let Some(obj) = args.get_or_undefined(1).as_callable() {
// a. Let root be ! OrdinaryObjectCreate(%Object.prototype%).
let root = context.construct_object();
// b. Let rootName be the empty String.
// c. Perform ! CreateDataPropertyOrThrow(root, rootName, unfiltered).
root.create_data_property_or_throw("", unfiltered, context)
.expect("CreateDataPropertyOrThrow should never throw here");
// d. Return ? InternalizeJSONProperty(root, rootName, reviver).
Self::internalize_json_property(&root, "".into(), obj, context)
} else {
// 12. Else,
// a. Return unfiltered.
Ok(unfiltered)
}
}
/// `25.5.1.1 InternalizeJSONProperty ( holder, name, reviver )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-internalizejsonproperty
fn internalize_json_property(
holder: &JsObject,
name: JsString,
reviver: &JsObject,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let val be ? Get(holder, name).
let val = holder.get(name.clone(), context)?;
// 2. If Type(val) is Object, then
if let Some(obj) = val.as_object() {
// a. Let isArray be ? IsArray(val).
// b. If isArray is true, then
if obj.is_array_abstract(context)? {
// i. Let I be 0.
// ii. Let len be ? LengthOfArrayLike(val).
// iii. Repeat, while I < len,
let len = obj.length_of_array_like(context)? as i64;
for i in 0..len {
// 1. Let prop be ! ToString(𝔽(I)).
// 2. Let newElement be ? InternalizeJSONProperty(val, prop, reviver).
let new_element = Self::internalize_json_property(
obj,
i.to_string().into(),
reviver,
context,
)?;
// 3. If newElement is undefined, then
if new_element.is_undefined() {
// a. Perform ? val.[[Delete]](prop).
obj.__delete__(&i.into(), context)?;
}
// 4. Else,
else {
// a. Perform ? CreateDataProperty(val, prop, newElement).
obj.create_data_property(i, new_element, context)?;
}
}
}
// c. Else,
else {
// i. Let keys be ? EnumerableOwnPropertyNames(val, key).
let keys = obj.enumerable_own_property_names(PropertyNameKind::Key, context)?;
// ii. For each String P of keys, do
for p in keys {
// This is safe, because EnumerableOwnPropertyNames with 'key' type only returns strings.
let p = p
.as_string()
.expect("EnumerableOwnPropertyNames only returns strings");
// 1. Let newElement be ? InternalizeJSONProperty(val, P, reviver).
let new_element =
Self::internalize_json_property(obj, p.clone(), reviver, context)?;
// 2. If newElement is undefined, then
if new_element.is_undefined() {
// a. Perform ? val.[[Delete]](P).
obj.__delete__(&p.clone().into(), context)?;
}
// 3. Else,
else {
// a. Perform ? CreateDataProperty(val, P, newElement).
obj.create_data_property(p.as_str(), new_element, context)?;
}
}
}
}
// 3. Return ? Call(reviver, holder, « name, val »).
reviver.call(&holder.clone().into(), &[name.into(), val], context)
}
/// `JSON.stringify( value[, replacer[, space]] )`
///
/// This `JSON` method converts a JavaScript object or value to a JSON string.
///
/// This method optionally replaces values if a `replacer` function is specified or
/// optionally including only the specified properties if a replacer array is specified.
///
/// An optional `space` argument can be supplied of type `String` or `Number` that's used to insert
/// white space into the output JSON string for readability purposes.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-json.stringify
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
pub(crate) fn stringify(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let stack be a new empty List.
let stack = Vec::new();
// 2. Let indent be the empty String.
let indent = JsString::new("");
// 3. Let PropertyList and ReplacerFunction be undefined.
let mut property_list = None;
let mut replacer_function = None;
let replacer = args.get_or_undefined(1);
// 4. If Type(replacer) is Object, then
if let Some(replacer_obj) = replacer.as_object() {
// a. If IsCallable(replacer) is true, then
if replacer_obj.is_callable() {
// i. Set ReplacerFunction to replacer.
replacer_function = Some(replacer_obj.clone());
// b. Else,
} else {
// i. Let isArray be ? IsArray(replacer).
// ii. If isArray is true, then
if replacer_obj.is_array_abstract(context)? {
// 1. Set PropertyList to a new empty List.
let mut property_set = indexmap::IndexSet::new();
// 2. Let len be ? LengthOfArrayLike(replacer).
let len = replacer_obj.length_of_array_like(context)?;
// 3. Let k be 0.
let mut k = 0;
// 4. Repeat, while k < len,
while k < len {
// a. Let prop be ! ToString(𝔽(k)).
// b. Let v be ? Get(replacer, prop).
let v = replacer_obj.get(k, context)?;
// c. Let item be undefined.
// d. If Type(v) is String, set item to v.
// e. Else if Type(v) is Number, set item to ! ToString(v).
// f. Else if Type(v) is Object, then
// g. If item is not undefined and item is not currently an element of PropertyList, then
// i. Append item to the end of PropertyList.
if let Some(s) = v.as_string() {
property_set.insert(s.clone());
} else if v.is_number() {
property_set.insert(
v.to_string(context)
.expect("ToString cannot fail on number value"),
);
} else if let Some(obj) = v.as_object() {
// i. If v has a [[StringData]] or [[NumberData]] internal slot, set item to ? ToString(v).
if obj.is_string() || obj.is_number() {
property_set.insert(v.to_string(context)?);
}
}
// h. Set k to k + 1.
k += 1;
}
property_list = Some(property_set.into_iter().collect());
}
}
}
let mut space = args.get_or_undefined(2).clone();
// 5. If Type(space) is Object, then
if let Some(space_obj) = space.as_object() {
// a. If space has a [[NumberData]] internal slot, then
if space_obj.is_number() {
// i. Set space to ? ToNumber(space).
space = space.to_number(context)?.into();
}
// b. Else if space has a [[StringData]] internal slot, then
else if space_obj.is_string() {
// i. Set space to ? ToString(space).
space = space.to_string(context)?.into();
}
}
// 6. If Type(space) is Number, then
let gap = if space.is_number() {
// a. Let spaceMV be ! ToIntegerOrInfinity(space).
// b. Set spaceMV to min(10, spaceMV).
// c. If spaceMV < 1, let gap be the empty String; otherwise let gap be the String value containing spaceMV occurrences of the code unit 0x0020 (SPACE).
match space
.to_integer_or_infinity(context)
.expect("ToIntegerOrInfinity cannot fail on number")
{
IntegerOrInfinity::PositiveInfinity => JsString::new(" "),
IntegerOrInfinity::NegativeInfinity => JsString::new(""),
IntegerOrInfinity::Integer(i) if i < 1 => JsString::new(""),
IntegerOrInfinity::Integer(i) => {
let mut s = String::new();
let i = std::cmp::min(10, i);
for _ in 0..i {
s.push(' ');
}
s.into()
}
}
// 7. Else if Type(space) is String, then
} else if let Some(s) = space.as_string() {
// a. If the length of space is 10 or less, let gap be space; otherwise let gap be the substring of space from 0 to 10.
String::from_utf16_lossy(&s.encode_utf16().take(10).collect::<Vec<u16>>()).into()
// 8. Else,
} else {
// a. Let gap be the empty String.
JsString::new("")
};
// 9. Let wrapper be ! OrdinaryObjectCreate(%Object.prototype%).
let wrapper = context.construct_object();
// 10. Perform ! CreateDataPropertyOrThrow(wrapper, the empty String, value).
wrapper
.create_data_property_or_throw("", args.get_or_undefined(0).clone(), context)
.expect("CreateDataPropertyOrThrow should never fail here");
// 11. Let state be the Record { [[ReplacerFunction]]: ReplacerFunction, [[Stack]]: stack, [[Indent]]: indent, [[Gap]]: gap, [[PropertyList]]: PropertyList }.
let mut state = StateRecord {
replacer_function,
stack,
indent,
gap,
property_list,
};
// 12. Return ? SerializeJSONProperty(state, the empty String, wrapper).
Ok(
Self::serialize_json_property(&mut state, JsString::new(""), &wrapper, context)?
.map(Into::into)
.unwrap_or_default(),
)
}
/// `25.5.2.1 SerializeJSONProperty ( state, key, holder )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-serializejsonproperty
fn serialize_json_property(
state: &mut StateRecord,
key: JsString,
holder: &JsObject,
context: &mut Context,
) -> JsResult<Option<JsString>> {
// 1. Let value be ? Get(holder, key).
let mut value = holder.get(key.clone(), context)?;
// 2. If Type(value) is Object or BigInt, then
if value.is_object() || value.is_bigint() {
// a. Let toJSON be ? GetV(value, "toJSON").
let to_json = value.get_v("toJSON", context)?;
// b. If IsCallable(toJSON) is true, then
if let Some(obj) = to_json.as_object() {
if obj.is_callable() {
// i. Set value to ? Call(toJSON, value, « key »).
value = obj.call(&value, &[key.clone().into()], context)?;
}
}
}
// 3. If state.[[ReplacerFunction]] is not undefined, then
if let Some(obj) = &state.replacer_function {
// a. Set value to ? Call(state.[[ReplacerFunction]], holder, « key, value »).
value = obj.call(&holder.clone().into(), &[key.into(), value], context)?;
}
// 4. If Type(value) is Object, then
if let Some(obj) = value.as_object().cloned() {
// a. If value has a [[NumberData]] internal slot, then
if obj.is_number() {
// i. Set value to ? ToNumber(value).
value = value.to_number(context)?.into();
}
// b. Else if value has a [[StringData]] internal slot, then
else if obj.is_string() {
// i. Set value to ? ToString(value).
value = value.to_string(context)?.into();
}
// c. Else if value has a [[BooleanData]] internal slot, then
else if let Some(boolean) = obj.borrow().as_boolean() {
// i. Set value to value.[[BooleanData]].
value = boolean.into();
}
// d. Else if value has a [[BigIntData]] internal slot, then
else if let Some(bigint) = obj.borrow().as_bigint() {
// i. Set value to value.[[BigIntData]].
value = bigint.clone().into();
}
}
// 5. If value is null, return "null".
if value.is_null() {
return Ok(Some(JsString::new("null")));
}
// 6. If value is true, return "true".
// 7. If value is false, return "false".
if value.is_boolean() {
return Ok(Some(JsString::new(if value.to_boolean() {
"true"
} else {
"false"
})));
}
// 8. If Type(value) is String, return QuoteJSONString(value).
if let Some(s) = value.as_string() {
return Ok(Some(Self::quote_json_string(s)));
}
// 9. If Type(value) is Number, then
if let Some(n) = value.as_number() {
// a. If value is finite, return ! ToString(value).
if n.is_finite() {
return Ok(Some(
value
.to_string(context)
.expect("ToString should never fail here"),
));
}
// b. Return "null".
return Ok(Some(JsString::new("null")));
}
// 10. If Type(value) is BigInt, throw a TypeError exception.
if value.is_bigint() {
return context.throw_type_error("cannot serialize bigint to JSON");
}
// 11. If Type(value) is Object and IsCallable(value) is false, then
if let Some(obj) = value.as_object() {
if !obj.is_callable() {
// a. Let isArray be ? IsArray(value).
// b. If isArray is true, return ? SerializeJSONArray(state, value).
// c. Return ? SerializeJSONObject(state, value).
return if obj.is_array_abstract(context)? {
Ok(Some(Self::serialize_json_array(state, obj, context)?))
} else {
Ok(Some(Self::serialize_json_object(state, obj, context)?))
};
}
}
// 12. Return undefined.
Ok(None)
}
/// `25.5.2.2 QuoteJSONString ( value )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-quotejsonstring
fn quote_json_string(value: &JsString) -> JsString {
// 1. Let product be the String value consisting solely of the code unit 0x0022 (QUOTATION MARK).
let mut product = String::from('"');
// 2. For each code point C of ! StringToCodePoints(value), do
for code_point in value.encode_utf16() {
match code_point {
// a. If C is listed in the “Code Point” column of Table 73, then
// i. Set product to the string-concatenation of product and the escape sequence for C as specified in the “Escape Sequence” column of the corresponding row.
0x8 => product.push_str("\\b"),
0x9 => product.push_str("\\t"),
0xA => product.push_str("\\n"),
0xC => product.push_str("\\f"),
0xD => product.push_str("\\r"),
0x22 => product.push_str("\\\""),
0x5C => product.push_str("\\\\"),
// b. Else if C has a numeric value less than 0x0020 (SPACE), or if C has the same numeric value as a leading surrogate or trailing surrogate, then
code_point
if is_leading_surrogate(code_point) || is_trailing_surrogate(code_point) =>
{
// i. Let unit be the code unit whose numeric value is that of C.
// ii. Set product to the string-concatenation of product and UnicodeEscape(unit).
product.push_str(&format!("\\\\uAA{code_point:x}"));
}
// c. Else,
code_point => {
// i. Set product to the string-concatenation of product and ! UTF16EncodeCodePoint(C).
product.push(
char::from_u32(u32::from(code_point))
.expect("char from code point cannot fail here"),
);
}
}
}
// 3. Set product to the string-concatenation of product and the code unit 0x0022 (QUOTATION MARK).
product.push('"');
// 4. Return product.
product.into()
}
/// `25.5.2.4 SerializeJSONObject ( state, value )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-serializejsonobject
fn serialize_json_object(
state: &mut StateRecord,
value: &JsObject,
context: &mut Context,
) -> JsResult<JsString> {
// 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
let limiter = RecursionLimiter::new(value);
if limiter.live {
return context.throw_type_error("cyclic object value");
}
// 2. Append value to state.[[Stack]].
state.stack.push(value.clone().into());
// 3. Let stepback be state.[[Indent]].
let stepback = state.indent.clone();
// 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
state.indent = JsString::concat(&state.indent, &state.gap);
// 5. If state.[[PropertyList]] is not undefined, then
let k = if let Some(p) = &state.property_list {
// a. Let K be state.[[PropertyList]].
p.clone()
// 6. Else,
} else {
// a. Let K be ? EnumerableOwnPropertyNames(value, key).
let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context)?;
// Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values.
keys.iter()
.map(|v| {
v.to_string(context)
.expect("EnumerableOwnPropertyNames only returns strings")
})
.collect()
};
// 7. Let partial be a new empty List.
let mut partial = Vec::new();
// 8. For each element P of K, do
for p in &k {
// a. Let strP be ? SerializeJSONProperty(state, P, value).
let str_p = Self::serialize_json_property(state, p.clone(), value, context)?;
// b. If strP is not undefined, then
if let Some(str_p) = str_p {
// i. Let member be QuoteJSONString(P).
// ii. Set member to the string-concatenation of member and ":".
// iii. If state.[[Gap]] is not the empty String, then
// 1. Set member to the string-concatenation of member and the code unit 0x0020 (SPACE).
// iv. Set member to the string-concatenation of member and strP.
let member = if state.gap.is_empty() {
format!("{}:{}", Self::quote_json_string(p).as_str(), str_p.as_str())
} else {
format!(
"{}: {}",
Self::quote_json_string(p).as_str(),
str_p.as_str()
)
};
// v. Append member to partial.
partial.push(member);
}
}
// 9. If partial is empty, then
let r#final = if partial.is_empty() {
// a. Let final be "{}".
JsString::new("{}")
// 10. Else,
} else {
// a. If state.[[Gap]] is the empty String, then
if state.gap.is_empty() {
// i. Let properties be the String value formed by concatenating all the element Strings of partial
// with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
// A comma is not inserted either before the first String or after the last String.
// ii. Let final be the string-concatenation of "{", properties, and "}".
format!("{{{}}}", partial.join(",")).into()
// b. Else,
} else {
// i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
// the code unit 0x000A (LINE FEED), and state.[[Indent]].
let separator = format!(",\n{}", state.indent.as_str());
// ii. Let properties be the String value formed by concatenating all the element Strings of partial
// with each adjacent pair of Strings separated with separator.
// The separator String is not inserted either before the first String or after the last String.
let properties = partial.join(&separator);
// iii. Let final be the string-concatenation of "{", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepback, and "}".
format!(
"{{\n{}{properties}\n{}}}",
state.indent.as_str(),
stepback.as_str()
)
.into()
}
};
// 11. Remove the last element of state.[[Stack]].
state.stack.pop();
// 12. Set state.[[Indent]] to stepback.
state.indent = stepback;
// 13. Return final.
Ok(r#final)
}
/// `25.5.2.5 SerializeJSONArray ( state, value )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-serializejsonarray
fn serialize_json_array(
state: &mut StateRecord,
value: &JsObject,
context: &mut Context,
) -> JsResult<JsString> {
// 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
let limiter = RecursionLimiter::new(value);
if limiter.live {
return context.throw_type_error("cyclic object value");
}
// 2. Append value to state.[[Stack]].
state.stack.push(value.clone().into());
// 3. Let stepback be state.[[Indent]].
let stepback = state.indent.clone();
// 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
state.indent = JsString::concat(&state.indent, &state.gap);
// 5. Let partial be a new empty List.
let mut partial = Vec::new();
// 6. Let len be ? LengthOfArrayLike(value).
let len = value.length_of_array_like(context)?;
// 7. Let index be 0.
let mut index = 0;
// 8. Repeat, while index < len,
while index < len {
// a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value).
let str_p =
Self::serialize_json_property(state, index.to_string().into(), value, context)?;
// b. If strP is undefined, then
if let Some(str_p) = str_p {
// i. Append strP to partial.
partial.push(str_p);
// c. Else,
} else {
// i. Append "null" to partial.
partial.push("null".into());
}
// d. Set index to index + 1.
index += 1;
}
// 9. If partial is empty, then
let r#final = if partial.is_empty() {
// a. Let final be "[]".
JsString::from("[]")
// 10. Else,
} else {
// a. If state.[[Gap]] is the empty String, then
if state.gap.is_empty() {
// i. Let properties be the String value formed by concatenating all the element Strings of partial
// with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
// A comma is not inserted either before the first String or after the last String.
// ii. Let final be the string-concatenation of "[", properties, and "]".
format!("[{}]", partial.join(",")).into()
// b. Else,
} else {
// i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
// the code unit 0x000A (LINE FEED), and state.[[Indent]].
let separator = format!(",\n{}", state.indent.as_str());
// ii. Let properties be the String value formed by concatenating all the element Strings of partial
// with each adjacent pair of Strings separated with separator.
// The separator String is not inserted either before the first String or after the last String.
let properties = partial.join(&separator);
// iii. Let final be the string-concatenation of "[", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepback, and "]".
format!(
"[\n{}{properties}\n{}]",
state.indent.as_str(),
stepback.as_str()
)
.into()
}
};
// 11. Remove the last element of state.[[Stack]].
state.stack.pop();
// 12. Set state.[[Indent]] to stepback.
state.indent = stepback;
// 13. Return final.
Ok(r#final)
}
}
struct StateRecord {
replacer_function: Option<JsObject>,
stack: Vec<JsValue>,
indent: JsString,
gap: JsString,
property_list: Option<Vec<JsString>>,
}

View File

@@ -0,0 +1,468 @@
use crate::{forward, forward_val, Context};
#[test]
fn json_sanity() {
let mut context = Context::default();
assert_eq!(
forward(&mut context, r#"JSON.parse('{"aaa":"bbb"}').aaa == 'bbb'"#),
"true"
);
assert_eq!(
forward(
&mut context,
r#"JSON.stringify({aaa: 'bbb'}) == '{"aaa":"bbb"}'"#
),
"true"
);
}
#[test]
fn json_stringify_remove_undefined_values_from_objects() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({ aaa: undefined, bbb: 'ccc' })"#,
);
let expected = r#""{"bbb":"ccc"}""#;
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_remove_function_values_from_objects() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({ aaa: () => {}, bbb: 'ccc' })"#,
);
let expected = r#""{"bbb":"ccc"}""#;
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_remove_symbols_from_objects() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({ aaa: Symbol(), bbb: 'ccc' })"#,
);
let expected = r#""{"bbb":"ccc"}""#;
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_replacer_array_strings() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({aaa: 'bbb', bbb: 'ccc', ccc: 'ddd'}, ['aaa', 'bbb'])"#,
);
let expected = forward(&mut context, r#"'{"aaa":"bbb","bbb":"ccc"}'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_replacer_array_numbers() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({ 0: 'aaa', 1: 'bbb', 2: 'ccc'}, [1, 2])"#,
);
let expected = forward(&mut context, r#"'{"1":"bbb","2":"ccc"}'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_replacer_function() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({ aaa: 1, bbb: 2}, (key, value) => {
if (key === 'aaa') {
return undefined;
}
return value;
})"#,
);
let expected = forward(&mut context, r#"'{"bbb":2}'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_arrays() {
let mut context = Context::default();
let actual = forward(&mut context, r#"JSON.stringify(['a', 'b'])"#);
let expected = forward(&mut context, r#"'["a","b"]'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_object_array() {
let mut context = Context::default();
let actual = forward(&mut context, r#"JSON.stringify([{a: 'b'}, {b: 'c'}])"#);
let expected = forward(&mut context, r#"'[{"a":"b"},{"b":"c"}]'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_array_converts_undefined_to_null() {
let mut context = Context::default();
let actual = forward(&mut context, r#"JSON.stringify([undefined])"#);
let expected = forward(&mut context, r#"'[null]'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_array_converts_function_to_null() {
let mut context = Context::default();
let actual = forward(&mut context, r#"JSON.stringify([() => {}])"#);
let expected = forward(&mut context, r#"'[null]'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_array_converts_symbol_to_null() {
let mut context = Context::default();
let actual = forward(&mut context, r#"JSON.stringify([Symbol()])"#);
let expected = forward(&mut context, r#"'[null]'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_function_replacer_propagate_error() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"
let thrown = 0;
try {
JSON.stringify({x: 1}, (key, value) => { throw 1 })
} catch (err) {
thrown = err;
}
thrown
"#,
);
let expected = forward(&mut context, "1");
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_function() {
let mut context = Context::default();
let actual_function = forward(&mut context, r#"JSON.stringify(() => {})"#);
let expected = forward(&mut context, r#"undefined"#);
assert_eq!(actual_function, expected);
}
#[test]
fn json_stringify_undefined() {
let mut context = Context::default();
let actual_undefined = forward(&mut context, r#"JSON.stringify(undefined)"#);
let expected = forward(&mut context, r#"undefined"#);
assert_eq!(actual_undefined, expected);
}
#[test]
fn json_stringify_symbol() {
let mut context = Context::default();
let actual_symbol = forward(&mut context, r#"JSON.stringify(Symbol())"#);
let expected = forward(&mut context, r#"undefined"#);
assert_eq!(actual_symbol, expected);
}
#[test]
fn json_stringify_no_args() {
let mut context = Context::default();
let actual_no_args = forward(&mut context, r#"JSON.stringify()"#);
let expected = forward(&mut context, r#"undefined"#);
assert_eq!(actual_no_args, expected);
}
#[test]
fn json_stringify_fractional_numbers() {
let mut context = Context::default();
let actual = forward(&mut context, r#"JSON.stringify(Math.round(1.0))"#);
let expected = forward(&mut context, r#""1""#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_pretty_print() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({a: "b", b: "c"}, undefined, 4)"#,
);
let expected = forward(
&mut context,
r#"'{\n'
+' "a": "b",\n'
+' "b": "c"\n'
+'}'"#,
);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_pretty_print_four_spaces() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({a: "b", b: "c"}, undefined, 4.3)"#,
);
let expected = forward(
&mut context,
r#"'{\n'
+' "a": "b",\n'
+' "b": "c"\n'
+'}'"#,
);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_pretty_print_twenty_spaces() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({a: "b", b: "c"}, ["a", "b"], 20)"#,
);
let expected = forward(
&mut context,
r#"'{\n'
+' "a": "b",\n'
+' "b": "c"\n'
+'}'"#,
);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_pretty_print_with_number_object() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({a: "b", b: "c"}, undefined, new Number(10))"#,
);
let expected = forward(
&mut context,
r#"'{\n'
+' "a": "b",\n'
+' "b": "c"\n'
+'}'"#,
);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_pretty_print_bad_space_argument() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({a: "b", b: "c"}, ["a", "b"], [])"#,
);
let expected = forward(&mut context, r#"'{"a":"b","b":"c"}'"#);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_pretty_print_with_too_long_string() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({a: "b", b: "c"}, undefined, "abcdefghijklmn")"#,
);
let expected = forward(
&mut context,
r#"'{\n'
+'abcdefghij"a": "b",\n'
+'abcdefghij"b": "c"\n'
+'}'"#,
);
assert_eq!(actual, expected);
}
#[test]
fn json_stringify_pretty_print_with_string_object() {
let mut context = Context::default();
let actual = forward(
&mut context,
r#"JSON.stringify({a: "b", b: "c"}, undefined, new String("abcd"))"#,
);
let expected = forward(
&mut context,
r#"'{\n'
+'abcd"a": "b",\n'
+'abcd"b": "c"\n'
+'}'"#,
);
assert_eq!(actual, expected);
}
#[test]
fn json_parse_array_with_reviver() {
let mut context = Context::default();
let result = forward_val(
&mut context,
r#"JSON.parse('[1,2,3,4]', function(k, v){
if (typeof v == 'number') {
return v * 2;
} else {
return v;
}
})"#,
)
.unwrap();
assert_eq!(
result
.get_v("0", &mut context)
.unwrap()
.to_number(&mut context)
.unwrap() as u8,
2u8
);
assert_eq!(
result
.get_v("1", &mut context)
.unwrap()
.to_number(&mut context)
.unwrap() as u8,
4u8
);
assert_eq!(
result
.get_v("2", &mut context)
.unwrap()
.to_number(&mut context)
.unwrap() as u8,
6u8
);
assert_eq!(
result
.get_v("3", &mut context)
.unwrap()
.to_number(&mut context)
.unwrap() as u8,
8u8
);
}
#[test]
fn json_parse_object_with_reviver() {
let mut context = Context::default();
let result = forward(
&mut context,
r#"
var myObj = new Object();
myObj.firstname = "boa";
myObj.lastname = "snake";
var jsonString = JSON.stringify(myObj);
function dataReviver(key, value) {
if (key == 'lastname') {
return 'interpreter';
} else {
return value;
}
}
var jsonObj = JSON.parse(jsonString, dataReviver);
JSON.stringify(jsonObj);"#,
);
assert_eq!(result, r#""{"firstname":"boa","lastname":"interpreter"}""#);
}
#[test]
fn json_parse_sets_prototypes() {
let mut context = Context::default();
let init = r#"
const jsonString = "{\"ob\":{\"ject\":1},\"arr\": [0,1]}";
const jsonObj = JSON.parse(jsonString);
"#;
eprintln!("{}", forward(&mut context, init));
let object_prototype = forward_val(&mut context, r#"jsonObj.ob"#)
.unwrap()
.as_object()
.unwrap()
.prototype()
.clone();
let array_prototype = forward_val(&mut context, r#"jsonObj.arr"#)
.unwrap()
.as_object()
.unwrap()
.prototype()
.clone();
let global_object_prototype = context
.intrinsics()
.constructors()
.object()
.prototype()
.into();
let global_array_prototype = context
.intrinsics()
.constructors()
.array()
.prototype()
.into();
assert_eq!(object_prototype, global_object_prototype);
assert_eq!(array_prototype, global_array_prototype);
}
#[test]
fn json_fields_should_be_enumerable() {
let mut context = Context::default();
let actual_object = forward(
&mut context,
r#"
var a = JSON.parse('{"x":0}');
a.propertyIsEnumerable('x');
"#,
);
let actual_array_index = forward(
&mut context,
r#"
var b = JSON.parse('[0, 1]');
b.propertyIsEnumerable('0');
"#,
);
let expected = forward(&mut context, r#"true"#);
assert_eq!(actual_object, expected);
assert_eq!(actual_array_index, expected);
}
#[test]
fn json_parse_with_no_args_throws_syntax_error() {
let mut context = Context::default();
let result = forward(&mut context, "JSON.parse();");
assert!(result.contains("SyntaxError"));
}

View File

@@ -0,0 +1,144 @@
use super::ordered_map::MapLock;
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue},
object::{JsObject, ObjectData},
property::{PropertyDescriptor, PropertyNameKind},
symbol::WellKnownSymbols,
Context, JsResult,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
/// The Map Iterator object represents an iteration over a map. It implements the iterator protocol.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-array-iterator-objects
#[derive(Debug, Clone, Finalize, Trace)]
pub struct MapIterator {
iterated_map: Option<JsObject>,
map_next_index: usize,
map_iteration_kind: PropertyNameKind,
lock: MapLock,
}
impl MapIterator {
pub(crate) const NAME: &'static str = "MapIterator";
/// Abstract operation `CreateMapIterator( map, kind )`
///
/// Creates a new iterator over the given map.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createmapiterator
pub(crate) fn create_map_iterator(
map: &JsValue,
kind: PropertyNameKind,
context: &mut Context,
) -> JsResult<JsValue> {
if let Some(map_obj) = map.as_object() {
if let Some(map) = map_obj.borrow_mut().as_map_mut() {
let lock = map.lock(map_obj.clone());
let iter = Self {
iterated_map: Some(map_obj.clone()),
map_next_index: 0,
map_iteration_kind: kind,
lock,
};
let map_iterator = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.map_iterator(),
ObjectData::map_iterator(iter),
);
return Ok(map_iterator.into());
}
}
context.throw_type_error("`this` is not a Map")
}
/// %MapIteratorPrototype%.next( )
///
/// Advances the iterator and gets the next result in the map.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%mapiteratorprototype%.next
pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let mut map_iterator = this.as_object().map(JsObject::borrow_mut);
let map_iterator = map_iterator
.as_mut()
.and_then(|obj| obj.as_map_iterator_mut())
.ok_or_else(|| context.construct_type_error("`this` is not a MapIterator"))?;
let item_kind = map_iterator.map_iteration_kind;
if let Some(obj) = map_iterator.iterated_map.take() {
let e = {
let map = obj.borrow();
let entries = map.as_map_ref().expect("iterator should only iterate maps");
let len = entries.full_len();
loop {
let element = entries
.get_index(map_iterator.map_next_index)
.map(|(v, k)| (v.clone(), k.clone()));
map_iterator.map_next_index += 1;
if element.is_some() || map_iterator.map_next_index >= len {
break element;
}
}
};
if let Some((key, value)) = e {
let item = match item_kind {
PropertyNameKind::Key => Ok(create_iter_result_object(key, false, context)),
PropertyNameKind::Value => Ok(create_iter_result_object(value, false, context)),
PropertyNameKind::KeyAndValue => {
let result = Array::create_array_from_list([key, value], context);
Ok(create_iter_result_object(result.into(), false, context))
}
};
map_iterator.iterated_map = Some(obj);
return item;
}
}
Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
))
}
/// Create the `%MapIteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%mapiteratorprototype%-object
pub(crate) fn create_prototype(
iterator_prototype: JsObject,
context: &mut Context,
) -> JsObject {
let _timer = Profiler::global().start_event(Self::NAME, "init");
// Create prototype
let map_iterator =
JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary());
make_builtin_fn(Self::next, "next", &map_iterator, 0, context);
let to_string_tag = WellKnownSymbols::to_string_tag();
let to_string_tag_property = PropertyDescriptor::builder()
.value("Map Iterator")
.writable(false)
.enumerable(false)
.configurable(true);
map_iterator.insert(to_string_tag, to_string_tag_property);
map_iterator
}
}

View File

@@ -0,0 +1,591 @@
//! This module implements the global `Map` object.
//!
//! The JavaScript `Map` class is a global object that is used in the construction of maps; which
//! are high-level, key-value stores.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-map-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
#![allow(clippy::mutable_key_type)]
use self::{map_iterator::MapIterator, ordered_map::OrderedMap};
use super::JsArgs;
use crate::{
builtins::BuiltIn,
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder,
JsObject, ObjectData,
},
property::{Attribute, PropertyNameKind},
symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use num_traits::Zero;
use tap::{Conv, Pipe};
pub mod map_iterator;
pub mod ordered_map;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub(crate) struct Map(OrderedMap<JsValue>);
impl BuiltIn for Map {
const NAME: &'static str = "Map";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let get_species = FunctionBuilder::native(context, Self::get_species)
.name("get [Symbol.species]")
.constructor(false)
.build();
let get_size = FunctionBuilder::native(context, Self::get_size)
.name("get size")
.length(0)
.constructor(false)
.build();
let entries_function = FunctionBuilder::native(context, Self::entries)
.name("entries")
.length(0)
.constructor(false)
.build();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().map().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.static_accessor(
WellKnownSymbols::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.property(
"entries",
entries_function.clone(),
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
WellKnownSymbols::iterator(),
entries_function,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
WellKnownSymbols::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.method(Self::clear, "clear", 0)
.method(Self::delete, "delete", 1)
.method(Self::for_each, "forEach", 1)
.method(Self::get, "get", 1)
.method(Self::has, "has", 1)
.method(Self::keys, "keys", 0)
.method(Self::set, "set", 2)
.method(Self::values, "values", 0)
.accessor("size", Some(get_size), None, Attribute::CONFIGURABLE)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Map {
pub(crate) const LENGTH: usize = 0;
/// `Map ( [ iterable ] )`
///
/// Constructor for `Map` objects.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map-iterable
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return context
.throw_type_error("calling a builtin Map constructor without new is forbidden");
}
// 2. Let map be ? OrdinaryCreateFromConstructor(NewTarget, "%Map.prototype%", « [[MapData]] »).
// 3. Set map.[[MapData]] to a new empty List.
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::map, context)?;
let map = JsObject::from_proto_and_data(prototype, ObjectData::map(OrderedMap::new()));
// 4. If iterable is either undefined or null, return map.
let iterable = match args.get_or_undefined(0) {
val if !val.is_null_or_undefined() => val,
_ => return Ok(map.into()),
};
// 5. Let adder be ? Get(map, "set").
let adder = map.get("set", context)?;
// 6. Return ? AddEntriesFromIterable(map, iterable, adder).
add_entries_from_iterable(&map, iterable, &adder, context)
}
/// `get Map [ @@species ]`
///
/// The `Map [ @@species ]` accessor property returns the Map constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-map-@@species
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@species
#[allow(clippy::unnecessary_wraps)]
fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// 1. Return the this value.
Ok(this.clone())
}
/// `Map.prototype.entries()`
///
/// Returns a new Iterator object that contains the [key, value] pairs for each element in the Map object in insertion order.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.entries
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries
pub(crate) fn entries(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let M be the this value.
// 2. Return ? CreateMapIterator(M, key+value).
MapIterator::create_map_iterator(this, PropertyNameKind::KeyAndValue, context)
}
/// `Map.prototype.keys()`
///
/// Returns a new Iterator object that contains the keys for each element in the Map object in insertion order.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.keys
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys
pub(crate) fn keys(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let M be the this value.
// 2. Return ? CreateMapIterator(M, key).
MapIterator::create_map_iterator(this, PropertyNameKind::Key, context)
}
/// `Map.prototype.set( key, value )`
///
/// Inserts a new entry in the Map object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.set
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set
pub(crate) fn set(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let key = args.get_or_undefined(0);
let value = args.get_or_undefined(1);
// 1. Let M be the this value.
if let Some(object) = this.as_object() {
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
// 3. Let entries be the List that is M.[[MapData]].
if let Some(map) = object.borrow_mut().as_map_mut() {
let key = match key {
JsValue::Rational(r) => {
// 5. If key is -0𝔽, set key to +0𝔽.
if r.is_zero() {
JsValue::Rational(0f64)
} else {
key.clone()
}
}
_ => key.clone(),
};
// 4. For each Record { [[Key]], [[Value]] } p of entries, do
// a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then
// i. Set p.[[Value]] to value.
// 6. Let p be the Record { [[Key]]: key, [[Value]]: value }.
// 7. Append p as the last element of entries.
map.insert(key, value.clone());
// ii. Return M.
// 8. Return M.
return Ok(this.clone());
}
}
context.throw_type_error("'this' is not a Map")
}
/// `get Map.prototype.size`
///
/// Obtains the size of the map, filtering empty keys to ensure it updates
/// while iterating.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-map.prototype.size
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size
pub(crate) fn get_size(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let M be the this value.
if let Some(object) = this.as_object() {
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
// 3. Let entries be the List that is M.[[MapData]].
if let Some(map) = object.borrow_mut().as_map_mut() {
// 4. Let count be 0.
// 5. For each Record { [[Key]], [[Value]] } p of entries, do
// a. If p.[[Key]] is not empty, set count to count + 1.
// 6. Return 𝔽(count).
return Ok(map.len().into());
}
}
context.throw_type_error("'this' is not a Map")
}
/// `Map.prototype.delete( key )`
///
/// Removes the element associated with the key, if it exists.
/// Returns true if there was an element, and false otherwise.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.delete
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete
pub(crate) fn delete(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let key = args.get_or_undefined(0);
// 1. Let M be the this value.
if let Some(object) = this.as_object() {
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
// 3. Let entries be the List that is M.[[MapData]].
if let Some(map) = object.borrow_mut().as_map_mut() {
// a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then
// i. Set p.[[Key]] to empty.
// ii. Set p.[[Value]] to empty.
// iii. Return true.
// 5. Return false.
return Ok(map.remove(key).is_some().into());
}
}
context.throw_type_error("'this' is not a Map")
}
/// `Map.prototype.get( key )`
///
/// Returns the value associated with the key, or undefined if there is none.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.get
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get
pub(crate) fn get(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
const JS_ZERO: &JsValue = &JsValue::Rational(0f64);
let key = args.get_or_undefined(0);
let key = match key {
JsValue::Rational(r) => {
if r.is_zero() {
JS_ZERO
} else {
key
}
}
_ => key,
};
// 1. Let M be the this value.
if let JsValue::Object(ref object) = this {
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
// 3. Let entries be the List that is M.[[MapData]].
if let Some(map) = object.borrow().as_map_ref() {
// 4. For each Record { [[Key]], [[Value]] } p of entries, do
// a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return p.[[Value]].
// 5. Return undefined.
return Ok(map.get(key).cloned().unwrap_or_default());
}
}
context.throw_type_error("'this' is not a Map")
}
/// `Map.prototype.clear( )`
///
/// Removes all entries from the map.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.clear
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear
pub(crate) fn clear(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let M be the this value.
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
if let Some(object) = this.as_object() {
// 3. Let entries be the List that is M.[[MapData]].
if let Some(map) = object.borrow_mut().as_map_mut() {
// 4. For each Record { [[Key]], [[Value]] } p of entries, do
// a. Set p.[[Key]] to empty.
// b. Set p.[[Value]] to empty.
map.clear();
// 5. Return undefined.
return Ok(JsValue::undefined());
}
}
context.throw_type_error("'this' is not a Map")
}
/// `Map.prototype.has( key )`
///
/// Checks if the map contains an entry with the given key.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
pub(crate) fn has(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
const JS_ZERO: &JsValue = &JsValue::Rational(0f64);
let key = args.get_or_undefined(0);
let key = match key {
JsValue::Rational(r) => {
if r.is_zero() {
JS_ZERO
} else {
key
}
}
_ => key,
};
// 1. Let M be the this value.
if let JsValue::Object(ref object) = this {
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
// 3. Let entries be the List that is M.[[MapData]].
if let Some(map) = object.borrow().as_map_ref() {
// 4. For each Record { [[Key]], [[Value]] } p of entries, do
// a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return true.
// 5. Return false.
return Ok(map.contains_key(key).into());
}
}
context.throw_type_error("'this' is not a Map")
}
/// `Map.prototype.forEach( callbackFn [ , thisArg ] )`
///
/// Executes the provided callback function for each key-value pair in the map.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.foreach
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach
pub(crate) fn for_each(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let M be the this value.
// 2. Perform ? RequireInternalSlot(M, [[MapData]]).
let map = this
.as_object()
.filter(|obj| obj.is_map())
.ok_or_else(|| context.construct_type_error("`this` is not a Map"))?;
// 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
let callback = args.get_or_undefined(0);
let callback = callback.as_callable().ok_or_else(|| {
context.construct_type_error(format!("{} is not a function", callback.display()))
})?;
let this_arg = args.get_or_undefined(1);
// NOTE:
//
// forEach does not directly mutate the object on which it is called but
// the object may be mutated by the calls to callbackfn. Each entry of a
// map's [[MapData]] is only visited once. New keys added after the call
// to forEach begins are visited. A key will be revisited if it is deleted
// after it has been visited and then re-added before the forEach call completes.
// Keys that are deleted after the call to forEach begins and before being visited
// are not visited unless the key is added again before the forEach call completes.
let _lock = map
.borrow_mut()
.as_map_mut()
.expect("checked that `this` was a map")
.lock(map.clone());
// 4. Let entries be the List that is M.[[MapData]].
// 5. For each Record { [[Key]], [[Value]] } e of entries, do
let mut index = 0;
loop {
let arguments = {
let map = map.borrow();
let map = map.as_map_ref().expect("checked that `this` was a map");
if index < map.full_len() {
map.get_index(index)
.map(|(k, v)| [v.clone(), k.clone(), this.clone()])
} else {
// 6. Return undefined.
return Ok(JsValue::undefined());
}
};
// a. If e.[[Key]] is not empty, then
if let Some(arguments) = arguments {
// i. Perform ? Call(callbackfn, thisArg, « e.[[Value]], e.[[Key]], M »).
callback.call(this_arg, &arguments, context)?;
}
index += 1;
}
}
/// `Map.prototype.values()`
///
/// Returns a new Iterator object that contains the values for each element in the Map object in insertion order.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.values
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values
pub(crate) fn values(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let M be the this value.
// 2. Return ? CreateMapIterator(M, value).
MapIterator::create_map_iterator(this, PropertyNameKind::Value, context)
}
}
/// `AddEntriesFromIterable`
///
/// Allows adding entries to a map from any object that has a `@@Iterator` field.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-add-entries-from-iterable
pub(crate) fn add_entries_from_iterable(
target: &JsObject,
iterable: &JsValue,
adder: &JsValue,
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If IsCallable(adder) is false, throw a TypeError exception.
let adder = adder.as_callable().ok_or_else(|| {
context.construct_type_error("property `set` of `NewTarget` is not callable")
})?;
// 2. Let iteratorRecord be ? GetIterator(iterable).
let iterator_record = iterable.get_iterator(context, None, None)?;
// 3. Repeat,
loop {
// a. Let next be ? IteratorStep(iteratorRecord).
let next = iterator_record.step(context)?;
// b. If next is false, return target.
// c. Let nextItem be ? IteratorValue(next).
let next_item = if let Some(next) = next {
next.value(context)?
} else {
return Ok(target.clone().into());
};
let next_item = if let Some(obj) = next_item.as_object() {
obj
// d. If Type(nextItem) is not Object, then
} else {
// i. Let error be ThrowCompletion(a newly created TypeError object).
let err = context
.throw_type_error("cannot get key and value from primitive item of `iterable`");
// ii. Return ? IteratorClose(iteratorRecord, error).
return iterator_record.close(err, context);
};
// e. Let k be Get(nextItem, "0").
// f. IfAbruptCloseIterator(k, iteratorRecord).
let key = match next_item.get(0, context) {
Ok(val) => val,
err => return iterator_record.close(err, context),
};
// g. Let v be Get(nextItem, "1").
// h. IfAbruptCloseIterator(v, iteratorRecord).
let value = match next_item.get(1, context) {
Ok(val) => val,
err => return iterator_record.close(err, context),
};
// i. Let status be Call(adder, target, « k, v »).
let status = adder.call(&target.clone().into(), &[key, value], context);
// j. IfAbruptCloseIterator(status, iteratorRecord).
if status.is_err() {
return iterator_record.close(status, context);
}
}
}

View File

@@ -0,0 +1,225 @@
use crate::{object::JsObject, JsValue};
use boa_gc::{custom_trace, Finalize, Trace};
use indexmap::{Equivalent, IndexMap};
use std::{
collections::hash_map::RandomState,
fmt::Debug,
hash::{BuildHasher, Hash, Hasher},
};
#[derive(PartialEq, Eq, Clone, Debug)]
enum MapKey {
Key(JsValue),
Empty(usize), // Necessary to ensure empty keys are still unique.
}
// This ensures that a MapKey::Key(value) hashes to the same as value. The derived PartialEq implementation still holds.
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for MapKey {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
MapKey::Key(v) => v.hash(state),
MapKey::Empty(e) => e.hash(state),
}
}
}
impl Equivalent<MapKey> for JsValue {
fn equivalent(&self, key: &MapKey) -> bool {
match key {
MapKey::Key(v) => v == self,
MapKey::Empty(_) => false,
}
}
}
/// A structure wrapping `indexmap::IndexMap`.
#[derive(Clone)]
pub struct OrderedMap<V, S = RandomState> {
map: IndexMap<MapKey, Option<V>, S>,
lock: u32,
empty_count: usize,
}
impl<V: Trace, S: BuildHasher> Finalize for OrderedMap<V, S> {}
unsafe impl<V: Trace, S: BuildHasher> Trace for OrderedMap<V, S> {
custom_trace!(this, {
for (k, v) in this.map.iter() {
if let MapKey::Key(key) = k {
mark(key);
}
mark(v);
}
});
}
impl<V: Debug> Debug for OrderedMap<V> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
self.map.fmt(formatter)
}
}
impl<V> Default for OrderedMap<V> {
fn default() -> Self {
Self::new()
}
}
impl<V> OrderedMap<V> {
pub fn new() -> Self {
Self {
map: IndexMap::new(),
lock: 0,
empty_count: 0,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
map: IndexMap::with_capacity(capacity),
lock: 0,
empty_count: 0,
}
}
/// Return the number of key-value pairs in the map, including empty values.
///
/// Computes in **O(1)** time.
pub fn full_len(&self) -> usize {
self.map.len()
}
/// Gets the number of key-value pairs in the map, not including empty values.
///
/// Computes in **O(1)** time.
pub fn len(&self) -> usize {
self.map.len() - self.empty_count
}
/// Returns true if the map contains no elements.
///
/// Computes in **O(1)** time.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Insert a key-value pair in the map.
///
/// If an equivalent key already exists in the map: the key remains and
/// retains in its place in the order, its corresponding value is updated
/// with `value` and the older value is returned inside `Some(_)`.
///
/// If no equivalent key existed in the map: the new key-value pair is
/// inserted, last in order, and `None` is returned.
///
/// Computes in **O(1)** time (amortized average).
pub fn insert(&mut self, key: JsValue, value: V) -> Option<V> {
self.map.insert(MapKey::Key(key), Some(value)).flatten()
}
/// Remove the key-value pair equivalent to `key` and return
/// its value.
///
/// Like `Vec::remove`, the pair is removed by shifting all of the
/// elements that follow it, preserving their relative order.
/// **This perturbs the index of all of those elements!**
///
/// Return `None` if `key` is not in map.
///
/// Computes in **O(n)** time (average).
pub fn remove(&mut self, key: &JsValue) -> Option<V> {
if self.lock == 0 {
self.map.shift_remove(key).flatten()
} else if self.map.contains_key(key) {
self.map.insert(MapKey::Empty(self.empty_count), None);
self.empty_count += 1;
self.map.swap_remove(key).flatten()
} else {
None
}
}
/// Removes all elements from the map and resets the counter of
/// empty entries.
pub fn clear(&mut self) {
self.map.clear();
self.map.shrink_to_fit();
self.empty_count = 0;
}
/// Return a reference to the value stored for `key`, if it is present,
/// else `None`.
///
/// Computes in **O(1)** time (average).
pub fn get(&self, key: &JsValue) -> Option<&V> {
self.map.get(key).and_then(Option::as_ref)
}
/// Get a key-value pair by index.
///
/// Valid indices are `0 <= index < self.full_len()`.
///
/// Computes in O(1) time.
pub fn get_index(&self, index: usize) -> Option<(&JsValue, &V)> {
if let (MapKey::Key(key), Some(value)) = self.map.get_index(index)? {
Some((key, value))
} else {
None
}
}
/// Return an iterator over the key-value pairs of the map, in their order
pub fn iter(&self) -> impl Iterator<Item = (&JsValue, &V)> {
self.map.iter().filter_map(|o| {
if let (MapKey::Key(key), Some(value)) = o {
Some((key, value))
} else {
None
}
})
}
/// Return `true` if an equivalent to `key` exists in the map.
///
/// Computes in **O(1)** time (average).
pub fn contains_key(&self, key: &JsValue) -> bool {
self.map.contains_key(key)
}
/// Increases the lock counter and returns a lock object that will decrement the counter when dropped.
///
/// This allows objects to be removed from the map during iteration without affecting the indexes until the iteration has completed.
pub(crate) fn lock(&mut self, map: JsObject) -> MapLock {
self.lock += 1;
MapLock(map)
}
/// Decreases the lock counter and, if 0, removes all empty entries.
fn unlock(&mut self) {
self.lock -= 1;
if self.lock == 0 {
self.map.retain(|k, _| matches!(k, MapKey::Key(_)));
self.empty_count = 0;
}
}
}
/// Increases the lock count of the map for the lifetime of the guard. This should not be dropped until iteration has completed.
#[derive(Debug, Trace)]
pub(crate) struct MapLock(JsObject);
impl Clone for MapLock {
fn clone(&self) -> Self {
let mut map = self.0.borrow_mut();
let map = map.as_map_mut().expect("MapLock does not point to a map");
map.lock(self.0.clone())
}
}
impl Finalize for MapLock {
fn finalize(&self) {
let mut map = self.0.borrow_mut();
let map = map.as_map_mut().expect("MapLock does not point to a map");
map.unlock();
}
}

View File

@@ -0,0 +1,406 @@
use crate::{forward, Context};
#[test]
fn construct_empty() {
let mut context = Context::default();
let init = r#"
var empty = new Map();
"#;
forward(&mut context, init);
let result = forward(&mut context, "empty.size");
assert_eq!(result, "0");
}
#[test]
fn construct_from_array() {
let mut context = Context::default();
let init = r#"
let map = new Map([["1", "one"], ["2", "two"]]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "map.size");
assert_eq!(result, "2");
}
#[test]
fn clone() {
let mut context = Context::default();
let init = r#"
let original = new Map([["1", "one"], ["2", "two"]]);
let clone = new Map(original);
"#;
forward(&mut context, init);
let result = forward(&mut context, "clone.size");
assert_eq!(result, "2");
let result = forward(
&mut context,
r#"
original.set("3", "three");
original.size"#,
);
assert_eq!(result, "3");
let result = forward(&mut context, "clone.size");
assert_eq!(result, "2");
}
#[test]
fn symbol_iterator() {
let mut context = Context::default();
let init = r#"
const map1 = new Map();
map1.set('0', 'foo');
map1.set(1, 'bar');
const iterator = map1[Symbol.iterator]();
let item1 = iterator.next();
let item2 = iterator.next();
let item3 = iterator.next();
"#;
forward(&mut context, init);
let result = forward(&mut context, "item1.value.length");
assert_eq!(result, "2");
let result = forward(&mut context, "item1.value[0]");
assert_eq!(result, "\"0\"");
let result = forward(&mut context, "item1.value[1]");
assert_eq!(result, "\"foo\"");
let result = forward(&mut context, "item1.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item2.value.length");
assert_eq!(result, "2");
let result = forward(&mut context, "item2.value[0]");
assert_eq!(result, "1");
let result = forward(&mut context, "item2.value[1]");
assert_eq!(result, "\"bar\"");
let result = forward(&mut context, "item2.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item3.value");
assert_eq!(result, "undefined");
let result = forward(&mut context, "item3.done");
assert_eq!(result, "true");
}
// Should behave the same as symbol_iterator
#[test]
fn entries() {
let mut context = Context::default();
let init = r#"
const map1 = new Map();
map1.set('0', 'foo');
map1.set(1, 'bar');
const entriesIterator = map1.entries();
let item1 = entriesIterator.next();
let item2 = entriesIterator.next();
let item3 = entriesIterator.next();
"#;
forward(&mut context, init);
let result = forward(&mut context, "item1.value.length");
assert_eq!(result, "2");
let result = forward(&mut context, "item1.value[0]");
assert_eq!(result, "\"0\"");
let result = forward(&mut context, "item1.value[1]");
assert_eq!(result, "\"foo\"");
let result = forward(&mut context, "item1.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item2.value.length");
assert_eq!(result, "2");
let result = forward(&mut context, "item2.value[0]");
assert_eq!(result, "1");
let result = forward(&mut context, "item2.value[1]");
assert_eq!(result, "\"bar\"");
let result = forward(&mut context, "item2.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item3.value");
assert_eq!(result, "undefined");
let result = forward(&mut context, "item3.done");
assert_eq!(result, "true");
}
#[test]
fn merge() {
let mut context = Context::default();
let init = r#"
let first = new Map([["1", "one"], ["2", "two"]]);
let second = new Map([["2", "second two"], ["3", "three"]]);
let third = new Map([["4", "four"], ["5", "five"]]);
let merged1 = new Map([...first, ...second]);
let merged2 = new Map([...second, ...third]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "merged1.size");
assert_eq!(result, "3");
let result = forward(&mut context, "merged1.get('2')");
assert_eq!(result, "\"second two\"");
let result = forward(&mut context, "merged2.size");
assert_eq!(result, "4");
}
#[test]
fn get() {
let mut context = Context::default();
let init = r#"
let map = new Map([["1", "one"], ["2", "two"]]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "map.get('1')");
assert_eq!(result, "\"one\"");
let result = forward(&mut context, "map.get('2')");
assert_eq!(result, "\"two\"");
let result = forward(&mut context, "map.get('3')");
assert_eq!(result, "undefined");
let result = forward(&mut context, "map.get()");
assert_eq!(result, "undefined");
}
#[test]
fn set() {
let mut context = Context::default();
let init = r#"
let map = new Map();
"#;
forward(&mut context, init);
let result = forward(&mut context, "map.set()");
assert_eq!(result, "Map { undefined → undefined }");
let result = forward(&mut context, "map.set('1', 'one')");
assert_eq!(result, "Map { undefined → undefined, \"1\"\"one\" }");
let result = forward(&mut context, "map.set('2')");
assert_eq!(
result,
"Map { undefined → undefined, \"1\"\"one\", \"2\" → undefined }"
);
}
#[test]
fn clear() {
let mut context = Context::default();
let init = r#"
let map = new Map([["1", "one"], ["2", "two"]]);
map.clear();
"#;
forward(&mut context, init);
let result = forward(&mut context, "map.size");
assert_eq!(result, "0");
}
#[test]
fn delete() {
let mut context = Context::default();
let init = r#"
let map = new Map([["1", "one"], ["2", "two"]]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "map.delete('1')");
assert_eq!(result, "true");
let result = forward(&mut context, "map.size");
assert_eq!(result, "1");
let result = forward(&mut context, "map.delete('1')");
assert_eq!(result, "false");
}
#[test]
fn has() {
let mut context = Context::default();
let init = r#"
let map = new Map([["1", "one"]]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "map.has()");
assert_eq!(result, "false");
let result = forward(&mut context, "map.has('1')");
assert_eq!(result, "true");
let result = forward(&mut context, "map.has('2')");
assert_eq!(result, "false");
}
#[test]
fn keys() {
let mut context = Context::default();
let init = r#"
const map1 = new Map();
map1.set('0', 'foo');
map1.set(1, 'bar');
const keysIterator = map1.keys();
let item1 = keysIterator.next();
let item2 = keysIterator.next();
let item3 = keysIterator.next();
"#;
forward(&mut context, init);
let result = forward(&mut context, "item1.value");
assert_eq!(result, "\"0\"");
let result = forward(&mut context, "item1.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item2.value");
assert_eq!(result, "1");
let result = forward(&mut context, "item2.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item3.value");
assert_eq!(result, "undefined");
let result = forward(&mut context, "item3.done");
assert_eq!(result, "true");
}
#[test]
fn for_each() {
let mut context = Context::default();
let init = r#"
let map = new Map([[1, 5], [2, 10], [3, 15]]);
let valueSum = 0;
let keySum = 0;
let sizeSum = 0;
function callingCallback(value, key, map) {
valueSum += value;
keySum += key;
sizeSum += map.size;
}
map.forEach(callingCallback);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "valueSum"), "30");
assert_eq!(forward(&mut context, "keySum"), "6");
assert_eq!(forward(&mut context, "sizeSum"), "9");
}
#[test]
fn values() {
let mut context = Context::default();
let init = r#"
const map1 = new Map();
map1.set('0', 'foo');
map1.set(1, 'bar');
const valuesIterator = map1.values();
let item1 = valuesIterator.next();
let item2 = valuesIterator.next();
let item3 = valuesIterator.next();
"#;
forward(&mut context, init);
let result = forward(&mut context, "item1.value");
assert_eq!(result, "\"foo\"");
let result = forward(&mut context, "item1.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item2.value");
assert_eq!(result, "\"bar\"");
let result = forward(&mut context, "item2.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item3.value");
assert_eq!(result, "undefined");
let result = forward(&mut context, "item3.done");
assert_eq!(result, "true");
}
#[test]
fn modify_key() {
let mut context = Context::default();
let init = r#"
let obj = new Object();
let map = new Map([[obj, "one"]]);
obj.field = "Value";
"#;
forward(&mut context, init);
let result = forward(&mut context, "map.get(obj)");
assert_eq!(result, "\"one\"");
}
#[test]
fn order() {
let mut context = Context::default();
let init = r#"
let map = new Map([[1, "one"]]);
map.set(2, "two");
"#;
forward(&mut context, init);
let result = forward(&mut context, "map");
assert_eq!(result, "Map { 1 → \"one\", 2 → \"two\" }");
let result = forward(&mut context, "map.set(1, \"five\");map");
assert_eq!(result, "Map { 1 → \"five\", 2 → \"two\" }");
let result = forward(&mut context, "map.set();map");
assert_eq!(
result,
"Map { 1 → \"five\", 2 → \"two\", undefined → undefined }"
);
let result = forward(&mut context, "map.delete(2);map");
assert_eq!(result, "Map { 1 → \"five\", undefined → undefined }");
let result = forward(&mut context, "map.set(2, \"two\");map");
assert_eq!(
result,
"Map { 1 → \"five\", undefined → undefined, 2 → \"two\" }"
);
}
#[test]
fn recursive_display() {
let mut context = Context::default();
let init = r#"
let map = new Map();
let array = new Array([map]);
map.set("y", map);
"#;
forward(&mut context, init);
let result = forward(&mut context, "map");
assert_eq!(result, "Map { \"y\" → Map(1) }");
let result = forward(&mut context, "map.set(\"z\", array)");
assert_eq!(result, "Map { \"y\" → Map(2), \"z\" → Array(1) }");
}
#[test]
fn not_a_function() {
let mut context = Context::default();
let init = r"
try {
let map = Map()
} catch(e) {
e.toString()
}
";
assert_eq!(
forward(&mut context, init),
"\"TypeError: calling a builtin Map constructor without new is forbidden\""
);
}
#[test]
fn for_each_delete() {
let mut context = Context::default();
let init = r#"
let map = new Map([[0, "a"], [1, "b"], [2, "c"]]);
let result = [];
map.forEach(function(value, key) {
if (key === 0) {
map.delete(0);
map.set(3, "d");
}
result.push([key, value]);
})
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "result[0][0]"), "0");
assert_eq!(forward(&mut context, "result[0][1]"), "\"a\"");
assert_eq!(forward(&mut context, "result[1][0]"), "1");
assert_eq!(forward(&mut context, "result[1][1]"), "\"b\"");
assert_eq!(forward(&mut context, "result[2][0]"), "2");
assert_eq!(forward(&mut context, "result[2][1]"), "\"c\"");
assert_eq!(forward(&mut context, "result[3][0]"), "3");
assert_eq!(forward(&mut context, "result[3][1]"), "\"d\"");
}
#[test]
fn for_of_delete() {
let mut context = Context::default();
let init = r#"
let map = new Map([[0, "a"], [1, "b"], [2, "c"]]);
let result = [];
for (a of map) {
if (a[0] === 0) {
map.delete(0);
map.set(3, "d");
}
result.push([a[0], a[1]]);
}
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "result[0][0]"), "0");
assert_eq!(forward(&mut context, "result[0][1]"), "\"a\"");
assert_eq!(forward(&mut context, "result[1][0]"), "1");
assert_eq!(forward(&mut context, "result[1][1]"), "\"b\"");
assert_eq!(forward(&mut context, "result[2][0]"), "2");
assert_eq!(forward(&mut context, "result[2][1]"), "\"c\"");
assert_eq!(forward(&mut context, "result[3][0]"), "3");
assert_eq!(forward(&mut context, "result[3][1]"), "\"d\"");
}

View File

@@ -0,0 +1,889 @@
//! This module implements the global `Math` object.
//!
//! `Math` is a built-in object that has properties and methods for mathematical constants and functions. Its not a function object.
//!
//! `Math` works with the `Number` type. It doesn't work with `BigInt`.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-math-object
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math
use super::JsArgs;
use crate::{
builtins::BuiltIn, object::ObjectInitializer, property::Attribute, symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
#[cfg(test)]
mod tests;
/// Javascript `Math` object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Math;
impl BuiltIn for Math {
const NAME: &'static str = "Math";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
let string_tag = WellKnownSymbols::to_string_tag();
ObjectInitializer::new(context)
.property("E", std::f64::consts::E, attribute)
.property("LN10", std::f64::consts::LN_10, attribute)
.property("LN2", std::f64::consts::LN_2, attribute)
.property("LOG10E", std::f64::consts::LOG10_E, attribute)
.property("LOG2E", std::f64::consts::LOG2_E, attribute)
.property("PI", std::f64::consts::PI, attribute)
.property("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2, attribute)
.property("SQRT2", std::f64::consts::SQRT_2, attribute)
.function(Self::abs, "abs", 1)
.function(Self::acos, "acos", 1)
.function(Self::acosh, "acosh", 1)
.function(Self::asin, "asin", 1)
.function(Self::asinh, "asinh", 1)
.function(Self::atan, "atan", 1)
.function(Self::atanh, "atanh", 1)
.function(Self::atan2, "atan2", 2)
.function(Self::cbrt, "cbrt", 1)
.function(Self::ceil, "ceil", 1)
.function(Self::clz32, "clz32", 1)
.function(Self::cos, "cos", 1)
.function(Self::cosh, "cosh", 1)
.function(Self::exp, "exp", 1)
.function(Self::expm1, "expm1", 1)
.function(Self::floor, "floor", 1)
.function(Self::fround, "fround", 1)
.function(Self::hypot, "hypot", 2)
.function(Self::imul, "imul", 2)
.function(Self::log, "log", 1)
.function(Self::log1p, "log1p", 1)
.function(Self::log10, "log10", 1)
.function(Self::log2, "log2", 1)
.function(Self::max, "max", 2)
.function(Self::min, "min", 2)
.function(Self::pow, "pow", 2)
.function(Self::random, "random", 0)
.function(Self::round, "round", 1)
.function(Self::sign, "sign", 1)
.function(Self::sin, "sin", 1)
.function(Self::sinh, "sinh", 1)
.function(Self::sqrt, "sqrt", 1)
.function(Self::tan, "tan", 1)
.function(Self::tanh, "tanh", 1)
.function(Self::trunc, "trunc", 1)
.property(
string_tag,
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Math {
/// Get the absolute value of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.abs
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs
pub(crate) fn abs(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 3. If n is -0𝔽, return +0𝔽.
// 2. If n is NaN, return NaN.
// 4. If n is -∞𝔽, return +∞𝔽.
// 5. If n < +0𝔽, return -n.
// 6. Return n.
.abs()
.into())
}
/// Get the arccos of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.acos
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos
pub(crate) fn acos(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n > 1𝔽, or n < -1𝔽, return NaN.
// 3. If n is 1𝔽, return +0𝔽.
// 4. Return an implementation-approximated value representing the result of the inverse cosine of (n).
.acos()
.into())
}
/// Get the hyperbolic arccos of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.acosh
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh
pub(crate) fn acosh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 4. If n < 1𝔽, return NaN.
// 2. If n is NaN or n is +∞𝔽, return n.
// 3. If n is 1𝔽, return +0𝔽.
// 5. Return an implementation-approximated value representing the result of the inverse hyperbolic cosine of (n).
.acosh()
.into())
}
/// Get the arcsine of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.asin
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin
pub(crate) fn asin(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n.
// 3. If n > 1𝔽 or n < -1𝔽, return NaN.
// 4. Return an implementation-approximated value representing the result of the inverse sine of (n).
.asin()
.into())
}
/// Get the hyperbolic arcsine of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.asinh
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh
pub(crate) fn asinh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n.
// 3. Return an implementation-approximated value representing the result of the inverse hyperbolic sine of (n).
.asinh()
.into())
}
/// Get the arctangent of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.atan
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan
pub(crate) fn atan(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n.
// 3. If n is +∞𝔽, return an implementation-approximated value representing π / 2.
// 4. If n is -∞𝔽, return an implementation-approximated value representing -π / 2.
// 5. Return an implementation-approximated value representing the result of the inverse tangent of (n).
.atan()
.into())
}
/// Get the hyperbolic arctangent of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.atanh
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh
pub(crate) fn atanh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n.
// 3. If n > 1𝔽 or n < -1𝔽, return NaN.
// 4. If n is 1𝔽, return +∞𝔽.
// 5. If n is -1𝔽, return -∞𝔽.
// 6. Return an implementation-approximated value representing the result of the inverse hyperbolic tangent of (n).
.atanh()
.into())
}
/// Get the four quadrant arctangent of the quotient y / x.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.atan2
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2
pub(crate) fn atan2(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let ny be ? ToNumber(y).
let y = args.get_or_undefined(0).to_number(context)?;
// 2. Let nx be ? ToNumber(x).
let x = args.get_or_undefined(1).to_number(context)?;
// 4. If ny is +∞𝔽, then
// a. If nx is +∞𝔽, return an implementation-approximated value representing π / 4.
// b. If nx is -∞𝔽, return an implementation-approximated value representing 3π / 4.
// c. Return an implementation-approximated value representing π / 2.
// 5. If ny is -∞𝔽, then
// a. If nx is +∞𝔽, return an implementation-approximated value representing -π / 4.
// b. If nx is -∞𝔽, return an implementation-approximated value representing -3π / 4.
// c. Return an implementation-approximated value representing -π / 2.
// 6. If ny is +0𝔽, then
// a. If nx > +0𝔽 or nx is +0𝔽, return +0𝔽.
// b. Return an implementation-approximated value representing π.
// 7. If ny is -0𝔽, then
// a. If nx > +0𝔽 or nx is +0𝔽, return -0𝔽.
// b. Return an implementation-approximated value representing -π.
// 8. Assert: ny is finite and is neither +0𝔽 nor -0𝔽.
// 9. If ny > +0𝔽, then
// a. If nx is +∞𝔽, return +0𝔽.
// b. If nx is -∞𝔽, return an implementation-approximated value representing π.
// c. If nx is +0𝔽 or nx is -0𝔽, return an implementation-approximated value representing π / 2.
// 10. If ny < +0𝔽, then
// a. If nx is +∞𝔽, return -0𝔽.
// b. If nx is -∞𝔽, return an implementation-approximated value representing -π.
// c. If nx is +0𝔽 or nx is -0𝔽, return an implementation-approximated value representing -π / 2.
// 11. Assert: nx is finite and is neither +0𝔽 nor -0𝔽.
// 12. Return an implementation-approximated value representing the result of the inverse tangent of the quotient (ny) / (nx).
Ok(y.atan2(x).into())
}
/// Get the cubic root of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.cbrt
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt
pub(crate) fn cbrt(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n.
// 3. Return an implementation-approximated value representing the result of the cube root of (n).
.cbrt()
.into())
}
/// Get lowest integer above a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.ceil
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil
pub(crate) fn ceil(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n.
// 3. If n < +0𝔽 and n > -1𝔽, return -0𝔽.
// 4. If n is an integral Number, return n.
// 5. Return the smallest (closest to -∞) integral Number value that is not less than n.
.ceil()
.into())
}
/// Get the number of leading zeros in the 32 bit representation of a number
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.clz32
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
pub(crate) fn clz32(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToUint32(x).
.to_u32(context)?
// 2. Let p be the number of leading zero bits in the unsigned 32-bit binary representation of n.
// 3. Return 𝔽(p).
.leading_zeros()
.into())
}
/// Get the cosine of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.cos
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos
pub(crate) fn cos(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +∞𝔽, or n is -∞𝔽, return NaN.
// 3. If n is +0𝔽 or n is -0𝔽, return 1𝔽.
// 4. Return an implementation-approximated value representing the result of the cosine of (n).
.cos()
.into())
}
/// Get the hyperbolic cosine of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.cosh
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh
pub(crate) fn cosh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, return NaN.
// 3. If n is +∞𝔽 or n is -∞𝔽, return +∞𝔽.
// 4. If n is +0𝔽 or n is -0𝔽, return 1𝔽.
// 5. Return an implementation-approximated value representing the result of the hyperbolic cosine of (n).
.cosh()
.into())
}
/// Get the power to raise the natural logarithm to get the number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.exp
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp
pub(crate) fn exp(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN or n is +∞𝔽, return n.
// 3. If n is +0𝔽 or n is -0𝔽, return 1𝔽.
// 4. If n is -∞𝔽, return +0𝔽.
// 5. Return an implementation-approximated value representing the result of the exponential function of (n).
.exp()
.into())
}
/// The Math.expm1() function returns e^x - 1, where x is the argument, and e the base of
/// the natural logarithms. The result is computed in a way that is accurate even when the
/// value of x is close 0
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.expm1
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1
pub(crate) fn expm1(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, or n is +∞𝔽, return n.
// 3. If n is -∞𝔽, return -1𝔽.
// 4. Return an implementation-approximated value representing the result of subtracting 1 from the exponential function of (n).
.exp_m1()
.into())
}
/// Get the highest integer below a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.floor
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor
pub(crate) fn floor(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n.
// 3. If n < 1𝔽 and n > +0𝔽, return +0𝔽.
// 4. If n is an integral Number, return n.
// 5. Return the greatest (closest to +∞) integral Number value that is not greater than n.
.floor()
.into())
}
/// Get the nearest 32-bit single precision float representation of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.fround
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround
pub(crate) fn fround(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let n be ? ToNumber(x).
let x = args.get_or_undefined(0).to_number(context)?;
// 2. If n is NaN, return NaN.
// 3. If n is one of +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return n.
// 4. Let n32 be the result of converting n to a value in IEEE 754-2019 binary32 format using roundTiesToEven mode.
// 5. Let n64 be the result of converting n32 to a value in IEEE 754-2019 binary64 format.
// 6. Return the ECMAScript Number value corresponding to n64.
Ok(f64::from(x as f32).into())
}
/// Get an approximation of the square root of the sum of squares of all arguments.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.hypot
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot
pub(crate) fn hypot(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let coerced be a new empty List.
// 2. For each element arg of args, do
// a. Let n be ? ToNumber(arg).
// b. Append n to coerced.
// 3. For each element number of coerced, do
// 5. For each element number of coerced, do
let mut result = 0f64;
for arg in args {
let num = arg.to_number(context)?;
// a. If number is +∞𝔽 or number is -∞𝔽, return +∞𝔽.
// 4. Let onlyZero be true.
// a. If number is NaN, return NaN.
// b. If number is neither +0𝔽 nor -0𝔽, set onlyZero to false.
// 6. If onlyZero is true, return +0𝔽.
// 7. Return an implementation-approximated value representing the square root of the sum of squares of the mathematical values of the elements of coerced.
result = result.hypot(num);
}
Ok(result.into())
}
/// Get the result of the C-like 32-bit multiplication of the two parameters.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.imul
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul
pub(crate) fn imul(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let a be (? ToUint32(x)).
let x = args.get_or_undefined(0).to_u32(context)?;
// 2. Let b be (? ToUint32(y)).
let y = args.get_or_undefined(1).to_u32(context)?;
// 3. Let product be (a × b) modulo 2^32.
// 4. If product ≥ 2^31, return 𝔽(product - 2^32); otherwise return 𝔽(product).
Ok((x.wrapping_mul(y) as i32).into())
}
/// Get the natural logarithm of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.log
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log
pub(crate) fn log(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN or n is +∞𝔽, return n.
// 3. If n is 1𝔽, return +0𝔽.
// 4. If n is +0𝔽 or n is -0𝔽, return -∞𝔽.
// 5. If n < +0𝔽, return NaN.
// 6. Return an implementation-approximated value representing the result of the natural logarithm of (n).
.ln()
.into())
}
/// Get approximation to the natural logarithm of 1 + x.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.log1p
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p
pub(crate) fn log1p(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, or n is +∞𝔽, return n.
// 3. If n is -1𝔽, return -∞𝔽.
// 4. If n < -1𝔽, return NaN.
// 5. Return an implementation-approximated value representing the result of the natural logarithm of 1 + (n).
.ln_1p()
.into())
}
/// Get the base 10 logarithm of the number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.log10
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10
pub(crate) fn log10(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN or n is +∞𝔽, return n.
// 3. If n is 1𝔽, return +0𝔽.
// 4. If n is +0𝔽 or n is -0𝔽, return -∞𝔽.
// 5. If n < +0𝔽, return NaN.
// 6. Return an implementation-approximated value representing the result of the base 10 logarithm of (n).
.log10()
.into())
}
/// Get the base 2 logarithm of the number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.log2
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2
pub(crate) fn log2(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN or n is +∞𝔽, return n.
// 3. If n is 1𝔽, return +0𝔽.
// 4. If n is +0𝔽 or n is -0𝔽, return -∞𝔽.
// 5. If n < +0𝔽, return NaN.
// 6. Return an implementation-approximated value representing the result of the base 2 logarithm of (n).
.log2()
.into())
}
/// Get the maximum of several numbers.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.max
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max
pub(crate) fn max(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let coerced be a new empty List.
// 2. For each element arg of args, do
// b. Append n to coerced.
// 3. Let highest be -∞𝔽.
let mut highest = f64::NEG_INFINITY;
// 4. For each element number of coerced, do
for arg in args {
// a. Let n be ? ToNumber(arg).
let num = arg.to_number(context)?;
highest = if highest.is_nan() {
continue;
} else if num.is_nan() {
// a. If number is NaN, return NaN.
f64::NAN
} else {
match (highest, num) {
// b. When x and y are +0𝔽 -0𝔽, return +0𝔽.
(x, y) if x == 0f64 && y == 0f64 && x.signum() != y.signum() => 0f64,
// c. Otherwise, return the maximum value.
(x, y) => x.max(y),
}
};
}
// 5. Return highest.
Ok(highest.into())
}
/// Get the minimum of several numbers.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.min
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min
pub(crate) fn min(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let coerced be a new empty List.
// 2. For each element arg of args, do
// b. Append n to coerced.
// 3. Let lowest be +∞𝔽.
let mut lowest = f64::INFINITY;
// 4. For each element number of coerced, do
for arg in args {
// a. Let n be ? ToNumber(arg).
let num = arg.to_number(context)?;
lowest = if lowest.is_nan() {
continue;
} else if num.is_nan() {
// a. If number is NaN, return NaN.
f64::NAN
} else {
match (lowest, num) {
// b. When x and y are +0𝔽 -0𝔽, return -0𝔽.
(x, y) if x == 0f64 && y == 0f64 && x.signum() != y.signum() => -0f64,
// c. Otherwise, return the minimum value.
(x, y) => x.min(y),
}
};
}
// 5. Return lowest.
Ok(lowest.into())
}
/// Raise a number to a power.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.pow
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow
#[allow(clippy::float_cmp)]
pub(crate) fn pow(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Set base to ? ToNumber(base).
let x = args.get_or_undefined(0).to_number(context)?;
// 2. Set exponent to ? ToNumber(exponent).
let y = args.get_or_undefined(1).to_number(context)?;
// 3. If |x| = 1 and the exponent is infinite, return NaN.
if f64::abs(x) == 1f64 && y.is_infinite() {
return Ok(f64::NAN.into());
}
// 4. Return ! Number::exponentiate(base, exponent).
Ok(x.powf(y).into())
}
/// Generate a random floating-point number between `0` and `1`.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.random
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn random(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// NOTE: Each Math.random function created for distinct realms must produce a distinct sequence of values from successive calls.
Ok(rand::random::<f64>().into())
}
/// Round a number to the nearest integer.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.round
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
#[allow(clippy::float_cmp)]
pub(crate) fn round(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let num = args
.get_or_undefined(0)
//1. Let n be ? ToNumber(x).
.to_number(context)?;
//2. If n is NaN, +∞𝔽, -∞𝔽, or an integral Number, return n.
//3. If n < 0.5𝔽 and n > +0𝔽, return +0𝔽.
//4. If n < +0𝔽 and n ≥ -0.5𝔽, return -0𝔽.
//5. Return the integral Number closest to n, preferring the Number closer to +∞ in the case of a tie.
if num.fract() == -0.5 {
Ok(num.ceil().into())
} else {
Ok(num.round().into())
}
}
/// Get the sign of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.sign
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign
pub(crate) fn sign(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let n be ? ToNumber(x).
let n = args.get_or_undefined(0).to_number(context)?;
// 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n.
if n == 0f64 {
return Ok(n.into());
}
// 3. If n < +0𝔽, return -1𝔽.
// 4. Return 1𝔽.
Ok(n.signum().into())
}
/// Get the sine of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.sin
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin
pub(crate) fn sin(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n.
// 3. If n is +∞𝔽 or n is -∞𝔽, return NaN.
// 4. Return an implementation-approximated value representing the result of the sine of (n).
.sin()
.into())
}
/// Get the hyperbolic sine of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.sinh
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh
pub(crate) fn sinh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n.
// 3. Return an implementation-approximated value representing the result of the hyperbolic sine of (n).
.sinh()
.into())
}
/// Get the square root of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.sqrt
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt
pub(crate) fn sqrt(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, or n is +∞𝔽, return n.
// 3. If n < +0𝔽, return NaN.
// 4. Return an implementation-approximated value representing the result of the square root of (n).
.sqrt()
.into())
}
/// Get the tangent of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.tan
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan
pub(crate) fn tan(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n.
// 3. If n is +∞𝔽, or n is -∞𝔽, return NaN.
// 4. Return an implementation-approximated value representing the result of the tangent of (n).
.tan()
.into())
}
/// Get the hyperbolic tangent of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.tanh
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh
pub(crate) fn tanh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n.
// 3. If n is +∞𝔽, return 1𝔽.
// 4. If n is -∞𝔽, return -1𝔽.
// 5. Return an implementation-approximated value representing the result of the hyperbolic tangent of (n).
.tanh()
.into())
}
/// Get the integer part of a number.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-math.trunc
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc
pub(crate) fn trunc(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(args
.get_or_undefined(0)
// 1. Let n be ? ToNumber(x).
.to_number(context)?
// 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n.
// 3. If n < 1𝔽 and n > +0𝔽, return +0𝔽.
// 4. If n < +0𝔽 and n > -1𝔽, return -0𝔽.
// 5. Return the integral Number nearest n in the direction of +0𝔽.
.trunc()
.into())
}
}

View File

@@ -0,0 +1,729 @@
#![allow(clippy::float_cmp)]
use crate::{forward, forward_val, Context};
use std::f64;
#[test]
fn abs() {
let mut context = Context::default();
let init = r#"
var a = Math.abs(3 - 5);
var b = Math.abs(1.23456 - 7.89012);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 2.0);
assert_eq!(b.to_number(&mut context).unwrap(), 6.655_559_999_999_999_5);
}
#[test]
fn acos() {
let mut context = Context::default();
let init = r#"
var a = Math.acos(8 / 10);
var b = Math.acos(5 / 3);
var c = Math.acos(1);
var d = Math.acos(2);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward(&mut context, "b");
let c = forward_val(&mut context, "c").unwrap();
let d = forward(&mut context, "d");
assert_eq!(a.to_number(&mut context).unwrap(), 0.643_501_108_793_284_3);
assert_eq!(b, "NaN");
assert_eq!(c.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(d, "NaN");
}
#[test]
fn acosh() {
let mut context = Context::default();
let init = r#"
var a = Math.acosh(2);
var b = Math.acosh(-1);
var c = Math.acosh(0.5);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward(&mut context, "b");
let c = forward(&mut context, "c");
assert_eq!(a.to_number(&mut context).unwrap(), 1.316_957_896_924_816_6);
assert_eq!(b, "NaN");
assert_eq!(c, "NaN");
}
#[test]
fn asin() {
let mut context = Context::default();
let init = r#"
var a = Math.asin(6 / 10);
var b = Math.asin(5 / 3);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward(&mut context, "b");
assert_eq!(a.to_number(&mut context).unwrap(), 0.643_501_108_793_284_4);
assert_eq!(b, String::from("NaN"));
}
#[test]
fn asinh() {
let mut context = Context::default();
let init = r#"
var a = Math.asinh(1);
var b = Math.asinh(0);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 0.881_373_587_019_542_9);
assert_eq!(b.to_number(&mut context).unwrap(), 0_f64);
}
#[test]
fn atan() {
let mut context = Context::default();
let init = r#"
var a = Math.atan(1);
var b = Math.atan(0);
var c = Math.atan(-0);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), f64::consts::FRAC_PI_4);
assert_eq!(b.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(c.to_number(&mut context).unwrap(), f64::from(-0));
}
#[test]
fn atan2() {
let mut context = Context::default();
let init = r#"
var a = Math.atan2(90, 15);
var b = Math.atan2(15, 90);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 1.405_647_649_380_269_9);
assert_eq!(b.to_number(&mut context).unwrap(), 0.165_148_677_414_626_83);
}
#[test]
fn cbrt() {
let mut context = Context::default();
let init = r#"
var a = Math.cbrt(64);
var b = Math.cbrt(-1);
var c = Math.cbrt(1);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 4_f64);
assert_eq!(b.to_number(&mut context).unwrap(), -1_f64);
assert_eq!(c.to_number(&mut context).unwrap(), 1_f64);
}
#[test]
fn ceil() {
let mut context = Context::default();
let init = r#"
var a = Math.ceil(1.95);
var b = Math.ceil(4);
var c = Math.ceil(-7.004);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 2_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 4_f64);
assert_eq!(c.to_number(&mut context).unwrap(), -7_f64);
}
#[test]
#[allow(clippy::many_single_char_names)]
fn clz32() {
let mut context = Context::default();
let init = r#"
var a = Math.clz32();
var b = Math.clz32({});
var c = Math.clz32(-173);
var d = Math.clz32("1");
var e = Math.clz32(2147483647);
var f = Math.clz32(Infinity);
var g = Math.clz32(true);
var h = Math.clz32(0);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
let d = forward_val(&mut context, "d").unwrap();
let e = forward_val(&mut context, "e").unwrap();
let f = forward_val(&mut context, "f").unwrap();
let g = forward_val(&mut context, "g").unwrap();
let h = forward_val(&mut context, "h").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 32_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 32_f64);
assert_eq!(c.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(d.to_number(&mut context).unwrap(), 31_f64);
assert_eq!(e.to_number(&mut context).unwrap(), 1_f64);
assert_eq!(f.to_number(&mut context).unwrap(), 32_f64);
assert_eq!(g.to_number(&mut context).unwrap(), 31_f64);
assert_eq!(h.to_number(&mut context).unwrap(), 32_f64);
}
#[test]
fn cos() {
let mut context = Context::default();
let init = r#"
var a = Math.cos(0);
var b = Math.cos(1);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 1_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 0.540_302_305_868_139_8);
}
#[test]
fn cosh() {
let mut context = Context::default();
let init = r#"
var a = Math.cosh(0);
var b = Math.cosh(1);
var c = Math.cosh(-1);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 1_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 1.543_080_634_815_243_7);
assert_eq!(c.to_number(&mut context).unwrap(), 1.543_080_634_815_243_7);
}
#[test]
fn exp() {
let mut context = Context::default();
let init = r#"
var a = Math.exp(0);
var b = Math.exp(-1);
var c = Math.exp(2);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 1_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 0.367_879_441_171_442_33);
assert_eq!(c.to_number(&mut context).unwrap(), 7.389_056_098_930_65);
}
#[test]
#[allow(clippy::many_single_char_names)]
fn expm1() {
let mut context = Context::default();
let init = r#"
var a = Math.expm1();
var b = Math.expm1({});
var c = Math.expm1(1);
var d = Math.expm1(-1);
var e = Math.expm1(0);
var f = Math.expm1(2);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward(&mut context, "a");
let b = forward(&mut context, "b");
let c = forward_val(&mut context, "c").unwrap();
let d = forward_val(&mut context, "d").unwrap();
let e = forward_val(&mut context, "e").unwrap();
let f = forward_val(&mut context, "f").unwrap();
assert_eq!(a, String::from("NaN"));
assert_eq!(b, String::from("NaN"));
assert!(float_cmp::approx_eq!(
f64,
c.to_number(&mut context).unwrap(),
1.718_281_828_459_045
));
assert!(float_cmp::approx_eq!(
f64,
d.to_number(&mut context).unwrap(),
-0.632_120_558_828_557_7
));
assert!(float_cmp::approx_eq!(
f64,
e.to_number(&mut context).unwrap(),
0_f64
));
assert!(float_cmp::approx_eq!(
f64,
f.to_number(&mut context).unwrap(),
6.389_056_098_930_65
));
}
#[test]
fn floor() {
let mut context = Context::default();
let init = r#"
var a = Math.floor(1.95);
var b = Math.floor(-3.01);
var c = Math.floor(3.01);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 1_f64);
assert_eq!(b.to_number(&mut context).unwrap(), -4_f64);
assert_eq!(c.to_number(&mut context).unwrap(), 3_f64);
}
#[test]
#[allow(clippy::many_single_char_names)]
fn fround() {
let mut context = Context::default();
let init = r#"
var a = Math.fround(NaN);
var b = Math.fround(Infinity);
var c = Math.fround(5);
var d = Math.fround(5.5);
var e = Math.fround(5.05);
var f = Math.fround(-5.05);
var g = Math.fround();
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward(&mut context, "a");
let b = forward(&mut context, "b");
let c = forward_val(&mut context, "c").unwrap();
let d = forward_val(&mut context, "d").unwrap();
let e = forward_val(&mut context, "e").unwrap();
let f = forward_val(&mut context, "f").unwrap();
let g = forward(&mut context, "g");
assert_eq!(a, String::from("NaN"));
assert_eq!(b, String::from("Infinity"));
assert_eq!(c.to_number(&mut context).unwrap(), 5f64);
assert_eq!(d.to_number(&mut context).unwrap(), 5.5f64);
assert_eq!(e.to_number(&mut context).unwrap(), 5.050_000_190_734_863);
assert_eq!(f.to_number(&mut context).unwrap(), -5.050_000_190_734_863);
assert_eq!(g, String::from("NaN"));
}
#[test]
#[allow(clippy::many_single_char_names)]
fn hypot() {
let mut context = Context::default();
let init = r#"
var a = Math.hypot();
var b = Math.hypot(3, 4);
var c = Math.hypot(5, 12);
var d = Math.hypot(3, 4, -5);
var e = Math.hypot(4, [5], 6);
var f = Math.hypot(3, -Infinity);
var g = Math.hypot(12);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
let d = forward_val(&mut context, "d").unwrap();
let e = forward_val(&mut context, "e").unwrap();
let f = forward_val(&mut context, "f").unwrap();
let g = forward_val(&mut context, "g").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 0f64);
assert_eq!(b.to_number(&mut context).unwrap(), 5f64);
assert_eq!(c.to_number(&mut context).unwrap(), 13f64);
assert_eq!(d.to_number(&mut context).unwrap(), 7.071_067_811_865_475_5);
assert_eq!(e.to_number(&mut context).unwrap(), 8.774964387392123);
assert!(f.to_number(&mut context).unwrap().is_infinite());
assert_eq!(g.to_number(&mut context).unwrap(), 12f64);
}
#[test]
#[allow(clippy::many_single_char_names)]
fn imul() {
let mut context = Context::default();
let init = r#"
var a = Math.imul(3, 4);
var b = Math.imul(-5, 12);
var c = Math.imul(0xffffffff, 5);
var d = Math.imul(0xfffffffe, 5);
var e = Math.imul(12);
var f = Math.imul();
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
let d = forward_val(&mut context, "d").unwrap();
let e = forward_val(&mut context, "e").unwrap();
let f = forward_val(&mut context, "f").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 12f64);
assert_eq!(b.to_number(&mut context).unwrap(), -60f64);
assert_eq!(c.to_number(&mut context).unwrap(), -5f64);
assert_eq!(d.to_number(&mut context).unwrap(), -10f64);
assert_eq!(e.to_number(&mut context).unwrap(), 0f64);
assert_eq!(f.to_number(&mut context).unwrap(), 0f64);
}
#[test]
fn log() {
let mut context = Context::default();
let init = r#"
var a = Math.log(1);
var b = Math.log(10);
var c = Math.log(-1);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward(&mut context, "c");
assert_eq!(a.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(b.to_number(&mut context).unwrap(), f64::consts::LN_10);
assert_eq!(c, String::from("NaN"));
}
#[test]
#[allow(clippy::many_single_char_names)]
fn log1p() {
let mut context = Context::default();
let init = r#"
var a = Math.log1p(1);
var b = Math.log1p(0);
var c = Math.log1p(-0.9999999999999999);
var d = Math.log1p(-1);
var e = Math.log1p(-1.000000000000001);
var f = Math.log1p(-2);
var g = Math.log1p();
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
let d = forward(&mut context, "d");
let e = forward(&mut context, "e");
let f = forward(&mut context, "f");
let g = forward(&mut context, "g");
assert_eq!(a.to_number(&mut context).unwrap(), f64::consts::LN_2);
assert_eq!(b.to_number(&mut context).unwrap(), 0f64);
assert_eq!(c.to_number(&mut context).unwrap(), -36.736_800_569_677_1);
assert_eq!(d, "-Infinity");
assert_eq!(e, String::from("NaN"));
assert_eq!(f, String::from("NaN"));
assert_eq!(g, String::from("NaN"));
}
#[test]
fn log10() {
let mut context = Context::default();
let init = r#"
var a = Math.log10(2);
var b = Math.log10(1);
var c = Math.log10(-2);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward(&mut context, "c");
assert_eq!(a.to_number(&mut context).unwrap(), f64::consts::LOG10_2);
assert_eq!(b.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(c, String::from("NaN"));
}
#[test]
fn log2() {
let mut context = Context::default();
let init = r#"
var a = Math.log2(3);
var b = Math.log2(1);
var c = Math.log2(-2);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward(&mut context, "c");
assert_eq!(a.to_number(&mut context).unwrap(), 1.584_962_500_721_156);
assert_eq!(b.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(c, String::from("NaN"));
}
#[test]
fn max() {
let mut context = Context::default();
let init = r#"
var a = Math.max(10, 20);
var b = Math.max(-10, -20);
var c = Math.max(-10, 20);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 20_f64);
assert_eq!(b.to_number(&mut context).unwrap(), -10_f64);
assert_eq!(c.to_number(&mut context).unwrap(), 20_f64);
}
#[test]
fn min() {
let mut context = Context::default();
let init = r#"
var a = Math.min(10, 20);
var b = Math.min(-10, -20);
var c = Math.min(-10, 20);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 10_f64);
assert_eq!(b.to_number(&mut context).unwrap(), -20_f64);
assert_eq!(c.to_number(&mut context).unwrap(), -10_f64);
}
#[test]
fn pow() {
let mut context = Context::default();
let init = r#"
var a = Math.pow(2, 10);
var b = Math.pow(-7, 2);
var c = Math.pow(4, 0.5);
var d = Math.pow(7, -2);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
let d = forward_val(&mut context, "d").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 1_024_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 49_f64);
assert_eq!(c.to_number(&mut context).unwrap(), 2.0);
assert_eq!(d.to_number(&mut context).unwrap(), 0.020_408_163_265_306_12);
}
#[test]
fn round() {
let mut context = Context::default();
let init = r#"
var a = Math.round(20.5);
var b = Math.round(-20.3);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 21.0);
assert_eq!(b.to_number(&mut context).unwrap(), -20.0);
}
#[test]
fn sign() {
let mut context = Context::default();
let init = r#"
var a = Math.sign(3);
var b = Math.sign(-3);
var c = Math.sign(0);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 1_f64);
assert_eq!(b.to_number(&mut context).unwrap(), -1_f64);
assert_eq!(c.to_number(&mut context).unwrap(), 0_f64);
}
#[test]
fn sin() {
let mut context = Context::default();
let init = r#"
var a = Math.sin(0);
var b = Math.sin(1);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 0.841_470_984_807_896_5);
}
#[test]
fn sinh() {
let mut context = Context::default();
let init = r#"
var a = Math.sinh(0);
var b = Math.sinh(1);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 1.175_201_193_643_801_4);
}
#[test]
fn sqrt() {
let mut context = Context::default();
let init = r#"
var a = Math.sqrt(0);
var b = Math.sqrt(2);
var c = Math.sqrt(9);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
let c = forward_val(&mut context, "c").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(b.to_number(&mut context).unwrap(), f64::consts::SQRT_2);
assert_eq!(c.to_number(&mut context).unwrap(), 3_f64);
}
#[test]
fn tan() {
let mut context = Context::default();
let init = r#"
var a = Math.tan(1.1);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
assert!(float_cmp::approx_eq!(
f64,
a.to_number(&mut context).unwrap(),
1.964_759_657_248_652_5
));
}
#[test]
fn tanh() {
let mut context = Context::default();
let init = r#"
var a = Math.tanh(1);
var b = Math.tanh(0);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 0.761_594_155_955_764_9);
assert_eq!(b.to_number(&mut context).unwrap(), 0_f64);
}
#[test]
fn trunc() {
let mut context = Context::default();
let init = r#"
var a = Math.trunc(13.37);
var b = Math.trunc(0.123);
"#;
eprintln!("{}", forward(&mut context, init));
let a = forward_val(&mut context, "a").unwrap();
let b = forward_val(&mut context, "b").unwrap();
assert_eq!(a.to_number(&mut context).unwrap(), 13_f64);
assert_eq!(b.to_number(&mut context).unwrap(), 0_f64);
}

View File

@@ -0,0 +1,221 @@
//! Builtins live here, such as Object, String, Math, etc.
pub mod array;
pub mod array_buffer;
pub mod async_function;
pub mod bigint;
pub mod boolean;
pub mod dataview;
pub mod date;
pub mod error;
pub mod eval;
pub mod function;
pub mod generator;
pub mod generator_function;
pub mod global_this;
pub mod infinity;
pub mod iterable;
pub mod json;
pub mod map;
pub mod math;
pub mod nan;
pub mod number;
pub mod object;
pub mod promise;
pub mod proxy;
pub mod reflect;
pub mod regexp;
pub mod set;
pub mod string;
pub mod symbol;
pub mod typed_array;
pub mod undefined;
#[cfg(feature = "console")]
pub mod console;
#[cfg(feature = "intl")]
pub mod intl;
pub(crate) use self::{
array::{array_iterator::ArrayIterator, Array},
async_function::AsyncFunction,
bigint::BigInt,
boolean::Boolean,
dataview::DataView,
date::Date,
error::{
AggregateError, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError,
UriError,
},
eval::Eval,
function::BuiltInFunctionObject,
global_this::GlobalThis,
infinity::Infinity,
json::Json,
map::map_iterator::MapIterator,
map::Map,
math::Math,
nan::NaN,
number::Number,
object::for_in_iterator::ForInIterator,
object::Object as BuiltInObjectObject,
promise::Promise,
proxy::Proxy,
reflect::Reflect,
regexp::RegExp,
set::set_iterator::SetIterator,
set::Set,
string::String,
symbol::Symbol,
typed_array::{
BigInt64Array, BigUint64Array, Float32Array, Float64Array, Int16Array, Int32Array,
Int8Array, Uint16Array, Uint32Array, Uint8Array, Uint8ClampedArray,
},
undefined::Undefined,
};
use crate::{
builtins::{
array_buffer::ArrayBuffer, generator::Generator, generator_function::GeneratorFunction,
typed_array::TypedArray,
},
property::{Attribute, PropertyDescriptor},
Context, JsValue,
};
/// Trait representing a global built-in object such as `Math`, `Object` or
/// `String`.
///
/// This trait must be implemented for any global built-in accessible from
/// Javascript.
pub(crate) trait BuiltIn {
/// Binding name of the built-in inside the global object.
///
/// E.g. If you want access the properties of a `Complex` built-in
/// with the name `Cplx` you must assign `"Cplx"` to this constant,
/// making any property inside it accessible from Javascript as `Cplx.prop`
const NAME: &'static str;
/// Property attribute flags of the built-in.
/// Check [Attribute] for more information.
const ATTRIBUTE: Attribute = Attribute::WRITABLE
.union(Attribute::NON_ENUMERABLE)
.union(Attribute::CONFIGURABLE);
/// Initialization code for the built-in.
/// This is where the methods, properties, static methods and the constructor
/// of a built-in must be initialized to be accessible from Javascript.
///
/// # Note
///
/// A return value of `None` indicates that the value must not be added as
/// a global attribute for the global object.
fn init(context: &mut Context) -> Option<JsValue>;
}
/// Utility function that checks if a type implements `BuiltIn` before
/// initializing it as a global built-in.
#[inline]
fn init_builtin<B: BuiltIn>(context: &mut Context) {
if let Some(value) = B::init(context) {
let property = PropertyDescriptor::builder()
.value(value)
.writable(B::ATTRIBUTE.writable())
.enumerable(B::ATTRIBUTE.enumerable())
.configurable(B::ATTRIBUTE.configurable())
.build();
context
.global_bindings_mut()
.insert(B::NAME.into(), property);
}
}
/// Initializes built-in objects and functions
#[inline]
pub fn init(context: &mut Context) {
macro_rules! globals {
($( $builtin:ty ),*) => {
$(init_builtin::<$builtin>(context)
);*
}
}
globals! {
Undefined,
Infinity,
NaN,
GlobalThis,
BuiltInFunctionObject,
BuiltInObjectObject,
Math,
Json,
Array,
Proxy,
ArrayBuffer,
BigInt,
Boolean,
Date,
DataView,
Map,
Number,
Eval,
Set,
String,
RegExp,
TypedArray,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
BigInt64Array,
BigUint64Array,
Float32Array,
Float64Array,
Symbol,
Error,
RangeError,
ReferenceError,
TypeError,
SyntaxError,
EvalError,
UriError,
AggregateError,
Reflect,
Generator,
GeneratorFunction,
Promise,
AsyncFunction
};
#[cfg(feature = "intl")]
init_builtin::<intl::Intl>(context);
#[cfg(feature = "console")]
init_builtin::<console::Console>(context);
}
pub trait JsArgs {
/// Utility function to `get` a parameter from
/// a `[JsValue]` or default to `JsValue::Undefined`
/// if `get` returns `None`.
///
/// Call this if you are thinking of calling something similar to
/// `args.get(n).cloned().unwrap_or_default()` or
/// `args.get(n).unwrap_or(&undefined)`.
///
/// This returns a reference for efficiency, in case
/// you only need to call methods of `JsValue`, so
/// try to minimize calling `clone`.
fn get_or_undefined(&self, index: usize) -> &JsValue;
}
impl JsArgs for [JsValue] {
fn get_or_undefined(&self, index: usize) -> &JsValue {
const UNDEFINED: &JsValue = &JsValue::Undefined;
self.get(index).unwrap_or(UNDEFINED)
}
}

View File

@@ -0,0 +1,35 @@
//! This module implements the global `NaN` property.
//!
//! The global `NaN` is a property of the global object. In other words,
//! it is a variable in global scope.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-value-properties-of-the-global-object-nan
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN
#[cfg(test)]
mod tests;
use crate::{builtins::BuiltIn, property::Attribute, Context, JsValue};
use boa_profiler::Profiler;
/// JavaScript global `NaN` property.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct NaN;
impl BuiltIn for NaN {
const NAME: &'static str = "NaN";
const ATTRIBUTE: Attribute = Attribute::READONLY
.union(Attribute::NON_ENUMERABLE)
.union(Attribute::PERMANENT);
fn init(_: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
Some(f64::NAN.into())
}
}

View File

@@ -0,0 +1,10 @@
use crate::exec;
#[test]
fn nan_exists_on_global_object_and_evaluates_to_nan_value() {
let scenario = r#"
NaN;
"#;
assert_eq!(&exec(scenario), "NaN");
}

View File

@@ -0,0 +1,86 @@
/// Converts a 64-bit floating point number to an `i32` according to the [`ToInt32`][ToInt32] algorithm.
///
/// [ToInt32]: https://tc39.es/ecma262/#sec-toint32
#[inline]
#[allow(clippy::float_cmp)]
pub(crate) fn f64_to_int32(number: f64) -> i32 {
const SIGN_MASK: u64 = 0x8000000000000000;
const EXPONENT_MASK: u64 = 0x7FF0000000000000;
const SIGNIFICAND_MASK: u64 = 0x000FFFFFFFFFFFFF;
const HIDDEN_BIT: u64 = 0x0010000000000000;
const PHYSICAL_SIGNIFICAND_SIZE: i32 = 52; // Excludes the hidden bit.
const SIGNIFICAND_SIZE: i32 = 53;
const EXPONENT_BIAS: i32 = 0x3FF + PHYSICAL_SIGNIFICAND_SIZE;
const DENORMAL_EXPONENT: i32 = -EXPONENT_BIAS + 1;
#[inline]
fn is_denormal(number: f64) -> bool {
(number.to_bits() & EXPONENT_MASK) == 0
}
#[inline]
fn exponent(number: f64) -> i32 {
if is_denormal(number) {
return DENORMAL_EXPONENT;
}
let d64 = number.to_bits();
let biased_e = ((d64 & EXPONENT_MASK) >> PHYSICAL_SIGNIFICAND_SIZE) as i32;
biased_e - EXPONENT_BIAS
}
#[inline]
fn significand(number: f64) -> u64 {
let d64 = number.to_bits();
let significand = d64 & SIGNIFICAND_MASK;
if is_denormal(number) {
significand
} else {
significand + HIDDEN_BIT
}
}
#[inline]
fn sign(number: f64) -> i64 {
if (number.to_bits() & SIGN_MASK) == 0 {
1
} else {
-1
}
}
if number.is_finite() && number <= f64::from(i32::MAX) && number >= f64::from(i32::MIN) {
let i = number as i32;
if f64::from(i) == number {
return i;
}
}
let exponent = exponent(number);
let bits = if exponent < 0 {
if exponent <= -SIGNIFICAND_SIZE {
return 0;
}
significand(number) >> -exponent
} else {
if exponent > 31 {
return 0;
}
(significand(number) << exponent) & 0xFFFFFFFF
};
(sign(number) * (bits as i64)) as i32
}
/// Converts a 64-bit floating point number to an `u32` according to the [`ToUint32`][ToUint32] algorithm.
///
/// [ToUint32]: https://tc39.es/ecma262/#sec-touint32
#[inline]
pub(crate) fn f64_to_uint32(number: f64) -> u32 {
f64_to_int32(number) as u32
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,886 @@
#![allow(clippy::float_cmp)]
use crate::{builtins::Number, forward, forward_val, value::AbstractRelation, Context};
#[test]
fn integer_number_primitive_to_number_object() {
let mut context = Context::default();
let scenario = r#"
(100).toString() === "100"
"#;
assert_eq!(forward(&mut context, scenario), "true");
}
#[test]
fn call_number() {
let mut context = Context::default();
let init = r#"
var default_zero = Number();
var int_one = Number(1);
var float_two = Number(2.1);
var str_three = Number('3.2');
var bool_one = Number(true);
var bool_zero = Number(false);
var invalid_nan = Number("I am not a number");
var from_exp = Number("2.34e+2");
"#;
eprintln!("{}", forward(&mut context, init));
let default_zero = forward_val(&mut context, "default_zero").unwrap();
let int_one = forward_val(&mut context, "int_one").unwrap();
let float_two = forward_val(&mut context, "float_two").unwrap();
let str_three = forward_val(&mut context, "str_three").unwrap();
let bool_one = forward_val(&mut context, "bool_one").unwrap();
let bool_zero = forward_val(&mut context, "bool_zero").unwrap();
let invalid_nan = forward_val(&mut context, "invalid_nan").unwrap();
let from_exp = forward_val(&mut context, "from_exp").unwrap();
assert_eq!(default_zero.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(int_one.to_number(&mut context).unwrap(), 1_f64);
assert_eq!(float_two.to_number(&mut context).unwrap(), 2.1);
assert_eq!(str_three.to_number(&mut context).unwrap(), 3.2);
assert_eq!(bool_one.to_number(&mut context).unwrap(), 1_f64);
assert!(invalid_nan.to_number(&mut context).unwrap().is_nan());
assert_eq!(bool_zero.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(from_exp.to_number(&mut context).unwrap(), 234_f64);
}
#[test]
fn to_exponential() {
let mut context = Context::default();
let init = r#"
var default_exp = Number().toExponential();
var int_exp = Number(5).toExponential();
var float_exp = Number(1.234).toExponential();
var big_exp = Number(1234).toExponential();
var nan_exp = Number("I am also not a number").toExponential();
var noop_exp = Number("1.23e+2").toExponential();
"#;
eprintln!("{}", forward(&mut context, init));
let default_exp = forward(&mut context, "default_exp");
let int_exp = forward(&mut context, "int_exp");
let float_exp = forward(&mut context, "float_exp");
let big_exp = forward(&mut context, "big_exp");
let nan_exp = forward(&mut context, "nan_exp");
let noop_exp = forward(&mut context, "noop_exp");
assert_eq!(default_exp, "\"0e+0\"");
assert_eq!(int_exp, "\"5e+0\"");
assert_eq!(float_exp, "\"1.234e+0\"");
assert_eq!(big_exp, "\"1.234e+3\"");
assert_eq!(nan_exp, "\"NaN\"");
assert_eq!(noop_exp, "\"1.23e+2\"");
}
#[test]
fn to_fixed() {
let mut context = Context::default();
let init = r#"
var default_fixed = Number().toFixed();
var pos_fixed = Number("3.456e+4").toFixed();
var neg_fixed = Number("3.456e-4").toFixed();
var noop_fixed = Number(5).toFixed();
var nan_fixed = Number("I am not a number").toFixed();
"#;
eprintln!("{}", forward(&mut context, init));
let default_fixed = forward(&mut context, "default_fixed");
let pos_fixed = forward(&mut context, "pos_fixed");
let neg_fixed = forward(&mut context, "neg_fixed");
let noop_fixed = forward(&mut context, "noop_fixed");
let nan_fixed = forward(&mut context, "nan_fixed");
assert_eq!(default_fixed, "\"0\"");
assert_eq!(pos_fixed, "\"34560\"");
assert_eq!(neg_fixed, "\"0\"");
assert_eq!(noop_fixed, "\"5\"");
assert_eq!(nan_fixed, "\"NaN\"");
}
#[test]
fn to_locale_string() {
let mut context = Context::default();
let init = r#"
var default_locale = Number().toLocaleString();
var small_locale = Number(5).toLocaleString();
var big_locale = Number("345600").toLocaleString();
var neg_locale = Number(-25).toLocaleString();
"#;
// TODO: We don't actually do any locale checking here
// To honor the spec we should print numbers according to user locale.
eprintln!("{}", forward(&mut context, init));
let default_locale = forward(&mut context, "default_locale");
let small_locale = forward(&mut context, "small_locale");
let big_locale = forward(&mut context, "big_locale");
let neg_locale = forward(&mut context, "neg_locale");
assert_eq!(default_locale, "\"0\"");
assert_eq!(small_locale, "\"5\"");
assert_eq!(big_locale, "\"345600\"");
assert_eq!(neg_locale, "\"-25\"");
}
#[test]
fn to_precision() {
let mut context = Context::default();
let init = r#"
var infinity = (1/0).toPrecision(3);
var default_precision = Number().toPrecision();
var explicit_ud_precision = Number().toPrecision(undefined);
var low_precision = (123456789).toPrecision(1);
var more_precision = (123456789).toPrecision(4);
var exact_precision = (123456789).toPrecision(9);
var over_precision = (123456789).toPrecision(50);
var neg_precision = (-123456789).toPrecision(4);
var neg_exponent = (0.1).toPrecision(4);
var ieee754_limits = (1/3).toPrecision(60);
"#;
eprintln!("{}", forward(&mut context, init));
let infinity = forward(&mut context, "infinity");
let default_precision = forward(&mut context, "default_precision");
let explicit_ud_precision = forward(&mut context, "explicit_ud_precision");
let low_precision = forward(&mut context, "low_precision");
let more_precision = forward(&mut context, "more_precision");
let exact_precision = forward(&mut context, "exact_precision");
let over_precision = forward(&mut context, "over_precision");
let neg_precision = forward(&mut context, "neg_precision");
let neg_exponent = forward(&mut context, "neg_exponent");
let ieee754_limits = forward(&mut context, "ieee754_limits");
assert_eq!(infinity, String::from("\"Infinity\""));
assert_eq!(default_precision, String::from("\"0\""));
assert_eq!(explicit_ud_precision, String::from("\"0\""));
assert_eq!(low_precision, String::from("\"1e+8\""));
assert_eq!(more_precision, String::from("\"1.235e+8\""));
assert_eq!(exact_precision, String::from("\"123456789\""));
assert_eq!(neg_precision, String::from("\"-1.235e+8\""));
assert_eq!(
over_precision,
String::from("\"123456789.00000000000000000000000000000000000000000\"")
);
assert_eq!(neg_exponent, String::from("\"0.1000\""));
assert_eq!(
ieee754_limits,
String::from("\"0.333333333333333314829616256247390992939472198486328125000000\"")
);
let expected = "Uncaught \"RangeError\": \"precision must be an integer at least 1 and no greater than 100\"";
let range_error_1 = r#"(1).toPrecision(101);"#;
let range_error_2 = r#"(1).toPrecision(0);"#;
let range_error_3 = r#"(1).toPrecision(-2000);"#;
let range_error_4 = r#"(1).toPrecision('%');"#;
assert_eq!(forward(&mut context, range_error_1), expected);
assert_eq!(forward(&mut context, range_error_2), expected);
assert_eq!(forward(&mut context, range_error_3), expected);
assert_eq!(forward(&mut context, range_error_4), expected);
}
#[test]
fn to_string() {
let mut context = Context::default();
assert_eq!("\"NaN\"", &forward(&mut context, "Number(NaN).toString()"));
assert_eq!(
"\"Infinity\"",
&forward(&mut context, "Number(1/0).toString()")
);
assert_eq!(
"\"-Infinity\"",
&forward(&mut context, "Number(-1/0).toString()")
);
assert_eq!("\"0\"", &forward(&mut context, "Number(0).toString()"));
assert_eq!("\"9\"", &forward(&mut context, "Number(9).toString()"));
assert_eq!("\"90\"", &forward(&mut context, "Number(90).toString()"));
assert_eq!(
"\"90.12\"",
&forward(&mut context, "Number(90.12).toString()")
);
assert_eq!("\"0.1\"", &forward(&mut context, "Number(0.1).toString()"));
assert_eq!(
"\"0.01\"",
&forward(&mut context, "Number(0.01).toString()")
);
assert_eq!(
"\"0.0123\"",
&forward(&mut context, "Number(0.0123).toString()")
);
assert_eq!(
"\"0.00001\"",
&forward(&mut context, "Number(0.00001).toString()")
);
assert_eq!(
"\"0.000001\"",
&forward(&mut context, "Number(0.000001).toString()")
);
assert_eq!(
"\"NaN\"",
&forward(&mut context, "Number(NaN).toString(16)")
);
assert_eq!(
"\"Infinity\"",
&forward(&mut context, "Number(1/0).toString(16)")
);
assert_eq!(
"\"-Infinity\"",
&forward(&mut context, "Number(-1/0).toString(16)")
);
assert_eq!("\"0\"", &forward(&mut context, "Number(0).toString(16)"));
assert_eq!("\"9\"", &forward(&mut context, "Number(9).toString(16)"));
assert_eq!("\"5a\"", &forward(&mut context, "Number(90).toString(16)"));
assert_eq!(
"\"5a.1eb851eb852\"",
&forward(&mut context, "Number(90.12).toString(16)")
);
assert_eq!(
"\"0.1999999999999a\"",
&forward(&mut context, "Number(0.1).toString(16)")
);
assert_eq!(
"\"0.028f5c28f5c28f6\"",
&forward(&mut context, "Number(0.01).toString(16)")
);
assert_eq!(
"\"0.032617c1bda511a\"",
&forward(&mut context, "Number(0.0123).toString(16)")
);
assert_eq!(
"\"605f9f6dd18bc8000\"",
&forward(&mut context, "Number(111111111111111111111).toString(16)")
);
assert_eq!(
"\"3c3bc3a4a2f75c0000\"",
&forward(&mut context, "Number(1111111111111111111111).toString(16)")
);
assert_eq!(
"\"25a55a46e5da9a00000\"",
&forward(&mut context, "Number(11111111111111111111111).toString(16)")
);
assert_eq!(
"\"0.0000a7c5ac471b4788\"",
&forward(&mut context, "Number(0.00001).toString(16)")
);
assert_eq!(
"\"0.000010c6f7a0b5ed8d\"",
&forward(&mut context, "Number(0.000001).toString(16)")
);
assert_eq!(
"\"0.000001ad7f29abcaf48\"",
&forward(&mut context, "Number(0.0000001).toString(16)")
);
assert_eq!(
"\"0.000002036565348d256\"",
&forward(&mut context, "Number(0.00000012).toString(16)")
);
assert_eq!(
"\"0.0000021047ee22aa466\"",
&forward(&mut context, "Number(0.000000123).toString(16)")
);
assert_eq!(
"\"0.0000002af31dc4611874\"",
&forward(&mut context, "Number(0.00000001).toString(16)")
);
assert_eq!(
"\"0.000000338a23b87483be\"",
&forward(&mut context, "Number(0.000000012).toString(16)")
);
assert_eq!(
"\"0.00000034d3fe36aaa0a2\"",
&forward(&mut context, "Number(0.0000000123).toString(16)")
);
assert_eq!("\"0\"", &forward(&mut context, "Number(-0).toString(16)"));
assert_eq!("\"-9\"", &forward(&mut context, "Number(-9).toString(16)"));
assert_eq!(
"\"-5a\"",
&forward(&mut context, "Number(-90).toString(16)")
);
assert_eq!(
"\"-5a.1eb851eb852\"",
&forward(&mut context, "Number(-90.12).toString(16)")
);
assert_eq!(
"\"-0.1999999999999a\"",
&forward(&mut context, "Number(-0.1).toString(16)")
);
assert_eq!(
"\"-0.028f5c28f5c28f6\"",
&forward(&mut context, "Number(-0.01).toString(16)")
);
assert_eq!(
"\"-0.032617c1bda511a\"",
&forward(&mut context, "Number(-0.0123).toString(16)")
);
assert_eq!(
"\"-605f9f6dd18bc8000\"",
&forward(&mut context, "Number(-111111111111111111111).toString(16)")
);
assert_eq!(
"\"-3c3bc3a4a2f75c0000\"",
&forward(&mut context, "Number(-1111111111111111111111).toString(16)")
);
assert_eq!(
"\"-25a55a46e5da9a00000\"",
&forward(
&mut context,
"Number(-11111111111111111111111).toString(16)"
)
);
assert_eq!(
"\"-0.0000a7c5ac471b4788\"",
&forward(&mut context, "Number(-0.00001).toString(16)")
);
assert_eq!(
"\"-0.000010c6f7a0b5ed8d\"",
&forward(&mut context, "Number(-0.000001).toString(16)")
);
assert_eq!(
"\"-0.000001ad7f29abcaf48\"",
&forward(&mut context, "Number(-0.0000001).toString(16)")
);
assert_eq!(
"\"-0.000002036565348d256\"",
&forward(&mut context, "Number(-0.00000012).toString(16)")
);
assert_eq!(
"\"-0.0000021047ee22aa466\"",
&forward(&mut context, "Number(-0.000000123).toString(16)")
);
assert_eq!(
"\"-0.0000002af31dc4611874\"",
&forward(&mut context, "Number(-0.00000001).toString(16)")
);
assert_eq!(
"\"-0.000000338a23b87483be\"",
&forward(&mut context, "Number(-0.000000012).toString(16)")
);
assert_eq!(
"\"-0.00000034d3fe36aaa0a2\"",
&forward(&mut context, "Number(-0.0000000123).toString(16)")
);
}
#[test]
fn num_to_string_exponential() {
let mut context = Context::default();
assert_eq!("\"0\"", forward(&mut context, "(0).toString()"));
assert_eq!("\"0\"", forward(&mut context, "(-0).toString()"));
assert_eq!(
"\"111111111111111110000\"",
forward(&mut context, "(111111111111111111111).toString()")
);
assert_eq!(
"\"1.1111111111111111e+21\"",
forward(&mut context, "(1111111111111111111111).toString()")
);
assert_eq!(
"\"1.1111111111111111e+22\"",
forward(&mut context, "(11111111111111111111111).toString()")
);
assert_eq!("\"1e-7\"", forward(&mut context, "(0.0000001).toString()"));
assert_eq!(
"\"1.2e-7\"",
forward(&mut context, "(0.00000012).toString()")
);
assert_eq!(
"\"1.23e-7\"",
forward(&mut context, "(0.000000123).toString()")
);
assert_eq!("\"1e-8\"", forward(&mut context, "(0.00000001).toString()"));
assert_eq!(
"\"1.2e-8\"",
forward(&mut context, "(0.000000012).toString()")
);
assert_eq!(
"\"1.23e-8\"",
forward(&mut context, "(0.0000000123).toString()")
);
}
#[test]
fn value_of() {
let mut context = Context::default();
// TODO: In addition to parsing numbers from strings, parse them bare As of October 2019
// the parser does not understand scientific e.g., Xe+Y or -Xe-Y notation.
let init = r#"
var default_val = Number().valueOf();
var int_val = Number("123").valueOf();
var float_val = Number(1.234).valueOf();
var exp_val = Number("1.2e+4").valueOf()
var neg_val = Number("-1.2e+4").valueOf()
"#;
eprintln!("{}", forward(&mut context, init));
let default_val = forward_val(&mut context, "default_val").unwrap();
let int_val = forward_val(&mut context, "int_val").unwrap();
let float_val = forward_val(&mut context, "float_val").unwrap();
let exp_val = forward_val(&mut context, "exp_val").unwrap();
let neg_val = forward_val(&mut context, "neg_val").unwrap();
assert_eq!(default_val.to_number(&mut context).unwrap(), 0_f64);
assert_eq!(int_val.to_number(&mut context).unwrap(), 123_f64);
assert_eq!(float_val.to_number(&mut context).unwrap(), 1.234);
assert_eq!(exp_val.to_number(&mut context).unwrap(), 12_000_f64);
assert_eq!(neg_val.to_number(&mut context).unwrap(), -12_000_f64);
}
#[test]
fn equal() {
assert!(Number::equal(0.0, 0.0));
assert!(Number::equal(-0.0, 0.0));
assert!(Number::equal(0.0, -0.0));
assert!(!Number::equal(f64::NAN, -0.0));
assert!(!Number::equal(0.0, f64::NAN));
assert!(Number::equal(1.0, 1.0));
}
#[test]
fn same_value() {
assert!(Number::same_value(0.0, 0.0));
assert!(!Number::same_value(-0.0, 0.0));
assert!(!Number::same_value(0.0, -0.0));
assert!(!Number::same_value(f64::NAN, -0.0));
assert!(!Number::same_value(0.0, f64::NAN));
assert!(Number::equal(1.0, 1.0));
}
#[test]
fn less_than() {
assert_eq!(
Number::less_than(f64::NAN, 0.0),
AbstractRelation::Undefined
);
assert_eq!(
Number::less_than(0.0, f64::NAN),
AbstractRelation::Undefined
);
assert_eq!(
Number::less_than(f64::NEG_INFINITY, 0.0),
AbstractRelation::True
);
assert_eq!(
Number::less_than(0.0, f64::NEG_INFINITY),
AbstractRelation::False
);
assert_eq!(
Number::less_than(f64::INFINITY, 0.0),
AbstractRelation::False
);
assert_eq!(
Number::less_than(0.0, f64::INFINITY),
AbstractRelation::True
);
}
#[test]
fn same_value_zero() {
assert!(Number::same_value_zero(0.0, 0.0));
assert!(Number::same_value_zero(-0.0, 0.0));
assert!(Number::same_value_zero(0.0, -0.0));
assert!(!Number::same_value_zero(f64::NAN, -0.0));
assert!(!Number::same_value_zero(0.0, f64::NAN));
assert!(Number::equal(1.0, 1.0));
}
#[test]
fn from_bigint() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "Number(0n)"), "0",);
assert_eq!(&forward(&mut context, "Number(100000n)"), "100000",);
assert_eq!(&forward(&mut context, "Number(100000n)"), "100000",);
assert_eq!(&forward(&mut context, "Number(1n << 1240n)"), "Infinity",);
}
#[test]
fn number_constants() {
let mut context = Context::default();
assert!(!forward_val(&mut context, "Number.EPSILON")
.unwrap()
.is_null_or_undefined());
assert!(!forward_val(&mut context, "Number.MAX_SAFE_INTEGER")
.unwrap()
.is_null_or_undefined());
assert!(!forward_val(&mut context, "Number.MIN_SAFE_INTEGER")
.unwrap()
.is_null_or_undefined());
assert!(!forward_val(&mut context, "Number.MAX_VALUE")
.unwrap()
.is_null_or_undefined());
assert!(!forward_val(&mut context, "Number.MIN_VALUE")
.unwrap()
.is_null_or_undefined());
assert!(!forward_val(&mut context, "Number.NEGATIVE_INFINITY")
.unwrap()
.is_null_or_undefined());
assert!(!forward_val(&mut context, "Number.POSITIVE_INFINITY")
.unwrap()
.is_null_or_undefined());
}
#[test]
fn parse_int_simple() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(\"6\")"), "6");
}
#[test]
fn parse_int_negative() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(\"-9\")"), "-9");
}
#[test]
fn parse_int_already_int() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(100)"), "100");
}
#[test]
fn parse_int_float() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(100.5)"), "100");
}
#[test]
fn parse_int_float_str() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(\"100.5\")"), "100");
}
#[test]
fn parse_int_inferred_hex() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(\"0xA\")"), "10");
}
/// This test demonstrates that this version of parseInt treats strings starting with 0 to be parsed with
/// a radix 10 if no radix is specified. Some alternative implementations default to a radix of 8.
#[test]
fn parse_int_zero_start() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(\"018\")"), "18");
}
#[test]
fn parse_int_varying_radix() {
let mut context = Context::default();
let base_str = "1000";
for radix in 2..36 {
let expected = i32::from_str_radix(base_str, radix).unwrap();
assert_eq!(
forward(&mut context, &format!("parseInt(\"{base_str}\", {radix} )")),
expected.to_string()
);
}
}
#[test]
fn parse_int_negative_varying_radix() {
let mut context = Context::default();
let base_str = "-1000";
for radix in 2..36 {
let expected = i32::from_str_radix(base_str, radix).unwrap();
assert_eq!(
forward(&mut context, &format!("parseInt(\"{base_str}\", {radix} )")),
expected.to_string()
);
}
}
#[test]
fn parse_int_malformed_str() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(\"hello\")"), "NaN");
}
#[test]
fn parse_int_undefined() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(undefined)"), "NaN");
}
/// Shows that no arguments to parseInt is treated the same as if undefined was
/// passed as the first argument.
#[test]
fn parse_int_no_args() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt()"), "NaN");
}
/// Shows that extra arguments to parseInt are ignored.
#[test]
fn parse_int_too_many_args() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseInt(\"100\", 10, 10)"), "100");
}
#[test]
fn parse_float_simple() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(\"6.5\")"), "6.5");
}
#[test]
fn parse_float_int() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(10)"), "10");
}
#[test]
fn parse_float_int_str() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(\"8\")"), "8");
}
#[test]
fn parse_float_already_float() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(17.5)"), "17.5");
}
#[test]
fn parse_float_negative() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(\"-99.7\")"), "-99.7");
}
#[test]
fn parse_float_malformed_str() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(\"hello\")"), "NaN");
}
#[test]
fn parse_float_undefined() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(undefined)"), "NaN");
}
/// No arguments to parseFloat is treated the same as passing undefined as the first argument.
#[test]
fn parse_float_no_args() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat()"), "NaN");
}
/// Shows that the parseFloat function ignores extra arguments.
#[test]
fn parse_float_too_many_args() {
let mut context = Context::default();
assert_eq!(&forward(&mut context, "parseFloat(\"100.5\", 10)"), "100.5");
}
#[test]
fn global_is_finite() {
let mut context = Context::default();
assert_eq!("false", &forward(&mut context, "isFinite(Infinity)"));
assert_eq!("false", &forward(&mut context, "isFinite(NaN)"));
assert_eq!("false", &forward(&mut context, "isFinite(-Infinity)"));
assert_eq!("true", &forward(&mut context, "isFinite(0)"));
assert_eq!("true", &forward(&mut context, "isFinite(2e64)"));
assert_eq!("true", &forward(&mut context, "isFinite(910)"));
assert_eq!("true", &forward(&mut context, "isFinite(null)"));
assert_eq!("true", &forward(&mut context, "isFinite('0')"));
assert_eq!("false", &forward(&mut context, "isFinite()"));
}
#[test]
fn global_is_nan() {
let mut context = Context::default();
assert_eq!("true", &forward(&mut context, "isNaN(NaN)"));
assert_eq!("true", &forward(&mut context, "isNaN('NaN')"));
assert_eq!("true", &forward(&mut context, "isNaN(undefined)"));
assert_eq!("true", &forward(&mut context, "isNaN({})"));
assert_eq!("false", &forward(&mut context, "isNaN(true)"));
assert_eq!("false", &forward(&mut context, "isNaN(null)"));
assert_eq!("false", &forward(&mut context, "isNaN(37)"));
assert_eq!("false", &forward(&mut context, "isNaN('37')"));
assert_eq!("false", &forward(&mut context, "isNaN('37.37')"));
assert_eq!("true", &forward(&mut context, "isNaN('37,5')"));
assert_eq!("true", &forward(&mut context, "isNaN('123ABC')"));
// Incorrect due to ToNumber implementation inconsistencies.
//assert_eq!("false", &forward(&mut context, "isNaN('')"));
//assert_eq!("false", &forward(&mut context, "isNaN(' ')"));
assert_eq!("true", &forward(&mut context, "isNaN('blabla')"));
}
#[test]
fn number_is_finite() {
let mut context = Context::default();
assert_eq!("false", &forward(&mut context, "Number.isFinite(Infinity)"));
assert_eq!("false", &forward(&mut context, "Number.isFinite(NaN)"));
assert_eq!(
"false",
&forward(&mut context, "Number.isFinite(-Infinity)")
);
assert_eq!("true", &forward(&mut context, "Number.isFinite(0)"));
assert_eq!("true", &forward(&mut context, "Number.isFinite(2e64)"));
assert_eq!("true", &forward(&mut context, "Number.isFinite(910)"));
assert_eq!("false", &forward(&mut context, "Number.isFinite(null)"));
assert_eq!("false", &forward(&mut context, "Number.isFinite('0')"));
assert_eq!("false", &forward(&mut context, "Number.isFinite()"));
assert_eq!("false", &forward(&mut context, "Number.isFinite({})"));
assert_eq!("true", &forward(&mut context, "Number.isFinite(Number(5))"));
assert_eq!(
"false",
&forward(&mut context, "Number.isFinite(new Number(5))")
);
assert_eq!(
"false",
&forward(&mut context, "Number.isFinite(new Number(NaN))")
);
assert_eq!(
"false",
&forward(&mut context, "Number.isFinite(BigInt(5))")
);
}
#[test]
fn number_is_integer() {
let mut context = Context::default();
assert_eq!("true", &forward(&mut context, "Number.isInteger(0)"));
assert_eq!("true", &forward(&mut context, "Number.isInteger(1)"));
assert_eq!("true", &forward(&mut context, "Number.isInteger(-100000)"));
assert_eq!(
"true",
&forward(&mut context, "Number.isInteger(99999999999999999999999)")
);
assert_eq!("false", &forward(&mut context, "Number.isInteger(0.1)"));
assert_eq!("false", &forward(&mut context, "Number.isInteger(Math.PI)"));
assert_eq!("false", &forward(&mut context, "Number.isInteger(NaN)"));
assert_eq!(
"false",
&forward(&mut context, "Number.isInteger(Infinity)")
);
assert_eq!(
"false",
&forward(&mut context, "Number.isInteger(-Infinity)")
);
assert_eq!("false", &forward(&mut context, "Number.isInteger('10')"));
assert_eq!("false", &forward(&mut context, "Number.isInteger(true)"));
assert_eq!("false", &forward(&mut context, "Number.isInteger(false)"));
assert_eq!("false", &forward(&mut context, "Number.isInteger([1])"));
assert_eq!("true", &forward(&mut context, "Number.isInteger(5.0)"));
assert_eq!(
"false",
&forward(&mut context, "Number.isInteger(5.000000000000001)")
);
assert_eq!(
"true",
&forward(&mut context, "Number.isInteger(5.0000000000000001)")
);
assert_eq!(
"false",
&forward(&mut context, "Number.isInteger(Number(5.000000000000001))")
);
assert_eq!(
"true",
&forward(&mut context, "Number.isInteger(Number(5.0000000000000001))")
);
assert_eq!("false", &forward(&mut context, "Number.isInteger()"));
assert_eq!(
"false",
&forward(&mut context, "Number.isInteger(new Number(5))")
);
}
#[test]
fn number_is_nan() {
let mut context = Context::default();
assert_eq!("true", &forward(&mut context, "Number.isNaN(NaN)"));
assert_eq!("true", &forward(&mut context, "Number.isNaN(Number.NaN)"));
assert_eq!("true", &forward(&mut context, "Number.isNaN(0 / 0)"));
assert_eq!("false", &forward(&mut context, "Number.isNaN(undefined)"));
assert_eq!("false", &forward(&mut context, "Number.isNaN({})"));
assert_eq!("false", &forward(&mut context, "Number.isNaN(true)"));
assert_eq!("false", &forward(&mut context, "Number.isNaN(null)"));
assert_eq!("false", &forward(&mut context, "Number.isNaN(37)"));
assert_eq!("false", &forward(&mut context, "Number.isNaN('37')"));
assert_eq!("false", &forward(&mut context, "Number.isNaN('37.37')"));
assert_eq!("false", &forward(&mut context, "Number.isNaN('37,5')"));
assert_eq!("false", &forward(&mut context, "Number.isNaN('123ABC')"));
// Incorrect due to ToNumber implementation inconsistencies.
//assert_eq!("false", &forward(&mut context, "Number.isNaN('')"));
//assert_eq!("false", &forward(&mut context, "Number.isNaN(' ')"));
assert_eq!("false", &forward(&mut context, "Number.isNaN('blabla')"));
assert_eq!("false", &forward(&mut context, "Number.isNaN(Number(5))"));
assert_eq!("true", &forward(&mut context, "Number.isNaN(Number(NaN))"));
assert_eq!("false", &forward(&mut context, "Number.isNaN(BigInt(5))"));
assert_eq!(
"false",
&forward(&mut context, "Number.isNaN(new Number(5))")
);
assert_eq!(
"false",
&forward(&mut context, "Number.isNaN(new Number(NaN))")
);
}
#[test]
fn number_is_safe_integer() {
let mut context = Context::default();
assert_eq!("true", &forward(&mut context, "Number.isSafeInteger(3)"));
assert_eq!(
"false",
&forward(&mut context, "Number.isSafeInteger(Math.pow(2, 53))")
);
assert_eq!(
"true",
&forward(&mut context, "Number.isSafeInteger(Math.pow(2, 53) - 1)")
);
assert_eq!("false", &forward(&mut context, "Number.isSafeInteger(NaN)"));
assert_eq!(
"false",
&forward(&mut context, "Number.isSafeInteger(Infinity)")
);
assert_eq!("false", &forward(&mut context, "Number.isSafeInteger('3')"));
assert_eq!("false", &forward(&mut context, "Number.isSafeInteger(3.1)"));
assert_eq!("true", &forward(&mut context, "Number.isSafeInteger(3.0)"));
assert_eq!(
"false",
&forward(&mut context, "Number.isSafeInteger(new Number(5))")
);
}

View File

@@ -0,0 +1,152 @@
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object},
object::{JsObject, ObjectData},
property::PropertyDescriptor,
property::PropertyKey,
symbol::WellKnownSymbols,
Context, JsResult, JsString, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use rustc_hash::FxHashSet;
use std::collections::VecDeque;
/// The `ForInIterator` object represents an iteration over some specific object.
/// It implements the iterator protocol.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-for-in-iterator-objects
#[derive(Debug, Clone, Finalize, Trace)]
pub struct ForInIterator {
object: JsValue,
visited_keys: FxHashSet<JsString>,
remaining_keys: VecDeque<JsString>,
object_was_visited: bool,
}
impl ForInIterator {
pub(crate) const NAME: &'static str = "ForInIterator";
fn new(object: JsValue) -> Self {
Self {
object,
visited_keys: FxHashSet::default(),
remaining_keys: VecDeque::default(),
object_was_visited: false,
}
}
/// `CreateForInIterator( object )`
///
/// Creates a new iterator over the given object.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createforiniterator
pub(crate) fn create_for_in_iterator(object: JsValue, context: &Context) -> JsValue {
let for_in_iterator = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.for_in_iterator(),
ObjectData::for_in_iterator(Self::new(object)),
);
for_in_iterator.into()
}
/// %ForInIteratorPrototype%.next( )
///
/// Gets the next result in the object.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%foriniteratorprototype%.next
pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let mut iterator = this.as_object().map(JsObject::borrow_mut);
let iterator = iterator
.as_mut()
.and_then(|obj| obj.as_for_in_iterator_mut())
.ok_or_else(|| context.construct_type_error("`this` is not a ForInIterator"))?;
let mut object = iterator.object.to_object(context)?;
loop {
if !iterator.object_was_visited {
let keys = object.__own_property_keys__(context)?;
for k in keys {
match k {
PropertyKey::String(ref k) => {
iterator.remaining_keys.push_back(k.clone());
}
PropertyKey::Index(i) => {
iterator.remaining_keys.push_back(i.to_string().into());
}
PropertyKey::Symbol(_) => {}
}
}
iterator.object_was_visited = true;
}
while let Some(r) = iterator.remaining_keys.pop_front() {
if !iterator.visited_keys.contains(&r) {
if let Some(desc) =
object.__get_own_property__(&PropertyKey::from(r.clone()), context)?
{
iterator.visited_keys.insert(r.clone());
if desc.expect_enumerable() {
return Ok(create_iter_result_object(
JsValue::new(r.to_string()),
false,
context,
));
}
}
}
}
let proto = object.prototype().clone();
match proto {
Some(o) => {
object = o;
}
_ => {
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
))
}
}
iterator.object = JsValue::new(object.clone());
iterator.object_was_visited = false;
}
}
/// Create the `%ArrayIteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%foriniteratorprototype%-object
pub(crate) fn create_prototype(
iterator_prototype: JsObject,
context: &mut Context,
) -> JsObject {
let _timer = Profiler::global().start_event(Self::NAME, "init");
// Create prototype
let for_in_iterator =
JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary());
make_builtin_fn(Self::next, "next", &for_in_iterator, 0, context);
let to_string_tag = WellKnownSymbols::to_string_tag();
let to_string_tag_property = PropertyDescriptor::builder()
.value("For In Iterator")
.writable(false)
.enumerable(false)
.configurable(true);
for_in_iterator.insert(to_string_tag, to_string_tag_property);
for_in_iterator
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,422 @@
use crate::{check_output, forward, Context, JsValue, TestAction};
#[test]
fn object_create_with_regular_object() {
let mut context = Context::default();
let init = r#"
const foo = { a: 5 };
const bar = Object.create(foo);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "bar.a"), "5");
assert_eq!(forward(&mut context, "Object.create.length"), "2");
}
#[test]
fn object_create_with_undefined() {
let mut context = Context::default();
let init = r#"
try {
const bar = Object.create();
} catch (err) {
err.toString()
}
"#;
let result = forward(&mut context, init);
assert_eq!(
result,
"\"TypeError: Object prototype may only be an Object or null: undefined\""
);
}
#[test]
fn object_create_with_number() {
let mut context = Context::default();
let init = r#"
try {
const bar = Object.create(5);
} catch (err) {
err.toString()
}
"#;
let result = forward(&mut context, init);
assert_eq!(
result,
"\"TypeError: Object prototype may only be an Object or null: 5\""
);
}
#[test]
#[ignore]
// TODO: to test on __proto__ somehow. __proto__ getter is not working as expected currently
fn object_create_with_function() {
let mut context = Context::default();
let init = r#"
const x = function (){};
const bar = Object.create(5);
bar.__proto__
"#;
let result = forward(&mut context, init);
assert_eq!(result, "...something on __proto__...");
}
#[test]
fn object_is() {
let mut context = Context::default();
let init = r#"
var foo = { a: 1};
var bar = { a: 1};
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "Object.is('foo', 'foo')"), "true");
assert_eq!(forward(&mut context, "Object.is('foo', 'bar')"), "false");
assert_eq!(forward(&mut context, "Object.is([], [])"), "false");
assert_eq!(forward(&mut context, "Object.is(foo, foo)"), "true");
assert_eq!(forward(&mut context, "Object.is(foo, bar)"), "false");
assert_eq!(forward(&mut context, "Object.is(null, null)"), "true");
assert_eq!(forward(&mut context, "Object.is(0, -0)"), "false");
assert_eq!(forward(&mut context, "Object.is(-0, -0)"), "true");
assert_eq!(forward(&mut context, "Object.is(NaN, 0/0)"), "true");
assert_eq!(forward(&mut context, "Object.is()"), "true");
assert_eq!(forward(&mut context, "Object.is(undefined)"), "true");
assert!(context.global_object().is_global());
}
#[test]
fn object_has_own_property() {
let scenario = r#"
let symA = Symbol('a');
let symB = Symbol('b');
let x = {
undefinedProp: undefined,
nullProp: null,
someProp: 1,
[symA]: 2,
100: 3,
};
"#;
check_output(&[
TestAction::Execute(scenario),
TestAction::TestEq("x.hasOwnProperty('hasOwnProperty')", "false"),
TestAction::TestEq("x.hasOwnProperty('undefinedProp')", "true"),
TestAction::TestEq("x.hasOwnProperty('nullProp')", "true"),
TestAction::TestEq("x.hasOwnProperty('someProp')", "true"),
TestAction::TestEq("x.hasOwnProperty(symB)", "false"),
TestAction::TestEq("x.hasOwnProperty(symA)", "true"),
TestAction::TestEq("x.hasOwnProperty(1000)", "false"),
TestAction::TestEq("x.hasOwnProperty(100)", "true"),
]);
}
#[test]
fn object_has_own() {
let scenario = r#"
let symA = Symbol('a');
let symB = Symbol('b');
let x = {
undefinedProp: undefined,
nullProp: null,
someProp: 1,
[symA]: 2,
100: 3,
};
"#;
check_output(&[
TestAction::Execute(scenario),
TestAction::TestEq("Object.hasOwn(x, 'hasOwnProperty')", "false"),
TestAction::TestEq("Object.hasOwn(x, 'undefinedProp')", "true"),
TestAction::TestEq("Object.hasOwn(x, 'nullProp')", "true"),
TestAction::TestEq("Object.hasOwn(x, 'someProp')", "true"),
TestAction::TestEq("Object.hasOwn(x, symB)", "false"),
TestAction::TestEq("Object.hasOwn(x, symA)", "true"),
TestAction::TestEq("Object.hasOwn(x, 1000)", "false"),
TestAction::TestEq("Object.hasOwn(x, 100)", "true"),
]);
}
#[test]
fn object_property_is_enumerable() {
let mut context = Context::default();
let init = r#"
let x = { enumerableProp: 'yes' };
"#;
eprintln!("{}", forward(&mut context, init));
assert_eq!(
forward(&mut context, r#"x.propertyIsEnumerable('enumerableProp')"#),
"true"
);
assert_eq!(
forward(&mut context, r#"x.propertyIsEnumerable('hasOwnProperty')"#),
"false"
);
assert_eq!(
forward(&mut context, r#"x.propertyIsEnumerable('not_here')"#),
"false",
);
assert_eq!(
forward(&mut context, r#"x.propertyIsEnumerable()"#),
"false",
);
}
#[test]
fn object_to_string() {
let mut context = Context::default();
let init = r#"
let u = undefined;
let n = null;
let a = [];
Array.prototype.toString = Object.prototype.toString;
let f = () => {};
Function.prototype.toString = Object.prototype.toString;
let e = new Error('test');
Error.prototype.toString = Object.prototype.toString;
let b = Boolean();
Boolean.prototype.toString = Object.prototype.toString;
let i = Number(42);
Number.prototype.toString = Object.prototype.toString;
let s = String('boa');
String.prototype.toString = Object.prototype.toString;
let d = new Date(Date.now());
Date.prototype.toString = Object.prototype.toString;
let re = /boa/;
RegExp.prototype.toString = Object.prototype.toString;
let o = Object();
"#;
eprintln!("{}", forward(&mut context, init));
assert_eq!(
forward(&mut context, "Object.prototype.toString.call(u)"),
"\"[object Undefined]\""
);
assert_eq!(
forward(&mut context, "Object.prototype.toString.call(n)"),
"\"[object Null]\""
);
assert_eq!(forward(&mut context, "a.toString()"), "\"[object Array]\"");
assert_eq!(
forward(&mut context, "f.toString()"),
"\"[object Function]\""
);
assert_eq!(forward(&mut context, "e.toString()"), "\"[object Error]\"");
assert_eq!(
forward(&mut context, "b.toString()"),
"\"[object Boolean]\""
);
assert_eq!(forward(&mut context, "i.toString()"), "\"[object Number]\"");
assert_eq!(forward(&mut context, "s.toString()"), "\"[object String]\"");
assert_eq!(forward(&mut context, "d.toString()"), "\"[object Date]\"");
assert_eq!(
forward(&mut context, "re.toString()"),
"\"[object RegExp]\""
);
assert_eq!(forward(&mut context, "o.toString()"), "\"[object Object]\"");
}
#[test]
fn define_symbol_property() {
let mut context = Context::default();
let init = r#"
let obj = {};
let sym = Symbol("key");
Object.defineProperty(obj, sym, { value: "val" });
"#;
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "obj[sym]"), "\"val\"");
}
#[test]
fn get_own_property_descriptor_1_arg_returns_undefined() {
let mut context = Context::default();
let code = r#"
let obj = {a: 2};
Object.getOwnPropertyDescriptor(obj)
"#;
assert_eq!(context.eval(code).unwrap(), JsValue::undefined());
}
#[test]
fn get_own_property_descriptor() {
let mut context = Context::default();
forward(
&mut context,
r#"
let obj = {a: 2};
let result = Object.getOwnPropertyDescriptor(obj, "a");
"#,
);
assert_eq!(forward(&mut context, "result.enumerable"), "true");
assert_eq!(forward(&mut context, "result.writable"), "true");
assert_eq!(forward(&mut context, "result.configurable"), "true");
assert_eq!(forward(&mut context, "result.value"), "2");
}
#[test]
fn get_own_property_descriptors() {
let mut context = Context::default();
forward(
&mut context,
r#"
let obj = {a: 1, b: 2};
let result = Object.getOwnPropertyDescriptors(obj);
"#,
);
assert_eq!(forward(&mut context, "result.a.enumerable"), "true");
assert_eq!(forward(&mut context, "result.a.writable"), "true");
assert_eq!(forward(&mut context, "result.a.configurable"), "true");
assert_eq!(forward(&mut context, "result.a.value"), "1");
assert_eq!(forward(&mut context, "result.b.enumerable"), "true");
assert_eq!(forward(&mut context, "result.b.writable"), "true");
assert_eq!(forward(&mut context, "result.b.configurable"), "true");
assert_eq!(forward(&mut context, "result.b.value"), "2");
}
#[test]
fn object_define_properties() {
let mut context = Context::default();
let init = r#"
const obj = {};
Object.defineProperties(obj, {
p: {
value: 42,
writable: true
}
});
"#;
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "obj.p"), "42");
}
#[test]
fn object_is_prototype_of() {
let mut context = Context::default();
let init = r#"
Object.prototype.isPrototypeOf(String.prototype)
"#;
assert_eq!(context.eval(init).unwrap(), JsValue::new(true));
}
#[test]
fn object_get_own_property_names_invalid_args() {
let error_message = r#"Uncaught "TypeError": "cannot convert 'null' or 'undefined' to object""#;
check_output(&[
TestAction::TestEq("Object.getOwnPropertyNames()", error_message),
TestAction::TestEq("Object.getOwnPropertyNames(null)", error_message),
TestAction::TestEq("Object.getOwnPropertyNames(undefined)", error_message),
]);
}
#[test]
fn object_get_own_property_names() {
check_output(&[
TestAction::TestEq("Object.getOwnPropertyNames(0)", "[]"),
TestAction::TestEq("Object.getOwnPropertyNames(false)", "[]"),
TestAction::TestEq(r#"Object.getOwnPropertyNames(Symbol("a"))"#, "[]"),
TestAction::TestEq("Object.getOwnPropertyNames({})", "[]"),
TestAction::TestEq("Object.getOwnPropertyNames(NaN)", "[]"),
TestAction::TestEq(
"Object.getOwnPropertyNames([1, 2, 3])",
r#"[ "0", "1", "2", "length" ]"#,
),
TestAction::TestEq(
r#"Object.getOwnPropertyNames({
"a": 1,
"b": 2,
[ Symbol("c") ]: 3,
[ Symbol("d") ]: 4,
})"#,
r#"[ "a", "b" ]"#,
),
]);
}
#[test]
fn object_get_own_property_symbols_invalid_args() {
let error_message = r#"Uncaught "TypeError": "cannot convert 'null' or 'undefined' to object""#;
check_output(&[
TestAction::TestEq("Object.getOwnPropertySymbols()", error_message),
TestAction::TestEq("Object.getOwnPropertySymbols(null)", error_message),
TestAction::TestEq("Object.getOwnPropertySymbols(undefined)", error_message),
]);
}
#[test]
fn object_get_own_property_symbols() {
check_output(&[
TestAction::TestEq("Object.getOwnPropertySymbols(0)", "[]"),
TestAction::TestEq("Object.getOwnPropertySymbols(false)", "[]"),
TestAction::TestEq(r#"Object.getOwnPropertySymbols(Symbol("a"))"#, "[]"),
TestAction::TestEq("Object.getOwnPropertySymbols({})", "[]"),
TestAction::TestEq("Object.getOwnPropertySymbols(NaN)", "[]"),
TestAction::TestEq("Object.getOwnPropertySymbols([1, 2, 3])", "[]"),
TestAction::TestEq(
r#"
Object.getOwnPropertySymbols({
"a": 1,
"b": 2,
[ Symbol("c") ]: 3,
[ Symbol("d") ]: 4,
})"#,
"[ Symbol(c), Symbol(d) ]",
),
]);
}
#[test]
fn object_from_entries_invalid_args() {
let error_message = r#"Uncaught "TypeError": "cannot convert null or undefined to Object""#;
check_output(&[
TestAction::TestEq("Object.fromEntries()", error_message),
TestAction::TestEq("Object.fromEntries(null)", error_message),
TestAction::TestEq("Object.fromEntries(undefined)", error_message),
]);
}
#[test]
fn object_from_entries() {
let scenario = r#"
let sym = Symbol("sym");
let map = Object.fromEntries([
["long key", 1],
["short", 2],
[sym, 3],
[5, 4],
]);
"#;
check_output(&[
TestAction::Execute(scenario),
TestAction::TestEq("map['long key']", "1"),
TestAction::TestEq("map.short", "2"),
TestAction::TestEq("map[sym]", "3"),
TestAction::TestEq("map[5]", "4"),
]);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
use super::{Promise, PromiseCapability};
use crate::{
builtins::promise::{ReactionRecord, ReactionType},
job::JobCallback,
object::{FunctionBuilder, JsObject},
Context, JsValue,
};
use boa_gc::{Finalize, Trace};
#[derive(Debug, Clone, Copy)]
pub(crate) struct PromiseJob;
impl PromiseJob {
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-newpromisereactionjob
pub(crate) fn new_promise_reaction_job(
reaction: ReactionRecord,
argument: JsValue,
context: &mut Context,
) -> JobCallback {
#[derive(Debug, Trace, Finalize)]
struct ReactionJobCaptures {
reaction: ReactionRecord,
argument: JsValue,
}
// 1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called:
let job = FunctionBuilder::closure_with_captures(
context,
|_this, _args, captures, context| {
let ReactionJobCaptures { reaction, argument } = captures;
let ReactionRecord {
// a. Let promiseCapability be reaction.[[Capability]].
promise_capability,
// b. Let type be reaction.[[Type]].
reaction_type,
// c. Let handler be reaction.[[Handler]].
handler,
} = reaction;
let handler_result = match handler {
// d. If handler is empty, then
None => match reaction_type {
// i. If type is Fulfill, let handlerResult be NormalCompletion(argument).
ReactionType::Fulfill => Ok(argument.clone()),
// ii. Else,
// 1. Assert: type is Reject.
ReactionType::Reject => {
// 2. Let handlerResult be ThrowCompletion(argument).
Err(argument.clone())
}
},
// e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)).
Some(handler) => {
handler.call_job_callback(&JsValue::Undefined, &[argument.clone()], context)
}
};
match promise_capability {
None => {
// f. If promiseCapability is undefined, then
// i. Assert: handlerResult is not an abrupt completion.
assert!(
handler_result.is_ok(),
"Assertion: <handlerResult is not an abrupt completion> failed"
);
// ii. Return empty.
Ok(JsValue::Undefined)
}
Some(promise_capability_record) => {
// g. Assert: promiseCapability is a PromiseCapability Record.
let PromiseCapability {
promise: _,
resolve,
reject,
} = promise_capability_record;
match handler_result {
// h. If handlerResult is an abrupt completion, then
Err(value) => {
// i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »).
context.call(&reject.clone().into(), &JsValue::Undefined, &[value])
}
// i. Else,
Ok(value) => {
// i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »).
context.call(&resolve.clone().into(), &JsValue::Undefined, &[value])
}
}
}
}
},
ReactionJobCaptures { reaction, argument },
)
.build()
.into();
// 2. Let handlerRealm be null.
// 3. If reaction.[[Handler]] is not empty, then
// a. Let getHandlerRealmResult be Completion(GetFunctionRealm(reaction.[[Handler]].[[Callback]])).
// b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]].
// c. Else, set handlerRealm to the current Realm Record.
// d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects.
// 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }.
JobCallback::make_job_callback(job)
}
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
pub(crate) fn new_promise_resolve_thenable_job(
promise_to_resolve: JsObject,
thenable: JsValue,
then: JobCallback,
context: &mut Context,
) -> JobCallback {
// 1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called:
let job = FunctionBuilder::closure_with_captures(
context,
|_this: &JsValue, _args: &[JsValue], captures, context: &mut Context| {
let JobCapture {
promise_to_resolve,
thenable,
then,
} = captures;
// a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve).
let resolving_functions =
Promise::create_resolving_functions(promise_to_resolve, context);
// b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).
let then_call_result = then.call_job_callback(
thenable,
&[
resolving_functions.resolve,
resolving_functions.reject.clone(),
],
context,
);
// c. If thenCallResult is an abrupt completion, then
if let Err(value) = then_call_result {
// i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »).
return context.call(
&resolving_functions.reject,
&JsValue::Undefined,
&[value],
);
}
// d. Return ? thenCallResult.
then_call_result
},
JobCapture::new(promise_to_resolve, thenable, then),
)
.build();
// 2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])).
// 3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]].
// 4. Else, let thenRealm be the current Realm Record.
// 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects.
// 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }.
JobCallback::make_job_callback(job.into())
}
}
#[derive(Debug, Trace, Finalize)]
struct JobCapture {
promise_to_resolve: JsObject,
thenable: JsValue,
then: JobCallback,
}
impl JobCapture {
fn new(promise_to_resolve: JsObject, thenable: JsValue, then: JobCallback) -> Self {
Self {
promise_to_resolve,
thenable,
then,
}
}
}

View File

@@ -0,0 +1,19 @@
use crate::{forward, Context};
#[test]
fn promise() {
let mut context = Context::default();
let init = r#"
let count = 0;
const promise = new Promise((resolve, reject) => {
count += 1;
resolve(undefined);
}).then((_) => (count += 1));
count += 1;
count;
"#;
let result = context.eval(init).unwrap();
assert_eq!(result.as_number(), Some(2_f64));
let after_completion = forward(&mut context, "count");
assert_eq!(after_completion, String::from("3"));
}

View File

@@ -0,0 +1,186 @@
//! This module implements the global `Proxy` object.
//!
//! The `Proxy` object enables you to create a proxy for another object,
//! which can intercept and redefine fundamental operations for that object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-proxy-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
use crate::{
builtins::{BuiltIn, JsArgs},
object::{ConstructorBuilder, FunctionBuilder, JsFunction, JsObject, ObjectData},
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
/// Javascript `Proxy` object.
#[derive(Debug, Clone, Trace, Finalize)]
pub struct Proxy {
// (target, handler)
data: Option<(JsObject, JsObject)>,
}
impl BuiltIn for Proxy {
const NAME: &'static str = "Proxy";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().proxy().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.has_prototype_property(false)
.static_method(Self::revocable, "revocable", 2)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Proxy {
const LENGTH: usize = 2;
pub(crate) fn new(target: JsObject, handler: JsObject) -> Self {
Self {
data: Some((target, handler)),
}
}
/// This is an internal method only built for usage in the proxy internal methods.
///
/// It returns the (target, handler) of the proxy.
pub(crate) fn try_data(&self, context: &mut Context) -> JsResult<(JsObject, JsObject)> {
self.data.clone().ok_or_else(|| {
context.construct_type_error("Proxy object has empty handler and target")
})
}
/// `28.2.1.1 Proxy ( target, handler )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-proxy-target-handler
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return context.throw_type_error("Proxy constructor called on undefined new target");
}
// 2. Return ? ProxyCreate(target, handler).
Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context).map(JsValue::from)
}
// `10.5.14 ProxyCreate ( target, handler )`
//
// More information:
// - [ECMAScript reference][spec]
//
// [spec]: https://tc39.es/ecma262/#sec-proxycreate
pub(crate) fn create(
target: &JsValue,
handler: &JsValue,
context: &mut Context,
) -> JsResult<JsObject> {
// 1. If Type(target) is not Object, throw a TypeError exception.
let target = target.as_object().ok_or_else(|| {
context.construct_type_error("Proxy constructor called with non-object target")
})?;
// 2. If Type(handler) is not Object, throw a TypeError exception.
let handler = handler.as_object().ok_or_else(|| {
context.construct_type_error("Proxy constructor called with non-object handler")
})?;
// 3. Let P be ! MakeBasicObject(« [[ProxyHandler]], [[ProxyTarget]] »).
// 4. Set P's essential internal methods, except for [[Call]] and [[Construct]], to the definitions specified in 10.5.
// 5. If IsCallable(target) is true, then
// a. Set P.[[Call]] as specified in 10.5.12.
// b. If IsConstructor(target) is true, then
// i. Set P.[[Construct]] as specified in 10.5.13.
// 6. Set P.[[ProxyTarget]] to target.
// 7. Set P.[[ProxyHandler]] to handler.
let p = JsObject::from_proto_and_data(
context.intrinsics().constructors().object().prototype(),
ObjectData::proxy(
Self::new(target.clone(), handler.clone()),
target.is_callable(),
target.is_constructor(),
),
);
// 8. Return P.
Ok(p)
}
pub(crate) fn revoker(proxy: JsObject, context: &mut Context) -> JsFunction {
// 3. Let revoker be ! CreateBuiltinFunction(revokerClosure, 0, "", « [[RevocableProxy]] »).
// 4. Set revoker.[[RevocableProxy]] to p.
FunctionBuilder::closure_with_captures(
context,
|_, _, revocable_proxy, _| {
// a. Let F be the active function object.
// b. Let p be F.[[RevocableProxy]].
// d. Set F.[[RevocableProxy]] to null.
if let Some(p) = revocable_proxy.take() {
// e. Assert: p is a Proxy object.
// f. Set p.[[ProxyTarget]] to null.
// g. Set p.[[ProxyHandler]] to null.
p.borrow_mut()
.as_proxy_mut()
.expect("[[RevocableProxy]] must be a proxy object")
.data = None;
}
// c. If p is null, return undefined.
// h. Return undefined.
Ok(JsValue::undefined())
},
Some(proxy),
)
.build()
}
/// `28.2.2.1 Proxy.revocable ( target, handler )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-proxy.revocable
fn revocable(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let p be ? ProxyCreate(target, handler).
let p = Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context)?;
// Revoker creation steps on `Proxy::revoker`
let revoker = Self::revoker(p.clone(), context);
// 5. Let result be ! OrdinaryObjectCreate(%Object.prototype%).
let result = context.construct_object();
// 6. Perform ! CreateDataPropertyOrThrow(result, "proxy", p).
result
.create_data_property_or_throw("proxy", p, context)
.expect("CreateDataPropertyOrThrow cannot fail here");
// 7. Perform ! CreateDataPropertyOrThrow(result, "revoke", revoker).
result
.create_data_property_or_throw("revoke", revoker, context)
.expect("CreateDataPropertyOrThrow cannot fail here");
// 8. Return result.
Ok(result.into())
}
}

View File

@@ -0,0 +1,397 @@
//! This module implements the global `Reflect` object.
//!
//! The `Reflect` global object is a built-in object that provides methods for interceptable
//! JavaScript operations.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-reflect-object
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect
use super::{Array, JsArgs};
use crate::{
builtins::{self, BuiltIn},
object::ObjectInitializer,
property::Attribute,
symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
#[cfg(test)]
mod tests;
/// Javascript `Reflect` object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Reflect;
impl BuiltIn for Reflect {
const NAME: &'static str = "Reflect";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let to_string_tag = WellKnownSymbols::to_string_tag();
ObjectInitializer::new(context)
.function(Self::apply, "apply", 3)
.function(Self::construct, "construct", 2)
.function(Self::define_property, "defineProperty", 3)
.function(Self::delete_property, "deleteProperty", 2)
.function(Self::get, "get", 2)
.function(
Self::get_own_property_descriptor,
"getOwnPropertyDescriptor",
2,
)
.function(Self::get_prototype_of, "getPrototypeOf", 1)
.function(Self::has, "has", 2)
.function(Self::is_extensible, "isExtensible", 1)
.function(Self::own_keys, "ownKeys", 1)
.function(Self::prevent_extensions, "preventExtensions", 1)
.function(Self::set, "set", 3)
.function(Self::set_prototype_of, "setPrototypeOf", 2)
.property(
to_string_tag,
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Reflect {
/// Calls a target function with arguments.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.apply
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply
pub(crate) fn apply(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be a function"))?;
let this_arg = args.get_or_undefined(1);
let args_list = args.get_or_undefined(2);
if !target.is_callable() {
return context.throw_type_error("target must be a function");
}
let args = args_list.create_list_from_array_like(&[], context)?;
target.call(this_arg, &args, context)
}
/// Calls a target function as a constructor with arguments.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.construct
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct
pub(crate) fn construct(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If IsConstructor(target) is false, throw a TypeError exception.
let target = args
.get_or_undefined(0)
.as_constructor()
.ok_or_else(|| context.construct_type_error("target must be a constructor"))?;
let new_target = if let Some(new_target) = args.get(2) {
// 3. Else if IsConstructor(newTarget) is false, throw a TypeError exception.
if let Some(new_target) = new_target.as_constructor() {
new_target
} else {
return context.throw_type_error("newTarget must be a constructor");
}
} else {
// 2. If newTarget is not present, set newTarget to target.
target
};
// 4. Let args be ? CreateListFromArrayLike(argumentsList).
let args = args
.get_or_undefined(1)
.create_list_from_array_like(&[], context)?;
// 5. Return ? Construct(target, args, newTarget).
target
.construct(&args, Some(new_target), context)
.map(JsValue::from)
}
/// Defines a property on an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.defineProperty
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/defineProperty
pub(crate) fn define_property(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args.get_or_undefined(1).to_property_key(context)?;
let prop_desc: JsValue = args
.get(2)
.and_then(|v| v.as_object().cloned())
.ok_or_else(|| context.construct_type_error("property descriptor must be an object"))?
.into();
target
.__define_own_property__(key, prop_desc.to_property_descriptor(context)?, context)
.map(Into::into)
}
/// Defines a property on an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.deleteproperty
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/deleteProperty
pub(crate) fn delete_property(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args.get_or_undefined(1).to_property_key(context)?;
Ok(target.__delete__(&key, context)?.into())
}
/// Gets a property of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.get
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
pub(crate) fn get(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. If Type(target) is not Object, throw a TypeError exception.
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
// 2. Let key be ? ToPropertyKey(propertyKey).
let key = args.get_or_undefined(1).to_property_key(context)?;
// 3. If receiver is not present, then
let receiver = if let Some(receiver) = args.get(2).cloned() {
receiver
} else {
// 3.a. Set receiver to target.
target.clone().into()
};
// 4. Return ? target.[[Get]](key, receiver).
target.__get__(&key, receiver, context)
}
/// Gets a property of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.getownpropertydescriptor
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor
pub(crate) fn get_own_property_descriptor(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if args.get_or_undefined(0).is_object() {
// This function is the same as Object.prototype.getOwnPropertyDescriptor, that why
// it is invoked here.
builtins::object::Object::get_own_property_descriptor(
&JsValue::undefined(),
args,
context,
)
} else {
context.throw_type_error("target must be an object")
}
}
/// Gets the prototype of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.getprototypeof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getPrototypeOf
pub(crate) fn get_prototype_of(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
Ok(target
.__get_prototype_of__(context)?
.map_or(JsValue::Null, JsValue::new))
}
/// Returns `true` if the object has the property, `false` otherwise.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.has
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/has
pub(crate) fn has(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args
.get(1)
.unwrap_or(&JsValue::undefined())
.to_property_key(context)?;
Ok(target.__has_property__(&key, context)?.into())
}
/// Returns `true` if the object is extensible, `false` otherwise.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.isextensible
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/isExtensible
pub(crate) fn is_extensible(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
Ok(target.__is_extensible__(context)?.into())
}
/// Returns an array of object own property keys.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.ownkeys
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys
pub(crate) fn own_keys(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let keys: Vec<JsValue> = target
.__own_property_keys__(context)?
.into_iter()
.map(Into::into)
.collect();
Ok(Array::create_array_from_list(keys, context).into())
}
/// Prevents new properties from ever being added to an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.preventextensions
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/preventExtensions
pub(crate) fn prevent_extensions(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
Ok(target.__prevent_extensions__(context)?.into())
}
/// Sets a property of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.set
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set
pub(crate) fn set(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args.get_or_undefined(1).to_property_key(context)?;
let value = args.get_or_undefined(2);
let receiver = if let Some(receiver) = args.get(3).cloned() {
receiver
} else {
target.clone().into()
};
Ok(target
.__set__(key, value.clone(), receiver, context)?
.into())
}
/// Sets the prototype of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.setprototypeof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/setPrototypeOf
pub(crate) fn set_prototype_of(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let target = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let proto = match args.get_or_undefined(1) {
JsValue::Object(obj) => Some(obj.clone()),
JsValue::Null => None,
_ => return context.throw_type_error("proto must be an object or null"),
};
Ok(target.__set_prototype_of__(proto, context)?.into())
}
}

View File

@@ -0,0 +1,191 @@
use crate::{forward, Context};
#[test]
fn apply() {
let mut context = Context::default();
let init = r#"
var called = {};
function f(n) { called.result = n };
Reflect.apply(f, undefined, [42]);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "called.result"), "42");
}
#[test]
fn construct() {
let mut context = Context::default();
let init = r#"
var called = {};
function f(n) { called.result = n };
Reflect.construct(f, [42]);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "called.result"), "42");
}
#[test]
fn define_property() {
let mut context = Context::default();
let init = r#"
let obj = {};
Reflect.defineProperty(obj, 'p', { value: 42 });
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "obj.p"), "42");
}
#[test]
fn delete_property() {
let mut context = Context::default();
let init = r#"
let obj = { p: 42 };
let deleted = Reflect.deleteProperty(obj, 'p');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "obj.p"), "undefined");
assert_eq!(forward(&mut context, "deleted"), "true");
}
#[test]
fn get() {
let mut context = Context::default();
let init = r#"
let obj = { p: 42 }
let p = Reflect.get(obj, 'p');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "p"), "42");
}
#[test]
fn get_own_property_descriptor() {
let mut context = Context::default();
let init = r#"
let obj = { p: 42 };
let desc = Reflect.getOwnPropertyDescriptor(obj, 'p');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "desc.value"), "42");
}
#[test]
fn get_prototype_of() {
let mut context = Context::default();
let init = r#"
function F() { this.p = 42 };
let f = new F();
let proto = Reflect.getPrototypeOf(f);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "proto.constructor.name"), "\"F\"");
}
#[test]
fn has() {
let mut context = Context::default();
let init = r#"
let obj = { p: 42 };
let hasP = Reflect.has(obj, 'p');
let hasP2 = Reflect.has(obj, 'p2');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "hasP"), "true");
assert_eq!(forward(&mut context, "hasP2"), "false");
}
#[test]
fn is_extensible() {
let mut context = Context::default();
let init = r#"
let obj = { p: 42 };
let isExtensible = Reflect.isExtensible(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "isExtensible"), "true");
}
#[test]
fn own_keys() {
let mut context = Context::default();
let init = r#"
let obj = { p: 42 };
let ownKeys = Reflect.ownKeys(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "ownKeys"), r#"[ "p" ]"#);
}
#[test]
fn prevent_extensions() {
let mut context = Context::default();
let init = r#"
let obj = { p: 42 };
let r = Reflect.preventExtensions(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "r"), "true");
}
#[test]
fn set() {
let mut context = Context::default();
let init = r#"
let obj = {};
Reflect.set(obj, 'p', 42);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "obj.p"), "42");
}
#[test]
fn set_prototype_of() {
let mut context = Context::default();
let init = r#"
function F() { this.p = 42 };
let obj = {}
Reflect.setPrototypeOf(obj, F);
let p = Reflect.getPrototypeOf(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "p.name"), "\"F\"");
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
//! This module implements the global `RegExp String Iterator` object.
//!
//! A `RegExp` String Iterator is an object, that represents a specific iteration over some
//! specific String instance object, matching against some specific `RegExp` instance object.
//! There is not a named constructor for `RegExp` String Iterator objects. Instead, `RegExp`
//! String Iterator objects are created by calling certain methods of `RegExp` instance objects.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-regexp-string-iterator-objects
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object, regexp},
object::{JsObject, ObjectData},
property::PropertyDescriptor,
symbol::WellKnownSymbols,
Context, JsResult, JsString, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use regexp::{advance_string_index, RegExp};
// TODO: See todos in create_regexp_string_iterator and next.
#[derive(Debug, Clone, Finalize, Trace)]
pub struct RegExpStringIterator {
matcher: JsObject,
string: JsString,
global: bool,
unicode: bool,
completed: bool,
}
// TODO: See todos in create_regexp_string_iterator and next.
impl RegExpStringIterator {
fn new(matcher: JsObject, string: JsString, global: bool, unicode: bool) -> Self {
Self {
matcher,
string,
global,
unicode,
completed: false,
}
}
/// `22.2.7.1 CreateRegExpStringIterator ( R, S, global, fullUnicode )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createregexpstringiterator
pub(crate) fn create_regexp_string_iterator(
matcher: JsObject,
string: JsString,
global: bool,
unicode: bool,
context: &mut Context,
) -> JsValue {
// TODO: Implement this with closures and generators.
// For now all values of the closure are stored in RegExpStringIterator and the actual closure execution is in `.next()`.
// 1. Assert: Type(S) is String.
// 2. Assert: Type(global) is Boolean.
// 3. Assert: Type(fullUnicode) is Boolean.
// 4. Let closure be a new Abstract Closure with no parameters that captures R, S, global,
// and fullUnicode and performs the following steps when called:
// 5. Return ! CreateIteratorFromClosure(closure, "%RegExpStringIteratorPrototype%", %RegExpStringIteratorPrototype%).
let regexp_string_iterator = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.regexp_string_iterator(),
ObjectData::reg_exp_string_iterator(Self::new(matcher, string, global, unicode)),
);
regexp_string_iterator.into()
}
pub fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let mut iterator = this.as_object().map(JsObject::borrow_mut);
let iterator = iterator
.as_mut()
.and_then(|obj| obj.as_regexp_string_iterator_mut())
.ok_or_else(|| context.construct_type_error("`this` is not a RegExpStringIterator"))?;
if iterator.completed {
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
}
// TODO: This is the code that should be created as a closure in create_regexp_string_iterator.
// i. Let match be ? RegExpExec(R, S).
let m = RegExp::abstract_exec(&iterator.matcher, iterator.string.clone(), context)?;
if let Some(m) = m {
// iii. If global is false, then
if !iterator.global {
// 1. Perform ? Yield(match).
// 2. Return undefined.
iterator.completed = true;
return Ok(create_iter_result_object(m.into(), false, context));
}
// iv. Let matchStr be ? ToString(? Get(match, "0")).
let m_str = m.get("0", context)?.to_string(context)?;
// v. If matchStr is the empty String, then
if m_str.is_empty() {
// 1. Let thisIndex be (? ToLength(? Get(R, "lastIndex"))).
let this_index = iterator
.matcher
.get("lastIndex", context)?
.to_length(context)?;
// 2. Let nextIndex be ! AdvanceStringIndex(S, thisIndex, fullUnicode).
let next_index =
advance_string_index(&iterator.string, this_index, iterator.unicode);
// 3. Perform ? Set(R, "lastIndex", 𝔽(nextIndex), true).
iterator
.matcher
.set("lastIndex", next_index, true, context)?;
}
// vi. Perform ? Yield(match).
Ok(create_iter_result_object(m.into(), false, context))
} else {
// ii. If match is null, return undefined.
iterator.completed = true;
Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
))
}
}
/// Create the `%ArrayIteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object
pub(crate) fn create_prototype(
iterator_prototype: JsObject,
context: &mut Context,
) -> JsObject {
let _timer = Profiler::global().start_event("RegExp String Iterator", "init");
// Create prototype
let result = JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary());
make_builtin_fn(Self::next, "next", &result, 0, context);
let to_string_tag = WellKnownSymbols::to_string_tag();
let to_string_tag_property = PropertyDescriptor::builder()
.value("RegExp String Iterator")
.writable(false)
.enumerable(false)
.configurable(true);
result.insert(to_string_tag, to_string_tag_property);
result
}
}

View File

@@ -0,0 +1,239 @@
use crate::{forward, Context};
#[test]
fn constructors() {
let mut context = Context::default();
let init = r#"
var constructed = new RegExp("[0-9]+(\\.[0-9]+)?");
var literal = /[0-9]+(\.[0-9]+)?/;
var ctor_literal = new RegExp(/[0-9]+(\.[0-9]+)?/);
"#;
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "constructed.test('1.0')"), "true");
assert_eq!(forward(&mut context, "literal.test('1.0')"), "true");
assert_eq!(forward(&mut context, "ctor_literal.test('1.0')"), "true");
}
#[test]
fn species() {
let mut context = Context::default();
let init = r#"
var descriptor = Object.getOwnPropertyDescriptor(RegExp, Symbol.species);
var accessor = Object.getOwnPropertyDescriptor(RegExp, Symbol.species).get;
var name = Object.getOwnPropertyDescriptor(descriptor.get, "name");
var length = Object.getOwnPropertyDescriptor(descriptor.get, "length");
var thisVal = {};
"#;
eprintln!("{}", forward(&mut context, init));
// length
assert_eq!(forward(&mut context, "descriptor.get.length"), "0");
assert_eq!(forward(&mut context, "length.enumerable"), "false");
assert_eq!(forward(&mut context, "length.writable"), "false");
assert_eq!(forward(&mut context, "length.configurable"), "true");
// return-value
assert_eq!(
forward(&mut context, "Object.is(accessor.call(thisVal), thisVal)"),
"true"
);
// symbol-species-name
assert_eq!(
forward(&mut context, "descriptor.get.name"),
"\"get [Symbol.species]\""
);
assert_eq!(forward(&mut context, "name.enumerable"), "false");
assert_eq!(forward(&mut context, "name.writable"), "false");
assert_eq!(forward(&mut context, "name.configurable"), "true");
// symbol-species
assert_eq!(forward(&mut context, "descriptor.set"), "undefined");
assert_eq!(
forward(&mut context, "typeof descriptor.get"),
"\"function\""
);
assert_eq!(forward(&mut context, "descriptor.enumerable"), "false");
assert_eq!(forward(&mut context, "descriptor.configurable"), "true");
}
// TODO: uncomment this test when property getters are supported
// #[test]
// fn flags() {
// let mut context = Context::default();
// let init = r#"
// var re_gi = /test/gi;
// var re_sm = /test/sm;
// "#;
//
// eprintln!("{}", forward(&mut context, init));
// assert_eq!(forward(&mut context, "re_gi.global"), "true");
// assert_eq!(forward(&mut context, "re_gi.ignoreCase"), "true");
// assert_eq!(forward(&mut context, "re_gi.multiline"), "false");
// assert_eq!(forward(&mut context, "re_gi.dotAll"), "false");
// assert_eq!(forward(&mut context, "re_gi.unicode"), "false");
// assert_eq!(forward(&mut context, "re_gi.sticky"), "false");
// assert_eq!(forward(&mut context, "re_gi.flags"), "gi");
//
// assert_eq!(forward(&mut context, "re_sm.global"), "false");
// assert_eq!(forward(&mut context, "re_sm.ignoreCase"), "false");
// assert_eq!(forward(&mut context, "re_sm.multiline"), "true");
// assert_eq!(forward(&mut context, "re_sm.dotAll"), "true");
// assert_eq!(forward(&mut context, "re_sm.unicode"), "false");
// assert_eq!(forward(&mut context, "re_sm.sticky"), "false");
// assert_eq!(forward(&mut context, "re_sm.flags"), "ms");
// }
#[test]
fn last_index() {
let mut context = Context::default();
let init = r#"
var regex = /[0-9]+(\.[0-9]+)?/g;
"#;
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "regex.lastIndex"), "0");
assert_eq!(forward(&mut context, "regex.test('1.0foo')"), "true");
assert_eq!(forward(&mut context, "regex.lastIndex"), "3");
assert_eq!(forward(&mut context, "regex.test('1.0foo')"), "false");
assert_eq!(forward(&mut context, "regex.lastIndex"), "0");
}
#[test]
fn exec() {
let mut context = Context::default();
let init = r#"
var re = /quick\s(brown).+?(jumps)/ig;
var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
"#;
eprintln!("{}", forward(&mut context, init));
assert_eq!(
forward(&mut context, "result[0]"),
"\"Quick Brown Fox Jumps\""
);
assert_eq!(forward(&mut context, "result[1]"), "\"Brown\"");
assert_eq!(forward(&mut context, "result[2]"), "\"Jumps\"");
assert_eq!(forward(&mut context, "result.index"), "4");
assert_eq!(
forward(&mut context, "result.input"),
"\"The Quick Brown Fox Jumps Over The Lazy Dog\""
);
}
#[test]
fn to_string() {
let mut context = Context::default();
assert_eq!(
forward(&mut context, "(new RegExp('a+b+c')).toString()"),
"\"/a+b+c/\""
);
assert_eq!(
forward(&mut context, "(new RegExp('bar', 'g')).toString()"),
"\"/bar/g\""
);
assert_eq!(
forward(&mut context, "(new RegExp('\\\\n', 'g')).toString()"),
"\"/\\n/g\""
);
assert_eq!(forward(&mut context, "/\\n/g.toString()"), "\"/\\n/g\"");
}
#[test]
fn no_panic_on_invalid_character_escape() {
let mut context = Context::default();
// This used to panic, we now return an error
// The line below should not cause Boa to panic
forward(&mut context, r"const a = /,\;/");
}
#[test]
fn search() {
let mut context = Context::default();
// coerce-string
assert_eq!(
forward(
&mut context,
r#"
var obj = {
toString: function() {
return 'toString value';
}
};
/ring/[Symbol.search](obj)
"#
),
"4"
);
// failure-return-val
assert_eq!(forward(&mut context, "/z/[Symbol.search]('a')"), "-1");
// length
assert_eq!(
forward(&mut context, "RegExp.prototype[Symbol.search].length"),
"1"
);
let init =
"var obj = Object.getOwnPropertyDescriptor(RegExp.prototype[Symbol.search], \"length\")";
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "obj.enumerable"), "false");
assert_eq!(forward(&mut context, "obj.writable"), "false");
assert_eq!(forward(&mut context, "obj.configurable"), "true");
// name
assert_eq!(
forward(&mut context, "RegExp.prototype[Symbol.search].name"),
"\"[Symbol.search]\""
);
let init =
"var obj = Object.getOwnPropertyDescriptor(RegExp.prototype[Symbol.search], \"name\")";
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "obj.enumerable"), "false");
assert_eq!(forward(&mut context, "obj.writable"), "false");
assert_eq!(forward(&mut context, "obj.configurable"), "true");
// prop-desc
let init = "var obj = Object.getOwnPropertyDescriptor(RegExp.prototype, Symbol.search)";
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "obj.enumerable"), "false");
assert_eq!(forward(&mut context, "obj.writable"), "true");
assert_eq!(forward(&mut context, "obj.configurable"), "true");
// success-return-val
assert_eq!(forward(&mut context, "/a/[Symbol.search]('abc')"), "0");
assert_eq!(forward(&mut context, "/b/[Symbol.search]('abc')"), "1");
assert_eq!(forward(&mut context, "/c/[Symbol.search]('abc')"), "2");
// this-val-non-obj
let error = "Uncaught \"TypeError\": \"RegExp.prototype[Symbol.search] method called on incompatible value\"";
let init = "var search = RegExp.prototype[Symbol.search]";
eprintln!("{}", forward(&mut context, init));
assert_eq!(forward(&mut context, "search.call()"), error);
assert_eq!(forward(&mut context, "search.call(undefined)"), error);
assert_eq!(forward(&mut context, "search.call(null)"), error);
assert_eq!(forward(&mut context, "search.call(true)"), error);
assert_eq!(forward(&mut context, "search.call('string')"), error);
assert_eq!(forward(&mut context, "search.call(Symbol.search)"), error);
assert_eq!(forward(&mut context, "search.call(86)"), error);
// u-lastindex-advance
assert_eq!(
forward(&mut context, "/\\udf06/u[Symbol.search]('\\ud834\\udf06')"),
"-1"
);
assert_eq!(forward(&mut context, "/a/[Symbol.search](\"a\")"), "0");
assert_eq!(forward(&mut context, "/a/[Symbol.search](\"ba\")"), "1");
assert_eq!(forward(&mut context, "/a/[Symbol.search](\"bb\")"), "-1");
assert_eq!(forward(&mut context, "/u/[Symbol.search](null)"), "1");
assert_eq!(forward(&mut context, "/d/[Symbol.search](undefined)"), "2");
}

View File

@@ -0,0 +1,451 @@
//! This module implements the global `Set` object.
//!
//! The JavaScript `Set` class is a global object that is used in the construction of sets; which
//! are high-level, collections of values.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-set-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
use self::{ordered_set::OrderedSet, set_iterator::SetIterator};
use super::JsArgs;
use crate::{
builtins::BuiltIn,
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder,
JsObject, ObjectData,
},
property::{Attribute, PropertyNameKind},
symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
pub mod ordered_set;
pub mod set_iterator;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub(crate) struct Set(OrderedSet<JsValue>);
impl BuiltIn for Set {
const NAME: &'static str = "Set";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let get_species = FunctionBuilder::native(context, Self::get_species)
.name("get [Symbol.species]")
.constructor(false)
.build();
let size_getter = FunctionBuilder::native(context, Self::size_getter)
.constructor(false)
.name("get size")
.build();
let iterator_symbol = WellKnownSymbols::iterator();
let to_string_tag = WellKnownSymbols::to_string_tag();
let values_function = FunctionBuilder::native(context, Self::values)
.name("values")
.length(0)
.constructor(false)
.build();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().set().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.static_accessor(
WellKnownSymbols::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.method(Self::add, "add", 1)
.method(Self::clear, "clear", 0)
.method(Self::delete, "delete", 1)
.method(Self::entries, "entries", 0)
.method(Self::for_each, "forEach", 1)
.method(Self::has, "has", 1)
.property(
"keys",
values_function.clone(),
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.accessor("size", Some(size_getter), None, Attribute::CONFIGURABLE)
.property(
"values",
values_function.clone(),
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
iterator_symbol,
values_function,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
to_string_tag,
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Set {
pub(crate) const LENGTH: usize = 0;
/// Create a new set
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return context
.throw_type_error("calling a builtin Set constructor without new is forbidden");
}
// 2. Let set be ? OrdinaryCreateFromConstructor(NewTarget, "%Set.prototype%", « [[SetData]] »).
// 3. Set set.[[SetData]] to a new empty List.
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::set, context)?;
let set = JsObject::from_proto_and_data(prototype, ObjectData::set(OrderedSet::default()));
// 4. If iterable is either undefined or null, return set.
let iterable = args.get_or_undefined(0);
if iterable.is_null_or_undefined() {
return Ok(set.into());
}
// 5. Let adder be ? Get(set, "add").
let adder = set.get("add", context)?;
// 6. If IsCallable(adder) is false, throw a TypeError exception.
let adder = adder.as_callable().ok_or_else(|| {
context.construct_type_error("'add' of 'newTarget' is not a function")
})?;
// 7. Let iteratorRecord be ? GetIterator(iterable).
let iterator_record = iterable.clone().get_iterator(context, None, None)?;
// 8. Repeat,
// a. Let next be ? IteratorStep(iteratorRecord).
// b. If next is false, return set.
// c. Let nextValue be ? IteratorValue(next).
// d. Let status be Completion(Call(adder, set, « nextValue »)).
// e. IfAbruptCloseIterator(status, iteratorRecord).
while let Some(next) = iterator_record.step(context)? {
// c
let next_value = next.value(context)?;
// d, e
if let Err(status) = adder.call(&set.clone().into(), &[next_value], context) {
return iterator_record.close(Err(status), context);
}
}
// 8.b
Ok(set.into())
}
/// Utility for constructing `Set` objects.
pub(crate) fn set_create(prototype: Option<JsObject>, context: &mut Context) -> JsObject {
let prototype =
prototype.unwrap_or_else(|| context.intrinsics().constructors().set().prototype());
JsObject::from_proto_and_data(prototype, ObjectData::set(OrderedSet::new()))
}
/// Utility for constructing `Set` objects from an iterator of `JsValue`'s.
pub(crate) fn create_set_from_list<I>(elements: I, context: &mut Context) -> JsObject
where
I: IntoIterator<Item = JsValue>,
{
// Create empty Set
let set = Self::set_create(None, context);
// For each element e of elements, do
for elem in elements {
Self::add(&set.clone().into(), &[elem], context)
.expect("adding new element shouldn't error out");
}
set
}
/// `get Set [ @@species ]`
///
/// The Set[Symbol.species] accessor property returns the Set constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-set-@@species
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/@@species
#[allow(clippy::unnecessary_wraps)]
fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// 1. Return the this value.
Ok(this.clone())
}
/// `Set.prototype.add( value )`
///
/// This method adds an entry with value into the set. Returns the set object
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-set.prototype.add
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add
pub(crate) fn add(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let value = args.get_or_undefined(0);
if let Some(object) = this.as_object() {
if let Some(set) = object.borrow_mut().as_set_mut() {
set.add(if value.as_number().map_or(false, |n| n == -0f64) {
JsValue::Integer(0)
} else {
value.clone()
});
} else {
return context.throw_type_error("'this' is not a Set");
}
} else {
return context.throw_type_error("'this' is not a Set");
};
Ok(this.clone())
}
/// `Set.prototype.clear( )`
///
/// This method removes all entries from the set.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-set.prototype.clear
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear
pub(crate) fn clear(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if let Some(object) = this.as_object() {
if object.borrow().is_set() {
this.set_data(ObjectData::set(OrderedSet::new()));
Ok(JsValue::undefined())
} else {
context.throw_type_error("'this' is not a Set")
}
} else {
context.throw_type_error("'this' is not a Set")
}
}
/// `Set.prototype.delete( value )`
///
/// This method removes the entry for the given value if it exists.
/// Returns true if there was an element, false otherwise.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-set.prototype.delete
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete
pub(crate) fn delete(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let value = args.get_or_undefined(0);
let res = if let Some(object) = this.as_object() {
if let Some(set) = object.borrow_mut().as_set_mut() {
set.delete(value)
} else {
return context.throw_type_error("'this' is not a Set");
}
} else {
return context.throw_type_error("'this' is not a Set");
};
Ok(res.into())
}
/// `Set.prototype.entries( )`
///
/// This method returns an iterator over the entries of the set
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-set.prototype.entries
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries
pub(crate) fn entries(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if let Some(object) = this.as_object() {
let object = object.borrow();
if !object.is_set() {
return context.throw_type_error(
"Method Set.prototype.entries called on incompatible receiver",
);
}
} else {
return context
.throw_type_error("Method Set.prototype.entries called on incompatible receiver");
}
Ok(SetIterator::create_set_iterator(
this.clone(),
PropertyNameKind::KeyAndValue,
context,
))
}
/// `Set.prototype.forEach( callbackFn [ , thisArg ] )`
///
/// This method executes the provided callback function for each value in the set
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-set.prototype.foreach
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/foreach
pub(crate) fn for_each(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if args.is_empty() {
return Err(JsValue::new("Missing argument for Set.prototype.forEach"));
}
let callback_arg = &args[0];
let this_arg = args.get_or_undefined(1);
// TODO: if condition should also check that we are not in strict mode
let this_arg = if this_arg.is_undefined() {
context.global_object().clone().into()
} else {
this_arg.clone()
};
let mut index = 0;
while index < Self::get_size(this, context)? {
let arguments = this
.as_object()
.and_then(|obj| {
obj.borrow().as_set_ref().map(|set| {
set.get_index(index)
.map(|value| [value.clone(), value.clone(), this.clone()])
})
})
.ok_or_else(|| context.construct_type_error("'this' is not a Set"))?;
if let Some(arguments) = arguments {
context.call(callback_arg, &this_arg, &arguments)?;
}
index += 1;
}
Ok(JsValue::Undefined)
}
/// `Map.prototype.has( key )`
///
/// This method checks if the map contains an entry with the given key.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
pub(crate) fn has(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let value = args.get_or_undefined(0);
this.as_object()
.and_then(|obj| {
obj.borrow()
.as_set_ref()
.map(|set| set.contains(value).into())
})
.ok_or_else(|| context.construct_type_error("'this' is not a Set"))
}
/// `Set.prototype.values( )`
///
/// This method returns an iterator over the values of the set
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-set.prototype.values
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/values
pub(crate) fn values(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if let Some(object) = this.as_object() {
let object = object.borrow();
if !object.is_set() {
return context.throw_type_error(
"Method Set.prototype.values called on incompatible receiver",
);
}
} else {
return context
.throw_type_error("Method Set.prototype.values called on incompatible receiver");
}
Ok(SetIterator::create_set_iterator(
this.clone(),
PropertyNameKind::Value,
context,
))
}
fn size_getter(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Self::get_size(this, context).map(JsValue::from)
}
/// Helper function to get the size of the `Set` object.
pub(crate) fn get_size(set: &JsValue, context: &mut Context) -> JsResult<usize> {
set.as_object()
.and_then(|obj| obj.borrow().as_set_ref().map(OrderedSet::size))
.ok_or_else(|| context.construct_type_error("'this' is not a Set"))
}
}

View File

@@ -0,0 +1,137 @@
use boa_gc::{custom_trace, Finalize, Trace};
use indexmap::{
set::{IntoIter, Iter},
IndexSet,
};
use std::{
collections::hash_map::RandomState,
fmt::Debug,
hash::{BuildHasher, Hash},
};
/// A type wrapping `indexmap::IndexSet`
#[derive(Clone)]
pub struct OrderedSet<V, S = RandomState>
where
V: Hash + Eq,
{
inner: IndexSet<V, S>,
}
impl<V: Eq + Hash + Trace, S: BuildHasher> Finalize for OrderedSet<V, S> {}
unsafe impl<V: Eq + Hash + Trace, S: BuildHasher> Trace for OrderedSet<V, S> {
custom_trace!(this, {
for v in this.inner.iter() {
mark(v);
}
});
}
impl<V: Hash + Eq + Debug> Debug for OrderedSet<V> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
self.inner.fmt(formatter)
}
}
impl<V: Hash + Eq> Default for OrderedSet<V> {
fn default() -> Self {
Self::new()
}
}
impl<V> OrderedSet<V>
where
V: Hash + Eq,
{
pub fn new() -> Self {
Self {
inner: IndexSet::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: IndexSet::with_capacity(capacity),
}
}
/// Return the number of key-value pairs in the map.
///
/// Computes in **O(1)** time.
pub fn size(&self) -> usize {
self.inner.len()
}
/// Returns true if the map contains no elements.
///
/// Computes in **O(1)** time.
pub fn is_empty(&self) -> bool {
self.inner.len() == 0
}
/// Insert a value pair in the set.
///
/// If an equivalent value already exists in the set: ???
///
/// If no equivalent value existed in the set: the new value is
/// inserted, last in order, and false
///
/// Computes in **O(1)** time (amortized average).
pub fn add(&mut self, value: V) -> bool {
self.inner.insert(value)
}
/// Delete the `value` from the set and return true if successful
///
/// Return `false` if `value` is not in map.
///
/// Computes in **O(n)** time (average).
pub fn delete(&mut self, value: &V) -> bool {
self.inner.shift_remove(value)
}
/// Checks if a given value is present in the set
///
/// Return `true` if `value` is present in set, false otherwise.
///
/// Computes in **O(n)** time (average).
pub fn contains(&self, value: &V) -> bool {
self.inner.contains(value)
}
/// Get a key-value pair by index
/// Valid indices are 0 <= index < self.len()
/// Computes in O(1) time.
pub fn get_index(&self, index: usize) -> Option<&V> {
self.inner.get_index(index)
}
/// Return an iterator over the values of the set, in their order
pub fn iter(&self) -> Iter<'_, V> {
self.inner.iter()
}
}
impl<'a, V, S> IntoIterator for &'a OrderedSet<V, S>
where
V: Hash + Eq,
S: BuildHasher,
{
type Item = &'a V;
type IntoIter = Iter<'a, V>;
fn into_iter(self) -> Self::IntoIter {
self.inner.iter()
}
}
impl<V, S> IntoIterator for OrderedSet<V, S>
where
V: Hash + Eq,
S: BuildHasher,
{
type Item = V;
type IntoIter = IntoIter<V>;
fn into_iter(self) -> IntoIter<V> {
self.inner.into_iter()
}
}

View File

@@ -0,0 +1,153 @@
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue},
object::{JsObject, ObjectData},
property::{PropertyDescriptor, PropertyNameKind},
symbol::WellKnownSymbols,
Context, JsResult,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
/// The Set Iterator object represents an iteration over a set. It implements the iterator protocol.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-set-iterator-objects
#[derive(Debug, Clone, Finalize, Trace)]
pub struct SetIterator {
iterated_set: JsValue,
next_index: usize,
iteration_kind: PropertyNameKind,
}
impl SetIterator {
pub(crate) const NAME: &'static str = "SetIterator";
/// Constructs a new `SetIterator`, that will iterate over `set`, starting at index 0
fn new(set: JsValue, kind: PropertyNameKind) -> Self {
Self {
iterated_set: set,
next_index: 0,
iteration_kind: kind,
}
}
/// Abstract operation `CreateSetIterator( set, kind )`
///
/// Creates a new iterator over the given set.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createsetiterator
pub(crate) fn create_set_iterator(
set: JsValue,
kind: PropertyNameKind,
context: &Context,
) -> JsValue {
let set_iterator = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.set_iterator(),
ObjectData::set_iterator(Self::new(set, kind)),
);
set_iterator.into()
}
/// %SetIteratorPrototype%.next( )
///
/// Advances the iterator and gets the next result in the set.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%setiteratorprototype%.next
pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let mut set_iterator = this.as_object().map(JsObject::borrow_mut);
let set_iterator = set_iterator
.as_mut()
.and_then(|obj| obj.as_set_iterator_mut())
.ok_or_else(|| context.construct_type_error("`this` is not an SetIterator"))?;
{
let m = &set_iterator.iterated_set;
let mut index = set_iterator.next_index;
let item_kind = &set_iterator.iteration_kind;
if set_iterator.iterated_set.is_undefined() {
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
}
let entries = m.as_object().map(JsObject::borrow);
let entries = entries
.as_ref()
.and_then(|obj| obj.as_set_ref())
.ok_or_else(|| context.construct_type_error("'this' is not a Set"))?;
let num_entries = entries.size();
while index < num_entries {
let e = entries.get_index(index);
index += 1;
set_iterator.next_index = index;
if let Some(value) = e {
match item_kind {
PropertyNameKind::Value => {
return Ok(create_iter_result_object(value.clone(), false, context));
}
PropertyNameKind::KeyAndValue => {
let result = Array::create_array_from_list(
[value.clone(), value.clone()],
context,
);
return Ok(create_iter_result_object(result.into(), false, context));
}
PropertyNameKind::Key => {
panic!("tried to collect only keys of Set")
}
}
}
}
}
set_iterator.iterated_set = JsValue::undefined();
Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
))
}
/// Create the `%SetIteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%setiteratorprototype%-object
pub(crate) fn create_prototype(
iterator_prototype: JsObject,
context: &mut Context,
) -> JsObject {
let _timer = Profiler::global().start_event(Self::NAME, "init");
// Create prototype
let set_iterator =
JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary());
make_builtin_fn(Self::next, "next", &set_iterator, 0, context);
let to_string_tag = WellKnownSymbols::to_string_tag();
let to_string_tag_property = PropertyDescriptor::builder()
.value("Set Iterator")
.writable(false)
.enumerable(false)
.configurable(true);
set_iterator.insert(to_string_tag, to_string_tag_property);
set_iterator
}
}

View File

@@ -0,0 +1,248 @@
use crate::{forward, Context};
#[test]
fn construct_empty() {
let mut context = Context::default();
let init = r#"
var empty = new Set();
"#;
forward(&mut context, init);
let result = forward(&mut context, "empty.size");
assert_eq!(result, "0");
}
#[test]
fn construct_from_array() {
let mut context = Context::default();
let init = r#"
let set = new Set(["one", "two"]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "set.size");
assert_eq!(result, "2");
}
#[test]
fn clone() {
let mut context = Context::default();
let init = r#"
let original = new Set(["one", "two"]);
let clone = new Set(original);
"#;
forward(&mut context, init);
let result = forward(&mut context, "clone.size");
assert_eq!(result, "2");
let result = forward(
&mut context,
r#"
original.add("three");
original.size"#,
);
assert_eq!(result, "3");
let result = forward(&mut context, "clone.size");
assert_eq!(result, "2");
}
#[test]
fn symbol_iterator() {
let mut context = Context::default();
let init = r#"
const set1 = new Set();
set1.add('foo');
set1.add('bar');
const iterator = set1[Symbol.iterator]();
let item1 = iterator.next();
let item2 = iterator.next();
let item3 = iterator.next();
"#;
forward(&mut context, init);
let result = forward(&mut context, "item1.value");
assert_eq!(result, "\"foo\"");
let result = forward(&mut context, "item1.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item2.value");
assert_eq!(result, "\"bar\"");
let result = forward(&mut context, "item2.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item3.value");
assert_eq!(result, "undefined");
let result = forward(&mut context, "item3.done");
assert_eq!(result, "true");
}
#[test]
fn entries() {
let mut context = Context::default();
let init = r#"
const set1 = new Set();
set1.add('foo');
set1.add('bar');
const entriesIterator = set1.entries();
let item1 = entriesIterator.next();
let item2 = entriesIterator.next();
let item3 = entriesIterator.next();
"#;
forward(&mut context, init);
let result = forward(&mut context, "item1.value.length");
assert_eq!(result, "2");
let result = forward(&mut context, "item1.value[0]");
assert_eq!(result, "\"foo\"");
let result = forward(&mut context, "item1.value[1]");
assert_eq!(result, "\"foo\"");
let result = forward(&mut context, "item1.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item2.value.length");
assert_eq!(result, "2");
let result = forward(&mut context, "item2.value[0]");
assert_eq!(result, "\"bar\"");
let result = forward(&mut context, "item2.value[1]");
assert_eq!(result, "\"bar\"");
let result = forward(&mut context, "item2.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item3.value");
assert_eq!(result, "undefined");
let result = forward(&mut context, "item3.done");
assert_eq!(result, "true");
}
#[test]
fn merge() {
let mut context = Context::default();
let init = r#"
let first = new Set(["one", "two"]);
let second = new Set(["three", "four"]);
let third = new Set(["four", "five"]);
let merged1 = new Set([...first, ...second]);
let merged2 = new Set([...second, ...third]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "merged1.size");
assert_eq!(result, "4");
let result = forward(&mut context, "merged2.size");
assert_eq!(result, "3");
}
#[test]
fn clear() {
let mut context = Context::default();
let init = r#"
let set = new Set(["one", "two"]);
set.clear();
"#;
forward(&mut context, init);
let result = forward(&mut context, "set.size");
assert_eq!(result, "0");
}
#[test]
fn delete() {
let mut context = Context::default();
let init = r#"
let set = new Set(["one", "two"]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "set.delete('one')");
assert_eq!(result, "true");
let result = forward(&mut context, "set.size");
assert_eq!(result, "1");
let result = forward(&mut context, "set.delete('one')");
assert_eq!(result, "false");
}
#[test]
fn has() {
let mut context = Context::default();
let init = r#"
let set = new Set(["one", "two"]);
"#;
forward(&mut context, init);
let result = forward(&mut context, "set.has('one')");
assert_eq!(result, "true");
let result = forward(&mut context, "set.has('two')");
assert_eq!(result, "true");
let result = forward(&mut context, "set.has('three')");
assert_eq!(result, "false");
let result = forward(&mut context, "set.has()");
assert_eq!(result, "false");
}
#[test]
fn values_and_keys() {
let mut context = Context::default();
let init = r#"
const set1 = new Set();
set1.add('foo');
set1.add('bar');
const valuesIterator = set1.values();
let item1 = valuesIterator.next();
let item2 = valuesIterator.next();
let item3 = valuesIterator.next();
"#;
forward(&mut context, init);
let result = forward(&mut context, "item1.value");
assert_eq!(result, "\"foo\"");
let result = forward(&mut context, "item1.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item2.value");
assert_eq!(result, "\"bar\"");
let result = forward(&mut context, "item2.done");
assert_eq!(result, "false");
let result = forward(&mut context, "item3.value");
assert_eq!(result, "undefined");
let result = forward(&mut context, "item3.done");
assert_eq!(result, "true");
let result = forward(&mut context, "set1.values == set1.keys");
assert_eq!(result, "true");
}
#[test]
fn for_each() {
let mut context = Context::default();
let init = r#"
let set = new Set([5, 10, 15]);
let value1Sum = 0;
let value2Sum = 0;
let sizeSum = 0;
function callingCallback(value1, value2, set) {
value1Sum += value1;
value2Sum += value2;
sizeSum += set.size;
}
set.forEach(callingCallback);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "value1Sum"), "30");
assert_eq!(forward(&mut context, "value2Sum"), "30");
assert_eq!(forward(&mut context, "sizeSum"), "9");
}
#[test]
fn recursive_display() {
let mut context = Context::default();
let init = r#"
let set = new Set();
let array = new Array([set]);
set.add(set);
"#;
forward(&mut context, init);
let result = forward(&mut context, "set");
assert_eq!(result, "Set { Set(1) }");
let result = forward(&mut context, "set.add(array)");
assert_eq!(result, "Set { Set(2), Array(1) }");
}
#[test]
fn not_a_function() {
let mut context = Context::default();
let init = r"
try {
let set = Set()
} catch(e) {
e.toString()
}
";
assert_eq!(
forward(&mut context, init),
"\"TypeError: calling a builtin Set constructor without new is forbidden\""
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
use crate::{
builtins::{
function::make_builtin_fn, iterable::create_iter_result_object, string::code_point_at,
},
object::{JsObject, ObjectData},
property::PropertyDescriptor,
symbol::WellKnownSymbols,
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
#[derive(Debug, Clone, Finalize, Trace)]
pub struct StringIterator {
string: JsValue,
next_index: i32,
}
impl StringIterator {
fn new(string: JsValue) -> Self {
Self {
string,
next_index: 0,
}
}
pub fn create_string_iterator(string: JsValue, context: &mut Context) -> JsResult<JsValue> {
let string_iterator = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.string_iterator(),
ObjectData::string_iterator(Self::new(string)),
);
Ok(string_iterator.into())
}
pub fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let mut string_iterator = this.as_object().map(JsObject::borrow_mut);
let string_iterator = string_iterator
.as_mut()
.and_then(|obj| obj.as_string_iterator_mut())
.ok_or_else(|| context.construct_type_error("`this` is not an ArrayIterator"))?;
if string_iterator.string.is_undefined() {
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
}
let native_string = string_iterator.string.to_string(context)?;
let len = native_string.encode_utf16().count() as i32;
let position = string_iterator.next_index;
if position >= len {
string_iterator.string = JsValue::undefined();
return Ok(create_iter_result_object(
JsValue::undefined(),
true,
context,
));
}
let (_, code_unit_count, _) = code_point_at(&native_string, position as usize);
string_iterator.next_index += i32::from(code_unit_count);
let result_string = crate::builtins::string::String::substring(
&string_iterator.string,
&[position.into(), string_iterator.next_index.into()],
context,
)?;
Ok(create_iter_result_object(result_string, false, context))
}
/// Create the `%ArrayIteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object
pub(crate) fn create_prototype(
iterator_prototype: JsObject,
context: &mut Context,
) -> JsObject {
let _timer = Profiler::global().start_event("String Iterator", "init");
// Create prototype
let array_iterator =
JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary());
make_builtin_fn(Self::next, "next", &array_iterator, 0, context);
let to_string_tag = WellKnownSymbols::to_string_tag();
let to_string_tag_property = PropertyDescriptor::builder()
.value("String Iterator")
.writable(false)
.enumerable(false)
.configurable(true);
array_iterator.insert(to_string_tag, to_string_tag_property);
array_iterator
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,346 @@
//! This module implements the global `Symbol` object.
//!
//! The data type symbol is a primitive data type.
//! The `Symbol()` function returns a value of type symbol, has static properties that expose
//! several members of built-in objects, has static methods that expose the global symbol registry,
//! and resembles a built-in object class, but is incomplete as a constructor because it does not
//! support the syntax "`new Symbol()`".
//!
//! Every symbol value returned from `Symbol()` is unique.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-symbol-value
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
#[cfg(test)]
mod tests;
use super::JsArgs;
use crate::{
builtins::BuiltIn,
object::{ConstructorBuilder, FunctionBuilder},
property::Attribute,
symbol::{JsSymbol, WellKnownSymbols},
value::JsValue,
Context, JsResult, JsString,
};
use boa_profiler::Profiler;
use rustc_hash::FxHashMap;
use std::cell::RefCell;
use tap::{Conv, Pipe};
thread_local! {
static GLOBAL_SYMBOL_REGISTRY: RefCell<GlobalSymbolRegistry> = RefCell::new(GlobalSymbolRegistry::new());
}
struct GlobalSymbolRegistry {
keys: FxHashMap<JsString, JsSymbol>,
symbols: FxHashMap<JsSymbol, JsString>,
}
impl GlobalSymbolRegistry {
fn new() -> Self {
Self {
keys: FxHashMap::default(),
symbols: FxHashMap::default(),
}
}
fn get_or_insert_key(&mut self, key: JsString) -> JsSymbol {
if let Some(symbol) = self.keys.get(&key) {
return symbol.clone();
}
let symbol = JsSymbol::new(Some(key.clone()));
self.keys.insert(key.clone(), symbol.clone());
self.symbols.insert(symbol.clone(), key);
symbol
}
fn get_symbol(&self, sym: &JsSymbol) -> Option<JsString> {
if let Some(key) = self.symbols.get(sym) {
return Some(key.clone());
}
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct Symbol;
impl BuiltIn for Symbol {
const NAME: &'static str = "Symbol";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let symbol_async_iterator = WellKnownSymbols::async_iterator();
let symbol_has_instance = WellKnownSymbols::has_instance();
let symbol_is_concat_spreadable = WellKnownSymbols::is_concat_spreadable();
let symbol_iterator = WellKnownSymbols::iterator();
let symbol_match = WellKnownSymbols::r#match();
let symbol_match_all = WellKnownSymbols::match_all();
let symbol_replace = WellKnownSymbols::replace();
let symbol_search = WellKnownSymbols::search();
let symbol_species = WellKnownSymbols::species();
let symbol_split = WellKnownSymbols::split();
let symbol_to_primitive = WellKnownSymbols::to_primitive();
let symbol_to_string_tag = WellKnownSymbols::to_string_tag();
let symbol_unscopables = WellKnownSymbols::unscopables();
let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT;
let to_primitive = FunctionBuilder::native(context, Self::to_primitive)
.name("[Symbol.toPrimitive]")
.length(1)
.constructor(false)
.build();
let get_description = FunctionBuilder::native(context, Self::get_description)
.name("get description")
.constructor(false)
.build();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().symbol().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.callable(true)
.constructor(true)
.static_method(Self::for_, "for", 1)
.static_method(Self::key_for, "keyFor", 1)
.static_property("asyncIterator", symbol_async_iterator, attribute)
.static_property("hasInstance", symbol_has_instance, attribute)
.static_property("isConcatSpreadable", symbol_is_concat_spreadable, attribute)
.static_property("iterator", symbol_iterator, attribute)
.static_property("match", symbol_match, attribute)
.static_property("matchAll", symbol_match_all, attribute)
.static_property("replace", symbol_replace, attribute)
.static_property("search", symbol_search, attribute)
.static_property("species", symbol_species, attribute)
.static_property("split", symbol_split, attribute)
.static_property("toPrimitive", symbol_to_primitive.clone(), attribute)
.static_property("toStringTag", symbol_to_string_tag.clone(), attribute)
.static_property("unscopables", symbol_unscopables, attribute)
.method(Self::to_string, "toString", 0)
.method(Self::value_of, "valueOf", 0)
.accessor(
"description",
Some(get_description),
None,
Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE,
)
.property(
symbol_to_string_tag,
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
symbol_to_primitive,
to_primitive,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Symbol {
/// The amount of arguments this function object takes.
pub(crate) const LENGTH: usize = 0;
/// The `Symbol()` constructor returns a value of type symbol.
///
/// It is incomplete as a constructor because it does not support
/// the syntax `new Symbol()` and it is not intended to be subclassed.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-symbol-description
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/Symbol
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. If NewTarget is not undefined, throw a TypeError exception.
if !new_target.is_undefined() {
return context.throw_type_error("Symbol is not a constructor");
}
// 2. If description is undefined, let descString be undefined.
// 3. Else, let descString be ? ToString(description).
let description = match args.get(0) {
Some(value) if !value.is_undefined() => Some(value.to_string(context)?),
_ => None,
};
// 4. Return a new unique Symbol value whose [[Description]] value is descString.
Ok(JsSymbol::new(description).into())
}
fn this_symbol_value(value: &JsValue, context: &mut Context) -> JsResult<JsSymbol> {
value
.as_symbol()
.or_else(|| value.as_object().and_then(|obj| obj.borrow().as_symbol()))
.ok_or_else(|| context.construct_type_error("'this' is not a Symbol"))
}
/// `Symbol.prototype.toString()`
///
/// This method returns a string representing the specified `Symbol` object.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.tostring
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toString
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_string(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let sym be ? thisSymbolValue(this value).
let symbol = Self::this_symbol_value(this, context)?;
// 2. Return SymbolDescriptiveString(sym).
Ok(symbol.descriptive_string().into())
}
/// `Symbol.prototype.valueOf()`
///
/// This method returns a `Symbol` that is the primitive value of the specified `Symbol` object.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript reference][spec]
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/valueOf
/// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.valueof
pub(crate) fn value_of(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Return ? thisSymbolValue(this value).
let symbol = Self::this_symbol_value(this, context)?;
Ok(JsValue::Symbol(symbol))
}
/// `get Symbol.prototype.description`
///
/// This accessor returns the description of the `Symbol` object.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.description
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/description
pub(crate) fn get_description(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let symbol = Self::this_symbol_value(this, context)?;
if let Some(ref description) = symbol.description() {
Ok(description.clone().into())
} else {
Ok(JsValue::undefined())
}
}
/// `Symbol.for( key )`
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.for
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for
pub(crate) fn for_(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let stringKey be ? ToString(key).
let string_key = args
.get(0)
.cloned()
.unwrap_or_default()
.to_string(context)?;
// 2. For each element e of the GlobalSymbolRegistry List, do
// a. If SameValue(e.[[Key]], stringKey) is true, return e.[[Symbol]].
// 3. Assert: GlobalSymbolRegistry does not currently contain an entry for stringKey.
// 4. Let newSymbol be a new unique Symbol value whose [[Description]] value is stringKey.
// 5. Append the Record { [[Key]]: stringKey, [[Symbol]]: newSymbol } to the GlobalSymbolRegistry List.
// 6. Return newSymbol.
Ok(GLOBAL_SYMBOL_REGISTRY
.with(move |registry| {
let mut registry = registry.borrow_mut();
registry.get_or_insert_key(string_key)
})
.into())
}
/// `Symbol.keyFor( sym )`
///
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.keyfor
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/keyFor
pub(crate) fn key_for(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let sym = args.get_or_undefined(0);
// 1. If Type(sym) is not Symbol, throw a TypeError exception.
if let Some(sym) = sym.as_symbol() {
// 2. For each element e of the GlobalSymbolRegistry List (see 20.4.2.2), do
// a. If SameValue(e.[[Symbol]], sym) is true, return e.[[Key]].
// 3. Assert: GlobalSymbolRegistry does not currently contain an entry for sym.
// 4. Return undefined.
let symbol = GLOBAL_SYMBOL_REGISTRY.with(move |registry| {
let registry = registry.borrow();
registry.get_symbol(&sym)
});
Ok(symbol.map(JsValue::from).unwrap_or_default())
} else {
context.throw_type_error("Symbol.keyFor: sym is not a symbol")
}
}
/// `Symbol.prototype [ @@toPrimitive ]`
///
/// This function is called by ECMAScript language operators to convert a Symbol object to a primitive value.
/// NOTE: The argument is ignored
///
/// More information:
/// - [MDN documentation][mdn]
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/multipage/#sec-symbol.prototype-@@toprimitive
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/@@toPrimitive
pub(crate) fn to_primitive(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let sym = Self::this_symbol_value(this, context)?;
// 1. Return ? thisSymbolValue(this value).
Ok(sym.into())
}
}

View File

@@ -0,0 +1,40 @@
use crate::{check_output, forward, forward_val, Context, TestAction};
#[test]
fn call_symbol_and_check_return_type() {
let mut context = Context::default();
let init = r#"
var sym = Symbol();
"#;
eprintln!("{}", forward(&mut context, init));
let sym = forward_val(&mut context, "sym").unwrap();
assert!(sym.is_symbol());
}
#[test]
fn print_symbol_expect_description() {
let mut context = Context::default();
let init = r#"
var sym = Symbol("Hello");
"#;
eprintln!("{}", forward(&mut context, init));
let sym = forward_val(&mut context, "sym.toString()").unwrap();
assert_eq!(sym.display().to_string(), "\"Symbol(Hello)\"");
}
#[test]
fn symbol_access() {
let init = r#"
var x = {};
var sym1 = Symbol("Hello");
var sym2 = Symbol("Hello");
x[sym1] = 10;
x[sym2] = 20;
"#;
check_output(&[
TestAction::Execute(init),
TestAction::TestEq("x[sym1]", "10"),
TestAction::TestEq("x[sym2]", "20"),
TestAction::TestEq("x['Symbol(Hello)']", "undefined"),
]);
}

View File

@@ -0,0 +1,151 @@
//! This module implements the `Integer-Indexed` exotic object.
//!
//! An `Integer-Indexed` exotic object is an exotic object that performs
//! special handling of integer index property keys.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects
use crate::{
builtins::typed_array::TypedArrayKind,
object::{JsObject, ObjectData},
Context,
};
use boa_gc::{unsafe_empty_trace, Finalize, Trace};
/// Type of the array content.
#[derive(Debug, Clone, Copy, Finalize, PartialEq)]
pub(crate) enum ContentType {
Number,
BigInt,
}
unsafe impl Trace for ContentType {
// safe because `ContentType` is `Copy`
unsafe_empty_trace!();
}
/// <https://tc39.es/ecma262/#integer-indexed-exotic-object>
#[derive(Debug, Clone, Trace, Finalize)]
pub struct IntegerIndexed {
viewed_array_buffer: Option<JsObject>,
typed_array_name: TypedArrayKind,
byte_offset: usize,
byte_length: usize,
array_length: usize,
}
impl IntegerIndexed {
pub(crate) fn new(
viewed_array_buffer: Option<JsObject>,
typed_array_name: TypedArrayKind,
byte_offset: usize,
byte_length: usize,
array_length: usize,
) -> Self {
Self {
viewed_array_buffer,
typed_array_name,
byte_offset,
byte_length,
array_length,
}
}
/// `IntegerIndexedObjectCreate ( prototype )`
///
/// Create a new `JsObject` from a prototype and a `IntegerIndexedObject`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-integerindexedobjectcreate
pub(super) fn create(prototype: JsObject, data: Self, context: &Context) -> JsObject {
// 1. Let internalSlotsList be « [[Prototype]], [[Extensible]], [[ViewedArrayBuffer]],
// [[TypedArrayName]], [[ContentType]], [[ByteLength]], [[ByteOffset]],
// [[ArrayLength]] ».
// 2. Let A be ! MakeBasicObject(internalSlotsList).
let a = context.construct_object();
// 3. Set A.[[GetOwnProperty]] as specified in 10.4.5.1.
// 4. Set A.[[HasProperty]] as specified in 10.4.5.2.
// 5. Set A.[[DefineOwnProperty]] as specified in 10.4.5.3.
// 6. Set A.[[Get]] as specified in 10.4.5.4.
// 7. Set A.[[Set]] as specified in 10.4.5.5.
// 8. Set A.[[Delete]] as specified in 10.4.5.6.
// 9. Set A.[[OwnPropertyKeys]] as specified in 10.4.5.7.
a.borrow_mut().data = ObjectData::integer_indexed(data);
// 10. Set A.[[Prototype]] to prototype.
a.set_prototype(prototype.into());
// 11. Return A.
a
}
/// Abstract operation `IsDetachedBuffer ( arrayBuffer )`.
///
/// Check if `[[ArrayBufferData]]` is null.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-isdetachedbuffer
pub(crate) fn is_detached(&self) -> bool {
if let Some(obj) = &self.viewed_array_buffer {
obj.borrow()
.as_array_buffer()
.expect("Typed array must have internal array buffer object")
.is_detached_buffer()
} else {
false
}
}
/// Get the integer indexed object's byte offset.
pub(crate) fn byte_offset(&self) -> usize {
self.byte_offset
}
/// Set the integer indexed object's byte offset.
pub(crate) fn set_byte_offset(&mut self, byte_offset: usize) {
self.byte_offset = byte_offset;
}
/// Get the integer indexed object's typed array name.
pub(crate) fn typed_array_name(&self) -> TypedArrayKind {
self.typed_array_name
}
/// Get a reference to the integer indexed object's viewed array buffer.
pub fn viewed_array_buffer(&self) -> Option<&JsObject> {
self.viewed_array_buffer.as_ref()
}
///(crate) Set the integer indexed object's viewed array buffer.
pub fn set_viewed_array_buffer(&mut self, viewed_array_buffer: Option<JsObject>) {
self.viewed_array_buffer = viewed_array_buffer;
}
/// Get the integer indexed object's byte length.
pub fn byte_length(&self) -> usize {
self.byte_length
}
/// Set the integer indexed object's byte length.
pub(crate) fn set_byte_length(&mut self, byte_length: usize) {
self.byte_length = byte_length;
}
/// Get the integer indexed object's array length.
pub fn array_length(&self) -> usize {
self.array_length
}
/// Set the integer indexed object's array length.
pub(crate) fn set_array_length(&mut self, array_length: usize) {
self.array_length = array_length;
}
}

Some files were not shown because too many files have changed in this diff Show More