Closure conversion. Here's the full L5 language: e ::= (lambda (x ...) e) | x | (let ([x e]) e) | (letrec ([x e]) e) | (if e e e) | (new-tuple e ...) | (begin e e) | (e e ...) ;; application expression | prim | num prim ::= biop | pred | print | new-array | aref | aset | alen biop ::= + | - | * | < | <= | = pred ::= number? | a? Goal: to turn a program in L5 into an L4 program. The key difference is that functions in L4 are all at the top (also we have to eliminate letrec expressions; see the end of the notes for that). So we need to find a way to lift out all functions. The only thing stopping us from doing that is the free variables in the body of the function. So, the way we do this is to create closures, via a program transformation. Namely, we create a pair that holds a label with the values of the free variables to stand for the lambda expression, and we pass that around at runtime. Then, when we call the function, we supply the extra variables (in the tuple) as an extra argument. For example, consider the inner procedure here: (let ([x 1]) (let ([f (lambda (y) (+ x y))]) (f 1))) There are two free variables in the body, x and y. Since y is the argument, we don't need to worry about it. That leaves x and means that that procedure needs turn into this closure record: (make-closure :f (new-tuple x)) where we have this :f defined at the top level: (:f (vars y) (+ (area vars 0) y)) and then the call site has to change too: ((closure-proc f) (closure-vars f) 1) meaning we get this L4 program: ((let ([x 1]) (let ([f (make-closure :f (new-tuple x))]) ((closure-proc f) (closure-vars f) 1))) (:f (vars y) (+ (area vars 0) y))) This example takes a few shortcuts, reusing the existing variable name f and substituting (aref vars 0) into the body. To make this transformation general purpose, however, we can in general introduce lets. Specifically, if we see (lambda (x ...) e) in the program, we replace it with (make-closure :f (new-tuple y1 y2 ... y-n)) where (y1 y2 ... y-n) are the free variables in (lambda (x ...) e), and we create a new procedure: (:f (vars-tup x ...) (let ([y1 (aref vars-tup 0)]) (let ([y2 (aref vars-tup 1)]) ... (let ([y-n (aref vars-tup n)]) e)))) Note that vars-tup does not need to be a fresh variable, but :f needs to be a fresh label (this is relatively easy, however, since there are no labels in L5 programs). If we see an application expression: (e0 e1 ... en) then we replace it with this: (let ([f e0]) ((closure-proc f) (closure-vars f) e1 ... en)) Note that the 'f' needs to be a fresh variable here, since it must not shadow any variables that are free in e1 ... en. There are a three issues to clean up here, tho: - L4 has an argument limit of 3 and L5 has no argument limit. One way to deal with this is to package up all of the arguments into a second tuple and thus make every function that the compiler generates take two arguments and the body then begins with two sets of 'let' expressions, one that unpacks the arguments and one that unpacks the closure variables. But a simple improvement is to have a special case: if there are 2 or fewer arguments, pass them directly as arguments. If there are 3 or more, create a tuple. Since passing the arguments in a tuple requires allocation, this can save a lot on tight loops. A better solution would be to have a more complex protocol at the lower layers that handles arbitrary numbers of arguments using the stack; on a 64 bit machine, tho, you can also just use more registers for arguments which is simpler and almost as good. - when a primitive operation shows up in the function position of an application, we need to just leave it there. But when it shows up in some other place, we just turn it into lambda expression and then closure convert it. For example: (+ x y) => (+ x y) (f +) => (f (lambda (x y) (+ x y))) That way, all primitives show up in the function position of an application and we already know how to deal with that newly created lambda. [ Note that although new-tuple is really a function, we treat it specially and do not allow you to pass it around (see the grammar above), because L5 does not have n-ary functions. That is, each function has only one fixed arity, but new-tuple's can accept any number of arguments so if someone passes around new-tuple then we cannot closure convert that program. ] - letrec expressions. They need to turn into 'let's, like this: (letrec ([x e1]) e2) => (let ([x (new-tuple 0)]) (begin (aset x 0 e1[x:=(aref x 0)]) e2[x:=(aref x 0)])) where the expression e[x:=(aref x 0)] means to replace all *free* occurrences of 'x' in e with the expression (aref x 0). (NB: the equation above is an L5 to L5 transformation.)