mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 20:01:37 +00:00
JavaScript: vectorcraft-js runs ES5 scripts with a garbage-collected heap and execution limits
Squashed from #6178 onto main.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "vectorcraft-js"
|
||||
description = "JavaScript for scripts: an ECMAScript 5.1 lexer and parser to a syntax tree with source positions"
|
||||
description = "JavaScript for scripts: an ECMAScript 5.1 parser, and an interpreter with a garbage-collected heap and execution limits"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
//! The built-in objects the interpreter itself needs (ECMA-262 5.1 section 15): `Object` and the
|
||||
//! basic methods of `Object.prototype`, `Function` with `Function.prototype.call`, `apply` and
|
||||
//! `toString`, `eval`, the error constructors and `Error.prototype.toString`, and the prototypes
|
||||
//! primitive values look properties up on. The rest of the standard library is not here yet.
|
||||
|
||||
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};
|
||||
|
||||
fn alloc(interp: &mut Interp, class: Class, proto: Option<ObjId>) -> ObjId {
|
||||
interp.heap.alloc_object(Object::new(class, proto))
|
||||
}
|
||||
|
||||
/// A built-in function object.
|
||||
fn native(interp: &mut Interp, name: &'static str, f: NativeFn, length: u32, kind: NativeKind, constructor: bool) -> JsResult<ObjId> {
|
||||
let proto = interp.intrinsics.function_proto;
|
||||
let o = alloc(interp, Class::Function(Func::Native { f, kind, constructor, name }), Some(proto));
|
||||
interp.define(o, key("length"), Prop::Data { value: Value::Number(f64::from(length)), flags: 0 })?;
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
fn method(interp: &mut Interp, on: ObjId, name: &'static str, f: NativeFn, length: u32) -> JsResult<()> {
|
||||
let m = native(interp, name, f, length, NativeKind::Plain, false)?;
|
||||
interp.define(on, key(name), Prop::Data { value: Value::Object(m), flags: HIDDEN })
|
||||
}
|
||||
|
||||
fn data(interp: &mut Interp, on: ObjId, name: &str, value: Value, flags: u8) -> JsResult<()> {
|
||||
interp.define(on, key(name), Prop::Data { value, flags })
|
||||
}
|
||||
|
||||
/// Links a constructor and its prototype and makes the constructor a global.
|
||||
fn constructor(interp: &mut Interp, name: &'static str, ctor: ObjId, proto: ObjId) -> JsResult<()> {
|
||||
data(interp, ctor, "prototype", Value::Object(proto), 0)?;
|
||||
data(interp, proto, "constructor", Value::Object(ctor), HIDDEN)?;
|
||||
let global = interp.global;
|
||||
data(interp, global, name, Value::Object(ctor), HIDDEN)
|
||||
}
|
||||
|
||||
/// Creates the built-ins and the global object's properties.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
fn try_install(interp: &mut Interp) -> JsResult<()> {
|
||||
let object_proto = alloc(interp, Class::Ordinary, None);
|
||||
interp.intrinsics.object_proto = object_proto;
|
||||
let function_proto = alloc(
|
||||
interp,
|
||||
Class::Function(Func::Native { f: function_proto_call, kind: NativeKind::Plain, constructor: false, name: "" }),
|
||||
Some(object_proto),
|
||||
);
|
||||
interp.intrinsics.function_proto = function_proto;
|
||||
data(interp, function_proto, "length", Value::Number(0.0), 0)?;
|
||||
let global = interp.global;
|
||||
if let Some(g) = interp.heap.object_mut(global) {
|
||||
g.proto = Some(object_proto);
|
||||
}
|
||||
interp.intrinsics.array_proto = alloc(interp, Class::Array { length: 0, length_writable: true }, Some(object_proto));
|
||||
interp.intrinsics.string_proto = alloc(interp, Class::String(Str::from("")), Some(object_proto));
|
||||
interp.intrinsics.number_proto = alloc(interp, Class::Number, Some(object_proto));
|
||||
interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean, Some(object_proto));
|
||||
interp.intrinsics.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) {
|
||||
t.extensible = false;
|
||||
}
|
||||
interp.intrinsics.throw_type_error = thrower;
|
||||
|
||||
data(interp, global, "NaN", Value::Number(f64::NAN), 0)?;
|
||||
data(interp, global, "Infinity", Value::Number(f64::INFINITY), 0)?;
|
||||
data(interp, global, "undefined", Value::Undefined, 0)?;
|
||||
let eval = native(interp, "eval", eval, 1, NativeKind::Eval, false)?;
|
||||
interp.intrinsics.eval = eval;
|
||||
data(interp, global, "eval", Value::Object(eval), HIDDEN)?;
|
||||
|
||||
let object = native(interp, "Object", object_ctor, 1, NativeKind::Plain, true)?;
|
||||
constructor(interp, "Object", object, object_proto)?;
|
||||
method(interp, object_proto, "toString", object_to_string, 0)?;
|
||||
method(interp, object_proto, "toLocaleString", object_to_locale_string, 0)?;
|
||||
method(interp, object_proto, "valueOf", object_value_of, 0)?;
|
||||
method(interp, object_proto, "hasOwnProperty", has_own_property, 1)?;
|
||||
method(interp, object_proto, "isPrototypeOf", is_prototype_of, 1)?;
|
||||
method(interp, object_proto, "propertyIsEnumerable", property_is_enumerable, 1)?;
|
||||
|
||||
let function = native(interp, "Function", function_ctor, 1, NativeKind::Plain, true)?;
|
||||
constructor(interp, "Function", function, function_proto)?;
|
||||
let call = native(interp, "call", unreachable_native, 1, NativeKind::Call, false)?;
|
||||
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, "toString", function_to_string, 0)?;
|
||||
|
||||
install_errors(interp, object_proto)
|
||||
}
|
||||
|
||||
fn install_errors(interp: &mut Interp, object_proto: ObjId) -> JsResult<()> {
|
||||
let kinds: [(&'static str, NativeFn); 7] = [
|
||||
("Error", error_ctor),
|
||||
("EvalError", eval_error_ctor),
|
||||
("RangeError", range_error_ctor),
|
||||
("ReferenceError", reference_error_ctor),
|
||||
("SyntaxError", syntax_error_ctor),
|
||||
("TypeError", type_error_ctor),
|
||||
("URIError", uri_error_ctor),
|
||||
];
|
||||
let mut error_proto = object_proto;
|
||||
for (i, (name, f)) in kinds.into_iter().enumerate() {
|
||||
// Error.prototype and each NativeError prototype are Error objects (sections 15.11.4 and
|
||||
// 15.11.7.7).
|
||||
let proto = alloc(interp, Class::Error, Some(if i == 0 { object_proto } else { error_proto }));
|
||||
if i == 0 {
|
||||
error_proto = proto;
|
||||
method(interp, proto, "toString", error_to_string, 0)?;
|
||||
}
|
||||
if let Some(slot) = interp.intrinsics.error_protos.get_mut(i) {
|
||||
*slot = proto;
|
||||
}
|
||||
data(interp, proto, "name", Value::str(name), HIDDEN)?;
|
||||
data(interp, proto, "message", Value::str(""), HIDDEN)?;
|
||||
let ctor = native(interp, name, f, 1, NativeKind::Plain, true)?;
|
||||
constructor(interp, name, ctor, proto)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn arg(args: &[Value], i: usize) -> Value {
|
||||
args.get(i).cloned().unwrap_or(Value::Undefined)
|
||||
}
|
||||
|
||||
fn unreachable_native(_: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
Err(Abrupt::Internal("call or apply reached its native body".into()))
|
||||
}
|
||||
|
||||
fn throw_type_error(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
Err(interp.type_error("'caller', 'callee' and 'arguments' cannot be used on strict mode functions"))
|
||||
}
|
||||
|
||||
fn function_proto_call(_: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
// ---- Object ----
|
||||
|
||||
/// `Object(value)` and `new Object(value)` (sections 15.2.1 and 15.2.2).
|
||||
fn object_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let v = arg(args, 0);
|
||||
if v.is_nullish() {
|
||||
return Ok(Value::Object(interp.new_object()));
|
||||
}
|
||||
interp.to_object(&v).map(Value::Object)
|
||||
}
|
||||
|
||||
/// `Object.prototype.toString` (section 15.2.4.2).
|
||||
fn object_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let class = match &this {
|
||||
Value::Undefined => "Undefined",
|
||||
Value::Null => "Null",
|
||||
_ => {
|
||||
let o = interp.to_object(&this)?;
|
||||
interp.heap.object(o).map_or("Object", |o| o.class.name())
|
||||
}
|
||||
};
|
||||
Ok(Value::str(&format!("[object {class}]")))
|
||||
}
|
||||
|
||||
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)?;
|
||||
if !interp.is_callable(&f) {
|
||||
return Err(interp.type_error("toString is not a function"));
|
||||
}
|
||||
interp.call(&f, &this, &[])
|
||||
}
|
||||
|
||||
fn object_value_of(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
interp.to_object(&this).map(Value::Object)
|
||||
}
|
||||
|
||||
fn has_own_property(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let k = interp.to_key(&arg(args, 0))?;
|
||||
let o = interp.to_object(&this)?;
|
||||
Ok(Value::Bool(interp.get_own_property(o, &k)?.is_some()))
|
||||
}
|
||||
|
||||
fn is_prototype_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let Value::Object(mut v) = arg(args, 0) else { return Ok(Value::Bool(false)) };
|
||||
let o = interp.to_object(&this)?;
|
||||
while let Some(p) = interp.heap.object(v).and_then(|v| v.proto) {
|
||||
if p == o {
|
||||
return Ok(Value::Bool(true));
|
||||
}
|
||||
v = p;
|
||||
interp.use_fuel()?;
|
||||
}
|
||||
Ok(Value::Bool(false))
|
||||
}
|
||||
|
||||
fn property_is_enumerable(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let k = interp.to_key(&arg(args, 0))?;
|
||||
let o = interp.to_object(&this)?;
|
||||
Ok(Value::Bool(interp.get_own_property(o, &k)?.is_some_and(|p| p.enumerable())))
|
||||
}
|
||||
|
||||
// ---- Function ----
|
||||
|
||||
/// `Function(p1, …, body)` and `new Function(…)` (section 15.3.2.1): the source is read as
|
||||
/// `(function anonymous(p1,…\n) {\nbody\n})` and must be exactly that one function.
|
||||
fn function_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
const HEAD: &str = "(function anonymous(";
|
||||
const MID: &str = "\n) {\n";
|
||||
const TAIL: &str = "\n})";
|
||||
let mut parts = Vec::with_capacity(args.len());
|
||||
for a in args {
|
||||
parts.push(interp.to_str(a)?);
|
||||
}
|
||||
let body = parts.pop().unwrap_or_else(|| Str::from(""));
|
||||
// The length is checked against the source limit before any argument is flattened or copied.
|
||||
let total = parts
|
||||
.iter()
|
||||
.map(Str::len)
|
||||
.chain([parts.len().saturating_sub(1), HEAD.len() + MID.len() + TAIL.len(), body.len()])
|
||||
.fold(0usize, usize::saturating_add);
|
||||
interp.check_source_units(total).map_err(|e| interp.compile_error_to_abrupt(e))?;
|
||||
let mut units = Vec::with_capacity(total);
|
||||
units.extend(HEAD.encode_utf16());
|
||||
for (i, p) in parts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
units.push(u16::from(b','));
|
||||
}
|
||||
units.extend_from_slice(&p.flat());
|
||||
}
|
||||
units.extend(MID.encode_utf16());
|
||||
units.extend_from_slice(&body.flat());
|
||||
units.extend(TAIL.encode_utf16());
|
||||
interp.source_units = interp.source_units.saturating_add(units.len());
|
||||
let remaining = interp.options.max_source_units.saturating_sub(interp.source_units.saturating_sub(units.len()));
|
||||
let options = crate::Options { strict: false, max_source_units: remaining, ..interp.options.clone() };
|
||||
let program = match crate::parse_utf16(&units, &options) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(interp.throw(ErrorType::Syntax, &e.message)),
|
||||
};
|
||||
let source = Rc::new(Source { lines: program.lines.clone(), text: units.into() });
|
||||
let code = match compile_constructed_function(&program, &source) {
|
||||
Ok(Some(code)) => code,
|
||||
Ok(None) => return Err(interp.throw(ErrorType::Syntax, "the parameters or body do not form one function")),
|
||||
Err(CompileFail::Syntax(e)) => return Err(interp.throw(ErrorType::Syntax, &e.message)),
|
||||
Err(CompileFail::Internal(m)) => return Err(Abrupt::Internal(m)),
|
||||
};
|
||||
interp.charge_code(&code, 0)?;
|
||||
let env = interp.global_env;
|
||||
interp.make_closure(code, env).map(Value::Object)
|
||||
}
|
||||
|
||||
/// `Function.prototype.toString` (section 15.3.4.2): a function's source text, or a stand-in
|
||||
/// for a built-in.
|
||||
fn function_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let func = this.as_object().and_then(|o| interp.heap.object(o)).and_then(|o| match &o.class {
|
||||
Class::Function(f) => Some(f.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let text = match func {
|
||||
Some(Func::Closure { code, .. }) => {
|
||||
let (start, end) = (code.span.0 as usize, code.span.1 as usize);
|
||||
let units = code.source.text.get(start..end).unwrap_or_default();
|
||||
let bytes = string_bytes(units.len());
|
||||
interp.reserve_heap(bytes)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
return Ok(Value::String(Str::from_units(units.to_vec())));
|
||||
}
|
||||
Some(Func::Native { name, .. }) => format!("function {name}() {{ [native code] }}"),
|
||||
Some(Func::Host { name, .. }) => format!("function {name}() {{ [native code] }}"),
|
||||
None => return Err(interp.type_error("Function.prototype.toString was called on a value that is not a function")),
|
||||
};
|
||||
Ok(Value::str(&text))
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let x = arg(args, 0);
|
||||
let Value::String(s) = &x else { return Ok(x) };
|
||||
interp.check_source_units(s.len()).map_err(|e| interp.compile_error_to_abrupt(e))?;
|
||||
let units = s.flat();
|
||||
let code = interp.compile_source(&units, false, CodeKind::Eval).map_err(|e| interp.compile_error_to_abrupt(e))?;
|
||||
interp.run_global(code, true)
|
||||
}
|
||||
|
||||
// ---- errors ----
|
||||
|
||||
fn make_error(interp: &mut Interp, t: ErrorType, args: &[Value]) -> JsResult<Value> {
|
||||
let message = arg(args, 0);
|
||||
let text = if matches!(message, Value::Undefined) { None } else { Some(interp.to_str(&message)?) };
|
||||
let o = interp.error_object(t, "");
|
||||
if let Some(text) = text {
|
||||
interp.define(o, key("message"), Prop::Data { value: Value::String(text), flags: HIDDEN })?;
|
||||
}
|
||||
Ok(Value::Object(o))
|
||||
}
|
||||
|
||||
fn error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
make_error(interp, ErrorType::Error, args)
|
||||
}
|
||||
|
||||
fn eval_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
make_error(interp, ErrorType::Eval, args)
|
||||
}
|
||||
|
||||
fn range_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
make_error(interp, ErrorType::Range, args)
|
||||
}
|
||||
|
||||
fn reference_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
make_error(interp, ErrorType::Reference, args)
|
||||
}
|
||||
|
||||
fn syntax_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
make_error(interp, ErrorType::Syntax, args)
|
||||
}
|
||||
|
||||
fn type_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
make_error(interp, ErrorType::Type, args)
|
||||
}
|
||||
|
||||
fn uri_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
make_error(interp, ErrorType::Uri, args)
|
||||
}
|
||||
|
||||
/// `Error.prototype.toString` (section 15.11.4.4).
|
||||
fn error_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let Value::Object(o) = this else { return Err(interp.type_error("Error.prototype.toString was called on a value that is not an object")) };
|
||||
let name = interp.get_from(o, &key("name"), &this)?;
|
||||
let name = if matches!(name, Value::Undefined) { Str::from("Error") } else { interp.to_str(&name)? };
|
||||
let message = interp.get_from(o, &key("message"), &this)?;
|
||||
let message = if matches!(message, Value::Undefined) { Str::from("") } else { interp.to_str(&message)? };
|
||||
if name.len() == 0 {
|
||||
return Ok(Value::String(message));
|
||||
}
|
||||
if message.len() == 0 {
|
||||
return Ok(Value::String(name));
|
||||
}
|
||||
let sep = Str::from(": ");
|
||||
let head = interp.concat(&name, &sep)?;
|
||||
interp.concat(&head, &message).map(Value::String)
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//! Bytecode: the instructions a function, a script or eval code compiles to, and the verifier
|
||||
//! that checks them before they run.
|
||||
//!
|
||||
//! The interpreter is a stack machine. Each instruction takes its operands from the top of the
|
||||
//! frame's operand stack and pushes its result there; jumps name an instruction index. The
|
||||
//! verifier walks every path through the code and checks that each jump target, exception handler
|
||||
//! and `finally` dispatch target is an instruction, that each constant, name and function index is
|
||||
//! in its table, that the stack never goes below the frame's base, that every path reaching an
|
||||
//! instruction arrives with the same stack depth, and that no path runs past the last
|
||||
//! instruction. Code that fails is reported as an internal error and never runs.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::LineTable;
|
||||
use crate::ast::BinaryOp;
|
||||
use crate::heap::Key;
|
||||
use crate::value::Str;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum Op {
|
||||
Undefined,
|
||||
Null,
|
||||
True,
|
||||
False,
|
||||
/// Pushes `numbers[i]`.
|
||||
Number(u32),
|
||||
/// Pushes `strings[i]`.
|
||||
String(u32),
|
||||
This,
|
||||
Pop,
|
||||
/// Removes the `n` values under the top one.
|
||||
PopUnder(u32),
|
||||
Dup,
|
||||
/// `a b` → `a b a b`.
|
||||
Dup2,
|
||||
Swap,
|
||||
/// `a b c` → `b c a`.
|
||||
Rot3,
|
||||
/// Pushes the value of the variable `names[i]`.
|
||||
GetVar(u32),
|
||||
/// Pushes the value of the variable and the `this` a call to it gets.
|
||||
GetVarThis(u32),
|
||||
/// `value` → `value`, assigned to the variable.
|
||||
SetVar(u32),
|
||||
/// Pushes the base of the reference to the variable `names[i]`, resolved before the
|
||||
/// right-hand side of an assignment runs.
|
||||
RefVar(u32),
|
||||
/// `base` → `value`: the variable's value read through the resolved base.
|
||||
GetRef(u32),
|
||||
/// `base value` → `value`, assigned to the variable through the resolved base.
|
||||
PutRef(u32),
|
||||
TypeofVar(u32),
|
||||
DeleteVar(u32),
|
||||
IncVar {
|
||||
name: u32,
|
||||
prefix: bool,
|
||||
dec: bool,
|
||||
},
|
||||
/// `object` → `object.name`.
|
||||
GetMember(u32),
|
||||
/// `object` → `object.name object`.
|
||||
GetMemberThis(u32),
|
||||
/// `object value` → `value`.
|
||||
SetMember(u32),
|
||||
DeleteMember(u32),
|
||||
IncMember {
|
||||
name: u32,
|
||||
prefix: bool,
|
||||
dec: bool,
|
||||
},
|
||||
/// `object key` → `object name`: checks that the object can have properties and turns the key
|
||||
/// into a property name, before the right-hand side of an assignment runs.
|
||||
ToKey,
|
||||
/// `object` → `object`: throws a `TypeError` when it is `undefined` or `null`, before the
|
||||
/// right-hand side of an assignment to one of its properties runs.
|
||||
CheckObject,
|
||||
/// `object key` → `object[key]`.
|
||||
GetIndex,
|
||||
/// `object key` → `object[key] object`.
|
||||
GetIndexThis,
|
||||
/// `object key value` → `value`.
|
||||
SetIndex,
|
||||
DeleteIndex,
|
||||
IncIndex {
|
||||
prefix: bool,
|
||||
dec: bool,
|
||||
},
|
||||
/// `function this arg…` → result.
|
||||
Call(u32),
|
||||
/// Like `Call`, but a direct eval when the function is the global `eval`.
|
||||
CallEval(u32),
|
||||
/// `function arg…` → the new object.
|
||||
New(u32),
|
||||
Binary(BinaryOp),
|
||||
Neg,
|
||||
Plus,
|
||||
BitNot,
|
||||
Not,
|
||||
Typeof,
|
||||
ToNumber,
|
||||
Jump(u32),
|
||||
/// Pops the top value and jumps when it is falsy.
|
||||
JumpIfFalse(u32),
|
||||
JumpIfTrue(u32),
|
||||
/// Jumps, keeping the top value, when it is falsy; otherwise pops it.
|
||||
JumpIfFalseKeep(u32),
|
||||
JumpIfTrueKeep(u32),
|
||||
Return,
|
||||
/// Returns the value of the last expression statement (script and eval code).
|
||||
ReturnCompletion,
|
||||
Throw,
|
||||
/// Throws a `ReferenceError` with the message `strings[i]` (an assignment to something that
|
||||
/// cannot be assigned to).
|
||||
ThrowReferenceError(u32),
|
||||
/// Ends a `finally` block: `value kind` on the stack say how it was entered. Kind 0 (normal)
|
||||
/// pops both and goes on, kind 1 throws the value, and kind `2 + k` jumps to entry `k` of
|
||||
/// `finally_tables[i]` with the value left on the stack.
|
||||
EndFinally(u32),
|
||||
NewObject,
|
||||
NewArray,
|
||||
/// `array value` → `array`, the value appended.
|
||||
ArrayPush,
|
||||
/// `array` → `array`, one longer.
|
||||
ArrayHole,
|
||||
/// `object value` → `object`.
|
||||
InitProp(u32),
|
||||
/// `object function` → `object`.
|
||||
InitGetter(u32),
|
||||
InitSetter(u32),
|
||||
/// Pushes a new function made from `functions[i]` and the current environment.
|
||||
Closure(u32),
|
||||
/// Pushes a new regular expression object for `regexps[i]`.
|
||||
RegExp(u32),
|
||||
/// `object` → nothing: enters a `with` statement's environment.
|
||||
PushWith,
|
||||
/// `exception` → nothing: enters a catch clause's environment, binding `names[i]`.
|
||||
PushCatch(u32),
|
||||
PopEnv,
|
||||
/// `object` → `iterator`.
|
||||
ForInStart,
|
||||
/// `iterator` → `iterator name`, or jumps with `iterator` when the names are used up.
|
||||
ForInNext(u32),
|
||||
/// Pops the value of an expression statement into the completion value.
|
||||
SetCompletion,
|
||||
/// Pushes the completion value, which a `finally` block in script and eval code keeps and
|
||||
/// sets back with `SetCompletion` when it ends normally.
|
||||
GetCompletion,
|
||||
}
|
||||
|
||||
/// What code is: a function body, a script, or eval code.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum CodeKind {
|
||||
Function,
|
||||
Global,
|
||||
Eval,
|
||||
}
|
||||
|
||||
/// A range of instructions an exception thrown in jumps out of: to `target`, with the operand
|
||||
/// stack cut back to `depth` values and the environments to `env_depth`, and the exception pushed.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Handler {
|
||||
pub(crate) start: u32,
|
||||
pub(crate) end: u32,
|
||||
pub(crate) target: u32,
|
||||
pub(crate) depth: u32,
|
||||
pub(crate) env_depth: u32,
|
||||
}
|
||||
|
||||
/// The source a piece of code came from, for error positions and `Function.prototype.toString`.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Source {
|
||||
pub(crate) lines: LineTable,
|
||||
pub(crate) text: Rc<[u16]>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Code {
|
||||
pub(crate) ops: Vec<Op>,
|
||||
/// The source offset each instruction comes from.
|
||||
pub(crate) positions: Vec<u32>,
|
||||
pub(crate) numbers: Vec<f64>,
|
||||
pub(crate) strings: Vec<Str>,
|
||||
pub(crate) names: Vec<Key>,
|
||||
pub(crate) functions: Vec<Rc<Code>>,
|
||||
pub(crate) regexps: Vec<(Str, Str)>,
|
||||
pub(crate) handlers: Vec<Handler>,
|
||||
pub(crate) finally_tables: Vec<Vec<u32>>,
|
||||
/// The most values the code keeps on the operand stack, set by the verifier.
|
||||
pub(crate) max_stack: u32,
|
||||
pub(crate) strict: bool,
|
||||
/// A function expression's own name, bound inside it.
|
||||
pub(crate) self_name: Option<Key>,
|
||||
pub(crate) params: Vec<Key>,
|
||||
pub(crate) vars: Vec<Key>,
|
||||
/// Function declarations to instantiate on entry: their names and indices in `functions`.
|
||||
pub(crate) fun_decls: Vec<(Key, u32)>,
|
||||
/// The body refers to `arguments` or calls `eval`.
|
||||
pub(crate) uses_arguments: bool,
|
||||
pub(crate) source: Rc<Source>,
|
||||
/// Where the function's text starts and ends in the source.
|
||||
pub(crate) span: (u32, u32),
|
||||
}
|
||||
|
||||
impl Code {
|
||||
/// The innermost handler around instruction `pc`.
|
||||
pub(crate) fn handler(&self, pc: usize) -> Option<Handler> {
|
||||
self.handlers.iter().copied().find(|h| (h.start as usize) <= pc && pc < h.end as usize)
|
||||
}
|
||||
|
||||
/// The line and column of instruction `pc`.
|
||||
pub(crate) fn line_column(&self, pc: usize) -> (u32, u32) {
|
||||
let offset = self.positions.get(pc).copied().unwrap_or(self.span.0);
|
||||
self.source.lines.line_column(offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// What an instruction does to the stack on each way out of it.
|
||||
struct Effect {
|
||||
pops: u32,
|
||||
/// Values pushed when it goes on to the next instruction; `None` when it never does.
|
||||
next: Option<u32>,
|
||||
/// Instruction indices it may jump to, each with the values it pushes on that path.
|
||||
jumps: Vec<(u32, u32)>,
|
||||
}
|
||||
|
||||
/// The values an instruction pops, and pushes when it goes on to the next instruction (0 for one
|
||||
/// that never does), for the compiler's count of the stack depth.
|
||||
pub(crate) fn fallthrough_effect(op: Op) -> (u32, u32) {
|
||||
match op {
|
||||
Op::EndFinally(_) => (2, 0),
|
||||
_ => effect(op, &[]).map_or((0, 0), |fx| (fx.pops, fx.next.unwrap_or(0))),
|
||||
}
|
||||
}
|
||||
|
||||
fn effect(op: Op, finally_tables: &[Vec<u32>]) -> Result<Effect, String> {
|
||||
let e = |pops, pushes| Effect { pops, next: Some(pushes), jumps: Vec::new() };
|
||||
let end = |pops| Effect { pops, next: None, jumps: Vec::new() };
|
||||
Ok(match op {
|
||||
Op::Undefined | Op::Null | Op::True | Op::False | Op::Number(_) | Op::String(_) | Op::This => e(0, 1),
|
||||
Op::GetVar(_) | Op::TypeofVar(_) | Op::DeleteVar(_) | Op::IncVar { .. } | Op::RefVar(_) => e(0, 1),
|
||||
Op::GetRef(_) => e(1, 1),
|
||||
Op::PutRef(_) => e(2, 1),
|
||||
Op::NewObject | Op::NewArray | Op::Closure(_) | Op::RegExp(_) | Op::GetCompletion => e(0, 1),
|
||||
Op::Pop | Op::PushWith | Op::PushCatch(_) | Op::SetCompletion => e(1, 0),
|
||||
Op::PopUnder(n) => e(n.checked_add(1).ok_or("operand too large")?, 1),
|
||||
Op::Dup => e(1, 2),
|
||||
Op::Dup2 => e(2, 4),
|
||||
Op::Swap | Op::ToKey => e(2, 2),
|
||||
Op::Rot3 => e(3, 3),
|
||||
Op::GetVarThis(_) => e(0, 2),
|
||||
Op::SetVar(_) | Op::GetMember(_) | Op::DeleteMember(_) | Op::IncMember { .. } => e(1, 1),
|
||||
Op::Neg | Op::Plus | Op::BitNot | Op::Not | Op::Typeof | Op::ToNumber | Op::ArrayHole | Op::ForInStart | Op::CheckObject => e(1, 1),
|
||||
Op::GetMemberThis(_) => e(1, 2),
|
||||
Op::SetMember(_) | Op::GetIndex | Op::DeleteIndex | Op::IncIndex { .. } | Op::Binary(_) => e(2, 1),
|
||||
Op::ArrayPush | Op::InitProp(_) | Op::InitGetter(_) | Op::InitSetter(_) => e(2, 1),
|
||||
Op::GetIndexThis => e(2, 2),
|
||||
Op::SetIndex => e(3, 1),
|
||||
Op::Call(n) | Op::CallEval(n) => e(n.checked_add(2).ok_or("operand too large")?, 1),
|
||||
Op::New(n) => e(n.checked_add(1).ok_or("operand too large")?, 1),
|
||||
Op::PopEnv => e(0, 0),
|
||||
Op::Jump(t) => Effect { pops: 0, next: None, jumps: vec![(t, 0)] },
|
||||
Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => Effect { pops: 1, next: Some(0), jumps: vec![(t, 0)] },
|
||||
Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) => Effect { pops: 1, next: Some(0), jumps: vec![(t, 1)] },
|
||||
Op::Return | Op::Throw => end(1),
|
||||
Op::ReturnCompletion | Op::ThrowReferenceError(_) => end(0),
|
||||
Op::EndFinally(t) => {
|
||||
let table = finally_tables.get(t as usize).ok_or_else(|| format!("finally table {t} out of range"))?;
|
||||
Effect { pops: 2, next: Some(0), jumps: table.iter().map(|&target| (target, 1)).collect() }
|
||||
}
|
||||
Op::ForInNext(t) => Effect { pops: 1, next: Some(2), jumps: vec![(t, 1)] },
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks the constant, name, function and regular expression indices an instruction uses.
|
||||
fn check_indices(op: Op, code: &Code) -> Result<(), String> {
|
||||
let check = |i: u32, len: usize, what: &str| if (i as usize) < len { Ok(()) } else { Err(format!("{what} {i} out of range")) };
|
||||
match op {
|
||||
Op::Number(i) => check(i, code.numbers.len(), "number"),
|
||||
Op::String(i) | Op::ThrowReferenceError(i) => check(i, code.strings.len(), "string"),
|
||||
Op::GetVar(i)
|
||||
| Op::GetVarThis(i)
|
||||
| Op::SetVar(i)
|
||||
| Op::RefVar(i)
|
||||
| Op::GetRef(i)
|
||||
| Op::PutRef(i)
|
||||
| Op::TypeofVar(i)
|
||||
| Op::DeleteVar(i)
|
||||
| Op::IncVar { name: i, .. }
|
||||
| Op::GetMember(i)
|
||||
| Op::GetMemberThis(i)
|
||||
| Op::SetMember(i)
|
||||
| Op::DeleteMember(i)
|
||||
| Op::IncMember { name: i, .. }
|
||||
| Op::InitProp(i)
|
||||
| Op::InitGetter(i)
|
||||
| Op::InitSetter(i)
|
||||
| Op::PushCatch(i) => check(i, code.names.len(), "name"),
|
||||
Op::Closure(i) => check(i, code.functions.len(), "function"),
|
||||
Op::RegExp(i) => check(i, code.regexps.len(), "regular expression"),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies `code` (not the functions inside it, which are verified when they are compiled) and
|
||||
/// returns the most values it keeps on the stack.
|
||||
pub(crate) fn verify(code: &Code) -> Result<u32, String> {
|
||||
let len = code.ops.len();
|
||||
if len == 0 {
|
||||
return Err("no instructions".into());
|
||||
}
|
||||
if code.positions.len() != len {
|
||||
return Err("the position table does not match the instructions".into());
|
||||
}
|
||||
for (name, _) in &code.fun_decls {
|
||||
if name.is_empty() {
|
||||
return Err("a function declaration without a name".into());
|
||||
}
|
||||
}
|
||||
if code.fun_decls.iter().any(|(_, i)| *i as usize >= code.functions.len()) {
|
||||
return Err("function declaration index out of range".into());
|
||||
}
|
||||
let mut depth: Vec<Option<u32>> = vec![None; len];
|
||||
let mut work: Vec<(u32, u32)> = vec![(0, 0)];
|
||||
for h in &code.handlers {
|
||||
if h.start > h.end || h.end as usize > len {
|
||||
return Err(format!("handler range {}..{} out of range", h.start, h.end));
|
||||
}
|
||||
work.push((h.target, h.depth.checked_add(1).ok_or("handler depth too large")?));
|
||||
}
|
||||
let mut max = 0;
|
||||
while let Some((pc, d)) = work.pop() {
|
||||
let slot = depth.get_mut(pc as usize).ok_or_else(|| format!("jump to {pc}, past the end"))?;
|
||||
match *slot {
|
||||
Some(seen) if seen == d => continue,
|
||||
Some(seen) => return Err(format!("instruction {pc} reached with stack depths {seen} and {d}")),
|
||||
None => *slot = Some(d),
|
||||
}
|
||||
max = max.max(d);
|
||||
let op = code.ops.get(pc as usize).copied().ok_or("instruction out of range")?;
|
||||
check_indices(op, code)?;
|
||||
let fx = effect(op, &code.finally_tables)?;
|
||||
let base = d.checked_sub(fx.pops).ok_or_else(|| format!("instruction {pc} ({op:?}) pops below the stack"))?;
|
||||
if let Some(pushes) = fx.next {
|
||||
if pc as usize + 1 >= len {
|
||||
return Err("the code runs past its last instruction".into());
|
||||
}
|
||||
work.push((pc + 1, base + pushes));
|
||||
}
|
||||
for (target, pushes) in fx.jumps {
|
||||
work.push((target, base + pushes));
|
||||
}
|
||||
}
|
||||
for h in &code.handlers {
|
||||
if let Some(Some(d)) = depth.get(h.start as usize)
|
||||
&& *d < h.depth
|
||||
&& h.start < h.end
|
||||
{
|
||||
return Err(format!("handler at {} expects {} values but its range starts with {d}", h.target, h.depth));
|
||||
}
|
||||
}
|
||||
Ok(max)
|
||||
}
|
||||
@@ -58,6 +58,39 @@ pub fn hex_value(c: u32) -> Option<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the digit values `digits` in `radix` (8 or 16), rounded once to the nearest number
|
||||
/// with ties to even (sections 7.8.3 and 9.3.1), or `None` when there are no digits. Digits are
|
||||
/// kept exactly in a `u128` while they fit; past that, each later digit counts only for its
|
||||
/// position and for whether any of the dropped bits is set.
|
||||
pub fn radix_value(digits: impl IntoIterator<Item = u32>, radix: u32) -> Option<f64> {
|
||||
let bits = radix.trailing_zeros();
|
||||
let mut kept: u128 = 0;
|
||||
let mut dropped_bits: u32 = 0;
|
||||
let mut sticky = false;
|
||||
let mut any = false;
|
||||
for d in digits {
|
||||
any = true;
|
||||
if kept.leading_zeros() >= bits {
|
||||
kept = kept << bits | u128::from(d);
|
||||
} else {
|
||||
dropped_bits = dropped_bits.saturating_add(bits);
|
||||
sticky |= d != 0;
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return None;
|
||||
}
|
||||
// `kept` holds at least 125 bits once a digit is dropped, so its lowest bit lies below the
|
||||
// rounding bit and stands in for every dropped bit.
|
||||
let mut value = (kept | u128::from(sticky)) as f64;
|
||||
while dropped_bits > 0 && value.is_finite() {
|
||||
let step = dropped_bits.min(64);
|
||||
value *= (1_u128 << step) as f64;
|
||||
dropped_bits -= step;
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_decimal_digit(c: u32) -> bool {
|
||||
(0x30..=0x39).contains(&c)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,594 @@
|
||||
//! The heap: objects and environments in one arena, referred to by index, and a mark-sweep
|
||||
//! collector over it.
|
||||
//!
|
||||
//! A cell is either free or holds an [`Object`] or an [`Env`]. The collector marks every cell
|
||||
//! reachable from the roots the interpreter passes in, with a worklist rather than recursion, and
|
||||
//! frees the rest; freed indices are reused by later allocations. Cycles between objects are freed
|
||||
//! like anything else that is unreachable. [`Heap::bytes`] is an estimate of the memory the heap
|
||||
//! holds, strings reachable from it included; the interpreter compares it with its heap limit.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::bytecode::{Code, Handler, Op};
|
||||
use crate::value::{Str, Value};
|
||||
|
||||
/// A property name: its UTF-16 code units.
|
||||
pub(crate) type Key = Rc<[u16]>;
|
||||
|
||||
pub(crate) fn key(s: &str) -> Key {
|
||||
s.encode_utf16().collect::<Vec<u16>>().into()
|
||||
}
|
||||
|
||||
/// An object's index in the heap.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct ObjId(pub(crate) u32);
|
||||
|
||||
/// An environment's index in the heap.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct EnvId(pub(crate) u32);
|
||||
|
||||
/// A map from property names to values that keeps insertion order (the order `for-in` lists own
|
||||
/// properties in). Small maps are searched in order; larger ones keep a hash index.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct PropMap<T> {
|
||||
entries: Vec<Option<(Key, T)>>,
|
||||
index: Option<HashMap<Key, usize>>,
|
||||
live: usize,
|
||||
}
|
||||
|
||||
/// Maps with more entries than this keep a hash index.
|
||||
const INDEX_AFTER: usize = 8;
|
||||
|
||||
impl<T> Default for PropMap<T> {
|
||||
fn default() -> Self {
|
||||
Self { entries: Vec::new(), index: None, live: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PropMap<T> {
|
||||
fn position(&self, k: &[u16]) -> Option<usize> {
|
||||
match &self.index {
|
||||
Some(index) => index.get(k).copied(),
|
||||
None => self.entries.iter().position(|e| e.as_ref().is_some_and(|(ek, _)| &**ek == k)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, k: &[u16]) -> Option<&T> {
|
||||
let i = self.position(k)?;
|
||||
self.entries.get(i)?.as_ref().map(|(_, v)| v)
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, k: &[u16]) -> Option<&mut T> {
|
||||
let i = self.position(k)?;
|
||||
self.entries.get_mut(i)?.as_mut().map(|(_, v)| v)
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, k: &[u16]) -> bool {
|
||||
self.position(k).is_some()
|
||||
}
|
||||
|
||||
/// Sets the value under `k`, keeping its place when it is there already.
|
||||
pub(crate) fn insert(&mut self, k: Key, v: T) {
|
||||
if let Some(slot) = self.get_mut(&k) {
|
||||
*slot = v;
|
||||
return;
|
||||
}
|
||||
let i = self.entries.len();
|
||||
if let Some(index) = &mut self.index {
|
||||
index.insert(k.clone(), i);
|
||||
}
|
||||
self.entries.push(Some((k, v)));
|
||||
self.live += 1;
|
||||
if self.index.is_none() && self.entries.len() > INDEX_AFTER {
|
||||
self.rebuild_index();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, k: &[u16]) -> Option<T> {
|
||||
let i = self.position(k)?;
|
||||
let (_, v) = self.entries.get_mut(i)?.take()?;
|
||||
if let Some(index) = &mut self.index {
|
||||
index.remove(k);
|
||||
}
|
||||
self.live -= 1;
|
||||
if self.entries.len() > 2 * INDEX_AFTER && self.live * 2 < self.entries.len() {
|
||||
self.entries.retain(Option::is_some);
|
||||
self.rebuild_index();
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn rebuild_index(&mut self) {
|
||||
if self.entries.len() <= INDEX_AFTER {
|
||||
self.index = None;
|
||||
return;
|
||||
}
|
||||
let index = self.entries.iter().enumerate().filter_map(|(i, e)| e.as_ref().map(|(k, _)| (k.clone(), i))).collect();
|
||||
self.index = Some(index);
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.live
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (&Key, &T)> {
|
||||
self.entries.iter().filter_map(|e| e.as_ref().map(|(k, v)| (k, v)))
|
||||
}
|
||||
|
||||
pub(crate) fn keys(&self) -> Vec<Key> {
|
||||
self.iter().map(|(k, _)| k.clone()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const WRITABLE: u8 = 1;
|
||||
pub(crate) const ENUMERABLE: u8 = 2;
|
||||
pub(crate) const CONFIGURABLE: u8 = 4;
|
||||
/// Writable, enumerable and configurable: a property made by assignment.
|
||||
pub(crate) const OPEN: u8 = WRITABLE | ENUMERABLE | CONFIGURABLE;
|
||||
/// Writable and configurable but not enumerable: the built-in methods.
|
||||
pub(crate) const HIDDEN: u8 = WRITABLE | CONFIGURABLE;
|
||||
|
||||
/// A property (section 8.6.1).
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Prop {
|
||||
Data { value: Value, flags: u8 },
|
||||
Accessor { get: Option<ObjId>, set: Option<ObjId>, flags: u8 },
|
||||
}
|
||||
|
||||
impl Prop {
|
||||
pub(crate) fn flags(&self) -> u8 {
|
||||
match self {
|
||||
Prop::Data { flags, .. } | Prop::Accessor { flags, .. } => *flags,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn enumerable(&self) -> bool {
|
||||
self.flags() & ENUMERABLE != 0
|
||||
}
|
||||
|
||||
pub(crate) fn configurable(&self) -> bool {
|
||||
self.flags() & CONFIGURABLE != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// A built-in function implemented in Rust: `(interp, this, arguments, called with new)`.
|
||||
pub(crate) type NativeFn = fn(&mut crate::interp::Interp, Value, &[Value], bool) -> Result<Value, crate::interp::Abrupt>;
|
||||
|
||||
/// A function the embedding defines.
|
||||
pub(crate) type HostFn = dyn Fn(&[crate::JsValue]) -> Result<crate::JsValue, String>;
|
||||
|
||||
/// The built-ins the interpreter treats specially when it calls them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum NativeKind {
|
||||
Plain,
|
||||
/// `Function.prototype.call`.
|
||||
Call,
|
||||
/// `Function.prototype.apply`.
|
||||
Apply,
|
||||
/// The global `eval`: a call to it by that name is a direct eval.
|
||||
Eval,
|
||||
}
|
||||
|
||||
#[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> },
|
||||
}
|
||||
|
||||
/// A sloppy mode `arguments` object's link to its function's parameters (section 10.6): index
|
||||
/// `i` reads and writes the binding `names[i]` while that is `Some`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ArgMap {
|
||||
pub(crate) env: EnvId,
|
||||
pub(crate) names: Vec<Option<Key>>,
|
||||
}
|
||||
|
||||
/// A `for-in` loop's state: the names listed when it started, and the object they came from.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ForIn {
|
||||
pub(crate) keys: Vec<Key>,
|
||||
pub(crate) pos: usize,
|
||||
pub(crate) object: Option<ObjId>,
|
||||
}
|
||||
|
||||
/// What kind of object it is, with the internal state that kind has.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Class {
|
||||
Ordinary,
|
||||
Array {
|
||||
length: u32,
|
||||
length_writable: bool,
|
||||
},
|
||||
Function(Func),
|
||||
Error,
|
||||
Arguments(Box<ArgMap>),
|
||||
/// A `Boolean` object made by ToObject.
|
||||
Boolean,
|
||||
/// A `Number` object made by ToObject.
|
||||
Number,
|
||||
String(Str),
|
||||
RegExp,
|
||||
ForIn(Box<ForIn>),
|
||||
}
|
||||
|
||||
impl Class {
|
||||
/// The `[[Class]]` name `Object.prototype.toString` reports.
|
||||
pub(crate) fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Class::Ordinary | Class::ForIn(_) => "Object",
|
||||
Class::Array { .. } => "Array",
|
||||
Class::Function(_) => "Function",
|
||||
Class::Error => "Error",
|
||||
Class::Arguments(_) => "Arguments",
|
||||
Class::Boolean => "Boolean",
|
||||
Class::Number => "Number",
|
||||
Class::String(_) => "String",
|
||||
Class::RegExp => "RegExp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Object {
|
||||
pub(crate) class: Class,
|
||||
pub(crate) proto: Option<ObjId>,
|
||||
pub(crate) extensible: bool,
|
||||
pub(crate) props: PropMap<Prop>,
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub(crate) fn new(class: Class, proto: Option<ObjId>) -> Self {
|
||||
Self { class, proto, extensible: true, props: PropMap::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A binding in a declarative environment.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Binding {
|
||||
pub(crate) value: Value,
|
||||
pub(crate) mutable: bool,
|
||||
pub(crate) deletable: bool,
|
||||
}
|
||||
|
||||
/// An environment record (section 10.2.1) with its outer environment.
|
||||
pub(crate) enum Env {
|
||||
Decl {
|
||||
bindings: PropMap<Binding>,
|
||||
outer: Option<EnvId>,
|
||||
},
|
||||
/// The global object, or a `with` statement's object (`with_this`: calls to its functions get
|
||||
/// it as `this`).
|
||||
Obj {
|
||||
object: ObjId,
|
||||
with_this: bool,
|
||||
outer: Option<EnvId>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Env {
|
||||
pub(crate) fn outer(&self) -> Option<EnvId> {
|
||||
match self {
|
||||
Env::Decl { outer, .. } | Env::Obj { outer, .. } => *outer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Cell {
|
||||
Free,
|
||||
Object(Box<Object>),
|
||||
Env(Box<Env>),
|
||||
}
|
||||
|
||||
/// The estimated bytes of a property or a binding entry. A collection adds its name's code units
|
||||
/// separately, counting a buffer that several names or strings share once.
|
||||
pub(crate) const PROP_BYTES: usize = 64;
|
||||
/// The estimated bytes of an object without its properties.
|
||||
pub(crate) const OBJECT_BYTES: usize = 96;
|
||||
/// The estimated bytes of an environment without its bindings.
|
||||
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 a piece of code without its instructions, tables and strings.
|
||||
const CODE_BYTES: usize = 384;
|
||||
|
||||
pub(crate) fn string_bytes(len: usize) -> usize {
|
||||
32 + 2 * len
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Heap {
|
||||
cells: Vec<Cell>,
|
||||
free: Vec<u32>,
|
||||
/// An estimate of the bytes the heap holds: the live total from the last collection plus what
|
||||
/// was allocated since.
|
||||
pub(crate) bytes: usize,
|
||||
/// How many collections have run.
|
||||
pub(crate) collections: usize,
|
||||
}
|
||||
|
||||
impl Heap {
|
||||
fn alloc(&mut self, cell: Cell, bytes: usize) -> u32 {
|
||||
self.bytes += bytes;
|
||||
if let Some(i) = self.free.pop()
|
||||
&& let Some(slot) = self.cells.get_mut(i as usize)
|
||||
{
|
||||
*slot = cell;
|
||||
return i;
|
||||
}
|
||||
self.cells.push(cell);
|
||||
u32::try_from(self.cells.len() - 1).unwrap_or(u32::MAX)
|
||||
}
|
||||
|
||||
pub(crate) fn alloc_object(&mut self, object: Object) -> ObjId {
|
||||
let bytes = OBJECT_BYTES + PROP_BYTES * object.props.len();
|
||||
ObjId(self.alloc(Cell::Object(Box::new(object)), bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn alloc_env(&mut self, env: Env) -> EnvId {
|
||||
let bytes = ENV_BYTES + if let Env::Decl { bindings, .. } = &env { PROP_BYTES * bindings.len() } else { 0 };
|
||||
EnvId(self.alloc(Cell::Env(Box::new(env)), bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn object(&self, id: ObjId) -> Option<&Object> {
|
||||
match self.cells.get(id.0 as usize) {
|
||||
Some(Cell::Object(o)) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn object_mut(&mut self, id: ObjId) -> Option<&mut Object> {
|
||||
match self.cells.get_mut(id.0 as usize) {
|
||||
Some(Cell::Object(o)) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn env(&self, id: EnvId) -> Option<&Env> {
|
||||
match self.cells.get(id.0 as usize) {
|
||||
Some(Cell::Env(e)) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn env_mut(&mut self, id: EnvId) -> Option<&mut Env> {
|
||||
match self.cells.get_mut(id.0 as usize) {
|
||||
Some(Cell::Env(e)) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Live cells, for tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn live_cells(&self) -> usize {
|
||||
self.cells.iter().filter(|c| !matches!(c, Cell::Free)).count()
|
||||
}
|
||||
|
||||
/// Marks everything reachable from `roots` and frees the rest. Afterwards [`Heap::bytes`] is
|
||||
/// the estimated size of what is left, strings reachable from it included.
|
||||
pub(crate) fn collect(&mut self, roots: &Roots<'_>) {
|
||||
let mut marks = vec![false; self.cells.len()];
|
||||
let mut work: Vec<u32> = Vec::new();
|
||||
let mut strings = StringSizes::default();
|
||||
let mut live = 0usize;
|
||||
for v in &roots.values {
|
||||
mark_value(v, &mut work, &mut strings);
|
||||
}
|
||||
work.extend(roots.objects.iter().map(|o| o.0));
|
||||
work.extend(roots.envs.iter().map(|e| e.0));
|
||||
for code in &roots.codes {
|
||||
mark_code(code, &mut strings);
|
||||
}
|
||||
while let Some(i) = work.pop() {
|
||||
let Some(mark) = marks.get_mut(i as usize) else { continue };
|
||||
if *mark {
|
||||
continue;
|
||||
}
|
||||
*mark = true;
|
||||
match self.cells.get(i as usize) {
|
||||
Some(Cell::Object(o)) => live += trace_object(o, &mut work, &mut strings),
|
||||
Some(Cell::Env(e)) => live += trace_env(e, &mut work, &mut strings),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for (i, (cell, marked)) in self.cells.iter_mut().zip(&marks).enumerate() {
|
||||
if !marked && !matches!(cell, Cell::Free) {
|
||||
*cell = Cell::Free;
|
||||
self.free.push(u32::try_from(i).unwrap_or(u32::MAX));
|
||||
}
|
||||
}
|
||||
self.bytes = live + strings.bytes;
|
||||
self.collections += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// What the collector starts from.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Roots<'a> {
|
||||
pub(crate) values: Vec<&'a Value>,
|
||||
pub(crate) objects: Vec<ObjId>,
|
||||
pub(crate) envs: Vec<EnvId>,
|
||||
pub(crate) codes: Vec<&'a Code>,
|
||||
}
|
||||
|
||||
/// Sums the sizes of the strings and property names reached, counting each node and each
|
||||
/// code-unit buffer once.
|
||||
#[derive(Default)]
|
||||
struct StringSizes {
|
||||
seen: HashSet<usize>,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl StringSizes {
|
||||
fn add(&mut self, s: &Str) {
|
||||
let mut stack = vec![s.clone()];
|
||||
while let Some(s) = stack.pop() {
|
||||
if !self.seen.insert(s.node_addr()) {
|
||||
continue;
|
||||
}
|
||||
match s.halves() {
|
||||
Some((l, r)) => {
|
||||
self.bytes += ROPE_BYTES;
|
||||
stack.push(l);
|
||||
stack.push(r);
|
||||
}
|
||||
None => {
|
||||
if s.buffer_addr().is_none_or(|a| self.seen.insert(a)) {
|
||||
self.bytes += string_bytes(s.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a property or binding name, unless a string or another name shares its buffer.
|
||||
fn add_key(&mut self, k: &Key) {
|
||||
if self.seen.insert(Rc::as_ptr(k) as *const () as usize) {
|
||||
self.bytes += string_bytes(k.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_value(v: &Value, work: &mut Vec<u32>, strings: &mut StringSizes) {
|
||||
match v {
|
||||
Value::Object(o) => work.push(o.0),
|
||||
Value::String(s) => strings.add(s),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The estimated bytes of `code` with the functions nested in it and its source text, for
|
||||
/// charging code made at run time to the heap before anything refers to it.
|
||||
pub(crate) fn code_bytes(code: &Code) -> usize {
|
||||
let mut sizes = StringSizes::default();
|
||||
mark_code(code, &mut sizes);
|
||||
sizes.bytes
|
||||
}
|
||||
|
||||
/// Adds the estimated size of `code` and the functions nested in it, counting each piece of code,
|
||||
/// each source text and each string once however many closures share them.
|
||||
fn mark_code(code: &Code, strings: &mut StringSizes) {
|
||||
let mut stack = vec![code];
|
||||
while let Some(code) = stack.pop() {
|
||||
if !strings.seen.insert(code as *const Code as usize) {
|
||||
continue;
|
||||
}
|
||||
if strings.seen.insert(Rc::as_ptr(&code.source) as usize) {
|
||||
strings.bytes += code.source.lines.heap_bytes();
|
||||
// The text can share its buffer with the string eval was given.
|
||||
if strings.seen.insert(Rc::as_ptr(&code.source.text) as *const () as usize) {
|
||||
strings.bytes += string_bytes(code.source.text.len());
|
||||
}
|
||||
}
|
||||
let handles = code.strings.len()
|
||||
+ 2 * code.regexps.len()
|
||||
+ code.names.len()
|
||||
+ code.functions.len()
|
||||
+ code.params.len()
|
||||
+ code.vars.len()
|
||||
+ 2 * code.fun_decls.len();
|
||||
let tables = code.finally_tables.iter().map(|t| 24 + 4 * t.len()).sum::<usize>();
|
||||
strings.bytes += CODE_BYTES
|
||||
+ size_of::<Op>() * code.ops.len()
|
||||
+ 4 * code.positions.len()
|
||||
+ 8 * code.numbers.len()
|
||||
+ size_of::<Handler>() * code.handlers.len()
|
||||
+ 16 * handles
|
||||
+ tables;
|
||||
for s in &code.strings {
|
||||
strings.add(s);
|
||||
}
|
||||
for (pattern, flags) in &code.regexps {
|
||||
strings.add(pattern);
|
||||
strings.add(flags);
|
||||
}
|
||||
let keys = code.names.iter().chain(&code.params).chain(&code.vars).chain(code.fun_decls.iter().map(|(k, _)| k));
|
||||
for k in keys.chain(&code.self_name) {
|
||||
strings.add_key(k);
|
||||
}
|
||||
stack.extend(code.functions.iter().map(|f| &**f));
|
||||
}
|
||||
}
|
||||
|
||||
fn trace_object(o: &Object, work: &mut Vec<u32>, strings: &mut StringSizes) -> usize {
|
||||
if let Some(p) = o.proto {
|
||||
work.push(p.0);
|
||||
}
|
||||
for (k, prop) in o.props.iter() {
|
||||
strings.add_key(k);
|
||||
match prop {
|
||||
Prop::Data { value, .. } => mark_value(value, work, strings),
|
||||
Prop::Accessor { get, set, .. } => work.extend(get.iter().chain(set.iter()).map(|f| f.0)),
|
||||
}
|
||||
}
|
||||
match &o.class {
|
||||
Class::Function(Func::Closure { env, code }) => {
|
||||
work.push(env.0);
|
||||
mark_code(code, strings);
|
||||
}
|
||||
Class::Arguments(map) => work.push(map.env.0),
|
||||
Class::String(s) => strings.add(s),
|
||||
Class::ForIn(state) => {
|
||||
work.extend(state.object.iter().map(|o| o.0));
|
||||
strings.bytes += state.keys.iter().map(|k| string_bytes(k.len())).sum::<usize>();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
OBJECT_BYTES + PROP_BYTES * o.props.len()
|
||||
}
|
||||
|
||||
fn trace_env(e: &Env, work: &mut Vec<u32>, strings: &mut StringSizes) -> usize {
|
||||
match e {
|
||||
Env::Decl { bindings, outer } => {
|
||||
work.extend(outer.iter().map(|o| o.0));
|
||||
for (k, b) in bindings.iter() {
|
||||
strings.add_key(k);
|
||||
mark_value(&b.value, work, strings);
|
||||
}
|
||||
ENV_BYTES + PROP_BYTES * bindings.len()
|
||||
}
|
||||
Env::Obj { object, outer, .. } => {
|
||||
work.push(object.0);
|
||||
work.extend(outer.iter().map(|o| o.0));
|
||||
ENV_BYTES
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Class, Heap, OBJECT_BYTES, Object, PROP_BYTES, Prop, PropMap, Roots, key, string_bytes};
|
||||
use crate::value::{Str, Value};
|
||||
|
||||
#[test]
|
||||
fn a_collection_counts_a_name_and_a_value_that_share_a_buffer_once() {
|
||||
let mut heap = Heap::default();
|
||||
let name = key(&"n".repeat(1000));
|
||||
let mut object = Object::new(Class::Ordinary, None);
|
||||
object.props.insert(name.clone(), Prop::Data { value: Value::String(Str::from_rc(name.clone())), flags: 0 });
|
||||
object.props.insert(key("other"), Prop::Data { value: Value::Undefined, flags: 0 });
|
||||
let id = heap.alloc_object(object);
|
||||
heap.collect(&Roots { objects: vec![id], ..Roots::default() });
|
||||
assert_eq!(heap.bytes, OBJECT_BYTES + 2 * PROP_BYTES + string_bytes(1000) + string_bytes(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_maps_keep_insertion_order_through_removals_and_growth() {
|
||||
let mut m = PropMap::default();
|
||||
for i in 0..40 {
|
||||
m.insert(key(&format!("k{i}")), i);
|
||||
}
|
||||
for i in (0..40).step_by(2) {
|
||||
assert_eq!(m.remove(&key(&format!("k{i}"))), Some(i));
|
||||
}
|
||||
m.insert(key("k1"), 100);
|
||||
m.insert(key("new"), 7);
|
||||
let keys: Vec<String> = m.iter().map(|(k, _)| String::from_utf16_lossy(k)).collect();
|
||||
let mut want: Vec<String> = (1..40).step_by(2).map(|i| format!("k{i}")).collect();
|
||||
want.push("new".into());
|
||||
assert_eq!(keys, want);
|
||||
assert_eq!(m.get(&key("k1")), Some(&100));
|
||||
assert_eq!(m.len(), 21);
|
||||
assert!(!m.contains(&key("k0")));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,852 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::bytecode::{Code, Handler, Op, Source, verify};
|
||||
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}");
|
||||
}
|
||||
|
||||
fn fail(src: &str) -> RunError {
|
||||
match Realm::new().run(src) {
|
||||
Ok(v) => panic!("{src} completed with {v:?}"),
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
|
||||
fn small(limits: Limits) -> Realm {
|
||||
Realm::with_options(Options::default(), limits)
|
||||
}
|
||||
|
||||
// ---- language ----
|
||||
|
||||
#[test]
|
||||
fn assignments_resolve_the_variable_before_the_right_hand_side() {
|
||||
// Section 11.13.1: the reference is resolved first, so a binding the right-hand side adds
|
||||
// does not catch the assignment.
|
||||
truth("var x = 0; var scope = {}; with (scope) { x = (scope.x = 2, 1); } scope.x === 2 && x === 1");
|
||||
truth("var x = 0; var scope = {}; with (scope) { x += (scope.x = 2, 1); } scope.x === 2 && x === 1");
|
||||
truth("var x = 5; function f() { var r = (x = (eval('var x;'), 1)); return [x, r]; } var a = f(); a[0] === undefined && a[1] === 1 && x === 1");
|
||||
// Section 11.3.1: the operand's reference is resolved before ToNumber runs.
|
||||
truth(
|
||||
"var x = 0; var scope = {x: {valueOf: function(){ delete scope.x; return 2; }}}; \
|
||||
with (scope) { x++; } scope.x === 3 && x === 0",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_comparison_and_conversions() {
|
||||
assert_eq!(num("1 + 2 * 3 - 4 / 2"), 5.0);
|
||||
assert_eq!(num("7 % 3 + (-7 % 3)"), 0.0);
|
||||
assert_eq!(string("'a' + 1 + 2"), "a12");
|
||||
assert_eq!(num("1 + 2 + '3' - 0"), 33.0);
|
||||
assert_eq!(num("(1 << 31) >> 31"), -1.0);
|
||||
assert_eq!(num("-1 >>> 28"), 15.0);
|
||||
assert_eq!(num("~5 ^ 3 | 8 & 12"), -7.0);
|
||||
truth("'10' == 10 && null == undefined && !(null == 0) && '' == 0 && '1e1' == 10");
|
||||
truth("NaN !== NaN && 0 === -0 && 1/-0 === -Infinity");
|
||||
truth("'b' > 'a' && 'B' < 'a' && !(NaN < 1) && !(NaN >= 1) && 2 >= 2 && '10' < '9' && 10 > '9'");
|
||||
truth(
|
||||
"typeof 1 === 'number' && typeof 'x' === 'string' && typeof null === 'object' && typeof undefined_name === 'undefined' && typeof function(){} === 'function'",
|
||||
);
|
||||
truth("({valueOf: function(){ return 4; }}) * 2 === 8 && ({toString: function(){ return 'x'; }}) + 'y' === 'xy'");
|
||||
truth("+'0x10' === 16 && +' 12 ' === 12 && isNaNish(+'1x'); function isNaNish(n) { return n !== n; }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variables_functions_and_closures() {
|
||||
assert_eq!(num("var x = 1; function f() { return x + 1; } f()"), 2.0);
|
||||
assert_eq!(num("function counter() { var n = 0; return function() { return ++n; }; } var c = counter(); c(); c(); c()"), 3.0);
|
||||
assert_eq!(num("f(); function f() { return 5; }"), 5.0);
|
||||
assert_eq!(num("var g = function fact(n) { return n <= 1 ? 1 : n * fact(n - 1); }; g(10)"), 3_628_800.0);
|
||||
assert_eq!(num("function f(a, b) { return arguments.length * 10 + (b === undefined ? 1 : 0); } f(1)"), 11.0);
|
||||
assert_eq!(num("function f(a) { arguments[0] = 9; return a; } f(1)"), 9.0);
|
||||
assert_eq!(num("function f(a) { 'use strict'; arguments[0] = 9; return a; } f(1)"), 1.0);
|
||||
assert_eq!(num("function f(a) { a = 4; return arguments[0]; } f(1)"), 4.0);
|
||||
truth("var o = { m: function() { return this; } }; o.m() === o");
|
||||
truth("function f() { return this; } f() === this");
|
||||
truth("function f() { 'use strict'; return this; } f() === undefined");
|
||||
assert_eq!(num("function P(x) { this.x = x; } P.prototype.get = function() { return this.x; }; new P(7).get()"), 7.0);
|
||||
truth("function P() { return {a: 1}; } new P().a === 1 && new P() instanceof Object");
|
||||
truth("function P() {} var p = new P(); p instanceof P && p.constructor === P && P.prototype.isPrototypeOf(p)");
|
||||
assert_eq!(num("(function(a, b, c) {}).length"), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_and_apply_set_this_and_arguments() {
|
||||
assert_eq!(num("function f(a, b) { return this.k + a + b; } f.call({k: 1}, 2, 3)"), 6.0);
|
||||
assert_eq!(num("function f(a, b) { return this.k + a + b; } f.apply({k: 1}, [2, 3])"), 6.0);
|
||||
assert_eq!(num("function f() { return arguments.length; } f.apply(null, undefined)"), 0.0);
|
||||
assert_eq!(num("Function.prototype.call.call(function(a) { return a; }, null, 8)"), 8.0);
|
||||
let e = fail("Function.prototype.call.call(1)");
|
||||
assert_eq!(e.kind, RunErrorKind::Thrown);
|
||||
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_arrays_and_accessors() {
|
||||
assert_eq!(num("var o = {a: 1, 'b': 2, 3: 4}; o.a + o['b'] + o[3]"), 7.0);
|
||||
assert_eq!(num("var a = [1, , 3]; a.length"), 3.0);
|
||||
assert_eq!(num("var a = []; a[5] = 1; a.length"), 6.0);
|
||||
assert_eq!(num("var a = [1, 2, 3, 4]; a.length = 1; a[2] === undefined ? a.length : -1"), 1.0);
|
||||
assert_eq!(num("var o = { get x() { return this.y * 2; }, set x(v) { this.y = v; }, y: 1 }; o.x = 5; o.x"), 10.0);
|
||||
truth("var o = {a: 1}; delete o.a && !('a' in o) && o.a === undefined");
|
||||
truth("'length' in [] && !delete [].length");
|
||||
truth("'abc'.length === 3 && 'abc'[1] === 'b' && 'abc'.x === undefined");
|
||||
truth("var o = {}; o.hasOwnProperty('x') === false && ({x: 1}).hasOwnProperty('x')");
|
||||
truth("Object.prototype.toString.call([]) === '[object Array]' && Object.prototype.toString.call(null) === '[object Null]'");
|
||||
// Error.prototype and each NativeError prototype are Error objects (sections 15.11.4 and 15.11.7.7).
|
||||
truth(
|
||||
"var s = Object.prototype.toString, all = [Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError], ok = true;\
|
||||
for (var i = 0; i < all.length; i++) ok = ok && s.call(all[i].prototype) === '[object Error]'; ok",
|
||||
);
|
||||
let e = fail("var a = []; a.length = -1");
|
||||
assert!(e.message.starts_with("RangeError"), "{}", e.message);
|
||||
let e = fail("'use strict'; var o = { get x() { return 1; } }; o.x = 2;");
|
||||
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
||||
let e = fail("null.x");
|
||||
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_in_lists_own_properties_in_insertion_order_then_inherited_ones() {
|
||||
assert_eq!(
|
||||
string("function P() { this.z = 1; this.a = 2; } P.prototype.m = 3; P.prototype.a = 9; var s = ''; for (var k in new P()) s += k; s"),
|
||||
"zam"
|
||||
);
|
||||
assert_eq!(string("var o = {a: 1, b: 2, c: 3}, s = ''; for (var k in o) { delete o.b; s += k; } s"), "ac");
|
||||
assert_eq!(string("var s = ''; for (var k in 'xyz') s += k; s"), "012");
|
||||
assert_eq!(num("var n = 0; for (var k in null) n++; for (k in undefined) n++; n"), 0.0);
|
||||
assert_eq!(string("var o = {p: 1}, t = {}, s = ''; for (t.k in o) s += t.k; s"), "p");
|
||||
assert_eq!(string("var a = ['x', 'y'], s = ''; for (var i in a) s += i + a[i]; s"), "0x1y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_flow_with_labels_switch_and_finally() {
|
||||
assert_eq!(
|
||||
num(
|
||||
"var n = 0; outer: for (var i = 0; i < 5; i++) { for (var j = 0; j < 5; j++) { if (j == 2) continue outer; if (i == 3) break outer; n++; } } n"
|
||||
),
|
||||
6.0
|
||||
);
|
||||
assert_eq!(num("var r = 0; switch (3) { case 1: r = 1; case 3: r += 3; case 4: r += 4; break; default: r = 100; } r"), 7.0);
|
||||
assert_eq!(num("var r = 0; switch ('z') { case 'a': r = 1; break; default: r = 2; case 'b': r += 10; } r"), 12.0);
|
||||
assert_eq!(num("var log = 0; function f() { try { return 1; } finally { log = 5; } } f() + log"), 6.0);
|
||||
assert_eq!(num("function f() { try { return 1; } finally { return 2; } } f()"), 2.0);
|
||||
assert_eq!(num("var n = 0; for (var i = 0; i < 3; i++) { try { if (i == 1) continue; n += 10; } finally { n += 1; } } n"), 23.0);
|
||||
assert_eq!(num("var n = 0; while (true) { try { try { break; } finally { n += 1; } } finally { n += 10; } } n"), 11.0);
|
||||
assert_eq!(num("var n = 0; try { throw 3; } catch (e) { n = e; } finally { n *= 2; } n"), 6.0);
|
||||
assert_eq!(num("var n = 0; try { try { throw 1; } finally { n = 7; } } catch (e) { n += e; } n"), 8.0);
|
||||
assert_eq!(num("function f() { for (var k in {a: 1}) { try { return 4; } finally { } } } f()"), 4.0);
|
||||
assert_eq!(num("var x = 0; lbl: { x = 1; break lbl; x = 2; } x"), 1.0);
|
||||
assert_eq!(num("var i = 0; do { i++; } while (i < 10); i"), 10.0);
|
||||
assert_eq!(num("var e = 1; try { throw 2; } catch (e) { e = 3; } e"), 1.0);
|
||||
assert_eq!(num("function f() { var r = 0; with ({a: 1}) { try { return a; } finally { r = 1; } } } f()"), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_finally_block_keeps_the_completion_value_unless_it_ends_abruptly() {
|
||||
// Section 12.14: a finally block that ends normally leaves the try or catch block's value.
|
||||
let cases = [
|
||||
("try { 1; } finally { 2; }", 1.0),
|
||||
("try { throw 0; } catch (e) { 1; } finally { 2; }", 1.0),
|
||||
("eval('try { 1; } finally { 2; }')", 1.0),
|
||||
("try { try { 1; } finally { 2; } } finally { 3; }", 1.0),
|
||||
("do { try { 1; } finally { 2; break; } } while (false)", 2.0),
|
||||
("do { try { 1; } finally { 2; continue; } } while (false)", 2.0),
|
||||
];
|
||||
let wrong: Vec<(&str, f64, f64)> = cases.iter().map(|&(src, want)| (src, want, num(src))).filter(|&(_, want, got)| want != got).collect();
|
||||
assert!(wrong.is_empty(), "{wrong:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_eval_and_new_function() {
|
||||
assert_eq!(num("var o = {a: 2}; with (o) { a = 3; } o.a"), 3.0);
|
||||
truth("var o = {f: function() { return this; }}; with (o) { f() === o }");
|
||||
assert_eq!(num("var x = 1; function f() { var x = 2; return eval('x'); } f()"), 2.0);
|
||||
assert_eq!(num("var x = 1; function f() { var x = 2; return (0, eval)('x'); } f()"), 1.0);
|
||||
assert_eq!(num("function f() { eval('var y = 4'); return y; } f()"), 4.0);
|
||||
truth("function f() { 'use strict'; eval('var y = 4'); return typeof y; } f() === 'undefined'");
|
||||
assert_eq!(num("eval('1; 2; if (false) {}')"), 2.0);
|
||||
assert_eq!(num("new Function('a', 'b', 'return a * b;')(6, 7)"), 42.0);
|
||||
assert_eq!(num("Function('return 3')()"), 3.0);
|
||||
assert_eq!(num("var anonymous = 42; Function('return anonymous')()"), 42.0);
|
||||
let e = fail("new Function('a){}; (function(', 'return 1')");
|
||||
assert!(e.message.starts_with("SyntaxError"), "{}", e.message);
|
||||
let e = fail("new Function('}); (function() {')");
|
||||
assert!(e.message.starts_with("SyntaxError"), "{}", e.message);
|
||||
let e = fail("eval('var = 1')");
|
||||
assert!(e.message.starts_with("SyntaxError"), "{}", e.message);
|
||||
truth("try { eval('a b') } catch (e) { e instanceof SyntaxError }");
|
||||
assert_eq!(string("(function f(a) { return a; }).toString()"), "function f(a) { return a; }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_mode_assignments_and_errors() {
|
||||
let e = fail("'use strict'; undeclared = 1;");
|
||||
assert!(e.message.starts_with("ReferenceError"), "{}", e.message);
|
||||
assert_eq!(num("undeclared = 1; undeclared"), 1.0);
|
||||
let e = fail("'use strict'; (function f() { f = 1; })()");
|
||||
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
||||
assert_eq!(num("var g = function f() { f = 1; return typeof f === 'function' ? 1 : 0; }; g()"), 1.0);
|
||||
let e = fail("'use strict'; NaN = 1;");
|
||||
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
||||
let e = fail("'use strict'; (function() {}).caller");
|
||||
assert!(e.message.starts_with("TypeError"), "{}", e.message);
|
||||
truth("var o = {}; try { o.x.y; false } catch (e) { e instanceof TypeError && e.constructor === TypeError }");
|
||||
truth(
|
||||
"try { nope; } catch (e) { e instanceof ReferenceError && e.name === 'ReferenceError' && String_(e) === 'ReferenceError: nope is not defined' } function String_(v) { return '' + v; }",
|
||||
);
|
||||
truth(
|
||||
"new Error('m').message === 'm' && Error('m') instanceof Error && new TypeError().message === '' && new RangeError('r').toString() === 'RangeError: r'",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_carry_line_and_column() {
|
||||
let e = fail("var a = 1;\nvar b = 2;\n nope();");
|
||||
assert_eq!((e.kind, e.line, e.column), (RunErrorKind::Thrown, 3, 3));
|
||||
assert_eq!(e.message, "ReferenceError: nope is not defined");
|
||||
let e = fail("function f() {\n throw new TypeError('bad');\n}\nf();");
|
||||
assert_eq!((e.line, e.column), (2, 3));
|
||||
assert_eq!(e.message, "TypeError: bad");
|
||||
let e = fail("var x = ;");
|
||||
assert_eq!((e.kind, e.line), (RunErrorKind::Syntax, 1));
|
||||
let e = fail("throw 42");
|
||||
assert_eq!(e.message, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_functions_receive_arguments_and_return_values() {
|
||||
let mut realm = Realm::new();
|
||||
realm.define_function("add", |args| match args {
|
||||
[JsValue::Number(a), JsValue::Number(b)] => Ok(JsValue::Number(a + b)),
|
||||
_ => Err("add takes two numbers".into()),
|
||||
});
|
||||
assert_eq!(realm.run("add(2, 3)"), Ok(JsValue::Number(5.0)));
|
||||
let e = realm.run("add('x')").unwrap_err();
|
||||
assert_eq!(e.message, "Error: add takes two numbers");
|
||||
assert_eq!(realm.run("try { add(); } catch (e) { e.message }"), Ok(JsValue::String("add takes two numbers".into())));
|
||||
assert_eq!(realm.run("var kept = 1; 'next'"), Ok(JsValue::String("next".into())));
|
||||
assert_eq!(realm.run("kept"), Ok(JsValue::Number(1.0)), "a realm keeps its globals between runs");
|
||||
}
|
||||
|
||||
// ---- limits ----
|
||||
|
||||
fn limit(realm: &mut Realm, src: &str) -> LimitKind {
|
||||
match realm.run(src) {
|
||||
Err(RunError { kind: RunErrorKind::Limit(k), .. }) => k,
|
||||
other => panic!("{src}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_big(limits: Limits) -> Realm {
|
||||
let mut realm = small(limits);
|
||||
realm.define_function("big", |args| match args {
|
||||
[JsValue::Number(n)] => Ok(JsValue::String("x".repeat(*n as usize))),
|
||||
_ => Err("big takes a length".into()),
|
||||
});
|
||||
realm.define_function("fail", |args| match args {
|
||||
[JsValue::Number(n)] => Err("x".repeat(*n as usize)),
|
||||
_ => Err("fail takes a length".into()),
|
||||
});
|
||||
realm
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_messages_from_host_functions_count_toward_the_heap() {
|
||||
let limits = Limits { heap_bytes: 1 << 20, fuel: 100_000, ..Limits::default() };
|
||||
let mut realm = with_big(limits.clone());
|
||||
assert_eq!(realm.run("try { fail(1000); } catch (e) { e.message.length }"), Ok(JsValue::Number(1000.0)));
|
||||
assert_eq!(limit(&mut with_big(limits.clone()), "try { fail(1000000); } catch (e) {}"), LimitKind::Heap);
|
||||
assert_eq!(limit(&mut with_big(limits), "var a = []; for (var i = 0; ; i++) try { fail(100000); } catch (e) { a[i] = e; }"), LimitKind::Heap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_from_source_count_toward_the_heap_when_they_are_bound() {
|
||||
// Without a collection in between, the estimate grows by at least each new name's code units:
|
||||
// a global variable (an object property), a binding made by direct eval, and a parameter of a
|
||||
// function made by the Function constructor.
|
||||
// Each case runs in a new realm, so the charges for the code itself do not add up to a
|
||||
// collection.
|
||||
let name = "n".repeat(50_000);
|
||||
let cases = [
|
||||
format!("var {name} = 1;"),
|
||||
format!("(function () {{ eval('var {name}2 = 1; {name}2'); }})();"),
|
||||
format!("Function('{name}3', 'return {name}3')(1);"),
|
||||
];
|
||||
let mut short = Vec::new();
|
||||
for (i, src) in cases.iter().enumerate() {
|
||||
let mut realm = small(Limits::default());
|
||||
let collections = realm.interp().heap.collections;
|
||||
let before = realm.interp().heap.bytes;
|
||||
realm.run(src).unwrap();
|
||||
assert_eq!(realm.interp().heap.collections, collections, "a collection ran in case {i}");
|
||||
let grown = realm.interp().heap.bytes - before;
|
||||
if grown < crate::heap::string_bytes(50_000) {
|
||||
short.push((i, grown));
|
||||
}
|
||||
}
|
||||
assert!(short.is_empty(), "cases that grew by less than the name: {short:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_from_host_functions_count_toward_the_heap() {
|
||||
let limits = Limits { heap_bytes: 1 << 20, fuel: 100_000, ..Limits::default() };
|
||||
let mut realm = with_big(limits.clone());
|
||||
assert_eq!(realm.run("big(1000).length"), Ok(JsValue::Number(1000.0)));
|
||||
assert_eq!(realm.run("var a = []; for (var i = 0; i < 4; i++) a[i] = big(50000); a[3].length"), Ok(JsValue::Number(50000.0)));
|
||||
assert_eq!(limit(&mut with_big(limits.clone()), "var a = []; for (var i = 0; ; i++) a[i] = big(100000);"), LimitKind::Heap);
|
||||
assert_eq!(limit(&mut with_big(limits), "big(1000000).length"), LimitKind::Heap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn functions_kept_across_runs_count_their_code_toward_the_heap() {
|
||||
// Each run keeps a function whose source holds a 64 Ki-unit comment, and drops the string the
|
||||
// source was made from. The kept code fills the heap after a few runs.
|
||||
let limits = Limits { heap_bytes: 1 << 20, ..Limits::default() };
|
||||
let mut realm = small(limits);
|
||||
assert_eq!(realm.run("var keep = []; keep.length"), Ok(JsValue::Number(0.0)));
|
||||
let step = "var s = 'x'; for (var i = 0; i < 16; i++) s += s; keep[keep.length] = Function('/*' + s + '*/'); s = null; keep.length";
|
||||
let mut ended = None;
|
||||
for run in 1..=20 {
|
||||
match realm.run(step) {
|
||||
Ok(v) => assert_eq!(v, JsValue::Number(f64::from(run))),
|
||||
Err(RunError { kind: RunErrorKind::Limit(k), .. }) => {
|
||||
ended = Some((run, k));
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("run {run}: {e:?}"),
|
||||
}
|
||||
}
|
||||
let Some((run, kind)) = ended else { panic!("20 kept functions fit in a 1 MiB heap") };
|
||||
assert_eq!(kind, LimitKind::Heap);
|
||||
assert!(run > 2, "the heap filled on run {run}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_source_text_counts_toward_the_heap() {
|
||||
let limits = Limits { heap_bytes: 1 << 20, ..Limits::default() };
|
||||
let comment = "x".repeat(300_000);
|
||||
let src = format!("var f = function () {{ /*{comment}*/ }}; f.toString().length");
|
||||
assert_eq!(limit(&mut small(limits.clone()), &src), LimitKind::Heap);
|
||||
let mut realm = small(limits);
|
||||
assert_eq!(realm.run("(function f(a) { return a; }).toString()"), Ok(JsValue::String("function f(a) { return a; }".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_in_names_take_fuel_and_heap_under_later_limits() {
|
||||
// The first run, under the default limits, builds an object with many properties. Later runs
|
||||
// with smaller limits list its names only within their own fuel and heap.
|
||||
let mut realm = small(Limits::default());
|
||||
assert_eq!(realm.run("var o = {}; for (var i = 0; i < 100000; i++) o['k' + i] = i; o.k99999"), Ok(JsValue::Number(99999.0)));
|
||||
realm.set_limits(Limits { fuel: 1000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "for (var k in o) break;"), LimitKind::Fuel);
|
||||
realm.set_limits(Limits::default());
|
||||
assert_eq!(realm.run("var n = 0; for (var k in o) n++; n"), Ok(JsValue::Number(100_000.0)));
|
||||
realm.interp().collect_garbage();
|
||||
let live = realm.interp().heap.bytes;
|
||||
// The list of names takes more than 3 MB (at least 32 + 2 * 2 bytes each), so 1 MiB more than
|
||||
// the live heap holds the object but not the list.
|
||||
let heap_bytes = live + (1 << 20);
|
||||
realm.set_limits(Limits { heap_bytes, ..Limits::default() });
|
||||
assert_eq!(realm.run("o.k0"), Ok(JsValue::Number(0.0)));
|
||||
assert_eq!(limit(&mut realm, "for (var k in o) break;"), LimitKind::Heap);
|
||||
// The run ends before the list is made, so the estimate stays within the limit.
|
||||
assert!(realm.interp().heap.bytes <= heap_bytes, "{} > {heap_bytes}", realm.interp().heap.bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shrinking_an_array_takes_fuel_for_each_property() {
|
||||
let mut realm = small(Limits::default());
|
||||
assert_eq!(realm.run("var a = []; for (var i = 0; i < 100000; i++) a[i] = i; a.length"), Ok(JsValue::Number(100_000.0)));
|
||||
realm.set_limits(Limits { fuel: 1000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "a.length = 0;"), LimitKind::Fuel);
|
||||
realm.set_limits(Limits::default());
|
||||
// The run stopped before anything was removed.
|
||||
assert_eq!(realm.run("a.length + a[99999]"), Ok(JsValue::Number(199_999.0)));
|
||||
assert_eq!(realm.run("a.length = 1; a.length + a[0] + (99999 in a ? 1 : 0)"), Ok(JsValue::Number(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_takes_fuel_for_each_argument_it_reads() {
|
||||
let mut realm = small(Limits { fuel: 1000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "(function () {}).apply(null, {length: 65536});"), LimitKind::Fuel);
|
||||
assert_eq!(realm.run("(function () { return arguments.length; }).apply(null, {length: 100})"), Ok(JsValue::Number(100.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declarations_check_the_heap_before_they_are_bound() {
|
||||
// Each script declares more than fits in the heap. The run ends with the heap limit before
|
||||
// the bindings are made, so the estimate never goes past the limit.
|
||||
// Short names keep the source strings well inside the heap while the bindings are not.
|
||||
let limits = Limits { heap_bytes: 2 << 20, ..Limits::default() };
|
||||
let short = |mut i: usize| {
|
||||
let mut name = String::from("_");
|
||||
loop {
|
||||
name.push(char::from(b'a' + (i % 26) as u8));
|
||||
i /= 26;
|
||||
if i == 0 {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
};
|
||||
let vars = (0..40_000).map(short).collect::<Vec<_>>().join(",");
|
||||
let funs = (0..20_000).map(|i| format!("function f{i}() {{}}")).collect::<String>();
|
||||
let cases = [
|
||||
format!("var {vars};"),
|
||||
funs.clone(),
|
||||
format!("(function () {{ eval('var {vars};'); }})();"),
|
||||
format!("(function () {{ eval('{funs}'); }})();"),
|
||||
format!("(0, eval)('\"use strict\"; var {vars};');"),
|
||||
format!("Function('var {vars};')();"),
|
||||
format!("Function('{funs}')();"),
|
||||
];
|
||||
let mut wrong = Vec::new();
|
||||
for (i, src) in cases.iter().enumerate() {
|
||||
let mut realm = small(limits.clone());
|
||||
let r = realm.run(src).map_err(|e| e.kind);
|
||||
let bytes = realm.interp().heap.bytes;
|
||||
if r != Err(RunErrorKind::Limit(LimitKind::Heap)) || bytes > limits.heap_bytes {
|
||||
wrong.push((i, r, bytes));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "cases that bound past the limit: {wrong:?}");
|
||||
let few = (0..1000).map(|i| format!("b{i}")).collect::<Vec<_>>().join(",");
|
||||
let mut realm = small(limits);
|
||||
assert_eq!(realm.run(&format!("var {few}; b999 = 1; Function('var {few}; return 2')() + b999")), Ok(JsValue::Number(3.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declarations_take_fuel() {
|
||||
let vars = (0..10_000).map(|i| format!("a{i}")).collect::<Vec<_>>().join(",");
|
||||
let mut realm = small(Limits { fuel: 1000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, &format!("var {vars};")), LimitKind::Fuel);
|
||||
assert_eq!(limit(&mut realm, &format!("Function('var {vars};')();")), LimitKind::Fuel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lower_heap_limit_applies_to_what_is_already_live() {
|
||||
// A heap already past the new limit ends the next run.
|
||||
let mut realm = small(Limits::default());
|
||||
realm.run("var keep = []; for (var i = 0; i < 100000; i++) keep[i] = {i: i};").unwrap();
|
||||
realm.set_limits(Limits { heap_bytes: 1 << 20, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "for (var j = 0; j < 1000; j++) ({j: j});"), LimitKind::Heap);
|
||||
realm.set_limits(Limits::default());
|
||||
realm.run("keep = null;").unwrap();
|
||||
realm.set_limits(Limits { heap_bytes: 1 << 20, ..Limits::default() });
|
||||
assert_eq!(realm.run("for (var j = 0; j < 1000; j++) ({j: j}); j"), Ok(JsValue::Number(1000.0)));
|
||||
// A heap below the new limit cannot grow past it: the next collection runs by the new limit,
|
||||
// not by when it would have run under the old, higher one.
|
||||
let mut realm = small(Limits::default());
|
||||
realm.run("var keep = []; for (var i = 0; i < 100000; i++) keep[i] = {i: i};").unwrap();
|
||||
let live = realm.collect_garbage();
|
||||
realm.set_limits(Limits { heap_bytes: live + (1 << 20), ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "var more = []; for (var j = 0; j < 20000; j++) more[j] = {j: j};"), LimitKind::Heap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_keeps_its_arguments_alive_while_a_native_function_runs() {
|
||||
// The first argument's toString makes 20,000 objects, more than a 1 MiB heap holds, so a
|
||||
// collection runs before it returns. The second argument is reachable only from the list
|
||||
// that apply read from the array-like object.
|
||||
let mut realm = small(Limits { heap_bytes: 1 << 20, ..Limits::default() });
|
||||
let src = "var list = {length: 2, \
|
||||
get 0() { return {toString: function () { for (var i = 0; i < 20000; i++) ({i: i}); return 'a'; }}; }, \
|
||||
get 1() { return {toString: function () { return 'return a * 2'; }}; }}; \
|
||||
Function.apply(null, list)(21)";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(42.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_number_skips_the_white_space_the_lexer_skips() {
|
||||
let mut realm = small(Limits::default());
|
||||
assert_eq!(realm.run("var n = +'\\u180E'; n !== n"), Ok(JsValue::Bool(true)), "U+180E is not white space");
|
||||
assert_eq!(realm.run("var m = +'\\u180E1'; m !== m"), Ok(JsValue::Bool(true)));
|
||||
assert_eq!(realm.run("+' \\t\\n\\u2028\\u3000\\uFEFF 1 \\u00A0'"), Ok(JsValue::Number(1.0)));
|
||||
assert_eq!(realm.run("+' 1 '"), Ok(JsValue::Number(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_infinite_loop_ends_with_the_fuel_limit() {
|
||||
let mut realm = small(Limits { fuel: 100_000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "for (;;) {}"), LimitKind::Fuel);
|
||||
assert_eq!(limit(&mut realm, "while (true) { try { for (;;) {} } catch (e) {} }"), LimitKind::Fuel);
|
||||
assert_eq!(limit(&mut realm, "var n = 0; try { for (;;) {} } finally { n = 1; } "), LimitKind::Fuel);
|
||||
assert_eq!(
|
||||
limit(&mut realm, "function f() { try { for (;;) {} } finally { for (;;) {} } } while (true) { try { f(); } catch (e) {} }"),
|
||||
LimitKind::Fuel
|
||||
);
|
||||
assert_eq!(realm.run("n"), Ok(JsValue::Number(0.0)), "the finally block did not run");
|
||||
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)), "each run gets fresh fuel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_recursion_ends_with_the_frame_limit_on_a_small_stack() {
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(256 << 10)
|
||||
.spawn(|| {
|
||||
let mut realm = Realm::new();
|
||||
let r = realm.run("function f(n) { return n === 0 ? 0 : 1 + f(n - 1); } f(1000000)");
|
||||
let ok = realm.run("function g(n) { return n === 0 ? 0 : 1 + g(n - 1); } g(5000)");
|
||||
let caught = realm.run("function h(n) { try { return h(n + 1); } catch (e) { return -1; } } h(0)");
|
||||
(r, ok, caught)
|
||||
})
|
||||
.unwrap();
|
||||
let (r, ok, caught) = handle.join().unwrap();
|
||||
assert_eq!(r.map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Frames)));
|
||||
assert_eq!(ok, Ok(JsValue::Number(5000.0)));
|
||||
assert_eq!(caught.map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Frames)), "catch cannot swallow the frame limit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reentry_through_getters_ends_with_the_reentry_limit() {
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(8 << 20)
|
||||
.spawn(|| {
|
||||
let mut realm = Realm::new();
|
||||
let getter = realm.run("var o = { get x() { return this.x; } }; o.x");
|
||||
let conversion = realm.run("var v = { valueOf: function() { return v * 1; } }; v * 1");
|
||||
let eval = realm.run("function e() { return (0, eval)('e()'); } e()");
|
||||
(getter, conversion, eval)
|
||||
})
|
||||
.unwrap();
|
||||
let (getter, conversion, eval) = handle.join().unwrap();
|
||||
for r in [getter, conversion, eval] {
|
||||
assert_eq!(r.map_err(|e| e.kind), Err(RunErrorKind::Limit(LimitKind::Reentry)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_script_itself_does_not_count_against_the_reentry_limit() {
|
||||
let mut realm = small(Limits { reentry: 0, ..Limits::default() });
|
||||
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)));
|
||||
assert_eq!(limit(&mut realm, "var o = { get x() { return 1; } }; o.x"), LimitKind::Reentry);
|
||||
realm.set_limits(Limits { reentry: 1, ..Limits::default() });
|
||||
assert_eq!(realm.run("o.x"), Ok(JsValue::Number(1.0)));
|
||||
assert_eq!(realm.run("(0, eval)('2 + 2')"), Ok(JsValue::Number(4.0)));
|
||||
assert_eq!(limit(&mut realm, "var p = { get x() { return o.x; } }; p.x"), LimitKind::Reentry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_bombs_end_with_the_heap_limit() {
|
||||
let limits = Limits { heap_bytes: 4 << 20, ..Limits::default() };
|
||||
for src in [
|
||||
"var a = null; for (;;) a = {next: a, v: [a, a]};",
|
||||
"var s = 'x'; for (;;) s = s + s;",
|
||||
"var a = []; for (var i = 0; ; i++) a[i] = 'item' + i;",
|
||||
"var s = 'x'; for (;;) { s = s + s; var o = {}; o[s] = 1; }",
|
||||
] {
|
||||
let mut realm = small(limits.clone());
|
||||
assert_eq!(limit(&mut realm, src), LimitKind::Heap, "{src}");
|
||||
// What the bomb built is still reachable from the globals; raising the limit lets the
|
||||
// realm drop it.
|
||||
realm.set_limits(Limits::default());
|
||||
assert_eq!(realm.run("a = s = null; 1"), Ok(JsValue::Number(1.0)));
|
||||
realm.set_limits(limits.clone());
|
||||
assert_eq!(realm.run("2"), Ok(JsValue::Number(2.0)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_is_collected_cycles_included() {
|
||||
let mut realm = small(Limits { heap_bytes: 2 << 20, ..Limits::default() });
|
||||
let src = "for (var i = 0; i < 200000; i++) { var a = {}, b = {a: a, s: 'x' + i}; a.b = b; } i";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(200_000.0)));
|
||||
assert!(realm.interp().heap.collections > 0);
|
||||
let before = realm.collect_garbage();
|
||||
realm.run("var keep = []; for (var j = 0; j < 1000; j++) keep[j] = {j: j};").unwrap();
|
||||
let live = realm.collect_garbage();
|
||||
realm.run("keep = null;").unwrap();
|
||||
let after = realm.collect_garbage();
|
||||
assert!(live > before && after < live, "{before} {live} {after}");
|
||||
let cells = realm.interp().heap.live_cells();
|
||||
assert!(cells < 200, "{cells} cells are still live");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_million_appends_flatten_without_recursion() {
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(1 << 20)
|
||||
.spawn(|| {
|
||||
let mut realm = Realm::new();
|
||||
realm.run("var s = '', t = ''; for (var i = 0; i < 1000000; i++) { s += 'ab'; t = t + 'ab'; } s.length === 2000000 && s === t && s[1999999] === 'b'")
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(handle.join().unwrap(), Ok(JsValue::Bool(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_interrupt_flag_stops_a_run() {
|
||||
let mut realm = Realm::new();
|
||||
let flag = realm.interrupt();
|
||||
flag.set();
|
||||
assert_eq!(limit(&mut realm, "1 + 1"), LimitKind::Interrupted);
|
||||
flag.clear();
|
||||
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)));
|
||||
let from_host = realm.interrupt();
|
||||
realm.define_function("stop", move |_| {
|
||||
from_host.set();
|
||||
Ok(JsValue::Undefined)
|
||||
});
|
||||
assert_eq!(limit(&mut realm, "for (var i = 0; ; i++) { if (i === 1000) stop(); }"), LimitKind::Interrupted);
|
||||
assert!(flag.is_set());
|
||||
assert_eq!(limit(&mut realm, "i"), LimitKind::Interrupted, "the flag stays set");
|
||||
flag.clear();
|
||||
assert_eq!(realm.run("i"), Ok(JsValue::Number(1000.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_and_function_source_counts_against_the_source_limit() {
|
||||
let mut realm = Realm::with_options(Options { max_source_units: 200, ..Options::default() }, Limits::default());
|
||||
assert_eq!(realm.run("eval('1 + 1')"), Ok(JsValue::Number(2.0)));
|
||||
let e = realm.run("var s = 'x'; for (var i = 0; i < 8; i++) s += s; eval(s)").unwrap_err();
|
||||
assert!(e.message.contains(crate::TOO_LONG), "{e:?}");
|
||||
let e = realm.run("for (var i = 0; i < 20; i++) eval('1 + 1 + 1 + 1 + 1 + 1 + 1 + 1');").unwrap_err();
|
||||
assert!(e.message.contains(crate::TOO_LONG), "the sources of one run count together: {e:?}");
|
||||
let e = realm.run("Function('return 1;' + ' ')").unwrap_err();
|
||||
assert!(e.message.contains(crate::TOO_LONG), "{e:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_function_constructor_checks_the_source_limit_before_reading_its_arguments() {
|
||||
// A rope argument that is too long is refused before it is flattened.
|
||||
let mut realm = Realm::with_options(Options { max_source_units: 1000, ..Options::default() }, Limits::default());
|
||||
let e = realm.run("var s = 'return 1;'; for (var i = 0; i < 12; i++) s += s; Function('a', s)").unwrap_err();
|
||||
assert!(e.message.contains(crate::TOO_LONG), "{e:?}");
|
||||
let interp = realm.interp();
|
||||
match interp.find_property(interp.global, &crate::heap::key("s")) {
|
||||
Ok(Some(crate::heap::Prop::Data { value: crate::value::Value::String(s), .. })) => assert!(s.is_rope(), "the argument was flattened"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// The source is read as code units, so a lone surrogate in an argument is kept.
|
||||
let mut realm = Realm::new();
|
||||
assert_eq!(realm.run("Function('return \"\\ud800\"')() === '\\ud800'"), Ok(JsValue::Bool(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_script_longer_than_the_source_limit_is_a_syntax_error() {
|
||||
let mut realm = Realm::with_options(Options { max_source_units: 10, ..Options::default() }, Limits::default());
|
||||
let e = realm.run("1 + 2 + 3 + 4").unwrap_err();
|
||||
assert_eq!((&e.kind, e.message.contains(crate::TOO_LONG)), (&RunErrorKind::Syntax, true), "{e:?}");
|
||||
// Ten UTF-16 code units in 18 bytes of UTF-8 fit.
|
||||
assert_eq!(realm.run("'\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}\u{e9}'"), Ok(JsValue::String("\u{e9}".repeat(8))));
|
||||
let mut realm = Realm::with_options(Options { max_source_units: 40, ..Options::default() }, Limits::default());
|
||||
let e = realm.run("Function('return 1;')").unwrap_err();
|
||||
assert_eq!((&e.kind, e.message.contains(crate::TOO_LONG)), (&RunErrorKind::Thrown, true), "{e:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_last_thrown_binds_undefined_after_a_run_that_did_not_throw() {
|
||||
let mut realm = Realm::new();
|
||||
assert_eq!(realm.run("throw 'first'").map_err(|e| e.kind), Err(RunErrorKind::Thrown));
|
||||
realm.bind_last_thrown("caught");
|
||||
assert_eq!(realm.run("caught"), Ok(JsValue::String("first".into())));
|
||||
assert_eq!(realm.run("1 + 1"), Ok(JsValue::Number(2.0)));
|
||||
realm.bind_last_thrown("caught");
|
||||
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined));
|
||||
let _ = realm.run("throw 'second'");
|
||||
assert_eq!(realm.run("var").map_err(|e| e.kind), Err(RunErrorKind::Syntax));
|
||||
realm.bind_last_thrown("caught");
|
||||
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined), "a run that does not parse throws nothing");
|
||||
realm.set_limits(Limits { fuel: 100, ..Limits::default() });
|
||||
let _ = realm.run("throw 'third'");
|
||||
assert_eq!(limit(&mut realm, "for (;;) {}"), LimitKind::Fuel);
|
||||
realm.bind_last_thrown("caught");
|
||||
realm.set_limits(Limits::default());
|
||||
assert_eq!(realm.run("caught"), Ok(JsValue::Undefined), "a run that reached a limit threw nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_strict_realm_runs_scripts_as_strict_mode_code() {
|
||||
let mut realm = Realm::with_options(Options { strict: true, ..Options::default() }, Limits::default());
|
||||
assert_eq!(realm.run("with ({}) {}").map_err(|e| e.kind), Err(RunErrorKind::Syntax));
|
||||
assert_eq!(realm.run("(function () { return this; })()"), Ok(JsValue::Undefined));
|
||||
assert_eq!(realm.run("(function () { return typeof this; })()"), Ok(JsValue::String("undefined".into())));
|
||||
let mut realm = Realm::new();
|
||||
assert_eq!(realm.run("with ({}) {} 1"), Ok(JsValue::Number(1.0)));
|
||||
assert_eq!(realm.run("(function () { return typeof this; })()"), Ok(JsValue::String("object".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_apply_that_throws_while_reading_its_arguments_keeps_nothing_alive() {
|
||||
// Each call reads a fresh array at index 0 before index 1 throws; the arrays must not stay
|
||||
// reachable after the call ends.
|
||||
let mut realm = small(Limits { heap_bytes: 2 << 20, ..Limits::default() });
|
||||
let src = "function f() {} var caught = 0; \
|
||||
var list = {length: 2, get 0() { var a = []; for (var k = 0; k < 100; k++) a[k] = k; return a; }, get 1() { throw 'stop'; }}; \
|
||||
for (var i = 0; i < 5000; i++) { try { f.apply(null, list); } catch (e) { caught++; } } caught";
|
||||
assert_eq!(realm.run(src), Ok(JsValue::Number(5000.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_in_over_a_long_string_checks_the_heap_before_listing_its_indices() {
|
||||
// The string fits in the heap and its 131,072 index names do not. The fuel runs out partway
|
||||
// through listing them, so only the check before the listing ends the run with the heap limit.
|
||||
let mut realm = small(Limits { heap_bytes: 1 << 20, fuel: 50_000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "var s = 'x'; while (s.length < 100000) s += s; for (var k in s) {}"), LimitKind::Heap);
|
||||
assert_eq!(realm.run("var n = ''; for (var k in 'abc') n += k; n"), Ok(JsValue::String("012".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_property_names_count_toward_the_heap_after_their_strings_are_collected() {
|
||||
// Each name is a 16,385-unit buffer that only the object's key or the function's binding
|
||||
// holds once the string that made it is garbage, so about 32 of them fill the heap well before
|
||||
// the fuel runs out. In the second script the property lookup flattens a long string each
|
||||
// time, so collections run.
|
||||
let mut realm = small(Limits { heap_bytes: 1 << 20, fuel: 20_000, ..Limits::default() });
|
||||
let src = "var s = 'x'; while (s.length < 10000) s += s; var o = {}; for (var i = 0; ; i++) o[s + i] = 1;";
|
||||
assert_eq!(limit(&mut realm, src), LimitKind::Heap);
|
||||
let src = "var s = 'x'; while (s.length < 10000) s += s; \
|
||||
(function () { for (var i = 0; ; i++) { eval('var ' + s + i + ' = 1'); ({})[s + i]; } })();";
|
||||
let mut realm = small(Limits { heap_bytes: 1 << 20, fuel: 20_000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, src), LimitKind::Heap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_in_over_a_long_string_takes_fuel_for_each_index() {
|
||||
let mut realm = small(Limits { fuel: 10_000, ..Limits::default() });
|
||||
assert_eq!(limit(&mut realm, "var s = 'x'; while (s.length < 100000) s += s; for (var k in s) break;"), LimitKind::Fuel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wasm_builds_have_smaller_default_limits() {
|
||||
let limits = Limits::default();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
assert_eq!(limits, Limits { fuel: 250_000_000, heap_bytes: 128 << 20, frames: 2_000, reentry: 16 });
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
assert_eq!(limits, Limits { fuel: 1_000_000_000, heap_bytes: 512 << 20, frames: 10_000, reentry: 32 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_nesting_compiles_on_a_small_stack() {
|
||||
let n = crate::DEFAULT_MAX_DEPTH - 5;
|
||||
let cases = [
|
||||
format!("x = {}1{};", "[".repeat(n), "]".repeat(n)),
|
||||
format!("x = {}1{};", "{a:".repeat(n), "}".repeat(n)),
|
||||
format!("{}{}", "function f(){".repeat(n), "}".repeat(n)),
|
||||
format!("{}x = 1;", "if (1) ".repeat(n)),
|
||||
format!("x = {}1;", "1 ? 1 : ".repeat(n)),
|
||||
format!("x = 0; {}x++;{}", "while (x < 1) { try { ".repeat(n / 2), " } finally { } }".repeat(n / 2)),
|
||||
];
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(1 << 20)
|
||||
.spawn(move || cases.iter().map(|src| Realm::new().run(src).map(|_| ()).map_err(|e| e.kind)).collect::<Vec<_>>())
|
||||
.unwrap();
|
||||
for r in handle.join().unwrap() {
|
||||
assert!(r.is_ok() || r == Err(RunErrorKind::Syntax), "{r:?}");
|
||||
}
|
||||
let long = [
|
||||
format!("x = 1{};", " + 1".repeat(100_000)),
|
||||
format!("{}x = 1;", "a = ".repeat(100_000)),
|
||||
format!("x = {{}}; x.y = x; x{};", ".y".repeat(100_000)),
|
||||
];
|
||||
for src in &long {
|
||||
Realm::new().run(src).unwrap();
|
||||
}
|
||||
assert_eq!(num(&format!("var a; {}1; a", "a = ".repeat(100_000))), 1.0);
|
||||
assert_eq!(num(&format!("x = 1{}", " + 1".repeat(100_000))), 100_001.0);
|
||||
}
|
||||
|
||||
// ---- the verifier ----
|
||||
|
||||
fn code(ops: Vec<Op>) -> Code {
|
||||
let source = Rc::new(Source { lines: crate::LineTable::new(&[]), text: Rc::from(Vec::<u16>::new()) });
|
||||
Code {
|
||||
positions: vec![0; ops.len()],
|
||||
ops,
|
||||
numbers: vec![1.0],
|
||||
strings: Vec::new(),
|
||||
names: Vec::new(),
|
||||
functions: Vec::new(),
|
||||
regexps: Vec::new(),
|
||||
handlers: Vec::new(),
|
||||
finally_tables: Vec::new(),
|
||||
max_stack: 0,
|
||||
strict: false,
|
||||
self_name: None,
|
||||
params: Vec::new(),
|
||||
vars: Vec::new(),
|
||||
fun_decls: Vec::new(),
|
||||
uses_arguments: false,
|
||||
source,
|
||||
span: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_verifier_rejects_bad_bytecode() {
|
||||
assert_eq!(verify(&code(vec![Op::Number(0), Op::Return])), Ok(1));
|
||||
let bad: Vec<(Vec<Op>, &str)> = vec![
|
||||
(vec![Op::Jump(9)], "past the end"),
|
||||
(vec![Op::Pop, Op::Undefined, Op::Return], "pops below"),
|
||||
(vec![Op::Number(5), Op::Return], "number 5 out of range"),
|
||||
(vec![Op::GetVar(0), Op::Return], "name 0 out of range"),
|
||||
(vec![Op::Closure(0), Op::Return], "function 0 out of range"),
|
||||
(vec![Op::Undefined], "past its last instruction"),
|
||||
(vec![Op::True, Op::JumpIfFalse(3), Op::Undefined, Op::Undefined, Op::Return], "depths"),
|
||||
(vec![Op::Undefined, Op::Undefined, Op::EndFinally(0)], "finally table 0"),
|
||||
(vec![], "no instructions"),
|
||||
];
|
||||
for (ops, want) in bad {
|
||||
let e = verify(&code(ops.clone())).unwrap_err();
|
||||
assert!(e.contains(want), "{ops:?}: {e}");
|
||||
}
|
||||
let mut c = code(vec![Op::Undefined, Op::Return]);
|
||||
c.handlers.push(Handler { start: 0, end: 5, target: 0, depth: 0, env_depth: 0 });
|
||||
assert!(verify(&c).unwrap_err().contains("handler range"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_compiled_function_passes_the_verifier() {
|
||||
// Each code is verified as it is compiled; a failure would surface as an internal error.
|
||||
let src = "function f(a) { try { for (var k in a) { switch (k) { case 'x': continue; default: break; } } } catch (e) { return e; } finally { a = 0; } \
|
||||
with (a) { lbl: do { if (b) break lbl; } while (0); } return function() { return arguments; }; } 1";
|
||||
assert_eq!(run(src), JsValue::Number(1.0));
|
||||
}
|
||||
|
||||
// ---- robustness ----
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(256))]
|
||||
|
||||
#[test]
|
||||
fn arbitrary_fragment_programs_never_panic(parts in proptest::collection::vec(0..crate::tests::FRAGMENTS.len(), 0..60)) {
|
||||
let src: String = parts.iter().map(|&i| crate::tests::FRAGMENTS[i]).collect();
|
||||
let mut realm = small(Limits { fuel: 20_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 });
|
||||
if let Err(e) = realm.run(&src) {
|
||||
prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_text_never_panics(src in ".{0,120}") {
|
||||
let mut realm = small(Limits { fuel: 20_000, heap_bytes: 1 << 20, frames: 200, reentry: 8 });
|
||||
if let Err(e) = realm.run(&src) {
|
||||
prop_assert!(e.kind != RunErrorKind::Internal, "{src:?}: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-19
@@ -13,7 +13,7 @@
|
||||
//!
|
||||
//! The lexer moves forward through the source one character at a time and never recurses.
|
||||
|
||||
use crate::chars::{hex_value, is_decimal_digit, is_id_part, is_id_start, is_line_terminator, is_octal_digit, is_whitespace};
|
||||
use crate::chars::{hex_value, is_decimal_digit, is_id_part, is_id_start, is_line_terminator, is_octal_digit, is_whitespace, radix_value};
|
||||
use crate::error::SyntaxError;
|
||||
use crate::regexp;
|
||||
use crate::token::{Directive, DirectiveName, Keyword, Punct, Span, Token, TokenKind};
|
||||
@@ -352,26 +352,10 @@ impl<'s> Lexer<'s> {
|
||||
/// Reads digits in `radix` (8 or 16) and returns their value, or `None` when there are none.
|
||||
fn radix_digits(&mut self, radix: u32) -> Option<f64> {
|
||||
let begin = self.pos;
|
||||
let mut exact: u128 = 0;
|
||||
let mut value = 0.0_f64;
|
||||
let mut overflow = false;
|
||||
while let Some(d) = self.peek().and_then(hex_value).filter(|&d| d < radix) {
|
||||
match exact.checked_mul(u128::from(radix)).and_then(|v| v.checked_add(u128::from(d))) {
|
||||
Some(v) if !overflow => exact = v,
|
||||
_ => {
|
||||
if !overflow {
|
||||
value = exact as f64;
|
||||
overflow = true;
|
||||
}
|
||||
value = value * f64::from(radix) + f64::from(d);
|
||||
}
|
||||
}
|
||||
while self.peek().and_then(hex_value).is_some_and(|d| d < radix) {
|
||||
self.pos += 1;
|
||||
}
|
||||
if self.pos == begin {
|
||||
return None;
|
||||
}
|
||||
Some(if overflow { value } else { exact as f64 })
|
||||
radix_value(self.src[begin..self.pos].iter().filter_map(|&u| hex_value(u32::from(u))), radix)
|
||||
}
|
||||
|
||||
/// After `0` and a digit: an octal literal when every digit is octal, else a decimal literal
|
||||
|
||||
@@ -18,16 +18,34 @@
|
||||
//! `SyntaxError` ("nesting too deep") instead of exhausting the stack, binary operators, `if` …
|
||||
//! `else if` chains, labels and prefix operators are read with loops, and the syntax tree is
|
||||
//! dropped without recursion.
|
||||
//!
|
||||
//! A [`Realm`] runs scripts. It compiles the syntax tree to bytecode, checks the bytecode with a
|
||||
//! verifier, and runs it on an interpreter that keeps JavaScript calls on its own frame stack, so
|
||||
//! deep recursion in a script does not use the native stack. Objects and environments live in an
|
||||
//! arena with a mark-and-sweep collector; strings built by repeated `+` are flattened and dropped
|
||||
//! with loops. The realm provides the language (sections 10 to 14 of ECMA-262 5.1) and a minimal
|
||||
//! set of built-ins: `Object` and `Object.prototype`'s basic methods, `Function` with
|
||||
//! `Function.prototype.call` and `apply`, `eval`, and the error constructors. Every run is bounded
|
||||
//! 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.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod ast;
|
||||
mod builtins;
|
||||
mod bytecode;
|
||||
mod chars;
|
||||
mod compiler;
|
||||
mod error;
|
||||
mod heap;
|
||||
mod interp;
|
||||
mod lexer;
|
||||
mod number;
|
||||
mod parser;
|
||||
mod realm;
|
||||
mod regexp;
|
||||
mod token;
|
||||
mod value;
|
||||
|
||||
pub use ast::{
|
||||
AssignOp, Ast, BinaryOp, CatchClause, Expr, ExprId, ForInTarget, ForInit, Function, FunctionId, FunctionKind, LogicalOp, Name, Program, Property,
|
||||
@@ -36,6 +54,7 @@ pub use ast::{
|
||||
pub use error::{SyntaxError, TOO_DEEP, TOO_LONG};
|
||||
pub use lexer::{Goal, Lexer};
|
||||
pub use number::to_string as number_to_string;
|
||||
pub use realm::{Interrupt, JsValue, LimitKind, Limits, Realm, RunError, RunErrorKind};
|
||||
pub use token::{Directive, DirectiveName, Keyword, Punct, Span, Token, TokenKind};
|
||||
|
||||
/// How a script is read.
|
||||
@@ -136,6 +155,11 @@ impl LineTable {
|
||||
(u32::try_from(line).unwrap_or(u32::MAX), offset.saturating_sub(start) + 1)
|
||||
}
|
||||
|
||||
/// The estimated bytes the table holds.
|
||||
pub(crate) fn heap_bytes(&self) -> usize {
|
||||
32 + 4 * self.starts.len()
|
||||
}
|
||||
|
||||
pub(crate) fn line_count(&self) -> u32 {
|
||||
if self.empty {
|
||||
return 0;
|
||||
@@ -145,5 +169,7 @@ impl LineTable {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod interp_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//! The embedding API: a [`Realm`] holds a global object and its built-ins, runs scripts in them,
|
||||
//! and enforces [`Limits`].
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::bytecode::CodeKind;
|
||||
use crate::heap::{Func, HIDDEN, Object, Prop, key};
|
||||
use crate::interp::{Abrupt, CompileError, Interp};
|
||||
use crate::value::Value;
|
||||
use crate::{Options, SyntaxError};
|
||||
|
||||
/// The limits a run is held to. Reaching one ends the run with [`RunErrorKind::Limit`]; scripts
|
||||
/// cannot catch it, and their `finally` blocks do not run.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Limits {
|
||||
/// Instructions a run may execute.
|
||||
pub fuel: u64,
|
||||
/// Estimated bytes the heap (objects, environments and the strings they hold) may reach
|
||||
/// after a collection. A string longer than a quarter of this many code units cannot be
|
||||
/// made.
|
||||
pub heap_bytes: usize,
|
||||
/// Call frames that may be active at once.
|
||||
pub frames: usize,
|
||||
/// How deeply the interpreter may run again inside an operation (getters and setters,
|
||||
/// conversions that call `valueOf` or `toString`, indirect `eval`).
|
||||
pub reentry: usize,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self { fuel: 1_000_000_000, heap_bytes: 512 << 20, frames: 10_000, reentry: 32 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A wasm build has less memory and a smaller stack.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Self { fuel: 250_000_000, heap_bytes: 128 << 20, frames: 2_000, reentry: 16 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Which limit a run reached.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LimitKind {
|
||||
Fuel,
|
||||
Heap,
|
||||
Frames,
|
||||
Reentry,
|
||||
/// The host set the interrupt flag.
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
/// A flag the host sets, from any thread, to stop the running script at its next instruction.
|
||||
/// It stays set until [`Interrupt::clear`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Interrupt(Arc<AtomicBool>);
|
||||
|
||||
impl Interrupt {
|
||||
pub fn set(&self) {
|
||||
self.0.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.0.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn is_set(&self) -> bool {
|
||||
self.0.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value passed between a script and the host. An object is given by its class name.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum JsValue {
|
||||
Undefined,
|
||||
Null,
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
Object(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RunErrorKind {
|
||||
/// The source does not parse.
|
||||
Syntax,
|
||||
/// The script threw an exception it did not catch.
|
||||
Thrown,
|
||||
Limit(LimitKind),
|
||||
/// The interpreter reached a state it never makes.
|
||||
Internal,
|
||||
}
|
||||
|
||||
/// Why a run did not complete. `line` and `column` count from 1 (the column in UTF-16 code units)
|
||||
/// and give where the error was raised; they are 0 when that is not known.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunError {
|
||||
pub kind: RunErrorKind,
|
||||
pub message: String,
|
||||
pub line: u32,
|
||||
pub column: u32,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RunError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} (line {}, column {})", self.message, self.line, self.column)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RunError {}
|
||||
|
||||
impl From<SyntaxError> for RunError {
|
||||
fn from(e: SyntaxError) -> Self {
|
||||
RunError { kind: RunErrorKind::Syntax, message: e.to_string(), line: e.line, column: e.column }
|
||||
}
|
||||
}
|
||||
|
||||
/// A global environment with the built-ins, in which scripts run one after another.
|
||||
pub struct Realm {
|
||||
interp: Interp,
|
||||
}
|
||||
|
||||
impl Default for Realm {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Realm {
|
||||
pub fn new() -> Self {
|
||||
Self::with_options(Options::default(), Limits::default())
|
||||
}
|
||||
|
||||
/// A realm whose scripts (and the source given to `eval` and `Function`) are read with
|
||||
/// `options`; [`Options::max_source_units`] caps the source parsed in one run, all of it
|
||||
/// counted together.
|
||||
pub fn with_options(options: Options, limits: Limits) -> Self {
|
||||
Self { interp: Interp::new(options, limits) }
|
||||
}
|
||||
|
||||
pub fn limits(&self) -> &Limits {
|
||||
&self.interp.limits
|
||||
}
|
||||
|
||||
/// Sets the limits for the next run. A heap that is already larger than the new
|
||||
/// [`Limits::heap_bytes`] ends the next run with the heap limit unless a collection frees
|
||||
/// enough of it.
|
||||
pub fn set_limits(&mut self, limits: Limits) {
|
||||
self.interp.limits = limits;
|
||||
self.interp.reset_gc_threshold();
|
||||
}
|
||||
|
||||
/// The realm's interrupt flag. Setting it stops a running script, or the next one to start.
|
||||
pub fn interrupt(&self) -> Interrupt {
|
||||
Interrupt(self.interp.interrupt.clone())
|
||||
}
|
||||
|
||||
/// Defines a global function that calls `f` with its arguments. An `Err` from `f` is thrown
|
||||
/// to the script as an `Error` with that message.
|
||||
pub fn define_function(&mut self, name: &str, f: impl Fn(&[JsValue]) -> Result<JsValue, String> + 'static) {
|
||||
let interp = &mut self.interp;
|
||||
let proto = interp.intrinsics.function_proto;
|
||||
let func = Func::Host { f: std::rc::Rc::new(f), name: name.into() };
|
||||
let o = interp.heap.alloc_object(Object::new(crate::heap::Class::Function(func), Some(proto)));
|
||||
let global = interp.global;
|
||||
let _ = interp.define(global, key(name), Prop::Data { value: Value::Object(o), flags: HIDDEN });
|
||||
}
|
||||
|
||||
/// Runs `source` as a script in the global environment and returns its completion value (the
|
||||
/// value of the last expression statement that ran).
|
||||
pub fn run(&mut self, source: &str) -> Result<JsValue, RunError> {
|
||||
let interp = &mut self.interp;
|
||||
interp.fuel = interp.limits.fuel;
|
||||
interp.source_units = 0;
|
||||
interp.throw_site = None;
|
||||
interp.last_thrown = Value::Undefined;
|
||||
let strict = interp.options.strict;
|
||||
let code = match interp.encode_source(source).and_then(|units| interp.compile_source(&units, strict, CodeKind::Global)) {
|
||||
Ok(code) => code,
|
||||
Err(CompileError::Syntax(e)) => return Err(e.into()),
|
||||
Err(CompileError::Internal(m)) => return Err(RunError { kind: RunErrorKind::Internal, message: m, line: 0, column: 0 }),
|
||||
};
|
||||
let result = interp.run_global(code, false);
|
||||
let (line, column) = interp.throw_site.take().map_or((0, 0), |(code, pc)| code.line_column(pc));
|
||||
interp.stack.clear();
|
||||
interp.frames.clear();
|
||||
interp.temp_roots.clear();
|
||||
match result {
|
||||
Ok(v) => Ok(interp.to_js_value(&v)),
|
||||
Err(Abrupt::Throw(v)) => {
|
||||
let message = interp.describe(&v);
|
||||
interp.last_thrown = v;
|
||||
Err(RunError { kind: RunErrorKind::Thrown, message, line, column })
|
||||
}
|
||||
Err(Abrupt::Limit(kind)) => Err(RunError { kind: RunErrorKind::Limit(kind), message: format!("limit reached: {kind:?}"), line, column }),
|
||||
Err(Abrupt::Internal(message)) => Err(RunError { kind: RunErrorKind::Internal, message, line, column }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the global variable `name` holding the exception the last run did not catch
|
||||
/// (`undefined` if there was none).
|
||||
pub fn bind_last_thrown(&mut self, name: &str) {
|
||||
let interp = &mut self.interp;
|
||||
let v = interp.last_thrown.clone();
|
||||
let global = interp.global;
|
||||
let _ = interp.define(global, key(name), Prop::Data { value: v, flags: HIDDEN });
|
||||
}
|
||||
|
||||
/// Collects garbage now and returns the heap's estimated size in bytes afterwards.
|
||||
pub fn collect_garbage(&mut self) -> usize {
|
||||
self.interp.collect_garbage();
|
||||
self.interp.heap.bytes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn interp(&mut self) -> &mut Interp {
|
||||
&mut self.interp
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,9 @@ fn numeric_literals() {
|
||||
assert_eq!(number("0x1F;"), 31.0);
|
||||
assert_eq!(number("0XfF;"), 255.0);
|
||||
assert_eq!(number("0x20000000000001;"), 9007199254740992.0);
|
||||
// 2^136 + 2^83 + 1 and 2^150 + 2^97 + 1: a set bit past 128 bits rounds the half-way case up.
|
||||
assert_eq!(number(&format!("0x1{}8{}1;", "0".repeat(13), "0".repeat(19))), 2f64.powi(136) + 2f64.powi(84));
|
||||
assert_eq!(number(&format!("01{}2{}1;", "0".repeat(17), "0".repeat(31))), 2f64.powi(150) + 2f64.powi(98));
|
||||
assert_eq!(number("017;"), 15.0);
|
||||
assert_eq!(number("08;"), 8.0);
|
||||
assert_eq!(number("09.5;"), 9.5);
|
||||
@@ -703,7 +706,7 @@ fn sources_over_the_length_limit_are_refused() {
|
||||
// ---- robustness ----
|
||||
|
||||
/// Fragments of the language, put together at random so the parser reaches its deeper states.
|
||||
const FRAGMENTS: &[&str] = &[
|
||||
pub(crate) const FRAGMENTS: &[&str] = &[
|
||||
"var ",
|
||||
"function ",
|
||||
"f",
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
//! Values (ECMA-262 5.1 section 8) and the conversions between primitives (section 9).
|
||||
//!
|
||||
//! A string is a rope: concatenation makes a node that points at its two halves, and the first
|
||||
//! read of its characters flattens it, with a loop and an explicit stack, into one buffer that
|
||||
//! replaces the node. Strings hold no objects, so the reference counts that share them cannot form
|
||||
//! a cycle; dropping a long rope also uses a loop, not recursion.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::heap::ObjId;
|
||||
|
||||
/// A JavaScript value. Objects are handles into the interpreter's heap.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Value {
|
||||
Undefined,
|
||||
Null,
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
String(Str),
|
||||
Object(ObjId),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub(crate) fn str(s: &str) -> Value {
|
||||
Value::String(Str::from(s))
|
||||
}
|
||||
|
||||
pub(crate) fn as_object(&self) -> Option<ObjId> {
|
||||
match self {
|
||||
Value::Object(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_nullish(&self) -> bool {
|
||||
matches!(self, Value::Undefined | Value::Null)
|
||||
}
|
||||
|
||||
/// ToBoolean (section 9.2).
|
||||
pub(crate) fn truthy(&self) -> bool {
|
||||
match self {
|
||||
Value::Undefined | Value::Null => false,
|
||||
Value::Bool(b) => *b,
|
||||
Value::Number(n) => !(n.is_nan() || *n == 0.0),
|
||||
Value::String(s) => s.len() > 0,
|
||||
Value::Object(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of a string value, in UTF-16 code units: a flat buffer or a concatenation.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Str(Rc<RefCell<Node>>);
|
||||
|
||||
enum Node {
|
||||
Flat(Rc<[u16]>),
|
||||
Concat {
|
||||
left: Str,
|
||||
right: Str,
|
||||
len: usize,
|
||||
},
|
||||
/// A node whose halves were moved out while it was dropped.
|
||||
Taken,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Str {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self.to_rust_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Str {
|
||||
fn from(s: &str) -> Self {
|
||||
Str::from_units(s.encode_utf16().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Strings at most this long are copied when concatenated instead of becoming a rope node.
|
||||
const FLAT_CONCAT: usize = 32;
|
||||
|
||||
impl Str {
|
||||
pub(crate) fn from_units(units: Vec<u16>) -> Self {
|
||||
Str::from_rc(units.into())
|
||||
}
|
||||
|
||||
pub(crate) fn from_rc(units: Rc<[u16]>) -> Self {
|
||||
Str(Rc::new(RefCell::new(Node::Flat(units))))
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
match &*self.0.borrow() {
|
||||
Node::Flat(f) => f.len(),
|
||||
Node::Concat { len, .. } => *len,
|
||||
Node::Taken => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The node is not yet flattened.
|
||||
pub(crate) fn is_rope(&self) -> bool {
|
||||
matches!(&*self.0.borrow(), Node::Concat { .. })
|
||||
}
|
||||
|
||||
/// An address that identifies the node, for counting shared nodes once.
|
||||
pub(crate) fn node_addr(&self) -> usize {
|
||||
Rc::as_ptr(&self.0) as *const () as usize
|
||||
}
|
||||
|
||||
/// The address of the code-unit buffer of a flat node, which a property name can share.
|
||||
pub(crate) fn buffer_addr(&self) -> Option<usize> {
|
||||
match &*self.0.borrow() {
|
||||
Node::Flat(f) => Some(Rc::as_ptr(f) as *const () as usize),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The two halves of a concatenation node.
|
||||
pub(crate) fn halves(&self) -> Option<(Str, Str)> {
|
||||
match &*self.0.borrow() {
|
||||
Node::Concat { left, right, .. } => Some((left.clone(), right.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `a + b`. The caller checks the length limit first.
|
||||
pub(crate) fn concat(a: &Str, b: &Str) -> Str {
|
||||
let (la, lb) = (a.len(), b.len());
|
||||
if la == 0 {
|
||||
return b.clone();
|
||||
}
|
||||
if lb == 0 {
|
||||
return a.clone();
|
||||
}
|
||||
if la + lb <= FLAT_CONCAT {
|
||||
let mut units = Vec::with_capacity(la + lb);
|
||||
units.extend_from_slice(&a.flat());
|
||||
units.extend_from_slice(&b.flat());
|
||||
return Str::from_units(units);
|
||||
}
|
||||
Str(Rc::new(RefCell::new(Node::Concat { left: a.clone(), right: b.clone(), len: la + lb })))
|
||||
}
|
||||
|
||||
/// The code units, flattening the rope first if it is one.
|
||||
pub(crate) fn flat(&self) -> Rc<[u16]> {
|
||||
if let Node::Flat(f) = &*self.0.borrow() {
|
||||
return f.clone();
|
||||
}
|
||||
let mut units = Vec::with_capacity(self.len());
|
||||
let mut stack = vec![self.clone()];
|
||||
while let Some(s) = stack.pop() {
|
||||
match &*s.0.borrow() {
|
||||
Node::Flat(f) => units.extend_from_slice(f),
|
||||
Node::Concat { left, right, .. } => {
|
||||
stack.push(right.clone());
|
||||
stack.push(left.clone());
|
||||
}
|
||||
Node::Taken => {}
|
||||
}
|
||||
}
|
||||
let flat: Rc<[u16]> = units.into();
|
||||
let old = std::mem::replace(&mut *self.0.borrow_mut(), Node::Flat(flat.clone()));
|
||||
drop_node(old);
|
||||
flat
|
||||
}
|
||||
|
||||
pub(crate) fn to_rust_string(&self) -> String {
|
||||
String::from_utf16_lossy(&self.flat())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Str {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.0, &other.0) || (self.len() == other.len() && self.flat() == other.flat())
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops a node's halves with a loop, so a rope of a million appends does not recurse a million
|
||||
/// levels deep.
|
||||
fn drop_node(node: Node) {
|
||||
let mut stack = Vec::new();
|
||||
if let Node::Concat { left, right, .. } = node {
|
||||
stack.push(left);
|
||||
stack.push(right);
|
||||
}
|
||||
while let Some(s) = stack.pop() {
|
||||
if Rc::strong_count(&s.0) == 1 {
|
||||
let taken = std::mem::replace(&mut *s.0.borrow_mut(), Node::Taken);
|
||||
if let Node::Concat { left, right, .. } = taken {
|
||||
stack.push(left);
|
||||
stack.push(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Str {
|
||||
fn drop(&mut self) {
|
||||
if Rc::strong_count(&self.0) == 1 && self.is_rope() {
|
||||
let node = std::mem::replace(&mut *self.0.borrow_mut(), Node::Taken);
|
||||
drop_node(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// White space and line terminators as StrWhiteSpaceChar (section 9.3.1) counts them: the same
|
||||
/// classes the lexer uses between tokens.
|
||||
pub(crate) fn is_str_space(u: u16) -> bool {
|
||||
crate::chars::is_whitespace(u32::from(u)) || crate::chars::is_line_terminator(u32::from(u))
|
||||
}
|
||||
|
||||
/// ToNumber applied to a string (section 9.3.1): the StringNumericLiteral grammar, NaN for
|
||||
/// anything else.
|
||||
pub(crate) fn string_to_number(units: &[u16]) -> f64 {
|
||||
let start = units.iter().position(|&u| !is_str_space(u)).unwrap_or(units.len());
|
||||
let end = units.iter().rposition(|&u| !is_str_space(u)).map_or(start, |i| i + 1);
|
||||
let Some(text) = units.get(start..end) else { return f64::NAN };
|
||||
if text.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let Ok(s) = String::from_utf16(text) else { return f64::NAN };
|
||||
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||
if hex.is_empty() || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return f64::NAN;
|
||||
}
|
||||
return crate::chars::radix_value(hex.bytes().filter_map(|b| crate::chars::hex_value(u32::from(b))), 16).unwrap_or(f64::NAN);
|
||||
}
|
||||
let (sign, body) = match s.as_bytes().first() {
|
||||
Some(b'+') => (1.0, &s[1..]),
|
||||
Some(b'-') => (-1.0, &s[1..]),
|
||||
_ => (1.0, s.as_str()),
|
||||
};
|
||||
if body == "Infinity" {
|
||||
return sign * f64::INFINITY;
|
||||
}
|
||||
if !is_decimal_literal(body) {
|
||||
return f64::NAN;
|
||||
}
|
||||
body.parse::<f64>().map_or(f64::NAN, |n| sign * n)
|
||||
}
|
||||
|
||||
/// `body` is a StrUnsignedDecimalLiteral without `Infinity`: digits with an optional fraction and
|
||||
/// exponent, at least one digit in the mantissa.
|
||||
fn is_decimal_literal(body: &str) -> bool {
|
||||
let b = body.as_bytes();
|
||||
let mut i = 0;
|
||||
let digits = |i: &mut usize| {
|
||||
let s = *i;
|
||||
while b.get(*i).is_some_and(u8::is_ascii_digit) {
|
||||
*i += 1;
|
||||
}
|
||||
*i - s
|
||||
};
|
||||
let mut mantissa = digits(&mut i);
|
||||
if b.get(i) == Some(&b'.') {
|
||||
i += 1;
|
||||
mantissa += digits(&mut i);
|
||||
}
|
||||
if mantissa == 0 {
|
||||
return false;
|
||||
}
|
||||
if matches!(b.get(i), Some(b'e' | b'E')) {
|
||||
i += 1;
|
||||
if matches!(b.get(i), Some(b'+' | b'-')) {
|
||||
i += 1;
|
||||
}
|
||||
if digits(&mut i) == 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
i == b.len()
|
||||
}
|
||||
|
||||
/// ToString applied to a number (section 9.8.1).
|
||||
pub(crate) fn number_to_str(n: f64) -> Str {
|
||||
Str::from(crate::number::to_string(n).as_str())
|
||||
}
|
||||
|
||||
/// ToInt32 (section 9.5).
|
||||
pub(crate) fn to_int32(n: f64) -> i32 {
|
||||
to_uint32(n) as i32
|
||||
}
|
||||
|
||||
/// ToUint32 (section 9.6).
|
||||
pub(crate) fn to_uint32(n: f64) -> u32 {
|
||||
if !n.is_finite() || n == 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let int = n.trunc();
|
||||
let m = int.rem_euclid(4_294_967_296.0);
|
||||
m as u32
|
||||
}
|
||||
|
||||
/// The array index a property name stands for (section 15.4: a canonical numeric string below
|
||||
/// 2^32 − 1).
|
||||
pub(crate) fn array_index(key: &[u16]) -> Option<u32> {
|
||||
if key.is_empty() || key.len() > 10 || (key.len() > 1 && key.first() == Some(&u16::from(b'0'))) {
|
||||
return None;
|
||||
}
|
||||
let mut n: u64 = 0;
|
||||
for &u in key {
|
||||
let d = u.checked_sub(u16::from(b'0')).filter(|d| *d <= 9)?;
|
||||
n = n * 10 + u64::from(d);
|
||||
}
|
||||
u32::try_from(n).ok().filter(|&n| n != u32::MAX)
|
||||
}
|
||||
|
||||
/// The decimal digits of an array index.
|
||||
pub(crate) fn index_key(i: u32) -> Rc<[u16]> {
|
||||
i.to_string().encode_utf16().collect::<Vec<u16>>().into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Str, array_index, string_to_number, to_int32, to_uint32};
|
||||
|
||||
fn u(s: &str) -> Vec<u16> {
|
||||
s.encode_utf16().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_convert_to_numbers_by_the_string_numeric_literal_grammar() {
|
||||
let cases: &[(&str, f64)] = &[
|
||||
("", 0.0),
|
||||
(" \n ", 0.0),
|
||||
("12", 12.0),
|
||||
(" -1.5e2 ", -150.0),
|
||||
("0x1F", 31.0),
|
||||
(".5", 0.5),
|
||||
("5.", 5.0),
|
||||
("+Infinity", f64::INFINITY),
|
||||
("-Infinity", f64::NEG_INFINITY),
|
||||
];
|
||||
for (s, want) in cases {
|
||||
assert_eq!(string_to_number(&u(s)), *want, "{s:?}");
|
||||
}
|
||||
for s in ["inf", "nan", "1_0", "0x", "1e", ".", "--1", "0x-1", "infinity", "1 2"] {
|
||||
assert!(string_to_number(&u(s)).is_nan(), "{s:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_strings_round_once_to_the_nearest_number() {
|
||||
assert_eq!(string_to_number(&u("0xc83ce80553ee648")), 0xc83_ce80_553e_e648_u64 as f64);
|
||||
// 2^136 + 2^83 + 1: a set bit past 128 bits rounds the half-way case up.
|
||||
let digits = format!("0x1{}8{}1", "0".repeat(13), "0".repeat(19));
|
||||
assert_eq!(string_to_number(&u(&digits)), 2f64.powi(136) + 2f64.powi(84));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int32_and_uint32_wrap() {
|
||||
assert_eq!(to_int32(4_294_967_296.0 + 5.0), 5);
|
||||
assert_eq!(to_int32(2_147_483_648.0), i32::MIN);
|
||||
assert_eq!(to_uint32(-1.0), u32::MAX);
|
||||
assert_eq!(to_int32(f64::NAN), 0);
|
||||
assert_eq!(to_int32(-1.9), -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_indices_are_canonical_and_below_the_largest_u32() {
|
||||
assert_eq!(array_index(&u("0")), Some(0));
|
||||
assert_eq!(array_index(&u("4294967294")), Some(4_294_967_294));
|
||||
assert_eq!(array_index(&u("4294967295")), None);
|
||||
assert_eq!(array_index(&u("01")), None);
|
||||
assert_eq!(array_index(&u("1.0")), None);
|
||||
assert_eq!(array_index(&u("")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_million_appends_flatten_and_drop_without_recursion() {
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(256 << 10)
|
||||
.spawn(|| {
|
||||
let x = Str::from("abcdefghijklmnopqrstuvwxyz0123456789");
|
||||
let mut s = Str::from("");
|
||||
for _ in 0..1_000_000 {
|
||||
s = Str::concat(&s, &x);
|
||||
}
|
||||
let len = s.len();
|
||||
let flat = s.flat();
|
||||
(len, flat.len())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(handle.join().unwrap(), (36_000_000, 36_000_000));
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(256 << 10)
|
||||
.spawn(|| {
|
||||
let x = Str::from("abcdefghijklmnopqrstuvwxyz0123456789");
|
||||
let mut s = Str::from("");
|
||||
for _ in 0..1_000_000 {
|
||||
s = Str::concat(&x, &s);
|
||||
}
|
||||
s.len()
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(handle.join().unwrap(), 36_000_000);
|
||||
}
|
||||
}
|
||||
+143
-1
@@ -11,17 +11,29 @@
|
||||
//! `INTERPRETING.md` says), unless its `onlyStrict`, `noStrict` or `raw` flag says otherwise. The
|
||||
//! test prints the counts and writes each failure to `failures.txt` beside the suite; it fails only
|
||||
//! when the suite cannot be read.
|
||||
//!
|
||||
//! A second test runs the same ES5 tests through the interpreter, each in a new realm after the
|
||||
//! suite's `assert.js`, `sta.js` and the files its `includes:` names: every test that must parse
|
||||
//! and uses only the built-ins the interpreter has (those that name a missing global or method,
|
||||
//! in their own source or an included file, are counted as skipped, as are `async` tests). A test
|
||||
//! passes when it completes, or, for `negative: phase: runtime`, when it throws an object whose
|
||||
//! `constructor` is the global its `type:` names. Its counts are printed apart from the parse
|
||||
//! counts, and its failures go to `run-failures.txt`.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use vectorcraft_js::{Options, parse};
|
||||
use vectorcraft_js::{JsValue, Limits, Options, Realm, RunErrorKind, TokenKind, parse, tokenize};
|
||||
|
||||
/// A test file's metadata, from its `/*--- … ---*/` block.
|
||||
#[derive(Default)]
|
||||
struct Meta {
|
||||
features: Vec<String>,
|
||||
flags: Vec<String>,
|
||||
includes: Vec<String>,
|
||||
negative_phase: Option<String>,
|
||||
/// The error constructor a negative test expects (`negative: type:`).
|
||||
negative_type: Option<String>,
|
||||
/// The test names the ES5 section it was written for (`es5id:`).
|
||||
es5: bool,
|
||||
}
|
||||
@@ -86,6 +98,10 @@ fn meta(source: &str) -> Meta {
|
||||
meta.features = list(v, &mut lines);
|
||||
} else if let Some(v) = trimmed.strip_prefix("flags:").filter(|_| !indented) {
|
||||
meta.flags = list(v, &mut lines);
|
||||
} else if let Some(v) = trimmed.strip_prefix("includes:").filter(|_| !indented) {
|
||||
meta.includes = list(v, &mut lines);
|
||||
} else if let Some(v) = trimmed.strip_prefix("type:").filter(|_| in_negative) {
|
||||
meta.negative_type = Some(v.trim().to_string());
|
||||
} else if trimmed.starts_with("es5id:") && !indented {
|
||||
meta.es5 = true;
|
||||
} else if trimmed == "negative:" && !indented {
|
||||
@@ -176,3 +192,129 @@ fn es5_parse_subset() {
|
||||
report.display()
|
||||
);
|
||||
}
|
||||
|
||||
/// 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";
|
||||
|
||||
/// The stand-in for `String` that the harness's failure messages call.
|
||||
const HARNESS_STRING: &str = "function String(value) { return '' + value; }";
|
||||
|
||||
/// The source names a missing built-in, as an identifier, a property name or a word inside a
|
||||
/// string literal (code passed to `eval` or `Function`), or does not parse as ES5 (the tests that
|
||||
/// use `let` and `const`).
|
||||
fn needs_missing_builtin(source: &str, missing: &HashSet<&str>) -> bool {
|
||||
let Ok(tokens) = tokenize(source, &Options::default()) else { return true };
|
||||
tokens.iter().any(|t| match &t.kind {
|
||||
TokenKind::Ident(name) => missing.contains(name.as_str()),
|
||||
TokenKind::String(units) => {
|
||||
String::from_utf16_lossy(units).split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '$')).any(|word| missing.contains(word))
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Runs one test in a new realm; `Err` describes a failure.
|
||||
fn run_one(script: &str, negative_type: Option<&str>) -> Result<(), String> {
|
||||
let mut realm = Realm::with_options(Options::default(), Limits { fuel: 20_000_000, heap_bytes: 256 << 20, ..Limits::default() });
|
||||
match (realm.run(script), negative_type) {
|
||||
(Ok(_), None) => Ok(()),
|
||||
(Ok(_), Some(t)) => Err(format!("completed; a {t} was expected")),
|
||||
(Err(e), Some(t)) if e.kind == RunErrorKind::Thrown => {
|
||||
realm.bind_last_thrown("__thrown");
|
||||
match realm.run(&format!("typeof __thrown === 'object' && __thrown !== null && __thrown.constructor === {t}")) {
|
||||
Ok(JsValue::Bool(true)) => Ok(()),
|
||||
_ => Err(format!("threw {} (line {}); a {t} was expected", e.message, e.line)),
|
||||
}
|
||||
}
|
||||
(Err(e), _) => Err(format!("{:?}: {} (line {}, column {})", e.kind, e.message, e.line, e.column)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn es5_language_subset_runs() {
|
||||
let Some(dir) = suite_dir() else {
|
||||
println!("test262 interpreter: SKIPPED (no suite; run cargo xtask test262)");
|
||||
return;
|
||||
};
|
||||
// Each test runs on a thread with a stack large enough for the nested runs the re-entry limit
|
||||
// allows in an unoptimized build.
|
||||
let handle = std::thread::Builder::new().stack_size(64 << 20).spawn(move || run_language_subset(&dir)).unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
fn run_language_subset(dir: &Path) {
|
||||
let language = dir.join("test").join("language");
|
||||
let harness_dir = dir.join("harness");
|
||||
// A file that cannot be read gives an empty source.
|
||||
let read = |p: &Path| String::from_utf8_lossy(&std::fs::read(p).unwrap_or_default()).into_owned();
|
||||
// `assert.js` formats a failed assertion's message with the global `String`, which the
|
||||
// interpreter does not provide yet; the stand-in converts with `+` so a failure reports the
|
||||
// assertion's message instead of a ReferenceError.
|
||||
let prelude = format!("{HARNESS_STRING}\n{}\n{}\n", read(&harness_dir.join("assert.js")), read(&harness_dir.join("sta.js")));
|
||||
let missing: HashSet<&str> = MISSING_BUILTINS.split_whitespace().collect();
|
||||
let verbose = std::env::var_os("VECTORCRAFT_TEST262_VERBOSE").is_some();
|
||||
let (mut tests, mut skipped, mut passed, mut failed, mut panicked) = (0usize, 0usize, 0usize, 0usize, 0usize);
|
||||
let mut failures = String::new();
|
||||
for path in test_files(&language) {
|
||||
let source = read(&path);
|
||||
let meta = meta(&source);
|
||||
let negative_parse = matches!(meta.negative_phase.as_deref(), Some("parse" | "resolution"));
|
||||
if !meta.es5 || !meta.features.is_empty() || negative_parse || meta.flags.iter().any(|f| f == "module") {
|
||||
continue;
|
||||
}
|
||||
let includes: String = meta.includes.iter().map(|i| read(&harness_dir.join(i)) + "\n").collect();
|
||||
if meta.flags.iter().any(|f| f == "async") || needs_missing_builtin(&source, &missing) || needs_missing_builtin(&includes, &missing) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
tests += 1;
|
||||
let has = |flag: &str| meta.flags.iter().any(|f| f == flag);
|
||||
let modes: &[bool] = if has("raw") || has("noStrict") {
|
||||
&[false]
|
||||
} else if has("onlyStrict") {
|
||||
&[true]
|
||||
} else {
|
||||
&[false, true]
|
||||
};
|
||||
let rel = path.strip_prefix(dir).unwrap_or(&path).display().to_string().replace('\\', "/");
|
||||
for &strict in modes {
|
||||
let mode = if strict { "strict" } else { "sloppy" };
|
||||
if verbose {
|
||||
eprintln!("{rel} ({mode})");
|
||||
}
|
||||
let body = if has("raw") { source.clone() } else { format!("{prelude}{includes}{source}") };
|
||||
let script = if strict { format!("\"use strict\";\n{body}") } else { body };
|
||||
let negative_type = meta.negative_type.clone().filter(|_| meta.negative_phase.as_deref() == Some("runtime"));
|
||||
let outcome = std::panic::catch_unwind(|| run_one(&script, negative_type.as_deref()));
|
||||
match outcome {
|
||||
Ok(Ok(())) => passed += 1,
|
||||
Ok(Err(why)) => {
|
||||
failed += 1;
|
||||
failures.push_str(&format!("{rel} ({mode}): {why}\n"));
|
||||
}
|
||||
Err(_) => {
|
||||
failed += 1;
|
||||
panicked += 1;
|
||||
failures.push_str(&format!("{rel} ({mode}): PANIC\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let report = dir.join("run-failures.txt");
|
||||
if let Err(e) = std::fs::write(&report, &failures) {
|
||||
println!("could not write {}: {e}", report.display());
|
||||
}
|
||||
println!(
|
||||
"test262 ES5 language subset through the interpreter: {tests} tests that need only the built-ins present, {passed} runs passed, \
|
||||
{failed} failed ({panicked} panicked). Skipped {skipped} tests that need other built-ins, use syntax after ES5 or are asynchronous. Failures: {}",
|
||||
report.display()
|
||||
);
|
||||
assert_eq!(panicked, 0, "the interpreter panicked; see {}", report.display());
|
||||
}
|
||||
|
||||
@@ -669,7 +669,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- MCP protocol: five prompts (`prompts/list` / `prompts/get`: poster, icon set, recolour, trace-and-style, export set), `completion/complete` for prompt arguments and template variables read from the live catalogues (formats, effect ids, command ids, trace presets, swatch and object ids), four resource templates (`vectorcraft://object/{id}`, `//command/{id}`, `//effect/{id}`, `//swatch/{name}`) so an agent reads one thing instead of the whole document, and `logging/setLevel` with `notifications/message`. `window.panel` ids are case-insensitive (display labels work too), `vectorcraft://object/{id}` reads through `document.node {summary: true}` (the same reply bytes from one lookup instead of summarizing the whole document: a leaf read on a 2,000-object document drops from ~20 ms to ~0.06 ms), large containers slice with `document.node {depth?, childLimit?}` (truncated levels report `childCount`), `document.inspect` and the `inspect_document` tool slice the same way so a depth-0 read is the layer skeleton (~9.7 KB instead of ~2 MB on a 2,000-object document), `document.find` locates nodes by name/kind/text with ancestor paths, and the CLI sweeps the protocol surface over stdio against the compiled binary. `docs/mcp.md` has a Protocol section covering the capability table and what is deliberately absent (subscriptions, progress, cancellation, sampling, elicitation, Streamable HTTP — all of which need the transport to interleave reads).
|
||||
- Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`). A `run` step, like a `command_batch` step over MCP, can use an earlier step's result (`"$1.id"`, #785); `run --commands FILE` (or `-` for standard input) reads the steps from a JSON array or JSON Lines, past the Windows command-line limit (#3147, `apps/vectorcraft-cli/tests/it/cli.rs` `run_reads_*`, `run_refuses_malformed_commands_naming_the_step`).
|
||||
- File › Scripts › Check Script… reads a script written for Illustrator (`.jsx`, `.jsxinc` or a compiled `.jsxbin`) without running any of it and reports what it needs (the document model, files, dialogs, menu commands, features only Illustrator has), what it would do outside the document, the menu commands and document-model members it uses, its directives and included files, and the problems that would stop it loading (`script.check`, which also reads `.js` files); File › Open and drag-and-drop show the report for `.jsx`, `.jsxinc` and `.jsxbin` files.
|
||||
- `vectorcraft-js` parses JavaScript as ECMAScript 5.1 defines it, with the legacy octal forms, block functions and lenient regular expressions of Annex B, into a syntax tree with source spans; it reads the `#target`, `#include` and other preprocessor lines as directives without running them, and a script that breaks the grammar or an early-error rule gives a `SyntaxError` with its line and column. Nesting past the parser's depth limit (`Options::max_depth`, 200 by default) gives a `SyntaxError` instead of exhausting the stack. `cargo xtask test262` fetches the conformance suite at a pinned commit and parses its ES5 tests: 5,411 of 5,418 runs pass (the 7 others use `let` and `const`). Nothing runs scripts yet. Tests: `crates/js/src/tests.rs`, `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs`.
|
||||
- `vectorcraft-js` 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`.
|
||||
- **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.
|
||||
|
||||
+4
-2
@@ -43,7 +43,8 @@ commands:
|
||||
pack PNGs into a Windows .ico (used by packaging/icons.sh)
|
||||
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) and parse its ES5 subset with vectorcraft-js, printing the counts
|
||||
verified), parse its ES5 subset with vectorcraft-js and run the ES5 language
|
||||
tests its interpreter supports, printing the counts
|
||||
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)
|
||||
";
|
||||
@@ -338,7 +339,8 @@ Tests that use a corpus skip cleanly when it is absent.
|
||||
corpus/affinity/ public Affinity documents (CC0/MIT/Apache-2.0) at pinned commits, sha256
|
||||
verified: oracles for the Affinity reader. Fetch: cargo xtask corpus --affinity
|
||||
corpus/test262/ the ECMAScript conformance suite (BSD-3-Clause) at a pinned commit, sha256
|
||||
verified: the JavaScript parser's ES5 subset. Fetch and run: cargo xtask test262
|
||||
verified: the JavaScript parser's and interpreter's ES5 subsets. Fetch and run:
|
||||
cargo xtask test262
|
||||
",
|
||||
corpus.display()
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! `cargo xtask test262`: the official ECMAScript conformance suite (https://github.com/tc39/test262,
|
||||
//! 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` test, which parses the ES5 parse subset and prints its counts.
|
||||
//! runs `vectorcraft-js`'s `test262` tests, which parse the ES5 parse subset, run the ES5
|
||||
//! `test/language` tests that need only the interpreter's built-ins, and print each one's counts.
|
||||
//!
|
||||
//! 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`].
|
||||
|
||||
Reference in New Issue
Block a user