use crate::smt::int_type_of;
use super::dataflow::{analyze_run, RunDataflow};
#[derive(Clone)]
pub(crate) struct Candidate {
pub(crate) fn_ident: String,
pub(crate) stmt_range: (usize, usize),
pub(crate) inputs: Vec<(String, syn::Type)>,
pub(crate) outputs: Vec<(String, syn::Type)>,
pub(crate) tail_is_output: bool,
}
pub(crate) fn rendered_line_count(func: &syn::ItemFn) -> usize {
let file = syn::File {
shebang: None,
attrs: Vec::new(),
items: vec![syn::Item::Fn(func.clone())],
};
prettyplease::unparse(&file).lines().count().max(1)
}
pub(crate) fn find_candidate(func: &syn::ItemFn, max_lines: usize) -> Option<Candidate> {
if rendered_line_count(func) <= max_lines {
return None;
}
let stmts = &func.block.stmts;
let n = stmts.len();
if n == 0 {
return None;
}
let ok_stmt: Vec<bool> = stmts.iter().map(stmt_is_extractable).collect();
let mut best: Option<Candidate> = None;
let mut i = 0usize;
while i < n {
if !ok_stmt[i] {
i += 1;
continue;
}
let mut k = i;
while k < n && ok_stmt[k] {
k += 1;
}
if let Some(cand) = best_run_in_window(func, i, k) {
let len = cand.stmt_range.1 - cand.stmt_range.0;
let better = match &best {
None => true,
Some(b) => len > (b.stmt_range.1 - b.stmt_range.0),
};
if better {
best = Some(cand);
}
}
i = k;
}
best
}
fn best_run_in_window(func: &syn::ItemFn, lo: usize, hi: usize) -> Option<Candidate> {
let window = hi - lo;
for len in (1..=window).rev() {
for s in lo..=(hi - len) {
let e = s + len;
if let Some(cand) = qualify_run(func, s, e) {
return Some(cand);
}
}
}
None
}
fn qualify_run(func: &syn::ItemFn, i: usize, j: usize) -> Option<Candidate> {
let stmts = &func.block.stmts;
let df: RunDataflow = analyze_run(func, i, j)?;
if !df.inputs.iter().all(|(_, ty)| int_type_of(ty).is_some()) {
return None;
}
if !df.outputs.iter().all(|(_, ty)| int_type_of(ty).is_some()) {
return None;
}
if df.outputs.len() > 4 {
return None;
}
if output_is_reshadowed(stmts, j, &df.outputs) {
return None;
}
if !df
.outputs
.iter()
.all(|(name, _)| df.defs.iter().any(|d| d == name))
{
return None;
}
let tail_is_output = j == stmts.len()
&& df.outputs.is_empty()
&& matches!(stmts.last(), Some(syn::Stmt::Expr(_, None)));
if df.outputs.is_empty() && !tail_is_output {
return None;
}
Some(Candidate {
fn_ident: func.sig.ident.to_string(),
stmt_range: (i, j),
inputs: df.inputs,
outputs: df.outputs,
tail_is_output,
})
}
fn stmt_is_extractable(stmt: &syn::Stmt) -> bool {
match stmt {
syn::Stmt::Local(local) => {
if !is_plain_ident_let(local) {
return false;
}
let Some(init) = &local.init else {
return false;
};
if init.diverge.is_some() {
return false;
}
crate::smt::is_pure_supported_expr(&init.expr).is_ok()
}
syn::Stmt::Expr(expr, _semi) => {
crate::smt::is_pure_supported_expr(expr).is_ok()
}
syn::Stmt::Item(_) | syn::Stmt::Macro(_) => false,
}
}
fn is_plain_ident_let(local: &syn::Local) -> bool {
match &local.pat {
syn::Pat::Ident(pat_ident) => pat_ident.by_ref.is_none() && pat_ident.subpat.is_none(),
syn::Pat::Type(pat_type) => match pat_type.pat.as_ref() {
syn::Pat::Ident(pat_ident) => pat_ident.by_ref.is_none() && pat_ident.subpat.is_none(),
_ => false,
},
_ => false,
}
}
fn output_is_reshadowed(
stmts: &[syn::Stmt],
after: usize,
outputs: &[(String, syn::Type)],
) -> bool {
if outputs.is_empty() || after >= stmts.len() {
return false;
}
let out_names: Vec<&String> = outputs.iter().map(|(n, _)| n).collect();
for stmt in &stmts[after..] {
if let syn::Stmt::Local(local) = stmt {
if let Some(name) = let_bound_name(local) {
if out_names.iter().any(|n| **n == name) {
return true;
}
}
}
}
false
}
fn let_bound_name(local: &syn::Local) -> Option<String> {
match &local.pat {
syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.to_string()),
syn::Pat::Type(pat_type) => match pat_type.pat.as_ref() {
syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.to_string()),
_ => None,
},
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_fn(src: &str) -> syn::ItemFn {
syn::parse_str(src).expect("parse fn")
}
#[test]
fn under_budget_is_skipped() {
let func = parse_fn("fn small(a: u32) -> u32 { a + 1 }");
assert!(find_candidate(&func, 1000).is_none());
}
#[test]
fn finds_straight_line_run() {
let func = parse_fn(
"fn calc(a: u32, b: u32) -> u32 {\n\
let s = a + b;\n\
let t = s * 2;\n\
let u = t + a;\n\
u\n\
}",
);
let cand = find_candidate(&func, 1).expect("should find a candidate");
assert_eq!(cand.fn_ident, "calc");
let in_names: Vec<&String> = cand.inputs.iter().map(|(n, _)| n).collect();
assert!(in_names.contains(&&"a".to_string()));
assert!(in_names.contains(&&"b".to_string()));
}
#[test]
fn never_extracts_across_a_call() {
let func = parse_fn(
"fn withcall(a: u32) -> u32 {\n\
let t = helper(a);\n\
t + 1\n\
}",
);
assert!(
find_candidate(&func, 1).is_none(),
"no pure run should qualify when every value flows through a call"
);
}
#[test]
fn candidate_run_never_spans_a_call() {
let func = parse_fn(
"fn mixed(a: u32) -> u32 {\n\
let s = a + 1;\n\
let t = helper(s);\n\
t\n\
}",
);
if let Some(cand) = find_candidate(&func, 1) {
let (i, j) = cand.stmt_range;
assert!(!(i <= 1 && 1 < j), "run must not span the call statement");
}
}
}