Chapter 6

Compilation

The model of evaluation introduced in Section 2.3 and formalized in Section 3.6 builds only on the expressions of the Mini-ML language itself. This leads very naturally to an interpreter in Elf which is given in Section 4.3. Our specification of the operational semantics is in the style of natural semantics which very often lends itself to direct, though inefficient, execution. The inefficiency of the interpreter in 4.3 is more than just a practical issue, since it is clearly the wrong model if we would like to reason about the complexity of functions defined in Mini-ML. One can refine the evaluation model in two ways: one is to consider more efficient interpreters (see Exercises 2.11 and 4.2), another is to consider compilation. In this chapter we pursue the latter possibility and describe and prove the correctness of a compiler for Mini-ML.

In order to define a compiler we need a target language for compilation, that is, the language into which programs in the source language are translated. This target language has its own operational semantics, and we must show the correctness of compilation with respect to these two languages and their semantics. The ultimate target language for compilation is determined by the architecture and instruction set of the machine the programs are to be run on. In order to insulate compilers from the details of particular machine architectures it is advisable to design an intermediate language and execution model which is influenced by a set of target architectures and by constructs of the source language. We refer to this intermediate level as an abstract machine. Abstract machine code can then itself either be interpreted or compiled further to actual machine code. In this chapter we take a stepwise approach to compilation, using two intermediate forms between Mini-ML and a variant of the SECD machine [Lan64] which is also related to the Categorical Abstract Machine (CAM) [CCM87]. This decomposition simplifies the correctness proofs and localizes ideas which are necessary to understand the compiler in its totality.

The material presented in this chapter follows work by Hannan [HM90, Han91], both in general approach and in many details. An extended abstract that also addresses correctness issues and methods of formalization can be found in [HP92]. A different approach to compilation using continuations may be found in Section 9.2.

6.1 An Environment Model for Evaluation

The evaluation judgment e ↪ v requires that all information about the state of the computation is contained in the Mini-ML expression e. The application of a function formed by λ-abstraction, lam x. e, to an argument v thus requires the substitution of v for x in e and evaluation of the result. In order to avoid this substitution it may seem reasonable to formulate evaluation as a hypothetical judgment (e is evaluated under the hypothesis that x evaluates to v) but this attempt fails (see Exercise 6.1). Instead, we allow free variables in expressions which are given values in an environment, which is explicitly represented as part of a revised evaluation judgment. Variables are evaluated by looking up their value in the environment; previously we always eliminated them by substitution, so no separate rule was required. However, this leads to a problem with the scope of variables. Consider the expression lam y. x in an environment that binds x to z. According to our natural semantics the value of this expression should be lam y. z, but this requires the substitution of z for x. Simply returning lam y. x is incorrect if this value may later be interpreted in an environment in which x is not bound, or bound to a different value. The practical solution is to return a closure consisting of an environment K and an expression lam y. e. K must contain at least all the variables free in lam y. e. We ignore certain questions of efficiency in our presentation and simply pair up the complete current environment with the expression to form the closure.

This approach leads to the question how to represent environments and closures. A simple solution is to represent an environment as a list of values and a variable as a pointer into this list. It was de Bruijn’s idea [dB72] to implement such pointers as natural numbers where n refers to the n^{th} element of the environment list. This works smoothly if we also represent bound variables in this fashion: an occurence of a bound variable points backwards to the place where it is bound. This pointer takes the form of a positive integer, where 1 refers to the innermost binder and 1 is added for every binding encountered when going upward through the expression. For example

lam x. lam y. x (lam z. y z)

would be written as Λ (Λ (2 (Λ (2 1))))

where Λ binds an (unnamed) variable. In this form expressions that differ only in the names of their bound variables are syntactically identical. If we restrict attention to pure λ-terms for the moment, this leads to the definition

de Bruijn Expressions D ::= n | ΛD | D_{1} D_{2}
de Bruijn Indices n ::= 1 | 2 | …

Instead of using integers and general arithmetic operations on them, we use only the integer 1 to refer to the innermost element of the environment and the operator ↑ (read: shift, written in post-fix notation) to increment variable references. That is, the integer n + 1 is represented as

1 \underbrace{↑ ⋯ ↑}_{n times} .

But ↑ can also be applied to other expressions, in effect raising each integer in the expression by 1. For example, the expression

lam x. lam y. x x

can be represented by

Λ (Λ ((1↑) (1↑)))

or

Λ (Λ ((1 1)↑)).

This is a very simple form of a λ-calculus with explicit substitutions where ↑ is the only available substitution (see [ACCL91]).

Modified de Bruijn Expressions F ::= 1 | F ↑ | ΛF | F_{1} F_{2}

We use the convention that the postfix operator ↑ binds stronger than application which in turn binds stronger that the prefix operator Λ. Thus the two examples above can be written as Λ Λ 1↑ 1↑ and Λ Λ (1 1)↑, respectively.

The next step is to introduce environments. These depend on values and vice versa, since a closure is a pair of an environment and an expression, and an environment is a list of values. This can be carried to the extreme: in the Categorical Abstract Machine (CAM), for example, environments are built as iterated pairs and are thus values. Our representation will not make this identification. Since we have simplified our language to a pure λ-calculus, the only kind of value which can arise is a closure.

Environments K ::= · | K; W
Values W ::= {K, F }

We write w for parameters ranging over values. During the course of evaluation, only closures over Λ-expressions will arise, that is, all closures have the form {K, ΛF′} (see Exercise 6.2).

The specification of modified de Bruijn expressions, values, and environments is straightforward. The abstract syntax is now first-order, since the language does not contain any name binding constructs.

  exp'   : type.  %name exp' F
  1      : exp'.
  ^: exp' -> exp'.  %postfix 20 ^
  lam'   : exp' -> exp'.
  app'   : exp' -> exp' -> exp'.
  env    : type.  %name env K
  val    : type.  %name val W
  empty  : env.
  ;      : env -> val -> env.   %infix left 10 ;
  clo    : env -> exp' -> val.

There are two main judgments that achieve compilation: one relates a de Bruijn expression F in an environment K to an ordinary expression e, another relates a value W to an expression v. We also need an evaluation judgment relating de Bruijn expressions and values in a given environment.

K ⊢ F ↔ e F translates to e in environment K
W ⇔ v  W translates to v
K ⊢ F ↪ W F evaluates to W in environment K

When we evaluate a given expression e using these judgments, we translate it to a de Bruijn expression F in the empty environment, evaluate F in the empty environment to obtain a value W, and then translate W to an expression v in the original language. This is depicted in the following diagram.

[figure: e; F; W; v; C :: · ⊢ F ↔ e; D′ :: · ⊢ F ↪ W; U :: W ⇔ v; D :: e ↪ v]

The correctness of this phase of compilation can then be decomposed into two statements. For completeness, we assume that D and therefore e and v are given, and we would like to show that there exist C, D′, and U completing the diagram. This means that for every evaluation of e to a value v, this value could also have been produced by evaluating the compiled expression and translating the resulting value back to the original language. The dual of this is soundness: we assume that C, D′ and U are given and we have to show that an evaluation D exists. That is, every value which can be produced by compilation and evaluation of compiled expressions can also be produced by direct evaluation.

We will continue to restrict ourselves to expressions built up only from abstraction and application. When we generalize this later only the case of fixpoint expressions will introduce an essential complication. First we define evaluation of de Bruijn expressions in an environment K, written as K ⊢ F ↪ W. The variable 1 refers to the first value in the environment (counting from right to left); its evaluation just returns that value.

\frac{fev 1}{K; W ⊢ 1 ↪ W}

The meaning of an expression F ↑ in an environment K; W is the same as the meaning of F in the environment K. Intuitively, the environment references from F into K are shifted by one. The typical case is one where a reference to the n^{th} value in K is represented by the expression 1↑ ⋯ ↑, where the shift operator is applied n − 1 times.

K ⊢ F ↪ W fev ↑

K; W′ ⊢ F ↑ ↪ W

A functional abstraction usually immediately evaluates to itself. Here this is insufficient, since an expression ΛF may contain references to the environment K. Thus we need to combine the environment K with ΛF to produce a closed (and self-contained) value.

\frac{fev lam}{K ⊢ ΛF ↪ \{K, ΛF \}}

In order to evaluate F_{1} F_{2} in an environment K we evaluate both F_{1} and F_{2} in that environment, yielding the closure {K′, ΛF′_{1}} and value W_{2}, respectively. We then add W_{2} to the environment K′, in effect binding the variable previously bound by Λ in ΛF′_{1} to W_{2} and then evaluate F′_{1} in the extended environment to obtain the overall value W.

K ⊢ F_{1} ↪ {K′, ΛF′_{1}}  K ⊢ F_{2} ↪ W_{2}  K′; W_{2} ⊢ F′_{1} ↪ W

fev app

K ⊢ F_{1} F_{2} ↪ W

Here is the implementation of this judgment as the type family feval in Elf.

  feval : env -> exp' -> val -> type.  %name feval D
  % Variables
  fev_1 : feval (K ; W) 1 W.
  fev_^: feval (K ; W') (F ^) W
             <- feval K F W.

  % Functions
  fev_lam : feval K (lam' F) (clo K (lam' F)).
  fev_app : feval K (app' F1 F2) W
             <- feval K F1 (clo K' (lam' F1'))
             <- feval K F2 W2
             <- feval (K' ; W2) F1' W.

We have written this signature in a way that emphasizes its operational reading, because it serves as an implementation of an interpreter. As an example, consider the evaluation of the expression (Λ (Λ (1↑))) (Λ 1), which is a representation of (lam x. lam y. x) (lam v. v).

  ?- D : feval empty (app' (lam' (lam' (1 ^))) (lam' 1)) W.
  W = clo (empty ; clo empty (lam' 1)) (lam' (1 ^)),
  D = fev_app fev_lam fev_lam fev_lam.

The resulting closure, {(·; {·, Λ1}), Λ(1↑)}, represents the de Bruijn expressions Λ(Λ1), since (1↑) refers to the first value in the environment.

The translation between ordinary and de Bruijn expressions is specified by the following rules which employ a parametric and hypothetical judgment.

\begin{aligned}K ⊢ F_{1} ↔ e_{1} K ⊢ F_{2} ↔ e_{2} \\ & tr app \\ K ⊢ F_{1} F_{2} ↔ e_{1} e_{2} \end{aligned}

\frac{u}{w ⇔ x}

⋮

K; w ⊢ F ↔ e
tr lam^{w,x,u} K ⊢ ΛF ↔ lam x. e

\begin{aligned}W ⇔ e \\ & tr 1 \\ K; W ⊢ 1 ↔ e \end{aligned}  \begin{aligned}K ⊢ F ↔ e \\ & tr ↑ \\ K; W ⊢ F ↑ ↔ e \end{aligned}

where the rule tr lam is restricted to the case where w and x are new parameters not free in any other hypothesis, and u is a new label. The translation of values is defined by a single rule in this language fragment.

K ⊢ ΛF ↔ lam x. e vtr lam {K, ΛF } ⇔ lam x. e

As remarked earlier this translation can be non-deterministic if K and e are given and F is to be generated. This is the direction in which this judgment would be used for compilation. Here is an example of a translation.

\frac{u}{w ⇔ x} ; tr 1 ; ·; w ⊢ 1 ↔ x

tr ↑

·; w; w′ ⊢ 1↑ ↔ x

tr lam^{w,′,y,u′} ·; w ⊢ Λ1↑ ↔ lam y. x
tr lam^{w,x,u} · ⊢ ΛΛ1↑ ↔ lam x. lam y. x

\frac{u′′}{w′′ ⇔ v} ; tr 1 ; ·; w′′ ⊢ 1 ↔ v

tr lam^{w′′,v,u′′} · ⊢ Λ1 ↔ lam v. v tr app
· ⊢ (ΛΛ1↑) (Λ1) ↔ (lam x. lam y. x) (lam v. v)

The representation of the translation judgment relies on the standard technique for representing deductions of hypothetical judgments as functions.

  trans  : env -> exp' -> exp -> type.  %name trans C
  vtrans : val -> exp -> type.          %name vtrans U
  tr_lam : trans K (lam' F) (lam E)
              <- {w:val} {x:exp}
                    vtrans w x -> trans (K ; w) F (E x).
  tr_app : trans K (app' F1 F2) (app E1 E2)
              <- trans K F1 E1
              <- trans K F2 E2.
  tr_1  : trans (K ; W) 1 E <- vtrans W E.
  tr_^: trans (K ; W) (F ^) E <- trans K F E.
  vtr_lam : vtrans (clo K (lam' F)) (lam E)
               <- trans K (lam' F) (lam E).

The judgment

\frac{u}{w ⇔ x}

⋮

K; w ⊢ F ↔ e

in the premiss of the tr lam is parametric in the variables w and x and hypothetical in u. It is represented by a function which, when given a value W′, an expression e′, and a deduction U′ :: W′ ⇔ e′ returns a deduction D′ :: K; W′ ⊢ F ↔ [e′/x]e. This property is crucial in the proof of compiler correctness.

The signature above can be executed as a non-deterministic program for translation between de Bruijn and ordinary expressions in both directions. For the compilation of expressions it is important to keep the clauses tr_1 and tr_^in the given order so as to avoid unnecessary backtracking. This non-determinism arises, since the expression E in the rules tr_1 and tr_^does not change in the recursive calls. For other possible implementations see Exercise 6.3. Here is an execution which yields the example deduction above.

  ?- C : trans empty F (app (lam [x] lam [y] x) (lam [v] v)).
  F = app' (lam' (lam' (1 ^))) (lam' 1),
  C =
     tr_app (tr_lam ([w'':val] [v:exp] [u'':vtrans w v] tr_1 u''))
        (tr_lam
            ([w:val] [x:exp] [u:vtrans w x]
                tr_lam ([w':val] [y:exp] [u':vtrans w' y]
                           tr_^(tr_1 u)))).
  ;
  no more solutions

It is not immediately obvious that every source expression e can in fact be compiled using this judgment. This is the subject of the following theorem.

Theorem 6.1 For every closed expression e there exists a de Bruijn expression F such that · ⊢ F ↔ e.

Proof: A direct attempt at an induction argument fails—a typical situation when proving properties of judgments which involve hypothetical reasoning. However, the theorem follows immediately from Lemma 6.2 below. □

Lemma 6.2 Let w_{1}, …, w_{n} be parameters ranging over values and let K be the environment ·; w_{n}; …; w_{1}. Furthermore, let x_{1}, …, x_{n} range over expression variables. For any expression e with free variables among x_{1}, …, x_{n} there exists a de Bruijn expression F and a deduction C of K ⊢ F ↔ e from hypotheses u_{1} :: w_{1} ⇔ x_{1}, …, u_{n} :: w_{n} ⇔ x_{n}.

Proof: By induction on the structure of e.

Case: e = e_{1} e_{2}. By induction hypothesis on e_{1} and e_{2}, there exist F_{1} and F_{2} and deductions C_{1} :: K ⊢ F_{1} ↔ e_{1} and C_{2} :: K ⊢ F_{2} ↔ e_{2}. Applying the rule

tr app to C_{1} and C_{2} yields the desired deduction C :: K ⊢ F_{1} F_{2} ↔ e_{1} e_{2}.

Case: e = lam x. e_{1}. Here we apply the induction hypothesis to the expression e_{1}, environment K; w for a new parameter w, and hypotheses u_{1} :: w_{1} ⇔ x_{1}, …, u_{n} :: w_{n} ⇔ x_{n}, u :: w ⇔ x to obtain an F_{1} and a deduction

\frac{u}{w ⇔ x}

C_{1} K; w ⊢ F_{1} ↔ e_{1} possibly also using hypotheses labelled u_{1}, …, u_{n}. Note that e_{1} is an expression with free variables among x_{1}, …, x_{n}, x. Applying the rule tr lam discharges the hypothesis u and we obtain the desired deduction

C = \frac{u}{w ⇔ x} ; C_{1} ; K; w ⊢ F_{1} ↔ e_{1} tr lam^{u} K ⊢ ΛF_{1} ↔ lam x. e_{1}

Case: e = x. Then x = x_{i} for some i between 1 and n and we let F = 1 \underbrace{↑ ⋯ ↑}_{i−1 times}

and

C = \frac{u_{i}}{w_{i} ⇔ x_{i}} ; tr 1 ; ·; w_{n}; …; w_{i} ⊢ 1 ↔ x_{i} ; tr ↑ ; ⋯; tr ↑ ; ·; w_{n}; …; w_{1} ⊢ 1↑ ⋯ ↑ ↔ x_{i}

□

At present we do not know how to represent this proof in Elf because we cannot employ the usual technique for representing hypothetical judgments as functions. The difficulty is that the order of the hypotheses is important for returning the correct variable 1↑ ⋯ ↑, but hypothetical judgments are generally invariant under reordering of hypotheses. Hannan [Han91] has suggested a different, deterministic translation for which termination is relatively easy to show, but which complicates the proofs of the remaining properties of compiler correctness. Thus our formalization does not capture the desirable property that compilation always terminates. All the remaining parts, however, are implemented. The first property states that translation followed by evaluation leads to the same result as evaluation followed by translation. We generalize this for arbitrary environments K in order to allow a proof by induction. This property is depicted in the following diagram.

[figure: e; K, F; W; v; C :: K ⊢ F ↔ e; D :: e ↪ v; . . . . . . . . . . . . . . . . . . .; D′ :: K ⊢ F ↪ W; ^{........}; U :: W ⇔ v]

The solid lines indicate deductions that are assumed, dotted lines represent the deductions whose existence we assert and prove below.

Lemma 6.3 For any closed expressions e and v, environment K, de Bruijn expression F, deductions D :: e ↪ v and C :: K ⊢ F ↔ e, there exist a value W and deductions D′ :: K ⊢ F ↪ W and U :: W ⇔ v.

Proof: By induction on the structures of D :: e ↪ v and C :: K ⊢ F ↔ e. In this induction we assume the induction hypothesis on the premisses of D and for arbitrary C and on the premisses of C, but for the same D. This is sometimes called lexicographic induction on the pair consisting of D and C. It should be intuitively clear that this form of induction is valid. We represent this proof as a judgment relating the four deductions involved in the diagram.

  map_eval : eval E V -> trans K F E
                -> feval K F W -> vtrans W V -> type.

Case: C ends in an application of the tr 1 rule.

C = \begin{matrix}U_{1} \\ W_{1} ⇔ e \end{matrix} ; tr 1 ; K_{1}; W_{1} ⊢ 1 ↔ e

D :: e ↪ v  Assumption
C_{1} :: K′_{1} ⊢ ΛF′_{1} ↔ e and W_{1} = {K′_{1}, ΛF′_{1}}  By inversion on U_{1}
e = lam x. e_{1}  By inversion on C_{1}
v = lam x. e_{1} = e  By inversion on D

Then W = W_{1}, U = U_{1} :: W_{1} ⇔ e and D′ = fev 1 :: K_{1}; W_{1} ⊢ 1 ↪ W_{1} satisfy the requirements of the theorem. This case is captured in the clause

       mp_1 : map_eval (ev_lam) (tr_1 (vtr_lam (tr_lam C2)))
                       (fev_1) (vtr_lam (tr_lam C2)).

Case: C ends in an application of the tr ↑ rule.

C = \begin{matrix}C_{1} \\ K_{1} ⊢ F_{1} ↔ e \end{matrix} ; tr ↑ ; K_{1}; W′_{1} ⊢ F_{1}↑ ↔ e

D :: e ↪ v  Assumption
D′_{1} :: K_{1} ⊢ F_{1} ↪ W_{1}
and U_{1} :: W_{1} ⇔ v  By ind. hyp. on D and C_{1}

Now we let W = W_{1}, U = U_{1}, and obtain D′ :: K_{1}; W′_{1} ⊢ F_{1}↑ ↪ W_{1} by fev ↑ from D′_{1}.

  mp_^: map_eval D (tr_^C1) (fev_^D1') U1
            <- map_eval D C1 D1' U1.

For the remaining cases we assume that the previous two cases do not apply. We refer to this assumption as exclusion.

Case: D ends in an application of the ev lam rule.

D = \frac{ev lam}{lam x. e_{1} ↪ lam x. e_{1}}
C :: K ⊢ F ↔ lam x. e_{1}  By assumption
F = ΛF_{1}  By inversion and exclusion

Then we let W = {K, ΛF_{1}}, D′ = fev lam :: K ⊢ ΛF_{1} ↪ {K, ΛF_{1}}, and obtain U :: {K, ΛF_{1}} ⇔ lam x. e_{1} by vtr lam from C.

  mp_lam : map_eval (ev_lam) (tr_lam C1)
                    (fev_lam) (vtr_lam (tr_lam C1)).

Case: D ends in an application of the ev app rule.

D = \begin{matrix}D_{1} \\ e_{1} ↪ lam x. e′_{1} \end{matrix}  \begin{matrix}D_{2} \\ e_{2} ↪ v_{2} \end{matrix}  \begin{matrix}D_{3} \\ [v_{2}/x]e′_{1} ↪ v \end{matrix} ev app e_{1} e_{2} ↪ v

This is the most interesting case, since it contains the essence of the argument how substitution can be replaced by binding variables to values in an environment.

C :: K ⊢ F ↔ e_{1} e_{2}  By assumption

F = F_{1} F_{2}, C_{1} :: K ⊢ F_{1} ↔ e_{1}, and

C_{2} :: K ⊢ F_{2} ↔ e_{2}  By inversion and exclusion

D′_{2} :: K ⊢ F_{2} ↪ W_{2} and

U_{2} :: W_{2} ⇔ v_{2}  By ind. hyp. on D_{2} and C_{2}

D′_{1} :: K ⊢ F_{1} ↪ W_{1} and

U_{1} :: W_{1} ⇔ lam x. e′_{1}  By ind. hyp. on D_{1} and C_{1}

W_{1} = {K_{1}, ΛF′_{1}} and

C′_{1} :: K_{1} ⊢ ΛF′_{1} ↔ lam x. e′_{1}  By inversion on U_{1}

Applying inversion again to C′_{1} shows that the premiss must be the deduction of a hypothetical judgment. That is,

C′_{1} = \frac{u}{w ⇔ x} ; C_{3} ; K_{1}; w ⊢ F′_{1} ↔ e′_{1}

where w is a new parameter ranging over values. This judgment is parametric in w and x and hypothetical in u. We can thus substitute W_{2} for w, v_{2} for x, and U_{2} for u to obtain a deduction

C′_{3} :: K_{1}; W_{2} ⊢ F′_{1} ↔ [v_{2}/x]e′_{1}.

Now we apply the induction hypothesis to D_{3} and C′_{3} to obtain a W_{3} and

D′_{3} :: K_{1}; W_{2} ⊢ F′_{1} ↪ W_{3} and U_{3} :: W_{3} ⇔ v.

We let W = W_{3}, U = U_{3}, and obtain D′ :: K ⊢ F_{1} F_{2} ↪ W by fev app from D′_{1}, D′_{2}, and D′_{3}.

The implementation of this relatively complex reasoning employs again the magic of hypothetical judgments: the substitution we need to carry out to obtain C′_{3} from C_{3} is implemented as a function application.

  mp_app : map_eval (ev_app D3 D2 D1) (tr_app C2 C1)
                    (fev_app D3' D2' D1') U3
                 <- map_eval D1 C1 D1' (vtr_lam (tr_lam C3))
                 <- map_eval D2 C2 D2' U2
                 <- map_eval D3 (C3 W2 V2 U2) D3' U3.

This completes the proof once we have convinced ourselves that all possible cases have been considered. Note that whenever C ends in an application of the tr 1 or tr ↑ rules, then the first two cases apply. Otherwise one of the other two cases must apply, depending on the shape of D. □

Theorem 6.1 and Lemma 6.3 together guarantee completeness of the translation.

Theorem 6.4 (Completeness) For any closed expressions e and v and evaluation D :: e ↪ v, there exist a de Bruijn expression F, a value W and deductions C :: · ⊢ F ↔ e, D′ :: · ⊢ F ↪ W, and U :: W ⇔ v.

Proof: Lemma 6.3 shows that an evaluation D′ :: K ⊢ F ↪ W and a translation W ⇔ v exist for any translation C :: K ⊢ F ↔ e. Theorem 6.1 shows that a particular F and translation C :: · ⊢ F ↔ e exist, thus proving the theorem. □

Completeness is insufficient to guarantee compiler correctness. For example, the translation of values W ⇔ v could relate any expression v to any value W, which would make the statement of the previous theorem almost trivially true. We need to check a further property, namely that any value which could be produced by evaluating the compiled code, could also be produced by direct evaluation as specified by the natural semantics. This is shown in the diagram below.

[figure: e; K, F; W; v; C :: K ⊢ F ↔ e; D′ :: K ⊢ F ↪ W; U :: W ⇔ v; ^{......................}; D :: e ↪ v]

We call this property soundness of the compiler, since it prohibits the compiled code from producing incorrect values. We prove this from a lemma which asserts the existence of an expression v, evaluation D and translation U, given the translation C and evaluation D′. This yields the theorem by showing that the translation U :: W ⇔ v, is uniquely determined from W.

Lemma 6.5 For any closed expression e, de Bruijn expression F, environment K, value W, deductions D′ :: K ⊢ F ↪ W and C :: K ⊢ F ↔ e, there exist an expression v and deductions D :: e ↪ v and U :: W ⇔ v.

Proof: The proof proceeds by a straightforward induction over the structure of D′ :: K ⊢ F ↪ W. It heavily employs inversion (as the proof of completeness, Lemma 6.3). Interestingly, this proof can be implemented by literally the same judgment. We leave it as exercise 6.6 to write out the informal proof—its representation from the proof of completeness is summarized below. Using is as a program in this instance means that we assume that second and third arguments are given and the first and last argument are logic variables whose instantiation terms are to be constructed.

  map_eval : eval E V -> trans K F E
                -> feval K F W -> vtrans W V -> type.
  mp_1 : map_eval (ev_lam) (tr_1 (vtr_lam (tr_lam C2)))
                  (fev_1) (vtr_lam (tr_lam C2)).
  mp_^: map_eval D (tr_^C1) (fev_^D1') U1
            <- map_eval D C1 D1' U1.
  mp_lam : map_eval (ev_lam) (tr_lam C1)

                    (fev_lam) (vtr_lam (tr_lam C1)).
  mp_app : map_eval (ev_app D3 D2 D1) (tr_app C2 C1)
                    (fev_app D3' D2' D1') U3
                 <- map_eval D1 C1 D1' (vtr_lam (tr_lam C3))
                 <- map_eval D2 C2 D2' U2
                 <- map_eval D3 (C3 W2 V2 U2) D3' U3.

□

Theorem 6.6 (Uniqueness of Translations) For any value W if there exist a v and a translation U :: W ⇔ v, then v and U are unique. Furthermore, for any environment K and de Bruijn expression F, if there exist an e and a translation C :: K ⊢ F ↔ e, then e and C are unique.

Proof: By simultaneous induction on the structures of U and C. In each case, either W or F uniquely determine the last inference. Since the translated expressions in the premisses are unique by induction hypothesis, so is the translated value in the conclusion. □

The proof requires no separate implementation in Elf in the same way that appeals to inversion remain implicit in the formulation of higher-level judgments. It is obtained by direct inspection of properties of the inference rules.

Theorem 6.7 (Soundness) For any closed expressions e and v, de Bruijn expression F, environment K, value W, deductions D′ :: K ⊢ F ↪ W, C :: K ⊢ F ↔ e, and U :: W ⇔ v, there exists a deduction D :: e ↪ v.

Proof: From Lemma 6.5 we infer the existence of a v, U, and D, given C and D′. Theorem 6.6 shows that v and U are unique, and thus the property must hold for all v and U :: W ⇔ v, which is what we needed to show. □

6.2 Adding Data Values and Recursion

In the previous section we treated only a very restricted core language of Mini-ML. In this section we will extend the compiler to the full Mini-ML language as presented in Chapter 2. The main additions to the core language which affect the compiler are data values (such as natural numbers and pairs) and recursion. The language of de Bruijn expressions is extended by allowing constructors that parallel ordinary expressions. We maintain a similar syntax, but mark de Bruijn expression constructors with a prime (′).

Expressions F ::= | z′ | s′ F | case′ F_{1} F_{2} F_{3} Natural Numbers
| ⟨F_{1}, F_{2}⟩′ | fst′ F | snd′ F Pairs
| ΛF | F_{1} F_{2}  Functions
| let′ val F_{1} in F_{2}  Definitions
| let′ name F_{1} in F_{2}
| fix′ F  Recursion
| 1 | F ↑  Variables

Expressions of the form F ↑ are not necessarily variables (where F is a sequence of shifts applied to 1), but it may be intuitively helpful to think of them that way. In the representation we need only first-order constants, since this language has no constructs binding variables by name.

exp'   : type.  %name exp' F
1      : exp'.
^: exp' -> exp'.  %postfix 20 ^
z'     : exp'.
s'     : exp' -> exp'.
case'  : exp' -> exp' -> exp' -> exp'.
pair'  : exp' -> exp' -> exp'.
fst'   : exp' -> exp'.
snd'   : exp' -> exp'.
lam'   : exp' -> exp'.
app'   : exp' -> exp' -> exp'.
letv'  : exp' -> exp' -> exp'.
letn'  : exp' -> exp' -> exp'.
fix'   : exp' -> exp'.

Next we need to extend the language of values. While data values can be added in a straightforward fashion, let name and recursion present some difficulties. Consider the evaluation rule for fixpoints.

[fix x. e/x]e ↪ v

ev fix

fix x. e ↪ v

We introduced the environment model of evaluation in order to eliminate the need for explicit substitution, where an environment is a list of values. In the case of the fixpoint construction we would need to bind the variable x to the expression fix x. e in the environment in order to avoid substutition, but fix x. e is not a value. The evaluation rules for de Bruijn expressions take advantage of the invariant that an environment contains only values. In particular, the rule

\frac{fev 1}{K; W ⊢ 1 ↪ W}

requires that an environment contain only values. We will thus need to add a new environment constructor K + F in order to allow unevaluated expressions in the environment. These considerations yield the following mutually recursive definitions of environments and values. We mark data values with a star (^{∗}) to distinguish them from expressions and de Bruijn expressions with the same name.

Environments K ::= · | K; W | K + F
Values W ::= | z^{∗} | s^{∗} W  Natural Numbers
| ⟨W_{1}, W_{2}⟩^{∗}  Pairs
| {K, F }  Closures

The Elf representation is direct.

  env    : type.  %name env K
  val    : type.  %name val W
  empty  : env.
  ;      : env -> val -> env.   %infix left 10 ;
  +      : env -> exp' -> env.  %infix left 10 +
  z*     : val.
  s*     : val -> val.
  pair*  : val -> val -> val.
  clo    : env -> exp' -> val.

In the extension of the evaluation rule to this completed language, we must exercise care in the treatment of the new environment constructor for unevaluated expression: when such an expression is looked up in the environment, it must be evaluated.

\begin{aligned}K ⊢ F ↪ W \\ & fev 1+ \\ K + F ⊢ 1 ↪ W \end{aligned}  \begin{aligned}K ⊢ F ↪ W \\ & fev ↑+ \\ K + F′ ⊢ F ↑ ↪ W \end{aligned}

The rules involving data values generally follow the patterns established in the natural semantics for ordinary expressions. The main departure from the earlier formulation is the separation of values from expressions. We show only four of the relevant rules.

\frac{fev z}{K ⊢ z′ ↪ z^{∗}}  \begin{aligned}K ⊢ F ↪ W \\ & fev s \\ K ⊢ s′ F ↪ s^{∗} W \end{aligned}
K ⊢ F_{1} ↪ z^{∗} K ⊢ F_{2} ↪ W

fev case z

K ⊢ case′ F_{1} F_{2} F_{3} ↪ W

K ⊢ F_{1} ↪ s^{∗} W′_{1} K; W′_{1} ⊢ F_{3} ↪ W

fev case s

K ⊢ case′ F_{1} F_{2} F_{3} ↪ W

Evaluating a let val-expression also binds a variable to value by extending the

environment. K ⊢ F_{1} ↪ W_{1} K; W_{1} ⊢ F_{2} ↪ W

fev letv

K ⊢ let val′ F_{1} in F_{2} ↪ W

Evaluating a let name-expression binds a variable to an expression and thus requires the new environment constructor.

K + F_{1} ⊢ F_{2} ↪ W

fev letn

K ⊢ let name′ F_{1} in F_{2} ↪ W

Fixpoint expressions are similar, except that the variable is bound to the fix expression itself.

K + fix′ F ⊢ F ↪ W

fev fix

K ⊢ fix′ F ↪ W

For example, fix x. x (considered on page 16) is represented by fix′ 1. Intuitively, evaluation of this expression should not terminate. An attempt to construct an evaluation leads to the sequence

⋮ fev fix

· ⊢ fix′ 1 ↪ W

fev 1+

· +fix′ 1 ⊢ 1 ↪ W

fev fix.

· ⊢ fix′ 1 ↪ W

The implementation of these rules in Elf poses no particular difficulties. We show only the rules from above.

  feval : env -> exp' -> val -> type.  %name feval D

% Variables
fev_1+ : feval (K + F) 1 W
            <- feval K F W.
fev_^+ : feval (K + F') (F ^) W
            <- feval K F W.
% Natural Numbers
fev_z : feval K z' z*.
fev_s : feval K (s' F) (s* W)
         <- feval K F W.
fev_case_z : feval K (case' F1 F2 F3) W
              <- feval K F1 z*
              <- feval K F2 W.
fev_case_s : feval K (case' F1 F2 F3) W
              <- feval K F1 (s* W1)
              <- feval (K ; W1) F3 W.
% Definitions
fev_letv : feval K (letv' F1 F2) W
            <- feval K F1 W1
            <- feval (K ; W1) F2 W.
fev_letn : feval K (letn' F1 F2) W
            <- feval (K + F1) F2 W.
% Recursion
fev_fix  : feval K (fix' F) W
             <- feval (K + (fix' F)) F W.

Next we need to extend the translation between expressions and de Bruijn expressions and values. We show a few interesting cases in the extended judgments K ⊢ F ↔ e and W ⇔ v. The case for let val is handled just like the case for lam, since we will always substitute a value for the variable bound by the let during execution.

\frac{tr z}{K ⊢ z′ ↔ z}  \begin{aligned}K ⊢ F ↔ e \\ & tr s \\ K ⊢ s′ F ↔ s e \end{aligned}

K ⊢ F_{1} ↔ e_{1}  \frac{u}{w ⇔ x} ; ⋮ ; K; w ⊢ F_{2} ↔ e_{2}
tr letv^{w,x,u}
K ⊢ let val′ F_{1} in F_{2} ↔ let x = e_{1} in e_{2}

where the right premiss of tr let is parametric in w and x and hypothetical in u. In order to preserve the basic structure of the proofs of lemmas 6.3 and 6.5, we must treat the let name and fix constructs somewhat differently: we extend the environment with an expression parameter (not a value parameter) using the new environment constructor +.

K ⊢ F_{1} ↔ e_{1}  \frac{u}{K ⊢ f ⇔ x} ; ⋮ ; K + f ⊢ F_{2} ↔ e_{2}
tr letn^{f,x,u}
K ⊢ let name′ F_{1} in F_{2} ↔ let x = e_{1} in e_{2}

\frac{u}{K ⊢ f ↔ x}

⋮

K + f ⊢ F ↔ e

tr fix^{f,x,u} K ⊢ fix′ F ↔ fix x. e

\begin{aligned}K ⊢ F ↔ e \\ & tr 1+ \\ K + F ⊢ 1 ↔ e \end{aligned}  \begin{aligned}K ⊢ F ↔ e \\ & tr ↑+ \\ K + F′ ⊢ F ↑ ↔ e \end{aligned}

Finally, the value translation does not have to deal with fixpoint-expressions (they are not values). We only show the three new cases.

\frac{vtr z}{z^{∗} ⇔ z}  \begin{aligned}W ⇔ v \\ & vtr s \\ s^{∗} W ⇔ s v \end{aligned}

W_{1} ⇔ v_{1} W_{2} ⇔ v_{2} vtr pair

⟨W_{1}, W_{2}⟩^{∗} ⇔ ⟨v_{1}, v_{2}⟩ Deductions of parametric and hypothetical judgments are represented by functions, as usual.

trans  : env -> exp' -> exp -> type.  %name trans C
vtrans : val -> exp -> type.          %name vtrans U
% Natural numbers
tr_z     : trans K z' z.
tr_s     : trans K (s' F) (s E)
            <- trans K F E.
% Definitions
tr_letv: trans K (letv' F1 F2) (letv E1 E2)
            <- trans K F1 E1
            <- ({w:val} {x:exp}
                  vtrans w x -> trans (K ; w) F2 (E2 x)).
tr_letn: trans K (letn' F1 F2) (letn E1 E2)
            <- trans K F1 E1
            <- ({f:exp'} {x:exp}
                  trans K f x -> trans (K + f) F2 (E2 x)).
% Recursion
tr_fix : trans K (fix' F) (fix E)
            <- ({f:exp'} {x:exp}
                   trans K f x -> trans (K + f) F (E x)).
% Variables
tr_1+ : trans (K + F) 1 E <- trans K F E.
tr_^+ : trans (K + F') (F ^) E <- trans K F E.
% Natural number values
vtr_z : vtrans z* z.
vtr_s : vtrans (s* W) (s V)
         <- vtrans W V.
% Pair values
vtr_pair : vtrans (pair* W1 W2) (pair V1 V2)
            <- vtrans W1 V1
            <- vtrans W2 V2.

In order to extend the proof of compiler correctness in Section 6.1 we need to extend various lemmas.

Theorem 6.8 For every closed expression e there exists a de Bruijn expression F such that · ⊢ F ↔ e.

Proof: We generalize analogously to Lemma 6.2 and prove the modified lemma by induction on the structure of e (see Exercise 6.7). □

Lemma 6.9 If W ⇔ e is derivable, then e Value is derivable.

Proof: By a straightforward induction on the structure of U :: W ⇔ e. □

Lemma 6.10 If e Value and e ↪ v are derivable then e = v.

Proof: By a straightforward induction on the structure of P :: e Value. □

The Elf implementations of the proofs of Lemmas 6.9 and 6.10 is straightforward and can be found in the on-line material that accompanies these notes. The type families are

  vtrans_val : vtrans W E -> value E -> type.
  val_eval  : value E -> eval E E -> type.

The next lemma is the main lemma is the proof of completeness, that is, every value which can obtained by direct evaluation can also be obtained by compilation, evaluation of the compiled code, and translation of the returned value to the original language.

Lemma 6.11 For any closed expressions e and v, environment K, de Bruijn expression F, deduction D :: e ↪ v and C :: K ⊢ F ↔ e, there exist a value W and deductions D′ :: K ⊢ F ↪ W and U :: W ⇔ v.

Proof: By induction on the structure of D :: e ↪ v and C :: K ⊢ F ↔ e. In this induction, as in the proof of Lemma 6.3, we assume the induction hypothesis on the premisses of D and for arbitrary C, and on the premisses of C if D remains fixed. The implementation is an extension of the previous higher-level judgment,

  map_eval : eval E V -> trans K F E
                -> feval K F W -> vtrans W V -> type.

We show only some of the typical cases—the others are straightforward and left to the reader or remain unchanged from the proof of Lemma 6.3

Case: C ends in an application of the tr 1 rule.

C = \begin{matrix}U_{1} \\ W_{1} ⇔ e \end{matrix} ; tr 1 ; K_{1}; W_{1} ⊢ 1 ↔ e

This case changes from the previous proof, since there we applied simple inversion (there was only one possible kind of value) to conclude that e = v. Here we need two lemmas from above.

D :: e ↪ v  Assumption
P :: e Value  By Lemma 6.9 from U_{1}
e = v  By Lemma 6.10 from P

Hence we can let W be W_{1}, U be U_{1}, and D′ be fev 1 :: K_{1}; W_{1} ⊢ 1 ↪ W_{1}. The implementation explicitly appeals to the implementations of the lemmas.

  mp_1 : map_eval D (tr_1 U1) (fev_1) U1
            <- vtrans_val U1 P
            <- val_eval P D.

Case: C ends in an application of the tr ↑ rule. This case proceeds as before.

  mp_^: map_eval D (tr_^C1) (fev_^D1') U1
            <- map_eval D C1 D1' U1.

Case: C ends in an application of the tr 1+ rule.

C = \begin{matrix}C_{1} \\ K_{1} ⊢ F_{1} ↔ e \end{matrix} ; tr 1+ ; K_{1} + F_{1} ⊢ 1 ↔ e

D :: e ↪ v  Assumption

D′_{1} :: K_{1} ⊢ F_{1} ↪ W_{1} and

U_{1} :: W_{1} ⇔ v  By ind. hyp. on D and C_{1}
D′ :: K_{1} + F_{1} ⊢ 1 ↪ W  By fev 1+ from D′_{1}

and we can let W = W_{1} and U = U_{1}.

  mp_1+ : map_eval D (tr_1+ C1) (fev_1+ D1') U1
             <- map_eval D C1 D1' U1.

Case: C ends in an application of the tr ↑+ rule. This case is just like the tr ↑ case.

  mp_^+ : map_eval D (tr_^+ C1) (fev_^+ D1') U1
             <- map_eval D C1 D1' U1.

For the remaining cases we may assume that none of the four cases above apply. We only show the case for fixpoints.

Case: D ends in an application of the ev fix rule.

D = \begin{matrix}D_{1} \\ [fix x. e_{1}/x]e_{1} ↪ v \end{matrix} ev fix fix x. e_{1} ↪ v

C :: K ⊢ F ↔ fix x. e_{1}  By assumption

By inversion and exclusion (of the previous cases), C must end in an application of the tr fix rule and thus F = fix′ F_{1} for some F_{1} and there is a deduction C_{1}, parametric in f and x and hypothetical in u, of the form

\frac{u}{K ⊢ f ↔ x}

C_{1} K + f ⊢ F_{1} ↔ e_{1}

In this deduction we can substitute fix′ F_{1} for f and fix x. e_{1} for x, and replace the resulting hypothesis u :: K ⊢ fix′ F_{1} ↔ fix x. e_{1} by C! This way we obtain a deduction

C′_{1} :: K + fix′ F_{1} ⊢ F_{1} ↔ [fix x. e_{1}/x]e_{1}.

Now we can apply the induction hypothesis to D_{1} and C′_{1} which yields a W_{1} and deductions

D′_{1} :: K + fix′ F_{1} ⊢ F_{1} ↪ W_{1} and

U_{1} :: W_{1} ⇔ v  By ind. hyp. on D_{1} and C′_{1}

Applying fev fix to D′_{1} results in a deduction

D′ :: K ⊢ fix′ F_{1} ↪ W_{1}

and we let W be W_{1} and U be U_{1}. In Elf, the substitutions into the hypothetical deduction are implemented by applications of the representing function

C1.
  mp_fix : map_eval (ev_fix D1) (tr_fix C1)
                    (fev_fix D1') U1
              <- map_eval D1 (C1 (fix' F1) (fix E1) (tr_fix C1))
                          D1' U1.

□

This lemma and the totality of the translation relation in its expression argument (Theorem 6.8) together guarantee completeness of the translation.

Theorem 6.12 (Completeness) For any closed expressions e and v and evaluation D :: e ↪ v, there exist a de Bruijn expression F, a value W and deductions C :: · ⊢ F ↔ e, D′ :: · ⊢ F ↪ W, and U :: W ⇔ v.

Proof: As in the proof of Theorem 6.4, but using Lemma 6.11 and Theorem 6.8 instead of Lemma 6.3 and Theorem 6.1. □

Lemma 6.13 For any closed expression e, de Bruijn expression F, environment K, value W, deduction D′ :: K ⊢ F ↪ W and C :: K ⊢ F ↔ e, there exist an expression v and deductions D :: e ↪ v and U :: W ⇔ v.

Proof: By induction on the structure of D′ :: K ⊢ F ↪ W. The family map_eval which implements the main lemma in the soundness proof, also implements the proof of this lemma without any change. □

Theorem 6.14 (Uniqueness of Translations) For any value W if there exist a v and a translation U :: W ⇔ v, then v and U are unique. Furthermore, for any environment K and de Bruijn expression F, if there exist an e and a translation C :: K ⊢ F ↔ e, then e and C are unique.

Proof: As before, by a simultaneous induction on the structures of U and C. □

Theorem 6.15 (Soundness) For any closed expressions e and v, de Bruijn expression F, environment K, value W, deductions D′ :: K ⊢ F ↪ W, C :: K ⊢ F ↔ e, and U :: W ⇔ v, there exists a deduction D :: e ↪ v.

Proof: From Lemma 6.13 we infer the existence of a v, U, and D, given C and D′. Theorem 6.14 shows that v and U are unique, and thus the property must hold for all v and U :: W ⇔ v, which is what we needed to show. □

6.3 Computations as Transition Sequences

So far, we have modelled evaluation as the construction of a deduction of the evaluation judgment. This is true for evaluation based on substitution in Section 2.3 and for evaluation based on environments in Section 6.1. In an abstract machine (and, of course, in an actual machine) a more natural model for computation is a sequence of states. In this section we will develop the CLS machine, an abstract machine similar in scope to the SECD machine [Lan64]. The CLS machine still interprets expressions, so the step from environment based evaluation to this abstract machine does not involve any compilation. Instead, we flatten evaluation trees to sequences of states that describe the computation. This flattening involves some rather arbitrary decisions about which subcomputations should be performed first. We linearize the evaluation deductions beginning with the deduction of the leftmost premiss.

Throughout the remainder of this chapter, we will drop the prime (′) from the expression constructors. This should not lead to any confusion, since we no longer need to refer to the original expressions. Now consider the rule for evaluating pairs as a simple example where an evaluation tree has two branches.

K ⊢ F_{1} ↪ W_{1} K ⊢ F_{2} ↪ W_{2} fev pair

K ⊢ ⟨F_{1}, F_{2}⟩ ↪ ⟨W_{1}, W_{2}⟩^{∗}

An abstract machine would presumably start in a state where it is given the environment K and the expression ⟨F_{1}, F_{2}⟩. The final state of the machine should somehow indicate the final value ⟨W_{1}, W_{2}⟩^{∗}. The computation naturally decomposes into three phases: the first phase computes the value of F_{1} in environment K, the second phase computes the value of F_{2} in environment K, and the third phase combines the two values to form a pair. These phases mean that we have to preserve the environment K and also the expression F_{2} while we are computing the value of F_{1}. Similarly, we have to save the value W_{1} while computing the value of F_{2}. A natural data structure for saving components of a state is a stack. The considerations above suggest three stacks: a stack KS of environments, a stack of expressions to be evaluated, and a stack S of values. However, we also need to remember that, after the evaluation of F_{2} we need to combine W_{1} and W_{2} into a pair. Thus, instead of a stack of expression to be evaluated, we maintain a program which consists of expressions and special instructions (such as: make a pair written as mkpair).

We will need more instructions later, but so far we have:

Instructions I ::= F | mkpair | …
Programs P ::= done | I & P
Environment Stacks KS ::= · | KS; K
Value Stacks S ::= · | S; W
State St ::= ⟨KS, P, S⟩

Note that value stacks are simply environments, so we will not formally distinguish them from environments. The instructions of a program a sequenced with &; the program done indicates that there are no further instructions, that is, computation should stop.

A state consists of an environment stack KS, a program P and a value stack S, written as ⟨KS, P, S⟩. We have single-step and multi-step transition judgments:

St ⟹ St′ St goes to St′ in one computation step
St ⟹^{∗} St′ St goes to St′ in zero or more steps

We define the transition judgment so that

⟨(·; K), F & done, ·⟩ ⟹^{∗} ⟨·, done, (·; W)⟩

corresponds to the evaluation of F in environment K to value W. The free variables of F are therefore bound in the innermost environment, and the value resulting from evaluation is deposited on the top of the value stack, which starts out empty. Global evaluation is expressed in the judgment

K ⊢ F ⟹ ⟹^{∗} W F computes to W in environment K

which is defined by the single inference rule

⟨(·; K), F & done, ·⟩ ⟹^{∗} ⟨·, done, (·; W)⟩

run.

K ⊢ F ⟹ ⟹^{∗} W

We prove in Theorem 6.19 that K ⊢ F ⟹ ⟹^{∗} W iff K ⊢ F ↪ W. We cannot prove this statement directly by induction (in either direction), since during a computation situations arise where the environment stack consists of more than a single environment, the remaining program is not done, etc. In one direction we generalize it to

⟨(KS; K), F & P, S⟩ ⟹^{∗} ⟨KS, P, (S; W)⟩

if K ⊢ F ↪ W. This is the subject of Lemma 6.16. A slightly modified form of the converse is given in Lemma 6.18.

The transition rules and the remaining instructions can be developed systematically from the intuition provided above. First, we reconsider the evaluation of pairing. The first rule decomposes the pair expression and saves the environment K on the environment stack.

c pair :: ⟨(KS; K), ⟨F_{1}, F_{2}⟩ & P, S⟩ ⟹ ⟨(KS; K; K), F_{1} & F_{2} & mkpair & P, S⟩

Here c pair labels the rule and can be thought of as the deduction of the given transition judgment. The evaluation of F_{1}, if it terminates, leads to a state

⟨(KS; K), F_{2} & mkpair & P, (S; W_{1})⟩,

and the further evaluation of F_{2} then leads to a state

⟨KS, mkpair & P, (S; W_{1}; W_{2})⟩.

Thus, the mkpair instruction should cause the machine to create a pair from the first two elements on the value stack and deposit the result again on the value stack. That is, we need as another rule:

c mkpair :: ⟨KS, mkpair & P, (S; W_{1}; W_{2})⟩ ⟹ ⟨KS, P, (S; ⟨W_{1}, W_{2}⟩^{∗})⟩.

We consider one other construct in detail: application. To evaluate an application F_{1} F_{2} we first evaluate F_{1} and then we evaluate F_{2}. If the value of F_{1} is a closure, we have to bind its variable to the value of F_{2} and continue evaluation in an extended environment. The instruction that unwraps the closure and extends the environment is called apply.

c app :: ⟨(KS; K), F_{1} F_{2} & P, S⟩ ⟹ ⟨(KS; K; K), F_{1} & F_{2} & apply & P, S⟩
c apply :: ⟨KS, apply & P, (S; {K′, ΛF′_{1}}; W_{2})⟩ ⟹ ⟨(KS; (K′; W_{2})), F′_{1} & P, S⟩

The rules for applying zero and successor are straightforward, but they necessitate a new operator add1 to increment the first value on the stack.

c z :: ⟨(KS; K), z & P, S⟩ ⟹ ⟨KS, P, (S; z^{∗})⟩
c s :: ⟨(KS; K), s F & P, S⟩ ⟹ ⟨(KS; K), F & add1 & P, S⟩
c add1 :: ⟨KS, add1 & P, (S; W)⟩ ⟹ ⟨KS, P, (S; s^{∗} W)⟩

For expressions of the form case F_{1} F_{2} F_{3}, we need to evaluate F_{1} and then evaluate either F_{2} or F_{3}, depending on the value of F_{1}. This requires a new instruction, branch, which either goes to the next instructions or skips the next instruction. In the latter case it also needs to bind a new variable in the environment to the predecessor of the value of F_{1}.

c case :: ⟨(KS; K), case F_{1} F_{2} F_{3} & P, S⟩
⟹ ⟨(KS; K; K), F_{1} & branch & F_{2} & F_{3} & P, S⟩
c branch z :: ⟨(KS; K), branch & F_{2} & F_{3} & P, (S; z^{∗})⟩ ⟹ ⟨(KS; K), F_{2} & P, S⟩
c branch s :: ⟨(KS; K), branch & F_{2} & F_{3} & P, (S; s^{∗} W)⟩
⟹ ⟨(KS; (K; W)), F_{3} & P, S⟩

Rules for fst and snd require new instructions to extract the first or second element of the value on the top of the stack.

c fst :: ⟨(KS; K), fst F & P, S⟩ ⟹ ⟨(KS; K), F & getfst & P, S⟩
c getfst :: ⟨KS, getfst & P, (S; ⟨W_{1}, W_{2}⟩^{∗})⟩ ⟹ ⟨KS, P, (S; W_{1})⟩
c snd :: ⟨(KS; K), snd F & P, S⟩ ⟹ ⟨(KS; K), F & getsnd & P, S⟩
c getsnd :: ⟨KS, getsnd & P, (S; ⟨W_{1}, W_{2}⟩^{∗})⟩ ⟹ ⟨KS, P, (S; W_{2})⟩

In order to handle let val we introduce another new instruction bind, even though it is not strictly necessary and could be simulated with other instructions (see Exercise 6.10).

c let :: ⟨(KS; K), let F_{1} in F_{2} & P, S⟩ ⟹ ⟨(KS; K; K), F_{1} & bind & F_{2} & P, S⟩
c bind :: ⟨(KS; K), bind & F_{2} & P, (S; W_{1})⟩ ⟹ ⟨(KS; (K; W_{1})), F_{2} & P, S⟩

We leave the rules for recursion to Exercise 6.11. The rules for variables and abstractions thus complete the specification of the single-step transition relation.

c 1 :: ⟨(KS; (K; W)), 1 & P, S⟩ ⟹ ⟨KS, P, (S; W)⟩
c ↑ :: ⟨(KS; (K; W′)), F ↑ & P, S⟩ ⟹ ⟨(KS; K), F & P, S⟩
c lam :: ⟨(KS; K), ΛF & P, S⟩ ⟹ ⟨KS, P, (S; {K, ΛF })⟩

The set of instructions extracted from these rules is

Instructions I ::= F | add1 | branch | mkpair | getfst | getsnd | apply | bind.

We view each of the transition rules for the single-step transition judgment as an axiom. Note that there are no other inference rules for this judgment. A partial computation is defined as a multi-step transition. This is easily defined via the following two inference rules.

\frac{id}{St ⟹^{∗} St}

St ⟹ St′  St′ ⟹^{∗} St′′

step

St ⟹^{∗} St′′

This definition guarantees that the end state of one transition matches the beginning state of the remaining transition sequence. Without the aid of dependent types we would have to define a computation as a list states and ensure externally that the end state of each transition matches the beginning state of the next. This use of dependent types to express complex constraints is one of the reasons why simple lists do not arise very frequently in Elf programming.

Deductions of the judgment St ⟹^{∗} St′ have a very simple form: They all consist of a sequence of single steps terminated by an application of the id rule. We will follow standard practice and use a linear notation for sequences of steps:

St_{1} ⟹ St_{2} ⟹ ⋯ ⟹ St_{n}

Similarly, we will mix multi-step and single-step transitions in sequences, with the obvious meaning. We write C_{1} ∘ C_{2} for the result of appending computations C_{1} and C_{2}. This only makes sense if the final state of C_{1} is the same as the start state of C_{2}. The ∘ operator is associative (see Exercise 6.12).

Recall that a complete computation was defined as a sequence of transitions from an initial state to a final state. The latter is characterized by the program done, and empty environment stack, and a value stack containing exactly one value, namely the result of the computation.

⟨(·; K), F & done, ·⟩ ⟹^{∗} ⟨·, done, (·; W)⟩

run

K ⊢ F ⟹ ⟹^{∗} W

The representation of the abstract machine and the computation judgments present no particular difficulties. We begin with the syntax.

  instruction : type.  %name instruction I
  program     : type.  %name program P
  envstack    : type.  %name envstack Ks
  state       : type.  %name state St

ev     : exp' -> instruction.
add1   : instruction.
branch : instruction.
mkpair : instruction.
getfst : instruction.
getsnd : instruction.
apply  : instruction.
bind   : instruction.
done : program.
&    : instruction -> program -> program.
%infix right 10 &
emptys : envstack.
;;     : envstack -> env -> envstack.
%infix left 10 ;;
st : envstack -> program -> env -> state.

The computation rules are also a straightforward transcription of the rules above. The judgment St ⟹^{∗} St′ is represented by a type St => St' where => is a type family indexed by two states and written in infix notation. We show only three example rules.

=> : state -> state -> type.  %infix none 10 =>
                              %name => R
c_z     : st (Ks ;; K) (ev z' & P) S => st Ks P (S ; z*).
c_app   : st (Ks ;; K) (ev (app' F1 F2) & P) S
           => st (Ks ;; K ;; K) (ev F1 & ev F2 & apply & P) S.
c_apply : st Ks (apply & P) (S ; clo K' (lam' F1') ; W2)
           => st (Ks ;; (K' ; W2)) (ev F1' & P) S.

The multi-step transition is defined by the transcription of its two inference rules. We write ~ in infix notation rather than step since it leads to a concise and readable notation for sequences of computation steps.

=>*  : state -> state -> type.  %infix none 10 =>*
                                %name =>* C
id   : St =>* St.

  ~    : St => St'
      -> St' =>* St''
      -> St =>* St''.
  %infix right 10 ~

Complete computations appeal directly to the multi-step computation judgment. We write ceval K F W for K ⊢ F ⟹ ⟹^{∗} W.

  ceval : env -> exp' -> val -> type.
  run : st (emptys ;; K) (ev F & done) (empty)
        =>* st (emptys) (done) (empty ; W)
         -> ceval K F W.

While this representation is declaratively adequate it has a serious operational defect when used for evaluation, that is, when K and F are given and W is to be determined. The declaration for step (written as ~) solves the innermost subgoal first, that is we reduce the goal of finding a computation C′′ :: St ⟹^{∗} St′′ to finding a state St′ and computation of C′ :: St′ ⟹^{∗} St′′ and only then a single transition R :: St ⟹ St′. This leads to non-termination, since the interpreter is trying to work its way backwards through the space of possible computation sequences. Instead, we can get linear, backtracking-free behavior if we first find the single step R :: St ⟹ St′ and then the remainder of the computation C′ :: St′ ⟹^{∗} St′′. Since there is exactly one rule for any instruction I and id will apply only when the program P is done, finding a computation now becomes a deterministic process. Executable versions of the last two judgments are given below. They differ from the one above only in the order of the recursive calls and it is a simple matter to relate the two versions formally.

  >=>*  : state -> state -> type.  %infix none 10 >=>*
  id<   : St >=>* St.
  <=<   : St >=>* St''
           <- St => St'
           <- St' >=>* St''.
  %infix left 10 <=<
  >ceval : env -> exp' -> val -> type.
  >run   : >ceval K F W
            <- st (emptys ;; K) (ev F & done) (empty)
               >=>* st (emptys) (done) (empty ; W).

This example clearly illustrates that Elf should be thought of a uniform language in which one can express specifications (such as the computations above) and implementations (the operational versions below), but that many specifications will not be executable. This is generally the situation in logic programming languages.

In the informal development it is clear (and not usually separately formulated as a lemma) that computation sequences can be concatenated if the final state of the first computation matches the initial state of the second computation. In the formalization of the proofs below, we will need to explicitly implement a type family that appends computation sequences. It cannot be formulated as a function, since such a function would have to be recursive and is thus not definable in LF.

  append : st Ks P S =>* st Ks' P' S'
            -> st Ks' P' S' =>* st Ks'' P'' S''
            -> st Ks P S =>* st Ks'' P'' S''
            -> type.

The defining clauses are left as Exercise 6.12.

We now return to the task of proving the correctness of the abstract machine. The first lemma states the fundamental motivating property for this model of computation.

Lemma 6.16 Let K be an environment, F an expression, and W a value such that K ⊢ F ↪ W. Then, for any environment stack KS, program P and stack S,

⟨(KS; K), F & P, S⟩ ⟹^{∗} ⟨KS, P, (S; W)⟩

Proof: By induction on the structure of D :: K ⊢ F ↪ W. We will construct a deduction of C :: ⟨(KS; K), F & P, S⟩ ⟹^{∗} ⟨KS, P, (S; W)⟩. The proof is straightforward and we show only two typical cases. The implementation in Elf takes the form of a higher-level judgment subcomp that relates evaluations to computation sequences.

  subcomp : feval K F W
             -> st (Ks ;; K) (ev F & P) S =>* st Ks P (S ; W)
             -> type.

Case: D ends in an application of the rule fev z.

D = \frac{fev z.}{K ⊢ z ↪ z^{∗}}

Then the single-step transition

⟨(KS; K), z & P, S⟩ ⟹^{∗} ⟨KS, P, (S; z^{∗})⟩

satisfies the requirements of the lemma. The clause corresponding to this case:

       sc_z : subcomp (fev_z) (c_z ~ id).

Case: D ends in an application of the fev app rule.

D = \begin{aligned}\begin{matrix}D_{1} \\ K ⊢ F_{1} ↪ \{K′, ΛF′_{1}\} \end{matrix} & \begin{matrix}D_{2} \\ K ⊢ F_{2} ↪ W_{2} \end{matrix} & \begin{matrix}D_{3} \\ K′; W_{2} ⊢ F′_{1} ↪ W \end{matrix} \\ & & fev app \\ K ⊢ F_{1} F_{2} ↪ W \end{aligned}

Then

⟨(KS; K), F_{1} F_{2} & P, S⟩

⟹ ⟨(KS; K; K), F_{1} & F_{2} & apply & P, S⟩  By rule c app
⟹^{∗} ⟨(KS; K), F_{2} & apply & P, (S; {K′, ΛF′_{1}})⟩  By ind. hyp. on D_{1}
⟹^{∗} ⟨KS, apply & P, (S; {K′, ΛF′_{1}}; W_{2})⟩  By ind. hyp. on D_{2}
⟹ ⟨(KS; (K′; W_{2})), F′_{1} & P, S⟩  By rule c apply
⟹^{∗} ⟨KS, P, (S; W)⟩  By ind. hyp. on D_{3}.

The implementation of this case requires the append family defined above. Note how an appeal to the induction hypothesis is represented as a recursive call.

       sc_app : subcomp (fev_app D3 D2 D1) C
                 <- subcomp D1 C1
                 <- subcomp D2 C2
                 <- subcomp D3 C3
                 <- append (c_app ~ C1) C2 C'
                 <- append C' (c_apply ~ C3) C.

□

The first direction of Theorem 6.19 is a special case of this lemma. The other direction is more intricate. The basic problem is to extract a tree-structured evaluation from a linear computation. We must then show that this extraction will always succeed for complete computations. Note that it is obviously not possible to extract evaluations from arbitrary incomplete sequences of transitions of the abstract machine.

In order to write computation sequences more concisely, we introduce some notation. Let R :: St ⟹ St′ and C :: St′ ⟹^{∗} St′′. Then we write

R ∼ C :: St ⟹ St′′

for the computation which begins with R and then proceeds with C. Such a computation exists by the step inference rule. This corresponds directly to the notation in the Elf implementation.

For the proof of the central lemma of this section, we will need a new form of induction often referred to as complete induction. During a proof by complete induction we assume the induction hypothesis not only for the immediate premisses of the last inference rule, but for all proper subderivations. Intuitively, this is justified, since all proper subderivations are “smaller” than the given derivation. For a more formal discussion of the complete induction principle for derivations see Section 6.4. The judgment C < C′ (C is a proper subcomputation of C′) is defined by the following inference rules.

\frac{sub imm}{C < R ∼ C}  \begin{aligned}C < C′ \\ & sub med \\ C < R ∼ C′ \end{aligned}

It is easy to see that the proper subcomputation relation is transitive.

Lemma 6.17 If C_{1} < C_{2} and C_{2} < C_{3} then C_{1} < C_{3}.

Proof: By a simple induction (see Exercise 6.12). □

The implementation of this ordering and the proof of transitivity are immediate.

   < : (st KS1 P1 S1) =>* (st KS P S)
        -> (st KS2 P2 S2) =>* (st KS P S)
        -> type.
  %infix none 8 <
  sub_imm : C < R ~ C.
  sub_med : C < C'
             -> C < R ~ C'.
  trans : C1 < C2 -> C2 < C3 -> C1 < C3 -> type.

The representation of the proof of transitivity is left to Exercise 6.12.

We are now prepared for the lemma that a complete computation with an appropriate initial state can be translated into an evaluation followed by another complete computation.

Lemma 6.18 If

C :: ⟨(KS; K), F & P, S⟩ ⟹^{∗} ⟨·, done, (·; W′)⟩

there exists a value W, an evaluation

D :: K ⊢ F ↪ W,

and a computation

C′ :: ⟨KS, P, (S; W)⟩ ⟹^{∗} ⟨·, done, (·; W′)⟩

such that C′ < C.

Proof: By complete induction on the C. We only show a few cases; the others similar. We use the abbreviation final = ⟨·, done, (·; W′)⟩. The representing type family spl is indexed by four deductions: C, C′, D, and the derivation which shows that C′ < C. In the declaration we need to use the dependent kind constructor in order to name C and C′ so they can be related explicitly.

  spl : {C : (st (KS ;; K) (ev F & P) S)
               =>* (st (emptys) (done) (empty ; W'))}
        feval K F W ->
        {C' : (st KS P (S ; W))
                =>* (st (emptys) (done) (empty ; W'))}
        C' < C -> type.

Case: C begins with c z, that is, C = c z ∼ C_{1}. Then W = z^{∗},

D = \frac{fev z,}{K ⊢ z ↪ z^{∗}}

and C′ = C_{1}. Furthermore, C′ = C_{1} < c z ∼ C_{1} by rule sub imm. The representation in Elf:

       spl_z : spl (c_z ~ C1) (fev_z) C1 (sub_imm).

Case: C begins with c app. Then C = c app ∼ C_{1} where

C_{1} :: ⟨(KS; K; K), F_{1} & F_{2} & apply & P, S⟩ ⟹^{∗} final.

By induction hypothesis on C_{1} there exists a W_{1}, an evaluation

D_{1} :: K ⊢ F_{1} ↪ W_{1}

and a computation

C_{2} :: ⟨(KS; K), F_{2} & apply & P, (S; W_{1})⟩ ⟹^{∗} final

such that C_{2} < C_{1}. We can thus apply the induction hypothesis to C_{2} to obtain a W_{2}, an evaluation

D_{2} :: K ⊢ F_{2} ↪ W_{2}

and a computation

C_{3} :: ⟨KS, apply & P, (S; W_{1}; W_{2})⟩ ⟹^{∗} final

such that C_{3} < C_{2}. By inversion, C_{3} = c apply ∼ C′_{3} and W_{1} = {K′, ΛF′_{1}} where

C′_{3} :: ⟨(KS; (K′; W_{2})), F′_{1} & P, S⟩ ⟹^{∗} final.

Then C′_{3} < C_{3} and by induction hypothesis on C′_{3} there is a value W_{3}, an evaluation

D_{3} :: K′; W_{2} ⊢ F′_{1} ↪ W_{3}

and a compuation

C_{4} :: ⟨KS, P, (S; W_{3})⟩ ⟹^{∗} final.

Now we let W = W_{3} and we construct D :: K ⊢ F_{1} F_{2} ↪ W_{3} by an application of the rule fev app to the premisses D_{1}, D_{2}, and D_{3}. Furthermore we let C′ = C_{4} and conclude by some elementary reasoning concerning the subcomputation relation that C′ < C.

The representation of this subcase of this case requires three explicit appeals to the transitivity of the subcomputation ordering. In order to make this at all intelligible, we use the name C2<C1 (one identifier) for the derivation that C_{2} < C_{1} and similarly for other such derivations.

  spl_app : spl (c_app ~ C1)
             (fev_app D3 D2 D1) C4
             (sub_med C4<C1)
              <- spl C1 D1 C2 C2<C1
              <- spl C2 D2 (c_apply ~ C3') C3<C2
              <- spl C3' D3 C4 C4<C3'
              <- trans C3<C2 C2<C1 C3<C1
              <- trans (sub_imm) C3<C1 C3'<C1
              <- trans C4<C3' C3'<C1 C4<C1.

□

Now we have all the essential lemmas to prove the main theorem.

Theorem 6.19 K ⊢ F ↪ W is derivable iff K ⊢ F ⟹ ⟹^{∗} W is derivable.

Proof: By definition, K ⊢ F ⟹ ⟹^{∗} W iff there is a computation

C :: ⟨(·; K), F & done, ·⟩ ⟹^{∗} ⟨·, done, (·; W)⟩.

One direction follows immediately from Lemma 6.16: if K ⊢ F ↪ W then

⟨(KS; K), F & P, S⟩ ⟹^{∗} ⟨KS, P, (S; W)⟩

for any KS, P, and S and in particular for KS = ·, P = done and S = ·. The implementation of this direction in Elf:

  cev_complete : feval K F W -> ceval K F W -> type.
  cevc : cev_complete D (run C) <- subcomp D C.

For the other direction, assume there is a deduction C of the form shown above. By Lemma 6.18 we know that there exist a W′, an evaluation

D′ :: K ⊢ F ↪ W′

and a computation

C′ :: ⟨·, done, (·; W′)⟩ ⟹^{∗} ⟨·, done, (·; W)⟩

such that C′ < C. Since there is no transition rule for the program done, C′ must be id and W = W′. Thus D = D′ fulfills the requirements of the theorem. This is implemented as follows.

  cls_sound : ceval K F W -> feval K F W -> type.
  clss : cls_sound (run C) D <- spl C D (id) Id<C.

□

6.4 Complete Induction over Computations

Here we briefly justify the principle of complete induction used in the proof of Lemma 6.18. We repeat the definition of proper subcomputations and also define a general subcomputation judgment which will be useful in the proof.

C < C′ C is a proper subcomputation of C′, and
C ≤ C′ C is a subcomputation of C′.

These judgments are defined via the following inference rules.

\frac{sub imm}{C < R ∼ C}  \begin{aligned}C < C′ \\ & sub med \\ C < R ∼ C′ \end{aligned}

\begin{aligned}C < C′ \\ & leq sub \\ C ≤ C′ \end{aligned}  \frac{leq eq}{C ≤ C}

We only need one simple lemma regarding the subcomputation judgment.

Lemma 6.20 If C ≤ C′ is derivable, then C < R ∼ C′ is derivable.

Proof: By analyzing the two possibilities for the deduction of the premiss and constructing an immediate deduction for the conclusion in each case. □

We call a property P of computations complete if it satisfies:

For every C, the assumption that P holds for all C′ < C implies that P holds for C.

Theorem 6.21 (Principle of Complete Induction over Computations) If a property P of computations is complete, then P holds for all computations.

Proof: We assume that P is complete and then prove by ordinary structural induction that for every C and for every C′ ≤ C, P holds of C.

Case: C = id. By inversion, there is no C′ such that C′ < id. Thus P holds for all C′ < id. Since P is complete, this implies that P holds for id.

Case: C = R ∼ C_{1}. The induction hypothesis states that for every C′_{1} ≤ C_{1}, P holds of C′_{1}. We have to show that for every C_{2} ≤ R ∼ C_{1}, property P holds of C_{2}. By inversion, there are two subcases, depending on the evidence for C_{2} ≤ R ∼ C_{1}.

Subcase: C_{2} = R ∼ C_{1}. The induction hypothesis and Lemma 6.20 yield that for every C′_{1} < R ∼ C_{1}, P holds of C_{1}. Since P is complete, P must thus hold for R ∼ C_{1} = C_{2}.

Subcase: C_{2} < R ∼ C_{1}. Then by inversion either C_{1} = C_{2} or C_{1} < C_{2}. In either case C_{2} ≤ C_{1} by one inference. Now we can apply the induction hypothesis to conclude that P holds of C_{2}.

□

6.5 Exercises

Exercise 6.1 If we replace the rule ev app in the natural semantics of Mini-ML (see Section 2.3) by

e_{1} ↪ lam x. e′_{1}  e_{2} ↪ v_{2}  \frac{u}{x ↪ v_{2}} ; ⋮ ; e′_{1} ↪ v
ev app′^{x,u}
e_{1} e_{2} ↪ v

in order to avoid explicitly substituting v_{2} for x, something goes wrong. What is it? Can you suggest a way to fix the problem which still employs hypothetical judgments?

(Note: We assume that the third premiss of the modified rule is parametric in x and hypothetical in u which is discharged as indicated. This implies that we assume that x is not already free in any other hypothesis and that all labels for hypotheses are distinct—so this is not the problem you are asked to detect.)

Exercise 6.2 Define the judgment W RealVal which restricts closures W to Λabstractions. Prove that · ⊢ F ↪ W then W RealVal and represent this proof in Elf.

Exercise 6.3 In this exercise we try to eliminate some of the non-determinism in compilation.

1. Define a judgment F std which should be derivable if the de Bruijn expression F is in the standard form in which the ↑ operator is not applied to applications or abstractions.

2. Rewrite the translation from ordinary expressions e such that only standard forms can be related to any expression e.

3. Prove the property in item 2.

4. Implement the judgments in items 1, 2, and the proof in item 3.

Exercise 6.4 Restrict yourself to the fragment of the language with variables, abstraction, and application, that is,

F ::= 1 | F ↑ | ΛF | F_{1} F_{2}

1. Define a judgment F Closed that is derivable iff the de Bruijn expression F is closed, that is, has no free variables at the object level.

2. Define a judgment for conversion of de Bruijn expressions F to standard form (as in Exercise 6.3, item 1) in a way that preserves meaning (as given by its interpretation as an ordinary expression e).

3. Prove that, under appropriate assumptions, this conversion results in a de Bruijn expression in standard form equivalent to the original expression.

4. Implement the judgments and correctness proofs in Elf.

Exercise 6.5 Restrict yourself to the same fragment as in Exercise 6.4 and define the operation of substitution as a judgment subst F_{1} F_{2} F. It should be a consequence of your definition that if ΛF_{1} represents lamx. e_{1}, F_{2} represents e_{2}, and subst F_{1} F_{2} F is derivable then F should represent [e_{2}/x]e_{1}. Furthermore, such an F should always exist if F_{1} and F_{2} are as indicated. With appropriate assumptions about free variables or indices (see Exercise 6.4) prove these properties, thereby establishing the correctness of your implementation of substitution.

Exercise 6.6 Write out the informal proof of Theorem 6.7.

Exercise 6.7 Prove Theorem 6.8 by appropriately generalizing Lemma 6.2.

Exercise 6.8 Standard ML [MTH90] and many other formulations do not contain a let name construct. Disregarding problems of polymorphic typing for the moment, it is quite simple to simulate let name with let val operationally using so-called thunks. The idea is that we can prohibit the evaluation of an arbitrary expression by wrapping it in a vacuous lam-abstraction. Evaluation can be forced by applying the function to some irrelevant value (we write z, most presentations use a unit element). That is, instead of

l = let name x = e_{1} in e_{2}

we write

l′ = let val x′ = lam y. e_{1} in [x′ z/x]e_{2}

where y is a new variable not free in e_{1}.

1. Show a counterexample to the conjecture “If l is closed, l ↪ v, and l′ ↪ v′ then v = v′ (modulo renaming of bound variables)”.

2. Show a counterexample to the conjecture “▷ l : τ iff ▷ l′ : τ”.

3. Define an appropriate congruence e ≅ e′ such that l ≅ l′ and if e ≅ e′, e ↪ v and e′ ↪ v′ then v ≅ v′.

4. Prove the properties in item 3.

5. Prove that if the values v and v′ are natural numbers, then v ≅ v′ iff v = v′.

We need a property such as the last one to make sure that the congruence we define does not identify all expressions. It is a special case of a so-called observational equivalence (see ??).

Exercise 6.9 The rules for evaluation in Section 6.2 have the drawback that looking up a variable in an environment and evaluation are mutually recursive, since the environment contains unevaluated expressions. Such expressions may be added to the environment during evaluation of a let name or fix construct. In the definition of Standard ML [MTH90] this problem is avoided by disallowing let name (see Exercise 6.8) and by syntactically restricting occurrences of the fix construct.

When translated into our setting, this restriction states that all occurrences of fixpoint expressions must be of the form fix x. lam y. e. Then we can dispense with the environment constructor + and instead introduce a constructor ∗ that builds a recursive environment. More precisely, we have

Environments K ::= · | K; W | K ∗ F

The evaluation rules fev 1+, fev ↑+, and fev fix on page 159 are replaced by

\frac{fev fix∗}{K ⊢ fix′ F ↪ \{K ∗ F, F \}}

\frac{fev 1∗}{K ∗ F ⊢ 1 ↪ \{K ∗ F, F \}}

K ⊢ F ↪ W fev ↑∗

K ∗ F′ ⊢ F ↑ ↪ W

1. Implement this modified evaluation judgment in Elf.

2. Prove that under the restriction that all occurrences of fix′ in de Bruijn expressions have the form fix′ ΛF for some F, the two sets of rules define an equivalent operational semantics. Take care to give a precise definition of the notion of equivalence you are considering and explain why it is appropriate.

3. Represent the equivalence proof in Elf.

4. Exhibit a counterexample which shows that some restriction on fixpoint expressions (as, for example, the one given above) is necessary in order to preserve equivalence.

5. Under the syntactic restriction from above we can also formulate a semantics which requires no new constructor for environments by forming closures over fixpoint expressions. Then we need to add another rule for application of an expression which evaluates to a closure over a fixpoint expression. Write out the rules and prove its equivalence to either the system above or the original evaluation judgment for de Bruijn expressions (under the appropriate restriction).

Exercise 6.10 Show how the effect of the bind instruction can be simulated in the CLS machine using the other instructions. Sketch the correctness proof for this simulation.

Exercise 6.11 Complete the presentation of the CLS machine by adding recursion. In particular 6.5. EXERCISES 183

1. Complete the computation rules on page 169.

2. Add appropriate cases to the proofs of Lemmas 6.16, and 6.18. Exercise 6.12 Prove the following carefully.

1. The concatenation operation “∘” on computations is associative.

2. The subcomputation relation “<” is transitive (Lemma 6.17). Show the implementation of your proofs as type families in Elf. Exercise 6.13 The machine instructions from Section 6.3 can simply quote expressions in de Bruijn form and consider them as instructions. As a next step in the (abstract) compilation process, we can convert the expressions to lower-level code which simulates the effect of instructions on the environment and value stacks in smaller steps.

1. Design an appropriate language of operations.

2. Specify and implement a compiler from expressions to code.

3. Prove the correctness of this step of compilation.

4. Implement your correctness proof in Elf.