0. Announcement on board: SLIDE 1. Final Exam Johnson Athletic Center, Wed. May 21 1:30pm to 4:30pm. I. Introduction, 5 mins., 10:05 - 10:10 A spectrum of interpreters so far: a. Simple meta-circular evaluator b. Analyzing evaluator (no EXP at runtime) c. Continuation passing for flow control (add a NEXT argument) ...Decaf Java-In-Scheme d. Explicit control evaluator (written in machine language) What would happen if we KNEW what program we wanted to run? We could translate it into a register machine directly. Could we write a program to do that? That's what a compiler is. II. Basic idea, 10 mins., 10:10 - 10:20 Let's just write down what instructions the explicit control evaluator does for a particular program. Example: (F A 1 2). Scissors, tape, and pen to cross out. Get a T.A. to write down EXP, UNEV, and CONTINUE/Stack III. Linkage, 5 mins., 10:20 - 10:25 We have little chunks of code. We have to put them together. The idea is like the continuation passing interpreter we used in Decaf Java-in-Scheme. There are three ways: a) Just tape them together. This is called NEXT. b) We don't know at compile time, but at run time the register CONTINUE will tell us where to go. This is called RETURN. c) A specific label, from a GOTO or known address at compile time. There are 25 instructions in our code, out of 59 from the interpreter (plus the code at eval-dispatch) SLIDE 2. (define (compile-linkage linkage) (cond ((eq? linkage 'return) (make-instruction-sequence '((goto (reg continue))))) ((eq? linkage 'next) (empty-instruction-sequence)) (else (make-instruction-sequence `((goto (label ,linkage))))))) IV. Preserving Registers, 5 mins., 10:25 - 10:30 Another common pattern we'll see is a desire to save some registers, doing some bit of code, and then restoring those registers: SLIDE 3. (define (preserving regs seq1 seq2) (if (null? regs) (append-instruction-sequences seq1 seq2) (let ((first-reg (car regs))) (preserving (cdr regs) (make-instruction-sequence (append `((save ,first-reg)) (statements seq1) `((restore ,first-reg)))) seq2)))) (define (end-with-linkage linkage instruction-sequence) (preserving '(continue) instruction-sequence (compile-linkage linkage))) V. Code for compiling, 10 mins., 10:30 - 10:40 SLIDE 4. (define (compile-self-evaluating exp target linkage) (end-with-linkage linkage (make-instruction-sequence `((assign ,target (const ,exp)))))) (define (compile-application exp target linkage) (let ((proc-code (compile (operator exp) 'proc 'next)) (operand-codes (map (lambda (operand) (compile operand 'val 'next)) (operands exp)))) (preserving '(env continue) proc-code (preserving '(proc continue) (construct-arglist operand-codes) (compile-procedure-call target linkage))))) SLIDE 5. (define (construct-arglist operand-codes) (let ((operand-codes (reverse operand-codes))) (if (null? operand-codes) (make-instruction-sequence '((assign argl (const ())))) (let ((code-to-get-last-arg (append-instruction-sequences (car operand-codes) (make-instruction-sequence '((assign argl (op list) (reg val))))))) (if (null? (cdr operand-codes)) code-to-get-last-arg (preserving '(env) code-to-get-last-arg (code-to-get-rest-args (cdr operand-codes)))))))) SLIDE 6. (define (code-to-get-rest-args operand-codes) (let ((code-for-next-arg (preserving '(argl) (car operand-codes) (make-instruction-sequence '((assign argl (op cons) (reg val) (reg argl))))))) (if (null? (cdr operand-codes)) code-for-next-arg (preserving '(env) code-for-next-arg (code-to-get-rest-args (cdr operand-codes)))))) VI. Resulting Code, 5 mins., 10:40 - 10:45 We do one small optimization, called "targetting". We direct the compiler to put the result of computing the operator of an application directly into PROC instead of moving it from VAL into PROC. Why didn't the interpreter do this? Our convention was that EVAL-DISPATCH would deliver the result to VAL and we would have had to double the code or be very, very clever. Look at your handout. This results in 31 instructions: more than our little scissors-and-tape exercise, still less than the interpreter. VII. Another optimization, 5 mins., 10:45 - 10:50 Let's look at PRESERVING. The interpreter has to save and restore because it doesn't look ahead to see what instructions it will have to execute. But the compiler can generate the instructions and look at them. Idea: save/restore only if the register is needed in the second sequence and is modified by the first. So we have to keep track of what registers are used and modified. SLIDE 7. (define (preserving regs seq1 seq2) (if (null? regs) (append-instruction-sequences seq1 seq2) (let ((first-reg (car regs))) (if (and (needs-register? seq2 first-reg) ; ** (modifies-register? seq1 first-reg)) ; ** (preserving (cdr regs) (make-instruction-sequence (list-union (list first-reg) ; ** needs (registers-needed seq1)) ; ** (list-difference (registers-modified seq1) ; ** modifies (list first-reg)) ; ** (append `((save ,first-reg)) (statements seq1) `((restore ,first-reg)))) seq2) (preserving (cdr regs) seq1 seq2))))) VIII. Summary, 5 mins., 10:50 - 10:55 The goal of the compiler is to take a high-level language and convert it into a lower-level language. We can think of it as just writing down the code sequences in the explicit control evaluator that would have been executed if you evaluated the program. We trade (typically) space (program is much bigger than interpreter) for time (it runs faster). We can improve the code by a number of analysis tricks, called optimizations. One neat idea is partial evaluation -- don't just simulate the evaluator with a known EXP and UNEV, but assume some of the ENV is known, too!