1use std::{
2 fs::{self, OpenOptions},
3 hash::{DefaultHasher, Hash, Hasher},
4 io::{self, Write},
5 path::{Path, PathBuf},
6 process::{Command, Stdio},
7};
8
9use flux_common::{
10 bug,
11 dbg::{self, SpanTrace},
12 result::ResultExt,
13};
14use flux_config as config;
15use flux_middle::{
16 def_id::{FluxDefId, MaybeExternId},
17 global_env::GlobalEnv,
18 queries::QueryErr,
19 rty::{BinOp, BvSize, PrettyMap, Sort, deps},
20};
21use itertools::Itertools;
22use rustc_data_structures::{
23 fx::{FxHashMap, FxIndexSet},
24 unord::UnordMap,
25};
26use rustc_hir::def_id::DefId;
27use rustc_span::ErrorGuaranteed;
28
29use crate::{
30 fixpoint_encoding::{ConstDeps, InterpretedConst, SortDeps, fixpoint},
31 lean_format::{self, LeanCtxt, WithLeanCtxt, def_id_to_pascal_case, snake_case_to_pascal_case},
32};
33
34fn sort_name_fragment(sort: &Sort) -> String {
35 match sort {
36 Sort::Int => "Int".to_string(),
37 Sort::Bool => "Bool".to_string(),
38 Sort::Real => "Real".to_string(),
39 Sort::BitVec(BvSize::Fixed(n)) => format!("Bv{n}"),
40 Sort::BitVec(BvSize::Param(p)) => format!("BvParam{}", p.as_u32()),
41 Sort::BitVec(BvSize::Infer(_)) => "BvInfer".to_string(),
42 _ => {
43 format!("{sort:?}")
44 .chars()
45 .filter(|c| c.is_alphanumeric())
46 .collect()
47 }
48 }
49}
50
51fn prim_op_lean_name(op: &BinOp) -> String {
52 match op {
53 BinOp::Add(s) => format!("PrimOpAdd{}", sort_name_fragment(s)),
54 BinOp::Sub(s) => format!("PrimOpSub{}", sort_name_fragment(s)),
55 BinOp::Mul(s) => format!("PrimOpMul{}", sort_name_fragment(s)),
56 BinOp::Div(s) => format!("PrimOpDiv{}", sort_name_fragment(s)),
57 BinOp::Mod(s) => format!("PrimOpMod{}", sort_name_fragment(s)),
58 BinOp::BitAnd(s) => format!("PrimOpBitAnd{}", sort_name_fragment(s)),
59 BinOp::BitOr(s) => format!("PrimOpBitOr{}", sort_name_fragment(s)),
60 BinOp::BitXor(s) => format!("PrimOpBitXor{}", sort_name_fragment(s)),
61 BinOp::BitShl(s) => format!("PrimOpBitShl{}", sort_name_fragment(s)),
62 BinOp::BitShr(s) => format!("PrimOpBitShr{}", sort_name_fragment(s)),
63 BinOp::Gt(s) => format!("PrimOpGt{}", sort_name_fragment(s)),
64 BinOp::Ge(s) => format!("PrimOpGe{}", sort_name_fragment(s)),
65 BinOp::Lt(s) => format!("PrimOpLt{}", sort_name_fragment(s)),
66 BinOp::Le(s) => format!("PrimOpLe{}", sort_name_fragment(s)),
67 BinOp::Eq => "PrimOpEq".to_string(),
68 BinOp::Ne => "PrimOpNe".to_string(),
69 BinOp::And => "PrimOpAnd".to_string(),
70 BinOp::Or => "PrimOpOr".to_string(),
71 BinOp::Iff => "PrimOpIff".to_string(),
72 BinOp::Imp => "PrimOpImp".to_string(),
73 }
74}
75
76macro_rules! string_vec {
78 ($($s:expr),* $(,)?) => {
79 vec![$($s.to_string()),*]
80 };
81}
82
83fn vc_name(genv: GlobalEnv, def_id: DefId) -> String {
84 def_id_to_pascal_case(&def_id, &genv.tcx())
85}
86
87fn proof_name(genv: GlobalEnv, def_id: DefId) -> String {
88 format!("{}_proof", vc_name(genv, def_id))
89}
90
91fn project() -> String {
92 config::lean_project().to_string()
93}
94
95fn namespaced<F>(f: &mut fs::File, w: F) -> io::Result<()>
96where
97 F: Fn(&mut fs::File) -> io::Result<()>,
98{
99 let namespace = "F";
100 writeln!(f, "\nnamespace {namespace}\n")?;
101 w(f)?;
102 writeln!(f, "\nend {namespace}")
103}
104
105fn rename_dir_contents(src: &Path, dst: &Path) -> io::Result<()> {
108 if !dst.exists() {
110 fs::create_dir_all(dst)?;
111 }
112
113 for entry in fs::read_dir(src)? {
114 let entry = entry?;
115 let file_type = entry.file_type()?;
116 let src_path = entry.path();
117 let dst_path = dst.join(entry.file_name());
118
119 if file_type.is_dir() {
120 rename_dir_contents(&src_path, &dst_path)?;
122 } else {
123 fs::rename(&src_path, &dst_path)?;
127 }
128 }
129 Ok(())
130}
131
132fn constant_deps(expr: &fixpoint::Expr, acc: &mut FxIndexSet<fixpoint::Var>) {
133 match expr {
134 fixpoint::Expr::Var(v) if matches!(v, fixpoint::Var::Const(..)) => {
135 acc.insert(*v);
136 }
137 fixpoint::Expr::App(func, _, args, _) => {
138 constant_deps(func, acc);
139 args.iter().for_each(|expr| constant_deps(expr, acc));
140 }
141 fixpoint::Expr::And(inner) | fixpoint::Expr::Or(inner) => {
142 inner.iter().for_each(|expr| constant_deps(expr, acc));
143 }
144 fixpoint::Expr::Atom(_, inner)
145 | fixpoint::Expr::BinaryOp(_, inner)
146 | fixpoint::Expr::Imp(inner)
147 | fixpoint::Expr::Iff(inner)
148 | fixpoint::Expr::Let(_, inner) => {
149 inner.iter().for_each(|expr| constant_deps(expr, acc));
150 }
151 fixpoint::Expr::IfThenElse(inner) => {
152 inner.iter().for_each(|expr| constant_deps(expr, acc));
153 }
154 fixpoint::Expr::Quantifier(_, _, inner)
155 | fixpoint::Expr::Neg(inner)
156 | fixpoint::Expr::Not(inner)
157 | fixpoint::Expr::IsCtor(_, inner) => {
158 constant_deps(inner, acc);
159 }
160 fixpoint::Expr::Var(..) | fixpoint::Expr::Constant(..) | fixpoint::Expr::ThyFunc(..) => {}
161 fixpoint::Expr::WKVar(..) => unimplemented!(),
162 }
163}
164
165pub fn project_digest(genv: GlobalEnv) -> io::Result<u64> {
169 fn collect(dir: &Path, acc: &mut Vec<PathBuf>) -> io::Result<()> {
170 for entry in fs::read_dir(dir)? {
171 let path = entry?.path();
172 if path
173 .file_name()
174 .is_some_and(|name| name.to_string_lossy().starts_with('.'))
175 {
176 continue;
177 }
178 if path.is_dir() {
179 collect(&path, acc)?;
180 } else if path.extension().is_some_and(|ext| ext == "lean") {
181 acc.push(path);
182 }
183 }
184 Ok(())
185 }
186
187 let root = final_project_path(genv);
188 let mut paths = vec![];
189 collect(&root, &mut paths)?;
190 for config_file in ["lakefile.toml", "lakefile.lean", "lake-manifest.json", "lean-toolchain"] {
191 let path = root.join(config_file);
192 if path.exists() {
193 paths.push(path);
194 }
195 }
196 paths.sort();
197
198 let mut hasher = DefaultHasher::new();
199 for path in paths {
200 path.strip_prefix(&root).unwrap_or(&path).hash(&mut hasher);
201 fs::read(&path)?.hash(&mut hasher);
202 }
203 Ok(hasher.finish())
204}
205
206pub fn finalize(genv: GlobalEnv) -> io::Result<()> {
207 let project = project();
208 let src = genv.temp_dir().path().join(&project);
209 let dst = final_project_path(genv);
210 if src.exists() { rename_dir_contents(&src, &dst) } else { Ok(()) }
211}
212
213fn final_project_path(genv: GlobalEnv) -> PathBuf {
214 genv.lean_parent_dir().join(project())
215}
216
217fn project_path(genv: GlobalEnv, kind: FileKind) -> PathBuf {
218 let project = project();
219 match kind {
220 FileKind::Flux => genv.temp_dir().path().join(project),
221 FileKind::User => final_project_path(genv),
222 }
223}
224
225#[derive(Debug, PartialEq, Eq, Clone, Copy)]
226enum LakeSeverity {
227 Info,
228 Warning,
229 Error,
230}
231
232#[derive(Debug)]
234struct LakeDiagnostic {
235 severity: LakeSeverity,
236 file: Option<String>,
238 message: String,
239}
240
241impl LakeDiagnostic {
242 fn to_note(&self) -> String {
243 format!("{}: {}", self.file.as_deref().unwrap_or(""), self.message)
244 }
245
246 fn is_sorry(&self) -> bool {
247 self.severity == LakeSeverity::Warning
248 && self.message.contains("declaration uses")
249 && self.message.contains("sorry")
250 }
251}
252
253fn parse_lake_output(output: &str) -> Vec<LakeDiagnostic> {
257 let mut diagnostics: Vec<LakeDiagnostic> = vec![];
258 let mut open = false;
259 const SEVERITIES: [(&str, LakeSeverity); 3] = [
260 ("error: ", LakeSeverity::Error),
261 ("warning: ", LakeSeverity::Warning),
262 ("info: ", LakeSeverity::Info),
263 ];
264 for line in output.lines() {
265 let severity = SEVERITIES
266 .iter()
267 .find_map(|(prefix, severity)| line.strip_prefix(prefix).map(|rest| (*severity, rest)));
268 if let Some((severity, rest)) = severity {
269 let (file, message) = match rest.split_once(".lean:") {
270 Some((file, pos_and_msg)) => {
271 let message = pos_and_msg.splitn(3, ':').nth(2).unwrap_or("").trim_start();
273 (Some(format!("{file}.lean")), message.to_string())
274 }
275 None => (None, rest.to_string()),
276 };
277 diagnostics.push(LakeDiagnostic { severity, file, message });
278 open = true;
279 } else if line.starts_with(['✔', '⚠', '✖', 'ℹ'])
280 || line.starts_with("trace: ")
281 || line.starts_with("Some required")
282 || line.starts_with("Build completed")
283 {
284 open = false;
285 } else if open && let Some(diagnostic) = diagnostics.last_mut() {
286 diagnostic.message.push('\n');
287 diagnostic.message.push_str(line);
288 }
289 }
290 diagnostics
291}
292
293fn run_lake_build(genv: GlobalEnv, def_ids: &[DefId]) -> io::Result<Vec<LakeDiagnostic>> {
295 let out = Command::new("lake")
298 .arg("build")
299 .args(
300 def_ids
301 .iter()
302 .map(|def_id| LeanFile::Checking(*def_id).module(genv)),
303 )
304 .stdin(Stdio::null())
305 .stdout(Stdio::piped())
306 .stderr(Stdio::piped())
307 .current_dir(project_path(genv, FileKind::User))
308 .spawn()?
309 .wait_with_output()?;
310 let stdout = String::from_utf8_lossy(&out.stdout);
311 let stderr = String::from_utf8_lossy(&out.stderr);
312 Ok(parse_lake_output(&format!("{stdout}\n{stderr}")))
313}
314
315#[derive(Default)]
317struct ProofReport {
318 axioms: Option<String>,
320 errors: Vec<String>,
322}
323
324impl ProofReport {
325 fn status(self, checking_file: &str, sorries: &[String]) -> Result<(), Vec<String>> {
331 let mut problems = self.errors;
332 if let Some(axioms) = &self.axioms
333 && axioms.contains("sorryAx")
334 {
335 problems.push(format!(
336 "the proof depends on `sorryAx`, i.e., it uses `sorry` directly or through a lemma: {axioms}"
337 ));
338 problems.extend_from_slice(sorries);
339 }
340
341 if self.axioms.is_some() && problems.is_empty() {
342 Ok(())
343 } else if problems.is_empty() {
344 Err(vec![format!(
345 "lean did not check `{checking_file}`; a dependency may have failed to build \
346 (run `lake build` in the lean project for details)"
347 )])
348 } else {
349 Err(problems)
350 }
351 }
352}
353
354fn proof_statuses(
358 genv: GlobalEnv,
359 def_ids: &[DefId],
360 diagnostics: Vec<LakeDiagnostic>,
361) -> Vec<Result<(), Vec<String>>> {
362 let checking_files = def_ids
363 .iter()
364 .map(|def_id| LeanFile::Checking(*def_id).relative_path(genv))
365 .collect_vec();
366 let checking_idx: FxHashMap<&str, usize> = checking_files
367 .iter()
368 .enumerate()
369 .map(|(i, f)| (f.as_str(), i))
370 .collect();
371 let proof_idx: FxHashMap<String, usize> = def_ids
372 .iter()
373 .enumerate()
374 .map(|(i, def_id)| (LeanFile::Proof(*def_id).relative_path(genv), i))
375 .collect();
376
377 let mut reports: Vec<ProofReport> = def_ids.iter().map(|_| ProofReport::default()).collect();
378 let mut sorries = vec![];
379 for d in diagnostics {
380 if d.is_sorry() {
381 sorries.push(d.to_note());
382 }
383 let Some(file) = d.file.as_deref() else { continue };
384 if let Some(&i) = checking_idx.get(file) {
385 match d.severity {
386 LakeSeverity::Info if d.message.contains("axioms") => {
387 reports[i].axioms = Some(d.message);
388 }
389 LakeSeverity::Error => reports[i].errors.push(d.to_note()),
390 _ => {}
391 }
392 } else if let Some(&i) = proof_idx.get(file)
393 && d.severity == LakeSeverity::Error
394 {
395 reports[i].errors.push(d.to_note());
396 }
397 }
398
399 reports
400 .into_iter()
401 .zip(&checking_files)
402 .map(|(report, checking_file)| report.status(checking_file, &sorries))
403 .collect()
404}
405
406pub fn check_proofs(genv: GlobalEnv, def_ids: &[DefId]) -> Vec<Result<(), ErrorGuaranteed>> {
409 if def_ids.is_empty() {
410 return vec![];
411 }
412 dbg::log_verbose!("FLUX running lean proofs for {def_ids:?}");
413 let statuses = match run_lake_build(genv, def_ids) {
414 Ok(diagnostics) => proof_statuses(genv, def_ids, diagnostics),
415 Err(err) => {
416 let note = format!("failed to run `lake build`: {err}");
417 def_ids.iter().map(|_| Err(vec![note.clone()])).collect()
418 }
419 };
420 def_ids
421 .iter()
422 .zip(statuses)
423 .map(|(def_id, status)| {
424 status.map_err(|notes| {
425 let msg = format!("failed to check external proof `{}`", proof_name(genv, *def_id));
426 let path: PathBuf = LeanFile::Proof(*def_id)
428 .path(genv, false)
429 .components()
430 .collect();
431 let span = genv.tcx().def_span(*def_id);
432 let mut diag = genv.sess().dcx().struct_span_err(span, msg);
433 diag.note(format!("proof file: `{}`", path.display()));
434 for note in notes {
435 diag.note(note);
436 }
437 diag.emit_err()
438 })
439 })
440 .collect()
441}
442
443fn create_file_with_dirs<P: AsRef<Path>>(path: P) -> io::Result<Option<fs::File>> {
445 let path = path.as_ref();
446 if let Some(parent) = path.parent() {
447 fs::create_dir_all(parent)?;
448 }
449 match OpenOptions::new().write(true).create_new(true).open(path) {
450 Ok(file) => Ok(Some(file)),
451 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(None),
452 Err(e) => Err(e),
453 }
454}
455
456fn create_or_truncate_file_with_dirs<P: AsRef<Path>>(path: P) -> io::Result<fs::File> {
458 let path = path.as_ref();
459 if let Some(parent) = path.parent() {
460 fs::create_dir_all(parent)?;
461 }
462 OpenOptions::new()
463 .write(true)
464 .create(true)
465 .truncate(true)
466 .open(path)
467}
468
469#[derive(Eq, PartialEq, Hash, Debug, Clone)]
470pub enum FileKind {
471 Flux,
473 User,
475}
476
477#[derive(Eq, PartialEq, Hash, Debug, Clone)]
479pub enum LeanFile {
480 Basic,
482 Fluxlib,
484 OpaqueSort(String),
486 Struct(String),
488 OpaqueFun(String),
490 Fun(String),
492 OpaqueConst(String),
494 Vc(DefId),
496 Proof(DefId),
498 Checking(DefId),
500}
501
502impl LeanFile {
503 fn kind(&self) -> FileKind {
504 match self {
505 LeanFile::Basic
506 | LeanFile::Fluxlib
507 | LeanFile::Vc(_)
508 | LeanFile::Checking(_)
509 | LeanFile::Fun(_)
510 | LeanFile::OpaqueConst(_)
511 | LeanFile::Struct(_) => FileKind::Flux,
512 LeanFile::OpaqueSort(_) | LeanFile::OpaqueFun(_) | LeanFile::Proof(_) => FileKind::User,
513 }
514 }
515
516 fn segments(&self, genv: GlobalEnv) -> Vec<String> {
517 let project_name = snake_case_to_pascal_case(&project());
518 match self {
519 LeanFile::Basic => {
520 string_vec![project_name, "Basic"]
521 }
522 LeanFile::Fluxlib => {
523 string_vec![project_name, "Flux", "Prelude"]
524 }
525 LeanFile::OpaqueSort(name) => {
526 string_vec![project_name, "User", "Struct", name]
528 }
529 LeanFile::Struct(name) => {
530 string_vec![project_name, "Flux", "Struct", name]
532 }
533 LeanFile::OpaqueFun(name) => {
534 string_vec![project_name, "User", "Fun", name]
536 }
537 LeanFile::Fun(name) => {
538 string_vec![project_name, "Flux", "Fun", name]
540 }
541 LeanFile::OpaqueConst(name) => {
542 string_vec![project_name, "Flux", "Const", name]
543 }
544 LeanFile::Vc(def_id) => {
545 let name = vc_name(genv, *def_id);
546 string_vec![project_name, "Flux", "VC", name]
547 }
548 LeanFile::Proof(def_id) => {
549 let name = format!("{}Proof", vc_name(genv, *def_id));
550 string_vec![project_name, "User", "Proof", name]
551 }
552 LeanFile::Checking(def_id) => {
553 let name = vc_name(genv, *def_id);
554 string_vec![project_name, "Flux", "Checking", name]
555 }
556 }
557 }
558
559 fn path(&self, genv: GlobalEnv, force_final: bool) -> PathBuf {
561 let mut path =
562 if force_final { final_project_path(genv) } else { project_path(genv, self.kind()) };
563 for segment in self.segments(genv) {
564 path = path.join(segment);
565 }
566 path.set_extension("lean");
567 path
568 }
569
570 pub fn import(&self, genv: GlobalEnv) -> String {
571 format!("import {}", self.module(genv))
572 }
573
574 fn module(&self, genv: GlobalEnv) -> String {
576 self.segments(genv).join(".")
577 }
578
579 fn relative_path(&self, genv: GlobalEnv) -> String {
581 format!("{}.lean", self.segments(genv).join("/"))
582 }
583}
584
585pub struct LeanEncoder<'genv, 'tcx> {
586 genv: GlobalEnv<'genv, 'tcx>,
587 def_id: MaybeExternId,
588 pretty_var_map: PrettyMap<fixpoint::LocalVar>,
589 sort_deps: SortDeps,
590 fun_deps: Vec<fixpoint::FunDef>,
591 constants: ConstDeps,
592 kvar_decls: Vec<fixpoint::KVarDecl>,
593 constraint: fixpoint::Constraint,
594 sort_files: UnordMap<fixpoint::DataSort, LeanFile>,
595 fun_files: UnordMap<FluxDefId, LeanFile>,
596 const_files: UnordMap<fixpoint::Var, LeanFile>,
597 primop_var_map: UnordMap<fixpoint::GlobalVar, String>,
598}
599
600impl<'genv, 'tcx> LeanEncoder<'genv, 'tcx> {
601 fn lean_cx(&self) -> LeanCtxt<'_, 'genv, 'tcx> {
602 LeanCtxt {
603 genv: self.genv,
604 pretty_var_map: &self.pretty_var_map,
605 adt_map: &self.sort_deps.adt_map,
606 opaque_adt_map: &self.sort_deps.opaque_sorts,
607 primop_var_map: &self.primop_var_map,
608 hide_sort_vars: false,
609 }
610 }
611
612 fn datasort_name(&self, sort: &fixpoint::DataSort) -> String {
613 let name = format!("{}", WithLeanCtxt { item: sort, cx: &self.lean_cx() });
614 snake_case_to_pascal_case(&name)
615 }
616
617 fn lean_file_for_fun(&self, fun: &fixpoint::FunDef) -> LeanFile {
618 let name = self.var_name(&fun.name);
619 if fun.body.is_some() { LeanFile::Fun(name) } else { LeanFile::OpaqueFun(name) }
620 }
621
622 fn lean_file_for_interpreted_const(&self, const_: &InterpretedConst) -> LeanFile {
623 let name = self.var_name(&const_.0.name);
624 LeanFile::Fun(name)
625 }
626
627 fn var_name(&self, var: &fixpoint::Var) -> String {
628 let name = format!("{}", WithLeanCtxt { item: var, cx: &self.lean_cx() });
629 snake_case_to_pascal_case(&name)
630 }
631
632 fn post_import_preamble(&self) -> &str {
633 "open Classical\nset_option linter.unusedVariables false\n"
634 }
635
636 fn new(
637 genv: GlobalEnv<'genv, 'tcx>,
638 def_id: MaybeExternId,
639 pretty_var_map: PrettyMap<fixpoint::LocalVar>,
640 sort_deps: SortDeps,
641 fun_deps: Vec<fixpoint::FunDef>,
642 constants: ConstDeps,
643 kvar_decls: Vec<fixpoint::KVarDecl>,
644 constraint: fixpoint::Constraint,
645 ) -> io::Result<Self> {
646 let primop_var_map: UnordMap<fixpoint::GlobalVar, String> = constants
647 .opaque
648 .iter()
649 .filter_map(|(decl, op)| {
650 if let fixpoint::Var::Const(gvar, None) = decl.name {
651 Some((gvar, prim_op_lean_name(op)))
652 } else {
653 None
654 }
655 })
656 .collect();
657 let mut encoder = Self {
658 genv,
659 def_id,
660 pretty_var_map,
661 sort_deps,
662 fun_deps,
663 constants,
664 kvar_decls,
665 constraint,
666 fun_files: UnordMap::default(),
667 sort_files: UnordMap::default(),
668 const_files: UnordMap::default(),
669 primop_var_map,
670 };
671 encoder.fun_files = encoder.fun_files();
672 encoder.sort_files = encoder.sort_files();
673 encoder.const_files = encoder.const_files();
674 Ok(encoder)
675 }
676
677 fn run(&self) -> io::Result<()> {
678 self.generate_lake_project_if_not_present()?;
679 self.generate_lib_if_absent()?;
680 self.generate_vc_file()?;
681 self.generate_proof_if_absent()?;
682 Ok(())
683 }
684
685 fn fun_files(&self) -> UnordMap<FluxDefId, LeanFile> {
686 let mut res = UnordMap::default();
687 for fun_def in &self.fun_deps {
688 let fixpoint::Var::Global(_, did) = fun_def.name else {
689 bug!("expected global var with id")
690 };
691 res.insert(did, self.lean_file_for_fun(fun_def));
692 }
693 res
694 }
695
696 fn sort_files(&self) -> UnordMap<fixpoint::DataSort, LeanFile> {
697 let mut res = UnordMap::default();
698 for (_, sort) in &self.sort_deps.opaque_sorts {
699 let data_sort = sort.name.clone();
700 let name = self.datasort_name(&sort.name);
701 let file = LeanFile::OpaqueSort(name);
702 res.insert(data_sort, file);
703 }
704 for data_decl in &self.sort_deps.data_decls {
705 let data_sort = data_decl.name.clone();
706 let name = self.datasort_name(&data_decl.name);
707 let file = LeanFile::Struct(name);
708 res.insert(data_sort, file);
709 }
710 res
711 }
712
713 fn const_files(&self) -> UnordMap<fixpoint::Var, LeanFile> {
714 let mut res = UnordMap::default();
715 for (decl, _) in &self.constants.interpreted {
716 res.insert(decl.name, LeanFile::Fun(self.var_name(&decl.name)));
717 }
718 for (decl, op) in &self.constants.opaque {
719 res.insert(decl.name, LeanFile::OpaqueConst(prim_op_lean_name(op)));
720 }
721 res
722 }
723
724 fn generate_lake_project_if_not_present(&self) -> io::Result<()> {
725 let path = project_path(self.genv, FileKind::User).join("lakefile.toml");
726 if !path.exists() {
727 Command::new("lake")
728 .current_dir(self.genv.lean_parent_dir())
729 .arg("+v4.28.0")
730 .arg("new")
731 .arg(project())
732 .arg("lib")
733 .spawn()
734 .and_then(|mut child| child.wait())
735 .map(|_| ())?;
736 }
737 Ok(())
738 }
739
740 fn generate_opaque_sort_file_if_not_present(
741 &self,
742 sort: &fixpoint::SortDecl,
743 ) -> io::Result<()> {
744 let name = self.datasort_name(&sort.name);
745 let file = &LeanFile::OpaqueSort(name);
746
747 let path = file.path(self.genv, false);
748 if let Some(mut file) = create_file_with_dirs(path)? {
749 writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
750 writeln!(file, "{}", self.post_import_preamble())?;
751 namespaced(&mut file, |f| {
752 writeln!(f, "def {} := sorry", WithLeanCtxt { item: sort, cx: &self.lean_cx() })
753 })?;
754 file.sync_all()?;
755 }
756 Ok(())
757 }
758
759 fn data_decl_dependencies(&self, data_decl: &fixpoint::DataDecl) -> Vec<&LeanFile> {
760 let name = &data_decl.name;
761 let mut acc = vec![];
762 data_decl.deps(&mut acc);
763 acc.into_iter()
764 .map(|data_sort| {
765 self.sort_files.get(&data_sort).unwrap_or_else(|| {
766 panic!(
767 "Missing sort file for dependency {:?} of data decl {:?}",
768 data_sort, name
769 )
770 })
771 })
772 .unique()
773 .collect()
774 }
775
776 fn generate_struct_file_if_not_present(
777 &self,
778 data_decl: &fixpoint::DataDecl,
779 ) -> io::Result<()> {
780 let name = self.datasort_name(&data_decl.name);
781 let file = &LeanFile::Struct(name);
782 let path = file.path(self.genv, false);
783 if let Some(mut file) = create_file_with_dirs(path)? {
785 writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
787 for dep in self.data_decl_dependencies(data_decl) {
789 writeln!(file, "{}", dep.import(self.genv))?;
790 }
791 writeln!(file, "{}", self.post_import_preamble())?;
792
793 namespaced(&mut file, |f| {
795 writeln!(f, "{}", WithLeanCtxt { item: data_decl, cx: &self.lean_cx() })
796 })?;
797 file.sync_all()?;
798 }
799 Ok(())
800 }
801
802 fn sort_file(&self, sort: &fixpoint::DataSort) -> &LeanFile {
803 self.sort_files
804 .get(sort)
805 .unwrap_or_else(|| panic!("Missing sort file for sort {:?}", sort))
806 }
807
808 fn fun_file(&self, did: &FluxDefId) -> &LeanFile {
809 self.fun_files
810 .get(did)
811 .unwrap_or_else(|| panic!("Missing fun file for fun {:?}", did))
812 }
813
814 fn const_file(&self, name: &fixpoint::Var) -> &LeanFile {
815 self.const_files
816 .get(name)
817 .unwrap_or_else(|| panic!("Missing const file for const {name:?}"))
818 }
819
820 fn fun_def_dependencies(&self, did: FluxDefId, fun_def: &fixpoint::FunDef) -> Vec<&LeanFile> {
821 let mut res = vec![];
822
823 let mut sorts = vec![];
825 fun_def.sort.deps(&mut sorts);
826 for data_sort in sorts {
827 res.push(self.sort_file(&data_sort));
828 }
829
830 if !self.genv.spec_func(did).is_uif() {
832 let body = self.genv.inlined_body(did);
833 for dep_id in deps(&body) {
834 res.push(self.fun_file(&dep_id));
835 }
836 }
837
838 let mut deps = FxIndexSet::default();
839 if let Some(body) = &fun_def.body {
840 constant_deps(&body.expr, &mut deps);
841 }
842 for (decl, _) in &self.constants.interpreted {
843 if deps.contains(&decl.name) {
844 res.push(self.const_file(&decl.name));
845 }
846 }
847 res
848 }
849
850 fn generate_fun_def_file_if_not_present(
851 &self,
852 did: FluxDefId,
853 fun_def: &fixpoint::FunDef,
854 ) -> io::Result<()> {
855 let path = self.lean_file_for_fun(fun_def).path(self.genv, false);
856 if let Some(mut file) = create_file_with_dirs(path)? {
857 writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
859 for dep in self.fun_def_dependencies(did, fun_def) {
861 writeln!(file, "{}", dep.import(self.genv))?;
862 }
863 writeln!(file, "{}", self.post_import_preamble())?;
864
865 namespaced(&mut file, |f| {
867 writeln!(f, "{}", WithLeanCtxt { item: fun_def, cx: &self.lean_cx() })
868 })?;
869 file.sync_all()?;
870 }
871 Ok(())
872 }
873
874 fn generate_interpreted_const_file_if_not_present(
875 &self,
876 interpreted_const: &InterpretedConst,
877 ) -> io::Result<()> {
878 let (const_decl, _) = interpreted_const;
879 let path = self
880 .lean_file_for_interpreted_const(interpreted_const)
881 .path(self.genv, false);
882 if let Some(mut file) = create_file_with_dirs(path)? {
883 writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
885
886 let mut sort_deps = vec![];
887 const_decl.sort.deps(&mut sort_deps);
888 for dep in sort_deps {
889 writeln!(file, "{}", self.sort_file(&dep).import(self.genv))?;
890 }
891
892 writeln!(file, "{}", self.post_import_preamble())?;
893
894 namespaced(&mut file, |f| {
895 if let Some(comment) = &const_decl.comment {
896 writeln!(f, "--{comment}")?;
897 }
898 writeln!(f, "{}", WithLeanCtxt { item: interpreted_const, cx: &self.lean_cx() })
899 })?;
900 file.sync_all()?;
901 }
902 Ok(())
903 }
904
905 fn generate_opaque_const_file(
906 &self,
907 const_decl: &fixpoint::ConstDecl,
908 op: &BinOp,
909 ) -> io::Result<()> {
910 let stable_name = prim_op_lean_name(op);
911 let file = LeanFile::OpaqueConst(stable_name.clone());
912 let path = file.path(self.genv, false);
913 let mut file = create_or_truncate_file_with_dirs(path)?;
914 writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
915
916 let mut sort_deps = vec![];
917 const_decl.sort.deps(&mut sort_deps);
918 for dep in sort_deps {
919 writeln!(file, "{}", self.sort_file(&dep).import(self.genv))?;
920 }
921
922 writeln!(file, "{}", self.post_import_preamble())?;
923
924 namespaced(&mut file, |f| {
925 if let Some(comment) = &const_decl.comment {
926 writeln!(f, "--{comment}")?;
927 }
928 writeln!(
929 f,
930 "axiom {stable_name} : {}",
931 WithLeanCtxt { item: &const_decl.sort, cx: &self.lean_cx() }
932 )
933 })?;
934 file.sync_all()?;
935 Ok(())
936 }
937
938 fn generate_lib_if_absent(&self) -> io::Result<()> {
939 let path = LeanFile::Fluxlib.path(self.genv, false);
940 if let Some(mut file) = create_file_with_dirs(path)? {
941 writeln!(file, "-- FLUX LIBRARY [DO NOT MODIFY] --")?;
942 writeln!(
944 file,
945 "abbrev BitVec_shiftLeft {{ n : Nat }} (x s : BitVec n) : BitVec n := BitVec.shiftLeft x (s.toNat)"
946 )?;
947 writeln!(
948 file,
949 "abbrev BitVec_ushiftRight {{ n : Nat }} (x s : BitVec n) : BitVec n := BitVec.ushiftRight x (s.toNat)"
950 )?;
951 writeln!(
952 file,
953 "abbrev BitVec_sshiftRight {{ n : Nat }} (x s : BitVec n) : BitVec n := BitVec.sshiftRight x (s.toNat)"
954 )?;
955 writeln!(
956 file,
957 "abbrev BitVec_uge {{ n : Nat }} (x y : BitVec n) := (BitVec.ult x y).not"
958 )?;
959 writeln!(
960 file,
961 "abbrev BitVec_sge {{ n : Nat }} (x y : BitVec n) := (BitVec.slt x y).not"
962 )?;
963 writeln!(
964 file,
965 "abbrev BitVec_ugt {{ n : Nat }} (x y : BitVec n) := (BitVec.ule x y).not"
966 )?;
967 writeln!(
968 file,
969 "abbrev BitVec_sgt {{ n : Nat }} (x y : BitVec n) := (BitVec.sle x y).not"
970 )?;
971 writeln!(
972 file,
973 "abbrev BitVec_zeroExtend {{n : Nat}} (extra : Nat) (x : BitVec n) : BitVec (n + extra) := BitVec.zeroExtend (n + extra) x"
974 )?;
975 writeln!(
976 file,
977 "abbrev BitVec_signExtend {{n : Nat}} (extra : Nat) (x : BitVec n) : BitVec (n + extra) := BitVec.signExtend (n + extra) x"
978 )?;
979 writeln!(
980 file,
981 "abbrev SmtMap (t0 t1 : Type) [Inhabited t0] [BEq t0] [Inhabited t1] : Type := t0 -> t1"
982 )?;
983 writeln!(
984 file,
985 "abbrev SmtMap_default {{ t0 t1: Type }} (v : t1) [Inhabited t0] [BEq t0] [Inhabited t1] : SmtMap t0 t1 := fun _ => v"
986 )?;
987 writeln!(
988 file,
989 "abbrev SmtMap_store {{ t0 t1 : Type }} [Inhabited t0] [BEq t0] [Inhabited t1] (m : SmtMap t0 t1) (k : t0) (v : t1) : SmtMap t0 t1 :=\n fun x => if x == k then v else m x"
990 )?;
991 writeln!(
992 file,
993 "abbrev SmtMap_select {{ t0 t1 : Type }} [Inhabited t0] [BEq t0] [Inhabited t1] (m : SmtMap t0 t1) (k : t0) := m k"
994 )?;
995 }
996 Ok(())
997 }
998
999 fn generate_vc_prelude(&self) -> io::Result<()> {
1000 self.generate_lib_if_absent()?;
1002
1003 for (_, sort) in &self.sort_deps.opaque_sorts {
1005 self.generate_opaque_sort_file_if_not_present(sort)?;
1006 }
1007 for data_decl in &self.sort_deps.data_decls {
1009 self.generate_struct_file_if_not_present(data_decl)?;
1010 }
1011 for fun_def in &self.fun_deps {
1013 let fixpoint::Var::Global(_, did) = fun_def.name else {
1014 bug!("expected global var with id")
1015 };
1016 self.generate_fun_def_file_if_not_present(did, fun_def)?;
1017 }
1018 for const_decl in &self.constants.interpreted {
1020 self.generate_interpreted_const_file_if_not_present(const_decl)?;
1021 }
1022 for (const_decl, op) in &self.constants.opaque {
1024 self.generate_opaque_const_file(const_decl, op)?;
1025 }
1026 Ok(())
1027 }
1028
1029 fn generate_vc_imports(&self, file: &mut fs::File) -> io::Result<()> {
1030 writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
1031
1032 for (_, sort) in &self.sort_deps.opaque_sorts {
1033 let name = self.datasort_name(&sort.name);
1034 writeln!(file, "{}", LeanFile::OpaqueSort(name).import(self.genv))?;
1035 }
1036
1037 for data_decl in &self.sort_deps.data_decls {
1038 let name = self.datasort_name(&data_decl.name);
1039 writeln!(file, "{}", LeanFile::Struct(name).import(self.genv))?;
1040 }
1041
1042 for fun_def in &self.fun_deps {
1043 writeln!(file, "{}", self.lean_file_for_fun(fun_def).import(self.genv))?;
1044 }
1045
1046 for const_decl in &self.constants.interpreted {
1047 writeln!(
1048 file,
1049 "{}",
1050 self.lean_file_for_interpreted_const(const_decl)
1051 .import(self.genv)
1052 )?;
1053 }
1054
1055 for (_, op) in &self.constants.opaque {
1056 writeln!(file, "{}", LeanFile::OpaqueConst(prim_op_lean_name(op)).import(self.genv))?;
1057 }
1058
1059 Ok(())
1060 }
1061
1062 fn generate_vc_file(&self) -> io::Result<()> {
1063 self.generate_vc_prelude()?;
1065
1066 let def_id = self.def_id.resolved_id();
1068 let path = LeanFile::Vc(def_id).path(self.genv, false);
1069 if let Some(mut file) = create_file_with_dirs(path)? {
1070 self.generate_vc_imports(&mut file)?;
1071 writeln!(file, "{}", self.post_import_preamble())?;
1072
1073 let vc_name = vc_name(self.genv, def_id);
1074 namespaced(&mut file, |f| {
1076 write!(
1077 f,
1078 "{}",
1079 WithLeanCtxt {
1080 item: lean_format::LeanKConstraint {
1081 theorem_name: &vc_name,
1082 kvars: &self.kvar_decls,
1083 const_binds: &self.constants.const_binds,
1084 constr: &self.constraint,
1085 should_fail: self
1086 .def_id
1087 .as_local()
1088 .map(|def_id| self.genv.should_fail(def_id))
1089 .unwrap_or(false)
1090 },
1091 cx: &self.lean_cx()
1092 }
1093 )
1094 })?;
1095 file.sync_all()?;
1096 }
1097
1098 Ok(())
1099 }
1100
1101 fn generate_proof_if_absent(&self) -> io::Result<()> {
1102 let def_id = self.def_id.resolved_id();
1103 let vc_name = vc_name(self.genv, def_id);
1104 let proof_name = proof_name(self.genv, def_id);
1105 let path = LeanFile::Proof(def_id).path(self.genv, false);
1106
1107 if let Some(mut file) = create_file_with_dirs(path)? {
1108 writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
1109 writeln!(file, "{}", LeanFile::Vc(def_id).import(self.genv))?;
1110 writeln!(file, "{}", self.post_import_preamble())?;
1111 namespaced(&mut file, |f| {
1112 writeln!(f, "def {proof_name} : {vc_name} := by")?;
1113 writeln!(f, " unfold {vc_name}")?;
1114 writeln!(f, " sorry")
1115 })?;
1116 file.sync_all()?;
1117 }
1118 Ok(())
1119 }
1120
1121 pub fn encode(
1122 genv: GlobalEnv<'genv, 'tcx>,
1123 def_id: MaybeExternId,
1124 pretty_var_map: PrettyMap<fixpoint::LocalVar>,
1125 sort_deps: SortDeps,
1126 fun_deps: Vec<fixpoint::FunDef>,
1127 constants: ConstDeps,
1128 kvar_decls: Vec<fixpoint::KVarDecl>,
1129 constraint: fixpoint::Constraint,
1130 ) -> io::Result<()> {
1131 let encoder = Self::new(
1132 genv,
1133 def_id,
1134 pretty_var_map,
1135 sort_deps,
1136 fun_deps,
1137 constants,
1138 kvar_decls,
1139 constraint,
1140 )?;
1141 encoder.run()?;
1142 Ok(())
1143 }
1144}
1145
1146pub fn hyperlink_proof(genv: GlobalEnv, def_id: MaybeExternId) {
1151 let Some(span) = genv.proven_externally(def_id.local_id()) else { return };
1152 let proof_name = proof_name(genv, def_id.resolved_id());
1153 let path: PathBuf = LeanFile::Proof(def_id.resolved_id())
1155 .path(genv, false)
1156 .components()
1157 .collect();
1158 let (line, col) = fs::read_to_string(&path)
1159 .ok()
1160 .and_then(|contents| find_def(&contents, &proof_name))
1161 .unwrap_or((1, 1));
1162 let dst_span = SpanTrace::from_path(&path, line, col, proof_name.len());
1163 dbg::hyperlink_json!(genv.tcx(), span, dst_span);
1164}
1165
1166fn find_def(contents: &str, name: &str) -> Option<(usize, usize)> {
1168 contents.lines().enumerate().find_map(|(idx, line)| {
1169 let indent = line.len() - line.trim_start().len();
1170 let rest = line.trim_start().strip_prefix("def ")?;
1171 let after = rest.strip_prefix(name)?;
1172 if after.starts_with(|c: char| c.is_alphanumeric() || c == '_' || c == '\'') {
1174 return None;
1175 }
1176 Some((idx + 1, indent + "def ".len() + 1))
1177 })
1178}
1179
1180fn generate_checking_file(genv: GlobalEnv, def_id: DefId) -> io::Result<()> {
1184 let vc_name = vc_name(genv, def_id);
1185 let proof_name = proof_name(genv, def_id);
1186 let path = LeanFile::Checking(def_id).path(genv, false);
1187
1188 let mut file = create_or_truncate_file_with_dirs(path)?;
1189 writeln!(file, "{}", LeanFile::Vc(def_id).import(genv))?;
1190 writeln!(file, "{}", LeanFile::Proof(def_id).import(genv))?;
1191 writeln!(file)?;
1192 writeln!(file, "#check (F.{proof_name} : F.{vc_name})")?;
1193 writeln!(file, "#print axioms F.{proof_name}")?;
1194 file.sync_all()?;
1195 Ok(())
1196}
1197
1198fn record_proof(genv: GlobalEnv, def_id: MaybeExternId) -> io::Result<()> {
1199 generate_checking_file(genv, def_id.resolved_id())?;
1200 let path = LeanFile::Basic.path(genv, false);
1201
1202 let mut file = match create_file_with_dirs(&path)? {
1203 Some(mut file) => {
1204 writeln!(file, "-- Flux Basic Imports [DO NOT MODIFY] --")?;
1206 file
1207 }
1208 None => fs::OpenOptions::new().append(true).open(path)?,
1209 };
1210 writeln!(file, "{}", LeanFile::Checking(def_id.resolved_id()).import(genv))
1211}
1212
1213pub fn log_proof(genv: GlobalEnv, def_id: MaybeExternId) -> Result<(), ErrorGuaranteed> {
1216 record_proof(genv, def_id)
1217 .map_err(|_| {
1218 let msg =
1219 format!("failed to record proof `{}`", proof_name(genv, def_id.resolved_id()));
1220 let span = genv.tcx().def_span(def_id);
1221 QueryErr::Emitted(genv.sess().dcx().struct_span_err(span, msg).emit_err())
1222 })
1223 .emit(&genv)?;
1224 Ok(())
1225}