Skip to content

Commit

Permalink
Migrate to Racket 🚀
Browse files Browse the repository at this point in the history
This patch probably broke some stuff and made a few things even more terrible,
but I couldn't stand Chez Scheme anymore and this took definitely less time than
rewriting everything in Haskell.

Particularly,

1. racket-mode seems nice, but there is a lot more work required to get
   comfortable with this.
2. Move files around such that state like pwd is the same for the CLI and repl.
   This is required to run tests both ways for example.
3. Reload works as long as I use code from just one buffer, but I've no idea how
   to use definitions from 2 modules at the same time in REPL.
4. Fixed some warts with the Makefile, related to point 2
5. Removed the srfi BS. Racket's stdlib is far richer.
6. Racket's stricter compiler found some issues with arity mismatch in
   `primitive`, I'm starting to see benefits already.
7. `assert` is gone, should bring it back soon along with typed racket
8. Moved things around to avoid cyclic dependencies. This is an error with
   Racket unlike Chez. I miss go where everything in a folder is a module and
   its easier to organize code.
9. Macros are way more painful to than it should ever be. Either I don't get
   this, or this is vaporware oversold. I'm probably going to replace them with
   regular functions soon.

tldr; I see no reason why I'd write Scheme over Haskell ever again.
  • Loading branch information
jaseemabid committed Jul 23, 2018
1 parent 5157d8e commit a8ab1e6
Show file tree
Hide file tree
Showing 20 changed files with 277 additions and 1,287 deletions.
17 changes: 1 addition & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,8 @@ FROM ubuntu:latest
# Setup base image deps
RUN apt-get update && apt-get install -y \
build-essential \
libncurses5-dev \
libx11-dev \
wget \
racket \
&& rm -rf /var/lib/apt/lists/*

# Install chez from source
RUN cd /tmp \
&& wget -q https://github.com/cisco/ChezScheme/releases/download/v9.5/csv9.5.tar.gz

RUN cd /tmp \
&& tar -xf csv9.5.tar.gz \
&& cd csv9.5 \
&& cp /usr/include/locale.h zlib/xlocale.h \
&& ./configure \
&& make install \
&& cd - \
&& rm -rf csv9.5.tar.gz csv9.5

WORKDIR /inc
ADD . /inc
13 changes: 4 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,21 @@
# `-fno-asynchronous-unwind-tables` gets rid of all the '.cfi' directives from
# the generated asm.

src/inc: src/inc.s src/runtime.c
cd src && \
inc: /tmp/inc.s src/runtime.c
gcc -m64 \
-g3 -ggdb3 \
-fomit-frame-pointer \
-fno-asynchronous-unwind-tables \
-O0 runtime.c inc.s \
-O0 src/runtime.c /tmp/inc.s \
-o inc

.PHONY: inc
inc: src/inc

.PHONY: test
test:
cd src && echo '(test-all)' | scheme compiler.scm --quiet
cd src && racket run.rkt

.PHONY: clean
clean:
cd src && rm -f inc.s inc inc.out
rm -f inc /tmp/inc.s /tmp/inc.out

.PHONY: container
container:
Expand All @@ -41,4 +37,3 @@ container:
.PHONY: ctest
ctest: container
docker run inc make test

2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ A tiny scheme to x86 asm compiler developed incrementally as described in the
paper [An Incremental Approach to Compiler Construction][1] by Abdulaziz
Ghuloum.

The code is tested with Chez Scheme 9.5.

NOTE: Mac users should stick with Docker for now.

$ make test
Expand Down
1 change: 0 additions & 1 deletion src/.dockerignore

This file was deleted.

73 changes: 37 additions & 36 deletions src/compiler.scm → src/compiler.rkt
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
;; An incremental compiler

(library-directories "../")
#lang racket

;; List utilities like that of Haskell.
(import (srfi s1 lists))

(load "tests-driver.scm")

(load "../tests/01-integers.scm")
(load "../tests/02-immediate.scm")
(load "../tests/03-unary.scm")
(load "../tests/04-binop.scm")
(load "../tests/05-if.scm")
(load "../tests/06-let.scm")
(load "../tests/07-cons.scm")
(load "../tests/08-procedures.scm")
(load "../tests/09-strings.scm")
(load "../tests/10-cc.scm")
(provide free-vars compile-port emit-program)

;; Constants
(define wordsize 8)
Expand Down Expand Up @@ -50,8 +36,8 @@
(define shift 3)

;; Boolean values can be computed just once upfront
(define bool-f (bitwise-ior (ash 0 shift) booltag))
(define bool-t (bitwise-ior (ash 1 shift) booltag))
(define bool-f (bitwise-ior (arithmetic-shift 0 shift) booltag))
(define bool-t (bitwise-ior (arithmetic-shift 1 shift) booltag))

;; Range for fixnums
(define fixnum-bits (- (* wordsize 8) shift))
Expand All @@ -60,11 +46,11 @@

(define (immediate-rep x)
(cond
[(fixnum? x) (ash x shift)]
[(boolean? x) (if x bool-t bool-f)]
[(null? x) niltag]
[(char? x) (bitwise-ior (ash (char->integer x) shift) chartag)]
[else (error 'immediate-rep (format "Unknown form ~a" x))]))
[(fixnum? x) (arithmetic-shift x shift)]
[(boolean? x) (if x bool-t bool-f)]
[(null? x) niltag]
[(char? x) (bitwise-ior (arithmetic-shift (char->integer x) shift) chartag)]
[else (error 'immediate-rep (format "Unknown form ~a" x))]))

(define (fixnum? x)
(and (integer? x) (exact? x) (<= fxlower x fxupper)))
Expand All @@ -89,14 +75,14 @@
;; Top level environment
(define prim-env '())

(define-record-type primitive (fields name arity body))
(struct primitive (name arity body))

(define-syntax define-primitive
(syntax-rules ()
[(_ (prim-name arg* ...) b b* ...)
(let ([p (make-primitive 'prim-name
(length '(arg* ...))
(lambda (arg* ...) b b* ...))])
(let ([p (primitive 'prim-name
(length '(arg* ...))
(lambda (arg* ...) b b* ...))])
(set! prim-env (extend 'prim-name p prim-env)))]))

(define (primcall? expr)
Expand Down Expand Up @@ -139,11 +125,11 @@
(define lambda? (tagged-list 'lambda))

(define (bindings expr)
(assert (let? expr))
;; (assert (let? expr))
(second expr))

(define (body expr)
(assert (let? expr))
;; (assert (let? expr))
(cddr expr))

(define (app? env expr)
Expand All @@ -162,7 +148,7 @@
[(immediate? expr) acc]
[(symbol? expr) (if (lookup expr env) acc
(begin
(hashtable-set! acc expr #t)
(hash-set! acc expr #t)
acc))]
[(lambda? expr)
(let* ([formals (second expr)]
Expand All @@ -173,16 +159,16 @@
[env (append env fenv)])
;; In which Scheme pretends to be functional
(if (list? body)
(fold-right (lambda (e acc) (f e env acc)) acc body)
(foldr (lambda (e acc) (f e env acc)) acc body)
(f body env acc)))]
[else (if (list? expr)
(fold-right (lambda (e acc) (f e env acc)) acc expr)
(foldr (lambda (e acc) (f e env acc)) acc expr)
(f body env acc))]))

;; Ignore keywords and primitives from free variables
(let* ([keywords '((if #t) (letrec #t))]
[new-env (if (null? env) (append prim-env keywords) env)])
(vector->list (hashtable-keys (f expr new-env (make-eq-hashtable))))))
(hash-keys (f expr new-env (make-hash)))))

;; Closure conversion
;;
Expand Down Expand Up @@ -228,7 +214,7 @@
[else expr])))

(define (lift-lambda name expr)
(assert (lambda? expr))
;;(assert (lambda? expr))

(let* ([formals (second expr)]
[body (third expr)]
Expand All @@ -241,7 +227,7 @@
(set! labels (cons (list name this) labels))))

;; Handle simplest letrec to begin with
(assert (letrec? expr))
;; (assert (letrec? expr))

;; OMG! Scheme is so fucking hard to read, write or understand.
(letrec ([bindings (second expr)])
Expand All @@ -259,6 +245,21 @@

;; Codegen helpers

;; Compile port is a parameter that returns a port to write the generated asm
;; to. This can be a file or stdout.
(define compile-port
(make-parameter
(current-output-port)
(lambda (p)
(unless (output-port? p)
(error 'compile-port "Not an output port ~s" p))
p)))

(define (emit . args)
(let ([out (compile-port)])
(apply fprintf out args)
(newline out)))

(define (emit-label label)
(emit "~a:" label))

Expand Down Expand Up @@ -594,7 +595,7 @@
(emit " shl rax, ~s" shift))

(define (get-stack-ea si)
(assert (not (= si 0)))
;; (assert (not (= si 0)))
(cond
[(> si 0) (format "[rbp + ~s]" si)]
[(< si 0) (format "[rbp - ~s]" (- si))]))
Expand Down
93 changes: 93 additions & 0 deletions src/driver.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#lang racket

(require "compiler.rkt")

(provide all-tests add-tests test-all)

(define file-asm "/tmp/inc.s")
(define file-bin "inc")
(define file-out "/tmp/inc.out")

;; Collect all tests in a global variable
(define all-tests '())

;; Indirect way to get around mutating module level variables.
;;
;; tldr; `set!` is not allowed on module level variables, this function prevents
;; the error `set!: cannot mutate module-required identifier`
;; https://docs.racket-lang.org/guide/module-set.html
(define (mut! val)
(set! all-tests val))

(define-syntax add-tests
(syntax-rules (=> >>)
[(_ test-name [input x expectation] ...)
(mut! (cons '(test-name [input x expectation] ...) all-tests))]))

;; Run the compiler but send the output to a file instead
(define (compile-program expr)
;; Delete previous output file if any; existence of this file fails the test
;; consistently on docker.
(system (format "rm -f ~a ~a" file-out file-bin))
(let ([p (open-output-file file-asm #:exists 'replace)])
(parameterize ([compile-port p])
(emit-program expr))
(close-output-port p)))

(define (build)
(unless (system "make -C .. --quiet")
(error 'make "Could not build target")))

;; Execute the binary and return the output as a string
(define (execute)
(let ([command (format "../~a > ~a" file-bin file-out)])
(unless (system command)
(error 'make "Produced program exited abnormally"))
(read (open-input-file file-out))))

;; Compile, build, execute and show the result in shell. Great for devel
(define (run expr)
(compile-program expr)
(build)
(execute))

(define (test-one test-id test)
(let ([input (car test)]
[type (cadr test)]
[expectation (caddr test)])
(printf "Test ~s: ~s ..." test-id input)

(case type

;; Compile, build, execute and assert output with expectation
['=> (let ([result (run input)])
(unless (equal? expectation result)
(error 'match-test
(format "~s. expected ~s, got ~s"
test-id expectation result))))]

;; Run a unit test
['>> (let* (;; Eval + namespace is really ugly and needs to be sorted out.
[ns (module->namespace "compiler.rkt")]
[result (eval input ns)])
(unless (equal? result (eval expectation ns))
(error 'unit-test
(format "~s. expected ~s, got ~s"
test-id (eval expectation ns) result))))])
(printf " ok\n")))

(define (test-all)
(let f ([i 0] [ls all-tests])
(if (null? ls)
(printf "Passed all ~s tests\n" i)
(let ([x (car ls)] [ls (cdr ls)])
(let* ([test-name (car x)]
[tests (cdr x)]
[n (length tests)])
(printf "Performing ~a tests ...\n" test-name)
(let g ([i i] [tests tests])
(cond
[(null? tests) (f i ls)]
[else
(test-one i (car tests))
(g (add1 i) (cdr tests))])))))))
17 changes: 17 additions & 0 deletions src/run.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#lang racket

(require "../tests/01-integers.rkt"
"../tests/02-immediate.rkt"
"../tests/03-unary.rkt"
"../tests/04-binop.rkt"
"../tests/05-if.rkt"
"../tests/06-let.rkt"
"../tests/07-cons.rkt"
"../tests/08-procedures.rkt"
"../tests/09-strings.rkt"
"../tests/10-cc.rkt"

"compiler.rkt"
"driver.rkt")

(test-all)
Loading

0 comments on commit a8ab1e6

Please sign in to comment.