Systems and
Formalisms Lab

Week 1: Basic Program Syntax

Author

Adam Chlipala, with modifications by CS-428 staff.

License

No redistribution allowed (usage by permission in CS-428).

Simple syntax

As a first example, let's look at the syntax of simple arithmetic expressions. We use the Coq feature of modules, which let us group related definitions together. A key benefit is that names can be reused across modules, which is helpful to define several variants of a suite of functionality, within a single source file.

Module ArithWithConstants.

The following definition closely mirrors a standard BNF grammar for expressions. It defines abstract syntax trees of arithmetic expressions.

Inductive arith :=
| Const (n : nat)
| Plus (e1 e2 : arith)
| Times (e1 e2 : arith).

Here are a few examples of specific expressions.

Example ex1 :=
  Const 42.
Example ex2 :=
  Plus (Const 1) (Times (Const 2) (Const 3)).

What's the value of an expression? Unlike in many programming languages, in Coq, recursive functions must be marked as recursive explicitly. That marking comes with the Fixpoint command, as opposed to Definition. Note also that Coq checks termination of each recursive definition. Intuitively, recursive calls must be on subterms of the original argument.

Fixpoint eval (e: arith) : nat :=
  match e with
  | Const n => n
  | Plus e1 e2 => eval e1 + eval e2
  | Times e1 e2 => eval e1 * eval e2
  end.

We can run this program (evaluate the corresponding term) using Compute:

= 42 : nat
= 7 : nat

How many nodes appear in the tree for an expression?

Fixpoint size (e : arith) : nat :=
  match e with
  | Const _ => 1
  | Plus e1 e2 => 1 + size e1 + size e2
  | Times e1 e2 => 1 + size e1 + size e2
  end.

= 1 : nat
= 5 : nat

What's the longest path from the root of a syntax tree to a leaf?

Fixpoint depth (e : arith) : nat :=
  match e with
  | Const _ => 1
  | Plus e1 e2 => 1 + max (depth e1) (depth e2)
  | Times e1 e2 => 1 + max (depth e1) (depth e2)
  end.

= 1 : nat
= 3 : nat

Proofs by induction

Our first proof! Size is an upper bound on depth:


forall e : arith, depth e <= size e

forall e : arith, depth e <= size e

Within a proof, we apply commands called tactics. Here's our first one. Keep in mind that the best way to understand what's going on is to run the proof script for yourself, inspecting intermediate states!

  
n: nat

depth (Const n) <= size (Const n)
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2
depth (Plus e1 e2) <= size (Plus e1 e2)
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2
depth (Times e1 e2) <= size (Times e1 e2)
induction x

Structure the proof by induction on the structure of x, where x is a variable in the theorem statement. You will get one generated subgoal per constructor in the inductive definition of x. (Indeed, it is required that x's type was introduced with Inductive.)

Bullets (-) are used to structure the proof: each bullet corresponds to one goal.

  
n: nat

depth (Const n) <= size (Const n)
n: nat

1 <= 1
simpl

Simplify throughout the goal, applying the definitions of recursive functions directly. That is, when a subterm matches one of the match cases in a defining Fixpoint, replace with the body of that case, then repeat.

    lia.
lia

A complete decision procedure for linear arithmetic. Relevant formulas are essentially those built up from variables and constant natural numbers and integers using only addition, with congruence and incongruence comparisons on top. (Multiplication by constants is supported, as a shorthand for repeated addition.)

  
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2

depth (Plus e1 e2) <= size (Plus e1 e2)
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2

S (max (depth e1) (depth e2)) <= S (size e1 + size e2)
lia.
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2

depth (Times e1 e2) <= size (Times e1 e2)
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2

S (max (depth e1) (depth e2)) <= S (size e1 + size e2)
lia. Qed.

We are applying the same operations to all goals — wouldn't it be better to say just that?


forall e : arith, depth e <= size e

forall e : arith, depth e <= size e
n: nat

depth (Const n) <= size (Const n)
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2
depth (Plus e1 e2) <= size (Plus e1 e2)
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2
depth (Times e1 e2) <= size (Times e1 e2)
n: nat

1 <= 1
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2
S (max (depth e1) (depth e2)) <= S (size e1 + size e2)
e1, e2: arith
IHe1: depth e1 <= size e1
IHe2: depth e2 <= size e2
S (max (depth e1) (depth e2)) <= S (size e1 + size e2)
all: lia. Qed.

Or even better, look at that!


forall e : arith, depth e <= size e

forall e : arith, depth e <= size e
induction e; simpl; lia.

Chaining tactics with semicolon, as in t1; t2, asks to run t1 on the goal, then run t2 on every generated subgoal. This is an essential ingredient for automation.

Qed.

A silly recursive function: swap the operand orders of all binary operators.

Fixpoint commuter (e : arith) : arith :=
  match e with
  | Const _ => e
  | Plus e1 e2 => Plus (commuter e2) (commuter e1)
  | Times e1 e2 => Times (commuter e2) (commuter e1)
  end.

ex1 = Const 42 : arith
= Const 42 : arith
ex2 = Plus (Const 1) (Times (Const 2) (Const 3)) : arith
= Plus (Times (Const 3) (Const 2)) (Const 1) : arith

commuter has all the appropriate interactions with other functions (and itself).


forall e : arith, eval (commuter e) = eval e

forall e : arith, eval (commuter e) = eval e
induction e; simpl; lia. Qed.

forall e : arith, size (commuter e) = size e

forall e : arith, size (commuter e) = size e
induction e; simpl; lia. Qed.

forall e : arith, depth (commuter e) = depth e

forall e : arith, depth (commuter e) = depth e
induction e; simpl; lia. Qed.

This last theorem is a bit different, since we prove equalities on arith terms, rather than natural numbers.


forall e : arith, commuter (commuter e) = e

forall e : arith, commuter (commuter e) = e
n: nat

Const n = Const n
e1, e2: arith
IHe1: commuter (commuter e1) = e1
IHe2: commuter (commuter e2) = e2
Plus (commuter (commuter e1)) (commuter (commuter e2)) = Plus e1 e2
e1, e2: arith
IHe1: commuter (commuter e1) = e1
IHe2: commuter (commuter e2) = e2
Times (commuter (commuter e1)) (commuter (commuter e2)) = Times e1 e2

To conclude the proof, we find ourselves needing a new tactic, congruence:

  all: congruence.
congruence

A complete decision procedure for the theory of equality and uninterpreted functions. That is, the goal must follow from only reflexivity, symmetry, transitivity, and congruence of equality, including that functions really do behave as functions.

Qed.

Working with variables

Let's shake things up a bit by adding variables to expressions. Note that all of the automated proof scripts from before will keep working with no changes! That sort of "free" proof evolution is invaluable for theorems about real-world compilers, say.

Module ArithWithVariables.

For readability, we let var refer to strings, using the Notation command, and use it in the definition of the new Var constructor:

Notation var := string.

Inductive arith : Set :=
| Const (n : nat)
| Var (x : var) (* <-- this is the new constructor! *)
| Plus (e1 e2 : arith)
| Times (e1 e2 : arith).

Example ex1 := Const 42.
Example ex2 := Plus (Const 1) (Times (Var "x") (Var "y")).

Adjusting eval to the Var case requires a new argument, vn: val, which maps variable names to values:

Notation val := (var -> nat).

Fixpoint eval (vn: val) (e: arith) : nat :=
  match e with
  | Const n => n
  | Var v => vn v
  | Plus e1 e2 => eval vn e1 + eval vn e2
  | Times e1 e2 => eval vn e1 * eval vn e2
  end.

The simplest valuation returns 0 for every input:

Example val0: val := fun _ => 0.
= 0 : nat
= 1 : nat

… but we can also pass a more complex function:

= 7 : nat

The functions size, depth, and commuter are similarly easy to adjust, this time with no additional parameters needed:

Fixpoint size (e : arith) : nat :=
  match e with
  | Const _ => 1
  | Var _ => 1
  | Plus e1 e2 => 1 + size e1 + size e2
  | Times e1 e2 => 1 + size e1 + size e2
  end.

= 1 : nat
= 5 : nat
Fixpoint depth (e : arith) : nat := match e with | Const _ => 1 | Var _ => 1 | Plus e1 e2 => 1 + max (depth e1) (depth e2) | Times e1 e2 => 1 + max (depth e1) (depth e2) end.
= 1 : nat
= 3 : nat
Fixpoint commuter (e : arith) : arith := match e with | Const _ => e | Var _ => e | Plus e1 e2 => Plus (commuter e2) (commuter e1) | Times e1 e2 => Times (commuter e2) (commuter e1) end.
= Const 42 : arith
= Plus (Times (Var "y") (Var "x")) (Const 1) : arith

Due to the automation, the proofs are unchanged:


forall e : arith, depth e <= size e

forall e : arith, depth e <= size e
induction e; simpl; lia. Qed.

forall (vn : val) (e : arith), eval vn (commuter e) = eval vn e

forall (vn : val) (e : arith), eval vn (commuter e) = eval vn e
induction e; simpl; lia. Qed.

forall e : arith, size (commuter e) = size e

forall e : arith, size (commuter e) = size e
induction e; simpl; lia. Qed.

forall e : arith, depth (commuter e) = depth e

forall e : arith, depth (commuter e) = depth e
induction e; simpl; lia. Qed.

forall e : arith, commuter (commuter e) = e

forall e : arith, commuter (commuter e) = e
induction e; simpl; congruence. Qed.

Complex transformations

Variable substitution

Now that we have variables, we can consider new operations, like substituting an expression for a variable.

To define this operation, we use an infix operator == for equality tests on strings. It has a somewhat funny and very expressive type, whose details we will try to gloss over.

Infix "==" := string_dec (at level 70).
= yes : {"x" = "x"} + {"x" <> "x"}
= no : {"x" = "y"} + {"x" <> "y"}
Fixpoint substitute (inThis : arith) (replaceThis : var) (withThis : arith) : arith := match inThis with | Const _ => inThis | Var x => if x == replaceThis then withThis else inThis | Plus e1 e2 => Plus (substitute e1 replaceThis withThis) (substitute e2 replaceThis withThis) | Times e1 e2 => Times (substitute e1 replaceThis withThis) (substitute e2 replaceThis withThis) end.

An intuitive property about how much substitute might increase depth:


forall (replaceThis : var) (withThis inThis : arith), depth (substitute inThis replaceThis withThis) <= depth inThis + depth withThis

forall (replaceThis : var) (withThis inThis : arith), depth (substitute inThis replaceThis withThis) <= depth inThis + depth withThis
replaceThis: var
withThis: arith
n: nat

1 <= S (depth withThis)
replaceThis: var
withThis: arith
x: var
depth (if x == replaceThis then withThis else Var x) <= S (depth withThis)
replaceThis: var
withThis, inThis1, inThis2: arith
IHinThis1: depth (substitute inThis1 replaceThis withThis) <= depth inThis1 + depth withThis
IHinThis2: depth (substitute inThis2 replaceThis withThis) <= depth inThis2 + depth withThis
S (max (depth (substitute inThis1 replaceThis withThis)) (depth (substitute inThis2 replaceThis withThis))) <= S (max (depth inThis1) (depth inThis2) + depth withThis)
replaceThis: var
withThis, inThis1, inThis2: arith
IHinThis1: depth (substitute inThis1 replaceThis withThis) <= depth inThis1 + depth withThis
IHinThis2: depth (substitute inThis2 replaceThis withThis) <= depth inThis2 + depth withThis
S (max (depth (substitute inThis1 replaceThis withThis)) (depth (substitute inThis2 replaceThis withThis))) <= S (max (depth inThis1) (depth inThis2) + depth withThis)
replaceThis: var
withThis: arith
n: nat

1 <= S (depth withThis)
lia.
replaceThis: var
withThis: arith
x: var

depth (if x == replaceThis then withThis else Var x) <= S (depth withThis)
replaceThis: var
withThis: arith
x: var
e: x = replaceThis

depth withThis <= S (depth withThis)
replaceThis: var
withThis: arith
x: var
n: x <> replaceThis
1 <= S (depth withThis)
destruct e

Break the proof into one case for each constructor that might have been used to build the value of expression e. In the special case where e essentially has a Boolean type, we consider whether e is true or false.

    
replaceThis: var
withThis: arith
x: var
e: x = replaceThis

depth withThis <= S (depth withThis)
lia.
replaceThis: var
withThis: arith
x: var
n: x <> replaceThis

1 <= S (depth withThis)
lia.
replaceThis: var
withThis, inThis1, inThis2: arith
IHinThis1: depth (substitute inThis1 replaceThis withThis) <= depth inThis1 + depth withThis
IHinThis2: depth (substitute inThis2 replaceThis withThis) <= depth inThis2 + depth withThis

S (max (depth (substitute inThis1 replaceThis withThis)) (depth (substitute inThis2 replaceThis withThis))) <= S (max (depth inThis1) (depth inThis2) + depth withThis)
lia.
replaceThis: var
withThis, inThis1, inThis2: arith
IHinThis1: depth (substitute inThis1 replaceThis withThis) <= depth inThis1 + depth withThis
IHinThis2: depth (substitute inThis2 replaceThis withThis) <= depth inThis2 + depth withThis

S (max (depth (substitute inThis1 replaceThis withThis)) (depth (substitute inThis2 replaceThis withThis))) <= S (max (depth inThis1) (depth inThis2) + depth withThis)
lia. Qed.

Let's get fancier about automation, using match goal to pattern-match the goal and decide what to do next! The |- syntax separates hypotheses and conclusion in a goal. The context syntax is for matching against any subterm of a term. The construct try is also useful, for attempting a tactic and rolling back the effect if any error is encountered.


forall (replaceThis : var) (withThis inThis : arith), depth (substitute inThis replaceThis withThis) <= depth inThis + depth withThis

forall (replaceThis : var) (withThis inThis : arith), depth (substitute inThis replaceThis withThis) <= depth inThis + depth withThis
induction inThis; simpl; try match goal with | [ |- context[if ?a == ?b then _ else _] ] => destruct (a == b); simpl end; lia. Qed.

A silly self-substitution has no effect.


forall (replaceThis : var) (inThis : arith), substitute inThis replaceThis (Var replaceThis) = inThis

forall (replaceThis : var) (inThis : arith), substitute inThis replaceThis (Var replaceThis) = inThis
induction inThis; simpl; try match goal with | [ |- context[if ?a == ?b then _ else _] ] => destruct (a == b); simpl end; congruence. Qed.

We can do substitution and commuting in either order.


forall (replaceThis : var) (withThis inThis : arith), commuter (substitute inThis replaceThis withThis) = substitute (commuter inThis) replaceThis (commuter withThis)

forall (replaceThis : var) (withThis inThis : arith), commuter (substitute inThis replaceThis withThis) = substitute (commuter inThis) replaceThis (commuter withThis)
induction inThis; simpl; try match goal with | [ |- context[if ?a == ?b then _ else _] ] => destruct (a == b); simpl end; congruence. Qed.

Constant folding

Constant folding is one of the classic compiler optimizations. We repeatedly find opportunities to replace fancier expressions with known constant values.

Fixpoint constantFold (e : arith) : arith :=
  match e with
  | Const _ => e
  | Var _ => e
  | Plus e1 e2 =>
      let e1' := constantFold e1 in
      let e2' := constantFold e2 in
      match e1', e2' with
      | Const n1, Const n2 => Const (n1 + n2)
      | Const 0, _ => e2'
      | _, Const 0 => e1'
      | _, _ => Plus e1' e2'
      end
  | Times e1 e2 =>
      let e1' := constantFold e1 in
      let e2' := constantFold e2 in
      match e1', e2' with
      | Const n1, Const n2 => Const (n1 * n2)
      | Const 1, _ => e2'
      | _, Const 1 => e1'
      | Const 0, _ => Const 0
      | _, Const 0 => Const 0
      | _, _ => Times e1' e2'
      end
  end.

This is supposed to be an optimization, so it had better not increase the size of an expression!

As before, automation makes short work of the proof. A new scripting construct is match patterns with dummy bodies. Such a pattern matches any match in a goal, over any type!


forall e : arith, size (constantFold e) <= size e

forall e : arith, size (constantFold e) <= size e
induction e; simpl; repeat match goal with | [ |- context[match ?E with _ => _ end] ] => destruct E; simpl in * end; lia. Qed.

The same principle applies for eval:


forall (vn : val) (e : arith), eval vn (constantFold e) = eval vn e

forall (vn : val) (e : arith), eval vn (constantFold e) = eval vn e
induction e; simpl; repeat match goal with | [ |- context[match ?E with _ => _ end] ] => destruct E; simpl in * end; lia. Qed.

And it's business as usual for commuter, with just two simple added cases:


forall e : arith, commuter (constantFold e) = constantFold (commuter e)

forall e : arith, commuter (constantFold e) = constantFold (commuter e)
induction e; simpl; repeat match goal with | [ |- context[match ?E with _ => _ end] ] => destruct E; simpl in * | [ H : ?f _ = ?f _ |- _ ] => injection H as H; subst | [ |- ?f _ = ?f _ ] => f_equal end; try (lia || congruence).
injection H as H

Replace a hypothesis H of the form f x = f y with x = y, when f is a constructor of an inductive type.

f_equal

Replace a goal of the form f x = f y with x = y, for any function f.

Qed.

Pushing constants into multiplications

To define our last transformation, we first write a roundabout way of testing whether an expression is a constant. This detour happens to be useful to avoid overhead in concert with pattern matching, since Coq internally elaborates wildcard _ patterns into separate destruct for all constructors not considered beforehand. That expansion can create serious code blow-ups, leading to serious proof blow-ups!

Definition isConst (e : arith) : option nat :=
  match e with
  | Const n => Some n
  | _ => None
  end.

Our next target is a function that finds multiplications by constants and pushes the multiplications to the leaves of syntax trees, ideally finding constants, which can be replaced by larger constants, not affecting the meanings of expressions. This helper function takes a coefficient multiplyBy that should be applied to an expression.

Fixpoint pushMultiplicationInside'
  (multiplyBy : nat) (e : arith) : arith :=
  match e with
  | Const n => Const (multiplyBy * n)
  | Var _ => Times (Const multiplyBy) e
  | Plus e1 e2 => Plus (pushMultiplicationInside' multiplyBy e1)
                    (pushMultiplicationInside' multiplyBy e2)
  | Times e1 e2 =>
      match isConst e1 with
      | Some k => pushMultiplicationInside' (k * multiplyBy) e2
      | None => Times (pushMultiplicationInside' multiplyBy e1) e2
      end
  end.

The overall transformation just fixes the initial coefficient as 1.

Definition pushMultiplicationInside (e : arith) : arith :=
  pushMultiplicationInside' 1 e.

A fun fact about pushing multiplication inside: the coefficient has no effect on depth! Let's start by showing any coefficient is equivalent to coefficient 0.


forall (e : arith) (multiplyBy : nat), depth (pushMultiplicationInside' multiplyBy e) = depth (pushMultiplicationInside' 0 e)

forall (e : arith) (multiplyBy : nat), depth (pushMultiplicationInside' multiplyBy e) = depth (pushMultiplicationInside' 0 e)
n, multiplyBy: nat

1 = 1
x: var
multiplyBy: nat
2 = 2
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat
S (max (depth (pushMultiplicationInside' multiplyBy e1)) (depth (pushMultiplicationInside' multiplyBy e2))) = S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' 0 e2)))
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat
depth match isConst e1 with | Some k => pushMultiplicationInside' (k * multiplyBy) e2 | None => Times (pushMultiplicationInside' multiplyBy e1) e2 end = depth match isConst e1 with | Some k => pushMultiplicationInside' (k * 0) e2 | None => Times (pushMultiplicationInside' 0 e1) e2 end
n, multiplyBy: nat

1 = 1
lia.
x: var
multiplyBy: nat

2 = 2
lia.
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat

S (max (depth (pushMultiplicationInside' multiplyBy e1)) (depth (pushMultiplicationInside' multiplyBy e2))) = S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' 0 e2)))
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat

S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' multiplyBy e2))) = S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' 0 e2)))
rewrite H

Where H is a hypothesis or previously proved theorem, establishing forall x1 .. xN, e1 = e2, find a subterm of the goal that equals e1, given the right choices of xi values, and replace that subterm with e2.

    
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat

S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' 0 e2))) = S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' 0 e2)))
lia.
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat

depth match isConst e1 with | Some k => pushMultiplicationInside' (k * multiplyBy) e2 | None => Times (pushMultiplicationInside' multiplyBy e1) e2 end = depth match isConst e1 with | Some k => pushMultiplicationInside' (k * 0) e2 | None => Times (pushMultiplicationInside' 0 e1) e2 end
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy, n: nat

depth (pushMultiplicationInside' (n * multiplyBy) e2) = depth (pushMultiplicationInside' (n * 0) e2)
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat
S (max (depth (pushMultiplicationInside' multiplyBy e1)) (depth e2)) = S (max (depth (pushMultiplicationInside' 0 e1)) (depth e2))
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy, n: nat

depth (pushMultiplicationInside' (n * multiplyBy) e2) = depth (pushMultiplicationInside' (n * 0) e2)
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy, n: nat

depth (pushMultiplicationInside' 0 e2) = depth (pushMultiplicationInside' (n * 0) e2)

Neither lia nor congruence is powerful enough on its own here, so we use a separate lemma to simplify n * 0:

      
mul_0_r: forall n : nat, n * 0 = 0
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy, n: nat

depth (pushMultiplicationInside' 0 e2) = depth (pushMultiplicationInside' (n * 0) e2)
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy, n: nat

depth (pushMultiplicationInside' 0 e2) = depth (pushMultiplicationInside' 0 e2)
lia.
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat

S (max (depth (pushMultiplicationInside' multiplyBy e1)) (depth e2)) = S (max (depth (pushMultiplicationInside' 0 e1)) (depth e2))
e1, e2: arith
IHe1: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e1) = depth (pushMultiplicationInside' 0 e1)
IHe2: forall multiplyBy : nat, depth (pushMultiplicationInside' multiplyBy e2) = depth (pushMultiplicationInside' 0 e2)
multiplyBy: nat

S (max (depth (pushMultiplicationInside' 0 e1)) (depth e2)) = S (max (depth (pushMultiplicationInside' 0 e1)) (depth e2))
lia. Qed.

It can be remarkably hard to get Coq's automation to be dumb enough to help us demonstrate all of the primitive tactics. ;-) In particular, we can redo the proof in an automated way, without the explicit rewrites.


forall (e : arith) (multiplyBy : nat), depth (pushMultiplicationInside' multiplyBy e) = depth (pushMultiplicationInside' 0 e)

forall (e : arith) (multiplyBy : nat), depth (pushMultiplicationInside' multiplyBy e) = depth (pushMultiplicationInside' 0 e)
induction e; simpl; try match goal with | [ |- context[match ?E with _ => _ end] ] => destruct E; simpl end; congruence. Qed.

Now the general corollary about irrelevance of coefficients for depth.


forall (e : arith) (multiplyBy1 multiplyBy2 : nat), depth (pushMultiplicationInside' multiplyBy1 e) = depth (pushMultiplicationInside' multiplyBy2 e)

forall (e : arith) (multiplyBy1 multiplyBy2 : nat), depth (pushMultiplicationInside' multiplyBy1 e) = depth (pushMultiplicationInside' multiplyBy2 e)
e: arith
multiplyBy1, multiplyBy2: nat

depth (pushMultiplicationInside' multiplyBy1 e) = depth (pushMultiplicationInside' multiplyBy2 e)
e: arith
multiplyBy1, multiplyBy2: nat

depth (pushMultiplicationInside' multiplyBy1 e) = depth (pushMultiplicationInside' 0 e)
e: arith
multiplyBy1, multiplyBy2: nat
depth (pushMultiplicationInside' 0 e) = depth (pushMultiplicationInside' multiplyBy2 e)
e: arith
multiplyBy1, multiplyBy2: nat

depth (pushMultiplicationInside' multiplyBy1 e) = depth (pushMultiplicationInside' 0 e)
apply depth_pushMultiplicationInside'_irrelevance0.
apply H

For H a hypothesis or previously proved theorem, establishing some fact that matches the structure of the current conclusion, switch to proving H's own hypotheses. This is backwards reasoning via a known fact.

  
e: arith
multiplyBy1, multiplyBy2: nat

depth (pushMultiplicationInside' 0 e) = depth (pushMultiplicationInside' multiplyBy2 e)
e: arith
multiplyBy1, multiplyBy2: nat

depth (pushMultiplicationInside' multiplyBy2 e) = depth (pushMultiplicationInside' 0 e)
symmetry

When proving X ℛ Y, switch to proving Y ℛ X (here is =).

    apply depth_pushMultiplicationInside'_irrelevance0.
Qed.

Let's prove that pushing-inside has only a small effect on depth, considering for now only coefficient 0.


forall e : arith, depth (pushMultiplicationInside' 0 e) <= S (depth e)

forall e : arith, depth (pushMultiplicationInside' 0 e) <= S (depth e)
n: nat

1 <= 2
x: var
2 <= 2
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)
S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' 0 e2))) <= S (S (max (depth e1) (depth e2)))
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)
depth match isConst e1 with | Some k => pushMultiplicationInside' (k * 0) e2 | None => Times (pushMultiplicationInside' 0 e1) e2 end <= S (S (max (depth e1) (depth e2)))
n: nat

1 <= 2
lia.
x: var

2 <= 2
lia.
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)

S (max (depth (pushMultiplicationInside' 0 e1)) (depth (pushMultiplicationInside' 0 e2))) <= S (S (max (depth e1) (depth e2)))
lia.
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)

depth match isConst e1 with | Some k => pushMultiplicationInside' (k * 0) e2 | None => Times (pushMultiplicationInside' 0 e1) e2 end <= S (S (max (depth e1) (depth e2)))
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)
n: nat

depth (pushMultiplicationInside' (n * 0) e2) <= S (S (max (depth e1) (depth e2)))
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)
S (max (depth (pushMultiplicationInside' 0 e1)) (depth e2)) <= S (S (max (depth e1) (depth e2)))
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)
n: nat

depth (pushMultiplicationInside' (n * 0) e2) <= S (S (max (depth e1) (depth e2)))
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)
n: nat

depth (pushMultiplicationInside' 0 e2) <= S (S (max (depth e1) (depth e2)))
lia.
e1, e2: arith
IHe1: depth (pushMultiplicationInside' 0 e1) <= S (depth e1)
IHe2: depth (pushMultiplicationInside' 0 e2) <= S (depth e2)

S (max (depth (pushMultiplicationInside' 0 e1)) (depth e2)) <= S (S (max (depth e1) (depth e2)))
lia. Qed. #[local] Hint Rewrite mul_0_r : arith.

Registering rewrite hints lets us use autorewrite to apply them for us automatically!


forall e : arith, depth (pushMultiplicationInside' 0 e) <= S (depth e)

forall e : arith, depth (pushMultiplicationInside' 0 e) <= S (depth e)
induction e; intros; simpl in *; try match goal with | [ |- context[match ?E with _ => _ end] ] => destruct E; simpl; autorewrite with arith end; lia. Qed.

forall e : arith, depth (pushMultiplicationInside e) <= S (depth e)

forall e : arith, depth (pushMultiplicationInside e) <= S (depth e)
e: arith

depth (pushMultiplicationInside e) <= S (depth e)
e: arith

depth (pushMultiplicationInside' 1 e) <= S (depth e)
unfold X

Replace X by its definition.

  
e: arith

depth (pushMultiplicationInside' 0 e) <= S (depth e)
apply depth_pushMultiplicationInside'. Qed. End ArithWithVariables.