The equational compiler was failing to generate equational lemmas for
equations such as:
def f : nat → nat → nat
| (x+1) (y+1) := f (x+10) y
| _ _ := 1
It would fail when trying to prove the following equation:
forall x, f 0 x = 1
using a "refl" proof. This equation does not hold definitionally.
It is not blocked by the internal pattern matching based on the
cases_on recursor, but it is blocked by the outer most brec_on
used to implement structural recursion. The solution is to
"complete" the set of equations. So, the structural_rec
module will replace the equation above with
def f : nat → nat → nat
| (x+1) (y+1) := f (x+10) y
| _ 0 := 1
| _ (y+1) := 1
and then (as before)
def f : Pi (x y : nat), below y → nat
| (x+1) (y+1) F := F^.fst^.fst (x+10)
| _ 0 F := 1
| _ (y+1) F := 1
There were two performance bottlenecks in the recursive equation
compiler. Both bottlenecks were due to conversion checking.
1- We allow patterns such as (x+1) in the left-hand-side of a
recursive equation. This is kind of pattern has to be reduced
since it is not a constructor. Moreover, when we are trying to
compile using structural recursion, we need to find an element
that is structurally smaller in recursive applications.
Again, we need to use reduction since the pattern may be (x+2),
and in the recursive application we have (x+1). Now, consider
the following equation
f (x+1) (y+1) := f complex_term y
It will first check whether complex_term is structurally smaller
than (x+1), and the compiler will timeout trying to reduce
complex_term.
This commit adds the following workaround. The structural
recursion module from now on will only unfold reducible constants
and constants marked as patterns. This is not a complete
solution. It will timeout in the following equation:
f (x+1) (y+1) := f (x+1000000000000) y
For this one, we need to add a whnf "fuel" option to type_context
2- Equational lemma generation was producing lemmas that are too
expensive to check. Suppose we the following two definitions
| f x 0 := 1
| f x (y+1) := f complex_term y
and
| g 0 y := 1
| g (x+1) y := g x complex_term
Before this commit, we would generate the following proofs for
the second equation of each definition:
eq.refl (f complex_term y)
eq.refl (g x complex_term)
This proof triggers the following definitionally equality test:
f x (y+1) =?= f complex_term y
g (x+1) y =?= g x complex_term
Since, we have f/g on both sides, the type checker will try
first to unify the arguments, and may timeout trying to solve
x =?= complex_term
y =?= complex_term
since it may take a long time to reduce `complex_term`.
We workaround this problem by creating a slightly different
proof.
eq.refl (unfold_of(f x (y+1)))
eq.refl (unfold_of(g (x+1) y))
where unfold_of(t) is the result of applying one delta reduction
step.
Fixes#1363
After error recovery has been implemented in the elaborator, a few
assumptions made in the type context are not valid anymore since we may
be recovering from errors, and the local and metavariable contexts may
be invalid.
I used the approach used in the class environment.
- find* methods return optional<...>
- get* methods throw exception for unknown elements
Remarks:
I preserved code patterns such as
optional<local_decl> d = lctx.find_local_decl(...)
lean_assert(d)
and did not convert them into
local_decl d = lctx.get_local_decl(...)
Reason: the intention is clear that the local must be defined there.
If it is not we should analyze the problem and decide whether we should
throw an exception or not.
However, I converted code patterns such as
local_decl d = *lctx.find_local_decl(...)
into
local_decl d = lctx.get_local_decl(...)
Disclaimer: this change fixes issue #1363, but it may obfuscate other bugs.
@johoelzl We now produce a better message for your example:
inductive R : ℕ → Prop
| pos : ∀p n, R (p + n)
lemma R_id : ∀n, R n → R n
| (.p + .n) (R.pos p n) := R.pos p n
The new error is:
file.lean:5:2: error: invalid function application in pattern, it cannot be reduced to a constructor (possible solution, mark term as inaccessible using '.( )')
.p + .n
(Type u) is the old (Type (u+1))
(PType u) is the old (Type u)
Type* is the old (Type (_+1))
PType* is the old Type*
The stdlib can be compiled, but we still have > 70 broken tests
See discussion at #1341
It also updates the condition for triggering the inaccessible
transition. Before this commit, we would only perform this kind
of transition if *all* terms were marked inaccessible. Now,
we perform it if *some* are marked inaccessible. Reason:
when we perform the complete transition we don't have enought
information for deciding whether an argument should be marked as
inaccessible or not. If this decision creates confusion for users,
we may try to mark them with an "maybe-inaccessible" annotation, and
then enforce that the inaccessible transition is performed onlty if
*all* terms are marked as inaccessible or *maybe-inaccessible"
They were at src/library because we hoped we would be able to use them
in the type_context unifier. However, the plan did not work for several
reasons. We saved the partial implementation in the branch: https://github.com/leodemoura/lean/tree/type_context_with_refl_lemmas
Here are the problems:
1) We have to be able to rewrite even when the type context is already in tmp-mode.
This is an issue because the tmp metavariables in the refl lemma clash with the ones created in the type context.
Solution: implemented lift operation for idx metavariables, and custom
match. This solution is not perfect since the lifting is extra overhead.
2) The term being "unfolded" may be stuck. Example:
nat.add n (@one nat ?m)
will not match the pattern
nat.add ?x_0 (nat.succ ?x_1)
because ?m is not assigned yet.
We can assign it during the matching process because it is a regular metavariable and the matching is performed in
tmp_mode.
Possible workaround a) try to instanciate type class instances before we try the refl lemmas.
This is a potential performance problem because the term can be arbitrarily big.
The current heuristics we use to speed up the process do not work for the example above.
Possible workaround b) allow regular metavariables be assigned by type class resolution even
when we are in tmp-mode.
We have not tried to implement any of these workarounds.
3) There are many more lazy-delta steps. Before this feature, when we unfold `nat.add a (succ ... (succ b) ...)`,
we are done with delta-reduction. It is just iota and beta after that.
However, with refl-lemmas, the term `nat.add a (succ ... (succ b) ...)` produces one lazy-delta step per succ.
This produces nasty side-effects because of the
The heuristic (f t =?= f s) ==> (t =?= s).
Examples such as
(fib 8) =?= 34
will take a very long time because of this heuristic.
Possible workaround: cache failures like we did in Lean2.
However, failure are only easy to cache if there are no meta-variables.
4) The type context trace gets very confusing since we use is_def_eq for matching lhs while we are computing is_def_eq.
Possible workaround: disable trace when trying refl_lemmas.
5) We must be able to temporarily disable the feature.
Example: when proving a refl_lemma for a definition `f`, we may have
to expand the nested definitions
(e.g., for match-end blocks)
6) refl/simp lemmas were designed to rewrite elaborated terms.
Using them during unification may produce a series of unexpected
behaviors since terms usually contain many regular and universe meta-variables.
7) We need to define a notion of "refl stuck application".
Right now, a metavar is stuck, a projection is stuck if the structure
is stuck, a recursor is stuck is the major premise is stuck.
An application (f ...) is refl-lemma stuck if f has refl-lemmas
associated with it, AND metavariables occurring in arguments are
preventing a refl-lemma from being applied.
Now, eta and impredicativity are assumed to be always true.
Motivation: the rest of the system assumes eta.
Regarding impredicativity, we decided to support only the standard library.