Files
vectorcraft/crates/js/src/interp.rs
T
dotCooCoo 16b291aca1 JavaScript: built-ins install under any limits, and class octal escapes match the compiler
A realm made with a low fuel or heap limit lost RegExp and Date, because
installing them compiled a pattern under that limit and the error was
dropped. The built-ins now install before the realm's limits apply.

The early pattern check read an octal class escape starting with 4 to 7
as three digits, while the compiler reads two, so `/[\477-z]/` gave a
SyntaxError for a pattern the compiler accepts. The check now reads
class octal escapes with the compiler's own reader.

docs/target-app-parity.md: the interpreter's entry lists the new
built-ins and the current language-test count.
2026-11-07 21:48:35 -08:00

2315 lines
93 KiB
Rust

//! The interpreter: runs bytecode on an explicit stack of frames.
//!
//! A call to a function written in JavaScript pushes a frame and goes on in the same loop, so
//! JavaScript recursion uses the heap, not the Rust stack. The loop runs again inside a Rust call
//! only where the language needs a function's result in the middle of an operation (a getter or
//! setter, `valueOf` and `toString` during a conversion, indirect `eval`); those nested runs are
//! counted against [`crate::Limits::reentry`].
//!
//! Before each instruction the interpreter takes one unit of fuel, checks the interrupt flag and,
//! when the heap has grown past its threshold, collects garbage. Running out of fuel, the heap
//! limit, the frame limit, the re-entry limit and the interrupt flag each end the run with an
//! [`Abrupt::Limit`] that unwinds every frame without running catch or finally blocks.
//!
//! Collection runs only between instructions. An operation that may run JavaScript in the middle
//! keeps its operands on the operand stack until it is done, so everything it holds is reachable
//! from the roots when a nested run collects.
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::ast::BinaryOp;
use crate::bytecode::{Code, CodeKind, Op, Source};
use crate::compiler::{CompileFail, compile_program};
use crate::heap::{
ArgMap, Binding, CONFIGURABLE, Class, ENUMERABLE, ENV_BYTES, Env, EnvId, ForIn, Func, HIDDEN, Heap, Key, NativeKind, OBJECT_BYTES, OPEN, ObjId,
Object, PROP_BYTES, Prop, PropMap, ROPE_BYTES, Roots, WRITABLE, code_bytes, key, string_bytes,
};
use crate::value::{Str, Value, array_index, index_key, number_to_str, string_to_number, to_int32, to_uint32};
use crate::{LimitKind, Limits, Options};
/// How an operation ended other than normally.
#[derive(Debug)]
pub(crate) enum Abrupt {
/// A JavaScript exception: `try` and `catch` can handle it.
Throw(Value),
/// A limit was reached; nothing handles it.
Limit(LimitKind),
/// The interpreter found itself in a state it never makes (a bug); nothing handles it.
Internal(String),
}
pub(crate) type JsResult<T> = Result<T, Abrupt>;
fn internal<T>(message: &str) -> JsResult<T> {
Err(Abrupt::Internal(message.into()))
}
/// The kinds of error object the engine throws.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ErrorType {
Error,
Eval,
Range,
Reference,
Syntax,
Type,
Uri,
}
/// The objects the interpreter itself refers to.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Intrinsics {
pub(crate) object_proto: ObjId,
pub(crate) function_proto: ObjId,
pub(crate) array_proto: ObjId,
pub(crate) string_proto: ObjId,
pub(crate) number_proto: ObjId,
pub(crate) boolean_proto: ObjId,
pub(crate) regexp_proto: ObjId,
pub(crate) date_proto: ObjId,
pub(crate) error_protos: [ObjId; 7],
pub(crate) eval: ObjId,
pub(crate) throw_type_error: ObjId,
}
impl Intrinsics {
fn placeholder() -> Self {
let o = ObjId(0);
Self {
object_proto: o,
function_proto: o,
array_proto: o,
string_proto: o,
number_proto: o,
boolean_proto: o,
regexp_proto: o,
date_proto: o,
error_protos: [o; 7],
eval: o,
throw_type_error: o,
}
}
pub(crate) fn error_proto(&self, t: ErrorType) -> ObjId {
let i = match t {
ErrorType::Error => 0,
ErrorType::Eval => 1,
ErrorType::Range => 2,
ErrorType::Reference => 3,
ErrorType::Syntax => 4,
ErrorType::Type => 5,
ErrorType::Uri => 6,
};
self.error_protos.get(i).copied().unwrap_or(self.object_proto)
}
fn all(&self) -> Vec<ObjId> {
let mut v = vec![
self.object_proto,
self.function_proto,
self.array_proto,
self.string_proto,
self.number_proto,
self.boolean_proto,
self.regexp_proto,
self.date_proto,
self.eval,
self.throw_type_error,
];
v.extend(self.error_protos);
v
}
}
pub(crate) struct Frame {
pub(crate) code: Rc<Code>,
pub(crate) pc: usize,
/// Where this frame's operands start on the operand stack.
base: usize,
env: EnvId,
var_env: EnvId,
/// `with` and catch environments entered and not yet left.
env_depth: u32,
this: Value,
callee: Option<ObjId>,
/// The object `new` made, returned unless the function returns an object.
construct: Option<ObjId>,
completion: Value,
/// Returning from this frame returns from the Rust call that pushed it.
boundary: bool,
}
/// Where the exception being thrown was raised: the code and the instruction.
type ThrowSite = (Rc<Code>, usize);
pub(crate) struct Interp {
pub(crate) heap: Heap,
pub(crate) stack: Vec<Value>,
pub(crate) frames: Vec<Frame>,
/// Values native code holds while it may run JavaScript.
pub(crate) temp_roots: Vec<Value>,
pub(crate) global: ObjId,
pub(crate) global_env: EnvId,
pub(crate) intrinsics: Intrinsics,
pub(crate) limits: Limits,
pub(crate) options: Options,
pub(crate) fuel: u64,
pub(crate) interrupt: Arc<AtomicBool>,
pub(crate) reentry: usize,
gc_threshold: usize,
/// UTF-16 code units of source parsed in this run, for [`Options::max_source_units`].
pub(crate) source_units: usize,
pub(crate) throw_site: Option<ThrowSite>,
/// The last exception a run did not catch.
pub(crate) last_thrown: Value,
/// The clock and time zone `Date` reads.
pub(crate) date_host: crate::date_builtins::DateHost,
/// The bytes native code holds outside the heap's cells through [`HeldBytes`].
pub(crate) held: Rc<HeldCount>,
}
/// The bytes all live [`HeldBytes`] hold, and the bytes dropped holders released that the heap's
/// total still counts.
#[derive(Default)]
pub(crate) struct HeldCount {
now: std::cell::Cell<usize>,
released: std::cell::Cell<usize>,
}
/// Bytes that native code holds in Rust memory while it works (a matcher's state and stack, a
/// replacement being built, a pattern being compiled), counted against the heap limit. They are
/// in the heap's total, and in the live total of each collection, until the holder is dropped;
/// the heap's total gives them back before the next limit check.
pub(crate) struct HeldBytes {
count: Rc<HeldCount>,
bytes: usize,
}
impl HeldBytes {
pub(crate) fn new(interp: &Interp) -> Self {
Self { count: interp.held.clone(), bytes: 0 }
}
/// The bytes this holder holds.
#[cfg(test)]
pub(crate) fn bytes(&self) -> usize {
self.bytes
}
/// Holds `n` more bytes, ending the run with the heap limit when they would not fit after a
/// collection. Everything the caller still needs must be reachable from the roots.
pub(crate) fn add(&mut self, interp: &mut Interp, n: usize) -> JsResult<()> {
interp.reserve_heap(n)?;
interp.heap.bytes = interp.heap.bytes.saturating_add(n);
self.bytes = self.bytes.saturating_add(n);
self.count.now.set(self.count.now.get().saturating_add(n));
Ok(())
}
}
impl Drop for HeldBytes {
fn drop(&mut self) {
self.count.now.set(self.count.now.get().saturating_sub(self.bytes));
self.count.released.set(self.count.released.get().saturating_add(self.bytes));
}
}
/// The least heap size a collection waits for.
const MIN_GC_BYTES: usize = 1 << 20;
/// The most arguments `Function.prototype.apply` passes.
pub(crate) const MAX_APPLY_ARGS: usize = 1 << 16;
/// The estimated bytes of a name's entries in the list and the set `for-in` builds.
const KEY_ENTRY_BYTES: usize = 32;
/// The estimated bytes of the index names `0` to `len - 1` while `for-in` lists them.
fn index_keys_bytes(len: usize) -> usize {
let mut total = 0usize;
let (mut lo, mut hi, mut digits) = (0usize, 10usize, 1usize);
while lo < len {
let count = hi.min(len) - lo;
total = total.saturating_add(count.saturating_mul(string_bytes(digits) + KEY_ENTRY_BYTES));
lo = hi;
hi = hi.saturating_mul(10);
digits += 1;
}
total
}
/// The estimated bytes of a binding or property named by `len` code units.
fn binding_bytes(len: usize) -> usize {
PROP_BYTES + string_bytes(len)
}
/// At least the bytes that a function object made from `code` adds to the heap: the function,
/// its prototype object, their properties and the environment holding a function expression's
/// own name.
fn closure_bytes(code: &Code) -> usize {
let props = if code.strict { 5 } else { 3 };
let named = code.self_name.as_ref().map_or(0, |n| ENV_BYTES + binding_bytes(n.len()));
2 * OBJECT_BYTES + props * binding_bytes("constructor".len()) + named
}
/// At least the bytes that declaration binding instantiation (section 10.5) adds to the heap for
/// the function declarations and variables of `code`.
fn declaration_bytes(code: &Code) -> usize {
let funs = code.fun_decls.iter().fold(0usize, |total, (name, fi)| {
let closure = code.functions.get(*fi as usize).map_or(0, |f| closure_bytes(f));
total.saturating_add(binding_bytes(name.len()) + closure)
});
code.vars.iter().fold(funs, |total, v| total.saturating_add(binding_bytes(v.len())))
}
/// A call to a function written in JavaScript, ready to enter.
struct Entry {
callee: ObjId,
code: Rc<Code>,
env: EnvId,
this: Value,
args: Vec<Value>,
}
/// What a call resolved to.
enum Prepared {
Closure(Entry),
Done(Value),
}
/// A variable found in the environment chain.
enum Resolved {
Decl(EnvId),
Obj { object: ObjId, with_this: bool },
Unresolved,
}
/// A reference base as an operand: a declarative environment's index as a number, a binding
/// object as itself, and an unresolvable reference as `undefined`. The environment stays
/// reachable through the frame's environment chain while the operand is on the stack.
fn resolved_to_value(r: &Resolved) -> Value {
match r {
Resolved::Decl(e) => Value::Number(f64::from(e.0)),
Resolved::Obj { object, .. } => Value::Object(*object),
Resolved::Unresolved => Value::Undefined,
}
}
fn value_to_resolved(v: &Value) -> JsResult<Resolved> {
match v {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Value::Number(n) if *n >= 0.0 && *n <= f64::from(u32::MAX) && n.fract() == 0.0 => Ok(Resolved::Decl(EnvId(*n as u32))),
Value::Object(object) => Ok(Resolved::Obj { object: *object, with_this: false }),
Value::Undefined => Ok(Resolved::Unresolved),
_ => internal("not a reference base"),
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Hint {
Default,
Number,
String,
}
fn is(k: &[u16], s: &str) -> bool {
k.len() == s.len() && k.iter().zip(s.bytes()).all(|(&a, b)| a == u16::from(b))
}
impl Interp {
pub(crate) fn new(options: Options, limits: Limits) -> Self {
let mut heap = Heap::default();
let global = heap.alloc_object(Object::new(Class::Ordinary, None));
let global_env = heap.alloc_env(Env::Obj { object: global, with_this: false, outer: None });
let mut interp = Self {
heap,
stack: Vec::new(),
frames: Vec::new(),
temp_roots: Vec::new(),
global,
global_env,
intrinsics: Intrinsics::placeholder(),
fuel: u64::MAX,
// The built-ins are installed without limits; the realm's limits apply from the
// first run.
limits: Limits { fuel: u64::MAX, heap_bytes: usize::MAX, frames: usize::MAX, reentry: usize::MAX },
options,
interrupt: Arc::new(AtomicBool::new(false)),
reentry: 0,
gc_threshold: MIN_GC_BYTES,
source_units: 0,
throw_site: None,
last_thrown: Value::Undefined,
date_host: crate::date_builtins::DateHost::default(),
held: Rc::default(),
};
crate::builtins::install(&mut interp);
interp.fuel = limits.fuel;
interp.limits = limits;
interp.gc_threshold = interp.next_threshold();
interp
}
fn next_threshold(&self) -> usize {
(self.heap.bytes.saturating_mul(2)).max(MIN_GC_BYTES).min(self.limits.heap_bytes)
}
/// Recomputes when the next collection runs, after the limits change.
pub(crate) fn reset_gc_threshold(&mut self) {
self.gc_threshold = self.next_threshold();
}
// ---- errors ----
pub(crate) fn error_object(&mut self, t: ErrorType, message: &str) -> ObjId {
let proto = self.intrinsics.error_proto(t);
let mut o = Object::new(Class::Error, Some(proto));
if !message.is_empty() {
o.props.insert(key("message"), Prop::Data { value: Value::str(message), flags: HIDDEN });
}
self.heap.alloc_object(o)
}
pub(crate) fn throw(&mut self, t: ErrorType, message: &str) -> Abrupt {
Abrupt::Throw(Value::Object(self.error_object(t, message)))
}
pub(crate) fn type_error(&mut self, message: &str) -> Abrupt {
self.throw(ErrorType::Type, message)
}
// ---- the operand stack ----
fn push(&mut self, v: Value) -> JsResult<()> {
self.stack.push(v);
Ok(())
}
fn frame(&self) -> JsResult<&Frame> {
self.frames.last().ok_or_else(|| Abrupt::Internal("no frame".into()))
}
fn frame_mut(&mut self) -> JsResult<&mut Frame> {
self.frames.last_mut().ok_or_else(|| Abrupt::Internal("no frame".into()))
}
fn base(&self) -> usize {
self.frames.last().map_or(0, |f| f.base)
}
fn pop(&mut self) -> JsResult<Value> {
if self.stack.len() <= self.base() {
return internal("operand stack underflow");
}
self.stack.pop().ok_or_else(|| Abrupt::Internal("operand stack underflow".into()))
}
/// The value `n` places below the top (0 is the top).
fn peek(&self, n: usize) -> JsResult<Value> {
let len = self.stack.len();
match len.checked_sub(n + 1).filter(|&i| i >= self.base()) {
Some(i) => self.stack.get(i).cloned().ok_or_else(|| Abrupt::Internal("operand stack underflow".into())),
None => internal("operand stack underflow"),
}
}
fn drop_n(&mut self, n: usize) -> JsResult<()> {
let len = self.stack.len();
match len.checked_sub(n).filter(|&i| i >= self.base()) {
Some(i) => {
self.stack.truncate(i);
Ok(())
}
None => internal("operand stack underflow"),
}
}
/// Replaces the top `n` values with `v`.
fn replace(&mut self, n: usize, v: Value) -> JsResult<()> {
self.drop_n(n)?;
self.push(v)
}
// ---- running ----
/// Takes a unit of fuel and checks the interrupt flag.
pub(crate) fn use_fuel(&mut self) -> JsResult<()> {
if self.fuel == 0 {
return Err(Abrupt::Limit(LimitKind::Fuel));
}
self.fuel -= 1;
if self.interrupt.load(Ordering::Relaxed) {
return Err(Abrupt::Limit(LimitKind::Interrupted));
}
Ok(())
}
fn tick(&mut self) -> JsResult<()> {
self.use_fuel()?;
self.settle_held();
if self.heap.bytes > self.gc_threshold {
self.collect_garbage();
if self.heap.bytes > self.limits.heap_bytes {
return Err(Abrupt::Limit(LimitKind::Heap));
}
self.gc_threshold = self.next_threshold();
}
Ok(())
}
/// Ends the run with the heap limit when `bytes` more would not fit in it after a
/// collection. Everything the caller still needs must be reachable from the roots.
pub(crate) fn reserve_heap(&mut self, bytes: usize) -> JsResult<()> {
self.settle_held();
if self.heap.bytes.saturating_add(bytes) <= self.limits.heap_bytes {
return Ok(());
}
self.collect_garbage();
self.gc_threshold = self.next_threshold();
if self.heap.bytes.saturating_add(bytes) > self.limits.heap_bytes {
return Err(Abrupt::Limit(LimitKind::Heap));
}
Ok(())
}
/// Takes the bytes that dropped [`HeldBytes`] released off the heap's total.
fn settle_held(&mut self) {
let released = self.held.released.replace(0);
self.heap.bytes = self.heap.bytes.saturating_sub(released);
}
/// Collects garbage now and sets when the next collection runs.
#[cfg(test)]
pub(crate) fn collect_for_room(&mut self) {
self.collect_garbage();
self.gc_threshold = self.next_threshold();
}
/// Charges code compiled at run time to the heap, reserving its estimated size and `extra`
/// more first. The code is not reachable from the roots yet, so a collection here leaves it
/// out of the live total and the charge adds it.
pub(crate) fn charge_code(&mut self, code: &Code, extra: usize) -> JsResult<()> {
let bytes = code_bytes(code);
self.reserve_heap(bytes.saturating_add(extra))?;
self.heap.bytes = self.heap.bytes.saturating_add(bytes);
Ok(())
}
pub(crate) fn collect_garbage(&mut self) {
let mut roots = Roots::default();
roots.values.extend(self.stack.iter());
roots.values.extend(self.temp_roots.iter());
roots.values.push(&self.last_thrown);
for f in &self.frames {
roots.values.push(&f.this);
roots.values.push(&f.completion);
roots.objects.extend(f.callee.iter().chain(f.construct.iter()));
roots.envs.push(f.env);
roots.envs.push(f.var_env);
roots.codes.push(&f.code);
}
roots.objects.push(self.global);
roots.objects.extend(self.intrinsics.all());
roots.envs.push(self.global_env);
self.heap.collect(&roots);
// The collection counts the live total again, so bytes already released are not in it.
self.held.released.set(0);
self.heap.bytes = self.heap.bytes.saturating_add(self.held.now.get());
}
/// Runs frames until the boundary frame on top returns, and returns its value.
pub(crate) fn execute(&mut self) -> JsResult<Value> {
loop {
match self.step() {
Ok(Some(v)) => return Ok(v),
Ok(None) => {}
Err(Abrupt::Throw(v)) => self.unwind(v)?,
Err(other) => {
self.note_site();
self.abort_to_boundary();
return Err(other);
}
}
}
}
fn step(&mut self) -> JsResult<Option<Value>> {
self.tick()?;
let frame = self.frame_mut()?;
let Some(&op) = frame.code.ops.get(frame.pc) else { return internal("ran past the end of the code") };
frame.pc += 1;
match op {
Op::Return => {
let v = self.pop()?;
self.return_value(v)
}
Op::ReturnCompletion => {
let v = self.frame()?.completion.clone();
self.return_value(v)
}
_ => self.exec(op).map(|()| None),
}
}
fn note_site(&mut self) {
if self.throw_site.is_none()
&& let Some(f) = self.frames.last()
{
self.throw_site = Some((f.code.clone(), f.pc.saturating_sub(1)));
}
}
/// Finds the handler for an exception: in the top frame, or in the frames below it up to the
/// boundary frame. Returns the exception when no handler takes it before the boundary frame.
fn unwind(&mut self, v: Value) -> JsResult<()> {
self.note_site();
loop {
let Some(frame) = self.frames.last_mut() else { return Err(Abrupt::Throw(v)) };
if let Some(h) = frame.code.handler(frame.pc.saturating_sub(1)) {
let target_len = frame.base + h.depth as usize;
while frame.env_depth > h.env_depth {
let outer = self.heap.env(frame.env).and_then(Env::outer);
let Some(outer) = outer else { return internal("environment chain ended early") };
frame.env = outer;
frame.env_depth -= 1;
}
frame.pc = h.target as usize;
if self.stack.len() < target_len {
return internal("handler stack deeper than the stack");
}
self.stack.truncate(target_len);
self.stack.push(v);
self.throw_site = None;
return Ok(());
}
let Some(frame) = self.frames.pop() else { return Err(Abrupt::Throw(v)) };
self.stack.truncate(frame.base);
if frame.boundary {
return Err(Abrupt::Throw(v));
}
}
}
fn abort_to_boundary(&mut self) {
while let Some(frame) = self.frames.pop() {
self.stack.truncate(frame.base);
if frame.boundary {
break;
}
}
}
fn return_value(&mut self, v: Value) -> JsResult<Option<Value>> {
let Some(frame) = self.frames.pop() else { return internal("return without a frame") };
self.stack.truncate(frame.base);
let v = match frame.construct {
Some(o) if !matches!(v, Value::Object(_)) => Value::Object(o),
_ => v,
};
if frame.boundary {
return Ok(Some(v));
}
self.push(v)?;
Ok(None)
}
fn jump_to(&mut self, target: u32) -> JsResult<()> {
self.frame_mut()?.pc = target as usize;
Ok(())
}
fn name(&self, i: u32) -> JsResult<Key> {
self.frame()?.code.names.get(i as usize).cloned().ok_or_else(|| Abrupt::Internal("name out of range".into()))
}
fn strict(&self) -> bool {
self.frames.last().is_some_and(|f| f.code.strict)
}
fn exec(&mut self, op: Op) -> JsResult<()> {
match op {
Op::Undefined => self.push(Value::Undefined),
Op::Null => self.push(Value::Null),
Op::True => self.push(Value::Bool(true)),
Op::False => self.push(Value::Bool(false)),
Op::Number(i) => {
let n = self.frame()?.code.numbers.get(i as usize).copied();
self.push(Value::Number(n.unwrap_or(f64::NAN)))
}
Op::String(i) => {
let s = self.frame()?.code.strings.get(i as usize).cloned();
self.push(s.map_or(Value::Undefined, Value::String))
}
Op::This => {
let this = self.frame()?.this.clone();
self.push(this)
}
Op::Pop => self.pop().map(drop),
Op::PopUnder(n) => self.op_pop_under(n),
Op::Dup => self.op_dup(1),
Op::Dup2 => self.op_dup(2),
Op::Swap => self.op_swap(),
Op::Rot3 => self.op_rot3(),
Op::GetVar(n) => self.op_get_var(n, false),
Op::GetVarThis(n) => self.op_get_var(n, true),
Op::SetVar(n) => self.op_set_var(n),
Op::RefVar(n) => self.op_ref_var(n),
Op::GetRef(n) => self.op_get_ref(n),
Op::PutRef(n) => self.op_put_ref(n),
Op::TypeofVar(n) => self.op_typeof_var(n),
Op::DeleteVar(n) => self.op_delete_var(n),
Op::IncVar { name, prefix, dec } => self.op_inc_var(name, prefix, dec),
Op::GetMember(n) => self.op_get_member(n, false),
Op::GetMemberThis(n) => self.op_get_member(n, true),
Op::SetMember(n) => self.op_set_member(n),
Op::DeleteMember(n) => self.op_delete_member(n),
Op::IncMember { name, prefix, dec } => self.op_inc_member(name, prefix, dec),
Op::ToKey => self.op_to_key(),
Op::CheckObject => self.op_check_object(),
Op::GetIndex => self.op_get_index(false),
Op::GetIndexThis => self.op_get_index(true),
Op::SetIndex => self.op_set_index(),
Op::DeleteIndex => self.op_delete_index(),
Op::IncIndex { prefix, dec } => self.op_inc_index(prefix, dec),
Op::Call(n) => self.op_call(n, false),
Op::CallEval(n) => self.op_call(n, true),
Op::New(n) => self.op_new(n),
Op::Binary(b) => self.op_binary(b),
Op::Neg | Op::Plus | Op::BitNot | Op::Not | Op::Typeof | Op::ToNumber => self.op_unary(op),
Op::Jump(t) => self.jump_to(t),
Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => self.op_branch(t, matches!(op, Op::JumpIfTrue(_)), false),
Op::JumpIfFalseKeep(t) | Op::JumpIfTrueKeep(t) => self.op_branch(t, matches!(op, Op::JumpIfTrueKeep(_)), true),
Op::Throw => Err(Abrupt::Throw(self.pop()?)),
Op::ThrowReferenceError(i) => self.op_throw_reference_error(i),
Op::EndFinally(t) => self.op_end_finally(t),
Op::NewObject => self.op_new_object(),
Op::NewArray => self.op_new_array(),
Op::ArrayPush => self.op_array_push(true),
Op::ArrayHole => self.op_array_push(false),
Op::InitProp(n) => self.op_init_prop(n),
Op::InitGetter(n) => self.op_init_accessor(n, true),
Op::InitSetter(n) => self.op_init_accessor(n, false),
Op::Closure(i) => self.op_closure(i),
Op::RegExp(i) => self.op_regexp(i),
Op::PushWith => self.op_push_with(),
Op::PushCatch(n) => self.op_push_catch(n),
Op::PopEnv => self.op_pop_env(),
Op::ForInStart => self.op_for_in_start(),
Op::ForInNext(t) => self.op_for_in_next(t),
Op::SetCompletion => {
let v = self.pop()?;
self.frame_mut()?.completion = v;
Ok(())
}
Op::GetCompletion => {
let v = self.frame()?.completion.clone();
self.push(v)
}
Op::Return | Op::ReturnCompletion => internal("return reached the dispatcher"),
}
}
// ---- stack operations ----
fn op_pop_under(&mut self, n: u32) -> JsResult<()> {
let top = self.pop()?;
self.drop_n(n as usize)?;
self.push(top)
}
fn op_dup(&mut self, n: usize) -> JsResult<()> {
for _ in 0..n {
let v = self.peek(n - 1)?;
self.push(v)?;
}
Ok(())
}
fn op_swap(&mut self) -> JsResult<()> {
let b = self.pop()?;
let a = self.pop()?;
self.push(b)?;
self.push(a)
}
fn op_rot3(&mut self) -> JsResult<()> {
let c = self.pop()?;
let b = self.pop()?;
let a = self.pop()?;
self.push(b)?;
self.push(c)?;
self.push(a)
}
fn op_branch(&mut self, target: u32, when: bool, keep: bool) -> JsResult<()> {
let v = self.peek(0)?;
let jump = v.truthy() == when;
if !(keep && jump) {
self.pop()?;
}
if jump { self.jump_to(target) } else { Ok(()) }
}
fn op_throw_reference_error(&mut self, i: u32) -> JsResult<()> {
let message = self.frame()?.code.strings.get(i as usize).map(Str::to_rust_string).unwrap_or_default();
Err(self.throw(ErrorType::Reference, &message))
}
fn op_end_finally(&mut self, table: u32) -> JsResult<()> {
let Value::Number(kind) = self.pop()? else { return internal("finally kind is not a number") };
if kind == 0.0 {
return self.pop().map(drop);
}
if kind == 1.0 {
return Err(Abrupt::Throw(self.pop()?));
}
let k = (kind - 2.0) as usize;
let target = self.frame()?.code.finally_tables.get(table as usize).and_then(|t| t.get(k)).copied();
match target {
Some(t) => self.jump_to(t),
None => internal("finally exit out of range"),
}
}
// ---- variables ----
fn resolve(&self, name: &[u16]) -> JsResult<Resolved> {
let mut cur = Some(self.frame()?.env);
while let Some(e) = cur {
match self.heap.env(e) {
Some(Env::Decl { bindings, outer }) => {
if bindings.contains(name) {
return Ok(Resolved::Decl(e));
}
cur = *outer;
}
Some(Env::Obj { object, with_this, outer }) => {
if self.has_property(*object, name)? {
return Ok(Resolved::Obj { object: *object, with_this: *with_this });
}
cur = *outer;
}
None => return internal("environment out of range"),
}
}
Ok(Resolved::Unresolved)
}
fn binding_value(&self, env: EnvId, name: &[u16]) -> JsResult<Value> {
match self.heap.env(env) {
Some(Env::Decl { bindings, .. }) => Ok(bindings.get(name).map_or(Value::Undefined, |b| b.value.clone())),
_ => internal("not a declarative environment"),
}
}
fn not_defined(&mut self, name: &[u16]) -> Abrupt {
let message = format!("{} is not defined", String::from_utf16_lossy(name));
self.throw(ErrorType::Reference, &message)
}
fn get_var(&mut self, name: &Key) -> JsResult<(Value, Value)> {
let r = self.resolve(name)?;
self.get_resolved(r, name)
}
/// GetValue of a reference whose base was resolved earlier (section 8.7.1).
fn get_resolved(&mut self, r: Resolved, name: &Key) -> JsResult<(Value, Value)> {
match r {
Resolved::Decl(e) => Ok((self.binding_value(e, name)?, Value::Undefined)),
Resolved::Obj { object, with_this } => {
let v = self.get_from(object, name, &Value::Object(object))?;
Ok((v, if with_this { Value::Object(object) } else { Value::Undefined }))
}
Resolved::Unresolved => Err(self.not_defined(name)),
}
}
fn set_var(&mut self, name: &Key, v: Value) -> JsResult<()> {
let r = self.resolve(name)?;
self.set_resolved(r, name, v)
}
/// PutValue of a reference whose base was resolved earlier (section 8.7.2).
fn set_resolved(&mut self, r: Resolved, name: &Key, v: Value) -> JsResult<()> {
let strict = self.strict();
match r {
Resolved::Decl(e) => {
let Some(Env::Decl { bindings, .. }) = self.heap.env_mut(e) else { return internal("not a declarative environment") };
match bindings.get_mut(name) {
Some(b) if b.mutable => {
b.value = v;
Ok(())
}
_ if strict => Err(self.type_error("assignment to a constant binding")),
_ => Ok(()),
}
}
Resolved::Obj { object, .. } => self.put(object, name.clone(), v, strict),
Resolved::Unresolved if strict => Err(self.not_defined(name)),
Resolved::Unresolved => {
let global = self.global;
self.put(global, name.clone(), v, false)
}
}
}
fn op_get_var(&mut self, n: u32, with_this: bool) -> JsResult<()> {
let name = self.name(n)?;
let (v, this) = self.get_var(&name)?;
self.push(v)?;
if with_this { self.push(this) } else { Ok(()) }
}
fn op_set_var(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let v = self.peek(0)?;
self.set_var(&name, v)
}
fn op_typeof_var(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let v = match self.resolve(&name)? {
Resolved::Unresolved => Value::Undefined,
_ => self.get_var(&name)?.0,
};
let t = self.type_of(&v);
self.push(Value::str(t))
}
fn op_delete_var(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let result = match self.resolve(&name)? {
Resolved::Unresolved => true,
Resolved::Decl(e) => match self.heap.env_mut(e) {
Some(Env::Decl { bindings, .. }) => {
if bindings.get(&name).is_some_and(|b| b.deletable) {
bindings.remove(&name);
true
} else {
false
}
}
_ => return internal("not a declarative environment"),
},
Resolved::Obj { object, .. } => self.delete(object, &name, false)?,
};
self.push(Value::Bool(result))
}
fn op_inc_var(&mut self, n: u32, prefix: bool, dec: bool) -> JsResult<()> {
let name = self.name(n)?;
let r = self.resolve(&name)?;
let base = resolved_to_value(&r);
let (v, _) = self.get_resolved(r, &name)?;
// The base stays on the stack, where the collector sees it, while ToNumber runs script.
self.push(base)?;
let old = self.to_number(&v)?;
let base = self.pop()?;
let new = if dec { old - 1.0 } else { old + 1.0 };
self.set_resolved(value_to_resolved(&base)?, &name, Value::Number(new))?;
self.push(Value::Number(if prefix { new } else { old }))
}
/// Resolves a variable before the right-hand side of an assignment runs, and pushes the
/// reference's base.
fn op_ref_var(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let r = self.resolve(&name)?;
self.push(resolved_to_value(&r))
}
/// Replaces the reference base on top of the stack with the variable's value.
fn op_get_ref(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let base = self.peek(0)?;
let (v, _) = self.get_resolved(value_to_resolved(&base)?, &name)?;
self.pop()?;
self.push(v)
}
/// Stores the value on top of the stack through the reference base under it, and leaves the
/// value.
fn op_put_ref(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let v = self.peek(0)?;
let base = self.peek(1)?;
self.set_resolved(value_to_resolved(&base)?, &name, v)?;
let v = self.pop()?;
self.pop()?;
self.push(v)
}
// ---- properties ----
fn object(&self, id: ObjId) -> JsResult<&Object> {
self.heap.object(id).ok_or_else(|| Abrupt::Internal("object out of range".into()))
}
fn object_mut(&mut self, id: ObjId) -> JsResult<&mut Object> {
self.heap.object_mut(id).ok_or_else(|| Abrupt::Internal("object out of range".into()))
}
/// [[GetOwnProperty]] (section 8.12.1, with the special cases of arrays, strings and
/// arguments objects).
pub(crate) fn get_own_property(&self, id: ObjId, k: &[u16]) -> JsResult<Option<Prop>> {
let o = self.object(id)?;
match &o.class {
Class::Array { length, length_writable } if is(k, "length") => {
return Ok(Some(Prop::Data { value: Value::Number(f64::from(*length)), flags: if *length_writable { WRITABLE } else { 0 } }));
}
Class::String(s) => {
if is(k, "length") {
return Ok(Some(Prop::Data { value: Value::Number(s.len() as f64), flags: 0 }));
}
if let Some(i) = array_index(k)
&& (i as usize) < s.len()
{
let unit = s.flat().get(i as usize).copied().unwrap_or(0);
return Ok(Some(Prop::Data { value: Value::String(Str::from_units(vec![unit])), flags: ENUMERABLE }));
}
}
Class::Arguments(map) => {
if let Some(name) = array_index(k).and_then(|i| map.names.get(i as usize)).and_then(Option::as_ref)
&& let Some(Prop::Data { flags, .. }) = o.props.get(k)
{
let flags = *flags;
return Ok(Some(Prop::Data { value: self.binding_value(map.env, name)?, flags }));
}
}
_ => {}
}
Ok(o.props.get(k).cloned())
}
/// [[GetProperty]]: the property found on the object or its prototype chain.
pub(crate) fn find_property(&self, id: ObjId, k: &[u16]) -> JsResult<Option<Prop>> {
let mut cur = Some(id);
let mut steps = 0usize;
while let Some(o) = cur {
if let Some(p) = self.get_own_property(o, k)? {
return Ok(Some(p));
}
cur = self.object(o)?.proto;
steps += 1;
if steps > 1_000_000 {
return internal("prototype chain too long");
}
}
Ok(None)
}
pub(crate) fn has_property(&self, id: ObjId, k: &[u16]) -> JsResult<bool> {
Ok(self.find_property(id, k)?.is_some())
}
/// [[Get]] starting at `start`, with `receiver` as `this` for a getter.
pub(crate) fn get_from(&mut self, start: ObjId, k: &[u16], receiver: &Value) -> JsResult<Value> {
match self.find_property(start, k)? {
None | Some(Prop::Accessor { get: None, .. }) => Ok(Value::Undefined),
Some(Prop::Data { value, .. }) => Ok(value),
Some(Prop::Accessor { get: Some(g), .. }) => self.call(&Value::Object(g), receiver, &[]),
}
}
/// The prototype a primitive's properties are looked up on.
fn primitive_proto(&mut self, v: &Value, k: &[u16]) -> JsResult<ObjId> {
match v {
Value::Object(o) => Ok(*o),
Value::String(_) => Ok(self.intrinsics.string_proto),
Value::Number(_) => Ok(self.intrinsics.number_proto),
Value::Bool(_) => Ok(self.intrinsics.boolean_proto),
Value::Undefined | Value::Null => {
let message = format!(
"cannot read property '{}' of {}",
String::from_utf16_lossy(k),
if matches!(v, Value::Null) { "null" } else { "undefined" }
);
Err(self.type_error(&message))
}
}
}
/// `base[k]` for any value (GetValue, section 8.7.1).
pub(crate) fn get_value(&mut self, base: &Value, k: &[u16]) -> JsResult<Value> {
if let Value::String(s) = base {
if is(k, "length") {
return Ok(Value::Number(s.len() as f64));
}
if let Some(i) = array_index(k)
&& (i as usize) < s.len()
{
let unit = s.flat().get(i as usize).copied().unwrap_or(0);
return Ok(Value::String(Str::from_units(vec![unit])));
}
}
let start = self.primitive_proto(base, k)?;
self.get_from(start, k, base)
}
/// Adds or replaces an own property directly, keeping the heap estimate.
pub(crate) fn define(&mut self, id: ObjId, k: Key, prop: Prop) -> JsResult<()> {
let o = self.object_mut(id)?;
let new = !o.props.contains(&k);
if let Class::Array { length, .. } = &mut o.class
&& let Some(i) = array_index(&k)
&& i >= *length
{
*length = i + 1;
}
let name_len = k.len();
o.props.insert(k, prop);
if new {
self.heap.bytes += PROP_BYTES + string_bytes(name_len);
}
Ok(())
}
fn reject(&mut self, throw: bool, k: &[u16], why: &str) -> JsResult<()> {
if throw {
let message = format!("cannot assign to property '{}': {why}", String::from_utf16_lossy(k));
return Err(self.type_error(&message));
}
Ok(())
}
/// [[Put]] (section 8.12.5).
pub(crate) fn put(&mut self, id: ObjId, k: Key, v: Value, throw: bool) -> JsResult<()> {
match self.get_own_property(id, &k)? {
Some(Prop::Data { flags, .. }) => {
if flags & WRITABLE == 0 {
return self.reject(throw, &k, "it is read-only");
}
return self.write_own(id, k, v, throw);
}
Some(Prop::Accessor { set: Some(s), .. }) => return self.call(&Value::Object(s), &Value::Object(id), &[v]).map(drop),
Some(Prop::Accessor { set: None, .. }) => return self.reject(throw, &k, "it has only a getter"),
None => {}
}
let proto = self.object(id)?.proto;
match proto.map(|p| self.find_property(p, &k)).transpose()?.flatten() {
Some(Prop::Accessor { set: Some(s), .. }) => return self.call(&Value::Object(s), &Value::Object(id), &[v]).map(drop),
Some(Prop::Accessor { set: None, .. }) => return self.reject(throw, &k, "it has only a getter"),
Some(Prop::Data { flags, .. }) if flags & WRITABLE == 0 => return self.reject(throw, &k, "it is read-only"),
_ => {}
}
let o = self.object(id)?;
if !o.extensible {
return self.reject(throw, &k, "the object is not extensible");
}
if let Class::Array { length, length_writable: false } = o.class
&& array_index(&k).is_some_and(|i| i >= length)
{
return self.reject(throw, &k, "the array's length is read-only");
}
self.define(id, k, Prop::Data { value: v, flags: OPEN })
}
/// Sets the value of an existing writable own data property.
fn write_own(&mut self, id: ObjId, k: Key, v: Value, throw: bool) -> JsResult<()> {
let o = self.object(id)?;
match &o.class {
Class::Array { .. } if is(&k, "length") => return self.set_array_length(id, &v, throw),
Class::Arguments(map) => {
if let Some(name) = array_index(&k).and_then(|i| map.names.get(i as usize)).and_then(Option::as_ref) {
let (env, name) = (map.env, name.clone());
if let Some(Env::Decl { bindings, .. }) = self.heap.env_mut(env)
&& let Some(b) = bindings.get_mut(&name)
{
b.value = v.clone();
}
}
}
_ => {}
}
match self.object_mut(id)?.props.get_mut(&k) {
Some(Prop::Data { value, .. }) => {
*value = v;
Ok(())
}
_ => internal("own data property vanished"),
}
}
/// Assigning to an array's `length` (section 15.4.5.1).
fn set_array_length(&mut self, id: ObjId, v: &Value, throw: bool) -> JsResult<()> {
let n = self.to_number(v)?;
let new = to_uint32(n);
if f64::from(new) != n {
return Err(self.throw(ErrorType::Range, "invalid array length"));
}
let o = self.object(id)?;
let Class::Array { length: old, length_writable } = o.class else { return internal("not an array") };
if !length_writable {
return self.reject(throw, &key("length"), "it is read-only");
}
if new < old {
// Shrinking visits every property: each takes fuel before anything is removed.
for _ in 0..o.props.len() {
self.use_fuel()?;
}
}
let o = self.object_mut(id)?;
let Class::Array { length, .. } = &mut o.class else { return internal("not an array") };
*length = new;
if new < old {
let doomed: Vec<Key> = o.props.keys().into_iter().filter(|k| array_index(k).is_some_and(|i| i >= new)).collect();
for k in doomed {
o.props.remove(&k);
}
}
Ok(())
}
/// [[Delete]] (section 8.12.7).
pub(crate) fn delete(&mut self, id: ObjId, k: &[u16], throw: bool) -> JsResult<bool> {
match self.get_own_property(id, k)? {
None => Ok(true),
Some(p) if p.configurable() => {
let o = self.object_mut(id)?;
o.props.remove(k);
if let Class::Arguments(map) = &mut o.class
&& let Some(slot) = array_index(k).and_then(|i| map.names.get_mut(i as usize))
{
*slot = None;
}
Ok(true)
}
Some(_) if throw => {
let message = format!("cannot delete property '{}'", String::from_utf16_lossy(k));
Err(self.type_error(&message))
}
Some(_) => Ok(false),
}
}
/// PutValue on a property reference (section 8.7.2), whose base may be a primitive.
fn put_value(&mut self, base: &Value, k: Key, v: Value) -> JsResult<()> {
let strict = self.strict();
match base {
Value::Object(o) => self.put(*o, k, v, strict),
Value::Undefined | Value::Null => {
let message = format!(
"cannot set property '{}' of {}",
String::from_utf16_lossy(&k),
if matches!(base, Value::Null) { "null" } else { "undefined" }
);
Err(self.type_error(&message))
}
_ => {
let proto = self.primitive_proto(base, &k)?;
let own = matches!(base, Value::String(s) if is(&k, "length") || array_index(&k).is_some_and(|i| (i as usize) < s.len()));
if !own && let Some(Prop::Accessor { set: Some(s), .. }) = self.find_property(proto, &k)? {
return self.call(&Value::Object(s), base, &[v]).map(drop);
}
self.reject(strict, &k, "the value is a primitive")
}
}
}
fn op_get_member(&mut self, n: u32, with_this: bool) -> JsResult<()> {
let name = self.name(n)?;
let base = self.peek(0)?;
let v = self.get_value(&base, &name)?;
self.replace(1, v)?;
if with_this { self.push(base) } else { Ok(()) }
}
fn op_set_member(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let v = self.peek(0)?;
let base = self.peek(1)?;
self.put_value(&base, name, v.clone())?;
self.replace(2, v)
}
fn check_coercible(&mut self, base: &Value, k: &[u16]) -> JsResult<()> {
if base.is_nullish() {
self.primitive_proto(base, k)?;
}
Ok(())
}
fn op_check_object(&mut self) -> JsResult<()> {
let base = self.peek(0)?;
self.check_coercible(&base, &[])
}
fn op_to_key(&mut self) -> JsResult<()> {
let k = self.peek(0)?;
let base = self.peek(1)?;
self.check_coercible(&base, &[])?;
let name = self.to_key(&k)?;
self.replace(1, Value::String(Str::from_rc(name)))
}
fn op_get_index(&mut self, with_this: bool) -> JsResult<()> {
let k = self.peek(0)?;
let base = self.peek(1)?;
self.check_coercible(&base, &[])?;
let name = self.to_key(&k)?;
let v = self.get_value(&base, &name)?;
self.replace(2, v)?;
if with_this { self.push(base) } else { Ok(()) }
}
fn op_set_index(&mut self) -> JsResult<()> {
let v = self.peek(0)?;
let k = self.peek(1)?;
let base = self.peek(2)?;
let name = self.to_key(&k)?;
self.put_value(&base, name, v.clone())?;
self.replace(3, v)
}
fn op_delete_member(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let base = self.peek(0)?;
let o = self.to_object(&base)?;
let strict = self.strict();
let r = self.delete(o, &name, strict)?;
self.replace(1, Value::Bool(r))
}
fn op_delete_index(&mut self) -> JsResult<()> {
let k = self.peek(0)?;
let base = self.peek(1)?;
self.check_coercible(&base, &[])?;
let name = self.to_key(&k)?;
let o = self.to_object(&base)?;
let strict = self.strict();
let r = self.delete(o, &name, strict)?;
self.replace(2, Value::Bool(r))
}
fn inc_property(&mut self, base: &Value, name: Key, prefix: bool, dec: bool) -> JsResult<Value> {
let v = self.get_value(base, &name)?;
let old = self.to_number(&v)?;
let new = if dec { old - 1.0 } else { old + 1.0 };
self.put_value(base, name, Value::Number(new))?;
Ok(Value::Number(if prefix { new } else { old }))
}
fn op_inc_member(&mut self, n: u32, prefix: bool, dec: bool) -> JsResult<()> {
let name = self.name(n)?;
let base = self.peek(0)?;
let r = self.inc_property(&base, name, prefix, dec)?;
self.replace(1, r)
}
fn op_inc_index(&mut self, prefix: bool, dec: bool) -> JsResult<()> {
let k = self.peek(0)?;
let base = self.peek(1)?;
self.check_coercible(&base, &[])?;
let name = self.to_key(&k)?;
let r = self.inc_property(&base, name, prefix, dec)?;
self.replace(2, r)
}
// ---- object literals and functions ----
fn op_new_object(&mut self) -> JsResult<()> {
let o = self.new_object();
self.push(Value::Object(o))
}
pub(crate) fn new_object(&mut self) -> ObjId {
let proto = self.intrinsics.object_proto;
self.heap.alloc_object(Object::new(Class::Ordinary, Some(proto)))
}
fn op_new_array(&mut self) -> JsResult<()> {
let proto = self.intrinsics.array_proto;
let a = self.heap.alloc_object(Object::new(Class::Array { length: 0, length_writable: true }, Some(proto)));
self.push(Value::Object(a))
}
fn op_array_push(&mut self, with_value: bool) -> JsResult<()> {
let v = if with_value { Some(self.pop()?) } else { None };
let Value::Object(a) = self.peek(0)? else { return internal("array literal is not an object") };
let o = self.object_mut(a)?;
let Class::Array { length, .. } = &mut o.class else { return internal("array literal is not an array") };
let i = *length;
if i == u32::MAX - 1 {
return Err(self.throw(ErrorType::Range, "array literal too long"));
}
*length = i + 1;
match v {
Some(v) => self.define(a, index_key(i), Prop::Data { value: v, flags: OPEN }),
None => Ok(()),
}
}
fn op_init_prop(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let v = self.pop()?;
let Value::Object(o) = self.peek(0)? else { return internal("object literal is not an object") };
self.define(o, name, Prop::Data { value: v, flags: OPEN })
}
fn op_init_accessor(&mut self, n: u32, getter: bool) -> JsResult<()> {
let name = self.name(n)?;
let Value::Object(f) = self.pop()? else { return internal("accessor is not a function") };
let Value::Object(o) = self.peek(0)? else { return internal("object literal is not an object") };
let (mut get, mut set) = match self.object(o)?.props.get(&name) {
Some(Prop::Accessor { get, set, .. }) => (*get, *set),
_ => (None, None),
};
if getter {
get = Some(f);
} else {
set = Some(f);
}
self.define(o, name, Prop::Accessor { get, set, flags: ENUMERABLE | CONFIGURABLE })
}
fn op_closure(&mut self, i: u32) -> JsResult<()> {
let frame = self.frame()?;
let env = frame.env;
let Some(code) = frame.code.functions.get(i as usize).cloned() else { return internal("function out of range") };
let f = self.make_closure(code, env)?;
self.push(Value::Object(f))
}
/// A function object for `code` closing over `env` (section 13.2).
pub(crate) fn make_closure(&mut self, code: Rc<Code>, env: EnvId) -> JsResult<ObjId> {
let env = match &code.self_name {
Some(_) => self.heap.alloc_env(Env::Decl { bindings: PropMap::default(), outer: Some(env) }),
None => env,
};
let proto = self.intrinsics.function_proto;
let params = code.params.len();
let strict = code.strict;
let self_name = code.self_name.clone();
let f = self.heap.alloc_object(Object::new(Class::Function(Func::Closure { code, env }), Some(proto)));
self.define(f, key("length"), Prop::Data { value: Value::Number(params as f64), flags: 0 })?;
let p = self.new_object();
self.define(p, key("constructor"), Prop::Data { value: Value::Object(f), flags: HIDDEN })?;
self.define(f, key("prototype"), Prop::Data { value: Value::Object(p), flags: WRITABLE })?;
if strict {
let thrower = Some(self.intrinsics.throw_type_error);
for name in ["caller", "arguments"] {
self.define(f, key(name), Prop::Accessor { get: thrower, set: thrower, flags: 0 })?;
}
}
if let Some(name) = self_name {
self.env_insert(env, name, Binding { value: Value::Object(f), mutable: false, deletable: false })?;
}
Ok(f)
}
fn env_insert(&mut self, env: EnvId, name: Key, b: Binding) -> JsResult<()> {
let Some(Env::Decl { bindings, .. }) = self.heap.env_mut(env) else { return internal("not a declarative environment") };
let new = !bindings.contains(&name);
let name_len = name.len();
bindings.insert(name, b);
if new {
self.heap.bytes += PROP_BYTES + string_bytes(name_len);
}
Ok(())
}
fn op_regexp(&mut self, i: u32) -> JsResult<()> {
let Some((source, flags)) = self.frame()?.code.regexps.get(i as usize).cloned() else { return internal("regular expression out of range") };
let r = crate::regexp_builtins::new_regexp(self, &source, &flags)?;
self.push(Value::Object(r))
}
// ---- environments ----
fn op_push_with(&mut self) -> JsResult<()> {
let v = self.peek(0)?;
let object = self.to_object(&v)?;
self.pop()?;
let outer = self.frame()?.env;
let env = self.heap.alloc_env(Env::Obj { object, with_this: true, outer: Some(outer) });
let frame = self.frame_mut()?;
frame.env = env;
frame.env_depth += 1;
Ok(())
}
fn op_push_catch(&mut self, n: u32) -> JsResult<()> {
let name = self.name(n)?;
let v = self.pop()?;
let outer = self.frame()?.env;
let mut bindings = PropMap::default();
let name_bytes = string_bytes(name.len());
bindings.insert(name, Binding { value: v, mutable: true, deletable: false });
let env = self.heap.alloc_env(Env::Decl { bindings, outer: Some(outer) });
self.heap.bytes += name_bytes;
let frame = self.frame_mut()?;
frame.env = env;
frame.env_depth += 1;
Ok(())
}
fn op_pop_env(&mut self) -> JsResult<()> {
let env = self.frame()?.env;
let Some(outer) = self.heap.env(env).and_then(Env::outer) else { return internal("left the outermost environment") };
let frame = self.frame_mut()?;
if frame.env_depth == 0 {
return internal("left more environments than were entered");
}
frame.env = outer;
frame.env_depth -= 1;
Ok(())
}
// ---- for-in ----
/// The names `for-in` visits (section 12.6.4): the enumerable properties of the object and
/// its prototypes, own properties in the order they were added, each name once, and none
/// that an object nearer the start of the chain has (enumerable or not).
///
/// For a String object, the heap budget for all its index names is checked before any is
/// built. Each index name and each property visited takes a unit of fuel.
fn enumerable_keys(&mut self, id: ObjId) -> JsResult<Vec<Key>> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
let mut cur = Some(id);
while let Some(o) = cur {
let obj = self.object(o)?;
let string_len = if let Class::String(s) = &obj.class { Some(s.len()) } else { None };
let is_array = matches!(obj.class, Class::Array { .. });
if let Some(len) = string_len {
self.reserve_heap(index_keys_bytes(len))?;
for i in 0..len {
self.use_fuel()?;
let k = index_key(u32::try_from(i).unwrap_or(u32::MAX));
if seen.insert(k.clone()) {
out.push(k);
}
}
seen.insert(key("length"));
}
if is_array {
seen.insert(key("length"));
}
for _ in 0..self.object(o)?.props.len() {
self.use_fuel()?;
}
let obj = self.object(o)?;
for (k, p) in obj.props.iter() {
if seen.insert(k.clone()) && p.enumerable() {
out.push(k.clone());
}
}
cur = obj.proto;
}
Ok(out)
}
fn op_for_in_start(&mut self) -> JsResult<()> {
let v = self.peek(0)?;
let (keys, object) = if v.is_nullish() {
(Vec::new(), None)
} else {
let o = self.to_object(&v)?;
// A wrapper made by ToObject is not on the stack; a collection while the names are
// listed or their bytes reserved must not free it.
let saved = self.temp_roots.len();
self.temp_roots.push(Value::Object(o));
let keys = self.enumerable_keys(o).and_then(|keys| {
let bytes = keys.iter().map(|k| string_bytes(k.len())).sum::<usize>();
self.reserve_heap(bytes)?;
self.heap.bytes = self.heap.bytes.saturating_add(bytes);
Ok(keys)
});
self.temp_roots.truncate(saved);
(keys?, Some(o))
};
let iter = self.heap.alloc_object(Object::new(Class::ForIn(Box::new(ForIn { keys, pos: 0, object })), None));
self.replace(1, Value::Object(iter))
}
fn op_for_in_next(&mut self, target: u32) -> JsResult<()> {
let Value::Object(iter) = self.peek(0)? else { return internal("for-in iterator is not an object") };
loop {
let Class::ForIn(state) = &mut self.object_mut(iter)?.class else { return internal("not a for-in iterator") };
let Some(k) = state.keys.get(state.pos).cloned() else { return self.jump_to(target) };
state.pos += 1;
let object = state.object;
if let Some(o) = object
&& self.has_property(o, &k)?
{
return self.push(Value::String(Str::from_rc(k)));
}
}
}
// ---- calls ----
pub(crate) fn is_callable(&self, v: &Value) -> bool {
v.as_object().and_then(|o| self.heap.object(o)).is_some_and(|o| matches!(o.class, Class::Function(_)))
}
fn func_of(&self, v: &Value) -> Option<Func> {
match &self.heap.object(v.as_object()?)?.class {
Class::Function(f) => Some(f.clone()),
_ => None,
}
}
/// Resolves `call` and `apply` and runs native and host functions; a function written in
/// JavaScript comes back to be entered by the caller.
fn prepare_call(&mut self, mut f: Value, mut this: Value, mut args: Vec<Value>) -> JsResult<Prepared> {
loop {
let Some(func) = self.func_of(&f) else { return Err(self.type_error("the value is not a function")) };
match func {
Func::Closure { code, env } => {
let callee = f.as_object().ok_or_else(|| Abrupt::Internal("callee is not an object".into()))?;
return Ok(Prepared::Closure(Entry { callee, code, env, this, args }));
}
Func::Native { kind: NativeKind::Call, .. } => {
self.use_fuel()?;
f = this;
this = if args.is_empty() { Value::Undefined } else { args.remove(0) };
}
Func::Native { kind: NativeKind::Apply, .. } => {
self.use_fuel()?;
if !self.is_callable(&this) {
return Err(self.type_error("apply was called on a value that is not a function"));
}
let list = args.get(1).cloned().unwrap_or(Value::Undefined);
f = this;
this = args.first().cloned().unwrap_or(Value::Undefined);
args = self.list_from_array_like(&list)?;
}
Func::Native { f: native, .. } => {
// A list from `apply` is not on the operand stack, and a native function can run
// JavaScript that collects, so the arguments stay rooted for the call.
let mark = self.temp_roots.len();
self.temp_roots.push(this.clone());
self.temp_roots.extend(args.iter().cloned());
let r = native(self, this, &args, false);
self.temp_roots.truncate(mark);
return Ok(Prepared::Done(r?));
}
Func::Host { f: host, .. } => {
let js_args: Vec<crate::JsValue> = args.iter().map(|v| self.to_js_value(v)).collect();
return match host(&js_args) {
Ok(v) => Ok(Prepared::Done(self.from_js_value(v)?)),
Err(message) => {
let bytes = string_bytes(message.encode_utf16().count());
self.reserve_heap(bytes)?;
self.heap.bytes += bytes;
Err(self.throw(ErrorType::Error, &message))
}
};
}
}
}
}
/// The elements of an array-like `apply` argument (section 15.3.4.3).
fn list_from_array_like(&mut self, list: &Value) -> JsResult<Vec<Value>> {
let Value::Object(o) = list else {
if list.is_nullish() {
return Ok(Vec::new());
}
return Err(self.type_error("the argument list is not an object"));
};
let len = self.get_from(*o, &key("length"), list)?;
let n = to_uint32(self.to_number(&len)?) as usize;
if n > MAX_APPLY_ARGS {
return Err(self.throw(ErrorType::Range, "too many arguments"));
}
let saved = self.temp_roots.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
// Each element read takes fuel, so a long array-like list stays within the run's limits.
match self.use_fuel().and_then(|()| self.get_from(*o, &index_key(u32::try_from(i).unwrap_or(u32::MAX)), list)) {
Ok(v) => {
self.temp_roots.push(v.clone());
out.push(v);
}
Err(e) => {
self.temp_roots.truncate(saved);
return Err(e);
}
}
}
self.temp_roots.truncate(saved);
Ok(out)
}
fn op_call(&mut self, argc: u32, eval: bool) -> JsResult<()> {
let n = argc as usize;
let start =
self.stack.len().checked_sub(n + 2).filter(|&s| s >= self.base()).ok_or_else(|| Abrupt::Internal("call below the stack".into()))?;
let f = self.stack.get(start).cloned().unwrap_or(Value::Undefined);
let this = self.stack.get(start + 1).cloned().unwrap_or(Value::Undefined);
let args: Vec<Value> = self.stack.get(start + 2..).map(<[Value]>::to_vec).unwrap_or_default();
if eval && f.as_object() == Some(self.intrinsics.eval) {
return self.direct_eval(start, args);
}
match self.prepare_call(f, this, args)? {
Prepared::Done(v) => {
self.stack.truncate(start);
self.push(v)
}
Prepared::Closure(entry) => {
self.stack.truncate(start);
self.enter_function(entry, None, false)
}
}
}
fn op_new(&mut self, argc: u32) -> JsResult<()> {
let n = argc as usize;
let start =
self.stack.len().checked_sub(n + 1).filter(|&s| s >= self.base()).ok_or_else(|| Abrupt::Internal("new below the stack".into()))?;
let f = self.stack.get(start).cloned().unwrap_or(Value::Undefined);
let args: Vec<Value> = self.stack.get(start + 1..).map(<[Value]>::to_vec).unwrap_or_default();
match self.func_of(&f) {
Some(Func::Closure { code, env }) => {
let Some(callee) = f.as_object() else { return internal("callee is not an object") };
let proto = match self.get_from(callee, &key("prototype"), &f)? {
Value::Object(p) => p,
_ => self.intrinsics.object_proto,
};
let obj = self.heap.alloc_object(Object::new(Class::Ordinary, Some(proto)));
self.stack.truncate(start);
self.enter_function(Entry { callee, code, env, this: Value::Object(obj), args }, Some(obj), false)
}
Some(Func::Native { f: native, constructor: true, .. }) => {
let v = native(self, Value::Undefined, &args, true)?;
self.stack.truncate(start);
self.push(v)
}
_ => Err(self.type_error("the value is not a constructor")),
}
}
/// Pushes a frame for a call to a function written in JavaScript, its operands already off
/// the stack.
fn enter_function(&mut self, entry: Entry, construct: Option<ObjId>, boundary: bool) -> JsResult<()> {
let Entry { callee, code, env, this, args } = entry;
if self.frames.len() >= self.limits.frames {
return Err(Abrupt::Limit(LimitKind::Frames));
}
let params = code.params.iter().fold(0usize, |total, p| total.saturating_add(binding_bytes(p.len())));
let arguments =
if code.uses_arguments { OBJECT_BYTES.saturating_add(args.len().saturating_add(4).saturating_mul(binding_bytes(10))) } else { 0 };
let need = declaration_bytes(&code).saturating_add(ENV_BYTES).saturating_add(params).saturating_add(arguments);
if self.heap.bytes.saturating_add(need) > self.limits.heap_bytes {
let mark = self.temp_roots.len();
self.temp_roots.push(Value::Object(callee));
self.temp_roots.push(this.clone());
self.temp_roots.extend(construct.map(Value::Object));
self.temp_roots.extend(args.iter().cloned());
let reserved = self.reserve_heap(need);
self.temp_roots.truncate(mark);
reserved?;
}
let this = if code.strict {
this
} else {
match this {
Value::Undefined | Value::Null => Value::Object(self.global),
Value::Object(_) => this,
prim => Value::Object(self.to_object(&prim)?),
}
};
let fenv = self.instantiate_function(&code, env, &args, callee)?;
let base = self.stack.len();
self.frames.push(Frame {
code,
pc: 0,
base,
env: fenv,
var_env: fenv,
env_depth: 0,
this,
callee: Some(callee),
construct,
completion: Value::Undefined,
boundary,
});
Ok(())
}
/// Declaration binding instantiation for function code (section 10.5). Each parameter and
/// declaration takes a unit of fuel.
fn instantiate_function(&mut self, code: &Rc<Code>, outer: EnvId, args: &[Value], callee: ObjId) -> JsResult<EnvId> {
let mut bindings = PropMap::default();
for (i, p) in code.params.iter().enumerate() {
self.use_fuel()?;
bindings.insert(p.clone(), Binding { value: args.get(i).cloned().unwrap_or(Value::Undefined), mutable: true, deletable: false });
}
let name_bytes = bindings.iter().map(|(k, _)| string_bytes(k.len())).sum::<usize>();
let env = self.heap.alloc_env(Env::Decl { bindings, outer: Some(outer) });
self.heap.bytes += name_bytes;
for (name, fi) in &code.fun_decls {
self.use_fuel()?;
let Some(fcode) = code.functions.get(*fi as usize).cloned() else { return internal("function out of range") };
let f = self.make_closure(fcode, env)?;
self.env_insert(env, name.clone(), Binding { value: Value::Object(f), mutable: true, deletable: false })?;
}
let has = |interp: &Self, name: &[u16]| matches!(interp.heap.env(env), Some(Env::Decl { bindings, .. }) if bindings.contains(name));
if code.uses_arguments && !has(self, &key("arguments")) {
let a = self.create_arguments(code, env, args, callee)?;
self.env_insert(env, key("arguments"), Binding { value: Value::Object(a), mutable: !code.strict, deletable: false })?;
}
for v in &code.vars {
self.use_fuel()?;
if !has(self, v) {
self.env_insert(env, v.clone(), Binding { value: Value::Undefined, mutable: true, deletable: false })?;
}
}
Ok(env)
}
/// The `arguments` object (section 10.6).
fn create_arguments(&mut self, code: &Code, env: EnvId, args: &[Value], callee: ObjId) -> JsResult<ObjId> {
let mut names = vec![None; args.len()];
if !code.strict {
let mut mapped = std::collections::HashSet::new();
for (i, p) in code.params.iter().enumerate().take(args.len()).rev() {
if mapped.insert(p.clone())
&& let Some(slot) = names.get_mut(i)
{
*slot = Some(p.clone());
}
}
}
let proto = self.intrinsics.object_proto;
let a = self.heap.alloc_object(Object::new(Class::Arguments(Box::new(ArgMap { env, names })), Some(proto)));
for (i, v) in args.iter().enumerate() {
self.define(a, index_key(u32::try_from(i).unwrap_or(u32::MAX)), Prop::Data { value: v.clone(), flags: OPEN })?;
}
self.define(a, key("length"), Prop::Data { value: Value::Number(args.len() as f64), flags: HIDDEN })?;
if code.strict {
let thrower = Some(self.intrinsics.throw_type_error);
for name in ["callee", "caller"] {
self.define(a, key(name), Prop::Accessor { get: thrower, set: thrower, flags: 0 })?;
}
} else {
self.define(a, key("callee"), Prop::Data { value: Value::Object(callee), flags: HIDDEN })?;
}
Ok(a)
}
/// Calls `f` from Rust and returns its result, running the loop again for a function written
/// in JavaScript.
pub(crate) fn call(&mut self, f: &Value, this: &Value, args: &[Value]) -> JsResult<Value> {
if self.reentry >= self.limits.reentry {
return Err(Abrupt::Limit(LimitKind::Reentry));
}
let saved = self.stack.len();
self.stack.push(f.clone());
self.stack.push(this.clone());
self.stack.extend(args.iter().cloned());
self.reentry += 1;
let r = self.call_rooted(f.clone(), this.clone(), args.to_vec(), saved);
self.reentry -= 1;
self.stack.truncate(saved);
r
}
fn call_rooted(&mut self, f: Value, this: Value, args: Vec<Value>, saved: usize) -> JsResult<Value> {
match self.prepare_call(f, this, args)? {
Prepared::Done(v) => Ok(v),
Prepared::Closure(entry) => {
self.stack.truncate(saved);
self.enter_function(entry, None, true)?;
self.execute()
}
}
}
// ---- eval and scripts ----
/// The options for source parsed next in this run: [`Options::max_source_units`] is what
/// the run may still parse.
fn remaining_source_options(&self) -> Options {
Options { max_source_units: self.options.max_source_units.saturating_sub(self.source_units), ..self.options.clone() }
}
/// Checks that `units` more code units of source fit in what this run may still parse,
/// before the source is flattened or encoded.
pub(crate) fn check_source_units(&self, units: usize) -> Result<(), CompileError> {
crate::check_length(units, &self.remaining_source_options()).map_err(CompileError::Syntax)
}
/// Encodes `text` as UTF-16 for parsing, after checking that it fits in what this run may
/// still parse.
pub(crate) fn encode_source(&self, text: &str) -> Result<Vec<u16>, CompileError> {
crate::to_utf16(text, &self.remaining_source_options()).map_err(CompileError::Syntax)
}
/// Parses and compiles source for eval code or a script, counting it against the source
/// limit.
pub(crate) fn compile_source(&mut self, units: &[u16], strict: bool, kind: CodeKind) -> Result<Rc<Code>, CompileError> {
self.source_units = self.source_units.saturating_add(units.len());
let remaining = self.options.max_source_units.saturating_sub(self.source_units.saturating_sub(units.len()));
let options = Options { strict, max_source_units: remaining, ..self.options.clone() };
let program = crate::parse_utf16(units, &options).map_err(CompileError::Syntax)?;
let source = Rc::new(Source { lines: program.lines.clone(), text: units.into() });
compile_program(&program, &source, kind).map_err(|e| match e {
CompileFail::Syntax(s) => CompileError::Syntax(s),
CompileFail::Internal(m) => CompileError::Internal(m),
})
}
/// Turns a compile failure inside a running script into what it throws.
pub(crate) fn compile_error_to_abrupt(&mut self, e: CompileError) -> Abrupt {
match e {
CompileError::Syntax(s) => self.throw(ErrorType::Syntax, &s.message),
CompileError::Internal(m) => Abrupt::Internal(m),
}
}
fn direct_eval(&mut self, start: usize, args: Vec<Value>) -> JsResult<()> {
let x = args.into_iter().next().unwrap_or(Value::Undefined);
let Value::String(s) = &x else {
self.stack.truncate(start);
return self.push(x);
};
let strict = self.strict();
self.check_source_units(s.len()).map_err(|e| self.compile_error_to_abrupt(e))?;
let units = s.flat();
let code = self.compile_source(&units, strict, CodeKind::Eval).map_err(|e| self.compile_error_to_abrupt(e))?;
self.charge_code(&code, declaration_bytes(&code).saturating_add(ENV_BYTES))?;
let frame = self.frame()?;
let (caller_env, caller_var_env, this) = (frame.env, frame.var_env, frame.this.clone());
let (env, var_env) = if code.strict {
let e = self.heap.alloc_env(Env::Decl { bindings: PropMap::default(), outer: Some(caller_env) });
(e, e)
} else {
(caller_env, caller_var_env)
};
self.stack.truncate(start);
self.instantiate_global(&code, var_env, true)?;
self.push_code_frame(code, env, var_env, this, false)
}
/// Indirect eval and scripts: runs `code` in the global environment and returns its
/// completion value.
pub(crate) fn run_global(&mut self, code: Rc<Code>, is_eval: bool) -> JsResult<Value> {
// Indirect eval runs inside an operation and counts against the re-entry limit; the
// script a realm runs does not.
let nested = usize::from(is_eval);
if is_eval && self.reentry >= self.limits.reentry {
return Err(Abrupt::Limit(LimitKind::Reentry));
}
self.charge_code(&code, declaration_bytes(&code).saturating_add(ENV_BYTES))?;
let (env, global) = (self.global_env, self.global);
let (env, var_env) = if code.strict && is_eval {
let e = self.heap.alloc_env(Env::Decl { bindings: PropMap::default(), outer: Some(env) });
(e, e)
} else {
(env, env)
};
self.instantiate_global(&code, var_env, is_eval)?;
self.push_code_frame(code, env, var_env, Value::Object(global), true)?;
self.reentry += nested;
let r = self.execute();
self.reentry -= nested;
r
}
fn push_code_frame(&mut self, code: Rc<Code>, env: EnvId, var_env: EnvId, this: Value, boundary: bool) -> JsResult<()> {
if self.frames.len() >= self.limits.frames {
return Err(Abrupt::Limit(LimitKind::Frames));
}
let base = self.stack.len();
self.frames.push(Frame {
code,
pc: 0,
base,
env,
var_env,
env_depth: 0,
this,
callee: None,
construct: None,
completion: Value::Undefined,
boundary,
});
Ok(())
}
/// Declaration binding instantiation for global and eval code (section 10.5). The caller has
/// reserved [`declaration_bytes`] of `code`; each declaration takes a unit of fuel.
fn instantiate_global(&mut self, code: &Rc<Code>, var_env: EnvId, is_eval: bool) -> JsResult<()> {
let configurable = if is_eval { CONFIGURABLE } else { 0 };
for (name, fi) in &code.fun_decls {
self.use_fuel()?;
let Some(fcode) = code.functions.get(*fi as usize).cloned() else { return internal("function out of range") };
let f = self.make_closure(fcode, var_env)?;
self.declare(var_env, name, Some(Value::Object(f)), configurable)?;
}
for v in &code.vars {
self.use_fuel()?;
self.declare(var_env, v, None, configurable)?;
}
Ok(())
}
/// Declares `name` in a variable environment, with `value` for a function declaration.
fn declare(&mut self, env: EnvId, name: &Key, value: Option<Value>, configurable: u8) -> JsResult<()> {
match self.heap.env(env) {
Some(Env::Decl { bindings, .. }) => {
let exists = bindings.contains(name);
match value {
Some(v) => self.env_insert(env, name.clone(), Binding { value: v, mutable: true, deletable: configurable != 0 }),
None if !exists => {
self.env_insert(env, name.clone(), Binding { value: Value::Undefined, mutable: true, deletable: configurable != 0 })
}
None => Ok(()),
}
}
Some(Env::Obj { object, .. }) => {
let object = *object;
let declared = self.has_property(object, name)?;
let flags = WRITABLE | ENUMERABLE | configurable;
match value {
None if declared => Ok(()),
None => self.define(object, name.clone(), Prop::Data { value: Value::Undefined, flags }),
Some(v) => {
let existing = self.find_property(object, name)?;
match existing {
None => self.define(object, name.clone(), Prop::Data { value: v, flags }),
Some(p) if p.configurable() => self.define(object, name.clone(), Prop::Data { value: v, flags }),
Some(Prop::Data { flags: f, .. }) if f & (WRITABLE | ENUMERABLE) == WRITABLE | ENUMERABLE => {
self.put(object, name.clone(), v, true)
}
Some(_) => {
let message = format!("cannot declare function {}", String::from_utf16_lossy(name));
Err(self.type_error(&message))
}
}
}
}
}
None => internal("environment out of range"),
}
}
// ---- conversions ----
pub(crate) fn type_of(&self, v: &Value) -> &'static str {
match v {
Value::Undefined => "undefined",
Value::Null => "object",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Object(_) if self.is_callable(v) => "function",
Value::Object(_) => "object",
}
}
/// ToPrimitive (section 9.1) through [[DefaultValue]] (section 8.12.8).
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_primitive(&mut self, v: &Value, hint: Hint) -> JsResult<Value> {
let Value::Object(o) = v else { return Ok(v.clone()) };
// Without a hint, a Date object converts as with the String hint.
let date = hint == Hint::Default && matches!(self.heap.object(*o).map(|ob| &ob.class), Some(Class::Date(_)));
let order = if hint == Hint::String || date { ["toString", "valueOf"] } else { ["valueOf", "toString"] };
for name in order {
let f = self.get_from(*o, &key(name), v)?;
if self.is_callable(&f) {
let r = self.call(&f, v, &[])?;
if !matches!(r, Value::Object(_)) {
return Ok(r);
}
}
}
Err(self.type_error("cannot convert the object to a primitive value"))
}
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_number(&mut self, v: &Value) -> JsResult<f64> {
Ok(match v {
Value::Undefined => f64::NAN,
Value::Null => 0.0,
Value::Bool(b) => f64::from(u8::from(*b)),
Value::Number(n) => *n,
Value::String(s) => string_to_number(&s.flat()),
Value::Object(_) => {
let p = self.to_primitive(v, Hint::Number)?;
return self.to_number(&p);
}
})
}
/// ToString applied to a primitive.
pub(crate) fn primitive_to_str(&mut self, v: &Value) -> Str {
match v {
Value::Undefined => Str::from("undefined"),
Value::Null => Str::from("null"),
Value::Bool(b) => Str::from(if *b { "true" } else { "false" }),
Value::Number(n) => {
let s = number_to_str(*n);
self.heap.bytes += string_bytes(s.len());
s
}
Value::String(s) => s.clone(),
Value::Object(_) => Str::from("[object]"),
}
}
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_str(&mut self, v: &Value) -> JsResult<Str> {
let p = self.to_primitive(v, Hint::String)?;
Ok(self.primitive_to_str(&p))
}
/// A property name from a value (ToString, with a short way for array indices).
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_key(&mut self, v: &Value) -> JsResult<Key> {
match v {
Value::String(s) => {
let rope = s.is_rope();
let flat = s.flat();
if rope {
self.heap.bytes += string_bytes(flat.len());
}
Ok(flat)
}
Value::Number(n) if *n >= 0.0 && *n < 4_294_967_295.0 && n.fract() == 0.0 => Ok(index_key(*n as u32)),
_ => Ok(self.to_str(v)?.flat()),
}
}
/// ToObject (section 9.9).
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_object(&mut self, v: &Value) -> JsResult<ObjId> {
let (class, proto) = match v {
Value::Object(o) => return Ok(*o),
Value::Undefined | Value::Null => return Err(self.type_error("cannot convert undefined or null to an object")),
Value::Bool(_) => (Class::Boolean, self.intrinsics.boolean_proto),
Value::Number(n) => (Class::Number(*n), self.intrinsics.number_proto),
Value::String(s) => (Class::String(s.clone()), self.intrinsics.string_proto),
};
Ok(self.heap.alloc_object(Object::new(class, Some(proto))))
}
/// Concatenates two strings, ending the run with the heap limit when the result could not
/// be flattened within it.
pub(crate) fn concat(&mut self, a: &Str, b: &Str) -> JsResult<Str> {
let len = a.len() + b.len();
if len > self.limits.heap_bytes / 4 {
return Err(Abrupt::Limit(LimitKind::Heap));
}
self.heap.bytes += ROPE_BYTES;
Ok(Str::concat(a, b))
}
// ---- operators ----
fn op_unary(&mut self, op: Op) -> JsResult<()> {
let v = self.peek(0)?;
let r = match op {
Op::Neg => Value::Number(-self.to_number(&v)?),
Op::Plus | Op::ToNumber => Value::Number(self.to_number(&v)?),
Op::BitNot => Value::Number(f64::from(!to_int32(self.to_number(&v)?))),
Op::Not => Value::Bool(!v.truthy()),
Op::Typeof => Value::str(self.type_of(&v)),
_ => return internal("not a unary operator"),
};
self.replace(1, r)
}
fn op_binary(&mut self, op: BinaryOp) -> JsResult<()> {
let b = self.peek(0)?;
let a = self.peek(1)?;
let r = self.binary(op, &a, &b)?;
self.replace(2, r)
}
pub(crate) fn binary(&mut self, op: BinaryOp, a: &Value, b: &Value) -> JsResult<Value> {
let num = |interp: &mut Self, f: fn(f64, f64) -> f64| -> JsResult<Value> {
let x = interp.to_number(a)?;
let y = interp.to_number(b)?;
Ok(Value::Number(f(x, y)))
};
let int = |interp: &mut Self, f: fn(i32, i32) -> i32| -> JsResult<Value> {
let x = to_int32(interp.to_number(a)?);
let y = to_int32(interp.to_number(b)?);
Ok(Value::Number(f64::from(f(x, y))))
};
match op {
BinaryOp::Add => self.add(a, b),
BinaryOp::Sub => num(self, |x, y| x - y),
BinaryOp::Mul => num(self, |x, y| x * y),
BinaryOp::Div => num(self, |x, y| x / y),
BinaryOp::Mod => num(self, |x, y| x % y),
BinaryOp::Shl => int(self, |x, y| x.wrapping_shl((y & 31) as u32)),
BinaryOp::Shr => int(self, |x, y| x >> (y & 31)),
BinaryOp::UShr => {
let x = to_uint32(self.to_number(a)?);
let y = to_uint32(self.to_number(b)?);
Ok(Value::Number(f64::from(x >> (y & 31))))
}
BinaryOp::BitAnd => int(self, |x, y| x & y),
BinaryOp::BitOr => int(self, |x, y| x | y),
BinaryOp::BitXor => int(self, |x, y| x ^ y),
BinaryOp::Lt => Ok(Value::Bool(self.less_than(a, b, true)? == Some(true))),
BinaryOp::Gt => Ok(Value::Bool(self.less_than(b, a, false)? == Some(true))),
BinaryOp::LtEq => Ok(Value::Bool(self.less_than(b, a, false)? == Some(false))),
BinaryOp::GtEq => Ok(Value::Bool(self.less_than(a, b, true)? == Some(false))),
BinaryOp::Eq => Ok(Value::Bool(self.loose_eq(a, b)?)),
BinaryOp::NotEq => Ok(Value::Bool(!self.loose_eq(a, b)?)),
BinaryOp::StrictEq => Ok(Value::Bool(strict_eq(a, b))),
BinaryOp::StrictNotEq => Ok(Value::Bool(!strict_eq(a, b))),
BinaryOp::Instanceof => self.instance_of(a, b).map(Value::Bool),
BinaryOp::In => {
let Value::Object(o) = b else { return Err(self.type_error("the right side of 'in' is not an object")) };
let k = self.to_key(a)?;
self.has_property(*o, &k).map(Value::Bool)
}
}
}
/// The addition operator (section 11.6.1).
fn add(&mut self, a: &Value, b: &Value) -> JsResult<Value> {
if let (Value::Number(x), Value::Number(y)) = (a, b) {
return Ok(Value::Number(x + y));
}
let pa = self.to_primitive(a, Hint::Default)?;
let pb = self.to_primitive(b, Hint::Default)?;
if matches!(pa, Value::String(_)) || matches!(pb, Value::String(_)) {
let sa = self.primitive_to_str(&pa);
let sb = self.primitive_to_str(&pb);
return self.concat(&sa, &sb).map(Value::String);
}
Ok(Value::Number(self.to_number(&pa)? + self.to_number(&pb)?))
}
/// The abstract relational comparison `x < y` (section 11.8.5): `None` when either side is
/// NaN.
fn less_than(&mut self, x: &Value, y: &Value, left_first: bool) -> JsResult<Option<bool>> {
let (px, py) = if left_first {
let px = self.to_primitive(x, Hint::Number)?;
(px, self.to_primitive(y, Hint::Number)?)
} else {
let py = self.to_primitive(y, Hint::Number)?;
(self.to_primitive(x, Hint::Number)?, py)
};
if let (Value::String(a), Value::String(b)) = (&px, &py) {
return Ok(Some(*a.flat() < *b.flat()));
}
let nx = self.to_number(&px)?;
let ny = self.to_number(&py)?;
if nx.is_nan() || ny.is_nan() {
return Ok(None);
}
Ok(Some(nx < ny))
}
/// The abstract equality comparison (section 11.9.3).
fn loose_eq(&mut self, a: &Value, b: &Value) -> JsResult<bool> {
let (mut x, mut y) = (a.clone(), b.clone());
loop {
match (&x, &y) {
(Value::Undefined | Value::Null, Value::Undefined | Value::Null) => return Ok(true),
(Value::Number(_), Value::Number(_))
| (Value::String(_), Value::String(_))
| (Value::Bool(_), Value::Bool(_))
| (Value::Object(_), Value::Object(_)) => return Ok(strict_eq(&x, &y)),
(Value::Number(n), Value::String(s)) | (Value::String(s), Value::Number(n)) => return Ok(*n == string_to_number(&s.flat())),
(Value::Bool(v), _) => x = Value::Number(f64::from(u8::from(*v))),
(_, Value::Bool(v)) => y = Value::Number(f64::from(u8::from(*v))),
(Value::Number(_) | Value::String(_), Value::Object(_)) => y = self.to_primitive(&y, Hint::Default)?,
(Value::Object(_), Value::Number(_) | Value::String(_)) => x = self.to_primitive(&x, Hint::Default)?,
_ => return Ok(false),
}
}
}
/// `instanceof` (sections 11.8.6 and 15.3.5.3).
fn instance_of(&mut self, v: &Value, f: &Value) -> JsResult<bool> {
if !self.is_callable(f) {
return Err(self.type_error("the right side of 'instanceof' is not a function"));
}
let Some(fid) = f.as_object() else { return Ok(false) };
let Value::Object(mut o) = v.clone() else { return Ok(false) };
let Value::Object(proto) = self.get_from(fid, &key("prototype"), f)? else {
return Err(self.type_error("the function's prototype is not an object"));
};
let mut steps = 0usize;
while let Some(p) = self.object(o)?.proto {
if p == proto {
return Ok(true);
}
o = p;
steps += 1;
if steps > 1_000_000 {
return internal("prototype chain too long");
}
}
Ok(false)
}
// ---- host values ----
pub(crate) fn to_js_value(&self, v: &Value) -> crate::JsValue {
match v {
Value::Undefined => crate::JsValue::Undefined,
Value::Null => crate::JsValue::Null,
Value::Bool(b) => crate::JsValue::Bool(*b),
Value::Number(n) => crate::JsValue::Number(*n),
Value::String(s) => crate::JsValue::String(s.to_rust_string()),
Value::Object(o) => crate::JsValue::Object(self.heap.object(*o).map_or("Object", |o| o.class.name()).to_string()),
}
}
/// A host value as a JavaScript value. A string is charged to the heap, and one that does not
/// fit in it ends the run with the heap limit. Everything the caller still needs must be
/// reachable from the roots.
#[allow(clippy::wrong_self_convention)]
pub(crate) fn from_js_value(&mut self, v: crate::JsValue) -> JsResult<Value> {
Ok(match v {
crate::JsValue::Undefined => Value::Undefined,
crate::JsValue::Null => Value::Null,
crate::JsValue::Bool(b) => Value::Bool(b),
crate::JsValue::Number(n) => Value::Number(n),
crate::JsValue::String(s) => {
let bytes = string_bytes(s.encode_utf16().count());
self.reserve_heap(bytes)?;
self.heap.bytes += bytes;
Value::str(&s)
}
crate::JsValue::Object(_) => Value::Object(self.new_object()),
})
}
/// A description of a thrown value that runs no JavaScript: `Name: message` for an error,
/// the text of a primitive.
pub(crate) fn describe(&mut self, v: &Value) -> String {
let Value::Object(o) = v else { return self.primitive_to_str(v).to_rust_string() };
let data = |interp: &Self, k: &str| match interp.find_property(*o, &key(k)) {
Ok(Some(Prop::Data { value: Value::String(s), .. })) => Some(s.to_rust_string()),
_ => None,
};
match (data(self, "name"), data(self, "message")) {
(Some(n), Some(m)) if !m.is_empty() => format!("{n}: {m}"),
(Some(n), _) => n,
(None, Some(m)) => m,
(None, None) => format!("[object {}]", self.heap.object(*o).map_or("Object", |o| o.class.name())),
}
}
}
/// A compile failure, before it becomes an exception or a run error.
pub(crate) enum CompileError {
Syntax(crate::SyntaxError),
Internal(String),
}
/// The strict equality comparison (section 11.9.6).
pub(crate) fn strict_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Undefined, Value::Undefined) | (Value::Null, Value::Null) => true,
(Value::Number(x), Value::Number(y)) => x == y,
(Value::String(x), Value::String(y)) => x == y,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Object(x), Value::Object(y)) => x == y,
_ => false,
}
}