feat: add dependency

This commit is contained in:
2023-01-20 22:36:19 +08:00
parent 68e8d103b4
commit cf8e579f27
644 changed files with 150099 additions and 14 deletions

View File

@@ -0,0 +1,95 @@
[package]
name = "boa_engine"
keywords = ["javascript", "js", "compiler", "lexer", "parser"]
categories = ["parser-implementations", "compilers"]
readme = "../README.md"
description.workspace = true
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[features]
profiler = ["boa_profiler/profiler"]
deser = ["boa_interner/serde", "boa_ast/serde"]
intl = [
"dep:boa_icu_provider",
"dep:icu_locid_transform",
"dep:icu_locid",
"dep:icu_datetime",
"dep:icu_plurals",
"dep:icu_provider",
"dep:icu_calendar",
"dep:icu_collator",
"dep:icu_list",
"dep:writeable",
"dep:sys-locale",
]
fuzz = ["boa_ast/fuzz", "boa_interner/fuzz"]
# Enable Boa's VM instruction flowgraph generator.
flowgraph = []
# Enable Boa's WHATWG console object implementation.
console = []
[dependencies]
boa_interner.workspace = true
boa_gc.workspace = true
boa_profiler.workspace = true
boa_macros.workspace = true
boa_ast.workspace = true
boa_parser.workspace = true
serde = { version = "1.0.152", features = ["derive", "rc"] }
serde_json = "1.0.91"
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.2"
ryu-js = "0.2.2"
chrono = "0.4.23"
fast-float = "0.2.0"
unicode-normalization = "0.1.22"
once_cell = "1.17.0"
tap = "1.0.1"
sptr = "0.3.2"
static_assertions = "1.1.0"
thiserror = "1.0.38"
dashmap = "5.4.0"
num_enum = "0.5.7"
# intl deps
boa_icu_provider = { workspace = true, optional = true }
icu_locid_transform = { version = "1.0.0", features = ["serde"], optional = true }
icu_locid = { version = "1.0.0", features = ["serde"], optional = true }
icu_datetime = { version = "1.0.0", features = ["serde", "experimental"], optional = true }
icu_calendar = { version = "1.0.0", optional = true }
icu_collator = { version = "1.0.1", features = ["serde"], optional = true }
icu_plurals = { version = "1.0.0", features = ["serde"], optional = true }
icu_provider = { version = "1.0.1", optional = true }
icu_list = { version = "1.0.0", features = ["serde"], optional = true }
writeable = { version = "0.5.0", optional = true }
sys-locale = { version = "0.2.3", optional = true }
[dev-dependencies]
criterion = "0.4.0"
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,95 @@
//! Benchmarks of the whole execution engine in Boa.
use boa_engine::{realm::Realm, Context};
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
#[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(None)));
}
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,465 @@
//! Boa's implementation of ECMAScript's bigint primitive type.
use crate::{builtins::Number, error::JsNativeError, JsResult};
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, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JsBigInt {
inner: Rc<RawBigInt>,
}
impl JsBigInt {
/// Create a new [`JsBigInt`].
#[must_use]
pub fn new<T: Into<Self>>(value: T) -> Self {
value.into()
}
/// Create a [`JsBigInt`] with value `0`.
#[inline]
#[must_use]
pub fn zero() -> Self {
Self {
inner: Rc::new(RawBigInt::zero()),
}
}
/// Check if is zero.
#[inline]
#[must_use]
pub fn is_zero(&self) -> bool {
self.inner.is_zero()
}
/// Create a [`JsBigInt`] with value `1`.
#[inline]
#[must_use]
pub fn one() -> Self {
Self {
inner: Rc::new(RawBigInt::one()),
}
}
/// Check if is one.
#[inline]
#[must_use]
pub fn is_one(&self) -> bool {
self.inner.is_one()
}
/// Convert bigint to string with radix.
#[inline]
#[must_use]
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]
#[must_use]
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]
#[must_use]
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]
#[must_use]
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]
#[must_use]
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]
#[must_use]
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]
#[must_use]
pub fn equal(x: &Self, y: &Self) -> bool {
x == y
}
/// Returns `x` to the power `y`.
#[inline]
pub fn pow(x: &Self, y: &Self) -> JsResult<Self> {
let y = y
.inner
.to_biguint()
.ok_or_else(|| JsNativeError::range().with_message("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 Err(JsNativeError::range()
.with_message("Maximum BigInt size exceeded")
.into());
}
Ok(Self::new(x.inner.as_ref().clone().pow(y)))
}
/// Performs the `>>` operation.
#[inline]
pub fn shift_right(x: &Self, y: &Self) -> JsResult<Self> {
match y.inner.to_i32() {
Some(n) if n > 0 => Ok(Self::new(x.inner.as_ref().clone().shr(n as usize))),
Some(n) => Ok(Self::new(x.inner.as_ref().clone().shl(n.unsigned_abs()))),
None => Err(JsNativeError::range()
.with_message("Maximum BigInt size exceeded")
.into()),
}
}
/// Performs the `<<` operation.
#[inline]
pub fn shift_left(x: &Self, y: &Self) -> JsResult<Self> {
match y.inner.to_i32() {
Some(n) if n > 0 => Ok(Self::new(x.inner.as_ref().clone().shl(n as usize))),
Some(n) => Ok(Self::new(x.inner.as_ref().clone().shr(n.unsigned_abs()))),
None => Err(JsNativeError::range()
.with_message("Maximum BigInt size exceeded")
.into()),
}
}
/// Floored integer modulo.
///
/// # Examples
/// ```
/// # use num_integer::Integer;
/// assert_eq!((8).mod_floor(&3), 2);
/// assert_eq!((8).mod_floor(&-3), -1);
/// ```
#[inline]
#[must_use]
pub fn mod_floor(x: &Self, y: &Self) -> Self {
Self::new(x.inner.mod_floor(&y.inner))
}
/// Performs the `+` operation.
#[inline]
#[must_use]
pub fn add(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().add(y.inner.as_ref()))
}
/// Performs the `-` operation.
#[inline]
#[must_use]
pub fn sub(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().sub(y.inner.as_ref()))
}
/// Performs the `*` operation.
#[inline]
#[must_use]
pub fn mul(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().mul(y.inner.as_ref()))
}
/// Performs the `/` operation.
#[inline]
#[must_use]
pub fn div(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().div(y.inner.as_ref()))
}
/// Performs the `%` operation.
#[inline]
#[must_use]
pub fn rem(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().rem(y.inner.as_ref()))
}
/// Performs the `&` operation.
#[inline]
#[must_use]
pub fn bitand(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().bitand(y.inner.as_ref()))
}
/// Performs the `|` operation.
#[inline]
#[must_use]
pub fn bitor(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().bitor(y.inner.as_ref()))
}
/// Performs the `^` operation.
#[inline]
#[must_use]
pub fn bitxor(x: &Self, y: &Self) -> Self {
Self::new(x.inner.as_ref().clone().bitxor(y.inner.as_ref()))
}
/// Performs the unary `-` operation.
#[inline]
#[must_use]
pub fn neg(x: &Self) -> Self {
Self::new(x.as_inner().neg())
}
/// Performs the unary `!` operation.
#[inline]
#[must_use]
pub fn not(x: &Self) -> Self {
Self::new(!x.as_inner())
}
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)),
}
}
}
/// The error indicates that the conversion from [`f64`] to [`JsBigInt`] failed.
#[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);
}
RawBigInt::from_f64(n).map_or(Err(TryFromF64Error), |bigint| Ok(Self::new(bigint)))
}
}
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 {
other.fract().is_zero()
&& RawBigInt::from_f64(*other).map_or(false, |bigint| self.inner.as_ref() == &bigint)
}
}
impl PartialEq<JsBigInt> for f64 {
#[inline]
fn eq(&self, other: &JsBigInt) -> bool {
self.fract().is_zero()
&& RawBigInt::from_f64(*self).map_or(false, |bigint| other.inner.as_ref() == &bigint)
}
}

View File

@@ -0,0 +1,160 @@
//! This module implements the `ArrayIterator` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-array-iterator-objects
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue},
error::JsNativeError,
object::{JsObject, ObjectData},
property::{PropertyDescriptor, PropertyNameKind},
symbol::JsSymbol,
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: u64,
#[unsafe_ignore_trace]
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(|| JsNativeError::typ().with_message("`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 Err(JsNativeError::typ()
.with_message(
"Cannot get value from typed array that has a detached array buffer",
)
.into());
}
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 = JsSymbol::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,842 @@
//! Boa's implementation of ECMAScript's global `ArrayBuffer` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-arraybuffer-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
#[cfg(test)]
mod tests;
use crate::{
builtins::{typed_array::TypedArrayKind, BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
error::JsNativeError,
native_function::NativeFunction,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder,
FunctionObjectBuilder, JsObject, ObjectData,
},
property::Attribute,
symbol::JsSymbol,
value::{IntegerOrInfinity, Numeric},
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use num_traits::{Signed, ToPrimitive};
use tap::{Conv, Pipe};
/// The internal representation of an `ArrayBuffer` object.
#[derive(Debug, Clone, Trace, Finalize)]
pub struct ArrayBuffer {
/// The `[[ArrayBufferData]]` internal slot.
pub array_buffer_data: Option<Vec<u8>>,
/// The `[[ArrayBufferByteLength]]` internal slot.
pub array_buffer_byte_length: u64,
/// The `[[ArrayBufferDetachKey]]` internal slot.
pub array_buffer_detach_key: JsValue,
}
impl ArrayBuffer {
pub(crate) const fn array_buffer_byte_length(&self) -> u64 {
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 =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::get_species))
.name("get [Symbol.species]")
.constructor(false)
.build();
let get_byte_length =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(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(
JsSymbol::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.static_method(Self::is_view, "isView", 1)
.method(Self::slice, "slice", 2)
.property(
JsSymbol::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 Err(JsNativeError::typ()
.with_message("ArrayBuffer.constructor called with undefined new target")
.into());
}
// 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],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
let obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("ArrayBuffer.byteLength called with non-object value")
})?;
let obj = obj.borrow();
let buf = obj.as_array_buffer().ok_or_else(|| {
JsNativeError::typ().with_message("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(buf) {
return Ok(0.into());
}
// 5. Let length be O.[[ArrayBufferByteLength]].
// 6. Return 𝔽(length).
Ok(buf.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 = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("ArrayBuffer.slice called with non-object value")
})?;
let obj_borrow = obj.borrow();
let buf = obj_borrow.as_array_buffer().ok_or_else(|| {
JsNativeError::typ().with_message("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(buf) {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer.slice called with detached buffer")
.into());
}
// 5. Let len be O.[[ArrayBufferByteLength]].
let len = buf.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 u64;
// 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(|| {
JsNativeError::typ().with_message("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 Err(JsNativeError::typ()
.with_message("ArrayBuffer constructor returned detached ArrayBuffer")
.into());
}
}
// 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 Err(JsNativeError::typ()
.with_message("New ArrayBuffer is the same as this ArrayBuffer")
.into());
}
{
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 Err(JsNativeError::typ()
.with_message("New ArrayBuffer length too small")
.into());
}
// 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(buf) {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer detached while ArrayBuffer.slice was running")
.into());
}
// 24. Let fromBuf be O.[[ArrayBufferData]].
let from_buf = buf
.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 as usize);
}
// 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: u64,
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,
)?;
// 2. Let block be ? CreateByteDataBlock(byteLength).
let block = create_byte_data_block(byte_length)?;
// 3. Set obj.[[ArrayBufferData]] to block.
// 4. Set obj.[[ArrayBufferByteLength]] to byteLength.
let obj = JsObject::from_proto_and_data(
prototype,
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) const 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: u64,
src_length: u64,
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 = self.array_buffer_data.as_deref().ok_or_else(|| {
JsNativeError::syntax().with_message("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 as usize,
src_length as usize,
);
}
// 6. Return targetBuffer.
Ok(target_buffer)
}
/// `25.1.2.6 IsUnclampedIntegerElementType ( type )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-isunclampedintegerelementtype
const 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
const 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)]
const 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: u64,
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() as usize;
// 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 byte_index = byte_index as usize;
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: u64,
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 as usize + 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: u64) -> 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 size = size.try_into().map_err(|e| {
JsNativeError::range().with_message(format!("couldn't allocate the data block: {e}"))
})?;
let mut data_block = Vec::new();
data_block.try_reserve(size).map_err(|e| {
JsNativeError::range().with_message(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,11 @@
use super::*;
#[test]
fn ut_sunny_day_create_byte_data_block() {
assert!(create_byte_data_block(100).is_ok());
}
#[test]
fn ut_rainy_day_create_byte_data_block() {
assert!(create_byte_data_block(u64::MAX).is_err());
}

View File

@@ -0,0 +1,109 @@
//! Boa's implementation of ECMAScript's 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,
},
native_function::NativeFunction,
object::ObjectData,
property::PropertyDescriptor,
symbol::JsSymbol,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
/// The internal representation of an `AsyncFunction` object.
#[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: NativeFunction::from_fn_ptr(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(JsSymbol::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,777 @@
//! Boa's implementation of ECMAScript's global `AsyncGenerator` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-asyncgenerator-objects
use crate::{
builtins::{
generator::GeneratorContext, iterable::create_iter_result_object,
promise::if_abrupt_reject_promise, promise::PromiseCapability, BuiltIn, JsArgs, Promise,
},
error::JsNativeError,
native_function::NativeFunction,
object::{ConstructorBuilder, FunctionObjectBuilder, JsObject, ObjectData},
property::{Attribute, PropertyDescriptor},
symbol::JsSymbol,
value::JsValue,
vm::GeneratorResumeKind,
Context, JsError, JsResult,
};
use boa_gc::{Finalize, Gc, GcCell, Trace};
use boa_profiler::Profiler;
use std::collections::VecDeque;
/// Indicates the state of an async generator.
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum AsyncGeneratorState {
Undefined,
SuspendedStart,
SuspendedYield,
Executing,
AwaitingReturn,
Completed,
}
/// `AsyncGeneratorRequest Records`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorrequest-records
#[derive(Debug, Clone, Finalize, Trace)]
pub(crate) struct AsyncGeneratorRequest {
/// The `[[Completion]]` slot.
pub(crate) completion: (JsResult<JsValue>, bool),
/// The `[[Capability]]` slot.
capability: PromiseCapability,
}
/// The internal representation of an `AsyncGenerator` object.
#[derive(Debug, Clone, Finalize, Trace)]
pub struct AsyncGenerator {
/// The `[[AsyncGeneratorState]]` internal slot.
#[unsafe_ignore_trace]
pub(crate) state: AsyncGeneratorState,
/// The `[[AsyncGeneratorContext]]` internal slot.
pub(crate) context: Option<Gc<GcCell<GeneratorContext>>>,
/// The `[[AsyncGeneratorQueue]]` internal slot.
pub(crate) queue: VecDeque<AsyncGeneratorRequest>,
}
impl BuiltIn for AsyncGenerator {
const NAME: &'static str = "AsyncGenerator";
fn init(context: &mut Context<'_>) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let iterator_prototype = context
.intrinsics()
.objects()
.iterator_prototypes()
.async_iterator_prototype();
let generator_function_prototype = context
.intrinsics()
.constructors()
.async_generator_function()
.prototype();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context
.intrinsics()
.constructors()
.async_generator()
.clone(),
)
.name(Self::NAME)
.length(0)
.property(
JsSymbol::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()
.async_generator()
.prototype
.insert_property(
"constructor",
PropertyDescriptor::builder()
.value(generator_function_prototype)
.writable(false)
.enumerable(false)
.configurable(true),
);
None
}
}
impl AsyncGenerator {
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn constructor(
_: &JsValue,
_: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
let prototype = context
.intrinsics()
.constructors()
.async_generator()
.prototype();
let this = JsObject::from_proto_and_data(
prototype,
ObjectData::async_generator(Self {
state: AsyncGeneratorState::Undefined,
context: None,
queue: VecDeque::new(),
}),
);
Ok(this.into())
}
/// `AsyncGenerator.prototype.next ( value )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgenerator-prototype-next
pub(crate) fn next(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let generator be the this value.
let generator = this;
// 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
context,
)
.expect("cannot fail with promise constructor");
// 3. Let result be Completion(AsyncGeneratorValidate(generator, empty)).
// 4. IfAbruptRejectPromise(result, promiseCapability).
let generator_object: JsResult<_> = generator.as_object().ok_or_else(|| {
JsNativeError::typ()
.with_message("generator resumed on non generator object")
.into()
});
if_abrupt_reject_promise!(generator_object, promise_capability, context);
let mut generator_obj_mut = generator_object.borrow_mut();
let generator: JsResult<_> = generator_obj_mut.as_async_generator_mut().ok_or_else(|| {
JsNativeError::typ()
.with_message("generator resumed on non generator object")
.into()
});
if_abrupt_reject_promise!(generator, promise_capability, context);
// 5. Let state be generator.[[AsyncGeneratorState]].
let state = generator.state;
// 6. If state is completed, then
if state == AsyncGeneratorState::Completed {
drop(generator_obj_mut);
// a. Let iteratorResult be CreateIterResultObject(undefined, true).
let iterator_result = create_iter_result_object(JsValue::undefined(), true, context);
// b. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »).
promise_capability
.resolve()
.call(&JsValue::undefined(), &[iterator_result], context)
.expect("cannot fail per spec");
// c. Return promiseCapability.[[Promise]].
return Ok(promise_capability.promise().clone().into());
}
// 7. Let completion be NormalCompletion(value).
let completion = (Ok(args.get_or_undefined(0).clone()), false);
// 8. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability).
generator.enqueue(completion.clone(), promise_capability.clone());
// 9. If state is either suspendedStart or suspendedYield, then
if state == AsyncGeneratorState::SuspendedStart
|| state == AsyncGeneratorState::SuspendedYield
{
// a. Perform AsyncGeneratorResume(generator, completion).
let generator_context = generator
.context
.clone()
.expect("generator context cannot be empty here");
drop(generator_obj_mut);
Self::resume(
generator_object,
state,
&generator_context,
completion,
context,
);
}
// 11. Return promiseCapability.[[Promise]].
Ok(promise_capability.promise().clone().into())
}
/// `AsyncGenerator.prototype.return ( value )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgenerator-prototype-return
pub(crate) fn r#return(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let generator be the this value.
let generator = this;
// 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
context,
)
.expect("cannot fail with promise constructor");
// 3. Let result be Completion(AsyncGeneratorValidate(generator, empty)).
// 4. IfAbruptRejectPromise(result, promiseCapability).
let generator_object: JsResult<_> = generator.as_object().ok_or_else(|| {
JsNativeError::typ()
.with_message("generator resumed on non generator object")
.into()
});
if_abrupt_reject_promise!(generator_object, promise_capability, context);
let mut generator_obj_mut = generator_object.borrow_mut();
let generator: JsResult<_> = generator_obj_mut.as_async_generator_mut().ok_or_else(|| {
JsNativeError::typ()
.with_message("generator resumed on non generator object")
.into()
});
if_abrupt_reject_promise!(generator, promise_capability, context);
// 5. Let completion be Completion Record { [[Type]]: return, [[Value]]: value, [[Target]]: empty }.
let completion = (Ok(args.get_or_undefined(0).clone()), true);
// 6. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability).
generator.enqueue(completion.clone(), promise_capability.clone());
// 7. Let state be generator.[[AsyncGeneratorState]].
let state = generator.state;
// 8. If state is either suspendedStart or completed, then
if state == AsyncGeneratorState::SuspendedStart || state == AsyncGeneratorState::Completed {
// a. Set generator.[[AsyncGeneratorState]] to awaiting-return.
generator.state = AsyncGeneratorState::AwaitingReturn;
// b. Perform ! AsyncGeneratorAwaitReturn(generator).
let next = generator
.queue
.front()
.cloned()
.expect("queue cannot be empty here");
drop(generator_obj_mut);
let (completion, _) = &next.completion;
Self::await_return(generator_object.clone(), completion.clone(), context);
}
// 9. Else if state is suspendedYield, then
else if state == AsyncGeneratorState::SuspendedYield {
// a. Perform AsyncGeneratorResume(generator, completion).
let generator_context = generator
.context
.clone()
.expect("generator context cannot be empty here");
drop(generator_obj_mut);
Self::resume(
generator_object,
state,
&generator_context,
completion,
context,
);
}
// 11. Return promiseCapability.[[Promise]].
Ok(promise_capability.promise().clone().into())
}
/// `AsyncGenerator.prototype.throw ( exception )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgenerator-prototype-throw
pub(crate) fn throw(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let generator be the this value.
let generator = this;
// 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
context,
)
.expect("cannot fail with promise constructor");
// 3. Let result be Completion(AsyncGeneratorValidate(generator, empty)).
// 4. IfAbruptRejectPromise(result, promiseCapability).
let generator_object: JsResult<_> = generator.as_object().ok_or_else(|| {
JsNativeError::typ()
.with_message("generator resumed on non generator object")
.into()
});
if_abrupt_reject_promise!(generator_object, promise_capability, context);
let mut generator_obj_mut = generator_object.borrow_mut();
let generator: JsResult<_> = generator_obj_mut.as_async_generator_mut().ok_or_else(|| {
JsNativeError::typ()
.with_message("generator resumed on non generator object")
.into()
});
if_abrupt_reject_promise!(generator, promise_capability, context);
// 5. Let state be generator.[[AsyncGeneratorState]].
let mut state = generator.state;
// 6. If state is suspendedStart, then
if state == AsyncGeneratorState::SuspendedStart {
// a. Set generator.[[AsyncGeneratorState]] to completed.
generator.state = AsyncGeneratorState::Completed;
// b. Set state to completed.
state = AsyncGeneratorState::Completed;
}
// 7. If state is completed, then
if state == AsyncGeneratorState::Completed {
drop(generator_obj_mut);
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « exception »).
promise_capability
.reject()
.call(
&JsValue::undefined(),
&[args.get_or_undefined(0).clone()],
context,
)
.expect("cannot fail per spec");
// b. Return promiseCapability.[[Promise]].
return Ok(promise_capability.promise().clone().into());
}
// 8. Let completion be ThrowCompletion(exception).
let completion = (
Err(JsError::from_opaque(args.get_or_undefined(0).clone())),
false,
);
// 9. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability).
generator.enqueue(completion.clone(), promise_capability.clone());
// 10. If state is suspendedYield, then
if state == AsyncGeneratorState::SuspendedYield {
let generator_context = generator
.context
.clone()
.expect("generator context cannot be empty here");
drop(generator_obj_mut);
// a. Perform AsyncGeneratorResume(generator, completion).
Self::resume(
generator_object,
state,
&generator_context,
completion,
context,
);
}
// 12. Return promiseCapability.[[Promise]].
Ok(promise_capability.promise().clone().into())
}
/// `AsyncGeneratorEnqueue ( generator, completion, promiseCapability )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorenqueue
pub(crate) fn enqueue(
&mut self,
completion: (JsResult<JsValue>, bool),
promise_capability: PromiseCapability,
) {
// 1. Let request be AsyncGeneratorRequest { [[Completion]]: completion, [[Capability]]: promiseCapability }.
let request = AsyncGeneratorRequest {
completion,
capability: promise_capability,
};
// 2. Append request to the end of generator.[[AsyncGeneratorQueue]].
self.queue.push_back(request);
}
/// `AsyncGeneratorCompleteStep ( generator, completion, done [ , realm ] )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorcompletestep
pub(crate) fn complete_step(
next: &AsyncGeneratorRequest,
completion: JsResult<JsValue>,
done: bool,
context: &mut Context<'_>,
) {
// 1. Let queue be generator.[[AsyncGeneratorQueue]].
// 2. Assert: queue is not empty.
// 3. Let next be the first element of queue.
// 4. Remove the first element from queue.
// 5. Let promiseCapability be next.[[Capability]].
let promise_capability = &next.capability;
// 6. Let value be completion.[[Value]].
match completion {
// 7. If completion.[[Type]] is throw, then
Err(e) => {
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « value »).
promise_capability
.reject()
.call(&JsValue::undefined(), &[e.to_opaque(context)], context)
.expect("cannot fail per spec");
}
// 8. Else,
Ok(value) => {
// a. Assert: completion.[[Type]] is normal.
// TODO: Realm handling not implemented yet.
// b. If realm is present, then
// i. Let oldRealm be the running execution context's Realm.
// ii. Set the running execution context's Realm to realm.
// iii. Let iteratorResult be CreateIterResultObject(value, done).
// iv. Set the running execution context's Realm to oldRealm.
// c. Else,
// i. Let iteratorResult be CreateIterResultObject(value, done).
let iterator_result = create_iter_result_object(value, done, context);
// d. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iteratorResult »).
promise_capability
.resolve()
.call(&JsValue::undefined(), &[iterator_result], context)
.expect("cannot fail per spec");
}
}
}
/// `AsyncGeneratorResume ( generator, completion )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorresume
pub(crate) fn resume(
generator: &JsObject,
state: AsyncGeneratorState,
generator_context: &Gc<GcCell<GeneratorContext>>,
completion: (JsResult<JsValue>, bool),
context: &mut Context<'_>,
) {
// 1. Assert: generator.[[AsyncGeneratorState]] is either suspendedStart or suspendedYield.
assert!(
state == AsyncGeneratorState::SuspendedStart
|| state == AsyncGeneratorState::SuspendedYield
);
// 2. Let genContext be generator.[[AsyncGeneratorContext]].
let mut generator_context_mut = generator_context.borrow_mut();
// 3. Let callerContext be the running execution context.
// 4. Suspend callerContext.
// 5. Set generator.[[AsyncGeneratorState]] to executing.
generator
.borrow_mut()
.as_async_generator_mut()
.expect("already checked before")
.state = AsyncGeneratorState::Executing;
// 6. Push genContext onto the execution context stack; genContext is now the running execution context.
std::mem::swap(
&mut context.realm.environments,
&mut generator_context_mut.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context_mut.stack);
context
.vm
.push_frame(generator_context_mut.call_frame.clone());
// 7. Resume the suspended evaluation of genContext using completion as the result of the operation that suspended it. Let result be the Completion Record returned by the resumed computation.
match completion {
(Ok(value), r#return) => {
context.vm.push(value);
if r#return {
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Return;
} else {
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal;
}
}
(Err(value), _) => {
let value = value.to_opaque(context);
context.vm.push(value);
context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw;
}
}
drop(generator_context_mut);
let result = context.run();
let mut generator_context_mut = generator_context.borrow_mut();
std::mem::swap(
&mut context.realm.environments,
&mut generator_context_mut.environments,
);
std::mem::swap(&mut context.vm.stack, &mut generator_context_mut.stack);
generator_context_mut.call_frame =
context.vm.pop_frame().expect("generator frame must exist");
drop(generator_context_mut);
// 8. Assert: result is never an abrupt completion.
assert!(result.is_ok());
// 9. Assert: When we return here, genContext has already been removed from the execution context stack and callerContext is the currently running execution context.
// 10. Return unused.
}
/// `AsyncGeneratorAwaitReturn ( generator )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn
pub(crate) fn await_return(
generator: JsObject,
completion: JsResult<JsValue>,
context: &mut Context<'_>,
) {
// 1. Let queue be generator.[[AsyncGeneratorQueue]].
// 2. Assert: queue is not empty.
// 3. Let next be the first element of queue.
// 4. Let completion be Completion(next.[[Completion]]).
// 5. Assert: completion.[[Type]] is return.
let value = completion.expect("completion must be a return completion");
// Note: The spec is currently broken here.
// See: https://github.com/tc39/ecma262/pull/2683
// 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]).
let promise_completion = Promise::promise_resolve(
context.intrinsics().constructors().promise().constructor(),
value,
context,
);
let promise = match promise_completion {
Ok(value) => value,
Err(value) => {
let mut generator_borrow_mut = generator.borrow_mut();
let gen = generator_borrow_mut
.as_async_generator_mut()
.expect("already checked before");
gen.state = AsyncGeneratorState::Completed;
let next = gen.queue.pop_front().expect("queue must not be empty");
drop(generator_borrow_mut);
Self::complete_step(&next, Err(value), true, context);
Self::drain_queue(&generator, context);
return;
}
};
// 7. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures generator and performs the following steps when called:
// 8. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »).
let on_fulfilled = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_this, args, generator, context| {
let next = {
let mut generator_borrow_mut = generator.borrow_mut();
let gen = generator_borrow_mut
.as_async_generator_mut()
.expect("already checked before");
// a. Set generator.[[AsyncGeneratorState]] to completed.
gen.state = AsyncGeneratorState::Completed;
gen.queue.pop_front().expect("must have one entry")
};
// b. Let result be NormalCompletion(value).
let result = Ok(args.get_or_undefined(0).clone());
// c. Perform AsyncGeneratorCompleteStep(generator, result, true).
Self::complete_step(&next, result, true, context);
// d. Perform AsyncGeneratorDrainQueue(generator).
Self::drain_queue(generator, context);
// e. Return undefined.
Ok(JsValue::undefined())
},
generator.clone(),
),
)
.name("")
.length(1)
.build();
// 9. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures generator and performs the following steps when called:
// 10. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
let on_rejected = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_this, args, generator, context| {
let mut generator_borrow_mut = generator.borrow_mut();
let gen = generator_borrow_mut
.as_async_generator_mut()
.expect("already checked before");
// a. Set generator.[[AsyncGeneratorState]] to completed.
gen.state = AsyncGeneratorState::Completed;
// b. Let result be ThrowCompletion(reason).
let result = Err(JsError::from_opaque(args.get_or_undefined(0).clone()));
// c. Perform AsyncGeneratorCompleteStep(generator, result, true).
let next = gen.queue.pop_front().expect("must have one entry");
drop(generator_borrow_mut);
Self::complete_step(&next, result, true, context);
// d. Perform AsyncGeneratorDrainQueue(generator).
Self::drain_queue(generator, context);
// e. Return undefined.
Ok(JsValue::undefined())
},
generator,
),
)
.name("")
.length(1)
.build();
// 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected).
Promise::perform_promise_then(
&promise,
&on_fulfilled.into(),
&on_rejected.into(),
None,
context,
);
}
/// `AsyncGeneratorDrainQueue ( generator )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratordrainqueue
pub(crate) fn drain_queue(generator: &JsObject, context: &mut Context<'_>) {
let mut generator_borrow_mut = generator.borrow_mut();
let gen = generator_borrow_mut
.as_async_generator_mut()
.expect("already checked before");
// 1. Assert: generator.[[AsyncGeneratorState]] is completed.
assert_eq!(gen.state, AsyncGeneratorState::Completed);
// 2. Let queue be generator.[[AsyncGeneratorQueue]].
let queue = &mut gen.queue;
// 3. If queue is empty, return unused.
if queue.is_empty() {
return;
}
// 4. Let done be false.
// 5. Repeat, while done is false,
loop {
// a. Let next be the first element of queue.
let next = queue.front().expect("must have entry");
// b. Let completion be Completion(next.[[Completion]]).
match &next.completion {
// c. If completion.[[Type]] is return, then
(completion, true) => {
// i. Set generator.[[AsyncGeneratorState]] to awaiting-return.
gen.state = AsyncGeneratorState::AwaitingReturn;
// ii. Perform ! AsyncGeneratorAwaitReturn(generator).
let completion = completion.clone();
drop(generator_borrow_mut);
Self::await_return(generator.clone(), completion, context);
// iii. Set done to true.
break;
}
// d. Else,
(completion, false) => {
// i. If completion.[[Type]] is normal, then
let completion = if completion.is_ok() {
// 1. Set completion to NormalCompletion(undefined).
Ok(JsValue::undefined())
} else {
completion.clone()
};
// ii. Perform AsyncGeneratorCompleteStep(generator, completion, true).
let next = queue.pop_front().expect("must have entry");
Self::complete_step(&next, completion, true, context);
// iii. If queue is empty, set done to true.
if queue.is_empty() {
break;
}
}
}
}
}
}

View File

@@ -0,0 +1,130 @@
//! Boa's implementation of ECMAScript's `AsyncGeneratorFunction` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorfunction-objects
use crate::{
builtins::{
function::{BuiltInFunctionObject, ConstructorKind, Function},
BuiltIn,
},
native_function::NativeFunction,
object::ObjectData,
property::PropertyDescriptor,
symbol::JsSymbol,
value::JsValue,
Context, JsResult,
};
use boa_profiler::Profiler;
/// The internal representation of an `AsyncGeneratorFunction` object.
#[derive(Debug, Clone, Copy)]
pub struct AsyncGeneratorFunction;
impl BuiltIn for AsyncGeneratorFunction {
const NAME: &'static str = "AsyncGeneratorFunction";
fn init(context: &mut Context<'_>) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let prototype = &context
.intrinsics()
.constructors()
.async_generator_function()
.prototype;
let constructor = &context
.intrinsics()
.constructors()
.async_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(Self::NAME)
.writable(false)
.enumerable(false)
.configurable(true);
constructor.borrow_mut().insert("name", property);
let property = PropertyDescriptor::builder()
.value(
context
.intrinsics()
.constructors()
.async_generator_function()
.prototype(),
)
.writable(false)
.enumerable(false)
.configurable(false);
constructor.borrow_mut().insert("prototype", property);
constructor.borrow_mut().data = ObjectData::function(Function::Native {
function: NativeFunction::from_fn_ptr(Self::constructor),
constructor: Some(ConstructorKind::Base),
});
prototype.set_prototype(Some(
context.intrinsics().constructors().function().prototype(),
));
let property = PropertyDescriptor::builder()
.value(
context
.intrinsics()
.constructors()
.async_generator_function()
.constructor(),
)
.writable(false)
.enumerable(false)
.configurable(true);
prototype.borrow_mut().insert("constructor", property);
let property = PropertyDescriptor::builder()
.value(
context
.intrinsics()
.constructors()
.async_generator()
.prototype(),
)
.writable(false)
.enumerable(false)
.configurable(true);
prototype.borrow_mut().insert("prototype", property);
let property = PropertyDescriptor::builder()
.value("AsyncGeneratorFunction")
.writable(false)
.enumerable(false)
.configurable(true);
prototype
.borrow_mut()
.insert(JsSymbol::to_string_tag(), property);
None
}
}
impl AsyncGeneratorFunction {
/// `AsyncGeneratorFunction ( p1, p2, … , pn, body )`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncgeneratorfunction
pub(crate) fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
BuiltInFunctionObject::create_dynamic_function(new_target, args, true, true, context)
.map(Into::into)
}
}

View File

@@ -0,0 +1,295 @@
//! Boa's implementation of ECMAScript's 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},
error::JsNativeError,
object::ConstructorBuilder,
property::Attribute,
symbol::JsSymbol,
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 = JsSymbol::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 Err(JsNativeError::typ()
.with_message("BigInt is not a constructor")
.into());
}
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);
}
// 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
fn number_to_bigint(number: f64) -> JsResult<JsValue> {
// 1. If IsIntegralNumber(number) is false, throw a RangeError exception.
if number.is_nan() || number.is_infinite() || number.fract() != 0.0 {
return Err(JsNativeError::range()
.with_message(format!("Cannot convert {number} to BigInt"))
.into());
}
// 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
fn this_bigint_value(value: &JsValue) -> 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(|| {
JsNativeError::typ()
.with_message("'this' is not a BigInt")
.into()
})
}
/// `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)?;
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 Err(JsNativeError::range()
.with_message("radix must be an integer at least 2 and no greater than 36")
.into())
}
};
// 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],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Ok(JsValue::new(Self::this_bigint_value(this)?))
}
/// `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))?
{
Ok(JsValue::new(JsBigInt::sub(
&modulo,
&JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits)))?,
)))
} 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)))?,
),
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,127 @@
//! Boa's implementation of ECMAScript's 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,
error::JsNativeError,
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) -> JsResult<bool> {
value
.as_boolean()
.or_else(|| value.as_object().and_then(|obj| obj.borrow().as_boolean()))
.ok_or_else(|| {
JsNativeError::typ()
.with_message("'this' is not a boolean")
.into()
})
}
/// 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],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
let boolean = Self::this_boolean_value(this)?;
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
pub(crate) fn value_of(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Ok(JsValue::new(Self::this_boolean_value(this)?))
}
}

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,634 @@
//! Boa's implementation of JavaScript's `console` Web API 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)]
enum LogMessage {
Log(String),
Info(String),
Warn(String),
Error(String),
}
/// Helper function for logging messages.
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> {
match data {
[] => Ok(String::new()),
[val] => Ok(val.to_string(context)?.to_std_string_escaped()),
data => {
let mut formatted = String::new();
let mut arg_index = 1;
let target = data
.get_or_undefined(0)
.to_string(context)?
.to_std_string_escaped();
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_or_undefined(arg_index)
.to_string(context)?
.to_std_string_escaped();
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)?.to_std_string_escaped()
));
}
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_block.name)
.to_string(),
);
}
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.to_std_string_escaped());
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.to_std_string_escaped())),
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 '{}' already exist",
label.to_std_string_escaped()
)),
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!("{}: {} ms", label.to_std_string_escaped(), 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 '{}' doesn't exist",
label.to_std_string_escaped()
)),
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) {
let time = Self::system_time_in_ms();
logger(
LogMessage::Info(format!(
"{}: {} ms - timer removed",
label.to_std_string_escaped(),
time - t
)),
context.console(),
);
} else {
logger(
LogMessage::Warn(format!(
"Timer '{}' doesn't exist",
label.to_std_string_escaped()
)),
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,199 @@
use chrono::{Datelike, Local, NaiveDateTime, TimeZone, Timelike};
use crate::value::IntegerOrNan;
/// The absolute maximum value of a timestamp
pub(super) const MAX_TIMESTAMP: i64 = 864 * 10i64.pow(13);
/// The number of milliseconds in a second.
pub(super) const MILLIS_PER_SECOND: i64 = 1000;
/// The number of milliseconds in a minute.
pub(super) const MILLIS_PER_MINUTE: i64 = MILLIS_PER_SECOND * 60;
/// The number of milliseconds in an hour.
pub(super) const MILLIS_PER_HOUR: i64 = MILLIS_PER_MINUTE * 60;
/// The number of milliseconds in a day.
pub(super) const MILLIS_PER_DAY: i64 = MILLIS_PER_HOUR * 24;
// https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-time-values-and-time-range
//
// The smaller range supported by a time value as specified in this section is approximately -273,790 to 273,790
// years relative to 1970.
pub(super) const MIN_YEAR: i64 = -300_000;
pub(super) const MAX_YEAR: i64 = -MIN_YEAR;
pub(super) const MIN_MONTH: i64 = MIN_YEAR * 12;
pub(super) const MAX_MONTH: i64 = MAX_YEAR * 12;
/// Calculates the absolute day number from the year number.
pub(super) const fn day_from_year(year: i64) -> i64 {
// Taken from https://chromium.googlesource.com/v8/v8/+/refs/heads/main/src/date/date.cc#496
// Useful to avoid negative divisions and overflows on 32-bit platforms (if we plan to support them).
const YEAR_DELTA: i64 = 399_999;
const fn day(year: i64) -> i64 {
let year = year + YEAR_DELTA;
365 * year + year / 4 - year / 100 + year / 400
}
assert!(MIN_YEAR <= year && year <= MAX_YEAR);
day(year) - day(1970)
}
/// Abstract operation [`MakeTime`][spec].
///
/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-maketime
pub(super) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option<i64> {
// 1. If hour is not finite or min is not finite or sec is not finite or ms is not finite, return NaN.
// 2. Let h be 𝔽(! ToIntegerOrInfinity(hour)).
// 3. Let m be 𝔽(! ToIntegerOrInfinity(min)).
// 4. Let s be 𝔽(! ToIntegerOrInfinity(sec)).
// 5. Let milli be 𝔽(! ToIntegerOrInfinity(ms)).
// 6. Let t be ((h * msPerHour + m * msPerMinute) + s * msPerSecond) + milli, performing the arithmetic according to IEEE 754-2019 rules (that is, as if using the ECMAScript operators * and +).
// 7. Return t.
let h_ms = hour.checked_mul(MILLIS_PER_HOUR)?;
let m_ms = min.checked_mul(MILLIS_PER_MINUTE)?;
let s_ms = sec.checked_mul(MILLIS_PER_SECOND)?;
h_ms.checked_add(m_ms)?.checked_add(s_ms)?.checked_add(ms)
}
/// Abstract operation [`MakeDay`][spec].
///
/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makeday
pub(super) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option<i64> {
// 1. If year is not finite or month is not finite or date is not finite, return NaN.
// 2. Let y be 𝔽(! ToIntegerOrInfinity(year)).
// 3. Let m be 𝔽(! ToIntegerOrInfinity(month)).
// 4. Let dt be 𝔽(! ToIntegerOrInfinity(date)).
if !(MIN_YEAR..=MAX_YEAR).contains(&year) || !(MIN_MONTH..=MAX_MONTH).contains(&month) {
return None;
}
// At this point, we've already asserted that year and month are much less than its theoretical
// maximum and minimum values (i64::MAX/MIN), so we don't need to do checked operations.
// 5. Let ym be y + 𝔽(floor((m) / 12)).
// 6. If ym is not finite, return NaN.
year += month / 12;
// 7. Let mn be 𝔽((m) modulo 12).
month %= 12;
if month < 0 {
month += 12;
year -= 1;
}
// 8. Find a finite time value t such that YearFromTime(t) is ym and MonthFromTime(t) is mn and DateFromTime(t) is
// 1𝔽; but if this is not possible (because some argument is out of range), return NaN.
let month = usize::try_from(month).expect("month must be between 0 and 11 at this point");
let mut day = day_from_year(year);
// Consider leap years when calculating the cumulative days added to the year from the input month
if (year % 4 != 0) || (year % 100 == 0 && year % 400 != 0) {
day += [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][month];
} else {
day += [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335][month];
}
// 9. Return Day(t) + dt - 1𝔽.
(day - 1).checked_add(date)
}
/// Abstract operation [`MakeDate`][spec].
///
/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makedate
pub(super) fn make_date(day: i64, time: i64) -> Option<i64> {
// 1. If day is not finite or time is not finite, return NaN.
// 2. Let tv be day × msPerDay + time.
// 3. If tv is not finite, return NaN.
// 4. Return tv.
day.checked_mul(MILLIS_PER_DAY)?.checked_add(time)
}
/// Abstract operation [`TimeClip`][spec]
/// Returns the timestamp (number of milliseconds) if it is in the expected range.
/// Otherwise, returns `None`.
///
/// [spec]: https://tc39.es/ecma262/#sec-timeclip
pub(super) fn time_clip(time: i64) -> Option<i64> {
// 1. If time is not finite, return NaN.
// 2. If abs((time)) > 8.64 × 10^15, return NaN.
// 3. Return 𝔽(! ToIntegerOrInfinity(time)).
(time.checked_abs()? <= MAX_TIMESTAMP).then_some(time)
}
#[derive(Default, Debug, Clone, Copy)]
pub(super) struct DateParameters {
pub(super) year: Option<IntegerOrNan>,
pub(super) month: Option<IntegerOrNan>,
pub(super) date: Option<IntegerOrNan>,
pub(super) hour: Option<IntegerOrNan>,
pub(super) minute: Option<IntegerOrNan>,
pub(super) second: Option<IntegerOrNan>,
pub(super) millisecond: Option<IntegerOrNan>,
}
/// Replaces some (or all) parameters of `date` with the specified parameters
pub(super) fn replace_params(
datetime: NaiveDateTime,
params: DateParameters,
local: bool,
) -> Option<NaiveDateTime> {
let DateParameters {
year,
month,
date,
hour,
minute,
second,
millisecond,
} = params;
let datetime = if local {
Local.from_utc_datetime(&datetime).naive_local()
} else {
datetime
};
let year = match year {
Some(i) => i.as_integer()?,
None => i64::from(datetime.year()),
};
let month = match month {
Some(i) => i.as_integer()?,
None => i64::from(datetime.month() - 1),
};
let date = match date {
Some(i) => i.as_integer()?,
None => i64::from(datetime.day()),
};
let hour = match hour {
Some(i) => i.as_integer()?,
None => i64::from(datetime.hour()),
};
let minute = match minute {
Some(i) => i.as_integer()?,
None => i64::from(datetime.minute()),
};
let second = match second {
Some(i) => i.as_integer()?,
None => i64::from(datetime.second()),
};
let millisecond = match millisecond {
Some(i) => i.as_integer()?,
None => i64::from(datetime.timestamp_subsec_millis()),
};
let new_day = make_day(year, month, date)?;
let new_time = make_time(hour, minute, second, millisecond)?;
let mut ts = make_date(new_day, new_time)?;
if local {
ts = Local
.from_local_datetime(&NaiveDateTime::from_timestamp_millis(ts)?)
.earliest()?
.naive_utc()
.timestamp_millis();
}
NaiveDateTime::from_timestamp_millis(time_clip(ts)?)
}

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, ErrorKind};
#[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(ErrorKind::Aggregate));
// 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, ErrorKind};
/// 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(ErrorKind::Eval));
// 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,267 @@
//! Boa's implementation of ECMAScript's 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,
error::JsNativeError,
js_string,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
string::utf16,
Context, JsResult, 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;
/// The kind of a `NativeError` object, per the [ECMAScript spec][spec].
///
/// This is used internally to convert between [`JsObject`] and
/// [`JsNativeError`] correctly, but it can also be used to manually create `Error`
/// objects. However, the recommended way to create them is to construct a
/// `JsNativeError` first, then call [`JsNativeError::to_opaque`],
/// which will assign its prototype, properties and kind automatically.
///
/// For a description of every error kind and its usage, see
/// [`JsNativeErrorKind`][crate::error::JsNativeErrorKind].
///
/// [spec]: https://tc39.es/ecma262/#sec-error-objects
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ErrorKind {
/// The `AggregateError` object type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-aggregate-error-objects
Aggregate,
/// The `Error` object type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-error-objects
Error,
/// The `EvalError` type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-evalerror
Eval,
/// The `TypeError` type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-typeerror
Type,
/// The `RangeError` type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror
Range,
/// The `ReferenceError` type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-referenceerror
Reference,
/// The `SyntaxError` type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-syntaxerror
Syntax,
/// The `URIError` type.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-urierror
Uri,
}
/// 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(ErrorKind::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.
// 2. If Type(O) is not Object, throw a TypeError exception.
let o = this
.as_object()
.ok_or_else(|| JsNativeError::typ().with_message("'this' is not an Object"))?;
// 3. Let name be ? Get(O, "name").
let name = o.get(js_string!("name"), context)?;
// 4. If name is undefined, set name to "Error"; otherwise set name to ? ToString(name).
let name = if name.is_undefined() {
js_string!("Error")
} else {
name.to_string(context)?
};
// 5. Let msg be ? Get(O, "message").
let msg = o.get(js_string!("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() {
js_string!()
} 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(js_string!(&name, utf16!(": "), &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, ErrorKind};
/// 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(ErrorKind::Range));
// 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, ErrorKind};
#[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(ErrorKind::Reference));
// 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, ErrorKind};
/// 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(ErrorKind::Syntax));
// 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,120 @@
//! 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,
error::JsNativeError,
native_function::NativeFunction,
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, ErrorKind};
/// 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(ErrorKind::Type));
// 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], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::typ().with_message("'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them").into())
}
let function = JsObject::from_proto_and_data(
context.intrinsics().constructors().function().prototype(),
ObjectData::function(Function::Native {
function: NativeFunction::from_fn_ptr(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, ErrorKind};
/// 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(ErrorKind::Uri));
// 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,273 @@
//! Boa's implementation of ECMAScript's global `eval` function.
//!
//! The `eval()` function evaluates ECMAScript 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},
environments::DeclarativeEnvironment,
error::JsNativeError,
native_function::NativeFunction,
object::FunctionObjectBuilder,
property::Attribute,
Context, JsResult, JsString, JsValue,
};
use boa_ast::operations::{
contains, contains_arguments, top_level_var_declared_names, ContainsSymbol,
};
use boa_gc::Gc;
use boa_parser::Parser;
use boa_profiler::Profiler;
#[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 = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(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<'_>) -> JsResult<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,
mut strict: bool,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
bitflags::bitflags! {
/// Flags used to throw early errors on invalid `eval` calls.
#[derive(Default)]
struct Flags: u8 {
const IN_FUNCTION = 0b0001;
const IN_METHOD = 0b0010;
const IN_DERIVED_CONSTRUCTOR = 0b0100;
const IN_CLASS_FIELD_INITIALIZER = 0b1000;
}
}
/// Possible actions that can be executed after exiting this function to restore the environment to its
/// original state.
#[derive(Debug)]
enum EnvStackAction {
Truncate(usize),
Restore(Vec<Gc<DeclarativeEnvironment>>),
}
/// Restores the environment after calling `eval` or after throwing an error.
fn restore_environment(context: &mut Context<'_>, action: EnvStackAction) {
match action {
EnvStackAction::Truncate(size) => {
context.realm.environments.truncate(size);
}
EnvStackAction::Restore(envs) => {
// Pop all environments created during the eval execution and restore the original stack.
context.realm.environments.truncate(1);
context.realm.environments.extend(envs);
}
}
}
// 1. Assert: If direct is false, then strictCaller is also false.
debug_assert!(direct || !strict);
// 2. If Type(x) is not String, return x.
// TODO: rework parser to take an iterator of `u32` unicode codepoints
let Some(x) = x.as_string().map(JsString::to_std_string_escaped) else {
return Ok(x.clone());
};
// Because of implementation details the following code differs from the spec.
// 5. Perform ? HostEnsureCanCompileStrings(evalRealm).
context.host_hooks().ensure_can_compile_strings(context)?;
// 11. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection:
// a. Let script be ParseText(StringToCodePoints(x), Script).
// b. If script is a List of errors, throw a SyntaxError exception.
// c. If script Contains ScriptBody is false, return undefined.
// d. Let body be the ScriptBody of script.
let mut parser = Parser::new(x.as_bytes());
if strict {
parser.set_strict();
}
let body = parser.parse_eval(direct, context.interner_mut())?;
// 6. Let inFunction be false.
// 7. Let inMethod be false.
// 8. Let inDerivedConstructor be false.
// 9. Let inClassFieldInitializer be false.
// a. Let thisEnvRec be GetThisEnvironment().
let flags = match context
.realm
.environments
.get_this_environment()
.as_function_slots()
{
// 10. If direct is true, then
// b. If thisEnvRec is a Function Environment Record, then
Some(function_env) if direct => {
let function_env = function_env.borrow();
// i. Let F be thisEnvRec.[[FunctionObject]].
let function_object = function_env.function_object().borrow();
// ii. Set inFunction to true.
let mut flags = Flags::IN_FUNCTION;
// iii. Set inMethod to thisEnvRec.HasSuperBinding().
if function_env.has_super_binding() {
flags |= Flags::IN_METHOD;
}
let function_object = function_object
.as_function()
.expect("must be function object");
// iv. If F.[[ConstructorKind]] is derived, set inDerivedConstructor to true.
if function_object.is_derived_constructor() {
flags |= Flags::IN_DERIVED_CONSTRUCTOR;
}
// v. Let classFieldInitializerName be F.[[ClassFieldInitializerName]].
// vi. If classFieldInitializerName is not empty, set inClassFieldInitializer to true.
if function_object.class_field_initializer_name().is_some() {
flags |= Flags::IN_CLASS_FIELD_INITIALIZER;
}
flags
}
_ => Flags::default(),
};
if !flags.contains(Flags::IN_FUNCTION) && contains(&body, ContainsSymbol::NewTarget) {
return Err(JsNativeError::syntax()
.with_message("invalid `new.target` expression inside eval")
.into());
}
if !flags.contains(Flags::IN_METHOD) && contains(&body, ContainsSymbol::SuperProperty) {
return Err(JsNativeError::syntax()
.with_message("invalid `super` reference inside eval")
.into());
}
if !flags.contains(Flags::IN_DERIVED_CONSTRUCTOR)
&& contains(&body, ContainsSymbol::SuperCall)
{
return Err(JsNativeError::syntax()
.with_message("invalid `super` call inside eval")
.into());
}
if flags.contains(Flags::IN_CLASS_FIELD_INITIALIZER) && contains_arguments(&body) {
return Err(JsNativeError::syntax()
.with_message("invalid `arguments` reference inside eval")
.into());
}
strict |= body.strict();
// Because our environment model does not map directly to the spec, this section looks very different.
// 12 - 13 are implicit in the call of `Context::compile_with_new_declarative`.
// 14 - 33 are in the following section, together with EvalDeclarationInstantiation.
let action = 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.
if !strict {
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();
// Pop any added runtime environments that were not removed during the eval execution.
EnvStackAction::Truncate(environments_len)
} 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.
if !strict {
context.realm.environments.poison_all();
}
// Pop all environments before the eval execution.
let environments = context.realm.environments.pop_to_global();
context.realm.compile_env = context.realm.environments.current_compile_environment();
// Restore all environments to the state from before the eval execution.
EnvStackAction::Restore(environments)
};
// Only need to check on non-strict mode since strict mode automatically creates a function
// environment for all eval calls.
if !strict {
// Error if any var declaration in the eval code already exists as a let/const declaration in the current running environment.
if let Some(name) = context
.realm
.environments
.has_lex_binding_until_function_environment(&top_level_var_declared_names(&body))
{
restore_environment(context, action);
let name = context.interner().resolve_expect(name.sym());
let msg = format!("variable declaration {name} in eval function already exists as a lexical variable");
return Err(JsNativeError::syntax().with_message(msg).into());
}
}
// TODO: check if private identifiers inside `eval` are valid.
// Compile and execute the eval statement list.
let code_block = context.compile_with_new_declarative(&body, strict)?;
// Indirect calls don't need extensions, because a non-strict indirect call modifies only
// the global object.
// Strict direct calls also don't need extensions, since all strict eval calls push a new
// function environment before evaluating.
if direct && !strict {
context
.realm
.environments
.extend_outer_function_environment();
}
let result = context.execute(code_block);
restore_environment(context, action);
result
}
}

View File

@@ -0,0 +1,293 @@
use crate::{
environments::DeclarativeEnvironment,
object::{JsObject, ObjectData},
property::PropertyDescriptor,
symbol::{self, JsSymbol},
Context, JsValue,
};
use boa_ast::{function::FormalParameterList, operations::bound_names};
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]] »).
// 3. Set obj.[[ParameterMap]] to undefined.
// skipped because the `Arguments` enum ensures ordinary argument objects don't have a `[[ParameterMap]]`
let obj = JsObject::from_proto_and_data(
context.intrinsics().constructors().object().prototype(),
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 }).
let values_function = context.intrinsics().objects().array_prototype_values();
obj.define_property_or_throw(
symbol::JsSymbol::iterator(),
PropertyDescriptor::builder()
.value(values_function)
.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;
for name in bound_names(formals) {
if property_index >= len {
break;
}
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 }).
let values_function = context.intrinsics().objects().array_prototype_values();
obj.define_property_or_throw(
JsSymbol::iterator(),
PropertyDescriptor::builder()
.value(values_function)
.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,275 @@
use crate::{
error::JsNativeError,
forward, forward_val, js_string,
native_function::NativeFunction,
object::{FunctionObjectBuilder, JsObject},
property::{Attribute, PropertyDescriptor},
string::utf16,
Context, JsNativeErrorKind,
};
#[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(), utf16!("[object Error]"));
}
#[test]
fn function_prototype_call_throw() {
let mut context = Context::default();
let throw = r#"
let call = Function.prototype.call;
call(call)
"#;
let err = forward_val(&mut context, throw).unwrap_err();
let err = err.as_native().unwrap();
assert!(matches!(
err,
JsNativeError {
kind: JsNativeErrorKind::Type,
..
}
));
}
#[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 = js_string!("Hello");
let object = JsObject::with_object_proto(&mut context);
object
.define_property_or_throw(
"key",
PropertyDescriptor::builder()
.value(" world!")
.writable(false)
.enumerable(false)
.configurable(false),
&mut context,
)
.unwrap();
let func = FunctionObjectBuilder::new(
&mut context,
NativeFunction::from_copy_closure_with_captures(
|_, _, captures, context| {
let (string, object) = &captures;
let hw = js_string!(
string,
&object
.__get_own_property__(&"key".into(), context)?
.and_then(|prop| prop.value().cloned())
.and_then(|val| val.as_string().cloned())
.ok_or_else(
|| JsNativeError::typ().with_message("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,431 @@
//! Boa's implementation of ECMAScript's 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,
error::JsNativeError,
object::{ConstructorBuilder, JsObject, ObjectData},
property::{Attribute, PropertyDescriptor},
symbol::JsSymbol,
value::JsValue,
vm::{CallFrame, GeneratorResumeKind, ReturnType},
Context, JsError, JsResult,
};
use boa_gc::{Finalize, Gc, GcCell, 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 of 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<GcCell<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(
JsSymbol::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).
this.as_object().map_or_else(
|| {
Err(JsNativeError::typ()
.with_message("Generator.prototype.next called on non generator")
.into())
},
|obj| Self::generator_resume(obj, args.get_or_undefined(0), context),
)
}
/// `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(JsError::from_opaque(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(|| {
JsNativeError::typ().with_message("generator resumed on non generator object")
})?;
let state = generator.state;
if state == GeneratorState::Executing {
return Err(JsNativeError::typ()
.with_message("Generator should not be executing")
.into());
}
// 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.clone());
}
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(|| {
JsNativeError::typ().with_message("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(|| {
JsNativeError::typ().with_message("generator resumed on non generator object")
})?;
let mut state = generator.state;
if state == GeneratorState::Executing {
return Err(JsNativeError::typ()
.with_message("Generator should not be executing")
.into());
}
// 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) => {
let value = value.to_opaque(context);
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,129 @@
//! Boa's implementation of ECMAScript's global `GeneratorFunction` object.
//!
//! The `GeneratorFunction` constructor creates a new generator function object.
//! In ECMAScript, 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,
},
native_function::NativeFunction,
object::ObjectData,
property::PropertyDescriptor,
symbol::JsSymbol,
value::JsValue,
Context, JsResult,
};
use boa_profiler::Profiler;
/// The internal representation of 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: NativeFunction::from_fn_ptr(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(JsSymbol::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 @@
//! Boa's implementation of ECMAScript's 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 @@
//! Boa's implementation of ECMAScript's 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,558 @@
use boa_gc::{custom_trace, Finalize, Trace};
use boa_profiler::Profiler;
use icu_collator::{
provider::CollationMetadataV1Marker, AlternateHandling, CaseFirst, MaxVariable, Numeric,
};
use icu_locid::{
extensions::unicode::Value, extensions_unicode_key as key, extensions_unicode_value as value,
Locale,
};
use icu_provider::DataLocale;
use tap::{Conv, Pipe};
use crate::{
builtins::{BuiltIn, JsArgs},
context::{intrinsics::StandardConstructors, BoaProvider},
native_function::NativeFunction,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder,
FunctionObjectBuilder, JsFunction, JsObject, ObjectData,
},
property::Attribute,
symbol::JsSymbol,
Context, JsNativeError, JsResult, JsValue,
};
use super::{
locale::{canonicalize_locale_list, resolve_locale, supported_locales, validate_extension},
options::{coerce_options_to_object, get_option, IntlOptions, LocaleMatcher},
Service,
};
mod options;
pub(crate) use options::*;
pub struct Collator {
locale: Locale,
collation: Value,
numeric: bool,
case_first: Option<CaseFirst>,
usage: Usage,
sensitivity: Sensitivity,
ignore_punctuation: bool,
collator: icu_collator::Collator,
bound_compare: Option<JsFunction>,
}
impl Finalize for Collator {}
// SAFETY: only `bound_compare` is a traceable object.
unsafe impl Trace for Collator {
custom_trace!(this, mark(&this.bound_compare));
}
impl Collator {
/// Gets the inner [`icu_collator::Collator`] comparator.
pub(crate) const fn collator(&self) -> &icu_collator::Collator {
&self.collator
}
}
impl std::fmt::Debug for Collator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Collator")
.field("locale", &self.locale)
.field("collation", &self.collation)
.field("numeric", &self.numeric)
.field("case_first", &self.case_first)
.field("usage", &self.usage)
.field("sensitivity", &self.sensitivity)
.field("ignore_punctuation", &self.ignore_punctuation)
.field("collator", &"ICUCollator")
.field("bound_compare", &self.bound_compare)
.finish()
}
}
#[derive(Debug, Clone)]
pub(in crate::builtins::intl) struct CollatorLocaleOptions {
collation: Option<Value>,
numeric: Option<bool>,
case_first: Option<CaseFirst>,
}
impl Service for Collator {
type LangMarker = CollationMetadataV1Marker;
type LocaleOptions = CollatorLocaleOptions;
fn resolve(locale: &mut Locale, options: &mut Self::LocaleOptions, provider: BoaProvider<'_>) {
let collation = options
.collation
.take()
.filter(|co| {
validate_extension::<Self::LangMarker>(locale.id.clone(), key!("co"), co, &provider)
})
.or_else(|| {
locale
.extensions
.unicode
.keywords
.get(&key!("co"))
.cloned()
.filter(|co| {
validate_extension::<Self::LangMarker>(
locale.id.clone(),
key!("co"),
co,
&provider,
)
})
})
.filter(|co| co != &value!("search"));
let numeric =
options.numeric.or_else(
|| match locale.extensions.unicode.keywords.get(&key!("kn")) {
Some(a) if a == &value!("true") => Some(true),
Some(_) => Some(false),
_ => None,
},
);
let case_first = options.case_first.or_else(|| {
match locale.extensions.unicode.keywords.get(&key!("kf")) {
Some(a) if a == &value!("upper") => Some(CaseFirst::UpperFirst),
Some(a) if a == &value!("lower") => Some(CaseFirst::LowerFirst),
Some(_) => Some(CaseFirst::Off),
_ => None,
}
});
locale.extensions.unicode.clear();
if let Some(co) = collation.clone() {
locale.extensions.unicode.keywords.set(key!("co"), co);
}
if let Some(kn) = numeric.map(|kn| if kn { value!("true") } else { value!("false") }) {
locale.extensions.unicode.keywords.set(key!("kn"), kn);
}
if let Some(kf) = case_first.map(|kf| match kf {
CaseFirst::Off => value!("false"),
CaseFirst::LowerFirst => value!("lower"),
CaseFirst::UpperFirst => value!("upper"),
_ => unreachable!(),
}) {
locale.extensions.unicode.keywords.set(key!("kf"), kf);
}
options.collation = collation;
options.numeric = numeric;
options.case_first = case_first;
}
}
impl BuiltIn for Collator {
const NAME: &'static str = "Collator";
fn init(context: &mut Context<'_>) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let compare =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::compare))
.name("get compare")
.constructor(false)
.build();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().collator().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.static_method(Self::supported_locales_of, "supportedLocalesOf", 1)
.property(
JsSymbol::to_string_tag(),
"Intl.Collator",
Attribute::CONFIGURABLE,
)
.accessor("compare", Some(compare), None, Attribute::CONFIGURABLE)
.method(Self::resolved_options, "resolvedOptions", 0)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Collator {
pub(crate) const LENGTH: usize = 0;
/// Constructor [`Intl.Collator ( [ locales [ , options ] ] )`][spec].
///
/// Constructor for `Collator` objects.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.collator
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator
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 internalSlotsList be « [[InitializedCollator]], [[Locale]], [[Usage]], [[Sensitivity]], [[IgnorePunctuation]], [[Collation]], [[BoundCompare]] ».
// 3. If %Collator%.[[RelevantExtensionKeys]] contains "kn", then
// a. Append [[Numeric]] as the last element of internalSlotsList.
// 4. If %Collator%.[[RelevantExtensionKeys]] contains "kf", then
// a. Append [[CaseFirst]] as the last element of internalSlotsList.
// 5. Let collator be ? OrdinaryCreateFromConstructor(newTarget, "%Collator.prototype%", internalSlotsList).
// 6. Return ? InitializeCollator(collator, locales, options).
let locales = args.get_or_undefined(0);
let options = args.get_or_undefined(1);
// Abstract operation `InitializeCollator ( collator, locales, options )`
// https://tc39.es/ecma402/#sec-initializecollator
// 1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
let requested_locales = canonicalize_locale_list(locales, context)?;
// 2. Set options to ? CoerceOptionsToObject(options).
let options = coerce_options_to_object(options, context)?;
// 3. Let usage be ? GetOption(options, "usage", string, « "sort", "search" », "sort").
// 4. Set collator.[[Usage]] to usage.
// 5. If usage is "sort", then
// a. Let localeData be %Collator%.[[SortLocaleData]].
// 6. Else,
// a. Let localeData be %Collator%.[[SearchLocaleData]].
let usage = get_option::<Usage>(&options, "usage", false, context)?.unwrap_or_default();
// 7. Let opt be a new Record.
// 8. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
// 9. Set opt.[[localeMatcher]] to matcher.
let matcher = get_option::<LocaleMatcher>(&options, "localeMatcher", false, context)?
.unwrap_or_default();
// 10. Let collation be ? GetOption(options, "collation", string, empty, undefined).
// 11. If collation is not undefined, then
// a. If collation does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
// 12. Set opt.[[co]] to collation.
let collation = get_option::<Value>(&options, "collation", false, context)?;
// 13. Let numeric be ? GetOption(options, "numeric", boolean, empty, undefined).
// 14. If numeric is not undefined, then
// a. Let numeric be ! ToString(numeric).
// 15. Set opt.[[kn]] to numeric.
let numeric = get_option::<bool>(&options, "numeric", false, context)?;
// 16. Let caseFirst be ? GetOption(options, "caseFirst", string, « "upper", "lower", "false" », undefined).
// 17. Set opt.[[kf]] to caseFirst.
let case_first = get_option::<CaseFirst>(&options, "caseFirst", false, context)?;
let mut intl_options = IntlOptions {
matcher,
service_options: CollatorLocaleOptions {
collation,
numeric,
case_first,
},
};
// 18. Let relevantExtensionKeys be %Collator%.[[RelevantExtensionKeys]].
// 19. Let r be ResolveLocale(%Collator%.[[AvailableLocales]], requestedLocales, opt, relevantExtensionKeys, localeData).
let mut locale =
resolve_locale::<Self>(&requested_locales, &mut intl_options, context.icu());
let collator_locale = {
// `collator_locale` needs to be different from the resolved locale because ECMA402 doesn't
// define `search` as a resolvable extension of a locale, so we need to add that extension
// only to the locale passed to the collator.
let mut col_loc = DataLocale::from(&locale);
if usage == Usage::Search {
intl_options.service_options.collation = None;
locale.extensions.unicode.keywords.remove(key!("co"));
col_loc.set_unicode_ext(key!("co"), value!("search"));
}
col_loc
};
// 20. Set collator.[[Locale]] to r.[[locale]].
// 21. Let collation be r.[[co]].
// 22. If collation is null, let collation be "default".
// 23. Set collator.[[Collation]] to collation.
let collation = intl_options
.service_options
.collation
.unwrap_or(value!("default"));
// 24. If relevantExtensionKeys contains "kn", then
// a. Set collator.[[Numeric]] to SameValue(r.[[kn]], "true").
let numeric = intl_options.service_options.numeric.unwrap_or_default();
// 25. If relevantExtensionKeys contains "kf", then
// a. Set collator.[[CaseFirst]] to r.[[kf]].
let case_first = intl_options.service_options.case_first;
// 26. Let sensitivity be ? GetOption(options, "sensitivity", string, « "base", "accent", "case", "variant" », undefined).
// 28. Set collator.[[Sensitivity]] to sensitivity.
let sensitivity = get_option::<Sensitivity>(&options, "sensitivity", false, context)?
// 27. If sensitivity is undefined, then
// a. If usage is "sort", then
// i. Let sensitivity be "variant".
// b. Else,
// i. Let dataLocale be r.[[dataLocale]].
// ii. Let dataLocaleData be localeData.[[<dataLocale>]].
// iii. Let sensitivity be dataLocaleData.[[sensitivity]].
.or_else(|| (usage == Usage::Sort).then_some(Sensitivity::Variant));
// 29. Let ignorePunctuation be ? GetOption(options, "ignorePunctuation", boolean, empty, false).
// 30. Set collator.[[IgnorePunctuation]] to ignorePunctuation.
let ignore_punctuation =
get_option::<bool>(&options, "ignorePunctuation", false, context)?.unwrap_or_default();
let (strength, case_level) = sensitivity.map(Sensitivity::to_collator_options).unzip();
let (alternate_handling, max_variable) = ignore_punctuation
.then_some((AlternateHandling::Shifted, MaxVariable::Punctuation))
.unzip();
let collator = context
.icu()
.provider()
.try_new_collator(&collator_locale, {
let mut options = icu_collator::CollatorOptions::new();
options.strength = strength;
options.case_level = case_level;
options.case_first = case_first;
options.numeric = Some(if numeric { Numeric::On } else { Numeric::Off });
options.alternate_handling = alternate_handling;
options.max_variable = max_variable;
options
})
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::collator, context)?;
let collator = JsObject::from_proto_and_data(
prototype,
ObjectData::collator(Collator {
locale,
collation,
numeric,
case_first,
usage,
sensitivity: sensitivity.unwrap_or(Sensitivity::Variant),
ignore_punctuation,
collator,
bound_compare: None,
}),
);
// 31. Return collator.
Ok(collator.into())
}
/// [`Intl.Collator.supportedLocalesOf ( locales [ , options ] )`][spec].
///
/// Returns an array containing those of the provided locales that are supported in collation
/// without having to fall back to the runtime's default locale.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.collator.supportedlocalesof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/supportedLocalesOf
fn supported_locales_of(
_: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
let locales = args.get_or_undefined(0);
let options = args.get_or_undefined(1);
// 1. Let availableLocales be %Collator%.[[AvailableLocales]].
// 2. Let requestedLocales be ? CanonicalizeLocaleList(locales).
let requested_locales = canonicalize_locale_list(locales, context)?;
// 3. Return ? SupportedLocales(availableLocales, requestedLocales, options).
supported_locales::<<Self as Service>::LangMarker>(&requested_locales, options, context)
.map(JsValue::from)
}
/// [`get Intl.Collator.prototype.compare`][spec].
///
/// Compares two strings according to the sort order of this Intl.Collator object.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.collator.prototype.compare
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/compare
fn compare(this: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let collator be the this value.
// 2. Perform ? RequireInternalSlot(collator, [[InitializedCollator]]).
let this = this.as_object().ok_or_else(|| {
JsNativeError::typ()
.with_message("`resolvedOptions` can only be called on a `Collator` object")
})?;
let collator_obj = this.clone();
let mut collator = this.borrow_mut();
let collator = collator.as_collator_mut().ok_or_else(|| {
JsNativeError::typ()
.with_message("`resolvedOptions` can only be called on a `Collator` object")
})?;
// 3. If collator.[[BoundCompare]] is undefined, then
// a. Let F be a new built-in function object as defined in 10.3.3.1.
// b. Set F.[[Collator]] to collator.
// c. Set collator.[[BoundCompare]] to F.
let bound_compare = if let Some(f) = collator.bound_compare.clone() {
f
} else {
let bound_compare = FunctionObjectBuilder::new(
context,
// 10.3.3.1. Collator Compare Functions
// https://tc39.es/ecma402/#sec-collator-compare-functions
NativeFunction::from_copy_closure_with_captures(
|_, args, collator, context| {
// 1. Let collator be F.[[Collator]].
// 2. Assert: Type(collator) is Object and collator has an [[InitializedCollator]] internal slot.
let collator = collator.borrow();
let collator = collator
.as_collator()
.expect("checked above that the object was a collator object");
// 3. If x is not provided, let x be undefined.
// 5. Let X be ? ToString(x).
let x = args.get_or_undefined(0).to_string(context)?;
// 4. If y is not provided, let y be undefined.
// 6. Let Y be ? ToString(y).
let y = args.get_or_undefined(1).to_string(context)?;
// 7. Return CompareStrings(collator, X, Y).
let result = collator.collator.compare_utf16(&x, &y) as i32;
Ok(result.into())
},
collator_obj,
),
)
.length(2)
.build();
collator.bound_compare = Some(bound_compare.clone());
bound_compare
};
// 4. Return collator.[[BoundCompare]].
Ok(bound_compare.into())
}
/// [`Intl.Collator.prototype.resolvedOptions ( )`][spec].
///
/// Returns a new object with properties reflecting the locale and collation options computed
/// during initialization of this `Intl.Collator` object.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.collator.prototype.resolvedoptions
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/resolvedOptions
fn resolved_options(
this: &JsValue,
_: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let collator be the this value.
// 2. Perform ? RequireInternalSlot(collator, [[InitializedCollator]]).
let collator = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`resolvedOptions` can only be called on a `Collator` object")
})?;
let collator = collator.as_collator().ok_or_else(|| {
JsNativeError::typ()
.with_message("`resolvedOptions` can only be called on a `Collator` object")
})?;
// 3. Let options be OrdinaryObjectCreate(%Object.prototype%).
let options = JsObject::from_proto_and_data(
context.intrinsics().constructors().object().prototype(),
ObjectData::ordinary(),
);
// 4. For each row of Table 4, except the header row, in table order, do
// a. Let p be the Property value of the current row.
// b. Let v be the value of collator's internal slot whose name is the Internal Slot value of the current row.
// c. If the current row has an Extension Key value, then
// i. Let extensionKey be the Extension Key value of the current row.
// ii. If %Collator%.[[RelevantExtensionKeys]] does not contain extensionKey, then
// 1. Let v be undefined.
// d. If v is not undefined, then
// i. Perform ! CreateDataPropertyOrThrow(options, p, v).
// 5. Return options.
options
.create_data_property_or_throw("locale", collator.locale.to_string(), context)
.expect("operation must not fail per the spec");
options
.create_data_property_or_throw(
"usage",
match collator.usage {
Usage::Search => "search",
Usage::Sort => "sort",
},
context,
)
.expect("operation must not fail per the spec");
options
.create_data_property_or_throw(
"sensitivity",
match collator.sensitivity {
Sensitivity::Base => "base",
Sensitivity::Accent => "accent",
Sensitivity::Case => "case",
Sensitivity::Variant => "variant",
},
context,
)
.expect("operation must not fail per the spec");
options
.create_data_property_or_throw(
"ignorePunctuation",
collator.ignore_punctuation,
context,
)
.expect("operation must not fail per the spec");
options
.create_data_property_or_throw("collation", collator.collation.to_string(), context)
.expect("operation must not fail per the spec");
options
.create_data_property_or_throw("numeric", collator.numeric, context)
.expect("operation must not fail per the spec");
if let Some(kf) = collator.case_first {
options
.create_data_property_or_throw(
"caseFirst",
match kf {
CaseFirst::Off => "false",
CaseFirst::LowerFirst => "lower",
CaseFirst::UpperFirst => "upper",
_ => unreachable!(),
},
context,
)
.expect("operation must not fail per the spec");
}
// 5. Return options.
Ok(options.into())
}
}

View File

@@ -0,0 +1,80 @@
use std::str::FromStr;
use icu_collator::{CaseLevel, Strength};
use crate::builtins::intl::options::OptionTypeParsable;
#[derive(Debug, Clone, Copy)]
pub(crate) enum Sensitivity {
Base,
Accent,
Case,
Variant,
}
impl Sensitivity {
/// Converts the sensitivity option to the equivalent ICU4X collator options.
pub(crate) const fn to_collator_options(self) -> (Strength, CaseLevel) {
match self {
Sensitivity::Base => (Strength::Primary, CaseLevel::Off),
Sensitivity::Accent => (Strength::Secondary, CaseLevel::Off),
Sensitivity::Case => (Strength::Primary, CaseLevel::On),
Sensitivity::Variant => (Strength::Tertiary, CaseLevel::On),
}
}
}
#[derive(Debug)]
pub(crate) struct ParseSensitivityError;
impl std::fmt::Display for ParseSensitivityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not `base`, `accent`, `case` or `variant`")
}
}
impl FromStr for Sensitivity {
type Err = ParseSensitivityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"base" => Ok(Self::Base),
"accent" => Ok(Self::Accent),
"case" => Ok(Self::Case),
"variant" => Ok(Self::Variant),
_ => Err(ParseSensitivityError),
}
}
}
impl OptionTypeParsable for Sensitivity {}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum Usage {
#[default]
Sort,
Search,
}
#[derive(Debug)]
pub(crate) struct ParseUsageError;
impl std::fmt::Display for ParseUsageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not `sort` or `search`")
}
}
impl FromStr for Usage {
type Err = ParseUsageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sort" => Ok(Self::Sort),
"search" => Ok(Self::Search),
_ => Err(ParseUsageError),
}
}
}
impl OptionTypeParsable for Usage {}

View File

@@ -0,0 +1,259 @@
//! 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,
error::JsNativeError,
js_string,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsFunction, JsObject,
ObjectData,
},
Context, JsResult, JsString, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use icu_datetime::options::preferences::HourCycle;
use super::options::OptionType;
impl OptionType for HourCycle {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
match value.to_string(context)?.to_std_string_escaped().as_str() {
"h11" => Ok(HourCycle::H11),
"h12" => Ok(HourCycle::H12),
"h23" => Ok(HourCycle::H23),
"h24" => Ok(HourCycle::H24),
_ => Err(JsNativeError::range()
.with_message("provided string was not `h11`, `h12`, `h23` or `h24`")
.into()),
}
}
}
/// 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: js_string!("en-US"),
calendar: js_string!("gregory"),
numbering_system: js_string!("arab"),
time_zone: js_string!("UTC"),
weekday: js_string!("narrow"),
era: js_string!("narrow"),
year: js_string!("numeric"),
month: js_string!("narrow"),
day: js_string!("numeric"),
day_period: js_string!("narrow"),
hour: js_string!("numeric"),
minute: js_string!("numeric"),
second: js_string!("numeric"),
fractional_second_digits: js_string!(""),
time_zone_name: js_string!(""),
hour_cycle: js_string!("h24"),
pattern: js_string!("{hour}:{minute}"),
bound_format: js_string!("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 Err(JsNativeError::typ()
.with_message("'date' is required, but timeStyle was defined")
.into());
}
// 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 Err(JsNativeError::typ()
.with_message("'time' is required, but dateStyle was defined")
.into());
}
// 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,498 @@
use std::fmt::Write;
use boa_profiler::Profiler;
use icu_list::{provider::AndListV1Marker, ListFormatter, ListLength};
use icu_locid::Locale;
use icu_provider::DataLocale;
use tap::{Conv, Pipe};
use crate::{
builtins::{Array, BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData,
},
property::Attribute,
symbol::JsSymbol,
Context, JsNativeError, JsResult, JsString, JsValue,
};
use super::{
locale::{canonicalize_locale_list, resolve_locale, supported_locales},
options::{get_option, get_options_object, IntlOptions, LocaleMatcher},
Service,
};
mod options;
pub(crate) use options::*;
pub struct ListFormat {
locale: Locale,
typ: ListFormatType,
style: ListLength,
formatter: ListFormatter,
}
impl std::fmt::Debug for ListFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListFormat")
.field("locale", &self.locale)
.field("typ", &self.typ)
.field("style", &self.style)
.field("formatter", &"ListFormatter")
.finish()
}
}
impl Service for ListFormat {
type LangMarker = AndListV1Marker;
type LocaleOptions = ();
}
impl BuiltIn for ListFormat {
const NAME: &'static str = "ListFormat";
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().list_format().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.static_method(Self::supported_locales_of, "supportedLocalesOf", 1)
.property(
JsSymbol::to_string_tag(),
"Intl.ListFormat",
Attribute::CONFIGURABLE,
)
.method(Self::format, "format", 1)
.method(Self::format_to_parts, "formatToParts", 1)
.method(Self::resolved_options, "resolvedOptions", 0)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl ListFormat {
pub(crate) const LENGTH: usize = 0;
/// Constructor [`Intl.ListFormat ( [ locales [ , options ] ] )`][spec].
///
/// Constructor for `ListFormat` objects.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.ListFormat
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat
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 Err(JsNativeError::typ()
.with_message("cannot call `Intl.ListFormat` constructor without `new`")
.into());
}
let locales = args.get_or_undefined(0);
let options = args.get_or_undefined(1);
// 3. Let requestedLocales be ? CanonicalizeLocaleList(locales).
let requested_locales = canonicalize_locale_list(locales, context)?;
// 4. Set options to ? GetOptionsObject(options).
let options = get_options_object(options)?;
// 5. Let opt be a new Record.
// 6. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
let matcher = get_option::<LocaleMatcher>(&options, "localeMatcher", false, context)?
.unwrap_or_default();
// 7. Set opt.[[localeMatcher]] to matcher.
// 8. Let localeData be %ListFormat%.[[LocaleData]].
// 9. Let r be ResolveLocale(%ListFormat%.[[AvailableLocales]], requestedLocales, opt, %ListFormat%.[[RelevantExtensionKeys]], localeData).
// 10. Set listFormat.[[Locale]] to r.[[locale]].
let locale = resolve_locale::<Self>(
&requested_locales,
&mut IntlOptions {
matcher,
..Default::default()
},
context.icu(),
);
// 11. Let type be ? GetOption(options, "type", string, « "conjunction", "disjunction", "unit" », "conjunction").
// 12. Set listFormat.[[Type]] to type.
let typ =
get_option::<ListFormatType>(&options, "type", false, context)?.unwrap_or_default();
// 13. Let style be ? GetOption(options, "style", string, « "long", "short", "narrow" », "long").
// 14. Set listFormat.[[Style]] to style.
let style = get_option::<ListLength>(&options, "style", false, context)?
.unwrap_or(ListLength::Wide);
// 15. Let dataLocale be r.[[dataLocale]].
// 16. Let dataLocaleData be localeData.[[<dataLocale>]].
// 17. Let dataLocaleTypes be dataLocaleData.[[<type>]].
// 18. Set listFormat.[[Templates]] to dataLocaleTypes.[[<style>]].
// 2. Let listFormat be ? OrdinaryCreateFromConstructor(NewTarget, "%ListFormat.prototype%", « [[InitializedListFormat]], [[Locale]], [[Type]], [[Style]], [[Templates]] »).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::list_format, context)?;
let list_format = JsObject::from_proto_and_data(
prototype,
ObjectData::list_format(ListFormat {
formatter: context
.icu()
.provider()
.try_new_list_formatter(&DataLocale::from(&locale), typ, style)
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?,
locale,
typ,
style,
}),
);
// 19. Return listFormat.
Ok(list_format.into())
}
/// [`Intl.ListFormat.supportedLocalesOf ( locales [ , options ] )`][spec].
///
/// Returns an array containing those of the provided locales that are supported in list
/// formatting without having to fall back to the runtime's default locale.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.ListFormat.supportedLocalesOf
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/supportedLocalesOf
fn supported_locales_of(
_: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
let locales = args.get_or_undefined(0);
let options = args.get_or_undefined(1);
// 1. Let availableLocales be %ListFormat%.[[AvailableLocales]].
// 2. Let requestedLocales be ? CanonicalizeLocaleList(locales).
let requested_locales = canonicalize_locale_list(locales, context)?;
// 3. Return ? SupportedLocales(availableLocales, requestedLocales, options).
supported_locales::<<Self as Service>::LangMarker>(&requested_locales, options, context)
.map(JsValue::from)
}
/// [`Intl.ListFormat.prototype.format ( list )`][spec].
///
/// Returns a language-specific formatted string representing the elements of the list.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.ListFormat.prototype.format
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/format
fn format(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let lf be the this value.
// 2. Perform ? RequireInternalSlot(lf, [[InitializedListFormat]]).
let lf = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`format` can only be called on a `ListFormat` object")
})?;
let lf = lf.as_list_format().ok_or_else(|| {
JsNativeError::typ()
.with_message("`format` can only be called on a `ListFormat` object")
})?;
// 3. Let stringList be ? StringListFromIterable(list).
// TODO: support for UTF-16 unpaired surrogates formatting
let strings = string_list_from_iterable(args.get_or_undefined(0), context)?;
// 4. Return ! FormatList(lf, stringList).
Ok(lf
.formatter
.format_to_string(strings.into_iter().map(|s| s.to_std_string_escaped()))
.into())
}
/// [`Intl.ListFormat.prototype.formatToParts ( list )`][spec].
///
/// Returns a language-specific formatted string representing the elements of the list.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.ListFormat.prototype.formatToParts
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/formatToParts
fn format_to_parts(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// TODO: maybe try to move this into icu4x?
use writeable::{PartsWrite, Writeable};
#[derive(Debug, Clone)]
enum Part {
Literal(String),
Element(String),
}
impl Part {
const fn typ(&self) -> &'static str {
match self {
Part::Literal(_) => "literal",
Part::Element(_) => "element",
}
}
#[allow(clippy::missing_const_for_fn)]
fn value(self) -> String {
match self {
Part::Literal(s) | Part::Element(s) => s,
}
}
}
#[derive(Debug, Clone)]
struct WriteString(String);
impl Write for WriteString {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.0.write_str(s)
}
fn write_char(&mut self, c: char) -> std::fmt::Result {
self.0.write_char(c)
}
}
impl PartsWrite for WriteString {
type SubPartsWrite = WriteString;
fn with_part(
&mut self,
_part: writeable::Part,
mut f: impl FnMut(&mut Self::SubPartsWrite) -> std::fmt::Result,
) -> std::fmt::Result {
f(self)
}
}
#[derive(Debug, Clone)]
struct PartsCollector(Vec<Part>);
impl Write for PartsCollector {
fn write_str(&mut self, _: &str) -> std::fmt::Result {
Ok(())
}
}
impl PartsWrite for PartsCollector {
type SubPartsWrite = WriteString;
fn with_part(
&mut self,
part: writeable::Part,
mut f: impl FnMut(&mut Self::SubPartsWrite) -> core::fmt::Result,
) -> core::fmt::Result {
assert!(part.category == "list");
let mut string = WriteString(String::new());
f(&mut string)?;
if !string.0.is_empty() {
match part.value {
"element" => self.0.push(Part::Element(string.0)),
"literal" => self.0.push(Part::Literal(string.0)),
_ => unreachable!(),
};
}
Ok(())
}
}
// 1. Let lf be the this value.
// 2. Perform ? RequireInternalSlot(lf, [[InitializedListFormat]]).
let lf = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`formatToParts` can only be called on a `ListFormat` object")
})?;
let lf = lf.as_list_format().ok_or_else(|| {
JsNativeError::typ()
.with_message("`formatToParts` can only be called on a `ListFormat` object")
})?;
// 3. Let stringList be ? StringListFromIterable(list).
// TODO: support for UTF-16 unpaired surrogates formatting
let strings = string_list_from_iterable(args.get_or_undefined(0), context)?
.into_iter()
.map(|s| s.to_std_string_escaped());
// 4. Return ! FormatListToParts(lf, stringList).
// Abstract operation `FormatListToParts ( listFormat, list )`
// https://tc39.es/ecma402/#sec-formatlisttoparts
// 1. Let parts be ! CreatePartsFromList(listFormat, list).
let mut parts = PartsCollector(Vec::new());
lf.formatter
.format(strings)
.write_to_parts(&mut parts)
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
// 2. Let result be ! ArrayCreate(0).
let result = Array::array_create(0, None, context)
.expect("creating an empty array with default proto must not fail");
// 3. Let n be 0.
// 4. For each Record { [[Type]], [[Value]] } part in parts, do
for (n, part) in parts.0.into_iter().enumerate() {
// a. Let O be OrdinaryObjectCreate(%Object.prototype%).
let o = JsObject::from_proto_and_data(
context.intrinsics().constructors().object().prototype(),
ObjectData::ordinary(),
);
// b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]).
o.create_data_property_or_throw("type", part.typ(), context)
.expect("operation must not fail per the spec");
// c. Perform ! CreateDataPropertyOrThrow(O, "value", part.[[Value]]).
o.create_data_property_or_throw("value", part.value(), context)
.expect("operation must not fail per the spec");
// d. Perform ! CreateDataPropertyOrThrow(result, ! ToString(n), O).
result
.create_data_property_or_throw(n, o, context)
.expect("operation must not fail per the spec");
// e. Increment n by 1.
}
// 5. Return result.
Ok(result.into())
}
/// [`Intl.ListFormat.prototype.resolvedOptions ( )`][spec].
///
/// Returns a new object with properties reflecting the locale and style formatting options
/// computed during the construction of the current `Intl.ListFormat` object.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.ListFormat.prototype.resolvedoptions
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/resolvedOptions
fn resolved_options(
this: &JsValue,
_: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let lf be the this value.
// 2. Perform ? RequireInternalSlot(lf, [[InitializedListFormat]]).
let lf = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`resolvedOptions` can only be called on a `ListFormat` object")
})?;
let lf = lf.as_list_format().ok_or_else(|| {
JsNativeError::typ()
.with_message("`resolvedOptions` can only be called on a `ListFormat` object")
})?;
// 3. Let options be OrdinaryObjectCreate(%Object.prototype%).
let options = JsObject::from_proto_and_data(
context.intrinsics().constructors().object().prototype(),
ObjectData::ordinary(),
);
// 4. For each row of Table 11, except the header row, in table order, do
// a. Let p be the Property value of the current row.
// b. Let v be the value of lf's internal slot whose name is the Internal Slot value of the current row.
// c. Assert: v is not undefined.
// d. Perform ! CreateDataPropertyOrThrow(options, p, v).
options
.create_data_property_or_throw("locale", lf.locale.to_string(), context)
.expect("operation must not fail per the spec");
options
.create_data_property_or_throw(
"type",
match lf.typ {
ListFormatType::Conjunction => "conjunction",
ListFormatType::Disjunction => "disjunction",
ListFormatType::Unit => "unit",
},
context,
)
.expect("operation must not fail per the spec");
options
.create_data_property_or_throw(
"style",
match lf.style {
ListLength::Wide => "long",
ListLength::Short => "short",
ListLength::Narrow => "narrow",
_ => unreachable!(),
},
context,
)
.expect("operation must not fail per the spec");
// 5. Return options.
Ok(options.into())
}
}
/// Abstract operation [`StringListFromIterable ( iterable )`][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-createstringlistfromiterable
fn string_list_from_iterable(
iterable: &JsValue,
context: &mut Context<'_>,
) -> JsResult<Vec<JsString>> {
// 1. If iterable is undefined, then
if iterable.is_undefined() {
// a. Return a new empty List.
return Ok(Vec::new());
}
// 2. Let iteratorRecord be ? GetIterator(iterable).
let iterator = iterable.get_iterator(context, None, None)?;
// 3. Let list be a new empty List.
let mut list = 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
while let Some(item) = iterator.step(context)? {
// i. Let nextValue be ? IteratorValue(next).
let item = item.value(context)?;
// ii. If Type(nextValue) is not String, then
let Some(s) = item.as_string().cloned() else {
// 1. Let error be ThrowCompletion(a newly created TypeError object).
// 2. Return ? IteratorClose(iteratorRecord, error).
return Err(iterator
.close(
Err(JsNativeError::typ()
.with_message("StringListFromIterable: can only format strings into a list")
.into()),
context,
)
.expect_err("Should return the provided error"));
};
// iii. Append nextValue to the end of the List list.
list.push(s);
}
// 6. Return list.
Ok(list)
}

View File

@@ -0,0 +1,53 @@
use std::str::FromStr;
use icu_list::ListLength;
use crate::{
builtins::intl::options::{OptionType, OptionTypeParsable},
Context, JsNativeError, JsResult, JsValue,
};
#[derive(Debug, Clone, Copy, Default)]
pub(crate) enum ListFormatType {
#[default]
Conjunction,
Disjunction,
Unit,
}
#[derive(Debug)]
pub(crate) struct ParseListFormatTypeError;
impl std::fmt::Display for ParseListFormatTypeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not `conjunction`, `disjunction` or `unit`")
}
}
impl FromStr for ListFormatType {
type Err = ParseListFormatTypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"conjunction" => Ok(Self::Conjunction),
"disjunction" => Ok(Self::Disjunction),
"unit" => Ok(Self::Unit),
_ => Err(ParseListFormatTypeError),
}
}
}
impl OptionTypeParsable for ListFormatType {}
impl OptionType for ListLength {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
match value.to_string(context)?.to_std_string_escaped().as_str() {
"long" => Ok(Self::Wide),
"short" => Ok(Self::Short),
"narrow" => Ok(Self::Narrow),
_ => Err(JsNativeError::range()
.with_message("provided string was not `long`, `short` or `narrow`")
.into()),
}
}
}

View File

@@ -0,0 +1,769 @@
use boa_profiler::Profiler;
use icu_collator::CaseFirst;
use icu_datetime::options::preferences::HourCycle;
use icu_locid::{
extensions::unicode::Value,
extensions_unicode_key as key, extensions_unicode_value as value,
subtags::{Language, Region, Script},
};
use tap::{Conv, Pipe};
#[cfg(test)]
mod tests;
mod utils;
pub(crate) use utils::*;
mod options;
use crate::{
builtins::{BuiltIn, JsArgs},
context::intrinsics::StandardConstructors,
js_string,
native_function::NativeFunction,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder,
FunctionObjectBuilder, JsObject, ObjectData,
},
property::Attribute,
symbol::JsSymbol,
Context, JsNativeError, JsResult, JsString, JsValue,
};
use super::options::{coerce_options_to_object, get_option};
#[derive(Debug, Clone)]
pub(crate) struct Locale;
impl BuiltIn for Locale {
const NAME: &'static str = "Locale";
fn init(context: &mut Context<'_>) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
let base_name =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::base_name))
.name("get baseName")
.constructor(false)
.build();
let calendar =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::calendar))
.name("get calendar")
.constructor(false)
.build();
let case_first =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::case_first))
.name("get caseFirst")
.constructor(false)
.build();
let collation =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::collation))
.name("get collation")
.constructor(false)
.build();
let hour_cycle =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::hour_cycle))
.name("get hourCycle")
.constructor(false)
.build();
let numeric =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::numeric))
.name("get numeric")
.constructor(false)
.build();
let numbering_system = FunctionObjectBuilder::new(
context,
NativeFunction::from_fn_ptr(Self::numbering_system),
)
.name("get numberingSystem")
.constructor(false)
.build();
let language =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::language))
.name("get language")
.constructor(false)
.build();
let script = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::script))
.name("get script")
.constructor(false)
.build();
let region = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::region))
.name("get region")
.constructor(false)
.build();
ConstructorBuilder::with_standard_constructor(
context,
Self::constructor,
context.intrinsics().constructors().locale().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.property(
JsSymbol::to_string_tag(),
"Intl.Locale",
Attribute::CONFIGURABLE,
)
.method(Self::maximize, "maximize", 0)
.method(Self::minimize, "minimize", 0)
.method(Self::to_string, "toString", 0)
.accessor("baseName", Some(base_name), None, Attribute::CONFIGURABLE)
.accessor("calendar", Some(calendar), None, Attribute::CONFIGURABLE)
.accessor("caseFirst", Some(case_first), None, Attribute::CONFIGURABLE)
.accessor("collation", Some(collation), None, Attribute::CONFIGURABLE)
.accessor("hourCycle", Some(hour_cycle), None, Attribute::CONFIGURABLE)
.accessor("numeric", Some(numeric), None, Attribute::CONFIGURABLE)
.accessor(
"numberingSystem",
Some(numbering_system),
None,
Attribute::CONFIGURABLE,
)
.accessor("language", Some(language), None, Attribute::CONFIGURABLE)
.accessor("script", Some(script), None, Attribute::CONFIGURABLE)
.accessor("region", Some(region), None, Attribute::CONFIGURABLE)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Locale {
pub(crate) const LENGTH: usize = 1;
/// Constructor [`Intl.Locale ( tag [ , options ] )`][spec].
///
/// Constructor for `Locale` objects.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.Locale
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale
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 Err(JsNativeError::typ()
.with_message("cannot call `Intl.Locale` constructor without `new`")
.into());
}
let tag = args.get_or_undefined(0);
let options = args.get_or_undefined(1);
// 2. Let relevantExtensionKeys be %Locale%.[[RelevantExtensionKeys]].
// 3. Let internalSlotsList be « [[InitializedLocale]], [[Locale]], [[Calendar]], [[Collation]], [[HourCycle]], [[NumberingSystem]] ».
// 4. If relevantExtensionKeys contains "kf", then
// a. Append [[CaseFirst]] as the last element of internalSlotsList.
// 5. If relevantExtensionKeys contains "kn", then
// a. Append [[Numeric]] as the last element of internalSlotsList.
// 7. If Type(tag) is not String or Object, throw a TypeError exception.
if !(tag.is_object() || tag.is_string()) {
return Err(JsNativeError::typ()
.with_message("Intl.Locale: `tag` should be a String or Object")
.into());
}
// 8. If Type(tag) is Object and tag has an [[InitializedLocale]] internal slot, then
let mut tag = if let Some(tag) = tag
.as_object()
.and_then(|obj| obj.borrow().as_locale().cloned())
{
// a. Let tag be tag.[[Locale]].
tag
}
// 9. Else,
else {
// a. Let tag be ? ToString(tag).
tag.to_string(context)?
.to_std_string_escaped()
.parse()
.map_err(|_| {
JsNativeError::range()
.with_message("Intl.Locale: `tag` is not a structurally valid language tag")
})?
};
// 10. Set options to ? CoerceOptionsToObject(options).
let options = &coerce_options_to_object(options, context)?;
// 11. Set tag to ? ApplyOptionsToTag(tag, options).
// Abstract operation [`ApplyOptionsToTag ( tag, options )`][https://tc39.es/ecma402/#sec-apply-options-to-tag]
{
// 1. Assert: Type(tag) is String.
// 2. Assert: Type(options) is Object.
// 3. If ! IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
// 4. Let language be ? GetOption(options, "language", string, empty, undefined).
// 5. If language is not undefined, then
let language = get_option::<JsString>(options, "language", false, context)?
// a. If language does not match the unicode_language_subtag production, throw a RangeError exception.
.map(|s| s.to_std_string_escaped().parse::<Language>())
.transpose()
.map_err(|e| JsNativeError::range().with_message(e.to_string()))?;
// 6. Let script be ? GetOption(options, "script", string, empty, undefined).
// 7. If script is not undefined, then
let script = get_option::<JsString>(options, "script", false, context)?
.map(|s| s.to_std_string_escaped().parse::<Script>())
.transpose()
// a. If script does not match the unicode_script_subtag production, throw a RangeError exception.
.map_err(|e| JsNativeError::range().with_message(e.to_string()))?;
// 8. Let region be ? GetOption(options, "region", string, empty, undefined).
// 9. If region is not undefined, then
let region = get_option::<JsString>(options, "region", false, context)?
.map(|s| s.to_std_string_escaped().parse::<Region>())
.transpose()
// a. If region does not match the unicode_region_subtag production, throw a RangeError exception.
.map_err(|e| JsNativeError::range().with_message(e.to_string()))?;
// 10. Set tag to ! CanonicalizeUnicodeLocaleId(tag).
context.icu().locale_canonicalizer().canonicalize(&mut tag);
// Skipping some boilerplate since this is easier to do using the `Locale` type, but putting the
// spec for completion.
// 11. Assert: tag matches the unicode_locale_id production.
// 12. Let languageId be the substring of tag corresponding to the unicode_language_id production.
// 13. If language is not undefined, then
// a. Set languageId to languageId with the substring corresponding to the unicode_language_subtag production replaced by the string language.
// 14. If script is not undefined, then
// a. If languageId does not contain a unicode_script_subtag production, then
// i. Set languageId to the string-concatenation of the unicode_language_subtag production of languageId, "-", script, and the rest of languageId.
// b. Else,
// i. Set languageId to languageId with the substring corresponding to the unicode_script_subtag production replaced by the string script.
// 15. If region is not undefined, then
// a. If languageId does not contain a unicode_region_subtag production, then
// i. Set languageId to the string-concatenation of the unicode_language_subtag production of languageId, the substring corresponding to "-"` and the `unicode_script_subtag` production if present, `"-", region, and the rest of languageId.
// b. Else,
// i. Set languageId to languageId with the substring corresponding to the unicode_region_subtag production replaced by the string region.
// 16. Set tag to tag with the substring corresponding to the unicode_language_id production replaced by the string languageId.
if let Some(language) = language {
tag.id.language = language;
}
if let Some(script) = script {
tag.id.script = Some(script);
}
if let Some(region) = region {
tag.id.region = Some(region);
}
// 17. Return ! CanonicalizeUnicodeLocaleId(tag).
context.icu().locale_canonicalizer().canonicalize(&mut tag);
}
// 12. Let opt be a new Record.
// 13. Let calendar be ? GetOption(options, "calendar", string, empty, undefined).
// 14. If calendar is not undefined, then
// 15. Set opt.[[ca]] to calendar.
// a. If calendar does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
let ca = get_option::<Value>(options, "calendar", false, context)?;
// 16. Let collation be ? GetOption(options, "collation", string, empty, undefined).
// 17. If collation is not undefined, then
// 18. Set opt.[[co]] to collation.
// a. If collation does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
let co = get_option::<Value>(options, "collation", false, context)?;
// 19. Let hc be ? GetOption(options, "hourCycle", string, « "h11", "h12", "h23", "h24" », undefined).
// 20. Set opt.[[hc]] to hc.
let hc =
get_option::<HourCycle>(options, "hourCycle", false, context)?.map(|hc| match hc {
HourCycle::H24 => value!("h24"),
HourCycle::H23 => value!("h23"),
HourCycle::H12 => value!("h12"),
HourCycle::H11 => value!("h11"),
});
// 21. Let kf be ? GetOption(options, "caseFirst", string, « "upper", "lower", "false" », undefined).
// 22. Set opt.[[kf]] to kf.
let kf =
get_option::<CaseFirst>(options, "caseFirst", false, context)?.map(|kf| match kf {
CaseFirst::UpperFirst => value!("upper"),
CaseFirst::LowerFirst => value!("lower"),
CaseFirst::Off => value!("false"),
_ => unreachable!(),
});
// 23. Let kn be ? GetOption(options, "numeric", boolean, empty, undefined).
// 24. If kn is not undefined, set kn to ! ToString(kn).
// 25. Set opt.[[kn]] to kn.
let kn = get_option::<bool>(options, "numeric", false, context)?.map(|b| {
if b {
value!("true")
} else {
value!("false")
}
});
// 26. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined).
// 27. If numberingSystem is not undefined, then
// 28. Set opt.[[nu]] to numberingSystem.
// a. If numberingSystem does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
let nu = get_option::<Value>(options, "numberingSystem", false, context)?;
// 29. Let r be ! ApplyUnicodeExtensionToTag(tag, opt, relevantExtensionKeys).
// 30. Set locale.[[Locale]] to r.[[locale]].
if let Some(ca) = ca {
// 31. Set locale.[[Calendar]] to r.[[ca]].
tag.extensions.unicode.keywords.set(key!("ca"), ca);
}
if let Some(co) = co {
// 32. Set locale.[[Collation]] to r.[[co]].
tag.extensions.unicode.keywords.set(key!("co"), co);
}
if let Some(hc) = hc {
// 33. Set locale.[[HourCycle]] to r.[[hc]].
tag.extensions.unicode.keywords.set(key!("hc"), hc);
}
if let Some(kf) = kf {
// 34. If relevantExtensionKeys contains "kf", then
// a. Set locale.[[CaseFirst]] to r.[[kf]].
tag.extensions.unicode.keywords.set(key!("kf"), kf);
}
if let Some(kn) = kn {
// 35. If relevantExtensionKeys contains "kn", then
// a. If SameValue(r.[[kn]], "true") is true or r.[[kn]] is the empty String, then
// i. Set locale.[[Numeric]] to true.
// b. Else,
// i. Set locale.[[Numeric]] to false.
tag.extensions.unicode.keywords.set(key!("kn"), kn);
}
if let Some(nu) = nu {
// 36. Set locale.[[NumberingSystem]] to r.[[nu]].
tag.extensions.unicode.keywords.set(key!("nu"), nu);
}
context.icu().locale_canonicalizer().canonicalize(&mut tag);
// 6. Let locale be ? OrdinaryCreateFromConstructor(NewTarget, "%Locale.prototype%", internalSlotsList).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::locale, context)?;
let locale = JsObject::from_proto_and_data(prototype, ObjectData::locale(tag));
// 37. Return locale.
Ok(locale.into())
}
/// [`Intl.Locale.prototype.maximize ( )`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.maximize
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/maximize
pub(crate) fn maximize(
this: &JsValue,
_: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("`maximize` can only be called on a `Locale` object")
})?;
let mut loc = loc
.as_locale()
.ok_or_else(|| {
JsNativeError::typ()
.with_message("`maximize` can only be called on a `Locale` object")
})?
.clone();
// 3. Let maximal be the result of the Add Likely Subtags algorithm applied to loc.[[Locale]]. If an error is signaled, set maximal to loc.[[Locale]].
context.icu().locale_expander().maximize(&mut loc);
// 4. Return ! Construct(%Locale%, maximal).
let prototype = context.intrinsics().constructors().locale().prototype();
Ok(JsObject::from_proto_and_data(prototype, ObjectData::locale(loc)).into())
}
/// [`Intl.Locale.prototype.minimize ( )`][spec]
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.minimize
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/minimize
pub(crate) fn minimize(
this: &JsValue,
_: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("`minimize` can only be called on a `Locale` object")
})?;
let mut loc = loc
.as_locale()
.ok_or_else(|| {
JsNativeError::typ()
.with_message("`minimize` can only be called on a `Locale` object")
})?
.clone();
// 3. Let minimal be the result of the Remove Likely Subtags algorithm applied to loc.[[Locale]]. If an error is signaled, set minimal to loc.[[Locale]].
context.icu().locale_expander().minimize(&mut loc);
// 4. Return ! Construct(%Locale%, minimal).
let prototype = context.intrinsics().constructors().locale().prototype();
Ok(JsObject::from_proto_and_data(prototype, ObjectData::locale(loc)).into())
}
/// [`Intl.Locale.prototype.toString ( )`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.toString
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/toString
pub(crate) fn to_string(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("`toString` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ().with_message("`toString` can only be called on a `Locale` object")
})?;
// 3. Return loc.[[Locale]].
Ok(js_string!(loc.to_string()).into())
}
/// [`get Intl.Locale.prototype.baseName`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/baseName
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.baseName
pub(crate) fn base_name(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get baseName` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get baseName` can only be called on a `Locale` object")
})?;
// 3. Let locale be loc.[[Locale]].
// 4. Return the substring of locale corresponding to the unicode_language_id production.
Ok(js_string!(loc.id.to_string()).into())
}
/// [`get Intl.Locale.prototype.calendar`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.calendar
pub(crate) fn calendar(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get calendar` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get calendar` can only be called on a `Locale` object")
})?;
// 3. Return loc.[[Calendar]].
Ok(loc
.extensions
.unicode
.keywords
.get(&key!("ca"))
.map(|v| js_string!(v.to_string()).into())
.unwrap_or_default())
}
/// [`get Intl.Locale.prototype.caseFirst`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.calendar
pub(crate) fn case_first(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get caseFirst` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get caseFirst` can only be called on a `Locale` object")
})?;
// 3. Return loc.[[CaseFirst]].
Ok(loc
.extensions
.unicode
.keywords
.get(&key!("kf"))
.map(|v| js_string!(v.to_string()).into())
.unwrap_or_default())
}
/// [`get Intl.Locale.prototype.collation`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/collation
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.collation
pub(crate) fn collation(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get collation` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get collation` can only be called on a `Locale` object")
})?;
// 3. Return loc.[[Collation]].
Ok(loc
.extensions
.unicode
.keywords
.get(&key!("co"))
.map(|v| js_string!(v.to_string()).into())
.unwrap_or_default())
}
/// [`get Intl.Locale.prototype.hourCycle`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.hourCycle
pub(crate) fn hour_cycle(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get hourCycle` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get hourCycle` can only be called on a `Locale` object")
})?;
// 3. Return loc.[[HourCycle]].
Ok(loc
.extensions
.unicode
.keywords
.get(&key!("hc"))
.map(|v| js_string!(v.to_string()).into())
.unwrap_or_default())
}
/// [`get Intl.Locale.prototype.numeric`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numeric
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.numeric
pub(crate) fn numeric(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get numeric` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get numeric` can only be called on a `Locale` object")
})?;
// 3. Return loc.[[Numeric]].
let kn = loc
.extensions
.unicode
.keywords
.get(&key!("kn"))
.map(Value::as_tinystr_slice);
Ok(JsValue::Boolean(match kn {
Some([]) => true,
Some([kn]) if kn == "true" => true,
_ => false,
}))
}
/// [`get Intl.Locale.prototype.numberingSystem`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numeric
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.numeric
pub(crate) fn numbering_system(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get numberingSystem` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get numberingSystem` can only be called on a `Locale` object")
})?;
// 3. Return loc.[[NumberingSystem]].
Ok(loc
.extensions
.unicode
.keywords
.get(&key!("nu"))
.map(|v| js_string!(v.to_string()).into())
.unwrap_or_default())
}
/// [`get Intl.Locale.prototype.language`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/language
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.language
pub(crate) fn language(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get language` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get language` can only be called on a `Locale` object")
})?;
// 3. Let locale be loc.[[Locale]].
// 4. Assert: locale matches the unicode_locale_id production.
// 5. Return the substring of locale corresponding to the unicode_language_subtag production of the unicode_language_id.
Ok(js_string!(loc.id.language.to_string()).into())
}
/// [`get Intl.Locale.prototype.script`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/script
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.script
pub(crate) fn script(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get script` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get script` can only be called on a `Locale` object")
})?;
// 3. Let locale be loc.[[Locale]].
// 4. Assert: locale matches the unicode_locale_id production.
// 5. If the unicode_language_id production of locale does not contain the ["-" unicode_script_subtag] sequence, return undefined.
// 6. Return the substring of locale corresponding to the unicode_script_subtag production of the unicode_language_id.
Ok(loc
.id
.script
.map(|sc| js_string!(sc.to_string()).into())
.unwrap_or_default())
}
/// [`get Intl.Locale.prototype.region`][spec].
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/region
/// [mdn]: https://tc39.es/ecma402/#sec-Intl.Locale.prototype.region
pub(crate) fn region(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
let loc = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ()
.with_message("`get region` can only be called on a `Locale` object")
})?;
let loc = loc.as_locale().ok_or_else(|| {
JsNativeError::typ()
.with_message("`get region` can only be called on a `Locale` object")
})?;
// 3. Let locale be loc.[[Locale]].
// 4. Assert: locale matches the unicode_locale_id production.
// 5. If the unicode_language_id production of locale does not contain the ["-" unicode_region_subtag] sequence, return undefined.
// 6. Return the substring of locale corresponding to the unicode_region_subtag production of the unicode_language_id.
Ok(loc
.id
.region
.map(|sc| js_string!(sc.to_string()).into())
.unwrap_or_default())
}
}

View File

@@ -0,0 +1,21 @@
use icu_locid::extensions::unicode::Value;
use crate::{builtins::intl::options::OptionType, Context, JsNativeError};
impl OptionType for Value {
fn from_value(value: crate::JsValue, context: &mut Context<'_>) -> crate::JsResult<Self> {
let val = value
.to_string(context)?
.to_std_string_escaped()
.parse::<Value>()
.map_err(|e| JsNativeError::range().with_message(e.to_string()))?;
if val.as_tinystr_slice().is_empty() {
return Err(JsNativeError::range()
.with_message("Unicode Locale Identifier `type` cannot be empty")
.into());
}
Ok(val)
}
}

View File

@@ -0,0 +1,121 @@
use icu_datetime::{
options::preferences::HourCycle, pattern::CoarseHourCycle,
provider::calendar::TimeLengthsV1Marker,
};
use icu_locid::{
extensions::unicode::Value, extensions_unicode_key as key, extensions_unicode_value as value,
locale, Locale,
};
use icu_plurals::provider::CardinalV1Marker;
use icu_provider::{DataLocale, DataProvider, DataRequest, DataRequestMetadata};
use crate::{
builtins::intl::{
locale::{best_locale_for_provider, default_locale, resolve_locale},
options::{IntlOptions, LocaleMatcher},
Service,
},
context::icu::{BoaProvider, Icu},
};
#[derive(Debug)]
struct TestOptions {
hc: Option<HourCycle>,
}
struct TestService;
impl Service for TestService {
type LangMarker = CardinalV1Marker;
type LocaleOptions = TestOptions;
fn resolve(locale: &mut Locale, options: &mut Self::LocaleOptions, provider: BoaProvider<'_>) {
let loc_hc = locale
.extensions
.unicode
.keywords
.get(&key!("hc"))
.and_then(Value::as_single_subtag)
.and_then(|s| match &**s {
"h11" => Some(HourCycle::H11),
"h12" => Some(HourCycle::H12),
"h23" => Some(HourCycle::H23),
"h24" => Some(HourCycle::H24),
_ => None,
});
let hc = options.hc.or(loc_hc).unwrap_or_else(|| {
let req = DataRequest {
locale: &DataLocale::from(&*locale),
metadata: DataRequestMetadata::default(),
};
let preferred = DataProvider::<TimeLengthsV1Marker>::load(&provider, req)
.unwrap()
.take_payload()
.unwrap()
.get()
.preferred_hour_cycle;
match preferred {
CoarseHourCycle::H11H12 => HourCycle::H11,
CoarseHourCycle::H23H24 => HourCycle::H23,
}
});
let hc_value = match hc {
HourCycle::H11 => value!("h11"),
HourCycle::H12 => value!("h12"),
HourCycle::H23 => value!("h23"),
HourCycle::H24 => value!("h24"),
};
locale.extensions.unicode.keywords.set(key!("hc"), hc_value);
options.hc = Some(hc);
}
}
#[test]
fn locale_resolution() {
let provider = boa_icu_provider::buffer();
let icu = Icu::new(BoaProvider::Buffer(provider)).unwrap();
let mut default = default_locale(icu.locale_canonicalizer());
default
.extensions
.unicode
.keywords
.set(key!("hc"), value!("h11"));
// test lookup
let mut options = IntlOptions {
matcher: LocaleMatcher::Lookup,
service_options: TestOptions {
hc: Some(HourCycle::H11),
},
};
let locale = resolve_locale::<TestService>(&[], &mut options, &icu);
assert_eq!(locale, default);
// test best fit
let mut options = IntlOptions {
matcher: LocaleMatcher::BestFit,
service_options: TestOptions {
hc: Some(HourCycle::H11),
},
};
let locale = resolve_locale::<TestService>(&[], &mut options, &icu);
let best = best_locale_for_provider::<<TestService as Service>::LangMarker>(
default.id.clone(),
&icu.provider(),
)
.unwrap();
let mut best = Locale::from(best);
best.extensions = locale.extensions.clone();
assert_eq!(locale, best);
// requested: [es-ES]
let mut options = IntlOptions {
matcher: LocaleMatcher::Lookup,
service_options: TestOptions { hc: None },
};
let locale = resolve_locale::<TestService>(&[locale!("es-AR")], &mut options, &icu);
assert_eq!(locale, "es-u-hc-h23".parse().unwrap());
}

View File

@@ -0,0 +1,608 @@
use crate::{
builtins::{
intl::{
options::{coerce_options_to_object, get_option, IntlOptions, LocaleMatcher},
Service,
},
Array,
},
context::{icu::Icu, BoaProvider},
object::JsObject,
Context, JsNativeError, JsResult, JsValue,
};
use icu_collator::provider::CollationMetadataV1Marker;
use icu_locid::{
extensions::unicode::{Key, Value},
subtags::Variants,
LanguageIdentifier, Locale,
};
use icu_locid_transform::LocaleCanonicalizer;
use icu_provider::{DataLocale, DataProvider, DataRequest, DataRequestMetadata, KeyedDataMarker};
use indexmap::IndexSet;
use tap::TapOptional;
/// 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
pub(crate) fn default_locale(canonicalizer: &LocaleCanonicalizer) -> Locale {
sys_locale::get_locale()
.and_then(|loc| loc.parse::<Locale>().ok())
.tap_some_mut(|loc| {
canonicalizer.canonicalize(loc);
})
.unwrap_or_default()
}
/// 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 [`Locale`]s instead 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
pub(crate) fn canonicalize_locale_list(
locales: &JsValue,
context: &mut Context<'_>,
) -> JsResult<Vec<Locale>> {
// 1. If locales is undefined, then
if locales.is_undefined() {
// a. Return a new empty List.
return Ok(Vec::default());
}
// 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
let o = if locales.is_string()
|| locales
.as_object()
.map_or(false, |o| o.borrow().is_locale())
{
// 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 Err(JsNativeError::typ()
.with_message("locale should be a String or Object")
.into());
}
// iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then
let mut tag = if let Some(tag) = k_value
.as_object()
.and_then(|obj| obj.borrow().as_locale().cloned())
{
// 1. Let tag be kValue.[[Locale]].
tag
}
// iv. Else,
else {
// 1. Let tag be ? ToString(kValue).
k_value
.to_string(context)?
.to_std_string_escaped()
.parse()
// v. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
.map_err(|_| {
JsNativeError::range()
.with_message("locale is not a structurally valid language tag")
})?
};
// vi. Let canonicalizedTag be CanonicalizeUnicodeLocaleId(tag).
context.icu().locale_canonicalizer().canonicalize(&mut tag);
// vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
seen.insert(tag);
}
// d. Increase k by 1.
}
// 8. Return seen.
Ok(seen.into_iter().collect())
}
/// 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.
///
/// We only work with language identifiers, which have the same semantics
/// but are a bit easier to manipulate.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-bestavailablelocale
pub(crate) fn best_available_locale<M: KeyedDataMarker>(
candidate: LanguageIdentifier,
provider: &(impl DataProvider<M> + ?Sized),
) -> Option<LanguageIdentifier> {
// 1. Let candidate be locale.
let mut candidate = candidate.into();
// 2. Repeat
loop {
// a. If availableLocales contains an element equal to candidate, return candidate.
// ICU4X requires doing data requests in order to check if a locale
// is part of the set of supported locales.
let response = DataProvider::<M>::load(
provider,
DataRequest {
locale: &candidate,
metadata: DataRequestMetadata::default(),
},
);
if let Ok(req) = response {
// `metadata.locale` returns None when the provider doesn't have a fallback mechanism,
// but supports the required locale. However, if the provider has a fallback mechanism,
// this will return `Some(locale)`, where the locale is the used locale after applying
// the fallback algorithm, even if the used locale is exactly the same as the required
// locale.
match req.metadata.locale {
Some(loc)
if loc == candidate
// TODO: ugly hack to accept locales that fallback to "und" in the collator service
|| (loc.is_empty() && M::KEY.path() == CollationMetadataV1Marker::KEY.path()) =>
{
return Some(candidate.into_locale().id)
}
None => return Some(candidate.into_locale().id),
_ => {}
}
}
// b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined.
// c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2.
// d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
//
// Since the definition of `LanguageIdentifier` allows us to manipulate it
// without using strings, we can replace these steps by a simpler
// algorithm.
if candidate.has_variants() {
let mut variants = candidate
.clear_variants()
.iter()
.copied()
.collect::<Vec<_>>();
variants.pop();
candidate.set_variants(Variants::from_vec_unchecked(variants));
} else if candidate.region().is_some() {
candidate.set_region(None);
} else if candidate.script().is_some() {
candidate.set_script(None);
} else {
return None;
}
}
}
/// Returns the locale resolved by the `provider` after using the ICU4X fallback
/// algorithm with `candidate` (if the provider supports this), or None if the locale is not
/// supported.
pub(crate) fn best_locale_for_provider<M: KeyedDataMarker>(
candidate: LanguageIdentifier,
provider: &(impl DataProvider<M> + ?Sized),
) -> Option<LanguageIdentifier> {
let response = DataProvider::<M>::load(
provider,
DataRequest {
locale: &DataLocale::from(&candidate),
metadata: DataRequestMetadata::default(),
},
)
.ok()?;
if candidate == LanguageIdentifier::UND {
return Some(LanguageIdentifier::UND);
}
response
.metadata
.locale
.map(|dl| {
// TODO: ugly hack to accept locales that fallback to "und" in the collator service
if M::KEY.path() == CollationMetadataV1Marker::KEY.path() && dl.is_empty() {
candidate.clone()
} else {
dl.into_locale().id
}
})
.or(Some(candidate))
.filter(|loc| loc != &LanguageIdentifier::UND)
}
/// Abstract operation [`LookupMatcher ( availableLocales, requestedLocales )`][spec]
///
/// 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.
///
/// # Note
///
/// This differs a bit from the spec, since we don't have an `[[AvailableLocales]]`
/// list to compare with. However, we can do data requests to a [`DataProvider`]
/// in order to see if a certain [`Locale`] is supported.
///
/// [spec]: https://tc39.es/ecma402/#sec-lookupmatcher
fn lookup_matcher<'provider, M: KeyedDataMarker>(
requested_locales: &[Locale],
icu: &Icu<'provider>,
) -> Locale
where
BoaProvider<'provider>: DataProvider<M>,
{
// 1. Let result be a new Record.
// 2. For each element locale of requestedLocales, do
for locale in requested_locales {
// a. Let noExtensionsLocale be the String value that is locale with any Unicode locale
// extension sequences removed.
let mut locale = locale.clone();
let id = std::mem::take(&mut locale.id);
// b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
let available_locale = best_available_locale::<M>(id, &icu.provider());
// 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
// 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.
locale.id = available_locale;
// iii. Return result.
return locale;
}
}
// 3. Let defLocale be ! DefaultLocale().
// 4. Set result.[[locale]] to defLocale.
// 5. Return result.
default_locale(icu.locale_canonicalizer())
}
/// Abstract operation [`BestFitMatcher ( availableLocales, requestedLocales )`][spec]
///
/// 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.
///
/// [spec]: https://tc39.es/ecma402/#sec-bestfitmatcher
fn best_fit_matcher<'provider, M: KeyedDataMarker>(
requested_locales: &[Locale],
icu: &Icu<'provider>,
) -> Locale
where
BoaProvider<'provider>: DataProvider<M>,
{
for mut locale in requested_locales
.iter()
.cloned()
.chain(std::iter::once_with(|| {
default_locale(icu.locale_canonicalizer())
}))
{
let id = std::mem::take(&mut locale.id);
if let Some(available) = best_locale_for_provider(id, &icu.provider()) {
locale.id = available;
return locale;
}
}
Locale::default()
}
/// 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
pub(in crate::builtins::intl) fn resolve_locale<'provider, S>(
requested_locales: &[Locale],
options: &mut IntlOptions<S::LocaleOptions>,
icu: &Icu<'provider>,
) -> Locale
where
S: Service,
BoaProvider<'provider>: DataProvider<S::LangMarker>,
{
// 1. Let matcher be options.[[localeMatcher]].
// 2. If matcher is "lookup", then
// a. Let r be ! LookupMatcher(availableLocales, requestedLocales).
// 3. Else,
// a. Let r be ! BestFitMatcher(availableLocales, requestedLocales).
// 4. Let foundLocale be r.[[locale]].
let mut found_locale = if options.matcher == LocaleMatcher::Lookup {
lookup_matcher::<S::LangMarker>(requested_locales, icu)
} else {
best_fit_matcher::<S::LangMarker>(requested_locales, icu)
};
// From here, the spec differs significantly from the implementation,
// since ICU4X allows us to skip some steps and modularize the
// extension resolution algorithm. However, the original spec is left here
// for completion purposes.
// 5. Let result be a new Record.
// 6. Set result.[[dataLocale]] to foundLocale.
// 7. If r has an [[extension]] field, then
// a. Let components be ! UnicodeExtensionComponents(r.[[extension]]).
// b. Let keywords be components.[[Keywords]].
// 9. For each element key of relevantExtensionKeys, do
// a. Let foundLocaleData be localeData.[[<foundLocale>]].
// b. Assert: Type(foundLocaleData) is Record.
// c. Let keyLocaleData be foundLocaleData.[[<key>]].
// d. Assert: Type(keyLocaleData) is List.
// e. Let value be keyLocaleData[0].
// f. Assert: Type(value) is either String or Null.
// g. Let supportedExtensionAddition be "".
// h. If r has an [[extension]] field, then
// 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.
// 2. Let requestedValue be entry.[[Value]].
// 3. If requestedValue is not the empty String, then
// a. If keyLocaleData contains requestedValue, then
// i. Let value be requestedValue.
// ii. Let supportedExtensionAddition be the string-concatenation of "-", key, "-", and value.
// 4. Else if keyLocaleData contains "true", then
// a. Let value be "true".
// b. Let supportedExtensionAddition be the string-concatenation of "-" and key.
// i. If options has a field [[<key>]], then
// i. Let optionsValue be options.[[<key>]].
// ii. Assert: Type(optionsValue) is either String, Undefined, or Null.
// iii. If Type(optionsValue) is String, then
// 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.
// 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
// a. Let optionsValue be "true".
// iv. If keyLocaleData contains optionsValue, then
// 1. If SameValue(optionsValue, value) is false, then
// a. Let value be optionsValue.
// b. Let supportedExtensionAddition be "".
// j. Set result.[[<key>]] to value.
// k. Append supportedExtensionAddition to supportedExtension.
// 10. If the number of elements in supportedExtension is greater than 2, then
// a. Let foundLocale be InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedExtension).
// 11. Set result.[[locale]] to foundLocale.
// 12. Return result.
S::resolve(
&mut found_locale,
&mut options.service_options,
icu.provider(),
);
icu.locale_canonicalizer().canonicalize(&mut found_locale);
found_locale
}
/// Abstract operation [`LookupSupportedLocales ( availableLocales, requestedLocales )`][spec]
///
/// Returns the subset of the provided BCP 47 language priority list requestedLocales for which
/// `availableLocales` has a matching locale when using the BCP 47 Lookup algorithm. Locales appear
/// in the same order in the returned list as in `requestedLocales`.
///
/// # Note
///
/// This differs a bit from the spec, since we don't have an `[[AvailableLocales]]`
/// list to compare with. However, we can do data requests to a [`DataProvider`]
/// in order to see if a certain [`Locale`] is supported.
///
/// [spec]: https://tc39.es/ecma402/#sec-lookupsupportedlocales
fn lookup_supported_locales<M: KeyedDataMarker>(
requested_locales: &[Locale],
provider: &impl DataProvider<M>,
) -> Vec<Locale> {
// 1. Let subset be a new empty List.
// 2. For each element locale of requestedLocales, do
// a. Let noExtensionsLocale be the String value that is locale with any Unicode locale extension sequences removed.
// b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
// c. If availableLocale is not undefined, append locale to the end of subset.
// 3. Return subset.
requested_locales
.iter()
.cloned()
.filter(|loc| best_available_locale(loc.id.clone(), provider).is_some())
.collect()
}
/// Abstract operation [`BestFitSupportedLocales ( availableLocales, requestedLocales )`][spec]
///
/// Returns the subset of the provided BCP 47 language priority list `requestedLocales` for which
/// `availableLocales` has a matching locale when using the Best Fit Matcher algorithm. Locales appear
/// in the same order in the returned list as in requestedLocales.
///
/// [spec]: https://tc39.es/ecma402/#sec-bestfitsupportedlocales
fn best_fit_supported_locales<M: KeyedDataMarker>(
requested_locales: &[Locale],
provider: &impl DataProvider<M>,
) -> Vec<Locale> {
requested_locales
.iter()
.cloned()
.filter(|loc| best_locale_for_provider(loc.id.clone(), provider).is_some())
.collect()
}
/// Abstract operation [`SupportedLocales ( availableLocales, requestedLocales, options )`][spec]
///
/// Returns the subset of the provided BCP 47 language priority list requestedLocales for which
/// availableLocales has a matching locale
///
/// [spec]: https://tc39.es/ecma402/#sec-supportedlocales
pub(in crate::builtins::intl) fn supported_locales<'ctx, 'icu: 'ctx, M: KeyedDataMarker>(
requested_locales: &[Locale],
options: &JsValue,
context: &'ctx mut Context<'icu>,
) -> JsResult<JsObject>
where
BoaProvider<'icu>: DataProvider<M>,
{
// 1. Set options to ? CoerceOptionsToObject(options).
let options = coerce_options_to_object(options, context)?;
// 2. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
let matcher =
get_option::<LocaleMatcher>(&options, "localeMatcher", false, context)?.unwrap_or_default();
let elements = match matcher {
// 4. Else,
// a. Let supportedLocales be LookupSupportedLocales(availableLocales, requestedLocales).
LocaleMatcher::Lookup => {
lookup_supported_locales(requested_locales, &context.icu().provider())
}
// 3. If matcher is "best fit", then
// a. Let supportedLocales be BestFitSupportedLocales(availableLocales, requestedLocales).
LocaleMatcher::BestFit => {
best_fit_supported_locales(requested_locales, &context.icu().provider())
}
};
// 5. Return CreateArrayFromList(supportedLocales).
Ok(Array::create_array_from_list(
elements.into_iter().map(|loc| loc.to_string().into()),
context,
))
}
/// Validates that the unicode extension `key` with `value` is a valid extension value for the
/// `language`.
pub(in crate::builtins::intl) fn validate_extension<M: KeyedDataMarker>(
language: LanguageIdentifier,
key: Key,
value: &Value,
provider: &impl DataProvider<M>,
) -> bool {
let mut locale = DataLocale::from(language);
locale.set_unicode_ext(key, value.clone());
let request = DataRequest {
locale: &locale,
metadata: DataRequestMetadata::default(),
};
DataProvider::load(provider, request)
.ok()
.map(|res| res.metadata.locale.unwrap_or_else(|| locale.clone()))
.filter(|loc| loc == &locale)
.is_some()
}
#[cfg(test)]
mod tests {
use icu_locid::{langid, locale, Locale};
use icu_plurals::provider::CardinalV1Marker;
use icu_provider::AsDeserializingBufferProvider;
use crate::{
builtins::intl::locale::utils::{
best_available_locale, best_fit_matcher, default_locale, lookup_matcher,
},
context::icu::{BoaProvider, Icu},
};
#[test]
fn best_avail_loc() {
let provider = boa_icu_provider::buffer();
let provider = provider.as_deserializing();
assert_eq!(
best_available_locale::<CardinalV1Marker>(langid!("en"), &provider),
Some(langid!("en"))
);
assert_eq!(
best_available_locale::<CardinalV1Marker>(langid!("es-ES"), &provider),
Some(langid!("es"))
);
assert_eq!(
best_available_locale::<CardinalV1Marker>(langid!("kr"), &provider),
None
);
}
#[test]
fn lookup_match() {
let provider = boa_icu_provider::buffer();
let icu = Icu::new(BoaProvider::Buffer(provider)).unwrap();
// requested: []
let res = lookup_matcher::<CardinalV1Marker>(&[], &icu);
assert_eq!(res, default_locale(icu.locale_canonicalizer()));
assert!(res.extensions.is_empty());
// requested: [fr-FR-u-hc-h12]
let requested: Locale = "fr-FR-u-hc-h12".parse().unwrap();
let result = lookup_matcher::<CardinalV1Marker>(&[requested.clone()], &icu);
assert_eq!(result.id, langid!("fr"));
assert_eq!(result.extensions, requested.extensions);
// requested: [kr-KR-u-hc-h12, gr-GR-u-hc-h24-x-4a, es-ES-valencia-u-ca-gregory, uz-Cyrl]
let kr = "kr-KR-u-hc-h12".parse().unwrap();
let gr = "gr-GR-u-hc-h24-x-4a".parse().unwrap();
let es: Locale = "es-ES-valencia-u-ca-gregory".parse().unwrap();
let uz = locale!("uz-Cyrl");
let requested = vec![kr, gr, es.clone(), uz];
let res = best_fit_matcher::<CardinalV1Marker>(&requested, &icu);
assert_eq!(res.id, langid!("es"));
assert_eq!(res.extensions, es.extensions);
}
}

View File

@@ -0,0 +1,155 @@
//! Boa's implementation of ECMAScript's 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
#![allow(clippy::string_lit_as_bytes)]
use super::JsArgs;
use crate::{
builtins::intl::date_time_format::DateTimeFormat,
builtins::{Array, BuiltIn},
context::BoaProvider,
object::ObjectInitializer,
property::Attribute,
symbol::JsSymbol,
Context, JsResult, JsValue,
};
use boa_profiler::Profiler;
use icu_provider::KeyedDataMarker;
use tap::{Conv, Pipe};
pub(crate) mod collator;
pub(crate) mod date_time_format;
pub(crate) mod list_format;
pub(crate) mod locale;
mod options;
pub(crate) mod segmenter;
use self::{collator::Collator, list_format::ListFormat, locale::Locale, segmenter::Segmenter};
/// 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 collator = Collator::init(context).expect("initialization should return a constructor");
let list_format =
ListFormat::init(context).expect("initialization should return a constructor");
let locale = Locale::init(context).expect("initialization should return a constructor");
let segmenter =
Segmenter::init(context).expect("initialization should return a constructor");
let date_time_format = DateTimeFormat::init(context);
ObjectInitializer::new(context)
.property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"Collator",
collator,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"ListFormat",
list_format,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"DateTimeFormat",
date_time_format,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"Locale",
locale,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"Segmenter",
segmenter,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.function(Self::get_canonical_locales, "getCanonicalLocales", 1)
.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> {
let locales = args.get_or_undefined(0);
// 1. Let ll be ? CanonicalizeLocaleList(locales).
let ll = locale::canonicalize_locale_list(locales, context)?;
// 2. Return CreateArrayFromList(ll).
Ok(JsValue::Object(Array::create_array_from_list(
ll.into_iter().map(|loc| loc.to_string().into()),
context,
)))
}
}
/// A service component that is part of the `Intl` API.
///
/// This needs to be implemented for every `Intl` service in order to use the functions
/// defined in `locale::utils`, such as locale resolution and selection.
trait Service {
/// The data marker used by [`resolve_locale`][locale::resolve_locale] to decide
/// which locales are supported by this service.
type LangMarker: KeyedDataMarker;
/// The set of options used in the [`Service::resolve`] method to resolve the provided
/// locale.
type LocaleOptions;
/// Resolves the final value of `locale` from a set of `options`.
///
/// The provided `options` will also be modified with the final values, in case there were
/// changes in the resolution algorithm.
///
/// # Note
///
/// - A correct implementation must ensure `locale` and `options` are both written with the
/// new final values.
/// - If the implementor service doesn't contain any `[[RelevantExtensionKeys]]`, this can be
/// skipped.
fn resolve(
_locale: &mut icu_locid::Locale,
_options: &mut Self::LocaleOptions,
_provider: BoaProvider<'_>,
) {
}
}

View File

@@ -0,0 +1,252 @@
use std::{fmt::Display, str::FromStr};
use icu_collator::CaseFirst;
use crate::{
object::{JsObject, ObjectData},
Context, JsNativeError, JsResult, JsString, JsValue,
};
/// `IntlOptions` aggregates the `locale_matcher` selector and any other object
/// property needed for `Intl` object constructors.
///
/// It is used as the type of the `options` parameter in the operation `resolve_locale`.
#[derive(Debug, Default)]
pub(super) struct IntlOptions<O> {
pub(super) matcher: LocaleMatcher,
pub(super) service_options: O,
}
/// A type used as an option parameter inside the `Intl` [spec].
///
/// [spec]: https://tc39.es/ecma402
pub(super) trait OptionType: Sized {
/// Parses a [`JsValue`] into an instance of `Self`.
///
/// Roughly equivalent to the algorithm steps of [9.12.13.3-7][spec], but allows for parsing
/// steps instead of returning a pure string, number or boolean.
///
/// [spec]: https://tc39.es/ecma402/#sec-getoption
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self>;
}
pub(super) trait OptionTypeParsable: FromStr {}
impl<T: OptionTypeParsable> OptionType for T
where
T::Err: Display,
{
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
value
.to_string(context)?
.to_std_string_escaped()
.parse::<Self>()
.map_err(|err| JsNativeError::range().with_message(err.to_string()).into())
}
}
impl OptionType for bool {
fn from_value(value: JsValue, _: &mut Context<'_>) -> JsResult<Self> {
// 5. If type is "boolean", then
// a. Set value to ! ToBoolean(value).
Ok(value.to_boolean())
}
}
impl OptionType for JsString {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
// 6. If type is "string", then
// a. Set value to ? ToString(value).
value.to_string(context)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub(super) enum LocaleMatcher {
Lookup,
#[default]
BestFit,
}
#[derive(Debug)]
pub(super) struct ParseLocaleMatcherError;
impl Display for ParseLocaleMatcherError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
"provided string was not `lookup` or `best fit`".fmt(f)
}
}
impl FromStr for LocaleMatcher {
type Err = ParseLocaleMatcherError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"lookup" => Ok(Self::Lookup),
"best fit" => Ok(Self::BestFit),
_ => Err(ParseLocaleMatcherError),
}
}
}
impl OptionTypeParsable for LocaleMatcher {}
impl OptionType for CaseFirst {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
match value.to_string(context)?.to_std_string_escaped().as_str() {
"upper" => Ok(CaseFirst::UpperFirst),
"lower" => Ok(CaseFirst::LowerFirst),
"false" => Ok(CaseFirst::Off),
_ => Err(JsNativeError::range()
.with_message("provided string was not `upper`, `lower` or `false`")
.into()),
}
}
}
/// Abstract operation [`GetOption ( options, property, type, values, fallback )`][spec]
///
/// Extracts the value of the property named `property` from the provided `options` object,
/// converts it to the required `type` and checks whether it is one of a `List` of allowed
/// `values`. If `values` is undefined, there is no fixed set of values and any is permitted.
/// If the value is `undefined`, `required` determines if the function should return `None` or
/// an `Err`. Use [`Option::unwrap_or`] and friends to manage the default value.
///
/// This is a safer alternative to `GetOption`, which tries to parse from the
/// provided property a valid variant of the provided type `T`. It doesn't accept
/// a `type` parameter since the type can specify in its implementation of [`TryFrom`] whether
/// it wants to parse from a [`str`] or convert directly from a boolean or number.
///
/// [spec]: https://tc39.es/ecma402/#sec-getoption
pub(super) fn get_option<T: OptionType>(
options: &JsObject,
property: &str,
required: bool,
context: &mut Context<'_>,
) -> JsResult<Option<T>> {
// 1. Let value be ? Get(options, property).
let value = options.get(property, context)?;
// 2. If value is undefined, then
if value.is_undefined() {
return if required {
// a. If default is required, throw a RangeError exception.
Err(JsNativeError::range()
.with_message("GetOption: option value cannot be undefined")
.into())
} else {
// b. Return default.
Ok(None)
};
}
// The steps 3 to 7 must be made for each `OptionType`.
T::from_value(value, context).map(Some)
}
/// 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(super) 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 )`][spec]
///
/// Converts `value` to a `Number value`, checks whether it is in the allowed range,
/// and fills in a `fallback` value if necessary.
///
/// [spec]: https://tc39.es/ecma402/#sec-defaultnumberoption
#[allow(unused)]
pub(super) 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 Err(JsNativeError::range()
.with_message("DefaultNumberOption: value is out of range.")
.into());
}
// 4. Return floor(value).
Ok(Some(value.floor()))
}
/// Abstract operation [`GetOptionsObject ( options )`][spec]
///
/// Returns a [`JsObject`] suitable for use with [`get_option`], either `options` itself or a default empty
/// `JsObject`. It throws a `TypeError` if `options` is not undefined and not a `JsObject`.
///
/// [spec]: https://tc39.es/ecma402/#sec-getoptionsobject
pub(super) fn get_options_object(options: &JsValue) -> JsResult<JsObject> {
match options {
// If options is undefined, then
JsValue::Undefined => {
// a. Return OrdinaryObjectCreate(null).
Ok(JsObject::from_proto_and_data(None, ObjectData::ordinary()))
}
// 2. If Type(options) is Object, then
JsValue::Object(obj) => {
// a. Return options.
Ok(obj.clone())
}
// 3. Throw a TypeError exception.
_ => Err(JsNativeError::typ()
.with_message("GetOptionsObject: provided options is not an object")
.into()),
}
}
/// Abstract operation [`CoerceOptionsToObject ( options )`][spec]
///
/// Coerces `options` into a [`JsObject`] suitable for use with [`get_option`], defaulting to an empty
/// `JsObject`.
/// Because it coerces non-null primitive values into objects, its use is discouraged for new
/// functionality in favour of [`get_options_object`].
///
/// [spec]: https://tc39.es/ecma402/#sec-coerceoptionstoobject
pub(super) fn coerce_options_to_object(
options: &JsValue,
context: &mut Context<'_>,
) -> JsResult<JsObject> {
// If options is undefined, then
if options.is_undefined() {
// a. Return OrdinaryObjectCreate(null).
return Ok(JsObject::from_proto_and_data(None, ObjectData::ordinary()));
}
// 2. Return ? ToObject(options).
options.to_object(context)
}

View File

@@ -0,0 +1,45 @@
// TODO: implement `Segmenter` when https://github.com/unicode-org/icu4x/issues/2259 closes.
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
use crate::{builtins::BuiltIn, object::ConstructorBuilder, Context, JsResult, JsValue};
mod options;
#[allow(unused)]
pub(crate) use options::*;
#[derive(Debug, Clone)]
pub(crate) struct Segmenter;
impl BuiltIn for Segmenter {
const NAME: &'static str = "Segmenter";
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().segmenter().clone(),
)
.name(Self::NAME)
.length(Self::LENGTH)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Segmenter {
pub(crate) const LENGTH: usize = 0;
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn constructor(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Ok(JsValue::Undefined)
}
}

View File

@@ -0,0 +1,29 @@
#[derive(Debug, Clone, Copy, Default)]
pub(crate) enum Granularity {
#[default]
Grapheme,
Word,
Sentence,
}
#[derive(Debug)]
pub(crate) struct ParseGranularityError;
impl std::fmt::Display for ParseGranularityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not `grapheme`, `word` or `sentence`")
}
}
impl std::str::FromStr for Granularity {
type Err = ParseGranularityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"grapheme" => Ok(Self::Grapheme),
"word" => Ok(Self::Word),
"sentence" => Ok(Self::Sentence),
_ => Err(ParseGranularityError),
}
}
}

View File

@@ -0,0 +1,444 @@
use crate::{
builtins::{
iterable::{create_iter_result_object, IteratorRecord, IteratorResult},
promise::{if_abrupt_reject_promise, PromiseCapability},
JsArgs, Promise,
},
native_function::NativeFunction,
object::{FunctionObjectBuilder, JsObject, ObjectData},
property::PropertyDescriptor,
Context, JsNativeError, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
/// Create the `%AsyncFromSyncIteratorPrototype%` object.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%asyncfromsynciteratorprototype%-object
pub(crate) fn create_async_from_sync_iterator_prototype(context: &mut Context<'_>) -> JsObject {
let _timer = Profiler::global().start_event("AsyncFromSyncIteratorPrototype", "init");
let prototype = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.async_iterator_prototype(),
ObjectData::ordinary(),
);
let next_function = FunctionObjectBuilder::new(
context,
NativeFunction::from_fn_ptr(AsyncFromSyncIterator::next),
)
.name("next")
.length(1)
.build();
let return_function = FunctionObjectBuilder::new(
context,
NativeFunction::from_fn_ptr(AsyncFromSyncIterator::r#return),
)
.name("return")
.length(1)
.build();
let throw_function = FunctionObjectBuilder::new(
context,
NativeFunction::from_fn_ptr(AsyncFromSyncIterator::throw),
)
.name("throw")
.length(1)
.build();
{
let mut prototype_mut = prototype.borrow_mut();
prototype_mut.insert(
"next",
PropertyDescriptor::builder()
.value(next_function)
.writable(true)
.enumerable(false)
.configurable(true),
);
prototype_mut.insert(
"return",
PropertyDescriptor::builder()
.value(return_function)
.writable(true)
.enumerable(false)
.configurable(true),
);
prototype_mut.insert(
"throw",
PropertyDescriptor::builder()
.value(throw_function)
.writable(true)
.enumerable(false)
.configurable(true),
);
}
prototype
}
/// The internal data for `%AsyncFromSyncIterator%` objects.
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-properties-of-async-from-sync-iterator-instances
#[derive(Clone, Debug, Finalize, Trace)]
pub struct AsyncFromSyncIterator {
// The [[SyncIteratorRecord]] internal slot.
sync_iterator_record: IteratorRecord,
}
impl AsyncFromSyncIterator {
/// `CreateAsyncFromSyncIterator ( syncIteratorRecord )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createasyncfromsynciterator
pub(crate) fn create(
sync_iterator_record: IteratorRecord,
context: &mut Context<'_>,
) -> IteratorRecord {
// 1. Let asyncIterator be OrdinaryObjectCreate(%AsyncFromSyncIteratorPrototype%, « [[SyncIteratorRecord]] »).
// 2. Set asyncIterator.[[SyncIteratorRecord]] to syncIteratorRecord.
let async_iterator = JsObject::from_proto_and_data(
context
.intrinsics()
.objects()
.iterator_prototypes()
.async_from_sync_iterator_prototype(),
ObjectData::async_from_sync_iterator(Self {
sync_iterator_record,
}),
);
// 3. Let nextMethod be ! Get(asyncIterator, "next").
let next_method = async_iterator
.get("next", context)
.expect("async from sync iterator prototype must have next method");
// 4. Let iteratorRecord be the Iterator Record { [[Iterator]]: asyncIterator, [[NextMethod]]: nextMethod, [[Done]]: false }.
// 5. Return iteratorRecord.
IteratorRecord::new(async_iterator, next_method, false)
}
/// `%AsyncFromSyncIteratorPrototype%.next ( [ value ] )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%asyncfromsynciteratorprototype%.next
fn next(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot.
// 4. Let syncIteratorRecord be O.[[SyncIteratorRecord]].
let sync_iterator_record = this
.as_object()
.expect("async from sync iterator prototype must be object")
.borrow()
.as_async_from_sync_iterator()
.expect("async from sync iterator prototype must be object")
.sync_iterator_record
.clone();
// 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
context,
)
.expect("cannot fail with promise constructor");
// 5. If value is present, then
// a. Let result be Completion(IteratorNext(syncIteratorRecord, value)).
// 6. Else,
// a. Let result be Completion(IteratorNext(syncIteratorRecord)).
let result = sync_iterator_record.next(args.get(0).cloned(), context);
// 7. IfAbruptRejectPromise(result, promiseCapability).
if_abrupt_reject_promise!(result, promise_capability, context);
// 8. Return AsyncFromSyncIteratorContinuation(result, promiseCapability).
Self::continuation(&result, &promise_capability, context)
}
/// `%AsyncFromSyncIteratorPrototype%.return ( [ value ] )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%asyncfromsynciteratorprototype%.return
fn r#return(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot.
// 4. Let syncIterator be O.[[SyncIteratorRecord]].[[Iterator]].
let sync_iterator = this
.as_object()
.expect("async from sync iterator prototype must be object")
.borrow()
.as_async_from_sync_iterator()
.expect("async from sync iterator prototype must be object")
.sync_iterator_record
.iterator()
.clone();
// 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
context,
)
.expect("cannot fail with promise constructor");
// 5. Let return be Completion(GetMethod(syncIterator, "return")).
let r#return = sync_iterator.get_method("return", context);
// 6. IfAbruptRejectPromise(return, promiseCapability).
if_abrupt_reject_promise!(r#return, promise_capability, context);
let result = match (r#return, args.get(0)) {
// 7. If return is undefined, then
(None, _) => {
// a. Let iterResult be CreateIterResultObject(value, true).
let iter_result =
create_iter_result_object(args.get_or_undefined(0).clone(), true, context);
// b. Perform ! Call(promiseCapability.[[Resolve]], undefined, « iterResult »).
promise_capability
.resolve()
.call(&JsValue::Undefined, &[iter_result], context)
.expect("cannot fail according to spec");
// c. Return promiseCapability.[[Promise]].
return Ok(promise_capability.promise().clone().into());
}
// 8. If value is present, then
(Some(r#return), Some(value)) => {
// a. Let result be Completion(Call(return, syncIterator, « value »)).
r#return.call(&sync_iterator.clone().into(), &[value.clone()], context)
}
// 9. Else,
(Some(r#return), None) => {
// a. Let result be Completion(Call(return, syncIterator)).
r#return.call(&sync_iterator.clone().into(), &[], context)
}
};
// 10. IfAbruptRejectPromise(result, promiseCapability).
if_abrupt_reject_promise!(result, promise_capability, context);
let Some(result) = result.as_object() else {
// 11. If Type(result) is not Object, then
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
promise_capability
.reject()
.call(
&JsValue::Undefined,
&[JsNativeError::typ()
.with_message("iterator return function returned non-object")
.to_opaque(context)
.into()],
context,
)
.expect("cannot fail according to spec");
// b. Return promiseCapability.[[Promise]].
return Ok(promise_capability.promise().clone().into());
};
// 12. Return AsyncFromSyncIteratorContinuation(result, promiseCapability).
Self::continuation(
&IteratorResult {
object: result.clone(),
},
&promise_capability,
context,
)
}
/// `%AsyncFromSyncIteratorPrototype%.throw ( [ value ] )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-%asyncfromsynciteratorprototype%.throw
fn throw(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Assert: O is an Object that has a [[SyncIteratorRecord]] internal slot.
// 4. Let syncIterator be O.[[SyncIteratorRecord]].[[Iterator]].
let sync_iterator = this
.as_object()
.expect("async from sync iterator prototype must be object")
.borrow()
.as_async_from_sync_iterator()
.expect("async from sync iterator prototype must be object")
.sync_iterator_record
.iterator()
.clone();
// 3. Let promiseCapability be ! NewPromiseCapability(%Promise%).
let promise_capability = PromiseCapability::new(
&context
.intrinsics()
.constructors()
.promise()
.constructor()
.into(),
context,
)
.expect("cannot fail with promise constructor");
// 5. Let throw be Completion(GetMethod(syncIterator, "throw")).
let throw = sync_iterator.get_method("throw", context);
// 6. IfAbruptRejectPromise(throw, promiseCapability).
if_abrupt_reject_promise!(throw, promise_capability, context);
let result = match (throw, args.get(0)) {
// 7. If throw is undefined, then
(None, _) => {
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « value »).
promise_capability
.reject()
.call(
&JsValue::Undefined,
&[args.get_or_undefined(0).clone()],
context,
)
.expect("cannot fail according to spec");
// b. Return promiseCapability.[[Promise]].
return Ok(promise_capability.promise().clone().into());
}
// 8. If value is present, then
(Some(throw), Some(value)) => {
// a. Let result be Completion(Call(throw, syncIterator, « value »)).
throw.call(&sync_iterator.clone().into(), &[value.clone()], context)
}
// 9. Else,
(Some(throw), None) => {
// a. Let result be Completion(Call(throw, syncIterator)).
throw.call(&sync_iterator.clone().into(), &[], context)
}
};
// 10. IfAbruptRejectPromise(result, promiseCapability).
if_abrupt_reject_promise!(result, promise_capability, context);
let Some(result) = result.as_object() else {
// 11. If Type(result) is not Object, then
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
promise_capability
.reject()
.call(
&JsValue::Undefined,
&[JsNativeError::typ()
.with_message("iterator throw function returned non-object")
.to_opaque(context)
.into()],
context,
)
.expect("cannot fail according to spec");
// b. Return promiseCapability.[[Promise]].
return Ok(promise_capability.promise().clone().into());
};
// 12. Return AsyncFromSyncIteratorContinuation(result, promiseCapability).
Self::continuation(
&IteratorResult {
object: result.clone(),
},
&promise_capability,
context,
)
}
/// `AsyncFromSyncIteratorContinuation ( result, promiseCapability )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asyncfromsynciteratorcontinuation
fn continuation(
result: &IteratorResult,
promise_capability: &PromiseCapability,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. NOTE: Because promiseCapability is derived from the intrinsic %Promise%,
// the calls to promiseCapability.[[Reject]] entailed by the
// use IfAbruptRejectPromise below are guaranteed not to throw.
// 2. Let done be Completion(IteratorComplete(result)).
let done = result.complete(context);
// 3. IfAbruptRejectPromise(done, promiseCapability).
if_abrupt_reject_promise!(done, promise_capability, context);
// 4. Let value be Completion(IteratorValue(result)).
let value = result.value(context);
// 5. IfAbruptRejectPromise(value, promiseCapability).
if_abrupt_reject_promise!(value, promise_capability, context);
// 6. Let valueWrapper be Completion(PromiseResolve(%Promise%, value)).
let value_wrapper = Promise::promise_resolve(
context.intrinsics().constructors().promise().constructor(),
value,
context,
);
// 7. IfAbruptRejectPromise(valueWrapper, promiseCapability).
if_abrupt_reject_promise!(value_wrapper, promise_capability, context);
// 8. Let unwrap be a new Abstract Closure with parameters (value)
// that captures done and performs the following steps when called:
// 9. Let onFulfilled be CreateBuiltinFunction(unwrap, 1, "", « »).
let on_fulfilled = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure(move |_this, args, context| {
// a. Return CreateIterResultObject(value, done).
Ok(create_iter_result_object(
args.get_or_undefined(0).clone(),
done,
context,
))
}),
)
.name("")
.length(1)
.build();
// 10. NOTE: onFulfilled is used when processing the "value" property of an
// IteratorResult object in order to wait for its value if it is a promise and
// re-package the result in a new "unwrapped" IteratorResult object.
// 11. Perform PerformPromiseThen(valueWrapper, onFulfilled, undefined, promiseCapability).
Promise::perform_promise_then(
&value_wrapper,
&on_fulfilled.into(),
&JsValue::Undefined,
Some(promise_capability.clone()),
context,
);
// 12. Return promiseCapability.[[Promise]].
Ok(promise_capability.promise().clone().into())
}
}

View File

@@ -0,0 +1,573 @@
//! Boa's implementation of ECMAScript's `IteratorRecord` and iterator prototype objects.
mod async_from_sync_iterator;
use crate::{
builtins::{
regexp::regexp_string_iterator::RegExpStringIterator,
string::string_iterator::StringIterator, ArrayIterator, ForInIterator, MapIterator,
SetIterator,
},
error::JsNativeError,
object::{JsObject, ObjectInitializer},
symbol::JsSymbol,
Context, JsResult, JsValue,
};
use async_from_sync_iterator::create_async_from_sync_iterator_prototype;
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
pub(crate) use async_from_sync_iterator::AsyncFromSyncIterator;
/// The built-in iterator prototypes.
#[derive(Debug, Default)]
pub struct IteratorPrototypes {
/// The `IteratorPrototype` object.
iterator_prototype: JsObject,
/// The `AsyncIteratorPrototype` object.
async_iterator_prototype: JsObject,
/// The `AsyncFromSyncIteratorPrototype` prototype object.
async_from_sync_iterator_prototype: JsObject,
/// The `ArrayIteratorPrototype` prototype object.
array_iterator: JsObject,
/// The `SetIteratorPrototype` prototype object.
set_iterator: JsObject,
/// The `StringIteratorPrototype` prototype object.
string_iterator: JsObject,
/// The `RegExpStringIteratorPrototype` prototype object.
regexp_string_iterator: JsObject,
/// The `MapIteratorPrototype` prototype object.
map_iterator: JsObject,
/// The `ForInIteratorPrototype` prototype object.
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);
let async_iterator_prototype = create_async_iterator_prototype(context);
let async_from_sync_iterator_prototype = create_async_from_sync_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,
async_iterator_prototype,
async_from_sync_iterator_prototype,
}
}
/// Returns the `ArrayIteratorPrototype` object.
#[inline]
pub fn array_iterator(&self) -> JsObject {
self.array_iterator.clone()
}
/// Returns the `IteratorPrototype` object.
#[inline]
pub fn iterator_prototype(&self) -> JsObject {
self.iterator_prototype.clone()
}
/// Returns the `AsyncIteratorPrototype` object.
#[inline]
pub fn async_iterator_prototype(&self) -> JsObject {
self.async_iterator_prototype.clone()
}
/// Returns the `AsyncFromSyncIteratorPrototype` object.
#[inline]
pub fn async_from_sync_iterator_prototype(&self) -> JsObject {
self.async_from_sync_iterator_prototype.clone()
}
/// Returns the `SetIteratorPrototype` object.
#[inline]
pub fn set_iterator(&self) -> JsObject {
self.set_iterator.clone()
}
/// Returns the `StringIteratorPrototype` object.
#[inline]
pub fn string_iterator(&self) -> JsObject {
self.string_iterator.clone()
}
/// Returns the `RegExpStringIteratorPrototype` object.
#[inline]
pub fn regexp_string_iterator(&self) -> JsObject {
self.regexp_string_iterator.clone()
}
/// Returns the `MapIteratorPrototype` object.
#[inline]
pub fn map_iterator(&self) -> JsObject {
self.map_iterator.clone()
}
/// Returns the `ForInIteratorPrototype` object.
#[inline]
pub fn for_in_iterator(&self) -> JsObject {
self.for_in_iterator.clone()
}
}
/// `CreateIterResultObject( value, done )`
///
/// Generates an object supporting the `IteratorResult` interface.
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 = JsObject::with_object_proto(context);
// 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 {
/// Hints that the iterator should be sync.
Sync,
/// Hints that the iterator should be async.
Async,
}
impl JsValue {
/// `GetIterator ( obj [ , hint [ , method ] ] )`
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-getiterator
pub fn get_iterator(
&self,
context: &mut Context<'_>,
hint: Option<IteratorHint>,
method: Option<JsObject>,
) -> 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 method.is_some() {
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(JsSymbol::async_iterator(), context)? {
Some(method)
} else {
// ii. If method is undefined, then
// 1. Let syncMethod be ? GetMethod(obj, @@iterator).
let sync_method = self.get_method(JsSymbol::iterator(), context)?;
// 2. Let syncIteratorRecord be ? GetIterator(obj, sync, syncMethod).
let sync_iterator_record =
self.get_iterator(context, Some(IteratorHint::Sync), sync_method)?;
// 3. Return ! CreateAsyncFromSyncIterator(syncIteratorRecord).
return Ok(AsyncFromSyncIterator::create(sync_iterator_record, context));
}
} else {
// b. Otherwise, set method to ? GetMethod(obj, @@iterator).
self.get_method(JsSymbol::iterator(), context)?
}
}
.ok_or_else(|| {
JsNativeError::typ().with_message(format!(
"value with type `{}` is not iterable",
self.type_of()
))
})?;
// 3. Let iterator be ? Call(method, obj).
let iterator = method.call(self, &[], context)?;
// 4. If Type(iterator) is not Object, throw a TypeError exception.
let iterator_obj = iterator.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("returned 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
fn create_iterator_prototype(context: &mut Context<'_>) -> JsObject {
let _timer = Profiler::global().start_event("Iterator Prototype", "init");
let symbol_iterator = JsSymbol::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 {
/// Create a new `IteratorResult`.
pub(crate) fn new(object: JsObject) -> Self {
Self { object }
}
/// `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(Clone, Debug, Finalize, Trace)]
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`.
pub(crate) const fn iterator(&self) -> &JsObject {
&self.iterator
}
/// Get the `[[NextMethod]]` field of the `IteratorRecord`.
pub(crate) const fn next_method(&self) -> &JsValue {
&self.next_method
}
/// Get the `[[Done]]` field of the `IteratorRecord`.
pub(crate) const fn done(&self) -> bool {
self.done
}
/// Sets the `[[Done]]` field of the `IteratorRecord`.
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
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 = self.next_method.as_callable().ok_or_else(|| {
JsNativeError::typ().with_message("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.
result
.as_object()
.map(|o| IteratorResult { object: o.clone() })
.ok_or_else(|| {
JsNativeError::typ()
.with_message("next value should be an object")
.into()
})
}
/// `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
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.
Err(JsNativeError::typ()
.with_message("inner result was not an object")
.into())
}
}
}
/// `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<JsObject>,
) -> JsResult<Vec<JsValue>> {
let _timer = Profiler::global().start_event("iterable_to_list", "iterator");
// 1. If method is present, then
// a. Let iteratorRecord be ? GetIterator(items, sync, method).
// 2. Else,
// a. Let iteratorRecord be ? GetIterator(items, sync).
let iterator_record = items.get_iterator(context, Some(IteratorHint::Sync), method)?;
// 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;
/// Create the `%AsyncIteratorPrototype%` object
///
/// More information:
/// - [ECMA reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-asynciteratorprototype
fn create_async_iterator_prototype(context: &mut Context<'_>) -> JsObject {
let _timer = Profiler::global().start_event("AsyncIteratorPrototype", "init");
let symbol_iterator = JsSymbol::async_iterator();
let iterator_prototype = ObjectInitializer::new(context)
.function(
|v, _, _| Ok(v.clone()),
(symbol_iterator, "[Symbol.asyncIterator]"),
0,
)
.build();
iterator_prototype
}

View File

@@ -0,0 +1,881 @@
//! Boa's implementation of ECMAScript's 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 std::{
borrow::Cow,
iter::{once, FusedIterator},
};
use crate::{
builtins::BuiltIn,
error::JsNativeError,
js_string,
object::{JsObject, ObjectInitializer, RecursionLimiter},
property::{Attribute, PropertyNameKind},
string::{utf16, CodePoint},
symbol::JsSymbol,
value::IntegerOrInfinity,
Context, JsResult, JsString, JsValue,
};
use boa_parser::Parser;
use boa_profiler::Profiler;
use tap::{Conv, Pipe};
#[cfg(test)]
mod tests;
// `Intersperse` impl taken from `itertools`
#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
#[derive(Clone, Debug)]
struct Intersperse<I>
where
I: Iterator,
{
element: I::Item,
iter: std::iter::Fuse<I>,
peek: Option<I::Item>,
}
fn intersperse<I>(iter: I, element: I::Item) -> Intersperse<I>
where
I: Iterator,
{
let mut iter = iter.fuse();
Intersperse {
peek: iter.next(),
iter,
element,
}
}
impl<I> Iterator for Intersperse<I>
where
I: Iterator,
I::Item: Clone,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
if self.peek.is_some() {
self.peek.take()
} else {
self.peek = self.iter.next();
if self.peek.is_some() {
Some(self.element.clone())
} else {
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
type SizeHint = (usize, Option<usize>);
const fn add(a: SizeHint, b: SizeHint) -> SizeHint {
let min = a.0.saturating_add(b.0);
let max = match (a.1, b.1) {
(Some(x), Some(y)) => x.checked_add(y),
_ => None,
};
(min, max)
}
fn add_scalar(sh: SizeHint, x: usize) -> SizeHint {
let (mut low, mut hi) = sh;
low = low.saturating_add(x);
hi = hi.and_then(|elt| elt.checked_add(x));
(low, hi)
}
// 2 * SH + { 1 or 0 }
let has_peek = usize::from(self.peek.is_some());
let sh = self.iter.size_hint();
add_scalar(add(sh, sh), has_peek)
}
fn fold<B, F>(mut self, init: B, mut f: F) -> B
where
Self: Sized,
F: FnMut(B, Self::Item) -> B,
{
let mut accum = init;
if let Some(x) = self.peek.take() {
accum = f(accum, x);
}
let element = &mut self.element;
self.iter.fold(accum, |accum, x| {
let accum = f(accum, element.clone());
f(accum, x)
})
}
}
impl<I> FusedIterator for Intersperse<I>
where
I: Iterator,
I::Item: Clone,
{
}
/// 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 = JsSymbol::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)?
.to_std_string()
.map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?;
// 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::<serde_json::Value>(&json_string) {
return Err(JsNativeError::syntax().with_message(e.to_string()).into());
}
// 3. Let scriptString be the string-concatenation of "(", jsonString, and ");".
// TODO: fix script read for eval
let script_string = format!("({json_string});");
// 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 mut parser = Parser::new(script_string.as_bytes());
parser.set_json_parse();
let statement_list = parser.parse_all(context.interner_mut())?;
let code_block = context.compile_json_parse(&statement_list)?;
let unfiltered = context.execute(code_block)?;
// 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 = JsObject::with_object_proto(context);
// 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()? {
// 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")
.clone();
// 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.into(), context)?;
}
// 3. Else,
else {
// a. Perform ? CreateDataProperty(val, P, newElement).
obj.create_data_property(p, 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 = js_string!();
// 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()? {
// 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 => js_string!(" "),
IntegerOrInfinity::NegativeInfinity => js_string!(),
IntegerOrInfinity::Integer(i) if i < 1 => js_string!(),
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.
js_string!(s.get(..10).unwrap_or(s))
// 8. Else,
} else {
// a. Let gap be the empty String.
js_string!()
};
// 9. Let wrapper be ! OrdinaryObjectCreate(%Object.prototype%).
let wrapper = JsObject::with_object_proto(context);
// 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, js_string!(), &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(js_string!("null")));
}
// 6. If value is true, return "true".
// 7. If value is false, return "false".
if value.is_boolean() {
return Ok(Some(js_string!(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(js_string!("null")));
}
// 10. If Type(value) is BigInt, throw a TypeError exception.
if value.is_bigint() {
return Err(JsNativeError::typ()
.with_message("cannot serialize bigint to JSON")
.into());
}
// 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()? {
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 {
let mut buf = [0; 2];
// 1. Let product be the String value consisting solely of the code unit 0x0022 (QUOTATION MARK).
let mut product = vec!['"' as u16];
// 2. For each code point C of ! StringToCodePoints(value), do
for code_point in value.code_points() {
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.
CodePoint::Unicode('\u{0008}') => product.extend_from_slice(utf16!(r"\b")),
CodePoint::Unicode('\u{0009}') => product.extend_from_slice(utf16!(r"\t")),
CodePoint::Unicode('\u{000A}') => product.extend_from_slice(utf16!(r"\n")),
CodePoint::Unicode('\u{000C}') => product.extend_from_slice(utf16!(r"\f")),
CodePoint::Unicode('\u{000D}') => product.extend_from_slice(utf16!(r"\r")),
CodePoint::Unicode('\u{0022}') => product.extend_from_slice(utf16!(r#"\""#)),
CodePoint::Unicode('\u{005C}') => product.extend_from_slice(utf16!(r"\\")),
// 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
// 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).
CodePoint::Unicode(c) if c < '\u{0020}' => {
product.extend(format!("\\u{:04x}", c as u32).encode_utf16());
}
CodePoint::UnpairedSurrogate(surr) => {
product.extend(format!("\\u{surr:04x}").encode_utf16());
}
// c. Else,
CodePoint::Unicode(c) => {
// i. Set product to the string-concatenation of product and ! UTF16EncodeCodePoint(C).
product.extend_from_slice(c.encode_utf16(&mut buf));
}
}
}
// 3. Set product to the string-concatenation of product and the code unit 0x0022 (QUOTATION MARK).
product.push('"' as u16);
// 4. Return product.
js_string!(&product[..])
}
/// `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 Err(JsNativeError::typ()
.with_message("cyclic object value")
.into());
}
// 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 = js_string!(&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).
let mut member = Self::quote_json_string(p).to_vec();
// ii. Set member to the string-concatenation of member and ":".
member.push(':' as u16);
// iii. If state.[[Gap]] is not the empty String, then
if !state.gap.is_empty() {
// 1. Set member to the string-concatenation of member and the code unit 0x0020 (SPACE).
member.push(' ' as u16);
}
// iv. Set member to the string-concatenation of member and strP.
member.extend_from_slice(&str_p);
// 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 "{}".
js_string!("{}")
// 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 "}".
let separator = utf16!(",");
let result = once(utf16!("{"))
.chain(intersperse(partial.iter().map(Vec::as_slice), separator))
.chain(once(utf16!("}")))
.flatten()
.copied()
.collect::<Vec<_>>();
js_string!(&result[..])
// 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 mut separator = utf16!(",\n").to_vec();
separator.extend_from_slice(&state.indent);
// 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.
// 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 "}".
let result = [utf16!("{\n"), &state.indent[..]]
.into_iter()
.chain(intersperse(partial.iter().map(Vec::as_slice), &separator))
.chain([utf16!("\n"), &stepback[..], utf16!("}")].into_iter())
.flatten()
.copied()
.collect::<Vec<_>>();
js_string!(&result[..])
}
};
// 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 Err(JsNativeError::typ()
.with_message("cyclic object value")
.into());
}
// 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 = js_string!(&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(Cow::Owned(str_p.to_vec()));
// c. Else,
} else {
// i. Append "null" to partial.
partial.push(Cow::Borrowed(utf16!("null")));
}
// 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 "[]".
js_string!("[]")
// 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 "]".
let separator = utf16!(",");
let result = once(utf16!("["))
.chain(intersperse(partial.iter().map(Cow::as_ref), separator))
.chain(once(utf16!("]")))
.flatten()
.copied()
.collect::<Vec<_>>();
js_string!(&result[..])
// 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 mut separator = utf16!(",\n").to_vec();
separator.extend_from_slice(&state.indent);
// 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.
// 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 "]".
let result = [utf16!("[\n"), &state.indent[..]]
.into_iter()
.chain(intersperse(partial.iter().map(Cow::as_ref), &separator))
.chain([utf16!("\n"), &stepback[..], utf16!("]")].into_iter())
.flatten()
.copied()
.collect::<Vec<_>>();
js_string!(&result[..])
}
};
// 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,159 @@
//! This module implements the `MapIterator` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-map-iterator-objects
use super::ordered_map::MapLock;
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue},
error::JsNativeError,
object::{JsObject, ObjectData},
property::{PropertyDescriptor, PropertyNameKind},
symbol::JsSymbol,
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-map-iterator-objects
#[derive(Debug, Clone, Finalize, Trace)]
pub struct MapIterator {
iterated_map: Option<JsObject>,
map_next_index: usize,
#[unsafe_ignore_trace]
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());
}
}
Err(JsNativeError::typ()
.with_message("`this` is not a Map")
.into())
}
/// %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(|| JsNativeError::typ().with_message("`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().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 = JsSymbol::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,598 @@
//! Boa's implementation of ECMAScript's global `Map` object.
//!
//! The ECMAScript `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
use self::{map_iterator::MapIterator, ordered_map::OrderedMap};
use super::JsArgs;
use crate::{
builtins::BuiltIn,
context::intrinsics::StandardConstructors,
error::JsNativeError,
native_function::NativeFunction,
object::{
internal_methods::get_prototype_from_constructor, ConstructorBuilder,
FunctionObjectBuilder, JsObject, ObjectData,
},
property::{Attribute, PropertyNameKind},
symbol::JsSymbol,
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;
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 =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::get_species))
.name("get [Symbol.species]")
.constructor(false)
.build();
let get_size =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::get_size))
.name("get size")
.length(0)
.constructor(false)
.build();
let entries_function =
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(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(
JsSymbol::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.property(
"entries",
entries_function.clone(),
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
JsSymbol::iterator(),
entries_function,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
JsSymbol::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 Err(JsNativeError::typ()
.with_message("calling a builtin Map constructor without new is forbidden")
.into());
}
// 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], _: &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());
}
}
Err(JsNativeError::typ()
.with_message("'this' is not a Map")
.into())
}
/// `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],
_: &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());
}
}
Err(JsNativeError::typ()
.with_message("'this' is not a Map")
.into())
}
/// `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],
_: &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());
}
}
Err(JsNativeError::typ()
.with_message("'this' is not a Map")
.into())
}
/// `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], _: &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() {
// 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());
}
}
Err(JsNativeError::typ()
.with_message("'this' is not a Map")
.into())
}
/// `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], _: &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());
}
}
Err(JsNativeError::typ()
.with_message("'this' is not a Map")
.into())
}
/// `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], _: &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() {
// 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());
}
}
Err(JsNativeError::typ()
.with_message("'this' is not a Map")
.into())
}
/// `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(|| JsNativeError::typ().with_message("`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(|| {
JsNativeError::typ().with_message(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().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(|| {
JsNativeError::typ().with_message("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 Some(next_item) = next else {
return Ok(target.clone().into());
};
let next_item = next_item.value(context)?;
let Some(next_item) = next_item.as_object() else {
// d. If Type(nextItem) is not Object, then
// i. Let error be ThrowCompletion(a newly created TypeError object).
let err = Err(JsNativeError::typ()
.with_message("cannot get key and value from primitive item of `iterable`")
.into());
// 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,235 @@
//! Implements a map type that preserves insertion order.
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 {
Self::Key(v) => v.hash(state),
Self::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> {
/// Creates a new empty `OrderedMap`.
#[must_use]
pub fn new() -> Self {
Self {
map: IndexMap::new(),
lock: 0,
empty_count: 0,
}
}
/// Creates a new empty `OrderedMap` with the specified capacity.
#[must_use]
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.
#[must_use]
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.
#[must_use]
pub fn len(&self) -> usize {
self.map.len() - self.empty_count
}
/// Returns true if the map contains no elements.
///
/// Computes in **O(1)** time.
#[must_use]
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.
#[must_use]
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\"");
}

File diff suppressed because it is too large Load Diff

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.774_964_387_392_123);
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,227 @@
//! Boa's ECMAScript built-in object implementations, e.g. Object, String, Math, Array, etc.
//!
//! This module also contains a JavaScript Console implementation.
pub mod array;
pub mod array_buffer;
pub mod async_function;
pub mod async_generator;
pub mod async_generator_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;
pub mod uri;
pub mod weak;
#[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, async_generator::AsyncGenerator,
async_generator_function::AsyncGeneratorFunction, generator::Generator,
generator_function::GeneratorFunction, typed_array::TypedArray, uri::Uri, weak::WeakRef,
},
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 ECMAScript/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 ECMAScript/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 ECMAScript/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.
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
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,
AsyncGenerator,
AsyncGeneratorFunction,
Uri,
WeakRef,
};
#[cfg(feature = "intl")]
init_builtin::<intl::Intl>(context);
#[cfg(feature = "console")]
init_builtin::<console::Console>(context);
}
/// A utility trait to make working with function arguments easier.
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 @@
//! Boa's implementation of ECMAScript's 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,80 @@
/// Converts a 64-bit floating point number to an `i32` according to the [`ToInt32`][ToInt32] algorithm.
///
/// [ToInt32]: https://tc39.es/ecma262/#sec-toint32
#[allow(clippy::float_cmp)]
pub(crate) fn f64_to_int32(number: f64) -> i32 {
const SIGN_MASK: u64 = 0x8000_0000_0000_0000;
const EXPONENT_MASK: u64 = 0x7FF0_0000_0000_0000;
const SIGNIFICAND_MASK: u64 = 0x000F_FFFF_FFFF_FFFF;
const HIDDEN_BIT: u64 = 0x0010_0000_0000_0000;
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;
fn is_denormal(number: f64) -> bool {
(number.to_bits() & EXPONENT_MASK) == 0
}
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
}
fn significand(number: f64) -> u64 {
let d64 = number.to_bits();
let significand = d64 & SIGNIFICAND_MASK;
if is_denormal(number) {
significand
} else {
significand + HIDDEN_BIT
}
}
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) & 0xFFFF_FFFF
};
(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
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,887 @@
#![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,160 @@
//! This module implements the `ForInIterator` object.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-for-in-iterator-objects
use crate::{
builtins::{function::make_builtin_fn, iterable::create_iter_result_object},
error::JsNativeError,
object::{JsObject, ObjectData},
property::PropertyDescriptor,
property::PropertyKey,
symbol::JsSymbol,
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(|| JsNativeError::typ().with_message("`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), 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 = JsSymbol::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 = "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 = "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 = "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,21 @@
use crate::{context::ContextBuilder, forward, job::SimpleJobQueue};
#[test]
fn promise() {
let queue = SimpleJobQueue::new();
let mut context = ContextBuilder::new().job_queue(&queue).build();
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));
context.run_jobs();
let after_completion = forward(&mut context, "count");
assert_eq!(after_completion, String::from("3"));
}

View File

@@ -0,0 +1,196 @@
//! Boa's implementation of ECMAScript's 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 std::cell::Cell;
use crate::{
builtins::{BuiltIn, JsArgs},
error::JsNativeError,
native_function::NativeFunction,
object::{ConstructorBuilder, FunctionObjectBuilder, 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) -> JsResult<(JsObject, JsObject)> {
self.data.clone().ok_or_else(|| {
JsNativeError::typ()
.with_message("Proxy object has empty handler and target")
.into()
})
}
/// `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 Err(JsNativeError::typ()
.with_message("Proxy constructor called on undefined new target")
.into());
}
// 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(|| {
JsNativeError::typ().with_message("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(|| {
JsNativeError::typ().with_message("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.
FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_, _, 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())
},
Cell::new(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 = JsObject::with_object_proto(context);
// 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,420 @@
//! Boa's implementation of ECMAScript's global `Reflect` object.
//!
//! The `Reflect` global object is a built-in object that provides methods for interceptable
//! ECMAScript 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},
error::JsNativeError,
object::ObjectInitializer,
property::Attribute,
symbol::JsSymbol,
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 = JsSymbol::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(|| JsNativeError::typ().with_message("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 Err(JsNativeError::typ()
.with_message("target must be a function")
.into());
}
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(|| JsNativeError::typ().with_message("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.
new_target.as_constructor().ok_or_else(|| {
JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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(|| {
JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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
// 3.a. Set receiver to target.
let receiver = args
.get(2)
.cloned()
.unwrap_or_else(|| 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 {
Err(JsNativeError::typ()
.with_message("target must be an object")
.into())
}
}
/// 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(|| JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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(|| JsNativeError::typ().with_message("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 = args
.get(3)
.cloned()
.unwrap_or_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(|| JsNativeError::typ().with_message("target must be an object"))?;
let proto = match args.get_or_undefined(1) {
JsValue::Object(obj) => Some(obj.clone()),
JsValue::Null => None,
_ => {
return Err(JsNativeError::typ()
.with_message("proto must be an object or null")
.into())
}
};
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\"");
}

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