use crate::heap::Handle;
use crate::nanbox::NanBox;
use crate::realm::Realm;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
pub type Reg = u16;
#[derive(Clone, Debug)]
#[allow(missing_docs)]
pub enum Op {
LoadConst { dst: Reg, value: NanBox },
Add { dst: Reg, a: Reg, b: Reg },
Sub { dst: Reg, a: Reg, b: Reg },
Mul { dst: Reg, a: Reg, b: Reg },
Div { dst: Reg, a: Reg, b: Reg },
Mod { dst: Reg, a: Reg, b: Reg },
ValueBin { dst: Reg, op: u8, a: Reg, b: Reg },
HasProp { dst: Reg, key: Reg, obj: Reg },
IsBuiltin { dst: Reg, obj: Reg, kind: u8 },
DeleteProp { dst: Reg, obj: Reg, key: Reg },
SetClassTag { obj: Reg, class_id: u32 },
DefineAccessor {
obj: Reg,
key: String,
getter: Reg,
setter: Reg,
},
InstanceOf {
dst: Reg,
obj: Reg,
ids: alloc::rc::Rc<[u32]>,
},
TypeOf { dst: Reg, a: Reg },
BitNot { dst: Reg, a: Reg },
Neg { dst: Reg, a: Reg },
Not { dst: Reg, a: Reg },
Lt { dst: Reg, a: Reg, b: Reg },
Move { dst: Reg, src: Reg },
JumpIfFalse { cond: Reg, target: usize },
Jump { target: usize },
AddValue { dst: Reg, a: Reg, b: Reg },
StrictEq { dst: Reg, a: Reg, b: Reg },
NewString { dst: Reg, value: String },
NewArray { dst: Reg, len: usize },
NewArrayCtor { dst: Reg, arg: Reg },
GetElem { dst: Reg, arr: Reg, index: Reg },
SetElem { arr: Reg, index: Reg, src: Reg },
GetKey { dst: Reg, obj: Reg, key: Reg },
SetKey { obj: Reg, key: Reg, src: Reg },
ObjectSpread { dst: Reg, src: Reg },
EnumKeys { dst: Reg, obj: Reg },
ArrayLen { dst: Reg, arr: Reg },
CollectionSize { dst: Reg, recv: Reg },
ArrayPush { arr: Reg, src: Reg },
ArrayExtend { arr: Reg, src: Reg },
ArraySliceFrom { dst: Reg, src: Reg, from: Reg },
ObjectRest {
dst: Reg,
src: Reg,
exclude: alloc::rc::Rc<[String]>,
},
NewCollection {
dst: Reg,
is_set: bool,
seed: Option<Reg>,
},
NewRegExp {
dst: Reg,
source: String,
flags: String,
},
NewObject { dst: Reg },
SetProp { obj: Reg, key: String, src: Reg },
GetProp { dst: Reg, obj: Reg, key: String },
Call { dst: Reg, func: u32, args: Vec<Reg> },
LoadFunc { dst: Reg, func: u32 },
MakeClosure {
dst: Reg,
func: u32,
captures: Vec<Reg>,
},
CallValue {
dst: Reg,
callee: Reg,
args: Vec<Reg>,
},
CallValueThis {
dst: Reg,
callee: Reg,
recv: Reg,
args: Vec<Reg>,
},
CallMethod {
dst: Reg,
recv: Reg,
key: String,
args: Vec<Reg>,
},
CallCtor {
ctor: u32,
recv: Reg,
args: Vec<Reg>,
},
CallNative {
dst: Reg,
native: u16,
args: Vec<Reg>,
},
PushHandler { target: usize, reg: Reg },
PopHandler,
Throw { src: Reg },
Return { src: Reg },
}
const VB_POW: u8 = 0;
pub(crate) const VB_BIT_AND: u8 = 1;
pub(crate) const VB_BIT_OR: u8 = 2;
pub(crate) const VB_BIT_XOR: u8 = 3;
pub(crate) const VB_SHL: u8 = 4;
pub(crate) const VB_SHR: u8 = 5;
pub(crate) const VB_USHR: u8 = 6;
pub(crate) const VB_LOOSE_EQ: u8 = 7;
const VB_LOOSE_NEQ: u8 = 8;
const NB_CONSOLE_LOG: u16 = 0;
pub(crate) const NB_MATH_MAX: u16 = 1;
pub(crate) const NB_MATH_MIN: u16 = 2;
pub(crate) const NB_MATH_ABS: u16 = 3;
pub(crate) const NB_MATH_FLOOR: u16 = 4;
pub(crate) const NB_MATH_CEIL: u16 = 5;
const NB_MATH_ROUND: u16 = 6;
pub(crate) const NB_MATH_SQRT: u16 = 7;
const NB_MATH_POW: u16 = 8;
const NB_STRING: u16 = 9;
const NB_NUMBER: u16 = 10;
const NB_PARSE_INT: u16 = 11;
const NB_PARSE_FLOAT: u16 = 12;
const NB_IS_NAN: u16 = 13;
const NB_IS_FINITE: u16 = 14;
const NB_OBJECT_KEYS: u16 = 15;
const NB_OBJECT_VALUES: u16 = 16;
const NB_OBJECT_ENTRIES: u16 = 17;
const NB_OBJECT_ASSIGN: u16 = 18;
const NB_JSON_STRINGIFY: u16 = 19;
const NB_JSON_PARSE: u16 = 20;
const NB_NUMBER_IS_INTEGER: u16 = 21;
const NB_NUMBER_IS_FINITE: u16 = 22;
const NB_NUMBER_IS_NAN: u16 = 23;
const NB_NUMBER_PARSE_FLOAT: u16 = 24;
const NB_NUMBER_PARSE_INT: u16 = 25;
const NB_STRING_FROM_CHAR_CODE: u16 = 26;
const NB_ARRAY_FROM: u16 = 27;
const NB_ARRAY_IS_ARRAY: u16 = 28;
const NB_OBJECT_FROM_ENTRIES: u16 = 29;
const NB_PROMISE_RESOLVE: u16 = 30;
const NB_PROMISE_REJECT: u16 = 31;
pub(crate) const NB_MATH_TRUNC: u16 = 32;
const KNOWN_GLOBALS: &[&str] = &[
"Math",
"console",
"Promise",
"Date",
"RegExp",
"JSON",
"Object",
"Array",
"String",
"Number",
"Boolean",
"parseInt",
"parseFloat",
"isNaN",
"isFinite",
"Map",
"Set",
"Symbol",
"BigInt",
"WeakMap",
"WeakSet",
"WeakRef",
"Proxy",
"Reflect",
"Function",
"ArrayBuffer",
"DataView",
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
"globalThis",
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
"Intl",
"WebAssembly",
"structuredClone",
"setTimeout",
"clearTimeout",
"queueMicrotask",
];
#[derive(Clone, Debug)]
pub struct FnProto {
pub ops: Vec<Op>,
pub n_regs: usize,
pub n_params: usize,
pub n_captures: usize,
pub rest_from: Option<usize>,
pub is_async: bool,
pub length: usize,
pub name: alloc::string::String,
}
struct Microtask {
handler: NanBox,
value: NanBox,
result: Handle,
fulfilled: bool,
}
struct Ctx<'a> {
realm: &'a mut Realm,
output: String,
microtasks: alloc::collections::VecDeque<Microtask>,
tiers: alloc::collections::BTreeMap<usize, TierState>,
#[cfg(all(feature = "jit", target_os = "linux", target_arch = "x86_64"))]
jit_cache: alloc::collections::BTreeMap<usize, Option<alloc::rc::Rc<crate::jit::JitProto>>>,
call_depth: usize,
}
type TierState = (u32, Option<alloc::rc::Rc<Vec<Op>>>);
pub fn run_program(
realm: &mut Realm,
funcs: &[FnProto],
id: usize,
args: &[NanBox],
) -> Result<NanBox, VmError> {
let mut ctx = Ctx {
realm,
output: String::new(),
microtasks: alloc::collections::VecDeque::new(),
tiers: alloc::collections::BTreeMap::new(),
#[cfg(all(feature = "jit", target_os = "linux", target_arch = "x86_64"))]
jit_cache: alloc::collections::BTreeMap::new(),
call_depth: 0,
};
let value = call(&mut ctx, funcs, id, args)?;
drain_microtasks(&mut ctx, funcs)?;
Ok(value)
}
pub fn run_program_capturing(
realm: &mut Realm,
funcs: &[FnProto],
id: usize,
args: &[NanBox],
) -> Result<(NanBox, String), VmError> {
let mut ctx = Ctx {
realm,
output: String::new(),
microtasks: alloc::collections::VecDeque::new(),
tiers: alloc::collections::BTreeMap::new(),
#[cfg(all(feature = "jit", target_os = "linux", target_arch = "x86_64"))]
jit_cache: alloc::collections::BTreeMap::new(),
call_depth: 0,
};
let value = call(&mut ctx, funcs, id, args)?;
drain_microtasks(&mut ctx, funcs)?;
Ok((value, ctx.output))
}
fn call(ctx: &mut Ctx, funcs: &[FnProto], id: usize, args: &[NanBox]) -> Result<NanBox, VmError> {
call_with(ctx, funcs, id, args, &[], NanBox::undefined())
}
fn call_with(
ctx: &mut Ctx,
funcs: &[FnProto],
id: usize,
args: &[NanBox],
captures: &[NanBox],
this_val: NanBox,
) -> Result<NanBox, VmError> {
if ctx.call_depth >= ctx.realm.limits.max_call_depth {
let e = make_error(ctx.realm, "RangeError", "Maximum call stack size exceeded");
return Err(VmError::Thrown(e));
}
ctx.call_depth += 1;
let result = call_with_inner(ctx, funcs, id, args, captures, this_val);
ctx.call_depth -= 1;
result
}
fn call_with_inner(
ctx: &mut Ctx,
funcs: &[FnProto],
id: usize,
args: &[NanBox],
captures: &[NanBox],
this_val: NanBox,
) -> Result<NanBox, VmError> {
let Some(proto) = funcs.get(id) else {
let e = make_error(ctx.realm, "TypeError", "not a function");
return Err(VmError::Thrown(e));
};
let mut regs: Vec<NanBox> = vec![NanBox::undefined(); proto.n_regs];
match proto.rest_from {
Some(fixed) => {
for (i, a) in args.iter().enumerate().take(fixed) {
regs[i] = *a;
}
let rest: Vec<NanBox> = args.get(fixed..).unwrap_or(&[]).to_vec();
let arr = ctx.realm.new_array(rest);
if fixed < regs.len() {
regs[fixed] = NanBox::handle(arr.to_raw());
}
}
None => {
for (i, a) in args.iter().enumerate().take(proto.n_params) {
regs[i] = *a;
}
}
}
for (i, c) in captures.iter().enumerate().take(proto.n_captures) {
regs[proto.n_params + i] = *c;
}
if let Some(slot) = regs.get_mut(proto.n_params + proto.n_captures) {
*slot = this_val;
}
let optimized: Option<alloc::rc::Rc<Vec<Op>>> = {
let entry = ctx.tiers.entry(id).or_default();
entry.0 = entry.0.saturating_add(1);
if entry.0 == TIER_UP_THRESHOLD && entry.1.is_none() {
entry.1 = Some(alloc::rc::Rc::new(optimize_ops(&funcs[id].ops)));
}
entry.1.clone()
};
let body: &[Op] = match &optimized {
Some(o) => o.as_slice(),
None => proto.ops.as_slice(),
};
#[cfg(all(feature = "jit", target_os = "linux", target_arch = "x86_64"))]
if optimized.is_some() && !proto.is_async && proto.rest_from.is_none() && proto.n_captures == 0
{
let mut stack = alloc::collections::BTreeSet::new();
let cached = ensure_jit(&mut ctx.jit_cache, funcs, id, &mut stack);
if let Some(jit) = cached
&& let Some(result) = jit.call_guarded(args)
{
return Ok(result);
}
}
if proto.is_async {
let p = ctx.realm.new_promise();
match run_frame(ctx, funcs, body, &mut regs) {
Ok(ret) => settle(ctx, p, ret.unwrap_or(NanBox::undefined()), true),
Err(VmError::Thrown(e)) => settle(ctx, p, e, false),
Err(other) => return Err(other),
}
return Ok(NanBox::handle(p.to_raw()));
}
Ok(run_frame(ctx, funcs, body, &mut regs)?.unwrap_or(NanBox::undefined()))
}
#[cfg(all(feature = "jit", target_os = "linux", target_arch = "x86_64"))]
fn ensure_jit(
cache: &mut alloc::collections::BTreeMap<usize, Option<alloc::rc::Rc<crate::jit::JitProto>>>,
funcs: &[FnProto],
id: usize,
stack: &mut alloc::collections::BTreeSet<usize>,
) -> Option<alloc::rc::Rc<crate::jit::JitProto>> {
if let Some(c) = cache.get(&id) {
return c.clone();
}
if stack.contains(&id) {
return None; }
stack.insert(id);
let mut registry = alloc::collections::BTreeMap::new();
for op in &funcs[id].ops {
if let Op::Call { func, .. } = op {
let fid = *func as usize;
if fid != id
&& fid < funcs.len()
&& let Some(j) = ensure_jit(cache, funcs, fid, stack)
{
registry.insert(*func, j.code_ptr() as u64);
}
}
}
stack.remove(&id);
let compiled =
crate::jit::JitProto::compile_with_registry(&funcs[id], ®istry).map(alloc::rc::Rc::new);
cache.insert(id, compiled.clone());
compiled
}
#[derive(Clone, PartialEq, Debug)]
pub enum VmError {
NotANumber,
NotAnObject,
Thrown(NanBox),
}
pub fn run(realm: &mut Realm, program: &[Op], register_count: usize) -> Result<NanBox, VmError> {
let mut regs: Vec<NanBox> = vec![NanBox::undefined(); register_count];
let mut ctx = Ctx {
realm,
output: String::new(),
microtasks: alloc::collections::VecDeque::new(),
tiers: alloc::collections::BTreeMap::new(),
#[cfg(all(feature = "jit", target_os = "linux", target_arch = "x86_64"))]
jit_cache: alloc::collections::BTreeMap::new(),
call_depth: 0,
};
Ok(run_frame(&mut ctx, &[], program, &mut regs)?.unwrap_or(NanBox::undefined()))
}
fn loose_eq_coerce(realm: &mut Realm, x: NanBox, y: NanBox) -> (NanBox, NanBox) {
let is_obj = |realm: &Realm, v: NanBox| {
v.as_handle()
.map(Handle::from_raw)
.is_some_and(|h| realm.string_value(h).is_none())
};
let is_prim = |realm: &Realm, v: NanBox| {
v.as_number().is_some()
|| matches!(v.unpack(), crate::nanbox::Unpacked::Bool(_))
|| v.as_handle()
.map(Handle::from_raw)
.is_some_and(|h| realm.string_value(h).is_some())
};
if is_obj(realm, x) && is_prim(realm, y) {
let s = realm.to_display_string(x);
(NanBox::handle(realm.new_string(&s).to_raw()), y)
} else if is_obj(realm, y) && is_prim(realm, x) {
let s = realm.to_display_string(y);
(x, NanBox::handle(realm.new_string(&s).to_raw()))
} else {
(x, y)
}
}
fn to_primitive(ctx: &mut Ctx, funcs: &[FnProto], v: NanBox, number_hint: bool) -> NanBox {
let Some(raw) = v.as_handle() else {
return v;
};
let h = Handle::from_raw(raw);
if ctx.realm.string_value(h).is_some() || ctx.realm.date_at(h).is_some() {
return v;
}
let order: [&str; 2] = if number_hint {
["valueOf", "toString"]
} else {
["toString", "valueOf"]
};
for name in order {
if let Some(method) = ctx.realm.get_property(h, name)
&& method.as_handle().is_some()
&& let Ok(res) = call_closure(ctx, funcs, method, &[], v)
{
use crate::nanbox::Unpacked;
let is_prim = res.as_number().is_some()
|| matches!(
res.unpack(),
Unpacked::Bool(_) | Unpacked::Null | Unpacked::Undefined
)
|| res
.as_handle()
.map(Handle::from_raw)
.is_some_and(|rh| ctx.realm.string_value(rh).is_some());
if is_prim {
return res;
}
}
}
v
}
fn make_error(realm: &mut Realm, name: &str, message: &str) -> NanBox {
let obj = realm.new_object();
let n = NanBox::handle(realm.new_string(name).to_raw());
realm.set_property(obj, "name", n);
let m = NanBox::handle(realm.new_string(message).to_raw());
realm.set_property(obj, "message", m);
NanBox::handle(obj.to_raw())
}
const SYM_NUM_ERR: &str = "Cannot convert a Symbol value to a number";
const SYM_STR_ERR: &str = "Cannot convert a Symbol value to a string";
fn symbol_coercion_error(realm: &mut Realm, x: NanBox, y: NanBox, msg: &str) -> Option<NanBox> {
let is_sym = |v: NanBox| {
v.as_handle()
.map(Handle::from_raw)
.is_some_and(|h| realm.symbol_at(h).is_some())
};
if is_sym(x) || is_sym(y) {
Some(make_error(realm, "TypeError", msg))
} else {
None
}
}
fn settle(ctx: &mut Ctx, p: Handle, value: NanBox, fulfilled: bool) {
use crate::cell::PromiseStatus;
let Some(state) = ctx.realm.promise_state(p) else {
return;
};
let reactions = {
let mut s = state.borrow_mut();
if s.status != PromiseStatus::Pending {
return;
}
s.status = if fulfilled {
PromiseStatus::Fulfilled
} else {
PromiseStatus::Rejected
};
s.value = value;
core::mem::take(&mut s.reactions)
};
for r in reactions {
let handler = if fulfilled {
r.on_fulfilled
} else {
r.on_rejected
};
ctx.microtasks.push_back(Microtask {
handler,
value,
result: r.result,
fulfilled,
});
}
}
fn promise_then(ctx: &mut Ctx, p: Handle, on_f: NanBox, on_r: NanBox) -> Handle {
use crate::cell::{PromiseStatus, Reaction};
let result = ctx.realm.new_promise();
let Some(state) = ctx.realm.promise_state(p) else {
return result;
};
let (status, value) = {
let s = state.borrow();
(s.status, s.value)
};
match status {
PromiseStatus::Pending => state.borrow_mut().reactions.push(Reaction {
on_fulfilled: on_f,
on_rejected: on_r,
result,
finally: false,
}),
PromiseStatus::Fulfilled => ctx.microtasks.push_back(Microtask {
handler: on_f,
value,
result,
fulfilled: true,
}),
PromiseStatus::Rejected => ctx.microtasks.push_back(Microtask {
handler: on_r,
value,
result,
fulfilled: false,
}),
}
result
}
fn drain_microtasks(ctx: &mut Ctx, funcs: &[FnProto]) -> Result<(), VmError> {
while let Some(task) = ctx.microtasks.pop_front() {
if task.handler.as_handle().is_some() {
match call_closure(ctx, funcs, task.handler, &[task.value], NanBox::undefined()) {
Ok(ret) => settle(ctx, task.result, ret, true),
Err(VmError::Thrown(e)) => settle(ctx, task.result, e, false),
Err(other) => return Err(other),
}
} else {
settle(ctx, task.result, task.value, task.fulfilled);
}
}
Ok(())
}
const TIER_UP_THRESHOLD: u32 = 8;
fn optimize_ops(ops: &[Op]) -> Vec<Op> {
use alloc::collections::{BTreeMap, BTreeSet};
let mut leaders: BTreeSet<usize> = BTreeSet::new();
for op in ops {
match op {
Op::Jump { target }
| Op::JumpIfFalse { target, .. }
| Op::PushHandler { target, .. } => {
leaders.insert(*target);
}
_ => {}
}
}
let mut consts: BTreeMap<Reg, NanBox> = BTreeMap::new();
let num = |consts: &BTreeMap<Reg, NanBox>, r: &Reg| consts.get(r).and_then(|v| v.as_number());
let mut out: Vec<Op> = Vec::with_capacity(ops.len());
for (i, op) in ops.iter().enumerate() {
if leaders.contains(&i) {
consts.clear();
}
let folded: Op = match op {
Op::LoadConst { dst, value } => {
consts.insert(*dst, *value);
op.clone()
}
Op::Add { dst, a, b } => fold2(*dst, num(&consts, a), num(&consts, b), |x, y| {
NanBox::number(x + y)
})
.unwrap_or_else(|| op.clone()),
Op::Sub { dst, a, b } => fold2(*dst, num(&consts, a), num(&consts, b), |x, y| {
NanBox::number(x - y)
})
.unwrap_or_else(|| op.clone()),
Op::Mul { dst, a, b } => fold2(*dst, num(&consts, a), num(&consts, b), |x, y| {
NanBox::number(x * y)
})
.unwrap_or_else(|| op.clone()),
Op::Div { dst, a, b } => fold2(*dst, num(&consts, a), num(&consts, b), |x, y| {
NanBox::number(x / y)
})
.unwrap_or_else(|| op.clone()),
Op::Mod { dst, a, b } => fold2(*dst, num(&consts, a), num(&consts, b), |x, y| {
NanBox::number(x % y)
})
.unwrap_or_else(|| op.clone()),
Op::Lt { dst, a, b } => fold2(*dst, num(&consts, a), num(&consts, b), |x, y| {
NanBox::boolean(x < y)
})
.unwrap_or_else(|| op.clone()),
Op::AddValue { dst, a, b } => fold2(*dst, num(&consts, a), num(&consts, b), |x, y| {
NanBox::number(x + y)
})
.unwrap_or_else(|| op.clone()),
Op::ValueBin { dst, op: vop, a, b } if matches!(*vop, VB_LOOSE_EQ | VB_LOOSE_NEQ) => {
let eq = matches!(*vop, VB_LOOSE_EQ);
fold2(*dst, num(&consts, a), num(&consts, b), move |x, y| {
NanBox::boolean((x == y) == eq)
})
.unwrap_or_else(|| op.clone())
}
Op::Neg { dst, a } => match num(&consts, a) {
Some(x) => Op::LoadConst {
dst: *dst,
value: NanBox::number(-x),
},
None => op.clone(),
},
Op::Not { dst, a } => match consts.get(a) {
Some(v) => Op::LoadConst {
dst: *dst,
value: NanBox::boolean(!v.to_boolean()),
},
None => op.clone(),
},
_ => {
consts.clear();
op.clone()
}
};
if let Op::LoadConst { dst, value } = &folded {
consts.insert(*dst, *value);
}
out.push(folded);
}
out
}
fn fold2(dst: Reg, a: Option<f64>, b: Option<f64>, f: impl Fn(f64, f64) -> NanBox) -> Option<Op> {
match (a, b) {
(Some(x), Some(y)) => Some(Op::LoadConst {
dst,
value: f(x, y),
}),
_ => None,
}
}
fn run_frame(
ctx: &mut Ctx,
funcs: &[FnProto],
program: &[Op],
regs: &mut [NanBox],
) -> Result<Option<NanBox>, VmError> {
let mut pc = 0;
let mut handlers: Vec<(usize, Reg)> = Vec::new();
let num = |v: NanBox| v.as_number().ok_or(VmError::NotANumber);
let object_handle = |v: NanBox| {
v.as_handle()
.map(Handle::from_raw)
.ok_or(VmError::NotAnObject)
};
macro_rules! handle_throw {
($e:expr) => {
match $e {
VmError::Thrown(v) => match handlers.pop() {
Some((target, reg)) => {
regs[reg as usize] = v;
pc = target;
}
None => return Err(VmError::Thrown(v)),
},
other => return Err(other),
}
};
}
while pc < program.len() {
let op = &program[pc];
pc += 1;
match op {
Op::LoadConst { dst, value } => regs[*dst as usize] = *value,
Op::Add { dst, a, b } => {
regs[*dst as usize] =
NanBox::number(num(regs[*a as usize])? + num(regs[*b as usize])?);
}
Op::Sub { dst, a, b } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
let y = to_primitive(ctx, funcs, regs[*b as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, y, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.sub(x, y);
}
Op::Mul { dst, a, b } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
let y = to_primitive(ctx, funcs, regs[*b as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, y, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.mul(x, y);
}
Op::Div { dst, a, b } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
let y = to_primitive(ctx, funcs, regs[*b as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, y, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.div(x, y);
}
Op::Mod { dst, a, b } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
let y = to_primitive(ctx, funcs, regs[*b as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, y, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.rem(x, y);
}
Op::HasProp { dst, key, obj } => {
let present = match regs[*obj as usize].as_handle().map(Handle::from_raw) {
Some(h) => {
let k = ctx.realm.to_display_string(regs[*key as usize]);
let mut found = false;
let mut cur = Some(h);
while let Some(c) = cur {
if ctx.realm.has_own(c, &k) {
found = true;
break;
}
cur = ctx.realm.object_proto(c);
}
found
|| ctx
.realm
.array_length(h)
.is_some_and(|len| k.parse::<usize>().is_ok_and(|i| i < len))
}
None => false,
};
regs[*dst as usize] = NanBox::boolean(present);
}
Op::IsBuiltin { dst, obj, kind } => {
let yes = regs[*obj as usize]
.as_handle()
.map(Handle::from_raw)
.is_some_and(|h| match kind {
0 => ctx.realm.regexp_at(h).is_some(),
1 => ctx.realm.is_array(h),
2 => ctx.realm.collection_is_set(h) == Some(false),
3 => ctx.realm.collection_is_set(h) == Some(true),
_ => false,
});
regs[*dst as usize] = NanBox::boolean(yes);
}
Op::DeleteProp { dst, obj, key } => {
let removed = match regs[*obj as usize].as_handle().map(Handle::from_raw) {
Some(h) => {
let k = ctx.realm.to_display_string(regs[*key as usize]);
ctx.realm.delete_property(h, &k)
}
None => true,
};
regs[*dst as usize] = NanBox::boolean(removed);
}
Op::SetClassTag { obj, class_id } => {
if let Some(h) = regs[*obj as usize].as_handle().map(Handle::from_raw) {
ctx.realm.set_class_tag(h, *class_id);
}
}
Op::DefineAccessor {
obj,
key,
getter,
setter,
} => {
if let Some(h) = regs[*obj as usize].as_handle().map(Handle::from_raw) {
ctx.realm.define_accessor(
h,
key,
regs[*getter as usize],
regs[*setter as usize],
);
}
}
Op::InstanceOf { dst, obj, ids } => {
let yes = regs[*obj as usize]
.as_handle()
.map(Handle::from_raw)
.and_then(|h| ctx.realm.class_tag(h))
.is_some_and(|t| ids.contains(&t));
regs[*dst as usize] = NanBox::boolean(yes);
}
Op::TypeOf { dst, a } => {
let t = ctx.realm.type_of_value(regs[*a as usize]);
regs[*dst as usize] = NanBox::handle(ctx.realm.new_string(t).to_raw());
}
#[cfg(feature = "std")]
Op::BitNot { dst, a } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, x, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.bit_not(x);
}
#[cfg(not(feature = "std"))]
Op::BitNot { dst, .. } => regs[*dst as usize] = NanBox::number(f64::NAN),
Op::ValueBin { dst, op, a, b } => {
let (x, y) = (regs[*a as usize], regs[*b as usize]);
regs[*dst as usize] = match *op {
VB_LOOSE_EQ | VB_LOOSE_NEQ => {
let (xc, yc) = loose_eq_coerce(ctx.realm, x, y);
let r = ctx.realm.loose_equals(xc, yc);
NanBox::boolean(if *op == VB_LOOSE_EQ { r } else { !r })
}
#[cfg(feature = "std")]
_ => {
let xn = to_primitive(ctx, funcs, x, true);
let yn = to_primitive(ctx, funcs, y, true);
if let Some(e) = symbol_coercion_error(ctx.realm, xn, yn, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
match *op {
VB_POW => ctx.realm.pow(xn, yn),
VB_BIT_AND => ctx.realm.bit_and(xn, yn),
VB_BIT_OR => ctx.realm.bit_or(xn, yn),
VB_BIT_XOR => ctx.realm.bit_xor(xn, yn),
VB_SHL => ctx.realm.shl(xn, yn),
VB_SHR => ctx.realm.shr(xn, yn),
VB_USHR => ctx.realm.ushr(xn, yn),
_ => NanBox::number(f64::NAN),
}
}
#[cfg(not(feature = "std"))]
_ => NanBox::number(f64::NAN),
};
}
Op::Neg { dst, a } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, x, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.neg(x);
}
Op::Not { dst, a } => {
regs[*dst as usize] = NanBox::boolean(!ctx.realm.truthy(regs[*a as usize]));
}
Op::Move { dst, src } => regs[*dst as usize] = regs[*src as usize],
Op::Lt { dst, a, b } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
let y = to_primitive(ctx, funcs, regs[*b as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, y, SYM_NUM_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.less_than(x, y);
}
Op::AddValue { dst, a, b } => {
let x = to_primitive(ctx, funcs, regs[*a as usize], true);
let y = to_primitive(ctx, funcs, regs[*b as usize], true);
if let Some(e) = symbol_coercion_error(ctx.realm, x, y, SYM_STR_ERR) {
handle_throw!(VmError::Thrown(e));
}
regs[*dst as usize] = ctx.realm.add(x, y);
}
Op::StrictEq { dst, a, b } => {
regs[*dst as usize] = NanBox::boolean(
ctx.realm
.strict_equals(regs[*a as usize], regs[*b as usize]),
);
}
Op::JumpIfFalse { cond, target } => {
if !ctx.realm.truthy(regs[*cond as usize]) {
pc = *target;
}
}
Op::Jump { target } => pc = *target,
Op::NewString { dst, value } => {
let handle = ctx.realm.new_string(value);
regs[*dst as usize] = NanBox::handle(handle.to_raw());
}
Op::NewArray { dst, len } => {
if *len > ctx.realm.limits.max_array_len {
let e = make_error(ctx.realm, "RangeError", "Array length too large");
return Err(VmError::Thrown(e));
}
let handle = ctx.realm.new_array(vec![NanBox::undefined(); *len]);
regs[*dst as usize] = NanBox::handle(handle.to_raw());
}
Op::NewArrayCtor { dst, arg } => {
let v = regs[*arg as usize];
let handle = if let Some(n) = v.as_number() {
if n < 0.0 || n > f64::from(u32::MAX) || n != f64::from(n as u32) {
let e = make_error(ctx.realm, "RangeError", "Invalid array length");
return Err(VmError::Thrown(e));
}
if n > ctx.realm.limits.max_array_len as f64 {
let e = make_error(ctx.realm, "RangeError", "Array length too large");
return Err(VmError::Thrown(e));
}
ctx.realm.new_array(vec![NanBox::undefined(); n as usize])
} else {
ctx.realm.new_array(vec![v])
};
regs[*dst as usize] = NanBox::handle(handle.to_raw());
}
Op::GetElem { dst, arr, index } => {
let handle = object_handle(regs[*arr as usize])?;
let i = num(regs[*index as usize])? as usize;
regs[*dst as usize] = ctx.realm.get_element(handle, i);
}
Op::SetElem { arr, index, src } => {
let handle = object_handle(regs[*arr as usize])?;
let i = num(regs[*index as usize])? as usize;
ctx.realm.set_element(handle, i, regs[*src as usize]);
}
Op::GetKey { dst, obj, key } => {
let handle = object_handle(regs[*obj as usize])?;
let k = regs[*key as usize];
regs[*dst as usize] = match k.as_number() {
Some(n) if ctx.realm.is_array(handle) => {
ctx.realm.get_element(handle, n as usize)
}
_ => {
let pk = to_primitive(ctx, funcs, k, false);
let ks = ctx.realm.to_display_string(pk);
if ctx.realm.is_array(handle)
&& let Ok(i) = ks.parse::<usize>()
&& alloc::format!("{i}") == ks
{
ctx.realm.get_element(handle, i)
} else if ks == "length"
&& let Some(len) = ctx.realm.array_length(handle).or_else(|| {
ctx.realm.string_value(handle).map(|s| s.chars().count())
})
{
NanBox::number(len as f64)
} else {
ctx.realm
.get_property(handle, &ks)
.unwrap_or(NanBox::undefined())
}
}
};
}
Op::SetKey { obj, key, src } => {
let handle = object_handle(regs[*obj as usize])?;
let k = regs[*key as usize];
match k.as_number() {
Some(n) if ctx.realm.is_array(handle) => {
ctx.realm
.set_element(handle, n as usize, regs[*src as usize]);
}
_ => {
let pk = to_primitive(ctx, funcs, k, false);
let ks = ctx.realm.to_display_string(pk);
ctx.realm.set_property(handle, &ks, regs[*src as usize]);
}
}
}
Op::EnumKeys { dst, obj } => {
let h = object_handle(regs[*obj as usize])?;
let mut seen = alloc::collections::BTreeSet::new();
let mut out = Vec::new();
if !ctx.realm.is_vm_function(h)
&& let Some(len) = ctx.realm.array_length(h)
{
for i in 0..len {
let k = alloc::format!("{i}");
if seen.insert(k.clone()) {
out.push(NanBox::handle(ctx.realm.new_string(&k).to_raw()));
}
}
}
let mut cur = Some(h);
while let Some(c) = cur {
let named = ctx
.realm
.object_keys(c)
.unwrap_or_else(|| ctx.realm.aux_named_keys(c));
for k in named {
if seen.insert(k.clone()) {
out.push(NanBox::handle(ctx.realm.new_string(&k).to_raw()));
}
}
cur = ctx.realm.object_proto(c);
}
let keys = out;
regs[*dst as usize] = NanBox::handle(ctx.realm.new_array(keys).to_raw());
}
Op::ObjectSpread { dst, src } => {
let target = object_handle(regs[*dst as usize])?;
if let Some(sh) = regs[*src as usize].as_handle().map(Handle::from_raw) {
if let Some(elems) = ctx.realm.array_elements(sh).map(<[_]>::to_vec) {
for (i, e) in elems.into_iter().enumerate() {
ctx.realm.set_property(target, &alloc::format!("{i}"), e);
}
} else {
for k in ctx.realm.object_keys(sh).unwrap_or_default() {
let v = ctx
.realm
.get_property(sh, &k)
.unwrap_or(NanBox::undefined());
ctx.realm.set_property(target, &k, v);
}
let recv = regs[*src as usize];
for k in ctx.realm.object_accessor_keys(sh) {
if let Some((getter, _)) = ctx.realm.accessor(sh, &k)
&& getter.as_handle().is_some()
{
let v = call_closure(ctx, funcs, getter, &[], recv)?;
ctx.realm.set_property(target, &k, v);
}
}
}
}
}
Op::ArrayLen { dst, arr } => {
let handle = object_handle(regs[*arr as usize])?;
if ctx.realm.is_vm_function(handle) {
let n = ctx
.realm
.get_element(handle, 0)
.as_number()
.and_then(|f| funcs.get(f as usize))
.map_or(0, |p| p.length);
regs[*dst as usize] = NanBox::number(n as f64);
} else {
let len = ctx
.realm
.array_length(handle)
.or_else(|| ctx.realm.string_value(handle).map(|s| s.chars().count()));
regs[*dst as usize] = match len {
Some(n) => NanBox::number(n as f64),
None => ctx
.realm
.get_property(handle, "length")
.unwrap_or(NanBox::undefined()),
};
}
}
Op::CollectionSize { dst, recv } => {
let h = object_handle(regs[*recv as usize])?;
let n = ctx
.realm
.collection_size(h)
.or_else(|| {
ctx.realm
.get_property(h, "size")
.and_then(|v| v.as_number().map(|n| n as usize))
})
.unwrap_or(0);
regs[*dst as usize] = NanBox::number(n as f64);
}
Op::ArrayPush { arr, src } => {
let handle = object_handle(regs[*arr as usize])?;
let len = ctx.realm.array_length(handle).unwrap_or(0);
ctx.realm.set_element(handle, len, regs[*src as usize]);
}
Op::ArrayExtend { arr, src } => {
let handle = object_handle(regs[*arr as usize])?;
let srch = object_handle(regs[*src as usize])?;
let elems = ctx
.realm
.array_elements(srch)
.map(<[_]>::to_vec)
.ok_or(VmError::NotAnObject)?;
let start = ctx.realm.array_length(handle).unwrap_or(0);
for (i, e) in elems.into_iter().enumerate() {
ctx.realm.set_element(handle, start + i, e);
}
}
Op::ObjectRest { dst, src, exclude } => {
let srch = object_handle(regs[*src as usize])?;
let new_obj = ctx.realm.new_object();
for k in ctx.realm.object_keys(srch).unwrap_or_default() {
if !exclude.contains(&k) {
let v = ctx
.realm
.get_property(srch, &k)
.unwrap_or(NanBox::undefined());
ctx.realm.set_property(new_obj, &k, v);
}
}
regs[*dst as usize] = NanBox::handle(new_obj.to_raw());
}
Op::NewCollection { dst, is_set, seed } => {
let coll = ctx.realm.new_collection(*is_set);
if let Some(seed) = seed
&& let Some(sh) = regs[*seed as usize].as_handle().map(Handle::from_raw)
{
let items = ctx
.realm
.array_elements(sh)
.map(<[_]>::to_vec)
.unwrap_or_default();
for item in items {
if *is_set {
ctx.realm.collection_set(coll, item, item);
} else if let Some(ih) = item.as_handle().map(Handle::from_raw) {
let k = ctx.realm.get_element(ih, 0);
let v = ctx.realm.get_element(ih, 1);
ctx.realm.collection_set(coll, k, v);
}
}
}
regs[*dst as usize] = NanBox::handle(coll.to_raw());
}
Op::ArraySliceFrom { dst, src, from } => {
let srch = object_handle(regs[*src as usize])?;
let from = num(regs[*from as usize])? as usize;
let rest: Vec<NanBox> = ctx
.realm
.array_elements(srch)
.map(|e| e.get(from..).map(<[_]>::to_vec).unwrap_or_default())
.unwrap_or_default();
regs[*dst as usize] = NanBox::handle(ctx.realm.new_array(rest).to_raw());
}
Op::NewRegExp { dst, source, flags } => {
let h = ctx.realm.new_regexp(source, flags);
regs[*dst as usize] = NanBox::handle(h.to_raw());
}
Op::NewObject { dst } => {
let handle = ctx.realm.new_object();
regs[*dst as usize] = NanBox::handle(handle.to_raw());
}
Op::SetProp { obj, key, src } => {
let handle = object_handle(regs[*obj as usize])?;
let recv = regs[*obj as usize];
if key.as_str() == "lastIndex" && ctx.realm.regexp_at(handle).is_some() {
let n = num(regs[*src as usize]).unwrap_or(0.0).max(0.0) as usize;
ctx.realm.set_regex_last_index(handle, n);
continue;
}
match ctx.realm.accessor(handle, key) {
Some((_, setter)) if setter.as_handle().is_some() => {
if let Err(e) =
call_closure(ctx, funcs, setter, &[regs[*src as usize]], recv)
{
handle_throw!(e);
}
}
_ => {
ctx.realm.set_property(handle, key, regs[*src as usize]);
}
}
}
Op::GetProp { dst, obj, key } => {
let recv = regs[*obj as usize];
match recv.as_handle().map(Handle::from_raw) {
None => {
use crate::nanbox::Unpacked;
match recv.unpack() {
Unpacked::Null | Unpacked::Undefined => {
let what = if matches!(recv.unpack(), Unpacked::Null) {
"null"
} else {
"undefined"
};
let e = make_error(
ctx.realm,
"TypeError",
&alloc::format!(
"Cannot read properties of {what} (reading '{key}')"
),
);
handle_throw!(VmError::Thrown(e));
}
_ => regs[*dst as usize] = NanBox::undefined(),
}
}
Some(handle) => {
if key.as_str() == "name"
&& ctx.realm.is_vm_function(handle)
&& !ctx.realm.has_own(handle, "name")
{
let nm = ctx
.realm
.get_element(handle, 0)
.as_number()
.and_then(|f| funcs.get(f as usize))
.map_or("", |p| p.name.as_str());
let s = ctx.realm.new_string(nm);
regs[*dst as usize] = NanBox::handle(s.to_raw());
continue;
}
let mut done = true;
if let Some((src, flags)) = ctx.realm.regexp_at(handle) {
match key.as_str() {
"source" => {
let s = ctx.realm.new_string(&src);
regs[*dst as usize] = NanBox::handle(s.to_raw());
}
"flags" => {
let s = ctx.realm.new_string(&flags);
regs[*dst as usize] = NanBox::handle(s.to_raw());
}
"global" => {
regs[*dst as usize] = NanBox::boolean(flags.contains('g'))
}
"ignoreCase" => {
regs[*dst as usize] = NanBox::boolean(flags.contains('i'));
}
"multiline" => {
regs[*dst as usize] = NanBox::boolean(flags.contains('m'));
}
"sticky" => {
regs[*dst as usize] = NanBox::boolean(flags.contains('y'))
}
"dotAll" => {
regs[*dst as usize] = NanBox::boolean(flags.contains('s'))
}
"unicode" => {
regs[*dst as usize] = NanBox::boolean(flags.contains('u'))
}
"hasIndices" => {
regs[*dst as usize] = NanBox::boolean(flags.contains('d'))
}
"lastIndex" => {
regs[*dst as usize] =
NanBox::number(ctx.realm.regex_last_index(handle) as f64);
}
_ => done = false,
}
} else {
done = false;
}
if !done {
match ctx.realm.accessor(handle, key) {
Some((getter, _)) if getter.as_handle().is_some() => {
match call_closure(ctx, funcs, getter, &[], recv) {
Ok(v) => regs[*dst as usize] = v,
Err(e) => handle_throw!(e),
}
}
_ => {
regs[*dst as usize] = ctx
.realm
.get_property(handle, key)
.unwrap_or(NanBox::undefined());
}
}
}
}
}
}
Op::Call { dst, func, args } => {
let argv: Vec<NanBox> = args.iter().map(|r| regs[*r as usize]).collect();
match call(ctx, funcs, *func as usize, &argv) {
Ok(ret) => regs[*dst as usize] = ret,
Err(VmError::Thrown(v)) => match handlers.pop() {
Some((target, reg)) => {
regs[reg as usize] = v;
pc = target;
}
None => return Err(VmError::Thrown(v)),
},
Err(other) => return Err(other),
}
}
Op::LoadFunc { dst, func } => {
let handle = ctx
.realm
.new_array(alloc::vec![NanBox::number(*func as f64)]);
ctx.realm
.set_hidden_property(handle, "\u{0}vmfn", NanBox::boolean(true));
regs[*dst as usize] = NanBox::handle(handle.to_raw());
}
Op::MakeClosure {
dst,
func,
captures,
} => {
let mut elems = alloc::vec![NanBox::number(*func as f64)];
elems.extend(captures.iter().map(|r| regs[*r as usize]));
let handle = ctx.realm.new_array(elems);
ctx.realm
.set_hidden_property(handle, "\u{0}vmfn", NanBox::boolean(true));
regs[*dst as usize] = NanBox::handle(handle.to_raw());
}
Op::CallValue { dst, callee, args } => {
let handle = object_handle(regs[*callee as usize])?;
let id = ctx
.realm
.get_element(handle, 0)
.as_number()
.ok_or(VmError::NotAnObject)? as usize;
let n_caps = ctx
.realm
.array_length(handle)
.unwrap_or(1)
.saturating_sub(1);
let caps: Vec<NanBox> = (0..n_caps)
.map(|i| ctx.realm.get_element(handle, i + 1))
.collect();
let argv: Vec<NanBox> = args.iter().map(|r| regs[*r as usize]).collect();
match call_with(ctx, funcs, id, &argv, &caps, NanBox::undefined()) {
Ok(ret) => regs[*dst as usize] = ret,
Err(VmError::Thrown(v)) => match handlers.pop() {
Some((target, reg)) => {
regs[reg as usize] = v;
pc = target;
}
None => return Err(VmError::Thrown(v)),
},
Err(other) => return Err(other),
}
}
Op::CallMethod {
dst,
recv,
key,
args,
} => {
let recv_val = regs[*recv as usize];
let argv: Vec<NanBox> = args.iter().map(|r| regs[*r as usize]).collect();
let user_method = recv_val
.as_handle()
.map(Handle::from_raw)
.and_then(|h| ctx.realm.get_property(h, key))
.filter(|p| p.as_handle().is_some());
let outcome = match user_method {
Some(closure) => call_closure(ctx, funcs, closure, &argv, recv_val),
None => match builtin_method(ctx, funcs, recv_val, key, &argv) {
Some(r) => r,
None => return Err(VmError::NotAnObject),
},
};
match outcome {
Ok(ret) => regs[*dst as usize] = ret,
Err(VmError::Thrown(v)) => match handlers.pop() {
Some((target, reg)) => {
regs[reg as usize] = v;
pc = target;
}
None => return Err(VmError::Thrown(v)),
},
Err(other) => return Err(other),
}
}
Op::CallValueThis {
dst,
callee,
recv,
args,
} => {
let closure = regs[*callee as usize];
let recv_val = regs[*recv as usize];
let argv: Vec<NanBox> = args.iter().map(|r| regs[*r as usize]).collect();
match call_closure(ctx, funcs, closure, &argv, recv_val) {
Ok(ret) => regs[*dst as usize] = ret,
Err(e) => handle_throw!(e),
}
}
Op::CallCtor { ctor, recv, args } => {
let recv_val = regs[*recv as usize];
let argv: Vec<NanBox> = args.iter().map(|r| regs[*r as usize]).collect();
match call_with(ctx, funcs, *ctor as usize, &argv, &[], recv_val) {
Ok(_) => {}
Err(VmError::Thrown(v)) => match handlers.pop() {
Some((target, reg)) => {
regs[reg as usize] = v;
pc = target;
}
None => return Err(VmError::Thrown(v)),
},
Err(other) => return Err(other),
}
}
Op::CallNative { dst, native, args } => {
let argv: Vec<NanBox> = args.iter().map(|r| regs[*r as usize]).collect();
if *native == NB_JSON_STRINGIFY {
match json_stringify(ctx, funcs, &argv) {
Ok(v) => regs[*dst as usize] = v,
Err(e) => handle_throw!(e),
}
} else if *native == NB_JSON_PARSE {
match json_parse(ctx, funcs, &argv) {
Ok(v) => regs[*dst as usize] = v,
Err(e) => handle_throw!(e),
}
} else {
regs[*dst as usize] = call_native(ctx, *native, &argv);
}
}
Op::PushHandler { target, reg } => {
if handlers.len() >= ctx.realm.limits.max_handler_depth {
let e = make_error(ctx.realm, "RangeError", "Handler stack overflow");
handle_throw!(VmError::Thrown(e));
} else {
handlers.push((*target, *reg));
}
}
Op::PopHandler => {
handlers.pop();
}
Op::Throw { src } => {
let v = regs[*src as usize];
match handlers.pop() {
Some((target, reg)) => {
regs[reg as usize] = v;
pc = target;
}
None => return Err(VmError::Thrown(v)),
}
}
Op::Return { src } => return Ok(Some(regs[*src as usize])),
}
}
Ok(None)
}
fn json_stringify(ctx: &mut Ctx, funcs: &[FnProto], args: &[NanBox]) -> Result<NanBox, VmError> {
let v = args.first().copied().unwrap_or(NanBox::undefined());
let space = args.get(2).copied().unwrap_or(NanBox::undefined());
let indent: alloc::string::String = if let Some(n) = space.as_number() {
" ".repeat((n.max(0.0) as usize).min(10))
} else if let Some(s) = space
.as_handle()
.and_then(|r| ctx.realm.string_value(Handle::from_raw(r)))
{
s.chars().take(10).collect()
} else {
alloc::string::String::new()
};
let replacer = args.get(1).copied().unwrap_or(NanBox::undefined());
let (repl_fn, allow): (Option<NanBox>, Option<Vec<alloc::string::String>>) =
match replacer.as_handle().map(Handle::from_raw) {
Some(rh) if ctx.realm.is_vm_function(rh) => (Some(replacer), None),
Some(rh) if ctx.realm.is_array(rh) => {
let a = ctx
.realm
.array_elements(rh)
.map(<[_]>::to_vec)
.unwrap_or_default()
.iter()
.map(|e| ctx.realm.to_display_string(*e))
.collect();
(None, Some(a))
}
_ => (None, None),
};
let holder = ctx.realm.new_object();
ctx.realm.set_property(holder, "", v);
let mut seen = Vec::new();
let v = json_normalize(
ctx,
funcs,
NanBox::handle(holder.to_raw()),
"",
v,
repl_fn,
allow.as_deref(),
&mut seen,
0,
)?;
let result = if indent.is_empty() {
crate::json::stringify(ctx.realm, v)
} else {
crate::json::stringify_pretty(ctx.realm, v, &indent)
};
Ok(match result {
Some(s) => NanBox::handle(ctx.realm.new_string(&s).to_raw()),
None => NanBox::undefined(),
})
}
fn json_parse(ctx: &mut Ctx, funcs: &[FnProto], args: &[NanBox]) -> Result<NanBox, VmError> {
let s = ctx
.realm
.to_display_string(args.first().copied().unwrap_or(NanBox::undefined()));
let value = match crate::json::parse(ctx.realm, &s) {
Ok(v) => v,
Err(msg) => return Err(VmError::Thrown(make_error(ctx.realm, "SyntaxError", &msg))),
};
let reviver = args.get(1).copied().unwrap_or(NanBox::undefined());
if reviver
.as_handle()
.map(Handle::from_raw)
.is_some_and(|r| ctx.realm.is_vm_function(r))
{
let holder = ctx.realm.new_object();
ctx.realm.set_property(holder, "", value);
return json_revive(ctx, funcs, holder, "", reviver, 0);
}
Ok(value)
}
fn json_revive(
ctx: &mut Ctx,
funcs: &[FnProto],
holder: Handle,
key: &str,
reviver: NanBox,
depth: usize,
) -> Result<NanBox, VmError> {
if depth >= ctx.realm.limits.max_json_depth {
return Err(VmError::Thrown(make_error(
ctx.realm,
"RangeError",
"Maximum JSON nesting depth exceeded",
)));
}
let value = if ctx.realm.is_array(holder)
&& let Ok(i) = key.parse::<usize>()
{
ctx.realm.get_element(holder, i)
} else {
ctx.realm
.get_property(holder, key)
.unwrap_or(NanBox::undefined())
};
if let Some(vh) = value.as_handle().map(Handle::from_raw) {
if ctx.realm.is_array(vh) {
let len = ctx.realm.array_length(vh).unwrap_or(0);
for i in 0..len {
let ks = alloc::format!("{i}");
let nv = json_revive(ctx, funcs, vh, &ks, reviver, depth + 1)?;
ctx.realm.set_element(vh, i, nv);
}
} else if let Some(keys) = ctx.realm.object_keys(vh) {
for k in keys {
let nv = json_revive(ctx, funcs, vh, &k, reviver, depth + 1)?;
if matches!(nv.unpack(), crate::nanbox::Unpacked::Undefined) {
ctx.realm.delete_property(vh, &k);
} else {
ctx.realm.set_property(vh, &k, nv);
}
}
}
}
let kb = NanBox::handle(ctx.realm.new_string(key).to_raw());
call_closure(
ctx,
funcs,
reviver,
&[kb, value],
NanBox::handle(holder.to_raw()),
)
}
fn json_read_prop(
ctx: &mut Ctx,
funcs: &[FnProto],
h: Handle,
key: &str,
) -> Result<NanBox, VmError> {
if let Some((getter, _)) = ctx.realm.accessor(h, key) {
if getter
.as_handle()
.map(Handle::from_raw)
.is_some_and(|r| ctx.realm.is_vm_function(r))
{
return call_closure(ctx, funcs, getter, &[], NanBox::handle(h.to_raw()));
}
return Ok(NanBox::undefined());
}
Ok(ctx
.realm
.get_property(h, key)
.unwrap_or(NanBox::undefined()))
}
#[allow(clippy::too_many_arguments)]
fn json_normalize(
ctx: &mut Ctx,
funcs: &[FnProto],
holder: NanBox,
key: &str,
value: NanBox,
replacer: Option<NanBox>,
allow: Option<&[alloc::string::String]>,
seen: &mut Vec<Handle>,
depth: usize,
) -> Result<NanBox, VmError> {
if depth >= ctx.realm.limits.max_json_depth {
return Err(VmError::Thrown(make_error(
ctx.realm,
"RangeError",
"Maximum JSON nesting depth exceeded",
)));
}
let mut v = value;
if let Some(h) = v.as_handle().map(Handle::from_raw)
&& ctx.realm.string_value(h).is_none()
&& ctx.realm.bigint_at(h).is_none()
&& ctx.realm.date_at(h).is_none()
{
let tj = json_read_prop(ctx, funcs, h, "toJSON")?;
if tj
.as_handle()
.map(Handle::from_raw)
.is_some_and(|r| ctx.realm.is_vm_function(r))
{
let kb = NanBox::handle(ctx.realm.new_string(key).to_raw());
v = call_closure(ctx, funcs, tj, &[kb], v)?;
}
}
if let Some(rf) = replacer {
let kb = NanBox::handle(ctx.realm.new_string(key).to_raw());
v = call_closure(ctx, funcs, rf, &[kb, v], holder)?;
}
if let Some(h) = v.as_handle().map(Handle::from_raw)
&& ctx.realm.string_value(h).is_none()
&& !ctx.realm.is_vm_function(h)
&& ctx.realm.date_at(h).is_none()
{
if let Some(elems) = ctx.realm.array_elements(h).map(<[_]>::to_vec) {
if seen.contains(&h) {
return Err(VmError::Thrown(make_error(
ctx.realm,
"TypeError",
"Converting circular structure to JSON",
)));
}
seen.push(h);
let mut out = Vec::with_capacity(elems.len());
for (i, e) in elems.into_iter().enumerate() {
let kk = alloc::format!("{i}");
out.push(json_normalize(
ctx,
funcs,
v,
&kk,
e,
replacer,
allow,
seen,
depth + 1,
)?);
}
seen.pop();
return Ok(NanBox::handle(ctx.realm.new_array(out).to_raw()));
}
if let Some(keys) = ctx.realm.object_keys(h) {
if seen.contains(&h) {
return Err(VmError::Thrown(make_error(
ctx.realm,
"TypeError",
"Converting circular structure to JSON",
)));
}
seen.push(h);
let key_list: Vec<alloc::string::String> = match allow {
Some(a) => {
let mut ks: Vec<alloc::string::String> = Vec::new();
for k in a {
if keys.contains(k) && !ks.contains(k) {
ks.push(k.clone());
}
}
ks
}
None => keys,
};
let new_obj = ctx.realm.new_object();
for k in key_list {
let pv = json_read_prop(ctx, funcs, h, &k)?;
let nv = json_normalize(ctx, funcs, v, &k, pv, replacer, allow, seen, depth + 1)?;
ctx.realm.set_property(new_obj, &k, nv);
}
seen.pop();
return Ok(NanBox::handle(new_obj.to_raw()));
}
}
Ok(v)
}
fn call_closure(
ctx: &mut Ctx,
funcs: &[FnProto],
closure: NanBox,
args: &[NanBox],
this_val: NanBox,
) -> Result<NanBox, VmError> {
let fh = closure
.as_handle()
.map(Handle::from_raw)
.ok_or(VmError::NotAnObject)?;
let id = ctx
.realm
.get_element(fh, 0)
.as_number()
.ok_or(VmError::NotAnObject)? as usize;
let n_caps = ctx.realm.array_length(fh).unwrap_or(1).saturating_sub(1);
let caps: Vec<NanBox> = (0..n_caps)
.map(|i| ctx.realm.get_element(fh, i + 1))
.collect();
call_with(ctx, funcs, id, args, &caps, this_val)
}
#[cfg(feature = "regex")]
fn char_substr(s: &str, st: usize, en: usize) -> String {
s.chars().skip(st).take(en.saturating_sub(st)).collect()
}
#[cfg(feature = "regex")]
fn char_substr_from(s: &str, st: usize) -> String {
s.chars().skip(st).collect()
}
#[cfg(feature = "regex")]
fn regex_match_object(
realm: &mut Realm,
text: &str,
caps: &crate::regex::Captures,
group_names: &[(usize, alloc::string::String)],
) -> NanBox {
let elems: Vec<NanBox> = caps
.groups
.iter()
.map(|g| match g {
Some((s, e)) => NanBox::handle(realm.new_string(&char_substr(text, *s, *e)).to_raw()),
None => NanBox::undefined(),
})
.collect();
let obj = realm.new_array(elems);
let index = caps.groups.first().and_then(|g| *g).map_or(0, |(s, _)| s);
realm.set_property(obj, "index", NanBox::number(index as f64));
let input = NanBox::handle(realm.new_string(text).to_raw());
realm.set_property(obj, "input", input);
let groups = if group_names.is_empty() {
NanBox::undefined()
} else {
let g = realm.new_object();
for (idx, name) in group_names {
let v = match caps.groups.get(*idx).and_then(|x| *x) {
Some((s, e)) => NanBox::handle(realm.new_string(&char_substr(text, s, e)).to_raw()),
None => NanBox::undefined(),
};
realm.set_property(g, name, v);
}
NanBox::handle(g.to_raw())
};
realm.set_property(obj, "groups", groups);
NanBox::handle(obj.to_raw())
}
#[cfg(feature = "regex")]
fn regex_method(
ctx: &mut Ctx,
recv: NanBox,
key: &str,
args: &[NanBox],
) -> Option<Result<NanBox, VmError>> {
use crate::regex::Regex;
let h = recv.as_handle().map(Handle::from_raw)?;
let arg0 = args.first().copied().unwrap_or(NanBox::undefined());
if let Some((source, flags)) = ctx.realm.regexp_at(h) {
if !matches!(key, "test" | "exec") {
return None;
}
let text = ctx.realm.to_display_string(arg0);
let Ok(re) = Regex::new(&source, &flags) else {
return Some(Ok(NanBox::null()));
};
let stateful = flags.contains('g') || flags.contains('y');
let start = if stateful {
ctx.realm.regex_last_index(h)
} else {
0
};
let caps = re.captures_from(&text, start);
if stateful {
let next = caps.as_ref().map_or(0, |c| c.whole().1);
ctx.realm.set_regex_last_index(h, next);
}
return Some(Ok(match (key, caps) {
("test", c) => NanBox::boolean(c.is_some()),
(_, Some(caps)) => regex_match_object(ctx.realm, &text, &caps, re.group_names()),
(_, None) => NanBox::null(),
}));
}
let text = ctx.realm.string_value(h)?;
if !matches!(key, "match" | "replace" | "replaceAll" | "split" | "search") {
return None;
}
let (src, flags) = arg0
.as_handle()
.map(Handle::from_raw)
.and_then(|rh| ctx.realm.regexp_at(rh))?;
let Ok(re) = Regex::new(&src, &flags) else {
return Some(Ok(NanBox::null()));
};
let global = flags.contains('g');
if !global && key == "replaceAll" {
return Some(Err(VmError::Thrown(make_error(
ctx.realm,
"TypeError",
"replaceAll must be called with a global RegExp",
))));
}
let chars: Vec<char> = text.chars().collect();
let result = match key {
"search" => {
let i = re.find_in(&chars, 0).map_or(-1.0, |(s, _)| s as f64);
NanBox::number(i)
}
"match" if !global => match re.captures_in(&chars, 0) {
Some(caps) => regex_match_object(ctx.realm, &text, &caps, re.group_names()),
None => NanBox::null(),
},
"match" => {
let mut out = Vec::new();
let mut pos = 0;
while let Some((s, e)) = re.find_in(&chars, pos) {
out.push(NanBox::handle(
ctx.realm
.new_string(&chars[s..e].iter().collect::<String>())
.to_raw(),
));
pos = if e > s { e } else { e + 1 };
}
if out.is_empty() {
NanBox::null()
} else {
NanBox::handle(ctx.realm.new_array(out).to_raw())
}
}
"replace" | "replaceAll" => {
let repl_val = args.get(1).copied().unwrap_or(NanBox::undefined());
if repl_val
.as_handle()
.is_some_and(|raw| ctx.realm.string_value(Handle::from_raw(raw)).is_none())
{
return None;
}
let repl = ctx.realm.to_display_string(repl_val);
NanBox::handle(ctx.realm.new_string(&re.replace(&text, &repl)).to_raw())
}
_ => {
let limit = match args.get(1) {
Some(a) if !matches!(a.unpack(), crate::nanbox::Unpacked::Undefined) => {
let n = ctx.realm.to_number(*a);
if n >= 0.0 { Some(n as usize) } else { None }
}
_ => None,
};
let mut out = Vec::new();
let mut seg_start = 0;
let mut search = 0;
while search < chars.len() && limit.is_none_or(|l| out.len() < l) {
let Some(caps) = re.captures_in(&chars, search) else {
break;
};
let Some((st, en)) = caps.groups[0] else {
break;
};
if en == seg_start {
if chars.get(search).is_some() {
search = search.max(st) + 1;
continue;
}
break;
}
out.push(NanBox::handle(
ctx.realm
.new_string(&chars[seg_start..st].iter().collect::<String>())
.to_raw(),
));
for g in &caps.groups[1..] {
out.push(match g {
Some((gs, ge)) => NanBox::handle(
ctx.realm
.new_string(&chars[*gs..*ge].iter().collect::<String>())
.to_raw(),
),
None => NanBox::undefined(),
});
}
seg_start = en;
search = if en > st { en } else { en + 1 };
}
if limit.is_none_or(|l| out.len() < l) {
out.push(NanBox::handle(
ctx.realm
.new_string(&char_substr_from(&text, seg_start))
.to_raw(),
));
}
if let Some(l) = limit {
out.truncate(l);
}
NanBox::handle(ctx.realm.new_array(out).to_raw())
}
};
Some(Ok(result))
}
fn builtin_method(
ctx: &mut Ctx,
funcs: &[FnProto],
recv: NanBox,
key: &str,
args: &[NanBox],
) -> Option<Result<NanBox, VmError>> {
use crate::nanbox::Unpacked;
let h = recv.as_handle().map(Handle::from_raw)?;
let arg0 = || args.first().copied().unwrap_or(NanBox::undefined());
#[cfg(feature = "regex")]
if let Some(r) = regex_method(ctx, recv, key, args) {
return Some(r);
}
if ctx.realm.is_array(h) {
let elems = |ctx: &Ctx| {
ctx.realm
.array_elements(h)
.map(<[_]>::to_vec)
.unwrap_or_default()
};
let result = match key {
"push" => {
let mut len = ctx.realm.array_length(h).unwrap_or(0);
for a in args {
ctx.realm.set_element(h, len, *a);
len += 1;
}
NanBox::number(len as f64)
}
"pop" => ctx.realm.array_pop(h),
"join" => {
let sep = if matches!(arg0().unpack(), Unpacked::Undefined) {
String::from(",")
} else {
ctx.realm.to_display_string(arg0())
};
let parts: Vec<String> = elems(ctx)
.iter()
.map(|e| match e.unpack() {
Unpacked::Undefined | Unpacked::Null => String::new(),
Unpacked::Handle(raw) if raw == h.to_raw() => String::new(),
_ => ctx.realm.to_display_string(*e),
})
.collect();
NanBox::handle(ctx.realm.new_string(&parts.join(&sep)).to_raw())
}
"includes" => {
let t = arg0();
let t_nan = t.as_number().is_some_and(f64::is_nan);
NanBox::boolean(elems(ctx).iter().any(|e| {
ctx.realm.strict_equals(*e, t)
|| (t_nan && e.as_number().is_some_and(f64::is_nan))
}))
}
"indexOf" => {
let t = arg0();
let i = elems(ctx)
.iter()
.position(|e| ctx.realm.strict_equals(*e, t));
NanBox::number(i.map_or(-1.0, |i| i as f64))
}
"map" => {
let f = arg0();
let mut out = Vec::new();
for (i, e) in elems(ctx).iter().enumerate() {
match call_closure(
ctx,
funcs,
f,
&[*e, NanBox::number(i as f64)],
NanBox::undefined(),
) {
Ok(v) => out.push(v),
Err(e) => return Some(Err(e)),
}
}
NanBox::handle(ctx.realm.new_array(out).to_raw())
}
"filter" => {
let f = arg0();
let mut out = Vec::new();
for (i, e) in elems(ctx).iter().enumerate() {
match call_closure(
ctx,
funcs,
f,
&[*e, NanBox::number(i as f64)],
NanBox::undefined(),
) {
Ok(v) if ctx.realm.truthy(v) => out.push(*e),
Ok(_) => {}
Err(e) => return Some(Err(e)),
}
}
NanBox::handle(ctx.realm.new_array(out).to_raw())
}
"forEach" => {
let f = arg0();
for (i, e) in elems(ctx).iter().enumerate() {
if let Err(e) = call_closure(
ctx,
funcs,
f,
&[*e, NanBox::number(i as f64)],
NanBox::undefined(),
) {
return Some(Err(e));
}
}
NanBox::undefined()
}
"reduce" => {
let f = arg0();
let mut acc = args.get(1).copied();
if acc.is_none() && elems(ctx).is_empty() {
let e = make_error(
ctx.realm,
"TypeError",
"Reduce of empty array with no initial value",
);
return Some(Err(VmError::Thrown(e)));
}
for (i, e) in elems(ctx).iter().enumerate() {
match acc {
None => acc = Some(*e), Some(a) => match call_closure(
ctx,
funcs,
f,
&[a, *e, NanBox::number(i as f64)],
NanBox::undefined(),
) {
Ok(v) => acc = Some(v),
Err(e) => return Some(Err(e)),
},
}
}
acc.unwrap_or(NanBox::undefined())
}
"find" => {
let f = arg0();
let mut found = NanBox::undefined();
for (i, e) in elems(ctx).iter().enumerate() {
match call_closure(
ctx,
funcs,
f,
&[*e, NanBox::number(i as f64)],
NanBox::undefined(),
) {
Ok(v) if ctx.realm.truthy(v) => {
found = *e;
break;
}
Ok(_) => {}
Err(e) => return Some(Err(e)),
}
}
found
}
"some" | "every" => {
let want_all = key == "every";
let f = arg0();
let mut acc = want_all;
for (i, e) in elems(ctx).iter().enumerate() {
let ok = match call_closure(
ctx,
funcs,
f,
&[*e, NanBox::number(i as f64)],
NanBox::undefined(),
) {
Ok(v) => ctx.realm.truthy(v),
Err(e) => return Some(Err(e)),
};
if want_all && !ok {
acc = false;
break;
}
if !want_all && ok {
acc = true;
break;
}
}
NanBox::boolean(acc)
}
"concat" => {
let mut out = elems(ctx);
for a in args {
match a.as_handle().map(Handle::from_raw) {
Some(ah) if ctx.realm.is_array(ah) => out.extend(
ctx.realm
.array_elements(ah)
.map(<[_]>::to_vec)
.unwrap_or_default(),
),
_ => out.push(*a),
}
}
NanBox::handle(ctx.realm.new_array(out).to_raw())
}
"reverse" => {
let mut out = elems(ctx);
out.reverse();
NanBox::handle(ctx.realm.new_array(out).to_raw())
}
_ => return None,
};
return Some(Ok(result));
}
if let Some(s) = ctx.realm.string_value(h) {
let result = match key {
"toUpperCase" => NanBox::handle(ctx.realm.new_string(&s.to_uppercase()).to_raw()),
"toLowerCase" => NanBox::handle(ctx.realm.new_string(&s.to_lowercase()).to_raw()),
"trim" => NanBox::handle(ctx.realm.new_string(s.trim()).to_raw()),
"includes" => NanBox::boolean(s.contains(&ctx.realm.to_display_string(arg0()))),
"startsWith" => NanBox::boolean(s.starts_with(&ctx.realm.to_display_string(arg0()))),
"endsWith" => NanBox::boolean(s.ends_with(&ctx.realm.to_display_string(arg0()))),
"indexOf" => {
let needle = ctx.realm.to_display_string(arg0());
let i = s.find(&needle).map(|b| s[..b].chars().count());
NanBox::number(i.map_or(-1.0, |i| i as f64))
}
"repeat" => {
let nf = ctx.realm.to_number(arg0());
let n = nf as usize;
let total = n.checked_mul(s.len());
if nf < 0.0
|| !nf.is_finite()
|| total.is_none_or(|t| t > ctx.realm.limits.max_string_len)
{
let e = make_error(ctx.realm, "RangeError", "Invalid string length");
return Some(Err(VmError::Thrown(e)));
}
NanBox::handle(ctx.realm.new_string(&s.repeat(n)).to_raw())
}
"charAt" => {
let n = ctx.realm.to_number(arg0());
let n = if n.is_nan() { 0.0 } else { n };
let c = if n >= 0.0 {
s.chars()
.nth(n as usize)
.map(String::from)
.unwrap_or_default()
} else {
String::new()
};
NanBox::handle(ctx.realm.new_string(&c).to_raw())
}
"split" => {
let sep = ctx.realm.to_display_string(arg0());
let mut parts: Vec<NanBox> = if sep.is_empty() {
s.chars()
.map(|c| NanBox::handle(ctx.realm.new_string(&String::from(c)).to_raw()))
.collect()
} else {
s.split(&sep)
.map(|p| NanBox::handle(ctx.realm.new_string(p).to_raw()))
.collect()
};
if let Some(lim) = args.get(1)
&& !matches!(lim.unpack(), Unpacked::Undefined)
{
let limit = ctx.realm.to_number(*lim);
if limit >= 0.0 {
parts.truncate(limit as usize);
}
}
NanBox::handle(ctx.realm.new_array(parts).to_raw())
}
_ => return None,
};
return Some(Ok(result));
}
if ctx.realm.promise_state(h).is_some() {
let result = match key {
"then" => promise_then(
ctx,
h,
arg0(),
args.get(1).copied().unwrap_or(NanBox::undefined()),
),
"catch" => promise_then(ctx, h, NanBox::undefined(), arg0()),
"finally" => promise_then(ctx, h, arg0(), arg0()),
_ => return None,
};
return Some(Ok(NanBox::handle(result.to_raw())));
}
if let Some(is_set) = ctx.realm.collection_is_set(h) {
let result = match key {
"set" if !is_set => {
let v = args.get(1).copied().unwrap_or(NanBox::undefined());
ctx.realm.collection_set(h, arg0(), v);
recv }
"add" if is_set => {
ctx.realm.collection_set(h, arg0(), arg0());
recv
}
"get" => ctx
.realm
.collection_get(h, arg0())
.unwrap_or(NanBox::undefined()),
"has" => NanBox::boolean(ctx.realm.collection_has(h, arg0())),
"delete" => NanBox::boolean(ctx.realm.collection_delete(h, arg0())),
"forEach" => {
let f = arg0();
for (k, v) in ctx.realm.collection_entries(h).unwrap_or_default() {
if let Err(e) = call_closure(ctx, funcs, f, &[v, k], NanBox::undefined()) {
return Some(Err(e));
}
}
NanBox::undefined()
}
"keys" | "values" | "entries" => {
let entries = ctx.realm.collection_entries(h).unwrap_or_default();
let items: Vec<NanBox> = entries
.into_iter()
.map(|(k, v)| match key {
"keys" => k,
"values" => v,
_ => NanBox::handle(ctx.realm.new_array(alloc::vec![k, v]).to_raw()),
})
.collect();
NanBox::handle(ctx.realm.new_array(items).to_raw())
}
_ => return None,
};
return Some(Ok(result));
}
None
}
fn call_native(ctx: &mut Ctx, native: u16, args: &[NanBox]) -> NanBox {
match native {
NB_CONSOLE_LOG => {
let line: Vec<String> = args
.iter()
.map(|a| ctx.realm.to_display_string(*a))
.collect();
ctx.output.push_str(&line.join(" "));
ctx.output.push('\n');
NanBox::undefined()
}
NB_MATH_MAX | NB_MATH_MIN | NB_MATH_ABS => {
let mut nums = args.iter().filter_map(|a| a.as_number());
let val = match native {
NB_MATH_ABS => nums.next().map(f64::abs).unwrap_or(f64::NAN),
NB_MATH_MAX => nums.fold(f64::NEG_INFINITY, f64::max),
_ => nums.fold(f64::INFINITY, f64::min),
};
NanBox::number(val)
}
#[cfg(feature = "std")]
NB_MATH_FLOOR | NB_MATH_CEIL | NB_MATH_ROUND | NB_MATH_SQRT | NB_MATH_POW
| NB_MATH_TRUNC => {
let a = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
let val = match native {
NB_MATH_FLOOR => a.floor(),
NB_MATH_CEIL => a.ceil(),
NB_MATH_ROUND => crate::common::js_round(a),
NB_MATH_SQRT => a.sqrt(),
NB_MATH_TRUNC => a.trunc(),
_ => a.powf(args.get(1).and_then(|v| v.as_number()).unwrap_or(f64::NAN)),
};
NanBox::number(val)
}
#[cfg(not(feature = "std"))]
NB_MATH_FLOOR | NB_MATH_CEIL | NB_MATH_ROUND | NB_MATH_SQRT | NB_MATH_POW
| NB_MATH_TRUNC => NanBox::number(f64::NAN),
NB_STRING => {
let s = ctx
.realm
.to_display_string(args.first().copied().unwrap_or(NanBox::undefined()));
NanBox::handle(ctx.realm.new_string(&s).to_raw())
}
NB_NUMBER => NanBox::number(
ctx.realm
.to_number(args.first().copied().unwrap_or(NanBox::undefined())),
),
NB_PARSE_INT => {
let s = ctx
.realm
.to_display_string(args.first().copied().unwrap_or(NanBox::undefined()));
let radix = args
.get(1)
.and_then(|r| r.as_number())
.filter(|n| n.is_finite())
.map_or(0i64, |n| n as i64);
if radix != 0 && !(2..=36).contains(&radix) {
NanBox::number(f64::NAN)
} else {
NanBox::number(parse_int(s.trim(), radix as u32))
}
}
NB_PARSE_FLOAT => {
let s = ctx
.realm
.to_display_string(args.first().copied().unwrap_or(NanBox::undefined()));
NanBox::number(parse_float_prefix(s.trim()))
}
NB_IS_NAN => NanBox::boolean(
ctx.realm
.to_number(args.first().copied().unwrap_or(NanBox::undefined()))
.is_nan(),
),
NB_IS_FINITE => NanBox::boolean(
ctx.realm
.to_number(args.first().copied().unwrap_or(NanBox::undefined()))
.is_finite(),
),
NB_OBJECT_KEYS | NB_OBJECT_VALUES | NB_OBJECT_ENTRIES => {
let h = args
.first()
.and_then(|a| a.as_handle())
.map(Handle::from_raw);
let mut pairs: Vec<(String, NanBox)> = Vec::new();
if let Some(h) = h {
if !ctx.realm.is_vm_function(h)
&& let Some(elems) = ctx.realm.array_elements(h).map(<[_]>::to_vec)
{
for (i, v) in elems.into_iter().enumerate() {
pairs.push((alloc::format!("{i}"), v));
}
}
let named = ctx
.realm
.object_keys(h)
.unwrap_or_else(|| ctx.realm.aux_named_keys(h));
for k in named {
let v = ctx.realm.get_property(h, &k).unwrap_or(NanBox::undefined());
pairs.push((k, v));
}
}
let mut out = Vec::with_capacity(pairs.len());
for (k, v) in pairs {
let item = match native {
NB_OBJECT_KEYS => NanBox::handle(ctx.realm.new_string(&k).to_raw()),
NB_OBJECT_VALUES => v,
_ => {
let key = NanBox::handle(ctx.realm.new_string(&k).to_raw());
NanBox::handle(ctx.realm.new_array(alloc::vec![key, v]).to_raw())
}
};
out.push(item);
}
NanBox::handle(ctx.realm.new_array(out).to_raw())
}
NB_JSON_STRINGIFY => {
let v = args.first().copied().unwrap_or(NanBox::undefined());
let space = args.get(2).copied().unwrap_or(NanBox::undefined());
let indent: String = if let Some(n) = space.as_number() {
" ".repeat((n.max(0.0) as usize).min(10))
} else if let Some(s) = space
.as_handle()
.and_then(|r| ctx.realm.string_value(Handle::from_raw(r)))
{
s.chars().take(10).collect()
} else {
String::new()
};
let result = if indent.is_empty() {
crate::json::stringify(ctx.realm, v)
} else {
crate::json::stringify_pretty(ctx.realm, v, &indent)
};
match result {
Some(s) => NanBox::handle(ctx.realm.new_string(&s).to_raw()),
None => NanBox::undefined(),
}
}
NB_JSON_PARSE => {
let s = ctx
.realm
.to_display_string(args.first().copied().unwrap_or(NanBox::undefined()));
crate::json::parse(ctx.realm, &s).unwrap_or(NanBox::undefined())
}
NB_NUMBER_IS_INTEGER => {
let v = args.first().copied().unwrap_or(NanBox::undefined());
NanBox::boolean(
v.as_number()
.is_some_and(|n| n.is_finite() && n % 1.0 == 0.0),
)
}
NB_NUMBER_IS_FINITE => {
let v = args.first().copied().unwrap_or(NanBox::undefined());
NanBox::boolean(v.as_number().is_some_and(f64::is_finite))
}
NB_NUMBER_IS_NAN => {
let v = args.first().copied().unwrap_or(NanBox::undefined());
NanBox::boolean(v.as_number().is_some_and(f64::is_nan))
}
NB_NUMBER_PARSE_FLOAT => {
let s = ctx
.realm
.to_display_string(args.first().copied().unwrap_or(NanBox::undefined()));
NanBox::number(parse_float_prefix(s.trim()))
}
NB_NUMBER_PARSE_INT => {
let s = ctx
.realm
.to_display_string(args.first().copied().unwrap_or(NanBox::undefined()));
let radix = args
.get(1)
.and_then(|r| r.as_number())
.filter(|n| n.is_finite())
.map_or(0i64, |n| n as i64);
if radix != 0 && !(2..=36).contains(&radix) {
NanBox::number(f64::NAN)
} else {
NanBox::number(parse_int(s.trim(), radix as u32))
}
}
NB_STRING_FROM_CHAR_CODE => {
let units: Vec<u16> = args
.iter()
.map(|a| {
let n = ctx.realm.to_number(*a);
if n.is_finite() {
(n as i64).rem_euclid(65536) as u16
} else {
0
}
})
.collect();
let s: String = char::decode_utf16(units)
.map(|r| r.unwrap_or('\u{FFFD}'))
.collect();
NanBox::handle(ctx.realm.new_string(&s).to_raw())
}
NB_ARRAY_IS_ARRAY => {
let yes = args
.first()
.and_then(|a| a.as_handle())
.map(Handle::from_raw)
.is_some_and(|h| ctx.realm.is_array(h) && !ctx.realm.is_vm_function(h));
NanBox::boolean(yes)
}
NB_ARRAY_FROM => {
let arg = args.first().copied().unwrap_or(NanBox::undefined());
let items: Vec<NanBox> = match arg.as_handle().map(Handle::from_raw) {
Some(h) if ctx.realm.is_array(h) => ctx
.realm
.array_elements(h)
.map(<[_]>::to_vec)
.unwrap_or_default(),
Some(h) if ctx.realm.collection_is_set(h).is_some() => ctx
.realm
.collection_entries(h)
.unwrap_or_default()
.into_iter()
.map(|(k, _)| k)
.collect(),
Some(h) if ctx.realm.string_value(h).is_some() => ctx
.realm
.string_value(h)
.unwrap_or_default()
.chars()
.map(|c| NanBox::handle(ctx.realm.new_string(&String::from(c)).to_raw()))
.collect(),
Some(h) => {
let len = ctx
.realm
.get_property(h, "length")
.map(|v| ctx.realm.to_number(v))
.unwrap_or(0.0)
.max(0.0) as usize;
(0..len)
.map(|i| {
ctx.realm
.get_property(h, &alloc::format!("{i}"))
.unwrap_or(NanBox::undefined())
})
.collect()
}
None => Vec::new(),
};
NanBox::handle(ctx.realm.new_array(items).to_raw())
}
NB_PROMISE_RESOLVE | NB_PROMISE_REJECT => {
let p = ctx.realm.new_promise();
let v = args.first().copied().unwrap_or(NanBox::undefined());
settle(ctx, p, v, native == NB_PROMISE_RESOLVE);
NanBox::handle(p.to_raw())
}
NB_OBJECT_FROM_ENTRIES => {
let obj = ctx.realm.new_object();
if let Some(h) = args
.first()
.and_then(|a| a.as_handle())
.map(Handle::from_raw)
{
for pair in ctx
.realm
.array_elements(h)
.map(<[_]>::to_vec)
.unwrap_or_default()
{
if let Some(ph) = pair.as_handle().map(Handle::from_raw) {
let k = ctx.realm.to_display_string(ctx.realm.get_element(ph, 0));
let v = ctx.realm.get_element(ph, 1);
ctx.realm.set_property(obj, &k, v);
}
}
}
NanBox::handle(obj.to_raw())
}
NB_OBJECT_ASSIGN => {
let target = args.first().copied().unwrap_or(NanBox::undefined());
if let Some(t) = target.as_handle().map(Handle::from_raw) {
for src in args.iter().skip(1) {
if let Some(sh) = src.as_handle().map(Handle::from_raw) {
for k in ctx.realm.object_keys(sh).unwrap_or_default() {
let v = ctx
.realm
.get_property(sh, &k)
.unwrap_or(NanBox::undefined());
ctx.realm.set_property(t, &k, v);
}
}
}
}
target
}
_ => NanBox::undefined(),
}
}
fn parse_float_prefix(s: &str) -> f64 {
let (sign, rest) = match s.strip_prefix('-') {
Some(r) => (-1.0, r),
None => (1.0, s.strip_prefix('+').unwrap_or(s)),
};
if rest.starts_with("Infinity") {
return sign * f64::INFINITY;
}
let bytes = s.as_bytes();
let mut end = 0;
let (mut dot, mut e) = (false, false);
while end < bytes.len() {
let ch = bytes[end] as char;
let ok = match ch {
'0'..='9' => true,
'+' | '-' if end == 0 || matches!(bytes[end - 1] as char, 'e' | 'E') => true,
'.' if !dot && !e => {
dot = true;
true
}
'e' | 'E' if !e && end > 0 => {
e = true;
true
}
_ => false,
};
if !ok {
break;
}
end += 1;
}
s[..end].parse::<f64>().unwrap_or(f64::NAN)
}
fn parse_int(s: &str, radix: u32) -> f64 {
let mut t = s;
let mut neg = false;
if let Some(r) = t.strip_prefix('-') {
neg = true;
t = r;
} else if let Some(r) = t.strip_prefix('+') {
t = r;
}
let mut radix = radix;
if (radix == 0 || radix == 16)
&& let Some(r) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X"))
{
t = r;
radix = 16;
}
if radix == 0 {
radix = 10;
}
if !(2..=36).contains(&radix) {
return f64::NAN;
}
let mut value = 0.0;
let mut any = false;
for c in t.chars() {
match c.to_digit(radix) {
Some(d) => {
value = value * f64::from(radix) + f64::from(d);
any = true;
}
None => break,
}
}
if !any {
return f64::NAN;
}
if neg { -value } else { value }
}
use crate::ast::{
ArrayElement, BinaryOp, BindingTarget, Expr, ForInit, Ident, LogicalOp, ObjectMember, Program,
PropertyKey, Stmt, UnaryOp,
};
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum CompileError {
Unsupported(&'static str),
Undefined(String),
}
pub fn compile_and_run(realm: &mut Realm, program: &Program) -> Result<NanBox, CompileError> {
let protos = compile_program(program)?;
run_program(realm, &protos, 0, &[]).map_err(|_| CompileError::Unsupported("runtime fault"))
}
pub fn compile_run_output(
realm: &mut Realm,
program: &Program,
) -> Result<(NanBox, String), CompileError> {
let protos = compile_program(program)?;
run_program_capturing(realm, &protos, 0, &[])
.map_err(|_| CompileError::Unsupported("runtime fault"))
}
#[cfg(feature = "std")]
pub fn execute(source: &str) -> Result<(String, String), String> {
execute_with_limits(source, crate::limits::Limits::default())
}
#[cfg(feature = "std")]
pub fn execute_with_limits(
source: &str,
limits: crate::limits::Limits,
) -> Result<(String, String), String> {
let program =
crate::parser::Parser::parse_program(source).map_err(|e| alloc::format!("{e}"))?;
let Ok(protos) = compile_program(&program) else {
return crate::nbexec::eval_source_with_limits(source, limits);
};
let mut realm = Realm::with_limits(limits);
match run_program_capturing(&mut realm, &protos, 0, &[]) {
Ok((value, output)) => Ok((output, realm.to_display_string(value))),
Err(_) => crate::nbexec::eval_source_with_limits(source, limits),
}
}
pub fn compile_program(program: &Program) -> Result<Vec<FnProto>, CompileError> {
let decls: Vec<&crate::ast::Function> = program
.body
.iter()
.filter_map(|s| match s {
Stmt::Function(f) => Some(f),
_ => None,
})
.collect();
let mut fn_ids = alloc::collections::BTreeMap::new();
for (i, f) in decls.iter().enumerate() {
if let Some(id) = &f.id {
fn_ids.insert(String::from(&*id.name), (i + 1) as u32);
}
}
let fn_ids = alloc::rc::Rc::new(fn_ids);
let mut next_id = (decls.len() + 1) as u32;
let mut class_map = alloc::collections::BTreeMap::new();
let mut class_jobs: Vec<ClassJob> = Vec::new();
let mut class_id = 0u32;
for s in &program.body {
let named = match s {
Stmt::Class(class) => class.id.as_ref().map(|id| (String::from(&*id.name), class)),
Stmt::Var(decl) => match decl.declarations.as_slice() {
[d] => match (&d.target, &d.init) {
(BindingTarget::Ident(id), Some(Expr::Class(class))) => {
Some((String::from(&*id.name), class))
}
_ => None,
},
_ => None,
},
_ => None,
};
if let Some((name, class)) = named {
let info = scan_class(class, class_id, &mut next_id, &mut class_jobs)?;
class_id += 1;
class_map.insert(name, info);
}
}
let classes = alloc::rc::Rc::new(class_map);
let protos = alloc::rc::Rc::new(core::cell::RefCell::new(Vec::new()));
let placeholder = || FnProto {
ops: Vec::new(),
n_regs: 0,
n_params: 0,
n_captures: 0,
rest_from: None,
is_async: false,
length: 0,
name: alloc::string::String::new(),
};
protos
.borrow_mut()
.extend((0..next_id).map(|_| placeholder()));
let main = Compiler::compile_fn(&fn_ids, &classes, &protos, &[], &[], &program.body, true)?;
protos.borrow_mut()[0] = main;
for (i, f) in decls.iter().enumerate() {
let mut proto = Compiler::compile_fn_inner(
&fn_ids,
&classes,
&protos,
&f.params,
&[],
&f.body,
false,
None,
&[],
None,
f.is_async,
)?;
if let Some(id) = &f.id {
proto.name = alloc::string::String::from(id.name.as_ref());
}
protos.borrow_mut()[i + 1] = proto;
}
for job in &class_jobs {
let super_ctor = job
.super_of
.as_deref()
.and_then(|name| nearest_ctor(name, &classes));
let proto = Compiler::compile_fn_inner(
&fn_ids,
&classes,
&protos,
job.params,
&[],
job.body,
false,
super_ctor,
&job.fields,
job.super_of.clone(),
false,
)?;
protos.borrow_mut()[job.id as usize] = proto;
}
Ok(alloc::rc::Rc::try_unwrap(protos)
.expect("unique proto table")
.into_inner())
}
fn native_call(callee: &Expr) -> Option<u16> {
let Expr::Member {
object, property, ..
} = callee
else {
return None;
};
let Expr::Ident(ns) = &**object else {
return None;
};
let (PropertyKey::Ident(method) | PropertyKey::Str(method)) = property else {
return None;
};
match (&*ns.name, &**method) {
("console", "log") => Some(NB_CONSOLE_LOG),
("Math", "max") => Some(NB_MATH_MAX),
("Math", "min") => Some(NB_MATH_MIN),
("Math", "abs") => Some(NB_MATH_ABS),
("Math", "floor") => Some(NB_MATH_FLOOR),
("Math", "ceil") => Some(NB_MATH_CEIL),
("Math", "trunc") => Some(NB_MATH_TRUNC),
("Math", "round") => Some(NB_MATH_ROUND),
("Math", "sqrt") => Some(NB_MATH_SQRT),
("Math", "pow") => Some(NB_MATH_POW),
("Object", "keys") => Some(NB_OBJECT_KEYS),
("Object", "values") => Some(NB_OBJECT_VALUES),
("Object", "entries") => Some(NB_OBJECT_ENTRIES),
("Object", "assign") => Some(NB_OBJECT_ASSIGN),
("Object", "fromEntries") => Some(NB_OBJECT_FROM_ENTRIES),
("JSON", "stringify") => Some(NB_JSON_STRINGIFY),
("JSON", "parse") => Some(NB_JSON_PARSE),
("Number", "isInteger") => Some(NB_NUMBER_IS_INTEGER),
("Number", "isFinite") => Some(NB_NUMBER_IS_FINITE),
("Number", "isNaN") => Some(NB_NUMBER_IS_NAN),
("Number", "parseFloat") => Some(NB_NUMBER_PARSE_FLOAT),
("Number", "parseInt") => Some(NB_NUMBER_PARSE_INT),
("String", "fromCharCode") => Some(NB_STRING_FROM_CHAR_CODE),
("Array", "from") => Some(NB_ARRAY_FROM),
("Array", "isArray") => Some(NB_ARRAY_IS_ARRAY),
("Promise", "resolve") => Some(NB_PROMISE_RESOLVE),
("Promise", "reject") => Some(NB_PROMISE_REJECT),
_ => None,
}
}
fn native_global(callee: &Expr) -> Option<u16> {
let Expr::Ident(id) = callee else {
return None;
};
match &*id.name {
"String" => Some(NB_STRING),
"Number" => Some(NB_NUMBER),
"parseInt" => Some(NB_PARSE_INT),
"parseFloat" => Some(NB_PARSE_FLOAT),
"isNaN" => Some(NB_IS_NAN),
"isFinite" => Some(NB_IS_FINITE),
_ => None,
}
}
fn static_key(key: &PropertyKey) -> Result<String, CompileError> {
match key {
PropertyKey::Ident(s) | PropertyKey::Str(s) => Ok(String::from(&**s)),
PropertyKey::Number(n) => Ok(alloc::format!("{n}")),
_ => Err(CompileError::Unsupported("computed/private key")),
}
}
use alloc::collections::BTreeSet;
fn free_of_function(params: &[crate::ast::Param], body: &[Stmt]) -> BTreeSet<String> {
let bound = bound_names(params, body);
let mut direct = BTreeSet::new();
let mut nested = BTreeSet::new();
for s in body {
refs_stmt(s, &mut direct, &mut nested);
}
direct
.into_iter()
.chain(nested)
.filter(|n| !bound.contains(n))
.collect()
}
fn bound_names(params: &[crate::ast::Param], body: &[Stmt]) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for p in params {
if let BindingTarget::Ident(Ident { name, .. }) = &p.target {
out.insert(String::from(&**name));
}
}
for s in body {
declared_in_stmt(s, &mut out);
}
out
}
fn block_can_exit_abruptly(stmts: &[Stmt]) -> bool {
stmts.iter().any(stmt_can_exit_abruptly)
}
fn stmt_can_exit_abruptly(s: &Stmt) -> bool {
match s {
Stmt::Return { .. } | Stmt::Break { .. } | Stmt::Continue { .. } => true,
Stmt::Block { body, .. } => block_can_exit_abruptly(body),
Stmt::If {
consequent,
alternate,
..
} => {
stmt_can_exit_abruptly(consequent)
|| alternate.as_deref().is_some_and(stmt_can_exit_abruptly)
}
Stmt::For { body, .. }
| Stmt::ForIn { body, .. }
| Stmt::ForOf { body, .. }
| Stmt::While { body, .. }
| Stmt::DoWhile { body, .. }
| Stmt::Labeled { body, .. }
| Stmt::With { body, .. } => stmt_can_exit_abruptly(body),
Stmt::Switch { cases, .. } => cases.iter().any(|c| block_can_exit_abruptly(&c.body)),
Stmt::Try {
block,
handler,
finalizer,
..
} => {
block_can_exit_abruptly(block)
|| handler
.as_ref()
.is_some_and(|h| block_can_exit_abruptly(&h.body))
|| finalizer
.as_ref()
.is_some_and(|f| block_can_exit_abruptly(f))
}
_ => false, }
}
fn captured_names(params: &[crate::ast::Param], body: &[Stmt]) -> BTreeSet<String> {
let bound = bound_names(params, body);
let mut direct = BTreeSet::new();
let mut nested = BTreeSet::new();
for s in body {
refs_stmt(s, &mut direct, &mut nested);
}
bound.intersection(&nested).cloned().collect()
}
fn declared_in_stmt(s: &Stmt, out: &mut BTreeSet<String>) {
let decl_target = |t: &BindingTarget, out: &mut BTreeSet<String>| {
if let BindingTarget::Ident(Ident { name, .. }) = t {
out.insert(String::from(&**name));
}
};
match s {
Stmt::Var(d) => {
for dr in &d.declarations {
decl_target(&dr.target, out);
}
}
Stmt::Function(f) => {
if let Some(id) = &f.id {
out.insert(String::from(&*id.name));
}
}
Stmt::Block { body, .. } => {
for s in body {
declared_in_stmt(s, out);
}
}
Stmt::If {
consequent,
alternate,
..
} => {
declared_in_stmt(consequent, out);
if let Some(a) = alternate {
declared_in_stmt(a, out);
}
}
Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => declared_in_stmt(body, out),
Stmt::For { init, body, .. } => {
if let Some(ForInit::Var(d)) = init {
for dr in &d.declarations {
decl_target(&dr.target, out);
}
}
declared_in_stmt(body, out);
}
Stmt::ForOf { left, body, .. } | Stmt::ForIn { left, body, .. } => {
if let crate::ast::ForLeft::Decl { target, .. } = left {
decl_target(target, out);
}
declared_in_stmt(body, out);
}
Stmt::Try {
block,
handler,
finalizer,
..
} => {
for s in block {
declared_in_stmt(s, out);
}
if let Some(h) = handler {
if let Some(p) = &h.param {
decl_target(p, out);
}
for s in &h.body {
declared_in_stmt(s, out);
}
}
if let Some(f) = finalizer {
for s in f {
declared_in_stmt(s, out);
}
}
}
Stmt::Switch { cases, .. } => {
for c in cases {
for s in &c.body {
declared_in_stmt(s, out);
}
}
}
_ => {}
}
}
fn refs_stmt(s: &Stmt, direct: &mut BTreeSet<String>, nested: &mut BTreeSet<String>) {
match s {
Stmt::Expr { expression, .. } => refs_expr(expression, direct, nested),
Stmt::Var(d) => {
for dr in &d.declarations {
if let Some(e) = &dr.init {
refs_expr(e, direct, nested);
}
}
}
Stmt::Return {
argument: Some(e), ..
}
| Stmt::Throw { argument: e, .. } => refs_expr(e, direct, nested),
Stmt::Block { body, .. } => body.iter().for_each(|s| refs_stmt(s, direct, nested)),
Stmt::If {
test,
consequent,
alternate,
..
} => {
refs_expr(test, direct, nested);
refs_stmt(consequent, direct, nested);
if let Some(a) = alternate {
refs_stmt(a, direct, nested);
}
}
Stmt::While { test, body, .. } | Stmt::DoWhile { body, test, .. } => {
refs_expr(test, direct, nested);
refs_stmt(body, direct, nested);
}
Stmt::For {
init,
test,
update,
body,
..
} => {
match init {
Some(ForInit::Var(d)) => {
for dr in &d.declarations {
if let Some(e) = &dr.init {
refs_expr(e, direct, nested);
}
}
}
Some(ForInit::Expr(e)) => refs_expr(e, direct, nested),
None => {}
}
if let Some(t) = test {
refs_expr(t, direct, nested);
}
if let Some(u) = update {
refs_expr(u, direct, nested);
}
refs_stmt(body, direct, nested);
}
Stmt::ForOf {
left, right, body, ..
}
| Stmt::ForIn {
left, right, body, ..
} => {
if let crate::ast::ForLeft::Target(e) = left {
refs_expr(e, direct, nested);
}
refs_expr(right, direct, nested);
refs_stmt(body, direct, nested);
}
Stmt::Try {
block,
handler,
finalizer,
..
} => {
block.iter().for_each(|s| refs_stmt(s, direct, nested));
if let Some(h) = handler {
h.body.iter().for_each(|s| refs_stmt(s, direct, nested));
}
if let Some(f) = finalizer {
f.iter().for_each(|s| refs_stmt(s, direct, nested));
}
}
Stmt::Switch {
discriminant,
cases,
..
} => {
refs_expr(discriminant, direct, nested);
for c in cases {
if let Some(t) = &c.test {
refs_expr(t, direct, nested);
}
c.body.iter().for_each(|s| refs_stmt(s, direct, nested));
}
}
_ => {}
}
}
fn refs_expr(e: &Expr, direct: &mut BTreeSet<String>, nested: &mut BTreeSet<String>) {
match e {
Expr::Ident(id) => {
direct.insert(String::from(&*id.name));
}
Expr::Function(f) => nested.extend(free_of_function(&f.params, &f.body)),
Expr::Arrow(a) => {
let body: Vec<Stmt> = match &a.body {
crate::ast::ArrowBody::Block(b) => b.clone(),
crate::ast::ArrowBody::Expr(e) => alloc::vec![Stmt::Return {
argument: Some(Box::new((**e).clone())),
span: crate::common::Span::point(0),
}],
};
nested.extend(free_of_function(&a.params, &body));
}
Expr::Unary { argument, .. } | Expr::Update { argument, .. } => {
refs_expr(argument, direct, nested);
}
Expr::Binary { left, right, .. } | Expr::Logical { left, right, .. } => {
refs_expr(left, direct, nested);
refs_expr(right, direct, nested);
}
Expr::Conditional {
test,
consequent,
alternate,
..
} => {
refs_expr(test, direct, nested);
refs_expr(consequent, direct, nested);
refs_expr(alternate, direct, nested);
}
Expr::Assign { target, value, .. } => {
refs_expr(target, direct, nested);
refs_expr(value, direct, nested);
}
Expr::Member {
object, property, ..
} => {
refs_expr(object, direct, nested);
if let PropertyKey::Computed(e) = property {
refs_expr(e, direct, nested);
}
}
Expr::Call {
callee, arguments, ..
} => {
refs_expr(callee, direct, nested);
for a in arguments {
if let crate::ast::Argument::Item(e) = a {
refs_expr(e, direct, nested);
}
}
}
Expr::Array { elements, .. } => {
for el in elements {
if let ArrayElement::Item(e) = el {
refs_expr(e, direct, nested);
}
}
}
Expr::Object { members, .. } => {
for m in members {
if let ObjectMember::Property { key, value, .. } = m {
if let PropertyKey::Computed(e) = key {
refs_expr(e, direct, nested);
}
refs_expr(value, direct, nested);
}
}
}
_ => {}
}
}
fn scan_class<'a>(
class: &'a crate::ast::Class,
class_id: u32,
next_id: &mut u32,
jobs: &mut Vec<ClassJob<'a>>,
) -> Result<ClassInfo, CompileError> {
use crate::ast::{ClassMember, Expr, MethodKind};
let super_name = match &class.super_class {
None => None,
Some(e) => match &**e {
Expr::Ident(id) => Some(String::from(&*id.name)),
_ => return Err(CompileError::Unsupported("computed extends")),
},
};
let mut info = ClassInfo {
class_id,
super_name: super_name.clone(),
ctor: None,
methods: Vec::new(),
accessors: Vec::new(),
statics: Vec::new(),
};
let mut ctor_member: Option<&crate::ast::ClassMethod> = None;
let mut fields: Vec<(String, Option<&'a Expr>)> = Vec::new();
let add_accessor = |name: String,
getter: Option<u32>,
setter: Option<u32>,
acc: &mut Vec<(String, Option<u32>, Option<u32>)>| {
if let Some(a) = acc.iter_mut().find(|(n, _, _)| *n == name) {
a.1 = a.1.or(getter);
a.2 = a.2.or(setter);
} else {
acc.push((name, getter, setter));
}
};
for member in &class.body {
match member {
ClassMember::Method(m) if !m.is_static && m.kind == MethodKind::Constructor => {
ctor_member = Some(m);
}
ClassMember::Method(m) if !m.is_static && m.kind == MethodKind::Method => {
let id = *next_id;
*next_id += 1;
let name = static_key(&m.key)?;
info.methods.push((name, id));
jobs.push(ClassJob {
id,
params: &m.value.params,
body: &m.value.body,
super_of: super_name.clone(),
fields: Vec::new(),
});
}
ClassMember::Method(m)
if !m.is_static && matches!(m.kind, MethodKind::Get | MethodKind::Set) =>
{
let id = *next_id;
*next_id += 1;
let name = static_key(&m.key)?;
jobs.push(ClassJob {
id,
params: &m.value.params,
body: &m.value.body,
super_of: super_name.clone(),
fields: Vec::new(),
});
if m.kind == MethodKind::Get {
add_accessor(name, Some(id), None, &mut info.accessors);
} else {
add_accessor(name, None, Some(id), &mut info.accessors);
}
}
ClassMember::Method(m) if m.is_static && m.kind == MethodKind::Method => {
let id = *next_id;
*next_id += 1;
let name = static_key(&m.key)?;
info.statics.push((name, id));
jobs.push(ClassJob {
id,
params: &m.value.params,
body: &m.value.body,
super_of: None,
fields: Vec::new(),
});
}
ClassMember::Field(f) if !f.is_static => {
fields.push((static_key(&f.key)?, f.value.as_ref()));
}
ClassMember::Field(f) if f.is_static => {}
_ => return Err(CompileError::Unsupported("class member")),
}
}
if !fields.is_empty() && super_name.is_some() {
return Err(CompileError::Unsupported("fields with extends"));
}
if ctor_member.is_some() || !fields.is_empty() {
let id = *next_id;
*next_id += 1;
info.ctor = Some(id);
let (params, body): (&[crate::ast::Param], &[Stmt]) = match ctor_member {
Some(m) => (&m.value.params, &m.value.body),
None => (&[], &[]),
};
jobs.push(ClassJob {
id,
params,
body,
super_of: super_name.clone(),
fields,
});
}
Ok(info)
}
struct ClassJob<'a> {
id: u32,
params: &'a [crate::ast::Param],
body: &'a [Stmt],
super_of: Option<String>,
fields: Vec<(String, Option<&'a crate::ast::Expr>)>,
}
fn nearest_ctor(
name: &str,
classes: &alloc::collections::BTreeMap<String, ClassInfo>,
) -> Option<u32> {
let info = classes.get(name)?;
if let Some(c) = info.ctor {
return Some(c);
}
nearest_ctor(info.super_name.as_deref()?, classes)
}
#[derive(Clone)]
struct ClassInfo {
class_id: u32,
super_name: Option<String>,
ctor: Option<u32>,
methods: Vec<(String, u32)>,
accessors: Vec<(String, Option<u32>, Option<u32>)>,
statics: Vec<(String, u32)>,
}
#[derive(Clone, Copy)]
struct Binding {
reg: Reg,
cell: bool,
konst: bool,
}
#[derive(Default)]
struct Compiler {
ops: Vec<Op>,
scopes: Vec<alloc::collections::BTreeMap<String, Binding>>,
next_reg: Reg,
fn_ids: alloc::rc::Rc<alloc::collections::BTreeMap<String, u32>>,
classes: alloc::rc::Rc<alloc::collections::BTreeMap<String, ClassInfo>>,
protos: alloc::rc::Rc<core::cell::RefCell<Vec<FnProto>>>,
cell_names: alloc::collections::BTreeSet<String>,
this_reg: Reg,
super_ctor: Option<u32>,
super_class: Option<String>,
break_sites: Vec<Vec<usize>>,
continue_sites: Vec<Vec<usize>>,
optchain_ends: Vec<Vec<usize>>,
labels: Vec<(String, usize)>,
fn_value_regs: alloc::collections::BTreeMap<String, Reg>,
reg_overflow: bool,
}
impl Compiler {
#[allow(clippy::too_many_arguments)]
fn compile_fn(
fn_ids: &alloc::rc::Rc<alloc::collections::BTreeMap<String, u32>>,
classes: &alloc::rc::Rc<alloc::collections::BTreeMap<String, ClassInfo>>,
protos: &alloc::rc::Rc<core::cell::RefCell<Vec<FnProto>>>,
params: &[crate::ast::Param],
captures: &[String],
body: &[Stmt],
is_main: bool,
) -> Result<FnProto, CompileError> {
Self::compile_fn_inner(
fn_ids,
classes,
protos,
params,
captures,
body,
is_main,
None,
&[],
None,
false,
)
}
#[allow(clippy::too_many_arguments)]
fn compile_fn_inner(
fn_ids: &alloc::rc::Rc<alloc::collections::BTreeMap<String, u32>>,
classes: &alloc::rc::Rc<alloc::collections::BTreeMap<String, ClassInfo>>,
protos: &alloc::rc::Rc<core::cell::RefCell<Vec<FnProto>>>,
params: &[crate::ast::Param],
captures: &[String],
body: &[Stmt],
is_main: bool,
super_ctor: Option<u32>,
fields: &[(String, Option<&crate::ast::Expr>)],
super_class: Option<String>,
is_async: bool,
) -> Result<FnProto, CompileError> {
let cell_names = captured_names(params, body);
let mut c = Compiler {
fn_ids: alloc::rc::Rc::clone(fn_ids),
classes: alloc::rc::Rc::clone(classes),
protos: alloc::rc::Rc::clone(protos),
cell_names,
super_ctor,
super_class,
..Compiler::default()
};
c.scopes.push(alloc::collections::BTreeMap::new());
let arg_regs: Vec<Reg> = params.iter().map(|_| c.alloc()).collect();
let cap_regs: Vec<Reg> = captures.iter().map(|_| c.alloc()).collect();
c.this_reg = c.alloc(); let rest_from = if params.last().is_some_and(|p| p.rest) {
Some(params.len() - 1)
} else {
None
};
for (i, p) in params.iter().enumerate() {
match &p.target {
BindingTarget::Ident(Ident { name, .. }) => {
let b = if c.cell_names.contains(&**name) {
let cell = c.alloc();
c.ops.push(Op::NewArray { dst: cell, len: 1 });
let bind = Binding {
reg: cell,
cell: true,
konst: false,
};
c.write_var(bind, arg_regs[i]);
bind
} else {
Binding {
reg: arg_regs[i],
cell: false,
konst: false,
}
};
c.scopes
.last_mut()
.expect("a scope")
.insert(String::from(&**name), b);
}
other => c.bind_pattern(other, arg_regs[i])?,
}
}
for (j, name) in captures.iter().enumerate() {
c.scopes.last_mut().expect("a scope").insert(
name.clone(),
Binding {
reg: cap_regs[j],
cell: true,
konst: false,
},
);
}
for p in params {
if let (Some(def), BindingTarget::Ident(Ident { name, .. })) = (&p.default, &p.target) {
let b = c.lookup(name).expect("a bound param");
let cur = c.read_var(b);
c.apply_default(cur, Some(def))?;
c.write_var(b, cur);
}
}
for (name, init) in fields {
let v = match init {
Some(e) => c.expr(e)?,
None => c.constant(NanBox::undefined())?,
};
let this = c.this_reg;
c.ops.push(Op::SetProp {
obj: this,
key: name.clone(),
src: v,
});
}
if is_main {
for stmt in body {
if let Stmt::Function(f) = stmt
&& let Some(id) = &f.id
&& let Some(&func) = c.fn_ids.get(&*id.name)
{
let reg = c.alloc();
c.ops.push(Op::LoadFunc { dst: reg, func });
c.fn_value_regs.insert(String::from(&*id.name), reg);
}
}
}
let mut last: Option<Reg> = None;
for stmt in body {
if let Some(r) = c.stmt(stmt)? {
last = Some(r);
}
}
if is_main {
let src = match last {
Some(r) => r,
None => c.constant(NanBox::undefined())?,
};
c.ops.push(Op::Return { src });
}
if c.reg_overflow {
return Err(CompileError::Unsupported("too many registers"));
}
let length = params
.iter()
.take_while(|p| !p.rest && p.default.is_none())
.count();
Ok(FnProto {
n_regs: c.next_reg as usize,
n_params: params.len(),
n_captures: captures.len(),
rest_from,
is_async,
length,
ops: c.ops,
name: alloc::string::String::new(),
})
}
}
impl Compiler {
fn alloc(&mut self) -> Reg {
let r = self.next_reg;
match self.next_reg.checked_add(1) {
Some(n) => self.next_reg = n,
None => self.reg_overflow = true,
}
r
}
fn declare(&mut self, name: &str) -> Binding {
let reg = self.alloc();
let cell = self.cell_names.contains(name);
if cell {
self.ops.push(Op::NewArray { dst: reg, len: 1 });
}
let b = Binding {
reg,
cell,
konst: false,
};
self.scopes
.last_mut()
.expect("a scope")
.insert(String::from(name), b);
b
}
fn mark_const(&mut self, name: &str) {
if let Some(b) = self.scopes.last_mut().and_then(|s| s.get_mut(name)) {
b.konst = true;
}
}
fn lookup(&self, name: &str) -> Option<Binding> {
self.scopes.iter().rev().find_map(|s| s.get(name).copied())
}
fn assign_pattern(&mut self, target: &Expr, value_reg: Reg) -> Result<(), CompileError> {
match target {
Expr::Ident(id) => {
let b = self
.lookup(&id.name)
.ok_or_else(|| CompileError::Undefined(String::from(&*id.name)))?;
self.write_var(b, value_reg);
Ok(())
}
Expr::Member {
object, property, ..
} => {
let obj = self.expr(object)?;
self.member_write(obj, property, value_reg)
}
Expr::Array { elements, .. } => {
for (i, el) in elements.iter().enumerate() {
match el {
ArrayElement::Item(e) => {
let idx = self.constant(NanBox::number(i as f64))?;
let v = self.alloc();
self.ops.push(Op::GetElem {
dst: v,
arr: value_reg,
index: idx,
});
self.assign_target_with_default(e, v)?;
}
ArrayElement::Hole => {}
ArrayElement::Spread(e) => {
let from = self.constant(NanBox::number(i as f64))?;
let rest = self.alloc();
self.ops.push(Op::ArraySliceFrom {
dst: rest,
src: value_reg,
from,
});
self.assign_pattern(e, rest)?;
}
}
}
Ok(())
}
Expr::Object { members, .. } => {
let mut named: Vec<String> = Vec::new();
for m in members {
match m {
ObjectMember::Property { key, value, .. } => {
let key = static_key(key)?;
let v = self.alloc();
self.ops.push(Op::GetProp {
dst: v,
obj: value_reg,
key: key.clone(),
});
self.assign_target_with_default(value, v)?;
named.push(key);
}
ObjectMember::Spread { value, .. } => {
let r = self.alloc();
self.ops.push(Op::ObjectRest {
dst: r,
src: value_reg,
exclude: named.clone().into(),
});
self.assign_pattern(value, r)?;
}
ObjectMember::Accessor { .. } => {
return Err(CompileError::Unsupported(
"accessor in assignment pattern",
));
}
}
}
Ok(())
}
_ => Err(CompileError::Unsupported("assignment pattern target")),
}
}
fn assign_target_with_default(
&mut self,
target: &Expr,
value_reg: Reg,
) -> Result<(), CompileError> {
if let Expr::Assign {
op: crate::ast::AssignOp::Assign,
target: inner,
value: default,
..
} = target
{
self.apply_default(value_reg, Some(default))?;
return self.assign_pattern(inner, value_reg);
}
self.assign_pattern(target, value_reg)
}
fn bind_pattern(&mut self, target: &BindingTarget, value_reg: Reg) -> Result<(), CompileError> {
match target {
BindingTarget::Ident(Ident { name, .. }) => {
let b = self.declare(name);
self.write_var(b, value_reg);
Ok(())
}
BindingTarget::Array(pat) => {
use crate::ast::ArrayPatternElement;
for (i, el) in pat.elements.iter().enumerate() {
match el {
ArrayPatternElement::Hole => {}
ArrayPatternElement::Item {
target, default, ..
} => {
let idx = self.constant(NanBox::number(i as f64))?;
let v = self.alloc();
self.ops.push(Op::GetElem {
dst: v,
arr: value_reg,
index: idx,
});
self.apply_default(v, default.as_ref())?;
self.bind_pattern(target, v)?;
}
ArrayPatternElement::Rest { target, .. } => {
let from = self.constant(NanBox::number(i as f64))?;
let rest = self.alloc();
self.ops.push(Op::ArraySliceFrom {
dst: rest,
src: value_reg,
from,
});
self.bind_pattern(target, rest)?;
}
}
}
Ok(())
}
BindingTarget::Object(pat) => {
let mut named: Vec<String> = Vec::new();
for prop in &pat.properties {
let key = static_key(&prop.key)?;
let v = self.alloc();
self.ops.push(Op::GetProp {
dst: v,
obj: value_reg,
key: key.clone(),
});
self.apply_default(v, prop.default.as_ref())?;
self.bind_pattern(&prop.value, v)?;
named.push(key);
}
if let Some(rest) = &pat.rest {
let r = self.alloc();
self.ops.push(Op::ObjectRest {
dst: r,
src: value_reg,
exclude: named.into(),
});
self.bind_pattern(rest, r)?;
}
Ok(())
}
}
}
fn apply_default(&mut self, reg: Reg, default: Option<&Expr>) -> Result<(), CompileError> {
let Some(e) = default else { return Ok(()) };
let undef = self.constant(NanBox::undefined())?;
let is_undef = self.alloc();
self.ops.push(Op::StrictEq {
dst: is_undef,
a: reg,
b: undef,
});
let jf = self.emit_jump_if_false(is_undef);
let d = self.expr(e)?;
self.ops.push(Op::Move { dst: reg, src: d });
self.patch(jf);
Ok(())
}
fn refresh_loop_cells(&mut self, bindings: &[Binding]) {
for b in bindings {
if b.cell {
let val = self.read_var(*b);
self.ops.push(Op::NewArray { dst: b.reg, len: 1 });
self.write_var(*b, val);
}
}
}
fn read_var(&mut self, b: Binding) -> Reg {
if b.cell {
let dst = self.alloc();
let idx = self.constant(NanBox::number(0.0)).expect("const");
self.ops.push(Op::GetElem {
dst,
arr: b.reg,
index: idx,
});
dst
} else {
b.reg
}
}
fn write_var(&mut self, b: Binding, src: Reg) {
if b.cell {
let idx = self.constant(NanBox::number(0.0)).expect("const");
self.ops.push(Op::SetElem {
arr: b.reg,
index: idx,
src,
});
} else {
self.ops.push(Op::Move { dst: b.reg, src });
}
}
fn stmt(&mut self, stmt: &Stmt) -> Result<Option<Reg>, CompileError> {
match stmt {
Stmt::Empty { .. } => Ok(None),
Stmt::Function(_) => Ok(None),
Stmt::Class(class) => {
if let Some(cid) = &class.id {
self.materialize_class(&cid.name, class)?;
}
Ok(None)
}
Stmt::Return { argument, .. } => {
let src = match argument {
Some(e) => self.expr(e)?,
None => self.constant(NanBox::undefined())?,
};
self.ops.push(Op::Return { src });
Ok(None)
}
Stmt::Throw { argument, .. } => {
let src = self.expr(argument)?;
self.ops.push(Op::Throw { src });
Ok(None)
}
Stmt::Switch {
discriminant,
cases,
..
} => {
let d = self.expr(discriminant)?;
self.break_sites.push(Vec::new());
let mut case_jumps: Vec<(usize, usize)> = Vec::new();
for (i, case) in cases.iter().enumerate() {
if let Some(test) = &case.test {
let t = self.expr(test)?;
let eq = self.alloc();
self.ops.push(Op::StrictEq {
dst: eq,
a: d,
b: t,
});
let skip = self.emit_jump_if_false(eq);
let to_body = self.emit_jump();
case_jumps.push((i, to_body));
self.patch(skip); }
}
let exit_dispatch = self.emit_jump(); let mut entries = alloc::vec![0usize; cases.len()];
for (i, case) in cases.iter().enumerate() {
entries[i] = self.ops.len();
self.scopes.push(alloc::collections::BTreeMap::new());
for s in &case.body {
self.stmt(s)?;
}
self.scopes.pop();
}
for (i, j) in case_jumps {
self.patch_to(j, entries[i]);
}
match cases.iter().position(|c| c.test.is_none()) {
Some(di) => self.patch_to(exit_dispatch, entries[di]),
None => self.patch(exit_dispatch), }
let breaks = self.break_sites.pop().unwrap_or_default();
let end = self.ops.len();
for b in breaks {
self.patch_to(b, end);
}
Ok(None)
}
Stmt::Try {
block,
handler,
finalizer,
..
} => {
if handler.is_none() && finalizer.is_none() {
return Err(CompileError::Unsupported("try without catch/finally"));
}
if finalizer.is_some()
&& (block_can_exit_abruptly(block)
|| handler
.as_ref()
.is_some_and(|h| block_can_exit_abruptly(&h.body)))
{
return Err(CompileError::Unsupported("try/finally with abrupt exit"));
}
let catch_reg = self.alloc();
let push = self.ops.len();
self.ops.push(Op::PushHandler {
target: 0,
reg: catch_reg,
});
self.block_stmts(block)?;
self.ops.push(Op::PopHandler);
if let Some(fin) = finalizer {
self.block_stmts(fin)?;
}
let jend = self.emit_jump();
self.patch(push);
if let Some(catch) = handler {
self.scopes.push(alloc::collections::BTreeMap::new());
if let Some(BindingTarget::Ident(Ident { name, .. })) = &catch.param {
let b = if self.cell_names.contains(&**name) {
let cell = self.alloc();
self.ops.push(Op::NewArray { dst: cell, len: 1 });
let bind = Binding {
reg: cell,
cell: true,
konst: false,
};
self.write_var(bind, catch_reg);
bind
} else {
Binding {
reg: catch_reg,
cell: false,
konst: false,
}
};
self.scopes
.last_mut()
.expect("a scope")
.insert(String::from(&**name), b);
}
for s in &catch.body {
self.stmt(s)?;
}
self.scopes.pop();
if let Some(fin) = finalizer {
self.block_stmts(fin)?;
}
} else {
if let Some(fin) = finalizer {
self.block_stmts(fin)?;
}
self.ops.push(Op::Throw { src: catch_reg });
}
self.patch(jend);
Ok(None)
}
Stmt::Expr { expression, .. } => Ok(Some(self.expr(expression)?)),
Stmt::Var(decl) => {
for d in &decl.declarations {
if let (BindingTarget::Ident(id), Some(Expr::Class(class))) =
(&d.target, &d.init)
&& self.classes.contains_key(&*id.name)
{
self.materialize_class(&id.name, class)?;
continue;
}
let value = match &d.init {
Some(e) => self.expr_named(e, &d.target)?,
None => self.constant(NanBox::undefined())?,
};
self.bind_pattern(&d.target, value)?;
if matches!(decl.kind, crate::ast::VarDeclKind::Const)
&& let BindingTarget::Ident(id) = &d.target
{
self.mark_const(&id.name);
}
}
Ok(None)
}
Stmt::Block { body, .. } => {
self.scopes.push(alloc::collections::BTreeMap::new());
for s in body {
self.stmt(s)?;
}
self.scopes.pop();
Ok(None)
}
Stmt::If {
test,
consequent,
alternate,
..
} => {
let cond = self.expr(test)?;
let jf = self.emit_jump_if_false(cond);
self.stmt(consequent)?;
if let Some(alt) = alternate {
let jend = self.emit_jump();
self.patch(jf);
self.stmt(alt)?;
self.patch(jend);
} else {
self.patch(jf);
}
Ok(None)
}
Stmt::Break { label: None, .. } => {
let j = self.emit_jump();
self.break_sites
.last_mut()
.ok_or(CompileError::Unsupported("break outside loop/switch"))?
.push(j);
Ok(None)
}
Stmt::Continue { label: None, .. } => {
let j = self.emit_jump();
self.continue_sites
.last_mut()
.ok_or(CompileError::Unsupported("continue outside loop"))?
.push(j);
Ok(None)
}
Stmt::Break {
label: Some(label), ..
} => {
let idx = self
.labels
.iter()
.rev()
.find(|(n, _)| n == &*label.name)
.map(|(_, i)| *i)
.ok_or(CompileError::Unsupported("break to unknown label"))?;
let j = self.emit_jump();
self.break_sites[idx].push(j);
Ok(None)
}
Stmt::Continue {
label: Some(label), ..
} => {
let idx = self
.labels
.iter()
.rev()
.find(|(n, _)| n == &*label.name)
.map(|(_, i)| *i)
.ok_or(CompileError::Unsupported("continue to unknown label"))?;
let j = self.emit_jump();
self.continue_sites[idx].push(j);
Ok(None)
}
Stmt::Labeled { label, body, .. } => {
let is_loop = matches!(
&**body,
Stmt::While { .. }
| Stmt::DoWhile { .. }
| Stmt::For { .. }
| Stmt::ForIn { .. }
| Stmt::ForOf { .. }
);
let idx = self.break_sites.len();
self.labels.push((String::from(&*label.name), idx));
if is_loop {
let r = self.stmt(body);
self.labels.pop();
r
} else {
self.break_sites.push(Vec::new());
self.continue_sites.push(Vec::new());
let r = self.stmt(body);
self.labels.pop();
let end = self.ops.len();
for b in self.break_sites.pop().unwrap_or_default() {
self.patch_to(b, end);
}
self.continue_sites.pop();
r
}
}
Stmt::While { test, body, .. } => {
let top = self.ops.len();
let cond = self.expr(test)?;
let jf = self.emit_jump_if_false(cond);
self.enter_loop();
self.stmt(body)?;
self.ops.push(Op::Jump { target: top });
self.patch(jf);
self.exit_loop(top); Ok(None)
}
Stmt::ForOf {
left, right, body, ..
} => {
use crate::ast::ForLeft;
let ForLeft::Decl { target, .. } = left else {
return Err(CompileError::Unsupported("for-of binding"));
};
self.scopes.push(alloc::collections::BTreeMap::new());
let arr = self.expr(right)?;
let len = self.alloc();
self.ops.push(Op::ArrayLen { dst: len, arr });
let i = self.alloc();
self.ops.push(Op::LoadConst {
dst: i,
value: NanBox::number(0.0),
});
let top = self.ops.len();
let cond = self.alloc();
self.ops.push(Op::Lt {
dst: cond,
a: i,
b: len,
});
let jf = self.emit_jump_if_false(cond);
let cur = self.alloc();
self.ops.push(Op::GetElem {
dst: cur,
arr,
index: i,
});
self.bind_pattern(target, cur)?;
self.enter_loop();
self.stmt(body)?;
let cont = self.ops.len(); let one = self.alloc();
self.ops.push(Op::LoadConst {
dst: one,
value: NanBox::number(1.0),
});
self.ops.push(Op::Add {
dst: i,
a: i,
b: one,
});
self.ops.push(Op::Jump { target: top });
self.patch(jf);
self.exit_loop(cont);
self.scopes.pop();
Ok(None)
}
Stmt::ForIn {
left, right, body, ..
} => {
use crate::ast::ForLeft;
let ForLeft::Decl { target, .. } = left else {
return Err(CompileError::Unsupported("for-in binding"));
};
self.scopes.push(alloc::collections::BTreeMap::new());
let obj = self.expr(right)?;
let arr = self.alloc();
self.ops.push(Op::EnumKeys { dst: arr, obj });
let len = self.alloc();
self.ops.push(Op::ArrayLen { dst: len, arr });
let i = self.alloc();
self.ops.push(Op::LoadConst {
dst: i,
value: NanBox::number(0.0),
});
let top = self.ops.len();
let cond = self.alloc();
self.ops.push(Op::Lt {
dst: cond,
a: i,
b: len,
});
let jf = self.emit_jump_if_false(cond);
let cur = self.alloc();
self.ops.push(Op::GetElem {
dst: cur,
arr,
index: i,
});
self.bind_pattern(target, cur)?;
self.enter_loop();
self.stmt(body)?;
let cont = self.ops.len();
let one = self.alloc();
self.ops.push(Op::LoadConst {
dst: one,
value: NanBox::number(1.0),
});
self.ops.push(Op::Add {
dst: i,
a: i,
b: one,
});
self.ops.push(Op::Jump { target: top });
self.patch(jf);
self.exit_loop(cont);
self.scopes.pop();
Ok(None)
}
Stmt::DoWhile { body, test, .. } => {
let top = self.ops.len();
self.enter_loop();
self.stmt(body)?;
let cont = self.ops.len(); let cond = self.expr(test)?;
let not = self.alloc();
self.ops.push(Op::Not { dst: not, a: cond });
let jf = self.emit_jump_if_false(not);
self.patch_to(jf, top);
self.exit_loop(cont);
Ok(None)
}
Stmt::For {
init,
test,
update,
body,
..
} => {
self.scopes.push(alloc::collections::BTreeMap::new());
match init {
Some(ForInit::Var(decl)) => {
self.stmt(&Stmt::Var(decl.clone()))?;
}
Some(ForInit::Expr(e)) => {
self.expr(e)?;
}
None => {}
}
let per_iter: Vec<Binding> = match init {
Some(ForInit::Var(decl)) if decl.kind != crate::ast::VarDeclKind::Var => decl
.declarations
.iter()
.filter_map(|d| match &d.target {
BindingTarget::Ident(id) => self.lookup(&id.name),
_ => None,
})
.filter(|b| b.cell)
.collect(),
_ => Vec::new(),
};
let top = self.ops.len();
let exit = match test {
Some(t) => {
let cond = self.expr(t)?;
Some(self.emit_jump_if_false(cond))
}
None => None,
};
self.enter_loop();
self.stmt(body)?;
let cont = self.ops.len();
self.refresh_loop_cells(&per_iter);
if let Some(u) = update {
self.expr(u)?;
}
self.ops.push(Op::Jump { target: top });
if let Some(jf) = exit {
self.patch(jf);
}
self.exit_loop(cont);
self.scopes.pop();
Ok(None)
}
_ => Err(CompileError::Unsupported("statement")),
}
}
fn expr(&mut self, expr: &Expr) -> Result<Reg, CompileError> {
match expr {
Expr::Number { value, .. } => self.constant(NanBox::number(*value)),
Expr::Bool { value, .. } => self.constant(NanBox::boolean(*value)),
Expr::Null(_) => self.constant(NanBox::null()),
Expr::Str { value, .. } => {
let r = self.alloc();
self.ops.push(Op::NewString {
dst: r,
value: String::from(&**value),
});
Ok(r)
}
Expr::Ident(id) => {
if let Some(b) = self.lookup(&id.name) {
Ok(self.read_var(b))
} else if let Some(®) = self.fn_value_regs.get(&*id.name) {
Ok(reg)
} else if let Some(&func) = self.fn_ids.get(&*id.name) {
let dst = self.alloc();
self.ops.push(Op::LoadFunc { dst, func });
Ok(dst)
} else {
match &*id.name {
"undefined" => self.constant(NanBox::undefined()),
"NaN" => self.constant(NanBox::number(f64::NAN)),
"Infinity" => self.constant(NanBox::number(f64::INFINITY)),
n if KNOWN_GLOBALS.contains(&n) => {
Err(CompileError::Undefined(String::from(n)))
}
_ => {
let msg = alloc::format!("{} is not defined", id.name);
Ok(self.emit_throw_error("ReferenceError", &msg))
}
}
}
}
Expr::Unary { op, argument, .. } => {
if matches!(op, UnaryOp::Delete) {
if let Expr::Member {
object, property, ..
} = &**argument
{
let obj = self.expr(object)?;
let key = match property {
PropertyKey::Computed(e) => self.expr(e)?,
_ => self.constant_str(&static_key(property)?),
};
let dst = self.alloc();
self.ops.push(Op::DeleteProp { dst, obj, key });
return Ok(dst);
}
return self.constant(NanBox::boolean(true));
}
if matches!(op, UnaryOp::Typeof)
&& let Expr::Ident(id) = &**argument
&& self.lookup(&id.name).is_none()
&& !self.fn_ids.contains_key(&*id.name)
&& !matches!(&*id.name, "undefined" | "NaN" | "Infinity")
&& !KNOWN_GLOBALS.contains(&&*id.name)
{
return Ok(self.constant_str("undefined"));
}
let a = self.expr(argument)?;
let dst = self.alloc();
match op {
UnaryOp::Minus => self.ops.push(Op::Neg { dst, a }),
UnaryOp::Not => self.ops.push(Op::Not { dst, a }),
UnaryOp::Plus => {
self.ops.push(Op::CallNative {
dst,
native: NB_NUMBER,
args: alloc::vec![a],
});
}
UnaryOp::Typeof => self.ops.push(Op::TypeOf { dst, a }),
UnaryOp::BitNot => self.ops.push(Op::BitNot { dst, a }),
UnaryOp::Void => {
self.ops.push(Op::LoadConst {
dst,
value: NanBox::undefined(),
});
}
UnaryOp::Delete => return Err(CompileError::Unsupported("delete")),
}
Ok(dst)
}
Expr::Binary {
op, left, right, ..
} => {
if matches!(op, BinaryOp::Instanceof)
&& let Expr::Ident(cls) = &**right
&& self.classes.contains_key(&*cls.name)
{
let target_name = &*cls.name;
let mut ids: Vec<u32> = Vec::new();
for (name, info) in self.classes.iter() {
let mut cur = Some(name.clone());
while let Some(n) = cur {
if n == target_name {
ids.push(info.class_id);
break;
}
cur = self.classes.get(&n).and_then(|c| c.super_name.clone());
}
}
let obj = self.expr(left)?;
let dst = self.alloc();
self.ops.push(Op::InstanceOf {
dst,
obj,
ids: ids.into(),
});
return Ok(dst);
}
if matches!(op, BinaryOp::Instanceof)
&& let Expr::Ident(cls) = &**right
&& matches!(
&*cls.name,
"Error"
| "TypeError"
| "RangeError"
| "SyntaxError"
| "ReferenceError"
| "EvalError"
| "URIError"
)
{
let obj = self.expr(left)?;
let name = self
.member_read(obj, &PropertyKey::Ident(alloc::boxed::Box::from("name")))?;
let want = self.constant_str(&cls.name);
let dst = self.alloc();
if &*cls.name == "Error" {
let suffix = self.constant_str("Error");
self.ops.push(Op::CallMethod {
dst,
recv: name,
key: String::from("endsWith"),
args: alloc::vec![suffix],
});
} else {
self.ops.push(Op::StrictEq {
dst,
a: name,
b: want,
});
}
return Ok(dst);
}
if matches!(op, BinaryOp::Instanceof)
&& let Expr::Ident(cls) = &**right
&& let Some(kind) = match &*cls.name {
"RegExp" => Some(0u8),
"Array" => Some(1),
"Map" => Some(2),
"Set" => Some(3),
_ => None,
}
&& self.classes.get(&*cls.name).is_none()
{
let obj = self.expr(left)?;
let dst = self.alloc();
self.ops.push(Op::IsBuiltin { dst, obj, kind });
return Ok(dst);
}
let a = self.expr(left)?;
let b = self.expr(right)?;
if matches!(op, BinaryOp::In) {
let dst = self.alloc();
self.ops.push(Op::HasProp {
dst,
key: a,
obj: b,
});
return Ok(dst);
}
self.emit_binop(*op, a, b)
}
Expr::Logical {
op, left, right, ..
} => {
let l = self.expr(left)?;
let dst = self.alloc();
self.ops.push(Op::Move { dst, src: l });
let guard = match op {
LogicalOp::And => dst,
LogicalOp::Or => {
let n = self.alloc();
self.ops.push(Op::Not { dst: n, a: dst });
n
}
LogicalOp::Nullish => {
let nn = self.emit_not_nullish(dst)?;
let g = self.alloc();
self.ops.push(Op::Not { dst: g, a: nn });
g
}
};
let jf = self.emit_jump_if_false(guard);
let r = self.expr(right)?;
self.ops.push(Op::Move { dst, src: r });
self.patch(jf);
Ok(dst)
}
Expr::Conditional {
test,
consequent,
alternate,
..
} => {
let cond = self.expr(test)?;
let dst = self.alloc();
let jf = self.emit_jump_if_false(cond);
let c = self.expr(consequent)?;
self.ops.push(Op::Move { dst, src: c });
let jend = self.emit_jump();
self.patch(jf);
let a = self.expr(alternate)?;
self.ops.push(Op::Move { dst, src: a });
self.patch(jend);
Ok(dst)
}
Expr::Array { elements, .. } => {
let dst = self.alloc();
self.ops.push(Op::NewArray { dst, len: 0 });
for el in elements {
match el {
ArrayElement::Item(e) => {
let v = self.expr(e)?;
self.ops.push(Op::ArrayPush { arr: dst, src: v });
}
ArrayElement::Hole => {
let u = self.constant(NanBox::undefined())?;
self.ops.push(Op::ArrayPush { arr: dst, src: u });
}
ArrayElement::Spread(e) => {
let s = self.expr(e)?;
self.ops.push(Op::ArrayExtend { arr: dst, src: s });
}
}
}
Ok(dst)
}
Expr::Object { members, .. } => {
let dst = self.alloc();
self.ops.push(Op::NewObject { dst });
for m in members {
match m {
ObjectMember::Property { key, value, .. } => {
let v = self.expr(value)?;
match key {
PropertyKey::Computed(e) => {
let k = self.expr(e)?;
self.ops.push(Op::SetKey {
obj: dst,
key: k,
src: v,
});
}
_ => self.ops.push(Op::SetProp {
obj: dst,
key: static_key(key)?,
src: v,
}),
}
}
ObjectMember::Spread { value, .. } => {
let src = self.expr(value)?;
self.ops.push(Op::ObjectSpread { dst, src });
}
ObjectMember::Accessor {
is_getter,
key,
value,
..
} => {
let key = static_key(key)?;
let f = self.make_closure(&value.params, &value.body, false, "")?;
let undef = self.constant(NanBox::undefined())?;
let (getter, setter) = if *is_getter { (f, undef) } else { (undef, f) };
self.ops.push(Op::DefineAccessor {
obj: dst,
key,
getter,
setter,
});
}
}
}
Ok(dst)
}
Expr::Member {
object,
property,
optional,
..
} => {
let obj = self.expr(object)?;
if *optional {
let go = self.emit_not_nullish(obj)?;
let jf = self.emit_jump_if_false(go); if self.optchain_ends.is_empty() {
let dst = self.alloc();
self.ops.push(Op::LoadConst {
dst,
value: NanBox::undefined(),
});
let v = self.member_read(obj, property)?;
self.ops.push(Op::Move { dst, src: v });
self.patch(jf);
Ok(dst)
} else {
self.optchain_ends.last_mut().unwrap().push(jf);
self.member_read(obj, property)
}
} else {
self.member_read(obj, property)
}
}
Expr::Call {
callee, arguments, ..
} => {
let mut args = Vec::with_capacity(arguments.len());
for a in arguments {
let crate::ast::Argument::Item(e) = a else {
return Err(CompileError::Unsupported("spread argument"));
};
args.push(self.expr(e)?);
}
if matches!(&**callee, Expr::Super(_)) {
if let Some(ctor) = self.super_ctor {
let recv = self.this_reg;
self.ops.push(Op::CallCtor { ctor, recv, args });
} else if self.super_class.is_none() {
return Err(CompileError::Unsupported("super outside a subclass ctor"));
}
return Ok(self.this_reg);
}
if let Some(native) = native_call(callee).or_else(|| native_global(callee)) {
let dst = self.alloc();
self.ops.push(Op::CallNative { dst, native, args });
return Ok(dst);
}
if let Expr::Ident(id) = &**callee
&& self.lookup(&id.name).is_none()
&& let Some(&func) = self.fn_ids.get(&*id.name)
{
let dst = self.alloc();
self.ops.push(Op::Call { dst, func, args });
return Ok(dst);
}
if let Expr::Member {
object,
property: PropertyKey::Ident(key) | PropertyKey::Str(key),
..
} = &**callee
&& matches!(&**object, Expr::Super(_))
{
let sup = self
.super_class
.clone()
.ok_or(CompileError::Unsupported("super outside a subclass"))?;
let func = self
.resolve_method(&sup, key)
.ok_or(CompileError::Unsupported("super method not found"))?;
let m = self.alloc();
self.ops.push(Op::LoadFunc { dst: m, func });
let recv = self.this_reg;
let dst = self.alloc();
self.ops.push(Op::CallValueThis {
dst,
callee: m,
recv,
args,
});
return Ok(dst);
}
if let Expr::Member {
object,
property: PropertyKey::Ident(key) | PropertyKey::Str(key),
..
} = &**callee
&& let Expr::Ident(cn) = &**object
&& self.lookup(&cn.name).is_none()
&& let Some(sid) = self.classes.get(&*cn.name).and_then(|info| {
info.statics
.iter()
.find(|(n, _)| n == &**key)
.map(|(_, id)| *id)
})
{
let dst = self.alloc();
self.ops.push(Op::Call {
dst,
func: sid,
args,
});
return Ok(dst);
}
if let Expr::Member {
object,
property: PropertyKey::Ident(key) | PropertyKey::Str(key),
..
} = &**callee
{
let recv = self.expr(object)?;
let dst = self.alloc();
self.ops.push(Op::CallMethod {
dst,
recv,
key: String::from(&**key),
args,
});
return Ok(dst);
}
let callee_reg = self.expr(callee)?;
let dst = self.alloc();
self.ops.push(Op::CallValue {
dst,
callee: callee_reg,
args,
});
Ok(dst)
}
Expr::Assign {
op, target, value, ..
} => {
use crate::ast::AssignOp;
if let Expr::Ident(id) = &**target
&& self.lookup(&id.name).is_some_and(|b| b.konst)
{
return Err(CompileError::Unsupported("assignment to const"));
}
if matches!(
op,
AssignOp::AndAssign | AssignOp::OrAssign | AssignOp::NullishAssign
) {
let cond = |this: &mut Self, cur: Reg| -> Result<Reg, CompileError> {
Ok(match op {
AssignOp::AndAssign => cur,
AssignOp::OrAssign => {
let n = this.alloc();
this.ops.push(Op::Not { dst: n, a: cur });
n
}
_ => {
let nn = this.emit_not_nullish(cur)?;
let g = this.alloc();
this.ops.push(Op::Not { dst: g, a: nn });
g
}
})
};
match &**target {
Expr::Ident(id) => {
let b = self
.lookup(&id.name)
.ok_or_else(|| CompileError::Undefined(String::from(&*id.name)))?;
let cur = self.read_var(b);
let c = cond(self, cur)?;
let jf = self.emit_jump_if_false(c);
let v = self.expr(value)?;
self.write_var(b, v);
self.patch(jf);
return Ok(self.read_var(b));
}
Expr::Member {
object, property, ..
} => {
let obj = self.expr(object)?;
let cur = self.member_read(obj, property)?;
let c = cond(self, cur)?;
let jf = self.emit_jump_if_false(c);
let v = self.expr(value)?;
self.member_write(obj, property, v)?;
self.patch(jf);
return self.member_read(obj, property);
}
_ => return Err(CompileError::Unsupported("logical assign target")),
}
}
let compound = !matches!(op, AssignOp::Assign);
match &**target {
Expr::Ident(id) => {
let b = self
.lookup(&id.name)
.ok_or_else(|| CompileError::Undefined(String::from(&*id.name)))?;
let v = self.expr(value)?;
let src = if compound {
let cur = self.read_var(b);
self.emit_binop(Self::compound_binop(*op)?, cur, v)?
} else {
v
};
self.write_var(b, src);
Ok(src)
}
Expr::Member {
object, property, ..
} => {
let obj = self.expr(object)?;
let v = self.expr(value)?;
let src = if compound {
let cur = self.member_read(obj, property)?;
self.emit_binop(Self::compound_binop(*op)?, cur, v)?
} else {
v
};
self.member_write(obj, property, src)?;
Ok(src)
}
Expr::Array { .. } | Expr::Object { .. } if !compound => {
let v = self.expr(value)?;
self.assign_pattern(target, v)?;
Ok(v)
}
_ => Err(CompileError::Unsupported("assignment target")),
}
}
Expr::Update {
op,
prefix,
argument,
..
} => {
let Expr::Ident(id) = &**argument else {
return Err(CompileError::Unsupported("update target"));
};
let b = self
.lookup(&id.name)
.ok_or_else(|| CompileError::Undefined(String::from(&*id.name)))?;
let one = self.constant(NanBox::number(1.0))?;
let bop = match op {
crate::ast::UpdateOp::Inc => BinaryOp::Add,
crate::ast::UpdateOp::Dec => BinaryOp::Sub,
};
let raw = self.read_var(b);
let cur = self.alloc();
self.ops.push(Op::CallNative {
dst: cur,
native: NB_NUMBER,
args: alloc::vec![raw],
});
let old = self.alloc();
self.ops.push(Op::Move { dst: old, src: cur });
let next = self.emit_binop(bop, cur, one)?;
self.write_var(b, next);
Ok(if *prefix { next } else { old })
}
Expr::This(_) => Ok(self.this_reg),
Expr::Regex { pattern, flags, .. } => {
let dst = self.alloc();
self.ops.push(Op::NewRegExp {
dst,
source: String::from(&**pattern),
flags: String::from(&**flags),
});
Ok(dst)
}
Expr::Sequence { expressions, .. } => {
let mut last = self.constant(NanBox::undefined())?;
for e in expressions {
last = self.expr(e)?;
}
Ok(last)
}
Expr::TaggedTemplate { tag, quasi, .. } => {
let strings = self.alloc();
self.ops.push(Op::NewArray {
dst: strings,
len: 0,
});
for q in &quasi.quasis {
let s = match q.cooked.as_deref() {
Some(c) => self.constant_str(c),
None => self.constant(NanBox::undefined())?,
};
self.ops.push(Op::ArrayPush {
arr: strings,
src: s,
});
}
let mut args = alloc::vec![strings];
for e in &quasi.expressions {
args.push(self.expr(e)?);
}
let dst = self.alloc();
if let Expr::Ident(id) = &**tag
&& self.lookup(&id.name).is_none()
&& let Some(&func) = self.fn_ids.get(&*id.name)
{
self.ops.push(Op::Call { dst, func, args });
} else {
let callee = self.expr(tag)?;
self.ops.push(Op::CallValue { dst, callee, args });
}
Ok(dst)
}
Expr::New {
callee, arguments, ..
} => {
let Expr::Ident(id) = &**callee else {
return Err(CompileError::Unsupported("new on non-class"));
};
if (&*id.name == "Map" || &*id.name == "Set")
&& self.classes.get(&*id.name).is_none()
{
let is_set = &*id.name == "Set";
let seed = match arguments.first() {
Some(crate::ast::Argument::Item(e)) => Some(self.expr(e)?),
_ => None,
};
let dst = self.alloc();
self.ops.push(Op::NewCollection { dst, is_set, seed });
return Ok(dst);
}
if self.classes.get(&*id.name).is_none()
&& matches!(
&*id.name,
"Error"
| "TypeError"
| "RangeError"
| "SyntaxError"
| "ReferenceError"
| "EvalError"
| "URIError"
)
{
let msg = match arguments.first() {
Some(crate::ast::Argument::Item(e)) => self.expr(e)?,
_ => self.constant_str(""),
};
let dst = self.alloc();
self.ops.push(Op::NewObject { dst });
let name = self.constant_str(&id.name);
self.ops.push(Op::SetProp {
obj: dst,
key: String::from("name"),
src: name,
});
self.ops.push(Op::SetProp {
obj: dst,
key: String::from("message"),
src: msg,
});
return Ok(dst);
}
if &*id.name == "RegExp" && self.classes.get("RegExp").is_none() {
let lit = |a: Option<&crate::ast::Argument>| match a {
Some(crate::ast::Argument::Item(Expr::Str { value, .. })) => {
Some(String::from(&**value))
}
None => Some(String::new()),
_ => None,
};
if let (Some(source), Some(flags)) =
(lit(arguments.first()), lit(arguments.get(1)))
{
let dst = self.alloc();
self.ops.push(Op::NewRegExp { dst, source, flags });
return Ok(dst);
}
return Err(CompileError::Unsupported("new RegExp with dynamic args"));
}
if &*id.name == "Array" && self.classes.get("Array").is_none() {
let dst = self.alloc();
if arguments.len() == 1
&& let crate::ast::Argument::Item(e) = &arguments[0]
{
let arg = self.expr(e)?;
self.ops.push(Op::NewArrayCtor { dst, arg });
return Ok(dst);
}
self.ops.push(Op::NewArray { dst, len: 0 });
for a in arguments {
if let crate::ast::Argument::Item(e) = a {
let v = self.expr(e)?;
self.ops.push(Op::ArrayPush { arr: dst, src: v });
}
}
return Ok(dst);
}
let Some(info) = self.classes.get(&*id.name).cloned() else {
return Err(CompileError::Unsupported("new on unknown class"));
};
let mut args = Vec::with_capacity(arguments.len());
for a in arguments {
let crate::ast::Argument::Item(e) = a else {
return Err(CompileError::Unsupported("spread argument"));
};
args.push(self.expr(e)?);
}
let instance = self.alloc();
self.ops.push(Op::NewObject { dst: instance });
self.ops.push(Op::SetClassTag {
obj: instance,
class_id: info.class_id,
});
let mut chain: Vec<String> = Vec::new();
let mut cur = Some(String::from(&*id.name));
while let Some(name) = cur {
cur = self.classes.get(&name).and_then(|c| c.super_name.clone());
chain.push(name);
}
for name in chain.iter().rev() {
let Some(cls) = self.classes.get(name) else {
return Err(CompileError::Unsupported("extends a native class"));
};
let methods = cls.methods.clone();
let accessors = cls.accessors.clone();
for (mname, mid) in &methods {
let m = self.alloc();
self.ops.push(Op::LoadFunc { dst: m, func: *mid });
self.ops.push(Op::SetProp {
obj: instance,
key: mname.clone(),
src: m,
});
}
for (aname, getter_id, setter_id) in &accessors {
let load = |this: &mut Self, id: Option<u32>| match id {
Some(fid) => {
let r = this.alloc();
this.ops.push(Op::LoadFunc { dst: r, func: fid });
r
}
None => this.constant(NanBox::undefined()).expect("const"),
};
let getter = load(self, *getter_id);
let setter = load(self, *setter_id);
self.ops.push(Op::DefineAccessor {
obj: instance,
key: aname.clone(),
getter,
setter,
});
}
}
if let Some(ctor) = nearest_ctor(&id.name, &self.classes) {
self.ops.push(Op::CallCtor {
ctor,
recv: instance,
args,
});
}
Ok(instance)
}
Expr::Template(t) => {
if t.quasis.iter().any(|q| q.cooked.is_none()) {
return Err(CompileError::Unsupported(
"invalid escape in template literal",
));
}
let cooked = |q: &crate::ast::TemplateElement| -> String {
q.cooked.as_deref().map(String::from).unwrap_or_default()
};
let mut acc = self.alloc();
self.ops.push(Op::NewString {
dst: acc,
value: t.quasis.first().map(cooked).unwrap_or_default(),
});
for (i, e) in t.expressions.iter().enumerate() {
let v = self.expr(e)?;
let s1 = self.alloc();
self.ops.push(Op::AddValue {
dst: s1,
a: acc,
b: v,
});
let q = self.alloc();
self.ops.push(Op::NewString {
dst: q,
value: t.quasis.get(i + 1).map(cooked).unwrap_or_default(),
});
acc = self.alloc();
self.ops.push(Op::AddValue {
dst: acc,
a: s1,
b: q,
});
}
Ok(acc)
}
Expr::Function(f) => {
let nm = f.id.as_ref().map_or("", |i| i.name.as_ref());
self.make_closure(&f.params, &f.body, f.is_async, nm)
}
Expr::Arrow(a) => {
let body: Vec<Stmt> = match &a.body {
crate::ast::ArrowBody::Block(b) => b.clone(),
crate::ast::ArrowBody::Expr(e) => alloc::vec![Stmt::Return {
argument: Some(Box::new((**e).clone())),
span: crate::common::Span::point(0),
}],
};
self.make_closure(&a.params, &body, a.is_async, "")
}
Expr::OptChain { expr, .. } => {
let result = self.alloc();
self.ops.push(Op::LoadConst {
dst: result,
value: NanBox::undefined(),
});
self.optchain_ends.push(Vec::new());
let v = self.expr(expr)?;
self.ops.push(Op::Move {
dst: result,
src: v,
});
let sites = self.optchain_ends.pop().unwrap_or_default();
let end = self.ops.len();
for s in sites {
self.patch_to(s, end);
}
Ok(result)
}
_ => Err(CompileError::Unsupported("expression")),
}
}
fn expr_named(&mut self, e: &Expr, target: &BindingTarget) -> Result<Reg, CompileError> {
if let BindingTarget::Ident(id) = target {
match e {
Expr::Function(f) if f.id.is_none() => {
return self.make_closure(&f.params, &f.body, f.is_async, id.name.as_ref());
}
Expr::Arrow(a) => {
let body: Vec<Stmt> = match &a.body {
crate::ast::ArrowBody::Block(b) => b.clone(),
crate::ast::ArrowBody::Expr(ex) => alloc::vec![Stmt::Return {
argument: Some(Box::new((**ex).clone())),
span: crate::common::Span::point(0),
}],
};
return self.make_closure(&a.params, &body, a.is_async, id.name.as_ref());
}
_ => {}
}
}
self.expr(e)
}
fn make_closure(
&mut self,
params: &[crate::ast::Param],
body: &[Stmt],
is_async: bool,
name: &str,
) -> Result<Reg, CompileError> {
let free = free_of_function(params, body);
let captures: Vec<String> = free
.into_iter()
.filter(|n| self.lookup(n).is_some())
.collect();
let id = {
let mut p = self.protos.borrow_mut();
p.push(FnProto {
ops: Vec::new(),
n_regs: 0,
n_params: 0,
n_captures: 0,
rest_from: None,
is_async: false,
length: 0,
name: alloc::string::String::new(),
});
(p.len() - 1) as u32
};
let proto = Compiler::compile_fn_inner(
&self.fn_ids,
&self.classes,
&self.protos,
params,
&captures,
body,
false,
None,
&[],
None,
is_async,
)?;
let mut proto = proto;
proto.name = alloc::string::String::from(name);
self.protos.borrow_mut()[id as usize] = proto;
let capture_regs: Vec<Reg> = captures
.iter()
.map(|n| self.lookup(n).expect("captured binding").reg)
.collect();
let dst = self.alloc();
self.ops.push(Op::MakeClosure {
dst,
func: id,
captures: capture_regs,
});
Ok(dst)
}
fn constant(&mut self, value: NanBox) -> Result<Reg, CompileError> {
let r = self.alloc();
self.ops.push(Op::LoadConst { dst: r, value });
Ok(r)
}
fn emit_throw_error(&mut self, error_name: &str, message: &str) -> Reg {
let obj = self.alloc();
self.ops.push(Op::NewObject { dst: obj });
let name = self.constant_str(error_name);
self.ops.push(Op::SetProp {
obj,
key: String::from("name"),
src: name,
});
let msg = self.constant_str(message);
self.ops.push(Op::SetProp {
obj,
key: String::from("message"),
src: msg,
});
self.ops.push(Op::Throw { src: obj });
obj
}
fn constant_str(&mut self, s: &str) -> Reg {
let r = self.alloc();
self.ops.push(Op::NewString {
dst: r,
value: String::from(s),
});
r
}
fn emit_not_nullish(&mut self, v: Reg) -> Result<Reg, CompileError> {
let null = self.constant(NanBox::null())?;
let undef = self.constant(NanBox::undefined())?;
let is_null = self.alloc();
self.ops.push(Op::StrictEq {
dst: is_null,
a: v,
b: null,
});
let is_undef = self.alloc();
self.ops.push(Op::StrictEq {
dst: is_undef,
a: v,
b: undef,
});
let go = self.alloc();
let not_null = self.alloc();
self.ops.push(Op::Not {
dst: not_null,
a: is_null,
});
let not_undef = self.alloc();
self.ops.push(Op::Not {
dst: not_undef,
a: is_undef,
});
self.ops.push(Op::Move {
dst: go,
src: not_null,
});
let jf = self.emit_jump_if_false(go);
self.ops.push(Op::Move {
dst: go,
src: not_undef,
});
self.patch(jf);
Ok(go)
}
fn block_stmts(&mut self, stmts: &'_ [Stmt]) -> Result<(), CompileError> {
self.scopes.push(alloc::collections::BTreeMap::new());
for s in stmts {
self.stmt(s)?;
}
self.scopes.pop();
Ok(())
}
fn emit_binop(&mut self, op: BinaryOp, a: Reg, b: Reg) -> Result<Reg, CompileError> {
let dst = self.alloc();
match op {
BinaryOp::Add => self.ops.push(Op::AddValue { dst, a, b }),
BinaryOp::Sub => self.ops.push(Op::Sub { dst, a, b }),
BinaryOp::Mul => self.ops.push(Op::Mul { dst, a, b }),
BinaryOp::Div => self.ops.push(Op::Div { dst, a, b }),
BinaryOp::Mod => self.ops.push(Op::Mod { dst, a, b }),
BinaryOp::Lt => self.ops.push(Op::Lt { dst, a, b }),
BinaryOp::Gt => self.ops.push(Op::Lt { dst, a: b, b: a }),
BinaryOp::LtEq => {
self.ops.push(Op::Lt { dst, a: b, b: a });
self.ops.push(Op::Not { dst, a: dst });
}
BinaryOp::GtEq => {
self.ops.push(Op::Lt { dst, a, b });
self.ops.push(Op::Not { dst, a: dst });
}
BinaryOp::EqEqEq => self.ops.push(Op::StrictEq { dst, a, b }),
BinaryOp::NotEqEq => {
self.ops.push(Op::StrictEq { dst, a, b });
self.ops.push(Op::Not { dst, a: dst });
}
BinaryOp::Exp => self.ops.push(Op::ValueBin {
dst,
op: VB_POW,
a,
b,
}),
BinaryOp::BitAnd => self.ops.push(Op::ValueBin {
dst,
op: VB_BIT_AND,
a,
b,
}),
BinaryOp::BitOr => self.ops.push(Op::ValueBin {
dst,
op: VB_BIT_OR,
a,
b,
}),
BinaryOp::BitXor => self.ops.push(Op::ValueBin {
dst,
op: VB_BIT_XOR,
a,
b,
}),
BinaryOp::Shl => self.ops.push(Op::ValueBin {
dst,
op: VB_SHL,
a,
b,
}),
BinaryOp::Shr => self.ops.push(Op::ValueBin {
dst,
op: VB_SHR,
a,
b,
}),
BinaryOp::Ushr => self.ops.push(Op::ValueBin {
dst,
op: VB_USHR,
a,
b,
}),
BinaryOp::EqEq => self.ops.push(Op::ValueBin {
dst,
op: VB_LOOSE_EQ,
a,
b,
}),
BinaryOp::NotEq => self.ops.push(Op::ValueBin {
dst,
op: VB_LOOSE_NEQ,
a,
b,
}),
BinaryOp::In | BinaryOp::Instanceof => {
return Err(CompileError::Unsupported("in / instanceof"));
}
}
Ok(dst)
}
fn compound_binop(op: crate::ast::AssignOp) -> Result<BinaryOp, CompileError> {
use crate::ast::AssignOp;
Ok(match op {
AssignOp::AddAssign => BinaryOp::Add,
AssignOp::SubAssign => BinaryOp::Sub,
AssignOp::MulAssign => BinaryOp::Mul,
AssignOp::DivAssign => BinaryOp::Div,
AssignOp::ModAssign => BinaryOp::Mod,
AssignOp::ExpAssign => BinaryOp::Exp,
AssignOp::ShlAssign => BinaryOp::Shl,
AssignOp::ShrAssign => BinaryOp::Shr,
AssignOp::UshrAssign => BinaryOp::Ushr,
AssignOp::BitAndAssign => BinaryOp::BitAnd,
AssignOp::BitOrAssign => BinaryOp::BitOr,
AssignOp::BitXorAssign => BinaryOp::BitXor,
_ => return Err(CompileError::Unsupported("compound assignment operator")),
})
}
fn member_read(&mut self, obj: Reg, property: &PropertyKey) -> Result<Reg, CompileError> {
let dst = self.alloc();
match property {
PropertyKey::Computed(e) => {
let key = self.expr(e)?;
self.ops.push(Op::GetKey { dst, obj, key });
}
_ => {
let key = static_key(property)?;
if key == "length" {
self.ops.push(Op::ArrayLen { dst, arr: obj });
} else if key == "size" {
self.ops.push(Op::CollectionSize { dst, recv: obj });
} else {
self.ops.push(Op::GetProp { dst, obj, key });
}
}
}
Ok(dst)
}
fn resolve_method(&self, class: &str, name: &str) -> Option<u32> {
let mut cur = Some(String::from(class));
while let Some(cname) = cur {
let info = self.classes.get(&cname)?;
if let Some((_, id)) = info.methods.iter().find(|(n, _)| n == name) {
return Some(*id);
}
cur = info.super_name.clone();
}
None
}
fn materialize_class(
&mut self,
name: &str,
class: &crate::ast::Class,
) -> Result<(), CompileError> {
let info = match self.classes.get(name) {
Some(i) => i.clone(),
None => return Ok(()), };
let cobj = self.alloc();
self.ops.push(Op::NewObject { dst: cobj });
let fn_tag = self.constant(NanBox::boolean(true))?;
self.ops.push(Op::SetProp {
obj: cobj,
key: String::from("\u{0}vmfn"),
src: fn_tag,
});
for (sname, sid) in &info.statics {
let f = self.alloc();
self.ops.push(Op::LoadFunc { dst: f, func: *sid });
self.ops.push(Op::SetProp {
obj: cobj,
key: sname.clone(),
src: f,
});
}
for member in &class.body {
if let crate::ast::ClassMember::Field(f) = member
&& f.is_static
{
let key = static_key(&f.key)?;
let v = match &f.value {
Some(e) => self.expr(e)?,
None => self.constant(NanBox::undefined())?,
};
self.ops.push(Op::SetProp {
obj: cobj,
key,
src: v,
});
}
}
let b = self.declare(name);
self.write_var(b, cobj);
Ok(())
}
fn member_write(
&mut self,
obj: Reg,
property: &PropertyKey,
src: Reg,
) -> Result<(), CompileError> {
match property {
PropertyKey::Computed(e) => {
let key = self.expr(e)?;
self.ops.push(Op::SetKey { obj, key, src });
}
_ => {
let key = static_key(property)?;
self.ops.push(Op::SetProp { obj, key, src });
}
}
Ok(())
}
fn emit_jump_if_false(&mut self, cond: Reg) -> usize {
let i = self.ops.len();
self.ops.push(Op::JumpIfFalse { cond, target: 0 });
i
}
fn emit_jump(&mut self) -> usize {
let i = self.ops.len();
self.ops.push(Op::Jump { target: 0 });
i
}
fn patch(&mut self, idx: usize) {
let target = self.ops.len();
self.patch_to(idx, target);
}
fn enter_loop(&mut self) {
self.break_sites.push(Vec::new());
self.continue_sites.push(Vec::new());
}
fn exit_loop(&mut self, continue_target: usize) {
let breaks = self.break_sites.pop().unwrap_or_default();
let continues = self.continue_sites.pop().unwrap_or_default();
let end = self.ops.len();
for b in breaks {
self.patch_to(b, end);
}
for c in continues {
self.patch_to(c, continue_target);
}
}
fn patch_to(&mut self, idx: usize, target: usize) {
match &mut self.ops[idx] {
Op::JumpIfFalse { target: t, .. }
| Op::Jump { target: t }
| Op::PushHandler { target: t, .. } => *t = target,
_ => unreachable!("patch a non-jump"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bc(src: &str) -> String {
let program = crate::parser::Parser::parse_program(src).expect("parse");
let mut realm = Realm::new();
let value = compile_and_run(&mut realm, &program).expect("compile+run");
realm.to_display_string(value)
}
#[test]
fn optional_chain_short_circuits_whole_chain() {
assert_eq!(bc("let n = null; n?.a.b.c"), "undefined");
assert_eq!(bc("let o = {a:{b:7}}; o?.a?.b"), "7");
assert_eq!(bc("let o = {a:{b:7}}; o?.x?.y.z"), "undefined");
assert_eq!(bc("let n = null; n?.a.b ?? 42"), "42");
assert_eq!(
bc("let o = {a:{}}; let r; try { r = o.a?.zzz.qqq; } catch (e) { r = 'threw'; } r"),
"threw"
);
}
#[test]
fn hot_integer_function_matches_interpreter() {
assert_eq!(
bc("function f(a,b){ return a*b + a - b; } \
let t = 0; \
for (let i = 0; i < 40; i = i + 1) { t = t + f(i, 2); } \
t"),
"2260"
);
assert_eq!(
bc("function sq(x){ return x*x; } \
let r = 0; \
for (let i = 0; i < 20; i = i + 1) { r = sq(100000000); } \
r"),
"10000000000000000"
);
assert_eq!(
bc("function g(a,b){ return a + b; } \
let s = 0; \
for (let i = 0; i < 30; i = i + 1) { s = g(i, 0.5); } \
s"),
"29.5"
);
}
#[test]
fn hot_function_with_a_call_matches_interpreter() {
assert_eq!(
bc("function triple(x){ return x*3; } \
function f(x){ return triple(x) + x; } \
let t = 0; \
for (let i = 0; i < 40; i = i + 1) { t = t + f(i); } \
t"),
"3120"
);
assert_eq!(
bc("function a(x){ return x+1; } \
function b(x){ return a(x)*2; } \
function c(x){ return b(x) + a(x); } \
let t = 0; \
for (let i = 0; i < 30; i = i + 1) { t = t + c(i); } \
t"),
"1395"
);
assert_eq!(
bc("function sq(x){ return x*x; } \
function h(x){ return sq(x) + 1; } \
let r = 0; \
for (let i = 0; i < 20; i = i + 1) { r = h(100000000); } \
r"),
"10000000000000000"
);
}
#[test]
fn hot_function_with_not_matches_interpreter() {
assert_eq!(
bc("function le(a,b){ return a <= b ? 1 : 0; } \
let c = 0; \
for (let i = 0; i < 40; i = i + 1) { c = c + le(i, 20); } \
c"),
"21"
);
assert_eq!(
bc("function ge(x){ return x >= 10 ? x : 0; } \
let s = 0; \
for (let i = 0; i < 20; i = i + 1) { s = s + ge(i); } \
s"),
"145"
);
}
#[test]
fn hot_function_with_strict_eq_matches_interpreter() {
assert_eq!(
bc("function is7(x){ return x === 7 ? 1 : 0; } \
let c = 0; \
for (let i = 0; i < 40; i = i + 1) { c = c + is7(i); } \
c"),
"1"
);
assert_eq!(
bc("function nz(x){ return x !== 0 ? 1 : 0; } \
let c = 0; \
for (let i = 0; i < 40; i = i + 1) { c = c + nz(i); } \
c"),
"39"
);
assert_eq!(
bc("function eq3(x){ return x == 3 ? 1 : 0; } \
let c = 0; \
for (let i = 0; i < 40; i = i + 1) { c = c + eq3(i); } \
c"),
"1"
);
}
#[test]
fn hot_function_with_neg_matches_interpreter() {
assert_eq!(
bc("function g(x){ return -x + 100; } \
let s = 0; \
for (let i = 0; i < 20; i = i + 1) { s = s + g(i); } \
s"),
"1810"
);
}
#[test]
fn hot_function_with_bitwise_matches_interpreter() {
assert_eq!(
bc("function mask(x){ return (x & 7) | 16; } \
let s = 0; \
for (let i = 0; i < 40; i = i + 1) { s = s + mask(i); } \
s"),
"780"
);
assert_eq!(
bc("function rt(x){ return (x ^ 255) ^ 255; } \
let s = 0; \
for (let i = 0; i < 20; i = i + 1) { s = s + rt(i); } \
s"),
"190"
);
}
#[test]
fn hot_function_with_mod_matches_interpreter() {
assert_eq!(
bc("function isMul7(x){ return (x % 7) == 0 ? 1 : 0; } \
let c = 0; \
for (let i = 0; i < 40; i = i + 1) { c = c + isMul7(i); } \
c"),
"6"
);
assert_eq!(
bc("function r(x){ return x % 10; } \
let s = 0; \
for (let i = 0; i < 20; i = i + 1) { s = s + r(i); } \
s"),
"90"
);
}
#[test]
fn hot_function_with_bitnot_matches_interpreter() {
assert_eq!(
bc("function f(x){ return (~x) + x; } \
let s = 0; \
for (let i = 0; i < 20; i = i + 1) { s = s + f(i); } \
s"),
"-20"
);
}
#[test]
fn hot_function_with_shifts_matches_interpreter() {
assert_eq!(
bc("function f(x){ return (x << 2) + (x >> 1); } \
let s = 0; \
for (let i = 0; i < 20; i = i + 1) { s = s + f(i); } \
s"),
"850"
);
}
#[test]
fn hot_float_function_matches_interpreter() {
assert_eq!(
bc("function ratio(a,b){ return (a + b) / b; } \
let r = 0; \
for (let i = 0; i < 30; i = i + 1) { r = ratio(3, 2); } \
r"),
"2.5"
);
assert_eq!(
bc("function half(x){ return x / 2; } \
let s = 0; \
for (let i = 0; i < 25; i = i + 1) { s = half(7); } \
s"),
"3.5"
);
assert_eq!(
bc(
"function tri(n){ let s = 0.0; for (let x = 0.0; x < n; x = x + 0.5) { s = s + x; } return s; } \
let r = 0; \
for (let i = 0; i < 20; i = i + 1) { r = tri(3); } \
r"
),
"7.5"
);
assert_eq!(
bc("function g(x){ return -x + 0.5; } \
let s = 0; \
for (let i = 0; i < 25; i = i + 1) { s = g(0.5); } \
s"),
"0"
);
assert_eq!(
bc("function le(a,b){ return a <= b ? 1 : 0; } \
let c = 0; \
for (let x = 0.0; x < 10.0; x = x + 0.5) { c = c + le(x, 4.5); } \
c"),
"10"
);
assert_eq!(
bc("function eq(a,b){ return a === b ? 1 : 0; } \
let c = 0; \
for (let x = 0.0; x < 5.0; x = x + 0.5) { c = c + eq(x, 2.5); } \
c"),
"1"
);
assert_eq!(
bc("function h(x){ return Math.sqrt(Math.abs(x)); } \
let r = 0; \
for (let i = 0; i < 40; i = i + 1) { r = h(-6.25); } \
r"),
"2.5"
);
assert_eq!(
bc(
"function clamp(x){ return Math.max(0.0, Math.min(10.0, x)); } \
let r = 0; \
for (let i = 0; i < 40; i = i + 1) { r = clamp(13.5); } \
let r2 = 0; \
for (let j = 0; j < 40; j = j + 1) { r2 = clamp(-2.5); } \
r + r2"
),
"10"
);
assert_eq!(
bc(
"function fc(x){ return Math.floor(x) + Math.ceil(x + 0.5); } \
let r = 0; \
for (let i = 0; i < 40; i = i + 1) { r = fc(3.2); } \
r"
),
"7"
);
}
#[test]
fn bytecode_valueof_in_operators() {
assert_eq!(bc("let m={valueOf(){return 5;}}; m - 2"), "3");
assert_eq!(bc("let m={valueOf(){return 5;}}; m + 1"), "6");
assert_eq!(bc("let m={valueOf(){return 5;}}; ~m"), "-6");
assert_eq!(bc("let m={valueOf(){return 5;}}; -m"), "-5");
assert_eq!(bc("let m={valueOf(){return 5;}}; m & 3"), "1");
assert_eq!(bc("let s={toString(){return 'x';}}; s + '!'"), "x!");
assert_eq!(bc("let x='5'; ++x"), "6");
assert_eq!(bc("let x='3'; let y=x++; y + ',' + x"), "3,4");
}
#[test]
fn bytecode_arithmetic_object_coercion() {
assert_eq!(bc("[5] - 2"), "3");
assert_eq!(bc("[10] / 2"), "5");
assert_eq!(bc("[10] % 3"), "1");
assert_eq!(bc("[6] & 3"), "2");
assert_eq!(bc("[2] ** 3"), "8");
assert_eq!(bc("'5' - 2"), "3");
assert_eq!(bc("({a:1}) - 1"), "NaN");
}
#[test]
fn bytecode_relational_object_coercion() {
assert_eq!(bc("[5] < 10"), "true");
assert_eq!(bc("[1] < [2]"), "true");
assert_eq!(bc("[10] < [9]"), "true");
assert_eq!(bc("({}) < 1"), "false");
}
#[test]
fn bytecode_loose_eq_object_coercion() {
assert_eq!(bc("String([] == false)"), "true");
assert_eq!(bc("String([] == 0)"), "true");
assert_eq!(bc("String({} == 0)"), "false");
assert_eq!(bc("String({} == {})"), "false");
assert_eq!(bc("String([1,2] == '1,2')"), "true");
}
#[test]
fn bytecode_array_string_index() {
assert_eq!(bc("let a=[10,20,30]; a['1']"), "20");
assert_eq!(bc("let a=[10,20,30]; let k='2'; a[k]"), "30");
assert_eq!(bc("let a=[10,20,30]; String(a['00'])"), "undefined");
assert_eq!(bc("let a=[1,2,3]; a['length']"), "3");
}
#[test]
fn bytecode_regex_split_matches_tree_walker() {
assert_eq!(bc("'a1b2c3'.split(/(\\d)/).join(',')"), "a,1,b,2,c,3,");
assert_eq!(
bc("'camelCaseWord'.split(/(?=[A-Z])/).join('|')"),
"camel|Case|Word"
);
assert_eq!(bc("'aXbYc'.split(/[XY]/).join(',')"), "a,b,c");
}
#[test]
fn bytecode_arithmetic_and_precedence() {
assert_eq!(bc("2 + 3 * 4"), "14");
assert_eq!(bc("(2 + 3) * 4"), "20");
assert_eq!(bc("10 / 4"), "2.5");
assert_eq!(bc("-5 + 3"), "-2");
assert_eq!(bc("'a' + 'b' + 'c'"), "abc");
assert_eq!(bc("1 + 2; 3 + 4"), "7"); }
#[test]
fn bytecode_comparisons_and_logic() {
assert_eq!(bc("3 < 5"), "true");
assert_eq!(bc("5 <= 5"), "true");
assert_eq!(bc("7 > 2"), "true");
assert_eq!(bc("2 >= 9"), "false");
assert_eq!(bc("1 === 1"), "true");
assert_eq!(bc("1 !== 2"), "true");
assert_eq!(bc("!false"), "true");
assert_eq!(bc("true && 7"), "7");
assert_eq!(bc("false || 'fallback'"), "fallback");
assert_eq!(bc("0 && 9"), "0"); assert_eq!(bc("(3 > 2) ? 'yes' : 'no'"), "yes");
}
#[test]
fn bytecode_variables_and_control_flow() {
assert_eq!(
bc("let sum = 0; let i = 1; while (i <= 5) { sum = sum + i; i = i + 1; } sum"),
"15"
);
assert_eq!(
bc("let x = 7; let r = 0; if (x > 5) { r = 1; } else { r = 2; } r"),
"1"
);
assert_eq!(bc("let a = 1; { let a = 99; } a"), "1");
assert_eq!(
bc(
"let a = 0; let b = 1; let n = 10; while (n > 0) { let t = a + b; a = b; b = t; n = n - 1; } a"
),
"55"
);
}
fn bc_out(src: &str) -> String {
let program = crate::parser::Parser::parse_program(src).expect("parse");
let mut realm = Realm::new();
let (_, output) = compile_run_output(&mut realm, &program).expect("compile+run");
output
}
#[cfg(feature = "std")]
#[test]
fn bytecode_math_float_natives() {
assert_eq!(bc("Math.floor(3.9)"), "3");
assert_eq!(bc("Math.ceil(3.1)"), "4");
assert_eq!(bc("Math.round(2.5)"), "3");
assert_eq!(bc("Math.sqrt(81)"), "9");
assert_eq!(bc("Math.pow(2, 10)"), "1024");
}
#[test]
fn bytecode_exceptions() {
assert_eq!(
bc("let r = 'none'; try { throw 'boom'; } catch (e) { r = 'caught:' + e; } r"),
"caught:boom"
);
assert_eq!(bc("let r = 0; try { r = 1; } catch (e) { r = 99; } r"), "1");
assert_eq!(
bc(
"function boom() { throw 'x'; } let r = 'ok'; try { boom(); r = 'no'; } catch (e) { r = 'got:' + e; } r"
),
"got:x"
);
assert_eq!(
bc("let r = 'a'; try { throw 1; } catch { r = 'b'; } r"),
"b"
);
assert_eq!(
bc(
"let s = 0; for (let i = 0; i < 5; i++) { try { if (i === 2) { throw 0; } s += i; } catch (e) { s += 100; } } s"
),
"108"
);
}
#[test]
fn bytecode_finally() {
assert_eq!(
bc("let log = ''; try { log += 't'; } finally { log += 'f'; } log"),
"tf"
);
assert_eq!(
bc(
"let log = ''; try { log += 't'; throw 1; } catch (e) { log += 'c'; } finally { log += 'f'; } log"
),
"tcf"
);
assert_eq!(
bc("let log = '';
try {
try { log += 't'; throw 'x'; } finally { log += 'f'; }
} catch (e) { log += 'o:' + e; }
log"),
"tfo:x"
);
}
#[test]
fn bytecode_console_and_math_natives() {
assert_eq!(bc_out("console.log('hello')"), "hello\n");
assert_eq!(bc_out("console.log(1 + 2, 'x')"), "3 x\n");
assert_eq!(
bc_out("for (let i = 1; i <= 3; i++) { console.log(i * i); }"),
"1\n4\n9\n"
);
assert_eq!(bc("Math.max(3, 9, 4)"), "9");
assert_eq!(bc("Math.min(3, -2, 8)"), "-2");
assert_eq!(bc("Math.abs(-7)"), "7");
assert_eq!(bc("String(42) + '!'"), "42!");
assert_eq!(bc("Number('15') + 5"), "20");
assert_eq!(
bc_out("function greet(n) { console.log('hi ' + n); } greet('ada'); greet('bob');"),
"hi ada\nhi bob\n"
);
}
#[test]
fn bytecode_matches_tree_walker() {
let programs = [
"let s = 0; for (let i = 1; i <= 10; i++) { s += i; } console.log(s);",
"function fib(n) { if (n < 2) { return n; } return fib(n-1) + fib(n-2); } console.log(fib(15));",
"let a = [5, 3, 8]; let m = a[0]; for (let i = 1; i < a.length; i++) { if (a[i] > m) { m = a[i]; } } console.log(m);",
];
for src in programs {
let program = crate::parser::Parser::parse_program(src).expect("parse");
let mut realm = Realm::new();
let (_, vm_out) = compile_run_output(&mut realm, &program).expect("bytecode");
let (tw_out, _) = crate::nbexec::eval_source(src).expect("tree-walker");
assert_eq!(vm_out, tw_out, "engines disagree on: {src}");
}
}
#[test]
fn bytecode_compound_update_and_do_while() {
assert_eq!(bc("let x = 10; x += 5; x"), "15");
assert_eq!(bc("let x = 10; x -= 3; x *= 2; x"), "14");
assert_eq!(bc("let o = { n: 1 }; o.n += 9; o.n"), "10");
assert_eq!(bc("let a = [1, 2, 3]; a[1] *= 10; a[1]"), "20");
assert_eq!(bc("let i = 5; i++; i"), "6");
assert_eq!(bc("let i = 5; let a = i++; a + ',' + i"), "5,6");
assert_eq!(bc("let i = 5; let a = ++i; a + ',' + i"), "6,6");
assert_eq!(bc("let i = 5; --i; i"), "4");
assert_eq!(
bc("let s = 0; for (let i = 0; i < 5; i++) { s += i; } s"),
"10"
);
assert_eq!(
bc("let n = 0; let s = 0; do { s += n; n++; } while (n < 4); s"),
"6"
);
assert_eq!(bc("let r = 0; do { r++; } while (false); r"), "1");
}
#[test]
fn bytecode_for_of_arrays() {
assert_eq!(
bc("let s = 0; for (const x of [3, 1, 4, 1, 5]) { s += x; } s"),
"14"
);
assert_eq!(
bc("let p = 1; for (const n of [1, 2, 3, 4]) { p *= n; } p"),
"24"
);
assert_eq!(
bc(
"let s = 0; for (const x of [1, 2, 3, 4, 5]) { if (x === 4) { break; } if (x === 2) { continue; } s += x; } s"
),
"4"
);
assert_eq!(
bc(
"function pair(a, b) { return [a, b]; } let s = ''; for (const v of pair('x', 'y')) { s += v; } s"
),
"xy"
);
}
#[test]
fn bytecode_break_continue_switch() {
assert_eq!(
bc("let s = 0; for (let i = 0; i < 100; i++) { if (i === 5) { break; } s += i; } s"),
"10"
);
assert_eq!(
bc(
"let s = 0; for (let i = 0; i < 6; i++) { if (i % 2 === 0) { continue; } s += i; } s"
),
"9"
);
assert_eq!(
bc(
"let i = 0; let s = 0; while (true) { i++; if (i > 5) { break; } if (i === 3) { continue; } s += i; } s"
),
"12"
);
assert_eq!(
bc(
"let i = 0; let s = 0; do { i++; if (i === 2) { continue; } s += i; } while (i < 4); s"
),
"8"
);
assert_eq!(
bc("function classify(n) {
let r = '';
switch (n) {
case 1: r = 'one'; break;
case 2:
case 3: r = 'few'; break;
default: r = 'many';
}
return r;
}
classify(1) + ',' + classify(2) + ',' + classify(3) + ',' + classify(9)"),
"one,few,few,many"
);
assert_eq!(
bc(
"let s = 0; for (let i = 0; i < 4; i++) { switch (i) { case 1: continue; default: s += i; } } s"
),
"5"
);
}
#[cfg(feature = "std")]
#[test]
fn typeof_of_a_user_function_is_function_on_the_vm() {
let (out, _) = execute(
"var f = function(){}; function g(){} var h = () => 1;
console.log(typeof f); console.log(typeof g); console.log(typeof h);
console.log(typeof []); console.log(typeof {});
console.log(Object.keys(f).indexOf('\u{0}vmfn'));",
)
.expect("ok");
assert_eq!(out, "function\nfunction\nfunction\nobject\nobject\n-1\n");
}
#[test]
fn execute_bytecode_first_with_tree_walker_fallback() {
let (out, _) = execute(
"function makeCounter() { let c = 0; return function() { c += 1; return c; }; }
let n = makeCounter(); console.log(n()); console.log(n());",
)
.expect("ok");
assert_eq!(out, "1\n2\n");
let (out, _) = execute(
"class Point { constructor(x, y) { this.x = x; this.y = y; }
sum() { return this.x + this.y; } }
console.log(new Point(3, 4).sum());",
)
.expect("ok");
assert_eq!(out, "7\n");
let (out, _) = execute(
"class Box { constructor(v) { this._v = v; } get value() { return this._v * 2; } }
console.log(new Box(21).value);",
)
.expect("ok");
assert_eq!(out, "42\n");
let (_, completion) = execute("1 + 2 * 3").expect("ok");
assert_eq!(completion, "7");
let src = "let s = 0; for (let i = 1; i <= 5; i++) { s += i; } console.log(s);";
let (bc, _) = execute(src).expect("ok");
let (tw, _) = crate::nbexec::eval_source(src).expect("ok");
assert_eq!(bc, tw);
}
#[test]
fn bytecode_classes() {
assert_eq!(
bc("class Point {
constructor(x, y) { this.x = x; this.y = y; }
sum() { return this.x + this.y; }
}
new Point(3, 4).sum()"),
"7"
);
assert_eq!(
bc("class Counter {
constructor() { this.n = 0; }
inc() { this.n += 1; return this.n; }
}
let c = new Counter(); c.inc(); c.inc(); c.inc()"),
"3"
);
assert_eq!(
bc("class Calc {
constructor(v) { this.v = v; }
dbl() { return this.v * 2; }
quad() { return this.dbl() * 2; }
}
new Calc(5).quad()"),
"20"
);
assert_eq!(
bc(
"class Box { constructor(v) { this.v = v; } get() { return this.v; } }
let a = new Box(1); let b = new Box(99);
a.get() + ',' + b.get()"
),
"1,99"
);
}
#[cfg(feature = "std")]
#[test]
fn bytecode_async_promise_and_runtime_errors() {
use crate::nbvm::execute;
let (out, _) = execute(
"async function f() { return 7; } f().then((v) => { console.log('async:' + v); });",
)
.expect("ok");
assert_eq!(out, "async:7\n");
let (out, _) = execute(
"Promise.resolve(1).then((v) => v + 1).then((v) => { console.log('chain:' + v); });",
)
.expect("ok");
assert_eq!(out, "chain:2\n");
let (out, _) =
execute("Promise.reject('boom').catch((e) => { console.log('caught:' + e); });")
.expect("ok");
assert_eq!(out, "caught:boom\n");
assert_eq!(
bc("let r = ''; try { undefinedXYZ; } catch (e) { r = e.name; } r"),
"ReferenceError"
);
assert_eq!(
bc("let r = ''; try { null.field; } catch (e) { r = e.name; } r"),
"TypeError"
);
assert_eq!(bc("Math.max(3, 7)"), "7");
}
#[test]
fn bytecode_class_expressions() {
assert_eq!(
bc("const Pair = class { constructor(a, b) { this.a = a; this.b = b; } sum() { return this.a + this.b; } };
new Pair(10, 20).sum()"),
"30"
);
assert_eq!(
bc("const Box = class { constructor(v) { this._v = v; } get value() { return this._v * 2; } };
new Box(21).value"),
"42"
);
assert_eq!(
bc("class MathUtil { static square(x) { return x * x; } static sumOfSquares(a, b) { return MathUtil.square(a) + MathUtil.square(b); } }
MathUtil.sumOfSquares(3, 4)"),
"25"
);
}
#[test]
fn bytecode_class_statics() {
assert_eq!(
bc("class M { static add(a, b) { return a + b; } } M.add(3, 4)"),
"7"
);
assert_eq!(bc("class C { static version = 42; } C.version"), "42");
assert_eq!(
bc(
"class P { constructor(x) { this.x = x; } static of(x) { return new P(x); } }
P.of(9).x"
),
"9"
);
assert_eq!(
bc("class Counter {
static total = 0;
constructor() { this.n = 1; }
static describe() { return 'counter'; }
bump() { return this.n + 1; }
}
Counter.describe() + ':' + Counter.total + ':' + new Counter().bump()"),
"counter:0:2"
);
}
#[test]
fn bytecode_class_accessors() {
assert_eq!(
bc("class C { constructor(w, h) { this.w = w; this.h = h; }
get area() { return this.w * this.h; } }
new C(3, 4).area"),
"12"
);
assert_eq!(
bc("class T { constructor() { this.c = 0; }
get count() { return this.c; }
set count(v) { this.c = v * 2; } }
let t = new T(); t.count = 5; t.count"),
"10"
);
assert_eq!(
bc("class Box { constructor(v) { this._v = v; }
get value() { return this._v; }
set value(x) { this._v = x + 1; } }
let b = new Box(10); b.value = 20; b.value"),
"21"
);
assert_eq!(
bc("class A { get kind() { return 'A'; } }
class B extends A {}
new B().kind"),
"A"
);
}
#[test]
fn bytecode_class_fields() {
assert_eq!(
bc("class Box { value = 42; get() { return this.value; } } new Box().get()"),
"42"
);
assert_eq!(
bc(
"class C { base = 10; constructor(n) { this.total = this.base + n; } }
new C(5).total"
),
"15"
);
assert_eq!(
bc("class C { x; constructor() { this.x = 7; } } new C().x"),
"7"
);
assert_eq!(
bc(
"class P { a = 1; b = 2; c = 3; sum() { return this.a + this.b + this.c; } }
new P().sum()"
),
"6"
);
}
#[test]
fn bytecode_instanceof() {
assert_eq!(bc("class A {} new A() instanceof A"), "true");
assert_eq!(
bc("class A {} class B extends A {}
let b = new B(); '' + (b instanceof B) + ',' + (b instanceof A)"),
"true,true"
);
assert_eq!(
bc("class A {} class B extends A {} new A() instanceof B"),
"false"
);
assert_eq!(bc("class A {} class C {} new A() instanceof C"), "false");
assert_eq!(
bc("class A {} class B extends A {} class C extends B {}
let c = new C(); '' + (c instanceof A) + (c instanceof B) + (c instanceof C)"),
"truetruetrue"
);
}
#[test]
fn bytecode_sequence_tagged_labeled_instanceof_error() {
assert_eq!(bc("let x = (1, 2, 3); x"), "3");
assert_eq!(bc("let a = 0; let b = (a = 5, a + 1); b"), "6");
assert_eq!(
bc("function tag(s, ...v) { let out = s[0]; for (let i = 0; i < v.length; i++) { out += '<' + v[i] + '>' + s[i + 1]; } return out; }
tag`a${1}b${2}c`"),
"a<1>b<2>c"
);
assert_eq!(
bc(
"let hits = ''; outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (j === 1) continue outer; hits += i + '' + j + ','; } } hits"
),
"00,10,20,"
);
assert_eq!(
bc(
"let n = 0; search: for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { n++; if (i + j === 3) break search; } } n"
),
"4"
);
assert_eq!(bc("new TypeError('x') instanceof TypeError"), "true");
assert_eq!(bc("new RangeError('x') instanceof TypeError"), "false");
assert_eq!(bc("new TypeError('x') instanceof Error"), "true");
}
#[test]
fn bytecode_default_and_rest_params() {
assert_eq!(bc("function f(a, b = 10) { return a + b; } f(5)"), "15");
assert_eq!(bc("function f(a, b = 10) { return a + b; } f(5, 20)"), "25");
assert_eq!(bc("let g = (x, y = x * 2) => x + y; g(3)"), "9");
assert_eq!(
bc("function sum(...nums) { return nums.reduce((a, b) => a + b, 0); } sum(1, 2, 3, 4)"),
"10"
);
assert_eq!(
bc(
"function tag(first, ...rest) { return first + ':' + rest.join(','); } tag('a', 'b', 'c')"
),
"a:b,c"
);
assert_eq!(
bc("function f(a, b = 2, ...rest) { return a + b + rest.length; } f(1)"),
"3"
);
assert_eq!(
bc("function f(a, b = 2, ...rest) { return a + b + rest.length; } f(1, 5, 9, 9, 9)"),
"9"
);
}
#[test]
fn bytecode_super_method() {
assert_eq!(
bc("class A { greet() { return 'hi'; } }
class B extends A { greet() { return super.greet() + '!'; } }
new B().greet()"),
"hi!"
);
assert_eq!(
bc("class Shape { describe() { return 'shape'; } }
class Round extends Shape {
constructor(r) { super(); this.r = r; }
describe() { return super.describe() + ' r=' + this.r; }
}
new Round(5).describe()"),
"shape r=5"
);
}
#[test]
fn bytecode_class_inheritance() {
assert_eq!(
bc(
"class Animal { constructor(n) { this.n = n; } describe() { return this.n; } }
class Dog extends Animal {}
new Dog('Rex').describe()"
),
"Rex"
);
assert_eq!(
bc("class Animal { constructor(n) { this.n = n; } }
class Dog extends Animal {
constructor(n, b) { super(n); this.b = b; }
tag() { return this.n + ':' + this.b; }
}
new Dog('Rex', 'Lab').tag()"),
"Rex:Lab"
);
assert_eq!(
bc("class A { kind() { return 'A'; } }
class B extends A { kind() { return 'B'; } }
new B().kind() + new A().kind()"),
"BA"
);
assert_eq!(
bc(
"class Base { constructor(v) { this.v = v; } get() { return this.v; } }
class Sub extends Base {}
new Sub(7).get()"
),
"7"
);
assert_eq!(
bc("class A { constructor() { this.a = 1; } }
class B extends A { constructor() { super(); this.b = 2; } }
class C extends B { constructor() { super(); this.c = 3; } }
let o = new C(); o.a + o.b + o.c"),
"6"
);
}
#[test]
fn bytecode_forin_builtins_and_destructuring_assign() {
assert_eq!(
bc("let o = { a: 1, b: 2, c: 3 }; let s = ''; for (const k in o) { s += k; } s"),
"abc"
);
assert_eq!(
bc("let sum = 0; for (const i in [9, 8, 7]) { sum += Number(i); } sum"),
"3"
);
assert_eq!(bc("new Error('boom').message"), "boom");
assert_eq!(bc("new TypeError('bad').name"), "TypeError");
assert_eq!(bc("new Array(1, 2, 3).join(',')"), "1,2,3");
assert_eq!(bc("let a = 1, b = 2; [a, b] = [b, a]; a + ',' + b"), "2,1");
assert_eq!(
bc("let h, t; [h, ...t] = [1, 2, 3, 4]; h + '|' + t.join(',')"),
"1|2,3,4"
);
assert_eq!(bc("let x, y; ({ x, y } = { x: 10, y: 20 }); x + y"), "30");
assert_eq!(
bc("let p = {}; ({ a: p.x, b: p.y } = { a: 3, b: 4 }); p.x * p.y"),
"12"
);
assert_eq!(
bc("function f({ a, b }) { return a + b; } f({ a: 3, b: 4 })"),
"7"
);
assert_eq!(bc("let g = ([x, y]) => x * y; g([3, 4])"), "12");
assert_eq!(
bc("[{ n: 1 }, { n: 2 }, { n: 3 }].map(({ n }) => n * 10).join(',')"),
"10,20,30"
);
}
#[cfg(feature = "regex")]
#[test]
fn bytecode_regex() {
assert_eq!(bc(r"/^\d{4}-\d{2}-\d{2}$/.test('2026-06-04')"), "true");
assert_eq!(bc(r"/^\d+$/.test('12a')"), "false");
assert_eq!(bc(r"/(\w+)\s+(\w+)/.exec('hello world')[2]"), "world");
assert_eq!(bc(r"/(\w+)\s+(\w+)/.exec('hello world').index"), "0");
assert_eq!(bc(r"'a1b2c3'.match(/\d/g).join('')"), "123");
assert_eq!(bc(r"'key=value'.match(/(\w+)=(\w+)/)[2]"), "value");
assert_eq!(
bc(r"'John Smith'.replace(/(\w+)\s(\w+)/, '$2, $1')"),
"Smith, John"
);
assert_eq!(bc(r"'aaa'.replace(/a/g, 'b')"), "bbb");
assert_eq!(bc(r"'1, 2,3 ,4'.split(/\s*,\s*/).join('|')"), "1|2|3|4");
assert_eq!(bc(r"'find the needle'.search(/needle/)"), "9");
assert_eq!(bc(r"'nope'.search(/xyz/)"), "-1");
assert_eq!(bc(r#"new RegExp('\\d+', 'g').test('abc123')"#), "true");
assert_eq!(bc(r"/x/ instanceof RegExp"), "true");
assert_eq!(bc(r"[] instanceof Array"), "true");
assert_eq!(bc(r"new Map() instanceof Map"), "true");
assert_eq!(bc(r"new Set() instanceof Set"), "true");
assert_eq!(bc(r"[] instanceof Map"), "false");
}
#[test]
fn bytecode_number_string_array_object_statics() {
assert_eq!(
bc("'' + Number.isInteger(42) + Number.isInteger(4.2)"),
"truefalse"
);
assert_eq!(
bc("'' + Number.isFinite(1) + Number.isNaN(0 / 0)"),
"truetrue"
);
assert_eq!(bc("String.fromCharCode(75, 97)"), "Ka");
assert_eq!(bc("Array.from(new Set([1, 1, 2])).length"), "2");
assert_eq!(bc("Array.from('abc').join('-')"), "a-b-c");
assert_eq!(
bc("'' + Array.isArray([1]) + Array.isArray(5)"),
"truefalse"
);
assert_eq!(bc("Object.fromEntries([['k', 'v'], ['n', 1]]).k"), "v");
assert_eq!(
bc("let o = Object.fromEntries(Object.entries({ a: 1, b: 2 })); o.a + o.b"),
"3"
);
}
#[test]
fn bytecode_json_and_object_features() {
assert_eq!(
bc("JSON.stringify({ a: 1, b: [2, 3], c: 'x' })"),
"{\"a\":1,\"b\":[2,3],\"c\":\"x\"}"
);
assert_eq!(bc("JSON.parse('{\"x\": 42}').x"), "42");
assert_eq!(bc("JSON.parse('[1, 2, 3]')[1]"), "2");
assert_eq!(
bc("let o = JSON.parse(JSON.stringify({ n: 7, s: 'hi' })); o.n + o.s"),
"7hi"
);
assert_eq!(
bc("let a = { x: 1, y: 2 }; let b = { ...a, y: 9, z: 3 }; b.x + ',' + b.y + ',' + b.z"),
"1,9,3"
);
assert_eq!(
bc("let k = 'dyn'; let o = { [k]: 5, ['a' + 'b']: 6 }; o.dyn + o.ab"),
"11"
);
assert_eq!(
bc("let o = { _v: 10, get v() { return this._v * 2; } }; o.v"),
"20"
);
assert_eq!(
bc("let o = { foo: 1, bar: 2 }; let key = 'bar'; o[key] = 9; o['foo'] + o.bar"),
"10"
);
}
#[test]
fn bytecode_map_and_set() {
assert_eq!(
bc("let m = new Map(); m.set('a', 1).set('b', 2); m.get('a') + m.get('b')"),
"3"
);
assert_eq!(
bc("let m = new Map(); m.set('k', 9); '' + m.has('k') + ',' + m.size"),
"true,1"
);
assert_eq!(
bc("let m = new Map(); m.set('x', 1); m.delete('x'); m.size"),
"0"
);
assert_eq!(
bc(
"let m = new Map([['a', 1], ['b', 2]]); let s = 0; m.forEach((v) => { s += v; }); s"
),
"3"
);
assert_eq!(
bc("let m = new Map([['a', 1], ['b', 2]]); m.keys().join(',')"),
"a,b"
);
assert_eq!(bc("let s = new Set(); s.add(1).add(2).add(1); s.size"), "2");
assert_eq!(
bc("let s = new Set([1, 2, 3, 2, 1]); s.size + ',' + s.has(3)"),
"3,true"
);
assert_eq!(bc("'hello'.length"), "5");
}
#[test]
fn bytecode_delete_and_member_logical_assign() {
assert_eq!(
bc("let o = { a: 1, b: 2 }; delete o.a; '' + ('a' in o) + ',' + o.b"),
"false,2"
);
assert_eq!(
bc("let o = { x: 1 }; let r = delete o.x; '' + r + ',' + ('x' in o)"),
"true,false"
);
assert_eq!(
bc("let o = { k: 5 }; let key = 'k'; delete o[key]; 'k' in o"),
"false"
);
assert_eq!(bc("let o = { a: 0 }; o.a ||= 7; o.a"), "7");
assert_eq!(bc("let o = { a: 3 }; o.a &&= 9; o.a"), "9");
assert_eq!(
bc("let o = {}; o.cache ??= 'computed'; o.cache"),
"computed"
);
assert_eq!(bc("let c = { v: 5 }; c.v ??= 99; c.v"), "5");
assert_eq!(
bc("let s = 0; for (const [a, b] of [[1, 2], [3, 4]]) { s += a * b; } s"),
"14"
);
assert_eq!(
bc(
"let names = ''; for (const { name } of [{ name: 'a' }, { name: 'b' }]) { names += name; } names"
),
"ab"
);
}
#[test]
fn bytecode_object_namespace_and_in() {
assert_eq!(bc("Object.keys({ a: 1, b: 2 }).join(',')"), "a,b");
assert_eq!(bc("Object.values({ a: 1, b: 2 }).join(',')"), "1,2");
assert_eq!(
bc("Object.entries({ a: 1, b: 2 }).map((e) => e[0] + '=' + e[1]).join(',')"),
"a=1,b=2"
);
assert_eq!(
bc("let t = Object.assign({}, { a: 1 }, { b: 2 }); t.a + t.b"),
"3"
);
assert_eq!(bc("'x' in { x: 1 }"), "true");
assert_eq!(bc("'y' in { x: 1 }"), "false");
assert_eq!(bc("0 in [10, 20]"), "true");
assert_eq!(bc("5 in [10, 20]"), "false");
assert_eq!(
bc("let c = {}; c.k = 7; let r = ('k' in c) ? c.k : -1; r"),
"7"
);
}
#[test]
fn optimizer_folds_constant_arithmetic() {
let ops = alloc::vec![
Op::LoadConst {
dst: 0,
value: NanBox::number(6.0)
},
Op::LoadConst {
dst: 1,
value: NanBox::number(7.0)
},
Op::Mul { dst: 2, a: 0, b: 1 },
Op::Return { src: 2 },
];
let opt = optimize_ops(&ops);
match &opt[2] {
Op::LoadConst { dst, value } => {
assert_eq!(*dst, 2);
assert_eq!(value.as_number(), Some(42.0));
}
other => panic!("expected a folded LoadConst, got {other:?}"),
}
}
#[test]
fn optimizer_folds_addvalue_and_loose_eq() {
let ops = alloc::vec![
Op::LoadConst {
dst: 0,
value: NanBox::number(40.0)
},
Op::LoadConst {
dst: 1,
value: NanBox::number(2.0)
},
Op::AddValue { dst: 2, a: 0, b: 1 },
Op::Return { src: 2 },
];
let opt = optimize_ops(&ops);
assert!(matches!(&opt[2], Op::LoadConst { value, .. } if value.as_number() == Some(42.0)));
let ops = alloc::vec![
Op::LoadConst {
dst: 0,
value: NanBox::number(3.0)
},
Op::LoadConst {
dst: 1,
value: NanBox::number(3.0)
},
Op::ValueBin {
dst: 2,
op: VB_LOOSE_EQ,
a: 0,
b: 1
},
Op::Return { src: 2 },
];
let opt = optimize_ops(&ops);
assert!(matches!(&opt[2], Op::LoadConst { value, .. } if value.to_boolean()));
}
#[test]
fn optimizer_propagates_folded_constants() {
let ops = alloc::vec![
Op::LoadConst {
dst: 0,
value: NanBox::number(10.0)
},
Op::LoadConst {
dst: 1,
value: NanBox::number(4.0)
},
Op::Sub { dst: 2, a: 0, b: 1 }, Op::LoadConst {
dst: 3,
value: NanBox::number(7.0)
},
Op::Mul { dst: 4, a: 2, b: 3 }, Op::Return { src: 4 },
];
let opt = optimize_ops(&ops);
assert!(matches!(&opt[2], Op::LoadConst { value, .. } if value.as_number() == Some(6.0)));
assert!(matches!(&opt[4], Op::LoadConst { value, .. } if value.as_number() == Some(42.0)));
}
#[test]
fn optimizer_does_not_fold_across_basic_blocks() {
let ops = alloc::vec![
Op::LoadConst {
dst: 0,
value: NanBox::number(2.0)
},
Op::Jump { target: 3 },
Op::LoadConst {
dst: 0,
value: NanBox::number(99.0)
}, Op::Mul { dst: 1, a: 0, b: 0 },
Op::Return { src: 1 },
];
let opt = optimize_ops(&ops);
assert!(matches!(&opt[3], Op::Mul { .. }));
}
#[test]
fn tier_up_preserves_results_over_many_calls() {
assert_eq!(
bc("function sq(x) { return x * x; }
let s = 0;
for (let i = 0; i < 12; i++) { s += sq(i); }
s"),
"506" );
assert_eq!(
bc("function f() { return 6 * 7 - 2; }
let t = 0;
for (let i = 0; i < 20; i++) { t += f(); }
t"),
"800" );
}
#[test]
fn bytecode_more_operators() {
assert_eq!(bc("2 ** 10"), "1024");
assert_eq!(bc("17 % 5"), "2");
assert_eq!(bc("6 & 3"), "2");
assert_eq!(bc("5 | 2"), "7");
assert_eq!(bc("5 ^ 1"), "4");
assert_eq!(bc("1 << 4"), "16");
assert_eq!(bc("256 >> 2"), "64");
assert_eq!(bc("'' + (1 == 1) + ',' + (1 != 2)"), "true,true");
assert_eq!(bc("typeof 42"), "number");
assert_eq!(bc("typeof 'hi'"), "string");
assert_eq!(bc("typeof undefinedVar"), "undefined");
assert_eq!(bc("+'15' + 5"), "20");
assert_eq!(bc("~5"), "-6");
assert_eq!(bc("undefined === undefined"), "true");
assert_eq!(bc("'' + Infinity"), "Infinity");
assert_eq!(bc("let x = null; x ?? 'fallback'"), "fallback");
assert_eq!(bc("let x = 0; x ?? 'fallback'"), "0"); assert_eq!(bc("let n = 12; n &= 10; n"), "8");
assert_eq!(bc("let n = 8; n |= 1; n"), "9");
assert_eq!(bc("let n = 1; n <<= 3; n"), "8");
assert_eq!(bc("let x = 0; x ||= 5; x"), "5");
assert_eq!(bc("let x = 1; x &&= 9; x"), "9");
assert_eq!(bc("let x = null; x ??= 7; x"), "7");
assert_eq!(bc("parseInt('42px')"), "42");
assert_eq!(bc("parseInt('ff', 16)"), "255");
assert_eq!(bc("parseFloat('3.14xyz')"), "3.14");
assert_eq!(bc("'' + isNaN(0 / 0) + ',' + isFinite(1)"), "true,true");
}
#[test]
fn bytecode_array_spread_and_optional_chaining() {
assert_eq!(bc("let a = [1, 2, 3]; [...a].join(',')"), "1,2,3");
assert_eq!(bc("let a = [2, 3]; [1, ...a, 4].join(',')"), "1,2,3,4");
assert_eq!(
bc("let a = [1]; let b = [2, 3]; [...a, ...b].join(',')"),
"1,2,3"
);
assert_eq!(bc("[...[1, 2], ...[3, 4], 5].length"), "5");
assert_eq!(bc("let o = { x: { y: 7 } }; o?.x?.y"), "7");
assert_eq!(bc("let o = { x: null }; '' + (o?.x?.y)"), "undefined");
assert_eq!(bc("let o = null; '' + (o?.x)"), "undefined");
assert_eq!(bc("let o = {}; o?.missing || 'default'"), "default");
assert_eq!(
bc("function nums() { return [10, 20]; } [...nums(), 30].join(',')"),
"10,20,30"
);
}
#[test]
fn bytecode_destructuring() {
assert_eq!(bc("let [a, b] = [1, 2]; a + b"), "3");
assert_eq!(bc("let [a, , c] = [1, 2, 3]; a + c"), "4");
assert_eq!(bc("let [a, b = 9] = [1]; a + b"), "10");
assert_eq!(bc("let [[a], [b]] = [[1], [2]]; a + b"), "3");
assert_eq!(
bc("let [h, ...t] = [1, 2, 3, 4]; h + '|' + t.join(',')"),
"1|2,3,4"
);
assert_eq!(
bc("let [, , ...rest] = [1, 2, 3, 4, 5]; rest.join(',')"),
"3,4,5"
);
assert_eq!(
bc("let { a, ...rest } = { a: 1, b: 2, c: 3 }; a + '|' + Object.keys(rest).join(',')"),
"1|b,c"
);
assert_eq!(
bc("let { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 }; rest.c + rest.d"),
"7"
);
assert_eq!(bc("let { x, y } = { x: 1, y: 2 }; x + y"), "3");
assert_eq!(bc("let { a: p, b: q } = { a: 10, b: 20 }; p + q"), "30");
assert_eq!(bc("let { m = 7 } = {}; m"), "7");
assert_eq!(bc("let { p: { q } } = { p: { q: 42 } }; q"), "42");
assert_eq!(
bc("function pair() { return [3, 4]; } let [a, b] = pair(); a * b"),
"12"
);
assert_eq!(
bc("let s = 0; for (const p of [[1, 2], [3, 4]]) { let [a, b] = p; s += a * b; } s"),
"14"
);
}
#[test]
fn bytecode_array_and_string_methods() {
assert_eq!(bc("[1, 2, 3, 4].map((x) => x * 2).join(',')"), "2,4,6,8");
assert_eq!(
bc("[1, 2, 3, 4, 5].filter((x) => x % 2 === 0).join(',')"),
"2,4"
);
assert_eq!(bc("[1, 2, 3, 4].reduce((a, b) => a + b, 0)"), "10");
assert_eq!(bc("[1, 2, 3].reduce((a, b) => a + b)"), "6"); assert_eq!(
bc("let s = 0; [10, 20, 30].forEach((x) => { s += x; }); s"),
"60"
);
assert_eq!(bc("[5, 8, 2].find((x) => x > 6)"), "8");
assert_eq!(
bc("'' + [1, 2, 3].some((x) => x > 2) + ',' + [1, 2, 3].every((x) => x > 0)"),
"true,true"
);
assert_eq!(
bc("let a = [1, 2]; a.push(3); a.push(4); a.join('')"),
"1234"
);
assert_eq!(bc("let a = [1, 2, 3]; a.pop(); a.join('')"), "12");
assert_eq!(
bc("[1, 2, 3].includes(2) + ',' + [1, 2, 3].indexOf(3)"),
"true,2"
);
assert_eq!(bc("[1, 2].concat([3, 4], 5).join('')"), "12345");
assert_eq!(bc("[1, 2, 3].reverse().join('')"), "321");
assert_eq!(
bc("[1, 2, 3, 4, 5].map((x) => x * x).filter((x) => x > 4).reduce((a, b) => a + b, 0)"),
"50"
);
assert_eq!(bc("'Hello'.toUpperCase()"), "HELLO");
assert_eq!(bc("'WORLD'.toLowerCase()"), "world");
assert_eq!(bc("' hi '.trim()"), "hi");
assert_eq!(bc("'a,b,c'.split(',').join('|')"), "a|b|c");
assert_eq!(bc("'abcabc'.indexOf('c')"), "2");
assert_eq!(bc("'ab'.repeat(3)"), "ababab");
assert_eq!(bc("'hello world'.includes('world')"), "true");
}
#[test]
fn repeat_allocation_bomb_throws_range_error() {
assert_eq!(
bc("let r; try { 'x'.repeat(1e12); r = 'no throw'; } catch (e) { r = e.name; } r"),
"RangeError"
);
assert_eq!(
bc("let r; try { 'x'.repeat(-1); r = 'no throw'; } catch (e) { r = e.name; } r"),
"RangeError"
);
assert_eq!(
bc("let r; try { 'x'.repeat(Infinity); r = 'no throw'; } catch (e) { r = e.name; } r"),
"RangeError"
);
assert_eq!(bc("'ab'.repeat(3)"), "ababab");
assert_eq!(bc("'x'.repeat(0)"), "");
}
#[test]
fn excessive_register_count_is_compile_error_not_panic() {
let mut src = String::new();
for i in 0..70_000 {
src.push_str(&alloc::format!("let v{i}=0;"));
}
let program = crate::parser::Parser::parse_program(&src).expect("parse");
let mut realm = Realm::new();
assert!(compile_and_run(&mut realm, &program).is_err());
}
#[test]
fn bytecode_this_and_methods() {
assert_eq!(
bc("let o = { x: 10, getX: function() { return this.x; } }; o.getX()"),
"10"
);
assert_eq!(
bc(
"let c = { n: 0, inc: function() { this.n += 1; return this.n; } };
c.inc(); c.inc(); c.inc()"
),
"3"
);
assert_eq!(
bc("let calc = {
v: 5,
dbl: function() { return this.v * 2; },
quad: function() { return this.dbl() * 2; }
};
calc.quad()"),
"20"
);
assert_eq!(
bc(
"let acc = { total: 0, add: function(n) { this.total += n; return this; } };
acc.add(3); acc.add(4); acc.total"
),
"7"
);
}
#[test]
fn bytecode_template_literals() {
assert_eq!(bc("let n = 'world'; `Hello, ${n}!`"), "Hello, world!");
assert_eq!(
bc("let a = 2, b = 3; `${a} + ${b} = ${a + b}`"),
"2 + 3 = 5"
);
assert_eq!(bc("`no interpolation`"), "no interpolation");
assert_eq!(
bc("function greet(who) { return `hi ${who}`; } greet('ada')"),
"hi ada"
);
}
#[test]
fn bytecode_closures_and_capture() {
assert_eq!(
bc("function adder(x) { return function(y) { return x + y; }; } adder(3)(4)"),
"7"
);
assert_eq!(
bc("let add = (a) => (b) => (c) => a + b + c; add(1)(2)(3)"),
"6"
);
assert_eq!(
bc(
"function makeCounter() { let c = 0; return function() { c = c + 1; return c; }; }
let n = makeCounter();
n(); n(); n()"
),
"3"
);
assert_eq!(
bc(
"function makeCounter() { let c = 0; return function() { c += 1; return c; }; }
let a = makeCounter(); let b = makeCounter();
a(); a(); b();
a() + ',' + b()"
),
"3,2"
);
assert_eq!(
bc(
"function f() { let v = 'before'; let read = function() { return v; }; v = 'after'; return read(); } f()"
),
"after"
);
assert_eq!(
bc("function makeAcc() { let total = 0; return function(n) { total += n; return total; }; }
let acc = makeAcc(); acc(10); acc(20); acc(5)"),
"35"
);
}
#[test]
fn bytecode_first_class_functions() {
assert_eq!(
bc(
"function apply(f, x) { return f(x); } function dbl(n) { return n * 2; } apply(dbl, 21)"
),
"42"
);
assert_eq!(
bc("function inc(n) { return n + 1; } let g = inc; g(g(g(10)))"),
"13"
);
assert_eq!(
bc("function add(a, b) { return a + b; }
function mul(a, b) { return a * b; }
function pick(cond) { if (cond) { return add; } return mul; }
pick(true)(3, 4) + ',' + pick(false)(3, 4)"),
"7,12"
);
assert_eq!(
bc("function sq(n) { return n * n; } let ops = [sq]; ops[0](9)"),
"81"
);
}
#[test]
fn bytecode_functions_and_recursion() {
assert_eq!(bc("function add(a, b) { return a + b; } add(3, 4)"), "7");
assert_eq!(
bc("function fact(n) { if (n <= 1) { return 1; } return n * fact(n - 1); } fact(6)"),
"720"
);
assert_eq!(
bc(
"function fib(n) { if (n < 2) { return n; } return fib(n - 1) + fib(n - 2); } fib(12)"
),
"144"
);
assert_eq!(
bc(
"function sum(a) { let s = 0; for (let i = 0; i < a.length; i = i + 1) { s = s + a[i]; } return s; } sum([5, 10, 15])"
),
"30"
);
assert_eq!(
bc("function f(x) { let y = x * 2; return y; } f(3) + f(10)"),
"26"
);
}
#[test]
fn bytecode_arrays_objects_and_for() {
assert_eq!(bc("let a = [10, 20, 30]; a[1]"), "20");
assert_eq!(bc("[1, 2, 3, 4].length"), "4");
assert_eq!(bc("let a = [0, 0, 0]; a[2] = 7; a[2]"), "7");
assert_eq!(bc("let o = { x: 1, y: 2 }; o.x + o.y"), "3");
assert_eq!(bc("let o = {}; o.k = 42; o.k"), "42");
assert_eq!(
bc(
"let a = [3, 1, 4, 1, 5]; let s = 0; for (let i = 0; i < a.length; i = i + 1) { s = s + a[i]; } s"
),
"14"
);
assert_eq!(
bc("let grid = [[1, 2], [3, 4]]; grid[1][0] + grid[0][1]"),
"5"
);
}
#[test]
fn arithmetic() {
let prog = [
Op::LoadConst {
dst: 0,
value: NanBox::number(2.0),
},
Op::LoadConst {
dst: 1,
value: NanBox::number(3.0),
},
Op::Add { dst: 0, a: 0, b: 1 },
Op::LoadConst {
dst: 1,
value: NanBox::number(4.0),
},
Op::Mul { dst: 0, a: 0, b: 1 },
Op::Return { src: 0 },
];
let mut realm = Realm::new();
let result = run(&mut realm, &prog, 2).unwrap();
assert_eq!(result.as_number(), Some(20.0));
}
#[test]
fn counting_loop_sums_one_to_ten() {
let prog = [
Op::LoadConst {
dst: 0,
value: NanBox::number(0.0),
}, Op::LoadConst {
dst: 1,
value: NanBox::number(1.0),
}, Op::LoadConst {
dst: 2,
value: NanBox::number(11.0),
}, Op::LoadConst {
dst: 3,
value: NanBox::number(1.0),
}, Op::Lt { dst: 4, a: 1, b: 2 }, Op::JumpIfFalse { cond: 4, target: 8 }, Op::Add { dst: 0, a: 0, b: 1 }, Op::Add { dst: 1, a: 1, b: 3 }, Op::Jump { target: 4 }, Op::Return { src: 0 }, ];
let prog = {
let mut p = prog.to_vec();
p[5] = Op::JumpIfFalse { cond: 4, target: 9 }; p[8] = Op::Jump { target: 4 }; p
};
let mut realm = Realm::new();
let result = run(&mut realm, &prog, 5).unwrap();
assert_eq!(result.as_number(), Some(55.0));
}
#[test]
fn object_property_round_trip() {
let prog = [
Op::NewObject { dst: 0 },
Op::LoadConst {
dst: 1,
value: NanBox::number(7.0),
},
Op::SetProp {
obj: 0,
key: String::from("x"),
src: 1,
},
Op::LoadConst {
dst: 1,
value: NanBox::number(8.0),
},
Op::SetProp {
obj: 0,
key: String::from("y"),
src: 1,
},
Op::GetProp {
dst: 2,
obj: 0,
key: String::from("x"),
},
Op::GetProp {
dst: 3,
obj: 0,
key: String::from("y"),
},
Op::Add { dst: 2, a: 2, b: 3 },
Op::Return { src: 2 },
];
let mut realm = Realm::new();
let result = run(&mut realm, &prog, 4).unwrap();
assert_eq!(result.as_number(), Some(15.0));
assert_eq!(realm.object_count(), 1);
}
#[test]
fn builds_and_compares_strings() {
let prog = [
Op::NewString {
dst: 0,
value: String::from("Hello, "),
},
Op::NewString {
dst: 1,
value: String::from("world"),
},
Op::AddValue { dst: 0, a: 0, b: 1 }, Op::NewString {
dst: 2,
value: String::from("Hello, world"),
},
Op::StrictEq { dst: 0, a: 0, b: 2 }, Op::Return { src: 0 },
];
let mut realm = Realm::new();
let result = run(&mut realm, &prog, 3).unwrap();
assert_eq!(result.as_boolean(), Some(true));
}
#[test]
fn string_concat_loop() {
let prog = vec![
Op::NewString {
dst: 0,
value: String::new(),
}, Op::LoadConst {
dst: 1,
value: NanBox::number(0.0),
}, Op::LoadConst {
dst: 2,
value: NanBox::number(5.0),
}, Op::LoadConst {
dst: 3,
value: NanBox::number(1.0),
}, Op::NewString {
dst: 4,
value: String::from("x"),
}, Op::Lt { dst: 5, a: 1, b: 2 }, Op::JumpIfFalse {
cond: 5,
target: 10,
}, Op::AddValue { dst: 0, a: 0, b: 4 }, Op::Add { dst: 1, a: 1, b: 3 }, Op::Jump { target: 5 }, Op::Return { src: 0 }, ];
let mut realm = Realm::new();
let result = run(&mut realm, &prog, 6).unwrap();
assert_eq!(realm.to_display_string(result), "xxxxx");
}
#[test]
fn sums_an_array_in_a_loop() {
let prog = vec![
Op::NewArray { dst: 0, len: 3 },
Op::LoadConst {
dst: 6,
value: NanBox::number(10.0),
},
Op::LoadConst {
dst: 2,
value: NanBox::number(0.0),
},
Op::SetElem {
arr: 0,
index: 2,
src: 6,
}, Op::LoadConst {
dst: 6,
value: NanBox::number(20.0),
},
Op::LoadConst {
dst: 2,
value: NanBox::number(1.0),
},
Op::SetElem {
arr: 0,
index: 2,
src: 6,
}, Op::LoadConst {
dst: 6,
value: NanBox::number(30.0),
},
Op::LoadConst {
dst: 2,
value: NanBox::number(2.0),
},
Op::SetElem {
arr: 0,
index: 2,
src: 6,
}, Op::LoadConst {
dst: 1,
value: NanBox::number(0.0),
}, Op::LoadConst {
dst: 2,
value: NanBox::number(0.0),
}, Op::ArrayLen { dst: 3, arr: 0 }, Op::LoadConst {
dst: 4,
value: NanBox::number(1.0),
}, Op::Lt { dst: 5, a: 2, b: 3 }, Op::JumpIfFalse {
cond: 5,
target: 19,
}, Op::GetElem {
dst: 6,
arr: 0,
index: 2,
}, Op::Add { dst: 1, a: 1, b: 6 }, Op::Add { dst: 2, a: 2, b: 4 }, Op::Return { src: 1 }, ];
let prog = {
let mut p = prog;
p.insert(19, Op::Jump { target: 14 });
p[15] = Op::JumpIfFalse {
cond: 5,
target: 20,
};
p
};
let mut realm = Realm::new();
let result = run(&mut realm, &prog, 7).unwrap();
assert_eq!(result.as_number(), Some(60.0));
}
#[test]
fn absent_property_reads_undefined() {
let prog = [
Op::NewObject { dst: 0 },
Op::GetProp {
dst: 1,
obj: 0,
key: String::from("missing"),
},
Op::Return { src: 1 },
];
let mut realm = Realm::new();
let result = run(&mut realm, &prog, 2).unwrap();
assert!(result.is_undefined());
}
#[test]
fn type_error_on_non_number_arithmetic() {
let prog = [
Op::LoadConst {
dst: 0,
value: NanBox::undefined(),
},
Op::LoadConst {
dst: 1,
value: NanBox::number(1.0),
},
Op::Add { dst: 0, a: 0, b: 1 },
Op::Return { src: 0 },
];
let mut realm = Realm::new();
assert_eq!(run(&mut realm, &prog, 2), Err(VmError::NotANumber));
}
}