Browse Source

Implement debug object for CLI (#2772)

Currently some debugging stuff in JavaScript land is difficult to impossible, like triggering a GC collect, this is not impossible to do in JavaScript the way I triggered it was by creating a huge amount of object `for (let i = 0; i < 100000; ++i) { ({}) }` but this is cumbersome and not guaranteed to trigger a gc.

This PR implements `--debug-object` flag that injects the `$boa` debug object in the context, the object is separated into modules currently `gc`, `function`, `object`.

We can now do `$boa.gc.collect()`, which force triggers a GC collect.

Or sometimes I wanted a trace (the current solution is great, you can trace stuff like `>>> 1 + 1` but that is also it's limitation), it traces everything, I sometimes have a scenario and just want to trace a single function in that scenario, that's why I added the `$boa.function.trace(func, this, ...args)`  It only traces the function.

```js
>> $boa.function.trace((a, b) => a + b, undefined, 1, 2)

-------------------------Compiled Output: ''--------------------------
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                     

... (cut for brevity) ...
```

It also implements `$boa.function.flowgraph(func, options)`:
```js
$boa.function.flowgraph(func, 'graphviz')
$boa.function.flowgraph(func, { format: 'mermaid', direction: 'TopBottom' })
```

Printing the object pointer:
```js
$boa.object.id({}) // '0x566464F33'
```

It currently implements some functionality which we can grow it with our debugging needs since we are not restricted by a spec we can add whatever we want :)

I was originally going to implement this in #2723 (but the PR is too big), for shapes having functions like:
```js
$boa.shape.type({}) // Shared shape
$boa.shape.id({}) // 0x8578FG355 (objects, shape pointer)
$boa.shape.flowgraph({}) // printing the shape transition chain, like $boa.function.flowgraph
```
Shapes chains are very hard to debug once they are big... so having this type of debugging capability would make it much easier.
pull/2787/head
Haled Odat 2 years ago
parent
commit
570bc85ebe
  1. 2
      Cargo.lock
  2. 2
      boa_cli/Cargo.toml
  3. 193
      boa_cli/src/debug/function.rs
  4. 13
      boa_cli/src/debug/gc.rs
  5. 48
      boa_cli/src/debug/mod.rs
  6. 27
      boa_cli/src/debug/object.rs
  7. 82
      boa_cli/src/debug/optimizer.rs
  8. 10
      boa_cli/src/main.rs
  9. 2
      boa_engine/src/bytecompiler/mod.rs
  10. 27
      boa_engine/src/object/mod.rs
  11. 14
      boa_engine/src/vm/code_block.rs
  12. 2
      boa_engine/src/vm/mod.rs
  13. 150
      docs/boa_object.md
  14. 15
      docs/debugging.md

2
Cargo.lock generated

@ -366,6 +366,8 @@ version = "0.16.0"
dependencies = [
"boa_ast",
"boa_engine",
"boa_gc",
"boa_interner",
"boa_parser",
"clap 4.2.1",
"colored",

2
boa_cli/Cargo.toml

@ -15,6 +15,8 @@ rust-version.workspace = true
boa_engine = { workspace = true, features = ["deser", "console", "flowgraph", "trace", "annex-b"] }
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"

193
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<FlowgraphFormat> {
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<Direction> {
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<JsValue> {
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<JsValue> {
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<JsValue> {
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()
}

13
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<JsValue> {
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()
}

48
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,
);
}

27
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<JsValue> {
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()
}

82
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<JsValue> {
Ok(context
.optimizer_options()
.contains(OptimizerOptions::CONSTANT_FOLDING)
.into())
}
fn set_constant_folding(
_: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
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<JsValue> {
Ok(context
.optimizer_options()
.contains(OptimizerOptions::STATISTICS)
.into())
}
fn set_statistics(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
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()
}

10
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<FlowgraphDirection>,
/// 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);

2
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),
}
}

27
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<K>(
&mut self,
key: K,
get: Option<JsFunction>,
set: Option<JsFunction>,
attribute: Attribute,
) -> &mut Self
where
K: Into<PropertyKey>,
{
// 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 {

14
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<bool>,
}
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

2
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

150
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:
<empty>
Bindings:
0000: a
0001: b
Functions:
<empty>
"
>>
```
### 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' <empty>
0μs RestParameterPop <empty>
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
>>
```

15
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

Loading…
Cancel
Save