use std::collections::{HashMap, HashSet};
use syn::visit::Visit;
#[derive(Clone, Default)]
pub(crate) struct RunDataflow {
pub(crate) defs: Vec<String>,
pub(crate) inputs: Vec<(String, syn::Type)>,
pub(crate) outputs: Vec<(String, syn::Type)>,
}
type TypeEnv = HashMap<String, syn::Type>;
pub(crate) fn analyze_run(func: &syn::ItemFn, i: usize, j: usize) -> Option<RunDataflow> {
let stmts = &func.block.stmts;
if i >= j || j > stmts.len() {
return None;
}
let mut pre_env: TypeEnv = HashMap::new();
collect_param_types(&func.sig, &mut pre_env);
collect_let_types(&stmts[0..i], &mut pre_env);
let pre_names = collect_pre_binding_names(&func.sig, &stmts[0..i]);
let mut run_env = pre_env.clone();
let mut defs: Vec<String> = Vec::new();
let mut def_types: TypeEnv = HashMap::new();
for stmt in &stmts[i..j] {
if let syn::Stmt::Local(local) = stmt {
if let Some((name, ty)) = let_binding(local, &run_env) {
if !defs.contains(&name) {
defs.push(name.clone());
}
if let Some(ty) = ty.clone() {
def_types.insert(name.clone(), ty.clone());
run_env.insert(name.clone(), ty);
}
let _ = name;
}
}
}
let read_in_run = reads_in_stmts(&stmts[i..j]);
let defs_set: HashSet<&String> = defs.iter().collect();
let mut inputs: Vec<(String, syn::Type)> = Vec::new();
let mut seen_input: HashSet<String> = HashSet::new();
for name in &read_in_run {
if defs_set.contains(name) {
continue; }
if let Some(ty) = pre_env.get(name) {
if seen_input.insert(name.clone()) {
inputs.push((name.clone(), ty.clone()));
}
} else if pre_names.contains(name) {
return None;
}
}
let read_after = reads_in_stmts(&stmts[j..]);
let mut outputs: Vec<(String, syn::Type)> = Vec::new();
let mut seen_output: HashSet<String> = HashSet::new();
for name in &defs {
if read_after.contains(name) {
let ty = def_types.get(name)?;
if seen_output.insert(name.clone()) {
outputs.push((name.clone(), ty.clone()));
}
}
}
Some(RunDataflow {
defs,
inputs,
outputs,
})
}
fn collect_param_types(sig: &syn::Signature, env: &mut TypeEnv) {
for input in &sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
if pat_ident.by_ref.is_none() && pat_ident.subpat.is_none() {
env.insert(pat_ident.ident.to_string(), (*pat_type.ty).clone());
}
}
}
}
}
fn collect_let_types(stmts: &[syn::Stmt], env: &mut TypeEnv) {
for stmt in stmts {
if let syn::Stmt::Local(local) = stmt {
if let Some((name, Some(ty))) = let_binding(local, env) {
env.insert(name, ty);
}
}
}
}
fn collect_pre_binding_names(sig: &syn::Signature, leading: &[syn::Stmt]) -> HashSet<String> {
let mut names = HashSet::new();
for input in &sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
names.insert(pat_ident.ident.to_string());
}
}
}
for stmt in leading {
if let syn::Stmt::Local(local) = stmt {
match &local.pat {
syn::Pat::Ident(pat_ident) => {
names.insert(pat_ident.ident.to_string());
}
syn::Pat::Type(pat_type) => {
if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
names.insert(pat_ident.ident.to_string());
}
}
_ => {}
}
}
}
names
}
fn let_binding(local: &syn::Local, env: &TypeEnv) -> Option<(String, Option<syn::Type>)> {
let (name, annotated) = match &local.pat {
syn::Pat::Ident(pat_ident) => {
if pat_ident.by_ref.is_some() || pat_ident.subpat.is_some() {
return None;
}
(pat_ident.ident.to_string(), None)
}
syn::Pat::Type(pat_type) => {
let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
return None;
};
if pat_ident.by_ref.is_some() || pat_ident.subpat.is_some() {
return None;
}
(pat_ident.ident.to_string(), Some((*pat_type.ty).clone()))
}
_ => return None,
};
if annotated.is_some() {
return Some((name, annotated));
}
let inferred = local
.init
.as_ref()
.and_then(|init| infer_expr_type(&init.expr, env));
Some((name, inferred))
}
fn infer_expr_type(expr: &syn::Expr, env: &TypeEnv) -> Option<syn::Type> {
match expr {
syn::Expr::Paren(p) => infer_expr_type(&p.expr, env),
syn::Expr::Group(g) => infer_expr_type(&g.expr, env),
syn::Expr::Path(path) => {
let ident = path.path.get_ident()?;
env.get(&ident.to_string()).cloned()
}
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Int(int_lit) => {
let suffix = int_lit.suffix();
if suffix.is_empty() {
None
} else {
syn::parse_str::<syn::Type>(suffix).ok()
}
}
syn::Lit::Bool(_) => syn::parse_str::<syn::Type>("bool").ok(),
_ => None,
},
syn::Expr::Cast(cast) => Some((*cast.ty).clone()),
syn::Expr::Unary(un) => match un.op {
syn::UnOp::Not(_) | syn::UnOp::Neg(_) => infer_expr_type(&un.expr, env),
_ => None,
},
syn::Expr::Binary(bin) => {
use syn::BinOp;
if matches!(
bin.op,
BinOp::Eq(_)
| BinOp::Ne(_)
| BinOp::Lt(_)
| BinOp::Le(_)
| BinOp::Gt(_)
| BinOp::Ge(_)
| BinOp::And(_)
| BinOp::Or(_)
) {
return syn::parse_str::<syn::Type>("bool").ok();
}
infer_expr_type(&bin.left, env).or_else(|| infer_expr_type(&bin.right, env))
}
syn::Expr::If(if_expr) => infer_block_tail_type(&if_expr.then_branch, env),
syn::Expr::Block(block) => infer_block_tail_type(&block.block, env),
_ => None,
}
}
fn infer_block_tail_type(block: &syn::Block, env: &TypeEnv) -> Option<syn::Type> {
let mut local_env = env.clone();
let (last, leading) = block.stmts.split_last()?;
collect_let_types(leading, &mut local_env);
match last {
syn::Stmt::Expr(expr, _) => infer_expr_type(expr, &local_env),
_ => None,
}
}
fn reads_in_stmts(stmts: &[syn::Stmt]) -> Vec<String> {
let mut visitor = ReadVisitor::default();
for stmt in stmts {
visitor.visit_stmt(stmt);
}
visitor.reads
}
#[derive(Default)]
struct ReadVisitor {
reads: Vec<String>,
seen: HashSet<String>,
}
impl ReadVisitor {
fn record(&mut self, name: String) {
if self.seen.insert(name.clone()) {
self.reads.push(name);
}
}
}
impl<'ast> Visit<'ast> for ReadVisitor {
fn visit_local(&mut self, local: &'ast syn::Local) {
if let Some(init) = &local.init {
self.visit_expr(&init.expr);
if let Some((_, _, diverge)) = init.diverge.as_ref().map(|d| ((), (), d.1.as_ref())) {
self.visit_expr(diverge);
}
}
}
fn visit_expr_path(&mut self, path: &'ast syn::ExprPath) {
if let Some(ident) = path.path.get_ident() {
self.record(ident.to_string());
}
syn::visit::visit_expr_path(self, path);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn analyze(src: &str, i: usize, j: usize) -> RunDataflow {
let func: syn::ItemFn = syn::parse_str(src).expect("parse fn");
analyze_run(&func, i, j).expect("dataflow should resolve")
}
fn names(pairs: &[(String, syn::Type)]) -> Vec<String> {
pairs.iter().map(|(n, _)| n.clone()).collect()
}
#[test]
fn inputs_are_pre_run_reads() {
let df = analyze(
"fn f(a: u32, b: u32) -> u32 { let s = a + b; let t = s * 2; t }",
0,
1,
);
assert_eq!(df.defs, vec!["s".to_string()]);
let mut got = names(&df.inputs);
got.sort();
assert_eq!(got, vec!["a".to_string(), "b".to_string()]);
assert_eq!(names(&df.outputs), vec!["s".to_string()]);
}
#[test]
fn multiple_outputs_tracked() {
let df = analyze(
"fn g(a: i32, b: i32) -> i32 { let p = a + b; let q = a - b; p * q }",
0,
2,
);
let mut outs = names(&df.outputs);
outs.sort();
assert_eq!(outs, vec!["p".to_string(), "q".to_string()]);
let mut ins = names(&df.inputs);
ins.sort();
assert_eq!(ins, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn dead_binding_is_not_an_output() {
let df = analyze(
"fn h(a: u32) -> u32 { let dead = a + 1; let live = a * 3; live }",
0,
1,
);
assert!(df.outputs.is_empty(), "dead binding must not be live-out");
assert_eq!(df.defs, vec!["dead".to_string()]);
}
#[test]
fn type_inference_resolves_let_output_type() {
let df = analyze("fn f(a: u32, b: u32) -> u32 { let s = a + b; s + 1 }", 0, 1);
assert_eq!(df.outputs.len(), 1);
let (name, ty) = &df.outputs[0];
assert_eq!(name, "s");
let rendered = quote::quote!(#ty).to_string();
assert_eq!(rendered, "u32");
}
}