Skip to content

Commit 8b5c0eb

Browse files
authored
Merge pull request #39 from dvanhorn/next
Next
2 parents 6079299 + ac318fc commit 8b5c0eb

7 files changed

Lines changed: 203 additions & 17 deletions

File tree

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ sudo: required
33
dist: trusty
44
os:
55
- linux
6-
- osx
76

87
env:
98
global:

www/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ $(course):
1717
$(course).scrbl
1818
mkdir -p $(course)/code/
1919
cd notes ; \
20-
tar -c `git ls-files -X .gitignore intro abscond blackmail con dupe extort fraud hustle` | \
20+
tar -c `git ls-files -X .gitignore intro abscond blackmail con dupe extort fraud hustle iniquity` | \
2121
(cd ../main/code ; tar -x) ; cd ../..
2222

2323
clean:

www/notes.scrbl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
@include-section{notes/grift.scrbl}
1616
@include-section{notes/hustle.scrbl}
1717
@include-section{notes/iniquity.scrbl}
18+
@include-section{notes/jig.scrbl}
1819
@;{
1920
@include-section{notes/5.scrbl}
2021
@include-section{notes/6.scrbl}

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"]

www/notes/jig/asm/printer.rkt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
[`(neg ,a1)
3232
(string-append "\tneg " (arg->string a1) "\n")]
3333
[`(call ,l)
34-
(string-append "\tcall " (arg->string l) "\n")]
34+
(string-append "\tcall " (label->string l) "\n")]
3535
[`(push ,r)
36-
(string-append "\tpush " (reg->string r) "\n")]
36+
(string-append "\tpush " (reg->string r) "\n")]
37+
[`(pop ,r)
38+
(string-append "\tpop " (reg->string r) "\n")]
3739
[l (string-append (label->string l) ":\n")]))
3840

3941
(define (opcode2? x)
@@ -53,7 +55,7 @@
5355
;; Any -> Boolean
5456
(define (reg? x)
5557
(and (symbol? x)
56-
(memq x '(rax rbx rcx rsp rdi rip))))
58+
(memq x '(rax rbx rcx rdx rsp rdi rip rbp rsi r8 r9 r10 r11 r12 r13 r14 r15))))
5759

5860
;; Reg -> String
5961
(define (reg->string r)
@@ -74,6 +76,7 @@
7476
(display
7577
(string-append "\tglobal " (label->string g) "\n"
7678
"\tdefault rel\n"
77-
"\textern " (label->string 'error) "\n"
79+
"\textern " (label->string 'error) "\n"
80+
"\textern " (label->string 'plus_two) "\n"
7881
"\tsection .text\n"
7982
(asm->string a)))))

www/notes/jig/compile.rkt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@
1818
(define imm-type-bool (arithmetic-shift #b01 result-shift))
1919
(define imm-type-char (arithmetic-shift #b10 result-shift))
2020
(define imm-type-empty (arithmetic-shift #b11 result-shift))
21-
22-
23-
2421
(define imm-val-false imm-type-bool)
25-
(define imm-val-true (bitwise-ior (arithmetic-shift 1 (add1 imm-shift)) imm-type-bool))
22+
(define imm-val-true
23+
(bitwise-ior (arithmetic-shift 1 (add1 imm-shift)) imm-type-bool))
2624

2725
;; Allocate in 64-bit (8-byte) increments, so pointers
2826
;; end in #b000 and we tag with #b001 for boxes, etc.
@@ -51,12 +49,10 @@
5149
`(entry
5250
,@(compile-tail-e e '())
5351
ret
54-
5552
err
5653
(push rbp)
57-
(call error)
58-
ret))
59-
54+
(call error)))
55+
6056
;; Expr CEnv -> Asm
6157
;; Compile an expression in tail position
6258
(define (compile-tail-e e c)

www/schedule.scrbl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@
4141
(list @wk{10/1}
4242
@seclink["Assignment 5"]{A5}
4343
@secref["Iniquity"]
44-
'cont)
44+
@elem{@secref["Iniquity"] (cont.)})
4545

4646
(list @wk{10/8}
4747
@bold{@seclink["Midterm_1"]{M1}}
48-
@elem{TBD}
48+
@elem{No lecture (exam)}
4949
'cont)
5050

5151
(list @wk{10/15}
52-
"A6"
52+
@seclink["Assignment 6"]{A6}
5353
@elem{TBD}
5454
'cont)
5555

0 commit comments

Comments
 (0)