-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathCustomRw.lean
More file actions
430 lines (381 loc) · 16.1 KB
/
CustomRw.lean
File metadata and controls
430 lines (381 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
import Batteries
import Lean
import Qq
/-!
# Tutorial: Writing `rw` from scratch
Content
(1) What `rw` does on proof term level?
(2) Basic implementation
(3) Options for normalization
(4) Unification - rewriting a quantified equality
-/
open Lean Elab Meta Tactic
open Qq
/-
## (1) What `rw` does on proof term level
Let us look at an example proof using `rw`.
-/
theorem rw_example (a b : Nat) (f : Nat → Nat) (p : Nat → Prop)
(h_eq : ∀ x : Nat, f x = x) (h_finish : p (a + b + a + b)) :
p (f a + f b + f a + f b) := by
rw [h_eq] -- rewrites two instances of `f a`
rw [h_eq] -- rewrites two instances of `f b`
exact h_finish
-- and print its proof term
#print rw_example
-- it should look similar to
example : ∀ (a b : Nat) (f : Nat → Nat) (p : Nat → Prop),
(∀ (x : Nat), f x = x) → p (a + b + a + b) → p (f a + f b + f a + f b) :=
fun a b f p h_eq h_finish =>
Eq.mpr (
congrArg (fun X => p (X + f b + X + f b)) (h_eq a : f a = a)
: p (f a + f b + f a + f b) = p (a + f b + a + f b)
)
(Eq.mpr (
congrArg (fun X => p (a + X + a + X)) (h_eq b : f b = b)
: p (a + f b + a + f b) = p (a + b + a + b)
) h_finish)
-- You can see two main theorems used for each rewrite:
#check congrArg -- digging into a subterm
#check Eq.mpr -- translate equality to implication
/-
Because of the theorems that `rw` is using, it first instantiates
the hypothesis `h_eq`, and then replaces all the instances in the goal
by a single call of `congrArg`.
-/
/-
(intermezzo) the role of `id`
You can also see some `id` in the proof which is the identity function.
It is used for typing -- `id` can change a type of its value in case
they are definitionally equal.
On metaprogramming level, this appeared in the proof by using
-/
#check mkExpectedPropHint
/-
## (2) Implementing basic `rw`
### Abstracting a variable
In the basic version, we will be only rewriting a term
with a single equality without quantifiers.
Say we are rewriting `A = B` in some expression
`e := ... A ... A ...`
First, we want to locate all the `a`-s in the expression,
and build a mapping
`fun X => ... X ... X ...`
This is called an abstraction, first we need to locate all `A`s in `e`,
and replace them with a corresponding `bvar`. A `bvar` is a variable
pointing to a quantifier with an index where quantifiers are indexed
from the inner-most to the outer-most.
To determine which `bvar` index to use, we carry `offset`.
-/
/-- (tutorial function) simplified `kabstract` -/
def myAbstract (e a : Expr) (offset : Nat := 0) : MetaM Expr := do
if !e.hasLooseBVars then -- we cannot rewrite a subterm containing a variable bound in `e`
if (← isDefEq e a) then -- check if already `a` is already the root
return mkBVar offset -- replace with bvar
-- otherwise, we recurse
match e with -- ctrl-click on `e.update...` to see the definition
| .app f x => return e.updateApp! (← myAbstract f a offset) (← myAbstract x a offset)
| .mdata _ b => return e.updateMData! (← myAbstract b a offset) -- `b` = body
| .proj _ _ b => return e.updateProj! (← myAbstract b a offset)
| .letE _ t v b _ =>
return e.updateLetE! (← myAbstract t a offset) -- diving under a binder -> increase offset
(← myAbstract v a offset) (← myAbstract b a (offset+1))
| .lam _ d b _ => return e.updateLambdaE! (← myAbstract d a offset) (← myAbstract b a (offset+1))
| .forallE _ d b _ => return e.updateForallE! (← myAbstract d a offset) (← myAbstract b a (offset+1))
| e => return e
-- lean's implementation is slightly more advanced but still readable
#check kabstract
-- the difference is that `kabstract` allows choosing positions,
-- where to apply the rewrite
def myAbstractAt (pos : Nat) (e a : Expr) :=
kabstract e a (.pos [pos])
-- Let's see
example (a b : Nat) (h : (2 * a + b + 1) = (2 * b + a + 1)) : True := by
run_tacq
let e1 ← myAbstract h.ty a
let e2 ← kabstract h.ty a
-- kabstract does the same as myAbstract
-- the #0 represents a bvar which is not bounded in the printed term
logInfo m!"e1: {e1}\ne2: {e2}"
-- Let's see how we can choose the positions for `kabstract`.
logInfo m!"pos1: {← myAbstractAt 1 h.ty a}"
logInfo m!"pos2: {← myAbstractAt 2 h.ty a}"
-- exercise: Re-implement `myAbstractAt` from scratch,
-- without using `kabstract`. Can you understand how
-- `kabstract` uses a State monad to do it?
trivial
-- Now, we can build the mapping
/-- (tutorial function) Runs `kabstract`, and wraps the result to a lambda. -/
def abstractToMapping (e a : Expr) : MetaM Expr := do
-- make sure that all previous mvar assignments were applied in `e`
-- this is necessary to recurse the term, not for `isDefEq`, so `a` doesn't need it
let e ← instantiateMVars e
let body ← myAbstract e a -- replace `a` with a `bvar`
return mkLambda
`X -- name for the variable, due to `bvar`s, we don't have to care about collisions,
BinderInfo.default -- could be also e.g. `implicit` (braces instead of parentheses)
(← inferType a) -- type for the variable
body
-- Let's build the lambda
example (A : Nat) (h : -- some crazy expression containing various constructions
∀ (y : Nat), (y = A) →
let z := A + y
y + A = (fun (X : Nat) => X + A - y) z -- no worry about the same name
) : True := by
-- `run_tacq` is like `run_tac` but makes the context directly accessible
-- not useful for writing tactics but handy for testing
run_tacq
logInfo m!"Before: {h.ty}" -- Qq-type (compile-time)
let f ← abstractToMapping h.ty A
logInfo m!"After: {f}"
trivial
/-
### Decomposing equality
-/
/-- (tutorial function)
`decomposeEq` takes a proof of equality `pf : a = b`, where `(a b : α : Sort u)`,
and returns (u, α, a b)
-/
def decomposeEq (pf : Expr) : MetaM (Level × Expr × Expr × Expr) := do
let t ← inferType pf
-- `whnf` helps to get `Eq` at the root, removing mdata,
-- instantiating mvars, expanding definitions...
let t ← whnf t
match t with
| .app (.app (.app (.const ``Eq [u]) α) a) b =>
return (u, α, a, b)
| _ => throwError "given term {pf} : {t} is not a proof of equality"
-- we also could have used `Expr.app3? ``Eq`, or
-- `matchEq?` but that doesn't give us the level
#check Expr.app3?
/-
### Building proof term
-/
/--
(tutorial function)
Takes a term `t := ... A ...`, and an equation
`eq : A = B`, and returns a proof of `... B ... → ... A ...`.
-/
def proveRwImp (eq t : Expr) :
MetaM Expr := do
let (u, α, a, b) ← decomposeEq eq
-- find the abstract function
let f ← abstractToMapping t a
logInfo m!"lhs := {a}, rhs := {b}\nabstr := {f}"
-- we also detect the sort level of t (usually Prop)
let tt ← inferType t
let v := (← inferType tt).sortLevel!
let rw_eq := mkApp6 -- build `@congrArg.{u,v} α tt a b f eq : f a = f b`
(mkConst ``congrArg [u,v])
α tt a b f eq
logInfo m!"rw_eq := {rw_eq}"
-- We know `f a = t` (definitionally)
-- Now let's calculate `f b` using famous "beta reduction".
let fb := f.beta #[b]
-- semantically it is the same as f.app b but the goal is not as pretty ;-)
-- try to uncomment the following line to see the difference:
-- let fb := f.app b
if !tt.isSort then -- why `t` must be a sort to build an implication?
throwError m!"Cannot convert equality between {tt} to an implication"
-- finally, let's build the implication
return mkApp3 -- build `Eq.mpr.{v-1} (f a) (f b) rw_eq`
(mkConst ``Eq.mpr [tt.sortLevel!])
t fb rw_eq
#check congrArg
#check Eq.mpr
/-
Let's test the function `proveRwImp`, and finish
the goal rewrite inside `run_tacq` script
-/
example (a b : Nat) (h : a = b) : 2*a + b = 2*b + a := by
run_tacq goal =>
let t := q($a + 5 - $a)
-- try what happens with a non-prop term by uncommenting the next line
let imp ← proveRwImp h goal.ty
let imp_t ← inferType imp
-- test the result of `proveRwImp`
logInfo m!"imp : {imp_t}"
-- finish rewriting the goal
let mt := imp_t.bindingDomain!
logInfo m!"Build mvar of type {mt}"
let m ← mkFreshExprSyntheticOpaqueMVar mt
goal.mvarId!.assign (mkApp imp m)
replaceMainGoal [m.mvarId!]
-- check that we have successfully rewritten the main goal
rfl
/-
## (3) Options for normalization
There are two places where normalization happens in the code of `rw` above.
* Calling `whnf` in decomposing equality
* The `isDefEq` check to determine whether to perform the abstraction.
Full normalization is not always desired, let us see an example of what can happen.
-/
def myAdd (a b : Nat) : Nat := a + b
def myEq (a b : Nat) : Prop := (a = b)
example (a b c : Nat) (h : myEq (myAdd a b) (myAdd b c))
(h2 : a + b = c) : True := by
run_tacq
let htn ← whnf h.ty
logInfo m!"{h.ty}\n→ {htn}" -- whnf unpacks `myEq`
let (u,α,lhs,rhs) ← decomposeEq h
logInfo m!"lhs := {lhs}, rhs := {rhs}" -- so `decomposeEq` splits the equality
-- This is a nice feature :-)
-- similarly isDefEq unpacks the definition
logInfo m!"({h.ty}) ?= ({htn}) : {← isDefEq h.ty htn}"
-- so myAbstract catches such occurence too
logInfo m!"Abstracting ({lhs}) in ({h2.ty}):\n{← abstractToMapping h2.ty lhs}"
-- This is rather confusing, we see no `(myAdd a b)` in `a + b = c`.
trivial
-- In both cases, normalization is performed based on the Meta.Config
#check Meta.Config
/-
To see all the options, Ctrl-click on that type to see all the options with their
explanation. There are many options to look at such as `beta`, `zeta`, `zetaDelta`.
For example, to prevent `1 + 3` being identified with 4, we need
`offsetCnstrs := false`
Here, we focus on the example of transparency -- expanding definitions.
-/
#check Meta.Config.transparency
-- The most typical options are
#check TransparencyMode.default
#check TransparencyMode.reducible -- prevent the above definition expansion
/-
Let us see how to set reducible transparency. The config lives
in a Reader monad, meaning it is read-only. We cannot change the config
for the outside code but we can run a local piece of code with a changed config.
-/
#check withConfig
#check withTransparency
example (a b : Nat) (h1 : a = b) (h2 : myEq a b) : True := by
run_tacq
logInfo m!"isDefEq default: {← isDefEq h1.ty h2.ty}"
-- general context-changing scope
withConfig (fun cfg => {cfg with transparency := .reducible}) do
-- anything running here has reducible trnasparency
logInfo m!"isDefEq reducible1: {← isDefEq h1.ty h2.ty}"
-- setting transparency in particular has a shortcut
withTransparency .reducible do
logInfo m!"isDefEq reducible2: {← isDefEq h1.ty h2.ty}"
-- we should do this when matching `lhs` to an expression
-- the same works with whnf
logInfo m!"whnf default: {← whnf h2.ty}"
withTransparency .reducible do
logInfo m!"whnf reducible1: {← whnf h2.ty}"
-- `whnfR` is a shortcut for above
logInfo m!"whnf reducible2: {← whnfR h2.ty}"
trivial
/-
## (4) Unification - rewriting a quantified equality.
Now, we want to be able to rewrite quantified equality, for example we have a rule
`∀ a b : Nat, p a + b = a + q b`,
and we want to use it to rewrite `p 1 + 2` to `1 + q 2`.
The main idea is to first replace the quantified variables with metavariables to obtain
`p ?a + ?b = p ?b + ?a`
Now the left hand side is structurally the same as `p 1 + 2` up to the
metavariables, so we need to find the right value for them.
### Unification
Finding values for metavariables actually happens automatically:
-/
example (p : Nat → Nat) : True := by
run_tacq
let a ← mkFreshExprMVarQ q(Nat) (userName := `a) -- just a Qq-sugar on top
let b ← mkFreshExprMVarQ q(Nat) (userName := `b) -- of `mkFreshExprMVar`
let pattern := q($p $a + $b)
let rhs := q($a + $p $b)
let target := q($p 1 + 2)
logInfo m!"matching {target} with {pattern}\n→ {rhs}" -- not assigned
if ← isDefEq pattern target then -- The magic happens here!
logInfo m!"matched {target} with {pattern}\n→ {rhs}" -- assigned
else
logInfo "Couldn't match"
logInfo m!"If match succeeded, the mvars are now assigned: a = {a}, b = {b}"
-- Note that using MessageData is crucial - the expressions `a`, `b` didn't change,
-- only the way we look at them inside MetaM Monad.
logInfo s!"As a string, we still see the metavariables:\na = {a}, b = {b}"
let a2 ← instantiateMVars a
let b2 ← instantiateMVars b
logInfo s!"Unless we instantiate:\na2 = {a2}, b2 = {b2}"
trivial
/-
As we saw, `isDefEq` is not just an innocent check, it can modify the proofstate
by assiging metavariables.
Besides checking modulo basic reductions, it tries to find variable assignment that
satisfies the equality. If there exist an assignment, the asignment is performed in
the proofstate, and `isDefEq` return `true`. On the other hand, if the return value
is `false`, we know there was no change in the proof state.
### Controling assignable meta-variables
There are two factors deciding whether a metavariable can be automatically
assigned with `isDefEq`. We have already seen the meta-variable kind.
-/
run_meta
let mNat1 ← mkFreshExprMVarQ q(Nat) .natural `mNat1
let mNat2 ← mkFreshExprMVarQ q(Nat) .natural `mNat2
let mSyn1 ← mkFreshExprMVarQ q(Nat) .synthetic `mSyn1
let mSyn2 ← mkFreshExprMVarQ q(Nat) .synthetic `mSyn2
-- natural mvars prefer to be assigned over synthetic
logInfo m!"mNat1 = mSyn1: {← isDefEq mNat1 mSyn1}"
logInfo m!"mNat2 = mSyn2: {← isDefEq mNat2 mSyn2}"
logInfo m!"{mNat1} = {mSyn1}, {mNat2} = {mSyn2}"
-- but synthetic can be assigned too if needed
logInfo m!"mSyn1 = mSyn2: {← isDefEq mSyn1 mSyn2}"
logInfo m!"{mSyn1} = {mSyn2}"
-- contrary to synthetic opaque
let mSO1 ← mkFreshExprMVarQ q(Nat) .syntheticOpaque `mSO1
let mSO2 ← mkFreshExprMVarQ q(Nat) .syntheticOpaque `mSO2
logInfo m!"mSO1 = mSO2: {← isDefEq mSO1 mSO2}"
/-
Often, we also want to prevent previously created mvars to be assigned no matter
of their kind. This can be done by entering a new `MetavarContext.depth`.
-/
#check withNewMCtxDepth
run_meta
let mNat1 ← mkFreshExprMVarQ q(Nat) .natural `mNat1
let mNat2 ← mkFreshExprMVarQ q(Nat) .natural `mNat2
-- normally, mNat would be prefered to be assigned,
-- we block it by entering a new level
withNewMCtxDepth do
logInfo m!"mNat1 = mNat2: {← isDefEq mNat1 mNat2}" -- now, they cannot be assigned
let mSyn ← mkFreshExprMVarQ q(Nat) .synthetic `mSyn
-- but a new synthetic can be assigned to them
logInfo m!"mSyn = mNat1: {← isDefEq mSyn mNat1}"
logInfo m!"{mSyn} = {mNat1}"
/-
### Quantified `rw`
Since we used `isDefEq` in `myAbstract`, it should be no surprise now why
rewriting with a quantified equality matches its first instantiation.
The unification happens the first moment it can, and later `f a` cannot
get unified with `f b`.
Let us finish the implementation by turning quantifiers into metavariables.
The function to do it is `forallMetaTelescope`
-/
#check forallMetaTelescope
-- rewrite a quantified equality
example (a b : Nat) (f : Nat → Nat) (p : Nat → Prop)
(h_eq : ∀ x : Nat, f x = x) (h_finish : p (a + b + a + b)) :
p (f a + f b + f a + f b) := by
run_tacq goal =>
let imp ← withNewMCtxDepth do
let (mvars, _, eq) ← forallMetaTelescope h_eq.ty -- turn quantifiers into mvars
let pf_eq := mkAppN h_eq mvars -- build the proof term
logInfo m!"before: {pf_eq} : {eq}"
-- the same rewrite code as before
let imp ← withTransparency .reducible <| proveRwImp pf_eq goal.ty
logInfo m!"after: {pf_eq} : {eq}"
-- we need to instantiate the variables before exiting the MCtx context
instantiateMVars imp
let imp_t ← inferType imp
logInfo m!"imp_t2 := {imp_t}"
let mt := imp_t.bindingDomain!
let m ← mkFreshExprSyntheticOpaqueMVar mt
goal.mvarId!.assign (mkApp imp m)
replaceMainGoal [m.mvarId!]
-- successfully rewritten `f a` → `a`
rw [h_eq b] -- let's do the second step with a simple `rw` :-)
trivial
-- Exercise: Implement the full `my_rw` tactic
example (a b : Nat) (f : Nat → Nat) (p : Nat → Prop)
(h_eq : ∀ x : Nat, f x = x) (h_finish : p (a + b + a + b)) :
p (f a + f b + f a + f b) := by
my_rw h_eq
my_rw h_eq
exact h_finish