use quote::ToTokens;
use std::collections::HashSet;
use std::path::Path;
use syn::{File, Item};
use walkdir::WalkDir;
use super::find_crate_name;
#[derive(Debug, Clone)]
pub struct ElicitType {
pub name: String,
pub shape: HarnessShape,
}
#[derive(Debug, Clone)]
pub enum HarnessShape {
Constructible {
first_variant: String,
},
KaniCompose,
NewtypeWrapper,
}
#[tracing::instrument(skip(root), fields(root = %root.as_ref().display()))]
pub fn scan_elicit_types(root: impl AsRef<Path>) -> Vec<ElicitType> {
let mut results: Vec<ElicitType> = Vec::new();
let mut seen_names: HashSet<String> = HashSet::new();
for entry in WalkDir::new(root.as_ref())
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file() && e.path().extension().is_some_and(|ext| ext == "rs"))
{
let path = entry.into_path();
let src = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
tracing::warn!(file = %path.display(), error = %e, "read failed");
continue;
}
};
let syntax: File = match syn::parse_str(&src) {
Ok(f) => f,
Err(e) => {
tracing::debug!(file = %path.display(), error = %e, "parse failed, skipping");
continue;
}
};
for item in &syntax.items {
if let Some(et) = extract_elicit_type(item)
&& seen_names.insert(et.name.clone())
{
results.push(et);
}
}
}
tracing::info!(count = results.len(), "elicit type scan complete");
results
}
#[tracing::instrument(skip(types, crate_root), fields(count = types.len()))]
pub fn generate_foundation_file(types: &[ElicitType], crate_root: impl AsRef<Path>) -> String {
let crate_name = find_crate_name(crate_root.as_ref());
let mut lines: Vec<String> = vec![
"// AUTO-GENERATED by `elicitation generate foundation` — DO NOT EDIT".to_string(),
"//".to_string(),
"// Kani constructibility harnesses for all #[derive(Elicit)] types.".to_string(),
"// Regenerate: elicitation generate foundation --crate-path <root>".to_string(),
String::new(),
];
if !types.is_empty() {
let type_list: Vec<&str> = types.iter().map(|t| t.name.as_str()).collect();
lines.push(format!("use {crate_name}::{{{}}};", type_list.join(", ")));
lines.push(String::new());
}
let mut seen_fns: HashSet<String> = HashSet::new();
for et in types {
let fn_name = harness_fn_name(et);
if !seen_fns.insert(fn_name.clone()) {
tracing::debug!(fn_name, "skipping duplicate harness");
continue;
}
lines.push(emit_harness(et, &fn_name));
}
lines.join("\n")
}
fn extract_elicit_type(item: &Item) -> Option<ElicitType> {
match item {
Item::Struct(s) if has_derive_elicit(&s.attrs) => {
let shape = if has_cfg_attr_kani_compose(&s.attrs) || has_derive_kani_compose(&s.attrs)
{
HarnessShape::KaniCompose
} else {
HarnessShape::NewtypeWrapper
};
Some(ElicitType {
name: s.ident.to_string(),
shape,
})
}
Item::Enum(e) if has_derive_elicit(&e.attrs) => {
let first_unit = e
.variants
.iter()
.find(|v| v.fields.is_empty())
.map(|v| v.ident.to_string());
match first_unit {
Some(variant) if e.variants.iter().all(|v| v.fields.is_empty()) => {
Some(ElicitType {
name: e.ident.to_string(),
shape: HarnessShape::Constructible {
first_variant: variant,
},
})
}
_ => {
let shape = if has_cfg_attr_kani_compose(&e.attrs)
|| has_derive_kani_compose(&e.attrs)
{
HarnessShape::KaniCompose
} else {
HarnessShape::NewtypeWrapper
};
Some(ElicitType {
name: e.ident.to_string(),
shape,
})
}
}
}
_ => None,
}
}
fn has_derive_elicit(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| {
if !a.path().is_ident("derive") {
return false;
}
a.parse_args_with(
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
)
.map(|paths| {
paths
.iter()
.any(|p| p.segments.last().is_some_and(|seg| seg.ident == "Elicit"))
})
.unwrap_or(false)
})
}
fn has_cfg_attr_kani_compose(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| {
if !a.path().is_ident("cfg_attr") {
return false;
}
a.to_token_stream().to_string().contains("KaniCompose")
})
}
fn has_derive_kani_compose(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| {
if !a.path().is_ident("derive") {
return false;
}
a.to_token_stream().to_string().contains("KaniCompose")
})
}
fn harness_fn_name(et: &ElicitType) -> String {
let lower = to_snake(&et.name);
match &et.shape {
HarnessShape::Constructible { .. } => format!("verify_{lower}_constructible"),
HarnessShape::KaniCompose => format!("verify_{lower}_kani_compose"),
HarnessShape::NewtypeWrapper => format!("verify_{lower}_newtype_wrapper"),
}
}
fn emit_harness(et: &ElicitType, fn_name: &str) -> String {
match &et.shape {
HarnessShape::Constructible { first_variant } => {
format!(
"#[kani::proof]\nfn {fn_name}() {{\n let _: {ty} = {ty}::{v};\n}}",
ty = et.name,
v = first_variant,
)
}
HarnessShape::KaniCompose => {
format!(
"#[cfg_attr(kani, ::kani::proof)]\nfn {fn_name}() {{\n let _: {ty} = <{ty} as ::elicitation::KaniCompose>::kani_depth0();\n}}",
ty = et.name,
)
}
HarnessShape::NewtypeWrapper => {
format!(
"#[cfg_attr(kani, ::kani::proof)]\nfn {fn_name}() {{\n let _: {ty} = kani::any();\n}}",
ty = et.name,
)
}
}
}
fn to_snake(s: &str) -> String {
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i != 0 {
out.push('_');
}
out.push(c.to_ascii_lowercase());
}
out
}