On this page:
17.1 Functions in their most general form
17.2 Long Live Lambda!
17.3 Lambda is Dead!
17.4 Defunctionalization at work
17.5 Compiling Loot
17.6 Compiling Function Definitions
17.7 Save the Environment:   Create a Closure!
17.8 Calling Functions
17.9 Recursive Functions
17.10 Syntactic sugar for function definitions
7.9

17 Loot: lambda the ultimate

    17.1 Functions in their most general form

    17.2 Long Live Lambda!

    17.3 Lambda is Dead!

    17.4 Defunctionalization at work

    17.5 Compiling Loot

    17.6 Compiling Function Definitions

    17.7 Save the Environment: Create a Closure!

    17.8 Calling Functions

    17.9 Recursive Functions

    17.10 Syntactic sugar for function definitions

17.1 Functions in their most general form

We’ve been building up the pieces of functions, first with second-class functions, then with tail-calls, then with first-class function pointers.

Now we’re ready to deal with functions in their most general form: λ-expressions.

We add λ-expressions to the syntax and remove the (fun ,Variable) and (call ,Expr ,@(Listof Expr)) forms. We no longer need a separate syntactic form for referencing the name of a function, we can just use variable binding. Likewise, we use the same syntax as Racket for function application:

;; type Expr =

;; | ....

;; | Lam Name (Listof Variable) Expr

;; | App Expr (Listof Expr)

Two things to note: for now you can ignore the Name parameter, and Formals can be defined as a list of variables:

;; type Formals = (Listof Variable)

But it’s possible to extend the λ-notation to include the ability to define variable-arity functions, as you will see in Assignment 6: Arities!.

17.2 Long Live Lambda!

Let’s start by developing the interpreter for Loot, where the relevant forms are λs and applications:

; Expr REnv -> Answer
(define (interp-env e r)
    ; ...
    [(Lam _ xs e)  '...]
    [(App e es)  '...])

These two parts of the interpreter must fit together: λ is the constructor for functions and application is deconstructor. An application will evaluate all its subexpressions and the value produced by e ought to be the kind of value constructed by λ. That value needs to include all the necessary information to, if given the values of the arguments es, evaluate the body of the function in an environment associating the parameter names with the arguments’ values.

So how should functions be represented? Here is a simple idea following the pattern we’ve used frequently in the interpreter:

So now:
  • Q: How can we represent functions?

  • A: With functions!?

Great, so we will use function to represent functions. We can implement function application with function application. Let’s fill in what we know so far:

; Expr REnv -> Answer
(define (interp-env e r)
    ; ...
    [(Lam _ xs e)
     (λ ??? '...)]
    [(App e es)
     (let ((f (interp-eval e r))
           (vs (interp-eval* es r)))
       (apply f vs))])

It’s not totally clear what parameters the representation of a function should have or what we should in the body of that function. However, the code in the interpretation of an application sheds light on both. First, it’s clear a function should potentially take any number of arguments:

; Expr REnv -> Answer
(define (interp-env e r)
    ; ...
    [(Lam _ xs e)
     (λ vs '...)]
    [(App e es)
     (let ((f (interp-eval e r))
           (vs (interp-eval* es r)))
       (apply f vs))])

Second, what should happen when a function is applied? It should produce the answer produced by the body of the λ expression in an environment that associates xs with vs. Translating that to code, we get:

; Expr REnv -> Answer
(define (interp-env e r)
    ; ...
    [(Lam _ xs e)
     (λ vs (interp-env e (zip xs vs)))]
    [(App e es)
     (let ((f (interp-eval e r))
           (vs (interp-eval* es r)))
       (apply f vs))])

And now we have simultaneously arrived at our representation of function values:
; type Value =
; | ....
; | (Value ... -> Answer)

and completed the implementation of the interpreter.

There are, however, problems.

For one, this approach does not model how λ-expressions are able to capture the environment in which they are evaluated. Consider:

(let ((y 8))
  (λ (x) (+ x y)))

This evaluates to a function that, when applied, should add 8 to its argument. It does so by evaluating the body of the λ, but in an environment that both associates x with the value of the argument, but also associates y with 8. That association comes from the environment in place when the λ-expression is evaluated. The interpreter as written will consider y is unbound!

The solution is easy: in order for (Loot) functions to capture their (implicit) environment, we should capture the (explicit) environment in the (Racket) function:

; Expr REnv -> Answer
(define (interp-env e r)
    ; ...
    [(Lam _ xs e)
     (λ (vs) (interp-env e (append (zip xs vs) r)))]
    [(App e es)
     (let ((f (interp-eval e r))
           (vs (interp-eval* es r)))
       (apply f vs))])

The last remaining issue is we should do some type and arity-checking:

; Expr REnv -> Answer
(define (interp-env e r)
    ; ...
    [(Lam _ xs e)
     (λ (vs)
       (if (= (length xs) (length vs))
           (interp-env e (append (zip xs vs) r))
           'err))]
    [(App e es)
     (let ((f (interp-eval e r))
           (vs (interp-eval* es r)))
       (if (procedure? f)
           (apply f vs)
           'err))])

The complete interpreter is:

loot/interp.rkt

  #lang racket
  (provide interp interp-env interp-prim1)
  (require "ast.rkt"
           "env.rkt"
           "interp-prims.rkt")
   
  ;; type Answer = Value | 'err
   
  ;; type Value =
  ;; | Integer
  ;; | Boolean
  ;; | Character
  ;; | (Fun f)
  ;; | Eof
  ;; | Void
  ;; | '()
  ;; | (cons Value Value)
  ;; | (box Value)
   
  ;; type REnv = (Listof (List Id Value))
  ;; type Defns = (Listof Defn)
   
  ;; Prog Defns -> Answer
  (define (interp p)
    (interp-env (desugar p) '()))
   
  ;; Expr Env Defns -> Answer
  (define (interp-env e r)
    (match e
      [(Prog '() e) (interp-env e r)]
      [(Int i)  i]
      [(Bool b) b]
      [(Char c) c]
      [(Eof)    eof]
      [(Empty)  '()]
      [(Var x)  (lookup r x)]
      [(Prim0 'void) (void)]
      [(Prim0 'read-byte) (read-byte)]
      [(Prim0 'peek-byte) (peek-byte)]
      [(Prim1 p e)
       (match (interp-env e r)
         ['err 'err]
         [v (interp-prim1 p v)])]
      [(Prim2 p e1 e2)
       (match (interp-env e1 r)
         ['err 'err]
         [v1 (match (interp-env e2 r)
               ['err 'err]
               [v2 (interp-prim2 p v1 v2)])])]
      [(If p e1 e2)
       (match (interp-env p r)
         ['err 'err]
         [v
          (if v
              (interp-env e1 r)
              (interp-env e2 r))])]
      [(Begin e1 e2)
       (match (interp-env e1 r)
         ['err 'err]
         [_ (interp-env e2 r)])]
      [(Let x e1 e2)
       (match (interp-env e1 r)
         ['err 'err]
         [v (interp-env e2 (ext r x v))])]
      [(LetRec bs e)
       (letrec ((r* (λ ()
                      (append
                       (zip (map car bs)
                            ;; η-expansion to delay evaluating r*
                            ;; relies on RHSs being functions
                            (map (λ (l) (λ vs (apply (interp-env l (r*)) vs)))
                                 (map cadr bs)))
                       r))))
         (interp-env e (r*)))]
      [(Lam _ xs e1)
          (lambda vs
            (if (= (length vs) (length xs))
                (interp-env e1 (append (zip xs vs) r))
                'err))]
      [(App f es)
       (match (interp-env* (cons f es) r)
        [(list f vs ...)
         (if (procedure? f)
             (apply f vs)
             'err)])]
      [_         'err]))
   
  ;; (Listof Expr) REnv Defns -> (Listof Value) | 'err
  (define (interp-env* es r)
    (match es
      ['() '()]
      [(cons e es)
        (match (interp-env e r)
         ['err 'err]
         [v (cons v (interp-env* es r))])]))
   
  (define (zip xs ys)
    (match* (xs ys)
      [('() '()) '()]
      [((cons x xs) (cons y ys))
       (cons (list x y)
             (zip xs ys))]))
   

We now have the full power of λ expressions in our language. We can write recursive functions, using only anonymous functions, via the Y-combinator:

Examples

> (interp (parse
    '(λ (t)
       ((λ (f) (t (λ (z) ((f f) z))))
        (λ (f) (t (λ (z) ((f f) z))))))))

#<procedure:...ngs/loot/interp.rkt:76:8>

For example, computing the triangular function applied to 10:

Examples

> (interp (parse
    '(((λ (t)
         ((λ (f) (t (λ (z) ((f f) z))))
          (λ (f) (t (λ (z) ((f f) z))))))
       (λ (tri)
         (λ (n)
           (if (zero? n)
               1
               (+ n (tri (sub1 n)))))))
      10)))

56

One of the niceties of using Racket functions to represent Loot functions is we can define Racket functions via the interpretation of Loot functions:

Examples

> (define Y
    (interp (parse
      '(λ (t)
         ((λ (f) (t (λ (z) ((f f) z))))
          (λ (f) (t (λ (z) ((f f) z)))))))))
> (define tri
    (interp (parse '(λ (tri)
                      (λ (n)
                         (if (zero? n)
                             1
                             (+ n (tri (sub1 n)))))))))

And then use them from within Racket:

Examples

> ((Y tri) 10)

56

We can also “import” Racket functions in to Loot:

Examples

> (interp-env (parse '(expt 2 10))
              `((expt ,expt)))

1024

17.3 Lambda is Dead!

Now the question you might naturally wonder is: how does implementing functions in terms of functions help me implement functions in x86, which after all, doesn’t have λ?

The answer is that from this point, in which we have an understandable account of functions, we can iteratively revise the interpreter to eliminate the use of functions while computing equivalent results. Doing so will shed light on the lower-level implementation of functions in the compiler.

Consider again what it is that a λ-expression is doing for you:

We can achive these things without using a function value by:

So we are changing the representation of functions from:

And now we have simultaneously arrived at our representation of function values:
; type Value =
; | ....
; | (Value ... -> Answer)

To:

; type Value =
; | ....
; | Closure Formals Expr Env

When a λ is evaluated, a closure is created. When a function is applied, we deconstruct the closure and execute the code that used to be in the (Racket) function:

; Expr REnv -> Answer
(define (interp-env e r)
    ; ...
    [(Lam _ xs e)
     (Closure xs e r)]
    [(App e es)
     (let ((f (interp-eval e r))
           (vs (interp-eval* es r)))
       (match f
         [(Closure xs e r)
          (if (= (length vs) (length xs))
              (interp-env e (append (zip xs vs) r))
              'err)]
         [_ 'err]))])

We can give it a try:

Examples

> (interp (parse '(λ (x) x)))

'#s(Closure (x) #s(Lam lam7242 (x) #s(Var x)) ())

> (interp (parse '((λ (x) (λ (y) x)) 8)))

'#s(Closure (x) #s(Lam lam7243 (x) #s(Lam lam7244 (y) #s(Var x))) ((x 8)))

Notice in the second example how the closure contains the body of the function and the environment mapping the free variable 'x to 8.

We can also confirm our larger example works:

Examples

> (interp (parse
    '(((λ (t)
         ((λ (f) (t (λ (z) ((f f) z))))
          (λ (f) (t (λ (z) ((f f) z))))))
       (λ (tri)
         (λ (n)
           (if (zero? n)
               1
               (+ n (tri (sub1 n)))))))
      10)))

'#s(Closure (t) #s(Lam lam7245 (t) #s(App #s(Lam lam7246 (f) #s(App #s(Var t) (#s(Lam lam7247 (z) #s(App #s(App #s(Var f) (#s(Var f))) (#s(Var z))))))) (#s(Lam lam7248 (f) #s(App #s(Var t) (#s(Lam lam7249 (z) #s(App #s(App #s(Var f) (#s(Var f))) (#s(Var z)))))))))) ((t 10) (t #s(Closure (tri) #s(Lam lam7250 (tri) #s(Lam lam7251 (n) #s(If #s(Prim1 zero? #s(Var n)) #s(Int 1) #s(Prim2 + #s(Var n) #s(App #s(Var tri) (#s(Prim1 sub1 #s(Var n)))))))) ()))))

While can’t apply the interpretation of functions in Racket like we did previously, we can apply-function the interpretation of functions:

Examples

> (define Y
    (interp (parse
      '(λ (t)
         ((λ (f) (t (λ (z) ((f f) z))))
          (λ (f) (t (λ (z) ((f f) z)))))))))
> (define tri
    (interp (parse
             '(λ (tri)
               (λ (n)
                 (if (zero? n)
                     1
                     (+ n (tri (sub1 n)))))))))
> (apply-function (apply-function Y tri) 10)

'#s(Closure (t) #s(Lam lam7252 (t) #s(App #s(Lam lam7253 (f) #s(App #s(Var t) (#s(Lam lam7254 (z) #s(App #s(App #s(Var f) (#s(Var f))) (#s(Var z))))))) (#s(Lam lam7255 (f) #s(App #s(Var t) (#s(Lam lam7256 (z) #s(App #s(App #s(Var f) (#s(Var f))) (#s(Var z)))))))))) ((t 10) (t #s(Closure (tri) #s(Lam lam7257 (tri) #s(Lam lam7258 (n) #s(If #s(Prim1 zero? #s(Var n)) #s(Int 1) #s(Prim2 + #s(Var n) #s(App #s(Var tri) (#s(Prim1 sub1 #s(Var n)))))))) ()))))

The process we used to eliminate function values from the interpreter is an instance of a general-purpose whole-program transformation called defunctionalization for replacing function values with data structures.

17.4 Defunctionalization at work

Let’s digress for a moment and learn this very useful transformation.

Here is a data type for representing regular expressions:

; type Regexp =
; | 'zero
; | 'one
; | (char ,Char)
; | (times ,Regexp ,Regexp)
; | (plus ,Regexp ,Regexp)
; | (star ,Regexp)

The regular expression 'zero matches nothing; 'one matches the empty string; `(char ,c) matches the character c; `(times ,r1 ,r2) matches the concatenation of a string matching r1 followed by a string matching r2; `(plus ,r1 ,r2) matching either a string matching r1 or a string matching r2; and `(star ,r) matches a string made up of any number of substrings, each of which match r.

A really nice way to write a matcher is to use a continuation-passing style that keeps track of what is required of the remainder of the string after matching a prefix against the regexp:

loot/regexp.rkt

  #lang racket
  (provide accepts)
   
  ;; type Regexp =
  ;; | 'zero
  ;; | 'one
  ;; | `(char ,Char)
  ;; | `(times ,Regexp ,Regexp)
  ;; | `(plus ,Regexp ,Regexp)
  ;; | `(star ,Regexp)
   
  ;; Regexp String -> Boolean
  (define (accepts r s)
    (matcher r (string->list s) (λ (cs) (empty? cs))))
   
  ;; Regexp (Listof Char) ((Listof Char) -> Bool) -> Bool
  (define (matcher r cs k)
    (match r
      ['zero #f]
      ['one (k cs)]
      [`(char ,c)
       (match cs
         ['() #f]
         [(cons d cs) (and (char=? c d) (k cs))])]
      [`(plus ,r1 ,r2)
       (or (matcher r1 cs k) (matcher r2 cs k))]
      [`(times ,r1 ,r2)
       (matcher r1 cs (λ (cs) (matcher r2 cs k)))]
      [`(star ,r)
       (letrec ((matcher* (λ (cs) (or (k cs) (matcher r cs matcher*)))))
         (matcher* cs))]))
   

Let’s give it a try:

Examples

> (accepts `(star (char #\a)) "aaaaa")

#t

> (accepts `(star (char #\a)) "aaaab")

#f

> (accepts `(star (plus (char #\a) (char #\b))) "aaaab")

#t

But what if needed to program this regular expression matching without the use of function values? We can arrive at such code systematically by applying defunctionalization.

loot/regexp-defun.rkt

  #lang racket
  (provide accepts)
   
  ;; type Regexp =
  ;; | 'zero
  ;; | 'one
  ;; | `(char ,Char)
  ;; | `(times ,Regexp ,Regexp)
  ;; | `(plus ,Regexp ,Regexp)
  ;; | `(star ,Regexp)
   
  ;; type K =
  ;; | '(k0)
  ;; | `(k1 ,Regexp ,K)
  ;; | `(k2 ,K ,Regexp)
   
  ;; Regexp String -> Boolean
  (define (accepts r s)
    (matcher r (string->list s) '(k0)))
   
  ;; Regexp (Listof Char) K -> Bool
  (define (matcher r cs k)
    (match r
      ['zero #f]
      ['one (apply-k k cs)]
      [`(char ,c)
       (match cs
         ['() #f]
         [(cons d cs)
          (and (char=? c d) (apply-k k cs))])]
      [`(plus ,r1 ,r2)
       (or (matcher r1 cs k) (matcher r2 cs k))]
      [`(times ,r1 ,r2)
       (matcher r1 cs `(k1 ,r2 ,k))]
      [`(star ,r)
       (apply-k `(k2 ,k ,r) cs)]))
   
  ;; K (Listof Char) -> Bool
  (define (apply-k k cs)
    (match k
      [`(k0) (empty? cs)]
      [`(k1 ,r2 ,k) (matcher r2 cs k)]
      [`(k2 ,k* ,r) (or (apply-k k* cs) (matcher r cs k))]))
   

And we get the same results:

Examples

> (accepts `(star (char #\a)) "aaaaa")

#t

> (accepts `(star (char #\a)) "aaaab")

#f

> (accepts `(star (plus (char #\a) (char #\b))) "aaaab")

#t

17.5 Compiling Loot

Compiling a λ-expression will involve generating two different chunks of instructions:

17.6 Compiling Function Definitions

The first part closely follows the appoach of defining a function definition (define (f x ...) e) from our previous compilers.

Ther are two important differences from the past though:

To deal with the first issue, we first make a pass over the program inserting computed names for each λ-expression.

This is the reason for the Name field in the Lam constructor.

; type Expr =
; ....
; | Lam Name [Variable] Expr

Now λ-expressions have the form like (Lam 'fred '(x) (+ x x)). The symbol 'fred here is used to give a name to the λ-expression.

The first step of the compiler will be to label every λ-expression using the following function:

; Expr -> Expr
(define (label-λ e)
  (match e
    [(Prog ds e)     (Prog (map label-λ ds) (label-λ e))]
    [(Defn f xs e)   (Defn f xs (label-λ e))]
    [(Prim1 p e)     (Prim1 p (label-λ e))]
    [(Prim2 p e1 e2) (Prim2 p (label-λ e1) (label-λ e2))]
    [(If e1 e2 e3)   (If (label-λ e1) (label-λ e2) (label-λ e3))]
    [(Begin e1 e2)   (Begin (label-λ e1) (label-λ e2))]
    [(Let x e1 e2)   (Let x (label-λ e1) (label-λ e2))]
    [(LetRec bs e1)  (LetRec (map (lambda (xs) (map label-λ xs)) bs) (label-λ e1))]
    [(Lam '() xs e)  (Lam (gensym 'lam) xs (label-λ e))]
    [(Lam n xs e)    (Lam (gensym n) xs (label-λ e))]
    [(App f es)      (App (label-λ f) (map label-λ es))]
    [_               e]))

Here it is at work:

Examples

> (label-λ (parse
    '(λ (t)
      ((λ (f) (t (λ (z) ((f f) z))))
       (λ (f) (t (λ (z) ((f f) z))))))))

'#s(Prog () #s(Lam lam7263 (t) #s(App #s(Lam lam7264 (f) #s(App #s(Var t) (#s(Lam lam7265 (z) #s(App #s(App #s(Var f) (#s(Var f))) (#s(Var z))))))) (#s(Lam lam7266 (f) #s(App #s(Var t) (#s(Lam lam7267 (z) #s(App #s(App #s(Var f) (#s(Var f))) (#s(Var z)))))))))))

Now turning to the second issue–λ-expression may reference variables bound outside of the expression—let’s consider how to compile something like (λ (x) z)?

There are many possible solutions, but perhaps the simplest is to compile this as a function that takes two arguments, i.e. compile it as if it were: (λ (x z) z). The idea is that a λ-expression defines a function of both explicit arguments (the parameters) and implicit arguments (the free variables of the λ-expression).

This will have to work in concert with closure creation and function calls. When the λ-expression is evaluated, a closure will be created storing the value of z. When the function is applied, the caller will need to retrieve that value and place it as the second argument on stack before calling the function’s code.

To implement this, we will need to compute the free variables, which we do with the following function:

; Expr -> (Listof Variable)
(define (fvs e)
  (define (fvs e)
    (match e
      [(Prim1 p e)     (fvs e)]
      [(Prim2 p e1 e2) (append (fvs e1) (fvs e2))]
      [(If e1 e2 e3)   (append (fvs e1) (fvs e2) (fvs e3))]
      [(Begin e1 e2)   (append (fvs e1) (fvs e2))]
      [(Let x e1 e2)   (append (fvs e1) (remq* (list x) (fvs e2)))]
      [(LetRec bs e1)  (let ((bound (map car bs))
                             (def-fvs (append-map fvs-bind bs)))
                            (remq* bound (append def-fvs (fvs e1))))]
      [(Lam n xs e1)   (remq* xs (fvs e1))]
      [(Var x)         (list x)]
      [(App f es)      (append (fvs f) (append-map fvs es))]
      [_               '()]))
  (remove-duplicates (fvs e)))

We can now write the function that compiles a labelled λ-expression into a function in assembly:

; Lambda -> Asm
(define (compile-λ-definition l)
  (match l
    [(Lam '() xs e) (error "Lambdas must be labelled before code-gen")]
    [(Lam f xs e)
     (let* ((free (remq* xs (fvs e)))
            ; leave space for RIP
            (env (parity (cons #f (cons #f (reverse (append xs free)))))))
           (seq
             (Label (symbol->label f))
             ; we need the #args on the frame, not the length of the entire
             ; env (which may have padding)
             ; Ignore tail calls for now
             (compile-e e env)
             (Ret)))]))

Here’s what’s emitted for a λ-expression with a free variable:

Examples

> (compile-λ-definition (Lam 'f '(x) (Var 'z)))

(list (Label 'label_f_5e96933745) (%% "Compiling the body of the function") (%% "free vars: (z)") (%% "args: (x)") (%% "env: (#f #f z x #f)") (Mov 'rax (Offset 'rsp 16)) (Ret))

Notice that it’s identical to a λ-expression with an added parameter and no free variables:

Examples

> (compile-λ-definition (Lam 'f '(x z) (Var 'z)))

(list (Label 'label_f_5e96933745) (%% "Compiling the body of the function") (%% "free vars: ()") (%% "args: (x z)") (%% "env: (#f #f z x #f)") (Mov 'rax (Offset 'rsp 16)) (Ret))

The compiler will need to generate one such function for each λ-expression in the program. So we use a helper function for extracting all the λ-expressions and another for compiling each of them:

; LExpr -> (Listof LExpr)
; Extract all the lambda expressions
(define (λs e)
  (match e
    [(Prog ds e)     (append (append-map λs ds) (λs e))]
    [(Defn f xs e)   (λs e)]
    [(Prim1 p e)     (λs e)]
    [(Prim2 p e1 e2) (append (λs e1) (λs e2))]
    [(If e1 e2 e3)   (append (λs e1) (λs e2) (λs e3))]
    [(Begin e1 e2)   (append (λs e1) (λs e2))]
    [(Let x e1 e2)   (append (λs e1) (λs e2))]
    [(LetRec bs e1)  (append (append-map lambda-defs bs) (λs e1))]
    [(Lam n xs e1)   (cons e (λs e1))]
    [(App f es)      (append (λs f) (append-map λs es))]
    [_               '()]))
 
; [Lam] -> Asm
(define (compile-λ-definitions ds)
  (seq
    (match ds
      ['() (seq)]
      [(cons d ds)
       (seq (compile-λ-definition d)
            (compile-λ-definitions ds))])))

The top-level compile function now labels inserts labels and compiles all the λ-expressions to functions:

; Prog -> Asm
(define (compile p)
  (match (label-λ (desugar p))
    [(Prog '() e)
     (prog (Extern 'peek_byte)
           (Extern 'read_byte)
           (Extern 'write_byte)
           (Extern 'raise_error)
           (Label 'entry)
           (Mov rbx rdi)
           (compile-e e '(#f))
           (Mov rdx rbx)
           (Ret)
           (compile-λ-definitions (λs e)))]))

What remains is the issue of compiling λ-expressions to code to create a closure.

17.7 Save the Environment: Create a Closure!

We’ve already seen how to create a reference to a function pointer, enabling functions to be first-class values that can be passed around, returned from other functions, stored in data structures, etc. The basic idea was to allocate a location in memory and save the address of a function label there.

A closure is just this, plus the environment that needs to be restored with the function is called. So representing a closure is fairly straightforward: we will allocate a location in memory and save the function label, plus each value that is needed from the environment. In order to keep track of how many values there are, we’ll also store the length of the environment.

Here’s the function for emitting closure construction code:

; (Listof Variable) Label (Listof Variable) CEnv -> Asm
(define (compile-λ xs f ys c)
  (seq
    ; Save label address
    (Lea rax (symbol->label f))
    (Mov (Offset rbx 0) rax)
 
    ; Save the environment
    (%% "Begin saving the env")
    (Mov r8 (length ys))
 
    (Mov (Offset rbx 8) r8)
    (Mov r9 rbx)
    (Add r9 16)
    (copy-env-to-heap ys c 0)
    (%% "end saving the env")
 
    ; Return a pointer to the closure
    (Mov rax rbx)
    (Or rax type-proc)
    (Add rbx (* 8 (+ 2 (length ys))))))

Compared the previous code we say for function pointer references, the only difference is the code to store the length and value of the free variables of the λ-expression. Also: the amount of memory allocated is no longer just a single cell, but depends on the number of free variables being closed over.

The copy-env-to-heap function generates instructions for dereferencing variables and copying them to the appropriate memory location where the closure is stored:

; (Listof Variable) CEnv Natural -> Asm
; Pointer to beginning of environment in r9
(define (copy-env-to-heap fvs c i)
  (match fvs
    ['() (seq)]
    [(cons x fvs)
     (seq
       ; Move the stack item  in question to a temp register
       (Mov r8 (Offset rsp (lookup x c)))
 
       ; Put the iterm in the heap
       (Mov (Offset r9 i) r8)
 
       ; Do it again for the rest of the items, incrementing how
       ; far away from r9 the next item should be
       (copy-env-to-heap fvs c (+ 8 i)))]))

That’s all there is to closure construction!

17.8 Calling Functions

The last final peice of the puzzle is making function calls and closures work together. Remember that a λ-expression is compiled into a function that expects two sets of arguments on the stack: the first are the explicit arguments that given at the call site; the other arguments are the implicit arguments corresponding to free variables the λ-expression being called. The value of these arguments are given by the environment saved in the closure of the λ-expressions.

So the code generated for a function call needs to manage running each subexpression, the first of which should evaluate to a function (a pointer to a closure). The arguments are saved on the stack, and then the values stored in the environment part of the closure need to be copied from the heap to the stack:

; Expr (Listof Expr) CEnv -> Asm
(define (compile-call f es c)
  (let* ((cnt (length es))
         (aligned (even? (+ cnt (length c))))
         (i (if aligned 1 2))
         (c+ (if aligned
                 c
                 (cons #f c)))
         (c++ (cons #f c+)))
    (seq
 
      (%% "Begin compile-call")
      ; Adjust the stack for alignment, if necessary
      (if aligned
          (seq)
          (Sub rsp 8))
 
      ; Generate the code for the thing being called
      ; and push the result on the stack
      (compile-e f c+)
      (%% "Push function on stack")
      (Push rax)
 
      ; Generate the code for the arguments
      ; all results will be put on the stack (compile-es does this)
      (compile-es es c++)
 
      ; Get the function being called off the stack
      ; Ensure it's a proc and remove the tag
      ; Remember it points to the _closure_
      (%% "Get function off stack")
      (Mov rax (Offset rsp (* 8 cnt)))
      (assert-proc rax)
      (Xor rax type-proc)
 
      (%% "Get closure env")
      (copy-closure-env-to-stack)
      (%% "finish closure env")
 
      ; get the size of the env and save it on the stack
      (Mov rcx (Offset rax 8))
      (Push rcx)
 
      ; Actually call the function
      (Call (Offset rax 0))
 
      ; Get the size of the env off the stack
      (Pop rcx)
      (Sal rcx 3)
 
      ; pop args
      ; First the number of arguments + alignment + the closure
      ; then captured values
      (Add rsp (* 8 (+ i cnt)))
      (Add rsp rcx))))

The main aspect involving lambdas is copy-closure-env-to-stack. Unlike the closure construction code, in which we statically know what and how many variables to save in a closure, we must dynamically loop over the environment to move values to the stack:

; -> Asm
; Copy closure's (in rax) env to stack in rcx
(define (copy-closure-env-to-stack)
  (let ((copy-loop (symbol->label (gensym 'copy_closure)))
        (copy-done (symbol->label (gensym 'copy_done))))
    (seq
 
      (Mov r8 (Offset rax 8)) ; length
      (Mov r9 rax)
      (Add r9 16)             ; start of env
      (Label copy-loop)
      (Cmp r8 0)
      (Je copy-done)
      (Mov rcx (Offset r9 0))
      (Push rcx)              ; Move val onto stack
      (Sub r8 1)
      (Add r9 8)
      (Jmp copy-loop)
      (Label copy-done))))

Let’s try it out:

Examples

> (asm-interp (compile (parse '((let ((x 8)) (λ (y) x)) 2))))

'(#<cpointer> . 128)

> (asm-interp (compile (parse '(((λ (x) (λ (y) x)) 8) 2))))

'(#<cpointer> . 128)

> (asm-interp (compile (parse '((λ (f) (f (f 0))) (λ (x) (add1 x))))))

'(#<cpointer> . 32)

17.9 Recursive Functions

Writing recursive programs with the Y-combinator is a bit inconvenient. Let us now add a recursive function binding construct: letrec.

A letrec-expression has a shape like a let-expression, but variables are bound in both the body and the right-hand-side of the letrec. To keep matters simple, we will assume the right-hand-sides of a letrec are all λ-expressions. (Racket eases this restriction, but it significantly complicates compilation.)

So for example, writing the even? and odd? functions using letrec looks like:

Examples

> (letrec ((even?
            (λ (x)
              (if (zero? x)
                  #t
                  (odd? (sub1 x)))))
           (odd?
            (λ (x)
              (if (zero? x)
                  #f
                  (even? (sub1 x))))))
    (even? 10))

#t

To compile a letrec-expression, we can compile the λ-expression as functions just as before. Notice that the recursive (or mutually recursive) occurrence will be considered a free variable within the λ-expression, so just like any other free variable, the closure creation should capture the value of this binding.

We need to extend the syntax functions for computing free variables, extracting λ-expressions, and so on. All of this is straightforward.

The key complication to compiling a letrec-expression is that the name of a function should be bound—to itself—within the body of the function. The key insight into achieving this is to first allocate closures, but to delay the actual population of the closures’ environments.

The way that compiling a letrec-expression works is roughly:

The compile-letrec function takes a list of variables to bind, the right-hand-side λ-expressions, body, and compile-time environment. It relies on three helper functions to handle the tasks listed above:

; (Listof Variable) (Listof Lambda) Expr CEnv -> Asm
(define (compile-letrec fs ls e c)
  (seq
    (compile-letrec-λs ls c)
    (compile-letrec-init fs ls (append (reverse fs) c))
    (compile-e e (append (reverse fs) c))
    (Add rsp (* 8 (length fs)))))

The first two tasks are taken care of by compile-letrec-λs, which allocates unitialized closures and pushes each on the stack.

; (Listof Lambda) CEnv -> Asm
; Create a bunch of uninitialized closures and push them on the stack
(define (compile-letrec-λs ls c)
  (match ls
    ['() (seq)]
    [(cons l ls)
     (match l
       [(Lam lab as body)
        (let ((ys (fvs l)))
             (seq
               (Lea rax (Offset (symbol->label lab) 0))
               (Mov (Offset rbx 0) rax)
               (Mov rax (length ys))
               (Mov (Offset rbx 8) rax)
               (Mov rax rbx)
               (Or rax type-proc)
               (Add rbx (* 8 (+ 2 (length ys))))
               (Push rax)
               (compile-letrec-λs ls (cons #f c))))])]))

The compile-letrec-init goes through each function and initializes its closure now that all of the function pointers are available. Finally the body is compiled in an extended environment.

; (Listof Variable) (Listof Lambda) CEnv -> Asm
(define (compile-letrec-init fs ls c)
  (match fs
    ['() (seq)]
    [(cons f fs)
     (let ((ys (fvs (first ls))))
          (seq
            (Mov r9 (Offset rsp (lookup f c)))
            (Xor r9 type-proc)
            (Add r9 16) ; move past label and length
            (copy-env-to-heap ys c 0)
            (compile-letrec-init fs (rest ls) c)))]))

We can give a spin:

Examples

> (asm-interp (compile (parse
                        '(letrec ((even?
                                  (λ (x)
                                    (if (zero? x)
                                        #t
                                        (odd? (sub1 x)))))
                                 (odd?
                                  (λ (x)
                                    (if (zero? x)
                                        #f
                                        (even? (sub1 x))))))
                          (even? 10)))))

'(#<cpointer> . 24)

> (asm-interp
    (compile (parse
      '(letrec ((map (λ (f ls)
                      (letrec ((mapper (λ (ls)
                                         (if (empty? ls)
                                           '()
                                           (cons (f (car ls)) (mapper (cdr ls)))))))
                        (mapper ls)))))
        (map (λ (f) (f 0))
             (cons (λ (x) (add1 x))
                   (cons (λ (x) (sub1 x))
                         '())))))))

'(#<cpointer> . 45030210)

17.10 Syntactic sugar for function definitions

The letrec form is a generlization of the (begin (define (f x ...) e) ... e0) form we started with when we first started looking at adding functions to the language. To fully subsume the language of Iniquity, we can add this form back in to the language as syntactic sugar for letrec, i.e. we can eliminate this form from programs by rewriting them.

Let Expr+ refer to programs containing (begin (define (f x ...) e) ... e0). The desugar function writes Expr+s into Exprs.

(define (desugar e+)
  (match e+
    [(Prog '() e)    (Prog '() (desugar e))]
    [(Prog ds e)     (let ((defs (map desugar ds)))
                          (Prog '() (LetRec defs e)))]
    [(Defn f xs e)   (list f (Lam f xs e))]
    [(Prim1 p e)     (Prim1 p (desugar e))]
    [(Prim2 p e1 e2) (Prim2 p (desugar e1) (desugar e2))]
    [(If e1 e2 e3)   (If (desugar e1) (desugar e2) (desugar e3))]
    [(Begin e1 e2)   (Begin (desugar e1) (desugar e2))]
    [(Let x e1 e2)   (Let x (desugar e1) (desugar e2))]
    [(LetRec bs e1)  (LetRec (map (lambda (xs) (map desugar xs)) bs) (desugar e1))]
    [(Lam n xs e)    (Lam (gensym 'lam) xs (desugar e))]
    [(App f es)      (App (desugar f) (map desugar es))]
    [_               e+]))

The compiler now just desugars before labeling and compiling expressions.

And here’s the complete compiler, including tail calls, letrec, etc.:

loot/compile.rkt

  #lang racket
  (provide (all-defined-out))
  (require "ast.rkt" "types.rkt" a86/ast)
   
  ;; Registers used
  (define rax 'rax) ; return
  (define rbx 'rbx) ; heap
  (define rcx 'rcx) ; scratch
  (define rdx 'rdx) ; return, 2
  (define r8  'r8)  ; scratch in +, -
  (define r9  'r9)  ; scratch in assert-type and tail-calls
  (define rsp 'rsp) ; stack
  (define rdi 'rdi) ; arg
   
  ;; type CEnv = [Listof Variable]
   
  ;; Expr -> Asm
  (define (compile p)
    (match (label-λ (desugar p))                ; <-- changed!
      [(Prog '() e)  
       (prog (Extern 'peek_byte)
             (Extern 'read_byte)
             (Extern 'write_byte)
             (Extern 'raise_error)
             (Label 'entry)
             (Mov rbx rdi)
             (compile-e e '(#f))
             (Mov rdx rbx)
             (Ret)
             (compile-λ-definitions (λs e)))])) ; <-- changed!
   
  ;; [Listof Defn] -> Asm
  (define (compile-λ-definitions ds)
    (seq
      (match ds
        ['() (seq)]
        [(cons d ds)
         (seq (compile-λ-definition d)
              (compile-λ-definitions ds))])))
   
  ;; This is the code generation for the lambdas themselves.
  ;; It's not very different from generating code for user-defined functions,
  ;; because lambdas _are_ user defined functions, they just don't have a name
  ;;
  ;; Defn -> Asm
  (define (compile-λ-definition l)
    (match l
      [(Lam '() xs e) (error "Lambdas must be labelled before code gen (contact your compiler writer)")]
      [(Lam f xs e)
       (let* ((free (remq* xs (fvs e)))
              ; leave space for RIP
              (env (parity (cons #f (cons #f (reverse (append xs free)))))))
             (seq (Label (symbol->label f))
               (%% "Compiling the body of the function")
               (%% (~a "free vars: " free))
               (%% (~a "args: " xs))
               (%% (~a "env: " env))
               ; we need the #args on the frame, not the length of the entire
               ; env (which may have padding)
               ; Ignore tail calls for now
               (compile-e e env)
               (Ret)))]))
   
  (define (parity c)
    (if (even? (length c))
        (append c (list #f))
        c))
   
  ;; Expr Expr Expr CEnv Int -> Asm
  (define (compile-tail-e e c s)
    (seq
      (match e
        [(If e1 e2 e3)  (compile-tail-if e1 e2 e3 c s)]
        [(Let x e1 e2)  (compile-tail-let x e1 e2 c s)]
        [(LetRec bs e1) (compile-tail-letrec (map car bs) (map cadr bs) e1 c)]
        [(App f es)     (if (<= (length es) s)
                            (compile-tail-call f es c)
                            (compile-call f es c))]
        [(Begin e1 e2)  (compile-tail-begin e1 e2 c s)]
        [_              (compile-e e c)])))
   
  ;; Expr CEnv -> Asm
  (define (compile-e e c)
    (seq
         (match e
           [(? imm? i)      (compile-value (get-imm i))]
           [(Var x)         (compile-variable x c)]
           [(App f es)      (compile-call f es c)]
           [(Lam l xs e0)   (compile-λ xs l (fvs e) c)] ; why do we ignore e0?
           [(Prim0 p)       (compile-prim0 p c)]
           [(Prim1 p e)     (compile-prim1 p e c)]
           [(Prim2 p e1 e2) (compile-prim2 p e1 e2 c)]
           [(If e1 e2 e3)   (compile-if e1 e2 e3 c)]
           [(Begin e1 e2)   (compile-begin e1 e2 c)]
           [(LetRec bs e1)  (compile-letrec (map car bs) (map cadr bs) e1 c)]
           [(Let x e1 e2)   (compile-let x e1 e2 c)])))
   
  ;; Value -> Asm
  (define (compile-value v)
    (seq (Mov rax (imm->bits v))))
   
  ;; Id CEnv -> Asm
  (define (compile-variable x c)
    (let ((i (lookup x c)))       
      (seq (Mov rax (Offset rsp i)))))
   
  ;; (Listof Variable) Label (Listof Variable) CEnv -> Asm
  (define (compile-λ xs f ys c)
    (seq
      ; Save label address
      (Lea rax (symbol->label f))
      (Mov (Offset rbx 0) rax)
   
      ; Save the environment
      (%% "Begin saving the env")
      (%% (~a "free vars: " ys))
      (Mov r8 (length ys))
   
      (Mov (Offset rbx 8) r8)
      (Mov r9 rbx)
      (Add r9 16)
      (copy-env-to-heap ys c 0)
      (%% "end saving the env")
   
      ; Return a pointer to the closure
      (Mov rax rbx)
      (Or rax type-proc)
      (Add rbx (* 8 (+ 2 (length ys))))))
   
  ;; (Listof Variable) CEnv Natural -> Asm
  ;; Pointer to beginning of environment in r9
  (define (copy-env-to-heap fvs c i)
    (match fvs
      ['() (seq)]
      [(cons x fvs)
       (seq
         ; Move the stack item  in question to a temp register
         (Mov r8 (Offset rsp (lookup x c)))
   
         ; Put the iterm in the heap
         (Mov (Offset r9 i) r8)
   
         ; Do it again for the rest of the items, incrementing how
         ; far away from r9 the next item should be
         (copy-env-to-heap fvs c (+ 8 i)))]))
   
  ;; Op0 CEnv -> Asm
  (define (compile-prim0 p c)
    (match p
      ['void      (seq (Mov rax val-void))]
      ['read-byte (seq (pad-stack c)
                       (Call 'read_byte)
                       (unpad-stack c))]
      ['peek-byte (seq (pad-stack c)
                       (Call 'peek_byte)
                       (unpad-stack c))]))
   
  ;; Op1 Expr CEnv -> Asm
  (define (compile-prim1 p e c)
    (seq (compile-e e c)
         (match p
           ['add1
            (seq (assert-integer rax)
                 (Add rax (imm->bits 1)))]
           ['sub1
            (seq (assert-integer rax)
                 (Sub rax (imm->bits 1)))]         
           ['zero?
            (let ((l1 (gensym)))
              (seq (assert-integer rax)
                   (Cmp rax 0)
                   (Mov rax val-true)
                   (Je l1)
                   (Mov rax val-false)
                   (Label l1)))]
           ['char?
            (let ((l1 (gensym)))
              (seq (And rax mask-char)
                   (Xor rax type-char)
                   (Cmp rax 0)
                   (Mov rax val-true)
                   (Je l1)
                   (Mov rax val-false)
                   (Label l1)))]
           ['char->integer
            (seq (assert-char rax)
                 (Sar rax char-shift)
                 (Sal rax int-shift))]
           ['integer->char
            (seq assert-codepoint
                 (Sar rax int-shift)
                 (Sal rax char-shift)
                 (Xor rax type-char))]
           ['eof-object? (eq-imm val-eof)]
           ['write-byte
            (seq assert-byte
                 (pad-stack c)
                 (Mov rdi rax)
                 (Call 'write_byte)
                 (unpad-stack c)
                 (Mov rax val-void))]
           ['box
            (seq (Mov (Offset rbx 0) rax)
                 (Mov rax rbx)
                 (Or rax type-box)
                 (Add rbx 8))]
           ['unbox
            (seq (assert-box rax)
                 (Xor rax type-box)
                 (Mov rax (Offset rax 0)))]
           ['car
            (seq (assert-cons rax)
                 (Xor rax type-cons)
                 (Mov rax (Offset rax 8)))]
           ['cdr
            (seq (assert-cons rax)
                 (Xor rax type-cons)
                 (Mov rax (Offset rax 0)))]
           ['empty? (eq-imm val-empty)])))
   
  ;; Op2 Expr Expr CEnv -> Asm
  (define (compile-prim2 p e1 e2 c)
    (seq (compile-e e1 c)
         (Push rax)
         (compile-e e2 (cons #f c))
         (match p
           ['+
            (seq (Pop r8)
                 (assert-integer r8)
                 (assert-integer rax)
                 (Add rax r8))]
           ['-
            (seq (Pop r8)
                 (assert-integer r8)
                 (assert-integer rax)
                 (Sub r8 rax)
                 (Mov rax r8))]
           ['eq?
            (let ((l (gensym)))
              (seq (Cmp rax (Offset rsp 0))
                   (Sub rsp 8)
                   (Mov rax val-true)
                   (Je l)
                   (Mov rax val-false)
                   (Label l)))]
           ['cons
            (seq (Mov (Offset rbx 0) rax)
                 (Pop rax)
                 (Mov (Offset rbx 8) rax)
                 (Mov rax rbx)
                 (Or rax type-cons)
                 (Add rbx 16))])))
   
  ;; Id [Listof Expr] CEnv -> Asm
  ;; Here's (part of) why this code is so gross: you have to align the stack for
  ;; the call but you have to do it *before* evaluating the arguments es,
  ;; because you need es's values to be just above 'rsp when the call is made.
  ;; But if you push a frame in order to align the call, you've got to compile es
  ;; in a static environment that accounts for that frame, hence:
  (define (compile-call f es c)
    (let* ((cnt (length es))
           (aligned (even? (+ cnt (length c))))
           (i (if aligned 1 2))
           (c+ (if aligned
                   c
                   (cons #f c)))
           (c++ (cons #f c+)))
      (seq
   
        (%% "Begin compile-call")
        ; Adjust the stack for alignment, if necessary
        (if aligned
            (seq)
            (Sub rsp 8))
   
        ; Generate the code for the thing being called
        ; and push the result on the stack
        (compile-e f c+)
        (%% "Push function on stack")
        (Push rax)
   
        ; Generate the code for the arguments
        ; all results will be put on the stack (compile-es does this)
        (compile-es es c++)
    
        ; Get the function being called off the stack
        ; Ensure it's a proc and remove the tag
        ; Remember it points to the _closure_
        (%% "Get function off stack")
        (Mov rax (Offset rsp (* 8 cnt)))
        (assert-proc rax)
        (Xor rax type-proc)
   
        (%% "Get closure env")
        (copy-closure-env-to-stack)
        (%% "finish closure env")
   
        ; get the size of the env and save it on the stack
        (Mov rcx (Offset rax 8))
        (Push rcx)
    
        ; Actually call the function
        (Mov rax (Offset rax 0))
        (Call rax)
    
        ; Get the size of the env off the stack
        (Pop rcx)
        (Sal rcx 3)
   
        ; pop args
        ; First the number of arguments + alignment + the closure
        ; then captured values
        (Add rsp (* 8 (+ i cnt)))
        (Add rsp rcx))))
   
   
  ;; LExpr (Listof LExpr) CEnv -> Asm
  (define (compile-tail-call e0 es c)
    (let ((cnt (length es)))
      (seq
        (%% (~a "Begin compile-tail-call:  function = " e0))
        ; Generate the code for the thing being called
        ; and push the result on the stack
        (compile-e e0 c)
        (%% "Push function on stack")
        (Push rax)
   
        ; Generate the code for the arguments
        ; all results will be put on the stack (compile-es does this)
        (compile-es es (cons #f c))
   
        ; Reuse the stack frame (as it's a tail call)
        (move-args cnt (+ cnt (+ 2 (in-frame c))))
   
        ; Get the function being called off the stack
        ; Ensure it's a proc and remove the tag
        ; Remember it points to the _closure_
        (Mov rax (Offset rsp (* 8 cnt)))
        (assert-proc rax)
        (Xor rax type-proc)
   
        ; Bump stack pointer (this is where the tail-call
        ; savings kick in)
        (Add rsp (* 8 (+ cnt (+ 2 (in-frame c)))))
   
        (copy-closure-env-to-stack)
   
        (Jmp (Offset rax 0)))))
   
  ;; -> Asm
  ;; Copy closure's (in rax) env to stack in rcx
  (define (copy-closure-env-to-stack)
    (let ((copy-loop (symbol->label (gensym 'copy_closure)))
          (copy-done (symbol->label (gensym 'copy_done))))
      (seq
   
        (Mov r8 (Offset rax 8)) ; length
        (Mov r9 rax)
        (Add r9 16)             ; start of env
        (Label copy-loop)
        (Cmp r8 0)
        (Je copy-done)
        (Mov rcx (Offset r9 0))
        (Push rcx)              ; Move val onto stack
        (Sub r8 1)
        (Add r9 8)
        (Jmp copy-loop)
        (Label copy-done))))
   
  ;; Integer Integer -> Asm
  ;; Move i arguments upward on stack by offset off
  (define (move-args i cnt)
    (match i
      [0 (seq)]
      [_ (seq
           ; mov first arg to temp reg
           (Mov r9 (Offset rsp (* 8 (sub1 i))))
           ; mov value to correct place on the old frame
           (Mov (Offset rsp (* 8 (+ i cnt))) r9)
           ; Now do the next one
           (move-args (sub1 i) cnt))]))
   
  ;; [Listof Expr] CEnv -> Asm
  (define (compile-es es c)
    (match es
      ['() '()]
      [(cons e es)
       (seq (compile-e e c)
            (Push rax)
            (compile-es es (cons #f c)))]))
   
  ;; Imm -> Asm
  (define (eq-imm imm)
    (let ((l1 (gensym)))
      (seq (Cmp rax imm)
           (Mov rax val-true)
           (Je l1)
           (Mov rax val-false)
           (Label l1))))
   
  ;; Expr Expr Expr CEnv -> Asm
  (define (compile-if e1 e2 e3 c)
    (let ((l1 (gensym 'if))
          (l2 (gensym 'if)))
      (seq (compile-e e1 c)
           (Cmp rax val-false)
           (Je l1)
           (compile-e e2 c)
           (Jmp l2)
           (Label l1)
           (compile-e e3 c)
           (Label l2))))
   
  ;; Expr Expr Expr CEnv -> Asm
  (define (compile-tail-if e1 e2 e3 c s)
    (let ((l1 (gensym 'if))
          (l2 (gensym 'if)))
      (seq (compile-e e1 c)
           (Cmp rax val-false)
           (Je l1)
           (compile-tail-e e2 c s)
           (Jmp l2)
           (Label l1)
           (compile-tail-e e3 c s)
           (Label l2))))
   
  ;; Expr Expr CEnv -> Asm
  (define (compile-begin e1 e2 c)
    (seq (compile-e e1 c)
         (compile-e e2 c)))
   
  ;; Expr Expr CEnv -> Asm
  (define (compile-tail-begin e1 e2 c s)
    (seq (compile-e e1 c)
         (compile-tail-e e2 c s)))
   
  ;; Id Expr Expr CEnv -> Asm
  (define (compile-let x e1 e2 c)
    (seq (compile-e e1 c)
         (Push rax)
         (compile-e e2 (cons x c))
         (Add rsp 8)))
   
  ;; Id Expr Expr CEnv -> Asm
  (define (compile-tail-let x e1 e2 c s)
    (seq (compile-e e1 c)
         (Push rax)
         (compile-tail-e e2 (cons x c) s)
         (Add rsp 8)))
   
  ;; (Listof Variable) (Listof Lambda) Expr CEnv -> Asm
  (define (compile-letrec fs ls e c)
    (seq
      (%% (~a  "Start compile letrec with" fs))
      (compile-letrec-λs ls c)
      (compile-letrec-init fs ls (append (reverse fs) c))
      (%% "Finish compile-letrec-init")
      (compile-e e (append (reverse fs) c))
      (Add rsp (* 8 (length fs)))))
   
  ;; (Listof Variable) (Listof Lambda) Expr CEnv -> Asm
  (define (compile-tail-letrec fs ls e c)
    (seq
      (compile-letrec-λs ls c)
      (compile-letrec-init fs ls (append (reverse fs) c))
      (%% "Finish compile-letrec-init")
      (compile-tail-e e (append (reverse fs) c))
      (Add rsp (* 8 (length fs)))))
   
  ;; (Listof Lambda) CEnv -> Asm
  ;; Create a bunch of uninitialized closures and push them on the stack
  (define (compile-letrec-λs ls c)
    (match ls
      ['() (seq)]
      [(cons l ls)
       (match l
         [(Lam lab as body)
          (let ((ys (fvs l)))
               (seq
                 (Lea rax (symbol->label lab))
                 (Mov (Offset rbx 0) rax)
                 (Mov rax (length ys))
                 (Mov (Offset rbx 8) rax)
                 (Mov rax rbx)
                 (Or rax type-proc)
                 (Add rbx (* 8 (+ 2 (length ys))))
                 (Push rax)
                 (compile-letrec-λs ls (cons #f c))))])]))
   
  ;; (Listof Variable) (Listof Lambda) CEnv -> Asm
  (define (compile-letrec-init fs ls c)
    (match fs
      ['() (seq)]
      [(cons f fs)
       (let ((ys (fvs (first ls))))
            (seq
              (Mov r9 (Offset rsp (lookup f c)))
              (Xor r9 type-proc)
              (Add r9 16) ; move past label and length
              (copy-env-to-heap ys c 0)
              (compile-letrec-init fs (rest ls) c)))]))
   
  ;; CEnv -> Asm
  ;; Pad the stack to be aligned for a call with stack arguments
  (define (pad-stack-call c i)
    (match (even? (+ (length c) i))
      [#f (seq (Sub rsp 8) (% "padding stack"))]
      [#t (seq)]))
   
  ;; CEnv -> Asm
  ;; Pad the stack to be aligned for a call
  (define (pad-stack c)
    (pad-stack-call c 0))
   
  ;; CEnv -> Asm
  ;; Undo the stack alignment after a call
  (define (unpad-stack-call c i)
    (match (even? (+ (length c) i))
      [#f (seq (Add rsp 8) (% "unpadding"))]
      [#t (seq)]))
   
  ;; CEnv -> Asm
  ;; Undo the stack alignment after a call
  (define (unpad-stack c)
    (unpad-stack-call c 0))
   
  ;; Id CEnv -> Integer
  (define (lookup x cenv)
    (match cenv
      ['() (error (~a "undefined variable:" x " Env: " cenv))]
      [(cons y rest)
       (match (eq? x y)
         [#t 0]
         [#f (+ 8 (lookup x rest))])]))
   
  (define (in-frame cenv)
    (match cenv
      ['() 0]
      [(cons #f rest) 0]
      [(cons y rest)  (+ 1 (in-frame rest))]))
   
  (define (assert-type mask type)
    (λ (arg)
      (seq (%% "Begin Assert")
           (Mov r9 arg)
           (And r9 mask)
           (Cmp r9 type)
           (Jne 'raise_error)
           (%% "End Assert"))))
   
  (define (type-pred mask type)
    (let ((l (gensym)))
      (seq (And rax mask)
           (Cmp rax type)
           (Mov rax (imm->bits #t))
           (Je l)
           (Mov rax (imm->bits #f))
           (Label l))))
           
  (define assert-integer
    (assert-type mask-int type-int))
  (define assert-char
    (assert-type mask-char type-char))
  (define assert-box
    (assert-type ptr-mask type-box))
  (define assert-cons
    (assert-type ptr-mask type-cons))
  (define assert-proc
    (assert-type ptr-mask type-proc))
   
  (define assert-codepoint
    (let ((ok (gensym)))
      (seq (assert-integer rax)
           (Cmp rax (imm->bits 0))
           (Jl 'raise_error)
           (Cmp rax (imm->bits 1114111))
           (Jg 'raise_error)
           (Cmp rax (imm->bits 55295))
           (Jl ok)
           (Cmp rax (imm->bits 57344))
           (Jg ok)
           (Jmp 'raise_error)
           (Label ok))))
         
  (define assert-byte
    (seq (assert-integer rax)
         (Cmp rax (imm->bits 0))
         (Jl 'raise_error)
         (Cmp rax (imm->bits 255))
         (Jg 'raise_error)))
         
  ;; Symbol -> Label
  ;; Produce a symbol that is a valid Nasm label
  (define (symbol->label s)
    (string->symbol
     (string-append
      "label_"
      (list->string
       (map (λ (c)
              (if (or (char<=? #\a c #\z)
                      (char<=? #\A c #\Z)
                      (char<=? #\0 c #\9)
                      (memq c '(#\_ #\$ #\# #\@ #\~ #\. #\?)))
                  c
                  #\_))
           (string->list (symbol->string s))))
      "_"
      (number->string (eq-hash-code s) 16))))