Skip to content

Commit 3a2c28b

Browse files
committed
Add comments
1 parent 031bdb2 commit 3a2c28b

1 file changed

Lines changed: 149 additions & 8 deletions

File tree

LeroyCompilerVerificationCourse/Constprop.lean

Lines changed: 149 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ Authors: Wojciech Różowski
66

77
import LeroyCompilerVerificationCourse.Imp
88
import Std.Data.HashMap
9+
10+
11+
/-
12+
8. An optimization: constant propagation
13+
14+
8.1 Simplifying expressions using smart constructors
15+
-/
16+
917
open Classical in
1018
instance [BEq α] [BEq β] [Hashable α] : BEq (Std.HashMap α β) where
1119
beq m n := Id.run do
@@ -16,13 +24,23 @@ instance [BEq α] [BEq β] [Hashable α] : BEq (Std.HashMap α β) where
1624
| some v => if e.2 != v then return false
1725
return true
1826

27+
/-
28+
`mk_PLUS_CONST a n` produces an expression equivalent to `.PLUS a (.CONST n)`
29+
but further simplified.
30+
-/
1931
@[grind] def mk_PLUS_CONST (a : aexp) (n : Int) : aexp :=
2032
if n = 0 then a else
2133
match a with
2234
| .CONST m => .CONST (m + n)
2335
| .PLUS a (.CONST m) => .PLUS a (.CONST (m + n))
2436
| _ => .PLUS a (.CONST n)
2537

38+
/-
39+
`mk_PLUS a1 a2` produces a simplified expression equivalent to `.PLUS a1 a2`.
40+
It uses associativity and commutativity to find the pattern
41+
"an expression plus a constant", then uses `mk_PLUS_CONST`to simplify
42+
further.
43+
-/
2644
@[grind] def mk_PLUS (a1 a2 : aexp) : aexp :=
2745
match a1, a2 with
2846
| .CONST m, _ => mk_PLUS_CONST a2 m
@@ -32,6 +50,12 @@ instance [BEq α] [BEq β] [Hashable α] : BEq (Std.HashMap α β) where
3250
| _, .PLUS a2 (.CONST m2) => mk_PLUS_CONST (.PLUS a1 a2) m2
3351
| _, _ => .PLUS a1 a2
3452

53+
/-
54+
`mk_MINUS a1 a2]` produces an expression equivalent to `MINUS a1 a2`
55+
using similar tricks. Note that "expression minus constant" is
56+
always normalized into "expression plus opposite constant",
57+
simplifying the case analyses. *)
58+
-/
3559
@[grind] def mk_MINUS (a1 a2 : aexp) : aexp :=
3660
match a1, a2 with
3761
| _, .CONST m => mk_PLUS_CONST a1 (-m)
@@ -40,17 +64,30 @@ instance [BEq α] [BEq β] [Hashable α] : BEq (Std.HashMap α β) where
4064
| _, .PLUS a2 (.CONST m2) => mk_PLUS_CONST (.MINUS a1 a2) (-m2)
4165
| _, _ => .MINUS a1 a2
4266

67+
/-
68+
To simplify an expression, we rewrite it bottom-up, applying the smart
69+
constructors at each step.
70+
-/
4371
@[grind] def simplif_aexp (a : aexp) : aexp :=
4472
match a with
4573
| .CONST n => .CONST n
4674
| .VAR x => .VAR x
4775
| .PLUS a1 a2 => mk_PLUS (simplif_aexp a1) (simplif_aexp a2)
4876
| .MINUS a1 a2 => mk_MINUS (simplif_aexp a1) (simplif_aexp a2)
4977

78+
/-
79+
An example
80+
81+
Compute `simplif_aexp (.MINUS (.PLUS (.VAR "x") (.CONST 1)) (.PLUS (.VAR "y") (.CONST 1)))`
82+
-/
5083
/-- info: aexp.MINUS (aexp.VAR "x") (aexp.VAR "y") -/
5184
#guard_msgs in
5285
#eval simplif_aexp (.MINUS (.PLUS (.VAR "x") (.CONST 1)) (.PLUS (.VAR "y") (.CONST 1)))
5386

87+
/-
88+
To show the soundness of these simplifications, we show that the
89+
optimized expressions evaluate to the same values as the original expressions.
90+
-/
5491
@[grind] theorem mk_PLUS_CONST_sound :
5592
∀ s a n, aeval s (mk_PLUS_CONST a n) = aeval s a + n := by
5693
intro s a n
@@ -74,6 +111,10 @@ theorem simplif_aexp_sound : ∀ s a, aeval s (simplif_aexp a) = aeval s a := by
74111
induction a
75112
any_goals grind [mk_PLUS_sound, mk_MINUS_sound]
76113

114+
/-
115+
We can play the same game for Boolean expressions.
116+
Here are the smart constructors and their proofs of correctness
117+
-/
77118
@[grind] def mk_EQUAL (a1 a2 : aexp) : bexp :=
78119
match a1, a2 with
79120
| .CONST n1, .CONST n2 => if n1 = n2 then .TRUE else .FALSE
@@ -121,7 +162,10 @@ theorem mk_AND_sound :
121162
intro s b1 b2
122163
fun_cases mk_AND b1 b2
123164
any_goals grind
124-
165+
/-
166+
Even commands can benefit from smart constructors! Here is a smart
167+
`.IFTHENELSE` and a smart `.WHILE` that take known conditions into account.
168+
-/
125169
@[grind] def mk_IFTHENELSE (b : bexp) (c1 c2 : com) : com :=
126170
match b with
127171
| .TRUE => c1
@@ -151,10 +195,37 @@ theorem cexec_mk_WHILE_loop : ∀ b c s1 s2 s3,
151195
intro b c s1 s2 s3 h1 h2 h3
152196
fun_cases (mk_WHILE b c) <;> grind
153197

198+
/-
199+
8.2 A static analysis for constant propagation
200+
201+
The static analysis we need operates over finite maps from variables (strings)
202+
to values (integers). We represent these using `Std.HashMap` from Lean's standard library.
203+
204+
Our static analysis is going to compute "abstract stores", that is,
205+
compile-time approximations of run-time stores. This is an instance
206+
of the general theory of abstract interpretation.
207+
208+
(Notational convention: we use Capitalized identifiers to refer to
209+
abstract things and lowercase identifiers to refer to concrete things.)
210+
211+
Abstract stores `Store` are represented as finite maps from variable names
212+
to integers. If a variable `x` is mapped to `n`, it means that
213+
we statically know that the run-time value of `x` is `n`. If `x` is not mapped,
214+
it means that the run-time value of `x` can be anything.
215+
216+
This meaning is captured by the `matches s s'` predicate below,
217+
which says whether a concrete store `s` belongs to an
218+
abstract store `s'` obtained by static analysis.
219+
(A bit like a term belongs to a type in a type system.
220+
-/
154221
def Store := Std.HashMap ident Int
155222
@[grind] def matches' (s : store) (S : Store) : Prop :=
156223
∀ x n, S.get? x = .some n -> s x = n
157224

225+
/-
226+
Abstract stores have a lattice structure, with an order `Le`, a top element,
227+
and a join operation. We can also test whether two abstract stores are equal.
228+
-/
158229
@[grind] def Le (S1 S2 : Store) : Prop :=
159230
∀ x n, S2.get? x = .some n -> S1.get? x = .some n
160231

@@ -163,12 +234,15 @@ def Store := Std.HashMap ident Int
163234
@[grind] def Join (S1 S2 : Store) : Store :=
164235
S1.filter (fun key _ => S2.get? key == S1.get? key)
165236

166-
-- In leroy's course this is decidable
167237
def Equal (S1 S2 : Store) := Std.HashMap.Equiv S1 S2
168238

169239
noncomputable instance : Decidable (Equal S' S) :=
170240
Classical.propDecidable (Equal S' S)
171241

242+
/-
243+
We show the soundness of these lattice operations with respect to
244+
the `matches` and the `Le` relations.
245+
-/
172246
theorem matches_Le : ∀ s S1 S2, Le S1 S2 -> matches' s S1 -> matches' s S2 := by
173247
intro s S1 S2 h1 h2
174248
grind
@@ -202,6 +276,12 @@ theorem Equal_Le : ∀ S1 S2, Equal S1 S2 -> Le S1 S2 := by
202276
have := @Std.HashMap.Equiv.getElem?_eq _ _ _ _ S1 S2 _ _ x eq
203277
grind
204278

279+
/-
280+
The abstract, compile-time evaluation of expressions returns `.some v`
281+
if the value `v` of the expression can be determined based on what
282+
the abstract store `S` tells us about the values of variables.
283+
Otherwise, `.none`is returned.
284+
-/
205285
@[grind] def lift1 {A B : Type} (f : A -> B) (o : Option A) : Option B :=
206286
match o with
207287
| .some x => .some (f x)
@@ -227,6 +307,9 @@ theorem Equal_Le : ∀ S1 S2, Equal S1 S2 -> Le S1 S2 := by
227307
| .NOT b1 => lift1 (fun m => !m) (Beval S b1)
228308
| .AND b1 b2 => lift2 (fun m n => m && n) (Beval S b1) (Beval S b2)
229309

310+
/-
311+
These compile-time evaluations are sound, in the following sense.
312+
-/
230313
@[grind] theorem Aeval_sound :
231314
∀ s S, matches' s S ->
232315
∀ a n, Aeval S a = .some n -> aeval s a = n := by
@@ -266,7 +349,10 @@ theorem Beval_sound :
266349
unfold lift2 at h2
267350
split at h2 <;> grind
268351

269-
352+
/-
353+
To analyze assignments, we need to update abstract stores with the result
354+
of `Aeval`.
355+
-/
270356
@[grind] def Update (x : ident) (N : Option Int) (S : Store) : Store :=
271357
match N with
272358
| .none => S.erase x
@@ -293,19 +379,36 @@ theorem matches_update : ∀ s S x n N,
293379
matches' (update x n s) (Update x N S) := by
294380
intro s S x n N m h
295381
grind
296-
382+
/-
383+
To analyze loops, we will need to find fixpoints of a function from
384+
abstract states to abstract states. Intuitively, this corresponds
385+
to executing the loop in the abstract, stopping iterations when the
386+
abstract state is stable.
387+
388+
Computing exact fixpoints with guaranteed termination is not easy;
389+
we will return to this question at the end of the lectures.
390+
In the meantime, we will use the simple, approximate algorithm below,
391+
which stops after a fixed number of iterations and returns `Top`
392+
if no fixpoint has been found earlier.
393+
-/
297394
@[grind] noncomputable def fixpoint_rec (F : Store -> Store) (fuel : Nat) (S : Store) : Store :=
298395
match fuel with
299396
| 0 => Top
300397
| fuel + 1 =>
301398
let S' := F S
302399
if Equal S' S then S else fixpoint_rec F fuel S'
303400

401+
/-
402+
Let's say that we will do at most 20 iterations.
403+
-/
304404
@[grind] def num_iter : Nat := 20
305405

306406
@[grind] noncomputable def fixpoint (F : Store -> Store) (init_S : Store) : Store :=
307407
fixpoint_rec F num_iter init_S
308-
408+
/-
409+
The result `S` of `fixpoint F` is sound, in that it satisfies
410+
`F S ⊑ S` in the lattice ordering.
411+
-/
309412
theorem fixpoint_sound (F : Store → Store) (init_S : Store) (h : S = fixpoint F init_S) :
310413
Le (F S) S := by
311414
have A : ∀ fuel S,
@@ -319,7 +422,13 @@ theorem fixpoint_sound (F : Store → Store) (init_S : Store) (h : S = fixpoint
319422
have E : S = Top \/ Equal (F S) S = true := by grind
320423
cases E <;> grind [Equal_Le]
321424

322-
425+
/-
426+
Now we can analyze commands by executing them "in the abstract".
427+
Given an abstract store `S` that represents what we statically know
428+
about the values of the variables before executing command `c`,
429+
`cexec'` returns an abstract store that describes the values of
430+
the variables after executing `c`.
431+
-/
323432
@[grind] noncomputable def Cexec (S : Store) (c : com) : Store :=
324433
match c with
325434
| .SKIP => S
@@ -333,6 +442,9 @@ theorem fixpoint_sound (F : Store → Store) (init_S : Store) (h : S = fixpoint
333442
| .WHILE _ c1 =>
334443
fixpoint (fun x => Join S (Cexec x c1)) S
335444

445+
/-
446+
The soundness of the analysis follows.
447+
-/
336448
@[grind] theorem Cexec_sound :
337449
∀ c s1 s2 S1,
338450
cexec s1 c s2 -> matches' s1 S1 -> matches' s2 (Cexec S1 c) := by
@@ -411,7 +523,13 @@ theorem fixpoint_sound (F : Store → Store) (init_S : Store) (h : S = fixpoint
411523
apply matches_Le
412524
· apply Le_Join_l
413525
· exact MATCHES
526+
/-
527+
8.3 The constant propagation optimization
414528
529+
We can use the results of the static analysis to simplify expressions
530+
further, replacing variables with known values by these values, then
531+
applying the smart constructors.
532+
-/
415533
@[grind =] def cp_aexp (S : Store) (a : aexp) : aexp :=
416534
match a with
417535
| .CONST n => .CONST n
@@ -421,7 +539,6 @@ theorem fixpoint_sound (F : Store → Store) (init_S : Store) (h : S = fixpoint
421539
| .PLUS a1 a2 => mk_PLUS (cp_aexp S a1) (cp_aexp S a2)
422540
| .MINUS a1 a2 => mk_MINUS (cp_aexp S a1) (cp_aexp S a2)
423541

424-
425542
@[grind] def cp_bexp (S : Store) (b : bexp) : bexp :=
426543
match b with
427544
| .TRUE => .TRUE
@@ -431,13 +548,17 @@ theorem fixpoint_sound (F : Store → Store) (init_S : Store) (h : S = fixpoint
431548
| .NOT b => mk_NOT (cp_bexp S b)
432549
| .AND b1 b2 => mk_AND (cp_bexp S b1) (cp_bexp S b2)
433550

551+
/-
552+
Under the assumption that the concrete store matchess with the abstract store,
553+
these optimized expressions evaluate to the same values as the original
554+
expressions.
555+
-/
434556
@[grind] theorem cp_aexp_sound :
435557
∀ s S, matches' s S ->
436558
∀ a, aeval s (cp_aexp S a) = aeval s a := by
437559
intro s S AG a
438560
induction a
439561
all_goals try grind
440-
-- Unfortunately this grind call explodes on one of the goals since v4.23.0-rc2
441562
all_goals grind [mk_PLUS_sound, mk_MINUS_sound]
442563

443564
theorem cp_bexp_sound :
@@ -446,6 +567,19 @@ theorem cp_bexp_sound :
446567
intro s S AG b
447568
induction b with grind [mk_EQUAL_sound, mk_LESSEQUAL_sound, mk_AND_sound, mk_NOT_sound]
448569

570+
/-
571+
The optimization of commands consists in propagating constants
572+
in expressions and simplifying the expressions, as above.
573+
In addition, conditionals and loops whose conditions are statically
574+
known will be simplified too, thanks to the smart constructors.
575+
576+
The `S` parameter is the abstract store "before" the execution of `c`.
577+
When recursing through `c`, it must be updated like the static analysis
578+
`Cexec` does. For example, if `c` is `.SEQ c1 c2`, `c2` is optimized
579+
using `Cexec S c1` as abstract store "before". Likewise, if
580+
`c` is a loop `.WHILE b c1`, the loop body `c1` is optimized using
581+
`Cexec S (.WHILE b c1)` as the abstract store "before".
582+
-/
449583
@[grind] noncomputable def cp_com (S : Store) (c : com) : com :=
450584
match c with
451585
| .SKIP => .SKIP
@@ -459,6 +593,13 @@ theorem cp_bexp_sound :
459593
let sfix := Cexec S (.WHILE b c)
460594
mk_WHILE (cp_bexp sfix b) (cp_com sfix c)
461595

596+
/-
597+
The proof of semantic preservation needs an unusual induction pattern:
598+
structural induction on the command `c`, plus an inner induction
599+
on the number of iterations if `c` is a loop `.WHILE b c1`.
600+
This pattern follows closely the structure of the abstract interpreter
601+
`Cexec`: structural induction on the command + local fixpoint for loops.
602+
-/
462603
theorem cp_com_correct_terminating :
463604
∀ c s1 s2 S1,
464605
cexec s1 c s2 -> matches' s1 S1 -> cexec s1 (cp_com S1 c) s2 := by

0 commit comments

Comments
 (0)