use fusevm::{Chunk, NumOp, VMResult, Value, VM};
use indexmap::IndexMap;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::rc::Rc;
use std::sync::mpsc::{Receiver, Sender};
use std::time::{Duration, Instant};
pub type IoTask = Box<dyn FnOnce() -> Result<(), String> + Send>;
pub mod ops {
pub const GETLOCAL: u16 = 1; pub const SETLOCAL: u16 = 2; pub const DECLARE: u16 = 3; pub const DELNAME: u16 = 4; pub const GETATTR: u16 = 5; pub const SETATTR: u16 = 6; pub const GETITEM: u16 = 7; pub const SETITEM: u16 = 8; pub const DELITEM: u16 = 9; pub const MKSTR: u16 = 10; pub const MKARR: u16 = 11; pub const MKOBJ: u16 = 12; pub const CALL: u16 = 13; pub const CALL_METHOD: u16 = 14; pub const CALL_VALUE: u16 = 15; pub const NEW: u16 = 16; pub const TRUTHY: u16 = 17; pub const TOSTR: u16 = 18; pub const MKFUNC: u16 = 19; pub const GETITER: u16 = 20; pub const FORITER: u16 = 21; pub const FORIN_KEYS: u16 = 22; pub const CONTAINS: u16 = 23; pub const SIG_RETURN: u16 = 24; pub const BINOP: u16 = 25; pub const UNARY: u16 = 26; pub const STRICT_EQ: u16 = 27; pub const LOOSE_EQ: u16 = 28; pub const TYPEOF: u16 = 29; pub const LOAD_NULL: u16 = 30; pub const THROW: u16 = 31; pub const TRY: u16 = 32; pub const NULLISH: u16 = 33; pub const UNPACK: u16 = 34; pub const BUILD_ARGS: u16 = 35; pub const THIS: u16 = 36; pub const INSTANCEOF: u16 = 37; pub const DELPROP_NAME: u16 = 38; pub const APPLY: u16 = 39; pub const APPLY_METHOD: u16 = 40; pub const OBJ_REST: u16 = 41; pub const DIV: u16 = 42; pub const MKCLASS: u16 = 43; pub const DEF_MEMBER: u16 = 44; pub const SUPER_CALL: u16 = 45; pub const SUPER_GET: u16 = 46; pub const YIELD: u16 = 47; pub const PROPKEY: u16 = 48; pub const NEW_TARGET: u16 = 49; pub const DEF_FIELD: u16 = 50; pub const AWAIT: u16 = 51; pub const DEF_ACCESSOR: u16 = 52; pub const DBG_LINE: u16 = 53; pub const MKBIGINT: u16 = 54; pub const MKREGEX: u16 = 55; pub const TAG_TMPL: u16 = 56; pub const GET_ASYNC_ITER: u16 = 57; pub const ASYNC_STEP: u16 = 58; pub const NUM_STEP: u16 = 59; pub const ITER_CLOSE: u16 = 60; pub const TYPEOF_NAME: u16 = 61; }
pub mod member {
pub const METHOD: i64 = 0;
pub const GET: i64 = 1;
pub const SET: i64 = 2;
}
pub mod binop {
pub const BITAND: i64 = 0;
pub const BITOR: i64 = 1;
pub const BITXOR: i64 = 2;
pub const SHL: i64 = 3;
pub const SHR: i64 = 4;
pub const USHR: i64 = 5;
}
pub mod unop {
pub const POS: i64 = 0; pub const BITNOT: i64 = 1; }
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct FuncDef {
pub name: String,
pub params: Vec<ParamSlot>,
pub chunk: Chunk,
pub is_arrow: bool,
pub is_generator: bool,
pub is_async: bool,
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct ParamSlot {
pub name: String,
pub rest: bool,
pub has_default: bool,
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct TryDef {
pub block: Chunk,
pub handler: Option<(Option<String>, Chunk)>,
pub finalizer: Option<Chunk>,
}
#[derive(Clone)]
pub struct FuncVal {
pub def_id: usize,
pub env: Option<Env>,
pub this: Option<Value>,
pub is_arrow: bool,
pub home_class: Option<String>,
}
#[derive(Clone)]
pub enum JsObj {
Str(String),
Array(Vec<Value>),
Object(IndexMap<String, Value>),
Func(FuncVal),
Builtin(String),
BoundMethod {
recv: Value,
name: String,
},
Null,
Iter {
items: Vec<Value>,
idx: usize,
},
BoundFunc {
target: Value,
this: Value,
args: Vec<Value>,
},
Class(ClassVal),
Symbol {
desc: Option<String>,
id: u64,
},
Map {
entries: IndexMap<MapKey, (Value, Value)>,
weak: bool,
},
Set {
entries: IndexMap<MapKey, Value>,
weak: bool,
},
Generator {
id: u32,
},
Promise {
id: u32,
},
BigInt(num_bigint::BigInt),
RegExp(Box<RegExpObj>),
}
#[derive(Clone)]
pub struct RegExpObj {
pub re: fancy_regex::Regex,
pub source: String,
pub flags: String,
pub global: bool,
pub ignore_case: bool,
pub multiline: bool,
pub dot_all: bool,
pub sticky: bool,
pub unicode: bool,
pub last_index: usize,
}
pub struct PromiseCell {
pub state: PromiseState,
pub value: Value,
pub reactions: Vec<PromiseReaction>,
pub handled: bool,
}
pub enum PromiseReaction {
Js {
on_ful: Value,
on_rej: Value,
result: Value,
},
Native(Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>),
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum PromiseState {
#[default]
Pending,
Fulfilled,
Rejected,
}
#[derive(Clone)]
pub struct ClassVal {
pub name: String,
pub ctor: Option<Value>,
pub parent: Option<Value>,
pub proto: Value,
pub statics: IndexMap<String, Value>,
pub fields: Vec<(String, Value)>,
}
pub enum SuperRef {
Getter(Value),
Data(Value),
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum MapKey {
Undef,
Null,
Bool(bool),
Num(u64),
Big(String),
Str(String),
Ref(u32),
}
pub struct EnvData {
pub vars: IndexMap<String, Value>,
pub parent: Option<Env>,
}
pub type Env = Rc<RefCell<EnvData>>;
pub type Accessor = (Option<Value>, Option<Value>);
fn new_env(parent: Option<Env>) -> Env {
Rc::new(RefCell::new(EnvData {
vars: IndexMap::new(),
parent,
}))
}
pub struct Frame {
pub env: Env,
pub this_obj: Option<Value>,
pub new_target: Option<Value>,
pub home_class: Option<Value>,
pub line: u32,
pub owner: Option<String>,
}
#[derive(Clone)]
pub enum Signal {
Return(Value),
Break,
Continue,
}
pub struct JsHost {
heap: Vec<JsObj>,
pub funcs: Vec<FuncDef>,
pub tries: Vec<TryDef>,
globals: IndexMap<String, Value>,
frames: Vec<Frame>,
pub error: Option<String>,
pub exc: Option<Value>,
pub signal: Option<Signal>,
null_val: Value,
protos: HashMap<u32, Value>,
null_proto_objs: HashSet<u32>,
fn_props: HashMap<u32, IndexMap<String, Value>>,
accessors: HashMap<u32, IndexMap<String, Accessor>>,
builtin_statics: HashMap<String, IndexMap<String, Value>>,
object_proto: Value,
proto_class: HashMap<u32, Value>,
class_registry: HashMap<String, Value>,
error_protos: HashMap<String, Value>,
symbol_registry: HashMap<String, Value>,
next_symbol: u64,
generators: Vec<GenCell>,
promises: Vec<PromiseCell>,
pub nextticks: std::collections::VecDeque<Task>,
pub microtasks: std::collections::VecDeque<Task>,
pub macrotasks: Vec<Timer>,
next_timer: u64,
io_tx: Sender<IoTask>,
io_rx: Option<Receiver<IoTask>>,
open_handles: usize,
}
pub enum Task {
Js { cb: Value, args: Vec<Value> },
Native(Box<dyn FnOnce() -> Result<(), String>>),
}
impl Task {
fn run(self) -> Result<(), String> {
match self {
Task::Js { cb, args } => invoke(&cb, args, None).map(|_| ()),
Task::Native(f) => f(),
}
}
}
pub struct Timer {
pub id: u64,
pub delay: f64,
pub seq: u64,
pub callback: Value,
pub args: Vec<Value>,
pub cancelled: bool,
pub deadline: Instant,
}
struct GenCell {
coro: Option<corosensei::Coroutine<Value, Value, Result<Value, String>>>,
yielder: *const (),
ctx: GenContext,
done: bool,
started: bool,
inject: Option<GenInject>,
}
enum GenInject {
Return(Value),
Throw(Value),
}
#[derive(Default)]
struct GenContext {
frames: Vec<Frame>,
error: Option<String>,
exc: Option<Value>,
signal: Option<Signal>,
}
thread_local! {
static CUR_GEN: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
}
thread_local! {
static HOST: RefCell<JsHost> = RefCell::new(JsHost::new());
}
pub fn with_host<R>(f: impl FnOnce(&mut JsHost) -> R) -> R {
HOST.with(|h| f(&mut h.borrow_mut()))
}
pub fn reset_host() {
with_host(|h| *h = JsHost::new());
crate::module::reset();
}
impl Default for JsHost {
fn default() -> Self {
Self::new()
}
}
impl JsHost {
pub fn new() -> JsHost {
let module_env = new_env(None);
let (io_tx, io_rx) = std::sync::mpsc::channel();
let mut h = JsHost {
heap: Vec::new(),
funcs: Vec::new(),
tries: Vec::new(),
globals: IndexMap::new(),
frames: vec![Frame {
env: module_env,
this_obj: None,
new_target: None,
home_class: None,
line: 0,
owner: None,
}],
error: None,
exc: None,
signal: None,
null_val: Value::Undef,
protos: HashMap::new(),
null_proto_objs: HashSet::new(),
fn_props: HashMap::new(),
accessors: HashMap::new(),
builtin_statics: HashMap::new(),
object_proto: Value::Undef,
proto_class: HashMap::new(),
class_registry: HashMap::new(),
error_protos: HashMap::new(),
symbol_registry: HashMap::new(),
next_symbol: 1,
generators: Vec::new(),
promises: Vec::new(),
microtasks: std::collections::VecDeque::new(),
nextticks: std::collections::VecDeque::new(),
macrotasks: Vec::new(),
next_timer: 1,
io_tx,
io_rx: Some(io_rx),
open_handles: 0,
};
h.null_val = h.alloc(JsObj::Null);
h.object_proto = h.new_object(IndexMap::new());
h
}
pub fn proto_of(&self, v: &Value) -> Option<Value> {
if let Value::Obj(i) = v {
self.protos.get(i).cloned()
} else {
None
}
}
pub fn set_proto(&mut self, v: &Value, proto: Value) {
if let Value::Obj(i) = v {
if self.is_null(&proto) {
self.protos.remove(i);
self.null_proto_objs.insert(*i);
} else if matches!(proto, Value::Undef) {
self.protos.remove(i);
} else {
self.protos.insert(*i, proto);
self.null_proto_objs.remove(i);
}
}
}
pub fn has_null_proto(&self, v: &Value) -> bool {
matches!(v, Value::Obj(i) if self.null_proto_objs.contains(i))
}
pub fn object_proto(&self) -> Value {
self.object_proto.clone()
}
pub fn tag_proto_class(&mut self, proto: &Value, class_val: Value) {
if let Value::Obj(i) = proto {
self.proto_class.insert(*i, class_val);
}
}
pub fn class_of(&self, obj: &Value) -> Option<Value> {
let mut cur = self.proto_of(obj);
while let Some(p) = cur {
if let Value::Obj(i) = &p {
if let Some(c) = self.proto_class.get(i) {
return Some(c.clone());
}
}
cur = self.proto_of(&p);
}
None
}
pub fn ctor_name(&self, obj: &Value) -> String {
match self.class_of(obj) {
Some(c) => match self.get(&c) {
Some(JsObj::Class(cv)) => cv.name.clone(),
_ => String::new(),
},
None => String::new(),
}
}
pub fn fn_prop(&self, v: &Value, name: &str) -> Option<Value> {
if let Value::Obj(i) = v {
self.fn_props.get(i).and_then(|m| m.get(name).cloned())
} else {
None
}
}
pub fn class_static(&self, class_val: &Value, name: &str) -> Option<Value> {
let mut cur = class_val.clone();
loop {
if let Some(v) = self.fn_prop(&cur, name) {
return Some(v);
}
match self.get(&cur) {
Some(JsObj::Class(c)) => cur = c.parent.clone()?,
_ => return None,
}
}
}
pub fn set_fn_prop(&mut self, v: &Value, name: &str, val: Value) {
if let Value::Obj(i) = v {
self.fn_props
.entry(*i)
.or_default()
.insert(name.to_string(), val);
}
}
pub fn builtin_static(&self, ns: &str, name: &str) -> Option<Value> {
self.builtin_statics
.get(ns)
.and_then(|m| m.get(name).cloned())
}
pub fn set_builtin_static(&mut self, ns: &str, name: &str, val: Value) {
self.builtin_statics
.entry(ns.to_string())
.or_default()
.insert(name.to_string(), val);
}
pub fn fn_prop_keys(&self, v: &Value) -> Vec<String> {
if let Value::Obj(i) = v {
self.fn_props
.get(i)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default()
} else {
Vec::new()
}
}
pub fn set_accessor(
&mut self,
owner: &Value,
key: &str,
get: Option<Value>,
set: Option<Value>,
) {
if let Value::Obj(i) = owner {
let slot = self
.accessors
.entry(*i)
.or_default()
.entry(key.to_string())
.or_insert((None, None));
if get.is_some() {
slot.0 = get;
}
if set.is_some() {
slot.1 = set;
}
}
}
pub fn own_accessor(&self, owner: &Value, key: &str) -> Option<(Option<Value>, Option<Value>)> {
if let Value::Obj(i) = owner {
self.accessors.get(i).and_then(|m| m.get(key).cloned())
} else {
None
}
}
pub fn new_symbol(&mut self, desc: Option<String>) -> Value {
let id = self.next_symbol;
self.next_symbol += 1;
self.alloc(JsObj::Symbol { desc, id })
}
pub fn symbol_for(&mut self, key: &str) -> Value {
if let Some(v) = self.symbol_registry.get(key) {
return v.clone();
}
let s = self.new_symbol(Some(key.to_string()));
self.symbol_registry.insert(key.to_string(), s.clone());
s
}
pub fn well_known_iterator(&mut self) -> Value {
self.symbol_for("@@Symbol.iterator")
}
pub fn well_known_async_iterator(&mut self) -> Value {
self.symbol_for("@@Symbol.asyncIterator")
}
pub fn property_key(&self, v: &Value) -> String {
if let Some(JsObj::Symbol { desc, id }) = self.get(v) {
if desc.as_deref() == Some("@@Symbol.iterator") {
return "@@iterator".to_string();
}
if desc.as_deref() == Some("@@Symbol.asyncIterator") {
return "@@asyncIterator".to_string();
}
return format!("@@sym:{id}");
}
self.str_of(v)
}
pub fn null(&self) -> Value {
self.null_val.clone()
}
pub fn is_null(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Null))
}
pub fn program_offsets(&self) -> (usize, usize) {
(self.funcs.len(), self.tries.len())
}
pub fn load_program(&mut self, funcs: Vec<FuncDef>, tries: Vec<TryDef>) {
self.funcs.extend(funcs);
self.tries.extend(tries);
}
pub fn try_def(&self, id: usize) -> Option<TryDef> {
self.tries.get(id).cloned()
}
pub fn alloc(&mut self, obj: JsObj) -> Value {
self.heap.push(obj);
Value::Obj((self.heap.len() - 1) as u32)
}
pub fn get(&self, v: &Value) -> Option<&JsObj> {
if let Value::Obj(i) = v {
self.heap.get(*i as usize)
} else {
None
}
}
pub fn get_mut(&mut self, v: &Value) -> Option<&mut JsObj> {
if let Value::Obj(i) = v {
self.heap.get_mut(*i as usize)
} else {
None
}
}
pub fn new_str(&mut self, s: impl Into<String>) -> Value {
self.alloc(JsObj::Str(s.into()))
}
pub fn new_array(&mut self, items: Vec<Value>) -> Value {
self.alloc(JsObj::Array(items))
}
pub fn new_object(&mut self, mut props: IndexMap<String, Value>) -> Value {
canonicalize_own_keys(&mut props);
self.alloc(JsObj::Object(props))
}
pub fn as_str(&self, v: &Value) -> Option<String> {
match v {
Value::Str(s) => Some((**s).clone()),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => Some(s.clone()),
_ => None,
},
_ => None,
}
}
fn frame(&self) -> &Frame {
self.frames.last().unwrap()
}
fn cur_env(&self) -> Env {
self.frame().env.clone()
}
pub fn frame_depth(&self) -> usize {
self.frames.len()
}
pub fn set_cur_line(&mut self, line: u32) {
if let Some(f) = self.frames.last_mut() {
f.line = line;
}
}
pub fn dbg_stack(&self) -> Vec<(String, u32)> {
self.frames
.iter()
.rev()
.map(|f| {
let name = f.owner.clone().unwrap_or_else(|| "<module>".to_string());
(name, f.line)
})
.collect()
}
pub fn dbg_locals(&self) -> Vec<(String, String)> {
let env = self.cur_env();
let names: Vec<String> = env.borrow().vars.keys().cloned().collect();
names
.into_iter()
.map(|n| {
let v = self.read_name(&n).unwrap_or(Value::Undef);
(n, self.inspect(&v))
})
.collect()
}
pub fn read_name(&self, name: &str) -> Option<Value> {
let mut env = Some(self.cur_env());
while let Some(e) = env {
if let Some(v) = e.borrow().vars.get(name) {
return Some(v.clone());
}
env = e.borrow().parent.clone();
}
self.globals.get(name).cloned()
}
pub fn read_global(&self, name: &str) -> Option<Value> {
self.globals.get(name).cloned()
}
pub fn set_name(&mut self, name: &str, val: Value) {
let mut env = Some(self.cur_env());
while let Some(e) = env {
if e.borrow().vars.contains_key(name) {
e.borrow_mut().vars.insert(name.to_string(), val);
return;
}
env = e.borrow().parent.clone();
}
self.globals.insert(name.to_string(), val);
}
pub fn declare_name(&mut self, name: &str, val: Value) {
if self.frames.len() == 1 {
self.globals.insert(name.to_string(), val);
} else {
self.cur_env()
.borrow_mut()
.vars
.insert(name.to_string(), val);
}
}
pub fn set_global(&mut self, name: &str, val: Value) {
self.globals.insert(name.to_string(), val);
}
pub fn del_name(&mut self, name: &str) {
if self
.cur_env()
.borrow_mut()
.vars
.shift_remove(name)
.is_some()
{
return;
}
self.globals.shift_remove(name);
}
pub fn current_this(&self) -> Option<Value> {
self.frame().this_obj.clone()
}
pub fn current_env_capture(&self) -> Env {
self.frame().env.clone()
}
pub fn current_new_target(&self) -> Option<Value> {
self.frame().new_target.clone()
}
fn current_home_class(&self) -> Option<Value> {
self.frame().home_class.clone()
}
pub fn super_context(&self) -> (Option<Value>, Vec<(String, Value)>) {
match self.current_home_class() {
Some(cv) => match self.get(&cv) {
Some(JsObj::Class(c)) => (c.parent.clone(), c.fields.clone()),
_ => (None, Vec::new()),
},
None => (None, Vec::new()),
}
}
pub fn super_resolve(&self, name: &str) -> SuperRef {
let parent = match self
.current_home_class()
.and_then(|cv| match self.get(&cv) {
Some(JsObj::Class(c)) => c.parent.clone(),
_ => None,
}) {
Some(p) => p,
None => return SuperRef::Data(Value::Undef),
};
let parent_proto = match self.get(&parent) {
Some(JsObj::Class(pc)) => pc.proto.clone(),
_ => self.fn_prop(&parent, "prototype").unwrap_or(Value::Undef),
};
if let Some((Some(getter), _)) = lookup_accessor(self, &parent_proto, name) {
return SuperRef::Getter(getter);
}
SuperRef::Data(lookup_chain(self, &parent_proto, name).unwrap_or(Value::Undef))
}
pub fn take_error(&mut self) -> Option<String> {
self.error.take()
}
pub fn raise_str(&mut self, class: &str, msg: &str) -> String {
let s = if msg.is_empty() {
class.to_string()
} else {
format!("{class}: {msg}")
};
self.error = Some(s.clone());
s
}
}
pub fn type_error(msg: &str) -> String {
format!("TypeError: {msg}")
}
pub fn ref_error(name: &str) -> String {
format!("ReferenceError: {name} is not defined")
}
pub fn range_error(msg: &str) -> String {
format!("RangeError: {msg}")
}
thread_local! {
static DEBUG_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub fn set_debug_mode(on: bool) {
DEBUG_MODE.with(|d| d.set(on));
}
pub fn run_chunk_on(chunk: Chunk) -> Result<Value, String> {
let mut vm = VM::new(chunk);
crate::builtins::install(&mut vm);
vm.set_numeric_hook(std::sync::Arc::new(|op, a, b| {
crate::builtins::numeric_hook(op, a, b)
}));
if DEBUG_MODE.with(|d| d.get()) {
vm.set_extension_handler(Box::new(|vm, id, _| {
crate::dap::on_ext(vm, id);
}));
} else {
vm.enable_tracing_jit();
}
let outcome = vm.run();
if let Some(e) = with_host(|h| h.take_error()) {
return Err(e);
}
match outcome {
VMResult::Ok(v) => Ok(v),
VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
VMResult::Error(e) => Err(e),
}
}
pub fn run_main(chunk: Chunk) -> Result<Value, String> {
let r = run_chunk_on(chunk);
with_host(|h| h.signal = None);
if r.is_ok() {
run_event_loop()?;
}
r
}
pub fn fmt_number(f: f64) -> String {
if f.is_nan() {
return "NaN".into();
}
if f.is_infinite() {
return if f > 0.0 { "Infinity" } else { "-Infinity" }.into();
}
if f == 0.0 {
return "0".into();
}
if f < 0.0 {
return format!("-{}", js_number_repr(-f));
}
js_number_repr(f)
}
pub fn array_index(k: &str) -> Option<u32> {
if k.is_empty() {
return None;
}
if k == "0" {
return Some(0);
}
if k.as_bytes()[0] == b'0' {
return None;
}
if !k.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
match k.parse::<u64>() {
Ok(n) if n < u32::MAX as u64 => Some(n as u32),
_ => None,
}
}
pub fn key_order_cmp(a: &str, b: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (array_index(a), array_index(b)) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
}
pub fn canonicalize_own_keys(props: &mut IndexMap<String, Value>) {
if props.keys().any(|k| array_index(k).is_some()) {
props.sort_by(|ak, _, bk, _| key_order_cmp(ak, bk));
}
}
fn js_number_repr(a: f64) -> String {
let sci = format!("{a:e}");
let (mant, exp_str) = sci.split_once('e').expect("LowerExp always has 'e'");
let e: i32 = exp_str.parse().expect("LowerExp exponent is an integer");
let s: String = mant.chars().filter(|c| *c != '.').collect();
let k = s.len() as i32; let n = e + 1;
if k <= n && n <= 21 {
let mut out = s;
out.push_str(&"0".repeat((n - k) as usize));
out
} else if 0 < n && n <= 21 {
format!("{}.{}", &s[..n as usize], &s[n as usize..])
} else if -6 < n && n <= 0 {
format!("0.{}{}", "0".repeat((-n) as usize), s)
} else {
let exp = n - 1;
let sign = if exp >= 0 { '+' } else { '-' };
let mag = exp.abs();
if k == 1 {
format!("{s}e{sign}{mag}")
} else {
format!("{}.{}e{sign}{mag}", &s[..1], &s[1..])
}
}
}
impl JsHost {
pub fn type_of(&self, v: &Value) -> &'static str {
match v {
Value::Undef => "undefined",
Value::Bool(_) => "boolean",
Value::Int(_) | Value::Float(_) => "number",
Value::Str(_) => "string",
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(_)) => "string",
Some(JsObj::Func(_))
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::Class(_)) => "function",
Some(JsObj::Builtin(n)) => {
const NON_CALLABLE_NS: &[&str] = &[
"Math",
"JSON",
"console",
"Reflect",
"process",
"Atomics",
"performance",
"fs",
"path",
"os",
"util",
"crypto",
"querystring",
"events",
"stream",
"timers",
"perf_hooks",
"async_hooks",
"diagnostics_channel",
"v8",
"dns",
"punycode",
"child_process",
"tty",
"url",
"zlib",
"string_decoder",
"assert",
"http",
"net",
"buffer",
];
if NON_CALLABLE_NS.contains(&n.as_str()) {
"object"
} else {
"function"
}
}
Some(JsObj::Symbol { .. }) => "symbol",
Some(JsObj::BigInt(_)) => "bigint",
_ => "object", },
_ => "object",
}
}
pub fn truthy(&self, v: &Value) -> bool {
match v {
Value::Undef => false,
Value::Bool(b) => *b,
Value::Int(n) => *n != 0,
Value::Float(f) => *f != 0.0 && !f.is_nan(),
Value::Str(s) => !s.is_empty(),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => !s.is_empty(),
Some(JsObj::Null) => false,
Some(JsObj::BigInt(b)) => !num_traits::Zero::is_zero(b),
_ => true, },
_ => true,
}
}
pub fn to_number(&self, v: &Value) -> f64 {
match v {
Value::Undef => f64::NAN,
Value::Bool(b) => {
if *b {
1.0
} else {
0.0
}
}
Value::Int(n) => *n as f64,
Value::Float(f) => *f,
Value::Str(s) => str_to_number(s),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => str_to_number(s),
Some(JsObj::Null) => 0.0,
Some(JsObj::BigInt(b)) => bigint_to_f64(b),
Some(JsObj::Array(items)) => {
if items.is_empty() {
0.0
} else if items.len() == 1 {
self.to_number(&items[0])
} else {
f64::NAN
}
}
_ => f64::NAN,
},
_ => f64::NAN,
}
}
pub fn str_of(&self, v: &Value) -> String {
match v {
Value::Undef => "undefined".into(),
Value::Bool(b) => if *b { "true" } else { "false" }.into(),
Value::Int(n) => n.to_string(),
Value::Float(f) => fmt_number(*f),
Value::Str(s) => (**s).clone(),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => s.clone(),
Some(JsObj::Null) => "null".into(),
Some(JsObj::BigInt(b)) => b.to_string(),
Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
Some(JsObj::Array(items)) => {
let parts: Vec<String> = items
.iter()
.map(|x| match x {
Value::Undef => String::new(),
_ if self.is_null(x) => String::new(),
_ => self.str_of(x),
})
.collect();
parts.join(",")
}
Some(JsObj::Object(props)) => {
if props.get("@@native").map(|t| self.str_of(t)).as_deref() == Some("Buffer") {
let bytes: Vec<u8> = match props.get("@@bytes").and_then(|b| self.get(b)) {
Some(JsObj::Array(items)) => {
items.iter().map(|x| self.to_number(x) as u8).collect()
}
_ => Vec::new(),
};
String::from_utf8_lossy(&bytes).into_owned()
} else {
"[object Object]".into()
}
}
Some(JsObj::Func(f)) => {
let name = self
.funcs
.get(f.def_id)
.map(|d| d.name.clone())
.unwrap_or_default();
format!("function {name}() {{ [code] }}")
}
Some(JsObj::Builtin(n)) => format!("function {n}() {{ [native code] }}"),
Some(JsObj::BoundMethod { .. }) | Some(JsObj::BoundFunc { .. }) => {
"function () { [native code] }".into()
}
Some(JsObj::Class(c)) => format!("class {} {{ }}", c.name),
Some(JsObj::Symbol { desc, .. }) => {
match desc {
Some(d) => format!("Symbol({d})"),
None => "Symbol()".into(),
}
}
_ => "[object Object]".into(),
},
_ => "[object Object]".into(),
}
}
pub fn console_format(&self, v: &Value) -> String {
match v {
Value::Str(_) => self.str_of(v),
Value::Obj(_) if matches!(self.get(v), Some(JsObj::Str(_))) => self.str_of(v),
_ => self.inspect(v),
}
}
pub fn inspect(&self, v: &Value) -> String {
self.inspect_lvl(v, 0)
}
fn inspect_lvl(&self, v: &Value, indent: usize) -> String {
match v {
Value::Undef => "undefined".into(),
Value::Bool(b) => if *b { "true" } else { "false" }.into(),
Value::Int(n) => n.to_string(),
Value::Float(f) if *f == 0.0 && f.is_sign_negative() => "-0".into(),
Value::Float(f) => fmt_number(*f),
Value::Str(s) => quote_str(s),
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(s)) => quote_str(s),
Some(JsObj::Null) => "null".into(),
Some(JsObj::BigInt(b)) => format!("{b}n"),
Some(JsObj::RegExp(r)) => format!("/{}/{}", r.source, r.flags),
Some(JsObj::Array(items)) => {
let prop_keys: Vec<String> = self
.fn_prop_keys(v)
.into_iter()
.filter(|k| !k.starts_with("@@") && !k.starts_with('#'))
.collect();
if items.is_empty() && prop_keys.is_empty() {
return "[]".into();
}
if indent > 2 * inspect_max_depth() {
return "[Array]".into();
}
let mut inner: Vec<String> = items
.iter()
.map(|x| self.inspect_lvl(x, indent + 2))
.collect();
let has_props = !prop_keys.is_empty();
for k in &prop_keys {
let val = self.fn_prop(v, k).unwrap_or(Value::Undef);
inner.push(format!(
"{}: {}",
fmt_key(k),
self.inspect_lvl(&val, indent + 2)
));
}
self.render_array(&inner, items, indent, has_props)
}
Some(JsObj::Object(props)) => {
let prefix = if self.has_null_proto(v) {
"[Object: null prototype] ".to_string()
} else {
match self.ctor_name(v) {
n if n.is_empty() || n == "Object" => String::new(),
n => format!("{n} "),
}
};
let shown: Vec<(&String, &Value)> = props
.iter()
.filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
.collect();
if shown.is_empty() {
return format!("{prefix}{{}}");
}
if indent > 2 * inspect_max_depth() {
return if prefix.is_empty() {
"[Object]".into()
} else if self.has_null_proto(v) {
prefix.trim_end().to_string()
} else {
format!("[{}]", prefix.trim_end())
};
}
let inner: Vec<String> = shown
.iter()
.map(|(k, val)| {
format!("{}: {}", fmt_key(k), self.inspect_lvl(val, indent + 2))
})
.collect();
self.render_object(&inner, &prefix, indent)
}
Some(JsObj::Symbol { desc, .. }) => match desc {
Some(d) => format!("Symbol({d})"),
None => "Symbol()".into(),
},
Some(JsObj::Class(c)) => {
if c.parent.is_some() {
let pname = c
.parent
.as_ref()
.map(|p| self.callable_name(p))
.unwrap_or_default();
format!("[class {} extends {}]", c.name, pname)
} else {
format!("[class {}]", c.name)
}
}
Some(JsObj::Map { entries, .. }) => {
if entries.is_empty() {
return "Map(0) {}".into();
}
let inner: Vec<String> = entries
.values()
.map(|(k, val)| format!("{} => {}", self.inspect(k), self.inspect(val)))
.collect();
format!("Map({}) {{ {} }}", entries.len(), inner.join(", "))
}
Some(JsObj::Set { entries, .. }) => {
if entries.is_empty() {
return "Set(0) {}".into();
}
let inner: Vec<String> = entries.values().map(|v| self.inspect(v)).collect();
format!("Set({}) {{ {} }}", entries.len(), inner.join(", "))
}
Some(JsObj::Generator { .. }) => "Object [Generator] {}".into(),
Some(JsObj::Promise { id }) => match self.promises.get(*id as usize) {
Some(c) => match c.state {
PromiseState::Pending => "Promise { <pending> }".into(),
PromiseState::Fulfilled => {
format!("Promise {{ {} }}", self.inspect(&c.value))
}
PromiseState::Rejected => {
format!("Promise {{ <rejected> {} }}", self.inspect(&c.value))
}
},
None => "Promise { <pending> }".into(),
},
Some(JsObj::Func(f)) => {
let name = self
.funcs
.get(f.def_id)
.map(|d| d.name.clone())
.unwrap_or_default();
if name.is_empty() {
"[Function (anonymous)]".into()
} else {
format!("[Function: {name}]")
}
}
Some(JsObj::Builtin(n)) => {
let short = n.rsplit('.').next().unwrap_or(n);
format!("[Function: {short}]")
}
Some(JsObj::BoundMethod { .. }) => "[Function (anonymous)]".into(),
Some(JsObj::BoundFunc { target, .. }) => {
let n = self.callable_name(target);
if n.is_empty() {
"[Function: bound ]".into()
} else {
format!("[Function: bound {n}]")
}
}
_ => "undefined".into(),
},
_ => "undefined".into(),
}
}
fn render_array(
&self,
output: &[String],
values: &[Value],
indent: usize,
has_props: bool,
) -> String {
let entries = output.len();
let (lines, grouped) = if entries > 6 && !has_props {
group_array_elements(self, output, values, indent)
} else {
(output.to_vec(), false)
};
if !grouped {
let start = output.len() + indent + 1 + 10;
if is_below_break_length(output, start) {
return format!("[ {} ]", output.join(", "));
}
}
let pad = " ".repeat(indent);
let sep = format!(",\n{pad} ");
format!("[\n{pad} {}\n{pad}]", lines.join(&sep))
}
fn render_object(&self, output: &[String], prefix: &str, indent: usize) -> String {
let braces0 = prefix.chars().count() + 1;
let start = output.len() + indent + braces0 + 10;
if is_below_break_length(output, start) {
return format!("{prefix}{{ {} }}", output.join(", "));
}
let pad = " ".repeat(indent);
let sep = format!(",\n{pad} ");
format!("{prefix}{{\n{pad} {}\n{pad}}}", output.join(&sep))
}
pub fn callable_name(&self, v: &Value) -> String {
if let Some(n) = self.fn_prop(v, "name") {
return self.str_of(&n);
}
match self.get(v) {
Some(JsObj::Func(f)) => self
.funcs
.get(f.def_id)
.map(|d| d.name.clone())
.unwrap_or_default(),
Some(JsObj::Class(c)) => c.name.clone(),
Some(JsObj::Builtin(n)) => n.rsplit('.').next().unwrap_or(n).to_string(),
Some(JsObj::BoundFunc { target, .. }) => {
format!("bound {}", self.callable_name(target))
}
_ => String::new(),
}
}
pub fn strict_eq(&self, a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Undef, Value::Undef) => true,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Str(x), Value::Str(y)) => x == y,
_ => {
let an = matches!(a, Value::Int(_) | Value::Float(_));
let bn = matches!(b, Value::Int(_) | Value::Float(_));
if an && bn {
let x = self.to_number(a);
let y = self.to_number(b);
return x == y;
}
if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
return x == y;
}
if let (Some(sa), Some(sb)) = (self.as_str(a), self.as_str(b)) {
return sa == sb;
}
let na = self.is_null(a);
let nb = self.is_null(b);
if na || nb {
return na && nb;
}
matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
}
}
}
pub fn is_nullish(&self, v: &Value) -> bool {
matches!(v, Value::Undef) || self.is_null(v)
}
fn js_type(&self, v: &Value) -> &'static str {
match v {
Value::Undef => "undefined",
Value::Bool(_) => "boolean",
Value::Int(_) | Value::Float(_) => "number",
Value::Str(_) => "string",
Value::Obj(_) => match self.get(v) {
Some(JsObj::Str(_)) => "string",
Some(JsObj::Null) => "null",
Some(JsObj::BigInt(_)) => "bigint",
_ => "object",
},
_ => "object",
}
}
pub fn loose_eq(&self, a: &Value, b: &Value) -> bool {
if self.strict_eq(a, b) {
return true;
}
let ta = self.js_type(a);
let tb = self.js_type(b);
if self.is_nullish(a) || self.is_nullish(b) {
return self.is_nullish(a) && self.is_nullish(b);
}
if ta == "bigint" || tb == "bigint" {
return self.bigint_loose_eq(a, b);
}
if ta == tb {
return false;
}
if (ta == "number" && tb == "string") || (ta == "string" && tb == "number") {
return self.to_number(a) == self.to_number(b);
}
if ta == "boolean" {
return self.loose_eq(&Value::Float(self.to_number(a)), b);
}
if tb == "boolean" {
return self.loose_eq(a, &Value::Float(self.to_number(b)));
}
if ta == "object" && (tb == "number" || tb == "string") {
let pa = self.str_of(a);
return if tb == "string" {
pa == self.str_of(b)
} else {
str_to_number(&pa) == self.to_number(b)
};
}
if tb == "object" && (ta == "number" || ta == "string") {
let pb = self.str_of(b);
return if ta == "string" {
self.str_of(a) == pb
} else {
self.to_number(a) == str_to_number(&pb)
};
}
false
}
pub fn arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
use NumOp::*;
match op {
Add => {
let a_str = self.prefers_string(a);
let b_str = self.prefers_string(b);
if a_str || b_str {
let s = format!("{}{}", self.str_of(a), self.str_of(b));
Ok(self.new_str(s))
} else if self.is_bigint_val(a) || self.is_bigint_val(b) {
self.bigint_arith(op, a, b)
} else {
Ok(Value::Float(self.to_number(a) + self.to_number(b)))
}
}
Sub | Mul | Div | Mod | Pow if self.is_bigint_val(a) || self.is_bigint_val(b) => {
self.bigint_arith(op, a, b)
}
Sub => Ok(Value::Float(self.to_number(a) - self.to_number(b))),
Mul => Ok(Value::Float(self.to_number(a) * self.to_number(b))),
Div => Ok(Value::Float(self.to_number(a) / self.to_number(b))),
Mod => Ok(Value::Float(js_mod(self.to_number(a), self.to_number(b)))),
Pow => Ok(Value::Float(self.to_number(a).powf(self.to_number(b)))),
Neg if self.is_bigint_val(a) => self.bigint_arith(op, a, b),
Neg => Ok(Value::Float(-self.to_number(a))),
Lt | Le | Gt | Ge => Ok(Value::Bool(self.relational(op, a, b))),
Eq => Ok(Value::Bool(self.loose_eq(a, b))),
Ne => Ok(Value::Bool(!self.loose_eq(a, b))),
}
}
fn prefers_string(&self, v: &Value) -> bool {
match v {
Value::Str(_) => true,
Value::Obj(_) => !matches!(
self.get(v),
Some(JsObj::Null) | Some(JsObj::BigInt(_)) | None
),
_ => false,
}
}
fn relational(&self, op: NumOp, a: &Value, b: &Value) -> bool {
use std::cmp::Ordering;
let ord = if let (Some(x), Some(y)) = (self.as_bigint(a), self.as_bigint(b)) {
x.cmp(&y)
} else if let (Some(x), Some(y)) = (self.as_str(a), self.as_str(b)) {
x.cmp(&y)
} else {
let x = self.to_number(a);
let y = self.to_number(b);
match x.partial_cmp(&y) {
Some(o) => o,
None => return false, }
};
match op {
NumOp::Lt => ord == Ordering::Less,
NumOp::Le => ord != Ordering::Greater,
NumOp::Gt => ord == Ordering::Greater,
NumOp::Ge => ord != Ordering::Less,
_ => false,
}
}
pub fn bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
if self.is_bigint_val(a) || self.is_bigint_val(b) {
return self.bigint_bitwise(tag, a, b);
}
let x = to_int32(self.to_number(a));
let y = to_int32(self.to_number(b));
let r: i64 = match tag {
binop::BITAND => (x & y) as i64,
binop::BITOR => (x | y) as i64,
binop::BITXOR => (x ^ y) as i64,
binop::SHL => (x.wrapping_shl((y as u32) & 31)) as i64,
binop::SHR => (x >> ((y as u32) & 31)) as i64,
binop::USHR => (to_uint32(self.to_number(a)) >> ((y as u32) & 31)) as i64,
_ => 0,
};
Ok(Value::Float(r as f64))
}
pub fn is_bigint_val(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::BigInt(_)))
}
pub fn as_bigint(&self, v: &Value) -> Option<num_bigint::BigInt> {
match self.get(v) {
Some(JsObj::BigInt(b)) => Some(b.clone()),
_ => None,
}
}
pub fn new_bigint(&mut self, b: num_bigint::BigInt) -> Value {
self.alloc(JsObj::BigInt(b))
}
fn bigint_arith(&mut self, op: NumOp, a: &Value, b: &Value) -> Result<Value, String> {
use num_traits::{Signed, Zero};
use NumOp::*;
if op == Neg {
let x = self.as_bigint(a).expect("bigint_arith Neg on non-bigint");
return Ok(self.new_bigint(-x));
}
let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
(Some(x), Some(y)) => (x, y),
_ => {
return Err(type_error(
"Cannot mix BigInt and other types, use explicit conversions",
))
}
};
let r = match op {
Add => x + y,
Sub => x - y,
Mul => x * y,
Div => {
if y.is_zero() {
return Err("RangeError: Division by zero".into());
}
x / y }
Mod => {
if y.is_zero() {
return Err("RangeError: Division by zero".into());
}
x % y }
Pow => {
if y.is_negative() {
return Err("RangeError: Exponent must be positive".into());
}
let exp = num_traits::ToPrimitive::to_u32(&y)
.ok_or_else(|| "RangeError: Maximum BigInt size exceeded".to_string())?;
num_traits::Pow::pow(x, exp)
}
_ => return Err(type_error("unsupported BigInt operation")),
};
Ok(self.new_bigint(r))
}
fn bigint_bitwise(&mut self, tag: i64, a: &Value, b: &Value) -> Result<Value, String> {
let (x, y) = match (self.as_bigint(a), self.as_bigint(b)) {
(Some(x), Some(y)) => (x, y),
_ => {
return Err(type_error(
"Cannot mix BigInt and other types, use explicit conversions",
))
}
};
let r = match tag {
binop::BITAND => x & y,
binop::BITOR => x | y,
binop::BITXOR => x ^ y,
binop::SHL => {
let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
if n >= 0 {
x << (n as usize)
} else {
x >> ((-n) as usize)
}
}
binop::SHR => {
let n = num_traits::ToPrimitive::to_i64(&y).unwrap_or(0);
if n >= 0 {
x >> (n as usize)
} else {
x << ((-n) as usize)
}
}
binop::USHR => {
return Err(type_error(
"BigInts have no unsigned right shift, use >> instead",
))
}
_ => return Err(type_error("unsupported BigInt operation")),
};
Ok(self.new_bigint(r))
}
fn bigint_loose_eq(&self, a: &Value, b: &Value) -> bool {
let (big, other) = match (self.as_bigint(a), self.as_bigint(b)) {
(Some(x), _) => (x, b),
(_, Some(y)) => (y, a),
_ => return false,
};
match other {
Value::Bool(bo) => big == num_bigint::BigInt::from(*bo as i64),
Value::Int(n) => big == num_bigint::BigInt::from(*n),
Value::Float(f) => {
if !f.is_finite() || f.fract() != 0.0 {
return false;
}
bigint_to_f64(&big) == *f
}
Value::Str(s) => match parse_bigint_str(s) {
Some(bs) => big == bs,
None => false,
},
Value::Obj(_) => match self.get(other) {
Some(JsObj::Str(s)) => parse_bigint_str(s).map(|bs| big == bs).unwrap_or(false),
_ => {
let s = self.str_of(other);
parse_bigint_str(&s).map(|bs| big == bs).unwrap_or(false)
}
},
_ => false,
}
}
}
pub fn parse_bigint_str(s: &str) -> Option<num_bigint::BigInt> {
let t = s.trim();
if t.is_empty() {
return Some(num_bigint::BigInt::from(0));
}
let (radix, digits) = if let Some(h) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
(16, h)
} else if let Some(o) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
(8, o)
} else if let Some(bb) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
(2, bb)
} else {
(10, t)
};
num_bigint::BigInt::parse_bytes(digits.as_bytes(), radix)
}
fn bigint_to_f64(b: &num_bigint::BigInt) -> f64 {
num_traits::ToPrimitive::to_f64(b).unwrap_or_else(|| {
if num_traits::Signed::is_negative(b) {
f64::NEG_INFINITY
} else {
f64::INFINITY
}
})
}
fn js_mod(a: f64, b: f64) -> f64 {
a % b
}
thread_local! {
static INSPECT_MAX_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(2) };
}
pub fn set_inspect_max_depth(d: usize) {
INSPECT_MAX_DEPTH.with(|c| c.set(d));
}
fn inspect_max_depth() -> usize {
INSPECT_MAX_DEPTH.with(|c| c.get())
}
fn to_int32(f: f64) -> i32 {
if !f.is_finite() {
return 0;
}
let n = f.trunc();
(n as i64 as u32) as i32
}
fn to_uint32(f: f64) -> u32 {
if !f.is_finite() {
return 0;
}
f.trunc() as i64 as u32
}
fn str_to_number(s: &str) -> f64 {
let t = s.trim();
if t.is_empty() {
return 0.0;
}
if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
return i64::from_str_radix(hex, 16)
.map(|n| n as f64)
.unwrap_or(f64::NAN);
}
if let Some(oct) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
return i64::from_str_radix(oct, 8)
.map(|n| n as f64)
.unwrap_or(f64::NAN);
}
if let Some(bin) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
return i64::from_str_radix(bin, 2)
.map(|n| n as f64)
.unwrap_or(f64::NAN);
}
match t {
"Infinity" | "+Infinity" => f64::INFINITY,
"-Infinity" => f64::NEG_INFINITY,
_ => t.parse::<f64>().unwrap_or(f64::NAN),
}
}
const BREAK_LENGTH: usize = 80;
const COMPACT: usize = 3;
fn is_below_break_length(output: &[String], start: usize) -> bool {
let mut total = output.len() + start;
if total + output.len() > BREAK_LENGTH {
return false;
}
for o in output {
if o.contains('\n') {
return false;
}
total += o.chars().count();
if total > BREAK_LENGTH {
return false;
}
}
true
}
fn group_array_elements(
host: &JsHost,
output: &[String],
values: &[Value],
indentation_lvl: usize,
) -> (Vec<String>, bool) {
let separator_space = 2usize; let output_length = output.len();
let data_len: Vec<usize> = output.iter().map(|o| o.chars().count()).collect();
let mut total_length = 0usize;
let mut max_length = 0usize;
for &len in &data_len {
total_length += len + separator_space;
if len > max_length {
max_length = len;
}
}
let actual_max = max_length + separator_space;
if !(actual_max * 3 + indentation_lvl < BREAK_LENGTH
&& (total_length as f64 / actual_max as f64 > 5.0 || max_length <= 6))
{
return (output.to_vec(), false);
}
let approx_char_heights = 2.5f64;
let average_bias = (actual_max as f64 - total_length as f64 / output_length as f64).sqrt();
let biased_max = (actual_max as f64 - 3.0 - average_bias).max(1.0);
let columns = [
((approx_char_heights * biased_max * output_length as f64).sqrt() / biased_max).round()
as i64,
((BREAK_LENGTH - indentation_lvl) as f64 / actual_max as f64).floor() as i64,
(COMPACT * 4) as i64,
15,
]
.into_iter()
.min()
.unwrap();
if columns <= 1 {
return (output.to_vec(), false);
}
let columns = columns as usize;
let mut max_line_length = vec![0usize; columns];
for (i, slot) in max_line_length.iter_mut().enumerate() {
let mut line_length = 0;
let mut j = i;
while j < output_length {
if data_len[j] > line_length {
line_length = data_len[j];
}
j += columns;
}
*slot = line_length + separator_space;
}
let pad_start = values.iter().all(|v| {
matches!(v, Value::Int(_) | Value::Float(_))
|| matches!(host.get(v), Some(JsObj::BigInt(_)))
});
let mut tmp = Vec::new();
let mut i = 0;
while i < output_length {
let max = (i + columns).min(output_length);
let mut str_line = String::new();
let mut j = i;
while j < max.saturating_sub(1) {
let col = j - i;
let cell = format!("{}, ", output[j]);
let target = max_line_length[col];
str_line.push_str(&pad_to(&cell, target, pad_start));
j += 1;
}
if pad_start {
let col = j - i;
let target = max_line_length[col] - separator_space;
str_line.push_str(&pad_to(&output[j], target, true));
} else {
str_line.push_str(&output[j]);
}
tmp.push(str_line);
i += columns;
}
(tmp, true)
}
fn pad_to(s: &str, width: usize, pad_start: bool) -> String {
let len = s.chars().count();
if len >= width {
return s.to_string();
}
let fill = " ".repeat(width - len);
if pad_start {
format!("{fill}{s}")
} else {
format!("{s}{fill}")
}
}
fn quote_str(s: &str) -> String {
let 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"),
_ => out.push(c),
}
}
out.push('\'');
out
}
fn fmt_key(k: &str) -> String {
let ok = !k.is_empty()
&& k.chars()
.next()
.map(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
.unwrap_or(false)
&& k.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
if ok {
k.to_string()
} else {
quote_str(k)
}
}
impl JsHost {
pub fn iter_vec(&mut self, v: &Value) -> Result<Vec<Value>, String> {
match self.get(v) {
Some(JsObj::Array(items)) => Ok(items.clone()),
Some(JsObj::Str(s)) => {
let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
Ok(chars.into_iter().map(|c| self.new_str(c)).collect())
}
Some(JsObj::Iter { items, idx }) => Ok(items[*idx..].to_vec()),
Some(JsObj::Set { entries, .. }) => Ok(entries.values().cloned().collect()),
Some(JsObj::Map { entries, .. }) => {
let pairs: Vec<(Value, Value)> = entries.values().cloned().collect();
Ok(pairs
.into_iter()
.map(|(k, v)| self.new_array(vec![k, v]))
.collect())
}
_ => Err(type_error(&format!("{} is not iterable", self.type_of(v)))),
}
}
pub fn enum_keys(&mut self, v: &Value) -> Vec<Value> {
let keys: Vec<String> = match self.get(v) {
Some(JsObj::Object(props)) => props
.keys()
.filter(|k| !k.starts_with("@@") && !k.starts_with('#'))
.cloned()
.collect(),
Some(JsObj::Array(items)) => (0..items.len()).map(|i| i.to_string()).collect(),
_ => Vec::new(),
};
keys.into_iter().map(|k| self.new_str(k)).collect()
}
}
fn marshal_ffi_arg(v: &Value) -> Value {
match v {
Value::Obj(_) => match with_host(|h| h.as_str(v)) {
Some(s) => Value::str(s),
None => v.clone(),
},
_ => v.clone(),
}
}
pub fn call_named(name: &str, args: Vec<Value>) -> Result<Value, String> {
if name == "__rust_compile" {
let b64 = args
.first()
.map(|v| with_host(|h| h.str_of(v)))
.unwrap_or_default();
return fusevm::ffi::compile_and_register(&b64).map(|_| Value::Undef);
}
if let Some(v) = with_host(|h| h.read_name(name)) {
return invoke(&v, args, None);
}
if crate::builtins::is_known_builtin(name) {
return crate::builtins::call_builtin_function(name, args);
}
if fusevm::ffi::is_registered(name) {
let margs: Vec<Value> = args.iter().map(marshal_ffi_arg).collect();
if let Some(r) = fusevm::ffi::try_call(name, &margs) {
return r;
}
}
Err(ref_error(name))
}
pub fn call_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
if let Some(JsObj::Builtin(ns)) = with_host(|h| h.get(recv).cloned()) {
let qualified = format!("{ns}.{name}");
if crate::builtins::is_known_builtin(&qualified) {
return crate::builtins::call_builtin_function(&qualified, args);
}
}
if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Object(_))) {
if let Some(tag) = crate::stdlib::native_tag(recv) {
if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
return crate::stdlib::instance_call(&tag, recv, name, args);
}
if let Some((Some(getter), _)) = with_host(|h| lookup_accessor(h, recv, name)) {
let f = invoke(&getter, Vec::new(), Some(recv.clone()))?;
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
return Err(type_error(&format!("{name} is not a function")));
}
if crate::builtins::is_object_builtin_method(name) {
return crate::builtins::object_builtin_method(recv, name, args);
}
return Err(type_error(&format!("{name} is not a function")));
}
if matches!(
with_host(|h| h.get(recv).cloned()),
Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::Builtin(_))
) {
if let Some(r) = crate::builtins::function_builtin_method(recv, name, &args)? {
return Ok(r);
}
let stat = if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Class(_))) {
with_host(|h| h.class_static(recv, name))
} else {
with_host(|h| h.fn_prop(recv, name))
};
if let Some(f) = stat {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if let Some(f) = with_host(|h| lookup_chain(h, recv, name)) {
if with_host(|h| is_callable(h, &f)) {
return invoke(&f, args, Some(recv.clone()));
}
}
if matches!(with_host(|h| h.get(recv).cloned()), Some(JsObj::Builtin(_)))
&& crate::builtins::is_object_builtin_method(name)
{
return crate::builtins::object_builtin_method(recv, name, args);
}
}
crate::builtins::call_type_method(recv, name, args)
}
pub fn invoke(callable: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
let obj = with_host(|h| h.get(callable).cloned());
match obj {
Some(JsObj::Builtin(name)) if name.starts_with("@proto:") => {
let recv = this.unwrap_or(Value::Undef);
crate::builtins::proto_method(&recv, &name["@proto:".len()..], args)
}
Some(JsObj::Builtin(name)) => crate::builtins::call_builtin_function(&name, args),
Some(JsObj::Func(fv)) => run_user_func(&fv, args, this),
Some(JsObj::BoundMethod { recv, name }) => call_method(&recv, &name, args),
Some(JsObj::BoundFunc {
target,
this: bthis,
args: pre,
}) => {
let mut all = pre;
all.extend(args);
invoke(&target, all, Some(bthis))
}
Some(JsObj::Class(c)) => Err(type_error(&format!(
"Class constructor {} cannot be invoked without 'new'",
c.name
))),
_ => Err(type_error(&format!(
"{} is not a function",
with_host(|h| h.str_of(callable))
))),
}
}
pub fn run_user_func(fv: &FuncVal, args: Vec<Value>, this: Option<Value>) -> Result<Value, String> {
run_user_func_nt(fv, args, this, None)
}
pub fn run_user_func_nt(
fv: &FuncVal,
args: Vec<Value>,
this: Option<Value>,
new_target: Option<Value>,
) -> Result<Value, String> {
let def = with_host(|h| h.funcs[fv.def_id].clone());
let env = new_env(fv.env.clone());
bind_params(&env, &def, args);
let this_val = if fv.is_arrow { fv.this.clone() } else { this };
if def.is_generator {
return Ok(make_generator(
def.chunk.clone(),
env,
this_val,
fv.home_class.clone(),
));
}
if def.is_async {
let gen = make_generator(def.chunk.clone(), env, this_val, fv.home_class.clone());
return Ok(run_async(gen));
}
let home = fv
.home_class
.as_ref()
.and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
with_host(|h| {
h.frames.push(Frame {
env,
this_obj: this_val,
new_target,
home_class: home,
line: 0,
owner: Some(def.name.clone()),
})
});
let r = run_chunk_on(def.chunk.clone());
let sig = with_host(|h| {
h.frames.pop();
h.signal.take()
});
match r {
Err(e) => Err(e),
Ok(_) => Ok(match sig {
Some(Signal::Return(v)) => v,
_ => Value::Undef,
}),
}
}
fn bind_params(env: &Env, def: &FuncDef, args: Vec<Value>) {
let mut vars: IndexMap<String, Value> = IndexMap::new();
let mut i = 0;
for slot in &def.params {
if slot.rest {
let rest: Vec<Value> = args.get(i..).map(|s| s.to_vec()).unwrap_or_default();
let arr = with_host(|h| h.new_array(rest));
vars.insert(slot.name.clone(), arr);
} else {
let v = args.get(i).cloned().unwrap_or(Value::Undef);
vars.insert(slot.name.clone(), v);
i += 1;
}
}
let args_arr = with_host(|h| h.new_array(args));
vars.entry("arguments".to_string()).or_insert(args_arr);
env.borrow_mut().vars = vars;
}
pub fn construct(ctor: &Value, args: Vec<Value>) -> Result<Value, String> {
construct_nt(ctor, args, ctor.clone())
}
pub fn construct_nt(ctor: &Value, args: Vec<Value>, new_target: Value) -> Result<Value, String> {
let obj = with_host(|h| h.get(ctor).cloned());
match obj {
Some(JsObj::Class(_)) => construct_class(ctor, args, new_target),
Some(JsObj::Func(fv)) => {
let inst = with_host(|h| {
let o = h.new_object(IndexMap::new());
let proto = h.fn_prop(ctor, "prototype").unwrap_or_else(|| {
let p = h.new_object(IndexMap::new());
if let Some(JsObj::Object(pp)) = h.get_mut(&p) {
pp.insert("constructor".to_string(), ctor.clone());
}
h.set_fn_prop(ctor, "prototype", p.clone());
p
});
h.set_proto(&o, proto);
o
});
let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target))?;
if returns_object(&r) {
Ok(r)
} else {
Ok(inst)
}
}
Some(JsObj::Builtin(name)) => crate::builtins::construct_builtin(&name, args),
Some(JsObj::BoundFunc {
target, args: pre, ..
}) => {
let mut all = pre;
all.extend(args);
construct_nt(&target, all, new_target)
}
_ => Err(type_error(&format!(
"{} is not a constructor",
with_host(|h| h.str_of(ctor))
))),
}
}
fn returns_object(r: &Value) -> bool {
matches!(
with_host(|h| h.get(r).cloned()),
Some(JsObj::Object(_))
| Some(JsObj::Array(_))
| Some(JsObj::Map { .. })
| Some(JsObj::Set { .. })
| Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::RegExp(_))
)
}
fn construct_class(
class_val: &Value,
args: Vec<Value>,
new_target: Value,
) -> Result<Value, String> {
let cv = match with_host(|h| h.get(class_val).cloned()) {
Some(JsObj::Class(c)) => c,
_ => return Err(type_error("not a class")),
};
let leaf_proto = match with_host(|h| h.get(&new_target).cloned()) {
Some(JsObj::Class(c)) => c.proto.clone(),
_ => cv.proto.clone(),
};
let inst = with_host(|h| {
let o = h.new_object(IndexMap::new());
h.set_proto(&o, leaf_proto.clone());
o
});
match run_class_ctor(&cv, &inst, args, &new_target)? {
Some(obj) if returns_object(&obj) => Ok(obj),
_ => Ok(inst),
}
}
fn run_class_ctor(
cv: &ClassVal,
inst: &Value,
args: Vec<Value>,
new_target: &Value,
) -> Result<Option<Value>, String> {
if cv.parent.is_none() {
init_fields(cv, inst)?;
}
match &cv.ctor {
Some(ctor_fn) => {
let fv = match with_host(|h| h.get(ctor_fn).cloned()) {
Some(JsObj::Func(f)) => f,
_ => return Err(type_error("class constructor is not a function")),
};
let r = run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
return Ok(Some(r));
}
None => {
if let Some(parent) = &cv.parent {
super_construct(parent, args, inst, new_target)?;
init_fields(cv, inst)?;
}
}
}
Ok(None)
}
fn init_fields(cv: &ClassVal, inst: &Value) -> Result<(), String> {
for (name, thunk) in &cv.fields {
let val = invoke(thunk, Vec::new(), Some(inst.clone()))?;
with_host(|h| {
if let Some(JsObj::Object(props)) = h.get_mut(inst) {
let is_new = !props.contains_key(name);
props.insert(name.clone(), val);
if is_new && array_index(name).is_some() {
canonicalize_own_keys(props);
}
}
});
}
Ok(())
}
pub fn super_construct(
parent: &Value,
args: Vec<Value>,
inst: &Value,
new_target: &Value,
) -> Result<(), String> {
match with_host(|h| h.get(parent).cloned()) {
Some(JsObj::Class(pcv)) => run_class_ctor(&pcv, inst, args, new_target).map(|_| ()),
Some(JsObj::Func(fv)) => {
run_user_func_nt(&fv, args, Some(inst.clone()), Some(new_target.clone()))?;
Ok(())
}
Some(JsObj::Builtin(name)) => {
let built = crate::builtins::construct_builtin(&name, args)?;
let entries: Vec<(String, Value)> = with_host(|h| match h.get(&built) {
Some(JsObj::Object(p)) => p.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => Vec::new(),
});
with_host(|h| {
if let Some(JsObj::Object(props)) = h.get_mut(inst) {
for (k, v) in entries {
props.insert(k, v);
}
canonicalize_own_keys(props);
}
});
Ok(())
}
_ => Err(type_error("super is not a constructor")),
}
}
pub fn build_class(name: &str, parent: Value, ctor: Value) -> Value {
with_host(|h| {
let parent_opt = if matches!(parent, Value::Undef) {
None
} else {
Some(parent.clone())
};
let parent_proto = match &parent_opt {
Some(p) => match h.get(p).cloned() {
Some(JsObj::Class(pc)) => pc.proto.clone(),
Some(JsObj::Builtin(bn)) => {
h.ensure_error_protos();
error_proto_of(h, &bn)
.or_else(|| h.fn_prop(p, "prototype"))
.unwrap_or_else(|| h.object_proto())
}
_ => h
.fn_prop(p, "prototype")
.unwrap_or_else(|| h.object_proto()),
},
None => h.object_proto(),
};
let proto = h.new_object(IndexMap::new());
h.set_proto(&proto, parent_proto);
let ctor_opt = if matches!(ctor, Value::Undef) {
None
} else {
Some(ctor.clone())
};
if let Some(cf) = &ctor_opt {
if let Some(JsObj::Func(f)) = h.get_mut(cf) {
f.home_class = Some(name.to_string());
}
}
let cval = ClassVal {
name: name.to_string(),
ctor: ctor_opt,
parent: parent_opt,
proto: proto.clone(),
statics: IndexMap::new(),
fields: Vec::new(),
};
let class_val = h.alloc(JsObj::Class(cval));
h.class_registry.insert(name.to_string(), class_val.clone());
h.tag_proto_class(&proto, class_val.clone());
h.set_fn_prop(&class_val, "prototype", proto.clone());
if let Some(JsObj::Object(p)) = h.get_mut(&proto) {
p.insert("constructor".to_string(), class_val.clone());
}
class_val
})
}
pub fn define_member(class_val: &Value, name: &str, kind: i64, is_static: bool, func: Value) {
with_host(|h| {
let cname = match h.get(class_val) {
Some(JsObj::Class(c)) => c.name.clone(),
_ => String::new(),
};
if let Some(JsObj::Func(f)) = h.get_mut(&func) {
f.home_class = Some(cname);
}
let target = if is_static {
class_val.clone()
} else {
match h.get(class_val) {
Some(JsObj::Class(c)) => c.proto.clone(),
_ => return,
}
};
match kind {
member::GET => h.set_accessor(&target, name, Some(func), None),
member::SET => h.set_accessor(&target, name, None, Some(func)),
_ => {
if is_static {
if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
c.statics.insert(name.to_string(), func.clone());
}
h.set_fn_prop(class_val, name, func);
} else if let Some(JsObj::Object(p)) = h.get_mut(&target) {
p.insert(name.to_string(), func);
}
}
}
});
}
pub fn define_field(class_val: &Value, name: &str, thunk: Value) {
with_host(|h| {
if let Some(JsObj::Class(c)) = h.get_mut(class_val) {
c.fields.push((name.to_string(), thunk));
}
});
}
fn ctor_prototype(h: &JsHost, ctor: &Value) -> Option<Value> {
match h.get(ctor) {
Some(JsObj::Class(c)) => Some(c.proto.clone()),
Some(JsObj::Func(_)) => h.fn_prop(ctor, "prototype"),
Some(JsObj::Builtin(name)) => h.error_protos.get(name).cloned(),
Some(JsObj::BoundFunc { target, .. }) => ctor_prototype(h, &target.clone()),
_ => None,
}
}
pub fn instance_of(obj: &Value, ctor: &Value) -> Result<bool, String> {
if !matches!(obj, Value::Obj(_)) {
return Ok(false);
}
let ctor_callable = with_host(|h| {
matches!(
h.get(ctor),
Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::Builtin(_))
| Some(JsObj::BoundFunc { .. })
)
});
if !ctor_callable {
return Err(type_error(
"Right-hand side of 'instanceof' is not callable",
));
}
if let Some(JsObj::Builtin(name)) = with_host(|h| h.get(ctor).cloned()) {
let kind = with_host(|h| h.get(obj).cloned());
match name.as_str() {
"Array" => return Ok(matches!(kind, Some(JsObj::Array(_)))),
"Function" => return Ok(with_host(|h| is_callable(h, obj))),
"Map" => return Ok(matches!(kind, Some(JsObj::Map { weak: false, .. }))),
"WeakMap" => return Ok(matches!(kind, Some(JsObj::Map { weak: true, .. }))),
"Set" => return Ok(matches!(kind, Some(JsObj::Set { weak: false, .. }))),
"WeakSet" => return Ok(matches!(kind, Some(JsObj::Set { weak: true, .. }))),
"Promise" => return Ok(matches!(kind, Some(JsObj::Promise { .. }))),
"Object" => {
let is_obj = matches!(
kind,
Some(JsObj::Object(_))
| Some(JsObj::Array(_))
| Some(JsObj::Func(_))
| Some(JsObj::Class(_))
| Some(JsObj::Map { .. })
| Some(JsObj::Set { .. })
| Some(JsObj::Promise { .. })
| Some(JsObj::Generator { .. })
);
if is_obj {
if with_host(|h| h.has_null_proto(obj)) {
return Ok(false);
}
return Ok(true);
}
return Ok(false);
}
other => {
if crate::stdlib::native_tag(obj).as_deref() == Some(other) {
return Ok(true);
}
}
}
}
with_host(|h| h.ensure_error_protos());
let target = match with_host(|h| ctor_prototype(h, ctor)) {
Some(p) => p,
None => return Ok(false),
};
let mut cur = with_host(|h| h.proto_of(obj));
while let Some(p) = cur {
if with_host(|h| h.strict_eq(&p, &target)) {
return Ok(true);
}
cur = with_host(|h| h.proto_of(&p));
}
Ok(false)
}
impl JsHost {
fn install_gen_ctx(&mut self, mut c: GenContext) -> GenContext {
std::mem::swap(&mut self.frames, &mut c.frames);
std::mem::swap(&mut self.error, &mut c.error);
std::mem::swap(&mut self.exc, &mut c.exc);
std::mem::swap(&mut self.signal, &mut c.signal);
c
}
pub fn is_generator_val(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Generator { .. }))
}
pub fn gen_done(&self, id: u32) -> bool {
self.generators
.get(id as usize)
.map(|g| g.done)
.unwrap_or(true)
}
fn gen_started(&self, id: u32) -> bool {
self.generators
.get(id as usize)
.map(|g| g.started)
.unwrap_or(false)
}
}
fn make_generator(
chunk: Chunk,
env: Env,
this_val: Option<Value>,
home_class: Option<String>,
) -> Value {
let home = home_class
.as_ref()
.and_then(|n| with_host(|h| h.class_registry.get(n).cloned()));
let frame = Frame {
env,
this_obj: this_val,
new_target: None,
home_class: home,
line: 0,
owner: None,
};
let id = with_host(|h| {
let id = h.generators.len() as u32;
h.generators.push(GenCell {
coro: None,
yielder: std::ptr::null(),
ctx: GenContext {
frames: vec![frame],
..GenContext::default()
},
done: false,
started: false,
inject: None,
});
id
});
let coro = corosensei::Coroutine::new(
move |yielder: &corosensei::Yielder<Value, Value>, _first: Value| {
with_host(|h| h.generators[id as usize].yielder = yielder as *const _ as *const ());
let r = run_chunk_on(chunk);
let ret = with_host(|h| match h.signal.take() {
Some(Signal::Return(v)) => v,
_ => Value::Undef,
});
r.map(|_| ret)
},
);
with_host(|h| h.generators[id as usize].coro = Some(coro));
with_host(|h| h.alloc(JsObj::Generator { id }))
}
pub fn gen_yield(v: Value) -> Result<Value, String> {
let id = match CUR_GEN.with(|c| c.get()) {
Some(id) => id,
None => return Err(type_error("yield outside a generator")),
};
let yp = with_host(|h| h.generators[id as usize].yielder);
let yielder = unsafe { &*(yp as *const corosensei::Yielder<Value, Value>) };
let sent = yielder.suspend(v);
if let Some(inj) = with_host(|h| h.generators[id as usize].inject.take()) {
match inj {
GenInject::Return(rv) => {
with_host(|h| h.signal = Some(Signal::Return(rv)));
return Ok(Value::Undef);
}
GenInject::Throw(ev) => {
let msg = with_host(|h| crate::builtins::error_string(h, &ev));
with_host(|h| h.exc = Some(ev));
return Err(msg);
}
}
}
Ok(sent)
}
pub fn gen_return(gen: &Value, v: Value) -> Result<GenStep, String> {
let id = match with_host(|h| h.get(gen).cloned()) {
Some(JsObj::Generator { id }) => id,
_ => return Err(type_error("not a generator")),
};
let started = with_host(|h| h.gen_started(id));
if with_host(|h| h.generators[id as usize].done) || !started {
with_host(|h| h.generators[id as usize].done = true);
return Ok(GenStep::Done(v));
}
with_host(|h| h.generators[id as usize].inject = Some(GenInject::Return(v)));
gen_resume(gen, Value::Undef)
}
pub fn gen_throw(gen: &Value, e: Value) -> Result<GenStep, String> {
let id = match with_host(|h| h.get(gen).cloned()) {
Some(JsObj::Generator { id }) => id,
_ => return Err(type_error("not a generator")),
};
let started = with_host(|h| h.gen_started(id));
if with_host(|h| h.generators[id as usize].done) || !started {
with_host(|h| h.generators[id as usize].done = true);
let msg = with_host(|h| crate::builtins::error_string(h, &e));
with_host(|h| h.exc = Some(e));
return Err(msg);
}
with_host(|h| h.generators[id as usize].inject = Some(GenInject::Throw(e)));
gen_resume(gen, Value::Undef)
}
pub enum GenStep {
Yield(Value),
Done(Value),
}
pub fn gen_resume(gen: &Value, send: Value) -> Result<GenStep, String> {
let id = match with_host(|h| h.get(gen).cloned()) {
Some(JsObj::Generator { id }) => id,
_ => return Err(type_error("not a generator")),
};
if with_host(|h| h.generators[id as usize].done) {
return Ok(GenStep::Done(Value::Undef));
}
let mut coro = match with_host(|h| h.generators[id as usize].coro.take()) {
Some(c) => c,
None => return Err("TypeError: generator already executing".into()),
};
with_host(|h| h.generators[id as usize].started = true);
let gen_ctx = with_host(|h| std::mem::take(&mut h.generators[id as usize].ctx));
let caller_ctx = with_host(|h| h.install_gen_ctx(gen_ctx));
let prev = CUR_GEN.with(|c| c.replace(Some(id)));
let out = coro.resume(send);
CUR_GEN.with(|c| c.set(prev));
let gen_ctx = with_host(|h| h.install_gen_ctx(caller_ctx));
with_host(|h| {
h.generators[id as usize].ctx = gen_ctx;
h.generators[id as usize].coro = Some(coro);
});
match out {
corosensei::CoroutineResult::Yield(y) => Ok(GenStep::Yield(y)),
corosensei::CoroutineResult::Return(r) => {
with_host(|h| h.generators[id as usize].done = true);
match r {
Ok(v) => Ok(GenStep::Done(v)),
Err(e) => Err(e),
}
}
}
}
pub fn gen_close(gen: &Value) {
if let Some(JsObj::Generator { id }) = with_host(|h| h.get(gen).cloned()) {
with_host(|h| h.generators[id as usize].done = true);
}
}
pub fn map_key(h: &JsHost, v: &Value) -> MapKey {
match v {
Value::Undef => MapKey::Undef,
Value::Bool(b) => MapKey::Bool(*b),
Value::Int(n) => MapKey::Num(norm_num_bits(*n as f64)),
Value::Float(f) => MapKey::Num(norm_num_bits(*f)),
Value::Str(s) => MapKey::Str((**s).clone()),
Value::Obj(i) => match h.get(v) {
Some(JsObj::Str(s)) => MapKey::Str(s.clone()),
Some(JsObj::Null) => MapKey::Null,
Some(JsObj::BigInt(b)) => MapKey::Big(b.to_string()),
_ => MapKey::Ref(*i),
},
_ => MapKey::Undef,
}
}
fn norm_num_bits(f: f64) -> u64 {
if f.is_nan() {
return f64::NAN.to_bits();
}
if f == 0.0 {
return 0.0f64.to_bits(); }
f.to_bits()
}
pub fn iter_all(v: &Value) -> Result<Vec<Value>, String> {
if with_host(|h| h.is_generator_val(v)) {
let mut out = Vec::new();
while let GenStep::Yield(x) = gen_resume(v, Value::Undef)? {
out.push(x);
}
return Ok(out);
}
if let Some(iter_fn) = user_iterator_fn(v) {
let iterator = invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
return drain_iterator(&iterator);
}
with_host(|h| h.iter_vec(v))
}
pub fn get_async_iterator(src: &Value) -> Result<Value, String> {
if let Some(f) = user_async_iterator_fn(src) {
return invoke(&f, Vec::new(), Some(src.clone()));
}
let items = iter_all(src)?;
Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
}
fn user_async_iterator_fn(v: &Value) -> Option<Value> {
let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
if !is_plain {
return None;
}
let f = with_host(|h| lookup_chain(h, v, "@@asyncIterator"));
match f {
Some(f) if with_host(|h| is_callable(h, &f)) => Some(f),
_ => None,
}
}
pub fn async_step(iterator: &Value) -> Result<Value, String> {
if let Some(JsObj::Iter { items, idx }) = with_host(|h| h.get(iterator).cloned()) {
if idx >= items.len() {
let rec = with_host(|h| {
let mut m = IndexMap::new();
m.insert("value".to_string(), Value::Undef);
m.insert("done".to_string(), Value::Bool(true));
h.new_object(m)
});
return Ok(promise_of(&rec));
}
let raw = items[idx].clone();
with_host(|h| {
if let Some(JsObj::Iter { idx, .. }) = h.get_mut(iterator) {
*idx += 1;
}
});
let step = with_host(|h| h.new_promise());
let sid = with_host(|h| h.promise_id(&step).unwrap());
let raw_p = promise_of(&raw);
let raw_id = with_host(|h| h.promise_id(&raw_p).unwrap());
subscribe_native(
raw_id,
Box::new(move |state, val| {
if state == PromiseState::Rejected {
reject_promise_val(sid, val);
} else {
let rec = with_host(|h| {
let mut m = IndexMap::new();
m.insert("value".to_string(), val.clone());
m.insert("done".to_string(), Value::Bool(false));
h.new_object(m)
});
resolve_promise_val(sid, rec);
}
Ok(())
}),
);
return Ok(step);
}
let r = call_method(iterator, "next", Vec::new())?;
Ok(promise_of(&r))
}
fn user_iterator_fn(v: &Value) -> Option<Value> {
let is_plain = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
if !is_plain {
return None;
}
let f = with_host(|h| lookup_chain(h, v, "@@iterator"));
match f {
Some(f) if with_host(|h| is_callable(h, &f)) => Some(f),
_ => None,
}
}
fn drain_iterator(iterator: &Value) -> Result<Vec<Value>, String> {
let mut out = Vec::new();
loop {
let step = call_method(iterator, "next", Vec::new())?;
let done = get_prop_chain(&step, "done")?;
if with_host(|h| h.truthy(&done)) {
break;
}
out.push(get_prop_chain(&step, "value")?);
}
Ok(out)
}
pub fn get_prop_chain(recv: &Value, name: &str) -> Result<Value, String> {
crate::builtins::get_property(recv, name)
}
pub fn to_string_value(v: &Value) -> Result<Value, String> {
if with_host(|h| matches!(h.get(v), Some(JsObj::Object(_)))) {
for m in ["toString", "valueOf"] {
if let Some(f) = with_host(|h| lookup_chain(h, v, m)) {
if with_host(|h| is_callable(h, &f)) {
let r = invoke(&f, Vec::new(), Some(v.clone()))?;
if !matches!(with_host(|h| h.get(&r).cloned()), Some(JsObj::Object(_))) {
return Ok(with_host(|h| {
let s = h.str_of(&r);
h.new_str(s)
}));
}
}
}
}
}
Ok(with_host(|h| {
let s = h.str_of(v);
h.new_str(s)
}))
}
pub fn is_callable(h: &JsHost, v: &Value) -> bool {
matches!(
h.get(v),
Some(JsObj::Func(_))
| Some(JsObj::Builtin(_))
| Some(JsObj::BoundMethod { .. })
| Some(JsObj::BoundFunc { .. })
| Some(JsObj::Class(_))
)
}
pub fn lookup_chain(h: &JsHost, recv: &Value, key: &str) -> Option<Value> {
if let Some(JsObj::Object(p)) = h.get(recv) {
if let Some(v) = p.get(key) {
return Some(v.clone());
}
}
let mut cur = h.proto_of(recv);
while let Some(p) = cur {
match h.get(&p) {
Some(JsObj::Object(props)) => {
if let Some(v) = props.get(key) {
return Some(v.clone());
}
}
Some(JsObj::Func(_)) | Some(JsObj::Class(_)) => {
if let Some(v) = h.fn_prop(&p, key) {
return Some(v);
}
}
_ => {}
}
cur = h.proto_of(&p);
}
None
}
pub fn lookup_accessor(
h: &JsHost,
recv: &Value,
key: &str,
) -> Option<(Option<Value>, Option<Value>)> {
if let Some(a) = h.own_accessor(recv, key) {
return Some(a);
}
let mut cur = h.proto_of(recv);
while let Some(p) = cur {
if let Some(a) = h.own_accessor(&p, key) {
return Some(a);
}
cur = h.proto_of(&p);
}
None
}
pub fn set_error_proto(name: &str, proto: Value) {
with_host(|h| {
h.error_protos.insert(name.to_string(), proto);
});
}
pub fn error_proto(name: &str) -> Option<Value> {
with_host(|h| h.error_protos.get(name).cloned())
}
pub fn error_proto_of(h: &JsHost, name: &str) -> Option<Value> {
h.error_protos.get(name).cloned()
}
pub const ERROR_NAMES: &[&str] = &[
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
];
impl JsHost {
pub fn ensure_error_protos(&mut self) {
if !self.error_protos.is_empty() {
return;
}
let obj_proto = self.object_proto();
let err_proto = self.new_object(IndexMap::new());
self.set_proto(&err_proto, obj_proto);
let nm = self.new_str("Error");
let empty = self.new_str("");
let ctor = self.alloc(JsObj::Builtin("Error".into()));
if let Some(JsObj::Object(p)) = self.get_mut(&err_proto) {
p.insert("name".into(), nm);
p.insert("message".into(), empty);
p.insert("constructor".into(), ctor);
}
self.error_protos.insert("Error".into(), err_proto.clone());
for name in &ERROR_NAMES[1..] {
let p = self.new_object(IndexMap::new());
self.set_proto(&p, err_proto.clone());
let nm = self.new_str(*name);
let ctor = self.alloc(JsObj::Builtin((*name).to_string()));
if let Some(JsObj::Object(o)) = self.get_mut(&p) {
o.insert("name".into(), nm);
o.insert("constructor".into(), ctor);
}
self.error_protos.insert((*name).to_string(), p);
}
}
}
impl JsHost {
pub fn func_arity(&self, v: &Value) -> usize {
let def_id = match self.get(v) {
Some(JsObj::Func(f)) => Some(f.def_id),
Some(JsObj::Class(c)) => match c.ctor.as_ref().and_then(|cf| self.get(cf)) {
Some(JsObj::Func(f)) => Some(f.def_id),
_ => None,
},
_ => None,
};
match def_id.and_then(|id| self.funcs.get(id)) {
Some(def) => def
.params
.iter()
.take_while(|p| !p.rest && !p.has_default)
.count(),
None => 0,
}
}
pub fn is_map(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Map { .. }))
}
pub fn is_set(&self, v: &Value) -> bool {
matches!(self.get(v), Some(JsObj::Set { .. }))
}
}
impl JsHost {
pub fn new_promise(&mut self) -> Value {
let id = self.promises.len() as u32;
self.promises.push(PromiseCell {
state: PromiseState::Pending,
value: Value::Undef,
reactions: Vec::new(),
handled: false,
});
self.alloc(JsObj::Promise { id })
}
pub fn promise_id(&self, v: &Value) -> Option<u32> {
match self.get(v) {
Some(JsObj::Promise { id }) => Some(*id),
_ => None,
}
}
pub fn promise_state(&self, id: u32) -> PromiseState {
self.promises[id as usize].state
}
pub fn promise_value(&self, id: u32) -> Value {
self.promises[id as usize].value.clone()
}
pub fn promise_mark_handled(&mut self, id: u32) {
self.promises[id as usize].handled = true;
}
pub fn take_reactions(&mut self, id: u32) -> Vec<PromiseReaction> {
std::mem::take(&mut self.promises[id as usize].reactions)
}
pub fn add_reaction(&mut self, id: u32, r: PromiseReaction) {
self.promises[id as usize].reactions.push(r);
}
pub fn settle_promise(&mut self, id: u32, state: PromiseState, value: Value) {
let c = &mut self.promises[id as usize];
if c.state != PromiseState::Pending {
return; }
c.state = state;
c.value = value;
}
pub fn queue_micro(&mut self, cb: Value, args: Vec<Value>) {
self.microtasks.push_back(Task::Js { cb, args });
}
pub fn queue_nexttick(&mut self, cb: Value, args: Vec<Value>) {
self.nextticks.push_back(Task::Js { cb, args });
}
pub fn queue_micro_native(&mut self, f: Box<dyn FnOnce() -> Result<(), String>>) {
self.microtasks.push_back(Task::Native(f));
}
pub fn add_timer(&mut self, delay: f64, callback: Value, args: Vec<Value>) -> u64 {
let id = self.next_timer;
self.next_timer += 1;
let deadline = Instant::now() + Duration::from_millis(delay.max(0.0) as u64);
self.macrotasks.push(Timer {
id,
delay,
seq: id,
callback,
args,
cancelled: false,
deadline,
});
id
}
pub fn io_sender(&self) -> Sender<IoTask> {
self.io_tx.clone()
}
pub fn incr_handle(&mut self) {
self.open_handles += 1;
}
pub fn decr_handle(&mut self) {
self.open_handles = self.open_handles.saturating_sub(1);
}
pub fn open_handles(&self) -> usize {
self.open_handles
}
fn pop_due_timer(&mut self, now: Instant) -> Option<Timer> {
let idx = self
.macrotasks
.iter()
.enumerate()
.filter(|(_, t)| !t.cancelled && t.deadline <= now)
.min_by(|(_, a), (_, b)| a.deadline.cmp(&b.deadline).then(a.seq.cmp(&b.seq)))
.map(|(i, _)| i);
idx.map(|i| self.macrotasks.remove(i))
}
fn next_timer_timeout(&self, now: Instant) -> Option<Duration> {
self.macrotasks
.iter()
.filter(|t| !t.cancelled)
.map(|t| t.deadline)
.min()
.map(|d| d.saturating_duration_since(now))
}
pub fn cancel_timer(&mut self, id: u64) {
for t in &mut self.macrotasks {
if t.id == id {
t.cancelled = true;
}
}
}
fn pop_next_timer(&mut self) -> Option<Timer> {
let idx = self
.macrotasks
.iter()
.enumerate()
.filter(|(_, t)| !t.cancelled)
.min_by(|(_, a), (_, b)| {
a.delay
.partial_cmp(&b.delay)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.seq.cmp(&b.seq))
})
.map(|(i, _)| i);
idx.map(|i| self.macrotasks.remove(i))
}
fn next_microtask(&mut self) -> Option<Task> {
self.nextticks
.pop_front()
.or_else(|| self.microtasks.pop_front())
}
fn has_microtasks(&self) -> bool {
!self.nextticks.is_empty() || !self.microtasks.is_empty()
}
fn has_macrotasks(&self) -> bool {
self.macrotasks.iter().any(|t| !t.cancelled)
}
}
pub fn run_event_loop() -> Result<(), String> {
let rx = with_host(|h| h.io_rx.take());
let result = drive_event_loop(rx.as_ref());
with_host(|h| h.io_rx = rx);
result
}
fn drive_event_loop(rx: Option<&Receiver<IoTask>>) -> Result<(), String> {
loop {
while let Some(task) = with_host(|h| h.next_microtask()) {
task.run()?;
}
if with_host(|h| h.open_handles()) == 0 {
match with_host(|h| h.pop_next_timer()) {
Some(t) => {
invoke(&t.callback, t.args, None)?;
}
None => {
if !with_host(|h| h.has_microtasks()) {
break;
}
}
}
if !with_host(|h| h.has_microtasks() || h.has_macrotasks()) {
break;
}
continue;
}
let now = Instant::now();
if let Some(t) = with_host(|h| h.pop_due_timer(now)) {
invoke(&t.callback, t.args, None)?;
continue; }
let rx = rx.expect("blocking-I/O regime requires the I/O receiver");
let timeout = with_host(|h| h.next_timer_timeout(now));
let recv = match timeout {
Some(d) => rx.recv_timeout(d),
None => rx
.recv()
.map_err(|_| std::sync::mpsc::RecvTimeoutError::Disconnected),
};
match recv {
Ok(task) => task()?,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, }
}
Ok(())
}
fn run_async(gen: Value) -> Value {
let result = with_host(|h| h.new_promise());
let rid = with_host(|h| h.promise_id(&result).unwrap());
drive_async(gen, rid, Value::Undef);
result
}
fn drive_async(gen: Value, rid: u32, send: Value) {
match gen_resume(&gen, send) {
Ok(GenStep::Yield(awaited)) => {
let ap = promise_of(&awaited);
let aid = with_host(|h| h.promise_id(&ap).unwrap());
let gen2 = gen.clone();
subscribe_native(
aid,
Box::new(move |state, val| {
let tag = if state == PromiseState::Rejected {
1.0
} else {
0.0
};
let packet = with_host(|h| h.new_array(vec![Value::Float(tag), val]));
drive_async(gen2, rid, packet);
Ok(())
}),
);
}
Ok(GenStep::Done(v)) => resolve_promise_val(rid, v),
Err(e) => {
let ev = take_exc_or_error(&e);
reject_promise_val(rid, ev);
}
}
}
pub fn await_value(awaited: Value) -> Result<Value, String> {
let packet = gen_yield(awaited)?;
let items = with_host(|h| h.iter_vec(&packet)).unwrap_or_default();
let tag = items
.first()
.map(|v| with_host(|h| h.to_number(v)))
.unwrap_or(0.0);
let val = items.get(1).cloned().unwrap_or(Value::Undef);
if tag == 1.0 {
with_host(|h| h.exc = Some(val.clone()));
Err(with_host(|h| crate::builtins::error_string(h, &val)))
} else {
Ok(val)
}
}
pub fn promise_of(v: &Value) -> Value {
if with_host(|h| h.promise_id(v)).is_some() {
return v.clone();
}
let p = with_host(|h| h.new_promise());
let id = with_host(|h| h.promise_id(&p).unwrap());
resolve_promise_val(id, v.clone());
p
}
pub fn subscribe_native(id: u32, f: Box<dyn FnOnce(PromiseState, Value) -> Result<(), String>>) {
let state = with_host(|h| h.promise_state(id));
if state == PromiseState::Pending {
with_host(|h| h.add_reaction(id, PromiseReaction::Native(f)));
} else {
let val = with_host(|h| h.promise_value(id));
with_host(|h| h.queue_micro_native(Box::new(move || f(state, val))));
}
}
pub fn resolve_promise_val(id: u32, value: Value) {
if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
return;
}
if let Some(vid) = with_host(|h| h.promise_id(&value)) {
if vid == id {
let e = with_host(|h| {
crate::builtins::synth_error(h, "TypeError: Chaining cycle detected")
});
reject_promise_val(id, e);
return;
}
subscribe_native(
vid,
Box::new(move |state, val| {
with_host(|h| h.settle_promise(id, state, val.clone()));
schedule_reactions(id);
Ok(())
}),
);
return;
}
with_host(|h| h.settle_promise(id, PromiseState::Fulfilled, value));
schedule_reactions(id);
}
pub fn reject_promise_val(id: u32, value: Value) {
if with_host(|h| h.promise_state(id)) != PromiseState::Pending {
return;
}
with_host(|h| h.settle_promise(id, PromiseState::Rejected, value));
schedule_reactions(id);
}
fn schedule_reactions(id: u32) {
let reactions = with_host(|h| h.take_reactions(id));
let state = with_host(|h| h.promise_state(id));
let value = with_host(|h| h.promise_value(id));
for r in reactions {
let value = value.clone();
match r {
PromiseReaction::Native(f) => {
with_host(|h| h.queue_micro_native(Box::new(move || f(state, value))));
}
PromiseReaction::Js {
on_ful,
on_rej,
result,
} => {
with_host(|h| {
h.queue_micro_native(Box::new(move || {
run_js_reaction(state, value, on_ful, on_rej, result)
}))
});
}
}
}
}
fn run_js_reaction(
state: PromiseState,
value: Value,
on_ful: Value,
on_rej: Value,
result: Value,
) -> Result<(), String> {
let rid = match with_host(|h| h.promise_id(&result)) {
Some(i) => i,
None => return Ok(()),
};
let handler = if state == PromiseState::Rejected {
on_rej
} else {
on_ful
};
if with_host(|h| is_callable(h, &handler)) {
match invoke(&handler, vec![value], None) {
Ok(r) => resolve_promise_val(rid, r),
Err(e) => reject_promise_val(rid, take_exc_or_error(&e)),
}
} else if state == PromiseState::Rejected {
reject_promise_val(rid, value);
} else {
resolve_promise_val(rid, value);
}
Ok(())
}
pub fn take_exc_or_error(e: &str) -> Value {
with_host(|h| {
h.error.take();
h.exc
.take()
.unwrap_or_else(|| crate::builtins::synth_error(h, e))
})
}
pub fn promise_then(p: &Value, on_ful: Value, on_rej: Value) -> Value {
let id = match with_host(|h| h.promise_id(p)) {
Some(i) => i,
None => return Value::Undef,
};
with_host(|h| h.promise_mark_handled(id));
let result = with_host(|h| h.new_promise());
let reaction = PromiseReaction::Js {
on_ful,
on_rej,
result: result.clone(),
};
let state = with_host(|h| h.promise_state(id));
if state == PromiseState::Pending {
with_host(|h| h.add_reaction(id, reaction));
} else {
let value = with_host(|h| h.promise_value(id));
if let PromiseReaction::Js {
on_ful,
on_rej,
result,
} = reaction
{
with_host(|h| {
h.queue_micro_native(Box::new(move || {
run_js_reaction(state, value, on_ful, on_rej, result)
}))
});
}
}
result
}