import LeanFoundations.PL.ImpParser

Evaluation Function for Imp #

We saw in the Imp chapter how a naive approach to defining a function representing evaluation for Imp runs into difficulties. There, we adopted the solution of defining evaluation as a relation rather than a function. But functions have some advantages over relations: for instance, we can run a function to compute the behavior of a program, while we can only reason about a relation.

It turns out that, with a bit more work, we can define evaluation as a function. The key insight is that we can make the function partial by having it return an Option type, where none represents non-termination and some st represents termination in state st.

A Broken Evaluator #

Here's an attempt at defining an evaluation function for commands, based on the evaluation relation we saw previously. This doesn't work because of termination issues with while loops:

def ceval_step_broken (st : State) : Com  Option State
  | Com.CSkip => some st
  | Com.CAss x a => some (x !-> (aeval st a) ; st)
  | Com.CSeq c1 c2 =>
    match ceval_step_broken st c1 with
    | none => none
    | some st' => ceval_step_broken st' c2
  | Com.CIf b c1 c2 =>
    if beval st b
    then ceval_step_broken st c1
    else ceval_step_broken st c2
  | Com.CWhile b c =>
    if beval st b
    then match ceval_step_broken st c with
         | none => none
         | some st' => ceval_step_broken st' (Com.CWhile b c)  -- Problem!
    else some st

The problem is that in the CWhile case, we make a recursive call to ceval_step_broken on the same CWhile command. Lean's termination checker rejects this definition because the recursive call is not on a structurally smaller argument.

A Step-Indexed Evaluator #

The solution is to use a step-indexed evaluator. The idea is to add an extra numeric argument that decreases with each step of evaluation. When this number reaches zero, we give up and return none, indicating that the evaluation didn't complete within the given number of steps.

def ceval_step (fuel : Nat) (st : State) : Com  Option State
  | Com.CSkip => some st
  | Com.CAss x a => some (x !-> (aeval st a) ; st)
  | Com.CSeq c1 c2 =>
    match ceval_step fuel st c1 with
    | none => none
    | some st' => ceval_step fuel st' c2
  | Com.CIf b c1 c2 =>
    if beval st b
    then ceval_step fuel st c1
    else ceval_step fuel st c2
  | Com.CWhile b c =>
    match fuel with
    | 0 => none  -- Out of fuel
    | fuel' + 1 =>
      if beval st b
      then match ceval_step fuel' st c with
           | none => none
           | some st' => ceval_step fuel' st' (Com.CWhile b c)
      else some st

Exercise: 2 stars, standard (ceval_step_example1) #

Before we prove the correctness of the step-indexed evaluator, let's make sure it works on a simple example.

example : ceval_step 10 empty_st (assign "X" (AExp.ANum 2)) = some ("X" !-> 2 ; empty_st) := by
  sorry

Exercise: 2 stars, standard (ceval_step_example2) #

Let's try a sequence:

example : ceval_step 10 empty_st
  (seq (assign "X" (AExp.ANum 1)) (assign "Y" (AExp.ANum 2)))
  = some ("Y" !-> 2 ; "X" !-> 1 ; empty_st) := by
  sorry

Exercise: 3 stars, standard (ceval_step_example3) #

Let's try a conditional:

example : ceval_step 10 empty_st
  (ifthenelse (BExp.BEq (AExp.ANum 1) (AExp.ANum 1))
              (assign "X" (AExp.ANum 1))
              (assign "X" (AExp.ANum 2)))
  = some ("X" !-> 1 ; empty_st) := by
  sorry

Exercise: 4 stars, standard (ceval_step_example4) #

Now let's try a while loop. This one should terminate:

def test_while : Com :=
  seq (assign "X" (AExp.ANum 3))
  (whiledo (BExp.BNot (BExp.BEq (AExp.AId "X") (AExp.ANum 0)))
           (assign "X" (AExp.AMinus (AExp.AId "X") (AExp.ANum 1))))

example : ceval_step 10 empty_st test_while = some ("X" !-> 0 ; empty_st) := by
  sorry

Exercise: 3 stars, standard (ceval_step_example5) #

What about a while loop that doesn't terminate? If we give it enough fuel, it should run for a while. If we don't give it enough fuel, it should return none.

def infinite_loop : Com := whiledo BExp.BTrue skip

example : ceval_step 5 empty_st infinite_loop = none := by
  sorry

Relational vs. Step-Indexed Evaluation #

As for arithmetic and boolean expressions, we'd hope that the two alternative definitions of evaluation would agree on those programs that terminate. We'll now investigate this.

Exercise: 3 stars, standard (ceval_step_more) #

The ceval_step function is monotonic in the sense that, if ceval_step i st c = some st', then ceval_step (i + j) st c = some st' for any j.

theorem ceval_step_more :  i j c st st',
  ceval_step i st c = some st' 
  ceval_step (i + j) st c = some st' := by
  sorry

Exercise: 4 stars, standard (ceval__ceval_step) #

The following theorem shows that the relational and step-indexed definitions of evaluation are equivalent for terminating programs.

theorem ceval__ceval_step :  c st st',
  ceval c st st' 
   i, ceval_step i st c = some st' := by
  sorry

Exercise: 4 stars, standard (ceval_step__ceval) #

For the converse direction, we need to be a bit careful. We can't prove that if ceval_step i st c = some st' then ceval c st st', because i might be too small for c to terminate. But we can prove that if ceval_step returns some st' for some number of steps i, then the relational evaluation succeeds.

theorem ceval_step__ceval :  i c st st',
  ceval_step i st c = some st' 
  ceval c st st' := by
  sorry

Exercise: 4 stars, standard (ceval_step_complete) #

The step-indexed evaluator is complete in the sense that if a program terminates according to the relational semantics, then the step-indexed evaluator will find a result for some sufficiently large step count.

theorem ceval_step_complete :  c st st',
  ceval c st st' 
   i, ceval_step i st c = some st' := by
  exact ceval__ceval_step

Determinism of Evaluation Again #

Using the functional evaluator, we can give a shorter proof that the evaluation relation is deterministic.

theorem ceval_deterministic' :  c st st1 st2,
  ceval c st st1 
  ceval c st st2 
  st1 = st2 := by
  sorry

Reasoning About Imp Programs #

We can use either the relational or the functional definition of evaluation to reason about Imp programs. The functional definition is often more convenient for computing the behavior of programs, while the relational definition can be more convenient for proving things about programs.

Exercise: 3 stars, standard (plus2_spec) #

Here's a specification of a program that adds 2 to the value of X:

theorem plus2_spec :  st n,
  st "X" = n 
  ceval plus2 st ("X" !-> (n + 2) ; st) := by
  intro st n H
  unfold plus2
  apply ceval.E_Ass
  simp [aeval, H]

Exercise: 3 stars, standard (XtimesYinZ_spec) #

Here's a specification of a program that multiplies the values of X and Y and stores the result in Z:

theorem XtimesYinZ_spec :  st x y,
  st "X" = x 
  st "Y" = y 
  ceval XtimesYinZ st ("Z" !-> (x * y) ; st) := by
  intro st x y Hx Hy
  unfold XtimesYinZ
  apply ceval.E_Ass
  simp [aeval, Hx, Hy]

Exercise: 3 stars, standard (loop_never_stops) #

Here's a proof that our infinite loop never terminates:

theorem loop_never_stops :  st st',
  ¬ ceval loop st st' := by
  sorry

Evaluation as a Lean Function #

Our step-indexed evaluator is written as a Lean function, which means we can actually run it to compute the behavior of Imp programs.

Exercise: 2 stars, standard (factorial_in_lean) #

Let's define the factorial program and test it:

def factorial_program : Com :=
  seq (assign "Y" (AExp.ANum 1))
  (whiledo (BExp.BNot (BExp.BEq (AExp.AId "X") (AExp.ANum 0)))
    (seq (assign "Y" (AExp.AMult (AExp.AId "Y") (AExp.AId "X")))
         (assign "X" (AExp.AMinus (AExp.AId "X") (AExp.ANum 1)))))

-- Test factorial of 3

example : ceval_step 100 ("X" !-> 3 ; empty_st) factorial_program
        = some ("Y" !-> 6 ; "X" !-> 0 ; empty_st) := by
  sorry

Additional Exercises #

Exercise: 4 stars, advanced (break_imp) #

Imperative languages usually include a break or similar statement for interrupting the execution of loops. In this exercise we consider how to add break to Imp. First, we need to enrich the language of commands with an additional case.

inductive ComBreak : Type where
  | CSkip : ComBreak
  | CBreak : ComBreak  -- New: break statement
  | CAss (x : Identifier) (a : AExp) : ComBreak
  | CSeq (c1 c2 : ComBreak) : ComBreak
  | CIf (b : BExp) (c1 c2 : ComBreak) : ComBreak
  | CWhile (b : BExp) (c : ComBreak) : ComBreak

Next, we need to define the behavior of break. One way of expressing this behavior is to add another parameter to the evaluation relation that specifies whether evaluation of a command executes a break statement:

inductive Result : Type where
  | SContinue (st : State) : Result  -- Normal termination
  | SBreak (st : State) : Result     -- Terminated by break

inductive cevalBreak : ComBreak  State  Result  Prop where
  | E_Skip (st : State) :
      cevalBreak ComBreak.CSkip st (Result.SContinue st)
  | E_Break (st : State) :
      cevalBreak ComBreak.CBreak st (Result.SBreak st)
  | E_Ass (st : State) (a1 : AExp) (n : Nat) (x : Identifier) :
      aeval st a1 = n 
      cevalBreak (ComBreak.CAss x a1) st (Result.SContinue (x !-> n ; st))
  | E_SeqContinue (c1 c2 : ComBreak) (st st' st'' : State) :
      cevalBreak c1 st (Result.SContinue st') 
      cevalBreak c2 st' (Result.SContinue st'') 
      cevalBreak (ComBreak.CSeq c1 c2) st (Result.SContinue st'')
  | E_SeqBreak (c1 c2 : ComBreak) (st st' : State) :
      cevalBreak c1 st (Result.SBreak st') 
      cevalBreak (ComBreak.CSeq c1 c2) st (Result.SBreak st')
  | E_IfTrue (st st' : State) (b : BExp) (c1 c2 : ComBreak) :
      beval st b = true 
      cevalBreak c1 st (Result.SContinue st') 
      cevalBreak (ComBreak.CIf b c1 c2) st (Result.SContinue st')
  | E_IfFalse (st st' : State) (b : BExp) (c1 c2 : ComBreak) :
      beval st b = false 
      cevalBreak c2 st (Result.SContinue st') 
      cevalBreak (ComBreak.CIf b c1 c2) st (Result.SContinue st')
  | E_WhileFalse (b : BExp) (c : ComBreak) (st : State) :
      beval st b = false 
      cevalBreak (ComBreak.CWhile b c) st (Result.SContinue st)
  | E_WhileTrue (st st' st'' : State) (b : BExp) (c : ComBreak) :
      beval st b = true 
      cevalBreak c st (Result.SContinue st') 
      cevalBreak (ComBreak.CWhile b c) st' (Result.SContinue st'') 
      cevalBreak (ComBreak.CWhile b c) st (Result.SContinue st'')
  | E_WhileBreak (st st' : State) (b : BExp) (c : ComBreak) :
      beval st b = true 
      cevalBreak c st (Result.SBreak st') 
      cevalBreak (ComBreak.CWhile b c) st (Result.SContinue st')

Exercise: 3 stars, advanced (break_ignore) #

Prove the following theorem:

theorem break_ignore :  c st st' s,
  cevalBreak c st (Result.SContinue st') 
  cevalBreak (ComBreak.CSeq c (ComBreak.CAss s (AExp.ANum 1))) st (Result.SContinue (s !-> 1 ; st')) := by
  sorry

Exercise: 4 stars, advanced (while_break_true) #

Prove the following theorem:

theorem while_break_true :  b c st st',
  cevalBreak (ComBreak.CWhile b c) st (Result.SContinue st') 
  beval st' b = false := by
  sorry