On this page:
5.1 A little review
5.2 Making Formal Judgments Really Formal
5.3 BNF Grammars – A short-hand for simple judgments
5.4 Modeling Operatings on Expressions
5.5 Modeling Evaluation
7.5.0.17

5 Lecture 2 – Modeling a Language, Continued

5.1 A little review

Last lecture (Lecture 1 – Introduction to Rebuilding the Universe), we introduced two Axioms and a small formal system from which we can rebuild the universe, and formally defined a collection of expressions for a language. The Axioms are implication and induction, and we use judgments to define new mathematical theories from these two axioms.

Using this formal system, we defined judgments that represent simple data types, like the natural numbers and booleans. We can define lots of data types this way. In class, we saw how to define another common data type: lists. Intuitively, a list is either empty, or contains some elements.

Something like this: any is List

------- [Empty]

() is List

 

--------[1-Element]

(any_1) is List

 

 

--------[2-Element]

(any_1 any_2) is List

 

--------[3-Element]

(any_1 any_2 any_3) is List

 

...

But this doesn’t work in our formal system. Instead, we use the Axiom of induction to build up unbounded things. Using induction, we define the is List judgment below. any is List

------- [Empty]

() is List

 

any2 is List

-------- [Cons]

(cons any1 any2) is List

This is essentially similar to how we define the natural numbers.

5.2 Making Formal Judgments Really Formal

While writing funny symbols on a board, or on paper, certainly seems like formal mathematics, how do we know what we’ve written actually make sense? There are several ways we can reassure ourselves.
  • Trust Me—I’m a professor

  • Learn some well-established mathematical theory that a large number of mathematicians have agreed is a good theory for a long time, such as set theory, and prove that your new formalism is sound with respect to that theory. Since mathematicians have been unable to break the original theory, and your theory is sound, probably your theory is formal system.

Obviously, each of these options is a bad plan. You shouldn’t trust me because even professors make mistakes. And learning set theory will take too long—we’re trying to learn about compilers. Thankfully, there’s a third option:
  • Run it on a computer

If a computer can take your theory and do things with it, it probably makes some amount of sense.

Thankfully, there exists a programming language that has essentially the same underlying Axioms as this class. We can use PLT Redex to write judgments, build derivations, and check derivations. This can be very useful for experimenting with judgments—checking that the definition formally means what you think it means intuitively, checking for typos, checking that a derivation is a valid formal proof of some judgment.

To use Redex, make sure you have Racket installed, then install Redex on the command line using raco pkg install redex.

We can translate all our judgments into Redex. We’ll start with the is Bool judgment To start, we require redex/reduction-semantics. )

Example:
> (require
    redex/reduction-semantics
    (only-in redex/reduction-semantics (define-language define-universe)))

I also import the define-language with the alias define-universe. Redex requires each judgment be defined in what is calls a language, but what I call a universe in this class.

To get started, we need to define a new universe, U1.

Example:
> (define-universe U1)

Now we can define judgment in the universe U1.

We start by translating the any : Bool judgment into Redex. We do this in Redex using define-judgment-form, which expects a universe name, optionally a declaration of the shape of the judgment (which Redex calls a #:contract), and a sequence of rules defining the judgment.

Example:
> (define-judgment-form U1
    #:contract (Is-Bool any_1)
  
    [---------- "True"
     (Is-Bool true)]
  
    [---------- "False"
     (Is-Bool false)])

Redex understand the meta-variable any in the same way we introduced it in class. The Redex pattern matcher will match any Racket symbol? in a placed declared to be any. You can call the Redex pattern matcher directly to test your own understanding of meta-variables:

Examples:
> (redex-match? U1 any (term true))

#t

> (redex-match? U1 any 'false)

#t

> (redex-match? U1 (any_1 any_2) (term (true false)))

#t

> (redex-match? U1 (any_1 any_1) (term (true false)))

#f

> (redex-match? U1 (any_1 any_1) '(true true))

#t

The function redex-match? takes a universe, a pattern, and a Redex term represented as an s-expression. You can build Redex terms using Racket lists, or the Redex constructor term.

Once we have a judgment, we can build derivations. Derivations are a kind of data structure. On paper, we write the following derivation to prove that true is judged to be is Bool.

-------- [True]

true is Bool

We can translate this into Redex as:

Example:
> (derivation
    '(Is-Bool true)
    "True"
    (list))

(derivation '(Is-Bool true) "True" '())

A derivation begins with the derivation constructor, and expects three arguments: a representation of the judgment we wish to conclude as a list, a rule name from which this conclusion follows, and a list of premises required by that rule. This derivation is simple since it follows by the rule "True", which takes no premises.

We can also ask Redex whether the derivation is in fact a proof of the judgment Is-Bool.

Example:
> (test-judgment-holds Is-Bool
    (derivation
      '(Is-Bool true)
      "True"
      (list)))

We use test-judgment-holds, which expects a judgment name and a derivation. The test will silently succeed if the derivation is valid, and raise an error if it is not valid.

Redex also understand inductive judgments. We can translate the any : Nat judgment into Redex as follows.

Example:
> (define-judgment-form U1
    #:contract (Is-Nat any)
  
    [----------- "Zero"
     (Is-Nat z)]
  
    [(Is-Nat any)
     ---------- "Add1"
     (Is-Nat (s any))])

And we can prove that (s z) Is-Nat.

Example:
> (test-judgment-holds Is-Nat
    (derivation
      '(Is-Nat (s z))
      "Add1"
      (list
        (derivation
          '(Is-Nat z)
          "Zero"
          (list)))))

This derivation requires a sub-derivation to prove that the premise to the rule "Add1" holds.

Finally, we can translate our simple language of boolean and natural number expressions.

Example:
> (define-judgment-form U1
    #:contract (Is-Exp any)
  
    [---------- "E-Zero"
     (Is-Exp z)]
  
    [(Is-Exp any)
     --------------- "E-Add1"
     (Is-Exp (s any))]
  
  
    [(Is-Exp any_1)
     (Is-Exp any_2)
     ------------------------ "E-Plus"
     (Is-Exp (any_1 + any_2))]
  
    [------------- "E-True"
     (Is-Exp true)]
  
    [------------- "E-False"
     (Is-Exp false)]
  
    [(Is-Exp any_1)
     (Is-Exp any_2)
     (Is-Exp any_3)
     ------------------------------------------ "E-If"
     (Is-Exp (if any_1 then any_2 else any_3))])

Now, we can formally prove that (s z) Is-Exp, and have the computer check our work.

Example:
> (test-judgment-holds
   Is-Exp
   (derivation
    '(Is-Exp (s z))
    "E-Add1"
    (list
     (derivation
      '(Is-Exp z)
      "E-Zero"
      (list)))))

5.3 BNF Grammars – A short-hand for simple judgments

Anyone familiar with compilers or programming languages may have seen an alternative method of describing syntax before—BNF grammars. BNF grammars are a much more terse description of a syntax, and most well-formed grammars ARE judgments, in a more economical notation.

We could easily have defined all of the above judgments as the following grammars.
  b = ::=
  | true
  | | false] [n ::= z |
  | (s n)
     
  l = ::=
  | ()
  | | (cons any l)] [e ::= n |
  | b
  | | (e_1 + e_2) |
  | (if e_1 then e_2 else e_3)
Using a BNF grammar, we define a new meta-variable. In the definition, self-references to the meta-variable stand in for inductive references to the judgment.

Redex also understands grammars. We could define a universe with these grammars by passing the grammar as an optional argument to define-universe:

Example:
> (define-universe U2
    [b ::= true false]
    [n ::= z (s n)]
    [l ::= () (cons any l)]
    [e ::= n b (e_1 + e_2) (if e_1 then e_2 else e_3)])

Redex does not expect a pipe character between expressions, but otherwise the notation is essentially the same.

Once we define a grammar, the Redex pattern matcher will understand the new meta-variables defined by the grammar.

Examples:
> (redex-match? U2 b 'true)

#t

> (redex-match? U2 n 'z)

#t

> (redex-match? U2 n '(s z))

#t

> (redex-match? U2 e '(s z))

#t

> (redex-match? U2 (e_1 e_2) '((s z) true))

#t

5.4 Modeling Operatings on Expressions

Until now, we have only brought things into existance in our universe. We declare that the symbols true and z and if exist in our universe, and are "Expressions", whatever that means. But they have no meaning at all. They are merely symbols that satsify some formal judgment. To give them meaning, we need to define operations on expressions—judgments that relate the expressions to other expressions.

The most common operation we want to define expressions in a language is evaluation. We want to know what an expression computes—what value it will return, what string it will print, or what meme it will display when browsing Twitter.

In programming languages, it’s common to start defining evaluation by defining one step of reduction. The reduction judgment, commonly written with a single right arrow , defines a single step of reduction. What happens when an expression that eliminates a value has a value to eliminate. We can define this on paper as the following judgment.

e1e2

 

------------ "Step-If-True"

(if true then e_1 else e_2) → e_1

 

------------ "Step-If-False"

(if false then e_1 else e_2) → e_2

 

------------- "Step-Add-Zero"

(z + e) → e)

 

------------- "Step-Add-Zero"

((s e_1) + e_2) → (e_1 + (s e_2))

This judgment defines how to reduce simple expressions. (z + e) can step to just e, representing the fact that zero plus anything expression should just reduce to the expression. When we have (if true then e1 else e2), this should just reduce to the true branch, e1.

By defining this judgment, we begin to give meaning to expressions. Earlier, we define true to be a Bool, but it had no interpretation. We could not reasonably think of it as representing anything at all. However, by declaring that (if true then e1 else e2) steps to e1, we declare that true formally represents our intuition for "something that is true". Similarly, by relating the expressions (z + e) and e, we declare that z behaves like the natural number 0 if we consider + to be the mathematical addition function on natural numbers.

We have now rebuilt enough of the universe that we can formally prove some interesting mathematical facts. For example, we can formally zero plus one is equal to one. First, we must interpret e1e2 as meaning e1 is equal to e2}. Then we interpret z as 0, and + as addition, and (s z) as one. Now we construct the derivation.

------------ "Step-Add-Zero"

(z + (s z)) → (s z)

Note that to interpret this as a proof of things we understand intuitively, like 0 and addition, we must assume an interpretation of the symbols we wrote down. This is fine as long as our interpretation is consisistent.

We can translate this judgment and proof into Redex as follows.

Examples:
> (define-judgment-form U2
    #:contract ( e e)
  
    [----------------- "Step-If-True"
     ( (if true then e_1 else e_2) e_1)]
  
    [----------------- "Step-If-False"
     ( (if false then e_1 else e_2) e_2)]
  
    [-------------- "Step-Add-Zero"
     ( (z + e) e)]
  
    [------------------------------------ "Step-Add-Add1"
     ( ((s e_1) + e_2) (e_1 + (s e_2)))])
> (test-judgment-holds 
    (derivation
      '( (z + (s z)) (s z))
      "Step-Add-Zero"
      (list)))

We can prove other simple facts, like that the expression if when given the boolean true will step to its first branch.

Example:
> (test-judgment-holds 
   (derivation
    '( (if true then z else (s z)) z)
    "Step-If-True"
    (list)))

5.5 Modeling Evaluation

The single-step reduction judgment forms the basis for an interpreter. We can model an interpreter, formally, as a judgment that applies each of the above reductions over and over and over again, until there are none left. Programming languages people sometimes call this the reflexive, transitive, compatible closure of the reduction judgment. I don’t know why, since these terms do not exist in our universe, but it’s good to know.

Example:
> (define-judgment-form U2
    #:contract (→* e_1 e_2)
  
    [------------- "Refl"
     (→* e_1 e_1)]
  
    [( e_1 e_2)
     ----------------- "Step"
     (→* e_1 e_2)]
  
    [(→* e_1 e_2)
     (→* e_2 e_3)
     ------------------ "Trans"
     (→* e_1 e_3)]
  
    [(→* e_1 e_11)
     ------------------- "If-Compat-e1"
     (→* (if e_1 then e_2 else e_3) (if e_11 then e_2 else e_3))]
  
    [(→* e_2 e_21)
     ------------------- "If-Compat-e2"
     (→* (if e_1 then e_2 else e_3) (if e_1 then e_21 else e_3))]
  
    [(→* e_3 e_31)
     ------------------- "If-Compat-e3"
     (→* (if e_1 then e_2 else e_3) (if e_1 then e_2 else e_31))]
  
    [(→* e_1 e_11)
     --------------------- "Plus-Compat-e1"
     (→* (e_1 + e_2) (e_11 + e_2))]
  
    [(→* e_2 e_21)
     --------------------- "Plus-Compat-e2"
     (→* (e_1 + e_2) (e_1 + e_21))])

To model the interpreter, we define the judgment →*. Intuitively, this judgment applies the judgment any number of times, to any sub-expression. We start by declaring that an expression can take no steps, or, step to itself (→* e_1 e_1). Next, we have a rule saying that if an expression steps in the judgment, then it can step in the →* judgment. That is, any expression can take a single step. Then, we have a rule that says we can evaluate from e_1 to e_2, and then continue evaluating e_2 to e_3.

Before explaining the rest of the rules, we should carefully look at the rule "Trans". Here, we’re defining an inductively relation. However, in the "Trans" rule, nothing gets "smaller". This violates one of the rules of thumb for defining good judgments. We can still use this judgment, but this will have consequences later.

I screwed up this explanation in lecture. In lecture, I claimed we had to modify the rule. This isn’t necessarily true, and the modification I made ended up giving us the wrong judgment.

The rest of the rules are called "compatibility" rules. We need one rule for each sub-expression, declaring that the interpreter is allowed to evaluate the sub-expressions.

With this judgment, we can now prove that more complex equations between expressions. For example, we can prove that (if true then ((s z) + (s z)) else false) evaluates to 2. First, we interpret →* as "evaluates". Then, we interpret (s (s z)) as 2. We already assumed earlier that (s z) is 1 and + is addition. Now we build the derivation

We’ll build the derivation in two steps. First, we’ll prove that 1 plus 1 equals 2, (→* ((s z) + (s z)) (s (s z)))). Then we’ll combine this fact with the fact that the if expression evaluates to its true branch.

Examples:
> (define one-plus-one-equals-two
    (derivation
     '(→* ((s z) + (s z)) (s (s z)))
     "Trans"
     (list
      (derivation
       '(→* ((s z) + (s z)) (z + (s (s z))))
       "Step"
       (list
        (derivation
         '( ((s z) + (s z)) (z + (s (s z))))
         "Step-Add-Add1"
         (list))))
      (derivation
       '(→* (z + (s (s z))) (s (s z)))
       "Step"
       (list
        (derivation
         '( (z + (s (s z))) (s (s z)))
         "Step-Add-Zero"
         (list)))))))
> (test-judgment-holds →*
    one-plus-one-equals-two)
> (test-judgment-holds →*
    (derivation
      '(→* (if true then ((s z) + (s z)) else false) (s (s z)))
      "Trans"
      (list
        (derivation
          '(→* (if true then ((s z) + (s z)) else false) ((s z) + (s z)))
          "Step"
          (list
            (derivation
              '( (if true then ((s z) + (s z)) else false) ((s z) + (s z)))
              "Step-If-True"
              (list))))
        one-plus-one-equals-two)))

The same derivation in on-paper notation is below:

Lemma 1 (One Plus One Equals Two): ((s z) + (s z)) →* (s (s z))

Proof:

--------------------------------- [Step-Add-Add1]      ------------------------------- [Step-Add-Zero]

((s z) + (s z)) → (z + (s (s z)))                       (z + (s (s z))) → (s (s z))))

---------------------------------- [Step]              ------------------------------ [Step]

((s z) + (s z)) →* (z + (s (s z)))                     (z + (s (s z))) →* (s (s z)))

------------------------------------------------------------------------------------- [Trans]

                          ((s z) + (s z)) →* (s (s z))

Theorem 1: (if true then ((s z) + (s z)) else false) →* (s (s z))

 

 

------------------------------------------------------------ [Step-If-True]

(if true then ((s z) + (s z)) else false) → ((s z) + (s z))                          Lemma 1

------------------------------------------------------------- [Step]        ------------------------------

(if true then ((s z) + (s z)) else false) →* ((s z) + (s z))                 ((s z) + (s z)) →* (s (s z))

------------------------------------------------------------------------------------- [Trans]

              (if true then ((s z) + (s z)) else false) →* (s (s z))