Skip to main content

flux_infer/
lean_encoding.rs

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
76/// Helper macro to create Vec<String> from string-like values
77macro_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
105// Via Gemini: https://gemini.google.com/share/9027e898b136
106/// Renames all files and directories from 'src' to 'dst'
107fn rename_dir_contents(src: &Path, dst: &Path) -> io::Result<()> {
108    // 1. Ensure the destination root exists
109    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            // 2. If it's a directory, recurse
121            rename_dir_contents(&src_path, &dst_path)?;
122        } else {
123            // 3. If it's a file, rename (overwrites if exists)
124            // On Windows, this will fail if the target file is "in use"
125            // TODO: how to FORCE overwrite on Windows?
126            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
165/// A digest of the lean sources in the project (skipping build artifacts in `.lake`) and of the lake
166/// configuration. A cached proof is only trusted if this digest is unchanged since the proof was
167/// checked, as the proof, or any lemma or dependency it uses, may have been edited in the meantime.
168pub 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/// A diagnostic reported by `lake build`, e.g., `error: LeanProofs/User/Proof/FooProof.lean:8:2: msg`
233#[derive(Debug)]
234struct LakeDiagnostic {
235    severity: LakeSeverity,
236    /// Path of the reported file relative to the lake project root, if any
237    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
253/// Parses the (human readable) output of `lake build` into a list of diagnostics. Each diagnostic
254/// starts with a line `<severity>: <file>:<line>:<col>: <message>` and extends over subsequent lines
255/// until the next diagnostic or a line reporting build progress.
256fn 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                    // skip `<line>:<col>: `
272                    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
293/// Runs a single `lake build` on the checking files of all the `def_ids`
294fn run_lake_build(genv: GlobalEnv, def_ids: &[DefId]) -> io::Result<Vec<LakeDiagnostic>> {
295    // NOTE: we must not pass `--quiet` or `--log-level=warning` as we rely on the `info` message
296    // produced by the `#check` in each checking file to know that the file was actually checked.
297    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/// What `lake build` reported about the external proof of a single function
316#[derive(Default)]
317struct ProofReport {
318    /// The message of the `#print axioms` in the checking file, if it was elaborated
319    axioms: Option<String>,
320    /// Errors reported in the checking or proof file
321    errors: Vec<String>,
322}
323
324impl ProofReport {
325    /// The proof is accepted iff the checking file was elaborated (witnessed by the `info` message
326    /// of its `#print axioms`), neither the checking nor the proof file report errors, and the proof
327    /// does not depend on `sorryAx`, i.e., neither the proof nor any lemma it (transitively) uses
328    /// has a `sorry`. On failure, returns the relevant messages reported by lean, where `sorries`
329    /// are all the `sorry` warnings in the build (one of which is the culprit for a `sorryAx`).
330    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
354/// Determines the status of the external proof of each of the `def_ids` with a single pass over the
355/// `diagnostics`, dispatching each one to the proof whose checking or proof file it is reported in.
356/// Returns the results in the same order as `def_ids`.
357fn 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
406/// Checks the external (lean) proofs of all the `def_ids` with a single `lake build`, reporting an
407/// error for each proof that fails to check. Returns the results in the same order as `def_ids`.
408pub 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                // Drop `.` segments, e.g. `crate/./lean_proofs` becomes `crate/lean_proofs`.
427                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
443/// Create a file at the given path, creating any missing parent directories.
444fn 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
456/// Create or truncate a file at the given path, creating any missing parent directories.
457fn 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    /// Files that are only written by Flux
472    Flux,
473    /// Files that are modified by users
474    User,
475}
476
477/// Different kinds of Lean files
478#[derive(Eq, PartialEq, Hash, Debug, Clone)]
479pub enum LeanFile {
480    /// "Root" of the lean project, importing all the generated checking files
481    Basic,
482    /// "builtin" definitions
483    Fluxlib,
484    /// (human) opaque flux sorts, to be defined by the user in Lean
485    OpaqueSort(String),
486    /// (machine) sorts generated from flux definitions
487    Struct(String),
488    /// (human) opaque flux functions, to be defined by the user in Lean
489    OpaqueFun(String),
490    /// (machine) functions generated from flux definitions
491    Fun(String),
492    /// (machine) opaque constant encoded as a Lean axiom
493    OpaqueConst(String),
494    /// (machine) propositions holding the flux VCs
495    Vc(DefId),
496    /// (human) interactively written proofs of flux VCs
497    Proof(DefId),
498    /// (machine) files checking that a proof has the expected theorem type
499    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                // let name = self.datasort_name(sort);
527                string_vec![project_name, "User", "Struct", name]
528            }
529            LeanFile::Struct(name) => {
530                // let name = self.datasort_name(sort);
531                string_vec![project_name, "Flux", "Struct", name]
532            }
533            LeanFile::OpaqueFun(name) => {
534                // let name = self.var_name(name);
535                string_vec![project_name, "User", "Fun", name]
536            }
537            LeanFile::Fun(name) => {
538                // let name = self.var_name(name);
539                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    /// All paths should be generated here
560    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    /// The lean module name, e.g., `LeanProofs.Flux.Checking.Foo`
575    fn module(&self, genv: GlobalEnv) -> String {
576        self.segments(genv).join(".")
577    }
578
579    /// The path relative to the project root, as reported in `lake` diagnostics
580    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        // No need to regenerate if created in this session; but otherwise regenerate as struct may have changed
784        if let Some(mut file) = create_file_with_dirs(path)? {
785            // import prelude
786            writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
787            // import sort dependencies
788            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            // write data decl
794            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        // 1. Collect the sort dependencies
824        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        // 2. Collect the fun dependencies
831        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            // import prelude
858            writeln!(file, "{}", LeanFile::Fluxlib.import(self.genv))?;
859            // import sort dependencies
860            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            // write fun def
866            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            // import prelude
884            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            // TODO: Can't we write this from a single `write!` call?
943            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        // 1. Generate lake project and lib file
1001        self.generate_lib_if_absent()?;
1002
1003        // 2. Generate Opaque Struct Files
1004        for (_, sort) in &self.sort_deps.opaque_sorts {
1005            self.generate_opaque_sort_file_if_not_present(sort)?;
1006        }
1007        // 2. Generate Struct Files
1008        for data_decl in &self.sort_deps.data_decls {
1009            self.generate_struct_file_if_not_present(data_decl)?;
1010        }
1011        // 3. Generate Func Def Files
1012        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        // 4. Generate Const Decl Files
1019        for const_decl in &self.constants.interpreted {
1020            self.generate_interpreted_const_file_if_not_present(const_decl)?;
1021        }
1022        // 5. Generate Opaque Const Files (primop axioms)
1023        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        // 1. Generate imports
1064        self.generate_vc_prelude()?;
1065
1066        // 2. Create file and add imports
1067        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            // 3. Write the VC
1075            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
1146/// Hyperlinks the `proven_externally` attribute to the proof's `def` in the proof file, so users
1147/// can easily jump to it. Call this after the Lean files are generated, so that the proof file
1148/// exists on the first run. The `def` is found by searching the file, since the user may have
1149/// edited it; if the file or the `def` is missing, we link to the first line of the file.
1150pub 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    // Drop `.` segments, e.g. `crate/./lean_proofs` becomes `crate/lean_proofs`.
1154    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
1166/// Returns the 1-based line and column of `name` in the line that defines it (`def <name> ...`).
1167fn 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        // Make sure we matched the whole name and not just a prefix of a longer one.
1173        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
1180/// Generates the file that checks that the proof has the type of the VC. The `#print axioms` is
1181/// used to reject proofs that (transitively) depend on `sorryAx`, see [`ProofReport::status`]. The file is
1182/// regenerated even when the VC is cached, so that it is always up to date.
1183fn 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            // First invocation: reset VCs
1205            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
1213/// We need to record the checking file in `Basic.lean` (so that it gets checked by `lake build`),
1214/// regardless of whether the proof was cached.
1215pub 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}