use crate::ast::FunctionNode;
use crate::cfg::{Cfg, NodeId, NodeKind};
use crate::language::cfg_builder::CfgBuilder;
use anyhow::{Context, Result};
use syn::{Arm, Block, Expr, ExprBlock, ExprForLoop, ExprIf, ExprLoop, ExprMatch, ExprWhile, Stmt};
pub struct RustCfgBuilder;
impl CfgBuilder for RustCfgBuilder {
fn build(&self, function: &FunctionNode) -> Cfg {
let source = function.body.as_rust();
build_cfg_from_source(source).unwrap_or_default()
}
}
fn build_cfg_from_source(source: &str) -> Result<Cfg> {
let item_fn: syn::ItemFn =
syn::parse_str(source).context("Failed to parse Rust function for CFG building")?;
let mut cfg = Cfg::new();
let entry = cfg.entry;
let exit = cfg.exit;
let last_node = build_block_cfg(&mut cfg, &item_fn.block, entry, exit);
cfg.add_edge(last_node, exit);
Ok(cfg)
}
enum Task<'a> {
Block {
stmts: &'a [Stmt],
idx: usize,
entry: NodeId,
exit: NodeId,
},
ContinueBlock {
stmts: &'a [Stmt],
idx: usize,
exit: NodeId,
},
Stmt {
stmt: &'a Stmt,
entry: NodeId,
exit: NodeId,
},
Expr {
expr: &'a Expr,
entry: NodeId,
exit: NodeId,
},
If {
expr_if: &'a ExprIf,
entry: NodeId,
exit: NodeId,
},
AfterIfThen {
join: NodeId,
condition: NodeId,
else_expr: Option<&'a Expr>,
exit: NodeId,
},
AfterIfElse {
join: NodeId,
},
Match {
expr_match: &'a ExprMatch,
entry: NodeId,
exit: NodeId,
},
AfterMatchArm {
arms: std::slice::Iter<'a, Arm>,
condition: NodeId,
join: NodeId,
exit: NodeId,
},
Loop {
expr_loop: &'a ExprLoop,
entry: NodeId,
},
AfterLoopBody {
header: NodeId,
},
While {
expr_while: &'a ExprWhile,
entry: NodeId,
},
AfterWhileBody {
condition: NodeId,
},
For {
expr_for: &'a ExprForLoop,
entry: NodeId,
},
AfterForBody {
condition: NodeId,
},
ExprBlock {
expr_block: &'a ExprBlock,
entry: NodeId,
exit: NodeId,
},
}
fn build_block_cfg(cfg: &mut Cfg, block: &Block, entry: NodeId, exit: NodeId) -> NodeId {
let mut stack: Vec<Task<'_>> = vec![Task::Block {
stmts: &block.stmts,
idx: 0,
entry,
exit,
}];
let mut last = entry;
while let Some(task) = stack.pop() {
match task {
Task::Block {
stmts,
idx,
entry,
exit,
} => {
if idx >= stmts.len() {
last = entry;
} else {
stack.push(Task::ContinueBlock {
stmts,
idx: idx + 1,
exit,
});
stack.push(Task::Stmt {
stmt: &stmts[idx],
entry,
exit,
});
}
}
Task::ContinueBlock { stmts, idx, exit } => {
let entry = last;
if idx >= stmts.len() {
last = entry;
} else {
stack.push(Task::ContinueBlock {
stmts,
idx: idx + 1,
exit,
});
stack.push(Task::Stmt {
stmt: &stmts[idx],
entry,
exit,
});
}
}
Task::Stmt { stmt, entry, exit } => match stmt {
Stmt::Expr(expr, _) => stack.push(Task::Expr { expr, entry, exit }),
Stmt::Local(local) => {
if let Some(init) = &local.init {
stack.push(Task::Expr {
expr: &init.expr,
entry,
exit,
});
} else {
let node = cfg.add_node(NodeKind::Statement);
cfg.add_edge(entry, node);
last = node;
}
}
Stmt::Item(_) | Stmt::Macro(_) => {
let node = cfg.add_node(NodeKind::Statement);
cfg.add_edge(entry, node);
last = node;
}
},
Task::Expr { expr, entry, exit } => match expr {
Expr::If(expr_if) => stack.push(Task::If {
expr_if,
entry,
exit,
}),
Expr::Match(expr_match) => stack.push(Task::Match {
expr_match,
entry,
exit,
}),
Expr::Loop(expr_loop) => stack.push(Task::Loop { expr_loop, entry }),
Expr::While(expr_while) => stack.push(Task::While { expr_while, entry }),
Expr::ForLoop(expr_for) => stack.push(Task::For { expr_for, entry }),
Expr::Block(expr_block) => stack.push(Task::ExprBlock {
expr_block,
entry,
exit,
}),
Expr::Return(_) => {
let node = cfg.add_node(NodeKind::Statement);
cfg.add_edge(entry, node);
cfg.add_edge(node, exit);
last = node;
}
Expr::Break(_) => {
let node = cfg.add_node(NodeKind::Statement);
cfg.add_edge(entry, node);
last = node;
}
Expr::Continue(_) => {
let node = cfg.add_node(NodeKind::Statement);
cfg.add_edge(entry, node);
last = node;
}
_ => {
let node = cfg.add_node(NodeKind::Statement);
cfg.add_edge(entry, node);
last = node;
}
},
Task::If {
expr_if,
entry,
exit,
} => {
let condition = cfg.add_node(NodeKind::Condition);
cfg.add_edge(entry, condition);
let then_entry = cfg.add_node(NodeKind::Statement);
cfg.add_edge(condition, then_entry);
let join = cfg.add_node(NodeKind::Join);
stack.push(Task::AfterIfThen {
join,
condition,
else_expr: expr_if.else_branch.as_ref().map(|(_, e)| e.as_ref()),
exit,
});
stack.push(Task::Block {
stmts: &expr_if.then_branch.stmts,
idx: 0,
entry: then_entry,
exit,
});
}
Task::AfterIfThen {
join,
condition,
else_expr,
exit,
} => {
let then_exit = last;
cfg.add_edge(then_exit, join);
match else_expr {
Some(else_expr) => {
let else_entry = cfg.add_node(NodeKind::Statement);
cfg.add_edge(condition, else_entry);
stack.push(Task::AfterIfElse { join });
stack.push(Task::Expr {
expr: else_expr,
entry: else_entry,
exit,
});
}
None => {
cfg.add_edge(condition, join);
last = join;
}
}
}
Task::AfterIfElse { join } => {
let else_exit = last;
cfg.add_edge(else_exit, join);
last = join;
}
Task::Match {
expr_match,
entry,
exit,
} => {
let condition = cfg.add_node(NodeKind::Condition);
cfg.add_edge(entry, condition);
let join = cfg.add_node(NodeKind::Join);
let mut arms = expr_match.arms.iter();
if let Some(first_arm) = arms.next() {
let arm_entry = cfg.add_node(NodeKind::Statement);
cfg.add_edge(condition, arm_entry);
stack.push(Task::AfterMatchArm {
arms,
condition,
join,
exit,
});
stack.push(Task::Expr {
expr: &first_arm.body,
entry: arm_entry,
exit,
});
} else {
cfg.add_edge(condition, join);
last = join;
}
}
Task::AfterMatchArm {
mut arms,
condition,
join,
exit,
} => {
let arm_exit = last;
cfg.add_edge(arm_exit, join);
if let Some(next_arm) = arms.next() {
let arm_entry = cfg.add_node(NodeKind::Statement);
cfg.add_edge(condition, arm_entry);
stack.push(Task::AfterMatchArm {
arms,
condition,
join,
exit,
});
stack.push(Task::Expr {
expr: &next_arm.body,
entry: arm_entry,
exit,
});
} else {
last = join;
}
}
Task::Loop { expr_loop, entry } => {
let header = cfg.add_node(NodeKind::LoopHeader);
cfg.add_edge(entry, header);
stack.push(Task::AfterLoopBody { header });
stack.push(Task::Block {
stmts: &expr_loop.body.stmts,
idx: 0,
entry: header,
exit: header,
});
}
Task::AfterLoopBody { header } => {
let body_exit = last;
cfg.add_edge(body_exit, header);
let loop_exit = cfg.add_node(NodeKind::Join);
cfg.add_edge(header, loop_exit);
last = loop_exit;
}
Task::While { expr_while, entry } => {
let condition = cfg.add_node(NodeKind::Condition);
cfg.add_edge(entry, condition);
let body_entry = cfg.add_node(NodeKind::Statement);
cfg.add_edge(condition, body_entry);
stack.push(Task::AfterWhileBody { condition });
stack.push(Task::Block {
stmts: &expr_while.body.stmts,
idx: 0,
entry: body_entry,
exit: condition,
});
}
Task::AfterWhileBody { condition } => {
let body_exit = last;
cfg.add_edge(body_exit, condition);
let loop_exit = cfg.add_node(NodeKind::Join);
cfg.add_edge(condition, loop_exit);
last = loop_exit;
}
Task::For { expr_for, entry } => {
let condition = cfg.add_node(NodeKind::Condition);
cfg.add_edge(entry, condition);
let body_entry = cfg.add_node(NodeKind::Statement);
cfg.add_edge(condition, body_entry);
stack.push(Task::AfterForBody { condition });
stack.push(Task::Block {
stmts: &expr_for.body.stmts,
idx: 0,
entry: body_entry,
exit: condition,
});
}
Task::AfterForBody { condition } => {
let body_exit = last;
cfg.add_edge(body_exit, condition);
let loop_exit = cfg.add_node(NodeKind::Join);
cfg.add_edge(condition, loop_exit);
last = loop_exit;
}
Task::ExprBlock {
expr_block,
entry,
exit,
} => {
stack.push(Task::Block {
stmts: &expr_block.block.stmts,
idx: 0,
entry,
exit,
});
}
}
}
last
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::FunctionId;
use crate::language::{FunctionBody, SourceSpan};
fn make_test_function(source: &str) -> FunctionNode {
FunctionNode {
id: FunctionId {
file_index: 0,
local_index: 0,
},
name: Some("test".to_string()),
span: SourceSpan::new(0, source.len(), 1, 1, 0),
body: FunctionBody::Rust {
source: source.to_string(),
},
suppression_reason: None,
}
}
#[test]
fn test_rust_cfg_builder_deeply_nested_does_not_overflow_and_builds_real_cfg() {
const DEPTH: usize = 50_000;
fn nested_if(depth: usize, cond: &Expr) -> Block {
let mut block = Block {
brace_token: syn::token::Brace::default(),
stmts: vec![Stmt::Expr(
Expr::Break(syn::ExprBreak {
attrs: Vec::new(),
break_token: Default::default(),
label: None,
expr: None,
}),
None,
)],
};
for _ in 0..depth {
let if_expr = Expr::If(ExprIf {
attrs: Vec::new(),
if_token: Default::default(),
cond: Box::new(cond.clone()),
then_branch: block,
else_branch: None,
});
block = Block {
brace_token: syn::token::Brace::default(),
stmts: vec![Stmt::Expr(if_expr, None)],
};
}
block
}
let node_count = std::thread::Builder::new()
.stack_size(256 * 1024) .spawn(move || {
let cond: Expr = syn::parse_str("true").unwrap();
let block = nested_if(DEPTH, &cond);
let mut cfg = Cfg::new();
let entry = cfg.entry;
let exit = cfg.exit;
let last = build_block_cfg(&mut cfg, &block, entry, exit);
cfg.add_edge(last, exit);
std::mem::forget(block);
cfg.node_count()
})
.unwrap()
.join()
.unwrap();
assert!(
node_count > DEPTH,
"expected a fully-built CFG with >{DEPTH} nodes, got {node_count}"
);
}
#[test]
fn test_rust_cfg_builder_simple() {
let source = r#"
fn simple() {
let x = 1;
}
"#;
let function = make_test_function(source);
let builder = RustCfgBuilder;
let cfg = builder.build(&function);
assert!(cfg.node_count() >= 2);
assert_eq!(cfg.entry, NodeId(0));
assert_eq!(cfg.exit, NodeId(1));
}
#[test]
fn test_rust_cfg_builder_if() {
let source = r#"
fn with_if(x: i32) {
if x > 0 {
println!("positive");
}
}
"#;
let function = make_test_function(source);
let builder = RustCfgBuilder;
let cfg = builder.build(&function);
assert!(cfg.node_count() >= 4);
}
#[test]
fn test_rust_cfg_builder_if_else() {
let source = r#"
fn with_if_else(x: i32) {
if x > 0 {
println!("positive");
} else {
println!("non-positive");
}
}
"#;
let function = make_test_function(source);
let builder = RustCfgBuilder;
let cfg = builder.build(&function);
assert!(cfg.node_count() >= 5);
}
#[test]
fn test_rust_cfg_builder_match() {
let source = r#"
fn with_match(x: i32) {
match x {
0 => println!("zero"),
1 => println!("one"),
_ => println!("other"),
}
}
"#;
let function = make_test_function(source);
let builder = RustCfgBuilder;
let cfg = builder.build(&function);
assert!(cfg.node_count() >= 4);
}
#[test]
fn test_rust_cfg_builder_nested_if_in_loop() {
let source = r#"
fn nested(x: i32) {
loop {
if x > 0 {
break;
}
}
}
"#;
let function = make_test_function(source);
let builder = RustCfgBuilder;
let cfg = builder.build(&function);
assert!(cfg.node_count() >= 5);
}
}