diff --git a/cli/src/cmd/eval.rs b/cli/src/cmd/eval.rs index c2b8ab49..094badd2 100644 --- a/cli/src/cmd/eval.rs +++ b/cli/src/cmd/eval.rs @@ -3,26 +3,29 @@ use clap::ArgMatches; use dash_middle::parser::error::IntoFormattableErrors; use dash_optimizer::OptLevel; use dash_rt::format_value; +use dash_rt::runtime::Runtime; use dash_vm::eval::EvalError; use dash_vm::value::Root; -use dash_vm::Vm; pub fn eval(args: &ArgMatches) -> anyhow::Result<()> { let source = args.value_of("source").context("Missing source")?; let opt = *args.get_one::("opt").unwrap(); - let mut vm = Vm::new(Default::default()); - let mut scope = vm.scope(); + tokio::runtime::Runtime::new()?.block_on(async move { + let mut runtime = Runtime::new(None); - match scope.eval(source, opt) { - Ok(value) => println!("{}", format_value(value.root(&mut scope), &mut scope).unwrap()), - Err(EvalError::Exception(value)) => { - println!("{}", format_value(value.root(&mut scope), &mut scope).unwrap()) - } - Err(EvalError::Middle(errs)) => println!("{}", errs.formattable(source, true)), - }; + runtime.vm_mut().with_scope(|scope| { + match scope.eval(source, opt) { + Ok(value) => println!("{}", format_value(value.root(scope), scope).unwrap()), + Err(EvalError::Exception(value)) => { + println!("{}", format_value(value.root(scope), scope).unwrap()) + } + Err(EvalError::Middle(errs)) => println!("{}", errs.formattable(source, true)), + }; + }); - scope.process_async_tasks(); + runtime.run_event_loop().await; + }); Ok(()) } diff --git a/cli/src/cmd/repl.rs b/cli/src/cmd/repl.rs index 99b512c8..05a81185 100644 --- a/cli/src/cmd/repl.rs +++ b/cli/src/cmd/repl.rs @@ -1,34 +1,37 @@ use dash_middle::parser::error::IntoFormattableErrors; use dash_optimizer::OptLevel; use dash_rt::format_value; +use dash_rt::runtime::Runtime; use dash_vm::eval::EvalError; use dash_vm::value::Root; -use dash_vm::Vm; use rustyline::Editor; pub fn repl() -> anyhow::Result<()> { let mut rl = Editor::<()>::new(); - let mut vm = Vm::new(Default::default()); - let mut scope = vm.scope(); + tokio::runtime::Runtime::new()?.block_on(async move { + let mut rt = Runtime::new(None); - while let Ok(input) = rl.readline("> ") { - if input.is_empty() { - continue; - } + while let Ok(input) = rl.readline("> ") { + if input.is_empty() { + continue; + } - rl.add_history_entry(&input); + rl.add_history_entry(&input); - match scope.eval(&input, OptLevel::Aggressive) { - Ok(value) => println!("{}", format_value(value.root(&mut scope), &mut scope).unwrap()), - Err(EvalError::Exception(value)) => { - println!("{}", format_value(value.root(&mut scope), &mut scope).unwrap()) - } - Err(EvalError::Middle(errs)) => println!("{}", errs.formattable(&input, true)), - } + rt.vm_mut().with_scope(|scope| { + match scope.eval(&input, OptLevel::Aggressive) { + Ok(value) => println!("{}", format_value(value.root(scope), scope).unwrap()), + Err(EvalError::Exception(value)) => { + println!("{}", format_value(value.root(scope), scope).unwrap()) + } + Err(EvalError::Middle(errs)) => println!("{}", errs.formattable(&input, true)), + } - scope.process_async_tasks(); - } + scope.process_async_tasks(); + }); + } + }); Ok(()) } diff --git a/cli/src/cmd/run.rs b/cli/src/cmd/run.rs index 80907247..be2543ba 100644 --- a/cli/src/cmd/run.rs +++ b/cli/src/cmd/run.rs @@ -2,7 +2,6 @@ use dash_middle::parser::error::IntoFormattableErrors; use dash_optimizer::OptLevel; use dash_rt::format_value; use dash_rt::runtime::Runtime; -use dash_rt::state::State; use dash_vm::eval::EvalError; use dash_vm::value::Root; use std::fs; @@ -53,7 +52,7 @@ fn run_normal_mode(path: &str, opt: OptLevel, quiet: bool, initial_gc_threshold: } async fn inner(source: String, opt: OptLevel, quiet: bool, initial_gc_threshold: Option) -> anyhow::Result<()> { - let mut rt = Runtime::new(initial_gc_threshold).await; + let mut rt = Runtime::new(initial_gc_threshold); let module = dash_rt_modules::init_modules(); rt.set_module_manager(module); @@ -76,11 +75,8 @@ async fn inner(source: String, opt: OptLevel, quiet: bool, initial_gc_threshold: println!("{}", format_value(value, &mut scope).unwrap()); } - let state = State::from_vm_mut(&mut scope); - if state.needs_event_loop() { - drop(scope); - rt.run_event_loop().await; - } + drop(scope); + rt.run_event_loop().await; Ok(()) } diff --git a/crates/dash_node_impl/src/lib.rs b/crates/dash_node_impl/src/lib.rs index 5ed1f106..616ad9a2 100644 --- a/crates/dash_node_impl/src/lib.rs +++ b/crates/dash_node_impl/src/lib.rs @@ -4,7 +4,7 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::rc::Rc; -use anyhow::{anyhow, Context}; +use anyhow::{Context, anyhow}; use dash_log::debug; use dash_middle::parser::error::IntoFormattableErrors; use dash_optimizer::OptLevel; @@ -19,7 +19,7 @@ use dash_vm::localscope::LocalScope; use dash_vm::value::array::Array; use dash_vm::value::object::{NamedObject, Object, PropertyValue}; use dash_vm::value::{Root, Unpack, Unrooted, Value, ValueKind}; -use dash_vm::{delegate, extract, throw, Vm}; +use dash_vm::{Vm, delegate, extract, throw}; use package::Package; use rustc_hash::FxHashMap; use state::Nodejs; @@ -76,7 +76,7 @@ async fn run_inner_fallible(path: &str, opt: OptLevel, initial_gc_threshold: Opt ongoing_requires: RefCell::new(FxHashMap::default()), }); - let mut rt = Runtime::new(initial_gc_threshold).await; + let mut rt = Runtime::new(initial_gc_threshold); let state @ state::State { sym: NodeSymbols { @@ -137,9 +137,7 @@ async fn run_inner_fallible(path: &str, opt: OptLevel, initial_gc_threshold: Opt ) })?; - if rt.state().needs_event_loop() { - rt.run_event_loop().await; - } + rt.run_event_loop().await; Ok(()) } diff --git a/crates/dash_rt/js/inspect.js b/crates/dash_rt/js/inspect.js deleted file mode 100644 index b51f427d..00000000 --- a/crates/dash_rt/js/inspect.js +++ /dev/null @@ -1,105 +0,0 @@ -const is = { - string: (value) => typeof value === 'string', - number: (value) => typeof value === 'number', - boolean: (value) => typeof value === 'boolean', - nullish: (value) => value === null || value === undefined, - error: (value) => value instanceof Error, - array: (value) => value instanceof Array, // TODO: Array.isArray - function: (value) => typeof value === 'function', - arraybuffer: function (value) { - return this.looseObject(value) && value.constructor.name === 'ArrayBuffer'; - }, - looseObject: function (value) { - return !this.nullish(value) && typeof value === 'object'; - }, - strictObject: function (value) { - // TODO: use Array.isArray once we have it - return this.looseObject(value) && !(value instanceof Array); - } -}; - -function inner(value, depth) { - if (is.string(value)) { - return '"' + value + '"'; - } - - if (is.error(value)) { - return value.stack; - } - - if (is.arraybuffer(value)) { - let length = value.byteLength(); // TODO: supposed to be a getter - let repr = `ArrayBuffer(${length}) { <`; - let view = new Uint8Array(value); - for (let i = 0; i < Math.min(length, 100); i++) { - if (i > 0) { - repr += ', '; - } - repr += view[i].toString(16); - } - if (length > 100) { - repr += `, ... ${length - 100} more`; - } - repr += '> }' - return repr; - } - - if (is.strictObject(value)) { - const keys = Object.keys(value); - const hasElements = keys.length > 0; - - let repr; - if (value.constructor !== Object) { - repr = value.constructor.name + ' {'; - } else { - repr = '{'; - } - - if (hasElements) repr += ' '; - - for (let i = 0; i < keys.length; i++) { - if (i > 0) { - repr += ', '; - } - - const key = keys[i]; - repr += key + ': ' + inner(value[key], depth + 1); - } - - if (hasElements) repr += ' '; - - repr += '}'; - - return repr; - } - - if (is.array(value)) { - const len = value.length; - - let repr = '['; - - for (let i = 0; i < len; i++) { - if (i > 0) { - repr += ', '; - } - - repr += inner(value[i], depth + 1); - } - - repr += ']'; - return repr; - } - - if (is.function(value)) { - const name = value.name || '(anonymous)'; - - return '[Function: ' + name + ']'; - } - - // if nothing matched, stringify - return String(value); -} - -export default function (value) { - return inner(value, 0); -} diff --git a/crates/dash_rt/src/inspect.rs b/crates/dash_rt/src/inspect.rs new file mode 100644 index 00000000..0320f6b4 --- /dev/null +++ b/crates/dash_rt/src/inspect.rs @@ -0,0 +1,244 @@ +use std::fmt::Write; + +use dash_middle::interner::{Symbol, sym}; + +use dash_vm::frame::This; +use dash_vm::localscope::LocalScope; +use dash_vm::util::intern_f64; +use dash_vm::value::array::{Array, ArrayIterator}; +use dash_vm::value::arraybuffer::ArrayBuffer; +use dash_vm::value::error::Error; +use dash_vm::value::typedarray::TypedArray; +use dash_vm::value::{Typeof, Unpack, ValueKind}; + +use dash_vm::value::object::{Object, PropertyDataDescriptor, PropertyKey, PropertyValueKind}; +use dash_vm::value::ops::conversions::ValueConversion; +use dash_vm::value::primitive::Number; +use dash_vm::value::root_ext::RootErrExt; +use dash_vm::value::{Root, Value}; + +#[derive(Copy, Clone)] +pub struct InspectOptions { + /// Whether to invoke any getters + invoke_getters: bool, + /// The max depth + depth: u32, + /// Whether to use colors + colors: bool, +} +impl Default for InspectOptions { + fn default() -> Self { + Self { + invoke_getters: false, + depth: 8, + colors: true, + } + } +} + +const GREEN: &str = "\x1b[32m"; +const YELLOW: &str = "\x1b[33m"; +const GREY: &str = "\x1b[38;5;m"; +const RESET: &str = "\x1b[0m"; + +fn colored(s: &mut String, options: InspectOptions, color: &str, f: impl FnOnce(&mut String) -> R) -> R { + if options.colors { + *s += color; + let res = f(s); + *s += RESET; + res + } else { + f(s) + } +} + +fn debug_inspect_string(s: &str) -> String { + format!("{s:?}") +} + +fn inspect_array_into( + array: Value, + scope: &mut LocalScope<'_>, + options: InspectOptions, + depth: u32, + out: &mut String, +) -> Result<(), Value> { + let iterator = ArrayIterator::new(scope, array)?; + if iterator.is_empty() { + *out += "[]"; + return Ok(()); + } + + *out += "[ "; + let mut count = 0; + while let Some(value) = iterator.next(scope).root(scope)? { + if count > 0 { + *out += ", "; + } + inspect_inner_into(value, scope, options, depth + 1, out)?; + count += 1; + } + *out += " ]"; + Ok(()) +} + +fn inspect_arraybuffer_into(arraybuffer: &ArrayBuffer, constructor: Symbol, out: &mut String) { + write!(out, "{constructor}({}) {{ ", arraybuffer.len()).unwrap(); + for (i, byte) in arraybuffer.storage().iter().enumerate().take(32) { + if i > 0 { + *out += " "; + } + write!(out, "{:02x}", byte.get()).unwrap(); + } + if arraybuffer.len() > 32 { + *out += " ..."; + } + *out += " }"; +} + +fn inspect_inner_into( + value: Value, + scope: &mut LocalScope<'_>, + options: InspectOptions, + depth: u32, + out: &mut String, +) -> Result<(), Value> { + if depth > options.depth { + *out += "..."; + return Ok(()); + } else if depth > 5000 { + *out += "/* recursion limit reached */"; + return Ok(()); + } + + match value.unpack() { + ValueKind::String(string) => { + if depth > 1 { + colored(out, options, GREEN, |s| { + *s += &*debug_inspect_string(string.res(scope)); + }) + } else { + *out += string.res(scope) + } + } + ValueKind::Number(Number(number)) => colored(out, options, YELLOW, |s| { + let sym = intern_f64(scope, number); + *s += scope.interner.resolve(sym); + }), + ValueKind::Boolean(boolean) => colored(out, options, YELLOW, |s| *s += if boolean { "true" } else { "false" }), + ValueKind::Undefined(_) => colored(out, options, GREY, |s| *s += "undefined"), + ValueKind::Null(_) => colored(out, options, GREY, |s| *s += "null"), + ValueKind::Symbol(symbol) => colored(out, options, YELLOW, |s| { + *s += &*("@@".to_owned() + scope.interner.resolve(symbol.sym())); + }), + ValueKind::Object(object) => { + let constructor = object.get_property(scope, sym::constructor.into()).root(scope)?; + let constructor_name = constructor + .get_property(scope, sym::name.into()) + .root(scope)? + .to_js_string(scope)?; + + if object.extract::(scope).is_some() { + return inspect_array_into(value, scope, options, depth, out); + } + + if let Some(error) = object.extract::(scope) { + *out += error.stack.res(scope); + return Ok(()); + } + + // ArrayBuffer and views + if let Some(arraybuffer) = object + .extract::(scope) + .or_else(|| object.extract::(scope).map(|t| t.arraybuffer(scope))) + { + inspect_arraybuffer_into(arraybuffer, constructor_name.sym(), out); + return Ok(()); + } + + if object.type_of(scope) == Typeof::Function { + let name = object + .get_own_property(scope, sym::name.into()) + .root(scope)? + .into_option() + .map(|v| v.to_js_string(scope)) + .transpose()? + .map(|s| s.res(scope)) + .filter(|v| !v.is_empty()) + .unwrap_or("(anonymous)"); + + write!(out, "[Function: {name}]").unwrap(); + return Ok(()); + } + + if constructor != Value::object(scope.statics.object_ctor) { + *out += constructor_name.res(scope); + *out += " "; + } + + *out += "{ "; + let keys = object.own_keys(scope)?; + for (i, key) in keys.into_iter().enumerate() { + let key = PropertyKey::from_value(scope, key)?; + + if let Some(property_value) = object.get_own_property_descriptor(scope, key).root_err(scope)? { + if property_value.descriptor.contains(PropertyDataDescriptor::ENUMERABLE) { + if i > 0 { + *out += ", "; + } + + match key { + PropertyKey::String(string) => { + let string = string.res(scope); + if string.bytes().any(|v| { + dash_middle::util::is_identifier_start(v) || dash_middle::util::is_alpha(v) + }) { + *out += string; + } else { + colored(out, options, GREEN, |s| *s += &*debug_inspect_string(string)); + } + } + PropertyKey::Symbol(symbol) => { + *out += "["; + inspect_inner_into(Value::symbol(symbol), scope, options, depth + 1, out)?; + *out += "]"; + } + }; + + *out += ": "; + + match property_value.kind { + PropertyValueKind::Trap { .. } => { + if options.invoke_getters { + colored(out, options, GREY, |s| *s += "(computed) "); + inspect_inner_into( + property_value.get_or_apply(scope, This::Bound(value)).root(scope)?, + scope, + options, + depth + 1, + out, + )?; + } else { + colored(out, options, GREY, |s| *s += "(trap)"); + } + } + PropertyValueKind::Static(value) => { + inspect_inner_into(value, scope, options, depth + 1, out)?; + } + } + } + } + } + *out += " }"; + } + ValueKind::External(_) => unreachable!(), + } + + Ok(()) +} + +/// Deeply "inspects" (debug formats) a JavaScript value. +pub fn inspect(value: Value, scope: &mut LocalScope<'_>, options: InspectOptions) -> Result { + let mut out = String::new(); + inspect_inner_into(value, scope, options, 1, &mut out).map(|_| out) +} diff --git a/crates/dash_rt/src/lib.rs b/crates/dash_rt/src/lib.rs index 41619502..a2355ef9 100755 --- a/crates/dash_rt/src/lib.rs +++ b/crates/dash_rt/src/lib.rs @@ -1,18 +1,17 @@ use std::future::Future; -use dash_compiler::FunctionCompiler; use dash_vm::PromiseAction; -use dash_vm::frame::{Exports, Frame, This}; use dash_vm::localscope::LocalScope; +use dash_vm::value::Value; use dash_vm::value::function::native::CallContext; -use dash_vm::value::ops::conversions::ValueConversion; use dash_vm::value::promise::Promise; -use dash_vm::value::{Root, Value}; use event::EventMessage; +use inspect::InspectOptions; use state::State; pub mod active_tasks; pub mod event; +pub mod inspect; pub mod module; pub mod runtime; pub mod state; @@ -59,31 +58,6 @@ where Ok(Value::object(promise)) } -pub fn format_value<'s>(value: Value, scope: &'s mut LocalScope) -> Result<&'s str, Value> { - let inspect_bc = FunctionCompiler::compile_str( - &mut scope.interner, - include_str!("../js/inspect.js"), - Default::default(), - ) - .unwrap(); - - // TODO: we need to somehow add `CompileResult` as an external? - - let Exports { - default: Some(inspect_fn), - .. - } = scope.execute_module(Frame::from_compile_result(inspect_bc)).unwrap() - else { - panic!("inspect module did not have a default export"); - }; - - let result = inspect_fn - .root(scope) - .apply(scope, This::Default, vec![value]) - .unwrap() - .root(scope) - .to_js_string(scope) - .unwrap(); - - Ok(result.res(scope)) +pub fn format_value(value: Value, scope: &mut LocalScope) -> Result { + inspect::inspect(value, scope, InspectOptions::default()) } diff --git a/crates/dash_rt/src/runtime.rs b/crates/dash_rt/src/runtime.rs index c0a2667f..edaaab67 100644 --- a/crates/dash_rt/src/runtime.rs +++ b/crates/dash_rt/src/runtime.rs @@ -2,16 +2,19 @@ use std::fmt::Debug; use std::time::SystemTime; use dash_middle::compiler::StaticImportKind; +use dash_middle::interner::sym; use dash_optimizer::OptLevel; use dash_vm::eval::EvalError; use dash_vm::params::VmParams; +use dash_vm::value::function::native::register_native_fn; +use dash_vm::value::object::{Object, PropertyValue}; use dash_vm::value::string::JsString; -use dash_vm::value::{Unrooted, Value}; -use dash_vm::{throw, Vm}; +use dash_vm::value::{Root, Unpack, Unrooted, Value, ValueKind}; +use dash_vm::{Vm, throw}; use tokio::sync::mpsc; -use tracing::info; use crate::event::{EventMessage, EventSender}; +use crate::inspect::{self, InspectOptions}; use crate::module::ModuleLoader; use crate::state::State; @@ -23,7 +26,7 @@ pub struct Runtime { } impl Runtime { - pub async fn new(initial_gc_threshold: Option) -> Self { + pub fn new(initial_gc_threshold: Option) -> Self { let rt = tokio::runtime::Handle::current(); let (etx, erx) = mpsc::unbounded_channel(); @@ -45,7 +48,38 @@ impl Runtime { } let vm = Vm::new(params); - Self { vm, event_rx: erx } + + let mut this = Self { vm, event_rx: erx }; + this.init_globals(); + this + } + + fn init_globals(&mut self) { + let scope = &mut self.vm.scope(); + let global = scope.global(); + let log = register_native_fn(scope, sym::log, |cx| { + if let [arg] = *cx.args { + if let ValueKind::String(s) = arg.unpack() { + // Fast path + println!("{}", s.res(cx.scope)); + return Ok(Value::undefined()); + } + } + + for arg in cx.args { + let string = inspect::inspect(arg, cx.scope, InspectOptions::default())?; + println!("{string} "); + } + + Ok(Value::undefined()) + }); + + global + .get_property(scope, sym::console.into()) + .root(scope) + .unwrap() + .set_property(scope, sym::log.into(), PropertyValue::static_default(log.into())) + .unwrap(); } pub fn vm_params(&mut self) -> &mut VmParams { @@ -69,6 +103,10 @@ impl Runtime { } pub async fn run_event_loop(mut self) { + if !self.state().needs_event_loop() { + return; + } + while let Some(message) = self.event_rx.recv().await { match message { EventMessage::ScheduleCallback(fun) => { @@ -79,9 +117,7 @@ impl Runtime { } } - let state = State::from_vm_mut(&mut self.vm); - if !state.needs_event_loop() { - info!("Event loop finished"); + if !self.state().needs_event_loop() { return; } } diff --git a/crates/dash_vm/src/lib.rs b/crates/dash_vm/src/lib.rs index 5f93e433..0e4834e2 100644 --- a/crates/dash_vm/src/lib.rs +++ b/crates/dash_vm/src/lib.rs @@ -73,7 +73,7 @@ pub struct Vm { // We can't do that in Persistent's Drop code, because we don't have access to the VM there. external_refs: ExternalRefs, scopes: LocalScopeList, - statics: Box, + pub statics: Box, #[cfg_attr(dash_lints, dash_lints::trusted_no_gc)] try_blocks: Vec, #[cfg_attr(dash_lints, dash_lints::trusted_no_gc)] diff --git a/crates/dash_vm/src/value/array/mod.rs b/crates/dash_vm/src/value/array/mod.rs index a8abcf8b..23e911ad 100644 --- a/crates/dash_vm/src/value/array/mod.rs +++ b/crates/dash_vm/src/value/array/mod.rs @@ -384,6 +384,14 @@ impl ArrayIterator { Ok(None) } } + + pub fn len(&self) -> usize { + self.length + } + + pub fn is_empty(&self) -> bool { + self.length == 0 + } } /// Equivalent to calling get_property, but specialized for arrays diff --git a/crates/dash_vm/src/value/inspect.rs b/crates/dash_vm/src/value/inspect.rs deleted file mode 100644 index 71698455..00000000 --- a/crates/dash_vm/src/value/inspect.rs +++ /dev/null @@ -1,6 +0,0 @@ -use super::Value; - -impl Value { - // TODO: - // pub fn inspect(&self) {} -} diff --git a/crates/dash_vm/src/value/mod.rs b/crates/dash_vm/src/value/mod.rs index e01f1352..c4efdb29 100755 --- a/crates/dash_vm/src/value/mod.rs +++ b/crates/dash_vm/src/value/mod.rs @@ -6,7 +6,6 @@ pub mod conversions; pub mod date; pub mod error; pub mod function; -pub mod inspect; pub mod map; pub mod object; pub mod ops; @@ -26,11 +25,11 @@ use dash_proc_macro::Trace; pub mod string; use crate::frame::This; -use crate::gc::trace::{Trace, TraceCtxt}; use crate::gc::ObjectId; +use crate::gc::trace::{Trace, TraceCtxt}; use crate::util::cold_path; use crate::value::primitive::{Null, Undefined}; -use crate::{delegate, throw, Vm}; +use crate::{Vm, delegate, throw}; use self::object::{Object, PropertyKey, PropertyValue}; use self::primitive::{InternalSlots, Number, Symbol};