oxc_cfg/
lib.rs

1mod block;
2mod builder;
3pub mod dot;
4pub mod visit;
5
6use std::fmt;
7
8use itertools::Itertools;
9use oxc_index::{IndexVec, define_nonmax_u32_index_type};
10use petgraph::{
11    Direction,
12    visit::{Control, DfsEvent, EdgeRef},
13};
14
15pub mod graph {
16    pub use petgraph::*;
17    pub mod visit {
18        pub use petgraph::visit::*;
19
20        pub use super::super::visit::*;
21    }
22}
23
24pub use block::*;
25pub use builder::{ControlFlowGraphBuilder, CtxCursor, CtxFlags};
26pub use dot::DisplayDot;
27use visit::set_depth_first_search;
28
29pub type BlockNodeId = petgraph::stable_graph::NodeIndex;
30
31define_nonmax_u32_index_type! {
32    pub struct BasicBlockId;
33}
34
35impl PartialEq<u32> for BasicBlockId {
36    #[inline]
37    fn eq(&self, other: &u32) -> bool {
38        self.0.get() == *other
39    }
40}
41
42impl fmt::Display for BasicBlockId {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        self.0.fmt(f)
45    }
46}
47
48pub type Graph = petgraph::graph::DiGraph<BasicBlockId, EdgeType>;
49
50#[derive(Debug, Clone)]
51pub enum EdgeType {
52    /// Conditional jumps
53    Jump,
54    /// Normal control flow path
55    Normal,
56    /// Cyclic aka loops
57    Backedge,
58    /// Marks start of a function subgraph
59    NewFunction,
60    /// Finally
61    Finalize,
62    /// Error Path
63    Error(ErrorEdgeKind),
64
65    // misc edges
66    Unreachable,
67    /// Used to mark the end of a finalizer. It is an experimental approach might
68    /// move to it's respective edge kind enum or get removed altogether.
69    Join,
70}
71
72#[derive(Default, Debug, Clone, Copy)]
73pub enum ErrorEdgeKind {
74    /// Error kind for edges between a block which can throw, to it's respective catch block.
75    Explicit,
76    /// Any block that can throw would have an implicit error block connected using this kind.
77    #[default]
78    Implicit,
79}
80
81pub enum EvalConstConditionResult {
82    NotFound,
83    Fail,
84    Eval(bool),
85}
86
87#[derive(Debug)]
88pub struct ControlFlowGraph {
89    pub graph: Graph,
90    pub basic_blocks: IndexVec<BasicBlockId, BasicBlock>,
91}
92
93impl ControlFlowGraph {
94    pub fn graph(&self) -> &Graph {
95        &self.graph
96    }
97
98    /// # Panics
99    pub fn basic_block(&self, id: BlockNodeId) -> &BasicBlock {
100        let ix = *self.graph.node_weight(id).expect("expected a valid node id in self.graph");
101        self.basic_blocks.get(ix).expect("expected a valid node id in self.basic_blocks")
102    }
103
104    /// # Panics
105    pub fn basic_block_mut(&mut self, id: BlockNodeId) -> &mut BasicBlock {
106        let ix = *self.graph.node_weight(id).expect("expected a valid node id in self.graph");
107        self.basic_blocks.get_mut(ix).expect("expected a valid node id in self.basic_blocks")
108    }
109
110    pub fn is_reachable(&self, from: BlockNodeId, to: BlockNodeId) -> bool {
111        self.is_reachable_filtered(from, to, |_| Control::Continue)
112    }
113
114    pub fn is_reachable_filtered<F: Fn(BlockNodeId) -> Control<bool>>(
115        &self,
116        from: BlockNodeId,
117        to: BlockNodeId,
118        filter: F,
119    ) -> bool {
120        if from == to {
121            return true;
122        }
123        let graph = &self.graph;
124        set_depth_first_search(&self.graph, Some(from), |event| match event {
125            DfsEvent::TreeEdge(a, b) => {
126                let filter_result = filter(a);
127                if !matches!(filter_result, Control::Continue) {
128                    return filter_result;
129                }
130                let unreachable = !graph.edges_connecting(a, b).any(|edge| {
131                    !matches!(edge.weight(), EdgeType::NewFunction | EdgeType::Unreachable)
132                });
133
134                if unreachable {
135                    Control::Prune
136                } else if b == to {
137                    Control::Break(true)
138                } else {
139                    Control::Continue
140                }
141            }
142            _ => Control::Continue,
143        })
144        .break_value()
145        .unwrap_or(false)
146    }
147
148    /// Returns `None` the given node isn't the cyclic point of an infinite loop.
149    /// Otherwise returns `Some(loop_start, loop_end)`.
150    ///
151    /// # Panics
152    ///
153    /// * There should only be one backedge to each basic block.
154    pub fn is_infinite_loop_start<F>(
155        &self,
156        node: BlockNodeId,
157        try_eval_const_condition: F,
158    ) -> Option<(BlockNodeId, BlockNodeId)>
159    where
160        F: Fn(&Instruction) -> EvalConstConditionResult,
161    {
162        fn get_jump_target(graph: &Graph, node: BlockNodeId) -> Option<BlockNodeId> {
163            graph
164                .edges_directed(node, Direction::Outgoing)
165                .find_or_first(|e| matches!(e.weight(), EdgeType::Jump))
166                .map(|it| it.target())
167        }
168
169        let basic_block = self.basic_block(node);
170        let mut backedges = self
171            .graph
172            .edges_directed(node, Direction::Incoming)
173            .filter(|e| matches!(e.weight(), EdgeType::Backedge));
174
175        // if this node doesn't have an backedge it isn't a loop starting point.
176        let backedge = backedges.next()?;
177
178        assert!(
179            backedges.next().is_none(),
180            "there should only be one backedge to each basic block."
181        );
182
183        // if instructions are empty we might be in a `for(;;)`.
184        if basic_block.instructions().is_empty()
185            && !self
186                .graph
187                .edges_directed(node, Direction::Outgoing)
188                .any(|e| matches!(e.weight(), EdgeType::Backedge))
189        {
190            return get_jump_target(&self.graph, node).map(|it| (it, node));
191        }
192
193        // if there are more than one instruction in this block it can't be a valid loop start.
194        let Ok(only_instruction) = basic_block.instructions().iter().exactly_one() else {
195            return None;
196        };
197
198        // if there is exactly one and it is a condition instruction we are in a loop so we
199        // check the condition to infer if it is always true.
200        if matches!(
201            try_eval_const_condition(only_instruction),
202            EvalConstConditionResult::Eval(true)
203        ) {
204            get_jump_target(&self.graph, node).map(|it| (it, node))
205        } else if matches!(
206            self.basic_block(backedge.source())
207                .instructions()
208                .iter()
209                .exactly_one()
210                .map_or_else(|_| EvalConstConditionResult::NotFound, try_eval_const_condition),
211            EvalConstConditionResult::Eval(true)
212        ) {
213            get_jump_target(&self.graph, node).map(|it| (node, it))
214        } else {
215            None
216        }
217    }
218
219    pub fn is_cyclic(&self, node: BlockNodeId) -> bool {
220        set_depth_first_search(&self.graph, Some(node), |event| match event {
221            DfsEvent::BackEdge(_, id) if id == node => Err(()),
222            _ => Ok(()),
223        })
224        .is_err()
225    }
226}