use rowan::TextRange;
use rowan::ast::AstNode as _;
use crate::ast::kinds::is_trivia;
use crate::ast::{
BinaryExpr, CallExpr, ForExpr, FunctionExpr, HasArgList as _, IfExpr, RepeatExpr, WhileExpr,
};
use crate::syntax::{NodePtr, SyntaxElement, SyntaxKind, SyntaxNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BlockId(u32);
impl BlockId {
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BasicBlock {
pub stmts: Vec<TextRange>,
pub terminator: Terminator,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Terminator {
Goto(BlockId),
Branch {
then_blk: BlockId,
else_blk: BlockId,
},
MultiBranch {
call: NodePtr,
targets: Vec<BlockId>,
},
Return,
Diverge,
Unreachable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ControlFlowGraph {
blocks: Vec<BasicBlock>,
entry: BlockId,
expressions: Vec<(BlockId, ControlFlowGraph)>,
}
impl Default for ControlFlowGraph {
fn default() -> Self {
Self {
blocks: vec![BasicBlock {
stmts: Vec::new(),
terminator: Terminator::Return,
}],
entry: BlockId(0),
expressions: Vec::new(),
}
}
}
impl ControlFlowGraph {
pub fn blocks(&self) -> &[BasicBlock] {
&self.blocks
}
pub fn entry(&self) -> BlockId {
self.entry
}
pub fn block(&self, id: BlockId) -> &BasicBlock {
&self.blocks[id.0 as usize]
}
fn build_region(stmts: &[SyntaxElement]) -> Self {
let mut builder = Builder {
blocks: Vec::new(),
expressions: Vec::new(),
};
let entry = builder.new_block();
if let Some(exit) = builder.lower_seq(stmts, entry, None) {
builder.set_term(exit, Terminator::Return);
}
ControlFlowGraph {
blocks: builder.blocks,
entry,
expressions: builder.expressions,
}
}
pub fn cyclomatic_complexity(&self, counts_switch: &impl Fn(NodePtr) -> bool) -> usize {
1 + self.decisions(counts_switch)
}
fn decisions(&self, counts_switch: &impl Fn(NodePtr) -> bool) -> usize {
let mut seen = vec![false; self.blocks.len()];
let mut pending = vec![self.entry];
let mut score = 0;
while let Some(id) = pending.pop() {
if std::mem::replace(&mut seen[id.index()], true) {
continue;
}
match &self.block(id).terminator {
Terminator::Goto(target) => pending.push(*target),
Terminator::Branch { then_blk, else_blk } => {
score += 1;
pending.extend([*then_blk, *else_blk]);
}
Terminator::MultiBranch { call, targets } => {
if counts_switch(*call) {
score += targets.len().saturating_sub(1);
}
pending.extend(targets.iter().copied());
}
Terminator::Return | Terminator::Diverge | Terminator::Unreachable => {}
}
}
score
+ self
.expressions
.iter()
.filter(|(owner, _)| seen[owner.index()])
.map(|(_, graph)| graph.decisions(counts_switch))
.sum::<usize>()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FileControlFlow {
toplevel: ControlFlowGraph,
functions: Vec<(NodePtr, ControlFlowGraph)>,
}
impl FileControlFlow {
pub fn build(root: &SyntaxNode) -> Self {
let toplevel = ControlFlowGraph::build_region(®ion_statements(root));
let functions = root
.descendants()
.filter_map(FunctionExpr::cast)
.filter_map(|function| {
let body = function.body()?;
let cfg = ControlFlowGraph::build_region(&body_statements(&body));
Some((NodePtr::from_node(function.syntax()), cfg))
})
.collect();
Self {
toplevel,
functions,
}
}
pub fn toplevel(&self) -> &ControlFlowGraph {
&self.toplevel
}
pub fn functions(&self) -> &[(NodePtr, ControlFlowGraph)] {
&self.functions
}
pub fn function(&self, ptr: NodePtr) -> Option<&ControlFlowGraph> {
self.functions
.iter()
.find(|(p, _)| *p == ptr)
.map(|(_, cfg)| cfg)
}
pub fn is_unreachable(&self, range: TextRange) -> bool {
std::iter::once(&self.toplevel)
.chain(self.functions.iter().map(|(_, cfg)| cfg))
.flat_map(ControlFlowGraph::blocks)
.any(|block| {
matches!(block.terminator, Terminator::Unreachable) && block.stmts.contains(&range)
})
}
pub fn render(&self, src: &str) -> String {
let mut out = String::new();
out.push_str("region: <toplevel>\n");
render_region(&self.toplevel, src, &mut out);
for (ptr, cfg) in &self.functions {
let head = snippet(src, ptr.text_range());
out.push_str(&format!("region: {head}\n"));
render_region(cfg, src, &mut out);
}
out
}
}
pub fn always_diverges(element: &SyntaxElement) -> bool {
let Some(node) = element.as_node() else {
return false;
};
match node.kind() {
SyntaxKind::CALL_EXPR => matches!(
CallExpr::cast(node.clone())
.and_then(|call| call.callee_name())
.as_deref(),
Some("return" | "stop")
),
SyntaxKind::BLOCK_EXPR => region_statements(node).iter().any(always_diverges),
SyntaxKind::IF_EXPR => {
let if_expr = IfExpr::cast(node.clone()).expect("IF_EXPR casts");
let (Some(then_body), Some(else_body)) = (
branch_single(if_expr.then_elements()),
branch_single(if_expr.else_elements()),
) else {
return false;
};
always_diverges(&then_body) && always_diverges(&else_body)
}
_ => false,
}
}
struct Builder {
blocks: Vec<BasicBlock>,
expressions: Vec<(BlockId, ControlFlowGraph)>,
}
#[derive(Clone, Copy)]
struct LoopCtx {
header: BlockId,
after: BlockId,
}
impl Builder {
fn new_block(&mut self) -> BlockId {
let id = BlockId(self.blocks.len() as u32);
self.blocks.push(BasicBlock {
stmts: Vec::new(),
terminator: Terminator::Return,
});
id
}
fn set_term(&mut self, block: BlockId, terminator: Terminator) {
self.blocks[block.0 as usize].terminator = terminator;
}
fn push_stmt(&mut self, block: BlockId, range: TextRange) {
self.blocks[block.0 as usize].stmts.push(range);
}
fn has_predecessor(&self, target: BlockId) -> bool {
self.blocks.iter().any(|block| match block.terminator {
Terminator::Goto(t) => t == target,
Terminator::Branch { then_blk, else_blk } => then_blk == target || else_blk == target,
Terminator::MultiBranch { ref targets, .. } => targets.contains(&target),
_ => false,
})
}
fn lower_seq(
&mut self,
stmts: &[SyntaxElement],
mut cur: BlockId,
loop_ctx: Option<LoopCtx>,
) -> Option<BlockId> {
for (i, stmt) in stmts.iter().enumerate() {
match self.lower_stmt(stmt, cur, loop_ctx) {
Some(next) => cur = next,
None => {
let rest = &stmts[i + 1..];
if !rest.is_empty() {
let dead = self.new_block();
self.set_term(dead, Terminator::Unreachable);
for stmt in rest {
self.push_stmt(dead, stmt.text_range());
}
}
return None;
}
}
}
Some(cur)
}
fn lower_stmt(
&mut self,
stmt: &SyntaxElement,
cur: BlockId,
loop_ctx: Option<LoopCtx>,
) -> Option<BlockId> {
let Some(node) = stmt.as_node() else {
if let Some(token) = stmt.as_token()
&& token.kind() == SyntaxKind::IDENT
{
match token.text() {
"break" => {
return self.lower_jump(cur, stmt.text_range(), loop_ctx, |lc| lc.after);
}
"next" => {
return self.lower_jump(cur, stmt.text_range(), loop_ctx, |lc| lc.header);
}
_ => {}
}
}
self.push_stmt(cur, stmt.text_range());
return Some(cur);
};
match node.kind() {
SyntaxKind::BLOCK_EXPR => self.lower_seq(®ion_statements(node), cur, loop_ctx),
SyntaxKind::IF_EXPR => {
self.push_stmt(cur, node.text_range());
self.lower_if(
&IfExpr::cast(node.clone()).expect("IF_EXPR casts"),
cur,
loop_ctx,
)
}
SyntaxKind::FOR_EXPR => {
self.push_stmt(cur, node.text_range());
let for_expr = ForExpr::cast(node.clone()).expect("FOR_EXPR casts");
for element in for_expr.clause_elements().unwrap_or_default() {
if let Some(node) = element.as_node() {
self.collect_expression(node, cur);
}
}
let body = for_expr.body_element();
self.lower_loop(cur, body.as_ref(), false)
}
SyntaxKind::WHILE_EXPR => {
self.push_stmt(cur, node.text_range());
let while_expr = WhileExpr::cast(node.clone()).expect("WHILE_EXPR casts");
for element in while_expr.condition_elements().unwrap_or_default() {
if let Some(node) = element.as_node() {
self.collect_expression(node, cur);
}
}
let body = while_expr.body_element();
self.lower_loop(cur, body.as_ref(), false)
}
SyntaxKind::REPEAT_EXPR => {
self.push_stmt(cur, node.text_range());
let body = RepeatExpr::cast(node.clone())
.expect("REPEAT_EXPR casts")
.body();
self.lower_loop(cur, body.as_ref(), true)
}
SyntaxKind::CALL_EXPR => {
self.push_stmt(cur, node.text_range());
self.collect_expression(node, cur);
match CallExpr::cast(node.clone())
.and_then(|call| call.callee_name())
.as_deref()
{
Some("return" | "stop") => {
self.set_term(cur, Terminator::Diverge);
None
}
_ => Some(cur),
}
}
_ => {
self.push_stmt(cur, node.text_range());
self.collect_expression(node, cur);
Some(cur)
}
}
}
fn collect_expression(&mut self, node: &SyntaxNode, owner: BlockId) {
if node.kind() == SyntaxKind::FUNCTION_EXPR
|| super::builder::call_quote_kind(node).is_some()
{
return;
}
if matches!(
node.kind(),
SyntaxKind::IF_EXPR
| SyntaxKind::FOR_EXPR
| SyntaxKind::WHILE_EXPR
| SyntaxKind::REPEAT_EXPR
| SyntaxKind::BLOCK_EXPR
) {
self.expressions.push((
owner,
ControlFlowGraph::build_region(&body_statements(&node.clone().into())),
));
return;
}
if let Some(binary) = BinaryExpr::cast(node.clone())
&& matches!(binary.op_kind(), Some(SyntaxKind::AND2 | SyntaxKind::OR2))
{
let mut builder = Builder {
blocks: Vec::new(),
expressions: Vec::new(),
};
let entry = builder.new_block();
builder.push_stmt(entry, node.text_range());
let rhs = builder.new_block();
let after = builder.new_block();
let (then_blk, else_blk) = if binary.op_kind() == Some(SyntaxKind::AND2) {
(rhs, after)
} else {
(after, rhs)
};
builder.set_term(entry, Terminator::Branch { then_blk, else_blk });
builder.set_term(rhs, Terminator::Goto(after));
if let Some(node) = binary.lhs().and_then(SyntaxElement::into_node) {
builder.collect_expression(&node, entry);
}
if let Some(node) = binary.rhs().and_then(SyntaxElement::into_node) {
builder.collect_expression(&node, rhs);
}
self.expressions.push((
owner,
ControlFlowGraph {
blocks: builder.blocks,
entry,
expressions: builder.expressions,
},
));
return;
}
if let Some(call) = CallExpr::cast(node.clone())
&& call.callee_name().as_deref() == Some("switch")
{
let mut args = call.args();
let Some(selector) = args.next() else { return };
let mut builder = Builder {
blocks: Vec::new(),
expressions: Vec::new(),
};
let entry = builder.new_block();
builder.push_stmt(entry, node.text_range());
let after = builder.new_block();
if let Some(node) = selector.value().and_then(SyntaxElement::into_node) {
builder.collect_expression(&node, entry);
}
let mut targets = vec![after];
for arg in args {
if let Some(value) = arg.value() {
let arm = builder.new_block();
targets.push(arm);
if let Some(node) = value.as_node() {
builder.collect_expression(node, arm);
}
builder.set_term(arm, Terminator::Goto(after));
}
}
builder.set_term(
entry,
Terminator::MultiBranch {
call: NodePtr::from_node(node),
targets,
},
);
self.expressions.push((
owner,
ControlFlowGraph {
blocks: builder.blocks,
entry,
expressions: builder.expressions,
},
));
return;
}
for child in node.children() {
self.collect_expression(&child, owner);
}
}
fn lower_jump(
&mut self,
cur: BlockId,
range: TextRange,
loop_ctx: Option<LoopCtx>,
target: impl Fn(LoopCtx) -> BlockId,
) -> Option<BlockId> {
self.push_stmt(cur, range);
match loop_ctx {
Some(lc) => {
self.set_term(cur, Terminator::Goto(target(lc)));
None
}
None => Some(cur),
}
}
fn lower_if(
&mut self,
if_expr: &IfExpr,
cur: BlockId,
loop_ctx: Option<LoopCtx>,
) -> Option<BlockId> {
for element in if_expr.condition_elements().unwrap_or_default() {
if let Some(node) = element.as_node() {
self.collect_expression(node, cur);
}
}
let then_blk = self.new_block();
let then_exit = self.lower_seq(
&branch_statements(if_expr.then_elements()),
then_blk,
loop_ctx,
);
if if_expr.else_keyword().is_none() {
let join = self.new_block();
self.set_term(
cur,
Terminator::Branch {
then_blk,
else_blk: join,
},
);
if let Some(exit) = then_exit {
self.set_term(exit, Terminator::Goto(join));
}
return Some(join);
}
let else_blk = self.new_block();
let else_exit = self.lower_seq(
&branch_statements(if_expr.else_elements()),
else_blk,
loop_ctx,
);
self.set_term(cur, Terminator::Branch { then_blk, else_blk });
match (then_exit, else_exit) {
(None, None) => None,
_ => {
let join = self.new_block();
if let Some(exit) = then_exit {
self.set_term(exit, Terminator::Goto(join));
}
if let Some(exit) = else_exit {
self.set_term(exit, Terminator::Goto(join));
}
Some(join)
}
}
}
fn lower_loop(
&mut self,
cur: BlockId,
body: Option<&SyntaxElement>,
is_repeat: bool,
) -> Option<BlockId> {
let header = self.new_block();
self.set_term(cur, Terminator::Goto(header));
let after = self.new_block();
let body_blk = self.new_block();
if is_repeat {
self.set_term(header, Terminator::Goto(body_blk));
} else {
self.set_term(
header,
Terminator::Branch {
then_blk: body_blk,
else_blk: after,
},
);
}
let loop_ctx = LoopCtx { header, after };
let body_stmts = body.map(body_statements).unwrap_or_default();
if let Some(exit) = self.lower_seq(&body_stmts, body_blk, Some(loop_ctx)) {
self.set_term(exit, Terminator::Goto(header)); }
if is_repeat && !self.has_predecessor(after) {
self.set_term(after, Terminator::Unreachable);
None
} else {
Some(after)
}
}
}
fn region_statements(container: &SyntaxNode) -> Vec<SyntaxElement> {
container
.children_with_tokens()
.filter(|element| {
!is_trivia(element.kind())
&& !matches!(
element.kind(),
SyntaxKind::COMMENT
| SyntaxKind::LBRACE
| SyntaxKind::RBRACE
| SyntaxKind::SEMICOLON
)
})
.collect()
}
fn body_statements(body: &SyntaxElement) -> Vec<SyntaxElement> {
match body.as_node() {
Some(node) if node.kind() == SyntaxKind::BLOCK_EXPR => region_statements(node),
_ => vec![body.clone()],
}
}
fn branch_statements(elements: Option<Vec<SyntaxElement>>) -> Vec<SyntaxElement> {
match branch_single(elements.clone()) {
Some(single) if single.kind() == SyntaxKind::BLOCK_EXPR => {
region_statements(single.as_node().expect("BLOCK_EXPR is a node"))
}
_ => elements
.unwrap_or_default()
.into_iter()
.filter(|element| !is_trivia(element.kind()) && element.kind() != SyntaxKind::COMMENT)
.collect(),
}
}
fn branch_single(elements: Option<Vec<SyntaxElement>>) -> Option<SyntaxElement> {
let mut body = elements?
.into_iter()
.filter(|element| !is_trivia(element.kind()) && element.kind() != SyntaxKind::COMMENT);
let first = body.next()?;
body.next().is_none().then_some(first)
}
fn render_region(cfg: &ControlFlowGraph, src: &str, out: &mut String) {
render_region_at(cfg, src, out, 2);
}
fn render_region_at(cfg: &ControlFlowGraph, src: &str, out: &mut String, indent: usize) {
for (i, block) in cfg.blocks().iter().enumerate() {
let stmts = block
.stmts
.iter()
.map(|range| snippet(src, *range))
.collect::<Vec<_>>()
.join("; ");
let term = match block.terminator {
Terminator::Goto(t) => format!("-> bb{}", t.index()),
Terminator::Branch { then_blk, else_blk } => {
format!(
"-> then bb{}, else bb{}",
then_blk.index(),
else_blk.index()
)
}
Terminator::MultiBranch { ref targets, .. } => format!(
"-> alternatives {:?}",
targets.iter().map(|id| id.index()).collect::<Vec<_>>()
),
Terminator::Return => "-> return".to_string(),
Terminator::Diverge => "-> diverge".to_string(),
Terminator::Unreachable => "(unreachable)".to_string(),
};
out.push_str(&format!("{}bb{i}: [{stmts}] {term}\n", " ".repeat(indent)));
for (_, graph) in cfg
.expressions
.iter()
.filter(|(owner, _)| owner.index() == i)
{
out.push_str(&format!("{}expression flow:\n", " ".repeat(indent + 2)));
render_region_at(graph, src, out, indent + 4);
}
}
}
fn snippet(src: &str, range: TextRange) -> String {
let text = &src[range];
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
if flat.len() > 40 {
format!(
"{}…",
&flat[..flat.char_indices().nth(40).map_or(flat.len(), |(i, _)| i)]
)
} else {
flat
}
}