Copyright © 2018–2023 Anne Baanen, Alexander Bentkamp, Jasmin Blanchette, Johannes Hölzl, and Jannis Limperg. See LICENSE.txt.

import LoVe.LoVelib

LoVe Demo 9: Operational Semantics

In this and the next chapter, we will see how to use Lean to specify the syntax and semantics of programming languages and to reason about the semantics.

namespace LoVe

Formal Semantics

A formal semantics helps specify and reason about the programming language itself, and about individual programs.

It can form the basis of verified compilers, interpreters, verifiers, static analyzers, type checkers, etc. Without formal proofs, these tools are almost always wrong.

In this area, proof assistants are widely used. Every year, about 10-20% of POPL papers are partially or totally formalized. Reasons for this success:

Case in point: WebAssembly. To quote Conrad Watt (with some abbreviations):

We have produced a full Isabelle mechanisation of the core execution
semantics and type system of the WebAssembly language. To complete this
proof, **several deficiencies** in the official WebAssembly specification,
uncovered by our proof and modelling work, needed to be corrected. In some
cases, these meant that the type system was **originally unsound**.

We have maintained a constructive dialogue with the working group,
verifying new features as they are added. In particular, the mechanism by
which a WebAssembly implementation interfaces with its host environment was
not formally specified in the working group's original paper. Extending our
mechanisation to model this feature revealed a deficiency in the WebAssembly
specification that **sabotaged the soundness** of the type system.

A Minimalistic Imperative Language

A state s is a function from variable names to values (String → ℕ).

WHILE is a minimalistic imperative language with the following grammar:

S  ::=  skip                 -- no-op
     |  x := a               -- assignment
     |  S; S                 -- sequential composition
     |  if B then S else S   -- conditional statement
     |  while B do S         -- while loop

where S stands for a statement (also called command or program), x for a variable, a for an arithmetic expression, and B for a Boolean expression.

#print State

inductive Stmt : Type where
  | skip       : Stmt
  | assign     : String → (State → ℕ) → Stmt
  | seq        : Stmt → Stmt → Stmt
  | ifThenElse : (State → Prop) → Stmt → Stmt → Stmt
  | whileDo    : (State → Prop) → Stmt → Stmt

infixr:90 "; " => Stmt.seq

In our grammar, we deliberately leave the syntax of arithmetic and Boolean expressions unspecified. In Lean, we have the choice:

This corresponds to the difference between deep and shallow embeddings:

A deep embedding allows us to reason about the syntax (and its semantics). A shallow embedding is more lightweight, because we can use it directly, without having to define a semantics.

We will use a deep embedding of programs (which we find interesting), and shallow embeddings of assignments and Boolean expressions (which we find boring).

The program

while x > y do skip; x := x - 1

is modeled as

def sillyLoop : Stmt :=
  Stmt.whileDo (fun s ↦ s "x" > s "y")
    (Stmt.skip;
     Stmt.assign "x" (fun s ↦ s "x" - 1))

Big-Step Semantics

An operational semantics corresponds to an idealized interpreter (specified in a Prolog-like language). Two main variants:

In a big-step semantics (also called natural semantics), judgments have the form (S, s) ⟹ t:

Starting in a state `s`, executing `S` terminates in the state `t`.

Example:

`(x := x + y; y := 0, [x ↦ 3, y ↦ 5]) ⟹ [x ↦ 8, y ↦ 0]`

Derivation rules:

——————————————— Skip
(skip, s) ⟹ s

——————————————————————————— Assign
(x := a, s) ⟹ s[x ↦ s(a)]

(S, s) ⟹ t   (T, t) ⟹ u
——————————————————————————— Seq
(S; T, s) ⟹ u

(S, s) ⟹ t
————————————————————————————— If-True   if s(B) is true
(if B then S else T, s) ⟹ t

(T, s) ⟹ t
————————————————————————————— If-False   if s(B) is false
(if B then S else T, s) ⟹ t

(S, s) ⟹ t   (while B do S, t) ⟹ u
—————————————————————————————————————— While-True   if s(B) is true
(while B do S, s) ⟹ u

————————————————————————— While-False   if s(B) is false
(while B do S, s) ⟹ s

Above, s(e) denotes the value of expression e in state s and s[x ↦ s(e)] denotes the state that is identical to s except that the variable x is bound to the value s(e).

In Lean, the judgment corresponds to an inductive predicate, and the derivation rules correspond to the predicate's introduction rules. Using an inductive predicate as opposed to a recursive function allows us to cope with nontermination (e.g., a diverging while) and nondeterminism (e.g., multithreading).

inductive BigStep : Stmt × State → State → Prop where
  | skip (s) :
    BigStep (Stmt.skip, s) s
  | assign (x a s) :
    BigStep (Stmt.assign x a, s) (s[x ↦ a s])
  | seq (S T s t u) (hS : BigStep (S, s) t)
      (hT : BigStep (T, t) u) :
    BigStep (S; T, s) u
  | if_true (B S T s t) (hcond : B s)
      (hbody : BigStep (S, s) t) :
    BigStep (Stmt.ifThenElse B S T, s) t
  | if_false (B S T s t) (hcond : ¬ B s)
      (hbody : BigStep (T, s) t) :
    BigStep (Stmt.ifThenElse B S T, s) t
  | while_true (B S s t u) (hcond : B s)
      (hbody : BigStep (S, s) t)
      (hrest : BigStep (Stmt.whileDo B S, t) u) :
    BigStep (Stmt.whileDo B S, s) u
  | while_false (B S s) (hcond : ¬ B s) :
    BigStep (Stmt.whileDo B S, s) s

infix:110 " ⟹ " => BigStep

theorem sillyLoop_from_1_BigStep :
  (sillyLoop, (fun _ ↦ 0)["x" ↦ 1]) ⟹ (fun _ ↦ 0) :=
  by
    rw [sillyLoop]
    apply BigStep.while_true
    { simp }
    { apply BigStep.seq
      { apply BigStep.skip }
      { apply BigStep.assign } }
    { simp
      apply BigStep.while_false
      simp }

Properties of the Big-Step Semantics

Equipped with a big-step semantics, we can

theorem BigStep_deterministic {Ss l r} (hl : Ss ⟹ l)
    (hr : Ss ⟹ r) :
  l = r
:=
  by
    induction hl generalizing r with
    | skip s =>
      cases hr with
      | skip => rfl
    | assign x a s =>
      cases hr with
      | assign => rfl
    | seq S T s l₀ l hS hT ihS ihT =>
      cases hr with
      | seq _ _ _ r₀ _ hS' hT' =>
        cases ihS hS' with
        | refl =>
          cases ihT hT' with
          | refl => rfl
    | if_true B S T s l hB hS ih =>
      cases hr with
      | if_true _ _ _ _ _ hB' hS'  => apply ih hS'
      | if_false _ _ _ _ _ hB' hS' => aesop
    | if_false B S T s l hB hT ih =>
      cases hr with
      | if_true _ _ _ _ _ hB' hS'  => aesop
      | if_false _ _ _ _ _ hB' hS' => apply ih hS'
    | while_true B S s l₀ l hB hS hw ihS ihw =>
      cases hr with
      | while_true _ _ _ r₀ hB' hB' hS' hw' =>
        cases ihS hS' with
        | refl =>
          cases ihw hw' with
          | refl => rfl
      | while_false _ _ _ hB' => aesop
    | while_false B S s hB =>
      cases hr with
      | while_true _ _ _ s' _ hB' hS hw => aesop
      | while_false _ _ _ hB'           => rfl


theorem BigStep_terminates {S s} :
  ∃t, (S, s) ⟹ t
:=
  sorry   -- unprovable

We can define inversion rules about the big-step semantics:

@[simp] theorem BigStep_skip_Iff {s t} :
  (Stmt.skip, s) ⟹ t ↔ t = s
:=
  by
    apply Iff.intro
    { intro h
      cases h with
      | skip => rfl }
    { intro h
      rw [h]
      apply BigStep.skip }

@[simp] theorem BigStep_assign_Iff {x a s t} :
  (Stmt.assign x a, s) ⟹ t ↔ t = s[x ↦ a s]
:=
  by
    apply Iff.intro
    { intro h
      cases h with
      | assign => rfl }
    { intro h
      rw [h]
      apply BigStep.assign }

@[simp] theorem BigStep_seq_Iff {S T s u} :
  (S; T, s) ⟹ u ↔ (∃t, (S, s) ⟹ t ∧ (T, t) ⟹ u)
:=
  by
    apply Iff.intro
    { intro h
      cases h with
      | seq =>
        apply Exists.intro
        apply And.intro <;>
          assumption }
    { intro h
      cases h with
      | intro s' h' =>
        cases h' with
        | intro hS hT =>
          apply BigStep.seq <;>
            assumption }

@[simp] theorem BigStep_if_Iff {B S T s t} :
  (Stmt.ifThenElse B S T, s) ⟹ t ↔
  (B s ∧ (S, s) ⟹ t) ∨ (¬ B s ∧ (T, s) ⟹ t)
:=
  by
    apply Iff.intro
    { intro h
      cases h with
      | if_true _ _ _ _ _ hB hS =>
        apply Or.intro_left
        aesop
      | if_false _ _ _ _ _ hB hT =>
        apply Or.intro_right
        aesop }
    { intro h
      cases h with
      | inl h =>
        cases h with
        | intro hB hS =>
          apply BigStep.if_true <;>
            assumption
      | inr h =>
        cases h with
        | intro hB hT =>
          apply BigStep.if_false <;>
            assumption }

theorem BigStep_while_Iff {B S s u} :
  (Stmt.whileDo B S, s) ⟹ u ↔
  (∃t, B s ∧ (S, s) ⟹ t ∧ (Stmt.whileDo B S, t) ⟹ u)
  ∨ (¬ B s ∧ u = s)
:=
  by
    apply Iff.intro
    { intro h
      cases h with
      | while_true _ _ _ t _ hB hS hw =>
        apply Or.intro_left
        apply Exists.intro t
        aesop
      | while_false _ _ _ hB =>
        apply Or.intro_right
        aesop }
    { intro h
      cases h with
      | inl hex =>
        cases hex with
        | intro t h =>
          cases h with
          | intro hB h =>
            cases h with
            | intro hS hwhile =>
              apply BigStep.while_true <;>
                assumption
      | inr h =>
        cases h with
        | intro hB hus =>
          rw [hus]
          apply BigStep.while_false
          assumption}

@[simp] theorem BigStep_while_true_Iff {B S s u}
    (hcond : B s) :
  (Stmt.whileDo B S, s) ⟹ u ↔
  (∃t, (S, s) ⟹ t ∧ (Stmt.whileDo B S, t) ⟹ u)
:=
  by
    rw [BigStep_while_Iff]
    simp [hcond]

@[simp] theorem BigStep_while_false_Iff {B S s t}
    (hcond : ¬ B s) :
  (Stmt.whileDo B S, s) ⟹ t ↔ t = s
:=
  by
    rw [BigStep_while_Iff]
    simp [hcond]

Small-Step Semantics

A big-step semantics

Small-step semantics (also called structural operational semantics) solve the above issues.

A judgment has the form (S, s) ⇒ (T, t):

Starting in a state `s`, executing one step of `S` leaves us in the
state `t`, with the program `T` remaining to be executed.

An execution is a finite or infinite chain (S₀, s₀) ⇒ (S₁, s₁) ⇒ ….

A pair (S, s) is called a configuration. It is final if no transition of the form (S, s) ⇒ _ is possible.

Example:

  `(x := x + y; y := 0, [x ↦ 3, y ↦ 5])`
`⇒ (skip; y := 0,       [x ↦ 8, y ↦ 5])`
`⇒ (y := 0,             [x ↦ 8, y ↦ 5])`
`⇒ (skip,               [x ↦ 8, y ↦ 0])`

Derivation rules:

————————————————————————————————— Assign
(x := a, s) ⇒ (skip, s[x ↦ s(a)])

(S, s) ⇒ (S', s')
———-——————————————————— Seq-Step
(S; T, s) ⇒ (S'; T, s')

————————————————————— Seq-Skip
(skip; S, s) ⇒ (S, s)

———————————————————————————————— If-True   if s(B) is true
(if B then S else T, s) ⇒ (S, s)

———————————————————————————————— If-False   if s(B) is false
(if B then S else T, s) ⇒ (T, s)

——————————————————————————————————————————————————————————————— While
(while B do S, s) ⇒ (if B then (S; while B do S) else skip, s)

There is no rule for skip (why?).

inductive SmallStep : Stmt × State → Stmt × State → Prop where
  | assign (x a s) :
    SmallStep (Stmt.assign x a, s) (Stmt.skip, s[x ↦ a s])
  | seq_step (S S' T s s') (hS : SmallStep (S, s) (S', s')) :
    SmallStep (S; T, s) (S'; T, s')
  | seq_skip (T s) :
    SmallStep (Stmt.skip; T, s) (T, s)
  | if_true (B S T s) (hcond : B s) :
    SmallStep (Stmt.ifThenElse B S T, s) (S, s)
  | if_false (B S T s) (hcond : ¬ B s) :
    SmallStep (Stmt.ifThenElse B S T, s) (T, s)
  | whileDo (B S s) :
    SmallStep (Stmt.whileDo B S, s)
      (Stmt.ifThenElse B (S; Stmt.whileDo B S) Stmt.skip, s)

infixr:100 " ⇒ " => SmallStep
infixr:100 " ⇒* " => RTC SmallStep

theorem sillyLoop_from_1_SmallStep :
  (sillyLoop, (fun _ ↦ 0)["x" ↦ 1]) ⇒*
  (Stmt.skip, (fun _ ↦ 0)) :=
  by
    rw [sillyLoop]
    apply RTC.head
    { apply SmallStep.whileDo }
    { apply RTC.head
      { apply SmallStep.if_true
        simp }
      { apply RTC.head
        { apply SmallStep.seq_step
          apply SmallStep.seq_skip }
        { apply RTC.head
          { apply SmallStep.seq_step
            apply SmallStep.assign }
          { apply RTC.head
            { apply SmallStep.seq_skip }
            { apply RTC.head
              { apply SmallStep.whileDo }
              { apply RTC.head
                { apply SmallStep.if_false
                  simp }
                { simp
                  apply RTC.refl } } } } } } }

Equipped with a small-step semantics, we can define a big-step semantics:

`(S, s) ⟹ t` if and only if `(S, s) ⇒* (skip, t)`

where r* denotes the reflexive transitive closure of a relation r.

Alternatively, if we have already defined a big-step semantics, we can prove the above equivalence theorem to validate our definitions.

The main disadvantage of small-step semantics is that we now have two relations, and ⇒*, and reasoning tends to be more complicated.

Properties of the Small-Step Semantics

We can prove that a configuration (S, s) is final if and only if S = skip. This ensures that we have not forgotten a derivation rule.

theorem SmallStep_final (S s) :
  (¬ ∃T t, (S, s) ⇒ (T, t)) ↔ S = Stmt.skip
:=
  by
    induction S with
    | skip =>
      simp
      intros T t hstep
      cases hstep
    | assign x a =>
      simp
      apply Exists.intro Stmt.skip
      apply Exists.intro (s[x ↦ a s])
      apply SmallStep.assign
    | seq S T ihS ihT =>
      simp
      cases Classical.em (S = Stmt.skip) with
      | inl h =>
        rw [h]
        apply Exists.intro T
        apply Exists.intro s
        apply SmallStep.seq_skip
      | inr h =>
        simp [h] at ihS
        cases ihS with
        | intro S' hS₀ =>
          cases hS₀ with
          | intro s' hS =>
            apply Exists.intro (S'; T)
            apply Exists.intro s'
            apply SmallStep.seq_step
            assumption
    | ifThenElse B S T ihS ihT =>
      simp
      cases Classical.em (B s) with
      | inl h =>
        apply Exists.intro S
        apply Exists.intro s
        apply SmallStep.if_true
        assumption
      | inr h =>
        apply Exists.intro T
        apply Exists.intro s
        apply SmallStep.if_false
        assumption
    | whileDo B S ih =>
      simp
      apply Exists.intro
        (Stmt.ifThenElse B (S; Stmt.whileDo B S)
           Stmt.skip)
      apply Exists.intro s
      apply SmallStep.whileDo

theorem SmallStep_deterministic {Ss Ll Rr}
    (hl : Ss ⇒ Ll) (hr : Ss ⇒ Rr) :
  Ll = Rr
:=
  by
    induction hl generalizing Rr with
    | assign x a s =>
      cases hr with
      | assign _ _ _ => rfl
    | seq_step S S₁ T s s₁ hS₁ ih =>
      cases hr with
      | seq_step S S₂ _ _ s₂ hS₂ =>
        have hSs₁₂ :=
          ih hS₂
        aesop
      | seq_skip => cases hS₁
    | seq_skip T s =>
      cases hr with
      | seq_step _ S _ _ s' hskip => cases hskip
      | seq_skip                  => rfl
    | if_true B S T s hB =>
      cases hr with
      | if_true  => rfl
      | if_false => aesop
    | if_false B S T s hB =>
      cases hr with
      | if_true  => aesop
      | if_false => rfl
    | whileDo B S s =>
      cases hr with
      | whileDo => rfl

We can define inversion rules also about the small-step semantics. Here are three examples:

theorem SmallStep_skip {S s t} :
  ¬ ((Stmt.skip, s) ⇒ (S, t))
:=
  by
    intro h
    cases h

@[simp] theorem SmallStep_seq_Iff {S T s Ut} :
  (S; T, s) ⇒ Ut ↔
  (∃S' t, (S, s) ⇒ (S', t) ∧ Ut = (S'; T, t))
  ∨ (S = Stmt.skip ∧ Ut = (T, s))
:=
  by
    apply Iff.intro
    { intro hST
      cases hST with
      | seq_step _ S' _ _ s' hS =>
        apply Or.intro_left
        apply Exists.intro S'
        apply Exists.intro s'
        aesop
      | seq_skip =>
        apply Or.intro_right
        aesop }
    {
      intro hor
      cases hor with
      | inl hex =>
        cases hex with
        | intro S' hex' =>
          cases hex' with
          | intro s' hand =>
            cases hand with
            | intro hS hUt =>
              rw [hUt]
              apply SmallStep.seq_step
              assumption
      | inr hand =>
        cases hand with
        | intro hS hUt =>
          rw [hS, hUt]
          apply SmallStep.seq_skip }

@[simp] theorem SmallStep_if_Iff {B S T s Us} :
  (Stmt.ifThenElse B S T, s) ⇒ Us ↔
  (B s ∧ Us = (S, s)) ∨ (¬ B s ∧ Us = (T, s))
:=
  by
    apply Iff.intro
    { intro h
      cases h with
      | if_true _ _ _ _ hB  => aesop
      | if_false _ _ _ _ hB => aesop }
    { intro hor
      cases hor with
      | inl hand =>
        cases hand with
        | intro hB hUs =>
          rw [hUs]
          apply SmallStep.if_true
          assumption
      | inr hand =>
        cases hand with
        | intro hB hUs =>
          rw [hUs]
          apply SmallStep.if_false
          assumption }

Equivalence of the Big-Step and the Small-Step Semantics (optional)

A more important result is the connection between the big-step and the small-step semantics:

`(S, s) ⟹ t ↔ (S, s) ⇒* (Stmt.skip, t)`

Its proof, given below, is beyond the scope of this course.

theorem RTC_SmallStep_seq {S T s u}
    (h : (S, s) ⇒* (Stmt.skip, u)) :
  (S; T, s) ⇒* (Stmt.skip; T, u) :=
  by
    apply RTC.lift (fun Ss ↦ (Prod.fst Ss; T, Prod.snd Ss)) _ h
    intro Ss Ss' hrtc
    cases Ss with
    | mk S s =>
      cases Ss' with
      | mk S' s' =>
        apply SmallStep.seq_step
        assumption

theorem RTC_SmallStep_of_BigStep {Ss t} (hS : Ss ⟹ t) :
  Ss ⇒* (Stmt.skip, t) :=
  by
    induction hS with
    | skip => exact RTC.refl
    | assign =>
      apply RTC.single
      apply SmallStep.assign
    | seq S T s t u hS hT ihS ihT =>
      apply RTC.trans
      { exact RTC_SmallStep_seq ihS }
      { apply RTC.head
        apply SmallStep.seq_skip
        assumption }
    | if_true B S T s t hB hst ih =>
      apply RTC.head
      { apply SmallStep.if_true
        assumption }
      { assumption }
    | if_false B S T s t hB hst ih =>
      apply RTC.head
      { apply SmallStep.if_false
        assumption }
      { assumption }
    | while_true B S s t u hB hS hw ihS ihw =>
      apply RTC.head
      { apply SmallStep.whileDo }
      { apply RTC.head
        { apply SmallStep.if_true
          assumption }
        { apply RTC.trans
          { exact RTC_SmallStep_seq ihS }
          { apply RTC.head
            apply SmallStep.seq_skip
            assumption } } }
    | while_false B S s hB =>
      apply RTC.tail
      apply RTC.single
      apply SmallStep.whileDo
      apply SmallStep.if_false
      assumption

theorem BigStep_of_SmallStep_of_BigStep {Ss₀ Ss₁ s₂}
    (h₁ : Ss₀ ⇒ Ss₁) :
  Ss₁ ⟹ s₂ → Ss₀ ⟹ s₂ :=
  by
    induction h₁ generalizing s₂ with
    | assign x a s               => simp
    | seq_step S S' T s s' hS ih => aesop
    | seq_skip T s               => simp
    | if_true B S T s hB         => aesop
    | if_false B S T s hB        => aesop
    | whileDo B S s              => aesop

theorem BigStep_of_RTC_SmallStep {Ss t} :
  Ss ⇒* (Stmt.skip, t) → Ss ⟹ t :=
  by
    intro hS
    induction hS using RTC.head_induction_on with
    | refl =>
      apply BigStep.skip
    | head Ss Ss' hST hsmallT ih =>
      cases Ss' with
      | mk S' s' =>
        apply BigStep_of_SmallStep_of_BigStep hST
        apply ih

theorem BigStep_Iff_RTC_SmallStep {Ss t} :
  Ss ⟹ t ↔ Ss ⇒* (Stmt.skip, t)
:=
  Iff.intro RTC_SmallStep_of_BigStep BigStep_of_RTC_SmallStep

Parallelism (optional)

Let's write down a small-step semantics for the parallel execution of WHILE programs on a single state.

inductive parStep : ℕ → (List Stmt × State) → (List Stmt × State) → Prop
| intro {Ss Ss' P P' st st' i}
  (hi : i < Ss.length)
  (hS : P = Ss.nthLe i hi)
  (hs : (P, st) ⇒ (P', st')):
    parStep i (Ss, st) (Ss', st')

There's a diamond property we'd like to be true. What does this mean?

lemma parStepDiamond {i j Ss Ts Ts' s t t'}
  (hi : i < Ss.length)
  (hj : j < Ss.length)
  (hij : i ≠ j)
  (hT : parStep i (Ss, s) (Ts, t))
  (hT' : parStep j (Ss, s) (Ts', (t'))) :
    ∃ u Us, parStep j (Ts, t) (Us, u) ∧
            parStep i (Ts', t') (Us, u) :=
sorry

Can we prove it? Can we fix it?

end LoVe