diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index e935099884..97966ca111 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] -boa_engine = { workspace = true, features = ["deser", "console"] } +boa_engine = { workspace = true, features = ["deser", "console", "flowgraph"] } boa_ast = { workspace = true, features = ["serde"]} boa_parser.workspace = true rustyline = "10.0.0" diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 928c4ca752..57f3cf3860 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -61,7 +61,10 @@ mod helper; use boa_ast::StatementList; -use boa_engine::Context; +use boa_engine::{ + vm::flowgraph::{Direction, Graph}, + Context, JsResult, +}; use clap::{Parser, ValueEnum, ValueHint}; use colored::{Color, Colorize}; use rustyline::{config::Config, error::ReadlineError, EditMode, Editor}; @@ -95,18 +98,40 @@ struct Opt { short = 'a', value_name = "FORMAT", ignore_case = true, - value_enum + value_enum, + conflicts_with = "graph" )] #[allow(clippy::option_option)] dump_ast: Option>, /// Dump the AST to stdout with the given format. - #[arg(long, short)] + #[arg(long, short, conflicts_with = "graph")] trace: bool, /// Use vi mode in the REPL #[arg(long = "vi")] vi_mode: bool, + + /// Generate instruction flowgraph. Default is Graphviz. + #[arg( + long, + value_name = "FORMAT", + ignore_case = true, + value_enum, + group = "graph" + )] + #[allow(clippy::option_option)] + flowgraph: Option>, + + /// Specifies the direction of the flowgraph. Default is TopToBottom. + #[arg( + long, + value_name = "FORMAT", + ignore_case = true, + value_enum, + requires = "graph" + )] + flowgraph_direction: Option, } impl Opt { @@ -136,6 +161,28 @@ enum DumpFormat { JsonPretty, } +/// Represents the format of the instruction flowgraph. +#[derive(Debug, Clone, Copy, ValueEnum)] +enum FlowgraphFormat { + /// Generates in [graphviz][graphviz] format. + /// + /// [graphviz]: https://graphviz.org/ + Graphviz, + /// Generates in [mermaid][mermaid] format. + /// + /// [mermaid]: https://mermaid-js.github.io/mermaid/#/ + Mermaid, +} + +/// Represents the direction of the instruction flowgraph. +#[derive(Debug, Clone, Copy, ValueEnum)] +enum FlowgraphDirection { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft, +} + /// Parses the the token stream into an AST and returns it. /// /// Returns a error of type String with a message, @@ -178,6 +225,31 @@ where Ok(()) } +fn generate_flowgraph( + context: &mut Context, + src: &[u8], + format: FlowgraphFormat, + direction: Option, +) -> JsResult { + let ast = context.parse(src)?; + let code = context.compile(&ast)?; + + let direction = match direction { + Some(FlowgraphDirection::TopToBottom) | None => Direction::TopToBottom, + Some(FlowgraphDirection::BottomToTop) => Direction::BottomToTop, + Some(FlowgraphDirection::LeftToRight) => Direction::LeftToRight, + Some(FlowgraphDirection::RightToLeft) => Direction::RightToLeft, + }; + + 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(result) +} + fn main() -> Result<(), io::Error> { let args = Opt::parse(); @@ -193,6 +265,16 @@ fn main() -> Result<(), io::Error> { if let Err(e) = dump(&buffer, &args, &mut context) { eprintln!("{e}"); } + } else if let Some(flowgraph) = args.flowgraph { + match generate_flowgraph( + &mut context, + &buffer, + flowgraph.unwrap_or(FlowgraphFormat::Graphviz), + args.flowgraph_direction, + ) { + Ok(v) => println!("{}", v), + Err(v) => eprintln!("Uncaught {v}"), + } } else { match context.eval(&buffer) { Ok(v) => println!("{}", v.display()), @@ -239,6 +321,16 @@ fn main() -> Result<(), io::Error> { if let Err(e) = dump(&line, &args, &mut context) { eprintln!("{e}"); } + } else if let Some(flowgraph) = args.flowgraph { + match generate_flowgraph( + &mut context, + line.trim_end().as_bytes(), + flowgraph.unwrap_or(FlowgraphFormat::Graphviz), + args.flowgraph_direction, + ) { + Ok(v) => println!("{}", v), + Err(v) => eprintln!("Uncaught {v}"), + } } else { match context.eval(line.trim_end()) { Ok(v) => println!("{}", v.display()), diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 81f9957e1a..4230391da5 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -26,6 +26,9 @@ intl = [ fuzz = ["boa_ast/fuzz", "boa_interner/fuzz"] +# Enable Boa's VM instruction flowgraph generator. +flowgraph = [] + # Enable Boa's WHATWG console object implementation. console = [] diff --git a/boa_engine/src/vm/flowgraph/color.rs b/boa_engine/src/vm/flowgraph/color.rs new file mode 100644 index 0000000000..5346d73bd0 --- /dev/null +++ b/boa_engine/src/vm/flowgraph/color.rs @@ -0,0 +1,81 @@ +use std::fmt::Display; + +/// Represents the color of a node or edge. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Color { + /// Represents the default color. + None, + /// Represents the color red. + Red, + /// Represents the color green. + Green, + /// Represents the color blue. + Blue, + /// Represents the color yellow. + Yellow, + /// Represents the color purple. + Purple, + /// Represents a RGB color. + Rgb { r: u8, g: u8, b: u8 }, +} + +impl Color { + /// Function for converting HSV to RGB color format. + #[allow(clippy::many_single_char_names)] + #[inline] + pub fn hsv_to_rgb(h: f64, s: f64, v: f64) -> Self { + let h_i = (h * 6.0) as i64; + let f = h * 6.0 - h_i as f64; + let p = v * (1.0 - s); + let q = v * (1.0 - f * s); + let t = v * (1.0 - (1.0 - f) * s); + + let (r, g, b) = match h_i { + 0 => (v, t, p), + 1 => (q, v, p), + 2 => (p, v, t), + 3 => (p, q, v), + 4 => (t, p, v), + 5 => (v, p, q), + _ => unreachable!(), + }; + + let r = (r * 256.0) as u8; + let g = (g * 256.0) as u8; + let b = (b * 256.0) as u8; + + Self::Rgb { r, g, b } + } + + /// This funcition takes a random value and converts it to + /// a pleasant to look at RGB color. + #[inline] + pub fn from_random_number(mut random: f64) -> Self { + const GOLDEN_RATIO_CONJUGATE: f64 = 0.618033988749895; + random += GOLDEN_RATIO_CONJUGATE; + random %= 1.0; + + Self::hsv_to_rgb(random, 0.7, 0.95) + } + + /// Check if the color is [`Self::None`]. + #[inline] + pub fn is_none(&self) -> bool { + *self == Self::None + } +} + +impl Display for Color { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Color::None => f.write_str(""), + Color::Red => f.write_str("red"), + Color::Green => f.write_str("green"), + Color::Blue => f.write_str("blue"), + Color::Yellow => f.write_str("yellow"), + Color::Purple => f.write_str("purple"), + Color::Rgb { r, g, b } => write!(f, "#{r:02X}{b:02X}{g:02X}"), + } + } +} diff --git a/boa_engine/src/vm/flowgraph/edge.rs b/boa_engine/src/vm/flowgraph/edge.rs new file mode 100644 index 0000000000..aeeb7e06d0 --- /dev/null +++ b/boa_engine/src/vm/flowgraph/edge.rs @@ -0,0 +1,65 @@ +use crate::vm::flowgraph::Color; + +/// Represents the edge (connection) style. +#[derive(Debug, Clone, Copy)] +pub enum EdgeStyle { + /// Represents a solid line. + Line, + /// Represents a dotted line. + Dotted, + /// Represents a dashed line. + Dashed, +} + +/// Represents the edge type. +#[derive(Debug, Clone, Copy)] +pub enum EdgeType { + /// Represents no decoration on the edge line. + None, + /// Represents arrow edge type. + Arrow, +} + +/// Represents an edge/connection in the flowgraph. +#[derive(Debug, Clone)] +pub struct Edge { + /// The location of the source node. + pub(super) from: usize, + /// The location of the destination node. + pub(super) to: usize, + /// The label on top of the edge. + pub(super) label: Option>, + /// The color of the line. + pub(super) color: Color, + /// The style of the line. + pub(super) style: EdgeStyle, + /// The type of the line. + pub(super) type_: EdgeType, +} + +impl Edge { + /// Construct a new edge. + #[inline] + pub(super) fn new( + from: usize, + to: usize, + label: Option>, + color: Color, + style: EdgeStyle, + ) -> Self { + Self { + from, + to, + label, + color, + style, + type_: EdgeType::Arrow, + } + } + + /// Set the type of the edge. + #[inline] + pub fn set_type(&mut self, type_: EdgeType) { + self.type_ = type_; + } +} diff --git a/boa_engine/src/vm/flowgraph/graph.rs b/boa_engine/src/vm/flowgraph/graph.rs new file mode 100644 index 0000000000..67b90fa2bc --- /dev/null +++ b/boa_engine/src/vm/flowgraph/graph.rs @@ -0,0 +1,303 @@ +use crate::vm::flowgraph::{Color, Edge, EdgeStyle, EdgeType, Node, NodeShape}; + +/// This represents the direction of flow in the flowgraph. +#[derive(Debug, Clone, Copy)] +pub enum Direction { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft, +} + +/// Represents a sub-graph in the graph. +/// +/// Sub-graphs can be nested. +#[derive(Debug, Clone)] +pub struct SubGraph { + /// The label on the sub-graph. + label: String, + /// The nodes it contains. + nodes: Vec, + /// The edges/connections in contains. + edges: Vec, + /// The direction of flow in the sub-graph. + direction: Direction, + + /// The sub-graphs this graph contains. + subgraphs: Vec, +} + +impl SubGraph { + /// Construct a new subgraph. + #[inline] + fn new(label: String) -> Self { + Self { + label, + nodes: Vec::default(), + edges: Vec::default(), + direction: Direction::TopToBottom, + subgraphs: Vec::default(), + } + } + + /// Set the label of the subgraph. + #[inline] + pub fn set_label(&mut self, label: String) { + self.label = label; + } + + /// Set the direction of the subgraph. + #[inline] + pub fn set_direction(&mut self, direction: Direction) { + self.direction = direction; + } + + /// Add a node to the subgraph. + #[inline] + pub fn add_node(&mut self, location: usize, shape: NodeShape, label: Box, color: Color) { + let node = Node::new(location, shape, label, color); + self.nodes.push(node); + } + + /// Add an edge to the subgraph. + #[inline] + pub fn add_edge( + &mut self, + from: usize, + to: usize, + label: Option>, + color: Color, + style: EdgeStyle, + ) -> &mut Edge { + let edge = Edge::new(from, to, label, color, style); + self.edges.push(edge); + self.edges.last_mut().expect("Already pushed edge") + } + + /// Create a subgraph in this subgraph. + #[inline] + pub fn subgraph(&mut self, label: String) -> &mut SubGraph { + self.subgraphs.push(SubGraph::new(label)); + let result = self + .subgraphs + .last_mut() + .expect("We just pushed a subgraph"); + result.set_direction(self.direction); + result + } + + /// Format into the graphviz format. + #[inline] + fn graphviz_format(&self, result: &mut String, prefix: &str) { + result.push_str(&format!("\tsubgraph cluster_{prefix}_{} {{\n", self.label)); + result.push_str("\t\tstyle = filled;\n"); + result.push_str(&format!( + "\t\tlabel = \"{}\";\n", + if self.label.is_empty() { + "Anonymous Function" + } else { + self.label.as_ref() + } + )); + + result.push_str(&format!( + "\t\t{prefix}_{}_start [label=\"Start\",shape=Mdiamond,style=filled,color=green]\n", + self.label + )); + if !self.nodes.is_empty() { + result.push_str(&format!( + "\t\t{prefix}_{}_start -> {prefix}_{}_i_0\n", + self.label, self.label + )); + } + + for node in &self.nodes { + let shape = match node.shape { + NodeShape::None => "", + NodeShape::Record => ", shape=record", + NodeShape::Diamond => ", shape=diamond", + }; + let color = format!(",style=filled,color=\"{}\"", node.color); + result.push_str(&format!( + "\t\t{prefix}_{}_i_{}[label=\"{:04}: {}\"{shape}{color}];\n", + self.label, node.location, node.location, node.label + )); + } + + for edge in &self.edges { + let color = format!(",color=\"{}\"", edge.color); + let style = match (edge.style, edge.type_) { + (EdgeStyle::Line, EdgeType::None) => ",dir=none", + (EdgeStyle::Line, EdgeType::Arrow) => "", + (EdgeStyle::Dotted, EdgeType::None) => ",style=dotted,dir=none", + (EdgeStyle::Dotted, EdgeType::Arrow) => ",style=dotted", + (EdgeStyle::Dashed, EdgeType::None) => ",style=dashed,dir=none", + (EdgeStyle::Dashed, EdgeType::Arrow) => ",style=dashed,", + }; + result.push_str(&format!( + "\t\t{prefix}_{}_i_{} -> {prefix}_{}_i_{} [label=\"{}\", len=f{style}{color}];\n", + self.label, + edge.from, + self.label, + edge.to, + edge.label.as_deref().unwrap_or("") + )); + } + for (index, subgraph) in self.subgraphs.iter().enumerate() { + let prefix = format!("{prefix}_F{index}"); + subgraph.graphviz_format(result, &prefix); + } + result.push_str("\t}\n"); + } + + /// Format into the mermaid format. + #[inline] + fn mermaid_format(&self, result: &mut String, prefix: &str) { + let rankdir = match self.direction { + Direction::TopToBottom => "TB", + Direction::BottomToTop => "BT", + Direction::LeftToRight => "LR", + Direction::RightToLeft => "RL", + }; + result.push_str(&format!( + " subgraph {prefix}_{}[\"{}\"]\n", + self.label, + if self.label.is_empty() { + "Anonymous Function" + } else { + self.label.as_ref() + } + )); + result.push_str(&format!(" direction {}\n", rankdir)); + + result.push_str(&format!(" {prefix}_{}_start{{Start}}\n", self.label)); + result.push_str(&format!( + " style {prefix}_{}_start fill:green\n", + self.label + )); + if !self.nodes.is_empty() { + result.push_str(&format!( + " {prefix}_{}_start --> {prefix}_{}_i_0\n", + self.label, self.label + )); + } + + for node in &self.nodes { + let (shape_begin, shape_end) = match node.shape { + NodeShape::None | NodeShape::Record => ('[', ']'), + NodeShape::Diamond => ('{', '}'), + }; + result.push_str(&format!( + " {prefix}_{}_i_{}{shape_begin}\"{:04}: {}\"{shape_end}\n", + self.label, node.location, node.location, node.label + )); + if !node.color.is_none() { + result.push_str(&format!( + " style {prefix}_{}_i_{} fill:{}\n", + self.label, node.location, node.color + )); + } + } + + for (index, edge) in self.edges.iter().enumerate() { + let style = match (edge.style, edge.type_) { + (EdgeStyle::Line, EdgeType::None) => "---", + (EdgeStyle::Line, EdgeType::Arrow) => "-->", + (EdgeStyle::Dotted | EdgeStyle::Dashed, EdgeType::None) => "-.-", + (EdgeStyle::Dotted | EdgeStyle::Dashed, EdgeType::Arrow) => "-.->", + }; + result.push_str(&format!( + " {prefix}_{}_i_{} {style}| {}| {prefix}_{}_i_{}\n", + self.label, + edge.from, + edge.label.as_deref().unwrap_or(""), + self.label, + edge.to, + )); + + if !edge.color.is_none() { + result.push_str(&format!( + " linkStyle {} stroke:{}, stroke-width: 4px\n", + index + 1, + edge.color + )); + } + } + for (index, subgraph) in self.subgraphs.iter().enumerate() { + let prefix = format!("{prefix}_F{index}"); + subgraph.mermaid_format(result, &prefix); + } + result.push_str(" end\n"); + } +} + +/// This represents the main graph that other [`SubGraph`]s can be nested in. +#[derive(Debug)] +pub struct Graph { + subgraphs: Vec, + direction: Direction, +} + +impl Graph { + /// Construct a [`Graph`] + #[inline] + pub fn new(direction: Direction) -> Self { + Graph { + subgraphs: Vec::default(), + direction, + } + } + + /// Create a [`SubGraph`] in this [`Graph`]. + #[inline] + pub fn subgraph(&mut self, label: String) -> &mut SubGraph { + self.subgraphs.push(SubGraph::new(label)); + let result = self + .subgraphs + .last_mut() + .expect("We just pushed a subgraph"); + result.set_direction(self.direction); + result + } + + /// Output the graph into the graphviz format. + #[inline] + pub fn to_graphviz_format(&self) -> String { + let mut result = String::new(); + result += "digraph {\n"; + result += "\tnode [shape=record];\n"; + + let rankdir = match self.direction { + Direction::TopToBottom => "TB", + Direction::BottomToTop => "BT", + Direction::LeftToRight => "LR", + Direction::RightToLeft => "RL", + }; + result += &format!("\trankdir={rankdir};\n"); + + for subgraph in &self.subgraphs { + subgraph.graphviz_format(&mut result, ""); + } + result += "}\n"; + result + } + + /// Output the graph into the mermaid format. + #[inline] + pub fn to_mermaid_format(&self) -> String { + let mut result = String::new(); + let rankdir = match self.direction { + Direction::TopToBottom => "TD", + Direction::BottomToTop => "DT", + Direction::LeftToRight => "LR", + Direction::RightToLeft => "RL", + }; + result += &format!("graph {}\n", rankdir); + + for subgraph in &self.subgraphs { + subgraph.mermaid_format(&mut result, ""); + } + result += "\n"; + result + } +} diff --git a/boa_engine/src/vm/flowgraph/mod.rs b/boa_engine/src/vm/flowgraph/mod.rs new file mode 100644 index 0000000000..fe0324f40d --- /dev/null +++ b/boa_engine/src/vm/flowgraph/mod.rs @@ -0,0 +1,490 @@ +//! This module is responsible for generating the vm instruction flowgraph. + +use std::mem::size_of; + +use boa_interner::{Interner, Sym}; + +use crate::vm::{CodeBlock, Opcode}; + +mod color; +mod edge; +mod graph; +mod node; + +pub use color::*; +pub use edge::*; +pub use graph::*; +pub use node::*; + +impl CodeBlock { + /// Output the [`CodeBlock`] VM instructions into a [`Graph`]. + #[inline] + pub fn to_graph(&self, interner: &Interner, graph: &mut SubGraph) { + let mut name = interner.resolve_expect(self.name).to_string(); + // Have to remove any invalid graph chars like `<` or `>`. + if self.name == Sym::MAIN { + name = "__main__".to_string(); + } + + graph.set_label(name); + + let mut environments = Vec::new(); + let mut try_entries = Vec::new(); + let mut returns = Vec::new(); + + let mut pc = 0; + while pc < self.code.len() { + let opcode: Opcode = self.code[pc].try_into().expect("invalid opcode"); + let opcode_str = opcode.as_str(); + let previous_pc = pc; + + let mut tmp = pc; + let label = format!( + "{opcode_str} {}", + self.instruction_operands(&mut tmp, interner) + ); + + pc += size_of::(); + match opcode { + Opcode::RotateLeft | Opcode::RotateRight => { + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::PushInt8 => { + pc += size_of::(); + + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::PushInt16 => { + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::PushInt32 => { + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::PushRational => { + pc += size_of::(); + + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::PushLiteral => { + let operand = self.read::(pc); + pc += size_of::(); + let operand_str = self.literals[operand as usize].display().to_string(); + let operand_str = operand_str.escape_debug(); + let label = format!("{opcode_str} {}", operand_str); + + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::Jump => { + let operand = self.read::(pc); + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::Diamond, label.into(), Color::None); + graph.add_edge( + previous_pc, + operand as usize, + None, + Color::None, + EdgeStyle::Line, + ); + } + Opcode::JumpIfFalse + | Opcode::JumpIfNotUndefined + | Opcode::JumpIfNullOrUndefined => { + let operand = self.read::(pc); + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::Diamond, label.into(), Color::None); + graph.add_edge( + previous_pc, + operand as usize, + Some("YES".into()), + Color::Green, + EdgeStyle::Line, + ); + graph.add_edge( + previous_pc, + pc, + Some("NO".into()), + Color::Red, + EdgeStyle::Line, + ); + } + Opcode::LogicalAnd | Opcode::LogicalOr | Opcode::Coalesce => { + let exit = self.read::(pc); + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + graph.add_edge( + previous_pc, + exit as usize, + Some("SHORT CIRCUIT".into()), + Color::Red, + EdgeStyle::Line, + ); + } + Opcode::Case => { + let address = self.read::(pc) as usize; + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge( + previous_pc, + pc, + Some("NO".into()), + Color::Red, + EdgeStyle::Line, + ); + graph.add_edge( + previous_pc, + address, + Some("YES".into()), + Color::Green, + EdgeStyle::Line, + ); + } + Opcode::Default => { + let address = self.read::(pc) as usize; + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, address, None, Color::None, EdgeStyle::Line); + } + Opcode::ForInLoopInitIterator => { + let address = self.read::(pc) as usize; + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + graph.add_edge( + previous_pc, + address, + Some("NULL OR UNDEFINED".into()), + Color::None, + EdgeStyle::Line, + ); + } + Opcode::ForInLoopNext + | Opcode::ForAwaitOfLoopNext + | Opcode::GeneratorNextDelegate => { + let address = self.read::(pc) as usize; + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + graph.add_edge( + previous_pc, + address, + Some("DONE".into()), + Color::None, + EdgeStyle::Line, + ); + } + Opcode::CatchStart + | Opcode::FinallySetJump + | Opcode::CallEval + | Opcode::Call + | Opcode::New + | Opcode::SuperCall + | Opcode::ConcatToString => { + pc += size_of::(); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::TryStart => { + let next_address = self.read::(pc); + pc += size_of::(); + let finally_address = self.read::(pc); + pc += size_of::(); + + try_entries.push(( + previous_pc, + next_address, + if finally_address == 0 { + None + } else { + Some(finally_address) + }, + )); + + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + graph.add_edge( + previous_pc, + next_address as usize, + Some("NEXT".into()), + Color::None, + EdgeStyle::Line, + ); + if finally_address != 0 { + graph.add_edge( + previous_pc, + finally_address as usize, + Some("FINALLY".into()), + Color::None, + EdgeStyle::Line, + ); + } + } + Opcode::CopyDataProperties => { + let operand1 = self.read::(pc); + pc += size_of::(); + let operand2 = self.read::(pc); + pc += size_of::(); + + let label = format!("{opcode_str} {operand1}, {operand2}"); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::PushDeclarativeEnvironment | Opcode::PushFunctionEnvironment => { + let random = rand::random(); + environments.push((previous_pc, random)); + + pc += size_of::(); + pc += size_of::(); + + graph.add_node( + previous_pc, + NodeShape::None, + label.into(), + Color::from_random_number(random), + ); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::PopEnvironment => { + let (environment_push, random) = environments + .pop() + .expect("There should be a push evironment before"); + + let color = Color::from_random_number(random); + graph.add_node(previous_pc, NodeShape::None, label.into(), color); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + graph + .add_edge( + previous_pc, + environment_push, + None, + color, + EdgeStyle::Dotted, + ) + .set_type(EdgeType::None); + } + Opcode::GetArrowFunction + | Opcode::GetAsyncArrowFunction + | Opcode::GetFunction + | Opcode::GetFunctionAsync + | Opcode::GetGenerator + | Opcode::GetGeneratorAsync => { + let operand = self.read::(pc); + let fn_name = interner + .resolve_expect(self.functions[operand as usize].name) + .to_string(); + pc += size_of::(); + let label = format!( + "{opcode_str} '{fn_name}' (length: {})", + self.functions[operand as usize].length + ); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::DefInitArg + | Opcode::DefVar + | Opcode::DefInitVar + | Opcode::DefLet + | Opcode::DefInitLet + | Opcode::DefInitConst + | Opcode::GetName + | Opcode::GetNameOrUndefined + | Opcode::SetName + | Opcode::DeleteName => { + let operand = self.read::(pc); + pc += size_of::(); + let label = format!( + "{opcode_str} '{}'", + interner.resolve_expect(self.bindings[operand as usize].name().sym()), + ); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::GetPropertyByName + | Opcode::SetPropertyByName + | Opcode::DefineOwnPropertyByName + | Opcode::DefineClassMethodByName + | Opcode::SetPropertyGetterByName + | Opcode::DefineClassGetterByName + | Opcode::SetPropertySetterByName + | Opcode::DefineClassSetterByName + | Opcode::AssignPrivateField + | Opcode::SetPrivateField + | Opcode::SetPrivateMethod + | Opcode::SetPrivateSetter + | Opcode::SetPrivateGetter + | Opcode::GetPrivateField + | Opcode::DeletePropertyByName + | Opcode::PushClassFieldPrivate + | Opcode::PushClassPrivateGetter + | Opcode::PushClassPrivateSetter + | Opcode::PushClassPrivateMethod => { + let operand = self.read::(pc); + pc += size_of::(); + let label = format!( + "{opcode_str} '{}'", + interner.resolve_expect(self.names[operand as usize].sym()), + ); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::Throw => { + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + if let Some((_try_pc, next, _finally)) = try_entries.last() { + graph.add_edge( + previous_pc, + *next as usize, + Some("CAUGHT".into()), + Color::None, + EdgeStyle::Line, + ); + } + } + Opcode::Pop + | Opcode::PopIfThrown + | Opcode::Dup + | Opcode::Swap + | Opcode::PushZero + | Opcode::PushOne + | Opcode::PushNaN + | Opcode::PushPositiveInfinity + | Opcode::PushNegativeInfinity + | Opcode::PushNull + | Opcode::PushTrue + | Opcode::PushFalse + | Opcode::PushUndefined + | Opcode::PushEmptyObject + | Opcode::PushClassPrototype + | Opcode::SetClassPrototype + | Opcode::SetHomeObject + | Opcode::Add + | Opcode::Sub + | Opcode::Div + | Opcode::Mul + | Opcode::Mod + | Opcode::Pow + | Opcode::ShiftRight + | Opcode::ShiftLeft + | Opcode::UnsignedShiftRight + | Opcode::BitOr + | Opcode::BitAnd + | Opcode::BitXor + | Opcode::BitNot + | Opcode::In + | Opcode::Eq + | Opcode::StrictEq + | Opcode::NotEq + | Opcode::StrictNotEq + | Opcode::GreaterThan + | Opcode::GreaterThanOrEq + | Opcode::LessThan + | Opcode::LessThanOrEq + | Opcode::InstanceOf + | Opcode::TypeOf + | Opcode::Void + | Opcode::LogicalNot + | Opcode::Pos + | Opcode::Neg + | Opcode::Inc + | Opcode::IncPost + | Opcode::Dec + | Opcode::DecPost + | Opcode::GetPropertyByValue + | Opcode::GetPropertyByValuePush + | Opcode::SetPropertyByValue + | Opcode::DefineOwnPropertyByValue + | Opcode::DefineClassMethodByValue + | Opcode::SetPropertyGetterByValue + | Opcode::DefineClassGetterByValue + | Opcode::SetPropertySetterByValue + | Opcode::DefineClassSetterByValue + | Opcode::DeletePropertyByValue + | Opcode::DeleteSuperThrow + | Opcode::ToPropertyKey + | Opcode::ToBoolean + | Opcode::CatchEnd + | Opcode::CatchEnd2 + | Opcode::FinallyStart + | Opcode::FinallyEnd + | Opcode::This + | Opcode::Super + | Opcode::LoopStart + | Opcode::LoopContinue + | Opcode::LoopEnd + | Opcode::InitIterator + | Opcode::InitIteratorAsync + | Opcode::IteratorNext + | Opcode::IteratorClose + | Opcode::IteratorToArray + | Opcode::RequireObjectCoercible + | Opcode::ValueNotNullOrUndefined + | Opcode::RestParameterInit + | Opcode::RestParameterPop + | Opcode::PushValueToArray + | Opcode::PushElisionToArray + | Opcode::PushIteratorToArray + | Opcode::PushNewArray + | Opcode::PopOnReturnAdd + | Opcode::PopOnReturnSub + | Opcode::Yield + | Opcode::GeneratorNext + | Opcode::AsyncGeneratorNext + | Opcode::PushClassField + | Opcode::SuperCallDerived + | Opcode::Await + | Opcode::PushNewTarget + | Opcode::CallEvalSpread + | Opcode::CallSpread + | Opcode::NewSpread + | Opcode::SuperCallSpread + | Opcode::ForAwaitOfLoopIterate + | Opcode::SetPrototype + | Opcode::Nop => { + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::TryEnd => { + try_entries + .pop() + .expect("there should already be try block"); + + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); + } + Opcode::Return => { + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + if let Some((_try_pc, _next, Some(finally))) = try_entries.last() { + graph.add_edge( + previous_pc, + *finally as usize, + None, + Color::None, + EdgeStyle::Line, + ); + } else { + returns.push(previous_pc); + } + } + } + } + + for ret in returns { + graph.add_edge(ret, pc, None, Color::None, EdgeStyle::Line); + } + + graph.add_node(pc, NodeShape::Diamond, "End".into(), Color::Red); + + for function in &self.functions { + let subgraph = graph.subgraph(String::new()); + function.to_graph(interner, subgraph); + } + } +} diff --git a/boa_engine/src/vm/flowgraph/node.rs b/boa_engine/src/vm/flowgraph/node.rs new file mode 100644 index 0000000000..2f3794eb28 --- /dev/null +++ b/boa_engine/src/vm/flowgraph/node.rs @@ -0,0 +1,38 @@ +use crate::vm::flowgraph::Color; + +/// Reperesents the shape of a node in the flowgraph. +#[derive(Debug, Clone, Copy)] +pub enum NodeShape { + // Represents the default shape used in the graph. + None, + /// Represents a rectangular node shape. + Record, + /// Represents a diamond node shape. + Diamond, +} + +/// This represents a node in the flowgraph. +#[derive(Debug, Clone)] +pub struct Node { + /// The opcode location. + pub(super) location: usize, + /// The shape of the opcode. + pub(super) shape: NodeShape, + /// The label/contents of the node. + pub(super) label: Box, + /// The background color of the node. + pub(super) color: Color, +} + +impl Node { + /// Construct a new node. + #[inline] + pub(super) fn new(location: usize, shape: NodeShape, label: Box, color: Color) -> Self { + Self { + location, + shape, + label, + color, + } + } +} diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 25bd0dd5d6..4f1cc67138 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -17,6 +17,9 @@ mod call_frame; mod code_block; mod opcode; +#[cfg(feature = "flowgraph")] +pub mod flowgraph; + pub use {call_frame::CallFrame, code_block::CodeBlock, opcode::Opcode}; pub(crate) use { diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index 2031d81252..fe3c814e54 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -1033,7 +1033,7 @@ generate_impl! { /// Start of a catch block. /// - /// Operands: + /// Operands: finally_address: `u32` /// /// Stack: **=>** CatchStart, diff --git a/docs/debugging.md b/docs/debugging.md index 65f2dc1c07..e6135bbcaf 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -43,6 +43,45 @@ You can print the bytecode and the executed instructions with the command-line f For more detailed information about the vm and the trace output look [here](./vm.md). +## Instruction flowgraph + +We can to get the vm instructions flowgraph, which is a visual representation of the instruction flow. + +The `Start` (in green) and `End` (in red) node in the graph represents the start and end point of execution. +They are not instructions, just markers. + +The conditional instructions are diamond shaped, with the `"YES"` branch in green and the `"NO"` branch in red. +The push and pop evironment pairs match colors and are connected by a dotted line. + +You can use the `--flowgraph` (or `--flowgraph=mermaid` for [mermaid][mermaid] format) flag which outputs +[graphviz][graphviz] format by default, and pipe it to `dot` (from the `graphviz` package which is installed +on most linux distros by default) or use an online editor like: to +view the graph. + +```bash +cargo run -- test.js --flowgraph | dot -Tpng > test.png +``` + +You can specify the `-Tsvg` to generate a `svg` instead of a `png` file. + +![Graphviz flowgraph](./img/graphviz_flowgraph.svg) + +Mermaid graphs can be displayed on github [natively without third-party programs][gihub-mermaid]. +By using a `mermaid` block as seen below. + +```` +```mermaid +// graph contents here... +``` +```` + +Additionaly you can specify the direction of "flow" by using the `--flowgraph-direction` cli option, +for example `--flowgraph-direction=left-to-right`, the default is `top-to-bottom`. + +[mermaid]: https://mermaid-js.github.io/ +[gihub-mermaid]: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams +[graphviz]: https://graphviz.org/ + ## Compiler panics In the case of a compiler panic, to get a full backtrace you will need to set diff --git a/docs/img/graphviz_flowgraph.svg b/docs/img/graphviz_flowgraph.svg new file mode 100644 index 0000000000..98106fe298 --- /dev/null +++ b/docs/img/graphviz_flowgraph.svg @@ -0,0 +1,333 @@ + + + + + + + + +cluster___main__ + +__main__ + + + +__main___start + + + + + +Start + + + +__main___i_0 + +0000: PushInt8 3 + + + +__main___start->__main___i_0 + + + + + +__main___i_2 + +0002: DefInitLet 'i' + + + +__main___i_0->__main___i_2 + + + + + +__main___i_7 + +0007: GetName 'i' + + + +__main___i_2->__main___i_7 + + + + + +__main___i_12 + +0012: PushInt8 10 + + + +__main___i_7->__main___i_12 + + + + + +__main___i_14 + +0014: LessThan + + + +__main___i_12->__main___i_14 + + + + + +__main___i_15 + +0015: JumpIfFalse 53 + + + +__main___i_14->__main___i_15 + + + + + +__main___i_20 + +0020: PushDeclarativeEnvironment 0, 0 + + + +__main___i_15->__main___i_20 + + +NO + + + +__main___i_53 + +0053: PushDeclarativeEnvironment 0, 1 + + + +__main___i_15->__main___i_53 + + +YES + + + +__main___i_29 + +0029: GetName 'print' + + + +__main___i_20->__main___i_29 + + + + + +__main___i_34 + +0034: PushUndefined + + + +__main___i_29->__main___i_34 + + + + + +__main___i_35 + +0035: Swap + + + +__main___i_34->__main___i_35 + + + + + +__main___i_36 + +0036: GetName 'i' + + + +__main___i_35->__main___i_36 + + + + + +__main___i_41 + +0041: Call 1 + + + +__main___i_36->__main___i_41 + + + + + +__main___i_46 + +0046: Pop + + + +__main___i_41->__main___i_46 + + + + + +__main___i_47 + +0047: PopEnvironment + + + +__main___i_46->__main___i_47 + + + + + +__main___i_47->__main___i_20 + + + + +__main___i_48 + +0048: Jump 78 + + + +__main___i_47->__main___i_48 + + + + + +__main___i_78 + +0078: End + + + +__main___i_48->__main___i_78 + + + + + +__main___i_62 + +0062: GetName 'print' + + + +__main___i_53->__main___i_62 + + + + + +__main___i_67 + +0067: PushUndefined + + + +__main___i_62->__main___i_67 + + + + + +__main___i_68 + +0068: Swap + + + +__main___i_67->__main___i_68 + + + + + +__main___i_69 + +0069: PushInt8 10 + + + +__main___i_68->__main___i_69 + + + + + +__main___i_71 + +0071: Call 1 + + + +__main___i_69->__main___i_71 + + + + + +__main___i_76 + +0076: Pop + + + +__main___i_71->__main___i_76 + + + + + +__main___i_77 + +0077: PopEnvironment + + + +__main___i_76->__main___i_77 + + + + + +__main___i_77->__main___i_53 + + + + +__main___i_77->__main___i_78 + + + + +