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::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 b_tag_tmpl(vm: &mut VM, argc: u8) -> Value {
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 tag = all.remove(0);
let n = int_of(&all.remove(0));
let mcount = int_of(&all.remove(0));
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 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);
h.set_prop_attrs(
&strings,
"raw",
host::PropAttrs {
writable: false,
enumerable: false,
configurable: false,
},
);
});
let mut call_args = vec![strings];
call_args.extend(values);
let r = host::invoke(&tag, call_args, None);
finish(vm, r)
}
fn b_get_async_iter(vm: &mut VM, _: u8) -> Value {
let src = vm.pop();
let r = host::get_async_iterator(&src);
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, _: u8) -> Value {
let ctor = vm.pop();
let parent = vm.pop();
let name = sval(&vm.pop());
host::build_class(&name, parent, ctor)
}
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(vm: &mut VM, argc: u8) -> Value {
let args = pop_n(vm, argc as usize);
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 r = host::super_construct(&parent, args, &this, &nt);
if let Err(e) = r {
return abort(vm, e);
}
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()
.map(|v| with_host(|h| h.str_of(v)))
.collect();
with_host(|h| {
let props: IndexMap<String, Value> = match h.get(&obj) {
Some(JsObj::Object(m)) => m
.iter()
.filter(|(k, _)| !excl.contains(k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
_ => IndexMap::new(),
};
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> {
if let Some(v) = with_host(|h| h.read_name(name)) {
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())),
_ => {}
}
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());
match global_binding(&name) {
Some(v) => v,
None => abort(vm, host::ref_error(&name)),
}
}
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| 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 {
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
})
}
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 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 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,
});
const CJS_WRAPPER_LOCALS: &[&str] = &[
"require",
"module",
"exports",
"__filename",
"__dirname",
"__cjs_require",
"__cjs_resolve",
];
if !own && !CJS_WRAPPER_LOCALS.contains(&name) {
if let Some(v) = global_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(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));
}
let kind = with_host(|h| h.kind_of(recv));
Ok(match kind {
Some(ObjKind::Object) => {
let numeric = !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit());
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::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),
_ => 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 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),
_ => 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),
_ => Value::Undef,
}
}
Some(ObjKind::Generator) => {
if is_generator_method(name) {
bound_method(recv, name)
} else {
Value::Undef
}
}
Some(ObjKind::Promise) => {
if matches!(name, "then" | "catch" | "finally") {
bound_method(recv, name)
} else {
Value::Undef
}
}
Some(ObjKind::Iter) => {
if matches!(name, "next" | "return" | "@@iterator") {
bound_method(recv, name)
} else {
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,
})
.unwrap_or(Value::Undef)
} else if name == "@@iterator" || is_array_method(name) || is_object_method(name) {
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();
namespace_property(&ns, name)
}
_ => {
if matches!(recv, Value::Float(_) | Value::Int(_)) && is_number_method(name) {
bound_method(recv, name)
} else {
Value::Undef
}
}
})
}
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(_)) | Some(JsObj::Class(_)) | Some(JsObj::BoundFunc { .. }) => {
Some("Function")
}
_ => match recv {
Value::Float(_) | Value::Int(_) => Some("Number"),
Value::Bool(_) => Some("Boolean"),
_ => None,
},
}
}
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"
| "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)
}
fn bound_method(recv: &Value, name: &str) -> Value {
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"
)
}
pub const OBJECT_PROTO_METHODS: &[&str] = &[
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toString",
"toLocaleString",
"valueOf",
];
pub fn is_object_builtin_method(name: &str) -> bool {
matches!(
name,
"hasOwnProperty"
| "isPrototypeOf"
| "propertyIsEnumerable"
| "toString"
| "toLocaleString"
| "valueOf"
)
}
pub fn object_builtin_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"hasOwnProperty" => {
let k = with_host(|h| h.property_key(&arg0(&args)));
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));
}
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" => 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 {
with_host(|h| h.iter_vec(&arr)).unwrap_or_default()
};
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"
)
}
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 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_property(ns: &str, name: &str) -> Value {
if ns == REQUIRE_CACHE {
return crate::module::cache_get(name).unwrap_or(Value::Undef);
}
if ns == "require" && name == "cache" {
return with_host(|h| h.alloc(JsObj::Builtin(REQUIRE_CACHE.to_string())));
}
let konst = match (ns, name) {
("Math", "PI") => Some(std::f64::consts::PI),
("Math", "E") => Some(std::f64::consts::E),
("Math", "LN2") => Some(std::f64::consts::LN_2),
("Math", "LN10") => Some(std::f64::consts::LN_10),
("Math", "LOG2E") => Some(std::f64::consts::LOG2_E),
("Math", "LOG10E") => Some(std::f64::consts::LOG10_E),
("Math", "SQRT2") => Some(std::f64::consts::SQRT_2),
("Math", "SQRT1_2") => Some(std::f64::consts::FRAC_1_SQRT_2),
("Number", "MAX_SAFE_INTEGER") => Some(9007199254740991.0),
("Number", "MIN_SAFE_INTEGER") => Some(-9007199254740991.0),
("Number", "MAX_VALUE") => Some(f64::MAX),
("Number", "MIN_VALUE") => Some(f64::from_bits(1)),
("Number", "EPSILON") => Some(f64::EPSILON),
("Number", "POSITIVE_INFINITY") => Some(f64::INFINITY),
("Number", "NEGATIVE_INFINITY") => Some(f64::NEG_INFINITY),
("Number", "NaN") => Some(f64::NAN),
_ => None,
};
if let Some(k) = konst {
return Value::Float(k);
}
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") {
return with_host(|h| h.alloc(JsObj::Builtin(format!("@proto:{ctor}:{name}"))));
}
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;
}
Value::Undef
}
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 ctor == "Error" && method == "toString" {
let s = with_host(|h| h.error_to_string(recv)).unwrap_or_else(|| {
with_host(|h| {
let name = host::lookup_chain(h, recv, "name")
.map(|n| h.str_of(&n))
.unwrap_or_else(|| "Error".into());
let msg = host::lookup_chain(h, recv, "message")
.map(|m| h.str_of(&m))
.unwrap_or_default();
if msg.is_empty() {
name
} else {
format!("{name}: {msg}")
}
})
});
return Ok(with_host(|h| h.new_str(s)));
}
if ctor == "Object" && method == "toString" {
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 {
let t = get_property(recv, "@@toStringTag")?;
if let Some(s) = with_host(|h| h.as_str(&t)) {
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 crate::stdlib::native_tag(recv).as_deref() == Some(ctor) {
return crate::stdlib::instance_call(ctor, recv, method, args);
}
host::call_method(recv, method, args)
}
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 object_tag(h: &host::JsHost, v: &Value) -> String {
format!("[object {}]", object_brand(h, v))
}
fn object_brand(h: &host::JsHost, v: &Value) -> 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(_)) => "Array".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::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 h.error_to_string(v).is_some() => "Error".into(),
_ => "Object".into(),
},
_ => "Object".into(),
},
_ => "Object".into(),
};
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)
}
fn set_property(recv: &Value, name: &str, val: Value) -> Result<(), String> {
if name.starts_with('#') && !with_host(|h| h.has_private(recv, name)) {
return Err(private_brand_message(name, true));
}
if crate::proxy::set(recv, name, &val, recv)? {
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 {
with_host(|h| h.set_proto(recv, val));
}
return Ok(());
}
}
if !with_host(|h| h.can_write_prop(recv, name)) {
return Ok(());
}
if let Some((_, Some(setter))) = with_host(|h| host::lookup_accessor(h, recv, name)) {
let _ = host::invoke(&setter, vec![val], Some(recv.clone()));
return Ok(());
}
if let Some((Some(_), None)) = with_host(|h| host::lookup_accessor(h, 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 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 !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 crate::stdlib::buffer::byte_set(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) {
Some(host::to_array_length(&val)?)
} 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 let Some(b) = crate::proxy::delete(recv, key)? {
return Ok(b);
}
if peek(recv, |o| match o {
JsObj::Builtin(ns) => Some(ns == REQUIRE_CACHE),
_ => None,
}) == Some(true)
{
return Ok(crate::module::cache_delete(key));
}
if !with_host(|h| h.prop_attrs(recv, key).configurable) {
return Ok(false);
}
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 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(b) => Value::Bool(b),
Err(e) => abort(vm, e),
}
}
fn b_delprop_name(vm: &mut VM, _: u8) -> Value {
let name = sval(&vm.pop());
let recv = vm.pop();
match delete_property(&recv, &name) {
Ok(b) => Value::Bool(b),
Err(e) => abort(vm, e),
}
}
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 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;
}
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 = host::own_enum_entries_deep(&src);
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);
}
}
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();
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: None,
}));
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 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(_)) {
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 = with_host(|h| h.property_key(&key));
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();
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();
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;
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);
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.strip_suffix('\'')),
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 })),
Ok(None) => abort(vm, "internal: kind_of said Proxy".into()),
Err(e) => abort(vm, e),
};
}
if let Some(iter_fn) = with_host(|h| host::lookup_chain(h, &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),
};
}
}
match with_host(|h| h.iter_vec(&v)) {
Ok(items) => with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })),
Err(e) => abort(vm, e),
}
}
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_enum_string_keys(&v) {
Ok(keys) => with_host(|h| {
let out: Vec<Value> = keys.into_iter().map(|k| h.new_str(k)).collect();
h.new_array(out)
}),
Err(e) => abort(vm, e),
};
}
let keys = with_host(|h| h.enum_keys(&v));
with_host(|h| h.new_array(keys))
}
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()),
};
let eager = with_host(|h| {
if let Some(JsObj::Iter { items, idx }) = h.get_mut(&it) {
if *idx < items.len() {
let v = items[*idx].clone();
*idx += 1;
return Some(Some(v));
}
return Some(None);
}
None
});
if let Some(step) = eager {
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 host::iter_all(&iterable) {
Ok(v) => v,
Err(e) => return abort(vm, e),
};
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) {
Ok(items) => out.extend(items),
Err(e) => return abort(vm, 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.str_of(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(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",
"BigInt",
"RegExp",
"Date",
"ArrayBuffer",
"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",
"Proxy",
"require",
"__cjs_require",
"__cjs_resolve",
"__cjs_cache",
];
const NS_METHODS: &[&str] = &[
"console.log",
"console.error",
"console.warn",
"console.info",
"console.debug",
"Math.floor",
"Math.ceil",
"Math.round",
"Math.trunc",
"Math.abs",
"Math.sign",
"Math.max",
"Math.min",
"Math.pow",
"Math.sqrt",
"Math.cbrt",
"Math.random",
"Math.hypot",
"Math.clz32",
"Math.fround",
"Math.imul",
"Math.sinh",
"Math.cosh",
"Math.tanh",
"Math.asinh",
"Math.acosh",
"Math.atanh",
"Math.log1p",
"Math.expm1",
"Math.log",
"Math.log2",
"Math.log10",
"Math.exp",
"Math.sin",
"Math.cos",
"Math.tan",
"Math.atan",
"Math.atan2",
"Math.asin",
"Math.acos",
"JSON.stringify",
"JSON.parse",
"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.isInteger",
"Number.isNaN",
"Number.isFinite",
"Number.isSafeInteger",
"Number.parseInt",
"Number.parseFloat",
"String.fromCharCode",
"String.fromCodePoint",
"String.raw",
"Symbol.for",
"Symbol.keyFor",
"BigInt.asIntN",
"BigInt.asUintN",
"Proxy.revocable",
"Reflect.ownKeys",
"Reflect.has",
"Reflect.get",
"Reflect.set",
"Reflect.getPrototypeOf",
"Reflect.setPrototypeOf",
"Reflect.getOwnPropertyDescriptor",
"Reflect.defineProperty",
"Reflect.deleteProperty",
"Reflect.apply",
"Reflect.construct",
"Reflect.isExtensible",
"Reflect.preventExtensions",
"Promise.resolve",
"Promise.reject",
"Promise.all",
"Promise.allSettled",
"Promise.race",
"Promise.any",
"Promise.withResolvers",
"Map.groupBy",
"Response.json",
"Response.error",
"Response.redirect",
"AbortSignal.abort",
"AbortSignal.timeout",
"process.nextTick",
"Error.captureStackTrace",
"require.resolve",
];
pub fn is_known_builtin(name: &str) -> bool {
GLOBAL_FUNCS.contains(&name)
|| NS_METHODS.contains(&name)
|| 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 chunk = crate::load_merged(crate::compile_completion(&src)?);
if direct {
host::run_chunk_on(chunk)
} else {
host::run_chunk_in_global_scope(chunk)
}
}
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 == "require.resolve" {
let spec = with_host(|h| h.str_of(&arg0(&args)));
if crate::stdlib::resolve(&spec).is_some() {
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::resolve(&spec).is_some() {
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 {
"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(arg_num(&args, 0).is_nan())),
"isFinite" => Ok(Value::Bool(arg_num(&args, 0).is_finite())),
"encodeURIComponent" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), false),
"encodeURI" => uri_encode(&with_host(|h| h.str_of(&arg0(&args))), true),
"decodeURIComponent" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), false),
"decodeURI" => uri_decode(&with_host(|h| h.str_of(&arg0(&args))), 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 {
host::to_number_value(&args[0])?
})),
"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" => Ok(with_host(|h| h.new_array(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(_))
)))
}
"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);
with_host(|h| h.seal_object(&v, true));
Ok(v)
}
"Object.seal" => {
let v = arg0(&args);
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" => Ok(Value::Bool(with_host(|h| h.is_sealed(&arg0(&args), true)))),
"Object.isSealed" => Ok(Value::Bool(with_host(|h| h.is_sealed(&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" => {
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)) {
let cur = prototype_of(&obj);
let same = with_host(|h| h.strict_eq(&cur, &proto));
if !same && !with_host(|h| h.is_extensible(&obj)) {
return Err(host::type_error("#<Object> is not extensible"));
}
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);
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" => {
object_define_property(args)?;
Ok(Value::Bool(true))
}
"Reflect.deleteProperty" => {
let obj = arg0(&args);
let k = with_host(|h| h.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));
}
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 = with_host(|h| h.iter_vec(&args.get(2).cloned().unwrap_or(Value::Undef)))
.unwrap_or_default();
host::invoke(&f, list, this.filter(|t| !with_host(|h| h.is_nullish(t))))
}
"Reflect.construct" => {
let f = arg0(&args);
let list = with_host(|h| h.iter_vec(&args.get(1).cloned().unwrap_or(Value::Undef)))
.unwrap_or_default();
host::construct(&f, list)
}
"Reflect.has" => {
let obj = arg0(&args);
let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
Ok(Value::Bool(has_property(&obj, &k)?))
}
"Reflect.get" => {
let obj = arg0(&args);
let k = with_host(|h| h.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);
let k = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
let v = args.get(2).cloned().unwrap_or(Value::Undef);
let _ = set_property(&obj, &k, v);
Ok(Value::Bool(true))
}
"JSON.stringify" => json_stringify(args),
"JSON.parse" => json_parse(args),
"structuredClone" => Ok(deep_clone(&arg0(&args))),
"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);
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" => 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(),
"Map.groupBy" => map_group_by(args),
n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
_ if name.starts_with("Math.") => math_fn(&name[5..], &args),
_ 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[10..].parse().unwrap_or(0);
let cb = Value::Obj(i);
host::invoke(&cb, Vec::new(), None)?;
Ok(arg0(&args))
}
_ if name.starts_with("@@finthrow:") => {
let i: u32 = name[11..].parse().unwrap_or(0);
let cb = Value::Obj(i);
host::invoke(&cb, Vec::new(), None)?;
let reason = arg0(&args);
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"))
}
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 {
with_host(|h| h.str_of(&a0))
};
(src, None)
}
};
let flags = match args.get(1) {
Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
_ => 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)))
}
fn object_call(args: Vec<Value>) -> Value {
let a = arg0(&args);
if matches!(
with_host(|h| h.get(&a).cloned()),
Some(JsObj::Object(_)) | Some(JsObj::Array(_))
) {
a
} else {
with_host(|h| h.new_object(IndexMap::new()))
}
}
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)),
"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)))
{
let pairs = host::iter_all(init)?;
for p in pairs {
let kv = host::iter_all(&p)?;
let k = kv.first().cloned().unwrap_or(Value::Undef);
let v = kv.get(1).cloned().unwrap_or(Value::Undef);
map_method(&m, "set", vec![k, v])?;
}
}
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)))
{
let vals = host::iter_all(init)?;
for v in vals {
set_method(&s, "add", vec![v])?;
}
}
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" => Ok(make_error(name, &args)),
n if host::ERROR_NAMES.contains(&n) => Ok(make_error(name, &args)),
_ => Err(host::type_error(&format!("{name} is not a constructor"))),
}
}
fn make_error(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);
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"] {
h.hide_prop(&e, k);
}
e
})
}
fn print_line(args: &[Value], stderr: bool) {
let line: String = crate::stdlib::util::format(args);
with_host(|h| h.write_out(&format!("{line}\n"), stderr));
}
fn arg0(args: &[Value]) -> Value {
args.first().cloned().unwrap_or(Value::Undef)
}
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]) -> f64 {
let s = with_host(|h| h.str_of(&arg0(args)));
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]) -> f64 {
let s = with_host(|h| h.str_of(&arg0(args)));
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> {
if fname != "random"
&& args
.iter()
.any(|a| with_host(|h| matches!(h.get(a), Some(JsObj::BigInt(_)))))
{
return Err(host::type_error(
"Cannot convert a BigInt value to a number",
));
}
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 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)
}));
}
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)
}));
}
if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(&v).cloned()) {
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}.");
names = NS_METHODS
.iter()
.filter_map(|q| q.strip_prefix(&prefix))
.map(|m| m.to_string())
.collect();
}
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 = if mode == 3 {
entries
} else {
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> {
let items = host::iter_all(&arg0(&args))?;
let cb = args.get(1).cloned().unwrap_or(Value::Undef);
let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
for (i, item) in items.into_iter().enumerate() {
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);
}
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 map_group_by(args: Vec<Value>) -> Result<Value, String> {
let items = host::iter_all(&arg0(&args))?;
let cb = args.get(1).cloned().unwrap_or(Value::Undef);
let m = with_host(|h| {
h.alloc(JsObj::Map {
entries: IndexMap::new(),
weak: false,
})
});
for (i, item) in items.into_iter().enumerate() {
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(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);
let items = match host::iter_all(&src) {
Ok(v) => v,
Err(_) => array_like_items(&src),
};
if let Some(cb) = args.get(1).cloned() {
let mut out = Vec::with_capacity(items.len());
for (i, it) in items.into_iter().enumerate() {
out.push(host::invoke(&cb, vec![it, Value::Float(i as f64)], None)?);
}
return Ok(with_host(|h| h.new_array(out)));
}
Ok(with_host(|h| h.new_array(items)))
}
fn array_like_items(src: &Value) -> Vec<Value> {
let len = get_property(src, "length")
.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 Vec<Value>,
rep: Option<&Value>,
) -> Result<Value, String> {
let mut v = v.clone();
if matches!(v, Value::Obj(_)) {
let tag = crate::stdlib::native_tag(&v);
let has_to_json = with_host(|h| match host::lookup_chain(h, &v, "toJSON") {
Some(f) => host::is_callable(h, &f),
None => false,
}) || 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()))?;
}
json_walk_children(&v, path, rep)
}
fn json_visible_key(k: &str) -> bool {
!k.starts_with("@@") && !k.starts_with('#')
}
fn json_walk_children(
v: &Value,
path: &mut Vec<Value>,
rep: Option<&Value>,
) -> Result<Value, String> {
if !matches!(v, Value::Obj(_)) {
return Ok(v.clone());
}
if with_host(|h| path.iter().any(|p| h.strict_eq(p, v))) {
return Err(host::type_error("Converting circular structure to JSON"));
}
if with_host(|h| h.kind_of(v)) == Some(ObjKind::Proxy) {
let snap = crate::proxy::json_snapshot(v)?;
path.push(v.clone());
let out = json_walk_children(&snap, path, rep);
path.pop();
return out;
}
let obj = with_host(|h| h.get(v).cloned());
path.push(v.clone());
let out = (|| match obj {
Some(JsObj::Array(items)) => {
let mut out = Vec::with_capacity(items.len());
let mut changed = false;
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()),
Some(JsObj::Map { .. }) | Some(JsObj::Set { .. }) => Some("{}".into()),
Some(JsObj::Func(_))
| Some(JsObj::Builtin(_))
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::Class(_))
| Some(JsObj::Symbol { .. })
| Some(JsObj::Generator { .. }) => 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)) => {
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,
};
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()
{
return json_revive("", v, &reviver);
}
Ok(v)
}
fn json_revive(key: &str, val: Value, reviver: &Value) -> Result<Value, String> {
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)?;
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)?;
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()));
host::invoke(reviver, vec![kv, val], None)
}
struct JsonParser {
chars: Vec<char>,
pos: usize,
}
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();
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)),
}
}
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"
)
}
fn is_string_method(name: &str) -> bool {
matches!(
name,
"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_regexp_arg(v: &Value) -> bool {
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)
}
fn is_number_method(name: &str) -> bool {
matches!(
name,
"toFixed" | "toExponential" | "toString" | "toPrecision" | "toLocaleString" | "valueOf"
)
}
pub fn call_type_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
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) => generator_method(recv, name, args),
Some(ObjKind::Promise) => promise_method(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 array_items(recv: &Value) -> Vec<Value> {
with_host(|h| match h.get(recv) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
})
}
fn hole_set(recv: &Value) -> rustc_hash::FxHashSet<usize> {
with_host(|h| h.hole_indices(recv)).into_iter().collect()
}
fn array_len(recv: &Value) -> usize {
peek(recv, |o| match o {
JsObj::Array(items) => Some(items.len()),
_ => None,
})
.unwrap_or(0)
}
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> {
match name {
"push" => {
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() {
",".to_string()
} else {
with_host(|h| h.str_of(&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 items = array_items(recv);
let holes = hole_set(recv);
let target = arg0(&args);
let idx = with_host(|h| {
items
.iter()
.enumerate()
.position(|(i, x)| !holes.contains(&i) && h.strict_eq(x, &target))
});
Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
}
"lastIndexOf" => {
let items = array_items(recv);
let holes = hole_set(recv);
let target = arg0(&args);
let idx = with_host(|h| {
items
.iter()
.enumerate()
.rposition(|(i, x)| !holes.contains(&i) && h.strict_eq(x, &target))
});
Ok(Value::Float(idx.map(|i| i as f64).unwrap_or(-1.0)))
}
"includes" => {
let items = array_items(recv);
let target = arg0(&args);
let tnan = matches!(target, Value::Float(f) if f.is_nan());
Ok(Value::Bool(with_host(|h| {
items.iter().any(|x| {
(tnan && matches!(x, Value::Float(f) if f.is_nan())) || h.strict_eq(x, &target)
})
})))
}
"slice" => {
let items = array_items(recv);
let (lo, hi) = slice_bounds(&args, items.len());
Ok(with_host(|h| {
let out = h.new_array(items[lo..hi].to_vec());
h.copy_holes(recv, &out, |i| (i >= lo && i < hi).then(|| i - lo));
out
}))
}
"concat" => {
let mut out = array_items(recv);
let mut holes = hole_set(recv);
let mut sources: Vec<(Value, usize)> = Vec::new();
for a in &args {
match with_host(|h| h.get(a).cloned()) {
Some(JsObj::Array(items)) => {
sources.push((a.clone(), out.len()));
out.extend(items);
}
_ => out.push(a.clone()),
}
}
for (src, base) in sources {
holes.extend(
with_host(|h| h.hole_indices(&src))
.into_iter()
.map(|i| i + base),
);
}
Ok(with_host(|h| {
let arr = h.new_array(out);
h.install_holes(&arr, holes);
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 = hole_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 items = array_items(recv);
let holes = hole_set(recv);
let cb = arg0(&args);
let mut out = Vec::with_capacity(items.len());
for (i, it) in items.iter().enumerate() {
if holes.contains(&i) {
out.push(Value::Undef);
continue;
}
out.push(host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
None,
)?);
}
Ok(with_host(|h| {
let arr = h.new_array(out);
h.install_holes(&arr, holes);
arr
}))
}
"flatMap" => {
let items = array_items(recv);
let cb = arg0(&args);
let holes = hole_set(recv);
let mut out = Vec::new();
for (i, it) in items.iter().enumerate() {
if holes.contains(&i) {
continue;
}
let r = host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
None,
)?;
match with_host(|h| h.get(&r).cloned()) {
Some(JsObj::Array(inner)) => out.extend(inner),
_ => out.push(r),
}
}
Ok(with_host(|h| h.new_array(out)))
}
"filter" => {
let items = array_items(recv);
let holes = hole_set(recv);
let cb = arg0(&args);
let mut out = Vec::new();
for (i, it) in items.iter().enumerate() {
if holes.contains(&i) {
continue;
}
let keep = host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
None,
)?;
if with_host(|h| h.truthy(&keep)) {
out.push(it.clone());
}
}
Ok(with_host(|h| h.new_array(out)))
}
"forEach" => {
let items = array_items(recv);
let holes = hole_set(recv);
let cb = arg0(&args);
for (i, it) in items.iter().enumerate() {
if holes.contains(&i) {
continue;
}
host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
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()],
None,
)?;
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()],
None,
)?;
if with_host(|h| h.truthy(&m)) {
return Ok(Value::Float(i as f64));
}
}
Ok(Value::Float(-1.0))
}
"some" => {
let items = array_items(recv);
let holes = hole_set(recv);
let cb = arg0(&args);
for (i, it) in items.iter().enumerate() {
if holes.contains(&i) {
continue;
}
let m = host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
None,
)?;
if with_host(|h| h.truthy(&m)) {
return Ok(Value::Bool(true));
}
}
Ok(Value::Bool(false))
}
"every" => {
let items = array_items(recv);
let holes = hole_set(recv);
let cb = arg0(&args);
for (i, it) in items.iter().enumerate() {
if holes.contains(&i) {
continue;
}
let m = host::invoke(
&cb,
vec![it.clone(), Value::Float(i as f64), this_value.clone()],
None,
)?;
if !with_host(|h| h.truthy(&m)) {
return Ok(Value::Bool(false));
}
}
Ok(Value::Bool(true))
}
"reduce" => {
let items = array_items(recv);
let holes = hole_set(recv);
let cb = arg0(&args);
let mut 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",
))
}
}
}
for (i, it) in items.iter().enumerate().skip(start) {
if holes.contains(&i) {
continue;
}
acc = host::invoke(
&cb,
vec![acc, it.clone(), Value::Float(i as f64), this_value.clone()],
None,
)?;
}
Ok(acc)
}
"reduceRight" => {
let items = array_items(recv);
let holes = hole_set(recv);
let cb = arg0(&args);
let n = items.len();
let mut acc;
let mut i = n; if args.len() >= 2 {
acc = args[1].clone();
} else {
match (0..n).rev().find(|i| !holes.contains(i)) {
Some(k) => {
acc = items[k].clone();
i = k;
}
None => {
return Err(host::type_error(
"Reduce of empty array with no initial value",
))
}
}
}
while i > 0 {
i -= 1;
if holes.contains(&i) {
continue;
}
acc = host::invoke(
&cb,
vec![
acc,
items[i].clone(),
Value::Float(i as f64),
this_value.clone(),
],
None,
)?;
}
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()],
None,
)?;
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()],
None,
)?;
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 = hole_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();
items.resize(all.len(), Value::Undef);
with_host(|h| {
if let Some(JsObj::Array(a)) = h.get_mut(recv) {
*a = items;
}
h.install_holes(recv, (present..all.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)?;
Ok(with_host(|h| h.new_array(out)))
}
"keys" => {
let n = array_len(recv);
let items: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
}
"values" | "@@iterator" => {
let items = array_items(recv);
Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
}
"entries" => {
let items = array_items(recv);
let pairs: Vec<Value> = items
.into_iter()
.enumerate()
.map(|(i, v)| with_host(|h| h.new_array(vec![Value::Float(i as f64), v])))
.collect();
Ok(with_host(|h| {
h.alloc(JsObj::Iter {
items: pairs,
idx: 0,
})
}))
}
"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 = join_parts(&array_items(recv));
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 = with_host(|h| h.inspect(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 = hole_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()
}
});
Ok(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(),
);
let out = h.new_array(removed);
h.install_holes(
&out,
holes
.iter()
.filter(|&&i| i >= start && i < start + delete)
.map(|&i| i - start)
.collect(),
);
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))
}
fn string_method(s: &str, name: &str, args: Vec<Value>) -> Result<Value, String> {
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 })))
}
"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),
))
}
"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)?)),
"match" => crate::regexp::str_match(s, &arg0(&args)),
"matchAll" => crate::regexp::str_match_all(s, &arg0(&args)),
"search" => {
if is_regexp_arg(&arg0(&args)) {
crate::regexp::str_search(s, &arg0(&args))
} else {
let needle = with_host(|h| h.str_of(&arg0(&args)));
Ok(Value::Float(byte_to_unit_index(s, s.find(&needle))))
}
}
"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(s.replacen(&from, &to, 1)))
}
}
"replaceAll" => {
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, 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(s.replace(&from, &to)))
}
}
"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 new_s(s: String) -> Value {
with_host(|h| h.new_str(s))
}
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 byte_to_unit_index(s: &str, byte: Option<usize>) -> f64 {
match byte {
Some(b) => crate::utf16::index_of_byte(s, b).get() as f64,
None => -1.0,
}
}
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> {
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()], None)?;
}
Ok(Value::Undef)
}
"keys" | "values" | "entries" | "@@iterator" => {
let items: Vec<Value> = with_host(|h| {
let pairs: Vec<(Value, Value)> = match h.get(recv) {
Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
_ => Vec::new(),
};
pairs
.into_iter()
.map(|(k, v)| match name {
"keys" => k,
"values" => v,
_ => h.new_array(vec![k, v]), })
.collect()
});
Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
}
_ => 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"
}))
}
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()], None)?;
}
Ok(Value::Undef)
}
"keys" | "values" | "entries" | "@@iterator" => {
let items: Vec<Value> = with_host(|h| {
let vals: Vec<Value> = match h.get(recv) {
Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
_ => Vec::new(),
};
if name == "entries" {
vals.into_iter()
.map(|v| h.new_array(vec![v.clone(), v]))
.collect()
} else {
vals
}
});
Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
}
_ => 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 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)
})
}
fn iter_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"next" => {
let step = with_host(|h| {
if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
if *idx < items.len() {
let v = items[*idx].clone();
*idx += 1;
return Some(v);
}
}
None
});
Ok(match step {
Some(v) => iter_result(v, false),
None => iter_result(Value::Undef, true),
})
}
"return" => {
with_host(|h| {
if let Some(JsObj::Iter { items, idx }) = h.get_mut(recv) {
*idx = 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)
})),
_ => 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 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());
}
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 = with_host(|h| h.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))
)));
}
crate::proxy::define_property(&obj, &key, &desc)?;
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 = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(Value::Undef)));
apply_descriptor(&obj, &key, &desc);
Ok(obj)
}
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))
)))
}
fn apply_descriptor(obj: &Value, key: &str, desc: &Value) {
let (value, get, set, attrs) = with_host(|h| match h.get(desc) {
Some(JsObj::Object(p)) => {
let flag = |n: &str| p.get(n).map(|v| h.truthy(v)).unwrap_or(false);
(
p.get("value").cloned(),
p.get("get").cloned(),
p.get("set").cloned(),
host::PropAttrs {
writable: flag("writable"),
enumerable: flag("enumerable"),
configurable: flag("configurable"),
},
)
}
_ => (None, None, None, host::PropAttrs::default()),
});
with_host(|h| h.set_prop_attrs(obj, key, attrs));
if get.is_some() || set.is_some() {
with_host(|h| h.set_accessor(obj, key, get, set));
} else if let Some(v) = value {
if matches!(
with_host(|h| h.get(obj).cloned()),
Some(JsObj::Func(_)) | Some(JsObj::Class(_))
) {
with_host(|h| h.set_fn_prop(obj, key, v));
} else 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);
});
} else {
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 object_get_own_descriptor(args: Vec<Value>) -> Result<Value, String> {
let obj = arg0(&args);
require_object_coercible(&obj)?;
let key = with_host(|h| h.property_key(&args.get(1).cloned().unwrap_or(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 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 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;
}
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(),
_ => false,
})
}
pub(crate) fn deep_clone(v: &Value) -> Value {
deep_clone_seen(v, &mut std::collections::HashMap::new())
}
fn deep_clone_seen(v: &Value, seen: &mut std::collections::HashMap<u32, Value>) -> Value {
let idx = match v {
Value::Obj(i) => *i,
_ => return v.clone(),
};
if let Some(done) = seen.get(&idx) {
return done.clone();
}
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 cloned: Vec<Value> = items.iter().map(|x| deep_clone_seen(x, seen)).collect();
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(props)) => {
let out = with_host(|h| h.new_object(IndexMap::new()));
seen.insert(idx, out.clone());
let cloned: IndexMap<String, Value> = props
.iter()
.map(|(k, val)| (k.clone(), deep_clone_seen(val, seen)))
.collect();
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(&out) {
*p = cloned;
}
if let Some(p) = h.proto_of(v) {
h.set_proto(&out, p);
}
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(),
}
}
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 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 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.alloc(JsObj::Builtin(format!("{c}.prototype"))),
None => h.null(),
}
})
}
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)
}
fn promise_resolve(v: Value) -> Result<Value, String> {
Ok(host::promise_of(&v))
}
fn promise_reject(v: Value) -> Result<Value, String> {
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
host::reject_promise_val(id, v);
Ok(p)
}
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)))
}
#[derive(Clone, Copy)]
enum AllMode {
All,
AllSettled,
}
fn promise_all(args: Vec<Value>, mode: AllMode) -> Result<Value, String> {
let items = host::iter_all(&arg0(&args))?;
let result = 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 = host::iter_all(&arg0(&args))?;
let result = 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("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);
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));
}