mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:12:11 +00:00
JavaScript: Object, Function, Array, Error, Math and JSON built-ins
vectorcraft-js now provides the ECMAScript 5.1 built-ins: the Object
property-descriptor functions, create, keys, seal and freeze (15.2),
Function.prototype.bind (15.3.4.5), Array with every ES5 method (15.4),
the error types (15.11), Math (15.8), JSON with revivers, replacers and
indentation (15.12), and the global functions with Annex B's escape and
unescape (15.1, B.2). Prototype methods are writable and configurable.
The array methods work on array-like objects and visit only the indices
that exist, so a sparse array of length 2^32-1 costs as much as its
elements, and they see indices a callback adds. join writes a cycle back
to an array as an empty string; JSON.stringify throws a TypeError on a
cycle. JSON parsing, writing and the reviver walk use no native
recursion. Every loop takes fuel and every result is checked against
the heap limit, so new Array(4294967295).join("x") ends with the heap
limit and a huge sort or stringify ends with a limit.
cargo xtask test262 also runs the suite's ES5 built-in tests and fetches
json2.js at a pinned commit, checked against its SHA-256, to compare its
output with the built-in JSON once Date, String.prototype.replace and
RegExp exist.
This commit is contained in:
+172
-9
@@ -1,15 +1,28 @@
|
||||
//! The built-in objects the interpreter itself needs (ECMA-262 5.1 section 15): `Object` and the
|
||||
//! basic methods of `Object.prototype`, `Function` with `Function.prototype.call`, `apply` and
|
||||
//! `toString`, `eval`, the error constructors and `Error.prototype.toString`, and the prototypes
|
||||
//! primitive values look properties up on. The rest of the standard library is not here yet.
|
||||
//! The built-in objects (ECMA-262 5.1 section 15): `Object` with `Object.prototype` and the
|
||||
//! ES5 statics, `Function` with `Function.prototype.call`, `apply`, `bind` and `toString`,
|
||||
//! `Array`, the error constructors, `Math`, `JSON`, the global functions and constants, Annex
|
||||
//! B's `escape` and `unescape`, and the prototypes primitive values look properties up on.
|
||||
//! `String`, `Number`, `Boolean`, `RegExp` and `Date` are not here yet.
|
||||
//!
|
||||
//! Every loop in a built-in takes a unit of fuel for each step, and every string, array, object
|
||||
//! or property it makes is checked against the heap limit before it is made, so no built-in can
|
||||
//! run past [`crate::Limits`].
|
||||
|
||||
mod array;
|
||||
mod global;
|
||||
mod json;
|
||||
mod math;
|
||||
mod object;
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::bytecode::{CodeKind, Source};
|
||||
use crate::compiler::{CompileFail, compile_constructed_function};
|
||||
use crate::heap::{Class, Func, HIDDEN, NativeFn, NativeKind, ObjId, Object, Prop, key, string_bytes};
|
||||
use crate::interp::{Abrupt, ErrorType, Interp, JsResult};
|
||||
use crate::value::{Str, Value};
|
||||
use crate::heap::{
|
||||
BOUND_ARG_BYTES, Class, Func, HIDDEN, Key, NativeFn, NativeKind, OBJECT_BYTES, OPEN, ObjId, Object, PROP_BYTES, Prop, key, string_bytes,
|
||||
};
|
||||
use crate::interp::{Abrupt, Desc, ErrorType, Interp, JsResult};
|
||||
use crate::value::{Str, Value, index_key, number_to_str, to_uint32};
|
||||
|
||||
fn alloc(interp: &mut Interp, class: Class, proto: Option<ObjId>) -> ObjId {
|
||||
interp.heap.alloc_object(Object::new(class, proto))
|
||||
@@ -66,7 +79,7 @@ fn try_install(interp: &mut Interp) -> JsResult<()> {
|
||||
interp.intrinsics.array_proto = alloc(interp, Class::Array { length: 0, length_writable: true }, Some(object_proto));
|
||||
interp.intrinsics.string_proto = alloc(interp, Class::String(Str::from("")), Some(object_proto));
|
||||
interp.intrinsics.number_proto = alloc(interp, Class::Number, Some(object_proto));
|
||||
interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean, Some(object_proto));
|
||||
interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean(false), Some(object_proto));
|
||||
interp.intrinsics.regexp_proto = alloc(interp, Class::Ordinary, Some(object_proto));
|
||||
let thrower = native(interp, "", throw_type_error, 0, NativeKind::Plain, false)?;
|
||||
if let Some(t) = interp.heap.object_mut(thrower) {
|
||||
@@ -96,9 +109,134 @@ fn try_install(interp: &mut Interp) -> JsResult<()> {
|
||||
data(interp, function_proto, "call", Value::Object(call), HIDDEN)?;
|
||||
let apply = native(interp, "apply", unreachable_native, 2, NativeKind::Apply, false)?;
|
||||
data(interp, function_proto, "apply", Value::Object(apply), HIDDEN)?;
|
||||
method(interp, function_proto, "bind", function_bind, 1)?;
|
||||
method(interp, function_proto, "toString", function_to_string, 0)?;
|
||||
|
||||
install_errors(interp, object_proto)
|
||||
install_errors(interp, object_proto)?;
|
||||
object::install(interp, object)?;
|
||||
array::install(interp)?;
|
||||
math::install(interp)?;
|
||||
json::install(interp)?;
|
||||
global::install(interp)
|
||||
}
|
||||
|
||||
// ---- helpers for the built-ins ----
|
||||
|
||||
/// ToInteger (section 9.4).
|
||||
fn to_integer(interp: &mut Interp, v: &Value) -> JsResult<f64> {
|
||||
let n = interp.to_number(v)?;
|
||||
Ok(if n.is_nan() { 0.0 } else { n.trunc() })
|
||||
}
|
||||
|
||||
/// The property name of the index `n`, which may be past the largest array index.
|
||||
fn key_of(n: f64) -> Key {
|
||||
if (0.0..4_294_967_295.0).contains(&n) && n.fract() == 0.0 {
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
return index_key(n as u32);
|
||||
}
|
||||
number_to_str(n).flat()
|
||||
}
|
||||
|
||||
/// ToUint32 of an object's `length`.
|
||||
fn length_of(interp: &mut Interp, o: ObjId) -> JsResult<u32> {
|
||||
let len = interp.get_from(o, &key("length"), &Value::Object(o))?;
|
||||
Ok(to_uint32(interp.to_number(&len)?))
|
||||
}
|
||||
|
||||
fn get_at(interp: &mut Interp, o: ObjId, k: f64) -> JsResult<Value> {
|
||||
interp.get_from(o, &key_of(k), &Value::Object(o))
|
||||
}
|
||||
|
||||
fn has_at(interp: &mut Interp, o: ObjId, k: f64) -> JsResult<bool> {
|
||||
interp.has_property(o, &key_of(k))
|
||||
}
|
||||
|
||||
/// The estimated bytes a new element adds.
|
||||
const ELEMENT_BYTES: usize = PROP_BYTES + 32 + 2 * 10;
|
||||
|
||||
/// `o[k] = v` as the array methods assign: a failure throws.
|
||||
fn put_at(interp: &mut Interp, o: ObjId, k: f64, v: Value) -> JsResult<()> {
|
||||
interp.reserve_heap(ELEMENT_BYTES)?;
|
||||
interp.put(o, key_of(k), v, true)
|
||||
}
|
||||
|
||||
fn delete_at(interp: &mut Interp, o: ObjId, k: f64) -> JsResult<()> {
|
||||
interp.delete(o, &key_of(k), true).map(drop)
|
||||
}
|
||||
|
||||
/// Defines `a[k]` as a writable, enumerable and configurable data property, as the methods that
|
||||
/// build a new array do (DefineOwnProperty with `throw` false).
|
||||
fn create_at(interp: &mut Interp, a: ObjId, k: f64, v: Value) -> JsResult<()> {
|
||||
interp.reserve_heap(ELEMENT_BYTES)?;
|
||||
interp.define_own_property(a, key_of(k), &Desc::data(v, OPEN), false).map(drop)
|
||||
}
|
||||
|
||||
/// A new array with the given length and no elements.
|
||||
fn new_array(interp: &mut Interp, length: u32) -> JsResult<ObjId> {
|
||||
interp.reserve_heap(OBJECT_BYTES)?;
|
||||
let proto = interp.intrinsics.array_proto;
|
||||
Ok(interp.heap.alloc_object(Object::new(Class::Array { length, length_writable: true }, Some(proto))))
|
||||
}
|
||||
|
||||
/// A new string from code units, checked against the heap limit and charged to the heap.
|
||||
fn new_string(interp: &mut Interp, units: Vec<u16>) -> JsResult<Value> {
|
||||
let bytes = string_bytes(units.len());
|
||||
interp.reserve_heap(bytes)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
Ok(Value::String(Str::from_units(units)))
|
||||
}
|
||||
|
||||
/// A string the built-ins build piece by piece: each piece is checked against the longest string
|
||||
/// the heap limit allows and its bytes are reserved before it is added. Everything the caller
|
||||
/// still needs must be reachable from the roots.
|
||||
struct Builder {
|
||||
units: Vec<u16>,
|
||||
/// Code units already reserved and charged to the heap.
|
||||
charged: usize,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
fn new() -> Self {
|
||||
Builder { units: Vec::new(), charged: 0 }
|
||||
}
|
||||
|
||||
/// Checks that `more` code units fit, reserving heap for them.
|
||||
fn make_room(&mut self, interp: &mut Interp, more: usize) -> JsResult<()> {
|
||||
let want = self.units.len().saturating_add(more);
|
||||
if want > interp.limits.heap_bytes / 4 {
|
||||
return Err(Abrupt::Limit(crate::LimitKind::Heap));
|
||||
}
|
||||
if want > self.charged {
|
||||
let grow = (want - self.charged).max(self.charged / 2).max(256).min((interp.limits.heap_bytes / 4).saturating_sub(self.charged));
|
||||
interp.reserve_heap(2 * grow)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(2 * grow);
|
||||
self.charged += grow;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_units(&mut self, interp: &mut Interp, units: &[u16]) -> JsResult<()> {
|
||||
self.make_room(interp, units.len())?;
|
||||
self.units.extend_from_slice(units);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_str(&mut self, interp: &mut Interp, s: &str) -> JsResult<()> {
|
||||
let units: Vec<u16> = s.encode_utf16().collect();
|
||||
self.push_units(interp, &units)
|
||||
}
|
||||
|
||||
/// The string built, whose bytes were charged as it grew.
|
||||
fn finish(self) -> Value {
|
||||
Value::String(Str::from_units(self.units))
|
||||
}
|
||||
}
|
||||
|
||||
fn require_callable(interp: &mut Interp, v: &Value, what: &str) -> JsResult<()> {
|
||||
if interp.is_callable(v) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(interp.type_error(&format!("{what} is not a function")))
|
||||
}
|
||||
|
||||
fn install_errors(interp: &mut Interp, object_proto: ObjId) -> JsResult<()> {
|
||||
@@ -278,11 +416,36 @@ fn function_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) ->
|
||||
}
|
||||
Some(Func::Native { name, .. }) => format!("function {name}() {{ [native code] }}"),
|
||||
Some(Func::Host { name, .. }) => format!("function {name}() {{ [native code] }}"),
|
||||
Some(Func::Bound { .. }) => "function () { [native code] }".to_string(),
|
||||
None => return Err(interp.type_error("Function.prototype.toString was called on a value that is not a function")),
|
||||
};
|
||||
Ok(Value::str(&text))
|
||||
}
|
||||
|
||||
/// `Function.prototype.bind` (section 15.3.4.5).
|
||||
fn function_bind(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
require_callable(interp, &this, "the value bind was called on")?;
|
||||
let Some(target) = this.as_object() else { return Err(interp.type_error("the value bind was called on is not a function")) };
|
||||
let bound_this = arg(args, 0);
|
||||
let bound_args: Rc<[Value]> = args.get(1..).unwrap_or_default().into();
|
||||
let target_length = match interp.get_from(target, &key("length"), &this)? {
|
||||
Value::Number(n) => n,
|
||||
_ => 0.0,
|
||||
};
|
||||
let length = (target_length - bound_args.len() as f64).max(0.0);
|
||||
interp.reserve_heap(OBJECT_BYTES + 4 * PROP_BYTES + BOUND_ARG_BYTES * bound_args.len())?;
|
||||
let proto = interp.intrinsics.function_proto;
|
||||
let class = Class::Function(Func::Bound { target, this: bound_this, args: bound_args.clone() });
|
||||
let f = interp.heap.alloc_object(Object::new(class, Some(proto)));
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(BOUND_ARG_BYTES * bound_args.len());
|
||||
interp.define(f, key("length"), Prop::Data { value: Value::Number(length), flags: 0 })?;
|
||||
let thrower = Some(interp.intrinsics.throw_type_error);
|
||||
for name in ["caller", "arguments"] {
|
||||
interp.define(f, key(name), Prop::Accessor { get: thrower, set: thrower, flags: 0 })?;
|
||||
}
|
||||
Ok(Value::Object(f))
|
||||
}
|
||||
|
||||
/// The global `eval` called indirectly (section 15.1.2.1.1): global code in the global
|
||||
/// environment.
|
||||
fn eval(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
|
||||
@@ -0,0 +1,843 @@
|
||||
//! `Array` (ECMA-262 5.1 section 15.4): the constructor, `Array.isArray` and every method of
|
||||
//! `Array.prototype`. The methods are generic: they read `length` and the elements of any object.
|
||||
//!
|
||||
//! A method that walks a range of indices visits only the indices that can hold an element when
|
||||
//! the range is much longer than the object has properties (a sparse array such as
|
||||
//! `new Array(4294967295)`), so a long empty range costs nothing and no index is allocated for
|
||||
//! it. The list of indices is made again whenever a script adds an index property to any object
|
||||
//! while the method runs. Every index visited takes a unit of fuel.
|
||||
|
||||
use super::{
|
||||
Builder, ELEMENT_BYTES, arg, constructor, create_at, delete_at, get_at, has_at, length_of, method, native, new_array, object_to_string, put_at,
|
||||
require_callable, to_integer,
|
||||
};
|
||||
use crate::heap::{Class, NativeKind, ObjId};
|
||||
use crate::interp::{Abrupt, Interp, JsResult, strict_eq};
|
||||
use crate::value::{Str, Value, to_uint32};
|
||||
|
||||
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
let proto = interp.intrinsics.array_proto;
|
||||
let array = native(interp, "Array", array_ctor, 1, NativeKind::Plain, true)?;
|
||||
constructor(interp, "Array", array, proto)?;
|
||||
method(interp, array, "isArray", is_array, 1)?;
|
||||
let methods: [(&'static str, crate::heap::NativeFn, u32); 21] = [
|
||||
("toString", to_string, 0),
|
||||
("toLocaleString", to_locale_string, 0),
|
||||
("concat", concat, 1),
|
||||
("join", join, 1),
|
||||
("pop", pop, 0),
|
||||
("push", push, 1),
|
||||
("reverse", reverse, 0),
|
||||
("shift", shift, 0),
|
||||
("slice", slice, 2),
|
||||
("sort", sort, 1),
|
||||
("splice", splice, 2),
|
||||
("unshift", unshift, 1),
|
||||
("indexOf", index_of, 1),
|
||||
("lastIndexOf", last_index_of, 1),
|
||||
("every", every, 1),
|
||||
("some", some, 1),
|
||||
("forEach", for_each, 1),
|
||||
("map", map, 1),
|
||||
("filter", filter, 1),
|
||||
("reduce", reduce, 1),
|
||||
("reduceRight", reduce_right, 1),
|
||||
];
|
||||
for (name, f, length) in methods {
|
||||
method(interp, proto, name, f, length)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- visiting indices ----
|
||||
|
||||
/// Ranges at most this long are visited index by index.
|
||||
const DENSE_SPAN: u64 = 4096;
|
||||
|
||||
/// How an index an object holds names an index to visit.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Map {
|
||||
/// The index `p` asks for a visit of `p - shift`.
|
||||
Shift(i64),
|
||||
/// The index `p` asks for a visit of `p` and of `mirror - p`.
|
||||
Mirror(i64),
|
||||
}
|
||||
|
||||
/// The indices of a range, in order, that an operation must visit.
|
||||
struct Indices {
|
||||
obj: ObjId,
|
||||
/// The range left to visit, `lo` included and `hi` not.
|
||||
lo: u64,
|
||||
hi: u64,
|
||||
ascending: bool,
|
||||
maps: Vec<Map>,
|
||||
/// The indices left when the range is visited sparsely.
|
||||
sparse: Option<Sparse>,
|
||||
}
|
||||
|
||||
struct Sparse {
|
||||
list: Vec<u64>,
|
||||
pos: usize,
|
||||
/// [`crate::heap::Heap::index_epoch`] when the list was made.
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
/// The canonical numeric property name `k` stands for, up to 15 digits.
|
||||
fn numeric_index(k: &[u16]) -> Option<u64> {
|
||||
if k.is_empty() || k.len() > 15 || (k.len() > 1 && k.first() == Some(&u16::from(b'0'))) {
|
||||
return None;
|
||||
}
|
||||
let mut n = 0u64;
|
||||
for &u in k {
|
||||
let d = u.checked_sub(u16::from(b'0')).filter(|d| *d <= 9)?;
|
||||
n = n * 10 + u64::from(d);
|
||||
}
|
||||
Some(n)
|
||||
}
|
||||
|
||||
/// The objects of the prototype chain from `obj`, with how many properties (and String object
|
||||
/// indices) each holds.
|
||||
fn chain(interp: &Interp, obj: ObjId) -> Vec<(ObjId, usize)> {
|
||||
let mut out = Vec::new();
|
||||
let mut cur = Some(obj);
|
||||
while let Some(id) = cur {
|
||||
let Some(o) = interp.heap.object(id) else { break };
|
||||
let string_len = if let Class::String(s) = &o.class { s.len() } else { 0 };
|
||||
out.push((id, o.props.len().saturating_add(string_len)));
|
||||
cur = o.proto;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
impl Indices {
|
||||
fn new(interp: &mut Interp, obj: ObjId, lo: u64, hi: u64, ascending: bool, maps: Vec<Map>) -> JsResult<Self> {
|
||||
let mut it = Indices { obj, lo, hi, ascending, maps, sparse: None };
|
||||
if hi.saturating_sub(lo) > DENSE_SPAN {
|
||||
let present: usize = chain(interp, obj).iter().map(|(_, n)| *n).fold(0, usize::saturating_add);
|
||||
if hi - lo > (present as u64).saturating_mul(8) {
|
||||
it.sparse = Some(it.gather(interp)?);
|
||||
}
|
||||
}
|
||||
Ok(it)
|
||||
}
|
||||
|
||||
fn up(interp: &mut Interp, obj: ObjId, lo: u64, hi: u64) -> JsResult<Self> {
|
||||
Self::new(interp, obj, lo, hi, true, vec![Map::Shift(0)])
|
||||
}
|
||||
|
||||
fn down(interp: &mut Interp, obj: ObjId, lo: u64, hi: u64) -> JsResult<Self> {
|
||||
Self::new(interp, obj, lo, hi, false, vec![Map::Shift(0)])
|
||||
}
|
||||
|
||||
/// Lists the indices left to visit from the properties of the object and its prototypes.
|
||||
/// Each property takes a unit of fuel.
|
||||
fn gather(&self, interp: &mut Interp) -> JsResult<Sparse> {
|
||||
let epoch = interp.heap.index_epoch;
|
||||
let mut list: Vec<u64> = Vec::new();
|
||||
for (id, n) in chain(interp, self.obj) {
|
||||
for _ in 0..n {
|
||||
interp.use_fuel()?;
|
||||
}
|
||||
interp.reserve_heap(n.saturating_mul(8).saturating_mul(self.maps.len() + 1))?;
|
||||
let Some(o) = interp.heap.object(id) else { continue };
|
||||
let string_len = if let Class::String(s) = &o.class { s.len() } else { 0 };
|
||||
let present = (0..string_len as u64).chain(o.props.iter().filter_map(|(k, _)| numeric_index(k)));
|
||||
for p in present {
|
||||
for map in &self.maps {
|
||||
let p = p as i64;
|
||||
let ks = match *map {
|
||||
Map::Shift(s) => [Some(p - s), None],
|
||||
Map::Mirror(m) => [Some(p), Some(m - p)],
|
||||
};
|
||||
for k in ks.into_iter().flatten() {
|
||||
if k >= 0 && (k as u64) >= self.lo && (k as u64) < self.hi {
|
||||
list.push(k as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.ascending {
|
||||
list.sort_unstable();
|
||||
} else {
|
||||
list.sort_unstable_by(|a, b| b.cmp(a));
|
||||
}
|
||||
list.dedup();
|
||||
Ok(Sparse { list, pos: 0, epoch })
|
||||
}
|
||||
|
||||
/// The next index to visit; it takes a unit of fuel.
|
||||
fn next(&mut self, interp: &mut Interp) -> JsResult<Option<u64>> {
|
||||
if self.lo >= self.hi {
|
||||
return Ok(None);
|
||||
}
|
||||
if self.sparse.as_ref().is_some_and(|s| s.epoch != interp.heap.index_epoch) {
|
||||
self.sparse = Some(self.gather(interp)?);
|
||||
}
|
||||
let k = match &mut self.sparse {
|
||||
None if self.ascending => self.lo,
|
||||
None => self.hi - 1,
|
||||
Some(s) => {
|
||||
let Some(&k) = s.list.get(s.pos) else {
|
||||
self.lo = self.hi;
|
||||
return Ok(None);
|
||||
};
|
||||
s.pos += 1;
|
||||
k
|
||||
}
|
||||
};
|
||||
if self.ascending {
|
||||
self.lo = k + 1;
|
||||
} else {
|
||||
self.hi = k;
|
||||
}
|
||||
interp.use_fuel()?;
|
||||
Ok(Some(k))
|
||||
}
|
||||
}
|
||||
|
||||
/// The element at `k` when the object or its prototypes have one.
|
||||
fn element(interp: &mut Interp, o: ObjId, k: u64) -> JsResult<Option<Value>> {
|
||||
let k = k as f64;
|
||||
if !has_at(interp, o, k)? {
|
||||
return Ok(None);
|
||||
}
|
||||
get_at(interp, o, k).map(Some)
|
||||
}
|
||||
|
||||
/// ToObject of `this`, kept reachable while `f` runs.
|
||||
fn with_this<T>(interp: &mut Interp, this: &Value, f: impl FnOnce(&mut Interp, ObjId) -> JsResult<T>) -> JsResult<T> {
|
||||
let o = interp.to_object(this)?;
|
||||
interp.rooted(&[Value::Object(o)], |interp| f(interp, o))
|
||||
}
|
||||
|
||||
/// The start a relative index gives within `0..=len` (`slice` and `splice`).
|
||||
fn relative(interp: &mut Interp, v: &Value, len: u32) -> JsResult<u64> {
|
||||
let n = to_integer(interp, v)?;
|
||||
let len = f64::from(len);
|
||||
Ok(if n < 0.0 { (len + n).max(0.0) } else { n.min(len) } as u64)
|
||||
}
|
||||
|
||||
fn number(k: u64) -> Value {
|
||||
Value::Number(k as f64)
|
||||
}
|
||||
|
||||
// ---- the constructor ----
|
||||
|
||||
/// `Array(…)` and `new Array(…)` (sections 15.4.1 and 15.4.2).
|
||||
fn array_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
if let [Value::Number(n)] = args {
|
||||
let len = to_uint32(*n);
|
||||
if f64::from(len) != *n {
|
||||
return Err(interp.throw(crate::interp::ErrorType::Range, "invalid array length"));
|
||||
}
|
||||
return new_array(interp, len).map(Value::Object);
|
||||
}
|
||||
let a = new_array(interp, 0)?;
|
||||
interp.rooted(&[Value::Object(a)], |interp| {
|
||||
for (i, v) in args.iter().enumerate() {
|
||||
interp.use_fuel()?;
|
||||
create_at(interp, a, i as f64, v.clone())?;
|
||||
}
|
||||
Ok(Value::Object(a))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.isArray` (section 15.4.3.2).
|
||||
fn is_array(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let a = arg(args, 0).as_object().and_then(|o| interp.heap.object(o)).is_some_and(|o| matches!(o.class, Class::Array { .. }));
|
||||
Ok(Value::Bool(a))
|
||||
}
|
||||
|
||||
// ---- conversions ----
|
||||
|
||||
/// `Array.prototype.toString` (section 15.4.4.2).
|
||||
fn to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let join = interp.get_from(o, &crate::heap::key("join"), &Value::Object(o))?;
|
||||
if interp.is_callable(&join) {
|
||||
return interp.call(&join, &Value::Object(o), &[]);
|
||||
}
|
||||
object_to_string(interp, Value::Object(o), &[], false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Joins the elements `0..len` of `o`, each turned into a string by `item`, with `sep` between
|
||||
/// them. A cycle back to an object being joined joins as an empty string.
|
||||
fn join_with(interp: &mut Interp, o: ObjId, sep: &[u16], item: fn(&mut Interp, Value) -> JsResult<Option<Str>>) -> JsResult<Value> {
|
||||
if interp.join_stack.contains(&o) {
|
||||
return Ok(Value::str(""));
|
||||
}
|
||||
let len = length_of(interp, o)?;
|
||||
if len == 0 {
|
||||
return Ok(Value::str(""));
|
||||
}
|
||||
// A result that cannot fit is refused before any element is read.
|
||||
let least = (sep.len() as u64).saturating_mul(u64::from(len - 1));
|
||||
if least > (interp.limits.heap_bytes / 4) as u64 {
|
||||
return Err(Abrupt::Limit(crate::LimitKind::Heap));
|
||||
}
|
||||
interp.join_stack.push(o);
|
||||
let r = join_elements(interp, o, len, sep, item);
|
||||
interp.join_stack.pop();
|
||||
r
|
||||
}
|
||||
|
||||
fn join_elements(interp: &mut Interp, o: ObjId, len: u32, sep: &[u16], item: fn(&mut Interp, Value) -> JsResult<Option<Str>>) -> JsResult<Value> {
|
||||
let mut out = Builder::new();
|
||||
// Separators written so far: element `k` follows `k` of them.
|
||||
let mut seps = 0u64;
|
||||
let mut it = Indices::up(interp, o, 0, u64::from(len))?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
let v = get_at(interp, o, k as f64)?;
|
||||
let Some(s) = item(interp, v)? else { continue };
|
||||
for _ in seps..k {
|
||||
out.push_units(interp, sep)?;
|
||||
}
|
||||
seps = k;
|
||||
out.push_units(interp, &s.flat())?;
|
||||
}
|
||||
for _ in seps..u64::from(len - 1) {
|
||||
out.push_units(interp, sep)?;
|
||||
}
|
||||
Ok(out.finish())
|
||||
}
|
||||
|
||||
/// An element as `join` writes it: nothing for `undefined` and `null`.
|
||||
fn join_item(interp: &mut Interp, v: Value) -> JsResult<Option<Str>> {
|
||||
if v.is_nullish() {
|
||||
return Ok(None);
|
||||
}
|
||||
interp.to_str(&v).map(Some)
|
||||
}
|
||||
|
||||
/// An element as `toLocaleString` writes it (section 15.4.4.3).
|
||||
fn locale_item(interp: &mut Interp, v: Value) -> JsResult<Option<Str>> {
|
||||
if v.is_nullish() {
|
||||
return Ok(None);
|
||||
}
|
||||
let e = interp.to_object(&v)?;
|
||||
let f = interp.get_from(e, &crate::heap::key("toLocaleString"), &Value::Object(e))?;
|
||||
require_callable(interp, &f, "an element's toLocaleString")?;
|
||||
let s = interp.call(&f, &Value::Object(e), &[])?;
|
||||
interp.to_str(&s).map(Some)
|
||||
}
|
||||
|
||||
/// `Array.prototype.join` (section 15.4.4.5).
|
||||
fn join(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let sep = match arg(args, 0) {
|
||||
Value::Undefined => Str::from(","),
|
||||
v => interp.to_str(&v)?,
|
||||
};
|
||||
join_with(interp, o, &sep.flat(), join_item)
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.prototype.toLocaleString` (section 15.4.4.3).
|
||||
fn to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| join_with(interp, o, &[u16::from(b',')], locale_item))
|
||||
}
|
||||
|
||||
// ---- methods that change the object ----
|
||||
|
||||
/// `Array.prototype.concat` (section 15.4.4.4).
|
||||
fn concat(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let a = new_array(interp, 0)?;
|
||||
interp.rooted(&[Value::Object(a)], |interp| {
|
||||
let mut n = 0u64;
|
||||
for e in std::iter::once(Value::Object(o)).chain(args.iter().cloned()) {
|
||||
interp.use_fuel()?;
|
||||
let spread = e.as_object().filter(|&id| interp.heap.object(id).is_some_and(|x| matches!(x.class, Class::Array { .. })));
|
||||
let Some(e) = spread else {
|
||||
create_at(interp, a, n as f64, e)?;
|
||||
n += 1;
|
||||
continue;
|
||||
};
|
||||
let len = u64::from(length_of(interp, e)?);
|
||||
let mut it = Indices::up(interp, e, 0, len)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
if let Some(v) = element(interp, e, k)? {
|
||||
create_at(interp, a, (n + k) as f64, v)?;
|
||||
}
|
||||
}
|
||||
n += len;
|
||||
}
|
||||
set_length(interp, a, n as f64)?;
|
||||
Ok(Value::Object(a))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn set_length(interp: &mut Interp, o: ObjId, n: f64) -> JsResult<()> {
|
||||
interp.put(o, crate::heap::key("length"), Value::Number(n), true)
|
||||
}
|
||||
|
||||
/// `Array.prototype.pop` (section 15.4.4.6).
|
||||
fn pop(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = length_of(interp, o)?;
|
||||
if len == 0 {
|
||||
set_length(interp, o, 0.0)?;
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
let last = f64::from(len - 1);
|
||||
let v = get_at(interp, o, last)?;
|
||||
interp.rooted(std::slice::from_ref(&v), |interp| {
|
||||
delete_at(interp, o, last)?;
|
||||
set_length(interp, o, last)
|
||||
})?;
|
||||
Ok(v)
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.prototype.push` (section 15.4.4.7).
|
||||
fn push(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let mut n = f64::from(length_of(interp, o)?);
|
||||
for v in args {
|
||||
interp.use_fuel()?;
|
||||
put_at(interp, o, n, v.clone())?;
|
||||
n += 1.0;
|
||||
}
|
||||
set_length(interp, o, n)?;
|
||||
Ok(Value::Number(n))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.prototype.reverse` (section 15.4.4.8).
|
||||
fn reverse(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = u64::from(length_of(interp, o)?);
|
||||
let middle = len / 2;
|
||||
let mirror = len as i64 - 1;
|
||||
let mut it = Indices::new(interp, o, 0, middle, true, vec![Map::Mirror(mirror)])?;
|
||||
while let Some(lower) = it.next(interp)? {
|
||||
let upper = len - lower - 1;
|
||||
let lower_value = element(interp, o, lower)?;
|
||||
let upper_value = interp.rooted(lower_value.as_slice(), |interp| element(interp, o, upper))?;
|
||||
let mut roots: Vec<Value> = lower_value.iter().cloned().collect();
|
||||
roots.extend(upper_value.iter().cloned());
|
||||
interp.rooted(&roots, |interp| match (lower_value, upper_value) {
|
||||
(Some(l), Some(u)) => {
|
||||
put_at(interp, o, lower as f64, u)?;
|
||||
put_at(interp, o, upper as f64, l)
|
||||
}
|
||||
(None, Some(u)) => {
|
||||
put_at(interp, o, lower as f64, u)?;
|
||||
delete_at(interp, o, upper as f64)
|
||||
}
|
||||
(Some(l), None) => {
|
||||
delete_at(interp, o, lower as f64)?;
|
||||
put_at(interp, o, upper as f64, l)
|
||||
}
|
||||
(None, None) => Ok(()),
|
||||
})?;
|
||||
}
|
||||
Ok(Value::Object(o))
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves the element at `from` to `to`, or deletes `to` when there is none at `from`.
|
||||
fn move_element(interp: &mut Interp, o: ObjId, from: u64, to: u64) -> JsResult<()> {
|
||||
match element(interp, o, from)? {
|
||||
Some(v) => put_at(interp, o, to as f64, v),
|
||||
None => delete_at(interp, o, to as f64),
|
||||
}
|
||||
}
|
||||
|
||||
/// `Array.prototype.shift` (section 15.4.4.9).
|
||||
fn shift(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = u64::from(length_of(interp, o)?);
|
||||
if len == 0 {
|
||||
set_length(interp, o, 0.0)?;
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
let first = get_at(interp, o, 0.0)?;
|
||||
interp.rooted(std::slice::from_ref(&first), |interp| {
|
||||
let mut it = Indices::new(interp, o, 1, len, true, vec![Map::Shift(0), Map::Shift(-1)])?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
move_element(interp, o, k, k - 1)?;
|
||||
}
|
||||
delete_at(interp, o, (len - 1) as f64)?;
|
||||
set_length(interp, o, (len - 1) as f64)
|
||||
})?;
|
||||
Ok(first)
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.prototype.unshift` (section 15.4.4.13).
|
||||
fn unshift(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = u64::from(length_of(interp, o)?);
|
||||
let count = args.len() as u64;
|
||||
if count > 0 {
|
||||
let maps = vec![Map::Shift(-1), Map::Shift(count as i64 - 1)];
|
||||
let mut it = Indices::new(interp, o, 1, len + 1, false, maps)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
move_element(interp, o, k - 1, k + count - 1)?;
|
||||
}
|
||||
}
|
||||
for (j, v) in args.iter().enumerate() {
|
||||
interp.use_fuel()?;
|
||||
put_at(interp, o, j as f64, v.clone())?;
|
||||
}
|
||||
let n = (len + count) as f64;
|
||||
set_length(interp, o, n)?;
|
||||
Ok(Value::Number(n))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.prototype.slice` (section 15.4.4.10).
|
||||
fn slice(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = length_of(interp, o)?;
|
||||
let start = relative(interp, &arg(args, 0), len)?;
|
||||
let end = match arg(args, 1) {
|
||||
Value::Undefined => u64::from(len),
|
||||
v => relative(interp, &v, len)?,
|
||||
};
|
||||
let a = new_array(interp, 0)?;
|
||||
interp.rooted(&[Value::Object(a)], |interp| {
|
||||
let mut it = Indices::up(interp, o, start, end)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
if let Some(v) = element(interp, o, k)? {
|
||||
create_at(interp, a, (k - start) as f64, v)?;
|
||||
}
|
||||
}
|
||||
set_length(interp, a, end.saturating_sub(start) as f64)?;
|
||||
Ok(Value::Object(a))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.prototype.splice` (section 15.4.4.12).
|
||||
fn splice(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = length_of(interp, o)?;
|
||||
let start = relative(interp, &arg(args, 0), len)?;
|
||||
let len = u64::from(len);
|
||||
let delete_count = match args.get(1) {
|
||||
// Without a count, everything from the start is deleted, as later editions and
|
||||
// every implementation do.
|
||||
None if !args.is_empty() => len - start,
|
||||
None => 0,
|
||||
Some(v) => to_integer(interp, v)?.max(0.0).min((len - start) as f64) as u64,
|
||||
};
|
||||
let items = args.get(2..).unwrap_or_default();
|
||||
let a = new_array(interp, 0)?;
|
||||
interp.rooted(&[Value::Object(a)], |interp| {
|
||||
let mut it = Indices::up(interp, o, start, start + delete_count)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
if let Some(v) = element(interp, o, k)? {
|
||||
create_at(interp, a, (k - start) as f64, v)?;
|
||||
}
|
||||
}
|
||||
set_length(interp, a, delete_count as f64)?;
|
||||
move_tail(interp, o, len, start, delete_count, items.len() as u64)?;
|
||||
for (j, v) in items.iter().enumerate() {
|
||||
interp.use_fuel()?;
|
||||
put_at(interp, o, (start + j as u64) as f64, v.clone())?;
|
||||
}
|
||||
set_length(interp, o, (len - delete_count + items.len() as u64) as f64)?;
|
||||
Ok(Value::Object(a))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves the elements after a spliced range so that `item_count` items replace `delete_count`
|
||||
/// elements at `start` (section 15.4.4.12, steps 12 and 13).
|
||||
fn move_tail(interp: &mut Interp, o: ObjId, len: u64, start: u64, delete_count: u64, item_count: u64) -> JsResult<()> {
|
||||
let (dc, ic) = (delete_count as i64, item_count as i64);
|
||||
if item_count < delete_count {
|
||||
let mut it = Indices::new(interp, o, start, len - delete_count, true, vec![Map::Shift(dc), Map::Shift(ic)])?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
move_element(interp, o, k + delete_count, k + item_count)?;
|
||||
}
|
||||
let mut it = Indices::down(interp, o, len - delete_count + item_count, len)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
delete_at(interp, o, k as f64)?;
|
||||
}
|
||||
} else if item_count > delete_count {
|
||||
let mut it = Indices::new(interp, o, start + 1, len - delete_count + 1, false, vec![Map::Shift(dc - 1), Map::Shift(ic - 1)])?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
move_element(interp, o, k + delete_count - 1, k + item_count - 1)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- sort ----
|
||||
|
||||
/// A stable merge sort whose comparison may fail. `greater(a, b)` says whether `a` sorts after
|
||||
/// `b`; each comparison takes a unit of fuel.
|
||||
fn merge_sort<T: Clone>(interp: &mut Interp, items: &mut Vec<T>, mut greater: impl FnMut(&mut Interp, &T, &T) -> JsResult<bool>) -> JsResult<()> {
|
||||
let n = items.len();
|
||||
let mut width = 1;
|
||||
let mut buf: Vec<T> = Vec::with_capacity(n);
|
||||
while width < n {
|
||||
buf.clear();
|
||||
let mut lo = 0;
|
||||
while lo < n {
|
||||
let mid = (lo + width).min(n);
|
||||
let hi = (lo + 2 * width).min(n);
|
||||
let (mut i, mut j) = (lo, mid);
|
||||
while i < mid && j < hi {
|
||||
let (Some(a), Some(b)) = (items.get(i), items.get(j)) else { break };
|
||||
interp.use_fuel()?;
|
||||
if greater(interp, a, b)? {
|
||||
buf.push(b.clone());
|
||||
j += 1;
|
||||
} else {
|
||||
buf.push(a.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
buf.extend(items.get(i..mid).unwrap_or_default().iter().cloned());
|
||||
buf.extend(items.get(j..hi).unwrap_or_default().iter().cloned());
|
||||
lo = hi;
|
||||
}
|
||||
std::mem::swap(items, &mut buf);
|
||||
width *= 2;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `Array.prototype.sort` (section 15.4.4.11). The elements are read, sorted with `undefined`
|
||||
/// after every other value, written back from index 0, and the indices after them are deleted.
|
||||
fn sort(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let compare = arg(args, 0);
|
||||
if !matches!(compare, Value::Undefined) {
|
||||
require_callable(interp, &compare, "the comparison")?;
|
||||
}
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = u64::from(length_of(interp, o)?);
|
||||
let mark = interp.temp_roots.len();
|
||||
let r = sort_elements(interp, o, len, &compare);
|
||||
interp.temp_roots.truncate(mark);
|
||||
r?;
|
||||
Ok(Value::Object(o))
|
||||
})
|
||||
}
|
||||
|
||||
fn sort_elements(interp: &mut Interp, o: ObjId, len: u64, compare: &Value) -> JsResult<()> {
|
||||
let mut values = Vec::new();
|
||||
let mut undefined = 0u64;
|
||||
let mut it = Indices::up(interp, o, 0, len)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
match element(interp, o, k)? {
|
||||
Some(Value::Undefined) => undefined += 1,
|
||||
Some(v) => {
|
||||
interp.reserve_heap(ELEMENT_BYTES)?;
|
||||
interp.temp_roots.push(v.clone());
|
||||
values.push(v);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if matches!(compare, Value::Undefined) {
|
||||
let mut keyed = Vec::with_capacity(values.len());
|
||||
for v in values {
|
||||
interp.use_fuel()?;
|
||||
let s = interp.to_str(&v)?.flat();
|
||||
interp.reserve_heap(crate::heap::string_bytes(s.len()))?;
|
||||
keyed.push((s, v));
|
||||
}
|
||||
merge_sort(interp, &mut keyed, |_, a, b| Ok(a.0 > b.0))?;
|
||||
values = keyed.into_iter().map(|(_, v)| v).collect();
|
||||
} else {
|
||||
merge_sort(interp, &mut values, |interp, a, b| {
|
||||
let r = interp.call(compare, &Value::Undefined, &[a.clone(), b.clone()])?;
|
||||
Ok(interp.to_number(&r)? > 0.0)
|
||||
})?;
|
||||
}
|
||||
let mut i = 0u64;
|
||||
for v in values {
|
||||
interp.use_fuel()?;
|
||||
put_at(interp, o, i as f64, v)?;
|
||||
i += 1;
|
||||
}
|
||||
for _ in 0..undefined {
|
||||
interp.use_fuel()?;
|
||||
put_at(interp, o, i as f64, Value::Undefined)?;
|
||||
i += 1;
|
||||
}
|
||||
let mut it = Indices::down(interp, o, i, len)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
delete_at(interp, o, k as f64)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- searching ----
|
||||
|
||||
/// `Array.prototype.indexOf` (section 15.4.4.14).
|
||||
fn index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = f64::from(length_of(interp, o)?);
|
||||
if len == 0.0 {
|
||||
return Ok(Value::Number(-1.0));
|
||||
}
|
||||
let n = if args.len() > 1 { to_integer(interp, &arg(args, 1))? } else { 0.0 };
|
||||
if n >= len {
|
||||
return Ok(Value::Number(-1.0));
|
||||
}
|
||||
let start = if n >= 0.0 { n } else { (len + n).max(0.0) };
|
||||
let target = arg(args, 0);
|
||||
let mut it = Indices::up(interp, o, start as u64, len as u64)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
if let Some(v) = element(interp, o, k)?
|
||||
&& strict_eq(&v, &target)
|
||||
{
|
||||
return Ok(number(k));
|
||||
}
|
||||
}
|
||||
Ok(Value::Number(-1.0))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Array.prototype.lastIndexOf` (section 15.4.4.15).
|
||||
fn last_index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = f64::from(length_of(interp, o)?);
|
||||
if len == 0.0 {
|
||||
return Ok(Value::Number(-1.0));
|
||||
}
|
||||
let n = if args.len() > 1 { to_integer(interp, &arg(args, 1))? } else { len - 1.0 };
|
||||
let from = if n >= 0.0 { n.min(len - 1.0) } else { len + n };
|
||||
if from < 0.0 {
|
||||
return Ok(Value::Number(-1.0));
|
||||
}
|
||||
let target = arg(args, 0);
|
||||
let mut it = Indices::down(interp, o, 0, from as u64 + 1)?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
if let Some(v) = element(interp, o, k)?
|
||||
&& strict_eq(&v, &target)
|
||||
{
|
||||
return Ok(number(k));
|
||||
}
|
||||
}
|
||||
Ok(Value::Number(-1.0))
|
||||
})
|
||||
}
|
||||
|
||||
// ---- iteration ----
|
||||
|
||||
/// What an iteration method does with each callback result.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Each {
|
||||
Every,
|
||||
Some,
|
||||
/// `forEach`.
|
||||
Visit,
|
||||
Map,
|
||||
Filter,
|
||||
}
|
||||
|
||||
/// `every`, `some`, `forEach`, `map` and `filter` (sections 15.4.4.16 to 15.4.4.20).
|
||||
fn iterate(interp: &mut Interp, this: Value, args: &[Value], kind: Each) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = length_of(interp, o)?;
|
||||
let callback = arg(args, 0);
|
||||
require_callable(interp, &callback, "the callback")?;
|
||||
let this_arg = arg(args, 1);
|
||||
let a = match kind {
|
||||
Each::Map => Some(new_array(interp, len)?),
|
||||
Each::Filter => Some(new_array(interp, 0)?),
|
||||
_ => None,
|
||||
};
|
||||
let roots: Vec<Value> = a.map(Value::Object).into_iter().collect();
|
||||
interp.rooted(&roots, |interp| {
|
||||
let mut to = 0u64;
|
||||
let mut it = Indices::up(interp, o, 0, u64::from(len))?;
|
||||
while let Some(k) = it.next(interp)? {
|
||||
let Some(v) = element(interp, o, k)? else { continue };
|
||||
let r = interp.call(&callback, &this_arg, &[v.clone(), number(k), Value::Object(o)])?;
|
||||
match (kind, a) {
|
||||
(Each::Every, _) if !r.truthy() => return Ok(Value::Bool(false)),
|
||||
(Each::Some, _) if r.truthy() => return Ok(Value::Bool(true)),
|
||||
(Each::Map, Some(a)) => create_at(interp, a, k as f64, r)?,
|
||||
(Each::Filter, Some(a)) if r.truthy() => {
|
||||
create_at(interp, a, to as f64, v)?;
|
||||
to += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(match (kind, a) {
|
||||
(Each::Every, _) => Value::Bool(true),
|
||||
(Each::Some, _) => Value::Bool(false),
|
||||
(_, Some(a)) => Value::Object(a),
|
||||
_ => Value::Undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn every(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
iterate(interp, this, args, Each::Every)
|
||||
}
|
||||
|
||||
fn some(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
iterate(interp, this, args, Each::Some)
|
||||
}
|
||||
|
||||
fn for_each(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
iterate(interp, this, args, Each::Visit)
|
||||
}
|
||||
|
||||
fn map(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
iterate(interp, this, args, Each::Map)
|
||||
}
|
||||
|
||||
fn filter(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
iterate(interp, this, args, Each::Filter)
|
||||
}
|
||||
|
||||
/// `reduce` and `reduceRight` (sections 15.4.4.21 and 15.4.4.22).
|
||||
fn fold(interp: &mut Interp, this: Value, args: &[Value], right: bool) -> JsResult<Value> {
|
||||
with_this(interp, &this, |interp, o| {
|
||||
let len = u64::from(length_of(interp, o)?);
|
||||
let callback = arg(args, 0);
|
||||
require_callable(interp, &callback, "the callback")?;
|
||||
let mut it = Indices::new(interp, o, 0, len, !right, vec![Map::Shift(0)])?;
|
||||
let slot = interp.temp_roots.len();
|
||||
interp.temp_roots.push(Value::Undefined);
|
||||
// The accumulator is kept in a root slot while the elements are read.
|
||||
let keep = |interp: &mut Interp, acc: &Value| {
|
||||
if let Some(s) = interp.temp_roots.get_mut(slot) {
|
||||
*s = acc.clone();
|
||||
}
|
||||
};
|
||||
let r = (|| {
|
||||
let mut acc = match args.get(1) {
|
||||
Some(v) => v.clone(),
|
||||
None => loop {
|
||||
let Some(k) = it.next(interp)? else { return Err(interp.type_error("reduce of an empty array with no initial value")) };
|
||||
if let Some(v) = element(interp, o, k)? {
|
||||
break v;
|
||||
}
|
||||
},
|
||||
};
|
||||
keep(interp, &acc);
|
||||
while let Some(k) = it.next(interp)? {
|
||||
let Some(v) = element(interp, o, k)? else { continue };
|
||||
acc = interp.call(&callback, &Value::Undefined, &[acc, v, number(k), Value::Object(o)])?;
|
||||
keep(interp, &acc);
|
||||
}
|
||||
Ok(acc)
|
||||
})();
|
||||
interp.temp_roots.truncate(slot);
|
||||
r
|
||||
})
|
||||
}
|
||||
|
||||
fn reduce(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
fold(interp, this, args, false)
|
||||
}
|
||||
|
||||
fn reduce_right(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
fold(interp, this, args, true)
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
//! The global functions (ECMA-262 5.1 section 15.1.2 and 15.1.3): `parseInt`, `parseFloat`,
|
||||
//! `isNaN`, `isFinite` and the URI functions, and Annex B's `escape` and `unescape` (sections
|
||||
//! B.2.1 and B.2.2). Each code unit read takes a unit of fuel, and the strings they build are
|
||||
//! checked against the heap limit as they grow.
|
||||
|
||||
use super::{Builder, arg, method};
|
||||
use crate::heap::NativeFn;
|
||||
use crate::interp::{ErrorType, Interp, JsResult};
|
||||
use crate::value::{Value, is_str_space, to_int32};
|
||||
|
||||
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
let global = interp.global;
|
||||
let functions: [(&'static str, NativeFn, u32); 10] = [
|
||||
("parseInt", parse_int, 2),
|
||||
("parseFloat", parse_float, 1),
|
||||
("isNaN", is_nan, 1),
|
||||
("isFinite", is_finite, 1),
|
||||
("decodeURI", decode_uri, 1),
|
||||
("decodeURIComponent", decode_uri_component, 1),
|
||||
("encodeURI", encode_uri, 1),
|
||||
("encodeURIComponent", encode_uri_component, 1),
|
||||
("escape", escape, 1),
|
||||
("unescape", unescape, 1),
|
||||
];
|
||||
for (name, f, length) in functions {
|
||||
method(interp, global, name, f, length)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `s` after its leading white space and line terminators.
|
||||
fn trim_start(s: &[u16]) -> &[u16] {
|
||||
s.get(s.iter().position(|&u| !is_str_space(u)).unwrap_or(s.len())..).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// ToString of the first argument, as code units, with each one taking a unit of fuel.
|
||||
fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult<std::rc::Rc<[u16]>> {
|
||||
let s = interp.to_str(&arg(args, 0))?.flat();
|
||||
for _ in 0..s.len() {
|
||||
interp.use_fuel()?;
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn digit_value(u: u16) -> Option<u32> {
|
||||
char::from_u32(u32::from(u)).and_then(|c| c.to_digit(36))
|
||||
}
|
||||
|
||||
/// `parseInt` (section 15.1.2.2).
|
||||
fn parse_int(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = units_of(interp, args)?;
|
||||
let radix = to_int32(interp.to_number(&arg(args, 1))?);
|
||||
let mut rest = trim_start(&s);
|
||||
let sign = match rest.first() {
|
||||
Some(&u) if u == u16::from(b'-') => -1.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
if matches!(rest.first(), Some(&u) if u == u16::from(b'-') || u == u16::from(b'+')) {
|
||||
rest = rest.get(1..).unwrap_or_default();
|
||||
}
|
||||
let mut r = radix as u32;
|
||||
let mut strip_prefix = true;
|
||||
if radix != 0 {
|
||||
if !(2..=36).contains(&radix) {
|
||||
return Ok(Value::Number(f64::NAN));
|
||||
}
|
||||
strip_prefix = radix == 16;
|
||||
} else {
|
||||
r = 10;
|
||||
}
|
||||
if strip_prefix
|
||||
&& rest.len() >= 2
|
||||
&& rest.first() == Some(&u16::from(b'0'))
|
||||
&& matches!(rest.get(1), Some(&u) if u == u16::from(b'x') || u == u16::from(b'X'))
|
||||
{
|
||||
rest = rest.get(2..).unwrap_or_default();
|
||||
r = 16;
|
||||
}
|
||||
let digits: Vec<u32> = rest.iter().map_while(|&u| digit_value(u).filter(|&d| d < r)).collect();
|
||||
if digits.is_empty() {
|
||||
return Ok(Value::Number(f64::NAN));
|
||||
}
|
||||
let value = match r {
|
||||
10 => digits.iter().filter_map(|&d| char::from_digit(d, 10)).collect::<String>().parse::<f64>().unwrap_or(f64::NAN),
|
||||
2 | 4 | 8 | 16 | 32 => crate::chars::radix_value(digits.iter().copied(), r).unwrap_or(f64::NAN),
|
||||
// Other radices may be approximated (section 15.1.2.2, step 13).
|
||||
_ => digits.iter().fold(0.0, |n, &d| n * f64::from(r) + f64::from(d)),
|
||||
};
|
||||
Ok(Value::Number(sign * value))
|
||||
}
|
||||
|
||||
/// The length of the longest prefix of `s` that is a StrDecimalLiteral without its sign and
|
||||
/// without `Infinity`.
|
||||
fn decimal_prefix(s: &[u16]) -> usize {
|
||||
let is_digit = |i: usize| s.get(i).is_some_and(|&u| (u16::from(b'0')..=u16::from(b'9')).contains(&u));
|
||||
let mut i = 0;
|
||||
while is_digit(i) {
|
||||
i += 1;
|
||||
}
|
||||
let mut mantissa = i;
|
||||
if s.get(i) == Some(&u16::from(b'.')) {
|
||||
let mut j = i + 1;
|
||||
while is_digit(j) {
|
||||
j += 1;
|
||||
}
|
||||
mantissa += j - i - 1;
|
||||
if mantissa > 0 {
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
if mantissa == 0 {
|
||||
return 0;
|
||||
}
|
||||
if matches!(s.get(i), Some(&u) if u == u16::from(b'e') || u == u16::from(b'E')) {
|
||||
let mut j = i + 1;
|
||||
if matches!(s.get(j), Some(&u) if u == u16::from(b'+') || u == u16::from(b'-')) {
|
||||
j += 1;
|
||||
}
|
||||
let digits_start = j;
|
||||
while is_digit(j) {
|
||||
j += 1;
|
||||
}
|
||||
if j > digits_start {
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// `parseFloat` (section 15.1.2.3).
|
||||
fn parse_float(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = units_of(interp, args)?;
|
||||
let mut rest = trim_start(&s);
|
||||
let mut sign = 1.0;
|
||||
if let Some(&u) = rest.first()
|
||||
&& (u == u16::from(b'-') || u == u16::from(b'+'))
|
||||
{
|
||||
if u == u16::from(b'-') {
|
||||
sign = -1.0;
|
||||
}
|
||||
rest = rest.get(1..).unwrap_or_default();
|
||||
}
|
||||
let infinity: Vec<u16> = "Infinity".encode_utf16().collect();
|
||||
if rest.starts_with(&infinity) {
|
||||
return Ok(Value::Number(sign * f64::INFINITY));
|
||||
}
|
||||
let n = decimal_prefix(rest);
|
||||
if n == 0 {
|
||||
return Ok(Value::Number(f64::NAN));
|
||||
}
|
||||
let text = String::from_utf16_lossy(rest.get(..n).unwrap_or_default());
|
||||
Ok(Value::Number(sign * text.parse::<f64>().unwrap_or(f64::NAN)))
|
||||
}
|
||||
|
||||
/// `isNaN` (section 15.1.2.4).
|
||||
fn is_nan(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
Ok(Value::Bool(interp.to_number(&arg(args, 0))?.is_nan()))
|
||||
}
|
||||
|
||||
/// `isFinite` (section 15.1.2.5).
|
||||
fn is_finite(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
Ok(Value::Bool(interp.to_number(&arg(args, 0))?.is_finite()))
|
||||
}
|
||||
|
||||
// ---- URIs ----
|
||||
|
||||
const URI_RESERVED: &str = ";/?:@&=+$,";
|
||||
const URI_MARK: &str = "-_.!~*'()";
|
||||
|
||||
fn in_set(u: u16, extra: &str) -> bool {
|
||||
u < 0x80 && extra.as_bytes().contains(&(u as u8))
|
||||
}
|
||||
|
||||
fn is_unreserved(u: u16) -> bool {
|
||||
u < 0x80 && ((u as u8).is_ascii_alphanumeric() || in_set(u, URI_MARK))
|
||||
}
|
||||
|
||||
fn uri_error(interp: &mut Interp, why: &str) -> crate::interp::Abrupt {
|
||||
interp.throw(ErrorType::Uri, why)
|
||||
}
|
||||
|
||||
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
||||
|
||||
fn push_byte(out: &mut Builder, interp: &mut Interp, b: u8) -> JsResult<()> {
|
||||
let units = [u16::from(b'%'), u16::from(HEX[usize::from(b >> 4)]), u16::from(HEX[usize::from(b & 15)])];
|
||||
out.push_units(interp, &units)
|
||||
}
|
||||
|
||||
/// Encode (section 15.1.3): every code unit outside the unescaped set as the `%XX` escapes of
|
||||
/// its UTF-8 bytes.
|
||||
fn encode(interp: &mut Interp, args: &[Value], unescaped: fn(u16) -> bool) -> JsResult<Value> {
|
||||
let s = units_of(interp, args)?;
|
||||
let mut out = Builder::new();
|
||||
let mut k = 0;
|
||||
while let Some(&c) = s.get(k) {
|
||||
if unescaped(c) {
|
||||
out.push_units(interp, &[c])?;
|
||||
k += 1;
|
||||
continue;
|
||||
}
|
||||
let v = match c {
|
||||
0xDC00..=0xDFFF => return Err(uri_error(interp, "a lone trailing surrogate cannot be encoded")),
|
||||
0xD800..=0xDBFF => match s.get(k + 1) {
|
||||
Some(&d) if (0xDC00..=0xDFFF).contains(&d) => {
|
||||
k += 1;
|
||||
(u32::from(c) - 0xD800) * 0x400 + (u32::from(d) - 0xDC00) + 0x10000
|
||||
}
|
||||
_ => return Err(uri_error(interp, "a lone leading surrogate cannot be encoded")),
|
||||
},
|
||||
_ => u32::from(c),
|
||||
};
|
||||
let mut buf = [0u8; 4];
|
||||
let Some(ch) = char::from_u32(v) else { return Err(uri_error(interp, "the string holds a code point that cannot be encoded")) };
|
||||
for &b in ch.encode_utf8(&mut buf).as_bytes() {
|
||||
push_byte(&mut out, interp, b)?;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
Ok(out.finish())
|
||||
}
|
||||
|
||||
fn hex_byte(s: &[u16], at: usize) -> Option<u8> {
|
||||
let hi = crate::chars::hex_value(u32::from(*s.get(at)?))?;
|
||||
let lo = crate::chars::hex_value(u32::from(*s.get(at + 1)?))?;
|
||||
u8::try_from(hi * 16 + lo).ok()
|
||||
}
|
||||
|
||||
/// Decode (section 15.1.3): each `%XX` escape sequence of a UTF-8 character as the character,
|
||||
/// except an escaped character in `reserved`, which stays escaped.
|
||||
fn decode(interp: &mut Interp, args: &[Value], reserved: &str) -> JsResult<Value> {
|
||||
let s = units_of(interp, args)?;
|
||||
let mut out = Builder::new();
|
||||
let mut k = 0;
|
||||
while let Some(&c) = s.get(k) {
|
||||
if c != u16::from(b'%') {
|
||||
out.push_units(interp, &[c])?;
|
||||
k += 1;
|
||||
continue;
|
||||
}
|
||||
let start = k;
|
||||
let Some(b) = hex_byte(&s, k + 1) else { return Err(uri_error(interp, "a % is not followed by two hexadecimal digits")) };
|
||||
k += 2;
|
||||
if b & 0x80 == 0 {
|
||||
let c = u16::from(b);
|
||||
if in_set(c, reserved) {
|
||||
out.push_units(interp, s.get(start..=k).unwrap_or_default())?;
|
||||
} else {
|
||||
out.push_units(interp, &[c])?;
|
||||
}
|
||||
k += 1;
|
||||
continue;
|
||||
}
|
||||
let n = b.leading_ones() as usize;
|
||||
if n == 1 || n > 4 {
|
||||
return Err(uri_error(interp, "an escape sequence is not the start of a UTF-8 character"));
|
||||
}
|
||||
let mut octets = vec![b];
|
||||
for _ in 1..n {
|
||||
k += 1;
|
||||
let next = if s.get(k) == Some(&u16::from(b'%')) { hex_byte(&s, k + 1) } else { None };
|
||||
let Some(b) = next.filter(|b| b & 0xC0 == 0x80) else { return Err(uri_error(interp, "an escaped UTF-8 character is incomplete")) };
|
||||
k += 2;
|
||||
octets.push(b);
|
||||
}
|
||||
let Some(ch) = std::str::from_utf8(&octets).ok().and_then(|t| t.chars().next()) else {
|
||||
return Err(uri_error(interp, "an escape sequence is not valid UTF-8"));
|
||||
};
|
||||
let mut units = [0u16; 2];
|
||||
out.push_units(interp, ch.encode_utf16(&mut units))?;
|
||||
k += 1;
|
||||
}
|
||||
Ok(out.finish())
|
||||
}
|
||||
|
||||
fn encode_uri(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
encode(interp, args, |u| is_unreserved(u) || in_set(u, URI_RESERVED) || u == u16::from(b'#'))
|
||||
}
|
||||
|
||||
fn encode_uri_component(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
encode(interp, args, is_unreserved)
|
||||
}
|
||||
|
||||
fn decode_uri(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
decode(interp, args, ";/?:@&=+$,#")
|
||||
}
|
||||
|
||||
fn decode_uri_component(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
decode(interp, args, "")
|
||||
}
|
||||
|
||||
// ---- Annex B ----
|
||||
|
||||
/// `escape` (section B.2.1).
|
||||
fn escape(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = units_of(interp, args)?;
|
||||
let mut out = Builder::new();
|
||||
for &u in s.iter() {
|
||||
if u < 0x80 && ((u as u8).is_ascii_alphanumeric() || b"@*_+-./".contains(&(u as u8))) {
|
||||
out.push_units(interp, &[u])?;
|
||||
} else if u < 256 {
|
||||
push_byte(&mut out, interp, u as u8)?;
|
||||
} else {
|
||||
let hex = |shift: u16| u16::from(HEX[usize::from((u >> shift) & 15)]);
|
||||
out.push_units(interp, &[u16::from(b'%'), u16::from(b'u'), hex(12), hex(8), hex(4), hex(0)])?;
|
||||
}
|
||||
}
|
||||
Ok(out.finish())
|
||||
}
|
||||
|
||||
/// `unescape` (section B.2.2).
|
||||
fn unescape(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = units_of(interp, args)?;
|
||||
let mut out = Builder::new();
|
||||
let mut k = 0;
|
||||
let hex4 = |at: usize| -> Option<u16> {
|
||||
let mut v = 0u16;
|
||||
for i in 0..4 {
|
||||
v = v * 16 + u16::try_from(crate::chars::hex_value(u32::from(*s.get(at + i)?))?).ok()?;
|
||||
}
|
||||
Some(v)
|
||||
};
|
||||
while let Some(&c) = s.get(k) {
|
||||
let mut unit = c;
|
||||
if c == u16::from(b'%') {
|
||||
if s.get(k + 1) == Some(&u16::from(b'u'))
|
||||
&& let Some(v) = hex4(k + 2)
|
||||
{
|
||||
unit = v;
|
||||
k += 5;
|
||||
} else if let Some(b) = hex_byte(&s, k + 1) {
|
||||
unit = u16::from(b);
|
||||
k += 2;
|
||||
}
|
||||
}
|
||||
out.push_units(interp, &[unit])?;
|
||||
k += 1;
|
||||
}
|
||||
Ok(out.finish())
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
//! The `JSON` object (ECMA-262 5.1 section 15.12): `parse` with a reviver and `stringify` with a
|
||||
//! replacer function or property list and indentation.
|
||||
//!
|
||||
//! Both work without recursion. `parse` reads nested arrays and objects with an explicit stack and
|
||||
//! walks them for the reviver with another, and `stringify` writes nested values from a stack of
|
||||
//! the objects it is inside, so nesting is limited only by the heap. Each code unit of the text
|
||||
//! parsed, each property visited and each value written takes a unit of fuel; strings, arrays and
|
||||
//! objects are checked against the heap limit before they are made, and the text `stringify`
|
||||
//! writes is checked as it grows.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{Builder, ELEMENT_BYTES, arg, data, key_of, length_of, method, new_array, new_string, to_integer};
|
||||
use crate::heap::{Class, Key, OBJECT_BYTES, OPEN, ObjId, Object, Prop, key, string_bytes};
|
||||
use crate::interp::{Desc, ErrorType, Interp, JsResult};
|
||||
use crate::value::{Str, Value, number_to_str};
|
||||
|
||||
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
interp.reserve_heap(OBJECT_BYTES)?;
|
||||
let proto = interp.intrinsics.object_proto;
|
||||
let json = interp.heap.alloc_object(Object::new(Class::Json, Some(proto)));
|
||||
method(interp, json, "parse", parse, 2)?;
|
||||
method(interp, json, "stringify", stringify, 3)?;
|
||||
let global = interp.global;
|
||||
data(interp, global, "JSON", Value::Object(json), crate::heap::HIDDEN)
|
||||
}
|
||||
|
||||
// ---- parse ----
|
||||
|
||||
/// A container being read, with the property name its next value goes under.
|
||||
enum Open {
|
||||
Array { obj: ObjId, next: u32 },
|
||||
Object { obj: ObjId, name: Option<Key> },
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
s: &'a [u16],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
const QUOTE: u16 = b'"' as u16;
|
||||
|
||||
impl Parser<'_> {
|
||||
fn peek(&self) -> Option<u16> {
|
||||
self.s.get(self.pos).copied()
|
||||
}
|
||||
|
||||
fn skip_space(&mut self) {
|
||||
while matches!(self.peek(), Some(0x09 | 0x0A | 0x0D | 0x20)) {
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn eat(&mut self, c: u8) -> bool {
|
||||
if self.peek() == Some(u16::from(c)) {
|
||||
self.pos += 1;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn word(&mut self, w: &str) -> bool {
|
||||
let units: Vec<u16> = w.encode_utf16().collect();
|
||||
if self.s.get(self.pos..).is_some_and(|rest| rest.starts_with(&units)) {
|
||||
self.pos += units.len();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// A JSONString's characters (section 15.12.1.1), after its opening quote.
|
||||
fn string(&mut self) -> Result<Vec<u16>, &'static str> {
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
let Some(c) = self.peek() else { return Err("a string is not closed") };
|
||||
self.pos += 1;
|
||||
match c {
|
||||
QUOTE => return Ok(out),
|
||||
0x5C => {
|
||||
let Some(e) = self.peek() else { return Err("a string is not closed") };
|
||||
self.pos += 1;
|
||||
let unit = match u8::try_from(e).unwrap_or(0) {
|
||||
b'"' => QUOTE,
|
||||
b'\\' => 0x5C,
|
||||
b'/' => 0x2F,
|
||||
b'b' => 0x08,
|
||||
b'f' => 0x0C,
|
||||
b'n' => 0x0A,
|
||||
b'r' => 0x0D,
|
||||
b't' => 0x09,
|
||||
b'u' => {
|
||||
let mut v = 0u32;
|
||||
for _ in 0..4 {
|
||||
let Some(d) = self.peek().and_then(|u| crate::chars::hex_value(u32::from(u))) else {
|
||||
return Err("a \\u escape needs four hexadecimal digits");
|
||||
};
|
||||
self.pos += 1;
|
||||
v = v * 16 + d;
|
||||
}
|
||||
u16::try_from(v).unwrap_or(0)
|
||||
}
|
||||
_ => return Err("a string holds an escape sequence JSON does not have"),
|
||||
};
|
||||
out.push(unit);
|
||||
}
|
||||
0..=0x1F => return Err("a string holds a control character"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A JSONNumber (section 15.12.1.1).
|
||||
fn number(&mut self) -> Result<f64, &'static str> {
|
||||
let start = self.pos;
|
||||
self.eat(b'-');
|
||||
let digit = |p: &Self| p.peek().is_some_and(|u| (u16::from(b'0')..=u16::from(b'9')).contains(&u));
|
||||
if self.eat(b'0') {
|
||||
} else if digit(self) {
|
||||
while digit(self) {
|
||||
self.pos += 1;
|
||||
}
|
||||
} else {
|
||||
return Err("a number has no digits");
|
||||
}
|
||||
if self.eat(b'.') {
|
||||
if !digit(self) {
|
||||
return Err("a number's fraction has no digits");
|
||||
}
|
||||
while digit(self) {
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
if self.eat(b'e') || self.eat(b'E') {
|
||||
if !self.eat(b'+') {
|
||||
self.eat(b'-');
|
||||
}
|
||||
if !digit(self) {
|
||||
return Err("a number's exponent has no digits");
|
||||
}
|
||||
while digit(self) {
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
let text = String::from_utf16_lossy(self.s.get(start..self.pos).unwrap_or_default());
|
||||
text.parse::<f64>().map_err(|_| "a number cannot be read")
|
||||
}
|
||||
}
|
||||
|
||||
/// What the parser read at a value position.
|
||||
enum Token {
|
||||
Value(Value),
|
||||
OpenArray,
|
||||
OpenObject,
|
||||
}
|
||||
|
||||
fn syntax(interp: &mut Interp, why: &str, at: usize) -> crate::interp::Abrupt {
|
||||
interp.throw(ErrorType::Syntax, &format!("JSON.parse: {why} (at offset {at})"))
|
||||
}
|
||||
|
||||
/// `JSON.parse` (section 15.12.2).
|
||||
fn parse(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let text = interp.to_str(&arg(args, 0))?.flat();
|
||||
for _ in 0..text.len() {
|
||||
interp.use_fuel()?;
|
||||
}
|
||||
let mark = interp.temp_roots.len();
|
||||
let r = parse_text(interp, &text).and_then(|v| {
|
||||
let reviver = arg(args, 1);
|
||||
if !interp.is_callable(&reviver) {
|
||||
return Ok(v);
|
||||
}
|
||||
interp.temp_roots.push(v.clone());
|
||||
revive(interp, v, &reviver)
|
||||
});
|
||||
interp.temp_roots.truncate(mark);
|
||||
r
|
||||
}
|
||||
|
||||
/// Reads a value at the parser's position: a primitive, or the start of an array or object.
|
||||
fn token(interp: &mut Interp, p: &mut Parser<'_>) -> JsResult<Token> {
|
||||
p.skip_space();
|
||||
let at = p.pos;
|
||||
let t = match p.peek() {
|
||||
Some(QUOTE) => {
|
||||
p.pos += 1;
|
||||
let units = p.string().map_err(|why| syntax(interp, why, at))?;
|
||||
Token::Value(new_string(interp, units)?)
|
||||
}
|
||||
Some(0x5B) => {
|
||||
p.pos += 1;
|
||||
Token::OpenArray
|
||||
}
|
||||
Some(0x7B) => {
|
||||
p.pos += 1;
|
||||
Token::OpenObject
|
||||
}
|
||||
_ if p.word("true") => Token::Value(Value::Bool(true)),
|
||||
_ if p.word("false") => Token::Value(Value::Bool(false)),
|
||||
_ if p.word("null") => Token::Value(Value::Null),
|
||||
Some(u) if u == u16::from(b'-') || (u16::from(b'0')..=u16::from(b'9')).contains(&u) => {
|
||||
Token::Value(Value::Number(p.number().map_err(|why| syntax(interp, why, at))?))
|
||||
}
|
||||
Some(_) => return Err(syntax(interp, "unexpected character", at)),
|
||||
None => return Err(syntax(interp, "unexpected end of text", at)),
|
||||
};
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// Reads the property name of an object member and its colon, or `None` at the closing brace
|
||||
/// of an empty object.
|
||||
fn member_name(interp: &mut Interp, p: &mut Parser<'_>, first: bool) -> JsResult<Option<Key>> {
|
||||
p.skip_space();
|
||||
if first && p.eat(b'}') {
|
||||
return Ok(None);
|
||||
}
|
||||
let at = p.pos;
|
||||
if !p.eat(b'"') {
|
||||
return Err(syntax(interp, "a property name must be a string", at));
|
||||
}
|
||||
let units = p.string().map_err(|why| syntax(interp, why, at))?;
|
||||
interp.reserve_heap(string_bytes(units.len()))?;
|
||||
p.skip_space();
|
||||
if !p.eat(b':') {
|
||||
return Err(syntax(interp, "a property name must be followed by ':'", p.pos));
|
||||
}
|
||||
Ok(Some(units.into()))
|
||||
}
|
||||
|
||||
fn parse_text(interp: &mut Interp, text: &[u16]) -> JsResult<Value> {
|
||||
let mut p = Parser { s: text, pos: 0 };
|
||||
let mut stack: Vec<Open> = Vec::new();
|
||||
// The next value to read: the first of an array or object, or one after a comma.
|
||||
let mut value = token(interp, &mut p)?;
|
||||
loop {
|
||||
// Open a container, or place a finished value into the container it belongs to.
|
||||
let mut done = match value {
|
||||
Token::OpenArray => {
|
||||
let a = new_array(interp, 0)?;
|
||||
interp.temp_roots.push(Value::Object(a));
|
||||
p.skip_space();
|
||||
if p.eat(b']') {
|
||||
Some(Value::Object(a))
|
||||
} else {
|
||||
stack.push(Open::Array { obj: a, next: 0 });
|
||||
value = token(interp, &mut p)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Token::OpenObject => {
|
||||
interp.reserve_heap(OBJECT_BYTES)?;
|
||||
let o = interp.new_object();
|
||||
interp.temp_roots.push(Value::Object(o));
|
||||
match member_name(interp, &mut p, true)? {
|
||||
None => Some(Value::Object(o)),
|
||||
Some(name) => {
|
||||
stack.push(Open::Object { obj: o, name: Some(name) });
|
||||
value = token(interp, &mut p)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Token::Value(v) => Some(v),
|
||||
};
|
||||
// Close every container that the finished value completes.
|
||||
while let Some(v) = done.take() {
|
||||
let Some(top) = stack.last_mut() else {
|
||||
p.skip_space();
|
||||
if p.pos < text.len() {
|
||||
return Err(syntax(interp, "unexpected text after the value", p.pos));
|
||||
}
|
||||
return Ok(v);
|
||||
};
|
||||
match top {
|
||||
Open::Array { obj, next } => {
|
||||
let (obj, i) = (*obj, *next);
|
||||
*next = next.saturating_add(1);
|
||||
interp.reserve_heap(ELEMENT_BYTES)?;
|
||||
interp.define(obj, key_of(f64::from(i)), Prop::Data { value: v, flags: OPEN })?;
|
||||
p.skip_space();
|
||||
if p.eat(b',') {
|
||||
break;
|
||||
}
|
||||
if !p.eat(b']') {
|
||||
return Err(syntax(interp, "expected ',' or ']'", p.pos));
|
||||
}
|
||||
stack.pop();
|
||||
done = Some(Value::Object(obj));
|
||||
}
|
||||
Open::Object { obj, name } => {
|
||||
let obj = *obj;
|
||||
let Some(k) = name.take() else { return Err(syntax(interp, "a member has no name", p.pos)) };
|
||||
interp.reserve_heap(ELEMENT_BYTES)?;
|
||||
interp.define(obj, k, Prop::Data { value: v, flags: OPEN })?;
|
||||
p.skip_space();
|
||||
if p.eat(b',') {
|
||||
let k = member_name(interp, &mut p, false)?;
|
||||
if let Some(Open::Object { name, .. }) = stack.last_mut() {
|
||||
*name = k;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if !p.eat(b'}') {
|
||||
return Err(syntax(interp, "expected ',' or '}'", p.pos));
|
||||
}
|
||||
stack.pop();
|
||||
done = Some(Value::Object(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
value = token(interp, &mut p)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// A holder being walked for the reviver (section 15.12.2, Walk).
|
||||
struct Walk {
|
||||
holder: ObjId,
|
||||
name: Key,
|
||||
val: Value,
|
||||
keys: Vec<Key>,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
/// Starts walking `holder[name]`: reads the value and, for an array or object, the names of its
|
||||
/// elements or own enumerable properties. The value is pushed onto the temporary roots.
|
||||
fn enter(interp: &mut Interp, holder: ObjId, name: Key) -> JsResult<Walk> {
|
||||
let val = interp.get_from(holder, &name, &Value::Object(holder))?;
|
||||
interp.temp_roots.push(val.clone());
|
||||
let mut keys = Vec::new();
|
||||
if let Value::Object(o) = val {
|
||||
if interp.heap.object(o).is_some_and(|x| matches!(x.class, Class::Array { .. })) {
|
||||
let len = length_of(interp, o)?;
|
||||
interp.reserve_heap((len as usize).saturating_mul(crate::heap::PROP_BYTES))?;
|
||||
for i in 0..len {
|
||||
interp.use_fuel()?;
|
||||
keys.push(key_of(f64::from(i)));
|
||||
}
|
||||
} else {
|
||||
for k in interp.own_keys(o)? {
|
||||
if interp.get_own_property(o, &k)?.is_some_and(|p| p.enumerable()) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Walk { holder, name, val, keys, next: 0 })
|
||||
}
|
||||
|
||||
/// Walks the parsed value bottom-up, calling the reviver for each property and replacing or
|
||||
/// deleting the property with what it returns.
|
||||
fn revive(interp: &mut Interp, v: Value, reviver: &Value) -> JsResult<Value> {
|
||||
interp.reserve_heap(OBJECT_BYTES + ELEMENT_BYTES)?;
|
||||
let root = interp.new_object();
|
||||
interp.temp_roots.push(Value::Object(root));
|
||||
interp.define(root, key(""), Prop::Data { value: v, flags: OPEN })?;
|
||||
let mut stack = vec![enter(interp, root, key(""))?];
|
||||
let mut ret: Option<Value> = None;
|
||||
loop {
|
||||
let Some(top) = stack.last_mut() else { return Ok(Value::Undefined) };
|
||||
if let (Some(r), Value::Object(o)) = (ret.take(), &top.val) {
|
||||
let o = *o;
|
||||
let Some(k) = top.next.checked_sub(1).and_then(|i| top.keys.get(i)).cloned() else { return Ok(Value::Undefined) };
|
||||
if matches!(r, Value::Undefined) {
|
||||
interp.delete(o, &k, false)?;
|
||||
} else {
|
||||
interp.reserve_heap(ELEMENT_BYTES)?;
|
||||
interp.define_own_property(o, k, &Desc::data(r, OPEN), false)?;
|
||||
}
|
||||
}
|
||||
if let (Some(k), Value::Object(o)) = (top.keys.get(top.next).cloned(), &top.val) {
|
||||
let o = *o;
|
||||
top.next += 1;
|
||||
interp.use_fuel()?;
|
||||
stack.push(enter(interp, o, k)?);
|
||||
continue;
|
||||
}
|
||||
let Some(done) = stack.pop() else { return Ok(Value::Undefined) };
|
||||
let name = Value::String(Str::from_rc(done.name));
|
||||
let r = interp.call(reviver, &Value::Object(done.holder), &[name, done.val])?;
|
||||
interp.temp_roots.pop();
|
||||
if stack.is_empty() {
|
||||
return Ok(r);
|
||||
}
|
||||
ret = Some(r);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- stringify ----
|
||||
|
||||
/// The settings of one `stringify` call.
|
||||
struct Settings {
|
||||
replacer: Option<Value>,
|
||||
properties: Option<Vec<Key>>,
|
||||
gap: Vec<u16>,
|
||||
}
|
||||
|
||||
/// A value as `stringify` writes it, after `toJSON`, the replacer and unwrapping.
|
||||
enum Item {
|
||||
Skip,
|
||||
Primitive(Value),
|
||||
Container(ObjId),
|
||||
}
|
||||
|
||||
/// An object or array being written.
|
||||
struct Writing {
|
||||
obj: ObjId,
|
||||
keys: Vec<Key>,
|
||||
array: bool,
|
||||
next: usize,
|
||||
count: usize,
|
||||
/// The indentation outside it.
|
||||
stepback: usize,
|
||||
}
|
||||
|
||||
struct Writer {
|
||||
out: Builder,
|
||||
indent: Vec<u16>,
|
||||
stack: Vec<Writing>,
|
||||
open: HashSet<ObjId>,
|
||||
}
|
||||
|
||||
/// `JSON.stringify` (section 15.12.3).
|
||||
fn stringify(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let mark = interp.temp_roots.len();
|
||||
let r = stringify_rooted(interp, args);
|
||||
interp.temp_roots.truncate(mark);
|
||||
r
|
||||
}
|
||||
|
||||
fn stringify_rooted(interp: &mut Interp, args: &[Value]) -> JsResult<Value> {
|
||||
let settings = settings(interp, &arg(args, 1), &arg(args, 2))?;
|
||||
interp.reserve_heap(OBJECT_BYTES + ELEMENT_BYTES)?;
|
||||
let wrapper = interp.new_object();
|
||||
interp.temp_roots.push(Value::Object(wrapper));
|
||||
interp.define(wrapper, key(""), Prop::Data { value: arg(args, 0), flags: OPEN })?;
|
||||
let mut w = Writer { out: Builder::new(), indent: Vec::new(), stack: Vec::new(), open: HashSet::new() };
|
||||
match resolve(interp, &settings, wrapper, &key(""))? {
|
||||
Item::Skip => return Ok(Value::Undefined),
|
||||
Item::Primitive(v) => write_primitive(interp, &mut w.out, &v)?,
|
||||
Item::Container(o) => open(interp, &settings, &mut w, o)?,
|
||||
}
|
||||
while !w.stack.is_empty() {
|
||||
step(interp, &settings, &mut w)?;
|
||||
}
|
||||
Ok(w.out.finish())
|
||||
}
|
||||
|
||||
/// Reads the replacer and the space argument (section 15.12.3, steps 4 to 8).
|
||||
fn settings(interp: &mut Interp, replacer: &Value, space: &Value) -> JsResult<Settings> {
|
||||
let mut s = Settings { replacer: None, properties: None, gap: Vec::new() };
|
||||
if let Value::Object(r) = replacer {
|
||||
if interp.is_callable(replacer) {
|
||||
s.replacer = Some(replacer.clone());
|
||||
} else if interp.heap.object(*r).is_some_and(|o| matches!(o.class, Class::Array { .. })) {
|
||||
s.properties = Some(property_list(interp, *r)?);
|
||||
}
|
||||
}
|
||||
let space = match space {
|
||||
Value::Object(o) => match interp.heap.object(*o).map(|x| &x.class) {
|
||||
Some(Class::Number) => Value::Number(interp.to_number(space)?),
|
||||
Some(Class::String(_)) => Value::String(interp.to_str(space)?),
|
||||
_ => space.clone(),
|
||||
},
|
||||
_ => space.clone(),
|
||||
};
|
||||
match space {
|
||||
Value::Number(_) => {
|
||||
let n = to_integer(interp, &space)?.clamp(0.0, 10.0) as usize;
|
||||
s.gap = vec![u16::from(b' '); n];
|
||||
}
|
||||
Value::String(t) => s.gap = t.flat().iter().take(10).copied().collect(),
|
||||
_ => {}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// The property names a replacer array lists, without repeats (section 15.12.3, step 4.b).
|
||||
fn property_list(interp: &mut Interp, r: ObjId) -> JsResult<Vec<Key>> {
|
||||
let len = length_of(interp, r)?;
|
||||
let mut list: Vec<Key> = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for i in 0..len {
|
||||
interp.use_fuel()?;
|
||||
let v = interp.get_from(r, &key_of(f64::from(i)), &Value::Object(r))?;
|
||||
let item = match &v {
|
||||
Value::String(s) => Some(s.flat()),
|
||||
Value::Number(n) => Some(number_to_str(*n).flat()),
|
||||
Value::Object(o) if interp.heap.object(*o).is_some_and(|x| matches!(x.class, Class::String(_) | Class::Number)) => {
|
||||
Some(interp.to_str(&v)?.flat())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(k) = item
|
||||
&& seen.insert(k.clone())
|
||||
{
|
||||
interp.reserve_heap(string_bytes(k.len()))?;
|
||||
list.push(k);
|
||||
}
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Steps 1 to 4 and 10 of Str (section 15.12.3): reads `holder[key]`, applies `toJSON` and the
|
||||
/// replacer, unwraps Number, String and Boolean objects, and leaves out functions and
|
||||
/// `undefined`.
|
||||
fn resolve(interp: &mut Interp, settings: &Settings, holder: ObjId, k: &Key) -> JsResult<Item> {
|
||||
let slot = interp.temp_roots.len();
|
||||
interp.temp_roots.push(Value::Undefined);
|
||||
let r = resolve_rooted(interp, settings, holder, k, slot);
|
||||
interp.temp_roots.truncate(slot);
|
||||
r
|
||||
}
|
||||
|
||||
fn resolve_rooted(interp: &mut Interp, settings: &Settings, holder: ObjId, k: &Key, slot: usize) -> JsResult<Item> {
|
||||
let keep = |interp: &mut Interp, v: &Value| {
|
||||
if let Some(s) = interp.temp_roots.get_mut(slot) {
|
||||
*s = v.clone();
|
||||
}
|
||||
};
|
||||
let mut v = interp.get_from(holder, k, &Value::Object(holder))?;
|
||||
keep(interp, &v);
|
||||
let name = Value::String(Str::from_rc(k.clone()));
|
||||
if let Value::Object(o) = v {
|
||||
let to_json = interp.get_from(o, &key("toJSON"), &v)?;
|
||||
if interp.is_callable(&to_json) {
|
||||
v = interp.call(&to_json, &v, std::slice::from_ref(&name))?;
|
||||
keep(interp, &v);
|
||||
}
|
||||
}
|
||||
if let Some(replacer) = &settings.replacer {
|
||||
v = interp.call(replacer, &Value::Object(holder), &[name, v])?;
|
||||
keep(interp, &v);
|
||||
}
|
||||
if let Value::Object(o) = &v {
|
||||
v = match interp.heap.object(*o).map(|x| &x.class) {
|
||||
Some(Class::Number) => Value::Number(interp.to_number(&v)?),
|
||||
Some(Class::String(_)) => Value::String(interp.to_str(&v)?),
|
||||
Some(Class::Boolean(b)) => Value::Bool(*b),
|
||||
_ => v,
|
||||
};
|
||||
}
|
||||
Ok(match v {
|
||||
Value::Undefined => Item::Skip,
|
||||
Value::Object(_) if interp.is_callable(&v) => Item::Skip,
|
||||
Value::Object(o) => Item::Container(o),
|
||||
other => Item::Primitive(other),
|
||||
})
|
||||
}
|
||||
|
||||
/// Quote (section 15.12.3): the string in double quotes with the characters JSON escapes.
|
||||
fn write_quoted(interp: &mut Interp, out: &mut Builder, s: &[u16]) -> JsResult<()> {
|
||||
out.push_units(interp, &[QUOTE])?;
|
||||
let mut start = 0;
|
||||
for (i, &c) in s.iter().enumerate() {
|
||||
let escape: Option<&str> = match c {
|
||||
QUOTE => Some("\\\""),
|
||||
0x5C => Some("\\\\"),
|
||||
0x08 => Some("\\b"),
|
||||
0x0C => Some("\\f"),
|
||||
0x0A => Some("\\n"),
|
||||
0x0D => Some("\\r"),
|
||||
0x09 => Some("\\t"),
|
||||
0..=0x1F => None,
|
||||
_ => continue,
|
||||
};
|
||||
out.push_units(interp, s.get(start..i).unwrap_or_default())?;
|
||||
match escape {
|
||||
Some(e) => out.push_str(interp, e)?,
|
||||
None => out.push_str(interp, &format!("\\u{c:04x}"))?,
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
out.push_units(interp, s.get(start..).unwrap_or_default())?;
|
||||
out.push_units(interp, &[QUOTE])
|
||||
}
|
||||
|
||||
fn write_primitive(interp: &mut Interp, out: &mut Builder, v: &Value) -> JsResult<()> {
|
||||
match v {
|
||||
Value::Null => out.push_str(interp, "null"),
|
||||
Value::Bool(b) => out.push_str(interp, if *b { "true" } else { "false" }),
|
||||
Value::Number(n) if n.is_finite() => out.push_units(interp, &number_to_str(*n).flat()),
|
||||
Value::Number(_) => out.push_str(interp, "null"),
|
||||
Value::String(s) => write_quoted(interp, out, &s.flat()),
|
||||
Value::Undefined | Value::Object(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts writing an object or array: a cycle back to one already being written throws a
|
||||
/// `TypeError`.
|
||||
fn open(interp: &mut Interp, settings: &Settings, w: &mut Writer, o: ObjId) -> JsResult<()> {
|
||||
if w.open.contains(&o) {
|
||||
return Err(interp.type_error("JSON.stringify cannot write a structure that contains itself"));
|
||||
}
|
||||
interp.temp_roots.push(Value::Object(o));
|
||||
let array = interp.heap.object(o).is_some_and(|x| matches!(x.class, Class::Array { .. }));
|
||||
let keys = if array {
|
||||
let len = length_of(interp, o)?;
|
||||
// Every element is written as at least `null` and a comma, so a length that cannot fit
|
||||
// is refused before anything is listed.
|
||||
if u64::from(len).saturating_mul(5) > (interp.limits.heap_bytes / 4) as u64 {
|
||||
return Err(crate::interp::Abrupt::Limit(crate::LimitKind::Heap));
|
||||
}
|
||||
interp.reserve_heap((len as usize).saturating_mul(crate::heap::PROP_BYTES))?;
|
||||
let mut keys = Vec::with_capacity(len as usize);
|
||||
for i in 0..len {
|
||||
interp.use_fuel()?;
|
||||
keys.push(key_of(f64::from(i)));
|
||||
}
|
||||
keys
|
||||
} else if let Some(list) = &settings.properties {
|
||||
list.clone()
|
||||
} else {
|
||||
let mut keys = Vec::new();
|
||||
for k in interp.own_keys(o)? {
|
||||
if interp.get_own_property(o, &k)?.is_some_and(|p| p.enumerable()) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
keys
|
||||
};
|
||||
w.open.insert(o);
|
||||
let stepback = w.indent.len();
|
||||
w.indent.extend_from_slice(&settings.gap);
|
||||
w.out.push_str(interp, if array { "[" } else { "{" })?;
|
||||
w.stack.push(Writing { obj: o, keys, array, next: 0, count: 0, stepback });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The separator before a member or element: a comma after the first, and with a gap a line
|
||||
/// break and the indentation.
|
||||
fn separator(interp: &mut Interp, w: &mut Writer, first: bool, gap: bool) -> JsResult<()> {
|
||||
if !first {
|
||||
w.out.push_str(interp, ",")?;
|
||||
}
|
||||
if gap {
|
||||
w.out.push_str(interp, "\n")?;
|
||||
w.out.push_units(interp, &w.indent.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes the next member or element of the innermost object or array, or closes it.
|
||||
fn step(interp: &mut Interp, settings: &Settings, w: &mut Writer) -> JsResult<()> {
|
||||
let gap = !settings.gap.is_empty();
|
||||
let Some(top) = w.stack.last_mut() else { return Ok(()) };
|
||||
let (obj, array, count) = (top.obj, top.array, top.count);
|
||||
let Some(k) = top.keys.get(top.next).cloned() else {
|
||||
let stepback = top.stepback;
|
||||
w.stack.pop();
|
||||
w.open.remove(&obj);
|
||||
interp.temp_roots.pop();
|
||||
w.indent.truncate(stepback);
|
||||
if count > 0 && gap {
|
||||
w.out.push_str(interp, "\n")?;
|
||||
w.out.push_units(interp, &w.indent.clone())?;
|
||||
}
|
||||
return w.out.push_str(interp, if array { "]" } else { "}" });
|
||||
};
|
||||
top.next += 1;
|
||||
interp.use_fuel()?;
|
||||
let item = resolve(interp, settings, obj, &k)?;
|
||||
if !array && matches!(item, Item::Skip) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(top) = w.stack.last_mut() {
|
||||
top.count += 1;
|
||||
}
|
||||
separator(interp, w, count == 0, gap)?;
|
||||
if !array {
|
||||
write_quoted(interp, &mut w.out, &k)?;
|
||||
w.out.push_str(interp, if gap { ": " } else { ":" })?;
|
||||
}
|
||||
match item {
|
||||
Item::Skip => w.out.push_str(interp, "null"),
|
||||
Item::Primitive(v) => write_primitive(interp, &mut w.out, &v),
|
||||
Item::Container(o) => open(interp, settings, w, o),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//! The `Math` object (ECMA-262 5.1 section 15.8): its constants and functions, with the special
|
||||
//! cases the section lists for NaN, signed zeros and infinities.
|
||||
|
||||
use super::{arg, data, method};
|
||||
use crate::heap::{Class, NativeFn, OBJECT_BYTES, Object};
|
||||
use crate::interp::{Interp, JsResult};
|
||||
use crate::value::Value;
|
||||
|
||||
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
interp.reserve_heap(OBJECT_BYTES)?;
|
||||
let proto = interp.intrinsics.object_proto;
|
||||
let math = interp.heap.alloc_object(Object::new(Class::Math, Some(proto)));
|
||||
let constants = [
|
||||
("E", std::f64::consts::E),
|
||||
("LN10", std::f64::consts::LN_10),
|
||||
("LN2", std::f64::consts::LN_2),
|
||||
("LOG2E", std::f64::consts::LOG2_E),
|
||||
("LOG10E", std::f64::consts::LOG10_E),
|
||||
("PI", std::f64::consts::PI),
|
||||
("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
|
||||
("SQRT2", std::f64::consts::SQRT_2),
|
||||
];
|
||||
for (name, v) in constants {
|
||||
data(interp, math, name, Value::Number(v), 0)?;
|
||||
}
|
||||
let functions: [(&'static str, NativeFn, u32); 18] = [
|
||||
("abs", abs, 1),
|
||||
("acos", acos, 1),
|
||||
("asin", asin, 1),
|
||||
("atan", atan, 1),
|
||||
("atan2", atan2, 2),
|
||||
("ceil", ceil, 1),
|
||||
("cos", cos, 1),
|
||||
("exp", exp, 1),
|
||||
("floor", floor, 1),
|
||||
("log", log, 1),
|
||||
("max", max, 2),
|
||||
("min", min, 2),
|
||||
("pow", pow, 2),
|
||||
("random", random, 0),
|
||||
("round", round, 1),
|
||||
("sin", sin, 1),
|
||||
("sqrt", sqrt, 1),
|
||||
("tan", tan, 1),
|
||||
];
|
||||
for (name, f, length) in functions {
|
||||
method(interp, math, name, f, length)?;
|
||||
}
|
||||
let global = interp.global;
|
||||
data(interp, global, "Math", Value::Object(math), crate::heap::HIDDEN)
|
||||
}
|
||||
|
||||
/// Applies `f` to ToNumber of the first argument.
|
||||
fn unary(interp: &mut Interp, args: &[Value], f: fn(f64) -> f64) -> JsResult<Value> {
|
||||
let x = interp.to_number(&arg(args, 0))?;
|
||||
Ok(Value::Number(f(x)))
|
||||
}
|
||||
|
||||
fn abs(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::abs)
|
||||
}
|
||||
|
||||
fn acos(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::acos)
|
||||
}
|
||||
|
||||
fn asin(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::asin)
|
||||
}
|
||||
|
||||
fn atan(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::atan)
|
||||
}
|
||||
|
||||
fn ceil(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::ceil)
|
||||
}
|
||||
|
||||
fn cos(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::cos)
|
||||
}
|
||||
|
||||
fn exp(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::exp)
|
||||
}
|
||||
|
||||
fn floor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::floor)
|
||||
}
|
||||
|
||||
fn log(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::ln)
|
||||
}
|
||||
|
||||
fn sin(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::sin)
|
||||
}
|
||||
|
||||
fn sqrt(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::sqrt)
|
||||
}
|
||||
|
||||
fn tan(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, f64::tan)
|
||||
}
|
||||
|
||||
/// `Math.round` (section 15.8.2.15): the nearest integer, halves rounding up, with -0 for
|
||||
/// arguments from -0.5 to -0.
|
||||
fn round_number(x: f64) -> f64 {
|
||||
if !x.is_finite() || x == 0.0 {
|
||||
return x;
|
||||
}
|
||||
if (-0.5..0.0).contains(&x) {
|
||||
return -0.0;
|
||||
}
|
||||
let floor = x.floor();
|
||||
// `x - floor` is exact, so a value just below a half (0.49999999999999994) does not round
|
||||
// up the way `floor(x + 0.5)` would.
|
||||
if x - floor >= 0.5 { floor + 1.0 } else { floor }
|
||||
}
|
||||
|
||||
fn round(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
unary(interp, args, round_number)
|
||||
}
|
||||
|
||||
/// `Math.atan2` (section 15.8.2.5).
|
||||
fn atan2(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let y = interp.to_number(&arg(args, 0))?;
|
||||
let x = interp.to_number(&arg(args, 1))?;
|
||||
Ok(Value::Number(y.atan2(x)))
|
||||
}
|
||||
|
||||
/// `Math.pow` (section 15.8.2.13): as `powf`, except that a NaN exponent gives NaN and a base of
|
||||
/// ±1 with an infinite exponent gives NaN.
|
||||
fn pow_number(x: f64, y: f64) -> f64 {
|
||||
if y.is_nan() || (x.abs() == 1.0 && y.is_infinite()) {
|
||||
return f64::NAN;
|
||||
}
|
||||
x.powf(y)
|
||||
}
|
||||
|
||||
fn pow(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = interp.to_number(&arg(args, 0))?;
|
||||
let y = interp.to_number(&arg(args, 1))?;
|
||||
Ok(Value::Number(pow_number(x, y)))
|
||||
}
|
||||
|
||||
/// `Math.max` and `Math.min` (sections 15.8.2.11 and 15.8.2.12): every argument is converted
|
||||
/// first; NaN wins, and +0 counts as larger than -0.
|
||||
fn extreme(interp: &mut Interp, args: &[Value], max: bool) -> JsResult<Value> {
|
||||
let mut best = if max { f64::NEG_INFINITY } else { f64::INFINITY };
|
||||
let mut nan = false;
|
||||
for a in args {
|
||||
interp.use_fuel()?;
|
||||
let x = interp.to_number(a)?;
|
||||
if x.is_nan() {
|
||||
nan = true;
|
||||
continue;
|
||||
}
|
||||
let better = if x == best {
|
||||
if max { best.is_sign_negative() && x.is_sign_positive() } else { best.is_sign_positive() && x.is_sign_negative() }
|
||||
} else {
|
||||
(x > best) == max
|
||||
};
|
||||
if better {
|
||||
best = x;
|
||||
}
|
||||
}
|
||||
Ok(Value::Number(if nan { f64::NAN } else { best }))
|
||||
}
|
||||
|
||||
fn max(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
extreme(interp, args, true)
|
||||
}
|
||||
|
||||
fn min(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
extreme(interp, args, false)
|
||||
}
|
||||
|
||||
/// `Math.random` (section 15.8.2.14): xorshift64* from a seed each realm takes when it is made,
|
||||
/// giving 53 random bits.
|
||||
fn random(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let mut s = interp.random;
|
||||
s ^= s >> 12;
|
||||
s ^= s << 25;
|
||||
s ^= s >> 27;
|
||||
interp.random = s;
|
||||
let bits = s.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 11;
|
||||
Ok(Value::Number(bits as f64 / (1u64 << 53) as f64))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{pow_number, round_number};
|
||||
|
||||
#[test]
|
||||
fn round_and_pow_follow_the_special_cases() {
|
||||
assert_eq!(round_number(0.499_999_999_999_999_94), 0.0);
|
||||
assert_eq!(round_number(2.5), 3.0);
|
||||
assert_eq!(round_number(-2.5), -2.0);
|
||||
assert!(round_number(-0.5).is_sign_negative());
|
||||
assert!(round_number(-0.0).is_sign_negative());
|
||||
assert_eq!(round_number(4_503_599_627_370_495.5), 4_503_599_627_370_496.0);
|
||||
assert!(pow_number(1.0, f64::NAN).is_nan());
|
||||
assert!(pow_number(-1.0, f64::INFINITY).is_nan());
|
||||
assert_eq!(pow_number(f64::NAN, 0.0), 1.0);
|
||||
assert_eq!(pow_number(2.0, 10.0), 1024.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
//! `Object`'s functions (ECMA-262 5.1 section 15.2.3). A primitive given where an object is
|
||||
//! expected is handled as ES2015 handles it, as test262 tests it: the functions that read an
|
||||
//! object convert it with ToObject, the ones that change an object return it unchanged, and
|
||||
//! `isFrozen`, `isSealed` and `isExtensible` answer for an object with no properties that cannot
|
||||
//! be extended.
|
||||
|
||||
use super::{ELEMENT_BYTES, arg, create_at, method, new_array};
|
||||
use crate::heap::{CONFIGURABLE, ENUMERABLE, Key, OBJECT_BYTES, OPEN, ObjId, Object, Prop, WRITABLE, key};
|
||||
use crate::interp::{Desc, Interp, JsResult};
|
||||
use crate::value::{Str, Value};
|
||||
|
||||
pub(super) fn install(interp: &mut Interp, object: ObjId) -> JsResult<()> {
|
||||
method(interp, object, "getPrototypeOf", get_prototype_of, 1)?;
|
||||
method(interp, object, "getOwnPropertyDescriptor", get_own_property_descriptor, 2)?;
|
||||
method(interp, object, "getOwnPropertyNames", get_own_property_names, 1)?;
|
||||
method(interp, object, "create", create, 2)?;
|
||||
method(interp, object, "defineProperty", define_property, 3)?;
|
||||
method(interp, object, "defineProperties", define_properties, 2)?;
|
||||
method(interp, object, "seal", seal, 1)?;
|
||||
method(interp, object, "freeze", freeze, 1)?;
|
||||
method(interp, object, "preventExtensions", prevent_extensions, 1)?;
|
||||
method(interp, object, "isSealed", is_sealed, 1)?;
|
||||
method(interp, object, "isFrozen", is_frozen, 1)?;
|
||||
method(interp, object, "isExtensible", is_extensible, 1)?;
|
||||
method(interp, object, "keys", keys, 1)
|
||||
}
|
||||
|
||||
fn require_object(interp: &mut Interp, v: &Value, function: &str) -> JsResult<ObjId> {
|
||||
match v {
|
||||
Value::Object(o) => Ok(*o),
|
||||
_ => Err(interp.type_error(&format!("Object.{function} was given a value that is not an object"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The values a descriptor holds, to keep them reachable while more JavaScript runs.
|
||||
fn desc_values(desc: &Desc) -> Vec<Value> {
|
||||
let mut out: Vec<Value> = desc.value.iter().cloned().collect();
|
||||
out.extend(desc.get.flatten().map(Value::Object));
|
||||
out.extend(desc.set.flatten().map(Value::Object));
|
||||
out
|
||||
}
|
||||
|
||||
/// ToPropertyDescriptor (section 8.10.5).
|
||||
pub(super) fn to_desc(interp: &mut Interp, v: &Value) -> JsResult<Desc> {
|
||||
let Value::Object(o) = v else { return Err(interp.type_error("a property descriptor must be an object")) };
|
||||
let o = *o;
|
||||
let mark = interp.temp_roots.len();
|
||||
let r = read_desc(interp, o);
|
||||
interp.temp_roots.truncate(mark);
|
||||
r
|
||||
}
|
||||
|
||||
fn read_desc(interp: &mut Interp, o: ObjId) -> JsResult<Desc> {
|
||||
let receiver = Value::Object(o);
|
||||
let field = |interp: &mut Interp, name: &str| -> JsResult<Option<Value>> {
|
||||
let k = key(name);
|
||||
if !interp.has_property(o, &k)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let v = interp.get_from(o, &k, &receiver)?;
|
||||
interp.temp_roots.push(v.clone());
|
||||
Ok(Some(v))
|
||||
};
|
||||
let mut desc = Desc {
|
||||
enumerable: field(interp, "enumerable")?.map(|v| v.truthy()),
|
||||
configurable: field(interp, "configurable")?.map(|v| v.truthy()),
|
||||
value: field(interp, "value")?,
|
||||
writable: field(interp, "writable")?.map(|v| v.truthy()),
|
||||
..Desc::default()
|
||||
};
|
||||
for (name, getter) in [("get", true), ("set", false)] {
|
||||
let Some(f) = field(interp, name)? else { continue };
|
||||
let f = match f {
|
||||
Value::Undefined => None,
|
||||
Value::Object(id) if interp.is_callable(&f) => Some(id),
|
||||
_ => return Err(interp.type_error(&format!("a property's {name} must be a function or undefined"))),
|
||||
};
|
||||
if getter {
|
||||
desc.get = Some(f);
|
||||
} else {
|
||||
desc.set = Some(f);
|
||||
}
|
||||
}
|
||||
if desc.is_accessor() && desc.is_data() {
|
||||
return Err(interp.type_error("a property descriptor cannot have both a value or writable and a get or set"));
|
||||
}
|
||||
Ok(desc)
|
||||
}
|
||||
|
||||
/// FromPropertyDescriptor (section 8.10.4).
|
||||
fn from_prop(interp: &mut Interp, prop: &Prop) -> JsResult<Value> {
|
||||
interp.reserve_heap(OBJECT_BYTES + 4 * ELEMENT_BYTES)?;
|
||||
let o = interp.new_object();
|
||||
let flags = prop.flags();
|
||||
match prop {
|
||||
Prop::Data { value, .. } => {
|
||||
interp.define(o, key("value"), Prop::Data { value: value.clone(), flags: OPEN })?;
|
||||
interp.define(o, key("writable"), Prop::Data { value: Value::Bool(flags & WRITABLE != 0), flags: OPEN })?;
|
||||
}
|
||||
Prop::Accessor { get, set, .. } => {
|
||||
interp.define(o, key("get"), Prop::Data { value: get.map_or(Value::Undefined, Value::Object), flags: OPEN })?;
|
||||
interp.define(o, key("set"), Prop::Data { value: set.map_or(Value::Undefined, Value::Object), flags: OPEN })?;
|
||||
}
|
||||
}
|
||||
interp.define(o, key("enumerable"), Prop::Data { value: Value::Bool(flags & ENUMERABLE != 0), flags: OPEN })?;
|
||||
interp.define(o, key("configurable"), Prop::Data { value: Value::Bool(flags & CONFIGURABLE != 0), flags: OPEN })?;
|
||||
Ok(Value::Object(o))
|
||||
}
|
||||
|
||||
/// An array of the names, kept reachable while it is filled.
|
||||
fn names_array(interp: &mut Interp, names: Vec<Key>) -> JsResult<Value> {
|
||||
let a = new_array(interp, 0)?;
|
||||
interp.rooted(&[Value::Object(a)], |interp| {
|
||||
for (i, k) in names.into_iter().enumerate() {
|
||||
interp.use_fuel()?;
|
||||
create_at(interp, a, i as f64, Value::String(Str::from_rc(k)))?;
|
||||
}
|
||||
Ok(Value::Object(a))
|
||||
})
|
||||
}
|
||||
|
||||
/// `Object.getPrototypeOf` (section 15.2.3.2).
|
||||
fn get_prototype_of(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = interp.to_object(&arg(args, 0))?;
|
||||
Ok(interp.heap.object(o).and_then(|o| o.proto).map_or(Value::Null, Value::Object))
|
||||
}
|
||||
|
||||
/// `Object.getOwnPropertyDescriptor` (section 15.2.3.3).
|
||||
fn get_own_property_descriptor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = interp.to_object(&arg(args, 0))?;
|
||||
interp.rooted(&[Value::Object(o)], |interp| {
|
||||
let k = interp.to_key(&arg(args, 1))?;
|
||||
match interp.get_own_property(o, &k)? {
|
||||
Some(p) => from_prop(interp, &p),
|
||||
None => Ok(Value::Undefined),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `Object.getOwnPropertyNames` (section 15.2.3.4).
|
||||
fn get_own_property_names(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = interp.to_object(&arg(args, 0))?;
|
||||
interp.rooted(&[Value::Object(o)], |interp| {
|
||||
let names = interp.own_keys(o)?;
|
||||
names_array(interp, names)
|
||||
})
|
||||
}
|
||||
|
||||
/// `Object.create` (section 15.2.3.5).
|
||||
fn create(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let proto = match arg(args, 0) {
|
||||
Value::Object(p) => Some(p),
|
||||
Value::Null => None,
|
||||
_ => return Err(interp.type_error("Object.create's prototype must be an object or null")),
|
||||
};
|
||||
interp.reserve_heap(OBJECT_BYTES)?;
|
||||
let o = interp.heap.alloc_object(Object::new(crate::heap::Class::Ordinary, proto));
|
||||
let props = arg(args, 1);
|
||||
if matches!(props, Value::Undefined) {
|
||||
return Ok(Value::Object(o));
|
||||
}
|
||||
interp.rooted(&[Value::Object(o)], |interp| define_all(interp, o, &props))?;
|
||||
Ok(Value::Object(o))
|
||||
}
|
||||
|
||||
/// `Object.defineProperty` (section 15.2.3.6).
|
||||
fn define_property(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = require_object(interp, &arg(args, 0), "defineProperty")?;
|
||||
let k = interp.to_key(&arg(args, 1))?;
|
||||
let desc = to_desc(interp, &arg(args, 2))?;
|
||||
let roots = desc_values(&desc);
|
||||
interp.rooted(&roots, |interp| interp.define_own_property(o, k, &desc, true))?;
|
||||
Ok(Value::Object(o))
|
||||
}
|
||||
|
||||
/// `Object.defineProperties` (section 15.2.3.7).
|
||||
fn define_properties(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = require_object(interp, &arg(args, 0), "defineProperties")?;
|
||||
define_all(interp, o, &arg(args, 1))?;
|
||||
Ok(Value::Object(o))
|
||||
}
|
||||
|
||||
/// Defines on `o` each own enumerable property of `props`, reading every descriptor before
|
||||
/// defining any.
|
||||
fn define_all(interp: &mut Interp, o: ObjId, props: &Value) -> JsResult<()> {
|
||||
let p = interp.to_object(props)?;
|
||||
let mark = interp.temp_roots.len();
|
||||
interp.temp_roots.push(Value::Object(p));
|
||||
let r = (|| {
|
||||
let mut descs = Vec::new();
|
||||
for k in interp.own_keys(p)? {
|
||||
interp.use_fuel()?;
|
||||
if !interp.get_own_property(p, &k)?.is_some_and(|prop| prop.enumerable()) {
|
||||
continue;
|
||||
}
|
||||
let v = interp.get_from(p, &k, &Value::Object(p))?;
|
||||
interp.temp_roots.push(v.clone());
|
||||
let desc = to_desc(interp, &v)?;
|
||||
interp.temp_roots.extend(desc_values(&desc));
|
||||
descs.push((k, desc));
|
||||
}
|
||||
for (k, desc) in descs {
|
||||
interp.use_fuel()?;
|
||||
interp.define_own_property(o, k, &desc, true)?;
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
interp.temp_roots.truncate(mark);
|
||||
r
|
||||
}
|
||||
|
||||
/// Makes every own property non-configurable, and with `freeze` every data property read-only,
|
||||
/// then makes the object non-extensible (sections 15.2.3.8 and 15.2.3.9).
|
||||
fn lock(interp: &mut Interp, v: Value, freeze: bool) -> JsResult<Value> {
|
||||
let Value::Object(o) = v else { return Ok(v) };
|
||||
for k in interp.own_keys(o)? {
|
||||
interp.use_fuel()?;
|
||||
let Some(prop) = interp.get_own_property(o, &k)? else { continue };
|
||||
let mut desc = match &prop {
|
||||
Prop::Data { value, flags } => Desc::data(value.clone(), *flags),
|
||||
Prop::Accessor { get, set, flags } => Desc {
|
||||
get: Some(*get),
|
||||
set: Some(*set),
|
||||
enumerable: Some(flags & ENUMERABLE != 0),
|
||||
configurable: Some(flags & CONFIGURABLE != 0),
|
||||
..Desc::default()
|
||||
},
|
||||
};
|
||||
desc.configurable = Some(false);
|
||||
if freeze && desc.is_data() {
|
||||
desc.writable = Some(false);
|
||||
}
|
||||
interp.define_own_property(o, k, &desc, true)?;
|
||||
}
|
||||
if let Some(obj) = interp.heap.object_mut(o) {
|
||||
obj.extensible = false;
|
||||
}
|
||||
Ok(Value::Object(o))
|
||||
}
|
||||
|
||||
fn seal(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
lock(interp, arg(args, 0), false)
|
||||
}
|
||||
|
||||
fn freeze(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
lock(interp, arg(args, 0), true)
|
||||
}
|
||||
|
||||
/// `Object.preventExtensions` (section 15.2.3.10).
|
||||
fn prevent_extensions(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let v = arg(args, 0);
|
||||
if let Value::Object(o) = v
|
||||
&& let Some(obj) = interp.heap.object_mut(o)
|
||||
{
|
||||
obj.extensible = false;
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Whether no own property is configurable (and with `frozen`, no data property is writable)
|
||||
/// and the object is not extensible (sections 15.2.3.11 and 15.2.3.12).
|
||||
fn locked(interp: &mut Interp, v: &Value, frozen: bool) -> JsResult<Value> {
|
||||
let Value::Object(o) = v else { return Ok(Value::Bool(true)) };
|
||||
let o = *o;
|
||||
for k in interp.own_keys(o)? {
|
||||
interp.use_fuel()?;
|
||||
let Some(prop) = interp.get_own_property(o, &k)? else { continue };
|
||||
if prop.configurable() || (frozen && matches!(prop, Prop::Data { flags, .. } if flags & WRITABLE != 0)) {
|
||||
return Ok(Value::Bool(false));
|
||||
}
|
||||
}
|
||||
Ok(Value::Bool(!interp.heap.object(o).is_some_and(|o| o.extensible)))
|
||||
}
|
||||
|
||||
fn is_sealed(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
locked(interp, &arg(args, 0), false)
|
||||
}
|
||||
|
||||
fn is_frozen(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
locked(interp, &arg(args, 0), true)
|
||||
}
|
||||
|
||||
/// `Object.isExtensible` (section 15.2.3.13).
|
||||
fn is_extensible(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let Value::Object(o) = arg(args, 0) else { return Ok(Value::Bool(false)) };
|
||||
Ok(Value::Bool(interp.heap.object(o).is_some_and(|o| o.extensible)))
|
||||
}
|
||||
|
||||
/// `Object.keys` (section 15.2.3.14).
|
||||
fn keys(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = interp.to_object(&arg(args, 0))?;
|
||||
interp.rooted(&[Value::Object(o)], |interp| {
|
||||
let mut names = Vec::new();
|
||||
for k in interp.own_keys(o)? {
|
||||
interp.use_fuel()?;
|
||||
if interp.get_own_property(o, &k)?.is_some_and(|p| p.enumerable()) {
|
||||
names.push(k);
|
||||
}
|
||||
}
|
||||
names_array(interp, names)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::{JsValue, LimitKind, Limits, Options, Realm, RunError, RunErrorKind};
|
||||
|
||||
fn run(src: &str) -> JsValue {
|
||||
match Realm::new().run(src) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{src}\n{e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn num(src: &str) -> f64 {
|
||||
match run(src) {
|
||||
JsValue::Number(n) => n,
|
||||
other => panic!("{src}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn string(src: &str) -> String {
|
||||
match run(src) {
|
||||
JsValue::String(s) => s,
|
||||
other => panic!("{src}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn truth(src: &str) {
|
||||
assert_eq!(run(src), JsValue::Bool(true), "{src}");
|
||||
}
|
||||
|
||||
/// The run throws an error whose message starts with `name`.
|
||||
fn throws(src: &str, name: &str) {
|
||||
match Realm::new().run(src) {
|
||||
Err(RunError { kind: RunErrorKind::Thrown, message, .. }) => assert!(message.starts_with(name), "{src}: {message}"),
|
||||
other => panic!("{src}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn small(limits: Limits) -> Realm {
|
||||
Realm::with_options(Options::default(), limits)
|
||||
}
|
||||
|
||||
fn limit(realm: &mut Realm, src: &str) -> LimitKind {
|
||||
match realm.run(src) {
|
||||
Err(RunError { kind: RunErrorKind::Limit(k), .. }) => k,
|
||||
other => panic!("{src}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Object ----
|
||||
|
||||
#[test]
|
||||
fn define_property_and_descriptors_follow_section_8_12_9() {
|
||||
truth(
|
||||
"var o = {}; Object.defineProperty(o, 'x', {value: 1}); var d = Object.getOwnPropertyDescriptor(o, 'x'); \
|
||||
d.value === 1 && d.writable === false && d.enumerable === false && d.configurable === false",
|
||||
);
|
||||
truth("var o = {}; Object.defineProperty(o, 'x', {value: 1}); o.x = 2; o.x === 1 && Object.keys(o).length === 0");
|
||||
throws("'use strict'; var o = {}; Object.defineProperty(o, 'x', {value: 1}); o.x = 2;", "TypeError");
|
||||
throws("var o = {}; Object.defineProperty(o, 'x', {value: 1}); Object.defineProperty(o, 'x', {value: 2});", "TypeError");
|
||||
truth("var o = {}; Object.defineProperty(o, 'x', {value: NaN}); Object.defineProperty(o, 'x', {value: NaN}); o.x !== o.x");
|
||||
throws("var o = {}; Object.defineProperty(o, 'x', {value: 0}); Object.defineProperty(o, 'x', {value: -0});", "TypeError");
|
||||
truth(
|
||||
"var o = {}, log = []; Object.defineProperty(o, 'x', {get: function () { return 7; }, set: function (v) { log.push(v); }, enumerable: true}); \
|
||||
o.x = 3; o.x === 7 && log[0] === 3 && Object.keys(o)[0] === 'x' && typeof Object.getOwnPropertyDescriptor(o, 'x').get === 'function'",
|
||||
);
|
||||
throws("Object.defineProperty({}, 'x', {get: function () {}, value: 1})", "TypeError");
|
||||
throws("Object.defineProperty({}, 'x', {get: 1})", "TypeError");
|
||||
throws("Object.defineProperty(1, 'x', {})", "TypeError");
|
||||
// A configurable data property becomes an accessor and keeps its enumerability.
|
||||
truth(
|
||||
"var o = {x: 1}; Object.defineProperty(o, 'x', {get: function () { return 2; }}); \
|
||||
var d = Object.getOwnPropertyDescriptor(o, 'x'); o.x === 2 && d.enumerable && d.configurable && d.set === undefined",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_length_and_index_definitions_follow_section_15_4_5_1() {
|
||||
truth("var a = [1, 2, 3]; Object.defineProperty(a, 'length', {value: 1}); a.length === 1 && !(1 in a)");
|
||||
truth(
|
||||
"var a = [1, 2, 3]; Object.defineProperty(a, '1', {value: 2, configurable: false}); \
|
||||
var threw = false; try { Object.defineProperty(a, 'length', {value: 0}); } catch (e) { threw = e instanceof TypeError; } \
|
||||
threw && a.length === 2 && a[1] === 2 && !(2 in a)",
|
||||
);
|
||||
truth("var a = [1, 2, 3]; Object.defineProperty(a, '1', {configurable: false}); a.length = 0; a.length === 2");
|
||||
truth("var a = [1]; Object.defineProperty(a, 'length', {writable: false}); a[5] = 1; a.length === 1 && !(5 in a)");
|
||||
throws("var a = [1]; Object.defineProperty(a, 'length', {writable: false}); Object.defineProperty(a, '3', {value: 1});", "TypeError");
|
||||
throws("Object.defineProperty([], 'length', {value: -1})", "RangeError");
|
||||
truth("var a = []; Object.defineProperty(a, '4', {value: 1, enumerable: true, writable: true, configurable: true}); a.length === 5");
|
||||
throws("var a = Object.freeze([1, 2]); a.push(3);", "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_functions_list_create_and_lock_objects() {
|
||||
truth(
|
||||
"var o = Object.create({inherited: 1}, {own: {value: 2, enumerable: true}}); o.inherited === 1 && o.own === 2 && Object.keys(o).join() === 'own'",
|
||||
);
|
||||
truth("Object.getPrototypeOf(Object.create(null)) === null && Object.getPrototypeOf([]) === Array.prototype");
|
||||
assert_eq!(string("Object.getOwnPropertyNames(Object('ab')).join()"), "0,1,length");
|
||||
assert_eq!(string("var a = [5]; a.k = 1; Object.getOwnPropertyNames(a).join()"), "length,0,k");
|
||||
// Object.keys lists names in the order for-in visits them (section 15.2.3.14).
|
||||
assert_eq!(string("var o = {b: 1, a: 2, 3: 3}, s = ''; for (var k in o) s += k; Object.keys(o).join('') === s ? s : 'differs'"), "ba3");
|
||||
truth("var o = Object.seal({x: 1}); o.x = 2; delete o.x; o.y = 1; o.x === 2 && o.y === undefined && Object.isSealed(o) && !Object.isFrozen(o)");
|
||||
truth("var o = Object.freeze({x: 1}); o.x = 2; o.x === 1 && Object.isFrozen(o) && !Object.isExtensible(o)");
|
||||
truth("var o = Object.preventExtensions({x: 1}); o.y = 1; o.y === undefined && Object.isSealed(o) === false && Object.isExtensible(o) === false");
|
||||
truth("Object.isFrozen(Object.preventExtensions({})) && Object.isFrozen(1) && Object.freeze(1) === 1 && Object.keys('ab').join() === '0,1'");
|
||||
truth("var o = Object.defineProperties({}, {a: {value: 1, enumerable: true}, b: {get: function () { return 2; }}}); o.a + o.b === 3");
|
||||
// A frozen arguments object no longer follows its parameters.
|
||||
truth("function f(a) { Object.freeze(arguments); a = 2; return arguments[0]; } f(1) === 1");
|
||||
truth("function f(a) { Object.defineProperty(arguments, '0', {value: 5}); return a; } f(1) === 5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prototype_methods_are_writable_so_scripts_can_replace_them() {
|
||||
truth("var d = Object.getOwnPropertyDescriptor(Array.prototype, 'map'); d.writable && d.configurable && !d.enumerable");
|
||||
truth("Array.prototype.first = function () { return this[0]; }; [4].first() === 4");
|
||||
truth(
|
||||
"var old = Array.prototype.join; Array.prototype.join = function () { return 'x'; }; var r = [1].toString(); Array.prototype.join = old; r === 'x'",
|
||||
);
|
||||
truth("var d = Object.getOwnPropertyDescriptor(Array, 'prototype'); !d.writable && !d.enumerable && !d.configurable");
|
||||
truth("var d = Object.getOwnPropertyDescriptor(Math, 'PI'); !d.writable && !d.enumerable && !d.configurable");
|
||||
}
|
||||
|
||||
// ---- Function ----
|
||||
|
||||
#[test]
|
||||
fn bind_fixes_this_and_leading_arguments() {
|
||||
assert_eq!(num("function f(a, b, c) { return this.k + a + b + c; } var g = f.bind({k: 1}, 2); g(3, 4)"), 10.0);
|
||||
assert_eq!(num("function f(a, b, c) {} f.bind(null, 1).length"), 2.0);
|
||||
assert_eq!(num("function f(a) {} f.bind(null, 1, 2, 3).length"), 0.0);
|
||||
truth("function P(x, y) { this.s = x + y; } var B = P.bind({ignored: 1}, 2); var p = new B(3); p.s === 5 && p instanceof P && p instanceof B");
|
||||
truth("var g = function () { return this; }.bind(7); typeof g() === 'object' && g().constructor === undefined || true");
|
||||
truth("var g = Math.max.bind(null, 5); g(1) === 5 && g(9) === 9");
|
||||
throws("Function.prototype.bind.call({})", "TypeError");
|
||||
throws("'use strict'; function f() {} f.bind(null).caller", "TypeError");
|
||||
truth("function f() { return arguments.length; } f.bind(null, 1).bind(null, 2)(3) === 3");
|
||||
truth("Object.prototype.toString.call(function () {}.bind()) === '[object Function]'");
|
||||
}
|
||||
|
||||
// ---- Array ----
|
||||
|
||||
#[test]
|
||||
fn the_array_constructor_and_is_array() {
|
||||
truth("var a = new Array(3); a.length === 3 && !(0 in a) && Array(2).length === 2");
|
||||
truth("var a = new Array(1, 2); a.length === 2 && a[1] === 2 && new Array('3').length === 1");
|
||||
throws("new Array(-1)", "RangeError");
|
||||
throws("new Array(1.5)", "RangeError");
|
||||
truth("new Array(4294967295).length === 4294967295");
|
||||
truth("Array.isArray([]) && !Array.isArray({length: 0}) && !Array.isArray(Array.prototype.slice) && Array.isArray(Array.prototype)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn methods_that_change_the_array() {
|
||||
assert_eq!(string("var a = [1, 2]; var n = a.push(3, 4); n + ':' + a.join()"), "4:1,2,3,4");
|
||||
assert_eq!(string("var a = [1, 2, 3]; a.pop() + ':' + a.join() + ':' + [].pop()"), "3:1,2:undefined");
|
||||
assert_eq!(string("var a = [1, 2, 3]; a.shift() + ':' + a.join()"), "1:2,3");
|
||||
assert_eq!(string("var a = [3]; a.unshift(1, 2) + ':' + a.join()"), "3:1,2,3");
|
||||
assert_eq!(string("[1, , 3].reverse().join()"), "3,,1");
|
||||
truth("var a = [1, , 3, , ]; a.reverse(); !(0 in a) && a[1] === 3 && !(2 in a) && a[3] === 1 && a.length === 4");
|
||||
assert_eq!(string("var a = [1, 2, 3, 4, 5]; var r = a.splice(1, 2, 'x'); r.join() + ':' + a.join()"), "2,3:1,x,4,5");
|
||||
assert_eq!(string("var a = [1, 2, 3]; var r = a.splice(1, 0, 'x', 'y'); r.length + ':' + a.join()"), "0:1,x,y,2,3");
|
||||
assert_eq!(string("var a = [1, 2, 3]; a.splice(-2).join() + ':' + a.join()"), "2,3:1");
|
||||
assert_eq!(string("[1, 2, 3, 4].slice(1, -1).join() + ':' + [1, 2].slice(5).length"), "2,3:0");
|
||||
assert_eq!(string("[1].concat([2, [3]], 4).length + ':' + [1].concat([2, , 3]).join()"), "4:1,2,,3");
|
||||
// The methods are generic over array-like objects.
|
||||
assert_eq!(
|
||||
string("var o = {length: 2, 0: 'a', 1: 'b'}; Array.prototype.push.call(o, 'c'); o.length + Array.prototype.join.call(o, '-')"),
|
||||
"3a-b-c"
|
||||
);
|
||||
assert_eq!(num("var o = {length: '2', 0: 1, 1: 2}; Array.prototype.reverse.call(o); o[0] * 10 + o[1]"), 21.0);
|
||||
throws("var a = [1]; Object.defineProperty(a, 'length', {writable: false}); a.push(2)", "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn searching_and_iteration_methods() {
|
||||
assert_eq!(num("[1, 2, 3, 2].indexOf(2) * 10 + [1, 2, 3, 2].lastIndexOf(2)"), 13.0);
|
||||
assert_eq!(num("[1, 2, 3].indexOf(1, -2) + [NaN].indexOf(NaN) + [1, 2].lastIndexOf(1, -3)"), -3.0);
|
||||
assert_eq!(num("[1, 2, 3].indexOf(3, -1) * 10 + [1, , 3].indexOf(undefined) * -1"), 21.0);
|
||||
truth("[2, 4].every(function (x) { return x % 2 === 0; }) && ![2, 3].every(function (x) { return x % 2 === 0; })");
|
||||
truth("[1, 2].some(function (x) { return x > 1; }) && ![].some(function () { return true; })");
|
||||
assert_eq!(string("var s = ''; [1, , 3].forEach(function (x, i, a) { s += x + '@' + i + (a.length); }); s"), "1@033@23");
|
||||
assert_eq!(string("[1, 2, 3].map(function (x) { return x * this.k; }, {k: 2}).join()"), "2,4,6");
|
||||
truth("var m = [1, , 3].map(function (x) { return x; }); m.length === 3 && !(1 in m)");
|
||||
assert_eq!(string("[1, 2, 3, 4].filter(function (x) { return x % 2; }).join()"), "1,3");
|
||||
assert_eq!(num("[1, 2, 3].reduce(function (a, b) { return a + b; })"), 6.0);
|
||||
assert_eq!(string("['a', 'b', 'c'].reduceRight(function (a, b) { return a + b; }, '')"), "cba");
|
||||
assert_eq!(num("[, 5, ].reduce(function (a, b) { return a + b; })"), 5.0);
|
||||
throws("[].reduce(function () {})", "TypeError");
|
||||
throws("[1].forEach(1)", "TypeError");
|
||||
// Elements appended during iteration are not visited; elements deleted before they are
|
||||
// reached are skipped.
|
||||
assert_eq!(num("var a = [1, 2, 3], n = 0; a.forEach(function (x, i) { n++; a.push(9); if (i === 0) delete a[1]; }); n"), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_orders_values_with_undefined_and_holes_last() {
|
||||
assert_eq!(string("[3, 1, 10, 2].sort().join()"), "1,10,2,3");
|
||||
assert_eq!(string("[3, 1, 10, 2].sort(function (a, b) { return a - b; }).join()"), "1,2,3,10");
|
||||
truth("var a = [3, undefined, , 1]; a.sort(); a[0] === 1 && a[1] === 3 && a[2] === undefined && 2 in a && !(3 in a) && a.length === 4");
|
||||
// The sort is stable.
|
||||
assert_eq!(
|
||||
string(
|
||||
"[{k: 1, v: 'a'}, {k: 0, v: 'b'}, {k: 1, v: 'c'}, {k: 0, v: 'd'}].sort(function (x, y) { return x.k - y.k; }).map(function (x) { return x.v; }).join('')"
|
||||
),
|
||||
"bdac"
|
||||
);
|
||||
// A comparison that throws stops the sort with its exception.
|
||||
truth("var a = [2, 1, 3]; var e; try { a.sort(function () { throw 'stop'; }); } catch (x) { e = x; } e === 'stop'");
|
||||
// A comparison that changes the array or answers inconsistently leaves an array of the same
|
||||
// length and no exception.
|
||||
truth("var a = [5, 4, 3, 2, 1]; a.sort(function (x, y) { a.length = 1; a[7] = 0; return Math.random() - 0.5; }); a.length >= 5");
|
||||
truth("var a = []; for (var i = 0; i < 100; i++) a[i] = (i * 37) % 100; a.sort(function () { return 1; }); a.length === 100");
|
||||
throws("[1, 2].sort(1)", "TypeError");
|
||||
assert_eq!(string("Array.prototype.sort.call({length: 3, 0: 'c', 2: 'a'}) && 'ok'"), "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_and_to_string_are_cycle_safe() {
|
||||
assert_eq!(string("var a = [1, 2]; a.push(a); a.join()"), "1,2,");
|
||||
assert_eq!(string("var a = [1]; var b = [a, 2]; a.push(b); String2(a); function String2(x) { return '' + x; }"), "1,,2");
|
||||
assert_eq!(string("var a = [1, [2, [3]]]; a.toString()"), "1,2,3");
|
||||
assert_eq!(string("[null, undefined, 0].join('-')"), "--0");
|
||||
assert_eq!(string("Array.prototype.toString.call({join: 1})"), "[object Object]");
|
||||
assert_eq!(string("var a = [{toLocaleString: function () { return 'L'; }}, null]; a.push(a); a.toLocaleString()"), "L,,");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparse_arrays_of_any_length_cost_only_their_elements() {
|
||||
// Each would visit four billion indices one by one; with 200,000 units of fuel each must
|
||||
// finish by visiting only the indices that exist.
|
||||
let mut realm = small(Limits { fuel: 200_000, ..Limits::default() });
|
||||
let cases = [
|
||||
("var a = new Array(4294967295); a.join('')", JsValue::String(String::new())),
|
||||
("var a = []; a[4294967294] = 'z'; a[3] = 'y'; a.join('').length", JsValue::Number(2.0)),
|
||||
("var a = []; a[4294967294] = 2; a[7] = 1; a.sort(); a[0] * 10 + a[1] + (a.length === 4294967295 ? 0 : 100)", JsValue::Number(12.0)),
|
||||
("var a = []; a[4294967294] = 1; a.indexOf(1)", JsValue::Number(4_294_967_294.0)),
|
||||
("var a = []; a[4294967294] = 1; a.lastIndexOf(1, -1) + a.lastIndexOf(2)", JsValue::Number(4_294_967_293.0)),
|
||||
("var n = 0; var a = []; a[4294967294] = 1; a[0] = 1; a.forEach(function () { n++; }); n", JsValue::Number(2.0)),
|
||||
("var a = []; a[4294967294] = 1; a.map(function (x) { return x + 1; })[4294967294]", JsValue::Number(2.0)),
|
||||
("var a = []; a[4000000000] = 1; a[1] = 2; a.reverse(); a[0] * 10 + a[3999999999]", JsValue::Number(12.0)),
|
||||
("var a = []; a[4000000000] = 1; a.shift(); a[3999999999] + (a.length === 4000000000 ? 0 : 100)", JsValue::Number(1.0)),
|
||||
("var a = []; a[4000000000] = 1; a.unshift(0); a[4000000001] + a[0]", JsValue::Number(1.0)),
|
||||
("var a = []; a[4000000000] = 1; a[1] = 2; var r = a.splice(1, 1); r[0] + a[3999999999]", JsValue::Number(3.0)),
|
||||
("var a = []; a[4000000000] = 1; a.splice(1, 0, 'x', 'y'); a[4000000002]", JsValue::Number(1.0)),
|
||||
("var a = []; a[4000000000] = 1; a.slice(1)[3999999999]", JsValue::Number(1.0)),
|
||||
("var a = []; a[4000000000] = 1; [0].concat(a).length", JsValue::Number(4_000_000_002.0)),
|
||||
("var a = []; a[4000000000] = 2; a.reduce(function (x, y) { return x + y; }, 1)", JsValue::Number(3.0)),
|
||||
];
|
||||
for (src, want) in cases {
|
||||
assert_eq!(realm.run(src), Ok(want), "{src}");
|
||||
}
|
||||
// An index added while a method runs is visited.
|
||||
let src = "var a = []; a[4000000000] = 1; var seen = []; a.forEach(function (x, i) { seen.push(i); if (i === 4000000000) {} }); \
|
||||
var b = []; b[1] = 1; b[3000000000] = 1; var n = 0; b.forEach(function (x, i) { n++; if (n === 1) { b[2000000000] = 2; b[3500000000] = 2; } }); n + seen.length";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(4.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn built_ins_end_with_a_limit_instead_of_running_on() {
|
||||
let limits = Limits { heap_bytes: 8 << 20, fuel: 5_000_000, ..Limits::default() };
|
||||
assert_eq!(limit(&mut small(limits.clone()), "new Array(4294967295).join('x')"), LimitKind::Heap);
|
||||
assert_eq!(limit(&mut small(limits.clone()), "JSON.stringify(new Array(4294967295))"), LimitKind::Heap);
|
||||
assert_eq!(limit(&mut small(limits.clone()), "var a = [1]; for (;;) a = a.concat(a);"), LimitKind::Heap);
|
||||
assert_eq!(
|
||||
limit(&mut small(limits.clone()), "var o = {}; for (var i = 0; i < 20; i++) o = {a: o, b: o, c: o}; JSON.stringify(o)"),
|
||||
LimitKind::Heap
|
||||
);
|
||||
assert_eq!(
|
||||
limit(&mut small(limits.clone()), "var o = []; for (var i = 0; i < 20000; i++) o = [o]; for (;;) JSON.stringify(o);"),
|
||||
LimitKind::Fuel
|
||||
);
|
||||
let mut realm = small(Limits { fuel: 100_000, ..Limits::default() });
|
||||
assert_eq!(
|
||||
limit(&mut realm, "var a = []; for (var i = 0; i < 20000; i++) a[i * 1000] = i; a.length = 4294967295; a.sort(); a.sort();"),
|
||||
LimitKind::Fuel
|
||||
);
|
||||
assert_eq!(limit(&mut realm, "var a = [1, 2, 3, 4, 5, 6, 7, 8]; for (var i = 0; i < 14; i++) a = a.concat(a); a.join('')"), LimitKind::Fuel);
|
||||
let mut realm = small(Limits { fuel: 1_000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "var s = 'x'; for (var i = 0; i < 20; i++) s += s; JSON.parse('\"' + s + '\"')"), LimitKind::Fuel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_built_ins_hold_survive_collections() {
|
||||
// A small heap makes the callbacks' garbage start collections while the results are built.
|
||||
let mut realm = small(Limits { heap_bytes: 2 << 20, ..Limits::default() });
|
||||
let src = "function junk() { var t = []; for (var j = 0; j < 200; j++) t[j] = {j: j}; return t.length; } \
|
||||
var src = []; for (var i = 0; i < 300; i++) src[i] = i; \
|
||||
var m = src.map(function (x) { junk(); return {v: x}; }); \
|
||||
var f = src.filter(function (x) { junk(); return x % 2; }); \
|
||||
var r = src.reduce(function (acc, x) { junk(); return {sum: acc.sum + x}; }, {sum: 0}); \
|
||||
var s = src.slice().sort(function (a, b) { junk(); return b - a; }); \
|
||||
var j = JSON.parse(JSON.stringify(m), function (k, v) { junk(); return v; }); \
|
||||
m[299].v + f.length + r.sum + s[0] + j[299].v + j.length";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(299.0 + 150.0 + 44_850.0 + 299.0 + 299.0 + 300.0)));
|
||||
assert!(realm.interp().heap.collections > 0);
|
||||
// The callback is reachable only through the list `apply` read once the list is emptied.
|
||||
let src = "var list = [function (x) { list.length = 0; junk(); return {v: x}; }]; \
|
||||
var r = Array.prototype.map.apply(src, list); \
|
||||
var g = Array.prototype.map.bind(src, function (x) { junk(); return {v: x}; }); g = g(); \
|
||||
r[299].v + g[299].v";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(598.0)));
|
||||
}
|
||||
|
||||
// ---- errors ----
|
||||
|
||||
#[test]
|
||||
fn error_objects_have_names_messages_and_to_string() {
|
||||
assert_eq!(string("new TypeError('bad').toString()"), "TypeError: bad");
|
||||
assert_eq!(string("RangeError('r').name + Error().message + (new Error(undefined).hasOwnProperty('message') ? 1 : 0)"), "RangeError0");
|
||||
truth("var e = new SyntaxError('s'); e instanceof SyntaxError && e instanceof Error && Object.prototype.toString.call(e) === '[object Error]'");
|
||||
assert_eq!(string("var e = new Error('m'); e.name = ''; e.toString()"), "m");
|
||||
assert_eq!(string("Error.prototype.toString.call({name: 'N'})"), "N");
|
||||
truth("URIError.prototype.name === 'URIError' && Object.getPrototypeOf(EvalError.prototype) === Error.prototype && Error.length === 1");
|
||||
}
|
||||
|
||||
// ---- Math ----
|
||||
|
||||
#[test]
|
||||
fn math_functions_follow_the_special_cases() {
|
||||
truth(
|
||||
"Math.max() === -Infinity && Math.min() === Infinity && isNaN(Math.max(1, NaN, 3)) && 1 / Math.max(-0, 0) === Infinity && 1 / Math.min(0, -0) === -Infinity",
|
||||
);
|
||||
truth("Math.round(2.5) === 3 && Math.round(-2.5) === -2 && 1 / Math.round(-0.4) === -Infinity && Math.round(0.49999999999999994) === 0");
|
||||
truth("isNaN(Math.pow(1, NaN)) && isNaN(Math.pow(-1, Infinity)) && Math.pow(NaN, 0) === 1 && Math.pow(2, -1) === 0.5");
|
||||
truth("Math.abs(-3) === 3 && Math.floor(-1.5) === -2 && Math.ceil(-1.5) === -1 && 1 / Math.ceil(-0.5) === -Infinity");
|
||||
truth("Math.sqrt(-1) !== Math.sqrt(-1) && Math.atan2(0, -0) === Math.PI && Math.exp(0) === 1 && Math.log(Math.E) === 1");
|
||||
truth("Math.abs(Math.sin(Math.PI)) < 1e-15 && Math.cos(0) === 1 && Math.tan(0) === 0 && Math.asin(1) === Math.PI / 2");
|
||||
truth("var ok = true; for (var i = 0; i < 1000; i++) { var r = Math.random(); if (!(r >= 0 && r < 1)) ok = false; } ok");
|
||||
truth("Math.SQRT2 * Math.SQRT1_2 > 0.9999 && Math.LN2 > 0.69 && Math.LOG10E > 0.43 && Object.prototype.toString.call(Math) === '[object Math]'");
|
||||
truth("var n = 0; Math.max({valueOf: function () { n++; return NaN; }}, {valueOf: function () { n++; return 1; }}); n === 2");
|
||||
}
|
||||
|
||||
// ---- JSON ----
|
||||
|
||||
#[test]
|
||||
fn json_parse_reads_the_grammar_of_section_15_12_1() {
|
||||
assert_eq!(num("var v = JSON.parse(' {\"a\": [1, -2.5e1, true, null, \"x\\\\u0041\\\\n\"], \"b\": {}} '); v.a[1] + v.a.length"), -20.0);
|
||||
assert_eq!(string("JSON.parse('\"\\\\ud800\"').charCodeAt === undefined ? 'no string methods' : ''"), "no string methods");
|
||||
truth("JSON.parse('[]').length === 0 && JSON.parse('0') === 0 && 1 / JSON.parse('-0') === -Infinity && JSON.parse('{\"a\":1,\"a\":2}').a === 2");
|
||||
for bad in ["'{a: 1}'", "'[1,]'", "'01'", "'1.'", "'.5'", "'\"\\t\"'", "\"'x'\"", "'[1] 2'", "''", "'+1'", "'\"\\\\x\"'", "'{\"a\" 1}'"] {
|
||||
throws(&format!("JSON.parse({bad})"), "SyntaxError");
|
||||
}
|
||||
// The reviver sees every value bottom-up and can delete or replace it.
|
||||
assert_eq!(
|
||||
string(
|
||||
"var seen = []; var v = JSON.parse('{\"a\": [1, 2], \"b\": 3}', function (k, v) { seen.push(k); return k === 'b' ? undefined : v; }); seen.join() + ':' + ('b' in v)"
|
||||
),
|
||||
"0,1,a,b,:false"
|
||||
);
|
||||
assert_eq!(num("JSON.parse('[1, 2]', function (k, v) { return typeof v === 'number' ? v * 10 : v; })[1]"), 20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_stringify_writes_values_with_replacers_and_indentation() {
|
||||
assert_eq!(
|
||||
string("JSON.stringify({a: [1, 'x', null, undefined, function () {}], b: undefined, c: true})"),
|
||||
"{\"a\":[1,\"x\",null,null,null],\"c\":true}"
|
||||
);
|
||||
assert_eq!(string("JSON.stringify('\\u0001\"\\\\\\n')"), "\"\\u0001\\\"\\\\\\n\"");
|
||||
assert_eq!(string("JSON.stringify([NaN, Infinity, -0, 1e21])"), "[null,null,0,1e+21]");
|
||||
assert_eq!(run("JSON.stringify(undefined)"), JsValue::Undefined);
|
||||
assert_eq!(run("JSON.stringify(function () {})"), JsValue::Undefined);
|
||||
assert_eq!(string("JSON.stringify({a: 1, b: [1, 2], c: {}}, null, 2)"), "{\n \"a\": 1,\n \"b\": [\n 1,\n 2\n ],\n \"c\": {}\n}");
|
||||
assert_eq!(string("JSON.stringify([1], null, 'abcdefghijkl')"), "[\nabcdefghij1\n]");
|
||||
assert_eq!(string("JSON.stringify({a: 1, b: 2, c: 3}, ['c', 'a', 'c', 1])"), "{\"c\":3,\"a\":1}");
|
||||
assert_eq!(string("JSON.stringify({a: 1, b: 'x'}, function (k, v) { return typeof v === 'number' ? v + 1 : v; })"), "{\"a\":2,\"b\":\"x\"}");
|
||||
assert_eq!(string("JSON.stringify({toJSON: function (k) { return 'k=' + k; }})"), "\"k=\"");
|
||||
assert_eq!(string("JSON.stringify({d: {toJSON: function (k) { return k; }}})"), "{\"d\":\"d\"}");
|
||||
assert_eq!(string("JSON.stringify([Object(false), Object(true)])"), "[false,true]");
|
||||
assert_eq!(string("JSON.stringify({}, null, 4) + JSON.stringify([], null, 4)"), "{}[]");
|
||||
throws("var a = []; a.push(a); JSON.stringify(a)", "TypeError");
|
||||
throws("var o = {}; o.x = {y: o}; JSON.stringify(o)", "TypeError");
|
||||
// The same object twice, not nested in itself, is not a cycle.
|
||||
assert_eq!(string("var x = {}; JSON.stringify([x, x])"), "[{},{}]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_json_nesting_needs_no_native_stack() {
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(256 << 10)
|
||||
.spawn(|| {
|
||||
let mut realm = Realm::new();
|
||||
realm.run(
|
||||
"var depth = 100000; var s = ''; for (var i = 0; i < depth; i++) s += '['; for (i = 0; i < depth; i++) s += ']'; \
|
||||
var v = JSON.parse(s); var back = JSON.stringify(v); var n = 0; JSON.parse(s, function (k, v) { n++; return v; }); \
|
||||
back.length === 2 * depth && n === depth",
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(handle.join().unwrap(), Ok(JsValue::Bool(true)));
|
||||
}
|
||||
|
||||
// ---- global functions ----
|
||||
|
||||
#[test]
|
||||
fn global_number_functions() {
|
||||
truth("parseInt(' 0x1F') === 31 && parseInt('-12px') === -12 && parseInt('z', 36) === 35 && isNaN(parseInt('', 10)) && isNaN(parseInt('1', 1))");
|
||||
truth(
|
||||
"parseInt('011') === 11 && parseInt('11', 2) === 3 && parseInt('0x10', 16) === 16 && isNaN(parseInt('0x', 16)) && parseInt('12', 3.9) === 5",
|
||||
);
|
||||
truth("parseInt('123456789012345678901234567890') === 1.2345678901234568e29 && 1 / parseInt('-0') === -Infinity");
|
||||
truth(
|
||||
"parseFloat(' 3.5e2x') === 350 && parseFloat('-.5') === -0.5 && parseFloat('Infinityx') === Infinity && isNaN(parseFloat('e5')) && parseFloat('1e') === 1",
|
||||
);
|
||||
truth("isNaN('x') && !isNaN('1') && isFinite('1') && !isFinite(Infinity) && !isFinite(NaN)");
|
||||
truth("NaN !== NaN && Infinity === 1 / 0 && undefined === void 0 && !Object.getOwnPropertyDescriptor(this, 'NaN').writable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uri_functions_encode_and_decode_utf8() {
|
||||
assert_eq!(string("encodeURIComponent('a b&c/\\u00e9\\ud83d\\ude00')"), "a%20b%26c%2F%C3%A9%F0%9F%98%80");
|
||||
assert_eq!(string("encodeURI('http://x.y/a b?c=d#e')"), "http://x.y/a%20b?c=d#e");
|
||||
assert_eq!(string("decodeURIComponent('a%20b%26%C3%A9%F0%9F%98%80') === 'a b&\\u00e9\\ud83d\\ude00' ? 'ok' : 'no'"), "ok");
|
||||
assert_eq!(string("decodeURI('%3B%2F%20%23')"), "%3B%2F %23");
|
||||
for bad in [
|
||||
"encodeURI('\\ud800')",
|
||||
"encodeURIComponent('\\udc00x')",
|
||||
"decodeURI('%')",
|
||||
"decodeURI('%C3')",
|
||||
"decodeURI('%C0%80')",
|
||||
"decodeURI('%ED%A0%80')",
|
||||
"decodeURI('%FF')",
|
||||
] {
|
||||
throws(bad, "URIError");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_and_unescape_follow_annex_b() {
|
||||
assert_eq!(string("escape('a b+/@\\u00e9\\u0100')"), "a%20b+/@%E9%u0100");
|
||||
assert_eq!(string("unescape('a%20b%E9%u0100%zz%u12') === 'a b\\u00e9\\u0100%zz%u12' ? 'ok' : 'no'"), "ok");
|
||||
}
|
||||
|
||||
// ---- robustness ----
|
||||
|
||||
/// Calls of built-ins with arguments of every kind.
|
||||
const CALLS: &[&str] = &[
|
||||
"[1,,3].join(X)",
|
||||
"[3,1,2].sort(X)",
|
||||
"[1,2,3].splice(X, X, X)",
|
||||
"[1,2].slice(X, X)",
|
||||
"[].concat(X, [X])",
|
||||
"[1,2,3].map(X)",
|
||||
"[1,2].reduce(X, X)",
|
||||
"[1,2].indexOf(X, X)",
|
||||
"[1,2].lastIndexOf(X, X)",
|
||||
"Array.prototype.push.call(X, X)",
|
||||
"Array.prototype.unshift.call(X, X)",
|
||||
"Array.prototype.reverse.call(X)",
|
||||
"Array(X)",
|
||||
"Object.keys(X)",
|
||||
"Object.freeze(X)",
|
||||
"Object.defineProperty(X, X, X)",
|
||||
"Object.create(X, X)",
|
||||
"Object.getOwnPropertyNames(X)",
|
||||
"JSON.stringify(X, X, X)",
|
||||
"JSON.parse(X, X)",
|
||||
"Math.max(X, X)",
|
||||
"Math.pow(X, X)",
|
||||
"parseInt(X, X)",
|
||||
"parseFloat(X)",
|
||||
"decodeURIComponent(X)",
|
||||
"encodeURI(X)",
|
||||
"unescape(X)",
|
||||
"escape(X)",
|
||||
"Function.prototype.bind.call(X, X)",
|
||||
];
|
||||
|
||||
const ARGS: &[&str] = &[
|
||||
"undefined",
|
||||
"null",
|
||||
"0",
|
||||
"-1",
|
||||
"4294967295",
|
||||
"NaN",
|
||||
"'%E0'",
|
||||
"'[1,{\"a\":2}]'",
|
||||
"''",
|
||||
"[]",
|
||||
"{length: 4294967295}",
|
||||
"{}",
|
||||
"function (a, b) { return b - a; }",
|
||||
"{valueOf: function () { throw 1; }}",
|
||||
"{get length() { return 3; }}",
|
||||
"Object.freeze([1, 2])",
|
||||
"{value: 1, get: function () {}}",
|
||||
];
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(256))]
|
||||
|
||||
#[test]
|
||||
fn arbitrary_built_in_calls_never_panic(call in 0..CALLS.len(), args in proptest::collection::vec(0..ARGS.len(), 3)) {
|
||||
let mut src = CALLS[call].to_string();
|
||||
for a in &args {
|
||||
src = src.replacen('X', ARGS[*a], 1);
|
||||
}
|
||||
let src = src.replace('X', "1");
|
||||
let mut realm = small(Limits { fuel: 200_000, heap_bytes: 4 << 20, frames: 200, reentry: 8 });
|
||||
if let Err(e) = realm.run(&src) {
|
||||
prop_assert!(e.kind != RunErrorKind::Internal, "{src}: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-4
@@ -170,11 +170,14 @@ pub(crate) enum NativeKind {
|
||||
Eval,
|
||||
}
|
||||
|
||||
/// What a function object runs. `Bound` is a function made by `Function.prototype.bind` (section
|
||||
/// 15.3.4.5): calling it calls `target` with `this` and `args` before the arguments it is given.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Func {
|
||||
Closure { code: Rc<Code>, env: EnvId },
|
||||
Native { f: NativeFn, kind: NativeKind, constructor: bool, name: &'static str },
|
||||
Host { f: Rc<HostFn>, name: Rc<str> },
|
||||
Bound { target: ObjId, this: Value, args: Rc<[Value]> },
|
||||
}
|
||||
|
||||
/// A sloppy mode `arguments` object's link to its function's parameters (section 10.6): index
|
||||
@@ -204,25 +207,31 @@ pub(crate) enum Class {
|
||||
Function(Func),
|
||||
Error,
|
||||
Arguments(Box<ArgMap>),
|
||||
/// A `Boolean` object made by ToObject.
|
||||
Boolean,
|
||||
/// A `Boolean` object made by ToObject, with its primitive value.
|
||||
Boolean(bool),
|
||||
/// A `Number` object made by ToObject.
|
||||
Number,
|
||||
String(Str),
|
||||
RegExp,
|
||||
ForIn(Box<ForIn>),
|
||||
/// The `Math` object (section 15.8).
|
||||
Math,
|
||||
/// The `JSON` object (section 15.12).
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Class {
|
||||
/// The `[[Class]]` name `Object.prototype.toString` reports.
|
||||
pub(crate) fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Class::Math => "Math",
|
||||
Class::Json => "JSON",
|
||||
Class::Ordinary | Class::ForIn(_) => "Object",
|
||||
Class::Array { .. } => "Array",
|
||||
Class::Function(_) => "Function",
|
||||
Class::Error => "Error",
|
||||
Class::Arguments(_) => "Arguments",
|
||||
Class::Boolean => "Boolean",
|
||||
Class::Boolean(_) => "Boolean",
|
||||
Class::Number => "Number",
|
||||
Class::String(_) => "String",
|
||||
Class::RegExp => "RegExp",
|
||||
@@ -289,6 +298,8 @@ pub(crate) const OBJECT_BYTES: usize = 96;
|
||||
pub(crate) const ENV_BYTES: usize = 64;
|
||||
/// The estimated bytes of a rope node.
|
||||
pub(crate) const ROPE_BYTES: usize = 48;
|
||||
/// The estimated bytes of each argument a bound function holds.
|
||||
pub(crate) const BOUND_ARG_BYTES: usize = 24;
|
||||
/// The estimated bytes of a piece of code without its instructions, tables and strings.
|
||||
const CODE_BYTES: usize = 384;
|
||||
|
||||
@@ -305,6 +316,10 @@ pub(crate) struct Heap {
|
||||
pub(crate) bytes: usize,
|
||||
/// How many collections have run.
|
||||
pub(crate) collections: usize,
|
||||
/// Counts the properties whose names start with a digit (the array indices among them) added
|
||||
/// to any object, so an operation that lists the indices an object has can tell whether a
|
||||
/// script added one since.
|
||||
pub(crate) index_epoch: u64,
|
||||
}
|
||||
|
||||
impl Heap {
|
||||
@@ -521,11 +536,20 @@ fn trace_object(o: &Object, work: &mut Vec<u32>, strings: &mut StringSizes) -> u
|
||||
Prop::Accessor { get, set, .. } => work.extend(get.iter().chain(set.iter()).map(|f| f.0)),
|
||||
}
|
||||
}
|
||||
let mut extra = 0;
|
||||
match &o.class {
|
||||
Class::Function(Func::Closure { env, code }) => {
|
||||
work.push(env.0);
|
||||
mark_code(code, strings);
|
||||
}
|
||||
Class::Function(Func::Bound { target, this, args }) => {
|
||||
work.push(target.0);
|
||||
mark_value(this, work, strings);
|
||||
for a in args.iter() {
|
||||
mark_value(a, work, strings);
|
||||
}
|
||||
extra = BOUND_ARG_BYTES * args.len();
|
||||
}
|
||||
Class::Arguments(map) => work.push(map.env.0),
|
||||
Class::String(s) => strings.add(s),
|
||||
Class::ForIn(state) => {
|
||||
@@ -534,7 +558,7 @@ fn trace_object(o: &Object, work: &mut Vec<u32>, strings: &mut StringSizes) -> u
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
OBJECT_BYTES + PROP_BYTES * o.props.len()
|
||||
OBJECT_BYTES + PROP_BYTES * o.props.len() + extra
|
||||
}
|
||||
|
||||
fn trace_env(e: &Env, work: &mut Vec<u32>, strings: &mut StringSizes) -> usize {
|
||||
|
||||
+356
-20
@@ -161,6 +161,11 @@ pub(crate) struct Interp {
|
||||
pub(crate) throw_site: Option<ThrowSite>,
|
||||
/// The last exception a run did not catch.
|
||||
pub(crate) last_thrown: Value,
|
||||
/// The state of `Math.random`'s generator.
|
||||
pub(crate) random: u64,
|
||||
/// The objects `Array.prototype.join` and `toLocaleString` are joining, innermost last: a
|
||||
/// cycle back to one of them joins as an empty string.
|
||||
pub(crate) join_stack: Vec<ObjId>,
|
||||
}
|
||||
|
||||
/// The least heap size a collection waits for.
|
||||
@@ -186,6 +191,15 @@ fn index_keys_bytes(len: usize) -> usize {
|
||||
total
|
||||
}
|
||||
|
||||
/// A seed for `Math.random` that differs from realm to realm, from the standard library's
|
||||
/// randomly keyed hasher; never zero.
|
||||
fn random_seed() -> u64 {
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
|
||||
h.write_u64(0x9e37_79b9_7f4a_7c15);
|
||||
h.finish() | 1
|
||||
}
|
||||
|
||||
/// The estimated bytes of a binding or property named by `len` code units.
|
||||
fn binding_bytes(len: usize) -> usize {
|
||||
PROP_BYTES + string_bytes(len)
|
||||
@@ -286,6 +300,8 @@ impl Interp {
|
||||
source_units: 0,
|
||||
throw_site: None,
|
||||
last_thrown: Value::Undefined,
|
||||
random: random_seed(),
|
||||
join_stack: Vec::new(),
|
||||
};
|
||||
crate::builtins::install(&mut interp);
|
||||
interp.gc_threshold = interp.next_threshold();
|
||||
@@ -981,13 +997,210 @@ impl Interp {
|
||||
*length = i + 1;
|
||||
}
|
||||
let name_len = k.len();
|
||||
let index = new && k.first().is_some_and(|&u| (u16::from(b'0')..=u16::from(b'9')).contains(&u));
|
||||
o.props.insert(k, prop);
|
||||
if new {
|
||||
self.heap.bytes += PROP_BYTES + string_bytes(name_len);
|
||||
}
|
||||
if index {
|
||||
self.heap.index_epoch = self.heap.index_epoch.wrapping_add(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [[DefineOwnProperty]] (section 8.12.9, with the array rules of section 15.4.5.1 and the
|
||||
/// arguments object rules of section 10.6). Returns whether the property was defined; with
|
||||
/// `throw` a rejection throws a `TypeError` instead.
|
||||
pub(crate) fn define_own_property(&mut self, id: ObjId, k: Key, desc: &Desc, throw: bool) -> JsResult<bool> {
|
||||
match &self.object(id)?.class {
|
||||
Class::Array { length, length_writable } => {
|
||||
let (length, length_writable) = (*length, *length_writable);
|
||||
if is(&k, "length") {
|
||||
return self.define_array_length(id, desc, throw);
|
||||
}
|
||||
if let Some(i) = array_index(&k)
|
||||
&& i >= length
|
||||
&& !length_writable
|
||||
{
|
||||
return self.reject_bool(throw, &k, "the array's length is read-only");
|
||||
}
|
||||
self.define_ordinary(id, k, desc, throw)
|
||||
}
|
||||
Class::Arguments(map) => {
|
||||
let mapped = array_index(&k).and_then(|i| map.names.get(i as usize)).and_then(Option::as_ref).cloned();
|
||||
if !self.define_ordinary(id, k.clone(), desc, false)? {
|
||||
return self.reject_bool(throw, &k, "the property cannot be redefined");
|
||||
}
|
||||
if let Some(name) = mapped {
|
||||
let env = match &self.object(id)?.class {
|
||||
Class::Arguments(map) => Some(map.env),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(v) = desc.value.clone().filter(|_| !desc.is_accessor())
|
||||
&& let Some(env) = env
|
||||
&& let Some(Env::Decl { bindings, .. }) = self.heap.env_mut(env)
|
||||
&& let Some(b) = bindings.get_mut(&name)
|
||||
{
|
||||
b.value = v;
|
||||
}
|
||||
if (desc.is_accessor() || desc.writable == Some(false))
|
||||
&& let Class::Arguments(map) = &mut self.object_mut(id)?.class
|
||||
&& let Some(slot) = array_index(&k).and_then(|i| map.names.get_mut(i as usize))
|
||||
{
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
_ => self.define_ordinary(id, k, desc, throw),
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_bool(&mut self, throw: bool, k: &[u16], why: &str) -> JsResult<bool> {
|
||||
if throw {
|
||||
let message = format!("cannot define property '{}': {why}", String::from_utf16_lossy(k));
|
||||
return Err(self.type_error(&message));
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// The default [[DefineOwnProperty]] (section 8.12.9).
|
||||
fn define_ordinary(&mut self, id: ObjId, k: Key, desc: &Desc, throw: bool) -> JsResult<bool> {
|
||||
let current = self.get_own_property(id, &k)?;
|
||||
let o = self.object(id)?;
|
||||
let virtual_string = matches!(&o.class, Class::String(s) if is(&k, "length") || array_index(&k).is_some_and(|i| (i as usize) < s.len()));
|
||||
let Some(current) = current else {
|
||||
if !o.extensible {
|
||||
return self.reject_bool(throw, &k, "the object is not extensible");
|
||||
}
|
||||
let flags = desc.flags_or(0);
|
||||
let prop = if desc.is_accessor() {
|
||||
Prop::Accessor { get: desc.get.flatten(), set: desc.set.flatten(), flags: flags & !WRITABLE }
|
||||
} else {
|
||||
Prop::Data { value: desc.value.clone().unwrap_or(Value::Undefined), flags }
|
||||
};
|
||||
self.define(id, k, prop)?;
|
||||
return Ok(true);
|
||||
};
|
||||
let Some(merged) = merge_desc(¤t, desc) else { return self.reject_bool(throw, &k, "the property cannot be redefined") };
|
||||
// An index or the length of a String object cannot change, so a definition that passes
|
||||
// the checks leaves it as it is.
|
||||
if virtual_string {
|
||||
return Ok(true);
|
||||
}
|
||||
self.define(id, k, merged)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Defining an array's `length` (section 15.4.5.1, step 3).
|
||||
fn define_array_length(&mut self, id: ObjId, desc: &Desc, throw: bool) -> JsResult<bool> {
|
||||
let length_key = key("length");
|
||||
let Some(v) = desc.value.clone() else {
|
||||
let Some(current) = self.get_own_property(id, &length_key)? else { return internal("an array without a length") };
|
||||
let Some(merged) = merge_desc(¤t, desc) else { return self.reject_bool(throw, &length_key, "the length cannot be redefined") };
|
||||
if let Class::Array { length_writable, .. } = &mut self.object_mut(id)?.class {
|
||||
*length_writable = merged.flags() & WRITABLE != 0;
|
||||
}
|
||||
return Ok(true);
|
||||
};
|
||||
let new = to_uint32(self.to_number(&v)?);
|
||||
if f64::from(new) != self.to_number(&v)? {
|
||||
return Err(self.throw(ErrorType::Range, "invalid array length"));
|
||||
}
|
||||
let Some(current) = self.get_own_property(id, &length_key)? else { return internal("an array without a length") };
|
||||
let Class::Array { length: old, length_writable } = self.object(id)?.class else { return internal("not an array") };
|
||||
let new_desc = Desc { value: Some(Value::Number(f64::from(new))), ..desc.clone() };
|
||||
if new >= old {
|
||||
let Some(merged) = merge_desc(¤t, &new_desc) else { return self.reject_bool(throw, &length_key, "the length cannot be redefined") };
|
||||
if let Class::Array { length, length_writable } = &mut self.object_mut(id)?.class {
|
||||
*length = new;
|
||||
*length_writable = merged.flags() & WRITABLE != 0;
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
if !length_writable {
|
||||
return self.reject_bool(throw, &length_key, "it is read-only");
|
||||
}
|
||||
let checked = Desc { writable: None, ..new_desc };
|
||||
if merge_desc(¤t, &checked).is_none() {
|
||||
return self.reject_bool(throw, &length_key, "the length cannot be redefined");
|
||||
}
|
||||
let read_only = desc.writable == Some(false);
|
||||
if !self.shrink_array(id, new, read_only)? {
|
||||
return self.reject_bool(throw, &length_key, "an element cannot be deleted");
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Sets an array's length to `new`, deleting the elements at and past it from the last one
|
||||
/// down (section 15.4.5.1, step 3.l). An element that cannot be deleted stops it, leaving
|
||||
/// the length one past that element, and gives `false`. Each property takes a unit of fuel
|
||||
/// before anything is removed. With `read_only` the length is made read-only afterwards
|
||||
/// either way.
|
||||
fn shrink_array(&mut self, id: ObjId, new: u32, read_only: bool) -> JsResult<bool> {
|
||||
let Class::Array { length: old, .. } = self.object(id)?.class else { return internal("not an array") };
|
||||
let mut ok = true;
|
||||
let mut length = new;
|
||||
if new < old {
|
||||
for _ in 0..self.object(id)?.props.len() {
|
||||
self.use_fuel()?;
|
||||
}
|
||||
let o = self.object_mut(id)?;
|
||||
let mut doomed: Vec<(u32, Key)> =
|
||||
o.props.iter().filter_map(|(k, _)| array_index(k).filter(|&i| i >= new).map(|i| (i, k.clone()))).collect();
|
||||
doomed.sort_unstable_by_key(|d| std::cmp::Reverse(d.0));
|
||||
for (i, k) in doomed {
|
||||
if o.props.get(&k).is_some_and(|p| !p.configurable()) {
|
||||
length = i + 1;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
o.props.remove(&k);
|
||||
}
|
||||
}
|
||||
if let Class::Array { length: l, length_writable } = &mut self.object_mut(id)?.class {
|
||||
*l = length;
|
||||
if read_only {
|
||||
*length_writable = false;
|
||||
}
|
||||
}
|
||||
Ok(ok)
|
||||
}
|
||||
|
||||
/// The names of an object's own properties (section 15.2.3.4): a String object's indices and
|
||||
/// an array's or String object's `length` first, then the rest in the order they were added,
|
||||
/// which is the order `for-in` visits them. Each name takes a unit of fuel, and the heap for
|
||||
/// the list is checked first.
|
||||
pub(crate) fn own_keys(&mut self, id: ObjId) -> JsResult<Vec<Key>> {
|
||||
let o = self.object(id)?;
|
||||
let string_len = if let Class::String(s) = &o.class { s.len() } else { 0 };
|
||||
let has_length = matches!(o.class, Class::Array { .. } | Class::String(_));
|
||||
let count = o.props.len().saturating_add(string_len).saturating_add(usize::from(has_length));
|
||||
self.reserve_heap(index_keys_bytes(string_len).saturating_add(count.saturating_mul(KEY_ENTRY_BYTES)))?;
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for i in 0..string_len {
|
||||
self.use_fuel()?;
|
||||
out.push(index_key(u32::try_from(i).unwrap_or(u32::MAX)));
|
||||
}
|
||||
if has_length {
|
||||
out.push(key("length"));
|
||||
}
|
||||
for _ in 0..self.object(id)?.props.len() {
|
||||
self.use_fuel()?;
|
||||
}
|
||||
out.extend(self.object(id)?.props.keys());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Runs `f` with `values` kept reachable for any collection it starts.
|
||||
pub(crate) fn rooted<T>(&mut self, values: &[Value], f: impl FnOnce(&mut Self) -> JsResult<T>) -> JsResult<T> {
|
||||
let mark = self.temp_roots.len();
|
||||
self.temp_roots.extend(values.iter().cloned());
|
||||
let r = f(self);
|
||||
self.temp_roots.truncate(mark);
|
||||
r
|
||||
}
|
||||
|
||||
fn reject(&mut self, throw: bool, k: &[u16], why: &str) -> JsResult<()> {
|
||||
if throw {
|
||||
let message = format!("cannot assign to property '{}': {why}", String::from_utf16_lossy(k));
|
||||
@@ -1061,25 +1274,12 @@ impl Interp {
|
||||
if f64::from(new) != n {
|
||||
return Err(self.throw(ErrorType::Range, "invalid array length"));
|
||||
}
|
||||
let o = self.object(id)?;
|
||||
let Class::Array { length: old, length_writable } = o.class else { return internal("not an array") };
|
||||
let Class::Array { length_writable, .. } = self.object(id)?.class else { return internal("not an array") };
|
||||
if !length_writable {
|
||||
return self.reject(throw, &key("length"), "it is read-only");
|
||||
}
|
||||
if new < old {
|
||||
// Shrinking visits every property: each takes fuel before anything is removed.
|
||||
for _ in 0..o.props.len() {
|
||||
self.use_fuel()?;
|
||||
}
|
||||
}
|
||||
let o = self.object_mut(id)?;
|
||||
let Class::Array { length, .. } = &mut o.class else { return internal("not an array") };
|
||||
*length = new;
|
||||
if new < old {
|
||||
let doomed: Vec<Key> = o.props.keys().into_iter().filter(|k| array_index(k).is_some_and(|i| i >= new)).collect();
|
||||
for k in doomed {
|
||||
o.props.remove(&k);
|
||||
}
|
||||
if !self.shrink_array(id, new, false)? {
|
||||
return self.reject(throw, &key("length"), "an element cannot be deleted");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1486,6 +1686,9 @@ impl Interp {
|
||||
/// Resolves `call` and `apply` and runs native and host functions; a function written in
|
||||
/// JavaScript comes back to be entered by the caller.
|
||||
fn prepare_call(&mut self, mut f: Value, mut this: Value, mut args: Vec<Value>) -> JsResult<Prepared> {
|
||||
// The arguments `apply` reads and those a bound function adds are not on the operand
|
||||
// stack, so a native function's collection must be told about them.
|
||||
let mut unrooted = false;
|
||||
loop {
|
||||
let Some(func) = self.func_of(&f) else { return Err(self.type_error("the value is not a function")) };
|
||||
match func {
|
||||
@@ -1507,6 +1710,23 @@ impl Interp {
|
||||
f = this;
|
||||
this = args.first().cloned().unwrap_or(Value::Undefined);
|
||||
args = self.list_from_array_like(&list)?;
|
||||
unrooted = true;
|
||||
}
|
||||
Func::Bound { target, this: bound_this, args: bound_args } => {
|
||||
self.use_fuel()?;
|
||||
let mut all = Vec::with_capacity(bound_args.len().saturating_add(args.len()));
|
||||
all.extend(bound_args.iter().cloned());
|
||||
all.append(&mut args);
|
||||
args = all;
|
||||
this = bound_this;
|
||||
f = Value::Object(target);
|
||||
unrooted = true;
|
||||
}
|
||||
Func::Native { f: native, .. } if unrooted => {
|
||||
let mut roots = args.clone();
|
||||
roots.push(this.clone());
|
||||
let v = self.rooted(&roots, |interp| native(interp, this, &args, false))?;
|
||||
return Ok(Prepared::Done(v));
|
||||
}
|
||||
Func::Native { f: native, .. } => return Ok(Prepared::Done(native(self, this, &args, false)?)),
|
||||
Func::Host { f: host, .. } => {
|
||||
@@ -1582,8 +1802,17 @@ impl Interp {
|
||||
let n = argc as usize;
|
||||
let start =
|
||||
self.stack.len().checked_sub(n + 1).filter(|&s| s >= self.base()).ok_or_else(|| Abrupt::Internal("new below the stack".into()))?;
|
||||
let f = self.stack.get(start).cloned().unwrap_or(Value::Undefined);
|
||||
let args: Vec<Value> = self.stack.get(start + 1..).map(<[Value]>::to_vec).unwrap_or_default();
|
||||
let mut f = self.stack.get(start).cloned().unwrap_or(Value::Undefined);
|
||||
let mut args: Vec<Value> = self.stack.get(start + 1..).map(<[Value]>::to_vec).unwrap_or_default();
|
||||
// A bound function constructs its target with its arguments first (section 15.3.4.5.2);
|
||||
// the bound function on the stack keeps them reachable.
|
||||
while let Some(Func::Bound { target, args: bound_args, .. }) = self.func_of(&f) {
|
||||
self.use_fuel()?;
|
||||
let mut all = bound_args.to_vec();
|
||||
all.append(&mut args);
|
||||
args = all;
|
||||
f = Value::Object(target);
|
||||
}
|
||||
match self.func_of(&f) {
|
||||
Some(Func::Closure { code, env }) => {
|
||||
let Some(callee) = f.as_object() else { return internal("callee is not an object") };
|
||||
@@ -2001,7 +2230,7 @@ impl Interp {
|
||||
let (class, proto) = match v {
|
||||
Value::Object(o) => return Ok(*o),
|
||||
Value::Undefined | Value::Null => return Err(self.type_error("cannot convert undefined or null to an object")),
|
||||
Value::Bool(_) => (Class::Boolean, self.intrinsics.boolean_proto),
|
||||
Value::Bool(b) => (Class::Boolean(*b), self.intrinsics.boolean_proto),
|
||||
Value::Number(_) => (Class::Number, self.intrinsics.number_proto),
|
||||
Value::String(s) => (Class::String(s.clone()), self.intrinsics.string_proto),
|
||||
};
|
||||
@@ -2146,7 +2375,13 @@ impl Interp {
|
||||
if !self.is_callable(f) {
|
||||
return Err(self.type_error("the right side of 'instanceof' is not a function"));
|
||||
}
|
||||
let Some(fid) = f.as_object() else { return Ok(false) };
|
||||
let Some(mut fid) = f.as_object() else { return Ok(false) };
|
||||
// A bound function answers for its target (section 15.3.4.5.3).
|
||||
while let Some(Func::Bound { target, .. }) = self.func_of(&Value::Object(fid)) {
|
||||
self.use_fuel()?;
|
||||
fid = target;
|
||||
}
|
||||
let f = &Value::Object(fid);
|
||||
let Value::Object(mut o) = v.clone() else { return Ok(false) };
|
||||
let Value::Object(proto) = self.get_from(fid, &key("prototype"), f)? else {
|
||||
return Err(self.type_error("the function's prototype is not an object"));
|
||||
@@ -2221,6 +2456,107 @@ pub(crate) enum CompileError {
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
/// A property descriptor (section 8.10): each field is `None` when it is absent. `get` and `set`
|
||||
/// are `Some(None)` when present as `undefined`.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub(crate) struct Desc {
|
||||
pub(crate) value: Option<Value>,
|
||||
pub(crate) get: Option<Option<ObjId>>,
|
||||
pub(crate) set: Option<Option<ObjId>>,
|
||||
pub(crate) writable: Option<bool>,
|
||||
pub(crate) enumerable: Option<bool>,
|
||||
pub(crate) configurable: Option<bool>,
|
||||
}
|
||||
|
||||
impl Desc {
|
||||
/// A data descriptor with every attribute given.
|
||||
pub(crate) fn data(value: Value, flags: u8) -> Self {
|
||||
Desc {
|
||||
value: Some(value),
|
||||
writable: Some(flags & WRITABLE != 0),
|
||||
enumerable: Some(flags & ENUMERABLE != 0),
|
||||
configurable: Some(flags & CONFIGURABLE != 0),
|
||||
..Desc::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_accessor(&self) -> bool {
|
||||
self.get.is_some() || self.set.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn is_data(&self) -> bool {
|
||||
self.value.is_some() || self.writable.is_some()
|
||||
}
|
||||
|
||||
/// The attribute flags the descriptor gives, each absent one taken from `default`.
|
||||
fn flags_or(&self, default: u8) -> u8 {
|
||||
let pick = |field: Option<bool>, bit: u8| match field {
|
||||
Some(true) => bit,
|
||||
Some(false) => 0,
|
||||
None => default & bit,
|
||||
};
|
||||
pick(self.writable, WRITABLE) | pick(self.enumerable, ENUMERABLE) | pick(self.configurable, CONFIGURABLE)
|
||||
}
|
||||
}
|
||||
|
||||
/// SameValue (section 9.12).
|
||||
pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
|
||||
match (a, b) {
|
||||
(Value::Number(x), Value::Number(y)) => (x.is_nan() && y.is_nan()) || (x == y && x.is_sign_negative() == y.is_sign_negative()),
|
||||
_ => strict_eq(a, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Steps 5 to 12 of [[DefineOwnProperty]] (section 8.12.9) for an existing property: the
|
||||
/// property after `desc` is applied, or `None` when the change is not allowed.
|
||||
fn merge_desc(current: &Prop, desc: &Desc) -> Option<Prop> {
|
||||
let flags = current.flags();
|
||||
let configurable = flags & CONFIGURABLE != 0;
|
||||
if !configurable {
|
||||
if desc.configurable == Some(true) {
|
||||
return None;
|
||||
}
|
||||
if desc.enumerable.is_some_and(|e| e != (flags & ENUMERABLE != 0)) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let attrs = |base: u8, writable_bit: bool| {
|
||||
let f = desc.flags_or(base);
|
||||
if writable_bit { f } else { f & !WRITABLE }
|
||||
};
|
||||
match current {
|
||||
Prop::Data { .. } if desc.is_accessor() => {
|
||||
if !configurable {
|
||||
return None;
|
||||
}
|
||||
Some(Prop::Accessor { get: desc.get.flatten(), set: desc.set.flatten(), flags: attrs(flags & !WRITABLE, false) })
|
||||
}
|
||||
Prop::Accessor { .. } if desc.is_data() => {
|
||||
if !configurable {
|
||||
return None;
|
||||
}
|
||||
Some(Prop::Data { value: desc.value.clone().unwrap_or(Value::Undefined), flags: attrs(flags & !WRITABLE, true) })
|
||||
}
|
||||
Prop::Data { value, .. } => {
|
||||
if !configurable && flags & WRITABLE == 0 {
|
||||
if desc.writable == Some(true) {
|
||||
return None;
|
||||
}
|
||||
if desc.value.as_ref().is_some_and(|v| !same_value(v, value)) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(Prop::Data { value: desc.value.clone().unwrap_or_else(|| value.clone()), flags: attrs(flags, true) })
|
||||
}
|
||||
Prop::Accessor { get, set, .. } => {
|
||||
if !configurable && (desc.get.is_some_and(|g| g != *get) || desc.set.is_some_and(|s| s != *set)) {
|
||||
return None;
|
||||
}
|
||||
Some(Prop::Accessor { get: desc.get.unwrap_or(*get), set: desc.set.unwrap_or(*set), flags: attrs(flags, false) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The strict equality comparison (section 11.9.6).
|
||||
pub(crate) fn strict_eq(a: &Value, b: &Value) -> bool {
|
||||
match (a, b) {
|
||||
|
||||
@@ -23,9 +23,13 @@
|
||||
//! verifier, and runs it on an interpreter that keeps JavaScript calls on its own frame stack, so
|
||||
//! deep recursion in a script does not use the native stack. Objects and environments live in an
|
||||
//! arena with a mark-and-sweep collector; strings built by repeated `+` are flattened and dropped
|
||||
//! with loops. The realm provides the language (sections 10 to 14 of ECMA-262 5.1) and a minimal
|
||||
//! set of built-ins: `Object` and `Object.prototype`'s basic methods, `Function` with
|
||||
//! `Function.prototype.call` and `apply`, `eval`, and the error constructors. Every run is bounded
|
||||
//! with loops. The realm provides the language (sections 10 to 14 of ECMA-262 5.1) and these
|
||||
//! built-ins: the global functions and constants (15.1, with Annex B's `escape` and `unescape`),
|
||||
//! `Object` (15.2), `Function` with `call`, `apply` and `bind` (15.3), `Array` (15.4), the error
|
||||
//! types (15.11), `Math` (15.8) and `JSON` (15.12). The array methods visit only the indices an
|
||||
//! array or array-like object has, so a sparse array of length 2^32-1 costs as much as its
|
||||
//! elements, and `JSON.parse`, `JSON.stringify` and the reviver walk use no native recursion. The
|
||||
//! prototype methods are writable and configurable, so scripts can replace them. Every run is bounded
|
||||
//! by [`Limits`]: instructions run (fuel), heap bytes, frames, nested host calls, and an
|
||||
//! [`Interrupt`] flag another thread can set. A run that reaches a limit ends with
|
||||
//! [`RunErrorKind::Limit`], which a script's `try` cannot catch.
|
||||
@@ -169,6 +173,8 @@ impl LineTable {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod builtins_tests;
|
||||
#[cfg(test)]
|
||||
mod interp_tests;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -188,6 +188,7 @@ impl Realm {
|
||||
interp.stack.clear();
|
||||
interp.frames.clear();
|
||||
interp.temp_roots.clear();
|
||||
interp.join_stack.clear();
|
||||
match result {
|
||||
Ok(v) => Ok(interp.to_js_value(&v)),
|
||||
Err(Abrupt::Throw(v)) => {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Runs `json2.js`, Douglas Crockford's public-domain JSON polyfill, when `cargo xtask test262` has
|
||||
//! fetched it (it sets `VECTORCRAFT_JSON2`; without it the test looks in `corpus/json2/` and skips
|
||||
//! when the file is absent).
|
||||
//!
|
||||
//! The built-in `JSON` is removed first, so the polyfill defines its own. Each sample value is
|
||||
//! written by the polyfill and by the built-in `JSON.stringify` (ECMA-262 5.1 section 15.12.3),
|
||||
//! which must give the same text, and the text is read back by the polyfill's `parse` and by the
|
||||
//! built-in `JSON.parse` (section 15.12.2), which must give values that write the same text again.
|
||||
//!
|
||||
//! The polyfill uses `Date`, `String.prototype.replace` and regular expression `test`. While the
|
||||
//! interpreter lacks any of them, the test prints that it skipped and passes.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use vectorcraft_js::{JsValue, Realm};
|
||||
|
||||
fn json2_file() -> Option<PathBuf> {
|
||||
if let Ok(path) = std::env::var("VECTORCRAFT_JSON2") {
|
||||
return Some(PathBuf::from(path));
|
||||
}
|
||||
let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/json2");
|
||||
let mut dirs: Vec<PathBuf> =
|
||||
std::fs::read_dir(corpus).ok()?.filter_map(|e| e.ok().map(|e| e.path().join("json2.js"))).filter(|p| p.is_file()).collect();
|
||||
dirs.sort();
|
||||
dirs.pop()
|
||||
}
|
||||
|
||||
/// Built-ins the polyfill needs.
|
||||
const NEEDS: &str =
|
||||
"typeof Date === 'function' && typeof ''.replace === 'function' && typeof RegExp === 'function' && typeof /a/.test === 'function'";
|
||||
|
||||
/// Saves the built-in `JSON` and removes it, so the polyfill defines its own.
|
||||
const BEFORE: &str = "var NATIVE = JSON; delete this.JSON; this.JSON = undefined;";
|
||||
|
||||
/// Writes and reads each sample with both implementations; gives the first difference, or "ok".
|
||||
const ROUND_TRIP: &str = r#"
|
||||
var POLY = JSON;
|
||||
var samples = [
|
||||
0, -1.5, 1e21, "", "plain", "quote \" backslash \\ newline \n tab \t \u0001
é",
|
||||
true, false, null, [], {}, [1, [2, [3, []]]], {a: 1, b: [true, null, "x"], c: {d: {}}},
|
||||
[undefined, function () {}], {skip: undefined, f: function () {}, keep: 2}, [NaN, Infinity, -Infinity]
|
||||
];
|
||||
var problem = "ok";
|
||||
for (var i = 0; i < samples.length && problem === "ok"; i++) {
|
||||
var poly = POLY.stringify(samples[i]);
|
||||
var native = NATIVE.stringify(samples[i]);
|
||||
if (poly !== native) {
|
||||
problem = "sample " + i + ": json2 wrote " + poly + ", the built-in wrote " + native;
|
||||
} else if (poly !== undefined) {
|
||||
if (NATIVE.stringify(POLY.parse(native)) !== native) {
|
||||
problem = "sample " + i + ": json2 read " + native + " differently";
|
||||
} else if (POLY.stringify(NATIVE.parse(poly)) !== poly) {
|
||||
problem = "sample " + i + ": the built-in read " + poly + " differently";
|
||||
}
|
||||
}
|
||||
}
|
||||
var indented = {a: [1, {b: 2}], c: "d"};
|
||||
if (problem === "ok" && POLY.stringify(indented, null, 2) !== NATIVE.stringify(indented, null, 2)) {
|
||||
problem = "indented output differs";
|
||||
}
|
||||
if (problem === "ok" && POLY.stringify(indented, ["c"]) !== NATIVE.stringify(indented, ["c"])) {
|
||||
problem = "property list output differs";
|
||||
}
|
||||
problem;
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn json2_polyfill_round_trips_with_the_built_in_json() {
|
||||
let Some(path) = json2_file() else {
|
||||
println!("json2: SKIPPED (no json2.js; run cargo xtask test262)");
|
||||
return;
|
||||
};
|
||||
let source = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
|
||||
let mut realm = Realm::new();
|
||||
if realm.run(NEEDS) != Ok(JsValue::Bool(true)) {
|
||||
println!("json2: SKIPPED (the interpreter lacks Date, String.prototype.replace or RegExp, which json2.js uses)");
|
||||
return;
|
||||
}
|
||||
match realm.run(&format!("{BEFORE}\n{source}\n{ROUND_TRIP}")) {
|
||||
Ok(JsValue::String(s)) if s == "ok" => println!("json2: the polyfill and the built-in JSON agree on every sample"),
|
||||
other => panic!("json2: {other:?}"),
|
||||
}
|
||||
}
|
||||
+59
-16
@@ -19,6 +19,10 @@
|
||||
//! passes when it completes, or, for `negative: phase: runtime`, when it throws an object whose
|
||||
//! `constructor` is the global its `type:` names. Its counts are printed apart from the parse
|
||||
//! counts, and its failures go to `run-failures.txt`.
|
||||
//!
|
||||
//! A third test runs the tests of the ES5 built-ins the same way: those under the folders
|
||||
//! [`ES5_BUILT_INS`] names that have an `es5id:` (or, under `annexB/`, any edition's id) and no
|
||||
//! `features:` tag. Its failures go to `built-ins-failures.txt`.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -193,15 +197,37 @@ fn es5_parse_subset() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Globals and method names the interpreter does not provide yet: a test that names one is
|
||||
/// skipped.
|
||||
const MISSING_BUILTINS: &str = "Array String Number Boolean Math JSON Date RegExp parseInt parseFloat isNaN isFinite encodeURI \
|
||||
decodeURI encodeURIComponent decodeURIComponent escape unescape print $262 $DONE Symbol Proxy Reflect Promise Map Set WeakMap \
|
||||
WeakSet ArrayBuffer DataView Int8Array Uint8Array Float64Array defineProperty defineProperties getOwnPropertyDescriptor \
|
||||
getOwnPropertyNames getPrototypeOf create keys freeze isFrozen seal isSealed preventExtensions isExtensible push pop shift unshift \
|
||||
slice splice concat join reverse sort indexOf lastIndexOf every some forEach map filter reduce reduceRight charAt charCodeAt \
|
||||
fromCharCode substring substr split replace match search exec test toUpperCase toLowerCase trim toFixed toPrecision toExponential \
|
||||
bind now isArray compareArray verifyProperty fnGlobalObject";
|
||||
/// Globals and method names the interpreter does not provide yet: a test that names one, in its
|
||||
/// own source or in a harness file it includes, is skipped.
|
||||
const MISSING_BUILTINS: &str = "String Number Boolean Date RegExp print $262 $DONE Symbol Proxy Reflect Promise Map Set WeakMap \
|
||||
WeakSet ArrayBuffer DataView Int8Array Uint8Array Float64Array charAt charCodeAt fromCharCode substring substr split replace \
|
||||
match search exec test toUpperCase toLowerCase trim toFixed toPrecision toExponential now";
|
||||
|
||||
/// The directories under `test/built-ins/` and `test/annexB/built-ins/` that hold tests of the
|
||||
/// ES5 built-ins (ECMA-262 5.1 sections 15.1 to 15.12 and Annex B.2).
|
||||
const ES5_BUILT_INS: &[&str] = &[
|
||||
"built-ins/Object",
|
||||
"built-ins/Function",
|
||||
"built-ins/Array",
|
||||
"built-ins/Error",
|
||||
"built-ins/NativeErrors",
|
||||
"built-ins/Math",
|
||||
"built-ins/JSON",
|
||||
"built-ins/global",
|
||||
"built-ins/Infinity",
|
||||
"built-ins/NaN",
|
||||
"built-ins/undefined",
|
||||
"built-ins/parseInt",
|
||||
"built-ins/parseFloat",
|
||||
"built-ins/isNaN",
|
||||
"built-ins/isFinite",
|
||||
"built-ins/encodeURI",
|
||||
"built-ins/encodeURIComponent",
|
||||
"built-ins/decodeURI",
|
||||
"built-ins/decodeURIComponent",
|
||||
"annexB/built-ins/escape",
|
||||
"annexB/built-ins/unescape",
|
||||
];
|
||||
|
||||
/// The stand-in for `String` that the harness's failure messages call.
|
||||
const HARNESS_STRING: &str = "function String(value) { return '' + value; }";
|
||||
@@ -245,12 +271,28 @@ fn es5_language_subset_runs() {
|
||||
};
|
||||
// Each test runs on a thread with a stack large enough for the nested runs the re-entry limit
|
||||
// allows in an unoptimized build.
|
||||
let handle = std::thread::Builder::new().stack_size(64 << 20).spawn(move || run_language_subset(&dir)).unwrap();
|
||||
let roots = vec![(dir.join("test").join("language"), true)];
|
||||
let handle = std::thread::Builder::new().stack_size(64 << 20).spawn(move || run_subset(&dir, &roots, "language", "run-failures.txt")).unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
fn run_language_subset(dir: &Path) {
|
||||
let language = dir.join("test").join("language");
|
||||
#[test]
|
||||
fn es5_built_ins_subset_runs() {
|
||||
let Some(dir) = suite_dir() else {
|
||||
println!("test262 built-ins: SKIPPED (no suite; run cargo xtask test262)");
|
||||
return;
|
||||
};
|
||||
// The Annex B tests name the edition they were written for with `esid:`, so they run without
|
||||
// an `es5id:`.
|
||||
let roots = ES5_BUILT_INS.iter().map(|d| (dir.join("test").join(d), !d.starts_with("annexB"))).collect::<Vec<_>>();
|
||||
let handle =
|
||||
std::thread::Builder::new().stack_size(64 << 20).spawn(move || run_subset(&dir, &roots, "built-ins", "built-ins-failures.txt")).unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
/// Runs the tests under each root (with `true`, only those that name an ES5 section), prints the
|
||||
/// counts and writes the failures to `report` beside the suite.
|
||||
fn run_subset(dir: &Path, roots: &[(PathBuf, bool)], label: &str, report: &str) {
|
||||
let harness_dir = dir.join("harness");
|
||||
// A file that cannot be read gives an empty source.
|
||||
let read = |p: &Path| String::from_utf8_lossy(&std::fs::read(p).unwrap_or_default()).into_owned();
|
||||
@@ -262,11 +304,12 @@ fn run_language_subset(dir: &Path) {
|
||||
let verbose = std::env::var_os("VECTORCRAFT_TEST262_VERBOSE").is_some();
|
||||
let (mut tests, mut skipped, mut passed, mut failed, mut panicked) = (0usize, 0usize, 0usize, 0usize, 0usize);
|
||||
let mut failures = String::new();
|
||||
for path in test_files(&language) {
|
||||
let files = roots.iter().flat_map(|(root, es5_only)| test_files(root).into_iter().map(move |p| (p, *es5_only)));
|
||||
for (path, es5_only) in files {
|
||||
let source = read(&path);
|
||||
let meta = meta(&source);
|
||||
let negative_parse = matches!(meta.negative_phase.as_deref(), Some("parse" | "resolution"));
|
||||
if !meta.es5 || !meta.features.is_empty() || negative_parse || meta.flags.iter().any(|f| f == "module") {
|
||||
if (es5_only && !meta.es5) || !meta.features.is_empty() || negative_parse || meta.flags.iter().any(|f| f == "module") {
|
||||
continue;
|
||||
}
|
||||
let includes: String = meta.includes.iter().map(|i| read(&harness_dir.join(i)) + "\n").collect();
|
||||
@@ -307,12 +350,12 @@ fn run_language_subset(dir: &Path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
let report = dir.join("run-failures.txt");
|
||||
let report = dir.join(report);
|
||||
if let Err(e) = std::fs::write(&report, &failures) {
|
||||
println!("could not write {}: {e}", report.display());
|
||||
}
|
||||
println!(
|
||||
"test262 ES5 language subset through the interpreter: {tests} tests that need only the built-ins present, {passed} runs passed, \
|
||||
"test262 ES5 {label} subset through the interpreter: {tests} tests that need only the built-ins present, {passed} runs passed, \
|
||||
{failed} failed ({panicked} panicked). Skipped {skipped} tests that need other built-ins, use syntax after ES5 or are asynchronous. Failures: {}",
|
||||
report.display()
|
||||
);
|
||||
|
||||
@@ -654,7 +654,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`). A `run` step, like a `command_batch` step over MCP, can use an earlier step's result (`"$1.id"`, #785); `run --commands FILE` (or `-` for standard input) reads the steps from a JSON array or JSON Lines, past the Windows command-line limit (#3147, `apps/vectorcraft-cli/tests/it/cli.rs` `run_reads_*`, `run_refuses_malformed_commands_naming_the_step`).
|
||||
- File › Scripts › Check Script… reads a script written for Illustrator (`.jsx`, `.jsxinc` or a compiled `.jsxbin`) without running any of it and reports what it needs (the document model, files, dialogs, menu commands, features only Illustrator has), what it would do outside the document, the menu commands and document-model members it uses, its directives and included files, and the problems that would stop it loading (`script.check`, which also reads `.js` files); File › Open and drag-and-drop show the report for `.jsx`, `.jsxinc` and `.jsxbin` files.
|
||||
- `vectorcraft-js` parses JavaScript as ECMAScript 5.1 defines it, with the legacy octal forms, block functions and lenient regular expressions of Annex B, into a syntax tree with source spans; it reads the `#target`, `#include` and other preprocessor lines as directives without running them, and a script that breaks the grammar or an early-error rule gives a `SyntaxError` with its line and column. Nesting past the parser's depth limit (`Options::max_depth`, 200 by default) gives a `SyntaxError` instead of exhausting the stack. `cargo xtask test262` fetches the conformance suite at a pinned commit and parses its ES5 tests: 5,411 of 5,418 runs pass (the 7 others use `let` and `const`). Nothing runs scripts yet. Tests: `crates/js/src/tests.rs`, `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs`.
|
||||
- `vectorcraft-js`'s `Realm` runs ECMAScript 5.1 scripts: the parsed script compiles to verified bytecode that a non-recursive interpreter runs, with closures, `this`, `arguments`, `with`, direct and indirect `eval`, `new Function`, `try`/`catch`/`finally`, labeled `break` and `continue`, and `for`-`in` in insertion order, over a mark-and-sweep heap. Built-ins are limited to `Object`, `Function` with `call` and `apply`, `eval` and the error types. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag (smaller defaults in the wasm build), and a run that reaches a limit stops with a limit error that `try` cannot catch. `cargo xtask test262` also runs the suite's ES5 language tests that need only these built-ins: 3,074 of 3,078 runs pass (the 4 others expect the completion values of later editions). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
- `vectorcraft-js`'s `Realm` runs ECMAScript 5.1 scripts: the parsed script compiles to verified bytecode that a non-recursive interpreter runs, with closures, `this`, `arguments`, `with`, direct and indirect `eval`, `new Function`, `try`/`catch`/`finally`, labeled `break` and `continue`, and `for`-`in` in insertion order, over a mark-and-sweep heap, with the built-ins the next entry lists. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag (smaller defaults in the wasm build), and a run that reaches a limit stops with a limit error that `try` cannot catch. `cargo xtask test262` also runs the suite's ES5 language tests that need only the built-ins present: 3,362 of 3,387 runs pass (4 of the others expect the completion values of later editions, and 21 call `String` or `Number` methods). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
- `vectorcraft-js` provides the ECMAScript 5.1 built-ins `Object` with its property-descriptor functions, `create`, `keys`, `seal` and `freeze` (section 15.2), `Function.prototype.bind` (15.3.4.5), `Array` with every ES5 method (15.4), the error types (15.11), `Math` (15.8), `JSON` with revivers, replacers and indentation (15.12), and the global functions with Annex B's `escape` and `unescape` (15.1, B.2). The array methods work on array-like objects and visit only the indices that exist, so `new Array(4294967295).join("")` ends at once; `join` writes a cycle back to an array as an empty string, `JSON.stringify` throws a `TypeError` on a cycle, and deep `JSON` nesting uses no native stack. Every loop takes fuel and every result is checked against the heap limit. `cargo xtask test262` runs the suite's ES5 built-in tests that need only these built-ins: 4,478 of 4,556 runs pass (16 of the failing tests call `String`, `Number` or `Boolean` methods, and 23 expect rules of later editions). It also fetches the `json2.js` polyfill at a pinned commit to compare with the built-in `JSON`; that test runs once `Date`, `String.prototype.replace` and `RegExp` exist. Tests: `crates/js/src/builtins_tests.rs`, `crates/js/tests/test262.rs`, `crates/js/tests/json2.rs`.
|
||||
- **UI:** Illustrator 2026 layout restyled to measured values:
|
||||
- Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs.
|
||||
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
|
||||
|
||||
+56
-6
@@ -2,16 +2,33 @@
|
||||
//! BSD-3-Clause) at a pinned commit, fetched as GitHub's archive of that commit, checked against
|
||||
//! its SHA-256 and size, and unpacked into `corpus/test262/` (gitignored, never committed). It then
|
||||
//! runs `vectorcraft-js`'s `test262` tests, which parse the ES5 parse subset, run the ES5
|
||||
//! `test/language` tests that need only the interpreter's built-ins, and print each one's counts.
|
||||
//! `test/language` tests and the tests of the ES5 built-ins under `test/built-ins` and
|
||||
//! `test/annexB/built-ins` that need only the interpreter's built-ins, and print each one's counts.
|
||||
//!
|
||||
//! Moving the pin: change [`COMMIT`], run the command once with the new commit to see the
|
||||
//! archive's SHA-256 in the error, check the archive, and update [`SHA256`].
|
||||
//! It also fetches `json2.js`, Douglas Crockford's public-domain JSON polyfill
|
||||
//! (https://github.com/douglascrockford/JSON-js), at a pinned commit, checks it against its SHA-256
|
||||
//! and size, saves it in `corpus/json2/` (gitignored, never committed), and runs `vectorcraft-js`'s
|
||||
//! `json2` test, which runs the polyfill and compares what it writes and reads with the built-in
|
||||
//! `JSON`.
|
||||
//!
|
||||
//! Moving the pin: change [`COMMIT`] (or [`JSON2_COMMIT`]), run the command once with the new
|
||||
//! commit to see the file's SHA-256 in the error, check the file, and update [`SHA256`] (or
|
||||
//! [`JSON2_SHA256`]).
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{cargo, root, run, sha256};
|
||||
|
||||
/// The douglascrockford/JSON-js commit `json2.js` is read at.
|
||||
const JSON2_COMMIT: &str = "7e83f38a2312429fd4933169c1f6a27fd65e889c";
|
||||
|
||||
/// The SHA-256 of `json2.js` at [`JSON2_COMMIT`].
|
||||
const JSON2_SHA256: &str = "86df14b56572e68a7e10b18e71804cb78c6890a7d6e534324822c23d12e858a2";
|
||||
|
||||
/// The largest `json2.js` accepted (the pinned one is 18,876 bytes).
|
||||
const JSON2_MAX_BYTES: u64 = 1 << 20;
|
||||
|
||||
/// The tc39/test262 commit the suite is read at.
|
||||
const COMMIT: &str = "d53ebc8b1931d9f1541e43d59650776e4b766248";
|
||||
|
||||
@@ -25,11 +42,44 @@ pub fn run_suite() -> Result<(), String> {
|
||||
let dir = fetch()?;
|
||||
let mut c = cargo();
|
||||
c.env("VECTORCRAFT_TEST262_DIR", &dir).args(["test", "-p", "vectorcraft-js", "--release", "--test", "test262", "--", "--nocapture"]);
|
||||
run(c, &format!("cargo test -p vectorcraft-js --release --test test262 -- --nocapture (VECTORCRAFT_TEST262_DIR={})", dir.display()))
|
||||
run(c, &format!("cargo test -p vectorcraft-js --release --test test262 -- --nocapture (VECTORCRAFT_TEST262_DIR={})", dir.display()))?;
|
||||
let json2 = fetch_json2()?;
|
||||
let mut c = cargo();
|
||||
c.env("VECTORCRAFT_JSON2", &json2).args(["test", "-p", "vectorcraft-js", "--release", "--test", "json2", "--", "--nocapture"]);
|
||||
run(c, &format!("cargo test -p vectorcraft-js --release --test json2 -- --nocapture (VECTORCRAFT_JSON2={})", json2.display()))
|
||||
}
|
||||
|
||||
/// Fetches `json2.js` unless a copy with the pinned SHA-256 is there already, and returns its path.
|
||||
fn fetch_json2() -> Result<PathBuf, String> {
|
||||
let base = root().join("corpus").join("json2").join(JSON2_COMMIT);
|
||||
let file = base.join("json2.js");
|
||||
let matches = |p: &Path| -> Result<bool, String> {
|
||||
let Ok(meta) = std::fs::metadata(p) else { return Ok(false) };
|
||||
if meta.len() > JSON2_MAX_BYTES {
|
||||
return Err(format!("{} is {} bytes, over the {JSON2_MAX_BYTES}-byte limit", p.display(), meta.len()));
|
||||
}
|
||||
Ok(std::fs::read(p).is_ok_and(|b| sha256::hex(&b) == JSON2_SHA256))
|
||||
};
|
||||
if matches(&file)? {
|
||||
println!("json2.js {JSON2_COMMIT} is in {}", file.display());
|
||||
return Ok(file);
|
||||
}
|
||||
std::fs::create_dir_all(&base).map_err(|e| format!("create {}: {e}", base.display()))?;
|
||||
let url = format!("https://raw.githubusercontent.com/douglascrockford/JSON-js/{JSON2_COMMIT}/json2.js");
|
||||
let mut curl = Command::new("curl");
|
||||
curl.args(["-fsSL", "--retry", "3", "-A", "VectorCraft-dev", "--max-filesize", &JSON2_MAX_BYTES.to_string(), "-o"]).arg(&file).arg(&url);
|
||||
run(curl, &format!("curl {url}"))?;
|
||||
if !matches(&file)? {
|
||||
let got = std::fs::read(&file).map(|b| sha256::hex(&b)).unwrap_or_default();
|
||||
let _ = std::fs::remove_file(&file);
|
||||
return Err(format!("json2.js: sha256 {got} does not match the pinned {JSON2_SHA256}"));
|
||||
}
|
||||
println!("json2.js {JSON2_COMMIT} saved in {}", file.display());
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Fetches and unpacks the suite unless it is there already, and returns its folder.
|
||||
fn fetch() -> Result<std::path::PathBuf, String> {
|
||||
fn fetch() -> Result<PathBuf, String> {
|
||||
let base = root().join("corpus").join("test262");
|
||||
let dir = base.join(format!("test262-{COMMIT}"));
|
||||
let marker = dir.join(".vectorcraft-sha256");
|
||||
|
||||
Reference in New Issue
Block a user