Skip to content

Commit c14e76f

Browse files
committed
Update jig notes
1 parent 925c3e1 commit c14e76f

File tree

1 file changed

+107
-3
lines changed

1 file changed

+107
-3
lines changed

www/notes/jig.scrbl

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#lang scribble/manual
22

3-
@(require (for-label (except-in racket ...)))
3+
@(require (for-label (except-in racket ... compile) a86))
44
@(require redex/pict
55
racket/runtime-path
66
scribble/examples
@@ -10,8 +10,11 @@
1010

1111
@(define codeblock-include (make-codeblock-include #'h))
1212

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"))
1518

1619
@title[#:tag "Jig"]{Jig: jumping to tail calls}
1720

@@ -181,4 +184,105 @@ re-write the interpreter, but as it is, we're already done.
181184

182185
@section[#:tag-prefix "jig"]{A Compiler with Proper Tail Calls}
183186

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+
184288
@codeblock-include["jig/compile.rkt"]

0 commit comments

Comments
 (0)