use std::collections::HashMap;
use quote::format_ident;
use crate::smt::{prove_equivalent, Counterexample, Verdict};
use super::detector::Candidate;
use super::rewriter::Rewrite;
#[derive(Debug)]
pub(crate) enum GateDecision {
Commit { block_verified: bool },
Refuted(Counterexample),
Skip { reason: String },
}
pub(crate) fn verify_and_decide(
original_fn: &syn::ItemFn,
cand: &Candidate,
rewrite: &Rewrite,
) -> GateDecision {
let whole_result = try_whole_function_proof(original_fn, cand, rewrite);
match whole_result {
Some(Verdict::Verified) => {
return GateDecision::Commit {
block_verified: false,
}
}
Some(Verdict::Refuted(cx)) => return GateDecision::Refuted(cx),
_ => {}
}
block_proof(original_fn, cand, rewrite)
}
fn try_whole_function_proof(
original_fn: &syn::ItemFn,
cand: &Candidate,
rewrite: &Rewrite,
) -> Option<Verdict> {
let post_inlined = build_post_inlined(cand, rewrite)?;
Some(prove_equivalent(original_fn, &post_inlined))
}
fn build_post_inlined(cand: &Candidate, rewrite: &Rewrite) -> Option<syn::ItemFn> {
if cand.outputs.len() > 1 {
return None;
}
let rename: HashMap<String, syn::Expr> = cand
.inputs
.iter()
.map(|(name, _)| {
let id = format_ident!("{}", name);
(name.clone(), syn::parse_quote!(#id))
})
.collect();
let inlined_block = substitute_idents_in_block(&rewrite.helper.block, &rename)?;
let inlined_expr: syn::Expr = syn::Expr::Block(syn::ExprBlock {
attrs: Vec::new(),
label: None,
block: inlined_block,
});
let mut item = rewrite.rewritten_fn.clone();
let stmts = &mut item.block.stmts;
let (i, _j) = cand.stmt_range;
if cand.tail_is_output {
if let Some(last) = stmts.last_mut() {
if let syn::Stmt::Expr(_, None) = last {
*last = syn::Stmt::Expr(inlined_expr, None);
} else {
return None;
}
} else {
return None;
}
} else {
let stmt = stmts.get_mut(i)?;
let syn::Stmt::Local(local) = stmt else {
return None;
};
let init = local.init.as_mut()?;
*init.expr = inlined_expr;
}
Some(item)
}
fn block_proof(original_fn: &syn::ItemFn, cand: &Candidate, rewrite: &Rewrite) -> GateDecision {
if cand.tail_is_output {
return block_proof_tail(original_fn, cand, rewrite);
}
block_proof_named(original_fn, cand, rewrite)
}
fn block_proof_tail(
original_fn: &syn::ItemFn,
cand: &Candidate,
rewrite: &Rewrite,
) -> GateDecision {
let ret_ty = match &original_fn.sig.output {
syn::ReturnType::Type(_, ty) => (**ty).clone(),
syn::ReturnType::Default => {
return GateDecision::Skip {
reason: "tail-output run in a function with no return type".to_string(),
}
}
};
let run = match run_statements(original_fn, cand) {
Some(r) => r,
None => {
return GateDecision::Skip {
reason: "could not isolate the extracted statement run".to_string(),
}
}
};
let probe_orig = match build_probe(&cand.inputs, &ret_ty, run, None) {
Some(f) => f,
None => {
return GateDecision::Skip {
reason: "could not synthesise the original tail probe".to_string(),
}
}
};
let probe_helper = match build_probe(
&cand.inputs,
&ret_ty,
rewrite.helper.block.stmts.clone(),
None,
) {
Some(f) => f,
None => {
return GateDecision::Skip {
reason: "could not synthesise the helper tail probe".to_string(),
}
}
};
match prove_equivalent(&probe_orig, &probe_helper) {
Verdict::Verified => GateDecision::Commit {
block_verified: true,
},
Verdict::Refuted(cx) => GateDecision::Refuted(cx),
Verdict::Unsupported { reason } => GateDecision::Skip {
reason: format!("tail output: {reason}"),
},
}
}
fn block_proof_named(
original_fn: &syn::ItemFn,
cand: &Candidate,
rewrite: &Rewrite,
) -> GateDecision {
let run = match run_statements(original_fn, cand) {
Some(r) => r,
None => {
return GateDecision::Skip {
reason: "could not isolate the extracted statement run".to_string(),
}
}
};
let helper_leading = helper_leading_statements(rewrite);
for (name, ty) in &cand.outputs {
let id = format_ident!("{}", name);
let proj: syn::Expr = syn::parse_quote!(#id);
let probe_orig = match build_probe(&cand.inputs, ty, run.clone(), Some(proj.clone())) {
Some(f) => f,
None => {
return GateDecision::Skip {
reason: format!("could not synthesise the original probe for `{name}`"),
}
}
};
let probe_helper =
match build_probe(&cand.inputs, ty, helper_leading.clone(), Some(proj.clone())) {
Some(f) => f,
None => {
return GateDecision::Skip {
reason: format!("could not synthesise the helper probe for `{name}`"),
}
}
};
match prove_equivalent(&probe_orig, &probe_helper) {
Verdict::Verified => {}
Verdict::Refuted(cx) => return GateDecision::Refuted(cx),
Verdict::Unsupported { reason } => {
return GateDecision::Skip {
reason: format!("output `{name}`: {reason}"),
}
}
}
}
GateDecision::Commit {
block_verified: true,
}
}
fn build_probe(
inputs: &[(String, syn::Type)],
ret_ty: &syn::Type,
body: Vec<syn::Stmt>,
proj: Option<syn::Expr>,
) -> Option<syn::ItemFn> {
let probe_id = format_ident!("__splitrs_probe");
let mut params = syn::punctuated::Punctuated::<syn::FnArg, syn::token::Comma>::new();
for (name, ty) in inputs {
let id = format_ident!("{}", name);
let arg: syn::FnArg = syn::parse_quote!( #id: #ty );
params.push(arg);
}
let mut stmts = body;
match proj {
Some(expr) => stmts.push(syn::Stmt::Expr(expr, None)),
None => {
if !matches!(stmts.last(), Some(syn::Stmt::Expr(_, None))) {
return None;
}
}
}
let block = syn::Block {
brace_token: Default::default(),
stmts,
};
let probe: syn::ItemFn = syn::parse_quote! {
fn #probe_id ( #params ) -> #ret_ty #block
};
Some(probe)
}
fn run_statements(original_fn: &syn::ItemFn, cand: &Candidate) -> Option<Vec<syn::Stmt>> {
let (i, j) = cand.stmt_range;
let stmts = &original_fn.block.stmts;
if i >= j || j > stmts.len() {
return None;
}
Some(stmts[i..j].to_vec())
}
fn helper_leading_statements(rewrite: &Rewrite) -> Vec<syn::Stmt> {
let stmts = &rewrite.helper.block.stmts;
if let Some((syn::Stmt::Expr(_, None), leading)) = stmts.split_last() {
leading.to_vec()
} else {
stmts.clone()
}
}
fn substitute_idents_in_block(
block: &syn::Block,
rename: &HashMap<String, syn::Expr>,
) -> Option<syn::Block> {
if rename
.iter()
.all(|(k, v)| matches!(v, syn::Expr::Path(p) if p.path.is_ident(k)))
{
return Some(block.clone());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extraction::detector::find_candidate;
use crate::extraction::rewriter::rewrite as do_rewrite;
fn parse_fn(src: &str) -> syn::ItemFn {
syn::parse_str(src).expect("parse fn")
}
#[test]
fn correct_extraction_commits() {
let func = parse_fn("fn calc(a: u32, b: u32) -> u32 { let s = a + b; let t = s * 2; t }");
let cand = find_candidate(&func, 1).expect("candidate");
let rw = do_rewrite(&func, &cand, &["calc".to_string()]).expect("rewrite");
match verify_and_decide(&func, &cand, &rw) {
GateDecision::Commit { .. } => {}
other => panic!("expected Commit, got {other:?}"),
}
}
#[test]
fn mis_derived_helper_is_refuted() {
let func = parse_fn("fn calc(a: u32, b: u32) -> u32 { let s = a + b; let t = s * 2; t }");
let cand = find_candidate(&func, 1).expect("candidate");
let mut rw = do_rewrite(&func, &cand, &["calc".to_string()]).expect("rewrite");
rw.helper = parse_fn("fn calc_extracted(a: u32, b: u32) -> u32 { a - b }");
match verify_and_decide(&func, &cand, &rw) {
GateDecision::Refuted(_) => {}
other => panic!("expected Refuted for a mis-derived helper, got {other:?}"),
}
}
}