|
1 | 1 | #lang scribble/manual |
2 | 2 |
|
3 | | -@(require (for-label (except-in racket ...))) |
| 3 | +@(require (for-label (except-in racket ... compile) a86)) |
4 | 4 | @(require redex/pict |
5 | 5 | racket/runtime-path |
6 | 6 | scribble/examples |
|
10 | 10 |
|
11 | 11 | @(define codeblock-include (make-codeblock-include #'h)) |
12 | 12 |
|
13 | | -@(for-each (λ (f) (ev `(require (file ,(path->string (build-path notes "jig" f)))))) |
14 | | - '("interp.rkt" "compile.rkt" "asm/interp.rkt" "asm/printer.rkt")) |
| 13 | +@(ev '(require rackunit a86)) |
| 14 | +@(ev `(current-directory ,(path->string (build-path notes "jig")))) |
| 15 | +@(void (ev '(with-output-to-string (thunk (system "make runtime.o"))))) |
| 16 | +@(for-each (λ (f) (ev `(require (file ,f)))) |
| 17 | + '("interp.rkt" "compile.rkt" "ast.rkt" "parse.rkt" "types.rkt")) |
15 | 18 |
|
16 | 19 | @title[#:tag "Jig"]{Jig: jumping to tail calls} |
17 | 20 |
|
@@ -181,4 +184,105 @@ re-write the interpreter, but as it is, we're already done. |
181 | 184 |
|
182 | 185 | @section[#:tag-prefix "jig"]{A Compiler with Proper Tail Calls} |
183 | 186 |
|
| 187 | +The compiler requires a bit more work, because of how the @tt{Call} instruction |
| 188 | +is implemented in the hardware itself, we always use a little bit of stack |
| 189 | +space each time we execute a function call. Therefore, in order to implement |
| 190 | +tail-calls correctly, we need to @emph{avoid} the @tt{Call} instruction! |
| 191 | + |
| 192 | +How do we perform function calls without the @tt{Call} instruction, well we're |
| 193 | +going to have to do a little bit of extra work in the compiler. First, let's |
| 194 | +remind ourselves of how a `normal' function call works (we'll just look at the |
| 195 | +case where we don't have to adjust for alignment): |
| 196 | + |
| 197 | +@#reader scribble/comment-reader |
| 198 | +(racketblock |
| 199 | +(define (compile-app f es c) |
| 200 | + |
| 201 | + ; Generate the code for each argument |
| 202 | + ; and push each on the stack |
| 203 | + (seq (compile-es es c) |
| 204 | + |
| 205 | + ; Generate the instruction for calling the function itself |
| 206 | + (Call (symbol->label f)) |
| 207 | + |
| 208 | + ; `pop` all of the arguments off of the stack |
| 209 | + (Add rsp (* 8 (length es))))) |
| 210 | +) |
| 211 | + |
| 212 | +The first insight regards what the stack will look like once we are |
| 213 | +@emph{inside the function we are calling}. Upon entry to the function's code, |
| 214 | +@tt{rsp} will point to the return address that the last @tt{Call} instruction |
| 215 | +pushed onto the stack, with the arguments to the function at positive offsets |
| 216 | +to @tt{rsp}. As long as we ensure that this is the case we don't @emph{have} to |
| 217 | +call functions with @tt{Call}. |
| 218 | + |
| 219 | +The second insight is what we mentioned above, when describing tail calls |
| 220 | +themselves: If we're performing a call in the tail position then there is |
| 221 | +nothing else to do when we return. So instead of returning here, we can return |
| 222 | +to the @emph{previous} call, we can overwrite the current environment on the |
| 223 | +stack, since we won't need it (there's nothing else to do, after all). In |
| 224 | +jargon: we can @emph{reuse the stack frame}. The only thing we have to |
| 225 | +be careful about is whether the current environment is `big enough' to |
| 226 | +hold all of the arguments for our function call, since we are going |
| 227 | +to reuse it, we'll want to make sure there's enough space. |
| 228 | + |
| 229 | +For now assume we've performed that check and that there is enough space. |
| 230 | +Let's go through the process bit by bit: |
| 231 | + |
| 232 | +@#reader scribble/comment-reader |
| 233 | +(racketblock |
| 234 | +;; Variable (Listof Expr) CEnv -> Asm |
| 235 | +;; Compile a call in tail position |
| 236 | +(define (compile-tail-call f es c) |
| 237 | + (let ((cnt (length es))) |
| 238 | + |
| 239 | + ; Generate the code for the arguments to the function, |
| 240 | + ; pushing them on the stack, this is no different |
| 241 | + ; than a normal call |
| 242 | + (seq (compile-es es c) |
| 243 | + |
| 244 | + |
| 245 | + ; Now we _move_ the arguments from where they are on the |
| 246 | + ; stack to where the _previous_ values in the environment |
| 247 | + ; the function `move-args` takes the number of values we |
| 248 | + ; have to move, and the number of stack slots that we have to |
| 249 | + ; move them. |
| 250 | + (move-args cnt (+ cnt (in-frame c))) |
| 251 | + |
| 252 | + ; Once we've moved the arguments, we no longer need them at the |
| 253 | + ; top of the stack. This is a big part of the benefit for |
| 254 | + ; tail-calls |
| 255 | + (Add rsp (* 8 (+ cnt (in-frame c)))) |
| 256 | + |
| 257 | + ; Now that `rsp` points to the _previous_ return address, |
| 258 | + ; and the arguments are at a positive offset of `rsp`, |
| 259 | + ; we no longer need the `call` instruction (in fact, it would |
| 260 | + ; be incorrect to use it!), instead we jump to the function |
| 261 | + ; directly. |
| 262 | + (Jmp (symbol->label f))))) |
| 263 | +) |
| 264 | + |
| 265 | +@tt{move-args} is defined below: |
| 266 | + |
| 267 | + |
| 268 | +@#reader scribble/comment-reader |
| 269 | +(racketblock |
| 270 | +;; Integer Integer -> Asm |
| 271 | +;; Move i arguments upward on stack by offset off |
| 272 | +(define (move-args i cnt) |
| 273 | + (match i |
| 274 | + [0 (seq)] |
| 275 | + [_ (seq |
| 276 | + ; mov first arg to temp reg |
| 277 | + (Mov r9 (Offset rsp (* 8 (sub1 i)))) |
| 278 | + ; mov value to correct place on the old frame |
| 279 | + (Mov (Offset rsp (* 8 (+ i cnt))) r9) |
| 280 | + ; Now do the next one |
| 281 | + (move-args (sub1 i) cnt))])) |
| 282 | +) |
| 283 | + |
| 284 | +The entire compiler will be illuminated for seeing how we keep track of which |
| 285 | +expressions are in a tail-call position and whether we have enough space to |
| 286 | +re-use the stack frame. |
| 287 | + |
184 | 288 | @codeblock-include["jig/compile.rkt"] |
0 commit comments