Zulip Chat Archive
Stream: lean4
Topic: hoisting a proof of termination to a separate definition
Jonathan Protzenko (Jan 27 2023 at 18:11):
consider the following definition (please ignore the terrible proof):
def fact (x: Nat) :=
if x <= 1 then
1
else
x * fact (x - 1)
termination_by fact x => x
decreasing_by
simp_wf
simp_arith
have sub_eq x y: Nat.sub x y = x - y := rfl
rewrite [ sub_eq ]
rewrite [ Nat.sub_add_cancel ]
simp_arith
have h: ¬ (x <= 1) := by assumption
have h: x > 1 := Nat.gt_of_not_le h
have h: 1 < x := by assumption
apply Nat.le_of_lt
assumption
I would like to hoist both the termination measure (termination_by
) and the proof of termination for that measure (decreasing_by
) to separate top-level definitions
for the termination measure, it's easy:
@[simp]
def fact_termination_measure (x: Nat) := x
def fact (x: Nat) :=
if x <= 1 then
1
else
x * fact (x - 1)
termination_by fact x => fact_termination_measure x
decreasing_by ...
but for the proof itself, I don't know how to "lift" it to its own tactic definition
syntax "fact_termination_proof" term : tactic
macro_rules | `(tactic| fact_termination_proof x) => `(tactic| (
... same contents as before ...
def fact
... as above ...
decreasing_by fact_termination_proof
doesn't seem to work -- is there a way to write this termination proof in its own separate definition?
context: I am writing a Lean backend for my Rust verification tool, and now I'm adding support for termination proofs... the tool translates a Rust definition to a Lean definition that contains "open variables" for the proof of termination, found in another module, and the user is expected to fill them out... in my case, fact_termination_proof
would be a stub in UserProvidedProofs.lean, and then the user would replace a sorry
with an actual proof of termination
Jonathan Protzenko (Jan 27 2023 at 18:13):
expanding on the "doesn't seem to work": I get a whole lot of errors along the lines of unknown identifier 'x✝'
so I'm probably not doing my binding right
Mario Carneiro (Jan 27 2023 at 18:15):
Note that it is unusual to provide a complicated proof of termination directly in decreasing_by
unless it is a general purpose tactic, because it runs on all termination goals. If you want to provide a specific termination argument, you should use decreasing_by assumption
(which is also the default, more or less) and then put have : x - 1 < x := by ...
before the call to fact (x - 1)
Jonathan Protzenko (Jan 27 2023 at 18:17):
ok, yes, good usability question: do I want the user to solve all termination goals at once, or have them fill out separate proofs for each recursive call... worth pondering (I was basing myself on the example here: https://leanprover.github.io/theorem_proving_in_lean4/induction_and_recursion.html for ack
which solves both goals at once)
Mario Carneiro (Jan 27 2023 at 18:19):
I would (and lean does) use a general purpose tactic which can prove most termination goals, and then have a fallback of manual proof which is for specific termination goals. Users can't easily write termination proofs that prove multiple goals at once
Mario Carneiro (Jan 27 2023 at 18:20):
normally there will only be one nontrivial termination goal in a given function, though
Jonathan Protzenko (Jan 27 2023 at 18:44):
thanks, two sub-questions then:
- in this particular case, decreasing_tactic does not work -- what would be the recommended style for this specific example?
- back to my original use-case, the function body is auto-generated by a tool, but the termination proofs are hand-written, so I need to modularly de-couple the two -- there are no annotations on the source program, so I really want to separate the function definition (auto-generated, may be overwritten at some point in the future) and the proof of termination (maintained by the user in a separate file that does not get overwritten when the code is re-generated)
James Gallicchio (Jan 27 2023 at 18:47):
RE: the first point, the idea is that decreasing_tactic
gets called on each of the termination goals. it includes (as one of the things it tries) assumption
, which just looks for a proof of the goal in context. so if you provide a goal with have : the_termination_goal := the_proof
right before making a recursive call, the default decreasing_by
tactic will find and use that proof.
Jonathan Protzenko (Jan 27 2023 at 18:49):
ah I see what you mean... bearing in mind that the definition is auto-generated, this means that I would have to generate the have : clause before the recursive call, refer to the proof that the user has written somewhere else (possibly via their own tactic), and maybe even pick a particular kind of ordering?
Mario Carneiro (Jan 27 2023 at 18:50):
In this specific example I think you could do something like this:
import Std.Data.Nat.Lemmas
theorem termination_proof (h : ¬x ≤ 1) : x - 1 < x :=
Nat.sub_lt (Nat.lt_trans (by decide) (Nat.not_le.1 h)) (by decide)
def fact (x: Nat) :=
if h : x <= 1 then
1
else
x * have := termination_proof h; fact (x - 1)
Mario Carneiro (Jan 27 2023 at 18:51):
the proof is not as automatic as I would like (it gets better if you can use mathlib linarith
)
James Gallicchio (Jan 27 2023 at 18:52):
out of curiosity, what's the use case? this seems like an interesting project
Jonathan Protzenko (Jan 27 2023 at 18:53):
thanks, no worries about the proof quality
Jonathan Protzenko (Jan 27 2023 at 18:53):
this is the tool: https://arxiv.org/abs/2206.07185, presented at ICFP of last year, see also https://github.com/AeneasVerif/aeneas: we generate a pure translation of Rust programs (currently, we have Coq + F* backends, with Isabelle/HOL and Lean4 in the works)
Jonathan Protzenko (Jan 27 2023 at 18:53):
then the user provides the proofs on top of the generated code
Jonathan Protzenko (Jan 27 2023 at 18:54):
here's where it gets complicated for me: because our tool auto-generates the definition fact
, right now, I don't know that h
is an important hypothesis that needs to be inserted in the output of the translation (the fact
) function, so I would be at a loss as to how to "infer" the have
clause to be inserted at the site of the recursive call
Jonathan Protzenko (Jan 27 2023 at 18:56):
here's an example using the F* backend: this is what the tool generates, notice how the decreases
clause is "generic" https://github.com/AeneasVerif/aeneas/blob/main/tests/fstar/hashmap_on_disk/HashmapMain.Funs.fst#L14-L34
then, the definition for this decreases clause is written by the user here: https://github.com/AeneasVerif/aeneas/blob/main/tests/fstar/hashmap_on_disk/HashmapMain.Clauses.fst#L10-L12 (possibly with something custom is the recursion is non-trivial)
Jonathan Protzenko (Jan 27 2023 at 18:57):
by the way, Aeneas is @Son Ho's PhD project (just realized he's on this Zulip, too)
James Gallicchio (Jan 27 2023 at 18:59):
F* has much more automation in its proof checker than Lean (where the automation is done during elaboration, not during typechecking). You can probably get something very close to the F* backend if you always use the proof-term-having control flow structures (like if h : whatever then .. else ..
and match h : object with | ... => ...
) and allow users to add hypotheses to the termination proofs, which you can then fill in at the call site with by assumption
or similar.
Mario Carneiro (Jan 27 2023 at 19:00):
Jonathan Protzenko said:
here's where it gets complicated for me: because our tool auto-generates the definition
fact
, right now, I don't know thath
is an important hypothesis that needs to be inserted in the output of the translation (thefact
) function, so I would be at a loss as to how to "infer" thehave
clause to be inserted at the site of the recursive call
You can just put everything in the context in there
James Gallicchio (Jan 27 2023 at 19:01):
^only thing is re-generating the def might change the available context. not sure what the best approach would be there
Jonathan Protzenko (Jan 27 2023 at 19:01):
yes, but that's a problem for other backends too if you e.g. add arguments to the function
Jonathan Protzenko (Jan 27 2023 at 19:01):
so it's not perfect, but at least it's an improvement
Jonathan Protzenko (Jan 27 2023 at 19:03):
so that could work, provided I know all of the hypotheses... for instance, what happens if the if-condition is negated? will the hypothesis still be name-able in the else branch?
Mario Carneiro (Jan 27 2023 at 19:03):
yes, in fact that is the case in this example
Jonathan Protzenko (Jan 27 2023 at 19:04):
indeed!
Mario Carneiro (Jan 27 2023 at 19:04):
it will be a double negative if you say if h : not (x > 1)
but I assume that isn't a major issue
Jonathan Protzenko (Jan 27 2023 at 19:04):
and do control-flow dependent hypotheses arise from anything other than if and match?
Mario Carneiro (Jan 27 2023 at 19:05):
well function calls can have complicated conditions on them but maybe you are fine with having the user supply the condition
Mario Carneiro (Jan 27 2023 at 19:06):
the lean compiler is basically just if
, match
and recursive functions, control-flow-wise
Mario Carneiro (Jan 27 2023 at 19:07):
so I think you are covered
Jonathan Protzenko (Jan 27 2023 at 19:14):
great -- thanks to both of you for the discussion, that was very helpful
Jonathan Protzenko (Jan 27 2023 at 19:15):
I'm still curious as to why I was getting these unknown identifier 'x✝'
errors in the first place, but I guess that's irrelevant... maybe I was violating hygiene or something
Mario Carneiro (Jan 27 2023 at 19:17):
do you have an MWE for that?
Jonathan Protzenko (Jan 27 2023 at 19:18):
@[simp]
def fact_termination_measure (x: Nat) := x
syntax "fact_termination_proof" term : tactic
macro_rules | `(tactic| fact_termination_proof x) => `(tactic| (
simp_wf
simp_arith
have sub_eq x y: Nat.sub x y = x - y := rfl
rewrite [ sub_eq ]
rewrite [ Nat.sub_add_cancel ]
simp_arith
have h: ¬ (x <= 1) := by assumption
have h: x > 1 := Nat.gt_of_not_le h
have h: 1 < x := by assumption
apply Nat.le_of_lt
assumption))
def fact (x: Nat) :=
if x <= 1 then
1
else
x * fact (x - 1)
termination_by fact x => fact_termination_measure x
decreasing_by
fact_termination_proof x
Jonathan Protzenko (Jan 27 2023 at 19:19):
I don't have a precise mental model for how the elaborator works and how binding resolution is done, so maybe this isn't supposed to work to begin with
Mario Carneiro (Jan 27 2023 at 19:19):
you are pattern matching a literal x
in your macro definition. The macro model is somewhat similar to rust hygienic macros
Mario Carneiro (Jan 27 2023 at 19:19):
this works:
macro_rules | `(tactic| fact_termination_proof $x) => `(tactic| (
simp_wf
simp_arith
have sub_eq x y: Nat.sub x y = x - y := rfl
rewrite [ sub_eq ]
rewrite [ Nat.sub_add_cancel ]
simp_arith
have h: ¬ ($x <= 1) := by assumption
have h: $x > 1 := Nat.gt_of_not_le h
have h: 1 < $x := by assumption
apply Nat.le_of_lt
assumption))
Jonathan Protzenko (Jan 27 2023 at 19:20):
ah yes of course... thanks!
Last updated: Dec 20 2023 at 11:08 UTC