doodle

CSE 230: Principles of Programming Languages CSE 230

Resources - Assignments - Schedule - Grading - Policies

Summary

CSE 230 is an introduction to the Semantics of Programming Languages. Unlike most engineering artifacts, programming languages and hence, programs are mathematical objects whose properties can be formalized. The goal of CSE 230 is to to introduce you to the fundamental mental and mechanical tools required to rigorously analyze languages and programs and to expose you to recent developments in and applications of these techniques/

We shall study operational and axiomatic semantics, two different ways of precisely capturing the meaning of programs by characterizing their executions.

We will see how the lambda calculus can be used to distill essence of computation into a few powerful constructs.

We use that as a launching pad to study expressive type systems useful for for analyzing the behavior of programs at compile-time.

We will study all of the above in the context of lean, an interactive proof assistant that will help us precisely formalize and verify our intuitions about languages and their semantics.

Students will be evaluated on the basis of 4-6 programming (proving!) assignments, and a final exam.

Prerequisites

Basic functional programming e.g. as taught in UCSD CSE 130 using languages like Haskell, OCaml, Scala, Rust, and undergraduate level discrete mathematics, i.e. logic, sets, relations.

Basics

  • Lecture: WLH 2005 TuTh 12:30p-1:50p
  • Final Exam: 03/18/2025 Tu 11:30a-2:29p
  • Podcasts: podcast.ucsd.edu
  • Piazza: Piazza

Staff and Office Hours

  • Ranjit Jhala: Tu, Th 2-3p (3110)
  • Nico Lehmann: Th 3:30pm-5:00pm (Zoom), Fr 9:30am-11am (B240A), Zoom link in canvas
  • Naomi Smith: Tu 12pm-3pm (Zoom)
  • Matt Kolosick: Mo 10am-12pm (Zoom), Thursday 10am-11am (Zoom)
  • Mingyao Shen: We 12-1p (B240A)
  • Kyle Thompson: Mo 1-2p (B260A)

Please check the CANVAS calendar before you come in case there have been any changes.

Resources

The course is loosely based on Concrete Semantics but there is no official textbook; we will link to any relevant resources that may be needed to supplement the lecture material.

Some other useful links are:

Assignments

  1. Assignment X - Github/ID due 1/17
  2. Assignment 0 - Induction due 1/22

Schedule

The schedule below outlines topics, due dates, and links to assignments. The schedule of lecture topics might change slightly, but I post a general plan so you can know roughly where we are headed.

Week 1 - Basics and Induction

Week 2 - Expressions and Evidence

Week 3 - Big Step Semantics

  • Induction on Evidence (Ch04)
  • Imperative Programs: States, Commands, Transitions
  • Program Equivalence (Ch07)

Week 4 - Small Step Semantics

  • Preservation and Progress (Ch09)

Week 5, 6 - Axiomatic Semantics

  • Assertions, Floyd-Hoare Logic, Soundness (Ch12)
  • Verification Conditions and Automatic Verification

Week 7, 8 - Simply Typed Lambda Calculus

  • Terms, Types, and Typing Rules
  • Denotational Semantics and Type Soundness

Week 9, 10 - Automated Verification

  • Satisfiability Modulo Theories
  • Flux: Refinement Type based verification for Rust

Grading

Your grade will be calculated from assignments, exam, and participation

  • Participation Quizzes (15%) Most lectures will come with a 1-2 page handout, and you can submit the handout any time up until the start of the next lecture. Credit is given for reasonable effort in engaging with the material from the day on the handout.

  • Programming Assignments (40%) There will be a total of 5-6 programming assigments, to be done in pairs but submitted individually.

  • In Class Exams (45%) We will have three "in-class exams" Th 1/30 and Tu 2/25 and Th 3/13 each worth 15% of the grade.

Comprehensive Exam: For graduate students using this course for a comprehensive exam requirement, you must get "A" achievement on the exams.

Policies

All assignments are Closed collaboration assignments, where you cannot collaborate with others (except your partner). Of course you can ask questions on Piazza, and come to office hours etc. In particular,

  • You cannot look at or use anyone else's code for the assignment
  • You cannot discuss the assignment with other students
  • You cannot post publicly about the assignment on the course message board (or on social media or other forums). Of course, you can still post questions about material from lecture or past assignments!

You should be familiar with the UCSD guidelines on academic integrity as well.

Late Work

You have a total of six late days that you can use throughout the quarter, but no more than four late days per assignment.

  • A late day means anything between 1 second and 23 hours 59 minutes and 59 seconds past a deadline
  • If you submit past the late day limit, you get 0 points for that assignment
  • There is no penalty for submitting late but within the limit

Regrades

Mistakes occur in grading. Once grades are posted for an assignment, we will allow a short period for you to request a fix (announced along with grade release). If you don't make a request in the given period, the grade you were initially given is final.

Laptop/Device Policy in Lecture

There are lots of great reasons to have a laptop, tablet, or phone open during class. You might be taking notes, getting a photo of an important moment on the board, trying out a program that we're developing together, and so on. The main issue with screens and technology in the classroom isn't your own distraction (which is your responsibility to manage), it's the distraction of other students. Anyone sitting behind you cannot help but have your screen in their field of view. Having distracting content on your screen can really harm their learning experience.

With this in mind, the device policy for the course is that if you have a screen open, you either:

  • Have only content onscreen that's directly related to the current lecture.
  • Have unrelated content open and sit in one of the back two rows of the room to mitigate the effects on other students. I may remind you of this policy if I notice you not following it in class. Note that I really don't mind if you want to sit in the back and try to multi-task in various ways while participating in lecture (I may not recommend it, but it's your time!)

Diversity and Inclusion

We are committed to fostering a learning environment for this course that supports a diversity of thoughts, perspectives and experiences, and respects your identities (including race, ethnicity, heritage, gender, sex, class, sexuality, religion, ability, age, educational background, etc.). Our goal is to create a diverse and inclusive learning environment where all students feel comfortable and can thrive.

Our instructional staff will make a concerted effort to be welcoming and inclusive to the wide diversity of students in this course. If there is a way we can make you feel more included please let one of the course staff know, either in person, via email/discussion board, or even in a note under the door. Our learning about diverse perspectives and identities is an ongoing process, and we welcome your perspectives and input.

We also expect that you, as a student in this course, will honor and respect your classmates, abiding by the UCSD Principles of Community (https://ucsd.edu/about/principles.html). Please understand that others’ backgrounds, perspectives and experiences may be different than your own, and help us to build an environment where everyone is respected and feels comfortable.

If you experience any sort of harassment or discrimination, please contact the instructor as soon as possible. If you prefer to speak with someone outside of the course, please contact the Office of Prevention of Harassment and Discrimination: https://ophd.ucsd.edu/.

Assignments

Expressions, Types and Functions

This material is based on sections 1-3 of Functional Programming in Lean

Expressions

In lean, programs are built from expressions which (if well-typed) have a value.

ExpressionValue
1 + 23
3 * 412
1 + 2 * 3 + 411

Lean expressions are mathematical expressions as in Haskell, Ocaml, etc.

  • variables get a "bound" to a single value (no "re-assignment"),
  • expressions always evaluate to the same value ("no side effects")
  • if two expressions have the same value, then replacing one with the other will not cause the program to compute a different result.

To evaluate an expression, write #eval before it in your editor, and see the result by hovering over #eval.

#eval 1 + 2

#eval 3 * 4

#check 1 + 2 * 3 + 4

Types

Every lean expression has a type.

The type tells lean -- without evaluating the expression -- what kind of value it will produce.

Why do Programming Languages need types???

Lean has a very sophisticated type system that we will learn more about soon...

To see the type of an expression, write #check before it in your editor,

Primitive: Nat

Lean has a "primitive" type Nat for natural numbers (0, 1, 2, ...) that we saw in the expressions above.

#check 1      -- Nat (natural number i.e. 0, 1, 2,...)

#check 1 + 2  -- Nat

QUIZ What is the value of the following expression?

#eval (5 - 10 : Int)


/-

(A) 5                 0%
(B) 10                0%
(C) -5                30%
(D) 0                 5%
(E) None of the above 17%

-/

Type Annotations

Sometimes lean can infer the type automatically.

But sometimes we have to (or want to) give it an annotation.

Primitive: Int

#check (1 : Int)      -- Int (..., -2, -1, 0, 1, 2,...)

#check (1 + 2 : Int)  -- Int

QUIZ What is the value of the following expression?

#eval (5 - 10 : Int)

Primitive: Float

#check 1.1      -- Float

#eval 1.1 + 2.2

Primitive: Bool

#eval true
#check true   -- Bool

#eval false
#check false  -- Bool

#eval true  && true
#eval true  && false
#eval false && true
#eval false && false

#eval true  || true
#eval true  || false
#eval false || true
#eval false || false

-- #eval false + 10    -- type error!

Primitive: String

#eval "Hello, " ++ "world!"
#check "Hello, " ++ "world!"  -- String

-- #eval "Hello, " ++ 10   -- type error!

-- #eval "Hello, " ++ world   -- unknown identifier!

Definitions

We can name expressions using the def keyword.

def hello := "hello"
def world := "world"
def one := 1
def two := 2
def five_plus_ten   := 5 + 10
def five_minus_ten  := 5 - 10
def five_minus_ten' : Int := 5 - 10

You can now #check or #eval these definitions.

#check hello
#eval hello
#eval hello ++ world
#eval five_minus_ten
#eval five_minus_ten'

Functions

In lean, functions are also expressions, as in the λ-calculus.

#check (fun (n : Nat) => n + 1)         -- Nat -> Nat
#check (λ (n : Nat) => n + 1)           -- Nat -> Nat

#check (fun (x y : Nat) => x + y)       -- Nat -> (Nat -> Nat)
#check (λ (x y : Nat) => x + y)         -- Nat -> Nat -> Nat
#check (λ (x : Nat) => λ (y: Nat) => x + y)         -- Nat -> Nat -> Nat

QUIZ What is the type of the following expression?

Nat -> Nat -> Bool

#check (fun (x y : Nat) => x > y)

Function Calls

You can call a function by putting the arguments in front

#eval (fun x => x + 1) 10

#eval ((fun x y => x + y) 100) 200

/-
((fun x y => x + y) 100) 200
==
((λ x => (λ y => x + y)) 100) 200
==
(( (λ y => 100 + y))) 200
==
100 + 200
==
300
-/

QUIZ What is the value of the following expression?

#eval (fun x y => x > y) 10 20

Function Definitions

Of course, it is more convenient to name functions as well, using def since naming allows us to reuse the function in many places.

def inc := fun (x : Int)  => x + 1

def inc'(x: Int) := x + 1

#eval inc 10
#eval inc 20
-- #eval inc true  -- type error!

def add := fun (x y : Nat) => x + y
def add' (x y : Nat) := x + y

#eval add 10 20
#eval add' 10 20

It is often convenient to write the parameters of the function to the left

The below definitions of inc' and add' are equivalent to the above definitions of inc and add.

def inc'' (x : Int) : Int := x + 1

#eval inc'' 10     -- 11


def add'' (x y : Nat) : Nat := x + y

#eval add'' 10 20  -- 30

EXERCISE Write a function max that takes two Nats and returns the maximum of the two.

EXERCISE Write a function max3 that takes three Nats and uses max to compute the maximum of all three

EXERCISE Write a function joinStringsWith of type String -> String -> String -> String that creates a new string by placing its first argument between its second and third arguments.

def mymax (x y : Nat) := if x > y then x else y

def mymax3 (x y z : Nat) := mymax x (mymax y z)

#eval mymax 67 92

-- #eval joinStringsWith ", " "this" "that"  -- "this, that"
-- #check joinStringsWith ", " -- **QUIZ** What is the type of the expression on the left?

EXERCISE Write a function volume of type Nat -> Nat -> Nat -> Nat which computes the volume of a brick given height, width, and depth.

set_option pp.fieldNotation false

Datatypes and Recursion

This material is based on

namespace MyNat

inductive Nat where
  | zero : Nat
  | succ : Nat -> Nat
  deriving Repr

open Nat

def add (n m : Nat) : Nat :=
  match n with
  | zero => m
  | succ n' => succ (add n' m)

Trick 1: Helper Lemmas

Example add_comm

Let's try to prove that add is commutative; that is, that the order of arguments does not matter.

If we plunge into trying a direct proof-by-induction, we get stuck at two places.

  1. What is the value of add m zero ?
  2. What is the value of add m (succ n) ?

It will be convenient to have helper lemmas that tell us that

  1. ∀ m, add m zero = m
  2. ∀ n m, add n (succ m) = succ (add n m)
theorem add_comm : ∀ (n m: Nat), add n m = add m n := by
  intros n m
  induction n
  sorry

end MyNat

open List

Example rev_rev

Lets look at another example where we will need helper lemmas.

Appending lists

def app {α : Type} (xs ys: List α) : List α :=
  match xs with
  | [] => ys
  | x::xs' => x :: app xs' ys

/-
app [] [3,4,5] = [3,4,5]

app (2::[]) [3,4,5] = [3,4,5]
==>
2 :: [3,4,5]
==>
[2,3,4,5]
-/

example : app [] [3,4,5] = [3,4,5] := rfl
example : app [0,1,2] [3,4,5] = [0,1,2,3,4,5] := rfl

Reversing lists

def rev {α : Type} (xs: List α) : List α :=
 match xs with
  | [] => []
  | x :: xs' => app (rev xs') [x]

example : rev [] = ([] : List Int) := rfl
example : rev [3] = [3] := rfl
example : rev [2,3] = [3,2] := rfl
example : rev [0,1,2,3] = [3,2,1,0] := rfl
example : rev (rev [0,1,2,3]) = [0,1,2,3] := rfl

rev [0,1,2,3] => rev (0 :: [1,2,3]) => app (rev [1,2,3]) [0] => app [3,2,1] [0] => [3,2,1,0]

Now, lets prove that the above was not a fluke: if you reverse a list twice then you get back the original list.

theorem rev_app : ∀ {α : Type} (xs ys : List α),
  rev (app xs ys) = app (rev ys) (rev xs) := by
    sorry

/-
rev (app xs ys) == app (rev ys) (rev xs)
rev (app [x1,x2,x3,...xn] [y1...ym])
== rev [x1...xn, y1...ym]
== [ym ... y1, xn ... x1]
== app [ym ... y1] [xn ... x1]
== app (rev ys) (rev xs)

-- |- rev (app (rev xs') [x]) = x :: xs'
      ==>
      app (rev [x]) (rev (rev xs'))
-/

theorem rev_rev : ∀ {α : Type} (xs: List α), rev (rev xs) = xs := by
  intros α xs
  induction xs
  . case nil =>
    rfl
  . case cons x xs' ih =>
    simp [rev, rev_app, app, ih]

Yikes. We're stuck again. What helper lemmas could we need?

Trick 2: Generalizing the Variables

Consider the below variant of add.

How is it different than the original?

open MyNat.Nat

def itadd (n m: MyNat.Nat) : MyNat.Nat :=
  match n with
  | zero => m
  | succ n' => itadd n' (succ m)

/-
  itadd (s s s z) (s s s s s z)
  =>
  itadd (s s z) (s s s s s s z)
  =>
  itadd (s z) (s s s s s s s z)
  =>
  itadd (z) (s s s s s s s s z)
  =>
  (s s s s s s s s z)
-/


example : itadd (succ (succ zero)) (succ zero) = succ (succ (succ zero)):= by rfl

Lets try to prove that itadd is equivalent to add.

add n' (succ m) == succ (add n' m)

theorem add_succ : ∀ (n m), MyNat.add n (succ m) = succ (MyNat.add n m) := by
 sorry

theorem itadd_eq : ∀ (n m), itadd n m = MyNat.add n m := by
  intros n
  induction n
  . case zero => simp [itadd, MyNat.add]
  . case succ n' ih =>
    simp [MyNat.add, itadd, add_succ, ih]

Ugh! Why does the proof fail???

We are left with the goal

m n' : MyNat.Nat
ih : itadd n' m = MyNat.add n' m
⊢ itadd n' (succ m) = succ (MyNat.add n' m)

IH is too weak

In the goal above, the ih only talks about itadd n m but says nothing about itadd n (succ m). In fact, the IH should be true for all values of m as we do not really care about m in this theorem. That is, we want to tell lean that the ih should be

m n' : MyNat.Nat
ih : ∀ m, itadd n' m = MyNat.add n' m
⊢ itadd n' (succ m) = succ (MyNat.add n' m)

Generalizing We can do so, by using the induction n generalizing m which tells lean that

  • we are doing induction on n and
  • we don't care about the value of m

Now, go and see what the ih looks like in the case succ ...

This time we can actually use the ih and so the proof works.

theorem add_succ : ∀ (n m), MyNat.add n (succ m) = succ (MyNat.add n m) := by
 sorry

theorem itadd_eq' : ∀ (n m: MyNat.Nat), itadd n m = MyNat.add n m := by
  intros n m
  induction n generalizing m
  case zero => simp [MyNat.add, itadd]
  case succ => simp [MyNat.add, MyNat.add_succ, itadd, *]

Trick 3: Generalizing the Induction Hypothesis

Often, the thing you want to prove is not "inductive" by itself. Meaning, that you want to prove a goal ∀n. P(n) but in fact you need to prove something stronger like ∀n. P(n) /\ Q(n) or even ∀n m, Q(n, m) which then implies your goal.

Example: Tail-Recursive sum

sum 3
=>
3 + sum 2
=>
3 + 2 + sum 1
=>
3 + 2 + 1 + sum 0
=>
3 + 2 + 1 + 0
def sum (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | n' + 1 => n + sum n'

def loop (n res : Nat) : Nat :=
  match n with
  | 0      => res
  | n' + 1 => loop n' (res + n)

def sum_tr (n: Nat) := loop n 0

-- loop (n' + 1) 0 == sum (n' + 1)
-- loop a 0  == sum a
-- loop n res  == (sum n) + res

theorem loop_sum : ∀ n res, loop n res = (sum n) + res := by
  intros n res
  induction n generalizing res
  . case zero =>
    simp [loop, sum]
  . case succ n' ih =>
    simp_arith [loop, sum, ih]

theorem sum_eq_sum_tr : ∀ n, sum_tr n = sum n := by
  simp [sum_tr, loop_sum]




/-

fn sum(mut n: Nat) -> Nat {
  let mut res = 0
  while let S n' = n {
    res = res + n;
    n   = n';
  }
  res
}
-/

Tail-Recursively Summing

In an iterative language you might write a loop to sum the numbers n + n-1 + ... + 0 e.g.

#![allow(unused)]
fn main() {
fn sum_tr(mut n: Nat) {
  let mut acc = 0;
  while let succ m = n {
    acc += n;
    n = m
  }
  return acc
}
}

We can write the above with tail-recursion

  • The recursive call is the "last thing" the function does
  • That is, the recursive result is returned without any further computation

NOTE: Go and look at itadd; is it tail recursive?

-- def sum_tr (n acc : Nat) : Nat :=
--   match n with
--   | 0 => acc
--   | n' + 1 => sum_tr n' (n + acc)

-- def sum' (n: Nat) := sum_tr n 0

Lets try to prove that sum and sum' always compute the same result.

theorem sum_eq_sum' : ∀ n, sum n = sum' n := by
  intros n
  induction n
  sorry

Oops, we are stuck.

We need to know something about sum_tr n' (n' + 1) but what?

Can you think of a suitable helper lemma, which would let us directly prove sum_eq_sum'?

theorem helper: ∀ n acc, sum_tr n acc = sum n + acc := by ...

theorem sum_eq_sum' : ∀ n, sum' n = sum n := by
  intros
  simp_arith [sum', helper]
open List

Example: Tail-Recursive sum_list

Summing a List*

def sum_list (xs : List Nat) : Nat :=
  match xs with
  | [] => 0
  | x ::xs' => x + sum_list xs'

Tail-Recursively Summing a List*

In an iterative language you might write a loop to sum the elements of a list, e.g.

#![allow(unused)]
fn main() {
fn sum_list(xs: List<Nat>) {
  let mut acc = 0;
  while let cons x ys = xs {
    acc += x;
    xs = ys
  }
  return acc
}
}

We can write the above with tail-recursion (where the recursive call is the "last thing") that the function does. (Hint: Go and look at itadd; is it tail recursive?)

def sum_list_tr (xs : List Nat) (acc : Nat): Nat :=
  match xs with
  | [] => acc
  | x :: ys => sum_list_tr ys (acc + x)

def sum_list' (xs: List Nat) := sum_list_tr xs 0

Can you figure out a suitable helper lemma that would let us complete the proof of sum_list_eq_sum_list' below?

theorem sum_list'_eq : ∀ (xs acc), sum_list_tr xs acc = sum_list xs + acc := by
  sorry

theorem sum_list_eq_sum_list' : ∀ xs, sum_list' xs = sum_list xs := by
  intros xs
  simp_arith [sum_list', sum_list'_eq]

Example: Tail-Recursive Reverse

rev_tr [0,1,2,3]
=>
loop [0,1,2,3] []
=>
loop [1,2,3] [0]
=>
loop [2,3] [1, 0]
=>
loop [3] [2, 1, 0]
=>
loop [] [3, 2, 1, 0]
=>
[3,2,1,0]
def rev_tr {α : Type} (xs res: List α) : List α :=
  match xs with
  | [] => res
  | x ::xs' => rev_tr xs' (x :: res)

def rev' (xs: List α) := rev_tr xs []

example : rev' [0,1,2,3] = [3,2,1,0] := by rfl

Can you figure out a suitable helper lemma rev_tr_app that would let us complete the proof of rev_eq_rev' below?

rev_tr xs [] == rev xs

rev_tr xs res
  == [xn, ...x3, x2,x1, 99]
  == rev xs ++ res
theorem app_nil : ∀ {α : Type} (xs: List α), app xs [] = xs := by
  sorry

theorem rev_tr_helper_theorem : ∀ {α : Type} (xs res : List α),
  rev_tr xs res = app (rev xs) res := by sorry

theorem rev_eq_rev' : ∀ {α : Type} (xs: List α), rev' xs = rev xs := by
  intros α xs
  simp [rev', rev_tr_helper_theorem, app_nil]

Example: Tail-Recursive Evaluator

Arithmetic Expressions

inductive Aexp : Type where
  | const : Nat -> Aexp
  | plus  : Aexp -> Aexp -> Aexp
  deriving Repr

open Aexp

def two_plus_three := plus (const 2) (const 3)
     alice_plus
    /          \
   bob_const   bob_const
   |            |
   2            3

Evaluating Expressions

def eval (e: Aexp) : Nat :=
  match e with
  | const n => n
  | plus e1 e2 => eval e1 + eval e2

#eval eval two_plus_three

def eval_acc (e: Aexp) (res: Nat) : Nat :=
  match e with
  | const n => n + res
  | plus e1 e2 => eval_acc e2 (eval_acc e1 res)

def eval' (e: Aexp) := eval_acc e 0

example : eval' two_plus_three = eval two_plus_three := by rfl
eval' two_plus_three
=>
eval' (plus (const 2) (const 3))
=>
eval_acc (plus (const 2) (const 3)) 0
=>
eval_acc (const 3) (eval_acc (const 2) 0)
=>
eval_acc (const 3) ( 2 + 0)
=>
eval_acc (const 3) 2
=>
3 + 2
=>
5

QUIZ: Is eval_acc tail recursive?

Lets try to prove that eval' and eval are "equivalent".

Can you figure out a suitable helper lemma that would let us complete the proof of eval_eq_eval'?

theorem magic_theorem : ∀ e res, eval_acc e res = eval e + res := by
  intros e res
  induction e generalizing res
  . case const n => rfl
  . case plus e1 e2 ih1 ih2 =>
    simp_arith [eval, eval_acc, ih1, ih2]

theorem eval_eq_eval' : ∀ e, eval e = eval' e := by
  intros e
  simp [eval', magic_theorem]

Trick 4: Functional Induction

Based on Joachim Breitner's notes on Functional Induction

def len (xs: List α) : Nat :=
  match xs with
  | [] => 0
  | _::xs' => 1 + len xs'

def alt (xs ys : List α) : List α :=
  match xs, ys with
  | [], ys => ys
  | xs, [] => xs
  | x::xs', y::ys' => x :: y :: alt xs' ys'

#eval alt [1,2,3,4] [10]

First, lets try a "brute force" proof.

exp ::= const c | var | plus exp exp | mult exp exp

(plus
  (mult
    (const 7)
    (mult (var (mult var var)))))
  (plus
    (mult (const 2) (mult var var))
    (const 10))
)

7x^3 + 2*x^2 + 10

5x^3 + 12*x^2

[7, 2, 0, 10]

[5, 12, 0, 0]

[12, 14, 0, 10]
theorem alt_len_easy : ∀ {α : Type} (xs ys : List α),
  len (alt xs ys) = len xs + len ys := by
  intros α xs ys
  induction xs, ys using alt.induct
  . case case1 => simp [alt, len]
  . case case2 => simp [alt, len]
  . case case3 x xs' y ys' ih => simp_arith [alt, len, *]


theorem alt_len : ∀ {α : Type} (xs ys : List α),
  len (alt xs ys) = len xs + len ys := by
  intros α xs ys
  induction xs
  . case nil =>
    simp [alt, len]
  . case cons x xs' ih_xs =>
    induction ys
    . case nil =>
      simp [alt, len]
    . case cons y ys' ih_ys =>
      simp [alt, len, *]
      sorry

Instead, it can be easier to do the same induction as mirrors the recursion in alt

theorem alt_len' : ∀ {α : Type} (xs ys : List α), len (alt xs ys) = len xs + len ys := by
  intros α xs ys
  induction xs, ys using alt.induct
  . case case1 => simp [alt, len]
  . case case2 => simp [alt, len]
  . case case3 => simp_arith [alt, len, *]
set_option pp.fieldNotation false

Expressions

This material is based on

Arithmetic Expressions

abbrev Val := Nat
abbrev Vname := String

inductive Aexp where
| num : Val -> Aexp
| var : Vname -> Aexp
| add : Aexp -> Aexp -> Aexp
deriving Repr

open Aexp

def aexp_5 := num 5
def aexp_x := var "x"
def aexp_x_plus_y := add (var "x") (var "y")
def aexp_2_plus_z_plus_3 := add (num 2) (add (var "z") (num 3))

States

abbrev State := Vname -> Val

-- initial state
def st0 : State := λ _ => 0


-- update state
def upd (s: State) (x: Vname) (v: Val) : State :=
  λ y => if y = x then v else s y

def st0' := upd st0 "x" 100

notation:10 st " [ " x " := " v " ] " => upd st x v

def st0'' := st0 [ "x" := 100 ] [ "y" := 99 ] [ "z" := 666 ]


#eval st0'' "z"

Evaluation

def aval (a: Aexp) (s: State) : Val :=
  match a with
  | num n => n
  | var x => s x
  | add a1 a2 => aval a1 s + aval a2 s


def st1 := st0 ["x" := 2] [ "y" := 10 ] [ "z" := 100 ]

example : aval aexp_5 st0 = 5 := rfl
example : aval aexp_x st0 = 0 := rfl

example : aval aexp_x_plus_y st1 = 12 := rfl
example : aval aexp_2_plus_z_plus_3 st1 = 105 := rfl

Constant Folding

Compilers often want to "simplify" expressions by performing, at compile time, various operations on constants.

For example, we may want to simplify an expression

add (var "x") (add (num 3) (num 1))

to just

add (var "x") (num 4)

We can implement this idea in a little recursive funciton asimp_const that does this form of constant folding in a bottom-up manner

def asimp_const (a: Aexp) : Aexp :=
  match a with
  | num n => num n
  | var x => var x
  | add a1 a2 => match asimp_const a1, asimp_const a2 with
                 | num n1, num n2 => num (n1 + n2)
                 | a1', a2' => add a1' a2'

Equivalence of Constant Folding

Lets prove that asimp_const does not change the meaning of the expression a.

theorem aval_asimp_const : ∀ a s, aval a s = aval (asimp_const a) s  := by
  sorry

TACTIC: simp_all to simplify hypotheses

Lets flip the order of the equality.

-- REWRITING hypotheses: simp_all
theorem aval_asimp_const' : ∀ a s, aval (asimp_const a) s  = aval a s := by
  sorry

Oh no! The exact same proof does not work! lean is stuck at the goal

s : State
a✝¹ a✝ : Aexp
n1✝ n2✝ : Val
x✝¹ : asimp_const a✝ = num n2✝
x✝ : asimp_const a✝¹ = num n1✝
ih2✝ : aval (asimp_const a✝¹) s = aval a✝¹ s
ih1✝ : aval (asimp_const a✝) s = aval a✝ s

⊢ n1✝ + n2✝ = aval a✝¹ s + aval a✝ s

Here, we have the hypotheses of the IH,

  • ih2✝ : aval (asimp_const a✝¹) s = aval a✝¹ s
  • ih1✝ : aval (asimp_const a✝) s = aval a✝ s

and separately we know that

  • ... : asimp_const a✝ = num n2✝
  • ... : asimp_const a✝¹ = num n1✝

So what we want is to apply the above "knowledge" to rewrite the ih1✝ and ih2✝, which is achieved by the tactic simp_all; which then makes the proof go through.

Extended Folding with a Smart Constructor

Lets extend the constant folding so that additions-by-zero are dropped, i.e. add (num 0) a or add a (num 0) just become a.

A clean way to do so is by writing a smart constructor plus which handles both the num-num-addition and the zero-addition cases as

def plus (a1 a2: Aexp) : Aexp :=
  match a1, a2 with
  | num n1, num n2 => num (n1 + n2)
  | num n , a      => if n = 0 then a else add (num n) a
  | a, num n       => if n = 0 then a else add a (num n)
  | _, _           => add a1 a2

We can now write a general simplifier asimp that recursively traverses the Aexp and invokes plus at each add

def asimp (a: Aexp) : Aexp :=
  match a with
  | num n => num n
  | var x => var x
  | add a1 a2 => plus (asimp a1) (asimp a2)

Lets try to prove that asimp does not change the meaning of an expression.

theorem aval_asimp_stuck : ∀ a s, aval a s = aval (asimp a) s := by
  intros a s
  induction a <;> simp [asimp, aval, *]
  sorry

Oof. The "direct" proof-by-induction is stuck. Can you think of a suitable "helper lemma"?

Yikes, we're stuck with a goal that looks like

⊢ aval (asimp a✝¹) s + aval (asimp a✝) s = aval (plus (asimp a✝¹) (asimp a✝)) s

We need a helper lemma that might tell us what aval (plus a1 a2) s should evaluate to?

theorem aval_aplus' : ∀ a1 a2 s,  aval (plus a1 a2) s = aval a1 s + aval a2 s := by
  intros a1 a2 s
  cases a1 <;> cases a2 <;> simp_arith [aval, plus] <;> sorry

The split tactic

Hmm, now we're left with a goal that looks like

s : State
a✝¹ : Val
a✝ : Vname
⊢ aval (if a✝¹ = 0 then var a✝ else add (num a✝¹) (var a✝)) s = a✝¹ + s a✝

We need to "split-cases" on the if a✝¹ = 0 branch ... convenient to do so with split (and then keep simp-ing!)

theorem aval_plus: ∀ a1 a2 s, aval (plus a1 a2) s = aval a1 s + aval a2 s := by
  intros a1 a2
  sorry

We can use this helper to complete the proof

theorem aval_asimp : ∀ a s, aval a s = aval (asimp a) s := by
  intros a s
  induction a <;> simp [asimp, aval, aval_plus, *]

Boolean Expressions

Lets define a datatype for boolean expressions

inductive Bexp where
  | bbool : Bool -> Bexp
  | bnot  : Bexp  -> Bexp
  | band  : Bexp  -> Bexp -> Bexp
  | bless : Aexp  -> Aexp -> Bexp
  deriving Repr

open Bexp

Boolean Evaluator

def bval (b: Bexp) (s: State) : Bool :=
  match b with
  | bbool v => v
  | bnot b' => !bval b' s
  | band b1 b2 => bval b1 s && bval b2 s
  | bless a1 a2 => aval a1 s < aval a2 s

"Smart Constructors" for Boolean Constant Folding

def smart_and (b1 b2: Bexp) : Bexp :=
  match b1, b2 with
  | bbool true, _  => b2
  | bbool false, _ => bbool false
  | _, bbool true  => b1
  | _, bbool false => bbool false
  | _, _           => band b1 b2

def smart_not (b: Bexp) : Bexp :=
  match b with
  | bbool true  => bbool false
  | bbool false => bbool true
  | _           => bnot b

def smart_less (a1 a2: Aexp) : Bexp :=
  match a1, a2 with
  | num n1, num n2 => bbool (n1 < n2)
  | _, _           => bless a1 a2

def bsimp (b: Bexp) : Bexp :=
  match b with
  | bbool v    => bbool v
  | bnot b     => smart_not (bsimp b)
  | band b1 b2 => smart_and (bsimp b1) (bsimp b2)
  | bless a1 a2 => smart_less (asimp a1) (asimp a2)

Smart Constructors are Equivalent

theorem smart_not_eq : ∀ b s, bval (smart_not b) s = bval (bnot b) s := by
  intros b s
  cases b
  . case bbool v => cases v <;> rfl
  . case bnot    => rfl
  . case band    => rfl
  . case bless   => rfl

theorem smart_and_eq : ∀ b1 b2 s,
  bval (smart_and b1 b2) s = bval (band b1 b2) s := by
  intros b1 b2 s
  cases b1 <;> cases b2 <;> simp_all [smart_and, bval] <;> split <;> simp_all [bval]

theorem smart_less_eq : ∀ a1 a2 s, bval (smart_less a1 a2) s = bval (bless a1 a2) s := by
  intros a1 a2 s
  cases a1 <;> cases a2 <;> simp_all [smart_less, bval, aval]

Correctness of Boolean Simplification

Lets prove that bval_bsimp b does not change the meaning of an expression b.

theorem bval_bsimp_stuck : ∀ b s, bval b s = bval (bsimp b) s := by
  intros b s
  sorry

Backwards Rewriting / Simplification

Boo! We cannot use aval_asimp directly.

The simp uses a theorem of the form lhs = rhs to rewrite occurrences of lhs with rhs. In this case, however,

  • the theorem aval_asimp says aval a s = aval (asimp a) s

  • but the goal contains a term aval (asimp a') s

  • where we want to rewrite the RHS of the theorem with the LHS!

You can do this either by

  1. making a new theorem where the LHS and RHS are flipped (which is a bit silly...)

  2. or instead by specifying <- in the simp tactic.

theorem bval_bsimp : ∀ b s, bval b s = bval (bsimp b) s := by
  intros b s
  sorry

Case Study: Compiling to a Stack Machine

In this case study, we will define a "stack machine" that operates on a stack of values.

Intuition

Given an a : Aexp , say

((x + 10) + (5 + y))

We will compile it into a sequence of "instructions" that operate on a stack.

LOAD x
LOADI 10
ADD
LOADI 5
LOAD y
ADD
ADD

Lets execute this sequence of instructions on a state s defined as [x |-> 100, y |-> 200] as

[]
--> LOAD x
[100]
--> LOADI 10
[10, 100]
--> ADD
[110]
--> LOADI 5
[5, 110]
--> LOAD y
[200, 5, 110]
--> ADD
[205, 110]
--> ADD
[315]

Note that the final result left on the stack is 315 which is exactly what aval a s would return.

Let's define a "compiler" and "exec" function and prove that the compiler is correct!

Stack Machine: Instructions

inductive Instr where
  | LOADI : Val -> Instr
  | LOAD  : Vname -> Instr
  | ADD   : Instr
  deriving Repr

open Instr

Stack Machine: Interpreter

A Stack is just a list of values

abbrev Stack := List Val

Here's a function exec1 that executes a single instruction

def exec1 (s:State) (i:Instr) (stk:Stack) : Stack :=
  match i, stk with
  | LOADI n, _         => (n :: stk)
  | LOAD  x, _         => (s x :: stk)
  | ADD    , n::m::stk => (m + n) :: stk
  | ADD    , _         => []

Here's a function exec that executes a sequence of instructions by invoking exec1 on each instruction.

def exec (s:State) (is: List Instr) (stk:Stack) : Stack :=
  match is with
  | []     => stk
  | i::is' => exec s is' (exec1 s i stk)

Stack Machine: Compiler

Here's a function that "compiles" an Aexp into a List Instr

def comp (a: Aexp) : List Instr :=
  match a with
  | num n => [LOADI n]
  | var x => [LOAD  x]
  | add a1 a2 => comp a1 ++ comp a2 ++ [ADD]

Proving Compiler Correctness

theorem comp_exec_stuck : ∀ {s : State} {a : Aexp} { stk : Stack },
  exec s (comp a) stk = aval a s :: stk := by
  intro s a stk
  induction a generalizing s stk
  case num n => rfl
  case var x => rfl
  case add a1 a2 ih1 ih2 => simp_all [comp, aval, exec, exec1]; sorry

Oh no! We're stuck, what helper lemma do we need?

theorem comp_exec : ∀ {s : State} {a : Aexp} { stk : Stack },
  exec s (comp a) stk = aval a s :: stk := by
  intro s a stk
  induction a generalizing s stk
  case num n => rfl
  case var x => rfl
  sorry
set_option pp.fieldNotation false

Evidence

This material is based on

The Collatz Conjecture

Per Wikipedia, the Collatz Conjecture is

one of the most famous unsolved problems in mathematics. The conjecture asks whether repeating two simple arithmetic operations will eventually transform every positive integer into 1... It concerns sequences of integers where each term is obtained from the previous term as follows:

  • if a term is even, the next term is one half of it.
  • if a term is odd, the next term is 3 times the previous term plus 1.

The conjecture is that these sequences always reach 1, no matter which positive integer is chosen to start the sequence.

The conjecture has been shown to hold for all positive integers up to $$2.95 \times 10^{20}$$, but no general proof has been found.

Lets try to model the Collatz Conjecture in Lean.

A function that computes the half of a Nat and that determines if a Nat is even.

def half (n : Nat) : Nat :=
  match n with
  | 0 => 0
  | 1 => 0
  | ((n + 1) + 1) => (half n) + 1

example : half 5 = 2 := by rfl
example : half 10 = 5 := by rfl

def even (n: Nat) : Bool :=
  match n with
  | 0 => true
  | 1 => false
  | (n + 1) + 1 => even n

example : even 5  = false := by rfl
example : even 10 = true  := by rfl

Now, lets try to write a function that computes the next term in the Collatz sequence, and use it to run the Collatz sequence starting from a given number.

def collatz_step (n : Nat) : Nat :=
  if even n then half n else 3 * n + 1

def run_collatz (steps n : Nat) : List Nat :=
  match steps with
  | 0 => [n]
  | steps + 1 => n :: run_collatz steps (collatz_step n)

Lets try to run_collatz for a few numbers...

#eval run_collatz 5  5
#eval run_collatz 17 15
#eval run_collatz 26 200

The Termination Restriction

Can we write a function to count the number of steps needed to reach 1?

-- def collatz_steps (n : Nat) : Nat :=
--   match n with
--   | 1 => 1
--   | n => collatz_steps (collatz_step n) + 1

Welp! lean is unhappy with this definition as it cannot determine the function terminates!

All our previous examples, lean was fine with because the "recursion" was on "smallar inputs, e.g. in plus below, in each recursive call, n is getting smaller -- we start with n+1 and recurse on n all the way till it reaches 0, so lean is happy. If you have (possibly) non-terminating functions, you can prove things like 1 = 2 which would make the whole system inconsistent (i.e. useless).

def plus (n m : Nat) : Nat :=
  match n with
  | 0 => m
  | n + 1 => (plus n m) + 1

So: lean and similar provers require that all recursive functions are provably terminating.

How can we then even specify the Collatz Conjecture in lean ?

Idea: Inductively Defined Evidence (or Proofs)

We can specify the collatz conjecture by defining a way to explicitly construct evidence that a given number n will eventually reach 1 in the Collatz sequence.

For example, we know that the number n reaches 1 in the Collatz sequence if

  • [ collatz-one ] if n is equal to 1, OR
  • [ collatz-evn ] if n is even and half n reaches 1, OR
  • [ collatz-odd ] if n is not even and 3*n + 1 reaches 1

We can define a datatype whose values represent the above conditions.

In PL, we often write the above three rules as follows, where the

  • "numerator" is the conditions or hypotheses under which the rule holds
  • "denominator" is the conclusion or consequence that follows from the conditions.

--------------------- [collatz-one]
collatz_reaches_one 1


even n      collatz_reaches_one (half n)
---------------------------------------- [collatz-even]
collatz_reaches_one n


¬ (even n)  collatz_reaches_one (3*n + 1)
----------------------------------------- [collatz-odd]
collatz_reaches n

Inductive Propositions

We can encode the above rules in lean as an inductive proposition

inductive collatz_reaches_one : Nat -> Prop where
  | collatz_one : collatz_reaches_one 1
  | collatz_evn : ∀ {n : Nat}, even n -> collatz_reaches_one (half n) -> collatz_reaches_one n
  | collatz_odd : ∀ {n : Nat}, ¬ even n -> collatz_reaches_one (3*n + 1) -> collatz_reaches_one n

open collatz_reaches_one

Note that the above is a rather fancy data type, where, unlike List or Nat

  1. the constructed "type" is Prop i.e. a proposition, and
  2. the actual Prop is different for the different constructors!

So, in

  • The one case: the term collatz_one is an object of type collatz_reaches_one 1

  • The evn case: the constructor collatz_evn takes as input two arguments,

    • a proof that n is even and
    • a proof that half n reaches 1, and constructs a proof that n reaches 1
  • The odd case: the constructor collatz_odd takes as input two arguments,

    • a proof that n is not even and
    • a proof that 3*n + 1 reaches 1, and constructs a proof that n reaches 1

As an example, we can construct evidence that 5 reaches 1 in the Collatz sequence

def collatz_1 : collatz_reaches_one 1 :=
  collatz_one

def collatz_2 : collatz_reaches_one 2 :=
  let even_2 : even 2 := by simp [even]
  collatz_evn even_2 (collatz_1)

def collatz_4 : collatz_reaches_one 4 :=
  let even_4 : even 4 := by simp [even]
  collatz_evn even_4 (collatz_2)

def collatz_8 : collatz_reaches_one 8 :=
  let even_8 : even 8 := by simp [even]
  collatz_evn even_8 (collatz_4)

def collatz_16 : collatz_reaches_one 16 :=
  let even_16 : even 16 := by simp [even]
  collatz_evn even_16 (collatz_8)

def collatz_5 : collatz_reaches_one 5 :=
  let odd_5 : ¬ (even 5) := by simp [even]
  collatz_odd odd_5 (collatz_16)

You can also construct evidence with tactics ...

theorem collatz_5_tt : collatz_reaches_one 5 := by
  apply collatz_odd
  simp [even]
  apply collatz_evn
  simp [even]
  simp [half]
  apply collatz_evn
  simp [even]
  simp [half]
  apply collatz_evn
  simp [even]
  simp [half]
  apply collatz_evn
  simp [even]
  simp [half]
  apply collatz_one

Finally, you can state the Collatz conjecture as follows:

theorem collatz_theorem : ∀ n, collatz_reaches_one n := by
  sorry  -- *well* out of the scope of CSE 230 ...

Now that we have motivated the need for explicit evidence, lets look at some more examples.

Example: Even Numbers

Lets try to define the notion of a number n being even as an inductive proposition ev n.

What are the rules for a number n to be even?

Lets try to write these rules down in lean...

inductive ev : Nat -> Prop where
  | evZ : ev 0
  | evSS : ∀ {n: Nat}, ev n -> ev (n + 2)

open ev

As before, we can construct explicit evidence for a few numbers...

def even_0  : ev 0 :=                  evZ
def even_2  : ev 2 :=             evSS evZ
def even_4  : ev 4 :=       evSS (evSS evZ)
def even_6  : ev 6 := evSS (evSS (evSS evZ))

Note that evidence is literally just a value so we can use even_4 to get a proof of even_6

def even_6' : ev 6 := evSS (   even_4     )

Tactic: apply

Building up these "trees" can quickly get tiring. Tactics help! The apply constructor takes the current goal and "applies" a constructor to it, thus reducing the goal to its "hypotheses".

def even_8 : ev 8 := by
  apply evSS
  apply evSS
  apply evSS
  apply evSS
  apply evZ

Tactic: constructor

The constructor tactic can let you skip the name of the actual constructor, as long as there is a particular constructor that matches the current goal.

(Of course, in the below you can also repeat the application of constructor).

def even_8' : ev 8 := by
  constructor
  constructor
  constructor
  constructor
  constructor

Tactic: solve_by_elim

The solve_by_elim tactic is even funner: it can do a bit of "search" by recursively apply-ing the appropriate constructors (upto some fixed search depth.)

def even_8'' : ev 8 := by
  solve_by_elim

Constructing / Producing Evidence

Lets try to prove that if even n returns true, then we can construct evidence that ev n.

theorem even_ev : ∀ {n : Nat}, even n = true -> ev n := by
  intros n h

induction n using even.induct case case1 => constructor case case2 => contradiction case case3 n' ih' => simp_all [even]; constructor; assumption

Destructing / Using Evidence

Next, lets try to prove the opposite: whenever we have evidence that ev n, we can prove that even n = true!

How can we possibly prove this?

theorem ev_even : ∀ {n : Nat}, ev n -> even n = true := by

intros n h induction h case evZ => rfl case evSS => simp [even, *]

Example: Reflexive, Transitive Closure

Suppose we have a datatype to represent some set of persons

inductive person : Type where
  | alice   : person
  | bob     : person
  | charlie : person
  | diane   : person
  | esther  : person
  | fred    : person
  | gerald  : person
  | harry   : person

open person

we can define a relation parent_of that specifies some relationship between two persons.

inductive parent_of : person -> person -> Prop where
  | parent_ab : parent_of alice   bob
  | parent_bc : parent_of bob     charlie
  | parent_cd : parent_of charlie diane
  | parent_ef : parent_of esther  fred
  | parent_fg : parent_of fred    gerald
  | parent_gh : parent_of gerald  fred

open parent_of

inductive anc_of : person -> person -> Prop where
  | anc_self : ∀ {x}, anc_of x x
  | anc_step : ∀ {x y z}, parent_of x y -> anc_of y z -> anc_of x z

open anc_of

example : anc_of alice diane := by
  repeat constructor

theorem anc_of_transitive : ∀ {a b c},
  anc_of a b -> anc_of b c -> anc_of a c
  := by
  intros a b c anc_ab anc_bc
  induction anc_ab
  . case anc_self =>
    assumption
  . case anc_step =>
    rename_i a x b _ _ ih
    simp_all []
    constructor
    assumption
    assumption

Now suppose we want to define an ancestor_of relationship, as

---------------- [anc-self] anc_of x x

parent_of x y anc_of y z ----------------------------- [anc-step] anc_of x z

  • (reflexive) x is an ancestor_of x` or
  • (transitive) if parent_of x y and ancestor_of y z then ancestor_of x z

That is, the ancestor_of relation is the reflexive and transitive closure of a relation parent_of.

Suppose that r is a binary relation on α i.e. some fact that holds on two α.

The reflexive transitive closure of r is defined as the relation rr where

  • (reflexive) rr x x i.e. rr relates every element x to itself,
  • (transitive) if r x y and rr y z then rr x z

Lets define this as an inductive proposition

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

open star

We can now define ancestor_of using star, and then test it out on a few examples.

abbrev ancestor_of := star parent_of

example : ancestor_of alice alice := by
  sorry

example : ancestor_of alice diane := by
  sorry

example : ancestor_of esther fred := by
  sorry

If you think about it, if

  • if a is an ancestor_of some person b,
  • and b is an ancestor_of some person c

then

  • a should also be an ancestor_of c` ?

In fact, this transitivity fact should hold for any star r. Lets try to prove it!

HINT: The solve_by_elim tactic is quite handy.

theorem star_trans : ∀ {α : Type} {r : α -> α -> Prop} {a b c : α},
  star r a b -> star r b c -> star r a c :=
  by
  intros α r a b c r_ab r_bc
  induction r_ab
  . case refl =>
    assumption
  . case step =>
    simp_all []
    constructor
    assumption
    assumption

Example: Grammars and Parsing

Lets look at one last example of doing induction on evidence. Consider two grammars for parsing strings over the characters a and b

The first grammar is defined by the non-terminal S

(((((((((((((())))))))))))))(((((())))))

  S ::= ε | a S b | S S

The second grammar is defined by the non-terminal T

  T ::= ε | T a T b

Do these grammars accept the same set of strings?

inductive Alphabet where
  | a : Alphabet
  | b : Alphabet
  deriving Repr

open Alphabet

inductive gS : List Alphabet -> Prop where
   | gS0 : gS []
   | gS1 : ∀ {s : List Alphabet}, gS s -> gS (a :: s ++ [b])
   | gS2 : ∀ {s1 s2 : List Alphabet}, gS s1 -> gS s2 -> gS (s1 ++ s2)
open gS

inductive gT : List Alphabet -> Prop where
  | gT0 : gT []
  | gT1 : ∀ {t1 t2 : List Alphabet}, gT t1 -> gT t2 -> gT (t1 ++ ([a] ++ t2 ++ [b]))
open gT

Exercise: Try to prove that the two grammars are equivalent, that is complete the proofs of S_imp_T and T_imp_S below. (Start with T_imp_S)

theorem T_append : ∀ {s1 s2 : List Alphabet}, gT s1 -> gT s2 -> gT (s1 ++ s2) := by intros s1 s2 T_s1 T_s2 induction T_s2 case gT0 => simp [List.append_nil]; assumption case gT1 t1' t2' _ _ _ _ => have h : (s1 ++ (t1' ++ ([a] ++ t2' ++ [b]))) = (s1 ++ t1' ++ ([a] ++ t2' ++ [b])) := by simp [List.append_assoc] rw [h] constructor assumption assumption

theorem S_imp_T : ∀ {w : List Alphabet}, gS w -> gT w := by

intros _ gs induction gs case gS0 => constructor case gS1 => apply (gT1 gT0); assumption case gS2 _ s1 s2 _ _ T_s1 T_s2 => apply T_append <;> assumption

theorem T_imp_S : ∀ {w : List Alphabet}, gT w -> gS w := by

intros _ gt induction gt case gT0 => constructor case gT1 => constructor . case _ => simp_all [] -- assumption . case _ => constructor simp_all [List.append]

theorem S_equiv_T : ∀ {w : List Alphabet}, gS w ↔ gT w := by
  intros w
  constructor
  apply S_imp_T
  apply T_imp_S

Big Step Semantics

Floyd-Hoare Logic