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 "knock " f))))))
15+ '("interp.rkt " "compile.rkt " "asm/interp.rkt " "asm/printer.rkt " ))
16+
17+ @title[#:tag "Knock " ]{Knock: first-class function (pointers)}
18+
19+ @table-of-contents[]
20+
21+ @section[#:tag-prefix "knock " ]{First-class function (pointers)}
22+
23+ With Iniquity and Jig, we have introduced functions and function
24+ calls, but functions are second-class language mechanisms: functions
25+ are not values. They cannot be computed. They cannot be stored in a
26+ list or box. They cannot be passed as arguments are returned as
27+ results of other functions.
28+
29+ This is too bad since so many program designs depend on the idea of
30+ computation-as-a-value at the heart of functional and object-oriented
31+ programming.
32+
33+ Let's now remedy this problem by making functions first-class values
34+ in our language. We'll call it @bold{Knock}.
35+
36+
37+ We add to the syntax two new forms, one for reference functions and
38+ one for calling a function where the function position is an arbitrary
39+ expression (that should evaluate to a function):
40+
41+ @verbatim|{
42+ ;; type Expr =
43+ ;; | ....
44+ ;; | `(fun ,Variable)
45+ ;; | `(call ,Expr ,@(Listof Expr))
46+ }|
47+
48+ These new syntactic forms are temporary forms that don't correspond
49+ anything in Racket but make it a bit easier to present how first-class
50+ functions work. The @racket[(fun _f)] form is a reference to a
51+ function @racket[_f], which is defined in the program. The
52+ @racket[(call _e0 _es ... )] form is a function call where the function
53+ position, @racket[_e0] is an arbitrary expression that should produce
54+ a function value.
55+
56+ We will end up eliminating them in future versions of the compiler;
57+ they are simply a crutch for now.
58+
59+ @section[#:tag-prefix "knock " ]{A Compiler with Function pointers}
60+
61+ The main idea in making functions into values is we will need to have
62+ a representation of functions. We will use a representation similar
63+ to boxes: functions will be heap allocated data structures. What will
64+ be stored in the heap? The address of the label of the function.
65+
66+ A function reference, @racket[(fun _f)], will allocate a 64-bit
67+ segment of the heap, store the location of the function's label,
68+ i.e. a pointer to the instructions for the function, and tag the
69+ pointer as a ``procedure value,'' which is new , disjoint kind of
70+ value.
71+
72+ @#reader scribble/comment-reader
73+ (racketblock
74+ ;; Variable -> Asm
75+ (define (compile-fun f)
76+ `(; rax <- address of label f
77+ (lea rax (offset ,(symbol->label f) 0 ))
78+ ; write in to heap
79+ (mov (offset rdi 0 ) rax)
80+ ; rax <- pointer into heap
81+ (mov rax rdi)
82+ ; tag as procedure pointer
83+ (or rax ,type-proc)
84+ ; alloc
85+ (add rdi 8 )))
86+ )
87+
88+ A function call, @racket[(call _e0 _es ... )] will evaluate on the
89+ subexpressions. The @racket[_e0] expression should produce a
90+ function, i.e. tagged pointer. We can erase the tag to compute the
91+ address in the heap. Dereferencing that location, gets us the label
92+ address, which can then jump to.
93+
94+ @#reader scribble/comment-reader
95+ (racketblock
96+ ;; Expr (Listof Expr) CEnv -> Asm
97+ (define (compile-fun-call e0 es c)
98+ (let ((cs (compile-es es (cons #f c)))
99+ (c0 (compile-e e0 c))
100+ (i (- (add1 (length c))))
101+ (stack-size (* 8 (length c))))
102+ `(,@c0
103+ ; save f in stack
104+ (mov (offset rsp ,i) rax)
105+ ,@cs
106+ ; restore f
107+ (mov rax (offset rsp ,i))
108+ ,@assert-proc
109+ (sub rsp ,stack-size)
110+ (xor rax ,type-proc)
111+ ; call f
112+ (call (offset rax 0 ))
113+ (add rsp ,stack-size))))
114+ )
115+
116+ A tail call version of the above can be defined as:
117+
118+ @#reader scribble/comment-reader
119+ (racketblock
120+ ;; Expr (Listof Expr) CEnv -> Asm
121+ (define (compile-fun-tail-call e0 es c)
122+ (let ((cs (compile-es es (cons #f c)))
123+ (c0 (compile-e e0 c))
124+ (i (- (add1 (length c)))))
125+ `(,@c0
126+ (mov (offset rsp ,i) rax)
127+ ,@cs
128+ (mov rax (offset rsp ,i))
129+ ,@(move-args (length es) i)
130+ ,@assert-proc
131+ (xor rax ,type-proc)
132+ (jmp (offset rax 0 )))))
133+ )
134+
135+ The complete compiler:
136+
137+ @codeblock-include["knock/compile.rkt " ]
138+
139+ We can verify that the compiler works for programs that use functions
140+ like before:
141+
142+ @ex[
143+ (asm-interp
144+ (compile '(begin (define (f x)
145+ (if (zero? x)
146+ 0
147+ (add1 (call (fun f) (sub1 x)))))
148+ (call (fun f) 10 ))))
149+ ]
150+
151+ But it also works when functions are put in lists:
152+
153+ @ex[
154+ (asm-interp
155+ (compile '(begin (define (f x) x)
156+ (call (car (cons (fun f) '() )) 7 ))))
157+ ]
158+
159+ And functions that produce functions:
160+
161+ @ex[
162+ (asm-interp
163+ (compile '(begin (define (f x) (fun h))
164+ (define (h y) y)
165+ (call (call (fun f) 5 ) 9 ))))
166+ ]
0 commit comments