Zulip Chat Archive

Stream: general

Topic: operational semantics


Joachim Breitner (Mar 10 2023 at 12:55):

Larry Paulson has written a blog post showing that Isabelle is nice for formalizing PL semantics, with a concrete example. Would be interesting to see someone reproducing it in Lean and comparing the two:
https://lawrencecpaulson.github.io/2023/03/08/Fun_Semantics.html

Joachim Breitner (Mar 10 2023 at 12:56):

The agda people already responded: https://github.com/pigworker/Samizdat/blob/main/ExampleSemantics.agda

Trebor Huang (Mar 10 2023 at 14:14):

Would be nice to build on the Agda one and cut out all those repetitions with tactics

Shreyas Srinivas (Mar 10 2023 at 15:26):

The original blogpost is explicitly about breaking the myth that dependent types are "THE" way to do op-semantics.

Shreyas Srinivas (Mar 10 2023 at 15:26):

Because typically this is done in something like Coq

Shreyas Srinivas (Mar 10 2023 at 15:28):

So you have the entire software foundations series.

Shreyas Srinivas (Mar 10 2023 at 15:28):

full of examples of how do to this in Coq.

Shreyas Srinivas (Mar 10 2023 at 15:28):

I can't see why doing it in lean is going to tell us anything new

Henrik Böving (Mar 10 2023 at 15:43):

I dont think this is about telling something new, this is more about showing we can do it as well.

Shreyas Srinivas (Mar 10 2023 at 16:08):

Okay. I can give it a shot. It will be useful for me. But from what I recall reading here in Zulip, the creator of PLFA is creating a version in Lean. This might be a good demo for us.

Joachim Breitner (Mar 10 2023 at 16:41):

As Hendrik says. A little Rosetta stone of semantics in theorem provers.

Sebastian Ullrich (Mar 10 2023 at 16:42):

Should also lend itself nicely to Aesop one-liners

Sebastian Ullrich (Mar 10 2023 at 16:44):

Shreyas Srinivas said:

Because typically this is done in something like Coq

By the way, as Jon pointed out in the blog post comments, this is a curious statement as Coq formalizations usually are not heavy in dependent types

Joachim Breitner (Mar 10 2023 at 16:48):

Showing off aesop would be great! This week I chatted with the person behind the Isabelle and Coq formalization of WebAssembly, and he said he'd be quite curious about lean, but is under the impression that the automation is just not there yet, compared to Isabelle's.

Shreyas Srinivas (Mar 10 2023 at 16:54):

Sebastian Ullrich said:

Shreyas Srinivas said:

Because typically this is done in something like Coq

By the way, as Jon pointed out in the blog post comments, this is a curious statement as Coq formalizations usually are not heavy in dependent types

I am not an expert on this stuff, so I won't speculate on the technical reasons. But even if Isabelle could sledgehammer the proofs away, I think usability-wise lean takes the cake. The fact that it is and works like a programming language makes it ridiculously easy for me to reason about what's happening and what to modularise away, compared to Isabelle, which often feels like some ancient sorcery ritual (especially for program generation stuff). There are some verification related things I am doing in lean, that I first tried on TLA+ and Isabelle. Lean simply makes more intuitive sense for the goal I am trying to achieve.

Shreyas Srinivas (Mar 10 2023 at 17:02):

Having said that, I'll try a dependent types based approach

Arthur Adjedj (Mar 10 2023 at 19:03):

Coq has many libraries which helps with formalizing PL related stuff, like AutoSubst, which allows you to generate many weakening/substitution lemmas automatically. This may be one of the reasons why it is seen as the de facto proof-assistant to do this kind of work

Joachim Breitner (Mar 10 2023 at 20:49):

And also generic program logic libraries like Iris, which keeps coming up all the time right now when I look for work that proves things similar to the things that I'd like to prove (or at least like to have proven). But I wouldn't mind if lean catches up eventually :-)

Shreyas Srinivas (Mar 10 2023 at 21:05):

Could we write lean versions of these libraries?

Joachim Breitner (Mar 10 2023 at 21:15):

Sure! It's just lots of work

Shreyas Srinivas (Mar 10 2023 at 21:33):

Since coq and lean use the same underlying theories, is it possible/feasible/(worthwhile considering) interoperability between coq and lean's term languages?

Mario Carneiro (Mar 10 2023 at 21:49):

https://github.com/larsk21/iris-lean

Mario Carneiro (Mar 10 2023 at 21:50):

unfortunately it is not under active development anymore as it was a student project

Shreyas Srinivas (Mar 10 2023 at 22:34):

I looked for previous work on this topic. In the book titled "Hitchhiker's guide to logical verification" written by people in this zulip, Chapter 8 covers how to do operational semantics and chapter 10, denotational semantics

Shreyas Srinivas (Mar 10 2023 at 22:36):

In lean ofc

Andrew Carter (Mar 10 2023 at 23:19):

I have church-rosser for untyped lambda calculus (which isn't quite the original blog post - but maybe in the spirit of it):
https://github.com/calcu16/lean_complexity/blob/main/src/lambda_calculus/utlc/beta/church_rosser.lean
and
https://github.com/calcu16/lean_complexity/blob/main/src/lambda_calculus/utlc/beta_eta/church_rosser.lean
I'm happy to try to get that portion of the repo into mathlib if there is interest (I think it needs a lot of cleaning up).

Kyle Miller (Mar 11 2023 at 00:28):

@Joachim Breitner Here's a Lean 4 version without using any fancy tactics: (gist link)

import Mathlib.Tactic.SolveByElim
import Std.Tactic.RCases

namespace semantics

/-- A simple expression language. -/
inductive exp
  | T | F | Zero | Succ (x : exp) | IF (p x y : exp) | EQ (x y : exp)

open exp

/-- Small-step semantics for the simple expression language. -/
inductive Eval : exp  exp  Prop
  | IF_T : Eval (IF T x y) x
  | IF_F : Eval (IF F x y) y
  | IF_Eval : Eval p q  Eval (IF p x y) (IF q x y)
  | Succ_Eval : Eval x y  Eval (Succ x) (Succ y)
  | EQ_same : Eval (EQ x x) T
  | EQ_S0 : Eval (EQ (Succ x) Zero) F
  | EQ_0S : Eval (EQ Zero (Succ y)) F
  | EQ_SS : Eval (EQ (Succ x) (Succ y)) (EQ x y)
  | EQ_Eval1 : Eval x z  Eval (EQ x y) (EQ z y)
  | EQ_Eval2 : Eval y z  Eval (EQ x y) (EQ x z)

@[simp] theorem T_simp : ¬ Eval T z := by rintro (_|_)
@[simp] theorem F_simp : ¬ Eval F z := by rintro (_|_)
@[simp] theorem Zero_simp : ¬ Eval Zero z := by rintro (_|_)

inductive tp
  | tbool | tnum

open tp

inductive TP : exp  tp  Prop
  | T : TP T tbool
  | F : TP F tbool
  | Zero : TP Zero tnum
  | IF : TP p tbool  TP x t  TP y t  TP (IF p x y) t
  | Succ : TP x tnum  TP (Succ x) tnum
  | EQ : TP x t  TP y t  TP (EQ x y) tbool

/-- Type preservation: small-step evaluation preserves the type. -/
theorem type_preservation (he : Eval x y) (ht : TP x t) : TP y t := by
  induction he generalizing t
  <;> (try rename_i ih) <;> cases ht
  <;> (try assumption)
  <;> try (constructor <;> (try apply ih) <;> assumption)
  -- One last case. The strategy seems to be to do cases on every `TP a b` where
  -- `a` or `b` isn't a variable, which could be automated.
  rename_i h1 h2
  cases h1
  cases h2
  constructor <;> assumption

/-- Evaluator for expressions. -/
def evl : exp  Nat
  | T => 1
  | F => 0
  | exp.Zero => 0
  | Succ x => evl x + 1
  | IF x y z => if evl x = 1 then evl y else evl z
  | EQ x y => if evl x = evl y then 1 else 0

@[simp] theorem Nat.add_one_ne_zero (a : Nat) : a + 1  0 := by
  simp [ Nat.succ_eq_add_one]

/-- Value preservation: the evaluator is constant modulo the small-step semantics. -/
theorem value_preservation (h : Eval x y) : evl x = evl y := by
  induction h <;> simp [evl, *, (Nat.add_one_ne_zero _).symm]

theorem lt_two_of_bool (ht : TP x tbool) : evl x < 2 := by
  generalize h : tbool = t at ht
  induction ht <;> simp [evl]
  <;> subst_vars <;> (try split) <;> simp at * <;> assumption

/-- Reflexive transitive closure of `Eval`. -/
inductive EvalStar : exp  exp  Prop
  | Id : EvalStar x x
  | Step : Eval x y  EvalStar y z  EvalStar x z

attribute [simp] EvalStar.Id

def Eval.Star (h : Eval x y) : EvalStar x y := EvalStar.Step h EvalStar.Id

@[simp] theorem exists_evalstar :  y, EvalStar x y := x, EvalStar.Id

@[simp] theorem eval_T : EvalStar T u  u = T := by
  constructor
  · generalize hT : T = v
    intro h
    induction h <;> subst_vars <;> simp at *
  · rintro rfl; simp

@[simp] theorem eval_F : EvalStar F u  u = F := by
  constructor
  · generalize hT : F = v
    intro h
    induction h <;> subst_vars <;> simp at *
  · rintro rfl; simp

@[simp] theorem eval_EQ_self : EvalStar (EQ x x) T := by
  apply Eval.Star
  constructor

@[simp] theorem eval_EQ_of_Eval (h : Eval x y) : EvalStar (EQ x y) T := by
  constructor
  apply Eval.EQ_Eval1; assumption
  apply Eval.Star
  constructor

@[simp] theorem eval_EQ_of_Eval2 (h : Eval y x) : EvalStar (EQ x y) T := by
  constructor
  apply Eval.EQ_Eval2; assumption
  apply Eval.Star
  constructor

theorem eval_not_EQ_of_succ_EQ (h : Eval (Succ x) y) : EvalStar (EQ y Zero) F := by
  cases h
  apply Eval.Star; constructor

theorem eval_not_EQ_of_succ_EQ2 (h : Eval (Succ x) y) : EvalStar (EQ Zero y) F := by
  cases h
  apply Eval.Star; constructor

/-- The obvious consequence of `type_preservation`. -/
theorem type_preservation_Star (h : EvalStar x y) (ht : TP x t) : TP y t := by
  induction h <;> solve_by_elim [type_preservation]

theorem EvalStar.Succ (h : EvalStar x y) : EvalStar (Succ x) (Succ y) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem EvalStar.IF (h : EvalStar p q) : EvalStar (IF p x y) (IF q x y) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem EvalStar.EQ1 (h : EvalStar x z) : EvalStar (EQ x y) (EQ z y) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem EvalStar.EQ2 (h : EvalStar y z) : EvalStar (EQ x y) (EQ x z) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem prediamond (f : exp  exp) (thm : {x y : exp}  EvalStar x y  EvalStar (f x) (f y))
    (h :  u, EvalStar x u  EvalStar y u) :  u, EvalStar (f x) u  EvalStar (f y) u := by
  obtain u, h, h' := h
  exact _, thm h, thm h'

/-- Partial Church-Rosser property for big-step semantics. -/
theorem diamond (hy : Eval x y) (hz : Eval x z) :  u, EvalStar y u  EvalStar z u := by
  induction hy generalizing z <;> cases hz <;> simp [*] at *
  <;> try (solve_by_elim [eval_not_EQ_of_succ_EQ, eval_not_EQ_of_succ_EQ2])
  · apply prediamond _ EvalStar.IF; solve_by_elim
  · apply prediamond _ EvalStar.Succ; solve_by_elim
  · rename_i h
    cases h
    rename_i a b c e
    exact EQ c b, e.Star.EQ1, Eval.EQ_SS.Star
  · rename_i h
    cases h
    rename_i a b c e
    exact EQ a c, e.Star.EQ2, Eval.EQ_SS.Star
  · rename_i a b c h ih
    cases h
    rename_i y h
    exact EQ y c, Eval.EQ_SS.Star, h.Star.EQ1
  · apply prediamond _ EvalStar.EQ1; solve_by_elim
  · rename_i a b c e' ih z e
    exact EQ b z, e.Star.EQ2, e'.Star.EQ1
  · rename_i a b c h ih
    cases h
    rename_i y h
    exact EQ b y, Eval.EQ_SS.Star, h.Star.EQ2
  · rename_i a b c h ih z h'
    exact EQ z b, h'.Star.EQ1, h.Star.EQ2
  · rename_i a b c h ih z h'
    -- an interesting application of ih
    obtain v, g, g' := ih h'
    exact EQ c v, g.EQ2, g'.EQ2

end semantics

Arthur Paulino (Mar 11 2023 at 00:29):

Kyle, did you just write all that? :open_mouth:

Kyle Miller (Mar 11 2023 at 00:29):

That's 181 lines (and could surely be done better) vs 115 for the Isabelle version.

Trebor Huang (Mar 11 2023 at 01:44):

Why is EQ T F stuck in the small step semantics? Or did I miss any rules?

Mario Carneiro (Mar 11 2023 at 07:20):

hm, that does seem like a bug in the formalization

Mario Carneiro (Mar 11 2023 at 07:22):

Here's a mostly-pointless golf of Kyle's proof (129 lines):

import Mathlib.Tactic.SolveByElim
import Std.Tactic.RCases

namespace semantics

/-- A simple expression language. -/
inductive exp | T | F | Zero | Succ (x : exp) | IF (p x y : exp) | EQ (x y : exp)

/-- Small-step semantics for the simple expression language. -/
inductive Eval : exp  exp  Prop
  | IF_T : Eval (.IF .T x y) x
  | IF_F : Eval (.IF .F x y) y
  | IF_Eval : Eval p q  Eval (.IF p x y) (.IF q x y)
  | Succ_Eval : Eval x y  Eval (.Succ x) (.Succ y)
  | EQ_same : Eval (.EQ x x) .T
  | EQ_S0 : Eval (.EQ (.Succ x) .Zero) .F
  | EQ_0S : Eval (.EQ .Zero (.Succ y)) .F
  | EQ_SS : Eval (.EQ (.Succ x) (.Succ y)) (.EQ x y)
  | EQ_Eval1 : Eval x z  Eval (.EQ x y) (.EQ z y)
  | EQ_Eval2 : Eval y z  Eval (.EQ x y) (.EQ x z)

@[simp] theorem T_simp : ¬ Eval .T z := fun.
@[simp] theorem F_simp : ¬ Eval .F z := fun.
@[simp] theorem Zero_simp : ¬ Eval .Zero z := fun.

inductive tp | bool | num

inductive TP : exp  tp  Prop
  | T : TP .T .bool
  | F : TP .F .bool
  | Zero : TP .Zero .num
  | IF : TP p .bool  TP x t  TP y t  TP (.IF p x y) t
  | Succ : TP x .num  TP (.Succ x) .num
  | EQ : TP x t  TP y t  TP (.EQ x y) .bool

/-- Type preservation: small-step evaluation preserves the type. -/
theorem type_preservation (he : Eval x y) (ht : TP x t) : TP y t := by
  induction he generalizing t <;> cases ht <;> (try assumption)
    <;> constructor <;> (try apply_assumption <;> assumption)
    <;> casesm* TP (.Succ _) _ <;> assumption

def evl : exp  Nat
  | .T => 1
  | .F => 0
  | .Zero => 0
  | .Succ x => evl x + 1
  | .IF x y z => if evl x = 1 then evl y else evl z
  | .EQ x y => if evl x = evl y then 1 else 0

/-- Value preservation: the evaluator is constant modulo the small-step semantics. -/
theorem value_preservation (h : Eval x y) : evl x = evl y := by
  induction h <;> simp [evl, *, (Nat.succ_ne_zero _).symm]

theorem lt_two_of_bool (ht : TP x .bool) : evl x < 2 := by
  generalize h : tp.bool = t at ht; induction ht <;> simp [evl]
    <;> subst_vars <;> (try split) <;> simp [*] at *

inductive EvalStar : exp  exp  Prop
  | id : EvalStar x x
  | step : Eval x y  EvalStar y z  EvalStar x z

attribute [simp] EvalStar.id

def Eval.star (h : Eval x y) : EvalStar x y := .step h .id

@[simp] theorem exists_evalstar :  y, EvalStar x y := x, .id

@[simp] theorem eval_T : EvalStar .T u  u = .T := by
  constructor
  · generalize hT : exp.T = v
    intro h
    induction h <;> subst_vars <;> simp at *
  · rintro rfl; simp

@[simp] theorem eval_F : EvalStar .F u  u = .F := by
  constructor
  · generalize hT : exp.F = v
    intro h
    induction h <;> subst_vars <;> simp at *
  · rintro rfl; simp

@[simp] theorem eval_EQ_self : EvalStar (.EQ x x) .T := Eval.EQ_same.star

@[simp] theorem eval_EQ_of_Eval (h : Eval x y) : EvalStar (.EQ x y) .T :=
  .step (.EQ_Eval1 h) Eval.EQ_same.star

@[simp] theorem eval_EQ_of_Eval2 (h : Eval y x) : EvalStar (.EQ x y) .T :=
  .step (.EQ_Eval2 h) Eval.EQ_same.star

theorem eval_not_EQ_of_succ_EQ (h : Eval (.Succ x) y) : EvalStar (.EQ y .Zero) .F := by
  cases h; exact Eval.EQ_S0.star

theorem eval_not_EQ_of_succ_EQ2 (h : Eval (.Succ x) y) : EvalStar (.EQ .Zero y) .F := by
  cases h; exact Eval.EQ_0S.star

/-- The obvious consequence of `type_preservation`. -/
theorem type_preservation_Star (h : EvalStar x y) (ht : TP x t) : TP y t := by
  induction h <;> solve_by_elim [type_preservation]

theorem EvalStar.Succ (h : EvalStar x y) : EvalStar (.Succ x) (.Succ y) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem EvalStar.IF (h : EvalStar p q) : EvalStar (.IF p x y) (.IF q x y) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem EvalStar.EQ1 (h : EvalStar x z) : EvalStar (.EQ x y) (.EQ z y) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem EvalStar.EQ2 (h : EvalStar y z) : EvalStar (.EQ x y) (.EQ x z) := by
  induction h <;> constructor <;> (try assumption); constructor; assumption

theorem prediamond (f : exp  exp) (thm : {x y : exp}  EvalStar x y  EvalStar (f x) (f y)) :
    ( u, EvalStar x u  EvalStar y u)   u, EvalStar (f x) u  EvalStar (f y) u
  | _, h, h' => _, thm h, thm h'

/-- Partial Church-Rosser property for big-step semantics. -/
theorem diamond (hy : Eval x y) (hz : Eval x z) :  u, EvalStar y u  EvalStar z u := by
  induction hy generalizing z <;> cases hz <;> simp [*] at *
    <;> try (solve_by_elim [eval_not_EQ_of_succ_EQ, eval_not_EQ_of_succ_EQ2])
  · solve_by_elim [prediamond _ .IF]
  · solve_by_elim [prediamond _ .Succ]
  · next h => let .Succ_Eval h := h; exact _, h.star.EQ1, Eval.EQ_SS.star
  · next h => let .Succ_Eval h := h; exact _, h.star.EQ2, Eval.EQ_SS.star
  · next h _ => let .Succ_Eval h := h; exact _, Eval.EQ_SS.star, h.star.EQ1
  · solve_by_elim [prediamond _ .EQ1]
  · next h' _ _ h => exact _, h.star.EQ2, h'.star.EQ1
  · next h _ => let .Succ_Eval h := h; exact _, Eval.EQ_SS.star, h.star.EQ2
  · next h _ _ h' => exact _, h'.star.EQ1, h.star.EQ2
  · next h ih _ h' => let v, g, g' := ih h'; exact _, g.EQ2, g'.EQ2

Anand Rao Tadipatri (Mar 11 2023 at 07:58):

Here is a version of @Kyle Miller's code using aesop. It is around 150 lines, but the aesop tags and a few new lemmas are taking up room that can be freed up. Every proof except the last one is a one-liner.

import Mathlib.Tactic.SolveByElim
import Mathlib.Tactic.Use
import Aesop

namespace Semantics

/-- A simple expression language. -/
inductive exp
  | T | F | Zero | Succ (x : exp) | IF (p x y : exp) | EQ (x y : exp)

open exp

/-- Small-step semantics for the simple expression language. -/
inductive Eval : exp  exp  Prop
  | IF_T : Eval (IF T x y) x
  | IF_F : Eval (IF F x y) y
  | IF_Eval : Eval p q  Eval (IF p x y) (IF q x y)
  | Succ_Eval : Eval x y  Eval (Succ x) (Succ y)
  | EQ_same : Eval (EQ x x) T
  | EQ_S0 : Eval (EQ (Succ x) Zero) F
  | EQ_0S : Eval (EQ Zero (Succ y)) F
  | EQ_SS : Eval (EQ (Succ x) (Succ y)) (EQ x y)
  | EQ_Eval1 : Eval x z  Eval (EQ x y) (EQ z y)
  | EQ_Eval2 : Eval y z  Eval (EQ x y) (EQ x z)

@[simp] theorem T_simp : ¬ Eval T z := by aesop (add safe cases [Eval, exp])
@[simp] theorem F_simp : ¬ Eval F z := by aesop (add safe cases [Eval, exp])
@[simp] theorem Zero_simp : ¬ Eval Zero z := by aesop (add safe cases [Eval, exp])


@[aesop safe cases] inductive tp
  | tbool | tnum

open tp

@[aesop unsafe constructors] inductive TP : exp  tp  Prop
  | T : TP T tbool
  | F : TP F tbool
  | Zero : TP Zero tnum
  | IF : TP p tbool  TP x t  TP y t  TP (IF p x y) t
  | Succ : TP x tnum  TP (Succ x) tnum
  | EQ : TP x t  TP y t  TP (EQ x y) tbool

-- Are these lemmas necessary?
@[simp] lemma TP_Succ_not_tbool : ¬(TP (Succ e) tbool) := by aesop (add unsafe cases [TP, exp])

@[aesop safe forward]
lemma TP_tnum_of_TP_Succ_tnum : TP (Succ e) tnum  TP e tnum := by aesop (add unsafe cases [TP, exp])

/-- Type preservation: small-step evaluation preserves the type. -/
theorem type_preservation (he : Eval x y) (ht : TP x t) : TP y t := by
  induction he generalizing t <;> cases ht <;> aesop

/-- Evaluator for expressions. -/
@[aesop norm unfold] def evl : exp  Nat
  | T => 1
  | F => 0
  | exp.Zero => 0
  | Succ x => evl x + 1
  | IF x y z => if evl x = 1 then evl y else evl z
  | EQ x y => if evl x = evl y then 1 else 0

@[simp] theorem Nat.add_one_ne_zero (a : Nat) : a + 1  0 := by aesop

/-- Value preservation: the evaluator is constant modulo the small-step semantics. -/
theorem value_preservation (h : Eval x y) : evl x = evl y := by induction h <;> aesop

theorem lt_two_of_bool (ht : TP x tbool) : evl x < 2 := by
  generalize h : tbool = t at ht
  induction ht <;> aesop

/-- Reflexive transitive closure of `Eval`. -/
@[aesop unsafe [cases, constructors]] inductive EvalStar : exp  exp  Prop
  | Id : EvalStar x x
  | Step : Eval x y  EvalStar y z  EvalStar x z

attribute [simp] EvalStar.Id

@[aesop unsafe apply] def Eval.Star (h : Eval x y) : EvalStar x y := EvalStar.Step h EvalStar.Id

@[simp] theorem exists_evalstar :  y, EvalStar x y := x, EvalStar.Id

@[simp] theorem eval_T : EvalStar T u  u = T := by aesop

@[simp] theorem eval_F : EvalStar F u  u = F := by aesop

@[simp] theorem eval_EQ_self : EvalStar (EQ x x) T := by
  aesop (add safe [apply Eval.Star, constructors Eval])

@[simp] theorem eval_EQ_of_Eval (h : Eval x y) : EvalStar (EQ x y) T := by
  aesop (add safe [apply Eval.EQ_Eval1, constructors Eval])

@[simp] theorem eval_EQ_of_Eval2 (h : Eval y x) : EvalStar (EQ x y) T := by
  aesop (add safe [apply Eval.EQ_Eval2, constructors Eval])

@[aesop unsafe forward, aesop safe apply]
theorem eval_not_EQ_of_succ_EQ (h : Eval (Succ x) y) : EvalStar (EQ y Zero) F := by
  cases h; aesop (add safe [apply Eval.Star, constructors Eval])

@[aesop unsafe forward, aesop safe apply]
theorem eval_not_EQ_of_succ_EQ2 (h : Eval (Succ x) y) : EvalStar (EQ Zero y) F := by
  cases h; aesop (add safe [apply Eval.Star, constructors Eval])

/-- The obvious consequence of `type_preservation`. -/
theorem type_preservation_Star (h : EvalStar x y) (ht : TP x t) : TP y t := by
  induction h <;> aesop (add safe forward [type_preservation])

@[aesop safe apply]
theorem EvalStar.Succ (h : EvalStar x y) : EvalStar (Succ x) (Succ y) := by
  induction h <;> aesop (add safe [constructors Eval])

@[trans] theorem EvalStar.trans (h : EvalStar x y) (h' : EvalStar y z) : EvalStar x z := by
  induction h <;> induction h' <;> aesop (add safe constructors [Eval, EvalStar])

@[aesop safe apply]
theorem EvalStar.IF (h : EvalStar p q) : EvalStar (IF p x y) (IF q x y) := by
  induction h <;> aesop (add safe [constructors Eval])

@[aesop safe apply]
theorem EvalStar.EQ1 (h : EvalStar x z) : EvalStar (EQ x y) (EQ z y) := by
  induction h <;> aesop (add safe [constructors Eval])

@[aesop safe apply]
theorem EvalStar.EQ2 (h : EvalStar y z) : EvalStar (EQ x y) (EQ x z) := by
  induction h <;> aesop (add safe [constructors Eval])

@[aesop safe apply]
theorem prediamond (f : exp  exp) (thm : {x y : exp}  EvalStar x y  EvalStar (f x) (f y))
    (h :  u, EvalStar x u  EvalStar y u) :  u, EvalStar (f x) u  EvalStar (f y) u := by aesop

set_option maxHeartbeats 10000000

/-- Partial Church-Rosser property for big-step semantics. -/
theorem diamond (hy : Eval x y) (hz : Eval x z) :  u, EvalStar y u  EvalStar z u := by
  induction hy generalizing z <;> cases hz <;>
  checkpoint aesop (options := {terminal := false, warnOnNonterminal := false})
  · apply prediamond _ EvalStar.IF; aesop
  · cases a; rename_i z h
    use (EQ z y_1); aesop (add unsafe apply [EvalStar.EQ1, EvalStar.EQ2, Eval.Star, Eval.EQ_SS], safe constructors [Eval])
  · cases a; rename_i y_2 h
    use (EQ x_1 y_2); aesop (add safe apply [Eval.Star], safe constructors [Eval])
  · cases a; rename_i x_2 h
    use (EQ x_2 y_1); aesop (add unsafe apply [EvalStar.EQ1, EvalStar.EQ2, Eval.Star, Eval.EQ_SS], safe constructors [Eval])
  · let w, _, _ := a_ih a_1
    use (EQ w y_1); aesop (add unsafe apply [EvalStar.EQ1, EvalStar.EQ2, Eval.Star, Eval.EQ_SS])
  · cases a; rename_i y_2 h
    use (EQ x_1 y_2); aesop (add unsafe apply [EvalStar.EQ1, EvalStar.EQ2, Eval.Star, Eval.EQ_SS], safe constructors [Eval])

end Semantics

Joachim Breitner (Mar 11 2023 at 10:15):

Trebor Huang said:

Why is EQ T F stuck in the small step semantics? Or did I miss any rules?

Yes, that is likely an omission in the original; this was also noticed in the Agda formalization

Sebastian Ullrich (Mar 11 2023 at 18:18):

Necessity of dependent types aside, I did find Conor McBride's approach quite elegant. Here is a Lean 4 version, making use of tactics where possible (90 lines without blank and comment lines).
In the end there wasn't really any part that could further be simplified with Aesop.

namespace Semantics

/-- A simple type language. -/
inductive Ty where
  | bool | num

/-- A simple expression language, intrinsically typed. -/
inductive Exp : Ty  Type where
  | true : Exp .bool
  | false : Exp .bool
  | zero : Exp .num
  | succ (n : Exp .num) : Exp .num
  | if (p : Exp .bool) (t e : Exp ty) : Exp ty
  | eq (x y : Exp ty) : Exp .bool

section
-- mutual notation/inductive workaround, I should really fix this at some point
set_option hygiene false
local infix:40 "~>" => Eval
/-- Small-step semantics for the simple expression language. -/
inductive Eval : Exp ty  Exp ty  Prop
  | if_true : .if .true t e ~> t
  | if_false : .if .false t e ~> e
  | if_eval : p ~> q 
              .if p t e ~> .if q t e
  | succ_eval : x ~> y 
                .succ x ~> .succ y
  | eq_same : .eq x x ~> .true
  | eq_s0 : .eq (.succ x) .zero ~> .false
  | eq_0s : .eq .zero (.succ y) ~> .false
  | eq_ss : .eq (.succ x) (.succ y) ~> .eq x y
  | eq_tf : .eq .true .false ~> .false
  | eq_ft : .eq .false .true ~> .false
  | eq_l : x ~> x' 
           .eq x y ~> .eq x' y
  | eq_r : y ~> y' 
           .eq x y ~> .eq x y'
end
infix:40 " ~> " => Eval

/-- Typed evaluation. -/
abbrev Val : Ty  Type
  | .bool => Bool
  | .num  => Nat

-- Lean's termination checker is (understandably) not happy when specified as polymorphic recursion
def Val.embed : (ty : Ty)  Val ty  Exp ty
  | .bool, true  => .true
  | .bool, false => .false
  | .num,  n     => n.fold (fun _ => .succ) .zero

instance : DecidableEq (Val ty) := by
  cases ty <;> infer_instance

def eval : Exp ty  Val ty
  | .true     => true
  | .false    => false
  | .zero     => 0
  | .succ n   => eval n + 1
  | .if p t e => if eval p then eval t else eval e
  | .eq x y   => eval x = eval y

/-- The reflexive-transitive closure -/
inductive RTC (r : α  α  Prop) : α  α  Prop where
  | refl : RTC r x x
  | step : r x y  RTC r y z  RTC r x z

theorem RTC.one : r x y  RTC r x y := (.step · .refl)

theorem RTC.map {f : α  β} (hrs :  {x y}, r x y  s (f x) (f y)) : RTC r x y  RTC s (f x) (f y) := by
  intro h
  induction h with
  | refl          => exact .refl
  | step hxz _ ih => exact .step (hrs hxz) ih

theorem RTC.trans : RTC r x y  RTC r y z  RTC r x z := by
  intro hxy hyz
  induction hxy with
  | refl         => exact hyz
  | step hx _ ih => exact .step hx (ih hyz)

infix:40 " ~>* " => RTC Eval

@[simp] theorem Nat.succ_ne_zero : n + 1  0 := by intro; trivial
@[simp] theorem Nat.zero_ne_succ : 0  n + 1 := by intro; trivial

/-- Operational and denotational semantics coincide. -/
theorem eval_eval : x ~>* (eval x).embed := by
  induction x with
    rw [eval]
  | succ => exact RTC.map .succ_eval _
  | «if» p t e =>
    -- first evaluate the predicate
    -- (Lean needs some help here with the higher-order unification)
    apply RTC.trans (RTC.map (f := (Exp.if · t e)) .if_eval _›)
    -- then do an if_true/if_false step depending on `p`
    cases eval p <;> exact RTC.step (by constructor) _
  | @eq ty x y =>
    -- first evaluate the LHS and RHS
    apply RTC.trans (RTC.map (f := (Exp.eq · y)) .eq_l _›)
    apply RTC.trans (RTC.map (f := (Exp.eq _ ·)) .eq_r _›)
    -- we want to worry about values, not terms
    generalize eval y = ey
    -- then do a case bash including one induction for the recursive `succ/succ` case
    cases ty <;> induction eval x generalizing ey <;> cases ey
    case num.succ.succ ih _ =>
      apply RTC.step .eq_ss
      -- massage goal a bit to fit IH
      simp only [Val, Nat.succ.injEq]
      apply ih
    -- all non-recursive cases can be solved uniformly
    all_goals exact RTC.one (by constructor)
  | _ => exact .refl

/-- Small steps preserve denotational semantics. -/
theorem step_eval : x ~> y  eval x = eval y := by
  intro hxy
  -- `simp` needs a bit extra help with unfolding `Val .num` to `Nat`
  induction hxy <;> (dsimp [eval, Val]; simp [*])

/-- Partial Church-Rosser is a direct consequence of the above two theorems. -/
theorem diamond : x ~> y  x ~> z   u, y ~>* u  z ~>* u :=
  fun hxy hxz =>
    ⟨(eval x).embed, step_eval hxy  eval_eval, step_eval hxz  eval_eval

end Semantics

Last updated: Dec 20 2023 at 11:08 UTC