1use std::path::{Path, PathBuf};
7
8use crate::binding::BindingRegistry;
9use crate::book_gen::generate_contract_page;
10use crate::coq_gen::generate_coq_spec;
11use crate::graph::{DependencyGraph, dependency_graph};
12use crate::invariant_gen::generate_invariants;
13use crate::kani_gen::generate_kani_harnesses;
14use crate::probar_gen::{generate_probar_tests, generate_wired_probar_tests};
15use crate::scaffold::generate_trait;
16use crate::schema::Contract;
17
18#[derive(Debug, Clone)]
20pub struct GeneratedFiles {
21 pub files: Vec<GeneratedFile>,
23}
24
25#[derive(Debug, Clone)]
27pub struct GeneratedFile {
28 pub relative_path: PathBuf,
30 pub absolute_path: PathBuf,
32 pub kind: ArtifactKind,
34 pub bytes: usize,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ArtifactKind {
41 Scaffold,
42 KaniHarness,
43 ProbarTest,
44 WiredProbarTest,
45 BookPage,
46 Invariants,
47 CoqSpec,
48}
49
50impl std::fmt::Display for ArtifactKind {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::Scaffold => write!(f, "scaffold"),
54 Self::KaniHarness => write!(f, "kani"),
55 Self::ProbarTest => write!(f, "probar"),
56 Self::WiredProbarTest => write!(f, "wired-probar"),
57 Self::BookPage => write!(f, "book-page"),
58 Self::Invariants => write!(f, "invariants"),
59 Self::CoqSpec => write!(f, "coq"),
60 }
61 }
62}
63
64pub fn generate_all(
76 contract: &Contract,
77 stem: &str,
78 output_dir: &Path,
79 binding: Option<&BindingRegistry>,
80) -> Result<GeneratedFiles, std::io::Error> {
81 std::fs::create_dir_all(output_dir)?;
82
83 let mut files = Vec::new();
84
85 let scaffold_content = generate_trait(contract);
87 let scaffold_path = output_dir.join(format!("{stem}_scaffold.rs"));
88 std::fs::write(&scaffold_path, &scaffold_content)?;
89 files.push(GeneratedFile {
90 relative_path: PathBuf::from(format!("{stem}_scaffold.rs")),
91 absolute_path: scaffold_path,
92 kind: ArtifactKind::Scaffold,
93 bytes: scaffold_content.len(),
94 });
95
96 let kani_content = generate_kani_harnesses(contract);
98 let kani_path = output_dir.join(format!("{stem}_kani.rs"));
99 std::fs::write(&kani_path, &kani_content)?;
100 files.push(GeneratedFile {
101 relative_path: PathBuf::from(format!("{stem}_kani.rs")),
102 absolute_path: kani_path,
103 kind: ArtifactKind::KaniHarness,
104 bytes: kani_content.len(),
105 });
106
107 let probar_content = generate_probar_tests(contract);
109 let probar_path = output_dir.join(format!("{stem}_probar.rs"));
110 std::fs::write(&probar_path, &probar_content)?;
111 files.push(GeneratedFile {
112 relative_path: PathBuf::from(format!("{stem}_probar.rs")),
113 absolute_path: probar_path,
114 kind: ArtifactKind::ProbarTest,
115 bytes: probar_content.len(),
116 });
117
118 if let Some(reg) = binding {
120 let contract_file = format!("{stem}.yaml");
121 let wired_content = generate_wired_probar_tests(contract, &contract_file, reg);
122 let wired_path = output_dir.join(format!("{stem}_wired_probar.rs"));
123 std::fs::write(&wired_path, &wired_content)?;
124 files.push(GeneratedFile {
125 relative_path: PathBuf::from(format!("{stem}_wired_probar.rs")),
126 absolute_path: wired_path,
127 kind: ArtifactKind::WiredProbarTest,
128 bytes: wired_content.len(),
129 });
130 }
131
132 let invariant_content = generate_invariants(contract);
134 if !invariant_content.is_empty() {
135 let inv_path = output_dir.join(format!("{stem}_invariants.rs"));
136 std::fs::write(&inv_path, &invariant_content)?;
137 files.push(GeneratedFile {
138 relative_path: PathBuf::from(format!("{stem}_invariants.rs")),
139 absolute_path: inv_path,
140 kind: ArtifactKind::Invariants,
141 bytes: invariant_content.len(),
142 });
143 }
144
145 let coq_content = generate_coq_spec(contract, stem);
147 let coq_path = output_dir.join(format!("{stem}.v"));
148 std::fs::write(&coq_path, &coq_content)?;
149 files.push(GeneratedFile {
150 relative_path: PathBuf::from(format!("{stem}.v")),
151 absolute_path: coq_path,
152 kind: ArtifactKind::CoqSpec,
153 bytes: coq_content.len(),
154 });
155
156 let single_graph = build_single_contract_graph(contract, stem);
158 let book_content = generate_contract_page(contract, stem, &single_graph);
159 let book_path = output_dir.join(format!("{stem}_book.md"));
160 std::fs::write(&book_path, &book_content)?;
161 files.push(GeneratedFile {
162 relative_path: PathBuf::from(format!("{stem}_book.md")),
163 absolute_path: book_path,
164 kind: ArtifactKind::BookPage,
165 bytes: book_content.len(),
166 });
167
168 Ok(GeneratedFiles { files })
169}
170
171fn build_single_contract_graph(contract: &Contract, stem: &str) -> DependencyGraph {
174 let refs = vec![(stem.to_string(), contract)];
175 dependency_graph(&refs)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::schema::parse_contract_str;
182
183 fn minimal_contract() -> Contract {
184 parse_contract_str(
185 r#"
186metadata:
187 version: "1.0.0"
188 description: "Test"
189 references: ["Paper"]
190equations:
191 f:
192 formula: "f(x) = x"
193proof_obligations:
194 - type: invariant
195 property: "output finite"
196falsification_tests:
197 - id: FALSIFY-001
198 rule: "finiteness"
199 prediction: "output is always finite"
200 if_fails: "overflow"
201kani_harnesses:
202 - id: KANI-001
203 obligation: OBL-001
204 bound: 16
205"#,
206 )
207 .unwrap()
208 }
209
210 #[test]
211 fn generate_all_without_binding() {
212 let c = minimal_contract();
213 let dir = tempfile::tempdir().unwrap();
214 let result = generate_all(&c, "test-kernel-v1", dir.path(), None).unwrap();
215 assert_eq!(result.files.len(), 5); assert!(
217 result
218 .files
219 .iter()
220 .any(|f| f.kind == ArtifactKind::Scaffold)
221 );
222 assert!(
223 result
224 .files
225 .iter()
226 .any(|f| f.kind == ArtifactKind::KaniHarness)
227 );
228 assert!(
229 result
230 .files
231 .iter()
232 .any(|f| f.kind == ArtifactKind::ProbarTest)
233 );
234 assert!(
235 result
236 .files
237 .iter()
238 .any(|f| f.kind == ArtifactKind::BookPage)
239 );
240 for f in &result.files {
241 assert!(f.absolute_path.exists());
242 assert!(f.bytes > 0);
243 }
244 }
245
246 #[test]
247 fn generate_all_with_binding() {
248 let c = minimal_contract();
249 let binding = crate::binding::parse_binding_str(
250 r#"
251version: "1.0.0"
252target_crate: test
253bindings:
254 - contract: test-kernel-v1.yaml
255 equation: f
256 module_path: "test::f"
257 function: f
258 signature: "fn f(x: &[f32]) -> Vec<f32>"
259 status: implemented
260"#,
261 )
262 .unwrap();
263 let dir = tempfile::tempdir().unwrap();
264 let result = generate_all(&c, "test-kernel-v1", dir.path(), Some(&binding)).unwrap();
265 assert_eq!(result.files.len(), 6); assert!(
267 result
268 .files
269 .iter()
270 .any(|f| f.kind == ArtifactKind::WiredProbarTest)
271 );
272 }
273
274 #[test]
275 fn generates_into_subdir() {
276 let c = minimal_contract();
277 let dir = tempfile::tempdir().unwrap();
278 let sub = dir.path().join("deep").join("nested");
279 let result = generate_all(&c, "test-kernel-v1", &sub, None).unwrap();
280 assert_eq!(result.files.len(), 5); assert!(sub.exists());
282 }
283
284 #[test]
285 fn artifact_kind_display() {
286 assert_eq!(ArtifactKind::Scaffold.to_string(), "scaffold");
287 assert_eq!(ArtifactKind::KaniHarness.to_string(), "kani");
288 assert_eq!(ArtifactKind::ProbarTest.to_string(), "probar");
289 assert_eq!(ArtifactKind::WiredProbarTest.to_string(), "wired-probar");
290 assert_eq!(ArtifactKind::BookPage.to_string(), "book-page");
291 }
292
293 #[test]
294 fn file_names_use_stem() {
295 let c = minimal_contract();
296 let dir = tempfile::tempdir().unwrap();
297 let result = generate_all(&c, "softmax-kernel-v1", dir.path(), None).unwrap();
298 let names: Vec<String> = result
299 .files
300 .iter()
301 .map(|f| f.relative_path.to_string_lossy().to_string())
302 .collect();
303 assert!(names.contains(&"softmax-kernel-v1_scaffold.rs".to_string()));
304 assert!(names.contains(&"softmax-kernel-v1_kani.rs".to_string()));
305 assert!(names.contains(&"softmax-kernel-v1_probar.rs".to_string()));
306 }
307}