provable_contracts/discharge/
summary.rs1use std::collections::{BTreeMap, BTreeSet};
14use std::path::{Path, PathBuf};
15
16use serde::{Deserialize, Serialize};
17
18use super::{escapes, is_theorem, Report, Tree};
19
20pub const SUMMARY_FILE: &str = "discharge-summary.json";
22pub const LOG_FILE: &str = "discharge.json";
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Module {
28 pub path: String,
30 pub blake3: String,
32 pub theorems: Vec<String>,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Summary {
39 pub tree_sha: Option<String>,
41 pub toolchain: Option<String>,
43 pub mathlib_rev: Option<String>,
45 pub build_exit: Option<i32>,
47 pub lake_exit: Option<i32>,
49 pub leanchecker_exit: Option<i32>,
51 pub axioms_ok: bool,
53 pub escapes_ok: bool,
55 pub challenges_closed: Option<String>,
57 pub modules: Vec<Module>,
59}
60
61impl Summary {
62 #[must_use]
64 pub fn is_green(&self) -> bool {
65 self.build_exit == Some(0)
66 && self.lake_exit == Some(0)
67 && self.leanchecker_exit == Some(0)
68 && self.axioms_ok
69 && self.escapes_ok
70 }
71
72 #[must_use]
74 pub fn derived(&self) -> BTreeSet<&str> {
75 if !self.is_green() {
76 return BTreeSet::new();
77 }
78 self.modules
79 .iter()
80 .flat_map(|m| m.theorems.iter().map(String::as_str))
81 .collect()
82 }
83
84 #[must_use]
86 pub fn render(&self) -> String {
87 let mut s = serde_json::to_string_pretty(self)
88 .unwrap_or_else(|e| format!("{{\"error\": {:?}}}", e.to_string()));
89 s.push('\n');
90 s
91 }
92}
93
94#[must_use]
96pub fn summary_path(lean_dir: &Path) -> PathBuf {
97 lean_dir
99 .parent()
100 .unwrap_or_else(|| Path::new(""))
101 .join(SUMMARY_FILE)
102}
103
104pub fn load(path: &Path) -> Result<Summary, String> {
106 let raw = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
107 serde_json::from_str(&raw)
108 .map_err(|e| format!("{}: not a discharge summary: {e}", path.display()))
109}
110
111#[must_use]
114pub fn claim_derived(derived: &BTreeSet<&str>, theorem: &str) -> bool {
115 derived.contains(theorem) || derived.contains(format!("ProvableContracts.{theorem}").as_str())
116}
117
118#[must_use]
121pub fn modules(tree: &Tree) -> Vec<Module> {
122 let cone = tree.cone();
123 let escaped: BTreeSet<(String, String)> = escapes(tree)
124 .into_iter()
125 .map(|e| (e.file, e.decl))
126 .collect();
127 let mut out: Vec<Module> = tree
128 .files
129 .iter()
130 .filter(|f| cone.contains(&f.module))
131 .map(|f| {
132 let bytes = std::fs::read(tree.dir.join(&f.rel));
133 let (blake3, readable) = match &bytes {
134 Ok(b) => (blake3::hash(b).to_hex().to_string(), true),
135 Err(_) => (String::new(), false),
136 };
137 let theorems = f
138 .decls
139 .iter()
140 .filter(|d| readable && is_theorem(d))
141 .filter(|d| !escaped.contains(&(f.rel.clone(), d.fqn.clone())))
142 .map(|d| d.fqn.clone())
143 .collect();
144 Module {
145 path: f.rel.clone(),
146 blake3,
147 theorems,
148 }
149 })
150 .collect();
151 out.sort_by(|a, b| a.path.cmp(&b.path));
152 out
153}
154
155#[must_use]
157pub fn pins(lean_dir: &Path) -> (Option<String>, Option<String>) {
158 let toolchain = std::fs::read_to_string(lean_dir.join("lean-toolchain"))
159 .ok()
160 .map(|s| s.trim().to_string())
161 .filter(|s| !s.is_empty());
162 let mathlib = std::fs::read_to_string(lean_dir.join("lake-manifest.json"))
163 .ok()
164 .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
165 .and_then(|doc| {
166 doc.get("packages")?
167 .as_array()?
168 .iter()
169 .find(|p| p.get("name").and_then(|n| n.as_str()) == Some("mathlib"))?
170 .get("rev")?
171 .as_str()
172 .map(str::to_string)
173 });
174 (toolchain, mathlib)
175}
176
177#[must_use]
180pub fn summarize(
181 r: &Report,
182 tree: Option<&Tree>,
183 lean_dir: &Path,
184 tree_sha: Option<String>,
185 build_exit: Option<i32>,
186) -> Summary {
187 let (toolchain, mathlib_rev) = pins(lean_dir);
188 Summary {
189 tree_sha,
190 toolchain,
191 mathlib_rev,
192 build_exit,
193 lake_exit: r.lake_exit,
194 leanchecker_exit: r.leanchecker_exit,
195 axioms_ok: r.axioms_fresh == Some(true) && r.lake_exit == Some(0),
196 escapes_ok: r.escapes_ok == Some(true),
197 challenges_closed: r.challenges.map(|c| format!("{}/{}", c.closed, c.total)),
198 modules: tree.map(modules).unwrap_or_default(),
199 }
200}
201
202#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "kebab-case")]
206pub enum L4Source {
207 Discharge,
208 #[default]
209 SelfDeclared,
210}
211
212#[derive(Debug, Clone, Default, PartialEq, Eq)]
214pub struct Discharged {
215 pub theorems: BTreeSet<String>,
216 pub withheld: Option<String>,
218 pub unrefined: BTreeSet<String>,
220 pub module_of: BTreeMap<String, String>,
222}
223
224impl Discharged {
225 #[must_use]
227 pub fn grants(&self, theorem: &str) -> bool {
228 let t = theorem.trim().trim_matches('"');
229 self.theorems.contains(t) || self.theorems.contains(&format!("ProvableContracts.{t}"))
230 }
231
232 pub fn require_refinement(&mut self, l4_modules: &BTreeSet<String>) {
235 let (kept, dropped) = std::mem::take(&mut self.theorems)
236 .into_iter()
237 .partition(|t| {
238 self.module_of
239 .get(t)
240 .is_some_and(|m| l4_modules.contains(m))
241 });
242 self.theorems = kept;
243 self.unrefined = dropped;
244 }
245}
246
247fn all_challenges_closed(closed: Option<&str>) -> bool {
249 let Some((n, m)) = closed.and_then(|c| c.split_once('/')) else {
250 return false;
251 };
252 matches!((n.trim().parse::<u32>(), m.trim().parse::<u32>()), (Ok(n), Ok(m)) if m > 0 && n == m)
253}
254
255#[must_use]
259pub fn discharged(s: &Summary, current_tree_sha: Option<&str>) -> Discharged {
260 let withheld = if s.tree_sha.is_none() || s.tree_sha.as_deref() != current_tree_sha {
261 Some(format!(
262 "stale discharge: summary tree_sha {} != current lean tree {}",
263 s.tree_sha.as_deref().unwrap_or("null"),
264 current_tree_sha.unwrap_or("unknown")
265 ))
266 } else if !s.is_green() {
267 Some(format!(
268 "red discharge: build_exit {:?}, lake_exit {:?}, leanchecker_exit {:?}, axioms_ok {}, escapes_ok {}",
269 s.build_exit, s.lake_exit, s.leanchecker_exit, s.axioms_ok, s.escapes_ok
270 ))
271 } else if !all_challenges_closed(s.challenges_closed.as_deref()) {
272 Some(format!(
273 "challenges not closed: {}",
274 s.challenges_closed.as_deref().unwrap_or("never judged")
275 ))
276 } else {
277 None
278 };
279 let theorems = if withheld.is_none() {
280 s.derived().into_iter().map(str::to_string).collect()
281 } else {
282 BTreeSet::new()
283 };
284 let module_of = s
285 .modules
286 .iter()
287 .flat_map(|m| m.theorems.iter().map(|t| (t.clone(), m.path.clone())))
288 .collect();
289 Discharged {
290 theorems,
291 withheld,
292 unrefined: BTreeSet::new(),
293 module_of,
294 }
295}
296
297#[must_use]
300pub fn current_tree_sha(lean_dir: &Path) -> Option<String> {
301 let o = std::process::Command::new("git")
302 .args(["rev-parse", "HEAD:./"])
303 .current_dir(lean_dir)
304 .output()
305 .ok()?;
306 let sha = String::from_utf8_lossy(&o.stdout).trim().to_string();
307 (o.status.success() && !sha.is_empty()).then_some(sha)
308}
309
310#[cfg(test)]
311#[path = "summary_tests.rs"]
312mod tests;