diff --git a/www/assignments.scrbl b/www/assignments.scrbl index 37a8f08b..33984fa8 100644 --- a/www/assignments.scrbl +++ b/www/assignments.scrbl @@ -9,3 +9,7 @@ @include-section{assignments/4.scrbl} @include-section{assignments/5.scrbl} @include-section{assignments/6.scrbl} +@include-section{assignments/7.scrbl} + +@;{assignment 8: quote in general, and quasiquote} +@;{assignment 9: standard library, IO} diff --git a/www/assignments/7.scrbl b/www/assignments/7.scrbl new file mode 100644 index 00000000..f2bbca36 --- /dev/null +++ b/www/assignments/7.scrbl @@ -0,0 +1,235 @@ +#lang scribble/manual +@title[#:tag "Assignment 7" #:style 'unnumbered]{Assignment 7: Symbols, interning, and gensym} + +@(require (for-label (except-in racket ...))) +@;(require "../notes/fraud-plus/semantics.rkt") +@;(require redex/pict) + +@(require "../notes/ev.rkt") + +@bold{Due: Tues, Nov 12, 11:59PM} + +@(define repo "https://classroom.github.com/a/5UM2CXXa") + +The goal of this assignment is to (1) implement symbols and the +@racket[eq?] primitive operation, (2) to implement symbol interning by +program transformation. + +Assignment repository: +@centered{@link[repo repo]} + +You are given a repository with a starter compiler similar to the +@seclink["Loot"]{Loot} language we studied in class. + +The given code also implements all the ``plus'' features we've +developed in past assignments. + +@section[#:tag-prefix "a7-" #:style 'unnumbered]{Symbols} + +Your first task is to implement symbols for the Loot+ language. +You've used symbols extensively throughout the semester, so their use +should be familiar to you. A symbol evaluates to itself: + +@ex[ +'foo +] + +Your first task is to implement a symbol data type. The given code +includes syntax checking for programs that may contain symbols and +run-time support for printing symbols. The compiler has been stubbed +for compiling symbols. You will need to implement +@racket[compile-symbol] in @tt{compile.rkt}. + +A symbol can be represented much like a string: as a continuous +sequence of characters in memory, along with a length field. The type +tag is different, since strings and symbols should be disjoint data +types. + +Once you implement @racket[compile-symbol], you should be able to +write programs that contain symbols. + +@section[#:tag-prefix "a7-" #:style 'unnumbered]{Pointer equality} + +Your next task is to implement the @racket[eq?] primitive operation, +which compares two values for pointer equality. Immediate values +(characters, integers, booleans, empty list, etc.) should be +pointer-equal to values that are ``the same.'' So for example: + +@ex[ +(eq? '() '()) +(eq? 5 5) +(eq? #\a #\a) +(eq? #\t #\t) +] + +On the other hand, values that are allocated in memory such as boxes, +pairs, procedures, etc., are only @racket[eq?] to each other if they +are allocated to the same location in memory. So for example, the +following could all produce @racket[#f]: + +@ex[ +(eq? (λ (x) x) (λ (x) x)) +(eq? (cons 1 2) (cons 1 2)) +(eq? (box 1) (box 1)) +] + +However these must be produce @racket[#t]: + +@ex[ +(let ((x (λ (x) x))) + (eq? x x)) +(let ((x (cons 1 2))) + (eq? x x)) +(let ((x (box 1))) + (eq? x x)) +] + +Applying @racket[eq?] to any two values from disjoint data types +should produce @racket[#f]: + +@ex[ +(eq? 0 #f) +(eq? #\a "a") +(eq? '() #t) +(eq? 'fred "fred") +] + +The given compiler is stubbed for the @racket[eq?] primitive. You +must implement @racket[compile-eq?]. + +@section[#:tag-prefix "a7-" #:style 'unnumbered]{Interning symbols} + +One thing you may notice at this point is that because symbols are +allocated in memory, the behavior @racket[eq?] with your compiler +differs from Racket's behavior. + +In Racket, two symbols which are written the same way in a given +program are @racket[eq?] to each other. + +@ex[ +(eq? 'x 'x) +] + +But your compiler will (probably) produce @racket[#f]. + +The problem is that Racket ``interns'' symbols, meaning that all +occurrences of a symbol are allocated to the same memory location. +(Languages like Java also do this with string literals.) + +Extend your compiler so that @racket[eq?] behaves correctly on +symbols. Note, you should @emph{not change the way @racket[eq?] +works}, rather you should change how symbols are handled by the +compiler. + +The most effective way to implement symbol interning is to apply a +program transformation to the given program to compile. This +transformation should replace multiple occurrences of the same symbol +with a variable that is bound to that symbol, and that symbol should +be allocated exactly once. + +So for example, + +@racketblock[ +(eq? 'fred 'fred) +] + +could be transformed to: + +@racket[ +(let ((x 'fred)) + (eq? x x)) +] + +The latter should result in @racket[#t] since the @racket['fred] +symbol is allocated exactly once. + +The compiler uses a @racket[intern-symbols] function, which does +nothing in the given code, but should be re-defined to perform the +symbol interning program transformation. Note: you probably want to +define a few helper functions to make @racket[intern-symbols] work. + +@section[#:tag-prefix "a7-" #:style 'unnumbered]{Generating symbols} + +Finally, implement the @racket[gensym] primitive, which generates a +symbol distinct from all other symbols. + +To keep things simple, you should implement the nullary version of +@racket[gensym], i.e. it should take zero arguments and produce a new +symbol. + +The following program should always produce @racket[#f]: + +@ex[ +(eq? (gensym) (gensym)) +] + +But the following should always produce @racket[#t]: + + +@ex[ +(let ((x (gensym))) + (eq? x x)) +] + +Note: Racket's @racket[gensym] will generate a new name for a symbol, +usually something like @racket['g123456], where each successive call +to @racket[gensym] will produce @racket['g123457], @racket['g123458], +@racket['g123459], etc. Yours does not have to do this (although it's +fine if it does). All that matters is that @racket[gensym] produces a +symbol that is not @racket[eq?] to any other symbol but itself. + +@section[#:tag-prefix "a7-" #:style 'unnumbered]{Bonus} + +Should you find yourself having completed the assignment with time to +spare, you could try implementing @racket[compile-tail-apply], which +compiles uses of @racket[apply] that appear in tail position. It is +currently defined to use the non-tail-call code generator, which means +@racket[apply] does not make a proper tail call. + +Keep in mind that this language, the subexpression of @racket[apply] +are arbitrary expressions: @racket[(apply _e0 _e1)] and that +@racket[_e0] may evaluate to a closure, i.e. a function with a saved +environment. Moreover, the function may have been defined to have +variable arity. All of these issues will conspire to make tail calls +with @racket[apply] tricky to get right. + +This isn't worth any credit, but you might learn something. + +@section[#:tag-prefix "a7-" #:style 'unnumbered]{Testing} + +You can test your code in several ways: + +@itemlist[ + + @item{Using the command line @tt{raco test .} from + the directory containing the repository to test everything.} + + @item{Using the command line @tt{raco test } to + test only @tt{}.} + + @item{Pushing to github. You can + see test reports at: + @centered{@link["https://travis-ci.com/cmsc430/"]{ + https://travis-ci.com/cmsc430/}} + + (You will need to be signed in in order see results for your private repo.)}] + +Note that only a small number of tests are given to you, so you should +write additional test cases. + +@bold{There is separate a repository for tests!} When you push your +code, Travis will automatically run your code against the tests. If +you would like to run the tests locally, clone the following +repository into the directory that contains your compiler and run +@tt{raco test .} to test everything: + +@centered{@tt{https://github.com/cmsc430/assign07-test.git}} + +This repository will evolve as the week goes on, but any time there's +a significant update it will be announced on Piazza. + +@section[#:tag-prefix "a7-" #:style 'unnumbered]{Submitting} + +Pushing your local repository to github ``submits'' your work. We +will grade the latest submission that occurs before the deadline. + diff --git a/www/notes/loot.scrbl b/www/notes/loot.scrbl index 7a129033..54494611 100644 --- a/www/notes/loot.scrbl +++ b/www/notes/loot.scrbl @@ -707,19 +707,19 @@ Here's the function for emitting closure construction code: @#reader scribble/comment-reader (racketblock -;; (Listof Variable) Label Expr CEnv -> Asm -(define (compile-λ xs f e0 c) +;; (Listof Variable) Label (Listof Varialbe) CEnv -> Asm +(define (compile-λ xs f ys c) `(;; Save label address (lea rax (offset ,f 0)) (mov (offset rdi 0) rax) - + ;; Save the environment (mov r8 ,(length ys)) (mov (offset rdi 1) r8) (mov r9 rdi) (add r9 16) ,@(copy-env-to-heap ys c 0) - + ;; Return a pointer to the closure (mov rax rdi) (or rax ,type-proc) @@ -903,8 +903,8 @@ handle the tasks listed above: ;; (Listof Variable) (Listof Lambda) Expr CEnv -> Asm (define (compile-letrec fs ls e c) (let ((c0 (compile-letrec-λs ls c)) - (c1 (compile-letrec-init fs ls (append fs c))) - (c2 (compile-e e (append fs c)))) + (c1 (compile-letrec-init fs ls (append (reverse fs) c))) + (c2 (compile-e e (append (reverse fs) c)))) `(,@c0 ,@c1 ,@c2))) @@ -969,9 +969,24 @@ We can give a spin: #f (even? (sub1 x)))))) (even? 10)))) + +(asm-interp + (compile + '(letrec ((map (λ (f ls) + (letrec ((mapper (λ (ls) + (if (empty? ls) + '() + (cons (f (car ls)) (mapper (cdr ls))))))) + (mapper ls))))) + (map (λ (f) (f 0)) + (cons (λ (x) (add1 x)) + (cons (λ (x) (sub1 x)) + '())))))) ] + + @section[#:tag-prefix "loot"]{Syntactic sugar for function definitions} The @racket[letrec] form is a generlization of the diff --git a/www/notes/loot/compile-file.rkt b/www/notes/loot/compile-file.rkt new file mode 100644 index 00000000..ac1309b1 --- /dev/null +++ b/www/notes/loot/compile-file.rkt @@ -0,0 +1,19 @@ +#lang racket +(provide (all-defined-out)) +(require "compile.rkt" #;"syntax.rkt" "asm/printer.rkt") + +;; String -> Void +;; Compile contents of given file name, +;; emit asm code on stdout +(define (main fn) + (with-input-from-file fn + (λ () + (let ((p (read-program))) + ; assumed OK for now + ;(unless (and (prog? p) (closed? p)) + ; (error "syntax error")) + (asm-display (compile p)))))) + +(define (read-program) + (regexp-match "^#lang racket" (current-input-port)) + (read)) diff --git a/www/notes/loot/compile.rkt b/www/notes/loot/compile.rkt index 9e25be54..1e5df7c9 100644 --- a/www/notes/loot/compile.rkt +++ b/www/notes/loot/compile.rkt @@ -78,7 +78,7 @@ [`(if ,e0 ,e1 ,e2) (compile-tail-if e0 e1 e2 c)] [`(+ ,e0 ,e1) (compile-+ e0 e1 c)] [`(let ((,x ,e0)) ,e1) (compile-tail-let x e0 e1 c)] - [`(letrec ,bs ,e0) (compile-tail-letrec (map first bs) (map second bs) e0 c)] + [`(letrec ,bs ,e0) (compile-tail-letrec (map first bs) (map second bs) e0 c)] [`(λ ,xs ',l ,e0) (compile-λ xs l (fvs e) c)] [`(,e . ,es) (compile-tail-call e es c)])) @@ -109,14 +109,14 @@ `(;; Save label address (lea rax (offset ,f 0)) (mov (offset rdi 0) rax) - + ;; Save the environment (mov r8 ,(length ys)) (mov (offset rdi 1) r8) (mov r9 rdi) (add r9 16) ,@(copy-env-to-heap ys c 0) - + ;; Return a pointer to the closure (mov rax rdi) (or rax ,type-proc) @@ -153,9 +153,13 @@ (mov rax (offset rsp ,i)) ,@assert-proc (xor rax ,type-proc) - (sub rsp ,stack-size) - ,@(copy-closure-env-to-stack (add1 (length es))) - (call (offset rax 0)) + (sub rsp ,stack-size) + + (mov rcx rsp) ; start of stack in rcx + (add rcx ,(- (* 8 (+ 2 (length es))))) + ,@(copy-closure-env-to-stack) + + (call (offset rax 0)) (add rsp ,stack-size)))) ;; LExpr (Listof LExpr) CEnv -> Asm @@ -170,35 +174,38 @@ ,@(move-args (length es) i) ,@assert-proc (xor rax ,type-proc) - ,@(copy-closure-env-to-stack (length es)) + + (mov rcx rsp) ; start of stack in rcx + (add rcx ,(- (* 8 (+ 1 (length es))))) + ,@(copy-closure-env-to-stack) + + ;,@(copy-closure-env-to-stack (length es)) (jmp (offset rax 0))))) -;; Natural -> Asm -;; Copy closure's (in rax) env to stack skipping n spots -(define (copy-closure-env-to-stack n) +;; -> Asm +;; Copy closure's (in rax) env to stack in rcx +(define (copy-closure-env-to-stack) (let ((copy-loop (gensym 'copy_closure)) (copy-done (gensym 'copy_done))) `((mov r8 (offset rax 1)) ; length (mov r9 rax) (add r9 16) ; start of env - (mov rcx rsp) ; start of stack - (add rcx ,(- (* 8 (add1 n)))) ,copy-loop (cmp r8 0) - (je ,copy-done) - (mov rbx (offset r9 0)) + (je ,copy-done) + (mov rbx (offset r9 0)) (mov (offset rcx 0) rbx) (sub r8 1) (add r9 8) - (sub rcx 8) + (sub rcx 8) (jmp ,copy-loop) ,copy-done))) ;; (Listof Variable) (Listof Lambda) Expr CEnv -> Asm -(define (compile-letrec fs ls e c) +(define (compile-letrec fs ls e c) (let ((c0 (compile-letrec-λs ls c)) - (c1 (compile-letrec-init fs ls (append fs c))) - (c2 (compile-e e (append fs c)))) + (c1 (compile-letrec-init fs ls (append (reverse fs) c))) + (c2 (compile-e e (append (reverse fs) c)))) `(,@c0 ,@c1 ,@c2))) @@ -474,21 +481,3 @@ (sar rbx ,(+ 11 imm-shift)) (cmp rbx #b11011) (je err))) - -;; Symbol -> Label -;; Produce a symbol that is a valid Nasm label -(define (symbol->label s) - (string->symbol - (string-append - "label_" - (list->string - (map (λ (c) - (if (or (char<=? #\a c #\z) - (char<=? #\A c #\Z) - (char<=? #\0 c #\9) - (memq c '(#\_ #\$ #\# #\@ #\~ #\. #\?))) - c - #\_)) - (string->list (symbol->string s)))) - "_" - (number->string (eq-hash-code s) 16)))) diff --git a/www/notes/loot/interp-defun.rkt b/www/notes/loot/interp-defun.rkt index b200aaab..a24bb8e7 100644 --- a/www/notes/loot/interp-defun.rkt +++ b/www/notes/loot/interp-defun.rkt @@ -1,5 +1,6 @@ #lang racket (provide (all-defined-out)) +(require "syntax.rkt") ;; type Expr = ;; ... @@ -11,10 +12,11 @@ ;; type Function = ;; | `(closure ,Formals ,Expr ,Env) +;; | `(rec-closure ,Lambda ,(-> Env)) ;; Expr -> Answer (define (interp e) - (interp-env e '())) + (interp-env (desugar e) '())) ;; Expr REnv -> Answer (define (interp-env e r) @@ -39,6 +41,17 @@ ['err 'err] [v (interp-env e1 (ext r x v))])] + [`(letrec ,bs ,e) + (letrec ((r* (λ () + (append + (zip (map first bs) + ;; η-expansion to delay evaluating r* + ;; relies on RHSs being functions + (map (λ (l) `(rec-closure ,l ,r*)) + (map second bs))) + r)))) + (interp-env e (r*)))] + [`(λ (,xs ...) ,e) `(closure ,xs ,e ,r)] [`(,e . ,es) @@ -50,6 +63,7 @@ (define (function? f) (match f [`(closure . ,_) #t] + [`(rec-closure . ,_) #t] [_ #f])) ;; Function Value ... -> Answer @@ -58,7 +72,9 @@ [`(closure ,xs ,e ,r) (if (= (length xs) (length vs)) (interp-env e (append (zip xs vs) r)) - 'err)])) + 'err)] + [`(rec-closure (λ (,xs ...) ,e) ,r*) + (apply apply-function `(closure ,xs ,e ,(r*)) vs)])) ;; (Listof Expr) REnv -> (Listof Value) | 'err diff --git a/www/notes/loot/interp.rkt b/www/notes/loot/interp.rkt index 0248fa28..adb22d1c 100644 --- a/www/notes/loot/interp.rkt +++ b/www/notes/loot/interp.rkt @@ -1,5 +1,6 @@ #lang racket (provide (all-defined-out)) +(require "syntax.rkt") ;; type Expr = ;; ... @@ -13,7 +14,7 @@ ;; | (Values ... -> Answer) (define (interp e) - (interp-env e '())) + (interp-env (desugar e) '())) ;; Expr REnv -> Answer (define (interp-env e r) @@ -37,7 +38,17 @@ (match (interp-env e0 r) ['err 'err] [v - (interp-env e1 (ext r x v))])] + (interp-env e1 (ext r x v))])] + [`(letrec ,bs ,e) + (letrec ((r* (λ () + (append + (zip (map first bs) + ;; η-expansion to delay evaluating r* + ;; relies on RHSs being functions + (map (λ (l) (λ vs (apply (interp-env l (r*)) vs))) + (map second bs))) + r)))) + (interp-env e (r*)))] [`(λ (,xs ...) ,e) (λ vs (if (= (length vs) (length xs)) @@ -51,7 +62,6 @@ 'err)] [_ 'err])])) - ;; (Listof Expr) REnv -> (Listof Value) | 'err (define (interp-env* es r) (match es diff --git a/www/notes/loot/test/compile.rkt b/www/notes/loot/test/compile.rkt index 848db999..451e24fa 100644 --- a/www/notes/loot/test/compile.rkt +++ b/www/notes/loot/test/compile.rkt @@ -71,6 +71,7 @@ (f 5))) 5) +;; Loot tests (check-equal? (run '((λ (x) x) 7)) 7) (check-equal? (run '(((λ (x) (λ (y) x)) 7) 8)) 7) (check-equal? (run '((λ (f) (f 0)) (λ (x) (add1 x)))) 1) @@ -131,3 +132,32 @@ '()))))) '(1 -1)) +(check-equal? (run + '(let ((id (λ (x) x))) + (letrec ((even? + (λ (x) + (if (zero? x) + #t + (id (odd? (sub1 x)))))) + (odd? + (λ (x) + (if (zero? x) + #f + (id (even? (sub1 x))))))) + (even? 101)))) + #f) + +(check-equal? (run + '(let ((id (λ (x) x))) + (id (letrec ((even? + (λ (x) + (if (zero? x) + #t + (odd? (sub1 x))))) + (odd? + (λ (x) + (if (zero? x) + #f + (even? (sub1 x)))))) + (even? 101))))) + #f) diff --git a/www/notes/loot/test/interp.rkt b/www/notes/loot/test/interp.rkt index a3001b65..776a48d3 100644 --- a/www/notes/loot/test/interp.rkt +++ b/www/notes/loot/test/interp.rkt @@ -34,41 +34,40 @@ (check-equal? (run '(unbox 8)) 'err) ;; Iniquity tests - #| (check-equal? (run - '(begin (define (f x) x) - (f 5))) - 5) + '(begin (define (f x) x) + (f 5))) + 5) (check-equal? (run - '(begin (define (tri x) - (if (zero? x) - 0 - (+ x (tri (sub1 x))))) - (tri 9))) + '(begin (define (tri x) + (if (zero? x) + 0 + (+ x (tri (sub1 x))))) + (tri 9))) 45) (check-equal? (run - '(begin (define (even? x) - (if (zero? x) - #t - (odd? (sub1 x)))) - (define (odd? x) - (if (zero? x) - #f - (even? (sub1 x)))) - (even? 101))) - #f) + '(begin (define (even? x) + (if (zero? x) + #t + (odd? (sub1 x)))) + (define (odd? x) + (if (zero? x) + #f + (even? (sub1 x)))) + (even? 101))) + #f) (check-equal? (run - '(begin (define (map-add1 xs) - (if (empty? xs) - '() - (cons (add1 (car xs)) - (map-add1 (cdr xs))))) - (map-add1 (cons 1 (cons 2 (cons 3 '())))))) - '(2 3 4)) - |# + '(begin (define (map-add1 xs) + (if (empty? xs) + '() + (cons (add1 (car xs)) + (map-add1 (cdr xs))))) + (map-add1 (cons 1 (cons 2 (cons 3 '())))))) + '(2 3 4)) + ;; Loot examples @@ -86,7 +85,78 @@ 1 (+ n (tri (sub1 n))))))) 10)) - 56)) + 56) + + + (check-equal? (run + '(begin (define (map-add1 xs) + (if (empty? xs) + '() + (cons (add1 (car xs)) + (map-add1 (cdr xs))))) + (map-add1 (cons 1 (cons 2 (cons 3 '())))))) + '(2 3 4)) + (check-equal? (run '(begin (define (f x) x) + f)) + 'procedure) + (check-equal? (run '(begin (define (f x) x) + (f 5))) + 5) + + (check-equal? (run '((λ (f) (f 0)) (λ (x) (add1 x)))) 1) + (check-equal? (run '((λ (f) (f (f 0))) (λ (x) (add1 x)))) 2) + (check-equal? (run '((let ((y 8)) (car (cons (λ (x) y) '()))) 2)) 8) + (check-equal? (run '(let ((y 8)) ((car (cons (λ (x) y) '())) 2))) 8) + + (check-equal? + (run + '(begin (define (map f ls) + (if (empty? ls) + '() + (cons (f (car ls)) (map f (cdr ls))))) + + (map (λ (f) (f 0)) + (cons (λ (x) (add1 x)) + (cons (λ (x) (sub1 x)) + '()))))) + '(1 -1)) + + (check-equal? + (run + '(begin (define (map f ls) + (letrec ((mapper (λ (ls) + (if (empty? ls) + '() + (cons (f (car ls)) (mapper (cdr ls))))))) + (mapper ls))) + (map (λ (f) (f 0)) + (cons (λ (x) (add1 x)) + (cons (λ (x) (sub1 x)) + '()))))) + '(1 -1)) + + (check-equal? + (run + '(begin (define (map f ls) + (begin (define (mapper ls) + (if (empty? ls) + '() + (cons (f (car ls)) (mapper (cdr ls))))) + (mapper ls))) + (map (λ (f) (f 0)) + (cons (λ (x) (add1 x)) + (cons (λ (x) (sub1 x)) + '()))))) + '(1 -1))) + +(test-suite + (λ (e) + (match (interp e) + [(? procedure?) 'procedure] + [v v]))) -(test-suite interp) -(test-suite defun:interp) +(test-suite + (λ (e) + (match (defun:interp e) + [(? defun:function?) 'procedure] + [v v])))