Skip to content

Commit e6fd485

Browse files
committed
Forgot jig.scrbl.
1 parent 422b15d commit e6fd485

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

www/notes/jig.scrbl

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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

Comments
 (0)