refactor(cfg): use IndexVec for storing basic blocks (#6323)

Use an `IndexVec` when storing basic blocks. This makes the link between nodes in `.graph` and elements of `.basic_blocks` more clear. I had to rename `BasicBlockId` to `BlockNodeId` to avoid a name collision. I wasn't sure what else to name the `Idx` type for the basic blocks vec.
This commit is contained in:
DonIsaac 2024-10-07 05:01:08 +00:00 committed by Don Isaac
parent 71ad5d3e3f
commit 40932f79b1
11 changed files with 115 additions and 81 deletions

2
Cargo.lock generated
View file

@ -1519,6 +1519,8 @@ version = "0.30.5"
dependencies = [
"bitflags 2.6.0",
"itertools",
"nonmax",
"oxc_index",
"oxc_syntax",
"petgraph",
"rustc-hash",

View file

@ -21,9 +21,11 @@ test = false
doctest = false
[dependencies]
oxc_index = { workspace = true }
oxc_syntax = { workspace = true }
bitflags = { workspace = true }
itertools = { workspace = true }
nonmax = { workspace = true }
petgraph = { workspace = true }
rustc-hash = { workspace = true }

View file

@ -1,7 +1,4 @@
use oxc_syntax::node::NodeId;
use petgraph::stable_graph::NodeIndex;
pub type BasicBlockId = NodeIndex;
#[derive(Debug, Clone)]
pub struct BasicBlock {

View file

@ -1,5 +1,5 @@
use super::ControlFlowGraphBuilder;
use crate::{BasicBlockId, EdgeType};
use crate::{BlockNodeId, EdgeType};
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -15,9 +15,9 @@ bitflags::bitflags! {
pub(super) struct Ctx<'a> {
flags: CtxFlags,
label: Option<&'a str>,
entries: Vec<(CtxFlags, BasicBlockId)>,
break_jmp: Option<BasicBlockId>,
continue_jmp: Option<BasicBlockId>,
entries: Vec<(CtxFlags, BlockNodeId)>,
break_jmp: Option<BlockNodeId>,
continue_jmp: Option<BlockNodeId>,
}
impl<'a> Ctx<'a> {
@ -29,11 +29,11 @@ impl<'a> Ctx<'a> {
self.label.as_ref().is_some_and(|it| *it == label)
}
fn r#break(&mut self, entry: BasicBlockId) {
fn r#break(&mut self, entry: BlockNodeId) {
self.entries.push((CtxFlags::BREAK, entry));
}
fn r#continue(&mut self, entry: BasicBlockId) {
fn r#continue(&mut self, entry: BlockNodeId) {
self.entries.push((CtxFlags::CONTINUE, entry));
}
}
@ -41,19 +41,19 @@ impl<'a> Ctx<'a> {
pub trait CtxCursor {
#![allow(clippy::return_self_not_must_use)]
/// Marks the break jump position in the current context.
fn mark_break(self, jmp_pos: BasicBlockId) -> Self;
fn mark_break(self, jmp_pos: BlockNodeId) -> Self;
/// Marks the continue jump position in the current context.
fn mark_continue(self, jmp_pos: BasicBlockId) -> Self;
fn mark_continue(self, jmp_pos: BlockNodeId) -> Self;
/// Creates a break entry in the current context.
fn r#break(self, bb: BasicBlockId) -> Self;
fn r#break(self, bb: BlockNodeId) -> Self;
/// Creates a continue entry in the current context.
fn r#continue(self, bb: BasicBlockId) -> Self;
fn r#continue(self, bb: BlockNodeId) -> Self;
}
pub struct QueryCtx<'a, 'c>(&'c mut ControlFlowGraphBuilder<'a>, /* label */ Option<&'a str>);
impl<'a, 'c> CtxCursor for QueryCtx<'a, 'c> {
fn mark_break(self, jmp_pos: BasicBlockId) -> Self {
fn mark_break(self, jmp_pos: BlockNodeId) -> Self {
self.0.in_break_context(self.1, |ctx| {
debug_assert!(ctx.break_jmp.is_none());
ctx.break_jmp = Some(jmp_pos);
@ -61,7 +61,7 @@ impl<'a, 'c> CtxCursor for QueryCtx<'a, 'c> {
self
}
fn mark_continue(self, jmp_pos: BasicBlockId) -> Self {
fn mark_continue(self, jmp_pos: BlockNodeId) -> Self {
self.0.in_continue_context(self.1, |ctx| {
debug_assert!(ctx.continue_jmp.is_none());
ctx.continue_jmp = Some(jmp_pos);
@ -69,14 +69,14 @@ impl<'a, 'c> CtxCursor for QueryCtx<'a, 'c> {
self
}
fn r#break(self, bb: BasicBlockId) -> Self {
fn r#break(self, bb: BlockNodeId) -> Self {
self.0.in_break_context(self.1, |ctx| {
ctx.r#break(bb);
});
self
}
fn r#continue(self, bb: BasicBlockId) -> Self {
fn r#continue(self, bb: BlockNodeId) -> Self {
self.0.in_continue_context(self.1, |ctx| {
ctx.r#continue(bb);
});
@ -192,24 +192,24 @@ impl<'a, 'c> RefCtxCursor<'a, 'c> {
}
impl<'a, 'c> CtxCursor for RefCtxCursor<'a, 'c> {
fn mark_break(self, jmp_pos: BasicBlockId) -> Self {
fn mark_break(self, jmp_pos: BlockNodeId) -> Self {
debug_assert!(self.0.break_jmp.is_none());
self.0.break_jmp = Some(jmp_pos);
self
}
fn mark_continue(self, jmp_pos: BasicBlockId) -> Self {
fn mark_continue(self, jmp_pos: BlockNodeId) -> Self {
debug_assert!(self.0.continue_jmp.is_none());
self.0.continue_jmp = Some(jmp_pos);
self
}
fn r#break(self, bb: BasicBlockId) -> Self {
fn r#break(self, bb: BlockNodeId) -> Self {
self.0.r#break(bb);
self
}
fn r#continue(self, bb: BasicBlockId) -> Self {
fn r#continue(self, bb: BlockNodeId) -> Self {
self.0.r#continue(bb);
self
}

View file

@ -2,28 +2,29 @@ mod context;
use context::Ctx;
pub use context::{CtxCursor, CtxFlags};
use oxc_index::IndexVec;
use oxc_syntax::node::NodeId;
use petgraph::Direction;
use super::{
BasicBlock, BasicBlockId, ControlFlowGraph, EdgeType, ErrorEdgeKind, Graph, Instruction,
BasicBlock, BlockNodeId, ControlFlowGraph, EdgeType, ErrorEdgeKind, Graph, Instruction,
InstructionKind, IterationInstructionKind, LabeledInstruction,
};
use crate::ReturnInstructionKind;
use crate::{BasicBlockId, ReturnInstructionKind};
#[derive(Debug, Default)]
struct ErrorHarness(ErrorEdgeKind, BasicBlockId);
struct ErrorHarness(ErrorEdgeKind, BlockNodeId);
#[derive(Debug, Default)]
pub struct ControlFlowGraphBuilder<'a> {
pub graph: Graph,
pub basic_blocks: Vec<BasicBlock>,
pub current_node_ix: BasicBlockId,
pub basic_blocks: IndexVec<BasicBlockId, BasicBlock>,
pub current_node_ix: BlockNodeId,
ctx_stack: Vec<Ctx<'a>>,
/// Contains the error unwinding path represented as a stack of `ErrorHarness`es
error_path: Vec<ErrorHarness>,
/// Stack of finalizers, the top most element is always the appropriate one for current node.
finalizers: Vec<Option<BasicBlockId>>,
finalizers: Vec<Option<BlockNodeId>>,
}
impl<'a> ControlFlowGraphBuilder<'a> {
@ -36,7 +37,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
}
/// # Panics
pub fn basic_block(&self, basic_block: BasicBlockId) -> &BasicBlock {
pub fn basic_block(&self, basic_block: BlockNodeId) -> &BasicBlock {
let idx = *self
.graph
.node_weight(basic_block)
@ -47,7 +48,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
}
/// # Panics
pub fn basic_block_mut(&mut self, basic_block: BasicBlockId) -> &mut BasicBlock {
pub fn basic_block_mut(&mut self, basic_block: BlockNodeId) -> &mut BasicBlock {
let idx = *self
.graph
.node_weight(basic_block)
@ -57,15 +58,14 @@ impl<'a> ControlFlowGraphBuilder<'a> {
.expect("expected `self.current_node_ix` to be a valid node index in self.graph")
}
pub(self) fn new_basic_block(&mut self) -> BasicBlockId {
pub(self) fn new_basic_block(&mut self) -> BlockNodeId {
// current length would be the index of block we are adding on the next line.
let basic_block_ix = self.basic_blocks.len();
self.basic_blocks.push(BasicBlock::new());
let basic_block_ix = self.basic_blocks.push(BasicBlock::new());
self.graph.add_node(basic_block_ix)
}
#[must_use]
pub fn new_basic_block_function(&mut self) -> BasicBlockId {
pub fn new_basic_block_function(&mut self) -> BlockNodeId {
// we might want to differentiate between function blocks and normal blocks down the road.
self.new_basic_block_normal()
}
@ -73,7 +73,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
/// # Panics
/// if there is no error harness to attach to.
#[must_use]
pub fn new_basic_block_normal(&mut self) -> BasicBlockId {
pub fn new_basic_block_normal(&mut self) -> BlockNodeId {
let graph_ix = self.new_basic_block();
self.current_node_ix = graph_ix;
@ -89,7 +89,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
graph_ix
}
pub fn add_edge(&mut self, a: BasicBlockId, b: BasicBlockId, weight: EdgeType) {
pub fn add_edge(&mut self, a: BlockNodeId, b: BlockNodeId, weight: EdgeType) {
if matches!(weight, EdgeType::NewFunction) {
self.basic_block_mut(b).mark_as_reachable();
} else if matches!(weight, EdgeType::Unreachable) || self.basic_block(a).is_unreachable() {
@ -117,7 +117,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
/// Creates and push a new `BasicBlockId` onto `self.error_path` stack.
/// Returns the `BasicBlockId` of the created error harness block.
pub fn attach_error_harness(&mut self, kind: ErrorEdgeKind) -> BasicBlockId {
pub fn attach_error_harness(&mut self, kind: ErrorEdgeKind) -> BlockNodeId {
let graph_ix = self.new_basic_block();
self.error_path.push(ErrorHarness(kind, graph_ix));
graph_ix
@ -126,7 +126,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
/// # Panics
/// if there is no error harness pushed onto the stack,
/// Or last harness doesn't match the expected `BasicBlockId`.
pub fn release_error_harness(&mut self, expect: BasicBlockId) {
pub fn release_error_harness(&mut self, expect: BlockNodeId) {
let harness = self
.error_path
.pop()
@ -139,7 +139,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
/// Creates and push a new `BasicBlockId` onto `self.finalizers` stack.
/// Returns the `BasicBlockId` of the created finalizer block.
pub fn attach_finalizer(&mut self) -> BasicBlockId {
pub fn attach_finalizer(&mut self) -> BlockNodeId {
let graph_ix = self.new_basic_block();
self.finalizers.push(Some(graph_ix));
graph_ix
@ -156,7 +156,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
/// # Panics
/// if last finalizer doesn't match the expected `BasicBlockId`.
pub fn release_finalizer(&mut self, expect: BasicBlockId) {
pub fn release_finalizer(&mut self, expect: BlockNodeId) {
// return early if there is no finalizer.
let Some(finalizer) = self.finalizers.pop() else { return };
assert_eq!(
@ -166,7 +166,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
);
}
pub fn append_condition_to(&mut self, block: BasicBlockId, node: Option<NodeId>) {
pub fn append_condition_to(&mut self, block: BlockNodeId, node: Option<NodeId>) {
self.push_instruction_to(block, InstructionKind::Condition, node);
}
@ -228,7 +228,7 @@ impl<'a> ControlFlowGraphBuilder<'a> {
#[inline]
pub(self) fn push_instruction_to(
&mut self,
block: BasicBlockId,
block: BlockNodeId,
kind: InstructionKind,
node_id: Option<NodeId>,
) {

View file

@ -3,7 +3,11 @@ mod builder;
pub mod dot;
pub mod visit;
use std::fmt;
use itertools::Itertools;
use nonmax::NonMaxU32;
use oxc_index::{Idx, IndexVec};
use petgraph::{
visit::{Control, DfsEvent, EdgeRef},
Direction,
@ -23,7 +27,37 @@ pub use builder::{ControlFlowGraphBuilder, CtxCursor, CtxFlags};
pub use dot::DisplayDot;
use visit::set_depth_first_search;
pub type Graph = petgraph::graph::DiGraph<usize, EdgeType>;
pub type BlockNodeId = petgraph::stable_graph::NodeIndex;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BasicBlockId(NonMaxU32);
impl Idx for BasicBlockId {
#[allow(clippy::cast_possible_truncation)]
fn from_usize(idx: usize) -> Self {
assert!(idx < u32::MAX as usize);
// SAFETY: We just checked `idx` is valid for `NonMaxU32`
Self(unsafe { NonMaxU32::new_unchecked(idx as u32) })
}
fn index(self) -> usize {
self.0.get() as usize
}
}
impl PartialEq<u32> for BasicBlockId {
#[inline]
fn eq(&self, other: &u32) -> bool {
self.0.get() == *other
}
}
impl fmt::Display for BasicBlockId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
pub type Graph = petgraph::graph::DiGraph<BasicBlockId, EdgeType>;
#[derive(Debug, Clone)]
pub enum EdgeType {
@ -65,7 +99,7 @@ pub enum EvalConstConditionResult {
#[derive(Debug)]
pub struct ControlFlowGraph {
pub graph: Graph,
pub basic_blocks: Vec<BasicBlock>,
pub basic_blocks: IndexVec<BasicBlockId, BasicBlock>,
}
impl ControlFlowGraph {
@ -74,25 +108,25 @@ impl ControlFlowGraph {
}
/// # Panics
pub fn basic_block(&self, id: BasicBlockId) -> &BasicBlock {
pub fn basic_block(&self, id: BlockNodeId) -> &BasicBlock {
let ix = *self.graph.node_weight(id).expect("expected a valid node id in self.graph");
self.basic_blocks.get(ix).expect("expected a valid node id in self.basic_blocks")
}
/// # Panics
pub fn basic_block_mut(&mut self, id: BasicBlockId) -> &mut BasicBlock {
pub fn basic_block_mut(&mut self, id: BlockNodeId) -> &mut BasicBlock {
let ix = *self.graph.node_weight(id).expect("expected a valid node id in self.graph");
self.basic_blocks.get_mut(ix).expect("expected a valid node id in self.basic_blocks")
}
pub fn is_reachable(&self, from: BasicBlockId, to: BasicBlockId) -> bool {
pub fn is_reachable(&self, from: BlockNodeId, to: BlockNodeId) -> bool {
self.is_reachable_filtered(from, to, |_| Control::Continue)
}
pub fn is_reachable_filtered<F: Fn(BasicBlockId) -> Control<bool>>(
pub fn is_reachable_filtered<F: Fn(BlockNodeId) -> Control<bool>>(
&self,
from: BasicBlockId,
to: BasicBlockId,
from: BlockNodeId,
to: BlockNodeId,
filter: F,
) -> bool {
if from == to {
@ -127,13 +161,13 @@ impl ControlFlowGraph {
/// Otherwise returns `Some(loop_start, loop_end)`.
pub fn is_infinite_loop_start<F>(
&self,
node: BasicBlockId,
node: BlockNodeId,
try_eval_const_condition: F,
) -> Option<(BasicBlockId, BasicBlockId)>
) -> Option<(BlockNodeId, BlockNodeId)>
where
F: Fn(&Instruction) -> EvalConstConditionResult,
{
fn get_jump_target(graph: &Graph, node: BasicBlockId) -> Option<BasicBlockId> {
fn get_jump_target(graph: &Graph, node: BlockNodeId) -> Option<BlockNodeId> {
graph
.edges_directed(node, Direction::Outgoing)
.find_or_first(|e| matches!(e.weight(), EdgeType::Jump))
@ -186,7 +220,7 @@ impl ControlFlowGraph {
}
}
pub fn is_cyclic(&self, node: BasicBlockId) -> bool {
pub fn is_cyclic(&self, node: BlockNodeId) -> bool {
set_depth_first_search(&self.graph, Some(node), |event| match event {
DfsEvent::BackEdge(_, id) if id == node => Err(()),
_ => Ok(()),

View file

@ -6,18 +6,18 @@ use petgraph::{
};
use rustc_hash::FxHashSet;
use crate::BasicBlockId;
use crate::BlockNodeId;
/// # Panics
pub fn neighbors_filtered_by_edge_weight<State: Default + Clone, NodeWeight, EdgeWeight, F, G>(
graph: &Graph<NodeWeight, EdgeWeight>,
node: BasicBlockId,
node: BlockNodeId,
edge_filter: &F,
visitor: &mut G,
) -> Vec<State>
where
F: Fn(&EdgeWeight) -> Option<State>,
G: FnMut(&BasicBlockId, State) -> (State, bool),
G: FnMut(&BlockNodeId, State) -> (State, bool),
{
let mut q = vec![];
let mut final_states = vec![];

View file

@ -11,7 +11,7 @@ use oxc_cfg::{
visit::{neighbors_filtered_by_edge_weight, EdgeRef},
Direction,
},
BasicBlockId, EdgeType, ErrorEdgeKind, InstructionKind,
BlockNodeId, EdgeType, ErrorEdgeKind, InstructionKind,
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
@ -274,7 +274,7 @@ impl Rule for NoFallthrough {
return;
};
let fallthroughs: FxHashSet<BasicBlockId> = neighbors_filtered_by_edge_weight(
let fallthroughs: FxHashSet<BlockNodeId> = neighbors_filtered_by_edge_weight(
graph,
switch_id,
&|e| match e {
@ -283,7 +283,7 @@ impl Rule for NoFallthrough {
}
_ => Some(None),
},
&mut |node, last_cond: Option<BasicBlockId>| {
&mut |node, last_cond: Option<BlockNodeId>| {
let node = *node;
if node == switch_id {
@ -448,10 +448,10 @@ fn get_switch_semantic_cases(
node: &AstNode,
switch: &SwitchStatement,
) -> (
Vec<BasicBlockId>,
FxHashMap<BasicBlockId, /* is_empty */ bool>,
/* default */ Option<BasicBlockId>,
/* exit */ Option<BasicBlockId>,
Vec<BlockNodeId>,
FxHashMap<BlockNodeId, /* is_empty */ bool>,
/* default */ Option<BlockNodeId>,
/* exit */ Option<BlockNodeId>,
) {
let cfg = ctx.cfg();
let graph = cfg.graph();

View file

@ -4,7 +4,7 @@ use oxc_ast::{
};
use oxc_cfg::{
graph::visit::{neighbors_filtered_by_edge_weight, EdgeRef},
BasicBlockId, ControlFlowGraph, EdgeType, ErrorEdgeKind,
BlockNodeId, ControlFlowGraph, EdgeType, ErrorEdgeKind,
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
@ -50,7 +50,7 @@ enum DefinitelyCallsThisBeforeSuper {
#[default]
No,
Yes,
Maybe(BasicBlockId),
Maybe(BlockNodeId),
}
impl Rule for NoThisBeforeSuper {
@ -60,9 +60,9 @@ impl Rule for NoThisBeforeSuper {
// first pass -> find super calls and local violations
let mut wanted_nodes = Vec::new();
let mut basic_blocks_with_super_called = FxHashSet::<BasicBlockId>::default();
let mut basic_blocks_with_super_called = FxHashSet::<BlockNodeId>::default();
let mut basic_blocks_with_local_violations =
FxHashMap::<BasicBlockId, Vec<NodeId>>::default();
FxHashMap::<BlockNodeId, Vec<NodeId>>::default();
for node in semantic.nodes() {
match node.kind() {
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_) => {
@ -151,9 +151,9 @@ impl NoThisBeforeSuper {
fn analyze(
cfg: &ControlFlowGraph,
id: BasicBlockId,
basic_blocks_with_super_called: &FxHashSet<BasicBlockId>,
basic_blocks_with_local_violations: &FxHashMap<BasicBlockId, Vec<NodeId>>,
id: BlockNodeId,
basic_blocks_with_super_called: &FxHashSet<BlockNodeId>,
basic_blocks_with_local_violations: &FxHashMap<BlockNodeId, Vec<NodeId>>,
follow_join: bool,
) -> Vec<DefinitelyCallsThisBeforeSuper> {
neighbors_filtered_by_edge_weight(
@ -211,8 +211,8 @@ impl NoThisBeforeSuper {
fn check_for_violation(
cfg: &ControlFlowGraph,
output: Vec<DefinitelyCallsThisBeforeSuper>,
basic_blocks_with_super_called: &FxHashSet<BasicBlockId>,
basic_blocks_with_local_violations: &FxHashMap<BasicBlockId, Vec<NodeId>>,
basic_blocks_with_super_called: &FxHashSet<BlockNodeId>,
basic_blocks_with_local_violations: &FxHashMap<BlockNodeId, Vec<NodeId>>,
) -> bool {
// Deciding whether we definitely call this before super in all
// codepaths is as simple as seeing if any individual codepath

View file

@ -86,10 +86,9 @@ fn main() -> std::io::Result<()> {
let basic_blocks_printed = cfg
.basic_blocks
.iter()
.map(DisplayDot::display_dot)
.enumerate()
.iter_enumerated()
.map(|(i, it)| {
let it = it.display_dot();
format!(
"bb{i}: {{\n{}\n---\n{}\n}}",
it.lines().map(|x| format!("\t{}", x.trim())).join("\n"),

View file

@ -1,5 +1,5 @@
use oxc_ast::AstKind;
use oxc_cfg::BasicBlockId;
use oxc_cfg::BlockNodeId;
use oxc_index::IndexVec;
use oxc_span::GetSpan;
pub use oxc_syntax::node::{NodeFlags, NodeId};
@ -17,7 +17,7 @@ pub struct AstNode<'a> {
scope_id: ScopeId,
/// Associated `BasicBlockId` in CFG (initialized by control_flow)
cfg_id: BasicBlockId,
cfg_id: BlockNodeId,
flags: NodeFlags,
}
@ -27,7 +27,7 @@ impl<'a> AstNode<'a> {
pub(crate) fn new(
kind: AstKind<'a>,
scope_id: ScopeId,
cfg_id: BasicBlockId,
cfg_id: BlockNodeId,
flags: NodeFlags,
id: NodeId,
) -> Self {
@ -44,7 +44,7 @@ impl<'a> AstNode<'a> {
///
/// See [oxc_cfg::ControlFlowGraph] for more information.
#[inline]
pub fn cfg_id(&self) -> BasicBlockId {
pub fn cfg_id(&self) -> BlockNodeId {
self.cfg_id
}
@ -236,7 +236,7 @@ impl<'a> AstNodes<'a> {
kind: AstKind<'a>,
scope_id: ScopeId,
parent_node_id: NodeId,
cfg_id: BasicBlockId,
cfg_id: BlockNodeId,
flags: NodeFlags,
) -> NodeId {
let node_id = self.parent_ids.push(Some(parent_node_id));
@ -250,7 +250,7 @@ impl<'a> AstNodes<'a> {
&mut self,
kind: AstKind<'a>,
scope_id: ScopeId,
cfg_id: BasicBlockId,
cfg_id: BlockNodeId,
flags: NodeFlags,
) -> NodeId {
let node_id = self.parent_ids.push(None);