import Cse230wi25.BigStep
import Cse230wi25.L05Evidence
set_option pp.fieldNotation false
set_option pp.proofs true
open Aexp
open Bexp
open Com
Small Step Semantics
The big-step semantics we've seen so far lets us write ⟨ c, s ⟩ ==> t
to mean that when c:Com
starts executing from s:State
it can terminate
execution in t:State
.
This style of defining what a program means was quite useful in that it let us derive:
- A way of proving when two families of programs are equivalent
- A way of proving assertions about particular programs, via Floyd-Hoare logic.
However, the big-step semantics do not give us any visibility into
what happens "in the middle", e.g. as the execution marches along from
s
to t
.
Sometimes we care about the intermediate sequence of steps because
-
maybe the program does not terminate and still we want to know that nothing "bad" happens during execution, or
-
we want to reason about some low-level operations (e.g. system calls) that happen during the execution, and not just the start/final states, or
-
we want to reason about concurrent executions where we need to talk about the fine-grained interleaved executions of different threads.
Strategy
So instead of saying ⟨ c, s ⟩ ==> t
we want to model executions as a series
c₀ -> c₁ -> c₂ -> c₃ -> ...
where
- Each
cᵢ
is aConfiguration
i.e. a snapshot of the entire system, - Each
cᵢ -> cⱼ
is aSmallStep
, and we can - Link the individual step to arbitrarily long sequences of
SmallSteps
.
So our strategy has three parts, we have to define a notion of
(1) Configuration
(2) SmallStep
(3) SmallSteps
IMP
Can you think about how we might do this in lean
for our IMP language
c ::= SKIP | x <~ a | c1 ;; c2 | IF b c1 c2 | WHILE b c
Example
Suppose we have a program like
x <~ 10;; y <~ x + 1 ;; z <~ y + 2
What do you think the sequence of configurations c₀ -> ...
might look like?
An IMP Configuration
To define a small-step semantics the Configuration
must have all the information
needed to determine the "next" thing that the program does. This means, a configuration
should include both
- the state that maps each
Vname
to itsValue
(as in theBigStep
semantics), and - some representation of the remaining instructions to execute next.
We can model the latter in several ways?
-
An "instruction memory" that stores the whole program plus a program counter indicating current instruction?
-
Just store the remainder of the program!
We opt for the second option as simpler for IMP:
a Configuration
is just a pair (or tuple)
of the remainder Com
and variables' State
abbrev Configuration := (Com × State)
Example
Lets return to our original example:
abbrev com₀ : Com := x <~ 10;; (y <~ x + 1 ;; z <~ y + 2)
Suppose we want to execute the program from some starting
state st₀
where all the variables are 0
def st₀ : State := λ _ => 0
What are the sequence c₀ -> c₁ -> c₂ -> c₃
of Configuration
that we might expect?
def c₀ : Configuration := sorry
def c₁ : Configuration := sorry
def c₂ : Configuration := sorry
def c₃ : Configuration := sorry
One SmallStep
Next, lets try to work out the rules that define the SmallStep
relation.
As usual, lets use the rules style formulation which says that if the stuff
above the line holds, then the stuff below the line holds as well.
Skip
???
------------------------- [skip]
(Skip, s) -> ???
Assign
???
------------------------- [assign]
(x <~ a, s) -> ???
Sequence
???
------------------------- [seq]
(c1;;c2, s) -> ???
If
bval b s = true
--------------------------------- [if-true]
(IF b THEN c1 ELSE c2, s) -> ???
bval b s = false
--------------------------------- [if-false]
(IF b THEN c1 ELSE c2, s) -> ???
While
???
-------------------------- [while]
(WHILE b DO c, s) -> ???
Next, lets formulate the above as an inductive SmallStep
relation:
inductive SmallStep : (Com × State) -> (Com × State) -> Prop where
| Assign : ∀ {x a s},
SmallStep (x <~ a, s) (Skip, s [x := aval a s])
| Seq1 : ∀ {c s},
SmallStep ((Skip ;; c), s) (c, s)
| Seq2 : ∀ {c1 c1' c2 s s'},
SmallStep (c1, s) (c1', s') ->
SmallStep ((c1 ;; c2) , s) ((c1' ;; c2), s')
| IfTrue : ∀ {b c1 c2 s},
bval b s == true ->
SmallStep (IF b THEN c1 ELSE c2, s) (c1, s)
| IfFalse : ∀ {b c1 c2 s},
bval b s == false ->
SmallStep (IF b THEN c1 ELSE c2, s) (c2, s)
| While : ∀ { b c s },
SmallStep (Com.While b c, s) (Com.If b (c ;; (Com.While b c)) Skip, s)
Many SmallSteps
We can now extend the single SmallStep
relation to handle
arbitrarily many steps, simply by taking its transitive closure!
That is, recall the definition of star r
as taking zero or more r
-steps
inductive star {α : Type} (r: α -> α -> Prop) : α -> α -> Prop where
| refl : ∀ {a : α}, star r a a
| step : ∀ {x y z : α}, r x y -> star r y z -> star r x z
abbrev Steps := star SmallStep
notation:12 cs "~~>" cs' => SmallStep cs cs'
notation:12 cs "~~>*" cs' => Steps cs cs'
Example: Skip
Lets show that a Skip
cannot update the state.
@[simp]
theorem skip_step : ∀ {s},
((Skip, s) ~~>* (Skip, t)) <-> s=t
:=
by
sorry
Example: Assignments
Lets show that an assignment x <~ a
updates the state as follows:
@[simp]
theorem assign_step : ∀ {x a c s},
((((x <~ a) ;; c), s) ~~>* (c, s [x := aval a s]))
:=
by
sorry
Example: A Sequence of Assignments
Lets revisit our old example with (com₀, st₀) ~~>* ...
example : (com₀, st₀) ~~>* (Skip, st₀[ x := 10 ][ y := 11][ z := 13])
:= by
sorry
SmallStep
is Deterministic
Let us prove that the ~~>
relation is deterministic,
meaning, that each Configuration
can only step to at most
one other Configuration
.
In other words, if a configuration cs ~~> cs1
and cs ~~> cs2
then cs1
and cs2
must be the same.
theorem smallstep_deterministic : ∀ {cs cs1 cs2},
(cs ~~> cs1) -> (cs ~~> cs2) -> cs1 = cs2
:= by
intros cs cs1 cs2 step1 step2
induction step1 generalizing cs2 <;> cases step2 <;>
sorry
The Equivalence of BigStep
and SmallStep
Semantics
Next, let us prove that our SmallStep
and BigStep
definitions are equivalent.
How would we state the above as a theorem?
Helper lemmas about BigStep
Semantics
It will be handy to have some @[simp]
lemmas to simplify facts about the BigStep
semantics.
(Lean will automatically apply these everytime we say simp
or simp_all
.)
@[simp] theorem BigStep_skip_Iff : ∀ {s t},
(⟨ Skip, s ⟩ ==> t) <-> (s = t) :=
by
intros s t; constructor
. case mp => intro h; cases h; simp_all []
. case mpr => intros; simp_all []; constructor
@[simp] theorem BigStep_assign_Iff: ∀ {x a s t},
(⟨ x <~ a, s ⟩ ==> t) <-> (t = (s[x := aval a s]))
:= by
intros x a s t
constructor
. case mp => intro h; cases h; simp_all []
. case mpr => intros; simp_all []; constructor
@[simp] theorem BigStep_seq_Iff: ∀ {c1 c2 s t},
(⟨ c1 ;; c2, s ⟩ ==> t) <-> (∃ s', (⟨ c1, s ⟩ ==> s') ∧ (⟨ c2, s' ⟩ ==> t)) := by
intros c1 c2 s t
apply Iff.intro
. case mp => intros h; cases h; repeat constructor; repeat assumption
. case mpr => intros h; cases h; rename_i _ h12; cases h12; constructor; repeat assumption
SmallStep
implies BigStep
Next, lets try to prove that
if using many small steps we have (c, s) ~~>* (Skip, t)
then, in a single big step we have ⟨ c, s ⟩ ==> t
.
The only way to do so will be an induction on the sequence of small steps.
theorem smallstep_implies_bigstep_stuck : ∀ {c s t},
((c, s) ~~>* (Skip, t)) -> (⟨ c, s ⟩ ==> t) := by
intros c s t steps
generalize h1 : (c, s) = c_s at steps --- this is ANF / Nico's tip
generalize h2 : (Skip, t) = skip_t at steps --- this is ANF / Nico's tip
induction steps generalizing c s t
. case refl =>
cases h1
cases h2
constructor
. case step cs cs1 skip_t step_head step_tail ih =>
cases h1
cases h2
sorry
Yikes! We are stuck! We need some sort of helper lemma that will tell us something about what happens after executing a single small step!
What should that lemma say?
HINT see where we got stuck in the above proof!
theorem step_case : ∀ {c c' s s' t},
((c, s) ~~> (c', s')) -> (⟨ c', s' ⟩ ==> t) -> (⟨ c, s ⟩ ==> t)
:= by
intros c c' s s' t step1 bigstep
generalize h1 : (c, s) = cs at step1 --- this is ANF / Nico's tip
generalize h2 : (c', s') = cs' at step1 --- this is ANF / Nico's tip
induction step1 generalizing c c' s s' t <;> cases h1 <;> cases h2
. case Assign x a s =>
simp_all []
. case Seq1 c s =>
constructor
constructor
assumption
. case Seq2 c1 c1' c2 s s' _hyp step_c1 =>
cases bigstep
rename_i st2 c1'_s'_st2' c2_st2
constructor
apply step_c1
apply c1'_s'_st2'
repeat simp_all []
. case IfTrue b c1 c2 s hyp =>
constructor
simp_all []
assumption
. case IfFalse b c1 c2 s hyp =>
apply BigStep.IfFalse
simp_all []
assumption
. case While b c s =>
generalize hyp : bval b s = bv
cases bv
-- bv = false
cases bigstep
repeat simp_all []
apply BigStep.WhileFalse
assumption
-- bv = true
cases bigstep
simp_all []
rename_i h_bv h_c_w
cases h_c_w
rename_i s' hh
cases hh
apply BigStep.WhileTrue
repeat assumption
simp_all []
We can now use the above lemma to complete the proof of the smallstep_implies_bigstep
theorem.
theorem smallstep_implies_bigstep : ∀ {c s t}, ((c, s) ~~>* (Skip, t)) -> (⟨ c, s ⟩ ==> t) := by
intros c s t steps
generalize h1 : (c, s) = c_s at steps --- this is ANF / Nico's tip
generalize h2 : (Skip, t) = skip_t at steps --- this is ANF / Nico's tip
induction steps generalizing c s t <;> cases h1 <;> cases h2
. case refl =>
constructor
. case step _ step_head _ ih =>
apply step_case
apply step_head
apply ih
repeat simp_all []
BigStep
implies SmallStep
Next, lets try to prove the other direction that
IF in a single big step we have ⟨ c, s ⟩ ==> t
.
THEN using many small steps we have (c, s) ~~>* (Skip, t)
.
Of course, this proof will be by induction on the BigStep
derivation.
theorem bigstep_implies_smallstep' : ∀ {c s t},
(⟨ c, s ⟩ ==> t) -> ((c, s) ~~>* (Skip, t))
:= by
intros c s t bs
induction bs
. case Skip =>
sorry
. case Assign s x a =>
sorry
. case Seq c1 c2 s1 s2 s3 _ _ sc1 sc2 =>
sorry
. case IfTrue =>
sorry
. case IfFalse =>
sorry
. case WhileFalse b c st b_false =>
sorry
. case WhileTrue =>
sorry
Uh oh, in the Seq
case we get stuck because
- we have
⟨ c1, s ⟩ ==> s1
, which implies(c1, s) ~~>* (Skip, s1)
(by the IH) - we have
⟨ c2, s1 ⟩ ==> t
, which implies(c2, s1) ~~>* (Skip, t)
(by the IH)
But we need to join the SmallStep
sequences for c1
and c2
to get one for c1;;c2
.
But how? We need some additional lemma, but what should that lemma say?
theorem seq_steps : ∀ {c1 c1' c2 s s'},
((c1, s) ~~>* (c1', s')) ->
((c1;;c2, s) ~~>* (c1';;c2, s'))
:=
by
intros c1 c1' c2 s s' steps_c1_c1'
generalize h1 : (c1, s) = c1s at steps_c1_c1'
generalize h2 : (c1', s') = c1s' at steps_c1_c1'
induction steps_c1_c1' generalizing c1 c1' c2 s s'
case refl =>
rename_i a
cases h1
simp_all []
apply star.refl
case step =>
cases h1
cases h2
rename_i c1s'' step_c1_hd _ ih
apply star.step
constructor
apply step_c1_hd
apply ih
simp_all []
simp_all []
We can now use the above lemma to complete the proof...
---------------------------------------------------------------------------------
theorem bigstep_implies_smallstep : ∀ {c s t},
(⟨ c, s ⟩ ==> t) -> ((c, s) ~~>* (Skip, t))
:= by
intros c s t bs
induction bs
. case Skip =>
constructor
. case Assign s x a =>
apply star.step; constructor; apply star.refl
. case Seq c1 c2 st1 st2 st3 _ _ sc1 sc2 =>
apply star_trans
apply seq_steps
apply sc1
apply star.step
apply SmallStep.Seq1
apply sc2
. case IfTrue =>
constructor; apply SmallStep.IfTrue; simp_all []; assumption
. case IfFalse =>
constructor; apply SmallStep.IfFalse; simp_all []; assumption
. case WhileFalse b c st b_false =>
apply star.step; constructor; apply star_one; constructor; simp_all []
. case WhileTrue =>
rename_i b c st st1 st2 _ _ _ c_steps ih
apply star.step; constructor
apply star.step; constructor
simp_all []
apply star_trans
apply seq_steps c_steps
apply star.step; constructor
assumption
----------------------------------------------------------------------------------------------