diff --git a/Cargo.lock b/Cargo.lock index 980296af424..a75d4993c00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,6 +366,8 @@ version = "0.16.0" dependencies = [ "boa_ast", "boa_engine", + "boa_gc", + "boa_interner", "boa_parser", "clap 4.2.1", "colored", diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index bfa1fe1bdc3..ec7f07c4abf 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -15,6 +15,8 @@ rust-version.workspace = true boa_engine = { workspace = true, features = ["deser", "console", "flowgraph", "trace"] } boa_ast = { workspace = true, features = ["serde"]} boa_parser.workspace = true +boa_gc.workspace = true +boa_interner.workspace = true rustyline = { version = "11.0.0", features = ["derive"]} clap = { version = "4.2.1", features = ["derive"] } serde_json = "1.0.95" diff --git a/boa_cli/src/debug/function.rs b/boa_cli/src/debug/function.rs new file mode 100644 index 00000000000..d413c064bfe --- /dev/null +++ b/boa_cli/src/debug/function.rs @@ -0,0 +1,193 @@ +use boa_engine::{ + builtins::function::Function, + object::ObjectInitializer, + vm::flowgraph::{Direction, Graph}, + Context, JsArgs, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, +}; +use boa_interner::ToInternedString; + +use crate::FlowgraphFormat; + +fn flowgraph_parse_format_option(value: &JsValue) -> JsResult { + if value.is_undefined() { + return Ok(FlowgraphFormat::Mermaid); + } + + if let Some(string) = value.as_string() { + return match string.to_std_string_escaped().to_lowercase().as_str() { + "mermaid" => Ok(FlowgraphFormat::Mermaid), + "graphviz" => Ok(FlowgraphFormat::Graphviz), + format => Err(JsNativeError::typ() + .with_message(format!("Unknown format type '{format}'")) + .into()), + }; + } + + Err(JsNativeError::typ() + .with_message("format type must be a string") + .into()) +} + +fn flowgraph_parse_direction_option(value: &JsValue) -> JsResult { + if value.is_undefined() { + return Ok(Direction::LeftToRight); + } + + if let Some(string) = value.as_string() { + return match string.to_std_string_escaped().to_lowercase().as_str() { + "leftright" | "lr" => Ok(Direction::LeftToRight), + "rightleft" | "rl" => Ok(Direction::RightToLeft), + "topbottom" | "tb" => Ok(Direction::TopToBottom), + "bottomtop" | "bt " => Ok(Direction::BottomToTop), + direction => Err(JsNativeError::typ() + .with_message(format!("Unknown direction type '{direction}'")) + .into()), + }; + } + + Err(JsNativeError::typ() + .with_message("direction type must be a string") + .into()) +} + +/// Get functions instruction flowgraph +fn flowgraph(_this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let Some(value) = args.get(0) else { + return Err(JsNativeError::typ() + .with_message("expected function argument") + .into()); + }; + + let Some(object) = value.as_object() else { + return Err(JsNativeError::typ() + .with_message(format!("expected object, got {}", value.type_of())) + .into()); + }; + + let mut format = FlowgraphFormat::Mermaid; + let mut direction = Direction::LeftToRight; + if let Some(arguments) = args.get(1) { + if let Some(arguments) = arguments.as_object() { + format = flowgraph_parse_format_option(&arguments.get("format", context)?)?; + direction = flowgraph_parse_direction_option(&arguments.get("direction", context)?)?; + } else if value.is_string() { + format = flowgraph_parse_format_option(value)?; + } else { + return Err(JsNativeError::typ() + .with_message("options argument must be a string or object") + .into()); + } + } + + let object = object.borrow(); + + let Some(function) = object.as_function() else { + return Err(JsNativeError::typ() + .with_message("expected function object") + .into()); + }; + + let code = match function { + Function::Ordinary { code, .. } + | Function::Async { code, .. } + | Function::Generator { code, .. } + | Function::AsyncGenerator { code, .. } => code, + Function::Native { .. } => { + return Err(JsNativeError::typ() + .with_message("native functions do not have bytecode") + .into()) + } + }; + + let mut graph = Graph::new(direction); + code.to_graph(context.interner(), graph.subgraph(String::default())); + let result = match format { + FlowgraphFormat::Graphviz => graph.to_graphviz_format(), + FlowgraphFormat::Mermaid => graph.to_mermaid_format(), + }; + + Ok(JsValue::new(result)) +} + +fn bytecode(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let Some(value) = args.get(0) else { + return Err(JsNativeError::typ() + .with_message("expected function argument") + .into()); + }; + + let Some(object) = value.as_object() else { + return Err(JsNativeError::typ() + .with_message(format!("expected object, got {}", value.type_of())) + .into()); + }; + let object = object.borrow(); + let Some(function) = object.as_function() else { + return Err(JsNativeError::typ() + .with_message("expected function object") + .into()); + }; + let code = match function { + Function::Ordinary { code, .. } + | Function::Async { code, .. } + | Function::Generator { code, .. } + | Function::AsyncGenerator { code, .. } => code, + Function::Native { .. } => { + return Err(JsNativeError::typ() + .with_message("native functions do not have bytecode") + .into()) + } + }; + + Ok(code.to_interned_string(context.interner()).into()) +} + +fn set_trace_flag_in_function_object(object: &JsObject, value: bool) -> JsResult<()> { + let object = object.borrow(); + let Some(function) = object.as_function() else { + return Err(JsNativeError::typ() + .with_message("expected function object") + .into()); + }; + let code = match function { + Function::Ordinary { code, .. } + | Function::Async { code, .. } + | Function::Generator { code, .. } + | Function::AsyncGenerator { code, .. } => code, + Function::Native { .. } => { + return Err(JsNativeError::typ() + .with_message("native functions do not have bytecode") + .into()) + } + }; + code.set_trace(value); + Ok(()) +} + +/// Trace function. +fn trace(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let value = args.get_or_undefined(0); + let this = args.get_or_undefined(1); + + let Some(callable) = value.as_callable() else { + return Err(JsNativeError::typ() + .with_message("expected callable object") + .into()); + }; + + let arguments = args.get(2..).unwrap_or(&[]); + + set_trace_flag_in_function_object(callable, true)?; + let result = callable.call(this, arguments, context); + set_trace_flag_in_function_object(callable, false)?; + + result +} + +pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { + ObjectInitializer::new(context) + .function(NativeFunction::from_fn_ptr(flowgraph), "flowgraph", 1) + .function(NativeFunction::from_fn_ptr(bytecode), "bytecode", 1) + .function(NativeFunction::from_fn_ptr(trace), "trace", 1) + .build() +} diff --git a/boa_cli/src/debug/gc.rs b/boa_cli/src/debug/gc.rs new file mode 100644 index 00000000000..93c4b625457 --- /dev/null +++ b/boa_cli/src/debug/gc.rs @@ -0,0 +1,13 @@ +use boa_engine::{object::ObjectInitializer, Context, JsObject, JsResult, JsValue, NativeFunction}; + +/// Trigger garbage collection. +fn collect(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult { + boa_gc::force_collect(); + Ok(JsValue::undefined()) +} + +pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { + ObjectInitializer::new(context) + .function(NativeFunction::from_fn_ptr(collect), "collect", 0) + .build() +} diff --git a/boa_cli/src/debug/mod.rs b/boa_cli/src/debug/mod.rs new file mode 100644 index 00000000000..48a02a0bd28 --- /dev/null +++ b/boa_cli/src/debug/mod.rs @@ -0,0 +1,48 @@ +// Allow lint so it, doesn't warn about `JsResult<>` unneeded return on functions. +#![allow(clippy::unnecessary_wraps)] + +use boa_engine::{object::ObjectInitializer, property::Attribute, Context, JsObject}; + +mod function; +mod gc; +mod object; +mod optimizer; + +fn create_boa_object(context: &mut Context<'_>) -> JsObject { + let function_module = function::create_object(context); + let object_module = object::create_object(context); + let optimizer_module = optimizer::create_object(context); + let gc_module = gc::create_object(context); + + ObjectInitializer::new(context) + .property( + "function", + function_module, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + "object", + object_module, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + "optimizer", + optimizer_module, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + "gc", + gc_module, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() +} + +pub(crate) fn init_boa_debug_object(context: &mut Context<'_>) { + let boa_object = create_boa_object(context); + context.register_global_property( + "$boa", + boa_object, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ); +} diff --git a/boa_cli/src/debug/object.rs b/boa_cli/src/debug/object.rs new file mode 100644 index 00000000000..7518e43bec8 --- /dev/null +++ b/boa_cli/src/debug/object.rs @@ -0,0 +1,27 @@ +use boa_engine::{ + object::ObjectInitializer, Context, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, +}; + +/// Returns objects pointer in memory. +fn id(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult { + let Some(value) = args.get(0) else { + return Err(JsNativeError::typ() + .with_message("expected object argument") + .into()); + }; + + let Some(object) = value.as_object() else { + return Err(JsNativeError::typ() + .with_message(format!("expected object, got {}", value.type_of())) + .into()); + }; + + let ptr: *const _ = object.as_ref(); + Ok(format!("0x{:X}", ptr as usize).into()) +} + +pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { + ObjectInitializer::new(context) + .function(NativeFunction::from_fn_ptr(id), "id", 1) + .build() +} diff --git a/boa_cli/src/debug/optimizer.rs b/boa_cli/src/debug/optimizer.rs new file mode 100644 index 00000000000..2580b776cd1 --- /dev/null +++ b/boa_cli/src/debug/optimizer.rs @@ -0,0 +1,82 @@ +use boa_engine::{ + object::{FunctionObjectBuilder, ObjectInitializer}, + optimizer::OptimizerOptions, + property::Attribute, + Context, JsArgs, JsObject, JsResult, JsValue, NativeFunction, +}; + +fn get_constant_folding( + _: &JsValue, + _: &[JsValue], + context: &mut Context<'_>, +) -> JsResult { + Ok(context + .optimizer_options() + .contains(OptimizerOptions::CONSTANT_FOLDING) + .into()) +} + +fn set_constant_folding( + _: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, +) -> JsResult { + let value = args.get_or_undefined(0).to_boolean(); + let mut options = context.optimizer_options(); + options.set(OptimizerOptions::CONSTANT_FOLDING, value); + context.set_optimizer_options(options); + Ok(JsValue::undefined()) +} + +fn get_statistics(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult { + Ok(context + .optimizer_options() + .contains(OptimizerOptions::STATISTICS) + .into()) +} + +fn set_statistics(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + let value = args.get_or_undefined(0).to_boolean(); + let mut options = context.optimizer_options(); + options.set(OptimizerOptions::STATISTICS, value); + context.set_optimizer_options(options); + Ok(JsValue::undefined()) +} + +pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { + let get_constant_folding = + FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_constant_folding)) + .name("get constantFolding") + .length(0) + .build(); + let set_constant_folding = + FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_constant_folding)) + .name("set constantFolding") + .length(1) + .build(); + + let get_statistics = + FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_statistics)) + .name("get statistics") + .length(0) + .build(); + let set_statistics = + FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_statistics)) + .name("set statistics") + .length(1) + .build(); + ObjectInitializer::new(context) + .accessor( + "constantFolding", + Some(get_constant_folding), + Some(set_constant_folding), + Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .accessor( + "statistics", + Some(get_statistics), + Some(set_statistics), + Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .build() +} diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 640216545bc..355492723b7 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -58,6 +58,7 @@ )] #![allow(clippy::option_if_let_else, clippy::redundant_pub_crate)] +mod debug; mod helper; use boa_ast::StatementList; @@ -70,6 +71,7 @@ use boa_engine::{ }; use clap::{Parser, ValueEnum, ValueHint}; use colored::{Color, Colorize}; +use debug::init_boa_debug_object; use rustyline::{config::Config, error::ReadlineError, EditMode, Editor}; use std::{cell::RefCell, collections::VecDeque, fs::read, fs::OpenOptions, io, path::PathBuf}; @@ -146,6 +148,10 @@ struct Opt { requires = "graph" )] flowgraph_direction: Option, + + /// Inject debugging object `$boa`. + #[arg(long)] + debug_object: bool, } impl Opt { @@ -308,6 +314,10 @@ fn main() -> Result<(), io::Error> { // Trace Output context.set_trace(args.trace); + if args.debug_object { + init_boa_debug_object(&mut context); + } + // Configure optimizer options let mut optimizer_options = OptimizerOptions::empty(); optimizer_options.set(OptimizerOptions::STATISTICS, args.optimizer_statistics); diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index a87151b0cc8..84868ca95fc 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -1327,6 +1327,8 @@ impl<'b, 'host> ByteCompiler<'b, 'host> { is_class_constructor: self.is_class_constructor, class_field_initializer_name: self.class_field_initializer_name, function_environment_push_location: self.function_environment_push_location, + #[cfg(feature = "trace")] + trace: std::cell::Cell::new(false), } } diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 142471cd35a..dd862edbdf4 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -2124,6 +2124,33 @@ impl<'ctx, 'host> ObjectInitializer<'ctx, 'host> { self } + /// Add new accessor property to the object. + /// + /// # Panics + /// + /// If both getter or setter are [`None`]. + pub fn accessor( + &mut self, + key: K, + get: Option, + set: Option, + attribute: Attribute, + ) -> &mut Self + where + K: Into, + { + // Accessors should have at least one function. + assert!(set.is_some() || get.is_some()); + + let property = PropertyDescriptor::builder() + .maybe_get(get) + .maybe_set(set) + .enumerable(attribute.enumerable()) + .configurable(attribute.configurable()); + self.object.borrow_mut().insert(key, property); + self + } + /// Build the object. #[inline] pub fn build(&mut self) -> JsObject { diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index 1eb1875e759..b99aebacb65 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -128,6 +128,11 @@ pub struct CodeBlock { /// We execute the parameter expressions in the function code and push the function environment afterward. /// When the execution of the parameter expressions throws an error, we do not need to pop the function environment. pub(crate) function_environment_push_location: u32, + + #[cfg(feature = "trace")] + /// Trace instruction execution to `stdout`. + #[unsafe_ignore_trace] + pub(crate) trace: std::cell::Cell, } impl CodeBlock { @@ -153,9 +158,18 @@ impl CodeBlock { is_class_constructor: false, class_field_initializer_name: None, function_environment_push_location: 0, + #[cfg(feature = "trace")] + trace: std::cell::Cell::new(false), } } + /// Enable or disable instruction tracing to `stdout`. + #[cfg(feature = "trace")] + #[inline] + pub fn set_trace(&self, value: bool) { + self.trace.set(value); + } + /// Read type T from code. /// /// # Safety diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 790a6687b73..2884f8e002a 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -216,7 +216,7 @@ impl Context<'_> { // 1. Run the next instruction. #[cfg(feature = "trace")] - let result = if self.vm.trace { + let result = if self.vm.trace || self.vm.frame().code_block.trace.get() { let mut pc = self.vm.frame().pc; let opcode: Opcode = self .vm diff --git a/docs/boa_object.md b/docs/boa_object.md new file mode 100644 index 00000000000..52c7c843927 --- /dev/null +++ b/docs/boa_object.md @@ -0,0 +1,150 @@ +# Boa Debug Object + +The `$boa` object contains useful utilities that can be used to debug JavaScript in JavaScript. + +It's injected into the context as global variable with the `--debug-object` command-line flag, +the object is separated into modules. + +## Module `$boa.gc` + +This module contains functions that are related the garbage collector. It currently has the `.collect()` method. + +```JavaScript +$boa.gc.collect() +``` + +This force triggers the GC to scan the heap and collect garbage. + +## Module `$boa.function` + +In this module are untility functions related to execution and debugging function. + +### Function `$boa.function.bytecode(func)` + +This function returns the compiled bytecode of a function as a string, + +```JavaScript +>> function add(x, y) { + return x + y +} +>> $boa.function.bytecode(add) +" +------------------------Compiled Output: 'add'------------------------ +Location Count Opcode Operands + +000000 0000 DefInitArg 0000: 'a' +000005 0001 DefInitArg 0001: 'b' +000010 0002 RestParameterPop +000011 0003 GetName 0000: 'a' +000016 0004 GetName 0001: 'b' +000021 0005 Add +000022 0006 Return +000023 0007 PushUndefined +000024 0008 Return + +Literals: + + +Bindings: + 0000: a + 0001: b + +Functions: + +" +>> +``` + +### Function `$boa.function.trace(func, this, ...args)` + +It only traces the specified function. If the specified function calls other functions, +their instructions aren't traced. + +```JavaScript +>> const add = (a, b) => a + b +>> $boa.function.trace(add, undefined, 1, 2) +5μs DefInitArg 0000: 'a' 2 +4μs DefInitArg 0001: 'b' +0μs RestParameterPop +3μs GetName 0000: 'a' 1 +1μs GetName 0001: 'b' 2 +2μs Add 3 +1μs Return 3 +3 +>> +``` + +The `this` value can be changed as well as the arguments that are passed to the function. + +## Function `$boa.function.flowgraph(func, options)` + +It can be used to get the instruction flowgraph, like the command-line flag. +This works on the function level, allows getting the flow graph without +quiting the boa shell and adding the specified flags. + +Besides the function it also takes an argument that, can be a string or an object. +If it is a string it represets the flowgraph format, otherwire if it's an object: + +```JavaScript +// These are the defaults, if not specified. +{ + format: 'mermaid' + direction: 'LeftRight' // or 'LR' shorthand. +} +``` + +Example: + +```JavaScript +$boa.function.flowgraph(func, 'graphviz') +$boa.function.flowgraph(func, { format: 'mermaid', direction: 'TopBottom' }) +``` + +## Module `$boa.object` + +Contains utility functions for getting internal information about an object. + +## Function `$boa.object.id(object)` + +This function returns memory address of the given object, as a string. + +Example: + +```JavaScript +let o = { x: 10, y: 20 } +$boa.object.id(o) // '0x7F5B3251B718' + +// Geting the address of the $boa object in memory +$boa.object.id($boa) // '0x7F5B3251B5D8' +``` + +## Module `$boa.optimizer` + +This modules contains getters and setters for enabling and disabling optimizations. + +### Getter & Setter `$boa.optimizer.constantFolding` + +This is and accessor property on the module, its getter returns `true` if enabled or `false` otherwise. +Its setter can be used to enable/disable the constant folding optimization. + +```JavaScript +$boa.optimizer.constantFolding = true +$boa.optimizer.constantFolding // true +``` + +### Getter & Setter `$boa.optimizer.statistics` + +This is and accessor property on the module, its getter returns `true` if enabled or `false` otherwise. +Its setter can be used to enable/disable optimization statistics, which are printed to `stdout`. + +```JavaScript +>> $boa.optimizer.constantFolding = true +>> $boa.optimizer.statistics = true +>> 1 + 1 +Optimizer { + constant folding: 1 run(s), 2 pass(es) (1 mutating, 1 checking) +} + +2 +>> +``` diff --git a/docs/debugging.md b/docs/debugging.md index e6135bbcaf4..a0e53827888 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -82,6 +82,21 @@ for example `--flowgraph-direction=left-to-right`, the default is `top-to-bottom [gihub-mermaid]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams [graphviz]: https://graphviz.org/ +## Debugging through the debug object $boa + +Certain debugging actions in JavaScript land are difficult to impossible, like triggering a GC collect. + +For such puroposes we have the `$boa` object that contains useful utilities that can be used to debug JavaScript in JavaScript. +The debug object becomes available with the `--debug-object` cli flag, It injects the `$boa` debug object in the context as global variable, +the object is separated into modules `gc`, `function`, `object`, etc. + +We can now do `$boa.gc.collect()`, which force triggers a GC collect. + +If you want to trace only a particular function (without being flodded by the `--trace` flag, that traces everything), +for that we have the `$boa.function.trace(func, this, ...args)`. + +The full documentation of the `$boa` object's modules and functionalities can be found [`here`](./boa_object.md). + ## Compiler panics In the case of a compiler panic, to get a full backtrace you will need to set