|
| 1 | +#lang scribble/manual |
| 2 | + |
| 3 | +@(require (for-label (except-in racket ...))) |
| 4 | +@(require redex/pict |
| 5 | + racket/runtime-path |
| 6 | + scribble/examples |
| 7 | + "hustle/semantics.rkt" |
| 8 | + "utils.rkt" |
| 9 | + "ev.rkt" |
| 10 | + "../utils.rkt") |
| 11 | + |
| 12 | +@(define codeblock-include (make-codeblock-include #'h)) |
| 13 | + |
| 14 | +@(for-each (λ (f) (ev `(require (file ,(path->string (build-path notes "jig" f)))))) |
| 15 | + '("interp.rkt" "compile.rkt" "asm/interp.rkt" "asm/printer.rkt")) |
| 16 | + |
| 17 | +@title[#:tag "Jig"]{Jig: jumping to tail calls} |
| 18 | + |
| 19 | +@table-of-contents[] |
| 20 | + |
| 21 | +@section[#:tag-prefix "jig"]{Tail Calls} |
| 22 | + |
| 23 | +With Iniquity, we've finally introduced some computational power via |
| 24 | +the mechanism of functions and function calls. Together with the |
| 25 | +notion of inductive data, which we have in the form of pairs, we can |
| 26 | +write fixed-sized programs that operate over arbitrarily large data. |
| 27 | + |
| 28 | +The problem, however, is that there are a class of programs that |
| 29 | +should operate with a fixed amount of memory, but instead consume |
| 30 | +memory in proportion to the size of the data they operate on. This is |
| 31 | +unfortunate because a design flaw in our compiler now leads to |
| 32 | +asympototically bad run-times. |
| 33 | + |
| 34 | +We can correct this problem by generating space-efficient code for |
| 35 | +function calls when those calls are in @bold{tail position}. |
| 36 | + |
| 37 | +Let's call this language @bold{Jig}. |
| 38 | + |
| 39 | +There are no syntactic additions required: we simply will properly |
| 40 | +handling function calls. |
| 41 | + |
| 42 | + |
| 43 | +@section[#:tag-prefix "jig"]{What is a Tail Call?} |
| 44 | + |
| 45 | +A @bold{tail call} is a function call that occurs in @bold{tail |
| 46 | +position}. What is tail position and why is important to consider |
| 47 | +function calls made in this position? |
| 48 | + |
| 49 | +Tail position captures the notion of ``the last subexpression that |
| 50 | +needs to be computed.'' If the whole program is some expression |
| 51 | +@racket[_e], the @racket[_e] is in tail position. Computing |
| 52 | +@racket[_e] is the last thing (it's the only thing!) the program needs |
| 53 | +to compute. |
| 54 | + |
| 55 | +Let's look at some examples to get a sense of the subexpressions in |
| 56 | +tail position. If @racket[_e] is in tail position and @racket[_e] is |
| 57 | +of the form: |
| 58 | + |
| 59 | +@itemlist[ |
| 60 | + |
| 61 | +@item{@racket[(let ((_x _e0)) _e1)]: then @racket[_e1] is in tail |
| 62 | +position, while @racket[_e0] is not. The reason @racket[_e0] is not |
| 63 | +in tail position is because after evaluating it, the @racket[_e1] |
| 64 | +still needs to be evaluated. On the other hand, once @racket[_e0] is |
| 65 | +evaluated, then whatever @racket[_e1] evaluates to is what the whole |
| 66 | +@racket[let]-expression evaluates to; it is all that remains to |
| 67 | +compute.} |
| 68 | + |
| 69 | +@item{@racket[(if _e0 _e1 _e2)]: then @emph{both} @racket[_e1] and |
| 70 | +@racket[_e2] are in tail position, while @racket[_e0] is not. After |
| 71 | +the @racket[_e0], then based on its result, either @racket[_e1] or |
| 72 | +@racket[_e2] is evaluated, but whichever it is determines the result |
| 73 | +of the @racket[if] expression.} |
| 74 | + |
| 75 | +@item{@racket[(+ _e0 _e1)]: then neither @racket[_e0] or @racket[_e1] |
| 76 | +are in tail position because after both are evaluated, their results |
| 77 | +still must be added together.} |
| 78 | + |
| 79 | +@item{@racket[(f _e0 ...)], where @racket[f] is a function |
| 80 | +@racket[(define (f _x ...) _e)]: then none of the arguments |
| 81 | +@racket[_e0 ...] are in tail position, because after evaluating them, |
| 82 | +the function still needs to be applied, but the body of the function, |
| 83 | +@racket[_e] is in tail position.} |
| 84 | + |
| 85 | +] |
| 86 | + |
| 87 | +The significance of tail position is relevant to the compilation of |
| 88 | +calls. Consider the compilation of a call as described in |
| 89 | +@secref{Iniquity}: arguments are pushed on the call stack, then the |
| 90 | +@racket['call] instruction is issued, which pushes the address of the |
| 91 | +return point on the stack and jumps to the called position. When the |
| 92 | +function returns, the return point is popped off the stack and jumped |
| 93 | +back to. |
| 94 | + |
| 95 | +But if the call is in tail position, what else is there to do? |
| 96 | +Nothing. So after the call, return transfers back to the caller, who |
| 97 | +then just returns itself. |
| 98 | + |
| 99 | +This leads to unconditional stack space consumption on @emph{every} |
| 100 | +function call, even function calls that don't need to consume space. |
| 101 | + |
| 102 | +Consider this program: |
| 103 | + |
| 104 | +@#reader scribble/comment-reader |
| 105 | +(racketblock |
| 106 | +;; (Listof Number) -> Number |
| 107 | +(define (sum xs) (sum/acc xs 0)) |
| 108 | + |
| 109 | +;; (Listof Number) Number -> Number |
| 110 | +(define (sum/acc xs a) |
| 111 | + (if (empty? xs) |
| 112 | + a |
| 113 | + (sum/acc (cdr xs) (+ (car xs) a)))) |
| 114 | +) |
| 115 | + |
| 116 | +The @racket[sum/acc] function @emph{should} operate as efficiently as |
| 117 | +a loop that iterates over the elements of a list accumulating their |
| 118 | +sum. But, as currently compiled, the function will push stack frames |
| 119 | +for each call. |
| 120 | + |
| 121 | +Matters become worse if we were re-write this program in a seemingly |
| 122 | +benign way to locally bind a variable: |
| 123 | + |
| 124 | +@#reader scribble/comment-reader |
| 125 | +(racketblock |
| 126 | +;; (Listof Number) Number -> Number |
| 127 | +(define (sum/acc xs a) |
| 128 | + (if (empty? xs) |
| 129 | + a |
| 130 | + (let ((b (+ (car xs) a))) |
| 131 | + (sum/acc (cdr xs) b)))) |
| 132 | +) |
| 133 | + |
| 134 | +Now the function pushes a return point @emph{and} a local binding for |
| 135 | +@racket[b] on every recursive call. |
| 136 | + |
| 137 | +But we know that whatever the recursive call produces is the answer to |
| 138 | +the overall call to @racket[sum]. There's no need for a new return |
| 139 | +point and there's no need to keep the local binding of @racket[b] |
| 140 | +since there's no way this program can depend on it after the recursive |
| 141 | +call. Instead of pushing a new, useless, return point, we should make |
| 142 | +the call with whatever the current return point. This is the idea of |
| 143 | +@tt{proper tail calls}. |
| 144 | + |
| 145 | +@bold{An axe to grind:} the notion of proper tail calls is often |
| 146 | +referred to with misleading terminology such as @bold{tail call |
| 147 | +optimization} or @bold{tail recursion}. Optimization seems to imply |
| 148 | +it is a nice, but optional strategy for implementing function calls. |
| 149 | +Consequently, a large number of mainstream programming languages, most |
| 150 | +notably Java, do not properly implement tail calls. But a language |
| 151 | +without proper tail calls is fundamentally @emph{broken}. It means |
| 152 | +that functions cannot reliably be designed to match the structure of |
| 153 | +the data they operate on. It means iteration cannot be expressed with |
| 154 | +function calls. There's really no justification for it. It's just |
| 155 | +broken. Similarly, it's not about recursion alone (although it is |
| 156 | +critical @emph{for} recursion), it really is about getting function |
| 157 | +calls, all calls, right. @bold{/rant} |
| 158 | + |
| 159 | + |
| 160 | + |
| 161 | +@section[#:tag-prefix "jig"]{An Interpreter for Proper Calls} |
| 162 | + |
| 163 | +Before addressing the issue of compiling proper tail calls, let's |
| 164 | +first think about the interpreter, starting from the interpreter we |
| 165 | +wrote for Iniquity: |
| 166 | + |
| 167 | +@codeblock-include["iniquity/interp.rkt"] |
| 168 | + |
| 169 | +What needs to be done to make it implement proper tail calls? |
| 170 | + |
| 171 | +Well... not much. Notice how every Iniquity subexpression that is in |
| 172 | +tail position is interpreted by a call to @racket[interp-env] that is |
| 173 | +itself in tail position in the Racket program! |
| 174 | + |
| 175 | +So long as Racket implements tail calls properly, which is does, then |
| 176 | +this interpreter implements tail calls properly. The interpreter |
| 177 | +@emph{inherits} the property of proper tail calls from the |
| 178 | +meta-language. This is but one reason to do tail calls correctly. |
| 179 | +Had we transliterated this program to Java, we'd be in trouble as the |
| 180 | +interpeter would inherit the lack of tail calls and we would have to |
| 181 | +re-write the interpreter, but as it is, we're already done. |
| 182 | + |
| 183 | +@section[#:tag-prefix "jig"]{An example} |
| 184 | + |
| 185 | +@section[#:tag-prefix "jig"]{A Compiler with Proper Tail Calls} |
| 186 | + |
| 187 | +@codeblock-include["jig/compile.rkt"] |
0 commit comments