mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 03:48:01 +00:00
Merge pull request #6189 from dotCooCoo/feat/js-builtins-core
JavaScript: vectorcraft-js adds ES5 Object, Array, Math, JSON and Function.prototype.bind
This commit is contained in:
+263
-15
@@ -1,16 +1,29 @@
|
||||
//! 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. `String`, `Number`, `RegExp` and `Date` are in their
|
||||
//! own modules; 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, with
|
||||
//! `Boolean.prototype`'s `toString` and `valueOf`. `String`, `Number`, `RegExp` and `Date` are in
|
||||
//! their own modules.
|
||||
//!
|
||||
//! 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))
|
||||
@@ -42,12 +55,16 @@ pub(crate) fn constructor(interp: &mut Interp, name: &'static str, ctor: ObjId,
|
||||
}
|
||||
|
||||
/// Creates the built-ins and the global object's properties. The caller installs them before it
|
||||
/// sets the realm's limits, so no limit stops the install.
|
||||
/// sets the realm's limits, so no limit stops the install. If it fails, the failure is kept in
|
||||
/// `interp.install_error` and every run of the realm ends with it.
|
||||
pub(crate) fn install(interp: &mut Interp) {
|
||||
if let Err(e) = try_install(interp) {
|
||||
// Installing touches only objects it has just made, so this does not happen; a realm
|
||||
// that somehow lacks a built-in reports it when a script uses it.
|
||||
let _ = e;
|
||||
let reason = match e {
|
||||
Abrupt::Throw(v) => interp.describe(&v),
|
||||
Abrupt::Limit(kind) => format!("limit reached: {kind:?}"),
|
||||
Abrupt::Internal(message) => message,
|
||||
};
|
||||
interp.install_error = Some(format!("the built-ins could not be installed: {reason}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +85,8 @@ 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(0.0), Some(object_proto));
|
||||
interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean, Some(object_proto));
|
||||
let boolean_proto = alloc(interp, Class::Boolean(false), Some(object_proto));
|
||||
interp.intrinsics.boolean_proto = boolean_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) {
|
||||
@@ -98,15 +116,189 @@ 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)?;
|
||||
|
||||
method(interp, boolean_proto, "toString", boolean_to_string, 0)?;
|
||||
method(interp, boolean_proto, "valueOf", boolean_value_of, 0)?;
|
||||
|
||||
install_errors(interp, object_proto)?;
|
||||
object::install(interp, object)?;
|
||||
array::install(interp)?;
|
||||
math::install(interp)?;
|
||||
json::install(interp)?;
|
||||
global::install(interp)?;
|
||||
crate::string_builtins::install(interp)?;
|
||||
crate::number_builtins::install(interp)?;
|
||||
crate::regexp_builtins::install(interp)?;
|
||||
crate::date_builtins::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. When `o` has no own property `k`, `v`
|
||||
/// stays rooted while heap is reserved for the new element, since the caller may hold it nowhere
|
||||
/// else; overwriting an own property reserves nothing.
|
||||
pub(crate) fn put_at(interp: &mut Interp, o: ObjId, k: f64, v: Value) -> JsResult<()> {
|
||||
let k = key_of(k);
|
||||
if interp.heap.object(o).is_some_and(|obj| obj.props.contains(&k)) {
|
||||
return interp.put(o, k, v, true);
|
||||
}
|
||||
interp.rooted(std::slice::from_ref(&v), |i| {
|
||||
i.reserve_heap(ELEMENT_BYTES)?;
|
||||
i.put(o, k.clone(), v.clone(), 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). `v` stays rooted while heap is
|
||||
/// reserved for it.
|
||||
fn create_at(interp: &mut Interp, a: ObjId, k: f64, v: Value) -> JsResult<()> {
|
||||
interp.rooted(std::slice::from_ref(&v), |i| {
|
||||
i.reserve_heap(ELEMENT_BYTES)?;
|
||||
i.define_own_property(a, key_of(k), &Desc::data(v.clone(), 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)))
|
||||
}
|
||||
|
||||
/// Code units a built-in may write into a string for one unit of fuel.
|
||||
const UNITS_PER_FUEL: usize = 16;
|
||||
|
||||
/// A string the built-ins build piece by piece: each piece takes fuel for its length, is checked
|
||||
/// against the longest string the heap limit allows, and has its bytes reserved before it is
|
||||
/// added. The buffer's bytes count toward the heap until `finish` or until the native call that
|
||||
/// made the builder returns. 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,
|
||||
/// Code units written since fuel was last taken, fewer than `UNITS_PER_FUEL`.
|
||||
unpaid: usize,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
fn new() -> Self {
|
||||
Builder { units: Vec::new(), charged: 0, unpaid: 0 }
|
||||
}
|
||||
|
||||
/// Checks that `more` code units fit, reserving heap for them, and takes fuel for writing 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));
|
||||
}
|
||||
self.unpaid += more;
|
||||
let fuel = (self.unpaid / UNITS_PER_FUEL) as u64;
|
||||
self.unpaid %= UNITS_PER_FUEL;
|
||||
if fuel > 0 {
|
||||
if interp.fuel < fuel {
|
||||
interp.fuel = 0;
|
||||
return Err(Abrupt::Limit(crate::LimitKind::Fuel));
|
||||
}
|
||||
// The last unit goes through `use_fuel`, which also checks for an interrupt.
|
||||
interp.fuel -= fuel - 1;
|
||||
interp.use_fuel()?;
|
||||
}
|
||||
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);
|
||||
interp.held_bytes = interp.held_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(())
|
||||
}
|
||||
|
||||
/// Appends `units` `count` times. Nothing is written for an empty `units`, however large
|
||||
/// `count` is.
|
||||
fn push_repeated(&mut self, interp: &mut Interp, units: &[u16], count: u64) -> JsResult<()> {
|
||||
if units.is_empty() || count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let more = usize::try_from(count).unwrap_or(usize::MAX).saturating_mul(units.len());
|
||||
self.make_room(interp, more)?;
|
||||
for _ in 0..count {
|
||||
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. From here on the heap counts them
|
||||
/// as the string's.
|
||||
fn finish(self, interp: &mut Interp) -> Value {
|
||||
interp.held_bytes = interp.held_bytes.saturating_sub(2 * self.charged);
|
||||
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<()> {
|
||||
let kinds: [(&'static str, NativeFn); 7] = [
|
||||
("Error", error_ctor),
|
||||
@@ -177,13 +369,40 @@ fn object_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> J
|
||||
Ok(Value::str(&format!("[object {class}]")))
|
||||
}
|
||||
|
||||
/// The Boolean a `Boolean.prototype` method was called on (sections 15.6.4.2 and 15.6.4.3): a
|
||||
/// primitive, or the primitive value of a Boolean object.
|
||||
fn this_boolean(interp: &mut Interp, this: &Value, name: &str) -> JsResult<bool> {
|
||||
let b = match this {
|
||||
Value::Bool(b) => Some(*b),
|
||||
Value::Object(o) => match interp.heap.object(*o).map(|x| &x.class) {
|
||||
Some(Class::Boolean(b)) => Some(*b),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
b.ok_or_else(|| interp.type_error(&format!("Boolean.prototype.{name} was called on a value that is not a Boolean")))
|
||||
}
|
||||
|
||||
/// `Boolean.prototype.toString` (section 15.6.4.2).
|
||||
fn boolean_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let b = this_boolean(interp, &this, "toString")?;
|
||||
Ok(Value::str(if b { "true" } else { "false" }))
|
||||
}
|
||||
|
||||
/// `Boolean.prototype.valueOf` (section 15.6.4.3).
|
||||
fn boolean_value_of(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
this_boolean(interp, &this, "valueOf").map(Value::Bool)
|
||||
}
|
||||
|
||||
/// `Object.prototype.toLocaleString` (section 15.2.4.3).
|
||||
fn object_to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = interp.to_object(&this)?;
|
||||
let f = interp.get_from(o, &key("toString"), &this)?;
|
||||
let receiver = Value::Object(o);
|
||||
let f = interp.get_from(o, &key("toString"), &receiver)?;
|
||||
if !interp.is_callable(&f) {
|
||||
return Err(interp.type_error("toString is not a function"));
|
||||
}
|
||||
interp.call(&f, &this, &[])
|
||||
interp.call(&f, &receiver, &[])
|
||||
}
|
||||
|
||||
fn object_value_of(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
@@ -283,11 +502,40 @@ 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();
|
||||
// A host function's `length` is an ordinary property a script can set, so it is converted
|
||||
// to an integer first, as later editions specify.
|
||||
let target_length = match interp.get_from(target, &key("length"), &this)? {
|
||||
Value::Number(n) if n.is_nan() => 0.0,
|
||||
Value::Number(n) => n.trunc(),
|
||||
_ => 0.0,
|
||||
};
|
||||
// Adding 0 turns a negative zero into 0.
|
||||
let length = (target_length - bound_args.len() as f64).max(0.0) + 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,854 @@
|
||||
//! `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 `separator`
|
||||
/// between them (`,` when it is `undefined`). `length` is read before the separator is converted,
|
||||
/// and the separator is converted even when `length` is 0 (section 15.4.4.5 steps 2 to 6). A cycle
|
||||
/// back to an object being joined joins as an empty string.
|
||||
fn join_with(interp: &mut Interp, o: ObjId, separator: &Value, 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)?;
|
||||
let sep = match separator {
|
||||
Value::Undefined => Str::from(","),
|
||||
v => interp.to_str(v)?,
|
||||
};
|
||||
if len == 0 {
|
||||
return Ok(Value::str(""));
|
||||
}
|
||||
let flat = sep.flat();
|
||||
let sep = &flat[..];
|
||||
// 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 };
|
||||
out.push_repeated(interp, sep, k.saturating_sub(seps))?;
|
||||
seps = k;
|
||||
out.push_units(interp, &s.flat())?;
|
||||
}
|
||||
out.push_repeated(interp, sep, u64::from(len - 1).saturating_sub(seps))?;
|
||||
Ok(out.finish(interp))
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let separator = arg(args, 0);
|
||||
with_this(interp, &this, |interp, o| join_with(interp, o, &separator, 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, &Value::Undefined, 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;
|
||||
// With no arguments each element is still read and written back in place (steps 6 and 7).
|
||||
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.temp_roots.push(v.clone());
|
||||
interp.reserve_heap(ELEMENT_BYTES)?;
|
||||
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)?;
|
||||
// Flattening a concatenation makes a new buffer, so it is charged. A key that is
|
||||
// not an element is rooted so that collections count it.
|
||||
let rope = s.buffer_addr().is_none();
|
||||
let bytes = crate::heap::string_bytes(s.len());
|
||||
if rope {
|
||||
interp.reserve_heap(bytes)?;
|
||||
}
|
||||
let units = s.flat();
|
||||
if rope {
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
}
|
||||
if !matches!(v, Value::String(_)) {
|
||||
interp.temp_roots.push(Value::String(s));
|
||||
}
|
||||
keyed.push((units, 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,364 @@
|
||||
//! 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. A
|
||||
/// concatenation is flattened into a new buffer, which is charged to the heap.
|
||||
pub(super) fn units_of(interp: &mut Interp, args: &[Value]) -> JsResult<std::rc::Rc<[u16]>> {
|
||||
let s = interp.to_str(&arg(args, 0))?;
|
||||
for _ in 0..s.len() {
|
||||
interp.use_fuel()?;
|
||||
}
|
||||
let rope = s.buffer_addr().is_none();
|
||||
let bytes = crate::heap::string_bytes(s.len());
|
||||
if rope {
|
||||
let mark = interp.temp_roots.len();
|
||||
interp.temp_roots.push(Value::String(s.clone()));
|
||||
let reserved = interp.reserve_heap(bytes);
|
||||
interp.temp_roots.truncate(mark);
|
||||
reserved?;
|
||||
}
|
||||
let units = s.flat();
|
||||
if rope {
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
}
|
||||
Ok(units)
|
||||
}
|
||||
|
||||
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 n = rest.iter().map_while(|&u| digit_value(u).filter(|&d| d < r)).count();
|
||||
let prefix = rest.get(..n).unwrap_or_default();
|
||||
if prefix.is_empty() {
|
||||
return Ok(Value::Number(f64::NAN));
|
||||
}
|
||||
let digits = prefix.iter().filter_map(|&u| digit_value(u));
|
||||
let value = match r {
|
||||
10 => parse_decimal(interp, prefix)?,
|
||||
2 | 4 | 8 | 16 | 32 => crate::chars::radix_value(digits, r).unwrap_or(f64::NAN),
|
||||
// Other radices may be approximated (section 15.1.2.2, step 13).
|
||||
_ => digits.fold(0.0, |n, d| n * f64::from(r) + f64::from(d)),
|
||||
};
|
||||
Ok(Value::Number(sign * value))
|
||||
}
|
||||
|
||||
/// The number that `text`, ASCII decimal digits with an optional fraction and exponent, stands
|
||||
/// for. The text is copied into a string of one byte per code unit for parsing, and that copy is
|
||||
/// checked against the heap limit first.
|
||||
fn parse_decimal(interp: &mut Interp, text: &[u16]) -> JsResult<f64> {
|
||||
interp.reserve_heap(text.len())?;
|
||||
let mut ascii = String::with_capacity(text.len());
|
||||
ascii.extend(text.iter().filter_map(|&u| char::from_u32(u32::from(u))));
|
||||
Ok(ascii.parse::<f64>().unwrap_or(f64::NAN))
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
Ok(Value::Number(sign * parse_decimal(interp, rest.get(..n).unwrap_or_default())?))
|
||||
}
|
||||
|
||||
/// `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(interp))
|
||||
}
|
||||
|
||||
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(interp))
|
||||
}
|
||||
|
||||
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(interp))
|
||||
}
|
||||
|
||||
/// `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(interp))
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
//! 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, Hint, Interp, JsResult};
|
||||
use crate::value::{Str, Value, number_to_str};
|
||||
|
||||
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
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 source = Value::String(interp.to_str(&arg(args, 0))?);
|
||||
let mark = interp.temp_roots.len();
|
||||
// The text stays rooted for the whole parse, so a collection still counts it.
|
||||
interp.temp_roots.push(source.clone());
|
||||
let parsed = super::global::units_of(interp, std::slice::from_ref(&source)).and_then(|text| parse_text(interp, &text));
|
||||
interp.temp_roots.truncate(mark);
|
||||
drop(source);
|
||||
let v = parsed?;
|
||||
let reviver = arg(args, 1);
|
||||
if !interp.is_callable(&reviver) {
|
||||
return Ok(v);
|
||||
}
|
||||
interp.temp_roots.push(v.clone());
|
||||
let r = 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))?;
|
||||
let bytes = string_bytes(units.len());
|
||||
interp.reserve_heap(bytes)?;
|
||||
// The name is counted while it waits for its value; `parse_text` releases it before `define`
|
||||
// accounts for it, and the native call's return releases it on every other path.
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
interp.held_bytes = interp.held_bytes.saturating_add(bytes);
|
||||
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.rooted(std::slice::from_ref(&v), |i| i.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.rooted(std::slice::from_ref(&v), |i| i.reserve_heap(ELEMENT_BYTES))?;
|
||||
let held = string_bytes(k.len());
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_sub(held);
|
||||
interp.held_bytes = interp.held_bytes.saturating_sub(held);
|
||||
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.rooted(std::slice::from_ref(&r), |i| {
|
||||
i.reserve_heap(ELEMENT_BYTES)?;
|
||||
i.define_own_property(o, k, &Desc::data(r.clone(), 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) => {
|
||||
interp.temp_roots.push(Value::Object(o));
|
||||
open(interp, &settings, &mut w, o)?;
|
||||
}
|
||||
}
|
||||
while !w.stack.is_empty() {
|
||||
step(interp, &settings, &mut w)?;
|
||||
}
|
||||
Ok(w.out.finish(interp))
|
||||
}
|
||||
|
||||
/// 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 = unwrap_primitive(interp, space, false)?.unwrap_or_else(|| 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.clone()),
|
||||
Value::Number(_) => Some(interp.primitive_to_str(&v)),
|
||||
Value::Object(_) => match unwrap_primitive(interp, &v, true)? {
|
||||
Some(Value::String(s)) => Some(s),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let Some(s) = item else { continue };
|
||||
// Flattening a concatenation makes a new buffer, so it is charged.
|
||||
let rope = s.buffer_addr().is_none();
|
||||
let bytes = string_bytes(s.len());
|
||||
if rope {
|
||||
interp.reserve_heap(bytes)?;
|
||||
}
|
||||
let k = s.flat();
|
||||
if seen.insert(k.clone()) {
|
||||
if rope {
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
}
|
||||
// The list lives until the call returns, so its names are rooted.
|
||||
interp.temp_roots.push(Value::String(s));
|
||||
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 {
|
||||
if let Some(Class::Boolean(b)) = interp.heap.object(*o).map(|x| &x.class) {
|
||||
v = Value::Bool(*b);
|
||||
} else if let Some(p) = unwrap_primitive(interp, &v, false)? {
|
||||
v = p;
|
||||
}
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
/// ToNumber of a Number object, or ToString of a String object (of either when `as_string` is
|
||||
/// set), as section 15.12.3 applies them in Str step 4, step 4.b.ii and step 5. Returns `None`
|
||||
/// for any other value. When the method that [[DefaultValue]] (section 8.12.8) calls first is not
|
||||
/// found on the object or on its prototype chain before the object's standard prototype, the
|
||||
/// result is the object's [[PrimitiveValue]] (sections 8.6.2 and 9.9), the value that
|
||||
/// `Number.prototype.valueOf` (15.7.4.4), `Number.prototype.toString` (15.7.4.2) and
|
||||
/// `String.prototype.toString` (15.5.4.2) return. Otherwise ToPrimitive calls the method found.
|
||||
fn unwrap_primitive(interp: &mut Interp, v: &Value, as_string: bool) -> JsResult<Option<Value>> {
|
||||
let Value::Object(o) = v else { return Ok(None) };
|
||||
let (primitive, proto, string) = match interp.heap.object(*o).map(|x| &x.class) {
|
||||
Some(Class::Number(n)) => (Value::Number(*n), interp.intrinsics.number_proto, as_string),
|
||||
Some(Class::String(s)) => (Value::String(s.clone()), interp.intrinsics.string_proto, true),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let (hint, name) = if string { (Hint::String, "toString") } else { (Hint::Number, "valueOf") };
|
||||
let p = if reaches_prototype(interp, *o, proto, name)? { primitive } else { interp.to_primitive(v, hint)? };
|
||||
Ok(Some(if string { Value::String(interp.primitive_to_str(&p)) } else { Value::Number(interp.to_number(&p)?) }))
|
||||
}
|
||||
|
||||
/// Whether looking up `name` from `o` reaches the object `proto` on the prototype chain before
|
||||
/// any object on the way, `proto` included, has its own property `name`.
|
||||
fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult<bool> {
|
||||
let k = key(name);
|
||||
let mut cur = Some(o);
|
||||
while let Some(id) = cur {
|
||||
interp.use_fuel()?;
|
||||
if interp.get_own_property(id, &k)?.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
if id == proto {
|
||||
return Ok(true);
|
||||
}
|
||||
cur = interp.heap.object(id).and_then(|x| x.proto);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// 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`. The caller pushes `o` onto the temporary roots first, and `step` pops it when the
|
||||
/// object or array is closed.
|
||||
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"));
|
||||
}
|
||||
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;
|
||||
}
|
||||
// The separator and the key reserve heap, so a container is rooted before they are written.
|
||||
if let Item::Container(o) = item {
|
||||
interp.temp_roots.push(Value::Object(o));
|
||||
}
|
||||
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,208 @@
|
||||
//! 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};
|
||||
use crate::interp::{Interp, JsResult};
|
||||
use crate::value::Value;
|
||||
|
||||
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
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,879 @@
|
||||
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");
|
||||
// Steps 3.c and 3.d convert the new length once with ToUint32 and once with ToNumber.
|
||||
let counted = "var calls = 0; var len = {valueOf: function () { calls++; return 2; }}; var a = [1, 2, 3];";
|
||||
assert_eq!(num(&format!("{counted} Object.defineProperty(a, 'length', {{value: len}}); calls * 10 + a.length")), 22.0);
|
||||
assert_eq!(num(&format!("{counted} a.length = len; calls * 10 + a.length")), 22.0);
|
||||
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("Object.getOwnPropertyNames([0, 1, 2]).join()"), "0,1,2,length");
|
||||
assert_eq!(string("var a = [5]; a.k = 1; Object.getOwnPropertyNames(a).join()"), "0,k,length");
|
||||
// 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]'");
|
||||
// A host function has no own `length`, so a script can define one, and bind converts it to
|
||||
// an integer.
|
||||
let mut realm = Realm::new();
|
||||
realm.define_function("host", |_| Ok(JsValue::Undefined));
|
||||
let src = "function len(v) { Object.defineProperty(host, 'length', {value: v, configurable: true}); return host; } \
|
||||
var a = len(2.9).bind(null).length; var b = 1 / len(-0.5).bind(null).length; var c = len(NaN).bind(null).length; \
|
||||
[a, b, c, len(Infinity).bind(null, 1).length].join()";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::String("2,Infinity,0,Infinity".into())));
|
||||
}
|
||||
|
||||
// ---- 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");
|
||||
// With no arguments, unshift still reads and writes each element (section 15.4.4.13).
|
||||
assert_eq!(
|
||||
run("var n = 0, a = []; Object.defineProperty(a, 0, {get: function () { n++; return 1; }, set: function () { n += 10; }}); a.unshift(); n"),
|
||||
JsValue::Number(11.0)
|
||||
);
|
||||
throws("var a = [1, 2]; Object.defineProperty(a, 0, {writable: false}); a.unshift()", "TypeError");
|
||||
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,,");
|
||||
// A primitive element is converted with ToObject and its toLocaleString is called on the object.
|
||||
assert_eq!(
|
||||
string(
|
||||
"Object.getPrototypeOf(Object(1)).toLocaleString = function () { 'use strict'; return typeof this; }; [1, undefined, 2].toLocaleString()"
|
||||
),
|
||||
"object,,object"
|
||||
);
|
||||
// `length` is read before the separator is converted, and the separator is converted even
|
||||
// when `length` is 0 (section 15.4.4.5 steps 2 to 6).
|
||||
assert_eq!(
|
||||
string(
|
||||
"var log = ''; Array.prototype.join.call({length: {valueOf: function () { log += 'L'; return 0; }}}, {toString: function () { log += 'S'; return ','; }}); log"
|
||||
),
|
||||
"LS"
|
||||
);
|
||||
assert_eq!(string("var log = ''; try { [].join({toString: function () { throw 1; }}); } catch (e) { log = 'threw ' + e; } log"), "threw 1");
|
||||
}
|
||||
|
||||
#[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)),
|
||||
// An empty separator is not written once per hole, so a thousand such joins finish.
|
||||
("var b = []; b[4294967294] = 'z'; for (var i = 0; i < 1000; i++) new Array(4294967295).join('') + b.join(''); i", JsValue::Number(1000.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 keys_built_ins_make_count_toward_the_heap_limit() {
|
||||
// Each key is a new 1 MiB string, so 64 of them do not fit in an 8 MiB heap.
|
||||
let limits = Limits { heap_bytes: 8 << 20, ..Limits::default() };
|
||||
let big = "var s = 'x'; for (var i = 0; i < 19; i++) s += s; ";
|
||||
let sort = format!("{big} var a = []; for (var i = 0; i < 64; i++) a[i] = {{n: i, toString: function () {{ return s + this.n; }}}}; a.sort();");
|
||||
assert_eq!(limit(&mut small(limits.clone()), &sort), LimitKind::Heap);
|
||||
let list = format!(
|
||||
"{big} var a = [], c = 0; for (var i = 0; i < 64; i++) Object.defineProperty(a, i, {{get: function () {{ return s + c++; }}}}); JSON.stringify({{}}, a);"
|
||||
);
|
||||
assert_eq!(limit(&mut small(limits), &list), LimitKind::Heap);
|
||||
// Keys that share a string's storage are not charged again: with half the string's size to
|
||||
// spare, sorting objects whose keys are that string still fits.
|
||||
let setup = format!(
|
||||
"{big} var keep = [s + 0, s + 1]; keep.sort(); var a = []; for (var i = 0; i < 64; i++) a[i] = s; a.sort(); \
|
||||
var b = []; for (var i = 0; i < 64; i++) b[i] = {{toString: function () {{ return s; }}}}; 0"
|
||||
);
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(&setup), Ok(JsValue::Number(0.0)));
|
||||
measure.interp().collect_garbage();
|
||||
let live = measure.interp().heap.bytes;
|
||||
let mut realm = small(Limits { heap_bytes: live + (512 << 10), ..Limits::default() });
|
||||
assert_eq!(realm.run(&setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(realm.run("b.sort(); JSON.stringify({}, a); JSON.stringify({}, [s, s, s]); b.length"), Ok(JsValue::Number(64.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn define_properties_checks_the_heap_before_each_new_property() {
|
||||
// `src` takes most of the heap; copying its 20,000 names onto a new object does not fit, and
|
||||
// the run ends before the heap estimate passes the limit.
|
||||
let setup = "var d = {value: 1}; var src = {}; for (var i = 0; i < 20000; i++) src['k' + i] = d; 0";
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0)));
|
||||
measure.interp().collect_garbage();
|
||||
let heap_bytes = measure.interp().heap.bytes + (1 << 20);
|
||||
let mut realm = small(Limits { heap_bytes, ..Limits::default() });
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(limit(&mut realm, "var o = Object.defineProperties({}, src); 0"), LimitKind::Heap);
|
||||
assert!(realm.interp().heap.bytes <= heap_bytes, "{} > {heap_bytes}", realm.interp().heap.bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_heap_limit_smaller_than_the_built_ins_still_installs_every_built_in() {
|
||||
// The heap limit bounds what scripts make, not the realm's own built-ins: a realm made with a
|
||||
// tiny limit refuses runs with the heap limit, and has every built-in once the limit is raised.
|
||||
let mut realm = small(Limits { heap_bytes: 1, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "[]"), LimitKind::Heap);
|
||||
realm.set_limits(Limits::default());
|
||||
assert_eq!(
|
||||
realm.run("typeof Math.max + typeof JSON.stringify + typeof parseInt + typeof escape + typeof decodeURIComponent"),
|
||||
Ok(JsValue::String("functionfunctionfunctionfunctionfunction".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_locale_string_calls_to_string_on_the_boxed_value() {
|
||||
assert_eq!(
|
||||
string(
|
||||
"Object.defineProperty(Object.getPrototypeOf(Object(5)), 'toString', {value: function () { 'use strict'; return typeof this; }}); \
|
||||
Object.prototype.toLocaleString.call(5)"
|
||||
),
|
||||
"object"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_locale_string_writes_the_primitive_of_a_boxed_value() {
|
||||
assert_eq!(string("[1, 'x', true, false, 1.5].toLocaleString()"), "1,x,true,false,1.5");
|
||||
assert_eq!(string("Object.prototype.toLocaleString.call(5)"), "5");
|
||||
assert_eq!(string("Object.prototype.toLocaleString.call(Object('s'))"), "s");
|
||||
// Object.prototype.toString comes after the standard prototypes on the chain.
|
||||
assert_eq!(string("Object.prototype.toString = function () { return 'z'; }; [1, 'x', true].toLocaleString()"), "1,x,true");
|
||||
// A toString on the object or on its standard prototype is called.
|
||||
assert_eq!(string("var b = Object(true); b.toString = function () { return 'own'; }; [b].toLocaleString()"), "own");
|
||||
assert_eq!(string("Object.getPrototypeOf(true).toString = function () { return 'p'; }; [true].toLocaleString()"), "p");
|
||||
assert_eq!(string("Object.getPrototypeOf(true).toString = Object.prototype.toString; [true].toLocaleString()"), "[object Boolean]");
|
||||
// Other objects still reach Object.prototype.toString.
|
||||
assert_eq!(string("Object.prototype.toLocaleString.call({})"), "[object Object]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boolean_prototype_converts_booleans() {
|
||||
assert_eq!(string("true.toString() + false.toString()"), "truefalse");
|
||||
assert_eq!(string("Object(false).toString()"), "false");
|
||||
truth("true.valueOf() === true && Object(false).valueOf() === false");
|
||||
assert_eq!(num("Object(true) + 1"), 2.0);
|
||||
assert_eq!(string("'' + Object(false)"), "false");
|
||||
truth("Object.getPrototypeOf(true).valueOf() === false && Object.getPrototypeOf(true).toString() === 'false'");
|
||||
throws("Object.getPrototypeOf(true).toString.call(1)", "TypeError");
|
||||
throws("Object.getPrototypeOf(true).valueOf.call({})", "TypeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_methods_overwriting_elements_need_no_heap_for_new_ones() {
|
||||
// With the heap exactly at its limit, an array method's write over an existing element
|
||||
// succeeds, and a write that adds an element stops with the heap limit.
|
||||
use crate::heap::key;
|
||||
use crate::interp::Abrupt;
|
||||
use crate::value::Value;
|
||||
let mut realm = small(Limits::default());
|
||||
assert_eq!(realm.run("var a = [1, 2]; 0"), Ok(JsValue::Number(0.0)));
|
||||
let interp = realm.interp();
|
||||
let global = interp.global;
|
||||
let a = match interp.get_from(global, &key("a"), &Value::Object(global)) {
|
||||
Ok(Value::Object(a)) => a,
|
||||
other => panic!("{other:?}"),
|
||||
};
|
||||
interp.collect_garbage();
|
||||
interp.limits.heap_bytes = interp.heap.bytes;
|
||||
assert!(crate::builtins::put_at(interp, a, 0.0, Value::Number(5.0)).is_ok());
|
||||
assert!(matches!(crate::builtins::put_at(interp, a, 2.0, Value::Number(5.0)), Err(Abrupt::Limit(LimitKind::Heap))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_realm_whose_built_ins_failed_to_install_runs_nothing() {
|
||||
let mut realm = Realm::new();
|
||||
realm.interp().install_error = Some("the built-ins could not be installed: test".into());
|
||||
match realm.run("var ran = true; 1") {
|
||||
Err(RunError { kind: RunErrorKind::Internal, message, .. }) => assert_eq!(message, "the built-ins could not be installed: test"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
realm.interp().install_error = None;
|
||||
assert_eq!(realm.run("typeof ran"), Ok(JsValue::String("undefined".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_parsing_counts_its_copy_toward_the_heap_limit() {
|
||||
// `s` is 1 Mi digits, so parsing it in base 10 copies 1 MiB, which does not fit in the
|
||||
// 512 KiB left. Bases 16 and 36 read the digits where they are and still fit, but
|
||||
// flattening a concatenation of `s` makes a 2 MiB buffer, which does not.
|
||||
let setup = "var s = new Array((1 << 20) + 1).join('1'); var pad = new Array(800000).join('2'); 0";
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0)));
|
||||
measure.interp().collect_garbage();
|
||||
let heap_bytes = measure.interp().heap.bytes + (512 << 10);
|
||||
assert!(heap_bytes >= 4 << 20, "{heap_bytes}");
|
||||
let limits = Limits { heap_bytes, ..Limits::default() };
|
||||
let mut realm = small(limits.clone());
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(
|
||||
realm.run("parseInt(s, 16) > 0 && parseInt(s, 36) > 0 && parseInt('12') === 12 && parseFloat('1.5') === 1.5"),
|
||||
Ok(JsValue::Bool(true))
|
||||
);
|
||||
assert_eq!(limit(&mut realm, "parseInt(s)"), LimitKind::Heap);
|
||||
let mut realm = small(limits.clone());
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(limit(&mut realm, "parseInt(s + '1', 16)"), LimitKind::Heap);
|
||||
let mut realm = small(limits);
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(limit(&mut realm, "parseFloat(s)"), LimitKind::Heap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_built_ins_write_take_fuel_and_stay_counted_across_collections() {
|
||||
// Each join writes 1 Mi code units, so a thousand of them do not fit in 2,000,000 units of
|
||||
// fuel, whether the units are separators or a string's contents.
|
||||
let limits = Limits { heap_bytes: 8 << 20, fuel: 2_000_000, ..Limits::default() };
|
||||
assert_eq!(limit(&mut small(limits.clone()), "var a = new Array((1 << 20) + 1); for (var i = 0; i < 1000; i++) a.join('x'); i"), LimitKind::Fuel);
|
||||
assert_eq!(
|
||||
limit(&mut small(limits), "var a = [new Array((1 << 20) + 1).join('x')]; for (var i = 0; i < 1000; i++) a.join(''); i"),
|
||||
LimitKind::Fuel
|
||||
);
|
||||
// `join` holds a 2 MiB buffer for `s` while the second element's `toString` keeps new objects.
|
||||
// A collection in between still counts the buffer, so the objects and the buffer together do
|
||||
// not fit in a heap with 1 MiB to spare beyond the objects.
|
||||
let setup = "var s = new Array((1 << 20) + 1).join('x'); var keep = []; 0";
|
||||
let fill = "for (var i = 0; i < 30000; i++) keep.push({});";
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(&format!("{setup}; {fill} 0")), Ok(JsValue::Number(0.0)));
|
||||
measure.interp().collect_garbage();
|
||||
let heap_bytes = measure.interp().heap.bytes + (1 << 20);
|
||||
let mut realm = small(Limits { heap_bytes, ..Limits::default() });
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
let join = format!("var f = {{toString: function () {{ {fill} return ''; }}}}; [s, f].join('').length");
|
||||
assert_eq!(limit(&mut realm, &join), LimitKind::Heap);
|
||||
// A join that throws gives its buffer back, so a hundred of them still fit in 8 MiB.
|
||||
let mut realm = small(Limits { heap_bytes: 8 << 20, ..Limits::default() });
|
||||
let src = "var s = new Array((1 << 20) + 1).join('x'); var t = {toString: function () { throw 1; }}; \
|
||||
for (var i = 0; i < 100; i++) { try { [s, t].join(''); } catch (e) {} } i";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(100.0)));
|
||||
}
|
||||
|
||||
#[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)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_values_survive_a_collection_while_they_are_stored() {
|
||||
// Below 1 MiB the only collections run at the heap limit, many of them while a built-in
|
||||
// reserves room for a value that a callback has just made and nothing else holds.
|
||||
let limits = Limits { heap_bytes: 768 << 10, ..Limits::default() };
|
||||
let src = "var bad = 0; \
|
||||
for (var n = 0; n < 2000; n++) { \
|
||||
var m = [1, 2, 3, 4, 5, 6, 7, 8].map(function (x) { var g = {x: x}; return {v: x}; }); \
|
||||
var f = [1, 2, 3, 4].filter(function () { return true; }).map(function (x) { return [x]; }); \
|
||||
for (var i = 0; i < 8; i++) if (!m[i] || m[i].v !== i + 1) bad++; \
|
||||
for (var i = 0; i < 4; i++) if (!f[i] || f[i][0] !== i + 1) bad++; \
|
||||
} bad";
|
||||
let mut realm = small(limits.clone());
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(0.0)));
|
||||
assert!(realm.interp().heap.collections > 0);
|
||||
// Getters make a new object for each read, which `shift` and `sort` then store.
|
||||
let src = "function fresh(v) { var g = {x: v}; return {v: v}; } \
|
||||
function set(i) { return function (x) { Object.defineProperty(this, i, {value: x, writable: true, enumerable: true, configurable: true}); }; } \
|
||||
var p = {}; \
|
||||
for (var i = 0; i < 4; i++) Object.defineProperty(p, i, {get: (function (v) { return function () { return fresh(v); }; })(4 - i), set: set(i)}); \
|
||||
var bad = 0; \
|
||||
for (var n = 0; n < 1000; n++) { \
|
||||
var o = Object.create(p); o.length = 4; Array.prototype.shift.call(o); \
|
||||
if (!o[0] || o[0].v !== 3 || o[2].v !== 1) bad++; \
|
||||
var s = Object.create(p); s.length = 4; Array.prototype.sort.call(s, function (a, b) { return a.v - b.v; }); \
|
||||
for (var i = 0; i < 4; i++) if (!s[i] || s[i].v !== i + 1) bad++; \
|
||||
} \
|
||||
var q = {}; \
|
||||
for (var i = 0; i < 4; i++) Object.defineProperty(q, i, {get: (function (v) { return function () { return {v: v}; }; })(4 - i), set: function () {}}); \
|
||||
for (var n = 0; n < 20000; n++) { \
|
||||
var u = Object.create(q); u.length = 4; Array.prototype.unshift.call(u, 0, 0, 0, 0); \
|
||||
for (var i = 4; i < 8; i++) if (!u.hasOwnProperty(i) || u[i].v !== 8 - i) bad++; \
|
||||
} \
|
||||
for (var n = 0; n < 5000; n++) { var t = Object.create(q); t.length = 4; Array.prototype.sort.call(t); } \
|
||||
bad";
|
||||
let mut realm = small(limits.clone());
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(0.0)));
|
||||
assert!(realm.interp().heap.collections > 0);
|
||||
// The `z` objects change each round's garbage, so the collections fall at different points.
|
||||
let src = "var bad = 0; \
|
||||
for (var n = 0; n < 10000; n++) { \
|
||||
for (var z = 0; z < n % 7; z++) var g = {z: z}; \
|
||||
var j = JSON.parse('[1, 2, 3, 4, 5, 6, 7, 8]', function (k, v) { return k === '' ? v : {v: v}; }); \
|
||||
for (var i = 0; i < 8; i++) if (!j[i] || j[i].v !== i + 1) bad++; \
|
||||
} bad";
|
||||
let mut realm = small(limits.clone());
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(0.0)));
|
||||
assert!(realm.interp().heap.collections > 0);
|
||||
let src = "var k = ''; for (var i = 0; i < 300; i++) k += 'k'; \
|
||||
var o = {}; o[k] = {toJSON: function () { var g = {x: 1}; return {a: 1}; }}; \
|
||||
var want = '{\"' + k + '\":{\"a\":1}}'; \
|
||||
var bad = 0; for (var n = 0; n < 4000; n++) if (JSON.stringify(o) !== want) bad++; bad";
|
||||
let mut realm = small(limits);
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(0.0)));
|
||||
assert!(realm.interp().heap.collections > 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!(num("JSON.parse('\"\\\\ud800\"').charCodeAt(0)"), 55296.0);
|
||||
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]");
|
||||
// Number and String objects go through ToNumber and ToString, which call valueOf and toString.
|
||||
assert_eq!(string("var n = Object(5); n.valueOf = function () { return 7; }; JSON.stringify([n])"), "[7]");
|
||||
assert_eq!(string("var s = Object('x'); s.toString = function () { return 'y'; }; JSON.stringify({s: s})"), "{\"s\":\"y\"}");
|
||||
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 json_stringify_unwraps_number_string_and_boolean_objects() {
|
||||
assert_eq!(string("JSON.stringify(Object(3))"), "3");
|
||||
assert_eq!(string("JSON.stringify(Object('abc'))"), "\"abc\"");
|
||||
assert_eq!(string("JSON.stringify(Object(true))"), "true");
|
||||
assert_eq!(string("JSON.stringify({n: Object(-0), m: Object(NaN), s: Object('')})"), "{\"n\":0,\"m\":null,\"s\":\"\"}");
|
||||
assert_eq!(string("JSON.stringify([Object.getPrototypeOf(Object(3)), Object.getPrototypeOf(Object('abc'))])"), "[0,\"\"]");
|
||||
// The space argument (step 5).
|
||||
assert_eq!(string("JSON.stringify([1], null, Object(2))"), "[\n 1\n]");
|
||||
assert_eq!(string("JSON.stringify([1], null, Object('--'))"), "[\n--1\n]");
|
||||
// Replacer list entries (step 4.b.ii).
|
||||
assert_eq!(string("JSON.stringify({a: 1, 3: 2, b: 3}, [Object('a'), Object(3)])"), "{\"a\":1,\"3\":2}");
|
||||
// Methods on Object.prototype do not reach the objects; methods on their own prototypes do.
|
||||
assert_eq!(string("Object.prototype.toString = function () { return 'z'; }; JSON.stringify(Object('abc'))"), "\"abc\"");
|
||||
assert_eq!(string("Object.prototype.valueOf = function () { return 9; }; JSON.stringify(Object(3))"), "3");
|
||||
assert_eq!(string("Object.getPrototypeOf(Object(1)).valueOf = function () { return 4; }; JSON.stringify(Object(3))"), "4");
|
||||
assert_eq!(string("Object.getPrototypeOf(Object('')).toString = function () { return 'q'; }; JSON.stringify(Object('abc'))"), "\"q\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_parse_counts_a_string_while_it_reserves_the_property_for_it() {
|
||||
// The limits around the parsed value's size include one where the reservation for the string's
|
||||
// property is what starts a collection. The text is a concatenation, so the parse also flattens
|
||||
// it. Whenever the parse fits, the heap total counts everything that is live.
|
||||
let setup = |text: &str| format!("var s = new Array(4097).join('x'); var text = {text}; 0");
|
||||
for text in ["'[\"' + s + '\"]'", "'{\"k\":\"' + s + '\"}'"] {
|
||||
let setup = setup(text);
|
||||
let parse = "var a = JSON.parse(text); 0";
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(&setup), Ok(JsValue::Number(0.0)));
|
||||
measure.interp().collect_garbage();
|
||||
assert_eq!(measure.run(parse), Ok(JsValue::Number(0.0)));
|
||||
let full = measure.interp().heap.bytes;
|
||||
let (mut fits, mut over) = (0, 0);
|
||||
for heap_bytes in (full - 8192..=full + 256).step_by(4) {
|
||||
let mut realm = small(Limits { heap_bytes, ..Limits::default() });
|
||||
assert_eq!(realm.run(&setup), Ok(JsValue::Number(0.0)));
|
||||
realm.interp().collect_garbage();
|
||||
match realm.run(parse) {
|
||||
Ok(_) => {
|
||||
let counted = realm.interp().heap.bytes;
|
||||
let live = realm.collect_garbage();
|
||||
assert!(counted >= live, "{text}: {counted} bytes counted with {live} live under a limit of {heap_bytes}");
|
||||
fits += 1;
|
||||
}
|
||||
Err(RunError { kind: RunErrorKind::Limit(LimitKind::Heap), .. }) => over += 1,
|
||||
Err(e) => panic!("{text}: {e:?}"),
|
||||
}
|
||||
}
|
||||
assert!(fits > 0 && over > 0, "{text}: {fits} limits fit and {over} did not");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_parse_keeps_its_text_counted_for_the_whole_parse() {
|
||||
// The text exists only as the ToString result of the argument, and the parsed value needs a
|
||||
// collection. Without the text the value fits under the limit; with it the value does not.
|
||||
let setup = "var ws = new Array(8193).join(' '); var items = new Array(257).join('{},'); \
|
||||
var o = {toString: function () { return ws + '[' + items + '{}]'; }}; 0";
|
||||
let parse = "var a = JSON.parse(o); 0";
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0)));
|
||||
measure.interp().collect_garbage();
|
||||
assert_eq!(measure.run(parse), Ok(JsValue::Number(0.0)));
|
||||
let full = measure.interp().heap.bytes;
|
||||
let text = 2 * (8192 + 2 + 3 * 256 + 2);
|
||||
let mut realm = small(Limits { heap_bytes: full - text / 2, ..Limits::default() });
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
realm.interp().collect_garbage();
|
||||
assert_eq!(limit(&mut realm, parse), LimitKind::Heap);
|
||||
// The text is released before the reviver runs, so the reviver can use the room it took.
|
||||
let setup = "var ws = new Array(65537).join(' '); var o = {toString: function () { return ws + '1'; }}; 0";
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0)));
|
||||
let heap_bytes = measure.collect_garbage() + 2 * 65537 + (16 << 10);
|
||||
let mut realm = small(Limits { heap_bytes, ..Limits::default() });
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(realm.run("JSON.parse(o, function () { return new Array(40001).join('x').length; })"), Ok(JsValue::Number(40000.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_parse_counts_names_that_wait_for_their_values() {
|
||||
// An unterminated text: every name read waits for a value that never comes, so nothing but
|
||||
// the parser holds them, and the names together pass the limit before the text ends.
|
||||
let setup = "var k = new Array(2049).join('k'); var text = new Array(33).join('{\"' + k + '\":'); 0";
|
||||
let mut measure = small(Limits::default());
|
||||
assert_eq!(measure.run(setup), Ok(JsValue::Number(0.0)));
|
||||
let heap_bytes = measure.collect_garbage() + (96 << 10);
|
||||
let mut realm = small(Limits { heap_bytes, ..Limits::default() });
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(limit(&mut realm, "JSON.parse(text)"), LimitKind::Heap);
|
||||
// A complete text gives its names back as it stores them, so one that fits parses.
|
||||
let mut realm = small(Limits { heap_bytes: heap_bytes + (64 << 10), ..Limits::default() });
|
||||
assert_eq!(realm.run(setup), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(realm.run("var a = JSON.parse('{\"' + k + '\":{\"' + k + 'x\":1}}'); 0"), Ok(JsValue::Number(0.0)));
|
||||
}
|
||||
|
||||
#[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,8 +207,8 @@ 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 and its number.
|
||||
Number(f64),
|
||||
String(Str),
|
||||
@@ -213,18 +216,24 @@ pub(crate) enum Class {
|
||||
/// A `Date` object and its time value.
|
||||
Date(f64),
|
||||
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",
|
||||
@@ -292,6 +301,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;
|
||||
|
||||
@@ -308,6 +319,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 {
|
||||
@@ -524,11 +539,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::RegExp(re) => {
|
||||
@@ -542,7 +566,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 {
|
||||
|
||||
+376
-28
@@ -164,6 +164,17 @@ 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>,
|
||||
/// Bytes of the string buffers native functions are building outside the heap. They are
|
||||
/// charged to `heap.bytes` and added back after each collection, and a native call's
|
||||
/// buffers are released when it returns.
|
||||
pub(crate) held_bytes: usize,
|
||||
/// Why installing the built-ins failed; a realm with this set runs no script.
|
||||
pub(crate) install_error: Option<String>,
|
||||
/// The clock and time zone `Date` reads.
|
||||
pub(crate) date_host: crate::date_builtins::DateHost,
|
||||
/// The bytes native code holds outside the heap's cells through [`HeldBytes`].
|
||||
@@ -239,6 +250,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)
|
||||
@@ -341,6 +361,10 @@ impl Interp {
|
||||
source_units: 0,
|
||||
throw_site: None,
|
||||
last_thrown: Value::Undefined,
|
||||
random: random_seed(),
|
||||
join_stack: Vec::new(),
|
||||
held_bytes: 0,
|
||||
install_error: None,
|
||||
date_host: crate::date_builtins::DateHost::default(),
|
||||
held: Rc::default(),
|
||||
};
|
||||
@@ -515,7 +539,7 @@ impl Interp {
|
||||
self.heap.collect(&roots);
|
||||
// The collection counts the live total again, so bytes already released are not in it.
|
||||
self.held.released.set(0);
|
||||
self.heap.bytes = self.heap.bytes.saturating_add(self.held.now.get());
|
||||
self.heap.bytes = self.heap.bytes.saturating_add(self.held_bytes).saturating_add(self.held.now.get());
|
||||
}
|
||||
|
||||
/// Runs frames until the boundary frame on top returns, and returns its value.
|
||||
@@ -1058,13 +1082,219 @@ 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");
|
||||
}
|
||||
// The object and the descriptor's values stay rooted while the heap is checked.
|
||||
let mut roots = vec![Value::Object(id)];
|
||||
roots.extend(desc.value.iter().cloned());
|
||||
roots.extend(desc.get.flatten().into_iter().chain(desc.set.flatten()).map(Value::Object));
|
||||
self.rooted(&roots, |i| i.reserve_heap(binding_bytes(k.len())))?;
|
||||
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
|
||||
/// `length` first, then the rest in the order they were added, which is the order `for-in`
|
||||
/// visits them, then an array's `length`. 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 is_string = matches!(o.class, Class::String(_));
|
||||
let is_array = matches!(o.class, Class::Array { .. });
|
||||
let count = o.props.len().saturating_add(string_len).saturating_add(usize::from(is_string || is_array));
|
||||
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 is_string {
|
||||
out.push(key("length"));
|
||||
}
|
||||
for _ in 0..self.object(id)?.props.len() {
|
||||
self.use_fuel()?;
|
||||
}
|
||||
out.extend(self.object(id)?.props.keys());
|
||||
if is_array {
|
||||
out.push(key("length"));
|
||||
}
|
||||
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));
|
||||
@@ -1131,32 +1361,19 @@ impl Interp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assigning to an array's `length` (section 15.4.5.1).
|
||||
/// Assigning to an array's `length` (section 15.4.5.1). [[Put]] defines the value, so the
|
||||
/// value is converted once with ToUint32 and once with ToNumber, as in steps 3.c and 3.d.
|
||||
fn set_array_length(&mut self, id: ObjId, v: &Value, throw: bool) -> JsResult<()> {
|
||||
let n = self.to_number(v)?;
|
||||
let new = to_uint32(n);
|
||||
if f64::from(new) != n {
|
||||
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 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(())
|
||||
}
|
||||
@@ -1576,13 +1793,25 @@ impl Interp {
|
||||
this = args.first().cloned().unwrap_or(Value::Undefined);
|
||||
args = self.list_from_array_like(&list)?;
|
||||
}
|
||||
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);
|
||||
}
|
||||
Func::Native { f: native, .. } => {
|
||||
// A list from `apply` is not on the operand stack, and a native function can run
|
||||
// JavaScript that collects, so the arguments stay rooted for the call.
|
||||
// A list from `apply` and the arguments a bound function adds are not on the
|
||||
// operand stack, and a native function can run JavaScript that collects, so the
|
||||
// arguments stay rooted for the call.
|
||||
let mark = self.temp_roots.len();
|
||||
self.temp_roots.push(this.clone());
|
||||
self.temp_roots.extend(args.iter().cloned());
|
||||
let held = self.held_bytes;
|
||||
let r = native(self, this, &args, false);
|
||||
self.held_bytes = held;
|
||||
self.temp_roots.truncate(mark);
|
||||
return Ok(Prepared::Done(r?));
|
||||
}
|
||||
@@ -1660,8 +1889,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") };
|
||||
@@ -1674,7 +1912,10 @@ impl Interp {
|
||||
self.enter_function(Entry { callee, code, env, this: Value::Object(obj), args }, Some(obj), false)
|
||||
}
|
||||
Some(Func::Native { f: native, constructor: true, .. }) => {
|
||||
let v = native(self, Value::Undefined, &args, true)?;
|
||||
let held = self.held_bytes;
|
||||
let r = native(self, Value::Undefined, &args, true);
|
||||
self.held_bytes = held;
|
||||
let v = r?;
|
||||
self.stack.truncate(start);
|
||||
self.push(v)
|
||||
}
|
||||
@@ -2081,7 +2322,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(n) => (Class::Number(*n), self.intrinsics.number_proto),
|
||||
Value::String(s) => (Class::String(s.clone()), self.intrinsics.string_proto),
|
||||
};
|
||||
@@ -2226,7 +2467,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"));
|
||||
@@ -2301,6 +2548,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) {
|
||||
|
||||
+12
-6
@@ -24,12 +24,16 @@
|
||||
//! 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 these
|
||||
//! built-ins: `Object` and `Object.prototype`'s basic methods, `Function` with
|
||||
//! `Function.prototype.call` and `apply`, `eval`, the error constructors, and `String`, `Number`,
|
||||
//! `RegExp` and `Date` (sections 15.5, 15.7, 15.9 and 15.10). Regular expressions run on a
|
||||
//! backtracking matcher that keeps its choice points on its own stack and takes fuel for every
|
||||
//! step; `Date` reads the clock and time zone the host sets with [`Realm::set_clock`] and
|
||||
//! [`Realm::set_time_zone`]. Every run is bounded
|
||||
//! 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), `String`
|
||||
//! (15.5), `Boolean.prototype`'s `toString` and `valueOf` (15.6.4), `Number` (15.7), `Math` (15.8),
|
||||
//! `Date` (15.9), `RegExp` (15.10), the error types (15.11) 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. Regular expressions run on a backtracking matcher that keeps its choice points
|
||||
//! on its own stack and takes fuel for every step; `Date` reads the clock and time zone the host
|
||||
//! sets with [`Realm::set_clock`] and [`Realm::set_time_zone`]. 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.
|
||||
@@ -178,6 +182,8 @@ impl LineTable {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod builtins_tests;
|
||||
#[cfg(test)]
|
||||
mod interp_tests;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -170,9 +170,13 @@ impl Realm {
|
||||
}
|
||||
|
||||
/// Runs `source` as a script in the global environment and returns its completion value (the
|
||||
/// value of the last expression statement that ran).
|
||||
/// value of the last expression statement that ran). A realm whose built-ins could not be
|
||||
/// installed runs nothing and gives an internal error.
|
||||
pub fn run(&mut self, source: &str) -> Result<JsValue, RunError> {
|
||||
let interp = &mut self.interp;
|
||||
if let Some(message) = &interp.install_error {
|
||||
return Err(RunError { kind: RunErrorKind::Internal, message: message.clone(), line: 0, column: 0 });
|
||||
}
|
||||
interp.fuel = interp.limits.fuel;
|
||||
interp.source_units = 0;
|
||||
interp.throw_site = None;
|
||||
@@ -188,6 +192,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,89 @@
|
||||
//! 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 samples leave out U+2028 and U+2029: the polyfill escapes them, and section 15.12.3 does not
|
||||
//! escape them.
|
||||
//!
|
||||
//! 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();
|
||||
match realm.run(NEEDS) {
|
||||
Ok(JsValue::Bool(true)) => {}
|
||||
Ok(JsValue::Bool(false)) => {
|
||||
println!("json2: SKIPPED (the interpreter lacks Date, String.prototype.replace or RegExp, which json2.js uses)");
|
||||
return;
|
||||
}
|
||||
other => panic!("json2: the built-in check failed: {other:?}"),
|
||||
}
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
+77
-14
@@ -20,10 +20,13 @@
|
||||
//! `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 under `test/built-ins/String`, `Number`, `RegExp` and `Date` the same
|
||||
//! way, counting the tests that name an ES5 section apart from the other tests without a feature
|
||||
//! tag; its failures go to `builtins-failures.txt`. The interpreter tests fail when the
|
||||
//! interpreter panics, when a harness or test file cannot be read, or when no ES5 test runs.
|
||||
//! 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`. A fourth test runs the tests under
|
||||
//! `test/built-ins/String`, `Number`, `RegExp` and `Date` the same way, counting the tests that name
|
||||
//! an ES5 section apart from the other tests without a feature tag; its failures go to
|
||||
//! `builtins-failures.txt`. The interpreter tests fail when the interpreter panics, when a harness
|
||||
//! or test file cannot be read, or when no ES5 test runs.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -198,15 +201,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 names in [`MISSING_BUILTINS`] that `String`, `Number`, `RegExp` and `Date` provide.
|
||||
const TEXT_BUILTINS: &str = "String Number Date RegExp charAt charCodeAt fromCharCode substring substr split replace match search exec test \
|
||||
@@ -310,11 +335,49 @@ fn run_language_subset(dir: &Path) {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn es5_text_builtins_run() {
|
||||
fn es5_built_ins_subset_runs() {
|
||||
let Some(dir) = suite_dir() else {
|
||||
println!("test262 built-ins: SKIPPED (no suite; run cargo xtask test262)");
|
||||
return;
|
||||
};
|
||||
let handle = std::thread::Builder::new().stack_size(64 << 20).spawn(move || run_built_ins(&dir)).unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
/// Runs the tests under the folders [`ES5_BUILT_INS`] names, prints the counts and writes the
|
||||
/// failures to `built-ins-failures.txt` beside the suite. The Annex B tests name the edition they
|
||||
/// were written for with `esid:`, so they run without an `es5id:`.
|
||||
fn run_built_ins(dir: &Path) {
|
||||
let missing = missing_builtins(&[TEXT_BUILTINS]);
|
||||
let mut all = RunCounts::default();
|
||||
for folder in ES5_BUILT_INS {
|
||||
let root = dir.join("test").join(folder);
|
||||
assert!(root.is_dir(), "{} is not a test262 checkout", dir.display());
|
||||
let (es5, later) = run_tests(dir, &root, &missing, folder.starts_with("annexB"));
|
||||
for counts in [es5, later] {
|
||||
all.tests += counts.tests;
|
||||
all.skipped += counts.skipped;
|
||||
all.passed += counts.passed;
|
||||
all.failed += counts.failed;
|
||||
all.panicked += counts.panicked;
|
||||
all.failures.push_str(&counts.failures);
|
||||
}
|
||||
}
|
||||
assert!(all.tests > 0, "no ES5 built-ins tests ran under {}", dir.display());
|
||||
let report = dir.join("built-ins-failures.txt");
|
||||
if let Err(e) = std::fs::write(&report, &all.failures) {
|
||||
println!("could not write {}: {e}", report.display());
|
||||
}
|
||||
println!("test262 ES5 built-ins subset through the interpreter: {}. Failures: {}", all.summary(), report.display());
|
||||
assert_eq!(all.panicked, 0, "the interpreter panicked; see {}", report.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn es5_text_builtins_run() {
|
||||
let Some(dir) = suite_dir() else {
|
||||
println!("test262 text built-ins: SKIPPED (no suite; run cargo xtask test262)");
|
||||
return;
|
||||
};
|
||||
let handle = std::thread::Builder::new().stack_size(64 << 20).spawn(move || run_text_builtins(&dir)).unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
@@ -693,8 +693,9 @@ 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`). This entry covers parsing; the next entry covers running scripts. 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` has `String`, `Number`, `RegExp` and `Date` as ECMAScript 5.1 defines them (sections 15.5, 15.7, 15.9 and 15.10, https://262.ecma-international.org/5.1/), with Annex B's `substr`, `getYear`, `setYear` and `toGMTString`. `toFixed`, `toExponential` and `toPrecision` format a number from its exact decimal value, and `toString(radix)` writes any base from 2 to 36. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher whose choice points and capture state count against the heap limit and which takes fuel for every step, so a catastrophic pattern stops at the fuel limit and 50,000 nested groups give a `SyntaxError` at the default depth limit. A string method that would build a string past the heap limit stops before it allocates. `Date` reads the clock and time zone the host sets (`Realm::set_clock`, `Realm::set_time_zone`; local time is UTC by default). `cargo xtask test262` runs the suite's `built-ins` tests for the four (https://github.com/tc39/test262): of the runs of the ES5 tests that need only the built-ins present, 1,113 of 1,169 pass for `String`, 262 of 262 for `Number`, 878 of 908 for `RegExp` and 6 of 6 for `Date`. The others need `Array` and `Boolean` methods, or expect later editions' rules (built-in functions whose `length` can be deleted, flags given with a RegExp object, a negative `lastIndex`, `RegExp.prototype` as a plain object, and `RegExp.prototype`'s `global`, `ignoreCase` and `multiline` properties that can be deleted). The ES5 language tests that can now run give 3,742 of 3,746 passing runs. Tests: `crates/js/src/text_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 two entries list. 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: 4,280 of 4,286 runs pass (4 of the others expect the completion values of later editions, and 2 expect a configurable function `length`). 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. A method visits only the indices that exist when its range is longer than 4,096 indices and more than 8 times the properties on the object and its prototypes, so `new Array(4294967295).join("")` ends at once; a shorter range is visited index by index, holes included, and each index takes fuel. `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: 6,798 of 6,866 runs pass (the 34 failing tests expect rules of later editions). It also fetches the `json2.js` polyfill at a pinned commit and checks that its `stringify` and `parse` agree with the built-in `JSON`. Tests: `crates/js/src/builtins_tests.rs`, `crates/js/tests/test262.rs`, `crates/js/tests/json2.rs`.
|
||||
- `vectorcraft-js` has `String`, `Number`, `RegExp` and `Date` as ECMAScript 5.1 defines them (sections 15.5, 15.7, 15.9 and 15.10, https://262.ecma-international.org/5.1/), with Annex B's `substr`, `getYear`, `setYear` and `toGMTString`. `toFixed`, `toExponential` and `toPrecision` format a number from its exact decimal value, and `toString(radix)` writes any base from 2 to 36. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher whose choice points and capture state count against the heap limit and which takes fuel for every step, so a catastrophic pattern stops at the fuel limit and 50,000 nested groups give a `SyntaxError` at the default depth limit. A string method that would build a string past the heap limit stops before it allocates. `Date` reads the clock and time zone the host sets (`Realm::set_clock`, `Realm::set_time_zone`; local time is UTC by default). `cargo xtask test262` runs the suite's `built-ins` tests for the four (https://github.com/tc39/test262): of the runs of the ES5 tests that need only the built-ins present, 1,379 of 1,411 pass for `String`, 298 of 298 for `Number`, 954 of 988 for `RegExp` and 8 of 8 for `Date`. The others expect later editions' rules (built-in functions whose `length` can be deleted, flags given with a RegExp object, a negative `lastIndex`, `RegExp.prototype` as a plain object, and `RegExp.prototype`'s `global`, `ignoreCase` and `multiline` properties that can be deleted). Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.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.
|
||||
|
||||
+5
-2
@@ -44,8 +44,9 @@ commands:
|
||||
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
|
||||
test262 fetch the pinned ECMAScript conformance suite into corpus/test262 (sha256
|
||||
verified), parse its ES5 subset with vectorcraft-js and run the ES5 language
|
||||
tests its interpreter supports and the String, Number, RegExp and Date
|
||||
built-ins tests, printing the counts
|
||||
and built-in tests its interpreter supports and the String, Number, RegExp
|
||||
and Date built-ins tests, printing the counts; then fetch json2.js into
|
||||
corpus/json2 (sha256 verified) and run it
|
||||
web-fonts copy the craft-fonts faces in crates/text/web-fonts.txt into the web site (the Trunk
|
||||
post_build hook; a no-op without CRAFT_FONTS_DIR)
|
||||
";
|
||||
@@ -342,6 +343,8 @@ Tests that use a corpus skip cleanly when it is absent.
|
||||
corpus/test262/ the ECMAScript conformance suite (BSD-3-Clause) at a pinned commit, sha256
|
||||
verified: the JavaScript parser's and interpreter's ES5 subsets. Fetch and run:
|
||||
cargo xtask test262
|
||||
corpus/json2/ json2.js, the public-domain JSON polyfill, at a pinned commit, sha256 verified:
|
||||
compared with vectorcraft-js's JSON. Fetch and run: cargo xtask test262
|
||||
",
|
||||
corpus.display()
|
||||
);
|
||||
|
||||
+57
-7
@@ -2,17 +2,34 @@
|
||||
//! 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, run the `test/built-ins` tests
|
||||
//! for `String`, `Number`, `RegExp` and `Date`, 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, run the `test/built-ins`
|
||||
//! tests for `String`, `Number`, `RegExp` and `Date`, 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";
|
||||
|
||||
@@ -26,11 +43,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