use std::collections::BTreeSet;
use std::path::Path;
use crate::cli::generate::{
ImportStyle, TypeResolver, find_crate_name,
scanner::{ArgKind, TransitionFn, VsmDescriptor},
};
pub fn generate_creusot_shared_file() -> String {
let lines = vec![
"// AUTO-GENERATED by `elicitation generate creusot` — DO NOT EDIT".to_string(),
"//".to_string(),
"// Shared elicitation extern_specs for this crate's Creusot companions.".to_string(),
"// Regenerate: elicitation generate creusot --crate-path <root>".to_string(),
"//".to_string(),
"// These are emitted once per crate to avoid duplicate extern_spec! errors.".to_string(),
"#![allow(unexpected_cfgs)]".to_string(),
String::new(),
"#[cfg(creusot)]".to_string(),
"use ::creusot_std::prelude::*;".to_string(),
"#[cfg(creusot)]".to_string(),
"use elicitation::{Established, Prop, ProvableFrom};".to_string(),
String::new(),
emit_established_extern_specs(),
String::new(),
];
lines.join("\n") + "\n"
}
#[tracing::instrument(skip(vsm, crate_root), fields(machine = %vsm.machine))]
pub fn generate_creusot_file(
vsm: &VsmDescriptor,
crate_root: impl AsRef<Path>,
) -> Result<String, String> {
generate_creusot_file_with_style(vsm, crate_root, ImportStyle::ExternalCrate)
}
#[tracing::instrument(skip(vsm, crate_root), fields(machine = %vsm.machine))]
pub fn generate_creusot_file_with_style(
vsm: &VsmDescriptor,
crate_root: impl AsRef<Path>,
import_style: ImportStyle,
) -> Result<String, String> {
let machine = &vsm.machine;
let state_ty = machine.replace("Machine", "State");
let crate_root = crate_root.as_ref();
let vsm_crate_root = vsm.source_file.parent().unwrap_or(crate_root);
let crate_name = find_crate_name(vsm_crate_root);
let inv_parts: Option<(String, String, String, String)> = match &vsm.invariant {
Some(p) => {
let source_fn_name = p.creusot_fn.as_deref().unwrap_or(p.name.as_str());
let generated_fn_name = format!("{source_fn_name}_creusot_logic");
let body = p.creusot_inv_body.as_deref().ok_or_else(|| {
let hint_fn = p.creusot_fn.as_deref().unwrap_or("creusot_invariant_fn");
format!(
"cannot generate Creusot companion for {machine}: \
invariant body for `{hint_fn}` not found\n \
hint: add `creusot_inv_body = \"...\"` to #[prop(...)] on `{name}`,\n \
or define `pub fn {hint_fn}(state: &{state_ty}) -> bool {{ ... }}`\n \
in a file under the scan root (without #[cfg(creusot)] gate)",
name = p.name,
)
})?;
Some((
generated_fn_name,
source_fn_name.to_string(),
p.name.clone(),
body.to_string(),
))
}
None => None,
};
let mut needed: BTreeSet<String> = BTreeSet::new();
TypeResolver::collect_type(&state_ty, &mut needed);
if let Some((_, _, consistent_ty, inv_body)) = &inv_parts {
TypeResolver::collect_type(consistent_ty, &mut needed);
TypeResolver::collect_type(inv_body, &mut needed);
}
for tfn in &vsm.transition_fns {
for arg in &tfn.args {
match &arg.kind {
ArgKind::State => {}
ArgKind::Proof { inner } => TypeResolver::collect_type(inner, &mut needed),
_ => TypeResolver::collect_type(&arg.ty, &mut needed),
}
}
if import_style == ImportStyle::InCrate {
if let Some(body) = &tfn.creusot_body {
TypeResolver::collect_type(body, &mut needed);
}
}
}
for name in &vsm.transitions {
needed.insert(name.clone());
}
let needs_kani_label = inv_parts
.as_ref()
.map(|(_, _, _, body)| body.contains("kani_label"))
.unwrap_or(false)
|| (import_style == ImportStyle::InCrate
&& vsm.transition_fns.iter().any(|tf| {
tf.creusot_body
.as_deref()
.is_some_and(|body| body.contains("kani_label"))
}));
let resolver = TypeResolver::build(&vsm.source_file, &crate_name, import_style);
let resolved_imports = resolver.grouped_imports(&needed);
let mut lines: Vec<String> = Vec::new();
lines.push("// AUTO-GENERATED by `elicitation generate creusot` — DO NOT EDIT".to_string());
lines.push("//".to_string());
lines.push(format!("// Creusot companion contracts for {machine}."));
lines.push("// Regenerate: elicitation generate creusot --crate-path <root>".to_string());
lines.push("//".to_string());
lines.push(
"// All items are gated with #[cfg(creusot)]; invisible to normal Rust builds.".to_string(),
);
lines.push(
"// The `creusot` cfg key is set by the Creusot toolchain, not by cargo.".to_string(),
);
lines.push("#![allow(unexpected_cfgs)]".to_string());
lines.push(String::new());
let mut fixed_imports = vec![
"::creusot_std::prelude::*".to_string(),
"elicitation::Established".to_string(),
];
if needs_kani_label {
fixed_imports.push("elicitation::kani_label".to_string());
}
for import in &fixed_imports {
lines.push("#[cfg(creusot)]".to_string());
lines.push(format!("use {import};"));
}
for import in &resolved_imports {
lines.push("#[cfg(creusot)]".to_string());
lines.push(format!("use {import};"));
}
lines.push(String::new());
if let Some((generated_inv_fn, _source_inv_fn, consistent_ty, inv_body)) = &inv_parts {
lines.push("#[cfg(creusot)]".to_string());
lines.push("#[logic]".to_string());
let body_expr = if inv_body.trim_start().starts_with("pearlite!") {
inv_body.to_string()
} else {
format!("pearlite! {{ {inv_body} }}")
};
let state_param = if inv_body.contains("state") {
"state"
} else {
"_state"
};
lines.push(format!(
"pub fn {generated_inv_fn}({state_param}: &{state_ty}) -> bool {{\n {body_expr}\n}}"
));
lines.push(String::new());
let snake = to_snake(consistent_ty);
lines.push("#[cfg(creusot)]".to_string());
lines.push("#[requires(true)]".to_string());
lines.push("#[ensures(result)]".to_string());
lines.push(format!(
"pub fn verify_{snake}_prop_creusot() -> bool {{ true }}"
));
lines.push(String::new());
for name in &vsm.transitions {
let tf = vsm.transition_fns.iter().find(|tf| &tf.name == name);
lines.push(emit_creusot_transition(
name,
&state_ty,
consistent_ty,
generated_inv_fn,
tf,
import_style,
));
}
}
Ok(lines.join("\n") + "\n")
}
fn emit_established_extern_specs() -> String {
[
"#[cfg(creusot)]",
"extern_spec! {",
" impl<P: Prop> Established<P> {",
" #[requires(true)]",
" fn prove<C>(_credential: &C) -> Established<P>",
" where P: ProvableFrom<C>;",
" }",
"}",
"",
"#[cfg(creusot)]",
"extern_spec! {",
" impl<P: Prop> Established<P> {",
" #[requires(true)]",
" fn assert() -> Established<P>;",
" }",
"}",
]
.join("\n")
}
fn emit_creusot_transition(
fn_name: &str,
state_ty: &str,
consistent_ty: &str,
inv_fn: &str,
tf: Option<&TransitionFn>,
import_style: ImportStyle,
) -> String {
let (params, call_args) = match tf {
Some(tf) => (emit_params(tf, state_ty), emit_call_args(tf)),
None => (
format!("state: {state_ty}, proof: Established<{consistent_ty}>"),
"state, proof".to_string(),
),
};
let ret = format!("({state_ty}, Established<{consistent_ty}>)");
let extra_requires = tf.map(|tf| tf.creusot_requires.as_slice()).unwrap_or(&[]);
let creusot_body = match import_style {
ImportStyle::ExternalCrate => None,
ImportStyle::InCrate => tf.and_then(|tf| {
if tf.has_instrument {
tf.creusot_body.as_deref()
} else {
None
}
}),
};
emit_creusot_transition_external(
fn_name,
¶ms,
&ret,
&call_args,
inv_fn,
extra_requires,
creusot_body,
)
}
fn emit_creusot_transition_external(
fn_name: &str,
params: &str,
ret: &str,
call_args: &str,
inv_fn: &str,
extra_requires: &[String],
creusot_body: Option<&str>,
) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push("#[cfg(creusot)]".to_string());
lines.push("extern_spec! {".to_string());
lines.push(format!(" #[requires({inv_fn}(&state))]"));
for req in extra_requires {
lines.push(format!(" #[requires({req})]"));
}
lines.push(format!(" #[ensures({inv_fn}(&result.0))]"));
lines.push(format!(" fn {fn_name}({params}) -> {ret};"));
lines.push("}".to_string());
lines.push(String::new());
if let Some(body) = creusot_body {
emit_requires_attrs(&mut lines, inv_fn, extra_requires);
lines.push(format!(
"fn {fn_name}_creusot_local({params}) -> {ret} {}",
format_creusot_body(body)
));
lines.push(String::new());
}
emit_requires_attrs(&mut lines, inv_fn, extra_requires);
lines.push("#[cfg(creusot)]".to_string());
let call_target = if creusot_body.is_some() {
format!("{fn_name}_creusot_local")
} else {
fn_name.to_string()
};
lines.push(format!(
"pub fn {fn_name}_creusot({params}) -> {ret} {{ {call_target}({call_args}) }}"
));
lines.push(String::new());
lines.join("\n")
}
fn emit_requires_attrs(lines: &mut Vec<String>, inv_fn: &str, extra_requires: &[String]) {
lines.push("#[cfg(creusot)]".to_string());
lines.push(format!("#[requires({inv_fn}(&state))]"));
for req in extra_requires {
lines.push(format!("#[requires({req})]"));
}
lines.push(format!("#[ensures({inv_fn}(&result.0))]"));
}
fn emit_params(tf: &TransitionFn, state_ty: &str) -> String {
let mut parts: Vec<String> = Vec::new();
for arg in &tf.args {
let (name, ty) = match &arg.kind {
ArgKind::State => ("state".to_string(), state_ty.to_string()),
ArgKind::Proof { inner } => (arg.name.clone(), format!("Established<{inner}>")),
_ => (arg.name.trim_start_matches('_').to_string(), arg.ty.clone()),
};
parts.push(format!("{name}: {ty}"));
}
parts.join(", ")
}
fn emit_call_args(tf: &TransitionFn) -> String {
tf.args
.iter()
.map(|arg| match arg.kind {
ArgKind::State => "state".to_string(),
ArgKind::Proof { .. } => arg.name.clone(),
_ => arg.name.trim_start_matches('_').to_string(),
})
.collect::<Vec<_>>()
.join(", ")
}
fn format_creusot_body(body: &str) -> String {
let trimmed = body.trim();
if trimmed.starts_with('{') {
trimmed.to_string()
} else {
format!("{{ {trimmed} }}")
}
}
fn to_snake(s: &str) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
}
out
}