Skip to main content

meta_language/translation/
proof.rs

1//! Portable proof plans.
2//!
3//! A source proof keeps its structure: which variable is split by induction or case analysis,
4//! and the names of fields and induction hypotheses. It also keeps its hints: definitions to
5//! unfold, lemmas and hypotheses to use, and whether arithmetic or computation closes a goal.
6//! Each target emits its own tactics from the plan and its kernel re-checks the result, so a
7//! proof is never copied as text into a language that cannot check it.
8//!
9//! Mirrors `js/src/translation/proof.js`.
10
11use std::collections::HashMap;
12
13use super::Language;
14use super::diagnostics::{Result, type_error, unsupported};
15use super::ir::{Binder, Ctor, Expr, Field, Hints, Node, Plan, PlanCase, Proof, Prop, Split};
16use super::surface::{SProof, SSplit, SStep};
17use super::types::Type;
18
19/// What a name in a proof refers to.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum ProofName {
22    Function(String),
23    Theorem(String),
24    Other,
25}
26
27/// The checker's view of the program, as a proof sees it.
28pub trait ProofContext {
29    /// Resolves a (dotted) name from the theorem's module.
30    ///
31    /// # Errors
32    /// When the name climbs above the crate root.
33    fn lookup(&self, name: &str) -> Result<Option<ProofName>>;
34
35    /// The constructors of a data type.
36    fn ctors(&self, data: &str) -> Vec<Ctor>;
37}
38
39/// The theorem a proof belongs to, with its binders already hoisted.
40pub struct TheoremHead<'a> {
41    pub full_name: &'a str,
42    pub binders: &'a [Binder],
43}
44
45/// Normalises a surface proof into a portable plan.
46///
47/// # Errors
48/// On steps outside the portable proof model, unknown constructors and self-reference.
49pub fn normalise_proof(
50    proof: &SProof,
51    theorem: &TheoremHead<'_>,
52    context: &dyn ProofContext,
53) -> Result<Proof> {
54    let mut hints = Hints::default();
55    let locals: HashMap<String, Type> = theorem
56        .binders
57        .iter()
58        .map(|binder| (binder.name.clone(), binder.ty.clone()))
59        .collect();
60    let normaliser = Normaliser {
61        theorem,
62        context,
63        language: proof.source_language,
64    };
65    let plan = normaliser.sequence(&proof.steps, &mut hints, &locals)?;
66    Ok(Proof {
67        plan,
68        source: proof.source.clone().unwrap_or_default(),
69        source_language: proof.source_language,
70    })
71}
72
73fn merge_hints(into: &mut Hints, from: &Hints) {
74    let merge = |into: &mut Vec<String>, from: &[String]| {
75        for value in from {
76            if !into.contains(value) {
77                into.push(value.clone());
78            }
79        }
80    };
81    merge(&mut into.unfold, &from.unfold);
82    merge(&mut into.lemmas, &from.lemmas);
83    merge(&mut into.hyps, &from.hyps);
84    merge(&mut into.library, &from.library);
85    into.arith |= from.arith;
86    into.compute |= from.compute;
87}
88
89struct Normaliser<'a> {
90    theorem: &'a TheoremHead<'a>,
91    context: &'a dyn ProofContext,
92    language: Language,
93}
94
95impl Normaliser<'_> {
96    fn sequence(
97        &self,
98        steps: &[SStep],
99        hints: &mut Hints,
100        locals: &HashMap<String, Type>,
101    ) -> Result<Plan> {
102        for (index, step) in steps.iter().enumerate() {
103            match step {
104                SStep::Intro { names } => {
105                    // Leading binders are hoisted into the theorem's parameters, where the
106                    // target introduces them; later `intro`s have nothing left to name.
107                    if names.iter().any(|name| !locals.contains_key(name)) {
108                        return Err(unsupported(
109                            "intro",
110                            "introducing hypotheses beyond the theorem binders is outside the portable proof model",
111                            None,
112                        ));
113                    }
114                }
115                SStep::Unfold { names, .. } => {
116                    for name in names {
117                        self.add_rule(hints, name, locals, false)?;
118                    }
119                }
120                SStep::Rewrite { rules, .. } => {
121                    for rule in rules {
122                        self.add_rule(hints, &rule.name, locals, rule.reverse)?;
123                    }
124                }
125                SStep::Simp { rules, .. } => {
126                    for rule in rules {
127                        self.add_rule(hints, &rule.name, locals, rule.reverse)?;
128                    }
129                    hints.compute = true;
130                }
131                SStep::Compute { .. } => hints.compute = true,
132                SStep::Arith { .. } => hints.arith = true,
133                SStep::Induction(split) => {
134                    return self.split(split, true, &steps[index + 1..], hints, locals);
135                }
136                SStep::Cases(split) => {
137                    return self.split(split, false, &steps[index + 1..], hints, locals);
138                }
139            }
140        }
141        Ok(Plan::Close {
142            hints: hints.clone(),
143        })
144    }
145
146    fn add_rule(
147        &self,
148        hints: &mut Hints,
149        name: &str,
150        locals: &HashMap<String, Type>,
151        reverse: bool,
152    ) -> Result<()> {
153        let push = |list: &mut Vec<String>, value: &str| {
154            if !list.iter().any(|item| item == value) {
155                list.push(value.to_owned());
156            }
157        };
158        if locals.contains_key(name) {
159            push(&mut hints.hyps, name);
160            return Ok(());
161        }
162        match self.context.lookup(name)? {
163            Some(ProofName::Function(full_name)) => {
164                push(&mut hints.unfold, &full_name);
165                return Ok(());
166            }
167            Some(ProofName::Theorem(full_name)) => {
168                if full_name == self.theorem.full_name {
169                    return Err(type_error(format!("{name} uses itself"), None));
170                }
171                push(&mut hints.lemmas, &full_name);
172                return Ok(());
173            }
174            _ => {}
175        }
176        // Library facts such as `Nat.mul_add` or `Nat.add_comm`: every target
177        // closes arithmetic with its own decision procedure, so the name is kept
178        // for provenance only.
179        if !hints.library.iter().any(|item| item == name) {
180            hints.library.push(if reverse {
181                format!("<-{name}")
182            } else {
183                name.to_owned()
184            });
185        }
186        hints.arith = true;
187        Ok(())
188    }
189
190    fn split(
191        &self,
192        step: &SSplit,
193        induction: bool,
194        trailing: &[SStep],
195        hints: &Hints,
196        locals: &HashMap<String, Type>,
197    ) -> Result<Plan> {
198        let tactic = if induction { "induction" } else { "cases" };
199        let Some(ty) = locals.get(&step.variable) else {
200            return Err(unsupported(
201                &format!("{tactic} on {}", step.variable),
202                "only theorem binders can be split",
203                None,
204            ));
205        };
206        let Some(ctors) = self.constructors_of(ty) else {
207            return Err(unsupported(
208                &format!("{tactic} on {}", ty.key()),
209                "only natural numbers and data types can be split",
210                None,
211            ));
212        };
213        // Rocq names cases by position (`as [| k ih]` and one bullet per
214        // subgoal, in constructor order); Lean names them by constructor.
215        if step.positional && step.cases.len() > ctors.len() {
216            return Err(type_error(
217                format!(
218                    "{tactic} on {} has {} cases but {} has {} constructors",
219                    step.variable,
220                    step.cases.len(),
221                    ty.key(),
222                    ctors.len()
223                ),
224                None,
225            ));
226        }
227        let mut cases = Vec::new();
228        for (ctor_index, ctor) in ctors.iter().enumerate() {
229            let written = if step.positional {
230                step.cases.get(ctor_index)
231            } else {
232                step.cases
233                    .iter()
234                    .find(|kase| canonical_ctor(kase.ctor.as_deref(), ty) == ctor.name)
235            };
236            let recursive: Vec<bool> = ctor
237                .fields
238                .iter()
239                .map(|field| field.ty.key() == ty.key())
240                .collect();
241            let binds = written.map_or(&[][..], |kase| &kase.binds[..]);
242            let (fields, ihs) = assign_binds(binds, ctor, &recursive, induction, self.language)?;
243            let mut inner = locals.clone();
244            for (field, name) in ctor.fields.iter().zip(&fields) {
245                inner.insert(name.clone(), field.ty.clone());
246            }
247            for ih in &ihs {
248                inner.insert(ih.clone(), Type::Hypothesis);
249            }
250            let mut case_hints = Hints::default();
251            merge_hints(&mut case_hints, hints);
252            let mut steps: Vec<SStep> = written.map(|kase| kase.steps.clone()).unwrap_or_default();
253            steps.extend(trailing.iter().cloned());
254            let plan = self.sequence(&steps, &mut case_hints, &inner)?;
255            cases.push(PlanCase {
256                ctor: ctor.name.clone(),
257                fields,
258                ihs,
259                recursive,
260                plan,
261            });
262        }
263        if !step.positional {
264            for kase in &step.cases {
265                let name = canonical_ctor(kase.ctor.as_deref(), ty);
266                if !ctors.iter().any(|ctor| ctor.name == name) {
267                    return Err(type_error(
268                        format!(
269                            "{} has no constructor {}",
270                            ty.key(),
271                            kase.ctor.as_deref().unwrap_or("undefined")
272                        ),
273                        None,
274                    ));
275                }
276            }
277        }
278        let split = Split {
279            variable: step.variable.clone(),
280            ty: ty.clone(),
281            cases,
282        };
283        Ok(if induction {
284            Plan::Induction(split)
285        } else {
286            Plan::Cases(split)
287        })
288    }
289
290    fn constructors_of(&self, ty: &Type) -> Option<Vec<Ctor>> {
291        match ty {
292            Type::Nat => Some(vec![
293                Ctor {
294                    name: "zero".to_owned(),
295                    fields: Vec::new(),
296                },
297                Ctor {
298                    name: "succ".to_owned(),
299                    fields: vec![Field {
300                        name: "pred".to_owned(),
301                        ty: ty.clone(),
302                    }],
303                },
304            ]),
305            Type::Data { name } => Some(self.context.ctors(name)),
306            _ => None,
307        }
308    }
309}
310
311fn canonical_ctor(name: Option<&str>, ty: &Type) -> String {
312    let name = name.unwrap_or("undefined");
313    if matches!(ty, Type::Nat) {
314        return match name {
315            "zero" | "O" | "Nat.zero" => "zero",
316            "succ" | "S" | "Nat.succ" => "succ",
317            other => other,
318        }
319        .to_owned();
320    }
321    name.rsplit('.').next().unwrap_or(name).to_owned()
322}
323
324/// Lean lists all fields first and then one hypothesis per recursive field;
325/// Rocq's `as` patterns put each hypothesis right after its field.
326fn assign_binds(
327    binds: &[Option<String>],
328    ctor: &Ctor,
329    recursive: &[bool],
330    induction: bool,
331    language: Language,
332) -> Result<(Vec<String>, Vec<String>)> {
333    let mut fields = Vec::new();
334    let mut ihs = Vec::new();
335    let mut cursor = 0;
336    let mut fresh = 0;
337    let mut take = |prefix: &str| {
338        fresh += 1;
339        let fallback = format!("ml_{prefix}{fresh}");
340        let name = binds.get(cursor).cloned().flatten();
341        cursor += 1;
342        match name {
343            Some(name) if !name.is_empty() && name != "_" => name,
344            _ => fallback,
345        }
346    };
347    if language == Language::Rocq {
348        for &is_recursive in recursive.iter().take(ctor.fields.len()) {
349            fields.push(take("x"));
350            if induction && is_recursive {
351                ihs.push(take("ih"));
352            }
353        }
354    } else {
355        for _ in &ctor.fields {
356            fields.push(take("x"));
357        }
358        if induction {
359            for &is_recursive in recursive {
360                if is_recursive {
361                    ihs.push(take("ih"));
362                }
363            }
364        }
365    }
366    if cursor < binds.len() {
367        return Err(type_error(
368            format!("{} case names {} variables", ctor.name, binds.len()),
369            None,
370        ));
371    }
372    Ok((fields, ihs))
373}
374
375/// Functions, in first-use order, that a proposition mentions.
376#[must_use]
377pub fn prop_functions(prop: &Prop, mut into: Vec<String>) -> Vec<String> {
378    fn visit(expr: &Expr, into: &mut Vec<String>) {
379        if let Node::Call { func, .. } = &expr.node
380            && !into.contains(func)
381        {
382            into.push(func.clone());
383        }
384        for child in expr.children() {
385            visit(child, into);
386        }
387    }
388    fn visit_prop(prop: &Prop, into: &mut Vec<String>) {
389        match prop {
390            Prop::Forall { body, .. } => visit_prop(body, into),
391            Prop::And { left, right }
392            | Prop::Or { left, right }
393            | Prop::Implies { left, right } => {
394                visit_prop(left, into);
395                visit_prop(right, into);
396            }
397            Prop::Not { arg } => visit_prop(arg, into),
398            Prop::Bool { expr } => visit(expr, into),
399            other => {
400                if let Some((_, comparison)) = other.comparison() {
401                    visit(&comparison.left, into);
402                    visit(&comparison.right, into);
403                }
404            }
405        }
406    }
407    visit_prop(prop, &mut into);
408    into
409}