use crate::host::{self, ops, with_host, FuncVal, JsObj, ObjKind};
use fusevm::{NumOp, Value, VM};
use indexmap::IndexMap;
pub fn install(vm: &mut VM) {
vm.register_builtin(ops::GETLOCAL, b_getlocal);
vm.register_builtin(ops::SETLOCAL, b_setlocal);
vm.register_builtin(ops::SETLOCAL_STRICT, b_setlocal_strict);
vm.register_builtin(ops::DECLARE, b_declare);
vm.register_builtin(ops::DECLARE_CONST, b_declare_const);
vm.register_builtin(ops::MARK_HOLE, b_mark_hole);
vm.register_builtin(ops::DELNAME, b_delname);
vm.register_builtin(ops::GETATTR, b_getattr);
vm.register_builtin(ops::SETATTR, b_setattr);
vm.register_builtin(ops::GETITEM, b_getitem);
vm.register_builtin(ops::SETITEM, b_setitem);
vm.register_builtin(ops::DELITEM, b_delitem);
vm.register_builtin(ops::MKSTR, b_mkstr);
vm.register_builtin(ops::MKARR, b_mkarr);
vm.register_builtin(ops::MKOBJ, b_mkobj);
vm.register_builtin(ops::CALL, b_call);
vm.register_builtin(ops::CALL_METHOD, b_call_method);
vm.register_builtin(ops::CALL_VALUE, b_call_value);
vm.register_builtin(ops::NEW, b_new);
vm.register_builtin(ops::TRUTHY, b_truthy);
vm.register_builtin(ops::TOSTR, b_tostr);
vm.register_builtin(ops::MKFUNC, b_mkfunc);
vm.register_builtin(ops::GETITER, b_getiter);
vm.register_builtin(ops::FORITER, b_foriter);
vm.register_builtin(ops::FORIN_KEYS, b_forin_keys);
vm.register_builtin(ops::FORIN_ALIVE, b_forin_alive);
vm.register_builtin(ops::HOIST_TDZ, b_hoist_tdz);
vm.register_builtin(ops::NEW_SPREAD, b_new_spread);
vm.register_builtin(ops::SUPER_CALL_SPREAD, b_super_call_spread);
vm.register_builtin(ops::CONTAINS, b_contains);
vm.register_builtin(ops::SIG_RETURN, b_sig_return);
vm.register_builtin(ops::BINOP, b_binop);
vm.register_builtin(ops::UNARY, b_unary);
vm.register_builtin(ops::STRICT_EQ, b_strict_eq);
vm.register_builtin(ops::LOOSE_EQ, b_loose_eq);
vm.register_builtin(ops::TYPEOF, b_typeof);
vm.register_builtin(ops::LOAD_NULL, b_load_null);
vm.register_builtin(ops::THROW, b_throw);
vm.register_builtin(ops::TRY, b_try);
vm.register_builtin(ops::NULLISH, b_nullish);
vm.register_builtin(ops::UNPACK, b_unpack);
vm.register_builtin(ops::BUILD_ARGS, b_build_args);
vm.register_builtin(ops::THIS, b_this);
vm.register_builtin(ops::INSTANCEOF, b_instanceof);
vm.register_builtin(ops::DELPROP_NAME, b_delprop_name);
vm.register_builtin(ops::APPLY, b_apply);
vm.register_builtin(ops::APPLY_METHOD, b_apply_method);
vm.register_builtin(ops::OBJ_REST, b_obj_rest);
vm.register_builtin(ops::DIV, b_div);
vm.register_builtin(ops::POW, b_pow);
vm.register_builtin(ops::MKCLASS, b_mkclass);
vm.register_builtin(ops::DEF_MEMBER, b_def_member);
vm.register_builtin(ops::DEF_FIELD, b_def_field);
vm.register_builtin(ops::SUPER_CALL, b_super_call);
vm.register_builtin(ops::SUPER_GET, b_super_get);
vm.register_builtin(ops::YIELD, b_yield);
vm.register_builtin(ops::PROPKEY, b_propkey);
vm.register_builtin(ops::NEW_TARGET, b_new_target);
vm.register_builtin(ops::AWAIT, b_await);
vm.register_builtin(ops::DEF_ACCESSOR, b_def_accessor);
vm.register_builtin(ops::DBG_LINE, b_dbg_line);
vm.register_builtin(ops::MKBIGINT, b_mkbigint);
vm.register_builtin(ops::MKREGEX, b_mkregex);
vm.register_builtin(ops::TAG_TMPL, b_tag_tmpl);
vm.register_builtin(ops::GET_ASYNC_ITER, b_get_async_iter);
vm.register_builtin(ops::ASYNC_STEP, b_async_step);
vm.register_builtin(ops::NUM_STEP, b_num_step);
vm.register_builtin(ops::ITER_CLOSE, b_iter_close);
vm.register_builtin(ops::TYPEOF_NAME, b_typeof_name);
vm.register_builtin(ops::SIG_BREAK, b_sig_break);
vm.register_builtin(ops::SIG_CONTINUE, b_sig_continue);
vm.register_builtin(ops::SIG_UNWIND, b_sig_unwind);
vm.register_builtin(ops::PUSH_SCOPE, b_push_scope);
vm.register_builtin(ops::POP_SCOPE, b_pop_scope);
vm.register_builtin(ops::COPY_SCOPE, b_copy_scope);
vm.register_builtin(ops::DECLARE_VAR, b_declare_var);
vm.register_builtin(ops::HOIST_VAR, b_hoist_var);
vm.register_builtin(ops::NAMED_EVAL, b_named_eval);
}
pub(crate) fn close_iterator(it: &Value) -> Result<(), String> {
if with_host(|h| h.is_generator_val(it)) {
host::gen_return(it, Value::Undef)?;
return Ok(());
}
if matches!(with_host(|h| h.get(it).cloned()), Some(JsObj::Object(_))) {
if let Some(f) = with_host(|h| host::lookup_chain(h, it, "return")) {
if with_host(|h| host::is_callable(h, &f)) {
host::invoke(&f, Vec::new(), Some(it.clone()))?;
}
}
}
Ok(())
}
fn b_iter_close(vm: &mut VM, _: u8) -> Value {
let it = vm.pop();
match close_iterator(&it) {
Ok(()) => Value::Undef,
Err(e) => abort(vm, e),
}
}
fn b_num_step(vm: &mut VM, _: u8) -> Value {
let old = vm.pop();
let tag = match vm.pop() {
Value::Int(n) => n,
Value::Float(f) => f as i64,
_ => 1,
};
if with_host(|h| h.is_bigint_val(&old)) {
let b = with_host(|h| h.as_bigint(&old)).unwrap();
let old_n = with_host(|h| h.new_bigint(b.clone()));
let new = with_host(|h| h.new_bigint(b + num_bigint::BigInt::from(tag)));
vm.push(old_n);
new
} else {
let n = with_host(|h| h.to_number(&old));
vm.push(Value::Float(n));
Value::Float(n + tag as f64)
}
}
fn b_async_step(vm: &mut VM, _: u8) -> Value {
let iter = vm.pop();
let r = host::async_step(&iter);
finish(vm, r)
}
fn b_mkbigint(vm: &mut VM, _: u8) -> Value {
let digits = sval(&vm.pop());
match digits.parse::<num_bigint::BigInt>() {
Ok(b) => with_host(|h| h.new_bigint(b)),
Err(_) => abort(vm, host::type_error("invalid BigInt literal")),
}
}
fn require_callback(cb: &Value) -> Result<(), String> {
if with_host(|h| host::is_callable(h, cb)) {
return Ok(());
}
Err(host::invalid_arg_type(
"callback", "argument", "function", cb,
))
}
fn b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
let chunk = vm.chunk.op_hash;
let mut all = pop_n(vm, argc as usize);
let int_of = |v: &Value| match v {
Value::Int(n) => *n as usize,
Value::Float(f) => *f as usize,
_ => 0,
};
let this = all.remove(0);
let tag = all.remove(0);
let n = int_of(&all.remove(0));
let mcount = int_of(&all.remove(0));
let site = int_of(&all.remove(0)) as u64;
let cooked: Vec<Value> = all.drain(0..n.min(all.len())).collect();
let raw: Vec<Value> = all.drain(0..n.min(all.len())).collect();
let values: Vec<Value> = all.drain(0..mcount.min(all.len())).collect();
let key = (chunk, site);
let strings = match with_host(|h| h.template_object(key)) {
Some(cached) => cached,
None => {
let strings = with_host(|h| h.new_array(cooked));
let raw_arr = with_host(|h| h.new_array(raw));
with_host(|h| {
h.set_fn_prop(&strings, "raw", raw_arr.clone());
h.set_prop_attrs(
&strings,
"raw",
host::PropAttrs {
writable: false,
enumerable: false,
configurable: false,
},
);
h.seal_object(&raw_arr, true);
h.seal_object(&strings, true);
h.set_template_object(key, strings.clone());
});
strings
}
};
let mut call_args = vec![strings];
call_args.extend(values);
let this = match this {
Value::Undef => None,
v => Some(v),
};
let r = host::invoke(&tag, call_args, this);
finish(vm, r)
}
fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
let src = vm.pop();
let r = host::get_async_iterator(&src).map_err(|e| {
match host::call_site_text(vm) {
Some(t) if e.ends_with(" is not iterable") => {
host::type_error(&format!("{t} is not async iterable"))
}
_ => e,
}
});
finish(vm, r)
}
fn b_mkregex(vm: &mut VM, _: u8) -> Value {
let flags = sval(&vm.pop());
let pattern = sval(&vm.pop());
match crate::regexp::build_regexp(&pattern, &flags) {
Ok(v) => v,
Err(e) => abort(vm, e),
}
}
fn b_dbg_line(vm: &mut VM, _: u8) -> Value {
let line = match vm.pop() {
Value::Int(n) => n as u32,
_ => 0,
};
crate::dap::on_debug_line(line);
Value::Undef
}
fn b_def_accessor(vm: &mut VM, _: u8) -> Value {
let func = vm.pop();
let kind = match vm.pop() {
Value::Int(n) => n,
_ => 0,
};
let name = sval(&vm.pop());
let obj = vm.pop();
with_host(|h| {
if kind == host::member::SET {
h.set_accessor(&obj, &name, None, Some(func));
} else {
h.set_accessor(&obj, &name, Some(func), None);
}
});
obj
}
fn b_await(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
match host::await_value(v) {
Ok(r) => r,
Err(e) => abort(vm, e),
}
}
fn b_mkclass(vm: &mut VM, argc: u8) -> Value {
let source_def = match argc {
4 => match vm.pop() {
Value::Int(n) => Some(n as usize),
_ => None,
},
_ => None,
};
let ctor = vm.pop();
let parent = vm.pop();
let name = sval(&vm.pop());
host::build_class(&name, parent, ctor, source_def)
}
fn b_def_member(vm: &mut VM, _: u8) -> Value {
let func = vm.pop();
let is_static = matches!(vm.pop(), Value::Bool(true));
let kind = match vm.pop() {
Value::Int(n) => n,
_ => 0,
};
let name = sval(&vm.pop());
let class_val = vm.pop();
host::define_member(&class_val, &name, kind, is_static, func);
class_val
}
fn b_def_field(vm: &mut VM, _: u8) -> Value {
let name_anon = matches!(vm.pop(), Value::Bool(true));
let thunk = vm.pop();
let name = sval(&vm.pop());
let class_val = vm.pop();
host::define_field(&class_val, &name, thunk, name_anon);
class_val
}
fn b_super_call_spread(vm: &mut VM, _: u8) -> Value {
let arr = vm.pop();
let args = host::iter_all(&arr).unwrap_or_default();
super_call_with(vm, args)
}
fn b_super_call(vm: &mut VM, argc: u8) -> Value {
let args = pop_n(vm, argc as usize);
super_call_with(vm, args)
}
fn super_call_with(vm: &mut VM, args: Vec<Value>) -> Value {
let this = with_host(|h| h.current_this());
let this = match this {
Some(t) => t,
None => return abort(vm, host::type_error("'super' keyword unexpected here")),
};
let (parent, fields) = with_host(|h| h.super_context());
let (parent, fields) = match parent {
Some(p) => (p, fields),
None => return abort(vm, host::type_error("'super' keyword unexpected here")),
};
let nt = with_host(|h| h.current_new_target()).unwrap_or_else(|| this.clone());
let this = match host::super_construct(&parent, args, &this, &nt) {
Err(e) => return abort(vm, e),
Ok(Some(replacement)) => {
with_host(|h| h.set_current_this(replacement.clone()));
replacement
}
Ok(None) => this,
};
if !with_host(|h| h.bind_super_this()) {
return abort(
vm,
"ReferenceError: Super constructor may only be called once".to_string(),
);
}
for (name, thunk, name_anon) in fields {
if let Err(e) = host::init_one_field(&this, &name, &thunk, name_anon) {
return abort(vm, e);
}
}
Value::Undef
}
fn b_super_get(vm: &mut VM, _: u8) -> Value {
let name = sval(&vm.pop());
match with_host(|h| h.super_resolve(&name)) {
host::SuperRef::Data(v) => v,
host::SuperRef::Getter(getter) => {
let this = with_host(|h| h.current_this());
match host::invoke(&getter, Vec::new(), this) {
Ok(v) => v,
Err(e) => abort(vm, e),
}
}
}
}
fn close_parked_iters(vm: &mut VM) {
let n = host::parked_iters(vm);
if n == 0 {
return;
}
let saved = with_host(|h| (h.signal.take(), h.error.take()));
for _ in 0..n {
let it = vm.pop();
let _ = close_iterator(&it);
}
with_host(|h| {
h.signal = saved.0;
h.error = saved.1;
});
}
fn b_yield(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
match host::gen_yield(v) {
Ok(sent) => {
if with_host(|h| h.error.is_some() || h.signal.is_some()) {
close_parked_iters(vm);
vm.ip = vm.chunk.ops.len();
}
sent
}
Err(e) => {
close_parked_iters(vm);
abort(vm, e)
}
}
}
fn b_propkey(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
match host::to_property_key(&v) {
Ok(k) => with_host(|h| h.new_str(k)),
Err(e) => abort(vm, e),
}
}
fn b_new_target(_vm: &mut VM, _: u8) -> Value {
with_host(|h| h.current_new_target().unwrap_or(Value::Undef))
}
fn b_div(vm: &mut VM, _: u8) -> Value {
let b = vm.pop();
let a = vm.pop();
let r = numeric_hook(NumOp::Div, &a, &b);
finish(vm, r)
}
fn b_pow(vm: &mut VM, _: u8) -> Value {
let b = vm.pop();
let a = vm.pop();
let r = numeric_hook(NumOp::Pow, &a, &b);
finish(vm, r)
}
fn b_obj_rest(vm: &mut VM, _: u8) -> Value {
let excluded = vm.pop();
let obj = vm.pop();
let excl: Vec<String> = with_host(|h| h.iter_vec(&excluded))
.unwrap_or_default()
.iter()
.filter_map(|v| host::to_property_key(v).ok())
.collect();
if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
let keys = match crate::proxy::own_keys(&obj) {
Ok(k) => k.unwrap_or_default(),
Err(e) => return abort(vm, e),
};
let mut pairs: Vec<(String, Value)> = Vec::new();
for k in keys {
if excl.contains(&k) {
continue;
}
match crate::proxy::own_enumerable(&obj, &k) {
Ok(false) => continue,
Ok(true) => {}
Err(e) => return abort(vm, e),
}
match get_property(&obj, &k) {
Ok(v) => pairs.push((k, v)),
Err(e) => return abort(vm, e),
}
}
return with_host(|h| h.new_object(pairs.into_iter().collect()));
}
let keys: Vec<String> = with_host(|h| {
let mut ks = h.own_enum_key_names(&obj);
if let Some(JsObj::Object(m)) = h.get(&obj) {
for k in m.keys() {
if host::is_symbol_key(k) && h.prop_attrs(&obj, k).enumerable {
ks.push(k.clone());
}
}
}
ks
})
.into_iter()
.filter(|k| {
!excl.contains(k)
&& (host::is_symbol_key(k) || !(k.starts_with("@@") || k.starts_with('#')))
})
.collect();
let mut pairs: Vec<(String, Value)> = Vec::with_capacity(keys.len());
for k in keys {
match get_property(&obj, &k) {
Ok(v) => pairs.push((k, v)),
Err(e) => return abort(vm, e),
}
}
with_host(|h| {
let props: IndexMap<String, Value> = pairs.into_iter().collect();
h.new_object(props)
})
}
fn pop_n(vm: &mut VM, n: usize) -> Vec<Value> {
let mut v = Vec::with_capacity(n);
for _ in 0..n {
v.push(vm.pop());
}
v.reverse();
v
}
fn sval(v: &Value) -> String {
if let Value::Str(s) = v {
return (**s).clone();
}
with_host(|h| h.as_str(v)).unwrap_or_default()
}
fn sname(v: &Value) -> std::sync::Arc<String> {
match v {
Value::Str(s) => s.clone(),
_ => std::sync::Arc::new(sval(v)),
}
}
fn abort(vm: &mut VM, e: String) -> Value {
with_host(|h| h.error = Some(e));
vm.ip = vm.chunk.ops.len();
Value::Undef
}
fn finish(vm: &mut VM, r: Result<Value, String>) -> Value {
match r {
Ok(v) => {
if with_host(|h| h.error.is_some() || h.signal.is_some()) {
vm.ip = vm.chunk.ops.len();
}
v
}
Err(e) => abort(vm, e),
}
}
pub(crate) fn global_binding(name: &str) -> Option<Value> {
global_binding_from(name, false)
}
pub(crate) fn global_object_binding(name: &str) -> Option<Value> {
global_binding_from(name, true)
}
fn global_binding_from(name: &str, object_only: bool) -> Option<Value> {
let bound = with_host(|h| {
if object_only {
h.read_global(name)
} else {
h.read_name(name)
}
});
if let Some(v) = bound {
return Some(v);
}
match name {
"undefined" => return Some(Value::Undef),
"NaN" => return Some(Value::Float(f64::NAN)),
"Infinity" => return Some(Value::Float(f64::INFINITY)),
"globalThis" | "global" => return Some(with_host(|h| h.global_object())),
"crypto" => return Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into())))),
_ => {}
}
if is_namespace(name) || is_known_builtin(name) {
return Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))));
}
None
}
fn b_getlocal(vm: &mut VM, _: u8) -> Value {
let name = sname(&vm.pop());
if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
return abort(vm, host::tdz_error(&name));
}
match global_binding(&name) {
Some(v) if with_host(|h| h.is_tdz(&v)) => abort(vm, host::tdz_error(&name)),
Some(v) => v,
None => abort(vm, host::ref_error(&name)),
}
}
fn b_hoist_tdz(vm: &mut VM, _: u8) -> Value {
let name = sname(&vm.pop());
with_host(|h| h.hoist_tdz(&name));
Value::Undef
}
const READONLY_GLOBALS: [&str; 3] = ["undefined", "NaN", "Infinity"];
fn readonly_global_error(name: &str) -> String {
host::type_error(&format!(
"Cannot assign to read only property '{name}' of object '#<Object>'"
))
}
fn b_setlocal(vm: &mut VM, _: u8) -> Value {
let val = vm.pop();
let name = sname(&vm.pop());
if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
return val;
}
if with_host(|h| match h.read_name(&name) {
Some(v) => h.is_tdz(&v),
None => h.is_tdz_global(&name),
}) {
return abort(vm, host::tdz_error(&name));
}
if !with_host(|h| h.set_name(&name, val.clone())) {
return abort(vm, host::type_error("Assignment to constant variable."));
}
val
}
fn b_setlocal_strict(vm: &mut VM, _: u8) -> Value {
let val = vm.pop();
let name = sname(&vm.pop());
if !binding_exists(&name) {
return abort(vm, host::ref_error(&name));
}
if READONLY_GLOBALS.contains(&name.as_str()) && !with_host(|h| h.has_name(&name)) {
return abort(vm, readonly_global_error(&name));
}
if !with_host(|h| h.set_name(&name, val.clone())) {
return abort(vm, host::type_error("Assignment to constant variable."));
}
val
}
fn binding_exists(name: &str) -> bool {
if with_host(|h| h.has_name(name)) {
return true;
}
matches!(
name,
"undefined" | "NaN" | "Infinity" | "globalThis" | "global"
) || is_namespace(name)
|| is_known_builtin(name)
}
fn b_declare(vm: &mut VM, _: u8) -> Value {
let val = vm.pop();
let name = sname(&vm.pop());
with_host(|h| h.declare_name(&name, val.clone()));
val
}
fn b_declare_const(vm: &mut VM, _: u8) -> Value {
let val = vm.pop();
let name = sname(&vm.pop());
with_host(|h| h.declare_const_name(&name, val.clone()));
val
}
fn b_hoist_var(vm: &mut VM, _: u8) -> Value {
let name = sname(&vm.pop());
with_host(|h| h.hoist_var_name(&name));
Value::Undef
}
fn b_declare_var(vm: &mut VM, _: u8) -> Value {
let val = vm.pop();
let name = sname(&vm.pop());
with_host(|h| h.declare_var_name(&name, val.clone()));
val
}
fn b_push_scope(_: &mut VM, _: u8) -> Value {
with_host(|h| h.push_scope());
Value::Undef
}
fn b_pop_scope(_: &mut VM, _: u8) -> Value {
with_host(|h| h.pop_scope());
Value::Undef
}
fn b_copy_scope(_: &mut VM, _: u8) -> Value {
with_host(|h| h.copy_scope());
Value::Undef
}
fn b_delname(vm: &mut VM, _: u8) -> Value {
let name = sval(&vm.pop());
with_host(|h| h.del_name(&name));
Value::Bool(true)
}
fn b_this(vm: &mut VM, _: u8) -> Value {
if with_host(|h| h.this_state()) == host::ThisState::Pending {
return abort(vm, host::this_before_super_error());
}
with_host(|h| h.current_this().unwrap_or(Value::Undef))
}
fn b_load_null(_vm: &mut VM, _: u8) -> Value {
with_host(|h| h.null())
}
fn b_getattr(vm: &mut VM, _: u8) -> Value {
let name = sval(&vm.pop());
let recv = vm.pop();
match get_property(&recv, &name) {
Ok(v) => v,
Err(e) => abort(vm, e),
}
}
fn peek<R>(recv: &Value, f: impl FnOnce(&JsObj) -> Option<R>) -> Option<R> {
with_host(|h| h.get(recv).and_then(f))
}
pub(crate) fn proxy_proto_link(recv: &Value, name: &str) -> Option<Value> {
with_host(|h| {
let mut cur = h.proto_of(recv);
for _ in 0..100 {
let p = cur?;
match h.get(&p) {
Some(JsObj::Proxy { .. }) => return Some(p),
Some(JsObj::Object(props)) if props.contains_key(name) => return None,
_ => {}
}
if h.own_accessor(&p, name).is_some() {
return None;
}
cur = h.proto_of(&p);
}
None
})
}
const CJS_WRAPPER_LOCALS: &[&str] = &[
"require",
"module",
"exports",
"__filename",
"__dirname",
"__cjs_require",
"__cjs_resolve",
];
const ENUMERABLE_GLOBALS: &[&str] = &[
"global",
"clearImmediate",
"setImmediate",
"clearInterval",
"clearTimeout",
"setInterval",
"setTimeout",
"queueMicrotask",
"structuredClone",
"atob",
"btoa",
"performance",
"fetch",
"crypto",
"navigator",
"sessionStorage",
];
pub fn get_property(recv: &Value, name: &str) -> Result<Value, String> {
if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
return Err(private_brand_message(name, false));
}
get_property_recv(recv, name, recv)
}
pub fn private_brand_message(name: &str, writing: bool) -> String {
if with_host(|h| h.is_private_method(name)) {
if let Some(class) = with_host(|h| h.current_home_class_name()) {
return host::type_error(&format!("Receiver must be an instance of class {class}"));
}
}
let verb = if writing { "write" } else { "read" };
let prep = if writing { "to" } else { "from" };
host::type_error(&format!(
"Cannot {verb} private member {name} {prep} an object whose class did not declare it"
))
}
pub const DEFAULT_PREPARE: &str = "ErrorPrepareStackTrace";
pub fn materialize_stack(recv: &Value) {
let Some(frames) = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@stackRaw").cloned(),
_ => None,
}) else {
return;
};
let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
if let Some(f) = prep.filter(|f| {
!matches!(
with_host(|h| h.get(f).cloned()),
Some(JsObj::Builtin(ref n)) if n == DEFAULT_PREPARE
) && matches!(
with_host(|h| h.get(f).cloned()),
Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
)
}) {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.shift_remove("@@stackRaw");
}
});
let limit = with_host(|h| h.stack_trace_limit());
if let Ok(sites) = crate::module::callsite_stack(limit) {
if let Ok(out) = host::invoke(&f, vec![recv.clone(), sites], None) {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert("stack".into(), out);
}
});
return;
}
}
}
with_host(|h| {
let frames = h.str_of(&frames);
let name = host::lookup_chain(h, recv, "name")
.map(|v| h.str_of(&v))
.unwrap_or_else(|| "Error".to_string());
let message = host::lookup_chain(h, recv, "message")
.map(|v| h.str_of(&v))
.unwrap_or_default();
let header = if message.is_empty() {
name
} else {
format!("{name}: {message}")
};
let sv = h.new_str(format!("{header}{frames}"));
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert("stack".into(), sv);
p.shift_remove("@@stackRaw");
}
});
}
pub fn get_property_recv(recv: &Value, name: &str, receiver: &Value) -> Result<Value, String> {
if let Some(v) = crate::proxy::get(recv, name, receiver)? {
return Ok(v);
}
if with_host(|h| h.is_nullish(recv)) {
return Err(host::type_error(&format!(
"Cannot read properties of {} (reading '{name}')",
with_host(|h| h.str_of(recv))
)));
}
if name == "stack" {
materialize_stack(recv);
}
if let Some(v) = dom_exception_slot(recv, name) {
return Ok(v);
}
if with_host(|h| h.is_global_object(recv)) {
let own = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.contains_key(name),
_ => false,
});
if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
if let Some(v) = global_object_binding(name) {
return Ok(v);
}
}
}
if let Some((getter, _)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
return match getter {
Some(g) => host::invoke(&g, Vec::new(), Some(receiver.clone())),
None => Ok(Value::Undef), };
}
if name == "@@toStringTag" && with_host(|h| host::lookup_chain(h, recv, name)).is_none() {
if let Some(tag) = with_host(|h| well_known_tag(h, recv)) {
return Ok(with_host(|h| h.new_str(tag)));
}
}
if name == "constructor" {
if let Some(v) = with_host(|h| {
match h.get(recv) {
Some(JsObj::Object(p)) => p.get("constructor").cloned(),
_ => None,
}
.or_else(|| host::lookup_chain(h, recv, "constructor"))
}) {
return Ok(v);
}
if let Some(c) = chain_intrinsic_ctors(recv)
.into_iter()
.find(|c| is_builtin_ctor(c))
{
return Ok(with_host(|h| h.alloc(JsObj::Builtin(c.to_string()))));
}
if let Some(cn) = with_host(|h| default_ctor_name(h, recv)) {
return Ok(with_host(|h| h.alloc(JsObj::Builtin(cn.to_string()))));
}
}
if name == "__proto__"
&& !with_host(|h| h.has_null_proto(recv))
&& peek(recv, |o| match o {
JsObj::Object(p) => Some(p.contains_key("__proto__")),
_ => Some(false),
}) != Some(true)
{
return Ok(prototype_of(recv));
}
if let Some(ctor) = intrinsic_proto_of(recv) {
if is_proto_accessor(&ctor, name) {
return proto_getter_call(&ctor, name, recv);
}
}
let kind = with_host(|h| h.kind_of(recv));
#[allow(unused_mut)]
let mut out = match kind {
Some(ObjKind::Object) => {
let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
if matches!(name, "length" | "byteLength" | "byteOffset")
&& crate::stdlib::typedarray::view_detached(recv)
{
match crate::stdlib::native_tag(recv).as_deref() {
Some("TypedArray") => return Ok(Value::Float(0.0)),
Some("DataView") => {
return Err(crate::stdlib::typedarray::detached_error(
"get DataView.prototype",
name,
false,
))
}
_ => {}
}
}
if numeric && crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray") {
if let Some(v) = crate::stdlib::typedarray::elem_get(recv, name) {
return Ok(v);
}
}
if numeric
&& peek(recv, |o| match o {
JsObj::Object(p) => Some(p.contains_key("@@bytes")),
_ => None,
})
.unwrap_or(false)
{
return Ok(crate::stdlib::buffer::byte_get(recv, name));
}
if let Some(v) = peek(recv, |o| match o {
JsObj::Object(p) => p.get(name).cloned(),
_ => None,
}) {
v
} else if let Some(link) = proxy_proto_link(recv, name) {
return Ok(crate::proxy::get(&link, name, recv)?.expect("link is a proxy"));
} else if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
v
} else if crate::stdlib::native_tag(recv)
.map(|tag| crate::stdlib::instance_has_method(&tag, name))
.unwrap_or(false)
{
bound_method(recv, name)
} else if is_object_method(name) && !with_host(|h| h.has_null_proto(recv)) {
bound_method(recv, name)
} else {
Value::Undef
}
}
Some(ObjKind::Class) | Some(ObjKind::Func) | Some(ObjKind::BoundFunc) => {
function_property(recv, name)
}
Some(ObjKind::BoundMethod) => bound_method_property(recv, name),
Some(ObjKind::Symbol) => match name {
"description" => {
match peek(recv, |o| match o {
JsObj::Symbol { desc, .. } => desc.clone(),
_ => None,
}) {
Some(d) => with_host(|h| h.new_str(d)),
None => Value::Undef,
}
}
"toString" => bound_method(recv, name),
_ => with_host(|h| {
h.ensure_wrapper_protos();
h.native_proto("Symbol")
})
.and_then(|p| with_host(|h| host::lookup_chain(h, &p, name)))
.unwrap_or(Value::Undef),
},
Some(ObjKind::BigInt) => {
if matches!(
name,
"toString" | "valueOf" | "toLocaleString" | "constructor"
) {
bound_method(recv, name)
} else {
Value::Undef
}
}
Some(ObjKind::RegExp) => {
let r = peek(recv, |o| match o {
JsObj::RegExp(r) => Some(r.clone()),
_ => None,
});
match r {
Some(r) => crate::regexp::regexp_property(&r, name).unwrap_or_else(|| {
if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
return v;
}
if crate::regexp::is_regexp_method(name) {
bound_method(recv, name)
} else {
Value::Undef
}
}),
None => Value::Undef,
}
}
Some(ObjKind::Map) => {
let (len, weak) = peek(recv, |o| match o {
JsObj::Map { entries, weak } => Some((entries.len(), *weak)),
_ => None,
})
.unwrap_or((0, false));
match name {
"size" if !weak => Value::Float(len as f64),
"@@iterator" => bound_method(recv, name),
_ if is_map_method(name) => bound_method(recv, name),
_ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
}
}
Some(ObjKind::Set) => {
let (len, weak) = peek(recv, |o| match o {
JsObj::Set { entries, weak } => Some((entries.len(), *weak)),
_ => None,
})
.unwrap_or((0, false));
match name {
"size" if !weak => Value::Float(len as f64),
"@@iterator" => bound_method(recv, name),
_ if is_set_method(name) => bound_method(recv, name),
_ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
}
}
Some(ObjKind::Generator) => {
let want = if with_host(|h| h.is_async_gen_val(recv)) {
"@@asyncIterator"
} else {
"@@iterator"
};
if name == want || is_generator_method(name) || crate::stdlib::iterator::is_helper(name)
{
bound_method(recv, name)
} else {
with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
}
}
Some(ObjKind::Promise) => {
if matches!(name, "then" | "catch" | "finally") {
bound_method(recv, name)
} else {
with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
}
}
Some(ObjKind::Iter) => {
if matches!(name, "next" | "return" | "@@iterator")
|| crate::stdlib::iterator::is_helper(name)
{
bound_method(recv, name)
} else {
with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef)
}
}
Some(ObjKind::Array) => {
if name == "length" {
let n = peek(recv, |o| match o {
JsObj::Array(items) => Some(items.len()),
_ => None,
})
.unwrap_or(0);
Value::Float(n as f64)
} else if let Ok(i) = name.parse::<usize>() {
peek(recv, |o| match o {
JsObj::Array(items) => items.get(i).cloned(),
_ => None,
})
.or_else(|| with_host(|h| h.fn_prop(recv, name)))
.unwrap_or(Value::Undef)
} else if name == "@@iterator"
|| is_object_method(name)
|| (is_array_method(name) && !is_arguments(recv))
{
bound_method(recv, name)
} else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
v
} else {
Value::Undef
}
}
Some(ObjKind::Str) => {
if name == "length" {
let n = peek(recv, |o| match o {
JsObj::Str(s) => Some(crate::utf16::len(s)),
_ => None,
})
.unwrap_or(0);
Value::Float(n as f64)
} else if let Ok(i) = name.parse::<usize>() {
match peek(recv, |o| match o {
JsObj::Str(s) => crate::utf16::Units::of(s).unit_str(i),
_ => None,
}) {
Some(c) => with_host(|h| h.new_str(c)),
None => Value::Undef,
}
} else if name == "@@iterator" || is_string_method(name) {
bound_method(recv, name)
} else {
Value::Undef
}
}
Some(ObjKind::Builtin) => {
let ns = peek(recv, |o| match o {
JsObj::Builtin(ns) => Some(ns.clone()),
_ => None,
})
.unwrap_or_default();
let v = namespace_property(&ns, name);
if matches!(v, Value::Undef)
&& is_function_method(name)
&& host::builtin_is_callable(&ns)
{
return Ok(bound_method(recv, name));
}
v
}
_ => {
if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
bound_method(recv, name)
} else {
Value::Undef
}
}
};
if name == "callee" && is_arguments(recv) && with_host(|h| h.current_strict()) {
return Err(host::type_error(POISON_PILL));
}
if matches!(name, "caller" | "arguments")
&& matches!(
with_host(|h| h.kind_of(recv)),
Some(ObjKind::Func) | Some(ObjKind::Class)
)
{
return poison_pill_read(recv);
}
if name == "callee" && is_arguments(recv) {
if let Some(f) = with_host(|h| h.fn_prop(recv, "@@callee")) {
return Ok(f);
}
}
if matches!(
with_host(|h| h.get(&out).cloned()),
Some(JsObj::BoundMethod { .. })
) || matches!(
with_host(|h| h.get(&out).cloned()),
Some(JsObj::Builtin(ns)) if ns.starts_with("@proto:")
) {
if !own_intrinsic_reachable(recv) && !has_own_for_shadow(recv, name) {
out = Value::Undef;
}
}
if !name.starts_with('#') && !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
if matches!(out, Value::Undef) {
if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
return Ok(v);
}
}
if let Some(v) = inherited_builtin_static(recv, name) {
return Ok(v);
}
}
if matches!(out, Value::Undef) && !name.starts_with('#') {
if let Some(owner) = inherited_method_owner(recv, name) {
if is_proto_accessor(owner, name) && !getter_in_flight(owner, name) {
return proto_getter_call(owner, name, recv);
}
let key = format!("@proto:{owner}:{name}");
if builtin_meta(&key).is_some() {
return Ok(with_host(|h| h.alloc(JsObj::Builtin(key))));
}
let v = namespace_property(&format!("{owner}.prototype"), name);
if !matches!(v, Value::Undef) {
return Ok(v);
}
}
}
Ok(out)
}
pub const REQUIRE_CACHE: &str = "__cjs_cache";
fn default_ctor_name(h: &host::JsHost, recv: &Value) -> Option<&'static str> {
match h.get(recv) {
Some(JsObj::Array(_)) => Some("Array"),
Some(JsObj::Object(props)) => {
match props.get("@@native").map(|t| h.str_of(t)).as_deref() {
Some("Buffer") => Some("Buffer"),
Some("URL") => Some("URL"),
Some("Date") => Some("Date"),
Some("WeakRef") => Some("WeakRef"),
Some("FinalizationRegistry") => Some("FinalizationRegistry"),
Some("TextEncoder") => Some("TextEncoder"),
Some("TextDecoder") => Some("TextDecoder"),
Some("EventEmitter") => Some("EventEmitter"),
Some("Timeout") => Some("Timeout"),
Some("Immediate") => Some("Immediate"),
_ => Some("Object"),
}
}
Some(JsObj::Map { weak, .. }) => Some(if *weak { "WeakMap" } else { "Map" }),
Some(JsObj::Set { weak, .. }) => Some(if *weak { "WeakSet" } else { "Set" }),
Some(JsObj::Promise { .. }) => Some("Promise"),
Some(JsObj::Str(_)) => Some("String"),
Some(JsObj::Symbol { .. }) => Some("Symbol"),
Some(JsObj::BigInt(_)) => Some("BigInt"),
Some(JsObj::RegExp(_)) => Some("RegExp"),
Some(JsObj::Iter { .. }) => Some("Iterator"),
Some(JsObj::Func(f)) => {
Some(match h.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",
})
}
Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => Some("Function"),
_ => match recv {
Value::Float(_) | Value::Int(_) => Some("Number"),
Value::Bool(_) => Some("Boolean"),
_ => None,
},
}
}
fn has_species(name: &str) -> bool {
matches!(
name,
"Array" | "Map" | "Set" | "WeakMap" | "WeakSet" | "Promise" | "RegExp" | "ArrayBuffer"
) || crate::stdlib::typedarray::is_ctor(name)
}
fn is_builtin_ctor(name: &str) -> bool {
matches!(
name,
"Array"
| "Object"
| "Number"
| "String"
| "Boolean"
| "Symbol"
| "Function"
| "Map"
| "Set"
| "WeakMap"
| "WeakSet"
| "Promise"
| "BigInt"
| "Iterator"
| "RegExp"
| "Date"
| "ArrayBuffer"
| "DataView"
| "Uint8Array"
| "Int8Array"
| "Uint8ClampedArray"
| "Int16Array"
| "Uint16Array"
| "Int32Array"
| "Uint32Array"
| "Float32Array"
| "Float64Array"
| "BigInt64Array"
| "BigUint64Array"
| "WeakRef"
| "FinalizationRegistry"
| "TextEncoder"
| "TextDecoder"
| "IncomingMessage"
| "ServerResponse"
| "EventEmitter"
| "Buffer"
| "URL"
| "URLSearchParams"
| "Timeout"
| "Immediate"
) || host::ERROR_NAMES.contains(&name)
|| crate::stdlib::stream::is_class(name)
}
fn bound_method_key(recv: &Value, method: &str) -> Option<String> {
let ctor = with_host(|h| default_ctor_name(h, recv))?;
Some(format!("@proto:{ctor}:{method}"))
}
fn bound_method_property(recv: &Value, name: &str) -> Value {
let method = peek(recv, |o| match o {
JsObj::BoundMethod { name, .. } => Some(name.clone()),
_ => None,
})
.unwrap_or_default();
let key = peek(recv, |o| match o {
JsObj::BoundMethod { recv, .. } => Some(recv.clone()),
_ => None,
})
.and_then(|inner| bound_method_key(&inner, &method));
let meta = key.as_deref().and_then(builtin_meta);
match name {
"name" => {
let n = meta.map(|(n, _)| n.to_string()).unwrap_or(method);
with_host(|h| h.new_str(n))
}
"length" => match meta {
Some((_, len)) => Value::Float(len as f64),
None => Value::Undef,
},
_ if is_function_method(name) => bound_method(recv, name),
_ => with_host(|h| h.fn_prop(recv, name)).unwrap_or(Value::Undef),
}
}
fn bound_method(recv: &Value, name: &str) -> Value {
if let Some(key) = bound_method_key(recv, name) {
if builtin_meta(&key).is_some() {
return with_host(|h| h.alloc(JsObj::Builtin(key)));
}
}
with_host(|h| {
h.alloc(JsObj::BoundMethod {
recv: recv.clone(),
name: name.to_string(),
})
})
}
fn is_object_method(name: &str) -> bool {
matches!(
name,
"hasOwnProperty"
| "isPrototypeOf"
| "propertyIsEnumerable"
| "toString"
| "toLocaleString"
| "valueOf"
| "constructor"
| "__defineGetter__"
| "__defineSetter__"
| "__lookupGetter__"
| "__lookupSetter__"
)
}
pub const OBJECT_PROTO_METHODS: &[&str] = &[
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toString",
"toLocaleString",
"valueOf",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
];
fn integrity_level(v: &Value, freeze: bool) -> Result<Value, String> {
if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
return Ok(Value::Bool(with_host(|h| h.is_sealed(v, freeze))));
}
if crate::proxy::is_extensible(v)?.unwrap_or(true) {
return Ok(Value::Bool(false));
}
for key in crate::proxy::own_keys(v)?.unwrap_or_default() {
let Some(d) = crate::proxy::get_own_descriptor(v, &key)? else {
continue;
};
let flag = |name: &str| {
with_host(|h| match h.get(&d) {
Some(JsObj::Object(p)) => p.get(name).map(|x| h.truthy(x)).unwrap_or(false),
_ => false,
})
};
let is_data = with_host(
|h| matches!(h.get(&d), Some(JsObj::Object(p)) if !p.contains_key("get") && !p.contains_key("set")),
);
if flag("configurable") || (freeze && is_data && flag("writable")) {
return Ok(Value::Bool(false));
}
}
Ok(Value::Bool(true))
}
fn seal_proxy(v: &Value, freeze: bool) -> Result<bool, String> {
if with_host(|h| h.kind_of(v)) != Some(ObjKind::Proxy) {
return Ok(false);
}
if !crate::proxy::prevent_extensions(v)? {
return Err(host::type_error("Object.freeze called on non-object"));
}
let keys = crate::proxy::own_keys(v)?.unwrap_or_default();
for key in keys {
let accessor = if freeze {
let Some(cur) = crate::proxy::get_own_descriptor(v, &key)? else {
continue;
};
with_host(
|h| matches!(h.get(&cur), Some(JsObj::Object(p)) if p.contains_key("get") || p.contains_key("set")),
)
} else {
false
};
let desc = with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("configurable".into(), Value::Bool(false));
if freeze && !accessor {
m.insert("writable".into(), Value::Bool(false));
}
h.new_object(m)
});
if !crate::proxy::define_property(v, &key, &desc)? {
return Err(host::type_error(&format!(
"'defineProperty' on proxy: trap returned falsish for property '{key}'"
)));
}
}
Ok(true)
}
fn reject_sealing_a_view(v: &Value, verb: &str) -> Result<(), String> {
let has_elements = matches!(
crate::stdlib::native_tag(v).as_deref(),
Some("TypedArray") | Some("Buffer")
) && !crate::stdlib::typedarray::elem_values(v).is_empty();
if has_elements {
return Err(host::type_error(&format!(
"Cannot {verb} array buffer views with elements"
)));
}
Ok(())
}
pub fn is_object_builtin_method(name: &str) -> bool {
matches!(
name,
"hasOwnProperty"
| "isPrototypeOf"
| "propertyIsEnumerable"
| "toString"
| "toLocaleString"
| "valueOf"
| "__defineGetter__"
| "__defineSetter__"
| "__lookupGetter__"
| "__lookupSetter__"
)
}
fn to_string_tag(recv: &Value) -> Result<Option<String>, String> {
let tagged = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
|| with_host(|h| {
host::lookup_chain(h, recv, "@@toStringTag").is_some()
|| host::lookup_accessor(h, recv, "@@toStringTag").is_some()
});
if !tagged {
return Ok(None);
}
let t = get_property(recv, "@@toStringTag")?;
Ok(with_host(|h| h.as_str(&t)))
}
pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"__defineGetter__" | "__defineSetter__" => {
let getter = name == "__defineGetter__";
let f = args.get(1).cloned().unwrap_or(Value::Undef);
if !with_host(|h| host::is_callable(h, &f)) {
return Err(host::type_error(&format!(
"Object.prototype.{name}: Expecting function"
)));
}
let key = host::to_property_key(&arg0(&args))?;
let desc = with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert(if getter { "get" } else { "set" }.into(), f);
m.insert("enumerable".into(), Value::Bool(true));
m.insert("configurable".into(), Value::Bool(true));
h.new_object(m)
});
apply_descriptor(recv, &key, &desc)?;
Ok(Value::Undef)
}
"__lookupGetter__" | "__lookupSetter__" => {
let want_get = name == "__lookupGetter__";
let key = host::to_property_key(&arg0(&args))?;
let found = with_host(|h| host::lookup_accessor(h, recv, &key));
Ok(match found {
Some((g, st)) => {
let side = if want_get { g } else { st };
side.unwrap_or(Value::Undef)
}
None => Value::Undef,
})
}
"hasOwnProperty" => {
let k = host::to_property_key(&arg0(&args))?;
if with_host(|h| h.is_global_object(recv))
&& !CJS_WRAPPER_LOCALS.contains(&k.as_str())
&& global_object_binding(&k).is_some()
{
return Ok(Value::Bool(true));
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Builtin) {
return Ok(Value::Bool(has_property(recv, &k)?));
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
let d = crate::proxy::get_own_descriptor(recv, &k)?.unwrap_or(Value::Undef);
return Ok(Value::Bool(!matches!(d, Value::Undef)));
}
if let Some(hit) = crate::stdlib::typedarray::has_index(recv, &k) {
return Ok(Value::Bool(hit));
}
if synthesized_own_descriptor(recv, &k).is_some() {
return Ok(Value::Bool(true));
}
if uses_side_table(recv) {
return Ok(Value::Bool(with_host(|h| h.fn_prop(recv, &k).is_some())));
}
let has = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.contains_key(&k) || h.own_accessor(recv, &k).is_some(),
Some(JsObj::Array(items)) => {
k == "length"
|| k.parse::<usize>()
.map(|i| i < items.len() && !h.is_hole(recv, i))
.unwrap_or(false)
}
_ => false,
});
Ok(Value::Bool(has))
}
"isPrototypeOf" => {
let target = arg0(&args);
let mut cur = match crate::proxy::get_prototype_of(&target)? {
Some(p) => Some(p).filter(|p| !with_host(|h| h.is_null(p))),
None => with_host(|h| h.proto_of(&target)),
};
while let Some(p) = cur {
if with_host(|h| h.strict_eq(&p, recv)) {
return Ok(Value::Bool(true));
}
cur = with_host(|h| h.proto_of(&p));
}
Ok(Value::Bool(false))
}
"propertyIsEnumerable" => {
let k = with_host(|h| h.str_of(&arg0(&args)));
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
let has = crate::proxy::own_enum_string_keys(recv)?.contains(&k);
return Ok(Value::Bool(has));
}
let has = with_host(|h| h.own_enum_key_names(recv).contains(&k));
Ok(Value::Bool(has))
}
"toString" => {
if let Some(t) = to_string_tag(recv)? {
return Ok(with_host(|h| h.new_str(format!("[object {t}]"))));
}
Ok(with_host(|h| {
let s = h.str_of(recv);
h.new_str(s)
}))
}
"toLocaleString" => {
let v = host::call_method(recv, "toString", Vec::new())?;
Ok(v)
}
"valueOf" => Ok(recv.clone()),
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
pub fn function_builtin_method(
recv: &Value,
name: &str,
args: &[Value],
) -> Result<Option<Value>, String> {
match name {
"call" => {
let this = args.first().cloned();
let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
Ok(Some(host::invoke(recv, rest, this)?))
}
"apply" => {
let this = args.first().cloned();
let arr = args.get(1).cloned().unwrap_or(Value::Undef);
let call_args = if matches!(arr, Value::Undef) || with_host(|h| h.is_null(&arr)) {
Vec::new()
} else {
create_list_from_array_like(&arr)?
};
Ok(Some(host::invoke(recv, call_args, this)?))
}
"bind" => {
let this = args.first().cloned().unwrap_or(Value::Undef);
let pre = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
Ok(Some(with_host(|h| {
h.alloc(JsObj::BoundFunc {
target: recv.clone(),
this,
args: pre,
})
})))
}
"toString" => Ok(Some(with_host(|h| {
let s = h.str_of(recv);
h.new_str(s)
}))),
_ => Ok(None),
}
}
fn is_function_method(name: &str) -> bool {
matches!(name, "call" | "apply" | "bind" | "toString")
}
fn is_map_method(name: &str) -> bool {
matches!(
name,
"get" | "set" | "has" | "delete" | "clear" | "forEach" | "keys" | "values" | "entries"
)
}
fn is_set_method(name: &str) -> bool {
matches!(
name,
"add"
| "has"
| "delete"
| "clear"
| "forEach"
| "keys"
| "values"
| "entries"
| "union"
| "intersection"
| "difference"
| "symmetricDifference"
| "isSubsetOf"
| "isSupersetOf"
| "isDisjointFrom"
)
}
fn is_generator_method(name: &str) -> bool {
matches!(name, "next" | "return" | "throw")
}
fn function_property(recv: &Value, name: &str) -> Value {
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Class) {
if let Some(v) = with_host(|h| h.class_static(recv, name)) {
return v;
}
if matches!(name, "name" | "length")
&& with_host(|h| h.class_static(recv, name)).is_none()
&& with_host(|h| h.class_builtin_ancestor(recv))
.is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
{
if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
return v;
}
if name == "name" {
let n = with_host(|h| h.callable_name(recv));
return with_host(|h| h.new_str(n));
}
let ctor = with_host(|h| match h.get(recv) {
Some(JsObj::Class(c)) => c.ctor.clone(),
_ => None,
});
return match ctor {
Some(c) => get_property(&c, "length").unwrap_or(Value::Float(0.0)),
None => Value::Float(0.0),
};
}
if name == "@@species"
&& with_host(|h| h.class_static(recv, "@@species")).is_none()
&& with_host(|h| h.class_builtin_ancestor(recv))
.is_some_and(|a| matches!(with_host(|h| h.kind_of(&a)), Some(ObjKind::Builtin)))
{
return recv.clone();
}
if let Some(anc) = with_host(|h| h.class_builtin_ancestor(recv)) {
if let Ok(v) = get_property(&anc, name) {
if !matches!(v, Value::Undef) {
return v;
}
}
}
} else if let Some(v) = with_host(|h| h.fn_prop(recv, name)) {
return v;
}
if let Some(v) = with_host(|h| host::lookup_chain(h, recv, name)) {
return v;
}
match name {
"name" => with_host(|h| {
let n = h.callable_name(recv);
h.new_str(n)
}),
"length" => Value::Float(with_host(|h| h.func_arity(recv)) as f64),
"prototype" => ensure_fn_prototype(recv),
_ if is_function_method(name) => bound_method(recv, name),
_ => Value::Undef,
}
}
fn ensure_fn_prototype(recv: &Value) -> Value {
if let Some(p) = with_host(|h| h.fn_prop(recv, "prototype")) {
return p;
}
if with_host(|h| h.kind_of(recv)) != Some(ObjKind::Func) {
return Value::Undef;
}
if !with_host(|h| h.owns_prototype(recv)) {
return Value::Undef;
}
with_host(|h| {
let proto = h.new_object(IndexMap::new());
if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
p.insert("constructor".to_string(), recv.clone());
}
h.hide_prop(&proto, "constructor");
h.set_fn_prop(recv, "prototype", proto.clone());
proto
})
}
pub fn namespace_constants(ns: &str) -> &'static [(&'static str, f64)] {
const MATH: &[(&str, f64)] = &[
("E", std::f64::consts::E),
("LN10", std::f64::consts::LN_10),
("LN2", std::f64::consts::LN_2),
("LOG10E", std::f64::consts::LOG10_E),
("LOG2E", std::f64::consts::LOG2_E),
("PI", std::f64::consts::PI),
("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
("SQRT2", std::f64::consts::SQRT_2),
];
const NUMBER: &[(&str, f64)] = &[
("MAX_VALUE", f64::MAX),
("MIN_VALUE", 5e-324),
("NaN", f64::NAN),
("NEGATIVE_INFINITY", f64::NEG_INFINITY),
("POSITIVE_INFINITY", f64::INFINITY),
("MAX_SAFE_INTEGER", 9007199254740991.0),
("MIN_SAFE_INTEGER", -9007199254740991.0),
("EPSILON", f64::EPSILON),
];
match ns {
"Math" => MATH,
"Number" => NUMBER,
_ => &[],
}
}
fn builtin_member_descriptor(ns: &str, key: &str, value: Value) -> Value {
let frozen = namespace_constants(ns).iter().any(|(k, _)| *k == key)
|| key == "prototype"
|| (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key));
let own_fn_meta = matches!(key, "name" | "length") && host::builtin_is_callable(ns);
let assigned = !intrinsic_proto_member(ns, key)
&& !crate::stdlib::namespace_keys(ns).iter().any(|k| k == key)
&& with_host(|h| h.builtin_static(ns, key).is_some());
let enumerable = assigned
|| (!frozen && !own_fn_meta && crate::stdlib::namespace_keys(ns).iter().any(|k| k == key));
with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), value);
m.insert(
"writable".into(),
Value::Bool(assigned || (!frozen && !own_fn_meta)),
);
m.insert("enumerable".into(), Value::Bool(enumerable));
m.insert("configurable".into(), Value::Bool(assigned || !frozen));
h.new_object(m)
})
}
fn intrinsic_proto_member(ns: &str, key: &str) -> bool {
intrinsic_proto_members(ns).is_some_and(|members| {
members
.iter()
.any(|m| m.strip_prefix('+').unwrap_or(m) == key)
})
}
fn builtin_member_configurable(ns: &str, key: &str) -> bool {
!(namespace_constants(ns).iter().any(|(k, _)| *k == key)
|| key == "prototype"
|| (ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&key)))
}
fn namespace_constant(ns: &str, name: &str) -> Option<f64> {
namespace_constants(ns)
.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| *v)
}
fn is_webidl_proto(ctor: &str) -> bool {
intrinsic_proto_members(&format!("{ctor}.prototype"))
.is_some_and(|ms| ms.iter().any(|m| m.starts_with('+')))
}
pub(crate) fn own_ctor_name(h: &host::JsHost, v: &Value) -> Option<&'static str> {
default_ctor_name(h, v)
}
pub(crate) fn is_proto_readonly(ctor: &str, key: &str) -> bool {
crate::arity::PROTO_READONLY
.binary_search_by(|(k, _)| (*k).cmp(ctor))
.ok()
.is_some_and(|i| crate::arity::PROTO_READONLY[i].1.contains(&key))
}
pub(crate) fn is_proto_accessor(ctor: &str, key: &str) -> bool {
crate::arity::PROTO_ACCESSORS
.binary_search_by(|(k, _)| (*k).cmp(ctor))
.ok()
.is_some_and(|i| crate::arity::PROTO_ACCESSORS[i].1.contains(&key))
}
pub(crate) fn intrinsic_proto_of(recv: &Value) -> Option<String> {
with_host(|h| match h.get(recv) {
Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
_ => h.intrinsic_proto_ctor(recv).map(str::to_string),
})
}
fn proto_getter(ctor: &str, key: &str) -> Value {
with_host(|h| h.alloc(JsObj::Builtin(format!("@protoget:{ctor}:{key}"))))
}
fn brand_matches(recv: &Value, ctor: &str) -> bool {
if let Some(tag) = crate::stdlib::native_tag(recv) {
if tag == ctor || (ctor == "TypedArray" && tag == "TypedArray") {
return true;
}
}
match ctor {
"TypedArray" => crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray"),
"ArrayBuffer" => with_host(
|h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@bytes")),
),
_ => {
let own = match wrapped_primitive(recv).as_ref().and_then(wrapper_ctor_of) {
Some(c) => Some(c),
None => with_host(|h| default_ctor_name(h, recv)),
};
own == Some(ctor)
}
}
}
thread_local! {
static GETTERS_IN_FLIGHT: std::cell::RefCell<Vec<(String, String)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn getter_in_flight(ctor: &str, key: &str) -> bool {
GETTERS_IN_FLIGHT.with(|g| g.borrow().iter().any(|(c, k)| c == ctor && k == key))
}
pub(crate) fn proto_getter_call(ctor: &str, key: &str, recv: &Value) -> Result<Value, String> {
let is_the_prototype = with_host(
|h| matches!(h.get(recv), Some(JsObj::Builtin(ns)) if *ns == format!("{ctor}.prototype")),
);
if is_the_prototype && ctor == "RegExp" {
return Ok(match key {
"source" => with_host(|h| h.new_str("(?:)".to_string())),
"flags" => with_host(|h| h.new_str(String::new())),
_ => Value::Undef,
});
}
if ctor == "RegExp" && key == "flags" && !brand_matches(recv, ctor) {
if !with_host(|h| is_object_like(h, recv)) {
return Err(regexp_brand_error(key, recv));
}
let mut out = String::new();
for (prop, letter) in REGEXP_FLAG_LETTERS {
let v = get_property(recv, prop)?;
if with_host(|h| h.truthy(&v)) {
out.push(*letter);
}
}
return Ok(with_host(|h| h.new_str(out)));
}
if ctor == "Function" && matches!(key, "arguments" | "caller") {
return poison_pill_read(recv);
}
if !brand_matches(recv, ctor) {
return Err(match ctor {
"RegExp" => regexp_brand_error(key, recv),
"Symbol" => {
host::type_error("Symbol.prototype.description requires that 'this' be a Symbol")
}
_ => host::type_error(&format!(
"Method get {ctor}.prototype.{key} called on incompatible receiver {}",
brand_receiver_string(recv)
)),
});
}
if let Some(v) = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
_ => None,
}) {
return Ok(v);
}
GETTERS_IN_FLIGHT.with(|g| g.borrow_mut().push((ctor.to_string(), key.to_string())));
let out = get_property(recv, key);
GETTERS_IN_FLIGHT.with(|g| {
g.borrow_mut().pop();
});
out
}
pub(crate) fn poison_pill_read(recv: &Value) -> Result<Value, String> {
if with_host(|h| h.fn_is_sloppy(recv)) {
return Ok(with_host(|h| h.null()));
}
Err(host::type_error(POISON_PILL))
}
pub(crate) const POISON_PILL: &str = "'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them";
fn brand_receiver_string(recv: &Value) -> String {
if let Some(ctor) = intrinsic_proto_of(recv) {
return format!("#<{ctor}>");
}
match crate::stdlib::native_tag(recv).as_deref() {
Some(tag @ ("ArrayBuffer" | "DataView")) => format!("#<{tag}>"),
_ => no_side_effects_string(recv),
}
}
const REGEXP_FLAG_LETTERS: &[(&str, char)] = &[
("hasIndices", 'd'),
("global", 'g'),
("ignoreCase", 'i'),
("multiline", 'm'),
("dotAll", 's'),
("unicode", 'u'),
("unicodeSets", 'v'),
("sticky", 'y'),
];
fn regexp_brand_error(key: &str, recv: &Value) -> String {
if key == "flags" && !with_host(|h| matches!(recv, Value::Obj(_)) && !h.is_null(recv)) {
return host::type_error(&format!(
"RegExp.prototype.flags getter called on non-object {}",
no_side_effects_string(recv)
));
}
host::type_error(&format!(
"RegExp.prototype.{key} getter called on non-RegExp object"
))
}
pub fn namespace_property(ns: &str, name: &str) -> Value {
if ns == REQUIRE_CACHE {
return crate::module::cache_get(name).unwrap_or(Value::Undef);
}
if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
return v;
}
if ns == "util.promisify" && name == "custom" {
return with_host(|h| h.symbol_for("nodejs.util.promisify.custom"));
}
if ns == "process.memoryUsage" && name == "rss" {
return with_host(|h| h.alloc(JsObj::Builtin("process.memoryUsage.rss".to_string())));
}
if ns == "require" && name == "extensions" {
return with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
for ext in [".js", ".json", ".node"] {
let f = h.alloc(JsObj::Builtin(format!("@@extension:{ext}")));
m.insert(ext.to_string(), f);
}
h.new_object(m)
});
}
if ns == "require.resolve" && name == "paths" {
return with_host(|h| h.alloc(JsObj::Builtin("require.resolve.paths".to_string())));
}
if ns == "require" && name == "cache" {
return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
}
if ns == "DOMException" {
if let Some((_, code)) = DOM_EXCEPTION_CODES
.iter()
.find(|(n, _)| legacy_code_name(n) == name)
{
return Value::Float(*code);
}
}
if let Some(k) = namespace_constant(ns, name) {
return Value::Float(k);
}
if name == "prototype"
&& matches!(
ns,
"GeneratorFunction" | "AsyncFunction" | "AsyncGeneratorFunction"
)
{
return with_host(|h| {
h.ensure_native_protos();
h.native_proto(ns).unwrap_or(Value::Undef)
});
}
if ns == "Error" && name == "prepareStackTrace" {
return with_host(|h| h.builtin_static("Error", "prepareStackTrace")).unwrap_or_else(
|| with_host(|h| h.alloc(JsObj::Builtin(DEFAULT_PREPARE.to_string()))),
);
}
if ns == "Error" && name == "stackTraceLimit" {
return with_host(|h| h.builtin_static("Error", "stackTraceLimit"))
.unwrap_or(Value::Float(10.0));
}
if name == "@@species" && has_species(ns) {
return with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())));
}
if name == "name" && is_builtin_ctor(ns) {
return with_host(|h| h.new_str(ns.to_string()));
}
if ns == "Symbol" && host::WELL_KNOWN_SYMBOLS.contains(&name) {
return with_host(|h| h.well_known_symbol(name));
}
if let Some(v) = crate::stdlib::constant(ns, name) {
return v;
}
if name == "prototype" && is_builtin_ctor(ns) {
if host::ERROR_NAMES.contains(&ns) {
if let Some(p) = with_host(|h| {
h.ensure_error_protos();
host::error_proto_of(h, ns)
}) {
return p;
}
}
if let Some(p) = with_host(|h| {
h.ensure_native_protos();
h.native_proto(ns)
}) {
return p;
}
let _ = ns;
return with_host(|h| h.alloc(JsObj::Builtin(format!("{ns}.prototype"))));
}
if name == "prototype" {
if let Some(p) = with_host(|h| h.ensure_ctor_proto(ns)) {
return p;
}
}
if let Some(ctor) = ns.strip_suffix(".prototype") {
if name == "@@unscopables" && ctor == "Array" {
return with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
for k in [
"at",
"copyWithin",
"entries",
"fill",
"find",
"findIndex",
"findLast",
"findLastIndex",
"flat",
"flatMap",
"includes",
"keys",
"toReversed",
"toSorted",
"toSpliced",
"values",
] {
m.insert(k.to_string(), Value::Bool(true));
}
let o = h.new_object(m);
let null = h.null();
h.set_proto(&o, null);
o
});
}
if builtin_meta(&format!("@proto:{ctor}:{name}")).is_some() {
return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
}
if ctor != "Object" && builtin_meta(&format!("@proto:Object:{name}")).is_some() {
return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:Object:{name}"))));
}
if name == "constructor" && is_builtin_ctor(ctor) {
return with_host(|h| h.alloc(JsObj::Builtin(ctor.to_string())));
}
return Value::Undef;
}
let qualified = format!("{ns}.{name}");
if is_known_builtin(&qualified) {
return with_host(|h| h.alloc(JsObj::Builtin(qualified)));
}
if let Some(v) = with_host(|h| h.builtin_static(ns, name)) {
return v;
}
if host::builtin_is_callable(ns) {
match name {
"name" => {
if let Some(n) = proto_getter_name(ns) {
return with_host(|h| h.new_str(n));
}
return with_host(|h| h.new_str(builtin_name(ns).to_string()));
}
"length" => {
if proto_getter_name(ns).is_some() {
return Value::Float(0.0);
}
if let Some((_, len)) = builtin_meta(ns) {
return Value::Float(len as f64);
}
}
_ => {}
}
}
Value::Undef
}
fn nullish_receiver_error(ctor: &str, method: &str, recv: &str) -> Option<String> {
const ARRAY_NAMED: &[&str] = &[
"concat",
"every",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"forEach",
"indexOf",
"map",
"reduce",
"reduceRight",
"some",
];
const TO_OBJECT: &str = "Cannot convert undefined or null to object";
let named = |c: &str| format!("{c}.prototype.{method} called on null or undefined");
let branded =
|c: &str, want: &str| format!("{c}.prototype.{method} requires that 'this' be a {want}");
let generic = |c: &str, m: &str| {
format!("Method {c}.prototype.{m} called on incompatible receiver {recv}")
};
Some(match ctor {
"Array" if ARRAY_NAMED.contains(&method) => named("Array"),
"Array" => TO_OBJECT.to_string(),
"Object" if method == "toString" => return None,
"Object" if method == "toLocaleString" => named("Object"),
"Object" => TO_OBJECT.to_string(),
"String" if method == "trimStart" => named("String").replace("trimStart", "trimLeft"),
"String" if method == "trimEnd" => named("String").replace("trimEnd", "trimRight"),
"String" if matches!(method, "toString" | "valueOf") => branded("String", "String"),
"String" => named("String"),
"Number" => branded("Number", "Number"),
"Boolean" => branded("Boolean", "Boolean"),
"Symbol" => branded("Symbol", "Symbol"),
"Function" if method == "bind" => "Bind must be called on a function".to_string(),
"Function" if matches!(method, "call" | "apply") => format!(
"Function.prototype.{method} was called on undefined, which is undefined and not a function"
),
"Function" => branded("Function", "Function"),
"Promise" if method == "catch" => {
"Cannot read properties of undefined (reading 'then')".to_string()
}
"Promise" if method == "finally" => {
"Promise.prototype.finally called on non-object".to_string()
}
"Date" if method == "toJSON" => TO_OBJECT.to_string(),
"Date"
if method == "valueOf"
|| (method.starts_with("get") && method != "getYear") =>
{
"this is not a Date object.".to_string()
}
"Date" if method == "toGMTString" => generic("Date", "toUTCString"),
"Set" if method == "keys" => generic("Set", "values"),
"ArrayBuffer" | "DataView" | "RegExp" | "WeakRef" | "Map" | "Set" | "WeakMap"
| "WeakSet" | "Promise" | "Date" => generic(ctor, method),
"URLSearchParams" => "Value of \"this\" must be of type URLSearchParams".to_string(),
"URL" => "Cannot read properties of undefined (reading 'URL')".to_string(),
_ => return None,
})
}
fn is_brand_checked_primitive_method(ctor: &str, method: &str) -> bool {
match ctor {
"Number" => matches!(
method,
"toString" | "toLocaleString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision"
),
"BigInt" => matches!(method, "toString" | "toLocaleString" | "valueOf"),
"String" | "Boolean" => matches!(method, "toString" | "valueOf"),
_ => false,
}
}
fn this_primitive_value(ctor: &str, recv: &Value) -> Option<Value> {
let expected = match ctor {
"Number" => "number",
"String" => "string",
"Boolean" => "boolean",
"BigInt" => "bigint",
_ => return None,
};
let is_expected = |v: &Value| with_host(|h| h.type_of(v)) == expected;
if is_expected(recv) {
return Some(recv.clone());
}
if let Some(prim) = wrapped_primitive(recv).filter(is_expected) {
return Some(prim);
}
if with_host(|h| h.intrinsic_proto_ctor(recv) == Some(ctor)) {
return match ctor {
"Number" => Some(Value::Float(0.0)),
"String" => Some(with_host(|h| h.new_str(""))),
"Boolean" => Some(Value::Bool(false)),
_ => None,
};
}
None
}
pub fn proto_method(recv: &Value, ctor_method: &str, args: Vec<Value>) -> Result<Value, String> {
let (ctor, method) = ctor_method.split_once(':').unwrap_or(("", ctor_method));
if let Some(key) = method.strip_prefix("@get@") {
if let Some(v) = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get(&format!("@@{key}")).cloned(),
_ => None,
}) {
return Ok(v);
}
let tag = crate::stdlib::native_tag(recv).unwrap_or_default();
return crate::stdlib::instance_call(&tag, recv, method, args);
}
if let Some(key) = method.strip_prefix("@set@") {
let v = args.first().cloned().unwrap_or(Value::Undef);
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert(format!("@@{key}"), v);
}
});
crate::stdlib::instance_accessor_written(ctor, key, recv);
return Ok(Value::Undef);
}
if with_host(|h| h.is_nullish(recv)) {
let shown = if with_host(|h| h.is_null(recv)) {
"null"
} else {
"undefined"
};
if let Some(msg) = nullish_receiver_error(ctor, method, shown) {
return Err(format!("TypeError: {msg}"));
}
}
if ctor == "Error" && method == "toString" {
if let Some(n) = dom_exception_slot(recv, "name") {
let name = with_host(|h| h.str_of(&n));
let msg = dom_exception_slot(recv, "message")
.map(|m| with_host(|h| h.str_of(&m)))
.unwrap_or_default();
let s = if msg.is_empty() {
name
} else {
format!("{name}: {msg}")
};
return Ok(with_host(|h| h.new_str(s)));
}
let via_proxy = with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy);
let stored = (!via_proxy).then(|| with_host(|h| h.error_to_string(recv)));
let s = match stored.flatten() {
Some(s) => s,
None => {
let read = |k: &str| -> Result<Option<String>, String> {
Ok(host::protocol_lookup(recv, k)?.map(|v| with_host(|h| h.str_of(&v))))
};
let name = read("name")?.unwrap_or_else(|| "Error".into());
let msg = read("message")?.unwrap_or_default();
if msg.is_empty() {
name
} else {
format!("{name}: {msg}")
}
}
};
return Ok(with_host(|h| h.new_str(s)));
}
if is_brand_checked_primitive_method(ctor, method) {
let Some(prim) = this_primitive_value(ctor, recv) else {
return Err(format!(
"TypeError: {ctor}.prototype.{method} requires that 'this' be a {ctor}"
));
};
return host::call_method(&prim, method, args);
}
if matches!(ctor, "String" | "Number" | "Boolean") {
let prim = wrapped_primitive(recv).unwrap_or_else(|| recv.clone());
return host::call_method(&prim, method, args);
}
if matches!(ctor, "Symbol" | "BigInt") {
if let Some(prim) = wrapped_primitive(recv) {
return host::call_method(&prim, method, args);
}
}
if ctor == "Object" && method == "toString" {
if let Some(s) = to_string_tag(recv)? {
return Ok(with_host(|h| h.new_str(format!("[object {s}]"))));
}
return Ok(with_host(|h| h.new_str(object_tag(h, recv))));
}
if ctor == "Object" && is_object_builtin_method(method) {
return object_builtin_method(recv, method, args);
}
if ctor == "EventEmitter" {
return crate::stdlib::events::instance_call(recv, method, args);
}
if ctor == "Buffer" && crate::stdlib::native_tag(recv).as_deref() == Some("Buffer") {
return crate::stdlib::buffer::instance_call(recv, method, &args);
}
if ctor == "Uint8Array" || ctor == "TypedArray" {
match crate::stdlib::native_tag(recv).as_deref() {
Some("Buffer") => return crate::stdlib::buffer::instance_call(recv, method, &args),
Some("TypedArray") => {
return crate::stdlib::typedarray::instance_call(recv, method, &args)
}
_ => {}
}
}
if ctor == "Array" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Array) {
return array_generic(recv, method, args);
}
if let Some(tag) = crate::stdlib::native_tag(recv) {
let mut c = Some(tag.as_str());
while let Some(t) = c {
if t == ctor {
return crate::stdlib::instance_call(&tag, recv, method, args);
}
c = crate::stdlib::native_parent(t);
}
}
if ctor == "Date" && crate::stdlib::native_tag(recv).as_deref() != Some("Date") {
const THIS_TIME_VALUE: &[&str] = &[
"getTime",
"valueOf",
"getYear",
"getFullYear",
"getMonth",
"getDate",
"getDay",
"getHours",
"getMinutes",
"getSeconds",
"getMilliseconds",
"getUTCFullYear",
"getUTCMonth",
"getUTCDate",
"getUTCDay",
"getUTCHours",
"getUTCMinutes",
"getUTCSeconds",
"getUTCMilliseconds",
"getTimezoneOffset",
];
if THIS_TIME_VALUE.contains(&method) {
return Err(host::type_error("this is not a Date object."));
}
if method != "toJSON" {
return Err(host::type_error(&format!(
"Method Date.prototype.{method} called on incompatible receiver {}",
no_side_effects_string(recv)
)));
}
}
if matches!(ctor, "TypedArray" | "Uint8Array")
&& !matches!(
crate::stdlib::native_tag(recv).as_deref(),
Some("TypedArray") | Some("Buffer")
)
{
const BRANDED: &[&str] = &[
"slice",
"subarray",
"join",
"sort",
"at",
"toReversed",
"toSorted",
"toLocaleString",
];
if BRANDED.contains(&method) {
return Err(host::type_error(&format!(
"Method %TypedArray%.prototype.{method} called on incompatible receiver {}",
no_side_effects_string(recv)
)));
}
if crate::stdlib::typedarray::UINT8_PROTOTYPE_METHODS.contains(&method) {
return Err(host::type_error(&format!(
"Method Uint8Array.prototype.{method} called on incompatible receiver {}",
no_side_effects_string(recv)
)));
}
if method != "toString" {
return Err(host::type_error("this is not a typed array."));
}
}
if ctor == "Function"
&& matches!(method, "call" | "apply" | "bind" | "toString")
&& with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy)
{
let mut rest = args.into_iter();
let this_arg = rest.next().unwrap_or(Value::Undef);
match method {
"call" => return host::invoke(recv, rest.collect(), Some(this_arg)),
"apply" => {
let list = match rest.next() {
None | Some(Value::Undef) => Vec::new(),
Some(v) if with_host(|h| h.is_null(&v)) => Vec::new(),
Some(v) => create_list_from_array_like(&v)?,
};
return host::invoke(recv, list, Some(this_arg));
}
"bind" => {
let target = recv.clone();
let pre: Vec<Value> = rest.collect();
return Ok(with_host(|h| {
h.alloc(JsObj::BoundFunc {
target,
this: this_arg,
args: pre,
})
}));
}
_ => return Ok(with_host(|h| h.new_str("function () { [native code] }"))),
}
}
if ctor == "Symbol" && with_host(|h| h.kind_of(recv)) != Some(ObjKind::Symbol) {
let named = match method.strip_prefix("@@") {
Some(sym) => format!("Symbol.prototype [ @@{sym} ]"),
None => format!("Symbol.prototype.{method}"),
};
return Err(host::type_error(&format!(
"{named} requires that 'this' be a Symbol"
)));
}
if let Some(label) = branded_method_label(ctor, recv) {
return Err(host::type_error(&format!(
"Method {label}.prototype.{method} called on incompatible receiver {}",
no_side_effects_string(recv)
)));
}
host::call_method(recv, method, args)
}
fn branded_method_label(ctor: &str, recv: &Value) -> Option<&'static str> {
let kind = with_host(|h| h.kind_of(recv));
let weak = peek(recv, |o| match o {
JsObj::Set { weak, .. } | JsObj::Map { weak, .. } => Some(*weak),
_ => None,
})
.unwrap_or(false);
let ok = match ctor {
"Set" => kind == Some(ObjKind::Set) && !weak,
"WeakSet" => kind == Some(ObjKind::Set) && weak,
"Map" => kind == Some(ObjKind::Map) && !weak,
"WeakMap" => kind == Some(ObjKind::Map) && weak,
"Promise" => kind == Some(ObjKind::Promise),
_ => return None,
};
if ok {
return None;
}
Some(match ctor {
"Set" => "Set",
"WeakSet" => "WeakSet",
"Map" => "Map",
"WeakMap" => "WeakMap",
_ => "Promise",
})
}
fn no_side_effects_string(recv: &Value) -> String {
if with_host(|h| host::is_primitive(h, recv)) || with_host(|h| host::is_callable(h, recv)) {
return with_host(|h| h.str_of(recv));
}
if let Some(s) = with_host(|h| h.error_to_string(recv)) {
return s;
}
let native = crate::stdlib::native_tag(recv).is_some();
let brands_itself = with_host(|h| {
let overridden = host::lookup_chain(h, recv, "toString")
.map(|f| !matches!(h.get(&f), Some(JsObj::Builtin(n)) if n == "@proto:Object:toString"))
.unwrap_or(false);
native
|| overridden
|| h.has_null_proto(recv)
|| !matches!(
h.kind_of(recv),
Some(ObjKind::Object)
| Some(ObjKind::Map)
| Some(ObjKind::Set)
| Some(ObjKind::Promise)
)
});
if brands_itself {
return with_host(|h| object_tag(h, recv));
}
let ctor = get_property(recv, "constructor")
.ok()
.map(|c| with_host(|h| h.callable_name(&c)))
.filter(|n| !n.is_empty())
.unwrap_or_else(|| "Object".to_string());
format!("#<{ctor}>")
}
pub(crate) fn well_known_tag(h: &host::JsHost, v: &Value) -> Option<String> {
let tag = object_brand(h, v);
const NO_TAG: &[&str] = &[
"Undefined",
"Null",
"Boolean",
"Number",
"String",
"Array",
"Function",
"Object",
"Date",
"RegExp",
"Error",
];
if NO_TAG.contains(&tag.as_str()) {
return None;
}
Some(tag)
}
fn chain_tag_ctor(h: &host::JsHost, v: &Value) -> Option<String> {
let mut cur = h.proto_of(v);
for _ in 0..100 {
let p = cur?;
if h.is_null(&p) {
return None;
}
let name = match h.get(&p) {
Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
_ => h.intrinsic_proto_ctor(&p).map(str::to_string),
}
.or_else(|| {
h.class_owning_proto(&p)
.and_then(|c| h.class_builtin_ancestor(&c))
.map(|b| h.callable_name(&b))
.filter(|n| !n.is_empty())
});
if let Some(n) = name {
if intrinsic_proto_members(&format!("{n}.prototype"))
.is_some_and(|ms| ms.contains(&"@@toStringTag"))
{
return Some(n);
}
}
cur = h.proto_of(&p);
}
None
}
pub(crate) fn object_tag(h: &host::JsHost, v: &Value) -> String {
format!("[object {}]", object_brand(h, v))
}
pub fn is_arguments(v: &Value) -> bool {
with_host(|h| is_arguments_h(h, v))
}
pub fn is_arguments_h(h: &host::JsHost, v: &Value) -> bool {
h.fn_prop(v, "@@arguments").is_some()
}
fn object_brand(h: &host::JsHost, v: &Value) -> String {
if let Some(ctor) = h.intrinsic_proto_ctor(v) {
return if BRANDED_PROTOS.contains(&ctor) {
ctor.to_string()
} else {
"Object".to_string()
};
}
let tag: String = match v {
Value::Undef => "Undefined".into(),
Value::Bool(_) => "Boolean".into(),
Value::Int(_) | Value::Float(_) => "Number".into(),
Value::Str(_) => "String".into(),
Value::Obj(_) => match h.get(v) {
Some(JsObj::Null) => "Null".into(),
Some(JsObj::Str(_)) => "String".into(),
Some(JsObj::Array(_)) if is_arguments_h(h, v) => "Arguments".into(),
Some(JsObj::Array(_)) => "Array".into(),
Some(JsObj::Object(p))
if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("IteratorHelper") =>
{
"Iterator Helper".into()
}
Some(JsObj::Object(p)) if p.contains_key("@@domName") => "DOMException".into(),
Some(JsObj::Object(p)) if p.contains_key("@@primitive") => match p["@@primitive"] {
Value::Bool(_) => "Boolean".into(),
Value::Int(_) | Value::Float(_) => "Number".into(),
_ => "String".into(),
},
Some(JsObj::Proxy { target, .. }) => {
let mut cur = target;
for _ in 0..100 {
match h.get(cur) {
Some(JsObj::Proxy { target: t, .. }) => cur = t,
_ => break,
}
}
match h.get(cur) {
Some(JsObj::Array(_)) => "Array".into(),
_ => "Object".into(),
}
}
Some(JsObj::Func(f)) => match h.funcs.get(f.def_id) {
Some(d) if d.is_generator && d.is_async => "AsyncGeneratorFunction".into(),
Some(d) if d.is_generator => "GeneratorFunction".into(),
Some(d) if d.is_async => "AsyncFunction".into(),
_ => "Function".into(),
},
Some(JsObj::Builtin(n)) if matches!(n.as_str(), "Math" | "JSON" | "Reflect") => {
n.clone()
}
Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
match n.strip_suffix(".prototype") {
Some(ctor) if BRANDED_PROTOS.contains(&ctor) => ctor.to_string(),
_ => "Object".into(),
}
}
Some(JsObj::Class(_))
| Some(JsObj::Builtin(_))
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::BoundMethod { .. }) => "Function".into(),
Some(JsObj::Generator { .. }) if h.is_async_gen_val(v) => "AsyncGenerator".into(),
Some(JsObj::Generator { .. }) => "Generator".into(),
Some(JsObj::RegExp(_)) => "RegExp".into(),
Some(JsObj::Map { weak, .. }) => if *weak { "WeakMap" } else { "Map" }.into(),
Some(JsObj::Set { weak, .. }) => if *weak { "WeakSet" } else { "Set" }.into(),
Some(JsObj::Promise { .. }) => "Promise".into(),
Some(JsObj::Symbol { .. }) => "Symbol".into(),
Some(JsObj::BigInt(_)) => "BigInt".into(),
Some(JsObj::Object(p)) => match p.get("@@native").map(|t| h.str_of(t)).as_deref() {
Some("TypedArray") => p
.get("@@kind")
.map(|k| h.str_of(k))
.unwrap_or_else(|| "Uint8Array".into()),
Some("Buffer") => "Uint8Array".into(),
Some(
t @ ("ArrayBuffer"
| "DataView"
| "Date"
| "WeakRef"
| "FinalizationRegistry"
| "TextEncoder"
| "TextDecoder"
| "URL"
| "URLSearchParams"),
) => t.into(),
_ if has_error_data(h, v) => "Error".into(),
_ => "Object".into(),
},
_ => "Object".into(),
},
_ => "Object".into(),
};
if tag == "Object" && !has_error_data(h, v) {
if let Some(ctor) = chain_tag_ctor(h, v) {
return ctor;
}
}
tag
}
fn b_setattr(vm: &mut VM, _: u8) -> Value {
let val = vm.pop();
let name = sval(&vm.pop());
let recv = vm.pop();
if let Err(e) = set_property(&recv, &name, val.clone()) {
return abort(vm, e);
}
val
}
fn b_named_eval(vm: &mut VM, _: u8) -> Value {
let func = vm.pop();
let kind = vm.pop().to_int();
let key = vm.pop();
let key = sval(&key);
let base = match with_host(|h| h.symbol_of_key(&key)) {
Some(sym) => match with_host(|h| h.get(&sym).cloned()) {
Some(JsObj::Symbol {
desc: Some(desc), ..
}) => format!("[{desc}]"),
_ => String::new(),
},
None => key,
};
let name = match kind {
host::member::GET => format!("get {base}"),
host::member::SET => format!("set {base}"),
_ => base,
};
with_host(|h| {
let s = h.new_str(name);
h.set_fn_prop(&func, "name", s);
});
func
}
pub fn set_property_pub(recv: &Value, name: &str, val: Value) -> Result<(), String> {
set_property(recv, name, val)
}
pub fn own_prop_facts(obj: &Value, key: &str) -> Option<(Value, bool, bool, bool)> {
let k = with_host(|h| h.new_str(key.to_string()));
let d = own_descriptor_pub(obj, k).ok()?;
if matches!(d, Value::Undef) {
return None;
}
let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
let value = field("value");
let writable = field("writable");
let configurable = field("configurable");
let truthy = |v: &Value| with_host(|h| h.truthy(v));
let is_accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
Some((value, truthy(&writable), truthy(&configurable), is_accessor))
}
pub fn set_with_receiver(
target: &Value,
key: &str,
val: Value,
receiver: &Value,
) -> Result<bool, String> {
if crate::proxy::parts(target).is_some() {
return crate::proxy::set(target, key, &val, receiver);
}
if let Some((_, setter)) = with_host(|h| host::lookup_accessor(h, target, key)) {
return match setter {
Some(s) => {
host::invoke(&s, vec![val], Some(receiver.clone()))?;
Ok(true)
}
None => Ok(false),
};
}
if !with_host(|h| h.can_write_prop(target, key)) {
return Ok(false);
}
if !with_host(|h| is_object_like(h, receiver)) {
return Ok(false);
}
if let Some((_, writable, _, is_accessor)) = own_prop_facts(receiver, key) {
if is_accessor || !writable {
return Ok(false);
}
}
let desc = with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), val);
m.insert("writable".into(), Value::Bool(true));
m.insert("enumerable".into(), Value::Bool(true));
m.insert("configurable".into(), Value::Bool(true));
h.new_object(m)
});
if crate::proxy::parts(receiver).is_some() {
return crate::proxy::define_property(receiver, key, &desc);
}
let k = with_host(|h| h.new_str(key.to_string()));
define_property_pub(receiver, k, desc)?;
Ok(true)
}
fn is_primitive_arg(args: &[Value]) -> bool {
let v = arg0(args);
with_host(|h| host::is_primitive(h, &v))
}
fn write_refused(recv: &Value, name: &str) -> String {
let extensible = with_host(|h| h.is_extensible(recv));
let has_own = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.contains_key(name),
Some(JsObj::Array(items)) => {
name.parse::<usize>()
.map(|i| i < items.len())
.unwrap_or(false)
|| h.fn_prop(recv, name).is_some()
}
Some(JsObj::RegExp(_)) => name == "lastIndex" || h.fn_prop(recv, name).is_some(),
_ => h.fn_prop(recv, name).is_some(),
});
if !extensible && !has_own {
return host::type_error(&format!(
"Cannot add property {name}, object is not extensible"
));
}
host::type_error(&format!(
"Cannot assign to read only property '{name}' of object '{}'",
no_side_effects_string(recv)
))
}
fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
if with_host(|h| h.is_nullish(recv)) {
return Err(host::type_error(&format!(
"Cannot set properties of {} (setting '{name}')",
with_host(|h| h.str_of(recv))
)));
}
if with_host(|h| host::is_primitive(h, recv)) && with_host(|h| h.current_strict()) {
return Err(host::type_error(&format!(
"Cannot create property '{name}' on {} '{}'",
with_host(|h| h.type_of(recv)),
with_host(|h| h.str_of(recv))
)));
}
if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
return Err(private_brand_message(name, true));
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
if crate::proxy::set(recv, name, &val, recv)? {
return Ok(());
}
if with_host(|h| h.current_strict()) {
return Err(host::type_error(&format!(
"'set' on proxy: trap returned falsish for property '{name}'"
)));
}
return Ok(());
}
if with_host(|h| h.is_global_object(recv)) && !name.starts_with("@@") {
with_host(|h| h.set_name(name, val.clone()));
}
if name == "__proto__" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Object) {
if with_host(|h| h.has_null_proto(recv)) {
} else {
let assignable =
with_host(|h| h.is_null(&val) || matches!(h.kind_of(&val), Some(ObjKind::Object)));
if assignable {
if would_cycle(recv, &val) {
return Err(host::type_error("Cyclic __proto__ value"));
}
if !with_host(|h| h.is_extensible(recv)) && !same_prototype(recv, &val) {
return Err(host::type_error(&format!(
"{} is not extensible",
no_side_effects_string(recv)
)));
}
with_host(|h| h.set_proto(recv, val));
}
return Ok(());
}
}
if !name.starts_with("@@")
&& with_host(
|h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
)
{
let text = with_host(|h| h.str_of(&val));
std::env::set_var(name, &text);
let sv = with_host(|h| h.new_str(text));
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert(name.to_string(), sv);
}
});
return Ok(());
}
if name == "stack" {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.shift_remove("@@stackRaw");
}
});
}
if let Some((getter, setter)) = with_host(|h| host::lookup_accessor(h, recv, name)) {
if let Some(setter) = setter {
let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
return Ok(());
}
let _ = getter;
if with_host(|h| h.current_strict()) {
return Err(host::type_error(&format!(
"Cannot set property {name} of #<Object> which has only a getter"
)));
}
return Ok(());
}
if !with_host(|h| h.can_write_prop(recv, name)) {
if with_host(|h| h.current_strict()) {
return Err(write_refused(recv, name));
}
return Ok(());
}
if matches!(
with_host(|h| h.kind_of(recv)),
Some(ObjKind::Func) | Some(ObjKind::Class)
) {
with_host(|h| h.set_fn_prop(recv, name, val));
return Ok(());
}
if let Some(ns) = peek(recv, |o| match o {
JsObj::Builtin(ns) => Some(ns.clone()),
_ => None,
}) {
if ns == "process" && name == "exitCode" {
return crate::stdlib::process::set_exit_code(&val);
}
with_host(|h| h.set_builtin_static(&ns, name, val));
return Ok(());
}
if let Some(ns) = with_host(|h| {
h.intrinsic_proto_ctor(recv)
.map(str::to_string)
.or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
}) {
with_host(|h| h.set_builtin_static(&format!("{ns}.prototype"), name, val.clone()));
}
if name == "lastIndex" {
if let Some(n) = with_host(|h| match h.get(recv) {
Some(JsObj::RegExp(_)) => Some(h.to_number(&val)),
_ => None,
}) {
with_host(|h| {
if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
r.last_index = if n.is_finite() && n >= 0.0 {
crate::utf16::U16Index::new(n as usize)
} else {
crate::utf16::U16Index::ZERO
};
}
});
return Ok(());
}
}
if let Ok(i) = name.parse::<usize>() {
if is_arguments(recv) && i >= array_len(recv) {
with_host(|h| h.set_fn_prop(recv, name, val));
return Ok(());
}
}
if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
let is_ta = crate::stdlib::native_tag(recv).as_deref() == Some("TypedArray");
if is_ta && crate::stdlib::typedarray::elem_set(recv, name, &val)? {
return Ok(());
}
if is_ta && crate::stdlib::typedarray::view_detached(recv) {
return Ok(());
}
if crate::stdlib::buffer::byte_set(recv, name, &val) {
return Ok(());
}
}
if uses_side_table(recv) {
with_host(|h| h.set_fn_prop(recv, name, val));
return Ok(());
}
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array)
&& name != "length"
&& name.parse::<usize>().is_err()
{
with_host(|h| h.set_fn_prop(recv, name, val));
return Ok(());
}
let new_len = if name == "length" && with_host(|h| h.kind_of(recv)) == Some(ObjKind::Array) {
let want = host::to_array_length(&val)?;
let floor = with_host(|h| {
let old = match h.get(recv) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
};
let mut stop = want;
for i in (want..old).rev() {
if !h.prop_attrs(recv, &i.to_string()).configurable {
stop = i + 1;
break;
}
}
stop
});
Some(floor.max(want))
} else {
None
};
with_host(|h| match h.get_mut(recv) {
Some(JsObj::Object(props)) => {
let is_new = !props.contains_key(name);
props.insert(name.to_string(), val);
if is_new && host::array_index(name).is_some() {
host::canonicalize_own_keys(props);
}
}
Some(JsObj::Array(items)) => {
if let Some(n) = new_len {
let old = items.len();
items.resize(n, Value::Undef);
if n > old {
h.mark_hole_range(recv, old..n);
} else {
h.truncate_holes(recv, n);
}
} else if let Ok(i) = name.parse::<usize>() {
let old = items.len();
if i >= old {
items.resize(i + 1, Value::Undef);
}
items[i] = val;
if i > old {
h.mark_hole_range(recv, old..i);
}
h.clear_hole(recv, i);
}
}
_ => {}
});
Ok(())
}
fn b_getitem(vm: &mut VM, _: u8) -> Value {
let idx = vm.pop();
let recv = vm.pop();
let key = match host::to_property_key(&idx) {
Ok(k) => k,
Err(e) => return abort(vm, e),
};
match get_property(&recv, &key) {
Ok(v) => v,
Err(e) => abort(vm, e),
}
}
fn b_setitem(vm: &mut VM, _: u8) -> Value {
let val = vm.pop();
let idx = vm.pop();
let recv = vm.pop();
let key = match host::to_property_key(&idx) {
Ok(k) => k,
Err(e) => return abort(vm, e),
};
if let Err(e) = set_property(&recv, &key, val.clone()) {
return abort(vm, e);
}
val
}
pub fn delete_property(recv: &Value, key: &str) -> Result<bool, String> {
if with_host(|h| h.is_nullish(recv)) {
return Err(host::type_error(
"Cannot convert undefined or null to object",
));
}
if let Some(b) = crate::proxy::delete(recv, key)? {
return Ok(b);
}
if with_host(|h| h.is_global_object(recv)) && with_host(|h| h.remove_global(key)) {
return Ok(true);
}
if peek(recv, |o| match o {
JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
_ => None,
}) == Some(true)
{
return Ok(crate::module::cache_delete(key));
}
if !key.starts_with("@@")
&& with_host(
|h| matches!(h.get(recv), Some(JsObj::Object(p)) if p.contains_key("@@envObject")),
)
{
std::env::remove_var(key);
}
if let Some(ns) = with_host(|h| {
h.intrinsic_proto_ctor(recv)
.map(str::to_string)
.or_else(|| (h.object_proto() == *recv).then(|| "Object".to_string()))
}) {
with_host(|h| h.remove_builtin_static(&format!("{ns}.prototype"), key));
}
if let Some(ns) = peek(recv, |o| match o {
JsObj::Builtin(ns) => Some(ns.clone()),
_ => None,
}) {
if with_host(|h| h.remove_builtin_static(&ns, key)) {
return Ok(true);
}
if ns != REQUIRE_CACHE && !builtin_member_configurable(&ns, key) {
return Ok(false);
}
}
if !with_host(|h| h.prop_attrs(recv, key).configurable) {
return Ok(false);
}
if with_host(|h| h.own_accessor(recv, key).is_some()) {
with_host(|h| h.remove_accessor(recv, key));
return Ok(true);
}
with_host(|h| {
let index = key.parse::<usize>();
match h.get_mut(recv) {
Some(JsObj::Object(props)) => {
props.shift_remove(key);
return;
}
Some(JsObj::Array(items)) => {
if let Ok(i) = index {
if i < items.len() {
items[i] = Value::Undef;
h.mark_hole(recv, i);
}
return;
}
}
_ => {}
}
h.remove_fn_prop(recv, key);
});
Ok(true)
}
fn b_delitem(vm: &mut VM, _: u8) -> Value {
let strict = vm.pop();
let idx = vm.pop();
let recv = vm.pop();
let key = match host::to_property_key(&idx) {
Ok(k) => k,
Err(e) => return abort(vm, e),
};
match delete_property(&recv, &key) {
Ok(false) if with_host(|h| h.truthy(&strict)) => {
abort(vm, refused_delete_error(&recv, &key))
}
Ok(b) => Value::Bool(b),
Err(e) => abort(vm, e),
}
}
fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
let strict = vm.pop();
let name = sval(&vm.pop());
let recv = vm.pop();
match delete_property(&recv, &name) {
Ok(false) if with_host(|h| h.truthy(&strict)) => {
abort(vm, refused_delete_error(&recv, &name))
}
Ok(b) => Value::Bool(b),
Err(e) => abort(vm, e),
}
}
fn refused_delete_error(recv: &Value, key: &str) -> String {
if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
return host::type_error(&format!(
"'deleteProperty' on proxy: trap returned falsish for property '{key}'"
));
}
let shown = match peek(recv, |o| match o {
JsObj::Builtin(ns) => Some(ns.clone()),
_ => None,
}) {
Some(ns) if !host::builtin_is_callable(&ns) => "#<Object>".to_string(),
_ => no_side_effects_string(recv),
};
host::type_error(&format!("Cannot delete property '{key}' of {shown}"))
}
fn b_mkstr(vm: &mut VM, argc: u8) -> Value {
let parts = pop_n(vm, argc as usize);
let s: String = with_host(|h| parts.iter().map(|p| h.str_of(p)).collect());
with_host(|h| h.new_str(s))
}
fn b_mkarr(vm: &mut VM, argc: u8) -> Value {
let items = pop_n(vm, argc as usize);
with_host(|h| h.new_array(items))
}
fn b_mark_hole(vm: &mut VM, _: u8) -> Value {
let idx = vm.pop();
let arr = vm.pop();
let i = match idx {
Value::Int(i) if i >= 0 => i as usize,
_ => return Value::Undef,
};
with_host(|h| h.mark_hole(&arr, i));
Value::Undef
}
fn b_mkobj(vm: &mut VM, argc: u8) -> Value {
let flat = pop_n(vm, argc as usize);
let mut props: IndexMap<String, Value> = IndexMap::new();
let mut proto_override: Option<Value> = None;
let mut method_keys: Vec<String> = Vec::new();
let mut i = 0;
while i + 2 < flat.len() || (i + 2 == flat.len() && flat.len() % 3 == 0 && i < flat.len()) {
if i + 2 >= flat.len() {
break;
}
if matches!(flat[i], Value::Int(2)) {
let key = with_host(|h| h.str_of(&flat[i + 1]));
props
.entry(format!("{}{key}", host::ORD_MARKER))
.or_insert(Value::Undef);
i += 3;
continue;
}
if matches!(flat[i], Value::Int(3)) {
let key = with_host(|h| h.str_of(&flat[i + 1]));
method_keys.push(key.clone());
props.insert(key, flat[i + 2].clone());
i += 3;
continue;
}
let spread = matches!(flat[i], Value::Int(1));
if spread {
let src = flat[i + 1].clone();
if let Some(s) = with_host(|h| h.as_str(&src)) {
for idx in 0..crate::utf16::len(&s) {
if let Ok(ch) = get_property(&src, &idx.to_string()) {
props.insert(idx.to_string(), ch);
}
}
i += 3;
continue;
}
let entries = match host::own_enum_entries_deep(&src) {
Ok(e) => e,
Err(e) => return abort(vm, e),
};
for (k, v) in entries {
props.insert(k, v);
}
for (k, v) in with_host(|h| h.own_symbol_entries(&src)) {
props.insert(k, v);
}
} else {
let key = with_host(|h| h.str_of(&flat[i + 1]));
if key == "__proto__" {
proto_override = Some(flat[i + 2].clone());
} else {
props.insert(key, flat[i + 2].clone());
}
}
i += 3;
}
with_host(|h| {
let o = h.new_object(props);
if let Some(p) = proto_override {
if matches!(p, Value::Obj(_)) {
h.set_proto(&o, p);
}
}
for key in &method_keys {
let m = match h.get(&o) {
Some(JsObj::Object(p)) => p.get(key).cloned(),
_ => None,
};
if let Some(m) = m {
if let Some(JsObj::Func(f)) = h.get_mut(&m) {
f.home_object = Some(o.clone());
}
}
}
o
})
}
fn b_mkfunc(vm: &mut VM, _: u8) -> Value {
let def_id = match vm.pop() {
Value::Int(n) => n as usize,
Value::Float(f) => f as usize,
_ => return abort(vm, "internal: MKFUNC id".into()),
};
let (is_arrow, self_name) = with_host(|h| match h.funcs.get(def_id) {
Some(d) => (
d.is_arrow,
(d.self_name && !d.name.is_empty()).then(|| d.name.clone()),
),
None => (false, None),
});
with_host(|h| {
let mut env = h.current_env_capture();
let this = h.current_this();
let (home_class, home_static, home_object) = if is_arrow {
h.current_home()
} else {
(None, false, None)
};
if self_name.is_some() {
env = host::child_env(env);
}
let f = h.alloc(JsObj::Func(FuncVal {
def_id,
env: Some(env.clone()),
this,
is_arrow,
home_class,
home_static,
home_object,
}));
if let Some(n) = self_name {
env.borrow_mut().vars.insert(n, f.clone());
}
f
})
}
fn b_truthy(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
Value::Bool(with_host(|h| h.truthy(&v)))
}
fn b_nullish(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
Value::Bool(with_host(|h| h.is_nullish(&v)))
}
fn b_tostr(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
match host::to_string_value(&v) {
Ok(s) => s,
Err(e) => abort(vm, e),
}
}
fn b_typeof(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
with_host(|h| {
let t = h.type_of(&v);
h.new_str(t)
})
}
fn b_typeof_name(vm: &mut VM, _: u8) -> Value {
let name = sval(&vm.pop());
if with_host(|h| h.is_tdz_global(&name) && h.read_name(&name).is_none()) {
return abort(vm, host::tdz_error(&name));
}
if let Some(v) = with_host(|h| h.read_name(&name)) {
if with_host(|h| h.is_tdz(&v)) {
return abort(vm, host::tdz_error(&name));
}
}
if let Some(v) = with_host(|h| h.read_name(&name)) {
return with_host(|h| {
let t = h.type_of(&v);
h.new_str(t)
});
}
let t = match name.as_str() {
"undefined" => "undefined".to_string(),
"NaN" | "Infinity" => "number".to_string(),
"globalThis" | "global" => "object".to_string(),
n if is_namespace(n) || is_known_builtin(n) => {
let v = with_host(|h| h.alloc(JsObj::Builtin(name.clone())));
with_host(|h| h.type_of(&v)).to_string()
}
_ => "undefined".to_string(), };
with_host(|h| h.new_str(t))
}
fn b_strict_eq(vm: &mut VM, _: u8) -> Value {
let b = vm.pop();
let a = vm.pop();
Value::Bool(with_host(|h| h.strict_eq(&a, &b)))
}
fn b_loose_eq(vm: &mut VM, _: u8) -> Value {
let b = vm.pop();
let a = vm.pop();
let (a, b) = match with_host(|h| (host::is_primitive(h, &a), host::is_primitive(h, &b))) {
(false, true) if coerces_against_object(&b) => match host::to_primitive(&a, "default") {
Ok(p) => (p, b),
Err(e) => return abort(vm, e),
},
(true, false) if coerces_against_object(&a) => match host::to_primitive(&b, "default") {
Ok(p) => (a, p),
Err(e) => return abort(vm, e),
},
_ => (a, b),
};
Value::Bool(with_host(|h| h.loose_eq(&a, &b)))
}
fn b_instanceof(vm: &mut VM, _: u8) -> Value {
let ctor = vm.pop();
let obj = vm.pop();
match host::instance_of(&obj, &ctor) {
Ok(b) => Value::Bool(b),
Err(e) => abort(vm, e),
}
}
fn b_binop(vm: &mut VM, _: u8) -> Value {
let b = vm.pop();
let a = vm.pop();
let tag = match vm.pop() {
Value::Int(n) => n,
_ => 0,
};
let r = host::to_primitive(&a, "number")
.and_then(|a| host::to_primitive(&b, "number").map(|b| (a, b)))
.and_then(|(a, b)| with_host(|h| h.bitwise(tag, &a, &b)));
finish(vm, r)
}
fn b_unary(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
let tag = match vm.pop() {
Value::Int(n) => n,
_ => 0,
};
if with_host(|h| h.is_bigint_val(&v)) {
return match tag {
host::unop::POS => abort(
vm,
host::type_error("Cannot convert a BigInt value to a number"),
),
host::unop::BITNOT => {
let b = with_host(|h| h.as_bigint(&v)).unwrap();
let r = -(b + num_bigint::BigInt::from(1));
with_host(|h| h.new_bigint(r))
}
_ => Value::Undef,
};
}
let n = match host::to_number_value(&v) {
Ok(n) => n,
Err(e) => return abort(vm, e),
};
match tag {
host::unop::POS => Value::Float(n),
host::unop::BITNOT => {
let i = if n.is_finite() {
n.trunc() as i64 as i32
} else {
0
};
Value::Float(!i as f64)
}
_ => Value::Undef,
}
}
fn b_contains(vm: &mut VM, _: u8) -> Value {
let container = vm.pop();
let key = vm.pop();
if !matches!(container, Value::Obj(_)) || with_host(|h| host::is_primitive(h, &container)) {
let (k, c) = with_host(|h| (h.property_key(&key), h.str_of(&container)));
return abort(
vm,
host::type_error(&format!(
"Cannot use 'in' operator to search for '{k}' in {c}"
)),
);
}
let k = match host::to_property_key(&key) {
Ok(k) => k,
Err(e) => return abort(vm, e),
};
match has_property(&container, &k) {
Ok(b) => Value::Bool(b),
Err(e) => abort(vm, e),
}
}
fn b_sig_return(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
with_host(|h| h.signal = Some(host::Signal::Return(v.clone())));
vm.ip = vm.chunk.ops.len();
v
}
fn b_sig_break(vm: &mut VM, _: u8) -> Value {
let label = sval(&vm.pop());
let label = (!label.is_empty()).then_some(label);
with_host(|h| h.signal = Some(host::Signal::Break(label)));
vm.ip = vm.chunk.ops.len();
Value::Undef
}
fn b_sig_continue(vm: &mut VM, _: u8) -> Value {
let label = sval(&vm.pop());
let label = (!label.is_empty()).then_some(label);
with_host(|h| h.signal = Some(host::Signal::Continue(label)));
vm.ip = vm.chunk.ops.len();
Value::Undef
}
fn b_sig_unwind(vm: &mut VM, _: u8) -> Value {
let cont_tag = sval(&vm.pop());
let brk_tag = sval(&vm.pop());
let sig = match with_host(|h| h.signal.clone()) {
Some(s) => s,
None => return Value::Int(host::unwind::NONE),
};
let propagate = |vm: &mut VM| {
vm.ip = vm.chunk.ops.len();
Value::Int(host::unwind::NONE)
};
match &sig {
host::Signal::Return(_) => propagate(vm),
host::Signal::Break(label) => {
if brk_tag == host::unwind::NO_LOOP {
return propagate(vm);
}
let mine = match label {
None => true, Some(l) => brk_tag == *l,
};
if mine {
with_host(|h| h.signal = None);
}
Value::Int(host::unwind::BREAK)
}
host::Signal::Continue(label) => {
let mine = match label {
None => cont_tag != host::unwind::NO_LOOP,
Some(l) => cont_tag == *l,
};
if mine {
with_host(|h| h.signal = None);
return Value::Int(host::unwind::CONTINUE);
}
if brk_tag == host::unwind::NO_LOOP {
return propagate(vm);
}
Value::Int(host::unwind::BREAK)
}
}
}
fn b_throw(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
let msg = with_host(|h| {
h.exc = Some(v.clone());
error_display(h, &v)
});
abort(vm, msg)
}
fn error_display(h: &host::JsHost, v: &Value) -> String {
if let Some(JsObj::Object(props)) = h.get(v) {
let name = props
.get("name")
.map(|x| h.str_of(x))
.unwrap_or_else(|| "Error".into());
if let Some(m) = props.get("message") {
return format!("Uncaught {name}: {}", h.str_of(m));
}
}
format!("Uncaught {}", h.str_of(v))
}
fn b_try(vm: &mut VM, _: u8) -> Value {
let id = match vm.pop() {
Value::Int(n) => n as usize,
_ => return abort(vm, "internal: TRY id".into()),
};
let (has_handler, catch_bind, has_finalizer) = match with_host(|h| h.try_shape(id)) {
Some(t) => t,
None => return abort(vm, "internal: unknown try id".into()),
};
let mut pending: Option<String> = None;
let scope = with_host(|h| h.scope_snapshot());
with_host(|h| h.push_scope()); let body_res = host::run_chunk_keyed(host::try_key(id, 0), || {
with_host(|h| h.try_chunk(id, 0)).expect("try block exists")
});
with_host(|h| h.restore_scope(scope.clone()));
let signal_after = with_host(|h| h.signal.is_some());
if let Err(e) = body_res {
if signal_after {
pending = Some(e);
} else if has_handler {
let thrown =
with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
with_host(|h| {
h.error = None;
h.exc = None;
});
with_host(|h| h.push_scope());
if let Some(name) = &catch_bind {
with_host(|h| h.declare_name(name, thrown));
}
let hres = host::run_chunk_keyed(host::try_key(id, 1), || {
with_host(|h| h.try_chunk(id, 1)).expect("handler exists")
});
with_host(|h| h.restore_scope(scope.clone()));
if let Err(e2) = hres {
pending = Some(e2);
}
} else {
pending = Some(e);
}
}
if has_finalizer {
let sig_before = with_host(|h| h.signal.take());
with_host(|h| h.push_scope()); let fres = host::run_chunk_keyed(host::try_key(id, 2), || {
with_host(|h| h.try_chunk(id, 2)).expect("finalizer exists")
});
with_host(|h| h.restore_scope(scope.clone()));
match fres {
Ok(_) => {
if with_host(|h| h.signal.is_none()) {
with_host(|h| h.signal = sig_before);
} else {
pending = None;
with_host(|h| {
h.error = None;
h.exc = None;
});
}
}
Err(e) => pending = Some(e),
}
}
if let Some(e) = pending {
return abort(vm, e);
}
Value::Undef
}
pub(crate) fn synth_error(h: &mut host::JsHost, e: &str) -> Value {
h.ensure_error_protos();
if let Some(rest) = e.strip_prefix(host::DOM_MARK) {
if let Some((name, msg)) = rest.split_once('\u{1}') {
return dom_exception_with(h, name, msg);
}
}
let (head, rest) = match e.split_once(": ") {
Some((n, m)) => (n, m.to_string()),
None => ("", e.to_string()),
};
let (base, code) = match head.split_once(" [") {
Some((n, c)) if c.ends_with(']') => (n, Some(c[..c.len() - 1].to_string())),
_ => (head, None),
};
let (name, mut message) = if host::ERROR_NAMES.contains(&base) {
(base.to_string(), rest)
} else {
("Error".to_string(), e.to_string())
};
let mut code = code;
let mut bracketed = code.is_some();
let mut fields: Vec<(String, String)> = Vec::new();
if let Some(rest) = message.strip_prefix(host::CODE_MARK) {
if let Some((c, m)) = rest.split_once('\u{1}') {
code = Some(c.to_string());
bracketed = false;
let (m, fs) = host::split_error_fields(m);
fields = fs
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
message = m.to_string();
}
}
let mut props: IndexMap<String, Value> = IndexMap::new();
let mv = h.new_str(message.clone());
props.insert("message".into(), mv);
if let Some(c) = &code {
let cv = h.new_str(c.clone());
props.insert("code".into(), cv);
for (k, v) in fields {
let fv = h.new_str(v);
props.insert(k, fv);
}
if bracketed {
props.insert("@@nodeError".into(), Value::Bool(true));
}
}
let label = match (&code, bracketed) {
(Some(c), true) => format!("{name} [{c}]"),
_ => name.clone(),
};
let frames = h.stack_frames();
let stack = if message.is_empty() {
format!("{label}{frames}")
} else {
format!("{label}: {message}{frames}")
};
let sv = h.new_str(stack);
props.insert("stack".into(), sv);
for (k, v) in syscall_error_fields(&message) {
let sv = match v {
SysField::Str(s) => h.new_str(s),
SysField::Num(n) => Value::Float(n),
};
props.insert(k.into(), sv);
}
let obj = h.new_object(props);
if let Some(p) = host::error_proto_of(h, &name) {
h.set_proto(&obj, p);
}
h.hide_prop(&obj, "message");
h.hide_prop(&obj, "stack");
obj
}
enum SysField {
Str(String),
Num(f64),
}
fn syscall_error_fields(message: &str) -> Vec<(&'static str, SysField)> {
let (code, rest) = match message.split_once(": ") {
Some((c, r))
if c.len() >= 2
&& c.starts_with('E')
&& c.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) =>
{
(c, r)
}
_ => return Vec::new(),
};
let mut out: Vec<(&'static str, SysField)> = vec![
("errno", SysField::Num(errno_for(code))),
("code", SysField::Str(code.to_string())),
];
if let Some((_, tail)) = rest.split_once(", ") {
let (syscall, path) = match tail.split_once(" '") {
Some((s, p)) => (s, p.split_once('\'').map(|(first, _)| first)),
None => (tail, None),
};
out.push(("syscall", SysField::Str(syscall.to_string())));
if let Some(p) = path {
out.push(("path", SysField::Str(p.to_string())));
}
}
out
}
fn errno_for(code: &str) -> f64 {
let n: i32 = match code {
"ENOENT" => 2,
"EACCES" => 13,
"EEXIST" => 17,
"ENOTDIR" => 20,
"EISDIR" => 21,
"EINVAL" => 22,
"EPIPE" => 32,
"ENOTEMPTY" => 66,
_ => 5, };
-f64::from(n)
}
fn b_getiter(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
if with_host(|h| h.is_generator_val(&v)) {
return v;
}
if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
return match crate::proxy::iterate(&v) {
Ok(Some(items)) => with_host(|h| {
h.alloc(JsObj::Iter {
items,
idx: 0,
array: None,
})
}),
Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
Err(e) => abort(vm, e),
};
}
let direct = matches!(
with_host(|h| h.kind_of(&v)),
Some(ObjKind::Array) | Some(ObjKind::Str)
);
if !own_intrinsic_reachable(&v)
&& !matches!(
get_property(&v, "@@iterator"),
Ok(ref f) if with_host(|h| host::is_callable(h, f))
)
{
let shown = with_host(|h| h.inspect(&v));
let msg = host::type_error(&format!("{shown} is not iterable"));
return abort(vm, host::name_call_site(vm, &shown, msg));
}
if !direct {
if let Ok(iter_fn) = get_property(&v, "@@iterator") {
if with_host(|h| host::is_callable(h, &iter_fn)) {
return match host::invoke(&iter_fn, Vec::new(), Some(v.clone())) {
Ok(it) => it,
Err(e) => abort(vm, e),
};
}
}
}
if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Array) {
return array_iterator(&v, host::ArrayIterKind::Values);
}
match with_host(|h| h.iter_vec(&v)) {
Ok(items) => with_host(|h| {
h.alloc(JsObj::Iter {
items,
idx: 0,
array: None,
})
}),
Err(e) => {
let shown = with_host(|h| h.inspect(&v));
let named = host::name_call_site(vm, &shown, e);
abort(vm, named)
}
}
}
fn b_forin_keys(vm: &mut VM, _: u8) -> Value {
let v = vm.pop();
if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
return match crate::proxy::own_keys(&v) {
Ok(keys) => with_host(|h| {
let out: Vec<Value> = keys
.unwrap_or_default()
.into_iter()
.filter(|k| !host::is_symbol_key(k))
.map(|k| h.new_str(k))
.collect();
h.new_array(out)
}),
Err(e) => abort(vm, e),
};
}
let mut keys = with_host(|h| h.enum_keys(&v));
if !with_host(|h| h.has_null_proto(&v)) {
let seen: Vec<String> = keys.iter().map(|k| with_host(|h| h.str_of(k))).collect();
for ns in intrinsic_proto_namespaces(&v) {
for k in with_host(|h| h.builtin_static_keys(&ns)) {
if !seen.contains(&k) && !intrinsic_proto_member(&ns, &k) {
keys.push(with_host(|h| h.new_str(k)));
}
}
}
}
with_host(|h| h.new_array(keys))
}
fn intrinsic_proto_namespaces(v: &Value) -> Vec<String> {
let ctor = match wrapped_primitive(v).as_ref().and_then(wrapper_ctor_of) {
Some(c) => Some(c),
None if is_arguments(v) => Some("Object"),
None => with_host(|h| default_ctor_name(h, v)),
};
let mut out: Vec<String> = ctor
.filter(|c| *c != "Object")
.map(|c| format!("{c}.prototype"))
.into_iter()
.collect();
out.push("Object.prototype".to_string());
out
}
fn b_forin_alive(vm: &mut VM, _: u8) -> Value {
let key = vm.pop();
let obj = vm.pop();
let name = with_host(|h| h.str_of(&key));
if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
return match crate::proxy::own_enumerable(&obj, &name) {
Ok(b) => Value::Bool(b),
Err(e) => abort(vm, e),
};
}
if let Some(s) = with_host(|h| h.as_str(&obj)) {
let len = crate::utf16::len(&s);
return Value::Bool(name.parse::<usize>().is_ok_and(|i| i < len));
}
Value::Bool(has_property_ordinary(&obj, &name))
}
fn b_foriter(vm: &mut VM, _: u8) -> Value {
let it = match vm.stack.last() {
Some(v) => v.clone(),
None => return abort(vm, "internal: FORITER with empty stack".into()),
};
if let Some(step) = iter_step(&it) {
return match step {
Some(v) => {
vm.push(v);
Value::Bool(true)
}
None => Value::Bool(false),
};
}
if with_host(|h| h.is_generator_val(&it)) {
return match host::gen_resume(&it, Value::Undef) {
Ok(host::GenStep::Yield(v)) => {
vm.push(v);
Value::Bool(true)
}
Ok(host::GenStep::Done(_)) => Value::Bool(false),
Err(e) => abort(vm, e),
};
}
match host::call_method(&it, "next", Vec::new()) {
Ok(step) => {
let done = get_property(&step, "done")
.map(|d| with_host(|h| h.truthy(&d)))
.unwrap_or(true);
if done {
Value::Bool(false)
} else {
match get_property(&step, "value") {
Ok(v) => {
vm.push(v);
Value::Bool(true)
}
Err(e) => abort(vm, e),
}
}
}
Err(e) => abort(vm, e),
}
}
fn b_unpack(vm: &mut VM, _: u8) -> Value {
let star = match vm.pop() {
Value::Int(n) => n,
_ => -1,
};
let count = match vm.pop() {
Value::Int(n) => n as usize,
_ => 0,
};
let iterable = vm.pop();
let items = match if star < 0 {
host::iter_take(&iterable, count)
} else {
host::iter_all(&iterable)
} {
Ok(v) => v,
Err(e) => {
let msg = match host::call_site_text(vm) {
Some(text) => host::type_error(&format!("{text} is not iterable")),
None if e.ends_with(" is not iterable") => {
host::type_error(¬_iterable_typed(&iterable))
}
None => e,
};
return abort(vm, msg);
}
};
let ordered: Vec<Value> = if star < 0 {
(0..count)
.map(|i| items.get(i).cloned().unwrap_or(Value::Undef))
.collect()
} else {
let si = star as usize;
let after = count.saturating_sub(si + 1);
let rest_end = items.len().saturating_sub(after).max(si);
let mut out: Vec<Value> = Vec::with_capacity(count);
for i in 0..si {
out.push(items.get(i).cloned().unwrap_or(Value::Undef));
}
let rest: Vec<Value> = items
.get(si..rest_end)
.map(|s| s.to_vec())
.unwrap_or_default();
out.push(with_host(|h| h.new_array(rest)));
for j in 0..after {
out.push(items.get(rest_end + j).cloned().unwrap_or(Value::Undef));
}
out
};
if ordered.is_empty() {
return Value::Undef;
}
for it in ordered[1..].iter().rev().cloned() {
vm.push(it);
}
ordered[0].clone()
}
fn b_build_args(vm: &mut VM, argc: u8) -> Value {
let flat = pop_n(vm, argc as usize);
let mut out = Vec::new();
let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
let mut i = 0;
while i + 1 < flat.len() {
let val = flat[i + 1].clone();
match flat[i] {
Value::Int(1) => match host::iter_all(&val).map_err(|e| {
let shown = with_host(|h| h.inspect(&val));
host::name_call_site(vm, &shown, e)
}) {
Ok(items) => out.extend(items),
Err(e) => return abort(vm, e),
},
Value::Int(3) => match host::iter_all(&val) {
Ok(items) => out.extend(items),
Err(e) => {
let shown = with_host(|h| h.is_nullish(&val).then(|| h.str_of(&val)));
return abort(
vm,
match shown {
Some(s) => host::type_error(&format!(
"{s} is not iterable (cannot read property {s})"
)),
None if e.ends_with(" is not iterable") => host::type_error(
"Spread syntax requires ...iterable[Symbol.iterator] to be a function",
),
None => e,
},
);
}
},
Value::Int(2) => {
holes.insert(out.len());
out.push(Value::Undef);
}
_ => out.push(val),
}
i += 2;
}
with_host(|h| {
let arr = h.new_array(out);
h.install_holes(&arr, holes);
arr
})
}
fn b_call(vm: &mut VM, argc: u8) -> Value {
let mut args = pop_n(vm, argc as usize);
let name = sval(&args.remove(0));
let r = host::call_named(&name, args);
let r = r.map_err(|e| {
let shown = global_binding(&name)
.map(|v| with_host(|h| h.str_of(&v)))
.unwrap_or_default();
host::name_call_site(vm, &shown, e)
});
finish(vm, r)
}
fn call_key_of(v: &Value) -> String {
if let Value::Str(s) = v {
return (**s).clone();
}
with_host(|h| h.property_key(v))
}
fn index_element_call(recv: &Value, name: &str, args: &[Value]) -> Option<Result<Value, String>> {
if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let f = get_property(recv, name).ok()?;
with_host(|h| host::is_callable(h, &f))
.then(|| host::invoke(&f, args.to_vec(), Some(recv.clone())))
}
fn b_call_method(vm: &mut VM, argc: u8) -> Value {
let mut args = pop_n(vm, argc as usize);
let recv = args.remove(0);
let name = call_key_of(&args.remove(0));
if let Some(r) = index_element_call(&recv, &name, &args) {
return finish(vm, r);
}
let r = host::call_method(&recv, &name, args);
let r = r.map_err(|e| host::name_call_site(vm, &name, e));
finish(vm, r)
}
fn b_call_value(vm: &mut VM, argc: u8) -> Value {
let mut args = pop_n(vm, argc as usize);
let callable = args.remove(0);
let r = host::invoke(&callable, args, None);
let r = r.map_err(|e| {
let shown = with_host(|h| h.str_of(&callable));
host::name_call_site(vm, &shown, e)
});
finish(vm, r)
}
fn b_new_spread(vm: &mut VM, _: u8) -> Value {
let args_arr = vm.pop();
let ctor = vm.pop();
let args = host::iter_all(&args_arr).unwrap_or_default();
let r = host::construct(&ctor, args).map_err(|e| {
let shown = with_host(|h| h.str_of(&ctor));
host::name_call_site(vm, &shown, e)
});
finish(vm, r)
}
fn b_new(vm: &mut VM, argc: u8) -> Value {
let mut args = pop_n(vm, argc as usize);
let ctor = args.remove(0);
let r = host::construct(&ctor, args);
let r = r.map_err(|e| {
let shown = with_host(|h| h.str_of(&ctor));
host::name_call_site(vm, &shown, e)
});
finish(vm, r)
}
fn b_apply(vm: &mut VM, _: u8) -> Value {
let args_arr = vm.pop();
let callable = vm.pop();
let args = host::iter_all(&args_arr).unwrap_or_default();
let r = host::invoke(&callable, args, None);
finish(vm, r)
}
fn b_apply_method(vm: &mut VM, _: u8) -> Value {
let args_arr = vm.pop();
let name = call_key_of(&vm.pop());
let recv = vm.pop();
let args = host::iter_all(&args_arr).unwrap_or_default();
if let Some(r) = index_element_call(&recv, &name, &args) {
return finish(vm, r);
}
let r = host::call_method(&recv, &name, args);
finish(vm, r)
}
pub fn numeric_hook(op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
use NumOp::*;
let (a, b) = match op {
Eq | Ne => {
let (pa, pb) = with_host(|h| (host::is_primitive(h, a), host::is_primitive(h, b)));
match (pa, pb) {
(false, true) if coerces_against_object(b) => {
(host::to_primitive(a, "default")?, b.clone())
}
(true, false) if coerces_against_object(a) => {
(a.clone(), host::to_primitive(b, "default")?)
}
_ => (a.clone(), b.clone()),
}
}
Add => (
host::to_primitive(a, "default")?,
host::to_primitive(b, "default")?,
),
_ => (
host::to_primitive(a, "number")?,
host::to_primitive(b, "number")?,
),
};
reject_symbol_operand(op, &a, &b)?;
with_host(|h| h.arith(op, &a, &b))
}
fn reject_symbol_operand(op: NumOp, a: &Value, b: &Value) -> Result<(), String> {
use NumOp::*;
if matches!(op, Eq | Ne) {
return Ok(());
}
let (sym, concat) = with_host(|h| {
let is_sym = |v: &Value| matches!(h.get(v), Some(JsObj::Symbol { .. }));
let is_str =
|v: &Value| matches!(v, Value::Str(_)) || matches!(h.get(v), Some(JsObj::Str(_)));
(is_sym(a) || is_sym(b), is_str(a) || is_str(b))
});
if !sym {
return Ok(());
}
Err(host::type_error(if matches!(op, Add) && concat {
"Cannot convert a Symbol value to a string"
} else {
"Cannot convert a Symbol value to a number"
}))
}
fn coerces_against_object(v: &Value) -> bool {
match v {
Value::Undef => false,
Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_) => true,
_ => with_host(|h| !h.is_null(v)),
}
}
fn is_namespace(name: &str) -> bool {
matches!(
name,
"console"
| "Math"
| "JSON"
| "Object"
| "Array"
| "Number"
| "String"
| "Boolean"
| "Symbol"
| "Reflect"
| "Promise"
| "process"
| "Buffer"
| "URL"
| "URLSearchParams"
)
}
const GLOBAL_FUNCS: &[&str] = &[
"parseInt",
"parseFloat",
"isNaN",
"isFinite",
"encodeURIComponent",
"decodeURIComponent",
"encodeURI",
"decodeURI",
"escape",
"unescape",
"eval",
"String",
"Number",
"Boolean",
"Array",
"Object",
"Function",
"Symbol",
"Map",
"Set",
"WeakMap",
"WeakSet",
"Promise",
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
"DOMException",
"Iterator",
"BigInt",
"RegExp",
"Date",
"ArrayBuffer",
"DataView",
"Uint8Array",
"Int8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
"WeakRef",
"FinalizationRegistry",
"TextEncoder",
"TextDecoder",
"fetch",
"Headers",
"Request",
"Response",
"Blob",
"File",
"FormData",
"AbortController",
"AbortSignal",
"queueMicrotask",
"setTimeout",
"setInterval",
"setImmediate",
"clearTimeout",
"clearInterval",
"clearImmediate",
"structuredClone",
"btoa",
"atob",
"Proxy",
"require",
"__cjs_require",
"__cjs_resolve",
"__cjs_cache",
];
const NS_METHODS: &[&str] = &[
"console.log",
"console.error",
"console.warn",
"console.info",
"console.debug",
"Math.abs",
"Math.acos",
"Math.acosh",
"Math.asin",
"Math.asinh",
"Math.atan",
"Math.atanh",
"Math.atan2",
"Math.ceil",
"Math.cbrt",
"Math.expm1",
"Math.clz32",
"Math.cos",
"Math.cosh",
"Math.exp",
"Math.floor",
"Math.fround",
"Math.hypot",
"Math.imul",
"Math.log",
"Math.log1p",
"Math.log2",
"Math.log10",
"Math.max",
"Math.min",
"Math.pow",
"Math.random",
"Math.round",
"Math.sign",
"Math.sin",
"Math.sinh",
"Math.sqrt",
"Math.tan",
"Math.tanh",
"Math.trunc",
"JSON.stringify",
"JSON.parse",
"JSON.rawJSON",
"JSON.isRawJSON",
"Object.keys",
"Object.values",
"Object.entries",
"Object.assign",
"Object.freeze",
"Object.is",
"Object.fromEntries",
"Object.getPrototypeOf",
"Object.setPrototypeOf",
"Object.create",
"Object.getOwnPropertyNames",
"Object.getOwnPropertySymbols",
"Object.defineProperty",
"Object.getOwnPropertyDescriptor",
"Object.getOwnPropertyDescriptors",
"Object.defineProperties",
"Object.isFrozen",
"Object.isSealed",
"Object.seal",
"Object.preventExtensions",
"Object.isExtensible",
"Object.hasOwn",
"Object.groupBy",
"Array.isArray",
"Array.from",
"Array.fromAsync",
"Array.of",
"Number.isFinite",
"Number.isInteger",
"Number.isNaN",
"Number.isSafeInteger",
"Number.parseFloat",
"Number.parseInt",
"String.fromCharCode",
"String.fromCodePoint",
"String.raw",
"Symbol.for",
"Symbol.keyFor",
"BigInt.asIntN",
"BigInt.asUintN",
"Proxy.revocable",
"Reflect.defineProperty",
"Reflect.deleteProperty",
"Reflect.apply",
"Reflect.construct",
"Reflect.get",
"Reflect.getOwnPropertyDescriptor",
"Reflect.getPrototypeOf",
"Reflect.has",
"Reflect.isExtensible",
"Reflect.ownKeys",
"Reflect.preventExtensions",
"Reflect.set",
"Reflect.setPrototypeOf",
"Promise.resolve",
"Promise.reject",
"Promise.all",
"Promise.allSettled",
"Promise.race",
"Promise.any",
"Promise.withResolvers",
"Promise.try",
"RegExp.escape",
"Error.isError",
"Map.groupBy",
"Response.json",
"Response.error",
"Response.redirect",
"AbortSignal.abort",
"AbortSignal.timeout",
"process.nextTick",
"Error.captureStackTrace",
"require.resolve",
"require.resolve.paths",
"process.memoryUsage.rss",
];
pub fn builtin_meta(key: &str) -> Option<(&'static str, u32)> {
crate::arity::BUILTIN_ARITY
.binary_search_by(|(k, _, _)| (*k).cmp(key))
.ok()
.map(|i| {
let (_, name, len) = crate::arity::BUILTIN_ARITY[i];
(name, len)
})
}
pub fn builtin_name(key: &str) -> &str {
if let Some((name, _)) = builtin_meta(key) {
return name;
}
match key.strip_prefix("@proto:") {
Some(rest) => rest.rsplit(':').next().unwrap_or(rest),
None => key.rsplit('.').next().unwrap_or(key),
}
}
pub fn proto_getter_name(key: &str) -> Option<String> {
let (verb, rest) = match key.strip_prefix("@protoget:") {
Some(rest) => ("get", rest),
None => ("set", key.strip_prefix("@protoset:")?),
};
let (_, member) = rest.split_once(':')?;
Some(format!("{verb} {member}"))
}
pub fn is_known_builtin(name: &str) -> bool {
static SORTED: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
let sorted = SORTED.get_or_init(|| {
let mut v: Vec<&'static str> = GLOBAL_FUNCS
.iter()
.chain(NS_METHODS.iter())
.copied()
.collect();
v.sort_unstable();
v
});
sorted.binary_search(&name).is_ok() || is_namespace(name) || crate::stdlib::is_method(name)
}
pub fn dynamic_function(src: &str) -> Result<Value, String> {
let f = crate::eval_in_global_scope(&format!("({src})"))?;
with_host(|h| {
let s = h.new_str(src.to_string());
h.set_fn_prop(&f, "@@source", s);
});
Ok(f)
}
pub fn function_ctor(args: &[Value]) -> Result<Value, String> {
let parts: Vec<String> = args.iter().map(|a| with_host(|h| h.str_of(a))).collect();
let (params, body) = match parts.split_last() {
Some((body, params)) => (params.join(","), body.clone()),
None => (String::new(), String::new()),
};
dynamic_function(&format!("function anonymous({params}\n) {{\n{body}\n}}"))
}
pub fn eval_source(arg: Option<&Value>, direct: bool) -> Result<Value, String> {
let v = arg.cloned().unwrap_or(Value::Undef);
let is_string =
matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_))));
if !is_string {
return Ok(v);
}
let src = with_host(|h| h.str_of(&v));
let caller_strict = direct && with_host(|h| h.current_strict());
let chunk = crate::load_merged(crate::compile_completion_strict(&src, caller_strict)?);
if !direct {
return host::run_chunk_in_global_scope(chunk);
}
let strict = caller_strict
|| src.trim_start().starts_with("'use strict'")
|| src.trim_start().starts_with("\"use strict\"");
if !strict {
with_host(|h| h.push_scope());
let out = host::run_chunk_on(chunk);
with_host(|h| h.pop_scope());
return out;
}
let prev = with_host(|h| h.push_var_scope());
let out = host::run_chunk_on(chunk);
with_host(|h| h.pop_var_scope(prev));
out
}
pub fn call_builtin_function(name: &str, args: Vec<Value>) -> Result<Value, String> {
if name == "require" {
let spec = with_host(|h| h.str_of(&arg0(&args)));
return crate::module::require(&spec, &crate::module::entry_dir());
}
if name == "__cjs_require" {
let spec = with_host(|h| h.str_of(&arg0(&args)));
let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
return crate::module::require(&spec, std::path::Path::new(&from));
}
if name == "process.memoryUsage.rss" {
return Ok(crate::stdlib::process::memory_usage_rss());
}
if name == "require.resolve.paths" {
let spec = with_host(|h| h.str_of(&arg0(&args)));
if crate::stdlib::is_core(&spec) {
return Ok(with_host(|h| h.null()));
}
let dirs = crate::module::resolve_paths(&spec, &crate::module::entry_dir());
return Ok(with_host(|h| {
let items: Vec<Value> = dirs.into_iter().map(|d| h.new_str(d)).collect();
h.new_array(items)
}));
}
if let Some(ext) = name.strip_prefix("@@extension:") {
let _ = ext;
return Ok(Value::Undef);
}
if name == "require.resolve" {
let spec = with_host(|h| h.str_of(&arg0(&args)));
if crate::stdlib::is_core(&spec) {
return Ok(with_host(|h| h.new_str(spec)));
}
return match crate::module::resolve(&spec, &crate::module::entry_dir()) {
Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
None => Err(crate::host::plain_coded_error(
"Error",
"MODULE_NOT_FOUND",
&format!("Cannot find module '{spec}'"),
)),
};
}
if name == "__cjs_resolve" {
let spec = with_host(|h| h.str_of(&arg0(&args)));
let from = with_host(|h| h.str_of(args.get(1).unwrap_or(&Value::Undef)));
if crate::stdlib::is_core(&spec) {
return Ok(with_host(|h| h.new_str(spec)));
}
return match crate::module::resolve(&spec, std::path::Path::new(&from)) {
Some(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().to_string()))),
None => Err(crate::host::plain_coded_error(
"Error",
"MODULE_NOT_FOUND",
&format!("Cannot find module '{spec}'"),
)),
};
}
if name == "Error.captureStackTrace" {
let target = arg0(&args);
let prep = with_host(|h| h.builtin_static("Error", "prepareStackTrace"));
let stack = match prep {
Some(f)
if matches!(
with_host(|h| h.get(&f).cloned()),
Some(JsObj::Func(_)) | Some(JsObj::Builtin(_)) | Some(JsObj::BoundFunc { .. })
) =>
{
let sites = crate::module::callsite_stack(10)?;
host::invoke(&f, vec![target.clone(), sites], None)?
}
_ => with_host(|h| h.new_str("")),
};
let _ = set_property(&target, "stack", stack);
return Ok(Value::Undef);
}
if let Some(r) = crate::stdlib::call(name, &args) {
return r;
}
match name {
DEFAULT_PREPARE => {
let err = arg0(&args);
let header = with_host(|h| {
let name = host::lookup_chain(h, &err, "name")
.map(|v| h.str_of(&v))
.unwrap_or_else(|| "Error".to_string());
let msg = host::lookup_chain(h, &err, "message")
.map(|v| h.str_of(&v))
.unwrap_or_default();
if msg.is_empty() {
name
} else {
format!("{name}: {msg}")
}
});
let sites = args.get(1).cloned().unwrap_or(Value::Undef);
let lines = with_host(|h| match h.get(&sites) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
});
let mut out = header;
for s in lines {
let rendered = host::to_string_value(&s)
.map(|v| with_host(|h| h.str_of(&v)))
.unwrap_or_default();
out.push_str("\n at ");
out.push_str(&rendered);
}
Ok(with_host(|h| h.new_str(out)))
}
"console.log" | "console.info" | "console.debug" => {
print_line(&args, false)?;
Ok(Value::Undef)
}
"console.error" | "console.warn" => {
print_line(&args, true)?;
Ok(Value::Undef)
}
"parseInt" | "Number.parseInt" => Ok(Value::Float(parse_int(&args)?)),
"parseFloat" | "Number.parseFloat" => Ok(Value::Float(parse_float(&args)?)),
"isNaN" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_nan())),
"isFinite" => Ok(Value::Bool(to_number_arg(&args, 0)?.is_finite())),
"encodeURIComponent" => uri_encode(&arg_to_string(&args, 0)?, false),
"encodeURI" => uri_encode(&arg_to_string(&args, 0)?, true),
"decodeURIComponent" => uri_decode(&arg_to_string(&args, 0)?, false),
"decodeURI" => uri_decode(&arg_to_string(&args, 0)?, true),
"escape" => legacy_escape(&with_host(|h| h.str_of(&arg0(&args)))),
"unescape" => legacy_unescape(&with_host(|h| h.str_of(&arg0(&args)))),
"eval" => eval_source(args.first(), false),
"Function" => function_ctor(&args),
"Buffer" => {
crate::stdlib::process::emit_deprecation_warning(
"DEP0005",
"Buffer() is deprecated due to security and usability issues. \
Please use the Buffer.alloc(), Buffer.allocUnsafe(), or \
Buffer.from() methods instead.",
);
crate::stdlib::construct("Buffer", &args)
.unwrap_or_else(|| Err(host::type_error("Buffer is not a function")))
}
"Number.isInteger" => Ok(Value::Bool(is_integer(arg0(&args)))),
"Number.isSafeInteger" => Ok(Value::Bool(is_safe_integer(arg0(&args)))),
"Number.isNaN" => Ok(Value::Bool(
matches!(arg0(&args), Value::Float(f) if f.is_nan()),
)),
"Number.isFinite" => Ok(Value::Bool(
matches!(arg0(&args), Value::Float(f) if f.is_finite())
|| matches!(arg0(&args), Value::Int(_)),
)),
"String" => {
if args.is_empty() {
Ok(with_host(|h| h.new_str("")))
} else {
host::string_ctor_value(&args[0])
}
}
"Number" => Ok(Value::Float(if args.is_empty() {
0.0
} else {
let prim = host::to_primitive(&args[0], "number")?;
match with_host(|h| h.as_bigint(&prim)) {
Some(b) => host::bigint_to_f64(&b),
None => host::to_number_value(&prim)?,
}
})),
"BigInt" => bigint_ctor(&arg0(&args)),
"RegExp" => regexp_ctor(&args),
"BigInt.asIntN" | "BigInt.asUintN" => bigint_as_n(name.ends_with("asUintN"), &args),
"Boolean" => Ok(Value::Bool(with_host(|h| h.truthy(&arg0(&args))))),
"String.fromCharCode" => Ok(with_host(|h| {
let units: Vec<u16> = args
.iter()
.map(|a| crate::utf16::to_uint16(h.to_number(a)))
.collect();
let s = crate::utf16::to_string_lossy(&units);
h.new_str(s)
})),
"String.fromCodePoint" => {
let mut s = String::new();
for a in &args {
let n = with_host(|h| h.to_number(a));
let cp = if n.is_finite() && n.trunc() == n && (0.0..=0x10FFFF as f64).contains(&n)
{
char::from_u32(n as u32)
} else {
None
};
match cp {
Some(c) => s.push(c),
None => {
return Err(format!(
"RangeError: Invalid code point {}",
with_host(|h| h.str_of(a))
))
}
}
}
Ok(new_s(s))
}
"String.raw" => string_raw(&args),
"Array" => construct_builtin("Array", args),
"Array.of" => construct_array_like(host::current_static_this(), args),
"Array.isArray" => {
let v = arg0(&args);
let subject = crate::proxy::ultimate_target(&v).unwrap_or(v);
Ok(Value::Bool(
matches!(
with_host(|h| h.get(&subject).cloned()),
Some(JsObj::Array(_))
) && !is_arguments(&subject),
))
}
"Array.from" => array_from(args),
"Array.fromAsync" => array_from_async(args),
"Object" => Ok(object_call(args)),
"Object.keys" => object_keys(args, 0),
"Object.values" => object_keys(args, 1),
"Object.entries" => object_keys(args, 2),
"Object.assign" => object_assign(args),
"Object.freeze" => {
let v = arg0(&args);
reject_sealing_a_view(&v, "freeze")?;
if seal_proxy(&v, true)? {
return Ok(v);
}
with_host(|h| h.seal_object(&v, true));
Ok(v)
}
"Object.seal" => {
let v = arg0(&args);
reject_sealing_a_view(&v, "seal")?;
if seal_proxy(&v, false)? {
return Ok(v);
}
with_host(|h| h.seal_object(&v, false));
Ok(v)
}
"Object.preventExtensions" => {
let v = arg0(&args);
if crate::proxy::prevent_extensions(&v)? {
return Ok(v);
}
with_host(|h| h.prevent_extensions(&v));
Ok(v)
}
"Object.isFrozen" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
"Object.isSealed" if is_primitive_arg(&args) => Ok(Value::Bool(true)),
"Object.isExtensible" if is_primitive_arg(&args) => Ok(Value::Bool(false)),
"Object.isFrozen" => integrity_level(&arg0(&args), true),
"Object.isSealed" => integrity_level(&arg0(&args), false),
"Object.isExtensible" => {
let v = arg0(&args);
match crate::proxy::is_extensible(&v)? {
Some(b) => Ok(Value::Bool(b)),
None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
}
}
"Object.is" => {
let a = arg0(&args);
let b = args.get(1).cloned().unwrap_or(Value::Undef);
let num = |v: &Value| match v {
Value::Int(n) => Some(*n as f64),
Value::Float(f) => Some(*f),
_ => None,
};
let r = match (num(&a), num(&b)) {
(Some(x), Some(y)) => {
if x.is_nan() && y.is_nan() {
true
} else if x == 0.0 && y == 0.0 {
x.is_sign_negative() == y.is_sign_negative()
} else {
x == y
}
}
_ => with_host(|h| h.strict_eq(&a, &b)),
};
Ok(Value::Bool(r))
}
"Object.fromEntries" => object_from_entries(args),
"Object.getPrototypeOf" | "Reflect.getPrototypeOf" => {
if name == "Reflect.getPrototypeOf" {
reflect_require_object(&arg0(&args), "getPrototypeOf")?;
}
let v = arg0(&args);
match crate::proxy::get_prototype_of(&v)? {
Some(p) => Ok(p),
None => Ok(prototype_of(&v)),
}
}
"Object.setPrototypeOf" => {
let obj = arg0(&args);
let proto = args.get(1).cloned().unwrap_or(Value::Undef);
if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
reject_bad_prototype(&proto)?;
crate::proxy::set_prototype_of(&obj, &proto)?;
return Ok(obj);
}
if with_host(|h| matches!(obj, Value::Undef) || h.is_null(&obj)) {
return Err(host::type_error(
"Object.setPrototypeOf called on null or undefined",
));
}
reject_bad_prototype(&proto)?;
if with_host(|h| is_object_like(h, &obj)) {
if would_cycle(&obj, &proto) {
return Err(host::type_error("Cyclic __proto__ value"));
}
if !same_prototype(&obj, &proto) && !with_host(|h| h.is_extensible(&obj)) {
return Err(host::type_error(&format!(
"{} is not extensible",
no_side_effects_string(&obj)
)));
}
with_host(|h| h.set_proto(&obj, proto));
}
Ok(obj)
}
"Object.create" => object_create(args),
"Object.getOwnPropertyNames" => object_keys(args, 3),
"Object.getOwnPropertySymbols" => {
let v = arg0(&args);
require_object_coercible(&v)?;
let syms = proxy_or_own_symbol_keys(&v)?;
Ok(with_host(|h| h.new_array(syms)))
}
"Object.hasOwn" => {
let obj = arg0(&args);
let key = args.get(1).cloned().unwrap_or(Value::Undef);
object_builtin_method(&obj, "hasOwnProperty", vec![key])
}
"Object.defineProperty" => object_define_property(args),
"Object.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
"Object.getOwnPropertyDescriptors" => object_get_own_descriptors(args),
"Object.defineProperties" => object_define_properties(args),
"Object.groupBy" => object_group_by(args),
"Symbol" => Ok(with_host(|h| {
let desc = args
.first()
.filter(|a| !matches!(a, Value::Undef))
.map(|a| h.str_of(a));
h.new_symbol(desc)
})),
"Symbol.for" => Ok(with_host(|h| {
let key = h.str_of(&arg0(&args));
h.symbol_for(&key)
})),
"Symbol.keyFor" => Ok(with_host(|h| h.symbol_registry_key(&arg0(&args)))),
"Map" | "WeakMap" | "Set" | "WeakSet" | "Promise" => construct_builtin(name, args),
"Proxy" => Err(host::type_error("Constructor Proxy requires 'new'")),
"Proxy.revocable" => crate::proxy::revocable(&args),
"Reflect.ownKeys" => {
let v = arg0(&args);
reflect_require_object(&v, "ownKeys")?;
let names = object_keys(args, 3)?;
let syms = proxy_or_own_symbol_keys(&v)?;
if syms.is_empty() {
return Ok(names);
}
let mut all = with_host(|h| h.iter_vec(&names)).unwrap_or_default();
all.extend(syms);
Ok(with_host(|h| h.new_array(all)))
}
"Reflect.getOwnPropertyDescriptor" => object_get_own_descriptor(args),
"Reflect.defineProperty" => {
reflect_require_object(&arg0(&args), "defineProperty")?;
Ok(Value::Bool(object_define_property(args).is_ok()))
}
"Reflect.deleteProperty" => {
let obj = arg0(&args);
reflect_require_object(&obj, "deleteProperty")?;
let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
Ok(Value::Bool(delete_property(&obj, &k)?))
}
"Reflect.setPrototypeOf" => {
let obj = arg0(&args);
let p = args.get(1).cloned().unwrap_or(Value::Undef);
if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
crate::proxy::set_prototype_of(&obj, &p)?;
return Ok(Value::Bool(true));
}
if would_cycle(&obj, &p) {
return Ok(Value::Bool(false));
}
if !with_host(|h| h.is_extensible(&obj)) {
return Ok(Value::Bool(same_prototype(&obj, &p)));
}
with_host(|h| h.set_proto(&obj, p));
Ok(Value::Bool(true))
}
"Reflect.isExtensible" => {
let v = arg0(&args);
match crate::proxy::is_extensible(&v)? {
Some(b) => Ok(Value::Bool(b)),
None => Ok(Value::Bool(with_host(|h| h.is_extensible(&v)))),
}
}
"Reflect.preventExtensions" => {
let v = arg0(&args);
if crate::proxy::prevent_extensions(&v)? {
return Ok(Value::Bool(true));
}
with_host(|h| h.prevent_extensions(&v));
Ok(Value::Bool(true))
}
"Reflect.apply" => {
let f = arg0(&args);
let this = args.get(1).cloned();
let list = create_list_from_array_like(&args.get(2).cloned().unwrap_or(Value::Undef))?;
host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
}
"Reflect.construct" => {
let f = arg0(&args);
let list = create_list_from_array_like(&args.get(1).cloned().unwrap_or(Value::Undef))?;
let new_target = args.get(2).cloned().unwrap_or_else(|| f.clone());
host::construct_nt(&f, list, new_target)
}
"Reflect.has" => {
let obj = arg0(&args);
reflect_require_object(&obj, "has")?;
let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
Ok(Value::Bool(has_property(&obj, &k)?))
}
"Reflect.get" => {
let obj = arg0(&args);
reflect_require_object(&obj, "get")?;
let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
let receiver = args.get(2).cloned().unwrap_or_else(|| obj.clone());
get_property_recv(&obj, &k, &receiver)
}
"Reflect.set" => {
let obj = arg0(&args);
reflect_require_object(&obj, "set")?;
let k = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
let v = args.get(2).cloned().unwrap_or(Value::Undef);
let receiver = args.get(3).cloned().unwrap_or_else(|| obj.clone());
Ok(Value::Bool(set_with_receiver(&obj, &k, v, &receiver)?))
}
"JSON.stringify" => json_stringify(args),
"JSON.parse" => json_parse(args),
"JSON.rawJSON" => json_raw(args),
"JSON.isRawJSON" => json_is_raw(args),
"structuredClone" => structured_clone(args),
_ if name.starts_with("@@transformCb:") => {
let idx: u32 = name["@@transformCb:".len()..].parse().unwrap_or(0);
crate::stdlib::stream::transform_callback(&Value::Obj(idx), &args)?;
Ok(Value::Undef)
}
_ if name.starts_with("@@streamFlush:") => {
let idx: u32 = name["@@streamFlush:".len()..].parse().unwrap_or(0);
crate::stdlib::stream::flush_from(&Value::Obj(idx))?;
Ok(Value::Undef)
}
"btoa" | "atob" => crate::stdlib::buffer::module_call(name, &args)
.unwrap_or_else(|| Err(host::type_error(&format!("{name} is not a function")))),
"fetch" => crate::stdlib::fetch::fetch(&args),
_ if name.starts_with("@@aborttimeout:") => {
let idx: u32 = name["@@aborttimeout:".len()..].parse().unwrap_or(0);
crate::stdlib::fetch::fire_timeout_abort(idx)
}
"@@streamWriteCallback" => Ok(Value::Undef),
"queueMicrotask" | "process.nextTick" => {
let cb = arg0(&args);
require_callback(&cb)?;
let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
enqueue_microtask(name == "process.nextTick", cb, rest);
Ok(Value::Undef)
}
"setTimeout" | "setInterval" | "setImmediate" => {
require_callback(&arg0(&args))?;
Ok(schedule_timer(name, args))
}
"clearTimeout" | "clearInterval" | "clearImmediate" => {
clear_timer(&arg0(&args));
Ok(Value::Undef)
}
"Promise.resolve" => promise_resolve(arg0(&args)),
"Promise.reject" => promise_reject(arg0(&args)),
"Promise.all" => promise_all(args, AllMode::All),
"Promise.allSettled" => promise_all(args, AllMode::AllSettled),
"Promise.race" => promise_race(args, false),
"Promise.any" => promise_race(args, true),
"Promise.withResolvers" => promise_with_resolvers(),
"Promise.try" => promise_try(args),
"RegExp.escape" => regexp_escape(args),
"Error.isError" => error_is_error(args),
"Map.groupBy" => map_group_by(args),
n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
_ if name.starts_with("Math.") => math_fn(&name[5..], &args),
"@@pnoop" => Ok(Value::Undef),
_ if name.starts_with("@@presolve:") => {
let id: u32 = name[11..].parse().unwrap_or(0);
host::resolve_promise_val(id, arg0(&args));
Ok(Value::Undef)
}
_ if name.starts_with("@@preject:") => {
let id: u32 = name[10..].parse().unwrap_or(0);
host::reject_promise_val(id, arg0(&args));
Ok(Value::Undef)
}
_ if name.starts_with("@@prevoke:") => {
let i: u32 = name[10..].parse().unwrap_or(0);
Ok(crate::proxy::revoke(i))
}
_ if name.starts_with("@@finpass:") => {
let i: u32 = name["@@finpass:".len()..].parse().unwrap_or(0);
let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
Ok(finally_chain(result, arg0(&args), false))
}
_ if name.starts_with("@@finthrow:") => {
let i: u32 = name["@@finthrow:".len()..].parse().unwrap_or(0);
let result = host::invoke(&Value::Obj(i), Vec::new(), None)?;
Ok(finally_chain(result, arg0(&args), true))
}
_ if name.starts_with("@@finret:") => {
let i: u32 = name["@@finret:".len()..].parse().unwrap_or(0);
get_property(&Value::Obj(i), "0")
}
_ if name.starts_with("@@finrethrow:") => {
let i: u32 = name["@@finrethrow:".len()..].parse().unwrap_or(0);
let reason = get_property(&Value::Obj(i), "0")?;
with_host(|h| h.exc = Some(reason.clone()));
Err(with_host(|h| error_string(h, &reason)))
}
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
fn bigint_convert_error(v: &Value) -> String {
let shown = with_host(|h| h.str_of(v));
host::type_error(&format!("Cannot convert {shown} to a BigInt"))
}
pub fn to_bigint(v: &Value) -> Result<num_bigint::BigInt, String> {
let prim = host::to_primitive(v, "number")?;
if let Some(b) = with_host(|h| match h.get(&prim) {
Some(JsObj::BigInt(b)) => Some(b.clone()),
_ => None,
}) {
return Ok(b);
}
match &prim {
Value::Bool(b) => Ok(num_bigint::BigInt::from(*b as i64)),
Value::Str(s) => host::parse_bigint_str(s)
.ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt")),
_ if with_host(|h| matches!(h.get(&prim), Some(JsObj::Str(_)))) => {
let s = with_host(|h| h.str_of(&prim));
host::parse_bigint_str(&s)
.ok_or_else(|| format!("SyntaxError: Cannot convert {s} to a BigInt"))
}
_ => Err(bigint_convert_error(&prim)),
}
}
fn bigint_ctor(v: &Value) -> Result<Value, String> {
use num_bigint::BigInt;
let big = match v {
Value::Bool(b) => BigInt::from(*b as i64),
Value::Int(n) => BigInt::from(*n),
Value::Float(f) => {
if !f.is_finite() || f.fract() != 0.0 {
let disp = with_host(|h| h.str_of(v));
return Err(format!(
"RangeError: The number {disp} cannot be converted to a BigInt because it is not an integer"
));
}
match BigInt::parse_bytes(format!("{f:.0}").as_bytes(), 10) {
Some(b) => b,
None => return Err(bigint_convert_error(v)),
}
}
Value::Str(s) => match host::parse_bigint_str(s) {
Some(b) => b,
None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
},
Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
Some(JsObj::BigInt(b)) => b,
Some(JsObj::Str(s)) => match host::parse_bigint_str(&s) {
Some(b) => b,
None => return Err(format!("SyntaxError: Cannot convert {s} to a BigInt")),
},
_ => return Err(bigint_convert_error(v)),
},
_ => return Err(bigint_convert_error(v)),
};
Ok(with_host(|h| h.new_bigint(big)))
}
fn regexp_ctor(args: &[Value]) -> Result<Value, String> {
let (source, existing_flags) = match with_host(|h| h.get(&arg0(args)).cloned()) {
Some(JsObj::RegExp(r)) => (r.source.clone(), Some(r.flags.clone())),
_ => {
let a0 = arg0(args);
let src = if matches!(a0, Value::Undef) {
String::new()
} else {
arg_to_string(args, 0)?
};
(src, None)
}
};
let flags = match args.get(1) {
Some(v) if !matches!(v, Value::Undef) => arg_to_string(args, 1)?,
_ => existing_flags.unwrap_or_default(),
};
let src = if source.is_empty() {
"(?:)".to_string()
} else {
source
};
crate::regexp::build_regexp(&src, &flags)
}
fn bigint_as_n(unsigned: bool, args: &[Value]) -> Result<Value, String> {
use num_bigint::BigInt;
use num_traits::Signed;
let bits = with_host(|h| h.to_number(&arg0(args))) as i64;
if bits < 0 {
return Err("RangeError: Invalid value: not (convertible to) a safe integer".into());
}
let x = match with_host(|h| h.as_bigint(&args.get(1).cloned().unwrap_or(Value::Undef))) {
Some(b) => b,
None => return Err(host::type_error("Cannot convert to a BigInt")),
};
let bits = bits as u32;
if bits == 0 {
return Ok(with_host(|h| h.new_bigint(BigInt::from(0))));
}
let modulus = BigInt::from(1) << bits; let mut r = &x % &modulus;
if r.is_negative() {
r += &modulus;
}
if !unsigned {
let half = BigInt::from(1) << (bits - 1);
if r >= half {
r -= &modulus;
}
}
Ok(with_host(|h| h.new_bigint(r)))
}
fn string_raw(args: &[Value]) -> Result<Value, String> {
let call_site = arg0(args);
let raw = get_property(&call_site, "raw")?;
let raws = with_host(|h| h.iter_vec(&raw)).unwrap_or_default();
let mut out = String::new();
for (i, r) in raws.iter().enumerate() {
out.push_str(&with_host(|h| h.str_of(r)));
if i + 1 < raws.len() {
if let Some(sub) = args.get(i + 1) {
out.push_str(&with_host(|h| h.str_of(sub)));
}
}
}
Ok(with_host(|h| h.new_str(out)))
}
pub fn uses_side_table(v: &Value) -> bool {
matches!(
with_host(|h| h.kind_of(v)),
Some(
ObjKind::Map
| ObjKind::Set
| ObjKind::Promise
| ObjKind::RegExp
| ObjKind::Generator
| ObjKind::Symbol
| ObjKind::BigInt
| ObjKind::Iter
)
)
}
fn object_call(args: Vec<Value>) -> Value {
let a = arg0(&args);
if matches!(a, Value::Undef) || with_host(|h| h.is_null(&a)) {
return with_host(|h| h.new_object(IndexMap::new()));
}
to_object(&a)
}
fn wrapper_ctor_of(v: &Value) -> Option<&'static str> {
match v {
Value::Int(_) | Value::Float(_) => Some("Number"),
Value::Bool(_) => Some("Boolean"),
Value::Obj(_) => match with_host(|h| h.get(v).cloned()) {
Some(JsObj::Str(_)) => Some("String"),
Some(JsObj::Symbol { .. }) => Some("Symbol"),
Some(JsObj::BigInt(_)) => Some("BigInt"),
_ => None,
},
_ => None,
}
}
pub fn wrapped_primitive(v: &Value) -> Option<Value> {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get("@@primitive").cloned(),
_ => None,
})
}
pub fn to_object(v: &Value) -> Value {
let Some(ctor) = wrapper_ctor_of(v) else {
return v.clone();
};
with_host(|h| h.ensure_wrapper_protos());
let chars: Vec<String> = if ctor == "String" {
with_host(|h| h.str_of(v))
.chars()
.map(|c| c.to_string())
.collect()
} else {
Vec::new()
};
with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
for (i, c) in chars.iter().enumerate() {
let s = h.new_str(c.clone());
m.insert(i.to_string(), s);
}
let w = h.new_object(m);
if ctor == "String" {
for i in 0..chars.len() {
h.set_prop_attrs(
&w,
&i.to_string(),
host::PropAttrs {
writable: false,
enumerable: true,
configurable: false,
},
);
}
let len = Value::Float(chars.len() as f64);
if let Some(JsObj::Object(p)) = h.get_mut(&w) {
p.insert("length".into(), len);
}
h.set_prop_attrs(
&w,
"length",
host::PropAttrs {
writable: false,
enumerable: false,
configurable: false,
},
);
}
if let Some(JsObj::Object(p)) = h.get_mut(&w) {
p.insert("@@primitive".into(), v.clone());
}
if let Some(proto) = h.native_proto(ctor) {
h.set_proto(&w, proto);
}
w
})
}
pub fn construct_builtin(name: &str, args: Vec<Value>) -> Result<Value, String> {
if let Some(r) = crate::stdlib::construct(name, &args) {
return r;
}
match name {
"Array" => {
if args.len() == 1 {
if let Value::Float(_) | Value::Int(_) = args[0] {
let n = host::to_array_length(&args[0])?;
return Ok(with_host(|h| {
let a = h.new_array(vec![Value::Undef; n]);
h.mark_hole_range(&a, 0..n);
a
}));
}
}
Ok(with_host(|h| h.new_array(args)))
}
"Object" => Ok(object_call(args)),
"String" => Ok(to_object(&host::to_string_value(
&args
.first()
.cloned()
.unwrap_or_else(|| with_host(|h| h.new_str(String::new()))),
)?)),
"Number" => Ok(to_object(&Value::Float(match args.first() {
Some(a) => host::to_number_value(a)?,
None => 0.0,
}))),
"Boolean" => Ok(to_object(&Value::Bool(with_host(|h| {
h.truthy(&arg0(&args))
})))),
"Map" | "WeakMap" => {
let weak = name == "WeakMap";
let m = with_host(|h| {
h.alloc(JsObj::Map {
entries: indexmap::IndexMap::new(),
weak,
})
});
if let Some(init) = args
.first()
.filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
{
host::iter_for_each(init, |p, _| {
if !with_host(|h| is_object_like(h, &p)) {
let shown = with_host(|h| h.str_of(&p));
return Err(host::type_error(&format!(
"Iterator value {shown} is not an entry object"
)));
}
let k = get_property(&p, "0")?;
let v = get_property(&p, "1")?;
map_method(&m, "set", vec![k, v])?;
Ok(())
})?;
}
Ok(m)
}
"Set" | "WeakSet" => {
let weak = name == "WeakSet";
let s = with_host(|h| {
h.alloc(JsObj::Set {
entries: indexmap::IndexMap::new(),
weak,
})
});
if let Some(init) = args
.first()
.filter(|a| !matches!(a, Value::Undef) && !with_host(|h| h.is_null(a)))
{
host::iter_for_each(init, |v, _| {
set_method(&s, "add", vec![v])?;
Ok(())
})?;
}
Ok(s)
}
"Promise" => new_promise(arg0(&args)),
"Proxy" => crate::proxy::create(&args),
"Function" => function_ctor(&args),
"RegExp" => regexp_ctor(&args),
"BigInt" => Err(host::type_error("BigInt is not a constructor")),
"Error" => make_error_checked(name, &args),
"DOMException" => Ok(dom_exception(&args)),
n if host::ERROR_NAMES.contains(&n) => make_error_checked(name, &args),
_ => Err(host::type_error(&format!("{name} is not a constructor"))),
}
}
pub const DOM_EXCEPTION_CODES: &[(&str, f64)] = &[
("IndexSizeError", 1.0),
("DOMStringSizeError", 2.0),
("HierarchyRequestError", 3.0),
("WrongDocumentError", 4.0),
("InvalidCharacterError", 5.0),
("NoDataAllowedError", 6.0),
("NoModificationAllowedError", 7.0),
("NotFoundError", 8.0),
("NotSupportedError", 9.0),
("InUseAttributeError", 10.0),
("InvalidStateError", 11.0),
("SyntaxError", 12.0),
("InvalidModificationError", 13.0),
("NamespaceError", 14.0),
("InvalidAccessError", 15.0),
("ValidationError", 16.0),
("TypeMismatchError", 17.0),
("SecurityError", 18.0),
("NetworkError", 19.0),
("AbortError", 20.0),
("URLMismatchError", 21.0),
("QuotaExceededError", 22.0),
("TimeoutError", 23.0),
("InvalidNodeTypeError", 24.0),
("DataCloneError", 25.0),
];
fn legacy_code_name(error_name: &str) -> String {
let stem = error_name.strip_suffix("Error").unwrap_or(error_name);
let mut out = String::new();
for (i, c) in stem.chars().enumerate() {
if c.is_ascii_uppercase() && i > 0 {
out.push('_');
}
out.push(c.to_ascii_uppercase());
}
out.push_str("_ERR");
out
}
pub fn dom_exception(args: &[Value]) -> Value {
let message = match args.first() {
None | Some(Value::Undef) => String::new(),
Some(v) => with_host(|h| h.str_of(v)),
};
let name = match args.get(1) {
None | Some(Value::Undef) => "Error".to_string(),
Some(v) => with_host(|h| h.str_of(v)),
};
with_host(|h| dom_exception_with(h, &name, &message))
}
pub(crate) fn dom_exception_with(h: &mut host::JsHost, name: &str, message: &str) -> Value {
let name = name.to_string();
let message = message.to_string();
let code = DOM_EXCEPTION_CODES
.iter()
.find(|(n, _)| *n == name)
.map(|(_, c)| *c)
.unwrap_or(0.0);
let head = if message.is_empty() {
name.clone()
} else {
format!("{name}: {message}")
};
let e = synth_error(h, &head);
{
let nv = h.new_str(name);
let mv = h.new_str(message);
let sv = h.new_str(head);
if let Some(JsObj::Object(p)) = h.get_mut(&e) {
p.shift_remove("message");
p.insert("@@domName".into(), nv);
p.insert("@@domMessage".into(), mv);
p.insert("@@domCode".into(), Value::Float(code));
p.insert("stack".into(), sv);
}
h.ensure_error_protos();
if let Some(proto) = host::error_proto_of(h, "DOMException") {
h.set_proto(&e, proto);
}
}
e
}
pub fn dom_exception_slot(recv: &Value, name: &str) -> Option<Value> {
let slot = match name {
"name" => "@@domName",
"message" => "@@domMessage",
"code" => "@@domCode",
_ => return None,
};
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) if p.contains_key("@@domName") => p.get(slot).cloned(),
_ => None,
})
}
pub(crate) fn make_error_pub(name: &str, msg: &str) -> Value {
let m = with_host(|h| h.new_str(msg.to_string()));
make_error_inner(name, &[m])
}
fn make_error_checked(name: &str, args: &[Value]) -> Result<Value, String> {
if let Some(m) = args.first().filter(|m| !matches!(m, Value::Undef)) {
let idx = usize::from(name == "AggregateError");
if idx == 0 {
host::to_string_value(m)?;
} else if let Some(m2) = args.get(idx).filter(|m| !matches!(m, Value::Undef)) {
host::to_string_value(m2)?;
}
}
Ok(make_error_inner(name, args))
}
fn make_error_inner(name: &str, args: &[Value]) -> Value {
let agg = name == "AggregateError";
let (errors, args) = if agg {
(
Some(args.first().cloned().unwrap_or(Value::Undef)),
args.get(1..).unwrap_or(&[]),
)
} else {
(None, args)
};
with_host(|h| {
h.ensure_error_protos();
let mut props: IndexMap<String, Value> = IndexMap::new();
let msg = args
.first()
.filter(|a| !matches!(a, Value::Undef))
.map(|a| h.str_of(a));
if let Some(m) = &msg {
let mv = h.new_str(m.clone());
props.insert("message".into(), mv);
}
let frames = h.stack_frames();
let stack = match &msg {
Some(m) if !m.is_empty() => format!("{name}: {m}{frames}"),
_ => format!("{name}{frames}"),
};
let sv = h.new_str(stack);
props.insert("stack".into(), sv);
let raw = h.new_str(frames);
props.insert("@@stackRaw".into(), raw);
if let Some(errs) = errors {
let items = h.iter_vec(&errs).unwrap_or_default();
let arr = h.new_array(items);
props.insert("errors".into(), arr);
}
let opts = args.get(1);
if let Some(cause) = opts.and_then(|o| match h.get(o) {
Some(JsObj::Object(p)) => p.get("cause").cloned(),
_ => None,
}) {
props.insert("cause".into(), cause);
}
let e = h.new_object(props);
if let Some(p) = host::error_proto_of(h, name) {
h.set_proto(&e, p);
}
for k in ["message", "stack", "errors", "cause", "@@stackRaw"] {
h.hide_prop(&e, k);
}
e
})
}
fn print_line(args: &[Value], stderr: bool) -> Result<(), String> {
let line: String = crate::stdlib::util::format(args)?;
with_host(|h| h.write_out(&format!("{line}\n"), stderr));
Ok(())
}
fn arg0(args: &[Value]) -> Value {
args.first().cloned().unwrap_or(Value::Undef)
}
fn arg_to_string(args: &[Value], i: usize) -> Result<String, String> {
let v = args.get(i).cloned().unwrap_or(Value::Undef);
let sv = host::to_string_value(&v)?;
Ok(with_host(|h| h.str_of(&sv)))
}
fn arg_num(args: &[Value], i: usize) -> f64 {
with_host(|h| h.to_number(&args.get(i).cloned().unwrap_or(Value::Undef)))
}
fn is_integer(v: Value) -> bool {
match v {
Value::Int(_) => true,
Value::Float(f) => f.is_finite() && f.fract() == 0.0,
_ => false,
}
}
fn is_safe_integer(v: Value) -> bool {
match v {
Value::Float(f) => f.is_finite() && f.fract() == 0.0 && f.abs() <= 9007199254740991.0,
Value::Int(_) => true,
_ => false,
}
}
fn uri_encode(s: &str, uri: bool) -> Result<Value, String> {
const UNRESERVED: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()";
const RESERVED: &[u8] = b";,/?:@&=+$#";
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
if UNRESERVED.contains(&b) || (uri && RESERVED.contains(&b)) {
out.push(b as char);
} else {
out.push('%');
out.push(
char::from_digit((b >> 4) as u32, 16)
.unwrap()
.to_ascii_uppercase(),
);
out.push(
char::from_digit((b & 0xf) as u32, 16)
.unwrap()
.to_ascii_uppercase(),
);
}
}
Ok(with_host(|h| h.new_str(out)))
}
fn uri_decode(s: &str, uri: bool) -> Result<Value, String> {
const RESERVED: &[u8] = b";,/?:@&=+$#";
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len() {
return Err("URIError: URI malformed".into());
}
let hi = (bytes[i + 1] as char).to_digit(16);
let lo = (bytes[i + 2] as char).to_digit(16);
match (hi, lo) {
(Some(h), Some(l)) => {
let byte = (h * 16 + l) as u8;
if uri && RESERVED.contains(&byte) {
out.extend_from_slice(&bytes[i..i + 3]);
} else {
out.push(byte);
}
i += 3;
}
_ => return Err("URIError: URI malformed".into()),
}
} else {
out.push(bytes[i]);
i += 1;
}
}
match String::from_utf8(out) {
Ok(decoded) => Ok(with_host(|h| h.new_str(decoded))),
Err(_) => Err("URIError: URI malformed".into()),
}
}
fn legacy_escape(s: &str) -> Result<Value, String> {
const KEEP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";
let mut out = String::with_capacity(s.len());
for u in s.encode_utf16() {
if u < 0x100 {
if KEEP.contains(&(u as u8)) {
out.push(u as u8 as char);
} else {
out.push_str(&format!("%{u:02X}"));
}
} else {
out.push_str(&format!("%u{u:04X}"));
}
}
Ok(with_host(|h| h.new_str(out)))
}
fn legacy_unescape(s: &str) -> Result<Value, String> {
let b = s.as_bytes();
let hex = |i: usize, n: usize| -> Option<u16> {
if i + n > b.len() {
return None;
}
let mut v: u16 = 0;
for &c in &b[i..i + n] {
v = v.checked_mul(16)? + (c as char).to_digit(16)? as u16;
}
Some(v)
};
let units: Vec<u16> = s.encode_utf16().collect();
let mut out: Vec<u16> = Vec::with_capacity(units.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'%' {
if let Some(u) = hex(i + 1, 2) {
out.push(u);
i += 3;
continue;
}
if b.get(i + 1) == Some(&b'u') {
if let Some(u) = hex(i + 2, 4) {
out.push(u);
i += 6;
continue;
}
}
}
let c = s[i..].chars().next().unwrap_or('%');
let mut buf = [0u16; 2];
out.extend_from_slice(c.encode_utf16(&mut buf));
i += c.len_utf8();
}
Ok(with_host(|h| {
h.new_str(crate::utf16::to_string_lossy(&out))
}))
}
fn parse_int(args: &[Value]) -> Result<f64, String> {
let sv = host::to_string_value(&arg0(args))?;
let radix = match args.get(1) {
Some(r) if !matches!(r, Value::Undef) => {
vec![arg0(args), Value::Float(to_number_arg(args, 1)?)]
}
_ => args.to_vec(),
};
Ok(parse_int_str(&with_host(|h| h.str_of(&sv)), &radix))
}
fn parse_int_str(s: &str, args: &[Value]) -> f64 {
let radix_arg = args
.get(1)
.map(|r| with_host(|h| host::to_int32(h.to_number(r))));
let radix = match radix_arg {
Some(0) | None => None,
Some(r) if (2..=36).contains(&r) => Some(r as u32),
Some(_) => return f64::NAN,
};
let t = crate::utf16::js_trim_start(s);
let (neg, digits) = match t.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, t.strip_prefix('+').unwrap_or(t)),
};
let (radix, digits) = match radix {
Some(16) => (
16u32,
digits
.strip_prefix("0x")
.or_else(|| digits.strip_prefix("0X"))
.unwrap_or(digits),
),
Some(r) => (r, digits),
None => {
if let Some(hex) = digits
.strip_prefix("0x")
.or_else(|| digits.strip_prefix("0X"))
{
(16, hex)
} else {
(10, digits)
}
}
};
let valid: String = digits.chars().take_while(|c| c.is_digit(radix)).collect();
if valid.is_empty() {
return f64::NAN;
}
let n = if radix == 10 {
valid.parse::<f64>().unwrap_or(f64::NAN)
} else {
let mut n = 0.0f64;
for c in valid.chars() {
n = n * radix as f64 + c.to_digit(radix).unwrap_or(0) as f64;
}
n
};
if neg {
-n
} else {
n
}
}
fn parse_float(args: &[Value]) -> Result<f64, String> {
let sv = host::to_string_value(&arg0(args))?;
Ok(parse_float_str(&with_host(|h| h.str_of(&sv))))
}
fn parse_float_str(s: &str) -> f64 {
let t = crate::utf16::js_trim_start(s);
let inf_body = t
.strip_prefix('+')
.or_else(|| t.strip_prefix('-'))
.unwrap_or(t);
if inf_body.starts_with("Infinity") {
return if t.starts_with('-') {
f64::NEG_INFINITY
} else {
f64::INFINITY
};
}
let mut end = 0;
let bytes = t.as_bytes();
let mut seen_dot = false;
let mut seen_e = false;
let mut digits_before_dot = false;
for (i, &c) in bytes.iter().enumerate() {
match c {
b'0'..=b'9' => {
if !seen_dot && !seen_e {
digits_before_dot = true;
}
end = i + 1;
}
b'+' | b'-' if i == 0 || bytes[i - 1] == b'e' || bytes[i - 1] == b'E' => {}
b'.' if !seen_dot && !seen_e => {
seen_dot = true;
if digits_before_dot {
end = i + 1;
}
}
b'e' | b'E' if !seen_e && end > 0 => seen_e = true,
_ => break,
}
}
if end == 0 {
return f64::NAN;
}
t[..end].parse::<f64>().unwrap_or(f64::NAN)
}
pub(crate) fn js_pow(base: f64, exp: f64) -> f64 {
if exp == 0.0 {
return 1.0;
}
if base.is_nan() || exp.is_nan() {
return f64::NAN;
}
if base.abs() == 1.0 && exp.is_infinite() {
return f64::NAN;
}
base.powf(exp)
}
fn math_fn(fname: &str, args: &[Value]) -> Result<Value, String> {
let is_bigint = |a: &Value| {
if with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))) {
return true;
}
match wrapped_primitive(a) {
Some(p) => with_host(|h| matches!(h.get(&p), Some(JsObj::BigInt(_)))),
None => false,
}
};
if fname != "random" && args.iter().any(is_bigint) {
return Err(host::type_error(
"Cannot convert a BigInt value to a number",
));
}
let mut coerced = Vec::with_capacity(args.len());
for a in args {
if matches!(a, Value::Undef) {
coerced.push(a.clone());
continue;
}
let p = host::to_primitive(a, "number")?;
coerced.push(Value::Float(with_host(|h| h.to_number(&p))));
}
let args: &[Value] = &coerced;
let x = arg_num(args, 0);
let r = match fname {
"floor" => x.floor(),
"ceil" => x.ceil(),
"round" => {
if !x.is_finite() || x == 0.0 {
x
} else if x > 0.0 && x < 0.5 {
0.0
} else if (-0.5..0.0).contains(&x) {
-0.0
} else {
let f = x.floor();
if x - f >= 0.5 {
f + 1.0
} else {
f
}
}
}
"trunc" => x.trunc(),
"abs" => x.abs(),
"sign" => {
if x.is_nan() {
f64::NAN
} else if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
x
}
}
"sqrt" => x.sqrt(),
"cbrt" => x.cbrt(),
"exp" => x.exp(),
"log" => x.ln(),
"log2" => x.log2(),
"log10" => x.log10(),
"sin" => x.sin(),
"cos" => x.cos(),
"tan" => x.tan(),
"asin" => x.asin(),
"acos" => x.acos(),
"atan" => x.atan(),
"atan2" => x.atan2(arg_num(args, 1)),
"pow" => js_pow(x, arg_num(args, 1)),
"sinh" => x.sinh(),
"cosh" => x.cosh(),
"tanh" => x.tanh(),
"asinh" => x.asinh(),
"acosh" => x.acosh(),
"atanh" => x.atanh(),
"log1p" => x.ln_1p(),
"expm1" => x.exp_m1(),
"imul" => (host::to_int32(x).wrapping_mul(host::to_int32(arg_num(args, 1)))) as f64,
"hypot" => {
let xs: Vec<f64> = args.iter().map(|a| with_host(|h| h.to_number(a))).collect();
let mut max = 0.0f64;
for x in &xs {
if x.abs() > max {
max = x.abs();
}
}
if xs.iter().any(|x| x.is_infinite()) {
f64::INFINITY
} else if max == 0.0 || !max.is_finite() {
max
} else {
let s: f64 = xs.iter().map(|x| (x / max) * (x / max)).sum();
max * s.sqrt()
}
}
"random" => pseudo_random(),
"max" => {
if args.is_empty() {
f64::NEG_INFINITY
} else {
let mut m = f64::NEG_INFINITY;
for a in args {
let n = with_host(|h| h.to_number(a));
if n.is_nan() {
return Ok(Value::Float(f64::NAN));
}
if n > m || (n == m && n == 0.0 && n.is_sign_positive()) {
m = n;
}
}
m
}
}
"min" => {
if args.is_empty() {
f64::INFINITY
} else {
let mut m = f64::INFINITY;
for a in args {
let n = with_host(|h| h.to_number(a));
if n.is_nan() {
return Ok(Value::Float(f64::NAN));
}
if n < m || (n == m && n == 0.0 && n.is_sign_negative()) {
m = n;
}
}
m
}
}
"clz32" => {
let u = if x.is_finite() {
x.trunc().rem_euclid(4294967296.0) as u32
} else {
0
};
u.leading_zeros() as f64
}
"fround" => (x as f32) as f64,
_ => return Err(host::type_error(&format!("Math.{fname} is not a function"))),
};
Ok(Value::Float(r))
}
fn pseudo_random() -> f64 {
use std::cell::Cell;
thread_local!(static SEED: Cell<u64> = const { Cell::new(0x2545F4914F6CDD1D) });
SEED.with(|s| {
let mut x = s.get();
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
s.set(x);
(x >> 11) as f64 / (1u64 << 53) as f64
})
}
fn string_primitive_units(v: &Value) -> Option<Vec<String>> {
let s = match v {
Value::Str(s) => (**s).clone(),
_ => with_host(|h| match h.get(v) {
Some(JsObj::Str(s)) => Some(s.clone()),
_ => None,
})?,
};
let units = crate::utf16::Units::of(&s);
Some((0..units.len()).filter_map(|i| units.unit_str(i)).collect())
}
fn object_keys(args: Vec<Value>, mode: u8) -> Result<Value, String> {
let v = arg0(&args);
require_object_coercible(&v)?;
if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
if mode == 3 {
let keys = crate::proxy::own_keys(&v)?.unwrap_or_default();
return Ok(with_host(|h| {
let out: Vec<Value> = keys
.into_iter()
.filter(|k| !host::is_symbol_key(k))
.map(|k| h.new_str(k))
.collect();
h.new_array(out)
}));
}
if mode == 0 {
let keys = crate::proxy::own_enum_string_keys(&v)?;
return Ok(with_host(|h| {
let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
h.new_array(out)
}));
}
let entries = crate::proxy::own_enum_entries(&v)?;
return Ok(with_host(|h| {
let out: Vec<Value> = entries
.into_iter()
.map(|(k, val)| match mode {
0 => h.new_str(k),
1 => val,
_ => {
let ks = h.new_str(k);
h.new_array(vec![ks, val])
}
})
.collect();
h.new_array(out)
}));
}
let real_proto_ns = with_host(|h| h.intrinsic_proto_ctor(&v).map(|c| format!("{c}.prototype")))
.filter(|ns| intrinsic_proto_members(ns).is_some());
if let Some(ns) = real_proto_ns.or_else(|| {
with_host(|h| match h.get(&v) {
Some(JsObj::Builtin(ns)) => Some(ns.clone()),
_ => None,
})
}) {
if let Some(members) = intrinsic_proto_members(&ns) {
let ctor = ns.trim_end_matches(".prototype");
let mut names: Vec<String> = members
.iter()
.filter(|m| mode == 3 || m.starts_with('+'))
.map(|m| m.strip_prefix('+').unwrap_or(m).to_string())
.filter(|m| !m.starts_with("@@"))
.collect();
for k in with_host(|h| h.builtin_static_keys(&ns)) {
if !intrinsic_proto_member(&ns, &k) && !names.contains(&k) {
names.push(k);
}
}
return Ok(with_host(|h| {
let out: Vec<Value> = names
.iter()
.map(|name| {
let val = |h: &mut host::JsHost| {
if let Some(v) = h.builtin_static(&ns, name) {
return v;
}
let key = format!("@proto:{ctor}:{name}");
if builtin_meta(&key).is_some() {
h.alloc(JsObj::Builtin(key))
} else {
Value::Undef
}
};
match mode {
1 => val(h),
2 => {
let ks = h.new_str(name.clone());
let v = val(h);
h.new_array(vec![ks, v])
}
_ => h.new_str(name.clone()),
}
})
.collect();
h.new_array(out)
}));
}
if let Some(names) = builtin_proto_method_names(&ns) {
return Ok(with_host(|h| {
let out: Vec<Value> = names
.iter()
.map(|name| match mode {
1 => h.alloc(JsObj::Builtin(format!(
"@proto:{}:{name}",
ns.trim_end_matches(".prototype")
))),
2 => {
let ks = h.new_str(*name);
let val = h.alloc(JsObj::Builtin(format!(
"@proto:{}:{name}",
ns.trim_end_matches(".prototype")
)));
h.new_array(vec![ks, val])
}
_ => h.new_str(*name),
})
.collect();
h.new_array(out)
}));
}
let mut names = crate::stdlib::namespace_keys(&ns);
if names.is_empty() && mode == 3 {
let prefix = format!("{ns}.");
if is_builtin_ctor(&ns) {
names.extend(["length", "name", "prototype"].map(str::to_string));
}
names.extend(
NS_METHODS
.iter()
.filter_map(|q| q.strip_prefix(&prefix))
.map(|m| m.to_string()),
);
names.extend(
namespace_constants(&ns)
.iter()
.map(|(k, _)| (*k).to_string()),
);
}
if names.is_empty() && mode == 3 && host::builtin_is_callable(&ns) {
if builtin_meta(&ns).is_some() {
names.push("length".to_string());
}
names.push("name".to_string());
}
for k in with_host(|h| h.builtin_static_keys(&ns)) {
if !names.contains(&k) {
names.push(k);
}
}
if !names.is_empty() {
let entries: Vec<(String, Value)> = names
.into_iter()
.map(|k| {
let val = namespace_property(&ns, &k);
(k, val)
})
.collect();
return Ok(with_host(|h| {
let out: Vec<Value> = entries
.into_iter()
.map(|(k, val)| match mode {
1 => val,
2 => {
let ks = h.new_str(k);
h.new_array(vec![ks, val])
}
_ => h.new_str(k),
})
.collect();
h.new_array(out)
}));
}
}
let entries: Vec<(String, Value)> = with_host(|h| {
if mode == 3 {
return h
.own_key_names(&v, false)
.into_iter()
.map(|k| (k, Value::Undef))
.collect();
}
Vec::new()
});
let entries = match mode {
3 => entries,
0 => with_host(|h| h.own_enum_key_names(&v))
.into_iter()
.map(|k| (k, Value::Undef))
.collect(),
_ => host::own_enum_entries_deep(&v)?,
};
Ok(with_host(|h| {
let out: Vec<Value> = entries
.into_iter()
.map(|(k, val)| match mode {
0 | 3 => h.new_str(k),
1 => val,
_ => {
let ks = h.new_str(k);
h.new_array(vec![ks, val])
}
})
.collect();
h.new_array(out)
}))
}
fn object_assign(args: Vec<Value>) -> Result<Value, String> {
let target = arg0(&args);
require_object_coercible(&target)?;
for src in args.iter().skip(1) {
let entries = host::own_enum_entries_deep(src)?;
let syms = with_host(|h| h.own_symbol_entries(src));
let filled = with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(&target) {
for (k, v) in entries.iter().cloned().chain(syms.iter().cloned()) {
p.insert(k, v);
}
host::canonicalize_own_keys(p);
return true;
}
false
});
if !filled {
for (k, v) in entries.into_iter().chain(syms) {
set_property(&target, &k, v)?;
}
}
}
Ok(target)
}
fn object_from_entries(args: Vec<Value>) -> Result<Value, String> {
let pairs = with_host(|h| h.iter_vec(&arg0(&args))).unwrap_or_default();
let mut props: IndexMap<String, Value> = IndexMap::new();
for p in pairs {
let kv = with_host(|h| h.iter_vec(&p)).unwrap_or_default();
let key = with_host(|h| h.str_of(&kv.first().cloned().unwrap_or(Value::Undef)));
let val = kv.get(1).cloned().unwrap_or(Value::Undef);
props.insert(key, val);
}
Ok(with_host(|h| h.new_object(props)))
}
fn object_group_by(args: Vec<Value>) -> Result<Value, String> {
group_by_check_iterable(&arg0(&args), "Object.groupBy")?;
let cb = args.get(1).cloned().unwrap_or(Value::Undef);
let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
host::iter_for_each(&arg0(&args), |item, i| {
let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
let key = with_host(|h| h.property_key(&key_v));
groups.entry(key).or_default().push(item);
Ok(())
})?;
let props: IndexMap<String, Value> = with_host(|h| {
groups
.into_iter()
.map(|(k, v)| (k, h.new_array(v)))
.collect()
});
let obj = with_host(|h| h.new_object(props));
with_host(|h| {
let nv = h.null();
h.set_proto(&obj, nv);
});
Ok(obj)
}
fn group_by_check_iterable(v: &Value, name: &str) -> Result<(), String> {
if with_host(|h| h.is_nullish(v)) {
return Err(host::type_error(&format!(
"{name} called on null or undefined"
)));
}
let iter_fn = get_property(v, "@@iterator").unwrap_or(Value::Undef);
if with_host(|h| host::is_callable(h, &iter_fn)) {
return Ok(());
}
Err(host::type_error(¬_iterable_typed(v)))
}
pub(crate) fn not_iterable_typed(v: &Value) -> String {
let shown = with_host(|h| {
let kind = h.type_of(v);
match kind {
"object" | "symbol" | "bigint" => kind.to_string(),
"string" => format!("string \"{}\"", h.str_of(v)),
_ => format!("{kind} {}", h.str_of(v)),
}
});
format!("{shown} is not iterable (cannot read property Symbol(Symbol.iterator))")
}
fn map_group_by(args: Vec<Value>) -> Result<Value, String> {
group_by_check_iterable(&arg0(&args), "Map.groupBy")?;
let cb = args.get(1).cloned().unwrap_or(Value::Undef);
let m = with_host(|h| {
h.alloc(JsObj::Map {
entries: IndexMap::new(),
weak: false,
})
});
host::iter_for_each(&arg0(&args), |item, i| {
let key_v = host::invoke(&cb, vec![item.clone(), Value::Float(i as f64)], None)?;
let existing = map_method(&m, "get", vec![key_v.clone()])?;
if matches!(existing, Value::Undef) {
let arr = with_host(|h| h.new_array(vec![item]));
map_method(&m, "set", vec![key_v, arr])?;
} else {
with_host(|h| {
if let Some(JsObj::Array(a)) = h.get_mut(&existing) {
a.push(item);
}
});
}
Ok(())
})?;
Ok(m)
}
fn array_from_async(args: Vec<Value>) -> Result<Value, String> {
thread_local! {
static IMPL: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
}
const SRC: &str = "(async function (items, mapFn, thisArg) {\n\
const out = []; let i = 0;\n\
const step = async (v) => { const a = await v; out.push(mapFn ? await mapFn.call(thisArg, a, i) : a); i++; };\n\
const iterable = items != null && (typeof items[Symbol.asyncIterator] === 'function'\n\
|| typeof items[Symbol.iterator] === 'function' || typeof items.next === 'function');\n\
if (iterable) {\n\
for await (const v of items) { out.push(mapFn ? await mapFn.call(thisArg, v, i) : v); i++; }\n\
return out;\n\
}\n\
const len = items == null ? 0 : (Math.trunc(Number(items.length)) || 0);\n\
while (i < len) { await step(items[i]); }\n\
return out;\n\
})";
let f = IMPL.with(|c| c.borrow().clone());
let f = match f {
Some(f) => f,
None => {
let f = crate::eval_in_global_scope(SRC)?;
IMPL.with(|c| *c.borrow_mut() = Some(f.clone()));
f
}
};
host::invoke(&f, args, None)
}
fn array_from(args: Vec<Value>) -> Result<Value, String> {
let src = arg0(&args);
if let Some(cb) = args.get(1).cloned() {
let this = this_arg(&args, 2);
let mut out = Vec::new();
let mapped = host::iter_for_each(&src, |v, i| {
out.push(host::invoke(
&cb,
vec![v, Value::Float(i as f64)],
this.clone(),
)?);
Ok(())
});
match mapped {
Ok(()) => {}
Err(e) if host::user_iterator_fn(&src).is_none() && e.ends_with(" is not iterable") => {
out.clear();
for (i, it) in array_like_items(&src).into_iter().enumerate() {
out.push(host::invoke(
&cb,
vec![it, Value::Float(i as f64)],
this.clone(),
)?);
}
}
Err(e) => return Err(e),
}
return construct_array_like(host::current_static_this(), out);
}
let items = match host::iter_all(&src) {
Ok(v) => v,
Err(_) => array_like_items(&src),
};
construct_array_like(host::current_static_this(), items)
}
pub(crate) fn array_like_items(src: &Value) -> Vec<Value> {
let len = get_property(src, "length")
.ok()
.and_then(|l| host::to_primitive(&l, "number").ok())
.map(|l| with_host(|h| h.to_number(&l)))
.unwrap_or(0.0);
if !len.is_finite() || len <= 0.0 {
return Vec::new();
}
(0..len as usize)
.map(|i| get_property(src, &i.to_string()).unwrap_or(Value::Undef))
.collect()
}
fn json_stringify(args: Vec<Value>) -> Result<Value, String> {
let replacer = args
.get(1)
.filter(|r| with_host(|h| host::is_callable(h, r)))
.cloned();
let root = arg0(&args);
let wrapper = with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert(String::new(), root.clone());
h.new_object(m)
});
let v = apply_to_json(&wrapper, "", &root, &mut Vec::new(), replacer.as_ref())?;
if with_host(|h| json_has_bigint(h, &v)) {
return Err(host::type_error("Do not know how to serialize a BigInt"));
}
let indent = match args.get(2) {
Some(Value::Float(f)) => " ".repeat((*f as usize).min(10)),
Some(other) => with_host(|h| h.as_str(other)).unwrap_or_default(),
None => String::new(),
};
let keys: Option<Vec<String>> = args.get(1).and_then(|r| {
with_host(|h| match h.get(r) {
Some(JsObj::Array(items)) => {
Some(items.iter().map(|k| h.str_of(k)).collect::<Vec<_>>())
}
_ => None,
})
});
let s = with_host(|h| json_str(h, &v, &indent, 0, keys.as_deref()));
match s {
Some(s) => Ok(with_host(|h| h.new_str(s))),
None => Ok(Value::Undef),
}
}
fn apply_to_json(
holder: &Value,
key: &str,
v: &Value,
path: &mut JsonPath,
rep: Option<&Value>,
) -> Result<Value, String> {
let mut v = v.clone();
if matches!(v, Value::Obj(_)) {
let tag = crate::stdlib::native_tag(&v);
let to_json = if with_host(|h| h.kind_of(&v)) == Some(ObjKind::Proxy) {
get_property(&v, "toJSON")?
} else {
with_host(|h| host::lookup_chain(h, &v, "toJSON")).unwrap_or(Value::Undef)
};
let has_to_json = with_host(|h| host::is_callable(h, &to_json))
|| tag
.as_deref()
.map(crate::stdlib::has_to_json)
.unwrap_or(false);
if has_to_json {
let k = with_host(|h| h.new_str(key.to_string()));
v = host::call_method(&v, "toJSON", vec![k])?;
}
}
if let Some(rep) = rep {
let k = with_host(|h| h.new_str(key.to_string()));
v = host::invoke(rep, vec![k, v.clone()], Some(holder.clone()))?;
}
let via = if matches!(with_host(|h| h.get(holder).cloned()), Some(JsObj::Array(_))) {
format!("index {key}")
} else {
format!("property '{key}'")
};
json_walk_children(&v, path, &via, rep)
}
type JsonPath = Vec<(String, Value)>;
fn circular_json_message(path: &JsonPath, start: usize, closing: &str) -> String {
const PREFIX: usize = 2;
const POSTFIX: usize = 1;
let ctor = |v: &Value| -> String {
with_host(|h| match h.get(v) {
Some(JsObj::Array(_)) if h.proto_of(v).is_none() => "Array".to_string(),
_ => match h.ctor_name(v) {
n if n.is_empty() => "Object".to_string(),
n => n,
},
})
};
let line = |i: usize| {
format!(
"\n | {} -> object with constructor '{}'",
path[i].0,
ctor(&path[i].1)
)
};
let mut msg = format!(
"Converting circular structure to JSON\n --> starting at object with constructor '{}'",
ctor(&path[start].1)
);
let prefix_end = path.len().min(start + 1 + PREFIX);
for i in start + 1..prefix_end {
msg.push_str(&line(i));
}
if path.len() > prefix_end + POSTFIX {
msg.push_str("\n | ...");
}
for i in prefix_end.max(path.len().saturating_sub(POSTFIX))..path.len() {
msg.push_str(&line(i));
}
msg.push_str(&format!("\n --- {closing} closes the circle"));
msg
}
fn json_visible_key(k: &str) -> bool {
!k.starts_with("@@") && !k.starts_with('#')
}
fn json_walk_children(
v: &Value,
path: &mut JsonPath,
via: &str,
rep: Option<&Value>,
) -> Result<Value, String> {
if !matches!(v, Value::Obj(_)) {
return Ok(v.clone());
}
if let Some(start) = with_host(|h| path.iter().position(|(_, p)| h.strict_eq(p, v))) {
return Err(host::type_error(&circular_json_message(path, start, via)));
}
if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
let snap = crate::proxy::json_snapshot(v)?;
path.push((via.to_string(), v.clone()));
let out = json_walk_children(&snap, path, via, rep);
path.pop();
return out;
}
let obj = with_host(|h| h.get(v).cloned());
path.push((via.to_string(), v.clone()));
let out = (|| match obj {
Some(JsObj::Array(items)) => {
let mut resolved = items;
let had_accessor = resolve_index_accessors(v, &mut resolved);
let items = resolved;
let mut out = Vec::with_capacity(items.len());
let mut changed = had_accessor;
for (i, it) in items.iter().enumerate() {
let nv = apply_to_json(v, &i.to_string(), it, path, rep)?;
changed |= !with_host(|h| h.strict_eq(&nv, it));
out.push(nv);
}
if changed {
Ok(with_host(|h| h.new_array(out)))
} else {
Ok(v.clone())
}
}
Some(JsObj::Object(props)) => {
let has_accessor = with_host(|h| {
h.own_accessor_keys(v)
.iter()
.any(|k| h.prop_attrs(v, k).enumerable)
});
if has_accessor {
let mut next: IndexMap<String, Value> = IndexMap::new();
for (k, val) in host::own_enum_entries_deep(v)? {
let nv = if json_visible_key(&k) {
apply_to_json(v, &k, &val, path, rep)?
} else {
val
};
next.insert(k, nv);
}
return Ok(with_host(|h| h.new_object(next)));
}
let mut next: IndexMap<String, Value> = IndexMap::new();
let mut changed = false;
for (k, val) in &props {
let nv = if json_visible_key(k) {
apply_to_json(v, k, val, path, rep)?
} else {
val.clone()
};
changed |= !with_host(|h| h.strict_eq(&nv, val));
next.insert(k.clone(), nv);
}
if changed {
Ok(with_host(|h| {
let o = h.new_object(next);
h.copy_prop_attrs(v, &o);
o
}))
} else {
Ok(v.clone())
}
}
_ => Ok(v.clone()),
})();
path.pop();
out
}
fn json_has_bigint(h: &host::JsHost, v: &Value) -> bool {
match h.get(v) {
Some(JsObj::BigInt(_)) => true,
Some(JsObj::Array(items)) => items.iter().any(|x| json_has_bigint(h, x)),
Some(JsObj::Object(props)) => props
.iter()
.filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
.any(|(_, val)| json_has_bigint(h, val)),
_ => false,
}
}
fn json_str(
h: &host::JsHost,
v: &Value,
indent: &str,
depth: usize,
keys: Option<&[String]>,
) -> Option<String> {
let sep = if indent.is_empty() { ":" } else { ": " };
match v {
Value::Undef => None,
Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
Value::Int(n) => Some(n.to_string()),
Value::Float(f) => Some(if f.is_finite() {
host::fmt_number(*f)
} else {
"null".into()
}),
Value::Str(s) => Some(json_quote(s)),
Value::Obj(_) => match h.get(v) {
Some(JsObj::Str(s)) => Some(json_quote(s)),
Some(JsObj::Null) => Some("null".into()),
_ if h.fn_prop(v, "@@rawJSON").is_some() => match h.get(v) {
Some(JsObj::Object(p)) => p.get("rawJSON").map(|r| h.str_of(r)),
_ => None,
},
Some(JsObj::Map { .. })
| Some(JsObj::Set { .. })
| Some(JsObj::RegExp(_))
| Some(JsObj::Promise { .. })
| Some(JsObj::Generator { .. }) => {
let parts: Vec<String> = h
.own_enum_entries(v)
.into_iter()
.filter(|(k, _)| !k.starts_with("@@") && !host::is_symbol_key(k))
.filter_map(|(k, val)| {
json_str(h, &val, indent, depth + 1, keys)
.map(|s| format!("{}{sep}{s}", json_quote(&k)))
})
.collect();
Some(wrap(&parts, "{", "}", indent, depth))
}
Some(JsObj::Builtin(n)) if !host::builtin_is_callable(n) => {
let parts: Vec<String> = crate::stdlib::namespace_keys(n)
.into_iter()
.filter_map(|k| {
let val = h.builtin_static(n, &k)?;
json_str(h, &val, indent, depth + 1, keys)
.map(|s| format!("{}{sep}{s}", json_quote(&k)))
})
.collect();
Some(wrap(&parts, "{", "}", indent, depth))
}
Some(JsObj::Func(_))
| Some(JsObj::Builtin(_))
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::Class(_))
| Some(JsObj::Symbol { .. }) => None,
Some(JsObj::Array(items)) => {
if items.is_empty() {
return Some("[]".into());
}
let parts: Vec<String> = items
.iter()
.map(|x| {
json_str(h, x, indent, depth + 1, keys).unwrap_or_else(|| "null".into())
})
.collect();
Some(wrap(&parts, "[", "]", indent, depth))
}
Some(JsObj::Object(props)) if props.contains_key("@@primitive") => {
json_str(h, &props["@@primitive"].clone(), indent, depth, keys)
}
Some(JsObj::Object(props)) => {
let parts: Vec<String> = match keys {
Some(allow) => allow
.iter()
.filter_map(|k| {
props.get(k).and_then(|val| {
json_str(h, val, indent, depth + 1, keys)
.map(|vs| format!("{}{sep}{vs}", json_quote(k)))
})
})
.collect(),
None => h
.own_enum_entries(v)
.iter()
.filter_map(|(k, val)| {
json_str(h, val, indent, depth + 1, keys)
.map(|vs| format!("{}{sep}{vs}", json_quote(k)))
})
.collect(),
};
if parts.is_empty() {
return Some("{}".into());
}
Some(wrap(&parts, "{", "}", indent, depth))
}
_ => Some("null".into()),
},
_ => Some("null".into()),
}
}
fn wrap(parts: &[String], open: &str, close: &str, indent: &str, depth: usize) -> String {
if indent.is_empty() {
format!("{open}{}{close}", parts.join(","))
} else {
let pad = indent.repeat(depth + 1);
let pad_close = indent.repeat(depth);
format!(
"{open}\n{pad}{}\n{pad_close}{close}",
parts.join(&format!(",\n{pad}"))
)
}
}
fn json_quote(s: &str) -> String {
let mut out = String::from("\"");
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
_ => out.push(c),
}
}
out.push('"');
out
}
fn json_parse(args: Vec<Value>) -> Result<Value, String> {
let s = with_host(|h| h.str_of(&arg0(&args)));
let mut p = JsonParser {
chars: s.chars().collect(),
pos: 0,
prims: Vec::new(),
record: args
.get(1)
.is_some_and(|r| with_host(|h| host::is_callable(h, r))),
};
p.skip_ws();
if p.peek().is_none() {
return Err("SyntaxError: Unexpected end of JSON input".into());
}
let v = p.parse_value()?;
let value_end = p.pos;
p.skip_ws();
if let Some(c) = p.peek() {
let after_number = value_end > 0
&& p.pos == value_end
&& p.chars[value_end - 1].is_ascii_digit()
&& c.is_ascii_digit();
return Err(if after_number {
p.err_at("Unexpected number", p.pos)
} else {
p.err_trailing(p.pos)
});
}
if let Some(reviver) = args
.get(1)
.filter(|r| with_host(|h| host::is_callable(h, r)))
.cloned()
{
let root = with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert(String::new(), v.clone());
h.new_object(m)
});
return json_revive("", v, &reviver, &root, &p.prims, &mut 0);
}
Ok(v)
}
fn json_raw(args: Vec<Value>) -> Result<Value, String> {
const INVALID: &str = "SyntaxError: Invalid value for JSON.rawJSON";
let s = with_host(|h| h.str_of(&arg0(&args)));
if s.is_empty() {
return Err(INVALID.into());
}
let mut p = JsonParser {
chars: s.chars().collect(),
pos: 0,
prims: Vec::new(),
record: false,
};
if matches!(p.peek(), Some('{') | Some('[')) || p.peek().is_some_and(|c| c.is_whitespace()) {
return Err(p.err_token(0));
}
p.parse_value()?;
if p.pos != p.chars.len() {
if p.chars[p.pos - 1].is_ascii_digit() && p.chars[p.pos].is_ascii_digit() {
return Err(p.err_at("Unexpected number", p.pos));
}
return Err(INVALID.into());
}
Ok(with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
let text = h.new_str(s);
m.insert("rawJSON".into(), text);
let o = h.new_object(m);
let null = h.null();
h.set_proto(&o, null);
h.set_fn_prop(&o, "@@rawJSON", Value::Bool(true));
h.seal_object(&o, true);
o
}))
}
fn json_is_raw(args: Vec<Value>) -> Result<Value, String> {
Ok(Value::Bool(is_raw_json(&arg0(&args))))
}
fn is_raw_json(v: &Value) -> bool {
with_host(|h| h.fn_prop(v, "@@rawJSON")).is_some()
}
fn json_revive(
key: &str,
val: Value,
reviver: &Value,
holder: &Value,
prims: &[String],
next: &mut usize,
) -> Result<Value, String> {
let is_container =
with_host(|h| matches!(h.get(&val), Some(JsObj::Array(_)) | Some(JsObj::Object(_))));
let source = if !is_container {
let s = prims.get(*next).cloned();
if s.is_some() {
*next += 1;
}
s
} else {
None
};
match with_host(|h| h.get(&val).cloned()) {
Some(JsObj::Array(items)) => {
for i in 0..items.len() {
let elem = with_host(|h| match h.get(&val) {
Some(JsObj::Array(it)) => it[i].clone(),
_ => Value::Undef,
});
let nv = json_revive(&i.to_string(), elem, reviver, &val, prims, next)?;
with_host(|h| {
if let Some(JsObj::Array(it)) = h.get_mut(&val) {
it[i] = nv;
}
});
}
}
Some(JsObj::Object(props)) => {
let keys: Vec<String> = props
.keys()
.filter(|k| !k.starts_with("@@"))
.cloned()
.collect();
for k in keys {
let elem = with_host(|h| match h.get(&val) {
Some(JsObj::Object(p)) => p.get(&k).cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
});
let nv = json_revive(&k, elem, reviver, &val, prims, next)?;
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(&val) {
if matches!(nv, Value::Undef) {
p.shift_remove(&k);
} else {
p.insert(k.clone(), nv);
}
}
});
}
}
_ => {}
}
let kv = with_host(|h| h.new_str(key.to_string()));
let ctx = with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
if let Some(s) = source {
let sv = h.new_str(s);
m.insert("source".into(), sv);
}
h.new_object(m)
});
host::invoke(reviver, vec![kv, val, ctx], Some(holder.clone()))
}
struct JsonParser {
chars: Vec<char>,
pos: usize,
prims: Vec<String>,
record: bool,
}
impl JsonParser {
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn at(&self, pos: usize) -> String {
let mut line = 1usize;
let mut col = 1usize;
for c in &self.chars[..pos.min(self.chars.len())] {
if *c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
format!(" at position {pos} (line {line} column {col})")
}
fn err_at(&self, what: &str, pos: usize) -> String {
format!("SyntaxError: {what} in JSON{}", self.at(pos))
}
fn err_trailing(&self, pos: usize) -> String {
format!(
"SyntaxError: Unexpected non-whitespace character after JSON{}",
self.at(pos)
)
}
fn err_token(&self, pos: usize) -> String {
const MAX_WHOLE: usize = 20;
const CONTEXT: usize = 10;
let len = self.chars.len();
let Some(c) = self.chars.get(pos) else {
return "SyntaxError: Unexpected end of JSON input".into();
};
let whole: String = self.chars.iter().collect();
if matches!(
whole.as_str(),
"undefined" | "NaN" | "Infinity" | "-Infinity"
) {
return format!("SyntaxError: \"{whole}\" is not valid JSON");
}
let snippet = if len <= MAX_WHOLE {
format!("\"{whole}\"")
} else {
let start = pos.saturating_sub(CONTEXT);
let end = (pos + CONTEXT).min(len);
let body: String = self.chars[start..end].iter().collect();
let head = if start > 0 { "..." } else { "" };
let tail = if end < len { "..." } else { "" };
format!("{head}\"{body}\"{tail}")
};
format!("SyntaxError: Unexpected token '{c}', {snippet} is not valid JSON")
}
fn skip_ws(&mut self) {
while matches!(
self.peek(),
Some(' ') | Some('\n') | Some('\t') | Some('\r')
) {
self.pos += 1;
}
}
fn parse_value(&mut self) -> Result<Value, String> {
self.skip_ws();
let start = self.pos;
let prim = matches!(self.peek(), Some(c) if c != '{' && c != '[');
let v = match self.peek() {
Some('{') => self.parse_object(),
Some('[') => self.parse_array(),
Some('"') => {
let s = self.parse_string()?;
Ok(with_host(|h| h.new_str(s)))
}
Some('t') | Some('f') => self.parse_bool(),
Some('n') => {
self.expect_lit("null")?;
Ok(with_host(|h| h.null()))
}
Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
None => Err("SyntaxError: Unexpected end of JSON input".into()),
_ => Err(self.err_token(self.pos)),
}?;
if prim && self.record {
self.prims
.push(self.chars[start..self.pos].iter().collect());
}
Ok(v)
}
fn expect_lit(&mut self, lit: &str) -> Result<(), String> {
for ch in lit.chars() {
match self.peek() {
Some(c) if c == ch => self.pos += 1,
None => return Err("SyntaxError: Unexpected end of JSON input".into()),
_ => return Err(self.err_token(self.pos)),
}
}
Ok(())
}
fn parse_bool(&mut self) -> Result<Value, String> {
if self.peek() == Some('t') {
self.expect_lit("true")?;
Ok(Value::Bool(true))
} else {
self.expect_lit("false")?;
Ok(Value::Bool(false))
}
}
fn parse_number(&mut self) -> Result<Value, String> {
let start = self.pos;
if self.peek() == Some('-') {
self.pos += 1;
if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
return Err(self.err_at("No number after minus sign", self.pos));
}
}
if self.peek() == Some('0') {
self.pos += 1;
} else {
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
}
if self.peek() == Some('.') {
self.pos += 1;
if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
return Err(self.err_at("Unterminated fractional number", self.pos));
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
}
if matches!(self.peek(), Some('e') | Some('E')) {
self.pos += 1;
if matches!(self.peek(), Some('+') | Some('-')) {
self.pos += 1;
}
if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
return Err(self.err_at("Exponent part is missing a number", self.pos));
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
}
let s: String = self.chars[start..self.pos].iter().collect();
s.parse::<f64>()
.map(Value::Float)
.map_err(|_| self.err_at("Unexpected number", start))
}
fn parse_string(&mut self) -> Result<String, String> {
self.pos += 1; let mut out = String::new();
loop {
match self.peek() {
None => return Err(self.err_at("Unterminated string", self.pos)),
Some('"') => {
self.pos += 1;
break;
}
Some('\\') => {
self.pos += 1;
match self.peek() {
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some('r') => out.push('\r'),
Some('"') => out.push('"'),
Some('\\') => out.push('\\'),
Some('/') => out.push('/'),
Some('b') => out.push('\u{08}'),
Some('f') => out.push('\u{0C}'),
Some('u') => {
let h: String = self.chars
[self.pos + 1..(self.pos + 5).min(self.chars.len())]
.iter()
.collect();
if let Ok(n) = u32::from_str_radix(&h, 16) {
if let Some(ch) = char::from_u32(n) {
out.push(ch);
}
}
self.pos += 4;
}
_ => {}
}
self.pos += 1;
}
Some(c) if (c as u32) < 0x20 => {
return Err(self.err_at("Bad control character in string literal", self.pos))
}
Some(c) => {
out.push(c);
self.pos += 1;
}
}
}
Ok(out)
}
fn parse_array(&mut self) -> Result<Value, String> {
self.pos += 1; let mut items = Vec::new();
self.skip_ws();
if self.peek() == Some(']') {
self.pos += 1;
return Ok(with_host(|h| h.new_array(items)));
}
loop {
items.push(self.parse_value()?);
self.skip_ws();
match self.peek() {
Some(',') => {
self.pos += 1;
}
Some(']') => {
self.pos += 1;
break;
}
_ => return Err(self.err_at("Expected ',' or ']' after array element", self.pos)),
}
}
Ok(with_host(|h| h.new_array(items)))
}
fn parse_object(&mut self) -> Result<Value, String> {
self.pos += 1; let mut props: IndexMap<String, Value> = IndexMap::new();
self.skip_ws();
if self.peek() == Some('}') {
self.pos += 1;
return Ok(with_host(|h| h.new_object(props)));
}
loop {
self.skip_ws();
if self.peek() != Some('"') {
return Err(if props.is_empty() {
self.err_at("Expected property name or '}'", self.pos)
} else {
self.err_at("Expected double-quoted property name", self.pos)
});
}
let key = self.parse_string()?;
self.skip_ws();
if self.peek() != Some(':') {
return Err(match self.peek() {
None => "SyntaxError: Unexpected end of JSON input".into(),
_ => self.err_at("Expected ':' after property name", self.pos),
});
}
self.pos += 1;
let val = self.parse_value()?;
props.insert(key, val);
self.skip_ws();
match self.peek() {
Some(',') => {
self.pos += 1;
}
Some('}') => {
self.pos += 1;
break;
}
_ => return Err(self.err_at("Expected ',' or '}' after property value", self.pos)),
}
}
Ok(with_host(|h| h.new_object(props)))
}
}
fn is_array_method(name: &str) -> bool {
matches!(
name,
"push"
| "pop"
| "shift"
| "unshift"
| "map"
| "filter"
| "forEach"
| "join"
| "slice"
| "indexOf"
| "lastIndexOf"
| "includes"
| "reduce"
| "concat"
| "reverse"
| "sort"
| "find"
| "findIndex"
| "some"
| "every"
| "flat"
| "fill"
| "splice"
| "keys"
| "values"
| "entries"
| "flatMap"
| "at"
| "toString"
| "reduceRight"
| "findLast"
| "findLastIndex"
| "copyWithin"
)
}
pub(crate) const STRING_PROTO_METHODS: &[&str] = &[
"toUpperCase",
"toLowerCase",
"charAt",
"charCodeAt",
"codePointAt",
"indexOf",
"lastIndexOf",
"includes",
"slice",
"substring",
"substr",
"split",
"trim",
"trimStart",
"trimEnd",
"replace",
"replaceAll",
"repeat",
"startsWith",
"endsWith",
"padStart",
"padEnd",
"concat",
"at",
"toString",
"toLocaleString",
"valueOf",
"match",
"matchAll",
"search",
"normalize",
"localeCompare",
"toLocaleUpperCase",
"toLocaleLowerCase",
"isWellFormed",
"toWellFormed",
];
fn is_string_method(name: &str) -> bool {
STRING_PROTO_METHODS.contains(&name)
}
pub(crate) fn proto_symbol_methods(ctor: &str) -> Vec<&'static str> {
let prefix = format!("@proto:{ctor}:");
crate::arity::BUILTIN_ARITY
.iter()
.filter_map(|(k, _, _)| k.strip_prefix(prefix.as_str()))
.filter(|m| m.starts_with("@@"))
.collect()
}
pub(crate) const BRANDED_PROTOS: &[&str] = &[
"Array",
"ArrayBuffer",
"BigInt",
"Boolean",
"DataView",
"FinalizationRegistry",
"Function",
"Iterator",
"Map",
"Number",
"Object",
"Promise",
"Set",
"SharedArrayBuffer",
"String",
"Symbol",
"TextDecoder",
"TextEncoder",
"URL",
"URLSearchParams",
"WeakMap",
"WeakRef",
"WeakSet",
];
fn symbol_protocol(arg: &Value, sym: &str) -> Option<Value> {
if matches!(arg, Value::Undef) || with_host(|h| h.is_null(arg)) {
return None;
}
let f = get_property(arg, sym).ok()?;
with_host(|h| host::is_callable(h, &f)).then_some(f)
}
fn is_regexp_arg(v: &Value) -> bool {
if let Ok(m) = get_property(v, "@@match") {
if !matches!(m, Value::Undef) {
return with_host(|h| h.truthy(&m));
}
}
with_host(|h| h.kind_of(v)) == Some(ObjKind::RegExp)
}
fn replace_str_fn(s: &str, pat: &str, repl: &Value, all: bool) -> Result<String, String> {
if pat.is_empty() {
return Ok(s.to_string());
}
let mut out = String::new();
let mut rest = s;
let mut base = 0usize;
while let Some(pos) = rest.find(pat) {
out.push_str(&rest[..pos]);
let offset = base + pos;
let m = with_host(|h| h.new_str(pat.to_string()));
let str_arg = with_host(|h| h.new_str(s.to_string()));
let r = host::invoke(repl, vec![m, Value::Float(offset as f64), str_arg], None)?;
out.push_str(&with_host(|h| h.str_of(&r)));
let consumed = pos + pat.len();
base += consumed;
rest = &rest[consumed..];
if !all {
break;
}
}
out.push_str(rest);
Ok(out)
}
pub(crate) const NUMBER_PROTO_METHODS: &[&str] = &[
"toFixed",
"toExponential",
"toString",
"toPrecision",
"toLocaleString",
"valueOf",
];
fn is_number_method(name: &str) -> bool {
NUMBER_PROTO_METHODS.contains(&name)
}
fn inherits_object_methods(recv: &Value) -> bool {
matches!(
with_host(|h| h.kind_of(recv)),
Some(
ObjKind::Map
| ObjKind::Set
| ObjKind::Promise
| ObjKind::RegExp
| ObjKind::Generator
| ObjKind::Symbol
| ObjKind::BigInt
| ObjKind::Iter
)
)
}
fn overrides_object_method(recv: &Value, name: &str) -> bool {
match with_host(|h| h.kind_of(recv)) {
Some(ObjKind::Map) => is_map_method(name),
Some(ObjKind::Set) => is_set_method(name),
Some(ObjKind::RegExp) => crate::regexp::is_regexp_method(name),
Some(ObjKind::Symbol) => matches!(name, "toString" | "valueOf" | "@@toPrimitive"),
Some(ObjKind::BigInt) => matches!(name, "toString" | "valueOf" | "toLocaleString"),
_ => false,
}
}
pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
if let Some(f) = with_host(|h| host::lookup_chain(h, recv, name)) {
if matches!(
with_host(|h| h.kind_of(&f)),
Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc)
) {
return host::invoke(&f, args, Some(recv.clone()));
}
}
if !own_intrinsic_reachable(recv)
&& inherited_method_owner(recv, name).is_none()
&& !has_own_for_shadow(recv, name)
&& inherited_builtin_static(recv, name).is_none()
&& with_host(|h| host::lookup_chain(h, recv, name)).is_none()
{
return Err(host::type_error(&format!("{name} is not a function")));
}
if !name.starts_with("@@") && !has_own_for_shadow(recv, name) {
if let Some(f) = inherited_builtin_static(recv, name) {
if with_host(|h| host::is_callable(h, &f)) {
return host::invoke(&f, args, Some(recv.clone()));
}
}
}
if is_object_builtin_method(name)
&& (inherited_method_owner(recv, name) == Some("Object")
|| (inherits_object_methods(recv) && !overrides_object_method(recv, name)))
{
if name == "toString" {
return proto_method(recv, "Object:toString", args);
}
return object_builtin_method(recv, name, args);
}
if name == "valueOf"
&& matches!(
with_host(|h| h.kind_of(recv)),
Some(
ObjKind::Array
| ObjKind::Map
| ObjKind::Set
| ObjKind::Generator
| ObjKind::Promise
| ObjKind::Iter
| ObjKind::RegExp
)
)
{
return Ok(recv.clone());
}
match with_host(|h| h.kind_of(recv)) {
Some(ObjKind::Array) => array_method(recv, name, args),
Some(ObjKind::Str) => {
let s = peek(recv, |o| match o {
JsObj::Str(s) => Some(s.clone()),
_ => None,
})
.unwrap_or_default();
string_method(&s, name, args)
}
Some(ObjKind::Map) => map_method(recv, name, args),
Some(ObjKind::Set) => set_method(recv, name, args),
Some(ObjKind::Generator) if crate::stdlib::iterator::is_helper(name) => {
crate::stdlib::iterator::call(recv, name, &args)
}
Some(ObjKind::Generator) => generator_method(recv, name, args),
Some(ObjKind::Promise) => promise_method(recv, name, args),
Some(ObjKind::Iter) if crate::stdlib::iterator::is_helper(name) => {
crate::stdlib::iterator::call(recv, name, &args)
}
Some(ObjKind::Iter) => iter_method(recv, name, args),
Some(ObjKind::Symbol) => symbol_method(recv, name, args),
Some(ObjKind::BigInt) => {
let b = peek(recv, |o| match o {
JsObj::BigInt(b) => Some(b.clone()),
_ => None,
})
.unwrap_or_default();
bigint_method(&b, name, args)
}
Some(ObjKind::RegExp) => crate::regexp::regexp_method(recv, name, args),
Some(ObjKind::Func) | Some(ObjKind::Class) | Some(ObjKind::BoundFunc) => {
match function_builtin_method(recv, name, &args)? {
Some(v) => Ok(v),
None => Err(host::type_error(&format!("{name} is not a function"))),
}
}
Some(ObjKind::Object) => {
if let Some(f) = peek(recv, |o| match o {
JsObj::Object(p) => p.get(name).cloned(),
_ => None,
}) {
host::invoke(&f, args, Some(recv.clone()))
} else if name == "hasOwnProperty" {
let k = with_host(|h| h.str_of(&arg0(&args)));
let has = peek(recv, |o| match o {
JsObj::Object(p) => Some(p.contains_key(&k)),
_ => None,
})
.unwrap_or(false);
Ok(Value::Bool(has))
} else if name == "toString" {
Ok(with_host(|h| h.new_str("[object Object]")))
} else {
Err(host::type_error(&format!("{} is not a function", name)))
}
}
_ => {
if let Value::Float(_) | Value::Int(_) = recv {
return number_method(with_host(|h| h.to_number(recv)), name, args);
}
if let Some(s) = with_host(|h| h.as_str(recv)) {
return string_method(&s, name, args);
}
if let Value::Bool(b) = recv {
return match name {
"toString" | "toLocaleString" => {
Ok(new_s(if *b { "true" } else { "false" }.to_string()))
}
"valueOf" => Ok(Value::Bool(*b)),
_ => Err(host::type_error(&format!("{name} is not a function"))),
};
}
Err(host::type_error(&format!("{} is not a function", name)))
}
}
}
fn collection_iterator(coll: &Value, kind: &str) -> Value {
with_host(|h| {
let mut m = IndexMap::new();
m.insert(
"@@native".into(),
h.new_str("CollectionIterator".to_string()),
);
m.insert("@@coll".into(), coll.clone());
m.insert("@@kind".into(), h.new_str(kind.to_string()));
m.insert("@@started".into(), Value::Bool(false));
m.insert("@@lastIdx".into(), Value::Float(0.0));
h.new_object(m)
})
}
pub(crate) fn collection_iterator_next(recv: &Value) -> Result<Value, String> {
let slot = |k: &str| {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get(k).cloned(),
_ => None,
})
};
let coll = slot("@@coll").unwrap_or(Value::Undef);
let kind = slot("@@kind")
.map(|v| with_host(|h| h.str_of(&v)))
.unwrap_or_default();
let started = slot("@@started").is_some_and(|v| with_host(|h| h.truthy(&v)));
let last_idx = slot("@@lastIdx")
.map(|v| with_host(|h| h.to_number(&v)) as usize)
.unwrap_or(0);
let last_key = slot("@@lastKey");
let next_idx = if !started {
0
} else {
match last_key
.as_ref()
.and_then(|k| with_host(|h| collection_index_of(h, &coll, k)))
{
Some(i) => i + 1,
None => last_idx,
}
};
let entry = with_host(|h| collection_entry_at(h, &coll, next_idx));
let Some((k, v)) = entry else {
return Ok(iter_result(Value::Undef, true));
};
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert("@@started".into(), Value::Bool(true));
p.insert("@@lastIdx".into(), Value::Float(next_idx as f64));
p.insert("@@lastKey".into(), k.clone());
}
});
let out = match kind.as_str() {
"keys" => k,
"values" => v,
_ => with_host(|h| h.new_array(vec![k, v])),
};
Ok(iter_result(out, false))
}
fn collection_entry_at(h: &host::JsHost, coll: &Value, idx: usize) -> Option<(Value, Value)> {
match h.get(coll) {
Some(JsObj::Map { entries, .. }) => entries.get_index(idx).map(|(_, kv)| kv.clone()),
Some(JsObj::Set { entries, .. }) => {
entries.get_index(idx).map(|(_, v)| (v.clone(), v.clone()))
}
_ => None,
}
}
fn collection_index_of(h: &host::JsHost, coll: &Value, key: &Value) -> Option<usize> {
let mk = host::map_key(h, key);
match h.get(coll) {
Some(JsObj::Map { entries, .. }) => entries.get_index_of(&mk),
Some(JsObj::Set { entries, .. }) => entries.get_index_of(&mk),
_ => None,
}
}
fn this_arg(args: &[Value], idx: usize) -> Option<Value> {
args.get(idx)
.filter(|v| !matches!(v, Value::Undef))
.cloned()
}
fn array_walk<T>(
recv: &Value,
mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
) -> Result<Option<T>, String> {
let len = array_len(recv);
for i in 0..len {
if index_absent(recv, i) || i >= array_len(recv) {
continue;
}
let v = get_property(recv, &i.to_string())?;
if let Some(out) = f(i, v)? {
return Ok(Some(out));
}
}
Ok(None)
}
fn array_walk_rev<T>(
recv: &Value,
from: usize,
mut f: impl FnMut(usize, Value) -> Result<Option<T>, String>,
) -> Result<Option<T>, String> {
for i in (0..from).rev() {
if index_absent(recv, i) || i >= array_len(recv) {
continue;
}
let v = get_property(recv, &i.to_string())?;
if let Some(out) = f(i, v)? {
return Ok(Some(out));
}
}
Ok(None)
}
fn array_elem_live(recv: &Value, i: usize) -> Result<Value, String> {
if i >= array_len(recv) {
return Ok(Value::Undef);
}
get_property(recv, &i.to_string())
}
fn array_items(recv: &Value) -> Vec<Value> {
let mut items = with_host(|h| match h.get(recv) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
});
resolve_index_accessors(recv, &mut items);
items
}
pub(crate) fn resolve_index_accessors_pub(recv: &Value, items: &mut [Value]) {
resolve_index_accessors(recv, items);
}
fn resolve_index_accessors(recv: &Value, items: &mut [Value]) -> bool {
let mut indices: Vec<usize> = with_host(|h| h.own_accessor_keys(recv))
.into_iter()
.filter_map(|k| k.parse::<usize>().ok())
.filter(|i| *i < items.len())
.collect();
let inherited: Vec<usize> = with_host(|h| h.hole_indices(recv))
.into_iter()
.filter(|i| *i < items.len() && !indices.contains(i))
.filter(|i| has_property(recv, &i.to_string()).unwrap_or(false))
.collect();
indices.extend(inherited);
let mut replaced = false;
for i in indices {
if let Ok(v) = get_property(recv, &i.to_string()) {
items[i] = v;
replaced = true;
}
}
replaced
}
fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
with_host(|h| h.hole_indices(recv)).into_iter().collect()
}
fn absent_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
hole_set(recv)
.into_iter()
.filter(|i| !has_property(recv, &i.to_string()).unwrap_or(false))
.collect()
}
fn index_absent(recv: &Value, i: usize) -> bool {
with_host(|h| h.is_hole(recv, i)) && !has_property(recv, &i.to_string()).unwrap_or(false)
}
fn array_len(recv: &Value) -> usize {
peek(recv, |o| match o {
JsObj::Array(items) => Some(items.len()),
_ => None,
})
.unwrap_or(0)
}
fn construct_array_like(ctor: Option<Value>, items: Vec<Value>) -> Result<Value, String> {
let Some(ctor) = ctor.filter(|c| {
matches!(
with_host(|h| h.kind_of(c)),
Some(ObjKind::Class) | Some(ObjKind::Func)
)
}) else {
return Ok(with_host(|h| h.new_array(items)));
};
let out = host::construct(&ctor, vec![Value::Float(items.len() as f64)])?;
write_elements(&out, items);
Ok(out)
}
fn write_elements(out: &Value, items: Vec<Value>) {
with_host(|h| {
h.clear_holes(out);
if let Some(JsObj::Array(dst)) = h.get_mut(out) {
*dst = items;
}
});
}
fn array_species_create(recv: &Value, items: Vec<Value>) -> Result<Value, String> {
let plain = || with_host(|h| h.new_array(items.clone()));
let ctor = if with_host(|h| h.kind_of(recv)) == Some(ObjKind::Proxy) {
get_property(recv, "constructor").unwrap_or(Value::Undef)
} else {
with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef)
};
if !matches!(
with_host(|h| h.kind_of(&ctor)),
Some(ObjKind::Class) | Some(ObjKind::Func)
) {
return Ok(plain());
}
let species = match get_property(&ctor, "@@species") {
Ok(Value::Undef) => ctor,
Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(plain()),
Ok(s) => s,
Err(_) => ctor,
};
if !matches!(
with_host(|h| h.kind_of(&species)),
Some(ObjKind::Class) | Some(ObjKind::Func)
) {
return Ok(plain());
}
let out = host::construct(&species, vec![Value::Float(items.len() as f64)])?;
write_elements(&out, items);
Ok(out)
}
fn array_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
array_method_on(recv, recv, name, args)
}
const ARRAY_MUTATORS: &[&str] = &[
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
"fill",
"copyWithin",
];
fn array_generic(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
let len = match get_property(recv, "length") {
Ok(v) => host::to_array_length(&v).unwrap_or(0),
Err(_) => 0,
};
let dense = with_host(|h| h.as_str(recv)).is_some();
let mut items = Vec::with_capacity(len);
let mut holes: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
for i in 0..len {
let k = i.to_string();
if dense || has_property(recv, &k)? {
items.push(get_property(recv, &k)?);
} else {
holes.insert(i);
items.push(Value::Undef);
}
}
let tmp = with_host(|h| {
let a = h.new_array(items);
h.install_holes(&a, holes);
a
});
let out = array_method_on(&tmp, recv, method, args)?;
if ARRAY_MUTATORS.contains(&method) {
let result = with_host(|h| match h.get(&tmp) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
});
for (i, v) in result.iter().enumerate() {
set_property(recv, &i.to_string(), v.clone())?;
}
set_property(recv, "length", Value::Float(result.len() as f64))?;
}
Ok(out)
}
fn array_method_on(
recv: &Value,
this_value: &Value,
name: &str,
args: Vec<Value>,
) -> Result<Value, String> {
let args = coerce_numeric_args(ARRAY_METHOD_NUMERIC_ARGS, name, args)?;
match name {
"push" => {
if !args.is_empty() && !with_host(|h| h.is_extensible(recv)) {
let at = array_len(recv);
return Err(host::type_error(&format!(
"Cannot add property {at}, object is not extensible"
)));
}
if !args.is_empty() && !with_host(|h| h.prop_attrs(recv, "length").writable) {
return Err(host::type_error(
"Cannot assign to read only property 'length' of object '[object Array]'",
));
}
let len = with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(recv) {
items.extend(args.iter().cloned());
items.len()
} else {
0
}
});
Ok(Value::Float(len as f64))
}
"pop" => Ok(with_host(|h| {
let popped = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
items.pop().unwrap_or(Value::Undef)
} else {
Value::Undef
};
let len = match h.get(recv) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
};
h.truncate_holes(recv, len);
popped
})),
"shift" => Ok(with_host(|h| {
let shifted = if let Some(JsObj::Array(items)) = h.get_mut(recv) {
if items.is_empty() {
Value::Undef
} else {
items.remove(0)
}
} else {
Value::Undef
};
h.remap_holes(recv, |i| i.checked_sub(1));
shifted
})),
"unshift" => {
with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(recv) {
for (i, a) in args.iter().enumerate() {
items.insert(i, a.clone());
}
}
let n = args.len();
h.remap_holes(recv, |i| Some(i + n));
});
Ok(Value::Float(array_len(recv) as f64))
}
"join" => {
let sep = if args.is_empty() || matches!(args[0], Value::Undef) {
",".to_string()
} else {
arg_to_string(&args, 0)?
};
join_array(recv, &sep)
}
"toLocaleString" => {
if !host::join_stack_push(recv) {
return Ok(with_host(|h| h.new_str(String::new())));
}
let items = array_items(recv);
let mut parts: Vec<String> = Vec::with_capacity(items.len());
for it in &items {
if with_host(|h| h.is_nullish(it)) {
parts.push(String::new());
continue;
}
let v = match host::call_method(it, "toLocaleString", Vec::new()) {
Ok(v) => v,
Err(e) => {
host::join_stack_pop();
return Err(e);
}
};
parts.push(with_host(|h| h.str_of(&v)));
}
host::join_stack_pop();
Ok(with_host(|h| h.new_str(parts.join(","))))
}
"indexOf" => {
let target = arg0(&args);
let len = array_len(recv);
let start = search_start(arg_num(&args, 1), len);
let mut idx = None;
for i in start..len {
if index_absent(recv, i) || i >= array_len(recv) {
continue;
}
let x = get_property(recv, &i.to_string())?;
if with_host(|h| h.strict_eq(&x, &target)) {
idx = Some(i);
break;
}
}
Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
}
"lastIndexOf" => {
let items = array_items(recv);
let holes = absent_set(recv);
let target = arg0(&args);
let from = (args.len() > 1).then(|| arg_num(&args, 1));
let idx = match search_start_last(from, items.len()) {
None => None,
Some(start) => with_host(|h| {
items[..=start]
.iter()
.enumerate()
.rev()
.find(|(i, x)| !holes.contains(i) && h.strict_eq(x, &target))
.map(|(i, _)| i)
}),
};
Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
}
"includes" => {
let target = arg0(&args);
let tnan = matches!(target, Value::Float(f) if f.is_nan());
let len = array_len(recv);
let start = search_start(arg_num(&args, 1), len);
let mut found = false;
for i in start..len {
let x = array_elem_live(recv, i)?;
if (tnan && matches!(x, Value::Float(f) if f.is_nan()))
|| with_host(|h| h.strict_eq(&x, &target))
{
found = true;
break;
}
}
Ok(Value::Bool(found))
}
"slice" => {
let items = array_items(recv);
let (lo, hi) = slice_bounds(&args, items.len());
let out = array_species_create(this_value, items[lo..hi].to_vec())?;
with_host(|h| h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo)));
Ok(out)
}
"concat" => {
let spreadable = |a: &Value| -> bool {
let flag = get_property(a, "@@isConcatSpreadable").unwrap_or(Value::Undef);
if matches!(flag, Value::Undef) {
matches!(with_host(|h| h.get(a).cloned()), Some(JsObj::Array(_)))
&& !is_arguments(a)
} else {
with_host(|h| h.truthy(&flag))
}
};
let (mut out, mut holes) = if spreadable(this_value) {
(array_items(recv), absent_set(recv))
} else {
(vec![to_object(this_value)], Default::default())
};
let mut sources: Vec<(Value, usize)> = Vec::new();
for a in &args {
if !spreadable(a) {
out.push(a.clone());
continue;
}
match with_host(|h| h.get(a).cloned()) {
Some(JsObj::Array(mut items)) => {
resolve_index_accessors(a, &mut items);
sources.push((a.clone(), out.len()));
out.extend(items);
}
_ => {
let len = get_property(a, "length").unwrap_or(Value::Undef);
let n = with_host(|h| h.to_number(&len));
let n = if n.is_finite() {
n.max(0.0) as usize
} else {
0
};
for i in 0..n {
out.push(get_property(a, &i.to_string()).unwrap_or(Value::Undef));
}
}
}
}
for (src, base) in sources {
holes.extend(
with_host(|h| h.hole_indices(&src))
.into_iter()
.map(|i| i + base),
);
}
let arr = array_species_create(this_value, out)?;
with_host(|h| h.install_holes(&arr, holes));
Ok(arr)
}
"reverse" => {
let len = array_len(recv);
with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(recv) {
items.reverse();
}
h.remap_holes(recv, |i| Some(len - 1 - i));
});
Ok(this_value.clone())
}
"fill" => {
let val = arg0(&args);
let len = array_len(recv) as i64;
let norm =
|v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
let start = if args.len() >= 2 {
norm(arg_num(&args, 1) as i64)
} else {
0
};
let end = if args.len() >= 3 {
norm(arg_num(&args, 2) as i64)
} else {
len as usize
};
with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(recv) {
for it in items.iter_mut().take(end).skip(start) {
*it = val.clone();
}
}
h.remap_holes(recv, |i| (i < start || i >= end).then_some(i));
});
Ok(this_value.clone())
}
"copyWithin" => {
let items = array_items(recv);
let len = items.len() as i64;
let norm =
|v: i64| -> usize { (if v < 0 { (len + v).max(0) } else { v.min(len) }) as usize };
let target = norm(arg_num(&args, 0) as i64);
let start = if args.len() >= 2 {
norm(arg_num(&args, 1) as i64)
} else {
0
};
let end = if args.len() >= 3 {
norm(arg_num(&args, 2) as i64)
} else {
len as usize
};
let slice: Vec<Value> = items[start..end.max(start)].to_vec();
let copied = slice.len();
let src_holes = absent_set(recv);
with_host(|h| {
if let Some(JsObj::Array(a)) = h.get_mut(recv) {
for (k, v) in slice.into_iter().enumerate() {
if target + k < a.len() {
a[target + k] = v;
}
}
}
let len = len as usize;
let mut holes: rustc_hash::FxHashSet<usize> = src_holes
.iter()
.copied()
.filter(|i| *i < target || *i >= (target + copied).min(len))
.collect();
for k in 0..copied {
if target + k < len && src_holes.contains(&(start + k)) {
holes.insert(target + k);
}
}
h.install_holes(recv, holes);
});
Ok(this_value.clone())
}
"at" => {
let items = array_items(recv);
let mut i = arg_num(&args, 0) as i64;
if i < 0 {
i += items.len() as i64;
}
Ok(if i >= 0 && (i as usize) < items.len() {
items[i as usize].clone()
} else {
Value::Undef
})
}
"map" => {
let holes = absent_set(recv);
let cb = arg0(&args);
let mut out = vec![Value::Undef; array_len(recv)];
array_walk(recv, |i, it| {
let v = host::invoke(
&cb,
vec![it, Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
if i < out.len() {
out[i] = v;
}
Ok(None::<()>)
})?;
let arr = array_species_create(this_value, out)?;
with_host(|h| h.install_holes(&arr, holes));
Ok(arr)
}
"flatMap" => {
let cb = arg0(&args);
let thisarg = this_arg(&args, 1);
let mut out = Vec::new();
array_walk(recv, |i, v| {
let r = host::invoke(
&cb,
vec![v, Value::Float(i as f64), this_value.clone()],
thisarg.clone(),
)?;
match with_host(|h| h.get(&r).cloned()) {
Some(JsObj::Array(inner)) => out.extend(inner),
_ => out.push(r),
}
Ok(None::<()>)
})?;
array_species_create(this_value, out)
}
"filter" => {
let cb = arg0(&args);
let mut out = Vec::new();
array_walk(recv, |i, it| {
let keep = host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
if with_host(|h| h.truthy(&keep)) {
out.push(it);
}
Ok(None::<()>)
})?;
array_species_create(this_value, out)
}
"forEach" => {
let cb = arg0(&args);
array_walk(recv, |i, it| {
host::invoke(
&cb,
vec![it, Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
Ok(None::<()>)
})?;
Ok(Value::Undef)
}
"find" => {
let items = array_items(recv);
let cb = arg0(&args);
for (i, it) in items.iter().enumerate() {
let m = host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
if with_host(|h| h.truthy(&m)) {
return Ok(it.clone());
}
}
Ok(Value::Undef)
}
"findIndex" => {
let items = array_items(recv);
let cb = arg0(&args);
for (i, it) in items.iter().enumerate() {
let m = host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
if with_host(|h| h.truthy(&m)) {
return Ok(Value::Float(i as f64));
}
}
Ok(Value::Float(-1.0))
}
"some" => {
let cb = arg0(&args);
let thisarg = this_arg(&args, 1);
let hit = array_walk(recv, |i, v| {
let m = host::invoke(
&cb,
vec![v, Value::Float(i as f64), this_value.clone()],
thisarg.clone(),
)?;
Ok(with_host(|h| h.truthy(&m)).then_some(()))
})?;
Ok(Value::Bool(hit.is_some()))
}
"every" => {
let cb = arg0(&args);
let failed = array_walk(recv, |i, it| {
let m = host::invoke(
&cb,
vec![it, Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
Ok((!with_host(|h| h.truthy(&m))).then_some(()))
})?;
Ok(Value::Bool(failed.is_none()))
}
"reduce" => {
let items = array_items(recv);
let holes = absent_set(recv);
let cb = arg0(&args);
let acc;
let mut start = 0;
if args.len() >= 2 {
acc = args[1].clone();
} else {
match (0..items.len()).find(|i| !holes.contains(i)) {
Some(i) => {
acc = items[i].clone();
start = i + 1;
}
None => {
return Err(host::type_error(
"Reduce of empty array with no initial value",
))
}
}
}
let mut cur = acc;
array_walk(recv, |i, it| {
if i < start {
return Ok(None::<()>);
}
cur = host::invoke(
&cb,
vec![
std::mem::replace(&mut cur, Value::Undef),
it,
Value::Float(i as f64),
this_value.clone(),
],
this_arg(&args, 1),
)?;
Ok(None::<()>)
})?;
Ok(cur)
}
"reduceRight" => {
let cb = arg0(&args);
let n = array_len(recv);
let mut acc;
let mut from = n; if args.len() >= 2 {
acc = args[1].clone();
} else {
let holes = absent_set(recv);
match (0..n).rev().find(|i| !holes.contains(i)) {
Some(k) => {
acc = get_property(recv, &k.to_string())?;
from = k;
}
None => {
return Err(host::type_error(
"Reduce of empty array with no initial value",
))
}
}
}
let mut slot = Some(acc);
array_walk_rev(recv, from, |i, v| {
let prev = slot.take().expect("accumulator is refilled each step");
slot = Some(host::invoke(
&cb,
vec![prev, v, Value::Float(i as f64), this_value.clone()],
None,
)?);
Ok(None::<()>)
})?;
acc = slot.expect("accumulator is refilled each step");
Ok(acc)
}
"findLast" => {
let items = array_items(recv);
let cb = arg0(&args);
for i in (0..items.len()).rev() {
let m = host::invoke(
&cb,
vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
if with_host(|h| h.truthy(&m)) {
return Ok(items[i].clone());
}
}
Ok(Value::Undef)
}
"findLastIndex" => {
let items = array_items(recv);
let cb = arg0(&args);
for i in (0..items.len()).rev() {
let m = host::invoke(
&cb,
vec![items[i].clone(), Value::Float(i as f64), this_value.clone()],
this_arg(&args, 1),
)?;
if with_host(|h| h.truthy(&m)) {
return Ok(Value::Float(i as f64));
}
}
Ok(Value::Float(-1.0))
}
"sort" => {
let all = array_items(recv);
let holes = absent_set(recv);
let mut items: Vec<Value> = all
.iter()
.enumerate()
.filter(|(i, _)| !holes.contains(i))
.map(|(_, v)| v.clone())
.collect();
sort_values(&mut items, args.first())?;
let present = items.len();
with_host(|h| {
let len = all.len();
if let Some(JsObj::Array(a)) = h.get_mut(recv) {
if a.len() < len {
a.resize(len, Value::Undef);
}
for (i, v) in items.into_iter().enumerate() {
a[i] = v;
}
for slot in a[present..len].iter_mut() {
*slot = Value::Undef;
}
}
h.install_holes(recv, (present..len).collect());
});
Ok(this_value.clone())
}
"toSorted" => {
let mut items = array_items(recv);
sort_values(&mut items, args.first())?;
Ok(with_host(|h| h.new_array(items)))
}
"toReversed" => {
let mut items = array_items(recv);
items.reverse();
Ok(with_host(|h| h.new_array(items)))
}
"toSpliced" => {
let mut items = array_items(recv);
let len = items.len();
let start = {
let s = arg_num(&args, 0);
if s < 0.0 {
((len as f64 + s).max(0.0)) as usize
} else {
(s as usize).min(len)
}
};
let delete = if args.len() >= 2 {
(arg_num(&args, 1).max(0.0) as usize).min(len - start)
} else if args.is_empty() {
0
} else {
len - start
};
let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
items.splice(start..start + delete, inserts);
Ok(with_host(|h| h.new_array(items)))
}
"with" => {
let mut items = array_items(recv);
let len = items.len() as i64;
let rel = arg_num(&args, 0) as i64;
let idx = if rel < 0 { len + rel } else { rel };
if idx < 0 || idx >= len {
return Err(host::range_error(&format!("Invalid index : {rel}")));
}
items[idx as usize] = args.get(1).cloned().unwrap_or(Value::Undef);
Ok(with_host(|h| h.new_array(items)))
}
"flat" => {
let raw = if args.is_empty() {
1.0
} else {
arg_num(&args, 0)
};
let depth = if raw.is_nan() {
0.0
} else if raw.is_infinite() {
raw
} else {
raw.trunc()
};
let mut out = Vec::new();
flatten_into(recv, depth, &mut out)?;
array_species_create(this_value, out)
}
"keys" => Ok(array_iterator(recv, host::ArrayIterKind::Keys)),
"values" | "@@iterator" => Ok(array_iterator(recv, host::ArrayIterKind::Values)),
"entries" => Ok(array_iterator(recv, host::ArrayIterKind::Entries)),
"splice" => array_splice(recv, args),
"toString" => join_array(recv, ","),
_ if is_object_builtin_method(name) => object_builtin_method(recv, name, args),
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
fn join_array(recv: &Value, sep: &str) -> Result<Value, String> {
if !host::join_stack_push(recv) {
return Ok(with_host(|h| h.new_str(String::new())));
}
let parts = (|| -> Result<Vec<String>, String> {
let len = array_len(recv);
let mut out = Vec::with_capacity(len);
for i in 0..len {
let v = array_elem_live(recv, i)?;
out.push(join_parts(std::slice::from_ref(&v))?.remove(0));
}
Ok(out)
})();
host::join_stack_pop();
let s = parts?.join(sep);
Ok(with_host(|h| h.new_str(s)))
}
fn join_parts(items: &[Value]) -> Result<Vec<String>, String> {
let fast = with_host(|h| {
items
.iter()
.map(|x| match x {
Value::Undef => Some(String::new()),
_ if h.is_null(x) => Some(String::new()),
_ if matches!(h.get(x), Some(JsObj::Symbol { .. })) => None,
_ if host::is_primitive(h, x) => Some(h.str_of(x)),
_ => None,
})
.collect::<Vec<_>>()
});
if fast.iter().all(Option::is_some) {
return Ok(fast.into_iter().flatten().collect());
}
let mut out = Vec::with_capacity(items.len());
for (x, p) in items.iter().zip(fast) {
match p {
Some(s) => out.push(s),
None => {
let s = host::to_string_value(x)?;
out.push(with_host(|h| h.str_of(&s)));
}
}
}
Ok(out)
}
pub(crate) fn sort_values(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
let cmp = match cmp {
Some(Value::Undef) => None,
Some(v) if !with_host(|h| host::is_callable(h, v)) => {
let shown = no_side_effects_string(v);
return Err(host::type_error(&format!(
"The comparison function must be either a function or undefined: {shown}"
)));
}
other => other,
};
let mut defined = 0;
for i in 0..items.len() {
if !matches!(items[i], Value::Undef) {
items.swap(defined, i);
defined += 1;
}
}
merge_sort(&mut items[..defined], cmp)
}
fn sort_compare(a: &Value, b: &Value, cmp: Option<&Value>) -> Result<f64, String> {
match cmp {
Some(cb) => {
let v = host::invoke(cb, vec![a.clone(), b.clone()], None)?;
Ok(with_host(|h| h.to_number(&v)))
}
None => {
let x = with_host(|h| h.str_of(a));
let y = with_host(|h| h.str_of(b));
if crate::utf16::cmp_units(&x, &y) == std::cmp::Ordering::Greater {
Ok(1.0)
} else {
Ok(-1.0)
}
}
}
}
fn merge_sort(items: &mut [Value], cmp: Option<&Value>) -> Result<(), String> {
let n = items.len();
if n < 2 {
return Ok(());
}
let mut src = items.to_vec();
let mut dst = src.clone();
let mut width = 1;
while width < n {
let mut lo = 0;
while lo < n {
let mid = (lo + width).min(n);
let hi = (lo + 2 * width).min(n);
merge(&src[lo..mid], &src[mid..hi], &mut dst[lo..hi], cmp)?;
lo = hi;
}
std::mem::swap(&mut src, &mut dst);
width *= 2;
}
items.clone_from_slice(&src);
Ok(())
}
fn merge(
left: &[Value],
right: &[Value],
out: &mut [Value],
cmp: Option<&Value>,
) -> Result<(), String> {
let (mut i, mut j, mut k) = (0, 0, 0);
while i < left.len() && j < right.len() {
if sort_compare(&left[i], &right[j], cmp)? > 0.0 {
out[k] = right[j].clone();
j += 1;
} else {
out[k] = left[i].clone();
i += 1;
}
k += 1;
}
for v in left[i..].iter().chain(&right[j..]) {
out[k] = v.clone();
k += 1;
}
Ok(())
}
fn flatten_into(src: &Value, depth: f64, out: &mut Vec<Value>) -> Result<(), String> {
if host::stack_exhausted() {
return Err(host::stack_overflow_error());
}
let items = array_items(src);
let holes = absent_set(src);
for (i, it) in items.into_iter().enumerate() {
if holes.contains(&i) {
continue;
}
let nested = depth > 0.0 && with_host(|h| h.kind_of(&it)) == Some(ObjKind::Array);
if nested {
flatten_into(&it, depth - 1.0, out)?;
} else {
out.push(it);
}
}
Ok(())
}
fn array_splice(recv: &Value, args: Vec<Value>) -> Result<Value, String> {
let len = array_len(recv);
let start = {
let s = arg_num(&args, 0);
if s < 0.0 {
((len as f64 + s).max(0.0)) as usize
} else {
(s as usize).min(len)
}
};
let delete = if args.len() >= 2 {
(arg_num(&args, 1).max(0.0) as usize).min(len - start)
} else {
len - start
};
let inserts: Vec<Value> = args.iter().skip(2).cloned().collect();
let inserted = inserts.len();
let holes = hole_set(recv);
let removed = with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(recv) {
let removed: Vec<Value> = items.splice(start..start + delete, inserts).collect();
removed
} else {
Vec::new()
}
});
let spliced = with_host(|h| {
h.install_holes(
recv,
holes
.iter()
.filter_map(|&i| {
if i < start {
Some(i)
} else if i < start + delete {
None
} else {
Some(i - delete + inserted)
}
})
.collect(),
);
(removed, holes.clone())
});
let (removed, holes) = spliced;
let out = array_species_create(recv, removed)?;
with_host(|h| {
h.install_holes(
&out,
holes
.iter()
.filter(|&&i| i >= start && i < start + delete)
.map(|&i| i - start)
.collect(),
);
});
Ok(out)
}
fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
let norm = |v: f64| -> usize {
if v < 0.0 {
((len as f64 + v).max(0.0)) as usize
} else {
(v as usize).min(len)
}
};
let lo = if args.is_empty() || matches!(args[0], Value::Undef) {
0
} else {
norm(arg_num(args, 0))
};
let hi = if args.len() < 2 || matches!(args[1], Value::Undef) {
len
} else {
norm(arg_num(args, 1))
};
(lo, hi.max(lo))
}
const STRING_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
("at", &[0]),
("charAt", &[0]),
("charCodeAt", &[0]),
("codePointAt", &[0]),
("endsWith", &[1]),
("includes", &[1]),
("indexOf", &[1]),
("lastIndexOf", &[1]),
("padEnd", &[0]),
("padStart", &[0]),
("repeat", &[0]),
("slice", &[0, 1]),
("split", &[1]),
("startsWith", &[1]),
("substr", &[0, 1]),
("substring", &[0, 1]),
];
fn reject_symbol_args(name: &str, args: &[Value]) -> Result<(), String> {
let numeric = STRING_METHOD_NUMERIC_ARGS
.iter()
.find(|(m, _)| *m == name)
.map(|(_, ps)| *ps)
.unwrap_or(&[]);
for (i, a) in args.iter().enumerate() {
if with_host(|h| matches!(h.get(a), Some(JsObj::Symbol { .. }))) {
let kind = if numeric.contains(&i) {
"number"
} else {
"string"
};
return Err(host::type_error(&format!(
"Cannot convert a Symbol value to a {kind}"
)));
}
}
Ok(())
}
const ARRAY_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
("at", &[0]),
("copyWithin", &[0, 1, 2]),
("fill", &[1, 2]),
("flat", &[0]),
("includes", &[1]),
("indexOf", &[1]),
("lastIndexOf", &[1]),
("slice", &[0, 1]),
("splice", &[0, 1]),
("toSpliced", &[0, 1]),
("with", &[0]),
];
const NUMBER_METHOD_NUMERIC_ARGS: &[(&str, &[usize])] = &[
("toExponential", &[0]),
("toFixed", &[0]),
("toPrecision", &[0]),
("toString", &[0]),
];
fn to_number_arg(args: &[Value], i: usize) -> Result<f64, String> {
let v = args.get(i).cloned().unwrap_or(Value::Undef);
let p = host::to_primitive(&v, "number")?;
Ok(with_host(|h| h.to_number(&p)))
}
fn coerce_numeric_args(
table: &[(&str, &[usize])],
name: &str,
mut args: Vec<Value>,
) -> Result<Vec<Value>, String> {
let Some((_, positions)) = table.iter().find(|(m, _)| *m == name) else {
return Ok(args);
};
for &i in *positions {
let Some(a) = args.get(i) else { continue };
if matches!(a, Value::Undef) {
continue;
}
let p = host::to_primitive(a, "number")?;
args[i] = Value::Float(with_host(|h| h.to_number(&p)));
}
Ok(args)
}
fn regexp_from_arg(v: &Value, flags: &str) -> Result<Value, String> {
let src = if matches!(v, Value::Undef) {
String::new()
} else {
with_host(|h| h.str_of(v))
};
let fv = with_host(|h| h.new_str(flags.to_string()));
let sv = with_host(|h| h.new_str(src));
regexp_ctor(&[sv, fv])
}
fn coerce_string_args(name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
let numeric = STRING_METHOD_NUMERIC_ARGS
.iter()
.find(|(m, _)| *m == name)
.map(|(_, ps)| *ps)
.unwrap_or(&[]);
let protocol = match name {
"replace" | "replaceAll" => Some("@@replace"),
"split" => Some("@@split"),
"match" => Some("@@match"),
"matchAll" => Some("@@matchAll"),
"search" => Some("@@search"),
"startsWith" | "endsWith" | "includes" => Some("@@match"),
_ => None,
};
let mut out = Vec::with_capacity(args.len());
for (i, a) in args.into_iter().enumerate() {
if matches!(a, Value::Undef) {
out.push(a);
continue;
}
if numeric.contains(&i) {
let p = host::to_primitive(&a, "number")?;
out.push(Value::Float(with_host(|h| h.to_number(&p))));
continue;
}
let is_regexp_like = matches!(name, "startsWith" | "endsWith" | "includes");
let carries = |p: &str| match host::protocol_lookup(&a, p) {
Ok(Some(m)) => {
if is_regexp_like {
with_host(|h| h.truthy(&m))
} else {
with_host(|h| host::is_callable(h, &m))
}
}
_ => false,
};
let exempt = with_host(|h| matches!(h.get(&a), Some(JsObj::RegExp(_))))
|| (i == 0 && protocol.is_some_and(carries))
|| (i == 1
&& matches!(name, "replace" | "replaceAll")
&& with_host(|h| host::is_callable(h, &a)));
if exempt {
out.push(a);
continue;
}
out.push(host::to_string_value(&a)?);
}
Ok(out)
}
fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
reject_symbol_args(name, &args)?;
let args = coerce_string_args(name, args)?;
let u = crate::utf16::Units::of(s);
match name {
"@@iterator" => {
let items: Vec<Value> = s.chars().map(|c| new_s(c.to_string())).collect();
Ok(with_host(|h| {
h.alloc(JsObj::Iter {
items,
idx: 0,
array: None,
})
}))
}
"toUpperCase" => Ok(new_s(s.to_uppercase())),
"toLowerCase" => Ok(new_s(s.to_lowercase())),
"toLocaleString" => Ok(new_s(s.to_string())),
"toLocaleUpperCase" => Ok(new_s(s.to_uppercase())),
"toLocaleLowerCase" => Ok(new_s(s.to_lowercase())),
"localeCompare" => {
let other = with_host(|h| h.str_of(&arg0(&args)));
let (la, lb) = (s.to_lowercase(), other.to_lowercase());
let r = match la.cmp(&lb) {
std::cmp::Ordering::Less => -1.0,
std::cmp::Ordering::Greater => 1.0,
std::cmp::Ordering::Equal => {
let mut t = 0.0;
for (ca, cb) in s.chars().zip(other.chars()) {
if ca != cb {
t = if ca.is_lowercase() { -1.0 } else { 1.0 };
break;
}
}
t
}
};
Ok(Value::Float(r))
}
"normalize" => {
use unicode_normalization::UnicodeNormalization;
let form = match args.first() {
Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
_ => "NFC".to_string(),
};
let out = match form.as_str() {
"NFC" => s.nfc().collect::<String>(),
"NFD" => s.nfd().collect::<String>(),
"NFKC" => s.nfkc().collect::<String>(),
"NFKD" => s.nfkd().collect::<String>(),
_ => {
return Err(host::range_error(
"The normalization form should be one of NFC, NFD, NFKC, NFKD.",
))
}
};
Ok(new_s(out))
}
"isWellFormed" => Ok(Value::Bool(true)),
"toWellFormed" => Ok(new_s(s.to_string())),
"trim" => Ok(new_s(crate::utf16::js_trim(s).to_string())),
"trimStart" => Ok(new_s(crate::utf16::js_trim_start(s).to_string())),
"trimEnd" => Ok(new_s(crate::utf16::js_trim_end(s).to_string())),
"toString" | "valueOf" => Ok(new_s(s.to_string())),
"charAt" => {
let at = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit_str(i));
Ok(new_s(at.unwrap_or_default()))
}
"at" => {
let n = arg_num(&args, 0);
let i = if n.is_nan() {
Some(0i64)
} else if n.is_finite() {
let i = n.trunc() as i64;
Some(if i < 0 { i + u.len() as i64 } else { i })
} else {
None
};
match i
.and_then(|i| usize::try_from(i).ok())
.and_then(|i| u.unit_str(i))
{
Some(c) => Ok(new_s(c)),
None => Ok(Value::Undef),
}
}
"charCodeAt" => {
let unit = unit_pos(arg_num(&args, 0)).and_then(|i| u.unit(i));
Ok(Value::Float(unit.map(f64::from).unwrap_or(f64::NAN)))
}
"codePointAt" => match unit_pos(arg_num(&args, 0)).and_then(|i| u.code_point(i)) {
Some(cp) => Ok(Value::Float(f64::from(cp))),
None => Ok(Value::Undef),
},
"indexOf" => {
let needle = needle_units(&args);
let from = clamp_pos(arg_num(&args, 1), u.len());
Ok(Value::Float(
search_from(u.as_slice(), needle.as_slice(), from)
.map(|i| i as f64)
.unwrap_or(-1.0),
))
}
"lastIndexOf" => {
let needle = needle_units(&args);
let n = arg_num(&args, 1);
let upto = if n.is_nan() {
u.len()
} else {
clamp_pos(n, u.len())
};
Ok(Value::Float(
search_last(u.as_slice(), needle.as_slice(), upto)
.map(|i| i as f64)
.unwrap_or(-1.0),
))
}
"startsWith" | "endsWith" | "includes" if is_regexp_arg(&arg0(&args)) => {
Err(host::type_error(&format!(
"First argument to String.prototype.{name} must not be a regular expression"
)))
}
"includes" => {
let needle = needle_units(&args);
let from = clamp_pos(arg_num(&args, 1), u.len());
Ok(Value::Bool(
search_from(u.as_slice(), needle.as_slice(), from).is_some(),
))
}
"startsWith" => {
let needle = needle_units(&args);
let from = clamp_pos(arg_num(&args, 1), u.len());
Ok(Value::Bool(
u.as_slice()[from..].starts_with(needle.as_slice()),
))
}
"endsWith" => {
let needle = needle_units(&args);
let end = if args.len() < 2 || matches!(args[1], Value::Undef) {
u.len()
} else {
clamp_pos(arg_num(&args, 1), u.len())
};
Ok(Value::Bool(
u.as_slice()[..end].ends_with(needle.as_slice()),
))
}
"slice" => {
let (lo, hi) = slice_bounds(&args, u.len());
Ok(new_s(u.slice(lo, hi)))
}
"substring" => {
let mut a = arg_num(&args, 0).max(0.0) as usize;
let mut b = if args.len() < 2 || matches!(args[1], Value::Undef) {
u.len()
} else {
(arg_num(&args, 1).max(0.0) as usize).min(u.len())
};
a = a.min(u.len());
if a > b {
std::mem::swap(&mut a, &mut b);
}
Ok(new_s(u.slice(a, b)))
}
"substr" => {
let len = u.len() as i64;
let mut start = arg_num(&args, 0) as i64;
if start < 0 {
start = (len + start).max(0);
}
let start = (start as usize).min(u.len());
let count = if args.len() >= 2 {
arg_num(&args, 1).max(0.0) as usize
} else {
u.len()
};
let end = start.saturating_add(count).min(u.len());
Ok(new_s(u.slice(start, end)))
}
"repeat" => {
let n = arg_num(&args, 0);
if n < 0.0 || !n.is_finite() {
return Err(host::range_error(&format!(
"Invalid count value: {}",
host::fmt_number(n)
)));
}
if n * crate::utf16::len(s) as f64 > host::MAX_STRING_LENGTH as f64 {
return Err(host::invalid_string_length());
}
Ok(new_s(s.repeat(n as usize)))
}
"concat" => {
let mut out = s.to_string();
for a in &args {
out.push_str(&with_host(|h| h.str_of(a)));
}
Ok(new_s(out))
}
"padStart" => Ok(new_s(pad(s, &args, true)?)),
"padEnd" => Ok(new_s(pad(s, &args, false)?)),
"replaceAll"
if is_regexp_arg(&arg0(&args))
&& !with_host(
|h| matches!(h.get(&arg0(&args)), Some(JsObj::RegExp(r)) if r.global),
) =>
{
Err(host::type_error(
"String.prototype.replaceAll called with a non-global RegExp argument",
))
}
"match" | "matchAll" | "search" | "split" | "replace" | "replaceAll"
if symbol_protocol(
&arg0(&args),
match name {
"match" => "@@match",
"matchAll" => "@@matchAll",
"search" => "@@search",
"split" => "@@split",
_ => "@@replace",
},
)
.is_some() =>
{
let sym = match name {
"match" => "@@match",
"matchAll" => "@@matchAll",
"search" => "@@search",
"split" => "@@split",
_ => "@@replace",
};
let f = symbol_protocol(&arg0(&args), sym).expect("guard checked");
let sv = with_host(|h| h.new_str(s.to_string()));
let mut rest = vec![sv];
rest.extend(args.iter().skip(1).cloned());
host::invoke(&f, rest, Some(arg0(&args)))
}
"match" => {
let a = arg0(&args);
let re = if is_regexp_arg(&a) {
a
} else {
regexp_from_arg(&a, "")?
};
crate::regexp::str_match(s, &re)
}
"matchAll" => {
let a = arg0(&args);
let re = if is_regexp_arg(&a) {
a
} else {
regexp_from_arg(&a, "g")?
};
crate::regexp::str_match_all(s, &re)
}
"search" => {
if is_regexp_arg(&arg0(&args)) {
crate::regexp::str_search(s, &arg0(&args))
} else {
let re = regexp_from_arg(&arg0(&args), "")?;
crate::regexp::str_search(s, &re)
}
}
"replace" => {
let pat = arg0(&args);
let repl = args.get(1).cloned().unwrap_or(Value::Undef);
if is_regexp_arg(&pat) {
crate::regexp::str_replace_regex(s, &pat, &repl, false)
} else if with_host(|h| host::is_callable(h, &repl)) {
Ok(new_s(replace_str_fn(
s,
&with_host(|h| h.str_of(&pat)),
&repl,
false,
)?))
} else {
let from = with_host(|h| h.str_of(&pat));
let to = with_host(|h| h.str_of(&repl));
Ok(new_s(replace_str_plain(s, &from, &to, false)))
}
}
"replaceAll" => {
let pat = arg0(&args);
let repl = args.get(1).cloned().unwrap_or(Value::Undef);
if is_regexp_arg(&pat) {
let global = with_host(|h| match h.get(&pat) {
Some(JsObj::RegExp(r)) => r.global,
_ => true,
});
if !global {
return Err(host::type_error(
"String.prototype.replaceAll called with a non-global RegExp argument",
));
}
crate::regexp::str_replace_regex(s, &pat, &repl, true)
} else if with_host(|h| host::is_callable(h, &repl)) {
Ok(new_s(replace_str_fn(
s,
&with_host(|h| h.str_of(&pat)),
&repl,
true,
)?))
} else {
let from = with_host(|h| h.str_of(&pat));
let to = with_host(|h| h.str_of(&repl));
Ok(new_s(replace_str_plain(s, &from, &to, true)))
}
}
"split" => {
if is_regexp_arg(&arg0(&args)) {
let limit = args
.get(1)
.filter(|v| !matches!(v, Value::Undef))
.map(|v| with_host(|h| h.to_number(v)) as usize);
return crate::regexp::str_split_regex(s, &arg0(&args), limit);
}
let mut parts: Vec<Value> = if args.is_empty() || matches!(args[0], Value::Undef) {
vec![new_s(s.to_string())]
} else {
let sep = with_host(|h| h.str_of(&args[0]));
if sep.is_empty() {
(0..u.len())
.filter_map(|i| u.unit_str(i))
.map(new_s)
.collect()
} else {
s.split(&sep as &str)
.map(|p| new_s(p.to_string()))
.collect()
}
};
if let Some(lim) = args.get(1).filter(|v| !matches!(v, Value::Undef)) {
let n = with_host(|h| h.to_number(lim));
if n.is_finite() && n >= 0.0 {
parts.truncate(n as usize);
}
}
Ok(with_host(|h| h.new_array(parts)))
}
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
fn substitute_plain(templ: &str, matched: &str, position: usize, subject: &str) -> String {
let chars: Vec<char> = templ.chars().collect();
let mut out = String::new();
let mut i = 0;
while i < chars.len() {
if chars[i] == '$' && i + 1 < chars.len() {
match chars[i + 1] {
'$' => {
out.push('$');
i += 2;
continue;
}
'&' => {
out.push_str(matched);
i += 2;
continue;
}
'`' => {
out.push_str(&subject[..position]);
i += 2;
continue;
}
'\'' => {
out.push_str(&subject[position + matched.len()..]);
i += 2;
continue;
}
_ => {}
}
}
out.push(chars[i]);
i += 1;
}
out
}
fn replace_str_plain(s: &str, from: &str, to: &str, all: bool) -> String {
if from.is_empty() && !all {
return format!("{}{s}", substitute_plain(to, "", 0, s));
}
let mut out = String::new();
let mut rest = 0usize;
while let Some(rel) = s[rest..].find(from) {
let at = rest + rel;
out.push_str(&s[rest..at]);
out.push_str(&substitute_plain(to, from, at, s));
rest = at + from.len();
if !all {
break;
}
if from.is_empty() {
if rest >= s.len() {
break;
}
let step = s[rest..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
out.push_str(&s[rest..rest + step]);
rest += step;
}
}
out.push_str(&s[rest..]);
out
}
fn new_s(s: String) -> Value {
with_host(|h| h.new_str(s))
}
pub(crate) fn search_start(n: f64, len: usize) -> usize {
if n.is_nan() {
return 0;
}
let n = n.trunc();
if n >= 0.0 {
if n >= len as f64 {
len
} else {
n as usize
}
} else {
let from_end = len as f64 + n;
if from_end <= 0.0 {
0
} else {
from_end as usize
}
}
}
pub(crate) fn search_start_last(from: Option<f64>, len: usize) -> Option<usize> {
if len == 0 {
return None;
}
let n = match from {
None => return Some(len - 1),
Some(v) if v.is_nan() => 0.0,
Some(v) => v.trunc(),
};
if n >= 0.0 {
Some(if n >= len as f64 { len - 1 } else { n as usize })
} else {
let k = len as f64 + n;
if k < 0.0 {
None
} else {
Some(k as usize)
}
}
}
fn clamp_pos(n: f64, len: usize) -> usize {
if n.is_nan() || n <= 0.0 {
0
} else if n >= len as f64 {
len
} else {
n.trunc() as usize
}
}
fn unit_pos(n: f64) -> Option<usize> {
if n.is_nan() {
Some(0)
} else if n < 0.0 || !n.is_finite() {
None
} else {
Some(n.trunc() as usize)
}
}
fn needle_units(args: &[Value]) -> crate::utf16::Units {
crate::utf16::Units::of(&with_host(|h| h.str_of(&arg0(args))))
}
fn search_from(hay: &[u16], needle: &[u16], from: usize) -> Option<usize> {
if needle.is_empty() {
return Some(from.min(hay.len()));
}
if needle.len() > hay.len() {
return None;
}
(from..=hay.len().saturating_sub(needle.len())).find(|&i| &hay[i..i + needle.len()] == needle)
}
fn search_last(hay: &[u16], needle: &[u16], upto: usize) -> Option<usize> {
if needle.is_empty() {
return Some(upto.min(hay.len()));
}
if needle.len() > hay.len() {
return None;
}
let last = hay.len() - needle.len();
(0..=upto.min(last))
.rev()
.find(|&i| &hay[i..i + needle.len()] == needle)
}
fn pad(s: &str, args: &[Value], start: bool) -> Result<String, String> {
let target_f = arg_num(args, 0);
let target = if target_f.is_finite() && target_f > 0.0 {
target_f as usize
} else {
0
};
let cur = crate::utf16::len(s);
if cur >= target {
return Ok(s.to_string());
}
let filler = if args.len() >= 2 {
with_host(|h| h.str_of(&args[1]))
} else {
" ".to_string()
};
if filler.is_empty() {
return Ok(s.to_string());
}
if target_f > host::MAX_STRING_LENGTH as f64 {
return Err(host::invalid_string_length());
}
let need = target - cur;
let fill = crate::utf16::Units::of(&filler);
let units: Vec<u16> = (0..need)
.filter_map(|i| fill.unit(i % fill.len()))
.collect();
let padding = crate::utf16::to_string_lossy(&units);
Ok(if start {
format!("{padding}{s}")
} else {
format!("{s}{padding}")
})
}
const RADIX_RANGE: &str = "toString() radix argument must be between 2 and 36";
fn bigint_method(b: &num_bigint::BigInt, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"toString" => {
let radix = match args.first() {
None | Some(Value::Undef) => 10,
Some(_) => {
let t = arg_num(&args, 0).trunc();
if !(2.0..=36.0).contains(&t) {
return Err(host::range_error(RADIX_RANGE));
}
t as u32
}
};
Ok(new_s(b.to_str_radix(radix)))
}
"toLocaleString" => {
let digits = b.magnitude().to_string();
let sign = if b.sign() == num_bigint::Sign::Minus {
"-"
} else {
""
};
Ok(new_s(format!("{sign}{}", group_thousands(&digits))))
}
"valueOf" => Ok(with_host(|h| h.new_bigint(b.clone()))),
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
fn number_method(n: f64, name: &str, args: Vec<Value>) -> Result<Value, String> {
let args = coerce_numeric_args(NUMBER_METHOD_NUMERIC_ARGS, name, args)?;
match name {
"toFixed" => {
let digits = arg_num(&args, 0);
if !(0.0..=100.0).contains(&digits.trunc()) {
return Err(host::range_error(
"toFixed() digits argument must be between 0 and 100",
));
}
Ok(new_s(to_fixed(n, digits as usize)))
}
"toExponential" => {
let f = match args.first() {
None | Some(Value::Undef) => None,
Some(_) => {
let d = arg_num(&args, 0).trunc();
if !(0.0..=100.0).contains(&d) {
return Err(host::range_error(
"toExponential() argument must be between 0 and 100",
));
}
Some(d as usize)
}
};
Ok(new_s(to_exponential(n, f)))
}
"toString" => {
let radix = match args.first() {
None | Some(Value::Undef) => 10,
Some(_) => {
let r = arg_num(&args, 0);
let t = r.trunc();
if !(2.0..=36.0).contains(&t) {
return Err(host::range_error(RADIX_RANGE));
}
t as u32
}
};
if radix == 10 {
Ok(new_s(host::fmt_number(n)))
} else {
Ok(new_s(to_radix(n, radix)))
}
}
"toPrecision" => {
match args.first() {
None | Some(Value::Undef) => Ok(new_s(host::fmt_number(n))),
Some(_) => {
let p = arg_num(&args, 0).trunc();
if !(1.0..=100.0).contains(&p) {
return Err(host::range_error(
"toPrecision() argument must be between 1 and 100",
));
}
Ok(new_s(to_precision(n, p as usize)))
}
}
}
"toLocaleString" => Ok(new_s(to_locale_string(n))),
"valueOf" => Ok(Value::Float(n)),
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
fn to_locale_string(n: f64) -> String {
if n.is_nan() {
return "NaN".to_string();
}
if n.is_infinite() {
return if n < 0.0 { "-∞" } else { "∞" }.to_string();
}
let neg = n.is_sign_negative();
let fixed = expand_exponential(&to_fixed(n.abs(), 3));
let trimmed = match fixed.split_once('.') {
Some(_) => fixed.trim_end_matches('0').trim_end_matches('.'),
None => fixed.as_str(),
};
let (int_part, frac_part) = match trimmed.split_once('.') {
Some((i, f)) => (i, Some(f)),
None => (trimmed, None),
};
let mut out = String::new();
if neg {
out.push('-'); }
out.push_str(&group_thousands(int_part));
if let Some(f) = frac_part {
out.push('.');
out.push_str(f);
}
out
}
fn expand_exponential(s: &str) -> String {
let Some((mantissa, exp)) = s.split_once(['e', 'E']) else {
return s.to_string();
};
let Ok(exp) = exp.trim_start_matches('+').parse::<i32>() else {
return s.to_string();
};
if exp <= 0 {
return s.to_string();
}
let (int_digits, frac_digits) = match mantissa.split_once('.') {
Some((i, f)) => (i.to_string(), f.to_string()),
None => (mantissa.to_string(), String::new()),
};
let mut digits = int_digits;
digits.push_str(&frac_digits);
let zeros = exp as usize - frac_digits.len().min(exp as usize);
digits.push_str(&"0".repeat(zeros));
digits
}
fn group_thousands(int_part: &str) -> String {
let bytes = int_part.as_bytes();
let n = bytes.len();
let mut out = String::with_capacity(n + n / 3);
for (i, &b) in bytes.iter().enumerate() {
if i > 0 && (n - i) % 3 == 0 {
out.push(',');
}
out.push(b as char);
}
out
}
fn to_fixed(n: f64, f: usize) -> String {
if !n.is_finite() {
return host::fmt_number(n);
}
if n.abs() >= 1e21 {
return host::fmt_number(n);
}
let neg = n < 0.0;
let full = format!("{:.*}", f + 25, n.abs());
let mut body = round_decimal_string(&full, f);
if neg {
body.insert(0, '-'); }
body
}
fn round_decimal_string(s: &str, f: usize) -> String {
let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
let mut digits: Vec<u8> = int_part
.bytes()
.chain(frac_part.bytes())
.map(|b| b - b'0')
.collect();
let point = int_part.len(); let keep = point + f;
if digits.get(keep).map(|&d| d >= 5).unwrap_or(false) {
let mut i = keep;
loop {
if i == 0 {
digits.insert(0, 1);
return assemble_decimal(&digits, point + 1, f);
}
i -= 1;
if digits[i] == 9 {
digits[i] = 0;
} else {
digits[i] += 1;
break;
}
}
}
assemble_decimal(&digits, point, f)
}
fn assemble_decimal(digits: &[u8], point: usize, f: usize) -> String {
let int_str: String = digits[..point].iter().map(|d| (d + b'0') as char).collect();
let int_str = int_str.trim_start_matches('0');
let int_str = if int_str.is_empty() { "0" } else { int_str };
if f == 0 {
return int_str.to_string();
}
let frac: String = digits[point..point + f]
.iter()
.map(|d| (d + b'0') as char)
.collect();
format!("{int_str}.{frac}")
}
fn round_significant(a: f64, p: usize) -> (String, i32) {
let sci = format!("{a:.*e}", p - 1 + 25);
let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
let mut e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
let all: Vec<u8> = mant
.chars()
.filter(|c| c.is_ascii_digit())
.map(|c| c as u8 - b'0')
.collect();
let mut s: String = all[..p].iter().map(|d| (d + b'0') as char).collect();
if all.get(p).map(|&d| d >= 5).unwrap_or(false) {
let mut d: Vec<u8> = all[..p].to_vec();
let mut i = p;
loop {
if i == 0 {
d.insert(0, 1);
d.truncate(p);
e += 1;
break;
}
i -= 1;
if d[i] == 9 {
d[i] = 0;
} else {
d[i] += 1;
break;
}
}
s = d.iter().map(|x| (x + b'0') as char).collect();
}
(s, e)
}
fn to_exponential(n: f64, f: Option<usize>) -> String {
if !n.is_finite() {
return host::fmt_number(n);
}
let neg = n < 0.0;
let a = n.abs();
let (s, e) = if a == 0.0 {
("0".repeat(f.unwrap_or(0) + 1), 0)
} else {
match f {
Some(f) => round_significant(a, f + 1),
None => {
let sci = format!("{a:e}");
let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
let trimmed = digits.trim_end_matches('0');
let digits = if trimmed.is_empty() { "0" } else { trimmed };
(digits.to_string(), exp_str.parse().unwrap_or(0))
}
}
};
let sign = if e >= 0 { '+' } else { '-' };
let mag = e.abs();
let body = if s.len() == 1 {
format!("{s}e{sign}{mag}")
} else {
format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
};
if neg {
format!("-{body}")
} else {
body
}
}
fn to_precision(n: f64, p: usize) -> String {
if !n.is_finite() {
return host::fmt_number(n);
}
if n == 0.0 {
return if p == 1 {
"0".into()
} else {
format!("0.{}", "0".repeat(p - 1))
};
}
let neg = n < 0.0;
let (s, e) = round_significant(n.abs(), p);
let pp = p as i32;
let body = if e < -6 || e >= pp {
let sign = if e >= 0 { '+' } else { '-' };
let mag = e.abs();
if p == 1 {
format!("{s}e{sign}{mag}")
} else {
format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
}
} else if e >= 0 {
let ip = (e + 1) as usize;
if ip == p {
s
} else {
format!("{}.{}", &s[..ip], &s[ip..])
}
} else {
format!("0.{}{}", "0".repeat((-e - 1) as usize), s)
};
if neg {
format!("-{body}")
} else {
body
}
}
fn to_radix(n: f64, radix: u32) -> String {
if !n.is_finite() {
return host::fmt_number(n);
}
let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
let rf = radix as f64;
let neg = n < 0.0;
let value = n.abs();
let mut integer = value.floor();
let mut fraction = value - integer;
let mut frac: Vec<u8> = Vec::new();
let mut delta = 0.5 * (next_up(value) - value);
delta = delta.max(next_up(0.0));
if fraction >= delta {
loop {
fraction *= rf;
delta *= rf;
let digit = fraction as usize;
frac.push(digits[digit]);
fraction -= digit as f64;
if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
loop {
match frac.pop() {
None => {
integer += 1.0;
break;
}
Some(c) => {
let d = if c > b'9' {
(c - b'a' + 10) as u32
} else {
(c - b'0') as u32
};
if d + 1 < radix {
frac.push(digits[(d + 1) as usize]);
break;
}
}
}
}
break;
}
if fraction < delta {
break;
}
}
}
let mut int_out: Vec<u8> = Vec::new();
while v8_exponent(integer / rf) > 0 {
integer /= rf;
int_out.push(b'0');
}
loop {
let remainder = integer % rf;
int_out.push(digits[remainder as usize]);
integer = (integer - remainder) / rf;
if integer <= 0.0 {
break;
}
}
int_out.reverse();
let mut out: Vec<u8> = Vec::new();
if neg {
out.push(b'-');
}
out.extend_from_slice(&int_out);
if !frac.is_empty() {
out.push(b'.');
out.extend_from_slice(&frac);
}
String::from_utf8(out).unwrap()
}
fn next_up(x: f64) -> f64 {
f64::from_bits(x.to_bits() + 1)
}
fn v8_exponent(x: f64) -> i32 {
let biased = ((x.to_bits() >> 52) & 0x7ff) as i32;
if biased == 0 {
-1074 } else {
biased - 1075
}
}
fn normalize_zero_key(v: Value) -> Value {
match v {
Value::Float(f) if f == 0.0 && f.is_sign_negative() => Value::Float(0.0),
other => other,
}
}
fn map_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"get" => {
let key = with_host(|h| host::map_key(h, &arg0(&args)));
Ok(with_host(|h| match h.get(recv) {
Some(JsObj::Map { entries, .. }) => entries
.get(&key)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Undef),
_ => Value::Undef,
}))
}
"set" => {
let kv = normalize_zero_key(arg0(&args));
let vv = args.get(1).cloned().unwrap_or(Value::Undef);
reject_non_object_weak_key(recv, &kv, "WeakMap")?;
let key = with_host(|h| host::map_key(h, &kv));
with_host(|h| {
if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
entries.insert(key, (kv, vv));
}
});
Ok(recv.clone())
}
"has" => {
let key = with_host(|h| host::map_key(h, &arg0(&args)));
Ok(Value::Bool(with_host(
|h| matches!(h.get(recv), Some(JsObj::Map { entries, .. }) if entries.contains_key(&key)),
)))
}
"delete" => {
let key = with_host(|h| host::map_key(h, &arg0(&args)));
Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
Some(JsObj::Map { entries, .. }) => entries.shift_remove(&key).is_some(),
_ => false,
})))
}
"clear" => {
with_host(|h| {
if let Some(JsObj::Map { entries, .. }) = h.get_mut(recv) {
entries.clear();
}
});
Ok(Value::Undef)
}
"forEach" => {
let cb = arg0(&args);
let pairs: Vec<(Value, Value)> = with_host(|h| match h.get(recv) {
Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
_ => Vec::new(),
});
for (k, v) in pairs {
host::invoke(&cb, vec![v, k, recv.clone()], this_arg(&args, 1))?;
}
Ok(Value::Undef)
}
"keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
recv,
if name == "@@iterator" {
"entries"
} else {
name
},
)),
_ => Err(host::type_error(&format!("map.{name} is not a function"))),
}
}
fn reject_non_object_weak_key(recv: &Value, key: &Value, kind: &str) -> Result<(), String> {
let weak = with_host(|h| {
matches!(
h.get(recv),
Some(JsObj::Map { weak: true, .. }) | Some(JsObj::Set { weak: true, .. })
)
});
if !weak {
return Ok(());
}
let is_object = with_host(|h| match key {
Value::Obj(_) => !h.is_null(key) && h.as_str(key).is_none() && h.as_bigint(key).is_none(),
_ => false,
});
if is_object {
return Ok(());
}
Err(host::type_error(if kind == "WeakMap" {
"Invalid value used as weak map key"
} else {
"Invalid value used in weak set"
}))
}
struct SetRecord {
obj: Value,
size: f64,
has: Value,
keys: Value,
}
fn get_set_record(other: &Value, method: &str) -> Result<SetRecord, String> {
if !with_host(|h| is_object_like(h, other)) {
return Err(host::type_error(&format!(
"Set.prototype.{method} argument must be an object"
)));
}
let raw = get_property(other, "size")?;
let num = host::to_number_value(&raw)?;
if num.is_nan() {
return Err(host::type_error("The .size property is NaN"));
}
let size = num.trunc();
if size < 0.0 {
return Err(host::range_error(&format!("'{size}' is an invalid size")));
}
let has = get_property(other, "has")?;
if !with_host(|h| host::is_callable(h, &has)) {
return Err(host::type_error("string \"has\" is not a function"));
}
let keys = get_property(other, "keys")?;
if !with_host(|h| host::is_callable(h, &keys)) {
return Err(host::type_error("string \"keys\" is not a function"));
}
Ok(SetRecord {
obj: other.clone(),
size,
has,
keys,
})
}
impl SetRecord {
fn has(&self, v: &Value) -> Result<bool, String> {
let r = host::invoke(&self.has, vec![v.clone()], Some(self.obj.clone()))?;
Ok(with_host(|h| h.truthy(&r)))
}
fn keys(&self) -> Result<Vec<Value>, String> {
let it = host::invoke(&self.keys, Vec::new(), Some(self.obj.clone()))?;
if !with_host(|h| is_object_like(h, &it)) {
return Err(host::type_error(
"Result of the keys method is not an object",
));
}
host::drain_iterator(&it)
}
}
fn require_set_receiver(recv: &Value, method: &str) -> Result<(), String> {
if with_host(|h| matches!(h.get(recv), Some(JsObj::Set { weak: false, .. }))) {
return Ok(());
}
Err(host::type_error(&format!(
"Method Set.prototype.{method} called on incompatible receiver {}",
with_host(|h| object_tag(h, recv))
)))
}
fn set_values(recv: &Value) -> Vec<Value> {
with_host(|h| match h.get(recv) {
Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
_ => Vec::new(),
})
}
fn set_size(recv: &Value) -> f64 {
with_host(|h| match h.get(recv) {
Some(JsObj::Set { entries, .. }) => entries.len() as f64,
_ => 0.0,
})
}
fn new_set(items: Vec<Value>) -> Result<Value, String> {
let s = with_host(|h| {
h.alloc(JsObj::Set {
entries: IndexMap::new(),
weak: false,
})
});
for v in items {
set_method(&s, "add", vec![v])?;
}
Ok(s)
}
fn set_contains(s: &Value, v: &Value) -> bool {
let key = with_host(|h| host::map_key(h, v));
with_host(
|h| matches!(h.get(s), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
)
}
fn set_operation(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
require_set_receiver(recv, name)?;
let other = get_set_record(&arg0(&args), name)?;
let my_size = set_size(recv);
match name {
"union" => {
let keys = other.keys()?;
let mut out = set_values(recv);
out.extend(keys);
new_set(out)
}
"intersection" => {
let mut out = Vec::new();
if my_size <= other.size {
for v in set_values(recv) {
if other.has(&v)? {
out.push(v);
}
}
} else {
for k in other.keys()? {
if set_contains(recv, &k) {
out.push(k);
}
}
}
new_set(out)
}
"difference" => {
if my_size <= other.size {
let mut out = Vec::new();
for v in set_values(recv) {
if !other.has(&v)? {
out.push(v);
}
}
return new_set(out);
}
let out = new_set(set_values(recv))?;
for k in other.keys()? {
set_method(&out, "delete", vec![k])?;
}
Ok(out)
}
"symmetricDifference" => {
let keys = other.keys()?;
let out = new_set(set_values(recv))?;
for k in keys {
if set_contains(recv, &k) {
set_method(&out, "delete", vec![k])?;
} else {
set_method(&out, "add", vec![k])?;
}
}
Ok(out)
}
"isSubsetOf" => {
if my_size > other.size {
return Ok(Value::Bool(false));
}
for v in set_values(recv) {
if !other.has(&v)? {
return Ok(Value::Bool(false));
}
}
Ok(Value::Bool(true))
}
"isSupersetOf" => {
if my_size < other.size {
return Ok(Value::Bool(false));
}
for k in other.keys()? {
if !set_contains(recv, &k) {
return Ok(Value::Bool(false));
}
}
Ok(Value::Bool(true))
}
"isDisjointFrom" => {
if my_size <= other.size {
for v in set_values(recv) {
if other.has(&v)? {
return Ok(Value::Bool(false));
}
}
} else {
for k in other.keys()? {
if set_contains(recv, &k) {
return Ok(Value::Bool(false));
}
}
}
Ok(Value::Bool(true))
}
_ => Err(host::type_error(&format!("set.{name} is not a function"))),
}
}
fn set_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"add" => {
let vv = normalize_zero_key(arg0(&args));
reject_non_object_weak_key(recv, &vv, "WeakSet")?;
let key = with_host(|h| host::map_key(h, &vv));
with_host(|h| {
if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
entries.insert(key, vv);
}
});
Ok(recv.clone())
}
"has" => {
let key = with_host(|h| host::map_key(h, &arg0(&args)));
Ok(Value::Bool(with_host(
|h| matches!(h.get(recv), Some(JsObj::Set { entries, .. }) if entries.contains_key(&key)),
)))
}
"delete" => {
let key = with_host(|h| host::map_key(h, &arg0(&args)));
Ok(Value::Bool(with_host(|h| match h.get_mut(recv) {
Some(JsObj::Set { entries, .. }) => entries.shift_remove(&key).is_some(),
_ => false,
})))
}
"clear" => {
with_host(|h| {
if let Some(JsObj::Set { entries, .. }) = h.get_mut(recv) {
entries.clear();
}
});
Ok(Value::Undef)
}
"forEach" => {
let cb = arg0(&args);
let vals: Vec<Value> = with_host(|h| match h.get(recv) {
Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
_ => Vec::new(),
});
for v in vals {
host::invoke(&cb, vec![v.clone(), v, recv.clone()], this_arg(&args, 1))?;
}
Ok(Value::Undef)
}
"union"
| "intersection"
| "difference"
| "symmetricDifference"
| "isSubsetOf"
| "isSupersetOf"
| "isDisjointFrom" => set_operation(recv, name, args),
"keys" | "values" | "entries" | "@@iterator" => Ok(collection_iterator(
recv,
if name == "entries" {
"entries"
} else {
"values"
},
)),
_ => Err(host::type_error(&format!("set.{name} is not a function"))),
}
}
fn generator_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
if matches!(name, "@@iterator" | "@@asyncIterator") {
return Ok(recv.clone());
}
if host::is_async_generator(recv) {
return match name {
"next" => Ok(host::async_gen_enqueue(
recv,
host::GenReq::Next(arg0(&args)),
)),
"return" => Ok(host::async_gen_enqueue(
recv,
host::GenReq::Return(arg0(&args)),
)),
"throw" => Ok(host::async_gen_enqueue(
recv,
host::GenReq::Throw(arg0(&args)),
)),
"@@asyncIterator" => Ok(recv.clone()),
_ => Err(host::type_error(&format!(
"asyncGenerator.{name} is not a function"
))),
};
}
match name {
"next" => {
let send = arg0(&args);
match host::gen_resume(recv, send)? {
host::GenStep::Yield(v) => Ok(iter_result(v, false)),
host::GenStep::Done(v) => Ok(iter_result(v, true)),
}
}
"return" => {
match host::gen_return(recv, arg0(&args))? {
host::GenStep::Yield(v) => Ok(iter_result(v, false)),
host::GenStep::Done(v) => Ok(iter_result(v, true)),
}
}
"throw" => {
match host::gen_throw(recv, arg0(&args))? {
host::GenStep::Yield(v) => Ok(iter_result(v, false)),
host::GenStep::Done(v) => Ok(iter_result(v, true)),
}
}
_ => Err(host::type_error(&format!(
"generator.{name} is not a function"
))),
}
}
fn iter_result(value: Value, done: bool) -> Value {
with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), value);
m.insert("done".into(), Value::Bool(done));
h.new_object(m)
})
}
pub(crate) fn array_iterator(arr: &Value, kind: host::ArrayIterKind) -> Value {
with_host(|h| {
h.alloc(JsObj::Iter {
items: Vec::new(),
idx: 0,
array: Some((arr.clone(), kind)),
})
})
}
pub(crate) fn iter_step(it: &Value) -> Option<Option<Value>> {
use host::ArrayIterKind;
let step = with_host(|h| {
let (arr, kind, i) = match h.get_mut(it) {
Some(JsObj::Iter {
items,
idx,
array: None,
}) => {
let v = items.get(*idx).cloned();
if v.is_some() {
*idx += 1;
}
return Some(Ok(v));
}
Some(JsObj::Iter {
idx,
array: Some((arr, kind)),
..
}) => (arr.clone(), *kind, *idx),
_ => return None,
};
let len = match h.get(&arr) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
};
let done = i == usize::MAX || i >= len;
if let Some(JsObj::Iter { idx, .. }) = h.get_mut(it) {
*idx = if done { usize::MAX } else { i + 1 };
}
if done {
return Some(Ok(None));
}
let key = Value::Float(i as f64);
let slot = match (kind, h.get(&arr)) {
(ArrayIterKind::Keys, _) => return Some(Ok(Some(key))),
(_, Some(JsObj::Array(items)))
if !h.is_hole(&arr, i) && h.own_accessor_keys(&arr).is_empty() =>
{
items[i].clone()
}
_ => return Some(Err((arr, kind, i))),
};
Some(Ok(Some(match kind {
ArrayIterKind::Entries => h.new_array(vec![key, slot]),
_ => slot,
})))
})?;
let (arr, kind, i) = match step {
Ok(step) => return Some(step),
Err(slow) => slow,
};
let value = get_property(&arr, &i.to_string()).unwrap_or(Value::Undef);
Some(Some(match kind {
ArrayIterKind::Entries => with_host(|h| h.new_array(vec![Value::Float(i as f64), value])),
_ => value,
}))
}
fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"next" => Ok(match iter_step(recv).flatten() {
Some(v) => iter_result(v, false),
None => iter_result(Value::Undef, true),
}),
"return" => {
with_host(|h| {
if let Some(JsObj::Iter { items, idx, array }) = h.get_mut(recv) {
*idx = if array.is_some() {
usize::MAX
} else {
items.len()
};
}
});
Ok(iter_result(arg0(&args), true))
}
"@@iterator" => Ok(recv.clone()),
_ => Err(host::type_error(&format!(
"iterator.{name} is not a function"
))),
}
}
fn symbol_method(recv: &Value, name: &str, _args: Vec<Value>) -> Result<Value, String> {
match name {
"toString" => Ok(with_host(|h| {
let s = h.str_of(recv);
h.new_str(s)
})),
"@@toPrimitive" | "valueOf" => Ok(recv.clone()),
_ => Err(host::type_error(&format!(
"symbol.{name} is not a function"
))),
}
}
fn object_create(args: Vec<Value>) -> Result<Value, String> {
let proto = arg0(&args);
reject_bad_prototype(&proto)?;
let obj = with_host(|h| h.new_object(IndexMap::new()));
with_host(|h| h.set_proto(&obj, proto));
if let Some(descs) = args.get(1).filter(|d| !matches!(d, Value::Undef)) {
let entries: Vec<(String, Value)> = with_host(|h| match h.get(descs) {
Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => Vec::new(),
});
for (k, d) in entries {
apply_descriptor(&obj, &k, &d)?;
}
}
Ok(obj)
}
fn intrinsic_proto_members(ns: &str) -> Option<&'static [&'static str]> {
let ctor = ns.strip_suffix(".prototype")?;
crate::arity::PROTO_MEMBERS
.binary_search_by(|(k, _)| (*k).cmp(ctor))
.ok()
.map(|i| crate::arity::PROTO_MEMBERS[i].1)
}
fn builtin_proto_method_names(ns: &str) -> Option<&'static [&'static str]> {
match ns {
"EventEmitter.prototype" => Some(crate::stdlib::events::METHODS),
_ => None,
}
}
fn proxy_or_own_symbol_keys(v: &Value) -> Result<Vec<Value>, String> {
if let Some(keys) = crate::proxy::own_keys(v)? {
return Ok(keys
.iter()
.filter(|k| host::is_symbol_key(k))
.map(|k| crate::proxy::key_value(k))
.collect());
}
if let Some(ns) = intrinsic_proto_of(v).map(|c| format!("{c}.prototype")) {
if let Some(members) = intrinsic_proto_members(&ns) {
return Ok(with_host(|h| {
members
.iter()
.filter_map(|m| m.strip_prefix('+').unwrap_or(m).strip_prefix("@@"))
.map(|name| h.well_known_symbol(name))
.collect()
}));
}
}
Ok(with_host(|h| h.own_symbol_keys(v)))
}
pub fn define_property_pub(obj: &Value, key: Value, desc: Value) -> Result<Value, String> {
object_define_property(vec![obj.clone(), key, desc])
}
pub fn own_descriptor_pub(obj: &Value, key: Value) -> Result<Value, String> {
object_get_own_descriptor(vec![obj.clone(), key])
}
fn object_define_property(args: Vec<Value>) -> Result<Value, String> {
let obj = arg0(&args);
if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
let desc = args.get(2).cloned().unwrap_or(Value::Undef);
if !with_host(|h| is_object_like(h, &desc)) {
return Err(host::type_error(&format!(
"Property description must be an object: {}",
with_host(|h| h.str_of(&desc))
)));
}
if !crate::proxy::define_property(&obj, &key, &desc)? {
return Err(host::type_error(&format!(
"'defineProperty' on proxy: trap returned falsish for property '{key}'"
)));
}
return Ok(obj);
}
if !with_host(|h| is_object_like(h, &obj)) {
return Err(host::type_error(
"Object.defineProperty called on non-object",
));
}
let desc = args.get(2).cloned().unwrap_or(Value::Undef);
if !with_host(|h| is_object_like(h, &desc)) {
return Err(host::type_error(&format!(
"Property description must be an object: {}",
with_host(|h| h.str_of(&desc))
)));
}
let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
apply_descriptor(&obj, &key, &desc)?;
Ok(obj)
}
fn would_cycle(obj: &Value, p: &Value) -> bool {
let mut cur = Some(p.clone());
for _ in 0..1000 {
let Some(c) = cur else { return false };
if with_host(|h| h.strict_eq(&c, obj)) {
return true;
}
if with_host(|h| h.kind_of(&c)) == Some(ObjKind::Proxy) {
return false;
}
cur = with_host(|h| h.proto_of(&c));
}
false
}
fn same_prototype(obj: &Value, p: &Value) -> bool {
let cur = prototype_of(obj);
with_host(|h| h.strict_eq(&cur, p) || (h.is_null(&cur) && h.is_null(p)))
}
fn create_list_from_array_like(v: &Value) -> Result<Vec<Value>, String> {
if !with_host(|h| is_object_like(h, v)) {
return Err(host::type_error(
"CreateListFromArrayLike called on non-object",
));
}
let len = get_property(v, "length")?;
let n = with_host(|h| h.to_number(&len));
let n = if n.is_finite() && n > 0.0 {
n as usize
} else {
0
};
(0..n).map(|i| get_property(v, &i.to_string())).collect()
}
fn reflect_require_object(v: &Value, method: &str) -> Result<(), String> {
if with_host(|h| is_object_like(h, v)) {
return Ok(());
}
Err(host::type_error(&format!(
"Reflect.{method} called on non-object"
)))
}
fn is_object_like(h: &host::JsHost, v: &Value) -> bool {
matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v)
}
fn require_object_coercible(v: &Value) -> Result<(), String> {
if with_host(|h| matches!(v, Value::Undef) || h.is_null(v)) {
return Err(host::type_error(
"Cannot convert undefined or null to object",
));
}
Ok(())
}
fn reject_bad_prototype(proto: &Value) -> Result<(), String> {
if with_host(|h| h.is_null(proto) || is_object_like(h, proto)) {
return Ok(());
}
Err(host::type_error(&format!(
"Object prototype may only be an Object or null: {}",
with_host(|h| h.str_of(proto))
)))
}
struct Requested {
value: Option<Value>,
get: Option<Option<Value>>,
set: Option<Option<Value>>,
writable: Option<bool>,
enumerable: Option<bool>,
configurable: Option<bool>,
}
impl Requested {
fn read(desc: &Value) -> Self {
let has = |k: &str| {
with_host(|h| {
host::lookup_chain(h, desc, k).is_some()
|| host::lookup_accessor(h, desc, k).is_some()
})
};
let val = |k: &str| get_property(desc, k).unwrap_or(Value::Undef);
let flag = |k: &str| {
has(k).then(|| {
let v = val(k);
with_host(|h| h.truthy(&v))
})
};
Requested {
value: has("value").then(|| val("value")),
get: has("get").then(|| match val("get") {
Value::Undef => None,
g => Some(g),
}),
set: has("set").then(|| match val("set") {
Value::Undef => None,
st => Some(st),
}),
writable: flag("writable"),
enumerable: flag("enumerable"),
configurable: flag("configurable"),
}
}
fn is_accessor(&self) -> bool {
self.get.is_some() || self.set.is_some()
}
fn is_data(&self) -> bool {
self.value.is_some() || self.writable.is_some()
}
}
struct Existing {
accessor: bool,
value: Value,
get: Option<Value>,
set: Option<Value>,
writable: bool,
enumerable: bool,
configurable: bool,
}
fn existing_property(obj: &Value, key: &str) -> Option<Existing> {
let k = with_host(|h| h.new_str(key.to_string()));
let d = own_descriptor_pub(obj, k).ok()?;
if matches!(d, Value::Undef) {
return None;
}
let field = |n: &str| get_property(&d, n).unwrap_or(Value::Undef);
let truthy = |n: &str| {
let v = field(n);
with_host(|h| h.truthy(&v))
};
let accessor = with_host(|h| host::lookup_chain(h, &d, "get").is_some());
Some(Existing {
accessor,
value: field("value"),
get: match field("get") {
Value::Undef => None,
g => Some(g),
},
set: match field("set") {
Value::Undef => None,
st => Some(st),
},
writable: truthy("writable"),
enumerable: truthy("enumerable"),
configurable: truthy("configurable"),
})
}
pub(crate) fn same_value(a: &Value, b: &Value) -> bool {
let num = |v: &Value| match v {
Value::Int(n) => Some(*n as f64),
Value::Float(f) => Some(*f),
_ => None,
};
match (num(a), num(b)) {
(Some(x), Some(y)) => {
if x.is_nan() && y.is_nan() {
true
} else if x == 0.0 && y == 0.0 {
x.is_sign_negative() == y.is_sign_negative()
} else {
x == y
}
}
_ => with_host(|h| h.strict_eq(a, b)),
}
}
fn apply_descriptor(obj: &Value, key: &str, desc: &Value) -> Result<(), String> {
let req = Requested::read(desc);
let cur = existing_property(obj, key);
if key == "length" && with_host(|h| h.kind_of(obj)) == Some(ObjKind::Array) {
if let Some(v) = req.value.clone() {
return set_property_pub(obj, "length", v);
}
}
if with_host(|h| h.kind_of(obj)) == Some(ObjKind::Builtin) {
if let Some(v) = req.value.clone() {
return set_property_pub(obj, key, v);
}
}
let exotic_own = (crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray")
&& key.parse::<usize>().is_ok())
|| (key == "lastIndex" && with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))));
if exotic_own {
if let Some(v) = req.value.clone() {
return set_property_pub(obj, key, v);
}
}
if cur.is_none() && !with_host(|h| h.is_extensible(obj)) {
return Err(host::type_error(&format!(
"Cannot define property {key}, object is not extensible"
)));
}
if let Some(c) = &cur {
if !c.configurable {
let rejected = req.configurable == Some(true)
|| req.enumerable.is_some_and(|e| e != c.enumerable)
|| (req.is_accessor() && !c.accessor)
|| (req.is_data() && c.accessor)
|| (c.accessor
&& ((req.get.is_some() && req.get.clone().flatten() != c.get)
|| (req.set.is_some() && req.set.clone().flatten() != c.set)))
|| (!c.accessor
&& !c.writable
&& (req.writable == Some(true)
|| req.value.as_ref().is_some_and(|v| !same_value(v, &c.value))));
if rejected {
return Err(host::type_error(&format!(
"Cannot redefine property: {key}"
)));
}
}
}
let attrs = host::PropAttrs {
writable: req
.writable
.unwrap_or(cur.as_ref().is_some_and(|c| c.writable)),
enumerable: req
.enumerable
.unwrap_or(cur.as_ref().is_some_and(|c| c.enumerable)),
configurable: req
.configurable
.unwrap_or(cur.as_ref().is_some_and(|c| c.configurable)),
};
with_host(|h| h.set_prop_attrs(obj, key, attrs));
if req.is_accessor() {
let get = req
.get
.clone()
.unwrap_or_else(|| cur.as_ref().and_then(|c| c.get.clone()));
let set = req
.set
.clone()
.unwrap_or_else(|| cur.as_ref().and_then(|c| c.set.clone()));
if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>())
{
with_host(|h| {
let old_len = match h.get(obj) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
};
if i >= old_len {
if let Some(JsObj::Array(items)) = h.get_mut(obj) {
items.resize(i + 1, Value::Undef);
}
h.mark_hole_range(obj, old_len..i + 1);
}
});
}
with_host(|h| h.set_accessor(obj, key, get, set));
return Ok(());
}
if let Some(c) = &cur {
if c.accessor {
if !req.is_data() {
return Ok(());
}
let v = req.value.clone().unwrap_or(Value::Undef);
with_host(|h| h.accessor_to_data(obj, key, v));
return Ok(());
}
}
let Some(v) = req.value else {
return Ok(());
};
write_data_slot(obj, key, v);
Ok(())
}
fn write_data_slot(obj: &Value, key: &str, v: Value) {
if matches!(
with_host(|h| h.get(obj).cloned()),
Some(JsObj::Func(_)) | Some(JsObj::Class(_))
) || uses_side_table(obj)
{
with_host(|h| h.set_fn_prop(obj, key, v));
return;
}
if let (Some(ObjKind::Array), Ok(i)) = (with_host(|h| h.kind_of(obj)), key.parse::<usize>()) {
with_host(|h| {
let old = match h.get(obj) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
};
if let Some(JsObj::Array(items)) = h.get_mut(obj) {
if i >= old {
items.resize(i + 1, Value::Undef);
}
items[i] = v;
}
if i > old {
h.mark_hole_range(obj, old..i);
}
h.clear_hole(obj, i);
});
return;
}
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(obj) {
p.insert(key.to_string(), v);
host::canonicalize_own_keys(p);
}
});
}
fn object_define_properties(args: Vec<Value>) -> Result<Value, String> {
let obj = arg0(&args);
let descs = args.get(1).cloned().unwrap_or(Value::Undef);
let entries: Vec<(String, Value)> = with_host(|h| match h.get(&descs) {
Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => Vec::new(),
});
for (k, d) in entries {
apply_descriptor(&obj, &k, &d)?;
}
Ok(obj)
}
fn synthesized_own_descriptor(obj: &Value, key: &str) -> Option<(Value, host::PropAttrs)> {
let ro_configurable = host::PropAttrs {
writable: false,
enumerable: false,
configurable: true,
};
if with_host(|h| host::is_callable(h, obj)) && !matches!(key, "length" | "name" | "prototype") {
return None;
}
if with_host(|h| host::is_callable(h, obj)) {
if key == "prototype" {
let p = get_property(obj, "prototype").ok()?;
if matches!(p, Value::Undef) {
return None;
}
return Some((
p,
host::PropAttrs {
writable: with_host(|h| h.kind_of(obj)) != Some(ObjKind::Class),
enumerable: false,
configurable: false,
},
));
}
return Some((get_property(obj, key).ok()?, ro_configurable));
}
if crate::stdlib::native_tag(obj).as_deref() == Some("TypedArray") {
let v = crate::stdlib::typedarray::elem_get(obj, key)?;
return Some((
v,
host::PropAttrs {
writable: true,
enumerable: true,
configurable: true,
},
));
}
if with_host(|h| matches!(h.get(obj), Some(JsObj::RegExp(_)))) && key == "lastIndex" {
return Some((
get_property(obj, "lastIndex").ok()?,
host::PropAttrs {
writable: true,
enumerable: false,
configurable: false,
},
));
}
None
}
fn object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
let obj = arg0(&args);
require_object_coercible(&obj)?;
let key = host::to_property_key(&args.get(1).cloned().unwrap_or(Value::Undef))?;
if let Some(units) = string_primitive_units(&obj) {
let entry = match key.parse::<usize>() {
Ok(i) => units
.get(i)
.map(|c| (with_host(|h| h.new_str(c.clone())), true)),
Err(_) if key == "length" => Some((Value::Float(units.len() as f64), false)),
Err(_) => None,
};
return Ok(match entry {
Some((value, enumerable)) => with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), value);
m.insert("writable".into(), Value::Bool(false));
m.insert("enumerable".into(), Value::Bool(enumerable));
m.insert("configurable".into(), Value::Bool(false));
h.new_object(m)
}),
None => Value::Undef,
});
}
if with_host(|h| h.kind_of(&obj)) == Some(ObjKind::Proxy) {
return Ok(crate::proxy::get_own_descriptor(&obj, &key)?.unwrap_or(Value::Undef));
}
if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&obj).cloned()) {
if let Some(names) = builtin_proto_method_names(&ns) {
if names.contains(&key.as_str()) {
return Ok(with_host(|h| {
let thunk = h.alloc(JsObj::Builtin(format!(
"@proto:{}:{key}",
ns.trim_end_matches(".prototype")
)));
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), thunk);
m.insert("writable".into(), Value::Bool(true));
m.insert("enumerable".into(), Value::Bool(true));
m.insert("configurable".into(), Value::Bool(true));
h.new_object(m)
}));
}
}
}
if with_host(|h| h.is_global_object(&obj)) {
let owned = with_host(|h| match h.get(&obj) {
Some(JsObj::Object(p)) => p.contains_key(&key),
_ => false,
});
if !owned && !CJS_WRAPPER_LOCALS.contains(&key.as_str()) {
let script_made = with_host(|h| h.read_global(&key).is_some());
if let Some(v) = global_object_binding(&key) {
let frozen = matches!(key.as_str(), "undefined" | "NaN" | "Infinity");
return Ok(with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), v);
m.insert("writable".into(), Value::Bool(!frozen));
m.insert(
"enumerable".into(),
Value::Bool(script_made || ENUMERABLE_GLOBALS.contains(&key.as_str())),
);
m.insert("configurable".into(), Value::Bool(!frozen));
h.new_object(m)
}));
}
}
}
if let Some(ctor) = intrinsic_proto_of(&obj) {
if is_proto_accessor(&ctor, &key) {
let getter = proto_getter(&ctor, &key);
let writable = (ctor == "Function" && matches!(key.as_str(), "arguments" | "caller"))
|| crate::stdlib::instance_accessors(&ctor)
.0
.iter()
.any(|(k, settable)| *k == key && *settable);
let setter = writable
.then(|| with_host(|h| h.alloc(JsObj::Builtin(format!("@protoset:{ctor}:{key}")))));
return Ok(with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("get".into(), getter);
m.insert("set".into(), setter.unwrap_or(Value::Undef));
m.insert("enumerable".into(), Value::Bool(is_webidl_proto(&ctor)));
m.insert("configurable".into(), Value::Bool(true));
h.new_object(m)
}));
}
}
if let Some(ns) = with_host(|h| match h.get(&obj) {
Some(JsObj::Builtin(ns)) => Some(ns.clone()),
_ => None,
}) {
let value = namespace_property(&ns, &key);
if !matches!(value, Value::Undef) {
return Ok(builtin_member_descriptor(&ns, &key, value));
}
}
if let Some((value, attrs)) = synthesized_own_descriptor(&obj, &key) {
return Ok(with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), value);
m.insert("writable".into(), Value::Bool(attrs.writable));
m.insert("enumerable".into(), Value::Bool(attrs.enumerable));
m.insert("configurable".into(), Value::Bool(attrs.configurable));
h.new_object(m)
}));
}
if let Some((get, set)) = with_host(|h| h.own_accessor(&obj, &key)) {
return Ok(with_host(|h| {
let a = h.prop_attrs(&obj, &key);
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("get".into(), get.unwrap_or(Value::Undef));
m.insert("set".into(), set.unwrap_or(Value::Undef));
m.insert("enumerable".into(), Value::Bool(a.enumerable));
m.insert("configurable".into(), Value::Bool(a.configurable));
h.new_object(m)
}));
}
let val = with_host(|h| match h.get(&obj) {
Some(JsObj::Object(p))
if p.get("@@native").map(|t| h.str_of(t)).as_deref() == Some("Buffer") =>
{
match (
p.get("@@bytes").and_then(|b| h.get(b)),
key.parse::<usize>(),
) {
(Some(JsObj::Array(items)), Ok(i)) => items.get(i).cloned(),
_ => None,
}
}
Some(JsObj::Object(p)) => p.get(&key).cloned(),
Some(JsObj::Array(items)) => match key.parse::<usize>() {
Ok(i) if h.is_hole(&obj, i) => None,
Ok(i) => items.get(i).cloned(),
Err(_) if key == "length" => Some(Value::Float(items.len() as f64)),
Err(_) => h.fn_prop(&obj, &key),
},
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(&obj, &key),
_ => None,
});
match val {
Some(v) => Ok(with_host(|h| {
let a = h.prop_attrs(&obj, &key);
let mut m: IndexMap<String, Value> = IndexMap::new();
m.insert("value".into(), v);
m.insert("writable".into(), Value::Bool(a.writable));
m.insert("enumerable".into(), Value::Bool(a.enumerable));
m.insert("configurable".into(), Value::Bool(a.configurable));
h.new_object(m)
})),
None => Ok(Value::Undef),
}
}
fn object_get_own_descriptors(args: Vec<Value>) -> Result<Value, String> {
let obj = arg0(&args);
let names = object_keys(vec![obj.clone()], 3)?;
let keys: Vec<String> = with_host(|h| match h.get(&names) {
Some(JsObj::Array(items)) => items.iter().map(|k| h.str_of(k)).collect(),
_ => Vec::new(),
});
let mut out: IndexMap<String, Value> = IndexMap::new();
for k in keys {
let ks = with_host(|h| h.new_str(k.clone()));
let d = object_get_own_descriptor(vec![obj.clone(), ks])?;
if !matches!(d, Value::Undef) {
out.insert(k, d);
}
}
Ok(with_host(|h| h.new_object(out)))
}
pub fn has_property(obj: &Value, key: &str) -> Result<bool, String> {
if let Some(b) = crate::proxy::has(obj, key)? {
return Ok(b);
}
Ok(has_property_ordinary(obj, key))
}
fn has_property_ordinary(obj: &Value, key: &str) -> bool {
if with_host(|h| h.is_global_object(obj))
&& !CJS_WRAPPER_LOCALS.contains(&key)
&& global_object_binding(key).is_some()
{
return true;
}
if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(obj).cloned()) {
return !matches!(namespace_property(&ns, key), Value::Undef);
}
if crate::stdlib::typedarray::has_index(obj, key) == Some(true) {
return true;
}
if with_host(|h| host::lookup_chain(h, obj, key)).is_some() {
return true;
}
if with_host(|h| host::lookup_accessor(h, obj, key)).is_some() {
return true;
}
if !key.starts_with('#') && inherited_builtin_static(obj, key).is_some() {
return true;
}
if with_host(|h| match h.get(obj) {
Some(JsObj::Object(p)) => p.contains_key(key),
Some(JsObj::Array(items)) => {
key == "length"
|| key
.parse::<usize>()
.map(|i| i < items.len() && !h.is_hole(obj, i))
.unwrap_or(false)
|| h.fn_prop(obj, key).is_some()
}
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => h.fn_prop(obj, key).is_some(),
Some(JsObj::RegExp(_)) => key == "lastIndex" || h.fn_prop(obj, key).is_some(),
_ => false,
}) {
return true;
}
inherited_builtin_method(obj, key)
}
pub(crate) fn inherited_builtin_static(obj: &Value, key: &str) -> Option<Value> {
if with_host(|h| h.has_null_proto(obj)) {
return None;
}
let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
Some(c) => Some(c),
None if is_arguments(obj) => Some("Object"),
None => with_host(|h| default_ctor_name(h, obj)),
};
let on = |c: &str| with_host(|h| h.builtin_static(&format!("{c}.prototype"), key));
let found = ctor.and_then(on).or_else(|| on("Object"))?;
let self_thunk = with_host(
|h| matches!(h.get(&found), Some(JsObj::Builtin(s)) if s.starts_with("@proto:") && s.ends_with(&format!(":{key}"))),
);
(!self_thunk).then_some(found)
}
fn has_own_for_shadow(recv: &Value, key: &str) -> bool {
with_host(|h| {
if h.fn_prop(recv, key).is_some() || h.own_accessor(recv, key).is_some() {
return true;
}
match h.get(recv) {
Some(JsObj::Object(p)) => p.contains_key(key),
Some(JsObj::Array(items)) => {
key == "length" || {
key.parse::<usize>()
.is_ok_and(|i| i < items.len() && !h.is_hole(recv, i))
}
}
_ => false,
}
})
}
fn inherited_builtin_method(obj: &Value, key: &str) -> bool {
if with_host(|h| h.has_null_proto(obj)) {
return false;
}
if let Some(tag) = crate::stdlib::native_tag(obj) {
if crate::stdlib::instance_has_method(&tag, key) {
return true;
}
}
inherited_method_owner(obj, key).is_some()
}
pub(crate) fn own_intrinsic_reachable_pub(recv: &Value) -> bool {
own_intrinsic_reachable(recv)
}
fn own_intrinsic_reachable(recv: &Value) -> bool {
with_host(|h| default_ctor_name(h, recv)).map_or(true, |c| intrinsic_reachable(recv, c))
}
fn intrinsic_reachable(recv: &Value, ctor: &str) -> bool {
let own = Some(ctor);
let mut cur = recv.clone();
for _ in 0..100 {
let explicit = with_host(|h| h.proto_of(&cur));
let Some(p) = explicit else {
if with_host(|h| h.has_null_proto(&cur)) {
return false;
}
let implicit = with_host(|h| default_ctor_name(h, &cur));
return implicit == own || ctor == "Object";
};
if with_host(|h| h.is_null(&p)) {
return false;
}
let hit = with_host(|h| {
own.is_some_and(|c| {
matches!(h.get(&p), Some(JsObj::Builtin(ns)) if *ns == format!("{c}.prototype"))
|| h.intrinsic_proto_ctor(&p) == Some(c)
|| (c == "Object" && h.object_proto() == p)
})
});
if hit {
return true;
}
if let Some(builtin) = with_host(|h| {
h.class_owning_proto(&p)
.and_then(|c| h.class_builtin_ancestor(&c))
.map(|b| h.callable_name(&b))
}) {
if own == Some(builtin.as_str()) || ctor == "Object" {
return true;
}
}
cur = p;
}
false
}
pub(crate) fn chain_intrinsic_ctors_pub(recv: &Value) -> Vec<&'static str> {
chain_intrinsic_ctors(recv)
}
fn chain_intrinsic_ctors(recv: &Value) -> Vec<&'static str> {
with_host(|h| chain_intrinsic_ctors_h(h, recv))
}
pub(crate) fn chain_intrinsic_ctors_h(h: &host::JsHost, recv: &Value) -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
let mut cur = recv.clone();
for _ in 0..100 {
let Some(p) = h.proto_of(&cur) else {
break;
};
if h.is_null(&p) {
break;
}
let name = match h.get(&p) {
Some(JsObj::Builtin(ns)) => ns.strip_suffix(".prototype").map(str::to_string),
_ => h.intrinsic_proto_ctor(&p).map(str::to_string),
};
if let Some(n) = name {
if let Some(c) = crate::arity::PROTO_MEMBERS
.iter()
.map(|(k, _)| *k)
.find(|k| *k == n)
{
if !out.contains(&c) {
out.push(c);
}
}
}
cur = p;
}
out
}
pub(crate) fn inherited_method_owner_pub(obj: &Value, key: &str) -> Option<&'static str> {
inherited_method_owner(obj, key)
}
fn inherited_method_owner(obj: &Value, key: &str) -> Option<&'static str> {
if with_host(|h| h.has_null_proto(obj)) {
return None;
}
let ctor = match wrapped_primitive(obj).as_ref().and_then(wrapper_ctor_of) {
Some(c) => Some(c),
None if is_arguments(obj) => Some("Object"),
None => with_host(|h| default_ctor_name(h, obj)),
};
let on_proto = |c: &str| {
crate::arity::PROTO_MEMBERS
.binary_search_by(|(k, _)| (*k).cmp(c))
.ok()
.is_some_and(|i| {
crate::arity::PROTO_MEMBERS[i]
.1
.iter()
.any(|m| m.strip_prefix('+').unwrap_or(m) == key)
})
};
let plain = with_host(|h| h.kind_of(obj)) == Some(ObjKind::Object)
&& crate::stdlib::native_tag(obj).is_none();
let on_proto_or_symbol =
|c: &str| on_proto(c) || (plain && builtin_meta(&format!("@proto:{c}:{key}")).is_some());
if let Some(c) = ctor.filter(|c| on_proto(c) && intrinsic_reachable(obj, c)) {
return Some(c);
}
if let Some(c) = chain_intrinsic_ctors(obj)
.into_iter()
.find(|c| on_proto_or_symbol(c))
{
return Some(c);
}
if on_proto("Object") && intrinsic_reachable(obj, "Object") {
return Some("Object");
}
None
}
fn clone_refusal(v: &Value) -> Option<String> {
let kind = with_host(|h| h.kind_of(v))?;
let render = |ctor: &str| Some(format!("#<{ctor}>"));
match kind {
ObjKind::Func | ObjKind::Class | ObjKind::BoundFunc | ObjKind::BoundMethod => {
Some(with_host(|h| h.str_of(v)))
}
ObjKind::Builtin if with_host(|h| host::is_callable(h, v)) => {
Some(with_host(|h| h.str_of(v)))
}
ObjKind::Symbol => Some(with_host(|h| h.str_of(v))),
ObjKind::Promise => render("Promise"),
ObjKind::Generator => Some("[object Generator]".to_string()),
ObjKind::Proxy => Some(if with_host(|h| host::is_callable(h, v)) {
with_host(|h| h.str_of(v))
} else {
"#<Object>".to_string()
}),
ObjKind::Map if with_host(|h| matches!(h.get(v), Some(JsObj::Map { weak: true, .. }))) => {
render("WeakMap")
}
ObjKind::Set if with_host(|h| matches!(h.get(v), Some(JsObj::Set { weak: true, .. }))) => {
render("WeakSet")
}
_ => match crate::stdlib::native_tag(v).as_deref() {
Some(t @ ("WeakRef" | "FinalizationRegistry")) => render(t),
_ => None,
},
}
}
fn structured_clone(args: Vec<Value>) -> Result<Value, String> {
let list: Vec<Value> = match args.get(1).filter(|v| !matches!(v, Value::Undef)) {
Some(opts) => {
let t = get_property(opts, "transfer")?;
if matches!(t, Value::Undef) {
Vec::new()
} else {
host::iter_all(&t)?
}
}
None => Vec::new(),
};
for item in &list {
if crate::stdlib::native_tag(item).as_deref() != Some("ArrayBuffer") {
return Err(host::dom_error(
"DataCloneError",
"Found invalid value in transferList.",
));
}
}
let out = deep_clone(&arg0(&args))?;
for item in &list {
crate::stdlib::typedarray::detach_buffer(item);
}
Ok(out)
}
pub(crate) fn deep_clone(v: &Value) -> Result<Value, String> {
deep_clone_seen(v, &mut std::collections::HashMap::new())
}
fn deep_clone_seen(
v: &Value,
seen: &mut std::collections::HashMap<u32, Value>,
) -> Result<Value, String> {
let idx = match v {
Value::Obj(i) => *i,
_ => return Ok(v.clone()),
};
if let Some(done) = seen.get(&idx) {
return Ok(done.clone());
}
if crate::stdlib::typedarray::is_detached(v) {
return Err(host::dom_error(
"DataCloneError",
"An ArrayBuffer is detached and could not be cloned.",
));
}
if let Some(render) = clone_refusal(v) {
return Err(host::dom_error(
"DataCloneError",
&format!("{render} could not be cloned."),
));
}
if let Some((src, flags)) = with_host(|h| match h.get(v) {
Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
_ => None,
}) {
let args = with_host(|h| vec![h.new_str(src), h.new_str(flags)]);
let out = regexp_ctor(&args)?;
seen.insert(idx, out.clone());
return Ok(out);
}
Ok(match with_host(|h| h.get(v).cloned()) {
Some(JsObj::Array(items)) => {
let out = with_host(|h| h.new_array(Vec::new()));
seen.insert(idx, out.clone());
let mut cloned: Vec<Value> = Vec::with_capacity(items.len());
for x in &items {
cloned.push(deep_clone_seen(x, seen)?);
}
with_host(|h| {
if let Some(JsObj::Array(a)) = h.get_mut(&out) {
*a = cloned;
}
h.copy_holes(v, &out, Some);
});
out
}
Some(JsObj::Object(_)) => {
let out = with_host(|h| h.new_object(IndexMap::new()));
seen.insert(idx, out.clone());
let is_error = with_host(|h| h.error_to_string(v)).is_some();
let proto = clone_proto(v);
let keeps_proto = !matches!(proto, CloneProto::Plain);
let keys: Vec<String> = if is_error {
["name", "message", "stack"]
.iter()
.filter(|k| has_property(v, k).unwrap_or(false))
.map(|k| (*k).to_string())
.collect()
} else if keeps_proto {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.keys().cloned().collect(),
_ => Vec::new(),
})
} else {
with_host(|h| h.own_enum_key_names(v))
};
let mut cloned: IndexMap<String, Value> = IndexMap::new();
for k in keys {
let val = if k.starts_with("@@") {
match with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get(&k).cloned(),
_ => None,
}) {
Some(val) => val,
None => continue,
}
} else {
get_property(v, &k)?
};
cloned.insert(k, deep_clone_seen(&val, seen)?);
}
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(&out) {
*p = cloned;
}
match &proto {
CloneProto::Same => {
if let Some(p) = h.proto_of(v) {
h.set_proto(&out, p);
}
}
CloneProto::Ctor(c) => {
h.ensure_error_protos();
let p = h.error_proto(c).or_else(|| h.ensure_ctor_proto(c));
if let Some(p) = p {
h.set_proto(&out, p);
}
if c == "Uint8Array" {
let tag = h.new_str("TypedArray");
let kind = h.new_str("Uint8Array");
if let Some(JsObj::Object(p)) = h.get_mut(&out) {
p.insert("@@native".into(), tag);
p.insert("@@kind".into(), kind);
}
}
}
CloneProto::Plain => {}
}
h.copy_prop_attrs(v, &out);
});
out
}
Some(JsObj::Map { entries, weak }) => {
let out = with_host(|h| {
h.alloc(JsObj::Map {
entries: IndexMap::new(),
weak,
})
});
seen.insert(idx, out.clone());
let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
for (k, val) in pairs {
let ck = deep_clone_seen(&k, seen)?;
let cv = deep_clone_seen(&val, seen)?;
let _ = map_method(&out, "set", vec![ck, cv]);
}
out
}
Some(JsObj::Set { entries, weak }) => {
let out = with_host(|h| {
h.alloc(JsObj::Set {
entries: IndexMap::new(),
weak,
})
});
seen.insert(idx, out.clone());
let vals: Vec<Value> = entries.values().cloned().collect();
for x in vals {
let cx = deep_clone_seen(&x, seen)?;
let _ = set_method(&out, "add", vec![cx]);
}
out
}
_ => v.clone(),
})
}
fn clone_proto(v: &Value) -> CloneProto {
if with_host(|h| h.error_to_string(v)).is_some() {
let name = get_property(v, "name")
.map(|n| with_host(|h| h.str_of(&n)))
.unwrap_or_else(|_| "Error".into());
let class = if host::ERROR_NAMES.contains(&name.as_str()) {
name
} else {
"Error".to_string()
};
return CloneProto::Ctor(class);
}
match crate::stdlib::native_tag(v).as_deref() {
Some("Buffer") => CloneProto::Ctor("Uint8Array".into()),
Some(_) => CloneProto::Same,
None if wrapped_primitive(v).is_some() => CloneProto::Same,
None => CloneProto::Plain,
}
}
enum CloneProto {
Same,
Ctor(String),
Plain,
}
pub fn error_string(h: &host::JsHost, v: &Value) -> String {
if let Some(JsObj::Object(props)) = h.get(v) {
let name = props
.get("name")
.map(|x| h.str_of(x))
.or_else(|| host::lookup_chain(h, v, "name").map(|x| h.str_of(&x)))
.unwrap_or_else(|| "Error".into());
if let Some(m) = props.get("message") {
return format!("{name}: {}", h.str_of(m));
}
return name;
}
h.str_of(v)
}
fn finally_chain(result: Value, carried: Value, rethrow: bool) -> Value {
let p = match with_host(|h| h.promise_id(&result)) {
Some(_) => result,
None => {
let fresh = with_host(|h| h.new_promise());
if let Some(pid) = with_host(|h| h.promise_id(&fresh)) {
host::resolve_promise_val(pid, result);
}
fresh
}
};
let cell = with_host(|h| h.new_array(vec![carried]));
let idx = match cell {
Value::Obj(i) => i,
_ => 0,
};
let tag = if rethrow { "finrethrow" } else { "finret" };
let thunk = make_builtin(format!("@@{tag}:{idx}"));
host::promise_then(&p, thunk, Value::Undef)
}
fn make_builtin(name: String) -> Value {
with_host(|h| h.alloc(JsObj::Builtin(name)))
}
pub fn prototype_of(v: &Value) -> Value {
if matches!(with_host(|h| h.get(v).cloned()), Some(JsObj::Builtin(ref n)) if n == "Buffer") {
return with_host(|h| h.alloc(JsObj::Builtin("Uint8Array".into())));
}
if let Some(JsObj::Class(c)) = with_host(|h| h.get(v).cloned()) {
if let Some(parent) = c.parent {
return parent;
}
}
if with_host(|h| h.has_null_proto(v)) {
return with_host(|h| h.null());
}
if with_host(|h| h.strict_eq(v, &h.object_proto())) {
return with_host(|h| h.null());
}
if matches!(
with_host(|h| h.get(v).cloned()),
Some(JsObj::Builtin(ref n)) if n.ends_with(".prototype")
) {
return with_host(|h| h.object_proto());
}
if let Some(p) = with_host(|h| h.proto_of(v)) {
return p;
}
with_host(|h| {
h.ensure_native_protos();
match default_ctor_name(h, v) {
Some("Object") => h.object_proto(),
Some(c) => h
.native_proto(c)
.unwrap_or_else(|| h.alloc(JsObj::Builtin(format!("{c}.prototype")))),
None => h.null(),
}
})
}
fn promise_species_create() -> Result<Option<Value>, String> {
let Some(ctor) = host::current_static_this() else {
return Ok(None);
};
if !matches!(
with_host(|h| h.kind_of(&ctor)),
Some(ObjKind::Class) | Some(ObjKind::Func)
) {
return Ok(None);
}
let species = match get_property(&ctor, "@@species") {
Ok(Value::Undef) => ctor,
Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
Ok(s) => s,
Err(_) => ctor,
};
if !matches!(
with_host(|h| h.kind_of(&species)),
Some(ObjKind::Class) | Some(ObjKind::Func)
) {
return Ok(None);
}
let noop = make_builtin("@@pnoop".to_string());
let p = host::construct(&species, vec![noop])?;
Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
}
pub fn promise_species_from(recv: &Value) -> Result<Option<Value>, String> {
let ctor = with_host(|h| host::lookup_chain(h, recv, "constructor")).unwrap_or(Value::Undef);
if !matches!(
with_host(|h| h.kind_of(&ctor)),
Some(ObjKind::Class) | Some(ObjKind::Func)
) {
return Ok(None);
}
let species = match get_property(&ctor, "@@species") {
Ok(Value::Undef) => ctor,
Ok(s) if with_host(|h| h.is_null(&s)) => return Ok(None),
Ok(s) => s,
Err(_) => ctor,
};
if !matches!(
with_host(|h| h.kind_of(&species)),
Some(ObjKind::Class) | Some(ObjKind::Func)
) {
return Ok(None);
}
let noop = make_builtin("@@pnoop".to_string());
let p = host::construct(&species, vec![noop])?;
Ok(with_host(|h| h.promise_id(&p)).map(|_| p))
}
fn new_promise(executor: Value) -> Result<Value, String> {
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
let res = make_builtin(format!("@@presolve:{id}"));
let rej = make_builtin(format!("@@preject:{id}"));
if let Err(e) = host::invoke(&executor, vec![res, rej], None) {
let ev = host::take_exc_or_error(&e);
host::reject_promise_val(id, ev);
}
Ok(p)
}
pub fn promise_resolve_pub(v: Value) -> Result<Value, String> {
promise_resolve(v)
}
fn promise_resolve(v: Value) -> Result<Value, String> {
if let Some(p) = promise_species_create()? {
let id = with_host(|h| h.promise_id(&p).unwrap());
host::resolve_promise_val(id, v);
return Ok(p);
}
Ok(host::promise_of(&v))
}
fn promise_reject(v: Value) -> Result<Value, String> {
let p = match promise_species_create()? {
Some(p) => p,
None => with_host(|h| h.new_promise()),
};
let id = with_host(|h| h.promise_id(&p).unwrap());
host::reject_promise_val(id, v);
Ok(p)
}
pub fn pending_promise_with_resolver() -> (Value, Value) {
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
let resolve = make_builtin(format!("@@presolve:{id}"));
(p, resolve)
}
fn regexp_escape(args: Vec<Value>) -> Result<Value, String> {
let v = arg0(&args);
if !matches!(v, Value::Str(_)) && !with_host(|h| matches!(h.get(&v), Some(JsObj::Str(_)))) {
return Err(host::type_error("input argument must be a string"));
}
let s = with_host(|h| h.str_of(&v));
const OTHER_PUNCTUATORS: &str = " !\"#%&',-:;<=>@`~";
const SYNTAX: &str = "^$\\.*+?()[]{}|/";
let mut out = String::with_capacity(s.len());
for (i, c) in s.chars().enumerate() {
if i == 0 && c.is_ascii_alphanumeric() {
out.push_str(&format!("\\x{:02x}", c as u32));
continue;
}
if SYNTAX.contains(c) {
out.push('\\');
out.push(c);
continue;
}
match c {
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\u{b}' => out.push_str("\\v"),
'\u{c}' => out.push_str("\\f"),
'\r' => out.push_str("\\r"),
_ if OTHER_PUNCTUATORS.contains(c) || is_regex_escape_space(c) => {
let n = c as u32;
if n <= 0xff {
out.push_str(&format!("\\x{n:02x}"));
} else {
out.push_str(&format!("\\u{n:04x}"));
}
}
_ => out.push(c),
}
}
Ok(with_host(|h| h.new_str(out)))
}
fn is_regex_escape_space(c: char) -> bool {
matches!(
c,
'\u{a0}' | '\u{1680}' | '\u{2000}'
..='\u{200a}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}'
| '\u{feff}'
)
}
fn error_is_error(args: Vec<Value>) -> Result<Value, String> {
let v = arg0(&args);
Ok(Value::Bool(with_host(|h| has_error_data(h, &v))))
}
pub(crate) fn has_error_data(h: &host::JsHost, v: &Value) -> bool {
match h.get(v) {
Some(JsObj::Object(p)) => {
p.contains_key("stack") || p.contains_key("@@stackRaw") || p.contains_key("@@domName")
}
_ => false,
}
}
fn promise_try(args: Vec<Value>) -> Result<Value, String> {
let f = arg0(&args);
if !with_host(|h| host::is_callable(h, &f)) {
let shown = with_host(|h| {
let kind = h.type_of(&f);
match kind {
"undefined" => "undefined".to_string(),
"symbol" | "bigint" => kind.to_string(),
"object" if h.is_null(&f) => "object null".to_string(),
"object" => "object".to_string(),
"string" => format!("string \"{}\"", h.str_of(&f)),
_ => format!("{kind} {}", h.str_of(&f)),
}
});
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
let reject = make_builtin(format!("@@preject:{id}"));
let err =
with_host(|h| synth_error(h, &host::type_error(&format!("{shown} is not a function"))));
host::invoke(&reject, vec![err], None)?;
return Ok(p);
}
let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
let resolve = make_builtin(format!("@@presolve:{id}"));
let reject = make_builtin(format!("@@preject:{id}"));
let promise = p;
match host::invoke(&f, rest, None) {
Ok(v) => {
host::invoke(&resolve, vec![v], None)?;
}
Err(e) => {
let err =
with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
with_host(|h| {
h.error = None;
h.exc = None;
});
host::invoke(&reject, vec![err], None)?;
}
}
Ok(promise)
}
fn promise_with_resolvers() -> Result<Value, String> {
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
let resolve = make_builtin(format!("@@presolve:{id}"));
let reject = make_builtin(format!("@@preject:{id}"));
let mut props: IndexMap<String, Value> = IndexMap::new();
props.insert("promise".into(), p);
props.insert("resolve".into(), resolve);
props.insert("reject".into(), reject);
Ok(with_host(|h| h.new_object(props)))
}
fn rejected_promise(e: String) -> Value {
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
let reject = make_builtin(format!("@@preject:{id}"));
let err = with_host(|h| h.exc.clone()).unwrap_or_else(|| with_host(|h| synth_error(h, &e)));
with_host(|h| {
h.error = None;
h.exc = None;
});
let _ = host::invoke(&reject, vec![err], None);
p
}
#[derive(Clone, Copy)]
enum AllMode {
All,
AllSettled,
}
fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
let items = match host::iter_all(&arg0(&args)) {
Ok(v) => v,
Err(e) => return Ok(rejected_promise(e)),
};
let result = match promise_species_create()? {
Some(p) => p,
None => with_host(|h| h.new_promise()),
};
let rid = with_host(|h| h.promise_id(&result).unwrap());
let n = items.len();
if n == 0 {
let empty = with_host(|h| h.new_array(Vec::new()));
host::resolve_promise_val(rid, empty);
return Ok(result);
}
let slots = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
for (i, it) in items.into_iter().enumerate() {
let ap = host::promise_of(&it);
let aid = with_host(|h| h.promise_id(&ap).unwrap());
let slots = slots.clone();
let remaining = remaining.clone();
host::subscribe_native(
aid,
Box::new(move |state, val| {
let settled = match mode {
AllMode::All => {
if state == host::PromiseState::Rejected {
host::reject_promise_val(rid, val);
return Ok(());
}
val
}
AllMode::AllSettled => with_host(|h| {
let mut m: IndexMap<String, Value> = IndexMap::new();
if state == host::PromiseState::Rejected {
m.insert("status".into(), h.new_str("rejected"));
m.insert("reason".into(), val);
} else {
m.insert("status".into(), h.new_str("fulfilled"));
m.insert("value".into(), val);
}
h.new_object(m)
}),
};
slots.borrow_mut()[i] = settled;
let mut r = remaining.borrow_mut();
*r -= 1;
if *r == 0 {
let arr = with_host(|h| h.new_array(slots.borrow().clone()));
host::resolve_promise_val(rid, arr);
}
Ok(())
}),
);
}
Ok(result)
}
fn promise_race(args: Vec<Value>, any: bool) -> Result<Value, String> {
let items = match host::iter_all(&arg0(&args)) {
Ok(v) => v,
Err(e) => return Ok(rejected_promise(e)),
};
let result = match promise_species_create()? {
Some(p) => p,
None => with_host(|h| h.new_promise()),
};
let rid = with_host(|h| h.promise_id(&result).unwrap());
let n = items.len();
let errors = std::rc::Rc::new(std::cell::RefCell::new(vec![Value::Undef; n]));
let remaining = std::rc::Rc::new(std::cell::RefCell::new(n));
for (i, it) in items.into_iter().enumerate() {
let ap = host::promise_of(&it);
let aid = with_host(|h| h.promise_id(&ap).unwrap());
let errors = errors.clone();
let remaining = remaining.clone();
host::subscribe_native(
aid,
Box::new(move |state, val| {
if any {
if state == host::PromiseState::Fulfilled {
host::resolve_promise_val(rid, val);
} else {
errors.borrow_mut()[i] = val;
let mut r = remaining.borrow_mut();
*r -= 1;
if *r == 0 {
let reasons = with_host(|h| h.new_array(errors.borrow().clone()));
let msg = with_host(|h| h.new_str("All promises were rejected"));
let agg = make_error_inner("AggregateError", &[reasons, msg]);
host::reject_promise_val(rid, agg);
}
}
} else if state == host::PromiseState::Rejected {
host::reject_promise_val(rid, val);
} else {
host::resolve_promise_val(rid, val);
}
Ok(())
}),
);
}
Ok(result)
}
fn promise_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"then" => Ok(host::promise_then(
recv,
args.first().cloned().unwrap_or(Value::Undef),
args.get(1).cloned().unwrap_or(Value::Undef),
)),
"catch" => Ok(host::promise_then(
recv,
Value::Undef,
args.first().cloned().unwrap_or(Value::Undef),
)),
"finally" => {
let cb = arg0(&args);
if !with_host(|h| host::is_callable(h, &cb)) {
return Ok(host::promise_then(recv, cb.clone(), cb));
}
let i = match cb {
Value::Obj(i) => i,
_ => 0,
};
let pass = make_builtin(format!("@@finpass:{i}"));
let throw = make_builtin(format!("@@finthrow:{i}"));
Ok(host::promise_then(recv, pass, throw))
}
_ => Err(host::type_error(&format!(
"promise.{name} is not a function"
))),
}
}
fn enqueue_microtask(next_tick: bool, cb: Value, args: Vec<Value>) {
with_host(|h| {
if next_tick {
h.queue_nexttick(cb, args);
} else {
h.queue_micro(cb, args);
}
});
}
fn schedule_timer(name: &str, args: Vec<Value>) -> Value {
let cb = arg0(&args);
let delay = if name == "setImmediate" {
-1.0 } else {
args.get(1)
.map(|d| with_host(|h| h.to_number(d)))
.unwrap_or(0.0)
.max(0.0)
};
let extra = if name == "setImmediate" {
args.get(1..).map(|s| s.to_vec()).unwrap_or_default()
} else {
args.get(2..).map(|s| s.to_vec()).unwrap_or_default()
};
let interval = (name == "setInterval").then(|| delay.max(1.0));
let id = with_host(|h| h.add_timer(delay, cb, extra, interval));
let tag = if name == "setImmediate" {
"Immediate"
} else {
"Timeout"
};
crate::stdlib::timers::new_handle(id, tag)
}
fn clear_timer(v: &Value) {
let id =
crate::stdlib::timers::handle_id(v).unwrap_or_else(|| with_host(|h| h.to_number(v)) as u64);
with_host(|h| h.cancel_timer(id));
}