On this page:
2.1 Syntax
2.2 Dynamic semantics
2.2.1 Errors
2.3 Static semantics
2.3.1 Type safety
2.3.1.1 Preservation
2.3.1.2 Progress
2.4 Termination

2 The let-zl language🔗

2.1 Syntax🔗

The let-zl language has expressions defined as follows:

There are two kinds of literal expressions, integers and the empty list . Additionally, we build longer lists with , which is our traditional cons that creates a linked list node with first and rest. We have two elimination forms for integers, and . Additionally, we have elimination forms for lists, and . Finally, we have variables , and we have a form of sharing in , which binds to the value of in .

2.2 Dynamic semantics🔗

We might have a decent guess as to what this language means, but to be precise, we will define its dynamic semantics using a rewriting system, which registers computation by rewriting expressions to expressions and eventually (hopefully) to values:

We define values—final results—to include numbers , the empty list , and pairs of values .

The reduction relation describes a single computation step, and has a case for each kind of basic computation step that our language performs. For example, here is how we perform addition:

The [plus] rule says that to reduce an addition expression where both parameters are already reduced to numbers, we add the numbers in the metalanguage. The portion of each term is the evaluation context, which means that addition can be performed not just on whole terms, but within terms according to a grammar given below.

The multiplication is similar, also allowing multiplication within any evaluation context:

We have two rules for getting the first and rest of a list:

These say that if we have a cons (pair) of values then extracts the first value and extracts the second value .

Finally (for now), the rule for involves substituting the value for the variable in the body:

In order to describe where evaluation can happen when when it is finished, we extend our syntax with values and evaluation contexts :

Evaluation context give a grammar for where evaluation can take place. For example, suppose we want to reduce the term . We need to decompose that term into an evaluation context and a redex, so that they match one of the reduction rules above. We can do that: the evaluation context is , which matches the grammar of , and the redex in the hole is thus . This decomposition matches rule [plus], which converts it to . Then to perform another reduction, we decompose again, into evaluation context and redex . That converts to plugged back into the evaluation context, for . Then to perform one more reduction step, we decompose into the evaluation context and the redex , which converts to .

We define to be the reflexive, transitive closure of . That is, means that reduces to in zero or more steps.

The dynamic semantics of let-zl is now given by the evaluation function eval, defined as:

eval() =

 

if

As we discuss below, eval is partial for let-zl because there are errors that cause reduction to get “stuck.”

Exercise 8. Extend the language with Booleans. Besides Boolean literals, what do you think are essential operations? Extend the dynamic semantics with the necessary reduction rule(s) and evaluation context(s).

Later we’re going to do induction on the size of terms rather than the structure of terms, and we’re going to use a particular size function, defined as:

Exercise 9. Prove that for all values, = 0.

2.2.1 Errors🔗

Can let-zl programs experience errors? What does it mean for a reduction semantics to have an error? Right now, there are no explicit, checked errors, but there are programs that don’t make sense. For example, . What do these non-sense terms do right now? They get stuck! That is, a term that has in the hole won’t reduce any further.

Indeed, there several classes of terms that get stuck in our definition of let-zl thus far:
  • and .

  • and , where is an integer.

  • or where or is not an integer.

  • Any open term, that is, a term with a variable that is not bound by .

What do these stuck states mean? They might correspond to a real language executing an invalid instruction or some other kind of undefined behavior. This is no good, but there are several ways we could solve the problem.

First, we could make such programs defined by adding transition rules. For example, we could add a rule that the car of a number is 0. Another way to make the programs defined, without sanctioning nonsense, is to add an error state. We do this by extending terms to configurations :

Then we add transition rules that detect all bad states and transition them to , thus flagging them as errors.

This approach is equivalent to adding errors or exceptions to our programming language. Note that the right-hand side of these two rules does not have a context on it. This is what makes these like (uncatchable) exceptions.

We now update our evaluation function eval to take these errors into account:

eval() =

 

if

eval() =

 

if

Alas, eval is still partial, because there are stuck states that we haven’t converted to wrong states. (The other reason that eval could be partial is non-termination, but as we will prove, we don’t have that.) A second way to rule out stuck states is to impose a type system, which rules out programs with some kinds of errors. We can then prove that no programs admitted by the type system get stuck, which will make eval total for this language.

2.3 Static semantics🔗

With a type system, we assign types to (some) terms to classify them by what kind of value they compute. In our first, simple type system, we will have only two types:

To keep things simple, we will limit to be lists of integers.

We then define a relation that assigns types to terms. For example, integer literals always have type :

Similarly, the literal empty list has type :

To type check an addition or multiplication, we check that the operands are both integers, and then the whole thing is an integer:

To type check a , we require that the first operand be an integer and the second be a list, and then the whole thing is a list:

To type check and , we require that the operand be a list; the result for is an integer, and the result for is another list:

But when we come to check a variable , we get stuck. What’s the type of a variable? To type check variables, we introduce type environments, which keep track of the type of each -bound variable:

We then retrofit all our rules to carry the environment through. For example, the rule for becomes

and similarly for the other rules we’ve seen so far.

Then we can write the rules for variables and for . To type check a variable, look it up in the environment:

If it isn’t found, then the term is open and does not type.

Finally, to type check , we first type check , yielding some type . We then type check with an environment extended with bound to . The resulting type, , is the type of the whole expression:

Exercise 10. Extend the type system to your language with Booleans.

Exercise 11 (Generic lists). Modify the type system as follows: instead of a single type for lists of s, allow , , and so on. How do you have to change the syntax of ? The typing rules?

2.3.1 Type safety🔗

The goal of our type system is to prevent undetected errors—that is, stuck terms—in our programs. To show that it does this, we will prove type safety: if a term has a type , then one of:
  • It will reduce in some number of steps to a value that also has type .

  • It will reduce in some number of steps to .

  • It will reduce forever.

The last case cannot happen with this language, but it will be possible with languages we study in the future.

It is conventional to prove this theorem in terms of two lemmas, progress and preservation:
  • Preservation: if has type and converts in one step to , then also has type .

  • Progress: if has a type , then either takes a conversion step or is a value.

2.3.1.1 Preservation🔗

Before we start, we make an observation about how typing derivations must be formed.

Lemma (Inversion). If then,
  • If the term is a variable then = .

  • If the term is an integer then = .

  • If the term is then = .

  • If the term is or then = and and .

  • If the term is , then = and and

  • If the term is then there is some type such that and .

Proof. By inspection of the typing rules.

We want to prove that if a term has a type and takes a step, the resulting term also has a type. We can do this be considering the cases of the reduction relation and showing that each preserves the type. Alas, each rule involves evaluation contexts in the way of the action. Consequently, we’ll have to prove a lemma about evaluation contexts.

Lemma (Replacement). If , then there exists some type such that . Furthermore, for any other term such that , it is the case that .

Proof. By induction on the structure of :

  • If is , then = , so must be . Then since , we have that .

  • If , then the only typing rule that applies is [cons], which means that must be . Furthermore, by inversion of that rule it must be the case that and . By the induction hypothesis on the former, has some type , and furthermore, for any term that also has type , we have that . Then by applying rule [cons], we have that .

  • If , then as in the previous case, the only typing rule that applies is [cons], which means that must be . It also means that must have type and must have type . Then by IH on the former, has a type , and furthermore, for any having type , . Then by reapplying rule [cons], we have that .

  • If , then the only typing rule that applies is [plus], which means that is . It also requires that and both have type . Then apply IH to the former, yielding that has some type . Furthermore, by the IH, for any other having type , we have that . Then reapplying rule [plus], we have that .

  • If or or , as in the previous case, m.m.

  • If (or ) then the only typing rule that applies is [car] (resp. [cdr]), which means that is (resp. ). Furthermore, rule [car] (resp. [cdr]) requires that must have type . Then apply IH to get that as well. Then as well. Then apply rule [car] (resp. [cdr]) to get that has type (resp. ).

  • If , then the only rule that applies is [let]. By that rule, must have some type , and . Then by the IH on the former, for some . Furthermore, for any other having type , the IH tells us that as well. Then we can reapply rule [let] to get .

QED.

There’s one more standard lemma we need before we can prove preservation:

Lemma (Substitution). If and then .

Proof. By induction on the typing derivation for ; by cases on the conclusion:

  • : Then is , and .

  • : Then is , and .

  • : Then we know that and . Then by the induction hypothesis, and . Then by rule [cons], we have that . But is , so .

  • : Then we know that and . Then by the induction hypothesis, and . Then apply rule [plus].

  • : as in the previous case.

  • : Then we know that . Then by IH, . And then by rule [car], .

  • : As in the previous case.

  • : There are two possibilities, whether = or not:
    • First, consider the case where . Then we know that for some , and that . Then by the induction hypothesis, . Because , = . (This reordering could be proved correct in an “exchange” lemma, but we take it to be obviously correct from the typing rules. Exchange will be of more interest when linear type systems force us to get serious about contexts.) So we have that . Then by the induction hypothesis, . Then by rule [let].

    • If = then, as before, the induction hypothesis gives us that . By the assumption we know that . By inversion, we know that . But from the way environments work, we know that is the same as . Thus we know , which gives us the pieces to use the let rule to conclude that , which is almost what we need to finish this case. Consider what the substituion function does when the variables are equal: = = . That means, that the typing derivation we just proved, namely is the same as the one that finishes this case, and thus .

  • : There are two possibilities, whether = or not:
    • If = , then is . Furthermore, this means that = . And we have from the premise that .

    • If , then is . Furthermore, we know that = = . Then .

QED.

Now we are ready to prove preservation:

Lemma (Preservation). If and then .

Proof. By cases on the reduction relation:

  • : By the replacement lemma, must have some type, and by inversion, that type must be . The result of the addition metafunction is also an integer with type . Then by replacement, .

  • : as in the previous case.

  • : By the replacement lemma, for some type . The only rule that applies is [car], which requires that = and . This types only by rule [cons], which requires that . Then by replacement, .

  • : By the replacement lemma, for some type . The only rule that applies is [cdr], which requires that = and . This types only by rule [cons], which requires that . Then by replacement, .

  • : By the replacement lemma, for some types . The only rule that applies is [let], which requires that for some such that . Then by the substitution lemma, . Then by replacement, .

QED.

2.3.1.2 Progress🔗

Before we can prove progress, we need to classify values by their types.

Lemma (Canonical forms).

If has type , then:
  • If is then is an integer literal .

  • If is , then either = or = where has type and has type .

Proof. By induction on the typing derivation of :

  • : Then is an integer literal.

  • : Then is the empty list.

  • : By the syntax of values it must be the case that is a value having type , and is a value having type .

  • : Vacuous, because not a value.

  • The remaining cases are all vacuous because they do not allow for value forms.

QED.

Lemma (Context replacement). If then . If then .

Proof. If then must be some redex in a hole: . Furthermore, it must take a step to some = . Then the same redex converts to the same contractum in any evaluation context, including .

If then must be some redex in a hole: which converts to . Then that same redex converts to in any evaluation context, including .

Lemma (Progress). If then term either converts or is a value.

Proof. By induction on the typing derivation; by cases on the conclusion:

  • : Then is a value.

  • : Then is a value.

  • : Then and . By the induction hypothesis, term either converts, or is a value. If converts to some term , then by the context replacement lemma. If converts to , then by the context replacement lemma. If is a value , then consider , which by the induction hypothesis either converts or is a value. If converts to a term , then by the context replacement lemma. If converts to , then by the context replacement lemma. Finally, if is a value then is a value .

  • : Then and . By the induction hypothesis, either converts or is a value. If converts to a term , then by the context replacement lemma. If converts to then by the context replacement lemma. If is a value , then consider , which by the induction hypothesis either converts or is a value. If converts to a term , then by the context replacement lemma. If converts to , then by the context replacement lemma. Otherwise, is a value . By the canonical forms lemma, is an integer and is an integer . Thus, we can take the step .

  • : As in the previous case.

  • : Then . By the induction hypothesis, either converts or is a value. If it converts to a term , then by the context replacement lemma. If it converts to , then by the context replacement lemma. Otherwise, is a value. By the canonical forms lemma, it has the form , so we can take a step .

  • : As in the previous case, but reducing to .

  • : Vacuous.

  • : Then and for some . Then by the induction hypothesis, either converts or is a value. If converts to a term , then by the context replacement lemma. If converts to then by the context replacement lemma. Otherwise, is a value , and .

QED.

Exercise 12. Prove progress and preservation for your language extended with Booleans.

Exercise 13. Prove progress and preservation for your language extended with generic lists.

Exercise 14. Are the previous two exercises orthogonal? How do they interact or avoid interaction?

2.4 Termination🔗

Now let’s prove a rather strong property about a rather weak language.

Theorem (Size is work). Suppose and = k. Then either reduces to a value or goes wrong in k or fewer steps.

Proof. This proof uses induction, but it uses induction on the set ℕ × ℕ, using a lexicographic ordering. That is, we consider the first natural number to be the number of nodes in the given (when viewed as a tree) and the second one to be . The lexicographic order is well-founded, and so we can use induction when we have a term where the is strictly less than the given one, or when is the same as the given one, but the number of nodes is strictly smaller.

  • : Then k = 0, and reduces to value in 0 steps.

  • : Also k = 0.

  • . Then by inversion of [cons], and . Let j be the size of ; then the size of is k – j. We can use induction on both and , because they both have strictly fewer nodes that , and and are both less than or equal to . (With the exception of the case, the justification for induction will be the same as this one in all the other cases.) Then by induction, reduces to a value or to in j or fewer steps. If it reduces to then by the context replacement lemma, also reduces to in j or fewer steps. Otherwise, consider the induction hypothesis on (size k – j); it must reduce to a value or to in k – j or fewer steps. If , then the whole thing goes wrong by context replacement. Otherwise, goes to in k or fewer steps.

  • . Then by inversion of the typing rule , both subterms have type . Let j be the size of ; then the size of is k – j – 1. Then by the induction hypothesis, each reduces to a value or goes wrong, in at most j and k – j – 1 steps respectively. If either goes wrong, then the whole term goes wrong because both and are evaluation contexts. Otherwise, by the canonical values lemma both values must be numbers and . Because in j or fewer steps, by context replacement in j or fewer steps. And because in k – j – 1 or fewer steps, by context replacement again in k – j – 1 or fewer steps. Then in one more step , which is a value. The total number of steps has been k or fewer.

  • : As in the previous case, m.m.

  • and : In either case, the subterm must have type by inversion of the typing rule. Furthermore, the size of must be k – 1. Then by the induction hypothesis, either reduces to a value or goes wrong in k – 1 or fewer steps. If it goes wrong then the whole term goes wrong. If it reduces to a value, then by preservation, that value also has type . (Note also that it also reduces to a value in the evaluation context .) Then by the canonical values lemma, that value must be either or for some values and . If the former then the whole term goes to in one more step by rule [car-nil] or rule [cdr-nil], respectively. If the latter, then it take one more step to or , respectively. In either case, k steps have transpired.

  • : Vacuous because open terms don’t type.

  • : By inversion, we know that for some type . And we know that . Let j be the size of ; then the size of is k – j – 1. We can use induction on because is less than and there are strictly few nodes. Thus, by induction on , we have that reduces to a value or goes wrong in j or fewer steps. If it goes wrong then the whole term goes wrong. If it reduces to a value , then by context replacement (and induction on the length of the reduction sequence), the whole term reduces in j or fewer steps. Then in one more step, . Note that because the size of a variable is 0 and so is the size of a value, the size of is the same as the size of , k – j – 1, which is strictly less than the size of . In this case, the number of nodes in might be many more than the number of nodes in because might be a long list. But we set up our induction using the lexicgraphic order so that we only need to consider the relative sizes of and , not the number of nodes them to justify induction. Now, by preservation, . So we an apply the induction hypothesis to , learning that it goes wrong or reaches a value in k – j – 1 or fewer steps. This yields a total of k or fewer steps.

QED.