From 744dd4ef9f511199db27d59c18d4cdfa0b008a02 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Wed, 5 Jun 2024 11:57:22 -0400 Subject: [PATCH 1/2] up(semantics): add method to print dot graphs --- crates/oxc_semantic/examples/cfg.rs | 43 +++-------- crates/oxc_semantic/src/control_flow/dot.rs | 65 +++++++++++++++++ crates/oxc_semantic/src/control_flow/mod.rs | 56 ++++++-------- crates/oxc_semantic/src/lib.rs | 8 +- .../tests/integration/util/mod.rs | 24 +----- crates/oxc_span/src/span.rs | 73 +++++++++++++++++++ 6 files changed, 179 insertions(+), 90 deletions(-) create mode 100644 crates/oxc_semantic/src/control_flow/dot.rs diff --git a/crates/oxc_semantic/examples/cfg.rs b/crates/oxc_semantic/examples/cfg.rs index a5442643dfcc1..1b7e48051f2d6 100644 --- a/crates/oxc_semantic/examples/cfg.rs +++ b/crates/oxc_semantic/examples/cfg.rs @@ -1,11 +1,10 @@ -use std::{collections::HashMap, env, path::Path, sync::Arc}; +use std::{collections::HashMap, env, fs, io, path::Path, sync::Arc}; use itertools::Itertools; use oxc_allocator::Allocator; use oxc_parser::Parser; -use oxc_semantic::{print_basic_block, SemanticBuilder}; +use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; -use petgraph::dot::{Config, Dot}; // Instruction: // 1. create a `test.js`, @@ -16,7 +15,7 @@ use petgraph::dot::{Config, Dot}; // - CFG blocks (test.cfg.txt) // - CFG graph (test.dot) -fn main() -> std::io::Result<()> { +fn main() -> io::Result<()> { let test_file_name = env::args().nth(1).unwrap_or_else(|| "test.js".to_string()); let ast_file_name = env::args().nth(1).unwrap_or_else(|| "test.ast.txt".to_string()); let cfg_file_name = env::args().nth(1).unwrap_or_else(|| "test.cfg.txt".to_string()); @@ -27,13 +26,13 @@ fn main() -> std::io::Result<()> { let cfg_file_path = Path::new(&cfg_file_name); let dot_file_path = Path::new(&dot_file_name); - let source_text = Arc::new(std::fs::read_to_string(test_file_path)?); + let source_text = Arc::new(fs::read_to_string(test_file_path)?); let allocator = Allocator::default(); let source_type = SourceType::from_path(test_file_path).unwrap(); let ret = Parser::new(&allocator, &source_text, source_type).parse(); let program = allocator.alloc(ret.program); - std::fs::write(ast_file_path, format!("{:#?}", &program))?; + fs::write(ast_file_path, format!("{:#?}", &program))?; println!("Wrote AST to: {}", &ast_file_name); let semantic = SemanticBuilder::new(&source_text, source_type) @@ -64,12 +63,11 @@ fn main() -> std::io::Result<()> { .cfg() .basic_blocks .iter() - .map(print_basic_block) .enumerate() - .map(|(i, it)| { + .map(|(i, basic_block)| { format!( "bb{i}: {{\n{}\n---\n{}\n}}", - it.lines().map(|x| format!("\t{}", x.trim())).join("\n"), + basic_block.iter().map(|el| format!("\t{el}")).join("\n"), ast_nodes_by_block .get(&i) .map(|nodes| { @@ -80,32 +78,11 @@ fn main() -> std::io::Result<()> { }) .join("\n\n"); - std::fs::write(cfg_file_path, basic_blocks_printed)?; + fs::write(cfg_file_path, basic_blocks_printed)?; println!("Wrote CFG blocks to: {}", &cfg_file_name); - let cfg_dot_diagram = format!( - "{:?}", - Dot::with_attr_getters( - &semantic.semantic.cfg().graph, - &[Config::EdgeNoLabel, Config::NodeNoLabel], - &|_graph, edge| format!("label = {:?}", edge.weight()), - &|_graph, node| format!( - "xlabel = {:?}, label = {:?}", - format!( - "bb{} [{}]", - node.1, - print_basic_block(&semantic.semantic.cfg().basic_blocks[*node.1],).trim() - ), - ast_nodes_by_block - .get(node.1) - .map(|nodes| { - nodes.iter().map(|node| format!("{}", node.kind().debug_name())).join("\n") - }) - .unwrap_or_default() - ) - ) - ); - std::fs::write(dot_file_path, cfg_dot_diagram)?; + let cfg_dot_diagram = format!("{:?}", semantic.semantic.cfg().dot()); + fs::write(dot_file_path, cfg_dot_diagram)?; println!("Wrote CFG dot diagram to: {}", &dot_file_name); Ok(()) diff --git a/crates/oxc_semantic/src/control_flow/dot.rs b/crates/oxc_semantic/src/control_flow/dot.rs new file mode 100644 index 0000000000000..15ea07efd12f1 --- /dev/null +++ b/crates/oxc_semantic/src/control_flow/dot.rs @@ -0,0 +1,65 @@ +use petgraph::{dot, stable_graph::NodeIndex}; + +use crate::ControlFlowGraph; + +use std::fmt::{self, Debug, Display}; + +impl ControlFlowGraph { + /// Returns an object that implements [`Display`] for printing a + /// [`GraphViz DOT`] representation of the control flow graph. + /// + /// [`GraphViz DOT`]: https://graphviz.org/doc/info/lang.html + pub fn dot(&self) -> Dot<'_> { + // Exposing our own struct instead of petgraph's allows us to control + // our API surface. + Dot { cfg: self } + } + + pub(crate) fn fmt_dot(&self, f: &mut fmt::Formatter) -> fmt::Result { + const CONFIG: &[dot::Config] = &[dot::Config::EdgeNoLabel, dot::Config::NodeNoLabel]; + + let get_node_attributes = |_graph, node: (NodeIndex, &usize)| { + let block = &self.basic_blocks[*node.1]; + + format!( + "label = \"{}\"", + block.iter().map(|el| format!("{el}")).collect::>().join("\\n") + ) + }; + + let dot = dot::Dot::with_attr_getters( + &self.graph, + CONFIG, + &|_graph, _edge| String::new(), + // todo: We currently do not print edge types into cfg dot diagram + // so they aren't snapshotted, but we could by uncommenting this. + // &|_graph, edge| format!("label = {:?}", edge.weight()), + // &self.node_attributes, + &get_node_attributes, + ); + + dot.fmt(f) + } +} + +/// Helper struct for rendering [`DOT`] diagrams of a [`ControlFlowGraph`]. +/// Returned from [`ControlFlowGraph::dot`]. +/// +/// [`DOT`]: https://graphviz.org/doc/info/lang.html +/// +#[derive(Clone, Copy)] +pub struct Dot<'a> { + cfg: &'a ControlFlowGraph, +} + +impl<'a> Debug for Dot<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.cfg.fmt_dot(f) + } +} + +impl<'a> Display for Dot<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.cfg.fmt_dot(f) + } +} diff --git a/crates/oxc_semantic/src/control_flow/mod.rs b/crates/oxc_semantic/src/control_flow/mod.rs index e92b5d28a630f..3586d8bd9c8ac 100644 --- a/crates/oxc_semantic/src/control_flow/mod.rs +++ b/crates/oxc_semantic/src/control_flow/mod.rs @@ -1,4 +1,7 @@ mod builder; +mod dot; + +use core::fmt; use oxc_span::CompactStr; use oxc_syntax::operator::{ @@ -156,43 +159,30 @@ pub struct PreservedExpressionState { pub store_final_assignments_into_this_array: Vec>, } -#[must_use] -fn print_register(register: Register) -> String { - match ®ister { - Register::Index(i) => format!("${i}"), - Register::Return => "$return".into(), +impl fmt::Display for Register { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Index(i) => write!(f, "${i}"), + Self::Return => write!(f, "$return"), + } } } -#[must_use] -pub fn print_basic_block(basic_block_elements: &Vec) -> String { - let mut output = String::new(); - for basic_block in basic_block_elements { - match basic_block { - BasicBlockElement::Unreachable => output.push_str("Unreachable()\n"), - BasicBlockElement::Throw(reg) => { - output.push_str(&format!("throw {}\n", print_register(*reg))); - } - - BasicBlockElement::Break(Some(reg)) => { - output.push_str(&format!("break {}\n", print_register(*reg))); - } - BasicBlockElement::Break(None) => { - output.push_str("break"); - } - BasicBlockElement::Assignment(to, with) => { - output.push_str(&format!("{} = ", print_register(*to))); - - match with { - AssignmentValue::ImplicitUndefined => { - output.push_str(""); - } - AssignmentValue::NotImplicitUndefined => output.push_str(""), - } - - output.push('\n'); +impl fmt::Display for BasicBlockElement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Unreachable => f.write_str("Unreachable()"), + Self::Throw(reg) => write!(f, "throw {reg}"), + Self::Break(Some(reg)) => write!(f, "break {reg}"), + Self::Break(None) => f.write_str("break"), + Self::Assignment(to, with) => { + // output.push_str(&format!("{} = ", print_register(*to))); + let value = match with { + AssignmentValue::ImplicitUndefined => "", + AssignmentValue::NotImplicitUndefined => "", + }; + write!(f, "{to} = {value}") } } } - output } diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 7f8fcbb9b6f1e..f2c0257bfca55 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -31,10 +31,10 @@ use rustc_hash::FxHashSet; pub use crate::{ control_flow::{ - print_basic_block, AssignmentValue, BasicBlockElement, BasicBlockId, BinaryAssignmentValue, - BinaryOp, CallType, CalleeWithArgumentsAssignmentValue, CollectionAssignmentValue, - ControlFlowGraph, EdgeType, ObjectPropertyAccessAssignmentValue, Register, - UnaryExpressioneAssignmentValue, UpdateAssignmentValue, + AssignmentValue, BasicBlockElement, BasicBlockId, BinaryAssignmentValue, BinaryOp, + CallType, CalleeWithArgumentsAssignmentValue, CollectionAssignmentValue, ControlFlowGraph, + EdgeType, ObjectPropertyAccessAssignmentValue, Register, UnaryExpressioneAssignmentValue, + UpdateAssignmentValue, }, node::{AstNode, AstNodeId, AstNodes}, reference::{Reference, ReferenceFlag, ReferenceId}, diff --git a/crates/oxc_semantic/tests/integration/util/mod.rs b/crates/oxc_semantic/tests/integration/util/mod.rs index 8154e892a353f..43bf82ea714c8 100644 --- a/crates/oxc_semantic/tests/integration/util/mod.rs +++ b/crates/oxc_semantic/tests/integration/util/mod.rs @@ -6,12 +6,11 @@ use std::{path::PathBuf, sync::Arc}; use itertools::Itertools; use oxc_allocator::Allocator; use oxc_diagnostics::{Error, NamedSource, OxcDiagnostic}; -use oxc_semantic::{print_basic_block, Semantic, SemanticBuilder}; +use oxc_semantic::{Semantic, SemanticBuilder}; use oxc_span::SourceType; pub use class_tester::ClassTester; pub use expect::Expect; -use petgraph::dot::{Config, Dot}; pub use symbol_tester::SymbolTester; pub struct SemanticTester<'a> { @@ -111,12 +110,11 @@ impl<'a> SemanticTester<'a> { .cfg() .basic_blocks .iter() - .map(print_basic_block) .enumerate() - .map(|(i, it)| { + .map(|(i, basic_block)| { format!( "bb{i}: {{\n{}\n}}", - it.lines().map(|x| format!("\t{}", x.trim())).join("\n") + basic_block.iter().map(|block_el| format!("\t{block_el}")).join("\n") ) }) .join("\n\n") @@ -124,21 +122,7 @@ impl<'a> SemanticTester<'a> { pub fn cfg_dot_diagram(&self) -> String { let built = self.build(); - format!( - "{:?}", - Dot::with_attr_getters( - &built.cfg().graph, - &[Config::EdgeNoLabel, Config::NodeNoLabel], - &|_graph, _edge| String::new(), - // todo: We currently do not print edge types into cfg dot diagram - // so they aren't snapshotted, but we could by uncommenting this. - // &|_graph, edge| format!("label = {:?}", edge.weight()), - &|_graph, node| format!( - "label = {:?}", - print_basic_block(&built.cfg().basic_blocks[*node.1],).trim() - ) - ) - ) + format!("{:?}", built.cfg().dot()) } /// Tests that a symbol with the given name exists at the top-level scope and provides a diff --git a/crates/oxc_span/src/span.rs b/crates/oxc_span/src/span.rs index 333df42e80db1..eba7c0302d519 100644 --- a/crates/oxc_span/src/span.rs +++ b/crates/oxc_span/src/span.rs @@ -27,21 +27,70 @@ pub struct Span { } impl Span { + /// Create a new [`Span`] from a start and end position. #[inline] pub const fn new(start: u32, end: u32) -> Self { Self { start, end } } + /// Get the number of characters covered by the [`Span`]. + /// + /// # Example + /// ``` + /// use oxc_span::Span; + /// + /// assert_eq!(Span::new(1, 1).size(), 0); + /// assert_eq!(Span::new(0, 5).size(), 5); + /// assert_eq!(Span::new(5, 10).size(), 5); + /// ``` + /// pub fn size(&self) -> u32 { debug_assert!(self.start <= self.end); self.end - self.start } + /// Returns `true` if `self` covers a range of zero length. + /// + /// # Example + /// ``` + /// use oxc_span::Span; + /// + /// assert!(Span::new(0, 0).is_empty()); + /// assert!(Span::new(5, 5).is_empty()); + /// assert!(!Span::new(0, 5).is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + debug_assert!(self.start <= self.end); + self.start == self.end + } + + /// Create a [`Span`] covering the maximum range of two [`Span`]s. + /// + /// # Example + /// ``` + /// use oxc_span::Span; + /// + /// let span1 = Span::new(0, 5); + /// let span2 = Span::new(3, 8); + /// let merged_span = span1.merge(&span2); + /// assert_eq!(merged_span, Span::new(0, 8)); + /// ``` #[must_use] pub fn merge(&self, other: &Self) -> Self { Self::new(self.start.min(other.start), self.end.max(other.end)) } + /// Get a snippet of text from a source string that the [`Span`] covers. + /// + /// # Example + /// ``` + /// use oxc_span::Span; + /// + /// let source = "function add (a, b) { return a + b; }"; + /// let name_span = Span::new(9, 12); + /// let name = name_span.source_text(source); + /// assert_eq!(name_span.size(), name.len() as u32); + /// ``` pub fn source_text<'a>(&self, source_text: &'a str) -> &'a str { &source_text[self.start as usize..self.end as usize] } @@ -69,3 +118,27 @@ impl From for LabeledSpan { pub trait GetSpan { fn span(&self) -> Span; } + +#[cfg(test)] +mod test { + use super::Span; + + #[test] + fn test_eq() { + assert_eq!(Span::new(0, 0), Span::new(0, 0)); + assert_eq!(Span::new(0, 1), Span::new(0, 1)); + assert_ne!(Span::new(0, 0), Span::new(0, 1)); + } + + #[test] + fn test_ordering_less() { + assert!(Span::new(0, 0) < Span::new(0, 1)); + assert!(Span::new(0, 3) < Span::new(2, 5)); + } + + #[test] + fn test_ordering_greater() { + assert!(Span::new(0, 1) > Span::new(0, 0)); + assert!(Span::new(2, 5) > Span::new(0, 3)); + } +} From 6844551a0975f4206bd9cec34425e160272bb357 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Wed, 5 Jun 2024 12:03:34 -0400 Subject: [PATCH 2/2] chore: remove unrelated span changes --- crates/oxc_span/src/span.rs | 73 ------------------------------------- 1 file changed, 73 deletions(-) diff --git a/crates/oxc_span/src/span.rs b/crates/oxc_span/src/span.rs index eba7c0302d519..333df42e80db1 100644 --- a/crates/oxc_span/src/span.rs +++ b/crates/oxc_span/src/span.rs @@ -27,70 +27,21 @@ pub struct Span { } impl Span { - /// Create a new [`Span`] from a start and end position. #[inline] pub const fn new(start: u32, end: u32) -> Self { Self { start, end } } - /// Get the number of characters covered by the [`Span`]. - /// - /// # Example - /// ``` - /// use oxc_span::Span; - /// - /// assert_eq!(Span::new(1, 1).size(), 0); - /// assert_eq!(Span::new(0, 5).size(), 5); - /// assert_eq!(Span::new(5, 10).size(), 5); - /// ``` - /// pub fn size(&self) -> u32 { debug_assert!(self.start <= self.end); self.end - self.start } - /// Returns `true` if `self` covers a range of zero length. - /// - /// # Example - /// ``` - /// use oxc_span::Span; - /// - /// assert!(Span::new(0, 0).is_empty()); - /// assert!(Span::new(5, 5).is_empty()); - /// assert!(!Span::new(0, 5).is_empty()); - /// ``` - pub fn is_empty(&self) -> bool { - debug_assert!(self.start <= self.end); - self.start == self.end - } - - /// Create a [`Span`] covering the maximum range of two [`Span`]s. - /// - /// # Example - /// ``` - /// use oxc_span::Span; - /// - /// let span1 = Span::new(0, 5); - /// let span2 = Span::new(3, 8); - /// let merged_span = span1.merge(&span2); - /// assert_eq!(merged_span, Span::new(0, 8)); - /// ``` #[must_use] pub fn merge(&self, other: &Self) -> Self { Self::new(self.start.min(other.start), self.end.max(other.end)) } - /// Get a snippet of text from a source string that the [`Span`] covers. - /// - /// # Example - /// ``` - /// use oxc_span::Span; - /// - /// let source = "function add (a, b) { return a + b; }"; - /// let name_span = Span::new(9, 12); - /// let name = name_span.source_text(source); - /// assert_eq!(name_span.size(), name.len() as u32); - /// ``` pub fn source_text<'a>(&self, source_text: &'a str) -> &'a str { &source_text[self.start as usize..self.end as usize] } @@ -118,27 +69,3 @@ impl From for LabeledSpan { pub trait GetSpan { fn span(&self) -> Span; } - -#[cfg(test)] -mod test { - use super::Span; - - #[test] - fn test_eq() { - assert_eq!(Span::new(0, 0), Span::new(0, 0)); - assert_eq!(Span::new(0, 1), Span::new(0, 1)); - assert_ne!(Span::new(0, 0), Span::new(0, 1)); - } - - #[test] - fn test_ordering_less() { - assert!(Span::new(0, 0) < Span::new(0, 1)); - assert!(Span::new(0, 3) < Span::new(2, 5)); - } - - #[test] - fn test_ordering_greater() { - assert!(Span::new(0, 1) > Span::new(0, 0)); - assert!(Span::new(2, 5) > Span::new(0, 3)); - } -}