Skip to content

Commit

Permalink
Implement instruction flowgraph generator (#2422)
Browse files Browse the repository at this point in the history
This PR is a WIP implementation of a vm instruction flowgraph generator

This aims to make the vm easier to debug and understand for both newcomers and experienced devs.

For example if we have the following code:
```js
let i = 0;
while (i < 10) {
    if (i == 3) {
        break;
    }
    i++;
}
```
It generates the following instructions (which is hard to read, especially jumps):
<details>

```
----------------------Compiled Output: '<main>'-----------------------
Location  Count   Opcode                     Operands

000000    0000    PushZero
000001    0001    DefInitLet                 0000: 'i'
000006    0002    LoopStart
000007    0003    LoopContinue
000008    0004    GetName                    0000: 'i'
000013    0005    PushInt8                   10
000015    0006    LessThan
000016    0007    JumpIfFalse                78
000021    0008    PushDeclarativeEnvironment 0, 1
000030    0009    GetName                    0000: 'i'
000035    0010    PushInt8                   3
000037    0011    Eq
000038    0012    JumpIfFalse                58
000043    0013    PushDeclarativeEnvironment 0, 0
000052    0014    Jump                       78
000057    0015    PopEnvironment
000058    0016    GetName                    0000: 'i'
000063    0017    IncPost
000064    0018    RotateRight                2
000066    0019    SetName                    0000: 'i'
000071    0020    Pop
000072    0021    PopEnvironment
000073    0022    Jump                       7
000078    0023    LoopEnd

Literals:
    <empty>

Bindings:
    0000: i

Functions:
    <empty>
```

</details>

And the flow graph is generated:
![flowgraph](https://user-images.githubusercontent.com/8566042/200589387-40b36ad7-d2f2-4918-a3e4-5a8fa5eee89b.png)

The beginning of the function is  marked by the `start` node (in green) and end (in red). In branching the "yes" branch is marked  in green and "no" in red.

~~This only generates in [graphviz format](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) (a widely used format) but it would be nice to also generate to a format that `mermaid.js` can understand and that could be put in articles boa-dev/boa-dev.github.io#26

TODO:
  - [x] Generate graphviz format
  - [x] Generate mermaid format
  - [x] Programmatically generate colors push and pop env instructions
  - [x] Display nested functions in sub-sub-graphs.
  - [x] Put under a feature (`"flowgraph"`)
  - [x] Handle try/catch, switch instructions
  - [x] CLI option for configuring direction of flow (by default it is top down)
  - [x] Handle `Throw` instruction (requires keeping track of try blocks)
  - [x] Documentation
  - [x] Prevent node name collisions (functions with the same name)
  • Loading branch information
HalidOdat committed Nov 22, 2022
1 parent 1ae4844 commit a2964e6
Show file tree
Hide file tree
Showing 12 changed files with 1,452 additions and 5 deletions.
2 changes: 1 addition & 1 deletion boa_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
98 changes: 95 additions & 3 deletions boa_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Option<DumpFormat>>,

/// 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<Option<FlowgraphFormat>>,

/// Specifies the direction of the flowgraph. Default is TopToBottom.
#[arg(
long,
value_name = "FORMAT",
ignore_case = true,
value_enum,
requires = "graph"
)]
flowgraph_direction: Option<FlowgraphDirection>,
}

impl Opt {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -178,6 +225,31 @@ where
Ok(())
}

fn generate_flowgraph(
context: &mut Context,
src: &[u8],
format: FlowgraphFormat,
direction: Option<FlowgraphDirection>,
) -> JsResult<String> {
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();

Expand All @@ -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()),
Expand Down Expand Up @@ -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()),
Expand Down
3 changes: 3 additions & 0 deletions boa_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand Down
81 changes: 81 additions & 0 deletions boa_engine/src/vm/flowgraph/color.rs
Original file line number Diff line number Diff line change
@@ -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}"),
}
}
}
65 changes: 65 additions & 0 deletions boa_engine/src/vm/flowgraph/edge.rs
Original file line number Diff line number Diff line change
@@ -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<Box<str>>,
/// 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<Box<str>>,
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_;
}
}
Loading

0 comments on commit a2964e6

Please sign in to comment.