@@ -6,6 +6,14 @@ Authors: Wojciech Różowski
66
77import LeroyCompilerVerificationCourse.Imp
88import Std.Data.HashMap
9+
10+
11+ /-
12+ 8. An optimization: constant propagation
13+
14+ 8.1 Simplifying expressions using smart constructors
15+ -/
16+
917open Classical in
1018instance [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+ -/
154221def 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
167237def Equal (S1 S2 : Store) := Std.HashMap.Equiv S1 S2
168238
169239noncomputable 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+ -/
172246theorem 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+ -/
309412theorem 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
443564theorem 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+ -/
462603theorem 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