use fusevm::{Chunk, NumOp, VMResult, Value, VM};
use indexmap::IndexMap;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::rc::Rc;
use std::sync::mpsc::{Receiver, Sender};
use std::time::{Duration, Instant};
pub type IoTask = Box<dyn FnOnce() -> Result<(), String> + Send>;
pub mod ops {
pub const GETLOCAL: u16 = 1; pub const SETLOCAL: u16 = 2; pub const DECLARE: u16 = 3; pub const DELNAME: u16 = 4; pub const GETATTR: u16 = 5; pub const SETATTR: u16 = 6; pub const GETITEM: u16 = 7; pub const SETITEM: u16 = 8; pub const DELITEM: u16 = 9; pub const MKSTR: u16 = 10; pub const MKARR: u16 = 11; pub const MKOBJ: u16 = 12; pub const CALL: u16 = 13; pub const CALL_METHOD: u16 = 14; pub const CALL_VALUE: u16 = 15; pub const NEW: u16 = 16; pub const TRUTHY: u16 = 17; pub const TOSTR: u16 = 18; pub const MKFUNC: u16 = 19; pub const GETITER: u16 = 20; pub const FORITER: u16 = 21; pub const FORIN_KEYS: u16 = 22; pub const CONTAINS: u16 = 23; pub const SIG_RETURN: u16 = 24; pub const BINOP: u16 = 25; pub const UNARY: u16 = 26; pub const STRICT_EQ: u16 = 27; pub const LOOSE_EQ: u16 = 28; pub const TYPEOF: u16 = 29; pub const LOAD_NULL: u16 = 30; pub const THROW: u16 = 31; pub const TRY: u16 = 32; pub const NULLISH: u16 = 33; pub const UNPACK: u16 = 34; pub const BUILD_ARGS: u16 = 35; pub const THIS: u16 = 36; pub const INSTANCEOF: u16 = 37; pub const DELPROP_NAME: u16 = 38; pub const APPLY: u16 = 39; pub const APPLY_METHOD: u16 = 40; pub const OBJ_REST: u16 = 41; pub const DIV: u16 = 42; pub const MKCLASS: u16 = 43; pub const DEF_MEMBER: u16 = 44; pub const SUPER_CALL: u16 = 45; pub const SUPER_GET: u16 = 46; pub const YIELD: u16 = 47; pub const PROPKEY: u16 = 48; pub const NEW_TARGET: u16 = 49; pub const DEF_FIELD: u16 = 50; pub const AWAIT: u16 = 51; pub const DEF_ACCESSOR: u16 = 52; pub const DBG_LINE: u16 = 53; pub const MKBIGINT: u16 = 54; pub const MKREGEX: u16 = 55; pub const TAG_TMPL: u16 = 56; pub const GET_ASYNC_ITER: u16 = 57; pub const ASYNC_STEP: u16 = 58; pub const NUM_STEP: u16 = 59; pub const ITER_CLOSE: u16 = 60; pub const TYPEOF_NAME: u16 = 61; pub const SIG_BREAK: u16 = 62; pub const SIG_CONTINUE: u16 = 63; pub const SIG_UNWIND: u16 = 64; pub const PUSH_SCOPE: u16 = 65; pub const POP_SCOPE: u16 = 66; pub const COPY_SCOPE: u16 = 67; pub const DECLARE_VAR: u16 = 68; pub const NAMED_EVAL: u16 = 69; pub const POW: u16 = 70; pub const DECLARE_CONST: u16 = 71; pub const MARK_HOLE: u16 = 72; pub const SETLOCAL_STRICT: u16 = 73; pub const HOIST_VAR: u16 = 74; pub const FORIN_ALIVE: u16 = 75; pub const HOIST_TDZ: u16 = 76; pub const NEW_SPREAD: u16 = 77; pub const SUPER_CALL_SPREAD: u16 = 78; }
mod call_sites {
use std::cell::RefCell;
thread_local! {
pub(super) static SITES: RefCell<rustc_hash::FxHashMap<(u64, usize), String>> =
RefCell::new(rustc_hash::FxHashMap::default());
}
pub fn register(op_hash: u64, sites: Vec<(usize, String)>) {
if sites.is_empty() {
return;
}
SITES.with(|m| {
let mut m = m.borrow_mut();
for (ip, text) in sites {
m.insert((op_hash, ip), text);
}
});
}
pub fn text(op_hash: u64, ip: usize) -> Option<String> {
SITES.with(|m| m.borrow().get(&(op_hash, ip)).cloned())
}
pub fn clear() {
SITES.with(|m| m.borrow_mut().clear());
}
}
pub use call_sites::{clear as clear_call_sites, register as register_call_sites};
mod yield_sites {
use std::cell::RefCell;
thread_local! {
pub(super) static DEPTHS: RefCell<rustc_hash::FxHashMap<(u64, usize), usize>> =
RefCell::new(rustc_hash::FxHashMap::default());
}
pub fn register(op_hash: u64, sites: Vec<(usize, usize)>) {
if sites.is_empty() {
return;
}
DEPTHS.with(|m| {
let mut m = m.borrow_mut();
for (ip, depth) in sites {
m.insert((op_hash, ip), depth);
}
});
}
pub fn depth(op_hash: u64, ip: usize) -> usize {
DEPTHS.with(|m| m.borrow().get(&(op_hash, ip)).copied().unwrap_or(0))
}
pub fn clear() {
DEPTHS.with(|m| m.borrow_mut().clear());
}
}
pub use yield_sites::{clear as clear_yield_sites, register as register_yield_sites};
pub type SiteTables = (Vec<((u64, usize), String)>, Vec<((u64, usize), usize)>);
pub fn site_tables() -> SiteTables {
let calls = call_sites::SITES.with(|m| {
m.borrow()
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>()
});
let yields =
yield_sites::DEPTHS.with(|m| m.borrow().iter().map(|(k, v)| (*k, *v)).collect::<Vec<_>>());
(calls, yields)
}
pub fn restore_site_tables(t: &SiteTables) {
call_sites::SITES.with(|m| {
let mut m = m.borrow_mut();
for (k, v) in &t.0 {
m.insert(*k, v.clone());
}
});
yield_sites::DEPTHS.with(|m| {
let mut m = m.borrow_mut();
for (k, v) in &t.1 {
m.insert(*k, *v);
}
});
}
pub fn parked_iters(vm: &fusevm::VM) -> usize {
yield_sites::depth(vm.chunk.op_hash, vm.ip.saturating_sub(1))
}
pub fn call_site_text(vm: &fusevm::VM) -> Option<String> {
call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1))
}
pub fn name_call_site(vm: &fusevm::VM, subject: &str, msg: String) -> String {
for tail in [
" is not a function",
" is not a constructor",
" is not iterable",
] {
let Some(head) = msg.strip_suffix(tail) else {
continue;
};
let (prefix, found) = match head.find(": ") {
Some(i) => (&head[..i + 2], &head[i + 2..]),
None => ("", head),
};
if !found.ends_with(subject) {
return msg;
}
let Some(text) = call_sites::text(vm.chunk.op_hash, vm.ip.saturating_sub(1)) else {
return msg;
};
return format!("{prefix}{text}{tail}");
}
msg
}
pub mod unwind {
pub const NO_LOOP: &str = "";
pub const PLAIN_LOOP: &str = "\u{0}";
pub const NONE: i64 = 0;
pub const BREAK: i64 = 1;
pub const CONTINUE: i64 = 2;
}
pub mod member {
pub const METHOD: i64 = 0;
pub const GET: i64 = 1;
pub const SET: i64 = 2;
pub const STATIC_FIELD: i64 = 3;
}
pub mod binop {
pub const BITAND: i64 = 0;
pub const BITOR: i64 = 1;
pub const BITXOR: i64 = 2;
pub const SHL: i64 = 3;
pub const SHR: i64 = 4;
pub const USHR: i64 = 5;
}
pub mod unop {
pub const POS: i64 = 0; pub const BITNOT: i64 = 1; }
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct FuncDef {
pub name: String,
pub params: Vec<ParamSlot>,
pub chunk: Chunk,
pub is_arrow: bool,
pub is_generator: bool,
pub is_async: bool,
#[serde(default)]
pub strict: bool,
#[serde(default)]
pub is_method: bool,
#[serde(default)]
pub self_name: bool,
#[serde(default)]
pub span: (u32, u32),
#[serde(default)]
pub script: Option<u32>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct ParamSlot {
pub name: String,
pub rest: bool,
pub has_default: bool,
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct TryDef {
pub block: Chunk,
pub handler: Option<(Option<String>, Chunk)>,
pub finalizer: Option<Chunk>,
}
#[derive(Clone)]
pub struct FuncVal {
pub def_id: usize,
pub env: Option<Env>,
pub this: Option<Value>,
pub is_arrow: bool,
pub home_class: Option<String>,
pub home_static: bool,
pub home_object: Option<Value>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ArrayIterKind {
Keys,
Values,
Entries,
}
#[derive(Clone)]
pub enum JsObj {
Str(String),
Array(Vec<Value>),
Object(IndexMap<String, Value>),
Func(FuncVal),
Builtin(String),
BoundMethod {
recv: Value,
name: String,
},
Null,
Iter {
items: Vec<Value>,
idx: usize,
array: Option<(Value, ArrayIterKind)>,
},
BoundFunc {
target: Value,
this: Value,
args: Vec<Value>,
},
Class(ClassVal),
Symbol {
desc: Option<String>,
id: u64,
},
Map {
entries: IndexMap<MapKey, (Value, Value)>,
weak: bool,
},
Set {
entries: IndexMap<MapKey, Value>,
weak: bool,
},
Generator {
id: u32,
},
Promise {
id: u32,
},
BigInt(num_bigint::BigInt),
RegExp(Box<RegExpObj>),
Proxy {
target: Value,
handler: Value,
revoked: bool,
},
}
pub const WELL_KNOWN_SYMBOLS: &[&str] = &[
"iterator",
"asyncIterator",
"toPrimitive",
"toStringTag",
"hasInstance",
"species",
"isConcatSpreadable",
"match",
"matchAll",
"replace",
"search",
"split",
"unscopables",
"dispose",
"asyncDispose",
];
pub fn is_symbol_key(k: &str) -> bool {
match k.strip_prefix("@@") {
Some(rest) => rest
.strip_prefix("sym:")
.map(|i| i.parse::<u64>().is_ok())
.unwrap_or_else(|| WELL_KNOWN_SYMBOLS.contains(&rest)),
None => false,
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ObjKind {
Str,
Array,
Object,
Func,
Builtin,
BoundMethod,
Null,
Iter,
BoundFunc,
Class,
Symbol,
Map,
Set,
Generator,
Promise,
BigInt,
RegExp,
Proxy,
}
impl JsObj {
pub fn kind(&self) -> ObjKind {
match self {
JsObj::Str(_) => ObjKind::Str,
JsObj::Array(_) => ObjKind::Array,
JsObj::Object(_) => ObjKind::Object,
JsObj::Func(_) => ObjKind::Func,
JsObj::Builtin(_) => ObjKind::Builtin,
JsObj::BoundMethod { .. } => ObjKind::BoundMethod,
JsObj::Null => ObjKind::Null,
JsObj::Iter { .. } => ObjKind::Iter,
JsObj::BoundFunc { .. } => ObjKind::BoundFunc,
JsObj::Class(_) => ObjKind::Class,
JsObj::Symbol { .. } => ObjKind::Symbol,
JsObj::Map { .. } => ObjKind::Map,
JsObj::Set { .. } => ObjKind::Set,
JsObj::Generator { .. } => ObjKind::Generator,
JsObj::Promise { .. } => ObjKind::Promise,
JsObj::BigInt(_) => ObjKind::BigInt,
JsObj::RegExp(_) => ObjKind::RegExp,
JsObj::Proxy { .. } => ObjKind::Proxy,
}
}
}
#[derive(Clone)]
pub struct RegExpObj {
pub re: std::rc::Rc<fancy_regex::Regex>,
pub source: String,
pub flags: String,
pub global: bool,
pub ignore_case: bool,
pub multiline: bool,
pub dot_all: bool,
pub sticky: bool,
pub unicode: bool,
pub last_index: crate::utf16::U16Index,
}
pub struct PromiseCell {
pub state: PromiseState,
pub value: Value,
pub reactions: Vec<PromiseReaction>,
pub handled: bool,
}
pub enum PromiseReaction {
Js {
on_ful: Value,
on_rej: Value,
result: Value,
},
Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum PromiseState {
#[default]
Pending,
Fulfilled,
Rejected,
}
#[derive(Clone)]
pub struct ClassVal {
pub name: String,
pub ctor: Option<Value>,
pub parent: Option<Value>,
pub proto: Value,
pub statics: IndexMap<String, Value>,
pub fields: Vec<(String, Value, bool)>,
pub source_def: Option<usize>,
}
pub enum SuperRef {
Getter(Value),
Data(Value),
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum MapKey {
Undef,
Null,
Bool(bool),
Num(u64),
Big(String),
Str(String),
Ref(u32),
Intrinsic(String),
}
pub type VarMap = IndexMap<String, Value>;
pub struct EnvData {
pub vars: VarMap,
pub consts: rustc_hash::FxHashSet<String>,
pub parent: Option<Env>,
}
pub type Env = Rc<RefCell<EnvData>>;
pub type Accessor = (Option<Value>, Option<Value>);
pub const ORD_MARKER: &str = "@@ord:";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PropAttrs {
pub writable: bool,
pub enumerable: bool,
pub configurable: bool,
}
impl Default for PropAttrs {
fn default() -> Self {
PropAttrs {
writable: true,
enumerable: true,
configurable: true,
}
}
}
impl PropAttrs {
pub const HIDDEN: PropAttrs = PropAttrs {
writable: true,
enumerable: false,
configurable: true,
};
}
fn new_env(parent: Option<Env>) -> Env {
Rc::new(RefCell::new(EnvData {
vars: VarMap::default(),
consts: rustc_hash::FxHashSet::default(),
parent,
}))
}
pub fn child_env(parent: Env) -> Env {
new_env(Some(parent))
}
pub struct Frame {
pub env: Env,
pub base_env: Env,
pub this_obj: Option<Value>,
pub new_target: Option<Value>,
pub home_class: Option<Value>,
pub home_static: bool,
pub home_object: Option<Value>,
pub strict: bool,
pub line: u32,
pub owner: Option<String>,
pub is_module: bool,
pub this_state: ThisState,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ThisState {
#[default]
Plain,
Pending,
Bound,
}
#[derive(Clone)]
pub enum Signal {
Return(Value),
Break(Option<String>),
Continue(Option<String>),
}
pub struct JsHost {
heap: Vec<JsObj>,
pub funcs: Vec<FuncDef>,
pub scripts: Vec<std::sync::Arc<str>>,
pub tries: Vec<TryDef>,
globals: VarMap,
tdz: Option<Value>,
tdz_globals: rustc_hash::FxHashSet<String>,
global_consts: rustc_hash::FxHashSet<String>,
frames: Vec<Frame>,
global_env: Env,
pub error: Option<String>,
pub exc: Option<Value>,
pub signal: Option<Signal>,
pub pending_rejections: Vec<u32>,
pub process_listeners: IndexMap<String, Vec<ProcListener>>,
null_val: Value,
protos: HashMap<u32, Value>,
null_proto_objs: HashSet<u32>,
fn_props: HashMap<u32, IndexMap<String, Value>>,
accessors: HashMap<u32, IndexMap<String, Accessor>>,
prop_attrs: HashMap<u32, IndexMap<String, PropAttrs>>,
non_extensible: HashSet<u32>,
private_methods: HashSet<String>,
array_holes: HashMap<u32, rustc_hash::FxHashSet<usize>>,
super_replacement: Option<Value>,
derived_ctor_next: bool,
module_scope: bool,
builtin_statics: HashMap<String, IndexMap<String, Value>>,
object_proto: Value,
proto_class: HashMap<u32, Value>,
class_registry: HashMap<String, Value>,
error_protos: HashMap<String, Value>,
template_objects: HashMap<(u64, u64), Value>,
native_protos: HashMap<String, Value>,
symbol_registry: HashMap<String, Value>,
next_symbol: u64,
symbols_by_id: HashMap<u64, Value>,
well_known_ids: HashMap<u64, String>,
generators: Vec<GenCell>,
promises: Vec<PromiseCell>,
draining_micro: bool,
pub nextticks: std::collections::VecDeque<Task>,
pub microtasks: std::collections::VecDeque<Task>,
pub macrotasks: Vec<Timer>,
next_timer: u64,
io_tx: Sender<IoTask>,
io_rx: Option<Receiver<IoTask>>,
open_handles: usize,
capture: Option<Vec<u8>>,
pub exit_code: Option<i32>,
pub exiting: bool,
global_obj: Value,
}
#[derive(Clone)]
pub struct ProcListener {
pub f: Value,
pub once: bool,
}
pub enum Task {
Js { cb: Value, args: Vec<Value> },
Native(Box<dyn FnOnce() -> Result<(), String>>),
}
impl Task {
fn run(self) -> Result<(), String> {
match self {
Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
Task::Native(f) => f(),
}
}
}
pub struct Timer {
pub id: u64,
pub delay: f64,
pub seq: u64,
pub callback: Value,
pub args: Vec<Value>,
pub cancelled: bool,
pub interval: Option<f64>,
pub refed: bool,
pub deadline: Instant,
}
struct GenCell {
coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
yielder: *const (),
ctx: GenContext,
done: bool,
started: bool,
inject: Option<GenInject>,
async_gen: bool,
queue: std::collections::VecDeque<(GenReq, u32)>,
running: bool,
stack_floor: usize,
}
enum GenInject {
Return(Value),
Throw(Value),
}
#[derive(Clone)]
pub enum GenReq {
Next(Value),
Return(Value),
Throw(Value),
}
#[derive(Default)]
struct GenContext {
frames: Vec<Frame>,
error: Option<String>,
exc: Option<Value>,
signal: Option<Signal>,
}
thread_local! {
static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
}
thread_local! {
static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
}
pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
HOST.with(|h| f(&mut h.borrow_mut()))
}
pub fn reset_host() {
with_host(|h| *h = JsHost::new());
crate::module::reset();
}
impl Default for JsHost {
fn default() -> Self {
Self::new()
}
}
impl JsHost {
pub fn new() -> JsHost {
let global_env = new_env(None);
let (io_tx, io_rx) = std::sync::mpsc::channel();
let mut h = JsHost {
tdz: None,
tdz_globals: Default::default(),
heap: Vec::new(),
funcs: Vec::new(),
scripts: Vec::new(),
tries: Vec::new(),
globals: VarMap::default(),
global_consts: rustc_hash::FxHashSet::default(),
frames: vec![Frame {
env: global_env.clone(),
base_env: global_env.clone(),
this_obj: None,
new_target: None,
home_class: None,
home_static: false,
home_object: None,
strict: false,
line: 0,
owner: None,
is_module: true,
this_state: ThisState::Plain,
}],
global_env,
error: None,
exc: None,
signal: None,
pending_rejections: Vec::new(),
process_listeners: IndexMap::new(),
null_val: Value::Undef,
protos: HashMap::new(),
null_proto_objs: HashSet::new(),
fn_props: HashMap::new(),
accessors: HashMap::new(),
prop_attrs: HashMap::new(),
non_extensible: HashSet::new(),
private_methods: HashSet::new(),
array_holes: HashMap::new(),
super_replacement: None,
derived_ctor_next: false,
module_scope: false,
builtin_statics: HashMap::new(),
object_proto: Value::Undef,
proto_class: HashMap::new(),
class_registry: HashMap::new(),
error_protos: HashMap::new(),
template_objects: HashMap::new(),
native_protos: HashMap::new(),
symbol_registry: HashMap::new(),
next_symbol: 1,
symbols_by_id: HashMap::new(),
well_known_ids: HashMap::new(),
generators: Vec::new(),
promises: Vec::new(),
microtasks: std::collections::VecDeque::new(),
draining_micro: false,
nextticks: std::collections::VecDeque::new(),
macrotasks: Vec::new(),
next_timer: 1,
io_tx,
io_rx: Some(io_rx),
open_handles: 0,
capture: None,
exit_code: None,
exiting: false,
global_obj: Value::Undef,
};
h.null_val = h.alloc(JsObj::Null);
h.object_proto = h.new_object(IndexMap::new());
h.global_obj = h.new_object(IndexMap::new());
h
}
pub fn is_global_object(&self, v: &Value) -> bool {
!matches!(self.global_obj, Value::Undef) && self.global_obj == *v
}
pub fn global_object(&mut self) -> Value {
if matches!(self.global_obj, Value::Undef) {
self.global_obj = self.new_object(IndexMap::new());
}
self.global_obj.clone()
}
pub fn proto_of(&self, v: &Value) -> Option<Value> {
if let Value::Obj(i) = v {
self.protos.get(i).cloned()
} else {
None
}
}
pub fn set_proto(&mut self, v: &Value, proto: Value) {
if let Value::Obj(i) = v {
if self.is_null(&proto) {
self.protos.remove(i);
self.null_proto_objs.insert(*i);
} else if matches!(proto, Value::Undef) {
self.protos.remove(i);
} else {
self.protos.insert(*i, proto);
self.null_proto_objs.remove(i);
}
}
}
pub fn has_null_proto(&self, v: &Value) -> bool {
matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
}
pub fn inspects_null_proto(&self, v: &Value) -> bool {
self.has_null_proto(v) || *v == self.object_proto
}
pub fn object_proto(&self) -> Value {
self.object_proto.clone()
}
pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
if let Value::Obj(i) = proto {
self.proto_class.insert(*i, class_val);
}
}
pub fn class_owning_proto(&self, v: &Value) -> Option<Value> {
match v {
Value::Obj(i) => self.proto_class.get(i).cloned(),
_ => None,
}
}
pub fn class_of(&self, obj: &Value) -> Option<Value> {
let mut cur = self.proto_of(obj);
while let Some(p) = cur {
if let Value::Obj(i) = &p {
if let Some(c) = self.proto_class.get(i) {
return Some(c.clone());
}
}
cur = self.proto_of(&p);
}
None
}
pub fn ctor_name(&self, obj: &Value) -> String {
if let Some(c) = self.class_of(obj) {
if let Some(JsObj::Class(cv)) = self.get(&c) {
return cv.name.clone();
}
}
let mut cur = self.proto_of(obj);
while let Some(p) = cur {
let ctor = match self.get(&p) {
Some(JsObj::Object(props)) => props.get("constructor").cloned(),
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => self.fn_prop(&p, "constructor"),
_ => None,
};
if let Some(f) = ctor {
let n = self.callable_name(&f);
if !n.is_empty() {
return n;
}
}
cur = self.proto_of(&p);
}
String::new()
}
pub fn owns_prototype(&self, v: &Value) -> bool {
match self.get(v) {
Some(JsObj::Class(_)) => true,
Some(JsObj::Func(f)) => match self.funcs.get(f.def_id) {
Some(d) => d.is_generator || !(d.is_arrow || d.is_async || d.is_method),
None => false,
},
_ => false,
}
}
pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
if let Value::Obj(i) = v {
self.fn_props.get(i).and_then(|m| m.get(name).cloned())
} else {
None
}
}
pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
let mut cur = class_val.clone();
loop {
if let Some(v) = self.fn_prop(&cur, name) {
return Some(v);
}
match self.get(&cur) {
Some(JsObj::Class(c)) => cur = c.parent.clone()?,
_ => return None,
}
}
}
pub fn class_builtin_ancestor(&self, class_val: &Value) -> Option<Value> {
let mut cur = class_val.clone();
loop {
match self.get(&cur) {
Some(JsObj::Class(c)) => cur = c.parent.clone()?,
_ => return Some(cur),
}
}
}
pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
if let Value::Obj(i) = v {
self.fn_props
.entry(*i)
.or_default()
.insert(name.to_string(), val);
}
if !matches!(self.kind_of(v), Some(ObjKind::Func) | Some(ObjKind::Class)) {
return;
}
let attrs = match name {
"name" => PropAttrs {
writable: false,
enumerable: false,
configurable: true,
},
"prototype" => PropAttrs {
writable: true,
enumerable: false,
configurable: false,
},
_ => return,
};
self.set_prop_attrs(v, name, attrs);
}
pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
self.builtin_statics
.get(ns)
.and_then(|m| m.get(name).cloned())
}
pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
self.builtin_statics
.entry(ns.to_string())
.or_default()
.insert(name.to_string(), val);
}
pub fn remove_builtin_static(&mut self, ns: &str, name: &str) -> bool {
self.builtin_statics
.get_mut(ns)
.is_some_and(|m| m.shift_remove(name).is_some())
}
pub fn builtin_static_keys(&self, ns: &str) -> Vec<String> {
self.builtin_statics
.get(ns)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default()
}
pub fn remove_fn_prop(&mut self, v: &Value, name: &str) -> bool {
match v {
Value::Obj(i) => self
.fn_props
.get_mut(i)
.map(|m| m.shift_remove(name).is_some())
.unwrap_or(false),
_ => false,
}
}
pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
if let Value::Obj(i) = v {
self.fn_props
.get(i)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default()
} else {
Vec::new()
}
}
pub fn set_accessor(
&mut self,
owner: &Value,
key: &str,
get: Option<Value>,
set: Option<Value>,
) {
if let Value::Obj(i) = owner {
let marker = format!("{ORD_MARKER}{key}");
match self.get_mut(owner) {
Some(JsObj::Object(props)) => {
if !props.contains_key(key) && !props.contains_key(&marker) {
props.insert(marker, Value::Undef);
}
}
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
let table = self.fn_props.entry(*i).or_default();
if !table.contains_key(key) && !table.contains_key(&marker) {
table.insert(marker, Value::Undef);
}
}
_ => {}
}
let slot = self
.accessors
.entry(*i)
.or_default()
.entry(key.to_string())
.or_insert((None, None));
if get.is_some() {
slot.0 = get;
}
if set.is_some() {
slot.1 = set;
}
}
}
pub fn remove_accessor(&mut self, owner: &Value, key: &str) {
if let Value::Obj(i) = owner {
if let Some(m) = self.accessors.get_mut(i) {
m.shift_remove(key);
}
}
let marker = format!("{ORD_MARKER}{key}");
match self.get_mut(owner) {
Some(JsObj::Object(props)) => {
props.shift_remove(&marker);
}
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
if let Value::Obj(i) = owner {
if let Some(t) = self.fn_props.get_mut(i) {
t.shift_remove(&marker);
}
}
}
_ => {}
}
}
pub fn accessor_to_data(&mut self, owner: &Value, key: &str, value: Value) {
if let Value::Obj(i) = owner {
if let Some(m) = self.accessors.get_mut(i) {
m.shift_remove(key);
}
}
let marker = format!("{ORD_MARKER}{key}");
let swap = |map: &mut IndexMap<String, Value>| match map.get_index_of(&marker) {
Some(pos) => {
*map = map
.iter()
.enumerate()
.map(|(n, (k, v))| {
if n == pos {
(key.to_string(), value.clone())
} else {
(k.clone(), v.clone())
}
})
.collect();
}
None => {
map.insert(key.to_string(), value.clone());
}
};
let fn_table = matches!(
self.get(owner),
Some(JsObj::Func(_)) | Some(JsObj::Class(_))
);
if fn_table {
if let Value::Obj(i) = owner {
swap(self.fn_props.entry(*i).or_default());
}
} else if let Some(JsObj::Object(props)) = self.get_mut(owner) {
swap(props);
}
}
pub fn move_index_state(&mut self, src: u32, dst: u32) {
if let Some(holes) = self.array_holes.remove(&src) {
self.array_holes.insert(dst, holes);
}
if let Some(attrs) = self.prop_attrs.remove(&src) {
self.prop_attrs.entry(dst).or_default().extend(attrs);
}
if let Some(props) = self.fn_props.remove(&src) {
self.fn_props.entry(dst).or_default().extend(props);
}
if let Some(acc) = self.accessors.remove(&src) {
self.accessors.entry(dst).or_default().extend(acc);
}
}
pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
if let Value::Obj(i) = owner {
self.accessors.get(i).and_then(|m| m.get(key).cloned())
} else {
None
}
}
pub fn own_accessor_keys(&self, owner: &Value) -> Vec<String> {
match owner {
Value::Obj(i) => self
.accessors
.get(i)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default(),
_ => Vec::new(),
}
}
pub fn set_prop_attrs(&mut self, owner: &Value, key: &str, attrs: PropAttrs) {
if let Value::Obj(i) = owner {
if attrs == PropAttrs::default() {
if let Some(m) = self.prop_attrs.get_mut(i) {
m.shift_remove(key);
}
} else {
self.prop_attrs
.entry(*i)
.or_default()
.insert(key.to_string(), attrs);
}
}
}
pub fn copy_prop_attrs(&mut self, from: &Value, to: &Value) {
if let (Value::Obj(f), Value::Obj(_)) = (from, to) {
if let Some(m) = self.prop_attrs.get(f).cloned() {
for (k, a) in m {
self.set_prop_attrs(to, &k, a);
}
}
}
}
pub fn prop_attrs(&self, owner: &Value, key: &str) -> PropAttrs {
if key == "length" && matches!(self.get(owner), Some(JsObj::Array(_))) {
let writable = match owner {
Value::Obj(i) => self
.prop_attrs
.get(i)
.and_then(|m| m.get(key))
.map(|a| a.writable)
.unwrap_or(true),
_ => true,
};
return PropAttrs {
writable,
enumerable: false,
configurable: crate::builtins::is_arguments_h(self, owner),
};
}
match owner {
Value::Obj(i) => self
.prop_attrs
.get(i)
.and_then(|m| m.get(key))
.copied()
.unwrap_or_default(),
_ => PropAttrs::default(),
}
}
pub fn is_enumerable(&self, owner: &Value, key: &str) -> bool {
!key.starts_with("@@") && !key.starts_with('#') && self.prop_attrs(owner, key).enumerable
}
pub fn hide_prop(&mut self, owner: &Value, key: &str) {
self.set_prop_attrs(owner, key, PropAttrs::HIDDEN);
}
pub fn can_write_prop(&self, owner: &Value, key: &str) -> bool {
if !self.prop_attrs(owner, key).writable {
return false;
}
if key.starts_with("@@")
&& crate::builtins::own_ctor_name(self, owner)
.into_iter()
.chain(crate::builtins::chain_intrinsic_ctors_h(self, owner))
.any(|c| crate::builtins::is_proto_readonly(c, key))
{
return false;
}
let has_own = match self.get(owner) {
Some(JsObj::Object(p)) => p.contains_key(key),
_ => true,
};
if !has_own {
let mut cur = self.proto_of(owner);
while let Some(proto) = cur {
if self.own_accessor(&proto, key).is_some() {
break;
}
let present =
matches!(self.get(&proto), Some(JsObj::Object(p)) if p.contains_key(key));
if present {
if !self.prop_attrs(&proto, key).writable {
return false;
}
break;
}
cur = self.proto_of(&proto);
}
}
if self.is_extensible(owner) {
return true;
}
match self.get(owner) {
Some(JsObj::Object(p)) => p.contains_key(key),
Some(JsObj::Array(items)) => {
key == "length"
|| key
.parse::<usize>()
.map(|i| i < items.len())
.unwrap_or(false)
|| self.fn_prop(owner, key).is_some()
}
Some(JsObj::RegExp(_)) => key == "lastIndex" || self.fn_prop(owner, key).is_some(),
_ => self.fn_prop(owner, key).is_some(),
}
}
pub fn prevent_extensions(&mut self, v: &Value) {
if let Value::Obj(i) = v {
self.non_extensible.insert(*i);
}
}
pub fn is_extensible(&self, v: &Value) -> bool {
!matches!(v, Value::Obj(i) if self.non_extensible.contains(i))
}
pub fn seal_object(&mut self, v: &Value, freeze: bool) {
self.prevent_extensions(v);
let mut keys = self.integrity_keys(v);
keys.extend(self.own_accessor_keys(v));
for k in keys {
let mut a = self.prop_attrs(v, &k);
a.configurable = false;
if freeze {
a.writable = false;
}
self.set_prop_attrs(v, &k, a);
}
}
fn integrity_keys(&self, v: &Value) -> Vec<String> {
let side_table_keys = |v: &Value| -> Vec<String> {
match v {
Value::Obj(i) => self
.fn_props
.get(i)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default(),
_ => Vec::new(),
}
};
match self.get(v) {
Some(JsObj::Object(p)) => p.keys().cloned().collect(),
Some(JsObj::RegExp(_)) => vec!["lastIndex".to_string()],
Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::Map { .. })
| Some(JsObj::Set { .. })
| Some(JsObj::Promise { .. }) => side_table_keys(v),
Some(JsObj::Array(items)) => (0..items.len())
.map(|i| i.to_string())
.chain(std::iter::once("length".to_string()))
.chain(match v {
Value::Obj(i) => self
.fn_props
.get(i)
.map(|m| m.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default(),
_ => Vec::new(),
})
.collect(),
_ => Vec::new(),
}
}
pub fn is_sealed(&self, v: &Value, freeze: bool) -> bool {
if self.is_extensible(v) {
return false;
}
let mut keys = self.integrity_keys(v);
keys.extend(self.own_accessor_keys(v));
keys.iter().all(|k| {
let a = self.prop_attrs(v, k);
!a.configurable && (!freeze || !a.writable)
})
}
pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
let id = self.next_symbol;
self.next_symbol += 1;
let v = self.alloc(JsObj::Symbol { desc, id });
self.symbols_by_id.insert(id, v.clone());
v
}
pub fn symbol_of_key(&self, k: &str) -> Option<Value> {
if let Some(id) = k.strip_prefix("@@sym:").and_then(|i| i.parse::<u64>().ok()) {
return self.symbols_by_id.get(&id).cloned();
}
let name = k.strip_prefix("@@")?;
WELL_KNOWN_SYMBOLS
.contains(&name)
.then(|| {
self.symbol_registry
.get(&format!("@@Symbol.{name}"))
.cloned()
})
.flatten()
}
pub fn own_symbol_keys(&self, v: &Value) -> Vec<Value> {
let keys: Vec<String> = match self.get(v) {
Some(JsObj::Object(p)) => p.keys().cloned().collect(),
Some(_) => self.fn_prop_keys(v),
None => return Vec::new(),
};
keys.iter().filter_map(|k| self.symbol_of_key(k)).collect()
}
pub fn own_symbol_entries(&self, v: &Value) -> Vec<(String, Value)> {
match self.get(v) {
Some(JsObj::Object(p)) => p
.iter()
.filter(|(k, _)| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
.map(|(k, val)| (k.clone(), val.clone()))
.collect(),
Some(_) => self
.fn_prop_keys(v)
.into_iter()
.filter(|k| is_symbol_key(k) && self.prop_attrs(v, k).enumerable)
.map(|k| {
let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
(k, val)
})
.collect(),
None => Vec::new(),
}
}
pub fn symbol_for(&mut self, key: &str) -> Value {
if let Some(v) = self.symbol_registry.get(key) {
return v.clone();
}
let s = self.new_symbol(Some(key.to_string()));
self.symbol_registry.insert(key.to_string(), s.clone());
s
}
pub fn symbol_registry_key(&mut self, sym: &Value) -> Value {
let Some(key) = self
.symbol_registry
.iter()
.find(|(k, v)| self.strict_eq(v, sym) && !k.starts_with("@@Symbol."))
.map(|(k, _)| k.clone())
else {
return Value::Undef;
};
self.new_str(key)
}
pub fn well_known_iterator(&mut self) -> Value {
self.symbol_for("@@Symbol.iterator")
}
pub fn well_known_async_iterator(&mut self) -> Value {
self.symbol_for("@@Symbol.asyncIterator")
}
pub fn well_known_symbol(&mut self, name: &str) -> Value {
let key = format!("@@Symbol.{name}");
if let Some(v) = self.symbol_registry.get(&key) {
return v.clone();
}
let s = self.new_symbol(Some(format!("Symbol.{name}")));
if let Some(JsObj::Symbol { id, .. }) = self.get(&s) {
self.well_known_ids.insert(*id, name.to_string());
}
self.symbol_registry.insert(key, s.clone());
s
}
pub fn property_key(&self, v: &Value) -> String {
if let Some(JsObj::Symbol { id, .. }) = self.get(v) {
if let Some(n) = self.well_known_ids.get(id) {
return format!("@@{n}");
}
return format!("@@sym:{id}");
}
self.str_of(v)
}
pub fn null(&self) -> Value {
self.null_val.clone()
}
pub fn is_null(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Null))
}
pub fn program_offsets(&self) -> (usize, usize) {
(self.funcs.len(), self.tries.len())
}
pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
self.funcs.extend(funcs);
self.tries.extend(tries);
}
pub fn func_source(&self, def_id: usize) -> Option<&str> {
let d = self.funcs.get(def_id)?;
let (start, end) = d.span;
if end == 0 {
return None;
}
self.scripts
.get(d.script? as usize)?
.get(start as usize..end as usize)
}
pub fn try_def(&self, id: usize) -> Option<TryDef> {
self.tries.get(id).cloned()
}
pub fn try_shape(&self, id: usize) -> Option<(bool, Option<String>, bool)> {
let t = self.tries.get(id)?;
Some((
t.handler.is_some(),
t.handler.as_ref().and_then(|(bind, _)| bind.clone()),
t.finalizer.is_some(),
))
}
pub fn try_chunk(&self, id: usize, part: u64) -> Option<Chunk> {
let t = self.tries.get(id)?;
match part {
0 => Some(t.block.clone()),
1 => t.handler.as_ref().map(|(_, body)| body.clone()),
_ => t.finalizer.clone(),
}
}
pub fn alloc(&mut self, obj: JsObj) -> Value {
self.heap.push(obj);
Value::Obj((self.heap.len() - 1) as u32)
}
pub fn get(&self, v: &Value) -> Option<&JsObj> {
if let Value::Obj(i) = v {
self.heap.get(*i as usize)
} else {
None
}
}
pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
if let Value::Obj(i) = v {
self.heap.get_mut(*i as usize)
} else {
None
}
}
pub fn kind_of(&self, v: &Value) -> Option<ObjKind> {
self.get(v).map(JsObj::kind)
}
pub fn new_str(&mut self, s: impl Into<String>) -> Value {
self.alloc(JsObj::Str(s.into()))
}
pub fn new_array(&mut self, items: Vec<Value>) -> Value {
self.alloc(JsObj::Array(items))
}
pub fn note_private_method(&mut self, name: &str) {
self.private_methods.insert(name.to_string());
}
pub fn is_private_method(&self, name: &str) -> bool {
self.private_methods.contains(name)
}
pub fn fn_is_sloppy(&self, v: &Value) -> bool {
match self.get(v) {
Some(JsObj::Func(fv)) => {
!fv.is_arrow && !self.funcs.get(fv.def_id).is_some_and(|d| d.strict)
}
_ => false,
}
}
pub fn current_strict(&self) -> bool {
self.frame().strict
}
pub fn set_current_strict(&mut self) {
if let Some(f) = self.frames.last_mut() {
f.strict = true;
}
}
pub fn current_home(&self) -> (Option<String>, bool, Option<Value>) {
(
self.current_home_class_name(),
self.frame().home_static,
self.frame().home_object.clone(),
)
}
pub fn current_home_class_name(&self) -> Option<String> {
match self.get(&self.current_home_class()?) {
Some(JsObj::Class(c)) => Some(c.name.clone()),
_ => None,
}
}
pub fn has_private(&self, recv: &Value, key: &str) -> bool {
let mut cur = Some(recv.clone());
while let Some(v) = cur {
let owns = match self.get(&v) {
Some(JsObj::Object(p)) => p.contains_key(key),
Some(JsObj::Class(c)) => c.statics.contains_key(key),
_ => false,
};
if owns || self.own_accessor(&v, key).is_some() || self.fn_prop(&v, key).is_some() {
return true;
}
cur = self.proto_of(&v);
}
false
}
pub fn is_hole(&self, arr: &Value, i: usize) -> bool {
match (arr, ()) {
(Value::Obj(idx), ()) => self.array_holes.get(idx).is_some_and(|hs| hs.contains(&i)),
_ => false,
}
}
pub fn has_holes(&self, arr: &Value) -> bool {
matches!(arr, Value::Obj(i) if self.array_holes.contains_key(i))
}
pub fn hole_indices(&self, arr: &Value) -> Vec<usize> {
let Value::Obj(i) = arr else {
return Vec::new();
};
let Some(hs) = self.array_holes.get(i) else {
return Vec::new();
};
let mut v: Vec<usize> = hs.iter().copied().collect();
v.sort_unstable();
v
}
pub fn mark_hole(&mut self, arr: &Value, i: usize) {
if let Value::Obj(idx) = arr {
self.array_holes.entry(*idx).or_default().insert(i);
}
}
pub fn mark_hole_range(&mut self, arr: &Value, range: std::ops::Range<usize>) {
if range.is_empty() {
return;
}
if let Value::Obj(idx) = arr {
self.array_holes.entry(*idx).or_default().extend(range);
}
}
pub fn clear_hole(&mut self, arr: &Value, i: usize) {
let Value::Obj(idx) = arr else { return };
let Some(hs) = self.array_holes.get_mut(idx) else {
return;
};
hs.remove(&i);
if hs.is_empty() {
self.array_holes.remove(idx);
}
}
pub fn clear_holes(&mut self, arr: &Value) {
if let Value::Obj(idx) = arr {
self.array_holes.remove(idx);
}
}
pub fn copy_holes(&mut self, src: &Value, dst: &Value, f: impl Fn(usize) -> Option<usize>) {
if !self.has_holes(src) {
return;
}
let moved: rustc_hash::FxHashSet<usize> =
self.hole_indices(src).into_iter().filter_map(f).collect();
self.install_holes(dst, moved);
}
pub fn remap_holes(&mut self, arr: &Value, f: impl Fn(usize) -> Option<usize>) {
if !self.has_holes(arr) {
return;
}
let moved: rustc_hash::FxHashSet<usize> =
self.hole_indices(arr).into_iter().filter_map(f).collect();
self.install_holes(arr, moved);
}
pub fn install_holes(&mut self, arr: &Value, holes: rustc_hash::FxHashSet<usize>) {
let Value::Obj(idx) = arr else { return };
if holes.is_empty() {
self.array_holes.remove(idx);
} else {
self.array_holes.insert(*idx, holes);
}
}
pub fn truncate_holes(&mut self, arr: &Value, len: usize) {
self.remap_holes(arr, |i| (i < len).then_some(i));
}
fn inspect_sparse(
&self,
v: &Value,
items: &[Value],
indent: usize,
st: &mut InspectCycles,
) -> (Vec<String>, bool) {
let holes: rustc_hash::FxHashSet<usize> = self.hole_indices(v).into_iter().collect();
let empties = |n: usize| {
let unit = if n == 1 { "item" } else { "items" };
format!("<{n} empty {unit}>")
};
let mut out: Vec<String> = Vec::new();
let mut index = 0usize;
for (i, it) in items.iter().enumerate() {
if out.len() >= inspect_max_array_length() {
break;
}
if holes.contains(&i) {
continue;
}
if i > index {
out.push(empties(i - index));
index = i;
if out.len() >= inspect_max_array_length() {
break;
}
}
out.push(self.inspect_lvl(it, indent + 2, st));
index = i + 1;
}
let remaining = items.len() - index;
if remaining == 0 {
return (out, false);
}
if out.len() < inspect_max_array_length() {
out.push(empties(remaining));
(out, false)
} else {
let unit = if remaining == 1 { "item" } else { "items" };
out.push(format!("... {remaining} more {unit}"));
(out, true)
}
}
pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
canonicalize_own_keys(&mut props);
let tag = props.get("@@native").and_then(|v| self.as_str(v));
let obj = self.alloc(JsObj::Object(props));
if let Some(proto) = tag.and_then(|t| self.ensure_ctor_proto(&t)) {
self.set_proto(&obj, proto);
}
obj
}
pub fn as_str(&self, v: &Value) -> Option<String> {
match v {
Value::Str(s) => Some((**s).clone()),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => Some(s.clone()),
_ => None,
},
_ => None,
}
}
fn frame(&self) -> &Frame {
self.frames.last().unwrap()
}
fn cur_env(&self) -> Env {
self.frame().env.clone()
}
pub fn frame_depth(&self) -> usize {
self.frames.len()
}
pub fn set_cur_line(&mut self, line: u32) {
if let Some(f) = self.frames.last_mut() {
f.line = line;
}
}
pub fn stack_trace_limit(&self) -> usize {
match self.builtin_static("Error", "stackTraceLimit") {
Some(v) => {
let n = self.to_number(&v);
if n.is_finite() && n > 0.0 {
n as usize
} else if n.is_nan() || n <= 0.0 {
0
} else {
usize::MAX
}
}
None => 10,
}
}
pub fn stack_frames(&self) -> String {
let limit = self.stack_trace_limit();
if limit == 0 {
return String::new();
}
let mut out = String::new();
for (i, f) in self.frames.iter().enumerate().rev().take(limit) {
let name = match (&f.owner, i) {
(Some(n), _) => n.clone(),
(None, 0) => "Object.<anonymous>".to_string(),
(None, _) => "<anonymous>".to_string(),
};
out.push_str("\n at ");
out.push_str(&name);
}
if out.is_empty() && limit > 0 {
out.push_str("\n at <anonymous>");
}
out
}
pub fn dbg_stack(&self) -> Vec<(String, u32)> {
self.frames
.iter()
.rev()
.map(|f| {
let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
(name, f.line)
})
.collect()
}
pub fn dbg_locals(&self) -> Vec<(String, String)> {
let env = self.cur_env();
let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
names
.into_iter()
.map(|n| {
let v = self.read_name(&n).unwrap_or(Value::Undef);
(n, self.inspect(&v))
})
.collect()
}
pub fn is_tdz_global(&self, name: &str) -> bool {
self.tdz_globals.contains(name)
}
pub fn read_name(&self, name: &str) -> Option<Value> {
let mut env = Some(self.cur_env());
while let Some(e) = env {
if let Some(v) = e.borrow().vars.get(name) {
return Some(v.clone());
}
env = e.borrow().parent.clone();
}
self.globals.get(name).cloned()
}
pub fn read_global(&self, name: &str) -> Option<Value> {
self.globals.get(name).cloned()
}
pub fn has_name(&self, name: &str) -> bool {
let mut env = Some(self.cur_env());
while let Some(e) = env {
if e.borrow().vars.contains_key(name) {
return true;
}
env = e.borrow().parent.clone();
}
self.globals.contains_key(name)
}
#[must_use]
pub fn set_name(&mut self, name: &str, val: Value) -> bool {
let mut env = Some(self.cur_env());
while let Some(e) = env {
let mut b = e.borrow_mut();
if b.vars.contains_key(name) {
if !b.consts.is_empty() && b.consts.contains(name) {
return false;
}
if let Some(slot) = b.vars.get_mut(name) {
*slot = val;
}
return true;
}
drop(b);
env = e.borrow().parent.clone();
}
if self.global_consts.contains(name) {
return false;
}
match self.globals.get_mut(name) {
Some(slot) => *slot = val,
None => {
self.globals.insert(name.to_string(), val);
}
}
true
}
pub fn declare_const_name(&mut self, name: &str, val: Value) {
let f = self.frame();
let to_globals = f.is_module && Rc::ptr_eq(&f.env, &f.base_env);
self.declare_name(name, val);
if to_globals {
self.global_consts.insert(name.to_string());
} else {
self.cur_env().borrow_mut().consts.insert(name.to_string());
}
}
pub fn tdz_marker(&mut self) -> Value {
if let Some(v) = &self.tdz {
return v.clone();
}
let v = self.alloc(JsObj::Builtin("@@tdz".into()));
self.tdz = Some(v.clone());
v
}
pub fn is_tdz(&self, v: &Value) -> bool {
matches!((&self.tdz, v), (Some(Value::Obj(a)), Value::Obj(b)) if a == b)
}
pub fn hoist_tdz(&mut self, name: &str) {
let marker = self.tdz_marker();
let f = self.frame();
if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
if !self.globals.contains_key(name) {
self.tdz_globals.insert(name.to_string());
}
return;
}
let env = self.cur_env();
let mut e = env.borrow_mut();
if !e.vars.contains_key(name) {
e.vars.insert(name.to_string(), marker);
}
}
pub fn declare_name(&mut self, name: &str, val: Value) {
let f = self.frame();
if f.is_module && Rc::ptr_eq(&f.env, &f.base_env) {
self.tdz_globals.remove(name);
self.globals.insert(name.to_string(), val);
} else {
self.cur_env()
.borrow_mut()
.vars
.insert(name.to_string(), val);
}
}
pub fn hoist_var_name(&mut self, name: &str) {
if self.frame().is_module && !self.module_scope {
self.globals.entry(name.to_string()).or_insert(Value::Undef);
return;
}
let base = self.frame().base_env.clone();
let mut env = base.borrow_mut();
if !env.vars.contains_key(name) {
env.vars.insert(name.to_string(), Value::Undef);
}
}
pub fn declare_var_name(&mut self, name: &str, val: Value) {
if self.frame().is_module && !self.module_scope {
self.globals.insert(name.to_string(), val);
return;
}
let base = self.frame().base_env.clone();
base.borrow_mut().vars.insert(name.to_string(), val);
}
pub fn push_scope(&mut self) {
let env = self.cur_env();
self.frames.last_mut().unwrap().env = child_env(env);
}
pub fn push_var_scope(&mut self) -> Env {
let env = child_env(self.cur_env());
let f = self.frames.last_mut().unwrap();
let prev = std::mem::replace(&mut f.base_env, env.clone());
f.env = env;
prev
}
pub fn pop_var_scope(&mut self, prev: Env) {
let f = self.frames.last_mut().unwrap();
f.env = prev.clone();
f.base_env = prev;
}
pub fn pop_scope(&mut self) {
let cur = self.cur_env();
if Rc::ptr_eq(&cur, &self.frame().base_env) {
return;
}
let parent = cur.borrow().parent.clone();
if let Some(p) = parent {
self.frames.last_mut().unwrap().env = p;
}
}
pub fn copy_scope(&mut self) {
let cur = self.cur_env();
if Rc::ptr_eq(&cur, &self.frame().base_env) {
return;
}
let parent = cur.borrow().parent.clone();
let fresh = new_env(parent);
fresh.borrow_mut().vars = cur.borrow().vars.clone();
self.frames.last_mut().unwrap().env = fresh;
}
pub fn scope_snapshot(&self) -> Env {
self.cur_env()
}
pub fn restore_scope(&mut self, env: Env) {
self.frames.last_mut().unwrap().env = env;
}
pub fn set_global(&mut self, name: &str, val: Value) {
self.globals.insert(name.to_string(), val);
}
pub fn begin_capture(&mut self) {
self.capture = Some(Vec::new());
}
pub fn end_capture(&mut self) -> String {
String::from_utf8_lossy(&self.capture.take().unwrap_or_default()).into_owned()
}
pub fn end_capture_bytes(&mut self) -> Vec<u8> {
self.capture.take().unwrap_or_default()
}
pub fn capturing(&self) -> bool {
self.capture.is_some()
}
pub fn write_out(&mut self, s: &str, stderr: bool) {
self.write_out_bytes(s.as_bytes(), stderr);
}
pub fn write_out_bytes(&mut self, bytes: &[u8], stderr: bool) {
if let Some(buf) = &mut self.capture {
buf.extend_from_slice(bytes);
return;
}
use std::io::Write as _;
if stderr {
let mut e = std::io::stderr();
let _ = e.write_all(bytes);
let _ = e.flush();
} else {
let mut o = std::io::stdout();
let _ = o.write_all(bytes);
let _ = o.flush();
}
}
pub fn del_name(&mut self, name: &str) {
if self
.cur_env()
.borrow_mut()
.vars
.shift_remove(name)
.is_some()
{
return;
}
self.globals.shift_remove(name);
}
pub fn current_this(&self) -> Option<Value> {
self.frame().this_obj.clone()
}
pub fn this_state(&self) -> ThisState {
self.frame().this_state
}
pub fn mark_next_call_derived_ctor(&mut self) {
self.derived_ctor_next = true;
}
pub fn bind_super_this(&mut self) -> bool {
let Some(f) = self
.frames
.iter_mut()
.rev()
.find(|f| f.this_state != ThisState::Plain)
else {
return true;
};
if f.this_state == ThisState::Bound {
return false;
}
f.this_state = ThisState::Bound;
true
}
pub fn take_super_replacement(&mut self) -> Option<Value> {
self.super_replacement.take()
}
pub fn swap_super_replacement(&mut self, v: Option<Value>) -> Option<Value> {
std::mem::replace(&mut self.super_replacement, v)
}
pub fn set_current_this(&mut self, v: Value) {
if let Some(f) = self.frames.last_mut() {
f.this_obj = Some(v.clone());
}
self.super_replacement = Some(v);
}
pub fn take_process_listeners(&mut self, event: &str) -> Vec<Value> {
let Some(list) = self.process_listeners.get_mut(event) else {
return Vec::new();
};
let fired: Vec<Value> = list.iter().map(|l| l.f.clone()).collect();
list.retain(|l| !l.once);
fired
}
pub fn set_top_this(&mut self, v: Value) {
if let Some(f) = self.frames.first_mut() {
f.this_obj = Some(v);
}
}
pub fn current_env_capture(&self) -> Env {
self.frame().env.clone()
}
pub fn current_new_target(&self) -> Option<Value> {
self.frame().new_target.clone()
}
fn current_home_class(&self) -> Option<Value> {
self.frame().home_class.clone()
}
pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value, bool)>) {
match self.current_home_class() {
Some(cv) => match self.get(&cv) {
Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
_ => (None, Vec::new()),
},
None => (None, Vec::new()),
}
}
pub fn super_resolve(&self, name: &str) -> SuperRef {
if let Some(home) = self.frame().home_object.clone() {
let target = self.proto_of(&home).unwrap_or(Value::Undef);
if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
return SuperRef::Getter(getter);
}
return SuperRef::Data(lookup_chain(self, &target, name).unwrap_or(Value::Undef));
}
let parent = match self
.current_home_class()
.and_then(|cv| match self.get(&cv) {
Some(JsObj::Class(c)) => c.parent.clone(),
_ => None,
}) {
Some(p) => p,
None => return SuperRef::Data(Value::Undef),
};
let target = if self.frame().home_static {
parent.clone()
} else {
match self.get(&parent) {
Some(JsObj::Class(pc)) => pc.proto.clone(),
_ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
}
};
if let Some((Some(getter), _)) = lookup_accessor(self, &target, name) {
return SuperRef::Getter(getter);
}
if let Some(v) = lookup_chain(self, &target, name) {
return SuperRef::Data(v);
}
SuperRef::Data(self.fn_prop(&target, name).unwrap_or(Value::Undef))
}
pub fn take_error(&mut self) -> Option<String> {
self.error.take()
}
pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
let s = if msg.is_empty() {
class.to_string()
} else {
format!("{class}: {msg}")
};
self.error = Some(s.clone());
s
}
}
pub fn type_error(msg: &str) -> String {
format!("TypeError: {msg}")
}
pub fn ref_error(name: &str) -> String {
format!("ReferenceError: {name} is not defined")
}
pub fn tdz_error(name: &str) -> String {
format!("ReferenceError: Cannot access '{name}' before initialization")
}
pub fn range_error(msg: &str) -> String {
format!("RangeError: {msg}")
}
pub const MAX_STRING_LENGTH: usize = 536_870_888;
pub fn invalid_string_length() -> String {
range_error("Invalid string length")
}
pub fn to_array_length(v: &Value) -> Result<usize, String> {
let u32_pass = to_number_value(v)?;
let n = to_number_value(v)?;
let _ = u32_pass;
let u = if n.is_finite() {
(n.trunc() as i64).rem_euclid(1i64 << 32) as u32
} else {
0
};
if (u as f64) != n {
return Err(range_error("Invalid array length"));
}
Ok(u as usize)
}
pub fn coded_error(class: &str, code: &str, msg: &str) -> String {
format!("{class} [{code}]: {msg}")
}
pub const CODE_MARK: &str = "\u{1}code:";
pub const DOM_MARK: &str = "\u{1}dom:";
pub fn dom_error(name: &str, msg: &str) -> String {
format!("{DOM_MARK}{name}\u{1}{msg}")
}
pub fn plain_coded_error(class: &str, code: &str, msg: &str) -> String {
format!("{class}: {CODE_MARK}{code}\u{1}{msg}")
}
pub const FIELDS_MARK: char = '\u{2}';
pub fn plain_coded_error_with(
class: &str,
code: &str,
msg: &str,
fields: &[(&str, &str)],
) -> String {
let mut s = plain_coded_error(class, code, msg);
s.push(FIELDS_MARK);
for (k, v) in fields {
s.push_str(&format!("{k}\u{3}{}\u{3}{v}", v.len()));
}
s
}
pub fn plain_error_text(e: &str) -> String {
let Some(i) = e.find(CODE_MARK) else {
return e.to_string();
};
let (head, rest) = e.split_at(i);
match rest[CODE_MARK.len()..].split_once('\u{1}') {
Some((_, m)) => format!("{head}{}", split_error_fields(m).0),
None => e.to_string(),
}
}
pub fn split_error_fields(msg: &str) -> (&str, Vec<(&str, &str)>) {
let Some((head, mut rest)) = msg.split_once(FIELDS_MARK) else {
return (msg, Vec::new());
};
let mut fields = Vec::new();
while let Some((k, tail)) = rest.split_once('\u{3}') {
let Some((len, tail)) = tail.split_once('\u{3}') else {
break;
};
let Ok(len) = len.parse::<usize>() else { break };
let Some(v) = tail.get(..len) else { break };
fields.push((k, v));
rest = &tail[len..];
}
(head, fields)
}
pub fn invalid_arg_type(name: &str, kind: &str, expected: &str, v: &Value) -> String {
coded_error(
"TypeError",
"ERR_INVALID_ARG_TYPE",
&format!(
"The \"{name}\" {kind} must be of type {expected}. Received {}",
crate::stdlib::received_desc(v)
),
)
}
thread_local! {
static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub fn set_debug_mode(on: bool) {
DEBUG_MODE.with(|d| d.set(on));
}
thread_local! {
static JOIN_STACK: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
}
pub fn join_stack_push(v: &Value) -> bool {
match v {
Value::Obj(i) => JOIN_STACK.with(|s| {
let mut s = s.borrow_mut();
if s.contains(i) {
false
} else {
s.push(*i);
true
}
}),
_ => true,
}
}
pub fn join_stack_pop() {
JOIN_STACK.with(|s| {
s.borrow_mut().pop();
});
}
thread_local! {
static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
const STACK_RESERVE_DIVISOR: usize = 8;
const STACK_RESERVE_MIN: usize = 512 * 1024;
const STACK_RESERVE_FALLBACK: usize = 1024 * 1024;
fn stack_pointer() -> usize {
let probe = 0u8;
std::hint::black_box(&probe) as *const u8 as usize
}
fn stack_bounds() -> Option<(usize, usize)> {
#[cfg(target_vendor = "apple")]
{
unsafe {
let me = libc::pthread_self();
let top = libc::pthread_get_stackaddr_np(me) as usize;
let size = libc::pthread_get_stacksize_np(me);
if size == 0 || top < size {
return None;
}
Some((top - size, size))
}
}
#[cfg(target_os = "linux")]
{
unsafe {
let mut attr: libc::pthread_attr_t = std::mem::zeroed();
if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
return None;
}
let mut low: *mut libc::c_void = std::ptr::null_mut();
let mut size: libc::size_t = 0;
let ok = libc::pthread_attr_getstack(&attr, &mut low, &mut size) == 0;
libc::pthread_attr_destroy(&mut attr);
if ok && size != 0 {
return Some((low as usize, size));
}
None
}
}
#[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
{
None
}
}
fn stack_floor() -> usize {
let cached = STACK_FLOOR.with(|c| c.get());
if cached != 0 {
return cached;
}
let floor = match stack_bounds() {
Some((low, size)) => low + (size / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN),
None => stack_pointer().saturating_sub(STACK_RESERVE_FALLBACK),
};
STACK_FLOOR.with(|c| c.set(floor));
floor
}
const CORO_STACK_SIZE: usize = 16 * 1024 * 1024;
fn coro_stack_floor(stack: &impl corosensei::stack::Stack) -> usize {
stack.limit().get() + (CORO_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN)
}
const CORO_FALLBACK_STACK_SIZE: usize = 1024 * 1024;
fn ensure_coroutine_floor() {
if STACK_FLOOR.with(|c| c.get()) != 0 {
return;
}
let budget = CORO_FALLBACK_STACK_SIZE
- (CORO_FALLBACK_STACK_SIZE / STACK_RESERVE_DIVISOR).max(STACK_RESERVE_MIN);
STACK_FLOOR.with(|c| c.set(stack_pointer().saturating_sub(budget)));
}
fn swap_stack_floor(floor: usize) -> usize {
STACK_FLOOR.with(|c| c.replace(floor))
}
pub fn stack_exhausted() -> bool {
stack_pointer() <= stack_floor()
}
pub fn stack_overflow_error() -> String {
range_error("Maximum call stack size exceeded")
}
pub fn func_key(def_id: usize) -> u64 {
1 << 40 | def_id as u64
}
pub fn try_key(try_id: usize, part: u64) -> u64 {
2 << 40 | (try_id as u64) << 2 | part
}
thread_local! {
static VM_POOL: RefCell<rustc_hash::FxHashMap<u64, Vec<VM>>> =
RefCell::new(rustc_hash::FxHashMap::default());
}
fn take_pooled(key: u64) -> Option<VM> {
VM_POOL.with(|p| p.borrow_mut().get_mut(&key).and_then(|v| v.pop()))
}
fn put_pooled(key: u64, vm: VM) {
VM_POOL.with(|p| p.borrow_mut().entry(key).or_default().push(vm));
}
fn acquire_vm(chunk: Chunk) -> VM {
if let Some(mut vm) = take_pooled(0) {
vm.reset(chunk);
return vm;
}
let mut vm = VM::new(chunk);
crate::builtins::install(&mut vm);
vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
crate::builtins::numeric_hook(op, a, b)
}));
if DEBUG_MODE.with(|d| d.get()) {
vm.set_extension_handler(Box::new(|vm, id, _| {
crate::dap::on_ext(vm, id);
}));
} else {
vm.enable_tracing_jit();
}
vm
}
pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
if stack_exhausted() {
return Err(stack_overflow_error());
}
finish_run(0, acquire_vm(chunk))
}
pub fn run_chunk_keyed(key: u64, make: impl FnOnce() -> Chunk) -> Result<Value, String> {
if stack_exhausted() {
return Err(stack_overflow_error());
}
let vm = match take_pooled(key) {
Some(mut vm) => {
let held = std::mem::take(&mut vm.chunk);
vm.reset(held);
vm
}
None => acquire_vm(make()),
};
finish_run(key, vm)
}
fn finish_run(key: u64, mut vm: VM) -> Result<Value, String> {
let outcome = vm.run();
let result = match outcome {
_ if with_host(|h| h.error.is_some()) => {
Err(with_host(|h| h.take_error()).expect("just checked"))
}
VMResult::Ok(v) => Ok(v),
VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
VMResult::Error(e) => Err(e),
};
put_pooled(key, vm);
result
}
pub fn run_chunk_in_global_scope(chunk: Chunk) -> Result<Value, String> {
let prev_scope = with_host(|h| std::mem::take(&mut h.module_scope));
let out = run_chunk_in_global_scope_inner(chunk);
with_host(|h| h.module_scope = prev_scope);
out
}
fn run_chunk_in_global_scope_inner(chunk: Chunk) -> Result<Value, String> {
let global_env = with_host(|h| h.global_env.clone());
with_host(|h| {
h.frames.push(Frame {
env: global_env.clone(),
base_env: global_env,
this_obj: None,
new_target: None,
home_class: None,
home_static: false,
home_object: None,
strict: false,
line: 0,
owner: None,
is_module: true,
this_state: ThisState::Plain,
})
});
let r = run_chunk_on(chunk);
with_host(|h| {
h.frames.pop();
});
r
}
pub fn run_main(chunk: Chunk) -> Result<Value, String> {
with_host(|h| h.module_scope = true);
let r = run_chunk_on(chunk);
with_host(|h| h.signal = None);
if r.is_ok() {
run_event_loop()?;
finish_process_events()?;
}
r
}
fn finish_process_events() -> Result<(), String> {
for _ in 0..1000 {
let code = with_host(|h| h.exit_code).unwrap_or(0);
if !crate::stdlib::process::emit_before_exit(code)? {
break;
}
let more =
with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
if !more {
break;
}
run_event_loop()?;
}
let code = with_host(|h| h.exit_code).unwrap_or(0);
crate::stdlib::process::emit_exit_event(code)
}
pub fn fmt_number(f: f64) -> String {
if f.is_nan() {
return "NaN".into();
}
if f.is_infinite() {
return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
}
if f == 0.0 {
return "0".into();
}
if f < 0.0 {
return format!("-{}", js_number_repr(-f));
}
js_number_repr(f)
}
pub fn array_index(k: &str) -> Option<u32> {
if k.is_empty() {
return None;
}
if k == "0" {
return Some(0);
}
if k.as_bytes()[0] == b'0' {
return None;
}
if !k.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
match k.parse::<u64>() {
Ok(n) if n < u32::MAX as u64 => Some(n as u32),
_ => None,
}
}
pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (array_index(a), array_index(b)) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
}
pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
if props.keys().any(|k| array_index(k).is_some()) {
props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
}
}
fn js_number_repr(a: f64) -> String {
let sci = format!("{a:e}");
let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
let s: String = mant.chars().filter(|c| *c != '.').collect();
let k = s.len() as i32; let n = e + 1;
if k <= n && n <= 21 {
let mut out = s;
out.push_str(&"0".repeat((n - k) as usize));
out
} else if 0 < n && n <= 21 {
format!("{}.{}", &s[..n as usize], &s[n as usize..])
} else if -6 < n && n <= 0 {
format!("0.{}{}", "0".repeat((-n) as usize), s)
} else {
let exp = n - 1;
let sign = if exp >= 0 { '+' } else { '-' };
let mag = exp.abs();
if k == 1 {
format!("{s}e{sign}{mag}")
} else {
format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
}
}
}
impl JsHost {
pub fn type_of(&self, v: &Value) -> &'static str {
match v {
Value::Undef => "undefined",
Value::Bool(_) => "boolean",
Value::Int(_) | Value::Float(_) => "number",
Value::Str(_) => "string",
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(_)) => "string",
Some(JsObj::Proxy { target, .. }) => {
let mut cur = target;
for _ in 0..100 {
match self.get(cur) {
Some(JsObj::Proxy { target: t, .. }) => cur = t,
_ => break,
}
}
if is_callable(self, cur) {
"function"
} else {
"object"
}
}
Some(JsObj::Func(_))
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::Class(_)) => "function",
Some(JsObj::Builtin(n)) => {
if builtin_is_callable(n) {
"function"
} else {
"object"
}
}
Some(JsObj::Symbol { .. }) => "symbol",
Some(JsObj::BigInt(_)) => "bigint",
_ => "object", },
_ => "object",
}
}
pub fn truthy(&self, v: &Value) -> bool {
match v {
Value::Undef => false,
Value::Bool(b) => *b,
Value::Int(n) => *n != 0,
Value::Float(f) => *f != 0.0 && !f.is_nan(),
Value::Str(s) => !s.is_empty(),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => !s.is_empty(),
Some(JsObj::Null) => false,
Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
_ => true, },
_ => true,
}
}
pub fn to_number(&self, v: &Value) -> f64 {
match v {
Value::Undef => f64::NAN,
Value::Bool(b) => {
if *b {
1.0
} else {
0.0
}
}
Value::Int(n) => *n as f64,
Value::Float(f) => *f,
Value::Str(s) => str_to_number(s),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => str_to_number(s),
Some(JsObj::Null) => 0.0,
Some(JsObj::BigInt(b)) => bigint_to_f64(b),
Some(JsObj::Array(items)) => {
if items.is_empty() {
0.0
} else if items.len() == 1 {
self.to_number(&items[0])
} else {
f64::NAN
}
}
_ => f64::NAN,
},
_ => f64::NAN,
}
}
pub fn str_of(&self, v: &Value) -> String {
match v {
Value::Undef => "undefined".into(),
Value::Bool(b) => if *b { "true" } else { "false" }.into(),
Value::Int(n) => n.to_string(),
Value::Float(f) => fmt_number(*f),
Value::Str(s) => (**s).clone(),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => s.clone(),
Some(JsObj::Null) => "null".into(),
Some(JsObj::BigInt(b)) => b.to_string(),
Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
Some(JsObj::Array(items)) => {
if !join_stack_push(v) {
return String::new();
}
let parts: Vec<String> = items
.iter()
.map(|x| match x {
Value::Undef => String::new(),
_ if self.is_null(x) => String::new(),
_ => self.str_of(x),
})
.collect();
join_stack_pop();
parts.join(",")
}
Some(JsObj::Object(props)) => {
if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
Some(JsObj::Array(items)) => {
items.iter().map(|x| self.to_number(x) as u8).collect()
}
_ => Vec::new(),
};
String::from_utf8_lossy(&bytes).into_owned()
} else if let Some(s) = self.error_to_string(v) {
s
} else {
"[object Object]".into()
}
}
Some(JsObj::Func(f)) => {
if let Some(src) = self.fn_prop(v, "@@source") {
return self.str_of(&src);
}
if let Some(text) = self.func_source(f.def_id) {
return text.to_string();
}
let name = self
.funcs
.get(f.def_id)
.map(|d| d.name.clone())
.unwrap_or_default();
format!("function {name}() {{ [code] }}")
}
Some(JsObj::Builtin(n)) => {
if n.starts_with("console.") {
"function () { [native code] }".into()
} else if let Some(accessor) = crate::builtins::proto_getter_name(n) {
format!("function {accessor}() {{ [native code] }}")
} else {
format!(
"function {}() {{ [native code] }}",
crate::builtins::builtin_name(n)
)
}
}
Some(JsObj::BoundMethod { name, .. }) => {
format!("function {name}() {{ [native code] }}")
}
Some(JsObj::BoundFunc { .. }) => "function () { [native code] }".into(),
Some(JsObj::Proxy { .. }) if is_callable(self, v) => {
"function () { [native code] }".into()
}
Some(JsObj::Class(c)) => match c.source_def.and_then(|d| self.func_source(d)) {
Some(text) => text.to_string(),
None => format!("class {} {{ }}", c.name),
},
Some(JsObj::Symbol { desc, .. }) => {
match desc {
Some(d) => format!("Symbol({d})"),
None => "Symbol()".into(),
}
}
_ => "[object Object]".into(),
},
_ => "[object Object]".into(),
}
}
fn inspect_wrapper(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Option<String> {
let prim = match self.get(v) {
Some(JsObj::Object(p)) => p.get("@@primitive").cloned()?,
_ => return None,
};
let ctor = match &prim {
Value::Bool(_) => "Boolean",
Value::Int(_) | Value::Float(_) => "Number",
_ => match self.get(&prim) {
Some(JsObj::BigInt(_)) => "BigInt",
Some(JsObj::Symbol { .. }) => "Symbol",
_ => "String",
},
};
let head = format!("[{ctor}: {}]", self.inspect_lvl(&prim, indent, st));
let width = if ctor == "String" {
self.str_of(&prim).chars().count()
} else {
0
};
let extras: Vec<String> = match self.get(v) {
Some(JsObj::Object(p)) => p
.iter()
.filter(|(k, _)| {
!k.starts_with("@@")
&& !k.starts_with('#')
&& self.prop_attrs(v, k).enumerable
&& !k.parse::<usize>().is_ok_and(|i| i < width)
})
.map(|(k, val)| {
format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2, st))
})
.collect(),
_ => Vec::new(),
};
if extras.is_empty() {
return Some(head);
}
Some(self.render_object(&extras, &format!("{head} "), indent, st))
}
fn side_table_parts(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> Vec<String> {
self.fn_prop_keys(v)
.into_iter()
.filter(|k| {
!k.starts_with("@@")
&& !k.starts_with('#')
&& !is_symbol_key(k)
&& self.prop_attrs(v, k).enumerable
})
.map(|k| {
let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
format!(
"{}: {}",
fmt_key(&k),
self.inspect_lvl(&val, indent + 2, st)
)
})
.collect()
}
fn inspect_tag(&self, v: &Value) -> Option<String> {
let own = matches!(self.get(v), Some(JsObj::Object(p)) if p.contains_key("@@toStringTag"));
if own && self.prop_attrs(v, "@@toStringTag").enumerable {
return None;
}
let t = lookup_chain(self, v, "@@toStringTag")?;
self.as_str(&t)
}
pub fn console_format(&self, v: &Value) -> String {
match v {
Value::Str(_) => self.str_of(v),
Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
_ => self.inspect(v),
}
}
pub fn inspect(&self, v: &Value) -> String {
self.inspect_lvl(v, 0, &mut InspectCycles::default())
}
fn renders_without_expanding(&self, v: &Value) -> bool {
let plain_props = |p: &IndexMap<String, Value>| {
p.keys().all(|k| k.starts_with("@@") || k.starts_with('#'))
};
match self.get(v) {
Some(JsObj::RegExp(_)) => true,
Some(JsObj::Map { entries, .. }) => entries.is_empty(),
Some(JsObj::Set { entries, .. }) => entries.is_empty(),
Some(JsObj::Array(items)) => items.is_empty() && self.own_symbol_entries(v).is_empty(),
Some(JsObj::Object(p)) => match p.get("@@native").map(|t| self.str_of(t)).as_deref() {
Some("Buffer") => inspect_custom(),
Some("Date") => plain_props(p),
Some(_) => false,
None => plain_props(p) && self.own_symbol_entries(v).is_empty(),
},
_ => false,
}
}
fn inspect_lvl(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
if !matches!(v, Value::Obj(_)) {
return self.inspect_value(v, indent, st);
}
if st.seen.iter().any(|p| self.strict_eq(p, v)) {
return format!("[Circular *{}]", st.mark(self, v));
}
st.seen.push(v.clone());
if indent as i64 <= inspect_indent_limit()
&& !is_primitive(self, v)
&& !self.renders_without_expanding(v)
{
st.deepest = indent;
}
let body = self.inspect_value(v, indent, st);
st.seen.pop();
match st.id_of(self, v) {
Some(id) => format!("<ref *{id}> {body}"),
None => body,
}
}
fn inspect_value(&self, v: &Value, indent: usize, st: &mut InspectCycles) -> String {
if let Some(s) = self.inspect_wrapper(v, indent, st) {
return s;
}
match v {
Value::Undef => "undefined".into(),
Value::Bool(b) => if *b { "true" } else { "false" }.into(),
Value::Int(n) => n.to_string(),
Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
Value::Float(f) => fmt_number(*f),
Value::Str(s) => quote_str(s),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => quote_str(s),
Some(JsObj::Null) => "null".into(),
Some(JsObj::BigInt(b)) => format!("{b}n"),
Some(JsObj::RegExp(r)) => {
let body = format!("/{}/{}", r.source, r.flags);
if inspect_show_hidden() {
format!("{body} {{ [lastIndex]: {} }}", r.last_index.get())
} else {
body
}
}
Some(JsObj::Proxy { target, .. }) => {
format!("Proxy({})", self.inspect_lvl(target, indent, st))
}
Some(JsObj::Array(items)) if crate::builtins::is_arguments_h(self, v) => {
if indent as i64 > inspect_indent_limit() {
return "[Arguments]".into();
}
let mut inner: Vec<String> = items
.iter()
.enumerate()
.map(|(i, x)| format!("'{i}': {}", self.inspect_lvl(x, indent + 2, st)))
.collect();
for k in self.fn_prop_keys(v) {
if k.starts_with("@@") || !self.prop_attrs(v, &k).enumerable {
continue;
}
let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
inner.push(format!(
"{}: {}",
fmt_key(&k),
self.inspect_lvl(&val, indent + 2, st)
));
}
if inner.is_empty() {
return "[Arguments] {}".into();
}
self.render_object(&inner, "[Arguments] ", indent, st)
}
Some(JsObj::Array(items)) => {
let prop_keys: Vec<String> = self
.fn_prop_keys(v)
.into_iter()
.filter(|k| {
!k.starts_with("@@")
&& !k.starts_with('#')
&& self.prop_attrs(v, k).enumerable
})
.collect();
let sub = match self.proto_of(v) {
Some(_) => self.ctor_name(v),
None => String::new(),
};
let base = if sub.is_empty() || sub == "Array" {
String::new()
} else {
format!("{sub}({}) ", items.len())
};
let sym_entries = self.own_symbol_entries(v);
if items.is_empty()
&& prop_keys.is_empty()
&& sym_entries.is_empty()
&& !inspect_show_hidden()
{
return format!("{base}[]");
}
if indent as i64 > inspect_indent_limit() {
return "[Array]".into();
}
let (mut inner, has_tail) = if self.has_holes(v) {
self.inspect_sparse(v, items, indent, st)
} else {
let shown = items.len().min(inspect_max_array_length());
let mut inner: Vec<String> = items[..shown]
.iter()
.map(|x| self.inspect_lvl(x, indent + 2, st))
.collect();
let remaining = items.len() - shown;
if remaining > 0 {
let unit = if remaining == 1 { "item" } else { "items" };
inner.push(format!("... {remaining} more {unit}"));
}
(inner, remaining > 0)
};
let show_hidden = inspect_show_hidden();
if show_hidden {
inner.push(format!("[length]: {}", items.len()));
}
let has_props = show_hidden || !prop_keys.is_empty() || !sym_entries.is_empty();
for k in &prop_keys {
let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
inner.push(format!(
"{}: {}",
fmt_key(k),
self.inspect_lvl(&val, indent + 2, st)
));
}
for (k, val) in &sym_entries {
let label = match self.symbol_of_key(k) {
Some(s) => self.inspect(&s),
None => continue,
};
inner.push(format!(
"{label}: {}",
self.inspect_lvl(val, indent + 2, st)
));
}
self.render_array(
&inner,
items,
indent,
ArrayLayout {
has_props,
has_tail,
base: &base,
},
st,
)
}
Some(JsObj::Object(props))
if props.get("@@native").map(|t| self.str_of(t)).as_deref()
== Some("URLSearchParams") =>
{
let pairs: Vec<Value> = match props.get("@@pairs").and_then(|a| self.get(a)) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
};
if pairs.is_empty() {
return "URLSearchParams {}".into();
}
let inner: Vec<String> = pairs
.iter()
.filter_map(|kv| match self.get(kv) {
Some(JsObj::Array(p)) if p.len() == 2 => Some(format!(
"{} => {}",
self.inspect_lvl(&p[0], indent + 2, st),
self.inspect_lvl(&p[1], indent + 2, st)
)),
_ => None,
})
.collect();
self.render_object(&inner, "URLSearchParams ", indent, st)
}
Some(JsObj::Object(props))
if props.get("@@native").map(|t| self.str_of(t)).as_deref()
== Some("TypedArray") =>
{
let kind = props
.get("@@kind")
.map(|k| self.str_of(k))
.unwrap_or_else(|| "TypedArray".into());
let elems = crate::stdlib::typedarray::elems_display(self, v);
let vals = crate::stdlib::typedarray::elems_with_host(self, v);
let base = format!("{kind}({}) ", elems.len());
if indent as i64 > inspect_indent_limit() {
return format!("[{kind}]");
}
let shown = elems.len().min(inspect_max_array_length());
let mut inner: Vec<String> = elems[..shown].to_vec();
let remaining = elems.len() - shown;
if remaining > 0 {
let unit = if remaining == 1 { "item" } else { "items" };
inner.push(format!("... {remaining} more {unit}"));
}
let show_hidden = inspect_show_hidden();
if show_hidden {
let bpe = crate::stdlib::typedarray::bytes_per_element(&kind);
let byte_offset = props
.get("byteOffset")
.map(|x| self.to_number(x))
.unwrap_or(0.0);
inner.push(format!("[BYTES_PER_ELEMENT]: {bpe}"));
inner.push(format!("[length]: {}", elems.len()));
inner.push(format!("[byteLength]: {}", elems.len() * bpe));
inner.push(format!("[byteOffset]: {}", fmt_number(byte_offset)));
let buf_len = props
.get("@@buffer")
.and_then(|b| self.get(b))
.and_then(|o| match o {
JsObj::Object(bp) => bp.get("@@bytes").cloned(),
_ => None,
})
.and_then(|b| {
self.get(&b).map(|o| match o {
JsObj::Array(items) => items.len(),
_ => 0,
})
})
.unwrap_or(0);
inner.push(format!(
"[buffer]: ArrayBuffer {{ [byteLength]: {buf_len} }}"
));
}
self.render_array(
&inner,
&vals,
indent,
ArrayLayout {
has_props: show_hidden,
has_tail: remaining > 0,
base: &base,
},
st,
)
}
Some(JsObj::Object(props))
if props.get("@@native").map(|t| self.str_of(t)).as_deref()
== Some("ArrayBuffer") =>
{
let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
Some(JsObj::Array(items)) => {
items.iter().map(|x| self.to_number(x) as u8).collect()
}
_ => Vec::new(),
};
let hex: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
let mut parts = vec![
format!("[Uint8Contents]: <{}>", hex.join(" ")),
format!("[byteLength]: {}", bytes.len()),
];
if props.contains_key("@@maxByteLength") {
let max = props
.get("@@maxByteLength")
.map(|m| self.to_number(m))
.unwrap_or(0.0);
parts.insert(1, format!("maxByteLength: {}", fmt_number(max)));
}
self.render_object(&parts, "ArrayBuffer ", indent, st)
}
Some(JsObj::Object(props))
if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Date") =>
{
let base = crate::stdlib::date::inspect_with_host(self, v);
let extra = self.side_table_parts(v, indent, st);
let mut inner: Vec<String> = props
.iter()
.filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
.map(|(k, val)| format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)))
.collect();
inner.extend(extra);
if inner.is_empty() {
return base;
}
self.render_object(&inner, &format!("{base} "), indent, st)
}
Some(JsObj::Object(props))
if props.get("@@native").map(|t| self.str_of(t)).as_deref()
== Some("Buffer") =>
{
let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
Some(JsObj::Array(items)) => {
items.iter().map(|x| self.to_number(x) as u8).collect()
}
_ => Vec::new(),
};
if !inspect_custom() {
let base = format!("Buffer({}) [Uint8Array] ", bytes.len());
if indent as i64 > inspect_indent_limit() {
return "[Buffer [Uint8Array]]".into();
}
let shown = bytes.len().min(inspect_max_array_length());
let mut inner: Vec<String> =
bytes[..shown].iter().map(|b| b.to_string()).collect();
let vals: Vec<Value> = bytes[..shown]
.iter()
.map(|b| Value::Float(*b as f64))
.collect();
let remaining = bytes.len() - shown;
if remaining > 0 {
let unit = if remaining == 1 { "item" } else { "items" };
inner.push(format!("... {remaining} more {unit}"));
}
return self.render_array(
&inner,
&vals,
indent,
ArrayLayout {
has_props: false,
has_tail: remaining > 0,
base: &base,
},
st,
);
}
const MAX: usize = 50;
let shown: Vec<String> =
bytes.iter().take(MAX).map(|b| format!("{b:02x}")).collect();
let mut out = format!("<Buffer {}", shown.join(" "));
if bytes.len() > MAX {
let more = bytes.len() - MAX;
let unit = if more == 1 { "byte" } else { "bytes" };
out.push_str(&format!(" ... {more} more {unit}"));
}
out.push('>');
out
}
Some(JsObj::Object(_)) if self.error_to_string(v).is_some() => {
let mut stack = lookup_chain(self, v, "stack")
.map(|s| self.str_of(&s))
.unwrap_or_else(|| self.error_to_string(v).unwrap_or_default());
if let Some(JsObj::Object(p)) = self.get(v) {
if let Some(n) = p.get("@@domName") {
let name = self.str_of(n);
stack = format!(
"DOMException [{name}]{}",
stack.strip_prefix(&name).unwrap_or(&stack)
);
}
}
let extra: Vec<String> = self
.own_enum_key_names(v)
.into_iter()
.filter(|k| k != "name")
.map(|k| {
let val = self.fn_prop(v, &k).unwrap_or_else(|| match self.get(v) {
Some(JsObj::Object(p)) => {
p.get(&k).cloned().unwrap_or(Value::Undef)
}
_ => Value::Undef,
});
format!(
"{}: {}",
fmt_key(&k),
self.inspect_lvl(&val, indent + 2, st)
)
})
.collect();
if extra.is_empty() {
stack
} else {
format!("{stack} {{ {} }}", extra.join(", "))
}
}
Some(JsObj::Object(props)) => {
let ctor = match self.ctor_name(v) {
n if n.is_empty() => "Object".to_string(),
n => n,
};
let plain_prefix = if ctor == "Object" {
String::new()
} else {
format!("{ctor} ")
};
let prefix = if self.inspects_null_proto(v) {
"[Object: null prototype] ".to_string()
} else {
match self.inspect_tag(v) {
Some(t) if t != ctor => format!("{ctor} [{t}] "),
_ => plain_prefix.clone(),
}
};
let mut shown: Vec<(String, Result<&Value, &'static str>)> = props
.iter()
.filter_map(|(k, val)| match k.strip_prefix(ORD_MARKER) {
Some(real) => {
let attrs = self.prop_attrs(v, real);
let label = match self.own_accessor(v, real)? {
(Some(_), Some(_)) => "[Getter/Setter]",
(Some(_), None) => "[Getter]",
(None, Some(_)) => "[Setter]",
(None, None) => return None,
};
attrs.enumerable.then(|| (fmt_key(real), Err(label)))
}
None if !k.starts_with("@@")
&& !k.starts_with('#')
&& self.prop_attrs(v, k).enumerable =>
{
Some((fmt_key(k), Ok(val)))
}
None => None,
})
.collect();
shown.extend(props.iter().filter_map(|(k, val)| {
let sym = self.symbol_of_key(k)?;
self.prop_attrs(v, k)
.enumerable
.then(|| (self.inspect(&sym), Ok(val)))
}));
if shown.is_empty() {
return format!("{prefix}{{}}");
}
if indent as i64 > inspect_indent_limit() {
return if self.inspects_null_proto(v) {
prefix.trim_end().to_string()
} else if plain_prefix.is_empty() {
"[Object]".into()
} else {
format!("[{}]", plain_prefix.trim_end())
};
}
let inner: Vec<String> = shown
.iter()
.map(|(k, val)| match val {
Ok(val) => format!("{k}: {}", self.inspect_lvl(val, indent + 2, st)),
Err(label) => format!("{k}: {label}"),
})
.collect();
self.render_object(&inner, &prefix, indent, st)
}
Some(JsObj::Symbol { desc, .. }) => match desc {
Some(d) => format!("Symbol({d})"),
None => "Symbol()".into(),
},
Some(JsObj::Class(c)) => {
let base = if c.parent.is_some() {
let pname = c
.parent
.as_ref()
.map(|p| self.callable_name(p))
.unwrap_or_default();
format!("[class {} extends {}]", c.name, pname)
} else {
format!("[class {}]", c.name)
};
self.with_callable_props(v, base, indent, st)
}
Some(JsObj::Map { weak: true, .. }) => "WeakMap { <items unknown> }".into(),
Some(JsObj::Set { weak: true, .. }) => "WeakSet { <items unknown> }".into(),
Some(JsObj::Map { entries, .. }) => {
let extra = self.side_table_parts(v, indent, st);
if entries.is_empty() && extra.is_empty() {
return "Map(0) {}".into();
}
if indent as i64 > inspect_indent_limit() {
return "[Map]".into();
}
let mut inner: Vec<String> = entries
.values()
.map(|(k, val)| {
let ks = self.inspect_lvl(k, indent + 2, st);
let vs = self.inspect_lvl(val, indent + 2, st);
format!("{ks} => {vs}")
})
.collect();
inner.extend(extra);
let prefix = format!("Map({}) ", entries.len());
self.render_object(&inner, &prefix, indent, st)
}
Some(JsObj::Set { entries, .. }) => {
let extra = self.side_table_parts(v, indent, st);
if entries.is_empty() && extra.is_empty() {
return "Set(0) {}".into();
}
if indent as i64 > inspect_indent_limit() {
return "[Set]".into();
}
let mut inner: Vec<String> = entries
.values()
.map(|v| self.inspect_lvl(v, indent + 2, st))
.collect();
inner.extend(extra);
let prefix = format!("Set({}) ", entries.len());
self.render_object(&inner, &prefix, indent, st)
}
Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
Some(c) => match c.state {
PromiseState::Pending => "Promise { <pending> }".into(),
PromiseState::Fulfilled => {
format!("Promise {{ {} }}", self.inspect_lvl(&c.value, 0, st))
}
PromiseState::Rejected => {
format!(
"Promise {{ <rejected> {} }}",
self.inspect_lvl(&c.value, 0, st)
)
}
},
None => "Promise { <pending> }".into(),
},
Some(JsObj::Func(f)) => {
let name = self.callable_name(v);
let kind = match self.funcs.get(f.def_id) {
Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction",
Some(d) if d.is_generator => "GeneratorFunction",
Some(d) if d.is_async => "AsyncFunction",
_ => "Function",
};
let base = if name.is_empty() {
format!("[{kind} (anonymous)]")
} else {
format!("[{kind}: {name}]")
};
self.with_callable_props(v, base, indent, st)
}
Some(JsObj::Builtin(n)) => {
if !builtin_is_callable(n) {
match crate::builtins::well_known_tag(self, v) {
Some(tag) => format!("Object [{tag}] {{}}"),
None => {
format!("Object [{}] {{}}", n.trim_end_matches(".prototype"))
}
}
} else {
format!("[Function: {}]", crate::builtins::builtin_name(n))
}
}
Some(JsObj::BoundMethod { name, .. }) => format!("[Function: {name}]"),
Some(JsObj::BoundFunc { target, .. }) => {
let n = self.callable_name(target);
if n.is_empty() {
"[Function: bound ]".into()
} else {
format!("[Function: bound {n}]")
}
}
_ => "undefined".into(),
},
_ => "undefined".into(),
}
}
fn with_callable_props(
&self,
v: &Value,
base: String,
indent: usize,
st: &mut InspectCycles,
) -> String {
let mut inner: Vec<String> = self
.own_enum_key_names(v)
.into_iter()
.map(|k| {
let val = self.fn_prop(v, &k).unwrap_or(Value::Undef);
format!(
"{}: {}",
fmt_key(&k),
self.inspect_lvl(&val, indent + 2, st)
)
})
.collect();
for (k, val) in self.own_symbol_entries(v) {
if let Some(sym) = self.symbol_of_key(&k) {
inner.push(format!(
"{}: {}",
self.inspect(&sym),
self.inspect_lvl(&val, indent + 2, st)
));
}
}
if inner.is_empty() {
return base;
}
self.render_object(&inner, &format!("{base} "), indent, st)
}
fn render_array(
&self,
output: &[String],
values: &[Value],
indent: usize,
opts: ArrayLayout<'_>,
st: &InspectCycles,
) -> String {
let ArrayLayout {
has_props,
has_tail,
base,
} = opts;
let entries = output.len();
let (lines, grouped) = if entries > 6 && !has_props && inspect_compact() >= 1 {
group_array_elements(self, output, values, indent, has_tail)
} else {
(output.to_vec(), false)
};
if output.is_empty() {
return format!("{base}[]");
}
if !grouped {
let start = output.len() + indent + 1 + base.chars().count() + 10;
if self.may_compact(indent, st) && is_below_break_length(output, start) {
return format!("{base}[ {} ]", output.join(", "));
}
}
let pad = " ".repeat(indent);
let sep = format!(",\n{pad} ");
format!("{base}[\n{pad} {}\n{pad}]", lines.join(&sep))
}
fn may_compact(&self, indent: usize, st: &InspectCycles) -> bool {
let compact = inspect_compact();
if compact < 1 {
return false;
}
let depth_below = (st.deepest.saturating_sub(indent)) / 2;
(depth_below as i64) < compact
}
fn render_object(
&self,
output: &[String],
prefix: &str,
indent: usize,
st: &InspectCycles,
) -> String {
let sorted_output;
let output = if inspect_sorted() {
let mut v = output.to_vec();
v.sort();
sorted_output = v;
&sorted_output[..]
} else {
output
};
let braces0 = prefix.chars().count() + 1;
let start = output.len() + indent + braces0 + 10;
if self.may_compact(indent, st) && is_below_break_length(output, start) {
return format!("{prefix}{{ {} }}", output.join(", "));
}
let pad = " ".repeat(indent);
let sep = format!(",\n{pad} ");
format!("{prefix}{{\n{pad} {}\n{pad}}}", output.join(&sep))
}
pub fn callable_name(&self, v: &Value) -> String {
if let Some(n) = self.fn_prop(v, "name") {
return self.str_of(&n);
}
match self.get(v) {
Some(JsObj::Func(f)) => self
.funcs
.get(f.def_id)
.map(|d| d.name.clone())
.unwrap_or_default(),
Some(JsObj::Class(c)) => c.name.clone(),
Some(JsObj::Builtin(n)) => crate::builtins::builtin_name(n).to_string(),
Some(JsObj::BoundFunc { target, .. }) => {
format!("bound {}", self.callable_name(target))
}
Some(JsObj::BoundMethod { name, .. }) => name.clone(),
_ => String::new(),
}
}
pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Undef, Value::Undef) => true,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Str(x), Value::Str(y)) => x == y,
_ => {
let an = matches!(a, Value::Int(_) | Value::Float(_));
let bn = matches!(b, Value::Int(_) | Value::Float(_));
if an && bn {
let x = self.to_number(a);
let y = self.to_number(b);
return x == y;
}
if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
return x == y;
}
if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
return sa == sb;
}
let na = self.is_null(a);
let nb = self.is_null(b);
if na || nb {
return na && nb;
}
if let (Some(JsObj::Builtin(x)), Some(JsObj::Builtin(y))) =
(self.get(a), self.get(b))
{
return builtin_identity(x) == builtin_identity(y);
}
matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
}
}
}
pub fn is_nullish(&self, v: &Value) -> bool {
matches!(v, Value::Undef) || self.is_null(v)
}
fn js_type(&self, v: &Value) -> &'static str {
match v {
Value::Undef => "undefined",
Value::Bool(_) => "boolean",
Value::Int(_) | Value::Float(_) => "number",
Value::Str(_) => "string",
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(_)) => "string",
Some(JsObj::Null) => "null",
Some(JsObj::BigInt(_)) => "bigint",
_ => "object",
},
_ => "object",
}
}
pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
if self.strict_eq(a, b) {
return true;
}
let ta = self.js_type(a);
let tb = self.js_type(b);
if self.is_nullish(a) || self.is_nullish(b) {
return self.is_nullish(a) && self.is_nullish(b);
}
if ta == "bigint" || tb == "bigint" {
return self.bigint_loose_eq(a, b);
}
if ta == tb {
return false;
}
if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
return self.to_number(a) == self.to_number(b);
}
if ta == "boolean" {
return self.loose_eq(&Value::Float(self.to_number(a)), b);
}
if tb == "boolean" {
return self.loose_eq(a, &Value::Float(self.to_number(b)));
}
if ta == "object" && (tb == "number" || tb == "string") {
let pa = self.str_of(a);
return if tb == "string" {
pa == self.str_of(b)
} else {
str_to_number(&pa) == self.to_number(b)
};
}
if tb == "object" && (ta == "number" || ta == "string") {
let pb = self.str_of(b);
return if ta == "string" {
self.str_of(a) == pb
} else {
self.to_number(a) == str_to_number(&pb)
};
}
false
}
pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
use NumOp::*;
match op {
Add => {
let a_str = self.prefers_string(a);
let b_str = self.prefers_string(b);
if a_str || b_str {
let s = format!("{}{}", self.str_of(a), self.str_of(b));
Ok(self.new_str(s))
} else if self.is_bigint_val(a) || self.is_bigint_val(b) {
self.bigint_arith(op, a, b)
} else {
Ok(Value::Float(self.to_number(a) + self.to_number(b)))
}
}
Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
self.bigint_arith(op, a, b)
}
Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
Pow => Ok(Value::Float(crate::builtins::js_pow(
self.to_number(a),
self.to_number(b),
))),
Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
Neg => Ok(Value::Float(-self.to_number(a))),
Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
Eq => Ok(Value::Bool(self.loose_eq(a, b))),
Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
}
}
fn prefers_string(&self, v: &Value) -> bool {
match v {
Value::Str(_) => true,
Value::Obj(_) => !matches!(
self.get(v),
Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
),
_ => false,
}
}
fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
use std::cmp::Ordering;
let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
x.cmp(&y)
} else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
crate::utf16::cmp_units(&x, &y)
} else {
let x = self.to_number(a);
let y = self.to_number(b);
match x.partial_cmp(&y) {
Some(o) => o,
None => return false, }
};
match op {
NumOp::Lt => ord == Ordering::Less,
NumOp::Le => ord != Ordering::Greater,
NumOp::Gt => ord == Ordering::Greater,
NumOp::Ge => ord != Ordering::Less,
_ => false,
}
}
pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
if self.is_bigint_val(a) || self.is_bigint_val(b) {
return self.bigint_bitwise(tag, a, b);
}
let x = to_int32(self.to_number(a));
let y = to_int32(self.to_number(b));
let r: i64 = match tag {
binop::BITAND => (x & y) as i64,
binop::BITOR => (x | y) as i64,
binop::BITXOR => (x ^ y) as i64,
binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
binop::SHR => (x >> ((y as u32) & 31)) as i64,
binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
_ => 0,
};
Ok(Value::Float(r as f64))
}
pub fn is_bigint_val(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::BigInt(_)))
}
pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
match self.get(v) {
Some(JsObj::BigInt(b)) => Some(b.clone()),
_ => None,
}
}
pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
self.alloc(JsObj::BigInt(b))
}
fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
use num_traits::{Signed, Zero};
use NumOp::*;
if op == Neg {
let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
return Ok(self.new_bigint(-x));
}
let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
(Some(x), Some(y)) => (x, y),
_ => {
return Err(type_error(
"Cannot mix BigInt and other types, use explicit conversions",
))
}
};
let r = match op {
Add => x + y,
Sub => x - y,
Mul => x * y,
Div => {
if y.is_zero() {
return Err("RangeError: Division by zero".into());
}
x / y }
Mod => {
if y.is_zero() {
return Err("RangeError: Division by zero".into());
}
x % y }
Pow => {
if y.is_negative() {
return Err("RangeError: Exponent must be positive".into());
}
let exp = num_traits::ToPrimitive::to_u32(&y)
.ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
num_traits::Pow::pow(x, exp)
}
_ => return Err(type_error("unsupported BigInt operation")),
};
Ok(self.new_bigint(r))
}
fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
(Some(x), Some(y)) => (x, y),
_ => {
return Err(type_error(
"Cannot mix BigInt and other types, use explicit conversions",
))
}
};
let r = match tag {
binop::BITAND => x & y,
binop::BITOR => x | y,
binop::BITXOR => x ^ y,
binop::SHL => {
let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
if n >= 0 {
x << (n as usize)
} else {
x >> ((-n) as usize)
}
}
binop::SHR => {
let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
if n >= 0 {
x >> (n as usize)
} else {
x << ((-n) as usize)
}
}
binop::USHR => {
return Err(type_error(
"BigInts have no unsigned right shift, use >> instead",
))
}
_ => return Err(type_error("unsupported BigInt operation")),
};
Ok(self.new_bigint(r))
}
fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
(Some(x), _) => (x, b),
(_, Some(y)) => (y, a),
_ => return false,
};
match other {
Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
Value::Int(n) => big == num_bigint::BigInt::from(*n),
Value::Float(f) => {
if !f.is_finite() || f.fract() != 0.0 {
return false;
}
bigint_to_f64(&big) == *f
}
Value::Str(s) => match parse_bigint_str(s) {
Some(bs) => big == bs,
None => false,
},
Value::Obj(_) => match self.get(other) {
Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
_ => {
let s = self.str_of(other);
parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
}
},
_ => false,
}
}
}
pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
let t = crate::utf16::js_trim(s);
if t.is_empty() {
return Some(num_bigint::BigInt::from(0));
}
let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
(16, h)
} else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
(8, o)
} else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
(2, bb)
} else {
(10, t)
};
num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
}
pub fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
if num_traits::Signed::is_negative(b) {
f64::NEG_INFINITY
} else {
f64::INFINITY
}
})
}
fn js_mod(a: f64, b: f64) -> f64 {
a % b
}
#[derive(Clone, Copy)]
struct ArrayLayout<'a> {
has_props: bool,
has_tail: bool,
base: &'a str,
}
#[derive(Default)]
struct InspectCycles {
seen: Vec<Value>,
refs: Vec<Value>,
deepest: usize,
}
impl InspectCycles {
fn mark(&mut self, h: &JsHost, v: &Value) -> usize {
if let Some(id) = self.id_of(h, v) {
return id;
}
self.refs.push(v.clone());
self.refs.len()
}
fn id_of(&self, h: &JsHost, v: &Value) -> Option<usize> {
self.refs
.iter()
.position(|p| h.strict_eq(p, v))
.map(|i| i + 1)
}
}
thread_local! {
static INSPECT_MAX_DEPTH: std::cell::Cell<i64> = const { std::cell::Cell::new(2) };
static INSPECT_COMPACT: std::cell::Cell<i64> = const { std::cell::Cell::new(DEFAULT_COMPACT) };
static INSPECT_BREAK_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(80) };
static INSPECT_SORTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static INSPECT_MAX_ARRAY_LENGTH: std::cell::Cell<usize> = const { std::cell::Cell::new(DEFAULT_MAX_ARRAY_LENGTH) };
static INSPECT_CUSTOM: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
static INSPECT_SHOW_HIDDEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub fn set_inspect_show_hidden(s: bool) {
INSPECT_SHOW_HIDDEN.with(|x| x.set(s));
}
pub(crate) fn inspect_show_hidden() -> bool {
INSPECT_SHOW_HIDDEN.with(|x| x.get())
}
pub fn set_inspect_custom(c: bool) {
INSPECT_CUSTOM.with(|x| x.set(c));
}
pub(crate) fn inspect_custom() -> bool {
INSPECT_CUSTOM.with(|x| x.get())
}
pub fn set_inspect_sorted(s: bool) {
INSPECT_SORTED.with(|x| x.set(s));
}
pub(crate) fn inspect_sorted() -> bool {
INSPECT_SORTED.with(|x| x.get())
}
pub fn set_inspect_max_array_length(n: usize) {
INSPECT_MAX_ARRAY_LENGTH.with(|x| x.set(n));
}
pub(crate) fn inspect_max_array_length() -> usize {
INSPECT_MAX_ARRAY_LENGTH.with(|x| x.get())
}
pub fn set_inspect_compact(c: i64) {
INSPECT_COMPACT.with(|x| x.set(c));
}
pub fn set_inspect_break_length(n: usize) {
INSPECT_BREAK_LENGTH.with(|x| x.set(n));
}
fn inspect_compact() -> i64 {
INSPECT_COMPACT.with(|x| x.get())
}
pub fn set_inspect_max_depth(d: i64) {
INSPECT_MAX_DEPTH.with(|c| c.set(d));
}
fn inspect_indent_limit() -> i64 {
inspect_max_depth().saturating_mul(2)
}
fn inspect_max_depth() -> i64 {
INSPECT_MAX_DEPTH.with(|c| c.get())
}
pub(crate) fn to_int32(f: f64) -> i32 {
to_uint32(f) as i32
}
pub(crate) fn to_uint32(f: f64) -> u32 {
if !f.is_finite() {
return 0;
}
f.trunc().rem_euclid(4294967296.0) as u32
}
fn str_to_number(s: &str) -> f64 {
let t = crate::utf16::js_trim(s);
if t.is_empty() {
return 0.0;
}
if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
return i64::from_str_radix(hex, 16)
.map(|n| n as f64)
.unwrap_or(f64::NAN);
}
if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
return i64::from_str_radix(oct, 8)
.map(|n| n as f64)
.unwrap_or(f64::NAN);
}
if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
return i64::from_str_radix(bin, 2)
.map(|n| n as f64)
.unwrap_or(f64::NAN);
}
match t {
"Infinity" | "+Infinity" => f64::INFINITY,
"-Infinity" => f64::NEG_INFINITY,
_ => t.parse::<f64>().unwrap_or(f64::NAN),
}
}
fn break_length() -> usize {
INSPECT_BREAK_LENGTH.with(|x| x.get())
}
const DEFAULT_COMPACT: i64 = 3;
pub(crate) const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
fn is_below_break_length(output: &[String], start: usize) -> bool {
let limit = break_length();
let mut total = output.len() + start;
if total + output.len() > limit {
return false;
}
for o in output {
if o.contains('\n') {
return false;
}
total += o.chars().count();
if total > limit {
return false;
}
}
true
}
fn group_array_elements(
host: &JsHost,
output: &[String],
values: &[Value],
indentation_lvl: usize,
has_tail: bool,
) -> (Vec<String>, bool) {
let separator_space = 2usize; let output_length = output.len() - usize::from(has_tail);
let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
let mut total_length = 0usize;
let mut max_length = 0usize;
for &len in &data_len[..output_length] {
total_length += len + separator_space;
if len > max_length {
max_length = len;
}
}
let actual_max = max_length + separator_space;
if !(actual_max * 3 + indentation_lvl < break_length()
&& (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
{
return (output.to_vec(), false);
}
let approx_char_heights = 2.5f64;
let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
let columns = [
((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
as i64,
((break_length() - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
inspect_compact().saturating_mul(4),
15,
]
.into_iter()
.min()
.unwrap();
if columns <= 1 {
return (output.to_vec(), false);
}
let columns = columns as usize;
let mut max_line_length = vec![0usize; columns];
for (i, slot) in max_line_length.iter_mut().enumerate() {
let mut line_length = 0;
let mut j = i;
while j < output_length {
if data_len[j] > line_length {
line_length = data_len[j];
}
j += columns;
}
*slot = line_length + separator_space;
}
let pad_start = values.iter().all(|v| {
matches!(v, Value::Int(_) | Value::Float(_))
|| matches!(host.get(v), Some(JsObj::BigInt(_)))
});
let mut tmp = Vec::new();
let mut i = 0;
while i < output_length {
let max = (i + columns).min(output_length);
let mut str_line = String::new();
let mut j = i;
while j < max.saturating_sub(1) {
let col = j - i;
let cell = format!("{}, ", output[j]);
let target = max_line_length[col];
str_line.push_str(&pad_to(&cell, target, pad_start));
j += 1;
}
if pad_start {
let col = j - i;
let target = max_line_length[col] - separator_space;
str_line.push_str(&pad_to(&output[j], target, true));
} else {
str_line.push_str(&output[j]);
}
tmp.push(str_line);
i += columns;
}
if has_tail {
tmp.push(output[output_length].clone());
}
(tmp, true)
}
fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
let len = s.chars().count();
if len >= width {
return s.to_string();
}
let fill = " ".repeat(width - len);
if pad_start {
format!("{fill}{s}")
} else {
format!("{s}{fill}")
}
}
fn quote_str(s: &str) -> String {
let quote = if !s.contains('\'') {
'\''
} else if !s.contains('"') {
'"'
} else if !s.contains('`') && !s.contains("${") {
'`'
} else {
'\''
};
let mut out = String::with_capacity(s.len() + 2);
out.push(quote);
for c in s.chars() {
match c {
_ if c == quote => {
out.push('\\');
out.push(c);
}
'\\' => out.push_str("\\\\"),
'\u{8}' => out.push_str("\\b"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\u{c}' => out.push_str("\\f"),
'\r' => out.push_str("\\r"),
'\u{0}'..='\u{1f}' | '\u{7f}' => out.push_str(&format!("\\x{:02X}", c as u32)),
_ => out.push(c),
}
}
out.push(quote);
out
}
fn fmt_key(k: &str) -> String {
let ok = !k.is_empty()
&& k.chars()
.next()
.map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
.unwrap_or(false)
&& k.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
if ok {
k.to_string()
} else {
quote_str(k)
}
}
impl JsHost {
pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
match self.get(v) {
Some(JsObj::Array(items)) => Ok(items.clone()),
Some(JsObj::Str(s)) => {
let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
}
Some(JsObj::Iter {
idx,
array: Some((arr, kind)),
..
}) => {
let (idx, kind) = (*idx, *kind);
let items = match self.get(arr) {
Some(JsObj::Array(items)) if idx < items.len() => items[idx..].to_vec(),
_ => Vec::new(),
};
Ok(items
.into_iter()
.enumerate()
.map(|(n, v)| {
let key = Value::Float((idx + n) as f64);
match kind {
ArrayIterKind::Keys => key,
ArrayIterKind::Values => v,
ArrayIterKind::Entries => self.new_array(vec![key, v]),
}
})
.collect())
}
Some(JsObj::Iter { items, idx, .. }) => Ok(items[*idx..].to_vec()),
Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
Some(JsObj::Map { entries, .. }) => {
let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
Ok(pairs
.into_iter()
.map(|(k, v)| self.new_array(vec![k, v]))
.collect())
}
Some(JsObj::Object(props))
if props.contains_key("@@bytes") || props.contains_key("@@buffer") =>
{
if crate::stdlib::typedarray::view_detached_h(self, v) {
return Err(crate::stdlib::typedarray::detached_error(
"%TypedArray%.prototype",
"values",
false,
));
}
Ok(crate::stdlib::typedarray::elems_mut_host(self, v))
}
_ => {
let shown = self.inspect(v);
Err(type_error(&format!("{shown} is not iterable")))
}
}
}
pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
let mut keys = self.own_enum_key_names(v);
let mut cur = self.proto_of(v);
let mut hops = 0;
while let Some(p) = cur {
hops += 1;
if hops > 100 || matches!(p, Value::Undef) || self.is_null(&p) {
break;
}
for k in self.own_enum_key_names(&p) {
if !keys.contains(&k) {
keys.push(k);
}
}
cur = self.proto_of(&p);
}
keys.into_iter().map(|k| self.new_str(k)).collect()
}
pub fn own_enum_key_names(&self, v: &Value) -> Vec<String> {
self.own_key_names(v, true)
}
pub fn own_key_names(&self, v: &Value, enum_only: bool) -> Vec<String> {
let mut keys = self.own_enum_data_keys(v, enum_only);
if self.is_global_object(v) {
for k in self.globals.keys() {
if !keys.contains(k) {
keys.push(k.clone());
}
}
}
if !enum_only && matches!(self.get(v), Some(JsObj::RegExp(_))) {
keys.push("lastIndex".to_string());
}
for k in self.own_accessor_keys(v) {
if (!enum_only || self.prop_attrs(v, &k).enumerable) && !keys.contains(&k) {
keys.push(k);
}
}
keys
}
pub fn script_global_names(&self) -> Vec<String> {
self.globals.keys().cloned().collect()
}
pub fn remove_global(&mut self, name: &str) -> bool {
self.globals.shift_remove(name).is_some()
}
fn own_enum_data_keys(&self, v: &Value, enum_only: bool) -> Vec<String> {
match self.get(v) {
Some(JsObj::Object(props))
if matches!(
props.get("@@native").map(|t| self.str_of(t)).as_deref(),
Some("Buffer") | Some("TypedArray")
) =>
{
if crate::stdlib::typedarray::view_detached_h(self, v) {
return Vec::new();
}
let n = match props.get("@@bytes").and_then(|b| self.get(b)) {
Some(JsObj::Array(items)) => items.len(),
_ => props
.get("length")
.map(|l| self.to_number(l))
.unwrap_or(0.0) as usize,
};
(0..n).map(|i| i.to_string()).collect()
}
Some(JsObj::Object(props)) => props
.keys()
.filter_map(|k| match k.strip_prefix(ORD_MARKER) {
Some(real) => Some(real.to_string()),
None if !k.starts_with("@@") && !k.starts_with('#') => Some(k.clone()),
None => None,
})
.filter(|k| !enum_only || self.prop_attrs(v, k).enumerable)
.collect(),
Some(JsObj::Str(s)) => {
let mut keys: Vec<String> =
(0..crate::utf16::len(s)).map(|i| i.to_string()).collect();
if !enum_only {
keys.push("length".into());
}
keys
}
Some(JsObj::Array(items)) => {
let mut keys: Vec<String> = (0..items.len())
.filter(|i| !self.is_hole(v, *i))
.map(|i| i.to_string())
.collect();
if !enum_only {
keys.push("length".into());
}
keys.extend(self.fn_prop_keys(v).into_iter().filter(|k| {
!k.starts_with("@@")
&& !k.starts_with('#')
&& (!enum_only || self.prop_attrs(v, k).enumerable)
}));
keys
}
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
let mut keys: Vec<String> = Vec::new();
if !enum_only {
keys.push("length".into());
keys.push("name".into());
if self.owns_prototype(v) {
keys.push("prototype".into());
}
}
let rest: Vec<String> = self
.fn_prop_keys(v)
.into_iter()
.filter_map(|k| match k.strip_prefix(ORD_MARKER) {
Some(real) => Some(real.to_string()),
None if !k.starts_with("@@") && !k.starts_with('#') => Some(k),
None => None,
})
.filter(|k| {
!keys.contains(k) && (!enum_only || self.prop_attrs(v, k).enumerable)
})
.collect();
keys.extend(rest);
keys
}
Some(JsObj::Builtin(ns)) => crate::stdlib::namespace_keys(&ns.clone()),
Some(_) => self
.fn_prop_keys(v)
.into_iter()
.filter(|k| {
!k.starts_with("@@")
&& !k.starts_with('#')
&& (!enum_only || self.prop_attrs(v, k).enumerable)
})
.collect(),
_ => Vec::new(),
}
}
pub fn own_enum_entries(&self, v: &Value) -> Vec<(String, Value)> {
self.own_enum_key_names(v)
.into_iter()
.map(|k| {
let val = match self.get(v) {
Some(JsObj::Object(props)) => props.get(&k).cloned().unwrap_or_else(|| {
match k.parse::<usize>() {
Ok(i) => crate::stdlib::typedarray::elems_with_host(self, v)
.get(i)
.cloned()
.unwrap_or(Value::Undef),
_ => Value::Undef,
}
}),
Some(
JsObj::Map { .. }
| JsObj::Set { .. }
| JsObj::Promise { .. }
| JsObj::RegExp(_)
| JsObj::Generator { .. }
| JsObj::Symbol { .. }
| JsObj::BigInt(_)
| JsObj::Iter { .. },
) => self.fn_prop(v, &k).unwrap_or(Value::Undef),
Some(JsObj::Array(items)) => k
.parse::<usize>()
.ok()
.and_then(|i| items.get(i).cloned())
.or_else(|| self.fn_prop(v, &k))
.unwrap_or(Value::Undef),
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
self.fn_prop(v, &k).unwrap_or(Value::Undef)
}
_ => Value::Undef,
};
(k, val)
})
.collect()
}
}
pub fn own_enum_entries_deep(v: &Value) -> Result<Vec<(String, Value)>, String> {
if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
return crate::proxy::own_enum_entries(v);
}
if let Some(ns) = with_host(|h| match h.get(v) {
Some(JsObj::Builtin(ns)) => Some(ns.clone()),
_ => None,
}) {
return Ok(with_host(|h| h.own_enum_key_names(v))
.into_iter()
.map(|k| {
let val = crate::builtins::namespace_property(&ns, &k);
(k, val)
})
.collect());
}
if let Some(sv) = with_host(|h| match h.get(v) {
Some(JsObj::Str(s)) => Some(s.clone()),
_ => None,
}) {
let units = crate::utf16::Units::of(&sv);
return Ok(with_host(|h| {
(0..units.len())
.filter_map(|i| units.unit_str(i).map(|c| (i.to_string(), h.new_str(c))))
.collect()
}));
}
let accessor_keys: Vec<String> = with_host(|h| {
h.own_accessor_keys(v)
.into_iter()
.filter(|k| h.prop_attrs(v, k).enumerable)
.collect()
});
let entries = with_host(|h| h.own_enum_entries(v));
let mut out = Vec::with_capacity(entries.len());
for (k, val) in entries {
if accessor_keys.contains(&k) {
out.push((k.clone(), get_prop_chain(v, &k)?));
} else {
out.push((k, val));
}
}
Ok(out)
}
fn marshal_ffi_arg(v: &Value) -> Value {
match v {
Value::Obj(_) => match with_host(|h| h.as_str(v)) {
Some(s) => Value::str(s),
None => v.clone(),
},
_ => v.clone(),
}
}
pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
if name == "__rust_compile" {
let b64 = args
.first()
.map(|v| with_host(|h| h.str_of(v)))
.unwrap_or_default();
return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
}
if let Some(v) = with_host(|h| h.read_name(name)) {
return invoke(&v, args, None);
}
if name == "eval" {
return crate::builtins::eval_source(args.first(), true);
}
if crate::builtins::is_known_builtin(name) {
return crate::builtins::call_builtin_function(name, args);
}
if fusevm::ffi::is_registered(name) {
let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
if let Some(r) = fusevm::ffi::try_call(name, &margs) {
return r;
}
}
Err(ref_error(name))
}
thread_local! {
static STATIC_THIS: std::cell::RefCell<Vec<Value>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn with_static_this<R>(recv: &Value, f: impl FnOnce() -> R) -> R {
STATIC_THIS.with(|s| s.borrow_mut().push(recv.clone()));
let out = f();
STATIC_THIS.with(|s| {
s.borrow_mut().pop();
});
out
}
pub fn current_static_this() -> Option<Value> {
STATIC_THIS.with(|s| s.borrow().last().cloned())
}
pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
if with_host(|h| h.is_nullish(recv)) {
return Err(type_error(&format!(
"Cannot read properties of {} (reading '{name}')",
with_host(|h| h.str_of(recv))
)));
}
if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
return Err(crate::builtins::private_brand_message(name, false));
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
let f = crate::builtins::get_property(recv, name)?;
if !with_host(|h| is_callable(h, &f)) {
return Err(type_error(&format!("{name} is not a function")));
}
if with_host(|h| matches!(h.get(&f), Some(JsObj::BoundMethod { .. }))) {
if with_host(|h| is_callable(h, recv)) {
if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
return Ok(r);
}
}
if matches!(
name,
"hasOwnProperty" | "propertyIsEnumerable" | "isPrototypeOf"
) {
return crate::builtins::object_builtin_method(recv, name, args);
}
if matches!(name, "toString" | "valueOf" | "toLocaleString") {
return invoke(&f, args, None);
}
}
return invoke(&f, args, Some(recv.clone()));
}
if let Some(ns) = with_host(|h| match h.get(recv) {
Some(JsObj::Builtin(ns)) => Some(ns.clone()),
_ => None,
}) {
let qualified = format!("{ns}.{name}");
if crate::builtins::is_known_builtin(&qualified) {
return crate::builtins::call_builtin_function(&qualified, args);
}
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
if let Some(tag) = crate::stdlib::native_tag(recv) {
if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if crate::builtins::is_object_builtin_method(name)
&& !crate::stdlib::instance_has_method(&tag, name)
{
return crate::builtins::object_builtin_method(recv, name, args);
}
return crate::stdlib::instance_call(&tag, recv, name, args);
}
if let Some(prim) = crate::builtins::wrapped_primitive(recv) {
if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if crate::builtins::is_object_builtin_method(name) {
return crate::builtins::object_builtin_method(recv, name, args);
}
return call_method(&prim, name, args);
}
if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if crate::builtins::proxy_proto_link(recv, name).is_some() {
let f = crate::builtins::get_property(recv, name)?;
if !with_host(|h| is_callable(h, &f)) {
return Err(type_error(&format!("{name} is not a function")));
}
return invoke(&f, args, Some(recv.clone()));
}
if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
return Err(type_error(&format!("{name} is not a function")));
}
if let Some(f) = crate::builtins::inherited_builtin_static(recv, name) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if let Some(owner) = crate::builtins::inherited_method_owner_pub(recv, name) {
if owner != "Object" {
return crate::builtins::proto_method(recv, &format!("{owner}:{name}"), args);
}
}
if crate::builtins::is_object_builtin_method(name) {
return crate::builtins::object_builtin_method(recv, name, args);
}
if name == "constructor" {
if let Some(r) = call_default_ctor(recv, &args) {
return r;
}
}
return Err(type_error(&format!("{name} is not a function")));
}
if matches!(
with_host(|h| h.kind_of(recv)),
Some(ObjKind::Func)
| Some(ObjKind::Class)
| Some(ObjKind::BoundFunc)
| Some(ObjKind::BoundMethod)
| Some(ObjKind::Builtin)
) {
if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
return Ok(r);
}
let stat = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
with_host(|h| h.class_static(recv, name))
} else {
with_host(|h| h.fn_prop(recv, name))
};
if let Some(f) = stat {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
if with_host(|h| h.kind_of(&anc)) == Some(ObjKind::Builtin) {
return with_static_this(recv, || call_method(&anc, name, args));
}
}
}
if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin)
&& crate::builtins::is_object_builtin_method(name)
{
return crate::builtins::object_builtin_method(recv, name, args);
}
}
if name == "constructor" {
if let Some(r) = call_default_ctor(recv, &args) {
return r;
}
}
crate::builtins::call_type_method(recv, name, args)
}
fn call_default_ctor(recv: &Value, args: &[Value]) -> Option<Result<Value, String>> {
let ctor = crate::builtins::get_property(recv, "constructor").ok()?;
with_host(|h| is_callable(h, &ctor)).then(|| invoke(&ctor, args.to_vec(), None))
}
pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
if with_host(|h| h.kind_of(callable)) == Some(ObjKind::Proxy) {
return crate::proxy::apply(callable, args, this).map(|r| r.expect("kind_of said Proxy"));
}
let obj = with_host(|h| h.get(callable).cloned());
match obj {
Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
let recv = this.unwrap_or(Value::Undef);
crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
}
Some(JsObj::Builtin(name)) if name.starts_with("@protoset:") => {
let _ = &name;
let recv = this.unwrap_or(Value::Undef);
if with_host(|h| h.fn_is_sloppy(&recv)) {
Ok(Value::Undef)
} else {
Err(type_error(crate::builtins::POISON_PILL))
}
}
Some(JsObj::Builtin(name)) if name.starts_with("@protoget:") => {
let recv = this.unwrap_or(Value::Undef);
let rest = &name["@protoget:".len()..];
let (ctor, key) = rest.split_once(':').unwrap_or((rest, ""));
crate::builtins::proto_getter_call(ctor, key, &recv)
}
Some(JsObj::Builtin(ref name)) if steals_ctor(name, this.as_ref()) => {
let target = this.expect("guard checked");
let built = crate::stdlib::construct(name, &args)
.expect("guard checked a native constructor")?;
adopt_native_slots(&target, &built);
Ok(Value::Undef)
}
Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
Some(JsObj::Func(fv)) => run_user_func_of(&fv, args, this, Some(callable.clone())),
Some(JsObj::BoundMethod { recv, name }) => {
let target = match &this {
Some(t) if !matches!(t, Value::Undef) && !with_host(|h| h.is_null(t)) => t,
_ => &recv,
};
if with_host(|h| h.kind_of(&recv)) == Some(ObjKind::Array) {
return crate::builtins::proto_method(target, &format!("Array:{name}"), args);
}
call_method(target, &name, args)
}
Some(JsObj::BoundFunc {
target,
this: bthis,
args: pre,
}) => {
let mut all = pre;
all.extend(args);
invoke(&target, all, Some(bthis))
}
Some(JsObj::Class(c)) => Err(type_error(&format!(
"Class constructor {} cannot be invoked without 'new'",
c.name
))),
_ => Err(type_error(&format!(
"{} is not a function",
with_host(|h| h.str_of(callable))
))),
}
}
fn steals_ctor(name: &str, this: Option<&Value>) -> bool {
let Some(target) = this else { return false };
if !with_host(|h| matches!(h.get(target), Some(JsObj::Object(_)))) {
return false;
}
if crate::stdlib::native_tag(target).is_some() {
return false;
}
let Some(proto) = with_host(|h| h.ensure_ctor_proto(name)) else {
return false;
};
let mut cur = with_host(|h| h.proto_of(target));
while let Some(p) = cur {
if p == proto {
return true;
}
cur = with_host(|h| h.proto_of(&p));
}
false
}
fn adopt_native_slots(target: &Value, built: &Value) {
let slots: Vec<(String, Value)> = with_host(|h| match h.get(built) {
Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => Vec::new(),
});
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(target) {
for (k, v) in slots {
p.insert(k, v);
}
}
});
}
pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
run_user_func_of(fv, args, this, None)
}
pub fn run_user_func_of(
fv: &FuncVal,
args: Vec<Value>,
this: Option<Value>,
callee: Option<Value>,
) -> Result<Value, String> {
run_user_func_full(fv, args, this, None, callee)
}
pub fn run_user_func_nt(
fv: &FuncVal,
args: Vec<Value>,
this: Option<Value>,
new_target: Option<Value>,
) -> Result<Value, String> {
run_user_func_full(fv, args, this, new_target, None)
}
fn run_user_func_full(
fv: &FuncVal,
args: Vec<Value>,
this: Option<Value>,
new_target: Option<Value>,
callee: Option<Value>,
) -> Result<Value, String> {
let derived_ctor = with_host(|h| std::mem::take(&mut h.derived_ctor_next));
let (params, is_generator, is_async, is_arrow_def, def_name) = with_host(|h| {
let d = &h.funcs[fv.def_id];
(
d.params.clone(),
d.is_generator,
d.is_async,
d.is_arrow,
d.name.clone(),
)
});
let env = new_env(fv.env.clone());
let fn_is_sloppy = with_host(|h| !h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
bind_params(
&env,
¶ms,
args,
is_arrow_def,
callee.as_ref(),
fn_is_sloppy && !is_arrow_def,
);
let mut this_val = if fv.is_arrow { fv.this.clone() } else { this };
let sloppy_this = !fv.is_arrow
&& !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict))
&& match &this_val {
None => true,
Some(v) => matches!(v, Value::Undef) || with_host(|h| h.is_null(v)),
};
if sloppy_this {
this_val = Some(with_host(|h| h.global_object()));
} else if !fv.is_arrow && !with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)) {
if let Some(t) = this_val.clone() {
let boxed = crate::builtins::to_object(&t);
this_val = Some(boxed);
}
}
if is_generator {
let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
let gen = make_generator(
chunk,
env,
this_val,
fv.home_class.clone(),
fv.home_static,
fv.home_object.clone(),
with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
);
if is_async {
if let Some(JsObj::Generator { id }) = with_host(|h| h.get(&gen).cloned()) {
with_host(|h| h.generators[id as usize].async_gen = true);
}
}
return Ok(gen);
}
if is_async {
let chunk = with_host(|h| h.funcs[fv.def_id].chunk.clone());
let gen = make_generator(
chunk,
env,
this_val,
fv.home_class.clone(),
fv.home_static,
fv.home_object.clone(),
with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict)),
);
return Ok(run_async(gen));
}
let home = fv
.home_class
.as_ref()
.and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
let fn_strict = with_host(|h| h.funcs.get(fv.def_id).is_some_and(|d| d.strict));
with_host(|h| {
h.frames.push(Frame {
base_env: env.clone(),
env,
this_obj: this_val,
new_target,
home_class: home,
home_static: fv.home_static,
home_object: fv.home_object.clone(),
strict: fn_strict,
line: 0,
owner: Some(def_name),
is_module: false,
this_state: if derived_ctor {
ThisState::Pending
} else {
ThisState::Plain
},
})
});
let r = run_chunk_keyed(func_key(fv.def_id), || {
with_host(|h| h.funcs[fv.def_id].chunk.clone())
});
let (sig, this_state) = with_host(|h| {
let frame = h.frames.pop();
(h.signal.take(), frame.map(|f| f.this_state))
});
let ret = match r {
Err(e) => return Err(e),
Ok(_) => match sig {
Some(Signal::Return(v)) => v,
_ => Value::Undef,
},
};
if derived_ctor && !returns_object(&ret) {
if !matches!(ret, Value::Undef) {
return Err(type_error(
"Derived constructors may only return object or undefined",
));
}
if this_state == Some(ThisState::Pending) {
return Err(this_before_super_error());
}
}
Ok(ret)
}
fn bind_params(
env: &Env,
params: &[ParamSlot],
args: Vec<Value>,
is_arrow: bool,
callee: Option<&Value>,
sloppy: bool,
) {
let mut vars = VarMap::default();
let mut i = 0;
for slot in params {
if slot.rest {
let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
let arr = with_host(|h| h.new_array(rest));
vars.insert(slot.name.clone(), arr);
} else {
let v = args.get(i).cloned().unwrap_or(Value::Undef);
vars.insert(slot.name.clone(), v);
i += 1;
}
}
if !is_arrow {
let args_arr = with_host(|h| {
let a = h.new_array(args);
h.set_fn_prop(&a, "@@arguments", Value::Bool(true));
if let Some(f) = callee {
if sloppy {
h.set_fn_prop(&a, "@@callee", f.clone());
}
}
a
});
vars.entry("arguments".to_string()).or_insert(args_arr);
}
env.borrow_mut().vars = vars;
}
pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
construct_nt(ctor, args, ctor.clone())
}
pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
if with_host(|h| h.kind_of(ctor)) == Some(ObjKind::Proxy) {
return crate::proxy::construct(ctor, args, &new_target)
.map(|r| r.expect("kind_of said Proxy"));
}
let obj = with_host(|h| h.get(ctor).cloned());
match obj {
Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
Some(JsObj::Func(fv)) => {
let non_ctor = with_host(|h| {
h.funcs
.get(fv.def_id)
.map(|d| d.is_generator || d.is_async || d.is_method)
.unwrap_or(false)
});
if fv.is_arrow || non_ctor {
return Err(not_a_constructor(ctor));
}
let inst = with_host(|h| {
let o = h.new_object(IndexMap::new());
let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
let p = h.new_object(IndexMap::new());
if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
pp.insert("constructor".to_string(), ctor.clone());
}
h.hide_prop(&p, "constructor");
h.set_fn_prop(ctor, "prototype", p.clone());
p
});
h.set_proto(&o, proto);
o
});
let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
if returns_object(&r) {
Ok(r)
} else {
Ok(inst)
}
}
Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
Some(JsObj::BoundFunc {
target, args: pre, ..
}) => {
let mut all = pre;
all.extend(args);
construct_nt(&target, all, new_target)
}
_ => Err(not_a_constructor(ctor)),
}
}
fn not_a_constructor(ctor: &Value) -> String {
let name = with_host(|h| match h.callable_name(ctor) {
n if n.is_empty() => h.str_of(ctor),
n => n,
});
type_error(&format!("{name} is not a constructor"))
}
fn returns_object(r: &Value) -> bool {
matches!(
with_host(|h| h.get(r).cloned()),
Some(JsObj::Object(_))
| Some(JsObj::Array(_))
| Some(JsObj::Map { .. })
| Some(JsObj::Set { .. })
| Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::RegExp(_))
)
}
fn construct_class(
class_val: &Value,
args: Vec<Value>,
new_target: Value,
) -> Result<Value, String> {
let cv = match with_host(|h| h.get(class_val).cloned()) {
Some(JsObj::Class(c)) => c,
_ => return Err(type_error("not a class")),
};
let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
Some(JsObj::Class(c)) => c.proto.clone(),
_ => cv.proto.clone(),
};
let inst = with_host(|h| {
let o = h.new_object(IndexMap::new());
h.set_proto(&o, leaf_proto.clone());
o
});
let saved = with_host(|h| h.swap_super_replacement(None));
let ran = run_class_ctor(&cv, &inst, args, &new_target);
let substituted = with_host(|h| {
let s = h.take_super_replacement();
h.swap_super_replacement(saved);
s
});
match ran? {
Some(obj) if returns_object(&obj) => Ok(obj),
_ => Ok(substituted.unwrap_or(inst)),
}
}
fn run_class_ctor(
cv: &ClassVal,
inst: &Value,
args: Vec<Value>,
new_target: &Value,
) -> Result<Option<Value>, String> {
if cv.parent.is_none() {
init_fields(cv, inst)?;
}
match &cv.ctor {
Some(ctor_fn) => {
let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
Some(JsObj::Func(f)) => f,
_ => return Err(type_error("class constructor is not a function")),
};
if cv.parent.is_some() {
with_host(|h| h.mark_next_call_derived_ctor());
}
let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
return Ok(Some(r));
}
None => {
if let Some(parent) = &cv.parent {
if let Some(replacement) = super_construct(parent, args, inst, new_target)? {
init_fields(cv, &replacement)?;
return Ok(Some(replacement));
}
init_fields(cv, inst)?;
}
}
}
Ok(None)
}
fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
for (name, thunk, name_anon) in &cv.fields {
init_one_field(inst, name, thunk, *name_anon)?;
}
Ok(())
}
pub fn init_one_field(
inst: &Value,
name: &str,
thunk: &Value,
name_anon: bool,
) -> Result<(), String> {
let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
with_host(|h| {
if name_anon {
let s = h.new_str(name.to_string());
h.set_fn_prop(&val, "name", s);
}
if let Some(JsObj::Object(props)) = h.get_mut(inst) {
let is_new = !props.contains_key(name);
props.insert(name.to_string(), val);
if is_new && array_index(name).is_some() {
canonicalize_own_keys(props);
}
}
});
Ok(())
}
pub fn super_construct(
parent: &Value,
args: Vec<Value>,
inst: &Value,
new_target: &Value,
) -> Result<Option<Value>, String> {
match with_host(|h| h.get(parent).cloned()) {
Some(JsObj::Class(pcv)) => Ok(run_class_ctor(&pcv, inst, args, new_target)?
.filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst)))),
Some(JsObj::Func(fv)) => {
let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
Ok(Some(r).filter(|r| returns_object(r) && !with_host(|h| h.strict_eq(r, inst))))
}
Some(JsObj::Builtin(name)) => {
let built = crate::builtins::construct_builtin(&name, args)?;
if !become_exotic(inst, &built) {
adopt_own_props(inst, &built);
}
Ok(None)
}
Some(JsObj::Proxy { .. }) => {
let built = construct_nt(parent, args, new_target.clone())?;
if !become_exotic(inst, &built) {
adopt_own_props(inst, &built);
}
Ok(None)
}
_ => Err(type_error("super is not a constructor")),
}
}
fn become_exotic(inst: &Value, built: &Value) -> bool {
let exotic = matches!(
with_host(|h| h.get(built).cloned()),
Some(JsObj::Array(_))
| Some(JsObj::Map { .. })
| Some(JsObj::Set { .. })
| Some(JsObj::RegExp(_))
| Some(JsObj::Promise { .. })
| Some(JsObj::Func(_))
| Some(JsObj::Str(_))
| Some(JsObj::BigInt(_))
| Some(JsObj::Symbol { .. })
);
if !exotic {
return false;
}
let (Value::Obj(dst), Value::Obj(src)) = (inst, built) else {
return false;
};
let (dst, src) = (*dst, *src);
with_host(|h| {
if let Some(obj) = h.get(built).cloned() {
if let Some(slot) = h.get_mut(inst) {
*slot = obj;
}
}
h.move_index_state(src, dst);
});
true
}
fn adopt_own_props(inst: &Value, built: &Value) {
let entries: Vec<(String, Value)> = with_host(|h| match h.get(built) {
Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => Vec::new(),
});
with_host(|h| {
let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
if let Some(JsObj::Object(props)) = h.get_mut(inst) {
for (k, v) in entries {
props.insert(k, v);
}
canonicalize_own_keys(props);
}
for k in keys {
let a = h.prop_attrs(built, &k);
h.set_prop_attrs(inst, &k, a);
}
});
}
pub fn build_class(name: &str, parent: Value, ctor: Value, source_def: Option<usize>) -> Value {
let proxy_parent_proto = (with_host(|h| h.kind_of(&parent)) == Some(ObjKind::Proxy))
.then(|| crate::builtins::get_property(&parent, "prototype").ok())
.flatten();
with_host(|h| {
let parent_opt = if matches!(parent, Value::Undef) {
None
} else {
Some(parent.clone())
};
let parent_proto = match &parent_opt {
Some(_) if proxy_parent_proto.is_some() => {
proxy_parent_proto.clone().expect("checked is_some")
}
Some(p) => match h.get(p).cloned() {
Some(JsObj::Class(pc)) => pc.proto.clone(),
Some(JsObj::Builtin(bn)) => {
h.ensure_error_protos();
h.ensure_native_protos();
error_proto_of(h, &bn)
.or_else(|| h.native_proto(&bn))
.or_else(|| h.fn_prop(p, "prototype"))
.unwrap_or_else(|| h.object_proto())
}
_ => h
.fn_prop(p, "prototype")
.unwrap_or_else(|| h.object_proto()),
},
None => h.object_proto(),
};
let proto = h.new_object(IndexMap::new());
h.set_proto(&proto, parent_proto);
let ctor_opt = if matches!(ctor, Value::Undef) {
None
} else {
Some(ctor.clone())
};
if let Some(cf) = &ctor_opt {
if let Some(JsObj::Func(f)) = h.get_mut(cf) {
f.home_class = Some(name.to_string());
}
}
let cval = ClassVal {
name: name.to_string(),
ctor: ctor_opt,
parent: parent_opt,
proto: proto.clone(),
statics: IndexMap::new(),
fields: Vec::new(),
source_def,
};
let class_val = h.alloc(JsObj::Class(cval));
h.class_registry.insert(name.to_string(), class_val.clone());
h.tag_proto_class(&proto, class_val.clone());
h.set_fn_prop(&class_val, "prototype", proto.clone());
if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
p.insert("constructor".to_string(), class_val.clone());
}
h.hide_prop(&proto, "constructor");
class_val
})
}
pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
with_host(|h| {
let cname = match h.get(class_val) {
Some(JsObj::Class(c)) => c.name.clone(),
_ => String::new(),
};
if name.starts_with('#') && kind != member::STATIC_FIELD {
h.note_private_method(name);
}
if let Some(JsObj::Func(f)) = h.get_mut(&func) {
f.home_class = Some(cname);
f.home_static = is_static;
}
let target = if is_static {
class_val.clone()
} else {
match h.get(class_val) {
Some(JsObj::Class(c)) => c.proto.clone(),
_ => return,
}
};
match kind {
member::GET => h.set_accessor(&target, name, Some(func), None),
member::SET => h.set_accessor(&target, name, None, Some(func)),
_ => {
if kind == member::STATIC_FIELD {
if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
c.statics.insert(name.to_string(), func.clone());
}
h.set_fn_prop(class_val, name, func);
return;
}
if is_static {
if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
c.statics.insert(name.to_string(), func.clone());
}
h.set_fn_prop(class_val, name, func);
} else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
p.insert(name.to_string(), func);
}
}
}
h.hide_prop(&target, name);
});
}
pub fn define_field(class_val: &Value, name: &str, thunk: Value, name_anon: bool) {
with_host(|h| {
if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
c.fields.push((name.to_string(), thunk, name_anon));
}
});
}
fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
match h.get(ctor) {
Some(JsObj::Class(c)) => Some(c.proto.clone()),
Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
Some(JsObj::Builtin(name)) => h
.error_protos
.get(name)
.or_else(|| h.native_protos.get(name))
.cloned(),
Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
_ => None,
}
}
fn walk_target_prototype(ctor: &Value) -> Option<Value> {
if let Some(p) = with_host(|h| ctor_prototype(h, ctor)) {
return Some(p);
}
let name = with_host(|h| match h.get(ctor) {
Some(JsObj::Builtin(n)) => Some(n.clone()),
_ => None,
})?;
if name == "Object" {
return Some(with_host(|h| h.object_proto()));
}
Some(with_host(|h| {
h.alloc(JsObj::Builtin(format!("{name}.prototype")))
}))
}
pub fn not_a_function_message(v: &Value) -> String {
with_host(|h| match v {
Value::Undef => "undefined is not a function".into(),
Value::Bool(b) => format!("boolean {b} is not a function"),
Value::Int(_) | Value::Float(_) => format!("number {} is not a function", h.str_of(v)),
Value::Str(s) => format!("string \"{s}\" is not a function"),
Value::Obj(_) => match h.get(v) {
Some(JsObj::Str(s)) => format!("string \"{s}\" is not a function"),
Some(JsObj::Symbol { .. }) => "symbol is not a function".into(),
Some(JsObj::BigInt(_)) => "bigint is not a function".into(),
_ => "object is not a function".into(),
},
_ => "object is not a function".into(),
})
}
pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
if matches!(ctor, Value::Obj(_)) {
let handler = match with_host(|h| h.class_static(ctor, "@@hasInstance")) {
Some(f) => Some(f),
None => protocol_lookup(ctor, "@@hasInstance")?,
};
match handler {
Some(f) if with_host(|h| is_callable(h, &f)) => {
let r = invoke(&f, vec![obj.clone()], Some(ctor.clone()))?;
return Ok(with_host(|h| h.truthy(&r)));
}
Some(f)
if !matches!(f, Value::Undef)
&& !with_host(|h| matches!(h.get(&f), Some(JsObj::Null))) =>
{
return Err(type_error(¬_a_function_message(&f)));
}
_ => {}
}
}
let ctor_callable = with_host(|h| {
matches!(
h.get(ctor),
Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::Builtin(_))
| Some(JsObj::BoundFunc { .. })
)
});
if !ctor_callable {
return Err(type_error(if with_host(|h| !is_primitive(h, ctor)) {
"Right-hand side of 'instanceof' is not callable"
} else {
"Right-hand side of 'instanceof' is not an object"
}));
}
if !matches!(obj, Value::Obj(_)) {
return Ok(false);
}
if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Proxy) {
with_host(|h| {
h.ensure_error_protos();
h.ensure_native_protos();
});
let Some(target) = walk_target_prototype(ctor) else {
return Ok(false);
};
let mut cur = crate::proxy::get_prototype_of(obj)?.unwrap_or(Value::Undef);
for _ in 0..100 {
if matches!(cur, Value::Undef) || with_host(|h| h.is_null(&cur)) {
return Ok(false);
}
if with_host(|h| h.strict_eq(&cur, &target)) {
return Ok(true);
}
cur = crate::builtins::prototype_of(&cur);
}
return Ok(false);
}
if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
if crate::builtins::chain_intrinsic_ctors_pub(obj).contains(&name.as_str()) {
return Ok(true);
}
let kind = with_host(|h| h.get(obj).cloned());
match name.as_str() {
"Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
"Function" => return Ok(with_host(|h| is_callable(h, obj))),
"Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
"WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
"Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
"WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
"Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
"RegExp" => return Ok(matches!(kind, Some(JsObj::RegExp(_)))),
"Object" => {
let is_obj = matches!(
kind,
Some(JsObj::Object(_))
| Some(JsObj::Array(_))
| Some(JsObj::Builtin(_))
| Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::Map { .. })
| Some(JsObj::Set { .. })
| Some(JsObj::Promise { .. })
| Some(JsObj::Generator { .. })
| Some(JsObj::RegExp(_))
);
if is_obj {
if with_host(|h| h.has_null_proto(obj)) {
return Ok(false);
}
return Ok(true);
}
return Ok(false);
}
"Uint8Array" if crate::stdlib::native_tag(obj).as_deref() == Some("Buffer") => {
return Ok(true);
}
k if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") => {
return Ok(crate::stdlib::typedarray::kind_of(obj) == k);
}
other => {
if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
return Ok(true);
}
}
}
}
with_host(|h| h.ensure_error_protos());
with_host(|h| h.ensure_native_protos());
let target = match with_host(|h| ctor_prototype(h, ctor)) {
Some(p) => p,
None => return Ok(false),
};
let mut cur = with_host(|h| h.proto_of(obj));
while let Some(p) = cur {
if with_host(|h| h.strict_eq(&p, &target)) {
return Ok(true);
}
cur = with_host(|h| h.proto_of(&p));
}
Ok(false)
}
impl JsHost {
fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
std::mem::swap(&mut self.frames, &mut c.frames);
std::mem::swap(&mut self.error, &mut c.error);
std::mem::swap(&mut self.exc, &mut c.exc);
std::mem::swap(&mut self.signal, &mut c.signal);
c
}
pub fn is_generator_val(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Generator { .. }))
}
pub fn is_async_gen_val(&self, v: &Value) -> bool {
match self.get(v) {
Some(JsObj::Generator { id }) => self
.generators
.get(*id as usize)
.map(|g| g.async_gen)
.unwrap_or(false),
_ => false,
}
}
pub fn gen_done(&self, id: u32) -> bool {
self.generators
.get(id as usize)
.map(|g| g.done)
.unwrap_or(true)
}
fn gen_started(&self, id: u32) -> bool {
self.generators
.get(id as usize)
.map(|g| g.started)
.unwrap_or(false)
}
}
fn make_generator(
chunk: Chunk,
env: Env,
this_val: Option<Value>,
home_class: Option<String>,
home_static: bool,
home_object: Option<Value>,
strict: bool,
) -> Value {
let home = home_class
.as_ref()
.and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
let frame = Frame {
base_env: env.clone(),
env,
this_obj: this_val,
new_target: None,
home_class: home,
home_static,
home_object,
strict,
line: 0,
owner: None,
is_module: false,
this_state: ThisState::Plain,
};
let id = with_host(|h| {
let id = h.generators.len() as u32;
h.generators.push(GenCell {
coro: None,
yielder: std::ptr::null(),
ctx: GenContext {
frames: vec![frame],
..GenContext::default()
},
done: false,
started: false,
inject: None,
async_gen: false,
queue: std::collections::VecDeque::new(),
running: false,
stack_floor: 0,
});
id
});
let body = move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
ensure_coroutine_floor();
with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
let r = run_chunk_on(chunk);
let ret = with_host(|h| match h.signal.take() {
Some(Signal::Return(v)) => v,
_ => Value::Undef,
});
r.map(|_| ret)
};
let (coro, floor) = match corosensei::stack::DefaultStack::new(CORO_STACK_SIZE) {
Ok(stack) => {
let floor = coro_stack_floor(&stack);
(corosensei::Coroutine::with_stack(stack, body), floor)
}
Err(_) => (corosensei::Coroutine::new(body), 0),
};
with_host(|h| {
h.generators[id as usize].coro = Some(coro);
h.generators[id as usize].stack_floor = floor;
});
with_host(|h| h.alloc(JsObj::Generator { id }))
}
pub fn gen_yield(v: Value) -> Result<Value, String> {
let id = match CUR_GEN.with(|c| c.get()) {
Some(id) => id,
None => return Err(type_error("yield outside a generator")),
};
let yp = with_host(|h| h.generators[id as usize].yielder);
let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
let sent = yielder.suspend(v);
if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
match inj {
GenInject::Return(rv) => {
with_host(|h| h.signal = Some(Signal::Return(rv)));
return Ok(Value::Undef);
}
GenInject::Throw(ev) => {
let msg = with_host(|h| crate::builtins::error_string(h, &ev));
with_host(|h| h.exc = Some(ev));
return Err(msg);
}
}
}
Ok(sent)
}
pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
let id = match with_host(|h| h.get(gen).cloned()) {
Some(JsObj::Generator { id }) => id,
_ => return Err(type_error("not a generator")),
};
let started = with_host(|h| h.gen_started(id));
if with_host(|h| h.generators[id as usize].done) || !started {
with_host(|h| h.generators[id as usize].done = true);
return Ok(GenStep::Done(v));
}
with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
gen_resume(gen, Value::Undef)
}
pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
let id = match with_host(|h| h.get(gen).cloned()) {
Some(JsObj::Generator { id }) => id,
_ => return Err(type_error("not a generator")),
};
let started = with_host(|h| h.gen_started(id));
if with_host(|h| h.generators[id as usize].done) || !started {
with_host(|h| h.generators[id as usize].done = true);
let msg = with_host(|h| crate::builtins::error_string(h, &e));
with_host(|h| h.exc = Some(e));
return Err(msg);
}
with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
gen_resume(gen, Value::Undef)
}
pub enum GenStep {
Yield(Value),
Done(Value),
}
pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
let id = match with_host(|h| h.get(gen).cloned()) {
Some(JsObj::Generator { id }) => id,
_ => return Err(type_error("not a generator")),
};
if with_host(|h| h.generators[id as usize].done) {
return Ok(GenStep::Done(Value::Undef));
}
let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
Some(c) => c,
None => return Err("TypeError: generator already executing".into()),
};
with_host(|h| h.generators[id as usize].started = true);
let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
let prev = CUR_GEN.with(|c| c.replace(Some(id)));
let coro_floor = with_host(|h| h.generators[id as usize].stack_floor);
let caller_floor = swap_stack_floor(coro_floor);
let out = coro.resume(send);
let measured = swap_stack_floor(caller_floor);
if coro_floor == 0 && measured != 0 {
with_host(|h| h.generators[id as usize].stack_floor = measured);
}
CUR_GEN.with(|c| c.set(prev));
let mut gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
let thrown = gen_ctx.exc.take();
with_host(|h| {
if let Some(v) = thrown {
h.exc = Some(v);
}
h.generators[id as usize].ctx = gen_ctx;
h.generators[id as usize].coro = Some(coro);
});
match out {
corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
corosensei::CoroutineResult::Return(r) => {
with_host(|h| {
let g = &mut h.generators[id as usize];
g.done = true;
g.coro = None;
});
match r {
Ok(v) => Ok(GenStep::Done(v)),
Err(e) => Err(e),
}
}
}
}
pub fn gen_close(gen: &Value) {
if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
with_host(|h| h.generators[id as usize].done = true);
}
}
pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
match v {
Value::Undef => MapKey::Undef,
Value::Bool(b) => MapKey::Bool(*b),
Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
Value::Str(s) => MapKey::Str((**s).clone()),
Value::Obj(i) => match h.get(v) {
Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
Some(JsObj::Null) => MapKey::Null,
Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
Some(JsObj::Builtin(n)) => MapKey::Intrinsic(builtin_identity(n).to_string()),
_ => MapKey::Ref(*i),
},
_ => MapKey::Undef,
}
}
fn norm_num_bits(f: f64) -> u64 {
if f.is_nan() {
return f64::NAN.to_bits();
}
if f == 0.0 {
return 0.0f64.to_bits(); }
f.to_bits()
}
pub fn iter_take(v: &Value, n: usize) -> Result<Vec<Value>, String> {
if let Some(items) = crate::proxy::iterate(v)? {
return Ok(items.into_iter().take(n).collect());
}
if with_host(|h| h.is_generator_val(v)) {
let mut out = Vec::new();
while out.len() < n {
match gen_resume(v, Value::Undef)? {
GenStep::Yield(x) => out.push(x),
_ => return Ok(out), }
}
let _ = gen_return(v, Value::Undef);
return Ok(out);
}
if let Some(iter_fn) = user_iterator_fn(v) {
let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
let mut out = Vec::new();
while out.len() < n {
let step = call_method(&iterator, "next", Vec::new())?;
let done = get_prop_chain(&step, "done")?;
if with_host(|h| h.truthy(&done)) {
return Ok(out);
}
out.push(get_prop_chain(&step, "value")?);
}
if let Ok(ret) = get_prop_chain(&iterator, "return") {
if with_host(|h| is_callable(h, &ret)) {
let _ = invoke(&ret, Vec::new(), Some(iterator.clone()));
}
}
return Ok(out);
}
if !crate::builtins::own_intrinsic_reachable_pub(v) {
let shown = with_host(|h| h.inspect(v));
return Err(type_error(&format!("{shown} is not iterable")));
}
with_host(|h| h.iter_vec(v)).map(|items| items.into_iter().take(n).collect())
}
pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
if let Some(items) = crate::proxy::iterate(v)? {
return Ok(items);
}
if with_host(|h| h.is_generator_val(v)) {
let mut out = Vec::new();
while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
out.push(x);
}
return Ok(out);
}
if with_host(|h| matches!(h.get(v), Some(JsObj::Iter { array: Some(_), .. }))) {
let mut out = Vec::new();
while let Some(Some(x)) = crate::builtins::iter_step(v) {
out.push(x);
}
return Ok(out);
}
if let Some(iter_fn) = user_iterator_fn(v) {
let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
return drain_iterator(&iterator);
}
if !crate::builtins::own_intrinsic_reachable_pub(v) {
let shown = with_host(|h| h.inspect(v));
return Err(type_error(&format!("{shown} is not iterable")));
}
if let Some(prim) = crate::builtins::wrapped_primitive(v) {
if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) {
return iter_all(&prim);
}
}
let mut items = with_host(|h| h.iter_vec(v))?;
if with_host(|h| matches!(h.get(v), Some(JsObj::Array(_)))) {
crate::builtins::resolve_index_accessors_pub(v, &mut items);
}
Ok(items)
}
pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
if let Some(f) = user_async_iterator_fn(src) {
return invoke(&f, Vec::new(), Some(src.clone()));
}
if let Some(JsObj::Generator { id }) = with_host(|h| h.get(src).cloned()) {
if with_host(|h| h.generators[id as usize].async_gen) {
return Ok(src.clone());
}
}
let items = iter_all(src)?;
Ok(with_host(|h| {
h.alloc(JsObj::Iter {
items,
idx: 0,
array: None,
})
}))
}
fn user_async_iterator_fn(v: &Value) -> Option<Value> {
if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
return protocol_lookup(v, "@@asyncIterator")
.ok()
.flatten()
.filter(|f| with_host(|h| is_callable(h, f)));
}
let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
if !is_plain {
return None;
}
let f = crate::builtins::get_property(v, "@@asyncIterator").ok()?;
with_host(|h| is_callable(h, &f)).then_some(f)
}
pub fn async_step(iterator: &Value) -> Result<Value, String> {
if let Some(JsObj::Generator { id }) = with_host(|h| h.get(iterator).cloned()) {
if with_host(|h| h.generators[id as usize].async_gen) {
return Ok(async_gen_step(iterator, Value::Undef));
}
}
if let Some(JsObj::Iter { items, idx, .. }) = with_host(|h| h.get(iterator).cloned()) {
if idx >= items.len() {
let step = with_host(|h| h.new_promise());
let sid = with_host(|h| h.promise_id(&step).unwrap());
with_host(|h| {
h.queue_micro_native(Box::new(move || {
resolve_promise_val(sid, iter_record(Value::Undef, true));
Ok(())
}))
});
return Ok(step);
}
let raw = items[idx].clone();
with_host(|h| {
if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
*idx += 1;
}
});
let step = with_host(|h| h.new_promise());
let sid = with_host(|h| h.promise_id(&step).unwrap());
let raw_p = promise_of(&raw);
let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
subscribe_native(
raw_id,
Box::new(move |state, val| {
if state == PromiseState::Rejected {
reject_promise_val(sid, val);
} else {
resolve_promise_val(sid, iter_record(val, false));
}
Ok(())
}),
);
return Ok(step);
}
let r = call_method(iterator, "next", Vec::new())?;
Ok(promise_of(&r))
}
pub fn user_iterator_fn(v: &Value) -> Option<Value> {
let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
if !is_plain {
return None;
}
let f = crate::builtins::get_property(v, "@@iterator").ok()?;
with_host(|h| is_callable(h, &f)).then_some(f)
}
pub fn iter_for_each(
src: &Value,
mut f: impl FnMut(Value, usize) -> Result<(), String>,
) -> Result<(), String> {
let Some(iter_fn) = user_iterator_fn(src) else {
for (i, v) in iter_all(src)?.into_iter().enumerate() {
f(v, i)?;
}
return Ok(());
};
let iterator = invoke(&iter_fn, Vec::new(), Some(src.clone()))?;
let mut i = 0usize;
loop {
let step = call_method(&iterator, "next", Vec::new())?;
let done = get_prop_chain(&step, "done")?;
if with_host(|h| h.truthy(&done)) {
return Ok(());
}
let value = get_prop_chain(&step, "value")?;
if let Err(e) = f(value, i) {
let _ = close_iterator(&iterator);
return Err(e);
}
i += 1;
}
}
pub fn close_iterator(iterator: &Value) -> Result<(), String> {
let has = crate::builtins::get_property(iterator, "return")?;
if with_host(|h| is_callable(h, &has)) {
call_method(iterator, "return", Vec::new())?;
}
Ok(())
}
pub(crate) fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
let mut out = Vec::new();
loop {
let step = call_method(iterator, "next", Vec::new())?;
let done = get_prop_chain(&step, "done")?;
if with_host(|h| h.truthy(&done)) {
break;
}
out.push(get_prop_chain(&step, "value")?);
}
Ok(out)
}
pub fn builtin_is_callable(n: &str) -> bool {
if n != "Function.prototype" && n.ends_with(".prototype") {
return false;
}
!matches!(
n,
"Math"
| "JSON"
| "console"
| "Reflect"
| "process"
| "Atomics"
| "performance"
| "fs"
| "path"
| "os"
| "util"
| "crypto"
| "webcrypto"
| "SubtleCrypto"
| "querystring"
| "events"
| "timers"
| "perf_hooks"
| "async_hooks"
| "diagnostics_channel"
| "v8"
| "dns"
| "punycode"
| "child_process"
| "tty"
| "url"
| "zlib"
| "string_decoder"
| "http"
| "net"
| "buffer"
| "function"
| "path/win32"
| "fs/promises"
| "stream/promises"
| "stream/consumers"
| "stream/web"
| "timers/promises"
| "dns/promises"
| "https"
| "http2"
| "tls"
| "dgram"
| "cluster"
| "worker_threads"
| "readline"
| "readline/promises"
| "repl"
| "vm"
| "domain"
| "trace_events"
| "wasi"
| "inspector"
| "object"
)
&& n != crate::builtins::REQUIRE_CACHE
}
pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
crate::builtins::get_property(recv, name)
}
pub fn is_primitive(h: &JsHost, v: &Value) -> bool {
match v {
Value::Obj(_) => matches!(
h.get(v),
None | Some(JsObj::Null)
| Some(JsObj::Str(_))
| Some(JsObj::Symbol { .. })
| Some(JsObj::BigInt(_))
),
_ => true,
}
}
pub fn to_primitive(v: &Value, hint: &str) -> Result<Value, String> {
if with_host(|h| is_primitive(h, v)) {
return Ok(v.clone());
}
if let Some(f) = protocol_lookup(v, "@@toPrimitive")? {
if with_host(|h| is_callable(h, &f)) {
let hv = with_host(|h| h.new_str(hint.to_string()));
let r = invoke(&f, vec![hv], Some(v.clone()))?;
if with_host(|h| is_primitive(h, &r)) {
return Ok(r);
}
return Err(type_error("Cannot convert object to primitive value"));
}
}
let hint = if hint == "default" && crate::stdlib::native_tag(v).as_deref() == Some("Date") {
"string"
} else {
hint
};
let order = if hint == "string" {
["toString", "valueOf"]
} else {
["valueOf", "toString"]
};
let mut called_any = false;
for m in order {
let f = crate::builtins::get_property(v, m).unwrap_or(Value::Undef);
if !with_host(|h| is_callable(h, &f)) {
continue;
}
called_any = true;
let r = if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
call_method(v, m, Vec::new())?
} else {
invoke(&f, Vec::new(), Some(v.clone()))?
};
if with_host(|h| is_primitive(h, &r)) {
return Ok(r);
}
}
if !called_any && !with_host(|h| h.has_null_proto(v)) && !crate::proxy::has_trap(v, "get") {
return crate::builtins::proto_method(v, "Object:toString", Vec::new());
}
Err(type_error("Cannot convert object to primitive value"))
}
pub fn to_string_value(v: &Value) -> Result<Value, String> {
let p = to_primitive(v, "string")?;
if with_host(|h| matches!(h.get(&p), Some(JsObj::Symbol { .. }))) {
return Err(type_error("Cannot convert a Symbol value to a string"));
}
Ok(with_host(|h| {
let s = h.str_of(&p);
h.new_str(s)
}))
}
pub fn string_ctor_value(v: &Value) -> Result<Value, String> {
if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
return Ok(with_host(|h| {
let s = h.str_of(v);
h.new_str(s)
}));
}
to_string_value(v)
}
pub fn to_number_value(v: &Value) -> Result<f64, String> {
if with_host(|h| matches!(h.get(v), Some(JsObj::Symbol { .. }))) {
return Err(type_error("Cannot convert a Symbol value to a number"));
}
if let Some(n) = with_host(|h| is_primitive(h, v).then(|| h.to_number(v))) {
return Ok(n);
}
let p = to_primitive(v, "number")?;
match with_host(|h| h.get(&p).cloned()) {
Some(JsObj::Symbol { .. }) => Err(type_error("Cannot convert a Symbol value to a number")),
Some(JsObj::BigInt(_)) => Err(type_error("Cannot convert a BigInt value to a number")),
_ => Ok(with_host(|h| h.to_number(&p))),
}
}
pub fn to_property_key(v: &Value) -> Result<String, String> {
if let Some(k) = with_host(|h| is_primitive(h, v).then(|| h.property_key(v))) {
return Ok(k);
}
let p = to_primitive(v, "string")?;
Ok(with_host(|h| h.str_of(&p)))
}
pub fn is_callable(h: &JsHost, v: &Value) -> bool {
match h.get(v) {
Some(JsObj::Builtin(n)) => builtin_is_callable(n),
Some(JsObj::Func(_))
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::Class(_)) => true,
Some(JsObj::Proxy { target, .. }) => is_callable(h, target),
_ => false,
}
}
pub fn protocol_lookup(v: &Value, key: &str) -> Result<Option<Value>, String> {
if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
let got = crate::builtins::get_property(v, key)?;
return Ok((!matches!(got, Value::Undef)).then_some(got));
}
Ok(with_host(|h| lookup_chain(h, v, key)))
}
pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
if let Some(JsObj::Object(p)) = h.get(recv) {
if let Some(v) = p.get(key) {
return Some(v.clone());
}
}
let mut cur = h.proto_of(recv);
while let Some(p) = cur {
match h.get(&p) {
Some(JsObj::Object(props)) => {
if let Some(v) = props.get(key) {
return Some(v.clone());
}
}
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
if let Some(v) = h.fn_prop(&p, key) {
return Some(v);
}
}
_ => {}
}
cur = h.proto_of(&p);
}
None
}
pub fn lookup_accessor(
h: &JsHost,
recv: &Value,
key: &str,
) -> Option<(Option<Value>, Option<Value>)> {
if let Some(a) = h.own_accessor(recv, key) {
return Some(a);
}
let mut cur = h.proto_of(recv);
while let Some(p) = cur {
if let Some(a) = h.own_accessor(&p, key) {
return Some(a);
}
cur = h.proto_of(&p);
}
let mut cls = recv.clone();
while let Some(JsObj::Class(c)) = h.get(&cls) {
let Some(parent) = c.parent.clone() else {
break;
};
if let Some(a) = h.own_accessor(&parent, key) {
return Some(a);
}
cls = parent;
}
None
}
pub fn set_error_proto(name: &str, proto: Value) {
with_host(|h| {
h.error_protos.insert(name.to_string(), proto);
});
}
pub fn error_proto(name: &str) -> Option<Value> {
with_host(|h| h.error_protos.get(name).cloned())
}
pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
h.error_protos.get(name).cloned()
}
impl JsHost {
pub fn error_to_string(&self, v: &Value) -> Option<String> {
let base = self.error_protos.get("Error")?;
let mut cur = self.proto_of(v);
let mut is_error = false;
while let Some(p) = cur {
if self.strict_eq(&p, base) {
is_error = true;
break;
}
cur = self.proto_of(&p);
}
if !is_error {
return None;
}
let name = lookup_chain(self, v, "name")
.map(|n| self.str_of(&n))
.unwrap_or_else(|| "Error".into());
let message = lookup_chain(self, v, "message")
.map(|m| self.str_of(&m))
.unwrap_or_default();
if let Some(JsObj::Object(p)) = self.get(v) {
if p.contains_key("@@nodeError") {
if let Some(code) = p.get("code").map(|c| self.str_of(c)) {
return Some(format!("{name} [{code}]: {message}"));
}
}
}
Some(match (name.is_empty(), message.is_empty()) {
(true, _) => message,
(false, true) => name,
(false, false) => format!("{name}: {message}"),
})
}
}
pub const ERROR_NAMES: &[&str] = &[
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
"AssertionError",
"DOMException",
];
impl JsHost {
pub fn ensure_native_protos(&mut self) {
self.ensure_wrapper_protos();
self.ensure_function_kind_protos();
if self.native_protos.contains_key("Buffer") {
return;
}
let obj_proto = self.object_proto();
self.native_protos
.insert("Object".to_string(), obj_proto.clone());
for m in crate::builtins::OBJECT_PROTO_METHODS {
let thunk = self.alloc(JsObj::Builtin(format!("@proto:Object:{m}")));
if let Some(JsObj::Object(p)) = self.get_mut(&obj_proto) {
p.insert((*m).to_string(), thunk);
}
self.hide_prop(&obj_proto, m);
}
let mut chain: Vec<(&str, Value)> = vec![("TypedArray", obj_proto)];
for kind in crate::stdlib::typedarray::ELEMENT_KINDS {
chain.push((kind, Value::Undef)); }
chain.push(("Buffer", Value::Undef));
let mut prev: Option<Value> = None;
for (ctor, parent) in chain.drain(..) {
let proto = self.new_object(IndexMap::new());
let parent = match ctor {
"TypedArray" => parent,
"Buffer" => self
.native_protos
.get("Uint8Array")
.cloned()
.unwrap_or_else(|| prev.clone().expect("intermediate built first")),
_ => self
.native_protos
.get("TypedArray")
.cloned()
.unwrap_or_else(|| prev.clone().expect("intermediate built first")),
};
self.set_proto(&proto, parent);
if ctor != "TypedArray" {
let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("constructor".into(), ctor_val);
}
self.hide_prop(&proto, "constructor");
}
let methods: &[&str] = match ctor {
"Buffer" => crate::stdlib::buffer::INSTANCE_METHODS,
"TypedArray" => crate::stdlib::typedarray::PROTOTYPE_METHODS,
"Uint8Array" => crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS,
_ => &[],
};
if crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor) {
let bpe = Value::Float(crate::stdlib::typedarray::bytes_per_element(ctor) as f64);
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("BYTES_PER_ELEMENT".into(), bpe);
}
self.hide_prop(&proto, "BYTES_PER_ELEMENT");
}
for m in methods {
let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert((*m).to_string(), thunk);
}
self.hide_prop(&proto, m);
}
self.native_protos.insert(ctor.to_string(), proto.clone());
prev = Some(proto);
}
}
pub fn ensure_function_kind_protos(&mut self) {
if self.native_protos.contains_key("GeneratorFunction") {
return;
}
let base = self
.native_protos
.get("Function")
.cloned()
.unwrap_or_else(|| self.object_proto());
for ctor in [
"GeneratorFunction",
"AsyncFunction",
"AsyncGeneratorFunction",
] {
let proto = self.new_object(IndexMap::new());
self.set_proto(&proto, base.clone());
let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
let tag = self.new_str(ctor);
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("constructor".into(), ctor_val);
p.insert("@@toStringTag".into(), tag);
}
self.hide_prop(&proto, "constructor");
self.hide_prop(&proto, "@@toStringTag");
self.native_protos.insert(ctor.to_string(), proto);
}
}
pub fn ensure_wrapper_protos(&mut self) {
if self.native_protos.contains_key("String") {
return;
}
let obj_proto = self.object_proto();
for ctor in ["String", "Number", "Boolean", "Symbol", "BigInt"] {
let proto = self.new_object(IndexMap::new());
self.set_proto(&proto, obj_proto.clone());
let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("constructor".into(), ctor_val);
}
self.hide_prop(&proto, "constructor");
let methods: Vec<&str> = ["toString", "valueOf", "toLocaleString"]
.into_iter()
.chain(match ctor {
"String" => crate::builtins::STRING_PROTO_METHODS.iter().copied(),
"Number" => crate::builtins::NUMBER_PROTO_METHODS.iter().copied(),
_ => [].iter().copied(),
})
.chain(crate::builtins::proto_symbol_methods(ctor))
.collect();
let mut seen: Vec<&str> = Vec::new();
for m in methods {
if seen.contains(&m) {
continue;
}
seen.push(m);
let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert(m.to_string(), thunk);
}
self.hide_prop(&proto, m);
}
if matches!(ctor, "Symbol" | "BigInt") {
let tag = self.new_str(ctor.to_string());
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("@@toStringTag".into(), tag);
}
self.set_prop_attrs(
&proto,
"@@toStringTag",
PropAttrs {
writable: false,
enumerable: false,
configurable: true,
},
);
}
if ctor == "Symbol" {
let thunk = self.alloc(JsObj::Builtin("@proto:Symbol:@@toPrimitive".to_string()));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("@@toPrimitive".into(), thunk);
}
self.hide_prop(&proto, "@@toPrimitive");
}
self.native_protos.insert(ctor.to_string(), proto);
}
}
pub fn template_object(&self, key: (u64, u64)) -> Option<Value> {
self.template_objects.get(&key).cloned()
}
pub fn set_template_object(&mut self, key: (u64, u64), v: Value) {
self.template_objects.insert(key, v);
}
pub fn native_proto(&self, ctor: &str) -> Option<Value> {
self.native_protos.get(ctor).cloned()
}
pub fn intrinsic_proto_ctor(&self, v: &Value) -> Option<&str> {
if !matches!(v, Value::Obj(_)) {
return None;
}
self.native_protos
.iter()
.chain(self.error_protos.iter())
.find(|(_, p)| *p == v)
.map(|(name, _)| name.as_str())
}
pub fn ensure_ctor_proto(&mut self, ctor: &str) -> Option<Value> {
if let Some(p) = self.native_protos.get(ctor) {
return Some(p.clone());
}
if ctor == "Buffer"
|| ctor == "TypedArray"
|| crate::stdlib::typedarray::ELEMENT_KINDS.contains(&ctor)
{
self.ensure_native_protos();
return self.native_protos.get(ctor).cloned();
}
let (own, emitter) = crate::stdlib::instance_method_lists(ctor);
let (accessor_list, _) = crate::stdlib::instance_accessors(ctor);
if own.is_empty() && emitter.is_empty() && accessor_list.is_empty() {
return None;
}
let parent_proto = match crate::stdlib::native_parent(ctor) {
Some(p) => self
.ensure_ctor_proto(p)
.unwrap_or_else(|| self.object_proto()),
None => self.object_proto(),
};
let proto = self.new_object(IndexMap::new());
self.set_proto(&proto, parent_proto);
let ctor_val = self.alloc(JsObj::Builtin(ctor.to_string()));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("constructor".into(), ctor_val);
}
self.hide_prop(&proto, "constructor");
let visible = crate::stdlib::instance_members_enumerable(ctor);
let symbols = crate::builtins::proto_symbol_methods(ctor);
for m in own.iter().chain(emitter.iter()).chain(symbols.iter()) {
let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert((*m).to_string(), thunk);
}
if !visible || m.starts_with("@@") {
self.hide_prop(&proto, m);
}
}
let (accessors, tag) = crate::stdlib::instance_accessors(ctor);
for (key, settable) in accessors {
let get = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@get@{key}")));
let set =
settable.then(|| self.alloc(JsObj::Builtin(format!("@proto:{ctor}:@set@{key}"))));
self.set_accessor(&proto, key, Some(get), set);
if !visible {
self.hide_prop(&proto, key);
}
}
for m in crate::stdlib::instance_late_methods(ctor) {
let thunk = self.alloc(JsObj::Builtin(format!("@proto:{ctor}:{m}")));
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert((*m).to_string(), thunk);
}
if !visible {
self.hide_prop(&proto, m);
}
}
if !tag.is_empty() {
let tag = self.new_str(tag.to_string());
if let Some(JsObj::Object(p)) = self.get_mut(&proto) {
p.insert("@@toStringTag".into(), tag);
}
self.hide_prop(&proto, "@@toStringTag");
}
self.native_protos.insert(ctor.to_string(), proto.clone());
Some(proto)
}
pub fn error_proto(&self, name: &str) -> Option<Value> {
self.error_protos.get(name).cloned()
}
pub fn ensure_error_protos(&mut self) {
if !self.error_protos.is_empty() {
return;
}
let obj_proto = self.object_proto();
let err_proto = self.new_object(IndexMap::new());
self.set_proto(&err_proto, obj_proto);
let nm = self.new_str("Error");
let empty = self.new_str("");
let ctor = self.alloc(JsObj::Builtin("Error".into()));
let to_string = self.alloc(JsObj::Builtin("@proto:Error:toString".into()));
if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
p.insert("name".into(), nm);
p.insert("message".into(), empty);
p.insert("constructor".into(), ctor);
p.insert("toString".into(), to_string);
}
for k in ["name", "message", "constructor", "toString"] {
self.hide_prop(&err_proto, k);
}
self.error_protos.insert("Error".into(), err_proto.clone());
for name in &ERROR_NAMES[1..] {
let p = self.new_object(IndexMap::new());
self.set_proto(&p, err_proto.clone());
let nm = self.new_str(*name);
let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
if let Some(JsObj::Object(o)) = self.get_mut(&p) {
o.insert("name".into(), nm);
o.insert("constructor".into(), ctor);
}
self.hide_prop(&p, "name");
self.hide_prop(&p, "constructor");
self.error_protos.insert((*name).to_string(), p);
}
}
}
impl JsHost {
pub fn func_arity(&self, v: &Value) -> usize {
if let Some(JsObj::BoundFunc { target, args, .. }) = self.get(v) {
return self.func_arity(&target.clone()).saturating_sub(args.len());
}
if let Some(JsObj::Builtin(n)) = self.get(v) {
return crate::builtins::builtin_meta(n)
.map(|(_, len)| len as usize)
.unwrap_or(0);
}
let def_id = match self.get(v) {
Some(JsObj::Func(f)) => Some(f.def_id),
Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
Some(JsObj::Func(f)) => Some(f.def_id),
_ => None,
},
_ => None,
};
match def_id.and_then(|id| self.funcs.get(id)) {
Some(def) => def
.params
.iter()
.take_while(|p| !p.rest && !p.has_default)
.count(),
None => 0,
}
}
pub fn is_map(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Map { .. }))
}
pub fn is_set(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Set { .. }))
}
}
impl JsHost {
pub fn new_promise(&mut self) -> Value {
let id = self.promises.len() as u32;
self.promises.push(PromiseCell {
state: PromiseState::Pending,
value: Value::Undef,
reactions: Vec::new(),
handled: false,
});
self.alloc(JsObj::Promise { id })
}
pub fn promise_id(&self, v: &Value) -> Option<u32> {
match self.get(v) {
Some(JsObj::Promise { id }) => Some(*id),
_ => None,
}
}
pub fn promise_state(&self, id: u32) -> PromiseState {
self.promises[id as usize].state
}
pub fn promise_value(&self, id: u32) -> Value {
self.promises[id as usize].value.clone()
}
pub fn promise_mark_handled(&mut self, id: u32) {
self.promises[id as usize].handled = true;
}
pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
std::mem::take(&mut self.promises[id as usize].reactions)
}
pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
self.promises[id as usize].reactions.push(r);
}
pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
let c = &mut self.promises[id as usize];
if c.state != PromiseState::Pending {
return; }
c.state = state;
c.value = value;
}
pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
self.microtasks.push_back(Task::Js { cb, args });
}
pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
self.nextticks.push_back(Task::Js { cb, args });
}
pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
self.microtasks.push_back(Task::Native(f));
}
pub fn add_timer(
&mut self,
delay: f64,
callback: Value,
args: Vec<Value>,
interval: Option<f64>,
) -> u64 {
let id = self.next_timer;
self.next_timer += 1;
let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
self.macrotasks.push(Timer {
id,
delay,
seq: id,
callback,
args,
cancelled: false,
interval,
refed: true,
deadline,
});
id
}
fn rearm_timer(&mut self, t: &Timer, period: f64) {
let seq = self.next_timer;
self.next_timer += 1;
let deadline = Instant::now() + Duration::from_millis(period.max(0.0) as u64);
self.macrotasks.push(Timer {
id: t.id,
delay: t.delay,
seq,
callback: t.callback.clone(),
args: t.args.clone(),
cancelled: false,
interval: Some(period),
refed: t.refed,
deadline,
});
}
pub fn set_timer_refed(&mut self, id: u64, refed: bool) {
for t in &mut self.macrotasks {
if t.id == id && !t.cancelled {
t.refed = refed;
}
}
}
pub fn timer_has_ref(&self, id: u64) -> bool {
self.macrotasks
.iter()
.any(|t| t.id == id && !t.cancelled && t.refed)
}
pub fn refresh_timer(&mut self, id: u64) {
let now = Instant::now();
for t in &mut self.macrotasks {
if t.id == id && !t.cancelled {
t.deadline = now + Duration::from_millis(t.delay.max(0.0) as u64);
}
}
}
pub fn io_sender(&self) -> Sender<IoTask> {
self.io_tx.clone()
}
pub fn incr_handle(&mut self) {
self.open_handles += 1;
}
pub fn decr_handle(&mut self) {
self.open_handles = self.open_handles.saturating_sub(1);
}
pub fn open_handles(&self) -> usize {
self.open_handles
}
fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
let idx = self
.macrotasks
.iter()
.enumerate()
.filter(|(_, t)| !t.cancelled && t.deadline <= now)
.min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
.map(|(i, _)| i);
idx.map(|i| self.macrotasks.remove(i))
}
fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
self.macrotasks
.iter()
.filter(|t| !t.cancelled)
.map(|t| t.deadline)
.min()
.map(|d| d.saturating_duration_since(now))
}
pub fn cancel_timer(&mut self, id: u64) {
for t in &mut self.macrotasks {
if t.id == id {
t.cancelled = true;
}
}
}
fn pop_next_timer(&mut self) -> Option<Timer> {
let idx = self
.macrotasks
.iter()
.enumerate()
.filter(|(_, t)| !t.cancelled)
.min_by(|(_, a), (_, b)| {
a.delay
.partial_cmp(&b.delay)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.seq.cmp(&b.seq))
})
.map(|(i, _)| i);
idx.map(|i| self.macrotasks.remove(i))
}
fn next_microtask(&mut self) -> Option<Task> {
if !self.draining_micro {
if let Some(t) = self.nextticks.pop_front() {
return Some(t);
}
}
if let Some(t) = self.microtasks.pop_front() {
self.draining_micro = !self.microtasks.is_empty();
return Some(t);
}
self.draining_micro = false;
self.nextticks.pop_front()
}
fn has_microtasks(&self) -> bool {
!self.nextticks.is_empty() || !self.microtasks.is_empty()
}
fn has_refed_macrotasks(&self) -> bool {
self.macrotasks.iter().any(|t| !t.cancelled && t.refed)
}
fn has_pending_interval(&self) -> bool {
self.macrotasks
.iter()
.any(|t| !t.cancelled && t.interval.is_some())
}
}
pub fn run_event_loop() -> Result<(), String> {
let rx = with_host(|h| h.io_rx.take());
let result = drive_event_loop(rx.as_ref());
with_host(|h| h.io_rx = rx);
result
}
fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
loop {
while let Some(task) = with_host(|h| h.next_microtask()) {
task.run()?;
}
check_unhandled_rejections()?;
let alive =
with_host(|h| h.has_microtasks() || h.open_handles() > 0 || h.has_refed_macrotasks());
if !alive {
break;
}
let virtual_clock = with_host(|h| h.open_handles() == 0 && !h.has_pending_interval());
if virtual_clock {
match with_host(|h| h.pop_next_timer()) {
Some(t) => fire_timer(t)?,
None => break,
}
continue;
}
let now = Instant::now();
if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
fire_timer(t)?;
continue; }
let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
let timeout = with_host(|h| h.next_timer_timeout(now));
let recv = match timeout {
Some(d) => rx.recv_timeout(d),
None => rx
.recv()
.map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
};
match recv {
Ok(task) => task()?,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, }
}
Ok(())
}
fn fire_timer(t: Timer) -> Result<(), String> {
if let Some(period) = t.interval {
with_host(|h| h.rearm_timer(&t, period));
}
invoke(&t.callback, t.args, None)?;
Ok(())
}
fn run_async(gen: Value) -> Value {
let result = with_host(|h| h.new_promise());
let rid = with_host(|h| h.promise_id(&result).unwrap());
drive_async(gen, rid, Value::Undef);
result
}
fn drive_async(gen: Value, rid: u32, send: Value) {
match gen_resume(&gen, send) {
Ok(GenStep::Yield(awaited)) => {
let ap = promise_of(&awaited);
let aid = with_host(|h| h.promise_id(&ap).unwrap());
let gen2 = gen.clone();
subscribe_native(
aid,
Box::new(move |state, val| {
let tag = if state == PromiseState::Rejected {
1.0
} else {
0.0
};
let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
drive_async(gen2, rid, packet);
Ok(())
}),
);
}
Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
Err(e) => {
let ev = take_exc_or_error(&e);
reject_promise_val(rid, ev);
}
}
}
pub fn await_value(awaited: Value) -> Result<Value, String> {
let awaited = match CUR_GEN.with(|c| c.get()) {
Some(id) if with_host(|h| h.generators[id as usize].async_gen) => with_host(|h| {
let mut m = IndexMap::new();
m.insert(AWAIT_MARKER.to_string(), awaited);
h.new_object(m)
}),
_ => awaited,
};
let packet = gen_yield(awaited)?;
let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
let tag = items
.first()
.map(|v| with_host(|h| h.to_number(v)))
.unwrap_or(0.0);
let val = items.get(1).cloned().unwrap_or(Value::Undef);
if tag == 1.0 {
with_host(|h| h.exc = Some(val.clone()));
Err(with_host(|h| crate::builtins::error_string(h, &val)))
} else {
Ok(val)
}
}
const AWAIT_MARKER: &str = "@@await";
fn await_marker(v: &Value) -> Option<Value> {
with_host(|h| match h.get(v) {
Some(JsObj::Object(props)) if props.len() == 1 => props.get(AWAIT_MARKER).cloned(),
_ => None,
})
}
pub fn async_gen_enqueue(gen: &Value, req: GenReq) -> Value {
let step = with_host(|h| h.new_promise());
let sid = with_host(|h| h.promise_id(&step).unwrap());
let id = match with_host(|h| match h.get(gen) {
Some(JsObj::Generator { id }) => Some(*id),
_ => None,
}) {
Some(id) => id,
None => return step,
};
with_host(|h| h.generators[id as usize].queue.push_back((req, sid)));
pump_async_gen(gen.clone(), id);
step
}
pub fn async_gen_step(gen: &Value, send: Value) -> Value {
async_gen_enqueue(gen, GenReq::Next(send))
}
fn pump_async_gen(gen: Value, id: u32) {
if with_host(|h| h.generators[id as usize].running) {
return;
}
let Some((req, sid)) = with_host(|h| h.generators[id as usize].queue.pop_front()) else {
return;
};
with_host(|h| h.generators[id as usize].running = true);
start_async_gen_req(gen, sid, req);
}
fn start_async_gen_req(gen: Value, sid: u32, req: GenReq) {
if matches!(req, GenReq::Return(_)) {
with_host(|h| {
h.queue_micro_native(Box::new(move || {
resume_async_gen_req(gen, sid, req);
Ok(())
}))
});
return;
}
resume_async_gen_req(gen, sid, req);
}
fn resume_async_gen_req(gen: Value, sid: u32, req: GenReq) {
let step = match req {
GenReq::Next(v) => gen_resume(&gen, v),
GenReq::Return(v) => gen_return(&gen, v),
GenReq::Throw(e) => gen_throw(&gen, e),
};
settle_async_gen_step(gen, sid, step);
}
fn finish_async_gen_step(gen: Value, id: u32) {
with_host(|h| h.generators[id as usize].running = false);
pump_async_gen(gen, id);
}
pub fn is_async_generator(v: &Value) -> bool {
let id = match with_host(|h| match h.get(v) {
Some(JsObj::Generator { id }) => Some(*id),
_ => None,
}) {
Some(id) => id,
None => return false,
};
with_host(|h| h.generators[id as usize].async_gen)
}
fn iter_record(value: Value, done: bool) -> Value {
with_host(|h| {
let mut m = IndexMap::new();
m.insert("value".to_string(), value);
m.insert("done".to_string(), Value::Bool(done));
h.new_object(m)
})
}
fn drive_async_gen(gen: Value, sid: u32, packet: Value) {
let step = gen_resume(&gen, packet);
settle_async_gen_step(gen, sid, step);
}
fn settle_async_gen_step(gen: Value, sid: u32, step: Result<GenStep, String>) {
let id = match with_host(|h| match h.get(&gen) {
Some(JsObj::Generator { id }) => Some(*id),
_ => None,
}) {
Some(id) => id,
None => return,
};
match step {
Ok(GenStep::Yield(v)) => match await_marker(&v) {
Some(awaited) => {
let ap = promise_of(&awaited);
let aid = with_host(|h| h.promise_id(&ap).unwrap());
subscribe_native(
aid,
Box::new(move |state, val| {
let tag = if state == PromiseState::Rejected {
1.0
} else {
0.0
};
let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
drive_async_gen(gen.clone(), sid, packet);
Ok(())
}),
);
}
None => {
let yp = promise_of(&v);
let yid = with_host(|h| h.promise_id(&yp).unwrap());
subscribe_native(
yid,
Box::new(move |state, val| {
if state == PromiseState::Rejected {
reject_promise_val(sid, val);
} else {
resolve_promise_val(sid, iter_record(val, false));
}
finish_async_gen_step(gen.clone(), id);
Ok(())
}),
);
}
},
Ok(GenStep::Done(v)) => {
resolve_promise_val(sid, iter_record(v, true));
finish_async_gen_step(gen, id);
}
Err(e) => {
let ev = take_exc_or_error(&e);
reject_promise_val(sid, ev);
finish_async_gen_step(gen, id);
}
}
}
pub fn promise_of(v: &Value) -> Value {
if with_host(|h| h.promise_id(v)).is_some() {
return v.clone();
}
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
resolve_promise_val(id, v.clone());
p
}
pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
with_host(|h| h.promise_mark_handled(id));
let state = with_host(|h| h.promise_state(id));
if state == PromiseState::Pending {
with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
} else {
let val = with_host(|h| h.promise_value(id));
with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
}
}
pub fn resolve_promise_val(id: u32, value: Value) {
if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
return;
}
if let Some(vid) = with_host(|h| h.promise_id(&value)) {
if vid == id {
let e = with_host(|h| {
crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
});
reject_promise_val(id, e);
return;
}
with_host(|h| {
h.queue_micro_native(Box::new(move || {
subscribe_native(
vid,
Box::new(move |state, val| {
with_host(|h| h.settle_promise(id, state, val.clone()));
schedule_reactions(id);
Ok(())
}),
);
Ok(())
}))
});
return;
}
if let Some(then) = thenable_then(&value) {
with_host(|h| {
h.queue_micro_native(Box::new(move || resolve_thenable_job(id, value, then)))
});
return;
}
with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
schedule_reactions(id);
}
fn thenable_then(value: &Value) -> Option<Value> {
if with_host(|h| h.kind_of(value)) == Some(ObjKind::Proxy) {
return protocol_lookup(value, "then")
.ok()
.flatten()
.filter(|f| with_host(|h| is_callable(h, f)));
}
if !with_host(|h| matches!(h.get(value), Some(JsObj::Object(_)))) {
return None;
}
let then = with_host(|h| lookup_chain(h, value, "then"))?;
with_host(|h| is_callable(h, &then)).then_some(then)
}
fn resolve_thenable_job(id: u32, thenable: Value, then: Value) -> Result<(), String> {
let res = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
let rej = with_host(|h| h.alloc(JsObj::Builtin(format!("@@preject:{id}"))));
if let Err(e) = invoke(&then, vec![res, rej], Some(thenable)) {
let ev = take_exc_or_error(&e);
reject_promise_val(id, ev);
}
Ok(())
}
pub fn reject_promise_val(id: u32, value: Value) {
if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
return;
}
with_host(|h| {
h.settle_promise(id, PromiseState::Rejected, value);
h.pending_rejections.push(id);
});
schedule_reactions(id);
}
fn check_unhandled_rejections() -> Result<(), String> {
loop {
let ids: Vec<u32> = with_host(|h| std::mem::take(&mut h.pending_rejections));
if ids.is_empty() {
return Ok(());
}
for id in ids {
let unhandled = with_host(|h| {
h.promise_state(id) == PromiseState::Rejected && !h.promises[id as usize].handled
});
if !unhandled {
continue;
}
with_host(|h| h.promise_mark_handled(id));
let val = with_host(|h| h.promise_value(id));
let listeners = with_host(|h| h.take_process_listeners("unhandledRejection"));
if listeners.is_empty() {
let msg = with_host(|h| crate::builtins::error_string(h, &val));
with_host(|h| h.exc = Some(val));
return Err(msg);
}
let promise = with_host(|h| h.alloc(JsObj::Promise { id }));
for f in listeners {
invoke(&f, vec![val.clone(), promise.clone()], None)?;
}
}
}
}
fn schedule_reactions(id: u32) {
let reactions = with_host(|h| h.take_reactions(id));
let state = with_host(|h| h.promise_state(id));
let value = with_host(|h| h.promise_value(id));
for r in reactions {
let value = value.clone();
match r {
PromiseReaction::Native(f) => {
with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
}
PromiseReaction::Js {
on_ful,
on_rej,
result,
} => {
with_host(|h| {
h.queue_micro_native(Box::new(move || {
run_js_reaction(state, value, on_ful, on_rej, result)
}))
});
}
}
}
}
fn run_js_reaction(
state: PromiseState,
value: Value,
on_ful: Value,
on_rej: Value,
result: Value,
) -> Result<(), String> {
let rid = match with_host(|h| h.promise_id(&result)) {
Some(i) => i,
None => return Ok(()),
};
let handler = if state == PromiseState::Rejected {
on_rej
} else {
on_ful
};
if with_host(|h| is_callable(h, &handler)) {
match invoke(&handler, vec![value], None) {
Ok(r) => resolve_promise_val(rid, r),
Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
}
} else if state == PromiseState::Rejected {
reject_promise_val(rid, value);
} else {
resolve_promise_val(rid, value);
}
Ok(())
}
pub fn take_exc_or_error(e: &str) -> Value {
with_host(|h| {
h.error.take();
h.exc
.take()
.unwrap_or_else(|| crate::builtins::synth_error(h, e))
})
}
pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
let id = match with_host(|h| h.promise_id(p)) {
Some(i) => i,
None => return Value::Undef,
};
with_host(|h| h.promise_mark_handled(id));
let result = match crate::builtins::promise_species_from(p) {
Ok(Some(sp)) => sp,
_ => with_host(|h| h.new_promise()),
};
let reaction = PromiseReaction::Js {
on_ful,
on_rej,
result: result.clone(),
};
let state = with_host(|h| h.promise_state(id));
if state == PromiseState::Pending {
with_host(|h| h.add_reaction(id, reaction));
} else {
let value = with_host(|h| h.promise_value(id));
if let PromiseReaction::Js {
on_ful,
on_rej,
result,
} = reaction
{
with_host(|h| {
h.queue_micro_native(Box::new(move || {
run_js_reaction(state, value, on_ful, on_rej, result)
}))
});
}
}
result
}
pub fn this_before_super_error() -> String {
"ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor".to_string()
}
fn builtin_identity(name: &str) -> &str {
match name {
"Number.parseInt" => "parseInt",
"Number.parseFloat" => "parseFloat",
_ => name,
}
}