gapt / gapt

GAPT: General Architecture for Proof Theory

Home Page:https://logic.at/gapt/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[RFC] Theory development in the large

gebner opened this issue · comments

Introduction

We want to have a large, coherent library of proofs of basic facts about natural numbers, lists, trees, etc. with consistent definitions. Importantly, I'm interested in theorems where the natural proof consists of a sequence of several lemmas where each follows from the previous ones via induction. Such a library has an immediate application as testing material for various proof transformations, not limited to: ND translation, schematic CERES, induction elimination, etc.

We have various proofs in the examples directory, however they fall short of this goal in various ways:

  • (Lack of) coherence: none of them build on each. Even the largest developments (Fürstenberg proof, induction.lists) have at most a handful of lemmas.
  • (Lack of) consistent definitions: Due to the different notation and definitions, you can't combine them either.
  • (Lack of) depth: we literally have dozens[1] of TIP proofs. Most of them can be directly proven from the definitions with one or two inductions. Many have a single, analytic induction.
  • (Lack of) induction: the Fürstenberg proof doesn't have induction since it was unrolled by hand..

One unfortunate consequence of the lack of coherence is that most if not all of the proofs above introduce definitions in a completely unsafe way: they just state them as assumptions/axioms without ever verifying that they actually exist. For example, prod/prop_01 has the two formulas d(0) = 0, ∀x d(S(x)) = S(S(d(x))) as assumptions--there is no check whatsoever that we didn't accidentally write ∀x d(S(x)) = S(d(S(x))) instead. We already had false definitions in practice: for a few years the Fürstenberg proof had false theory axioms.

[1] I never thought I'd use "dozens" as a large number.

So why haven't we done this already? After considering the reasons for a while, I believe it comes down to one big immediate problem---namely that reusing lemmas is cumbersome. Don't worry though, this is not the last problem.

Challenge: reusing previous lemmas

We have the insert and include (cut+insert) tactics, but they lack in ergonomics and don't scale well. Consider the following example from induction.lists:

val apprev = Lemma( ( appth ++ revth ) :+
    ( "goal" -> hof"∀x ∀y rev(x+y) = rev(y) + rev(x)" ) ) {
  include( "appnil", appnil )
  include( "appassoc", appassoc )
  // ...

Every lemma that we want to reuse needs to be explicitly specified. Furthermore, the generated proof will contain lots of duplicated subproofs (e.g. if appnil and appassoc use the same lemma then it will appear twice).

Another option is to state the lemmas as assumptions, example from the Fürstenberg proof:

val psi2: LKProof = Lemma( hols"F $k, REM, EXT, PRE :- C (S $k)" ) { // ...

Here we need to manually introduce a definition for the statement of each used lemma, and obscure the statement of the current theorem (bonus question: which of the 5 formulas is a lemma?).

Goals

  • It should be as easy to use previously proven lemmas as it is to use formulas in the current sequent.
  • The proof of lemma_1, ..., lemma_n :- proposition should be easily accessible, and it should not contain definition rules or proof links.
  • Getting a (reasonably small) proof of :- proposition should be just as easy.

Conclusion

The current lemma infrastructure makes it prohibitively expensive to develop large theories in gapt. I will describe the proposed new implementation and show how it fixes this issue in future posts.

I'm not entirely sure what you mean by "reusing" lemmas. There are two questions here: what is the end result, the proof, supposed to look like; and what is the user interface for obtaining it. I'm honestly not clear on either point right now.

To clarify: by reusing lemmas, I mean the general situation where you prove a lemma, then use it in the proof of another lemma, and so on (without committing to a particular choice of rules in LK or so).

I haven't yet talked at all about your questions, I tried to get the motivation out of the way first. So let me answer them:

1) How is the proof supposed to look like?

Broadly speaking, I want proofs of sequents of the following form:

some definitions, some lemmas :- my proposition

Where "some definitions" are formulas like ∀x (D(x) <-> φ(x)), "some lemmas" could be ∀x∀y x*y=y*x, and "my proposition" could be !x!y!z pow(x*y, z) = pow(x,z) * pow(y,z).

I want these proofs to not contain any definition rules, Skolem inference rules, or proof links. (Since they cause all kinds of issues.)

1a) How is the proof supposed to be constructed?

My plan is to first produce proofs of the following sequents:

all previous definitions, all previous lemmas :- my proposition

In particular, previous lemmas are not inserted directly, via cut, or via proof link---but instead always passed down to the end-sequent. Furthermore, the statement of a lemma is always a formula (and not a general sequent).

Just to make it clear: I never, never want to write the list of previous lemmas by hand. Not even their names.

These proofs are then combined in a second step using cuts. Doing this in a second step has two benefits:

  • The construction is both linear-time and linear-space.
  • It is easy to produce proofs of different granularity and complexity, depending on what lemmas are included via cut or remain as assumptions in the end-sequent.

2) What is the user interface?

At the end, for each lemma I only want to write its name, its statement, and a short one-line proof. Something like this:

val appcomm = Lemma(hof"!x!y x+y=y+x") { ind("x") onAll simp }

In particular, simp rewrites using previously proven lemmas without having to explicitly specify them. This style is somewhat common in other provers as well, look here for example: lean, isabelle.

We're almost there, I have been experimenting a bit in the lscth branch:

val mul0l = Lemma( hof"!x 0*x = 0" ) { include( "mul", "add0l" ); anaInd }
val mulsl = Lemma( hof"!x!y s(x)*y = x*y+y" ) { include( "mul", "add0", "adds", "addcomm", "addassoc" ); anaInd }
val mulcomm = Lemma( hof"!x!y x*y=y*x" ) { include( "mul", "mul0l", "mulsl" ); anaInd }
val muladd = Lemma( hof"!x!y!z x*(y+z) = x*y + x*z" ) { include( "mul", "add", "addassoc", "addcomm" ); anaInd }
val addmul = Lemma( hof"!x!y!z (x+y)*z = x*z + y*z" ) { include( "mulcomm", "muladd" ); escrgt }
val mulassoc = Lemma( hof"!x!y!z x*(y*z)=(x*y)*z" ) { include( "mul", "muladd" ); anaInd }
val mul1 = Lemma( hof"!x x*1 = x" ) { include( "1", "mul", "add0l" ); escrgt }

(Note that the proofs are constructed via links and cuts, that part is completely missing.)

2a) What is the user interface for obtaining the proofs?

A function probably. It will likely take an argument that tells it what lemmas not to put in via cut.

Thanks for the explanation. What I took away from it is this: you want to just be able to write the proposition you're proving in the succedent end sequent and have the system infer which lemmas and definitions are needed in the antecedent to prove it. Once you have a proof of L_1,…,L_n :- P, you can remove some or all of the L_i from the end sequent by inserting their proofs and cutting. Is that about right?

Yes, that's a good description. Although "inferring" the needed lemmas might give the wrong impression, it should just collect the lemmas that are used by the tactics---I don't plan any machine learning-based lemma selection or anything. (At least for now. 😄)

It seems to me that the way of stating theorems and writing proofs that corresponds most closely to a mathematical text wouldn't insert the proofs of used lemmas above the cut, but rather use a proof link—after all, you don't reprove a lemma every time you use it.

after all, you don't reprove a lemma every time you use it.

I agree with you, but only on this part. Indeed you don't reprove it, you reference it in one way or another. The question is how to model these references in a proof calculus such as LK.

To be fair, the approach in this proposal is grounded more on technical rather than mathematico-philosphical considerations. Avoiding proof links, definition rules, and Skolem rules has a variety of technical advantages:

  • We have cut-elimination. (EDIT: except for cuts on inductions, of course)
  • Expansion proof extraction works.
  • Converting expansion proofs to LK works as well.
  • Expansion proofs are interesting--otherwise a proof of !x!y (x+y=y+x) would extract to a "trivial" expansion proof (with two eigenvariable nodes plus an atom).
  • The proofs are small (only linear-size as opposed to the worst-case exponential proof link version).
  • Aside from the induction inferences, this is a well-studied and well-understood logical system (first-order logic with equality).

As for how well this approach corresponds to mathematical practice, I think it is a reasonable approximation on a large scale: from a big picture point of view, the main features of a mathematical text that contain formal logical content[1] are definitions, theorems, and proofs. And these features are preserved. We can still see what definitions and lemmas there are. We can also figure out which previous lemmas and definitions are used in the proof of a lemma.

Clearly, there is always a mismatch between a formalization and an informal mathematical text. Just using LK as a proof system puts us miles apart from prose. Fundamentally, I doubt many mathematicians would recognize a proof in LK as a mathematical proof. So I believe that using LK to encode the proofs is just an implementation detail, and we should choose whatever conventions turn out to be technically convenient.

[1] Mathematical texts of course also contain other parts that have no significance for the formal content. They contain motivation, intuition, historical remarks, and even jokes. However I believe it is out of scope of gapt to model these features.

I definitely agree that technical considerations need to take priority over questions of style. I have one more question, though: if I decide to leave a lemma in the antecedent of some proposition that I prove, does this mean that I can't in turn use the proposition in further proofs? You wrote before that the statements of lemmas should only amount to a single formula and not a sequent.

You wrote before that the statements of lemmas should only amount to a single formula and not a sequent.

I was referring to the statement of the theorem. Instead of implications, some lemmas use sequents currently:

val intersectionOpen = Lemma( hols"O(X), O(Y) :- O(intersection X Y)" ) { // ...

This would become ∀X ∀Y (O(X) & O(X) -> O(intersection X Y)) instead. The reason to have formulas here is that it then becomes straightforward to use the statement in the antecedent.

if I decide to leave a lemma in the antecedent of some proposition that I prove, does this mean that I can't in turn use the proposition in further proofs?

Part of the proposal is to change Lemma so that it only accepts a formula as argument. You'd then use a tactic to add previous lemmas to the antecedent of the current goal, if you want. From a user interface point of view, you don't see that there are formulas in the antecedent of the lemmas when writing them--it would look pretty much like this:

val mul0l = Lemma( hof"!x 0*x = 0" ) { include( "mul", "add0l" ); anaInd }
val mulsl = Lemma( hof"!x!y s(x)*y = x*y+y" ) { include( "mul", "add0", "adds", "addcomm", "addassoc" ); anaInd }
val mulcomm = Lemma( hof"!x!y x*y=y*x" ) { include( "mul", "mul0l", "mulsl" ); anaInd }
val muladd = Lemma( hof"!x!y!z x*(y+z) = x*y + x*z" ) { include( "mul", "add", "addassoc", "addcomm" ); anaInd }
val addmul = Lemma( hof"!x!y!z (x+y)*z = x*z + y*z" ) { include( "mulcomm", "muladd" ); escrgt }
val mulassoc = Lemma( hof"!x!y!z x*(y*z)=(x*y)*z" ) { include( "mul", "muladd" ); anaInd }

The change is only "under the hood", so to speak. The mulassoc lemma would generate an LK proof of the following sequent (no matter what the proof of muladd looks like):

!x x*0=0 & !x!y x*s(y)=x*y+x, !x!y!z x*(y+z) = x*y + x*z :- !x!y!z x*(y*z)=(x*y)*z

A separate function then allows you to easily cut away assumptions using previous lemmas, e.g. to get:

!x x+0=0 & !x!y x+s(y)=s(x+y), !x x*0=0 & !x!y x*s(y)=x*y+x :- !x!y!z x*(y*z)=(x*y)*z

Essentially, the antecedents of the lemmas are hidden from the user and treated as an implementation detail to refer to previous lemmas. So yes, you can use lemmas that use lemmas in further lemmas since we just ignore the antecedent.