Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Implement debug object for CLI #2772

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions boa_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
193 changes: 193 additions & 0 deletions boa_cli/src/debug/function.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions boa_cli/src/debug/gc.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions boa_cli/src/debug/mod.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions boa_cli/src/debug/object.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 82 additions & 0 deletions boa_cli/src/debug/optimizer.rs
Original file line number Diff line number Diff line change
@@ -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()
}
Loading