summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md25
-rw-r--r--input/add-loop.asm22
-rw-r--r--input/simple.asm5
-rw-r--r--rva.asd11
-rw-r--r--src/lex.lisp108
-rw-r--r--src/main.lisp20
-rw-r--r--src/package.lisp23
-rw-r--r--src/parse.lisp55
-rw-r--r--src/util.lisp22
-rw-r--r--t/lex.lisp100
-rw-r--r--t/parse.lisp26
-rw-r--r--t/util.lisp12
12 files changed, 411 insertions, 18 deletions
diff --git a/README.md b/README.md
index d797781..aa61af6 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,23 @@
-# RISC V[ECTOR] Assembler
+# rva
+
+This is an assembler for a custom ISA nicknamed "RISC V[ECTOR]". It takes in an assembly program syntactically similar to MIPS (see input) and outputs a list of binary numbers corresponding to the instructions. The output is compatible with the [RISC V[ECTOR]](https://github.com/bdunahu/RISC-V-ECTOR-) simulator.
## Dependencies
-- SBCL
-- ASDF
-- fiveam
-- clingon
+A common-lisp implementation (SBCL) and the following libraries are required to compile:
+
+- SBCL (tested with v2.5.2)
+- ASDF (tested with v3.3.7)
+- fiveam (tested with v3.3.7)
+- clingon (tested with v0.5.0-1.f2a730f)
+- trivia (tested with v0.1-0.8b406c3)
+
+## To run
+
+Run `make` to produce a binary file in `/bin/`. To run the unit tests, run `make test`. See the make file for further options.
+
+# About
-## To compile
+Created at the University of Massachusetts, Amherst
-make
+CS535 -- Computer Architecture and ISA Design \ No newline at end of file
diff --git a/input/add-loop.asm b/input/add-loop.asm
new file mode 100644
index 0000000..6379831
--- /dev/null
+++ b/input/add-loop.asm
@@ -0,0 +1,22 @@
+ addi $fp $0 0x200
+ addi $5 $0 0x1
+ store $5 0($fp)
+ addi $5 $0 0x2
+ store $5 1($fp)
+ addi $5 $0 0x3
+ store $5 2($fp)
+ addi $5 $0 0x4
+ store $5 3($fp)
+ addi $5 $0 0x0
+ addi $6 $0 0x3
+ jrl CHECK
+LOOP:
+ add $9 $fp $5
+ load $7 -0($9)
+ load $8 +1($9)
+ add $7 $7 $8
+ store $7 0($9)
+ addi $5 $5 0x1
+CHECK:
+ cmp $6 $5
+ bgt LOOP
diff --git a/input/simple.asm b/input/simple.asm
new file mode 100644
index 0000000..3e60ee8
--- /dev/null
+++ b/input/simple.asm
@@ -0,0 +1,5 @@
+ addi $sp $0 512
+ addi $5 $0 1
+ store $5 0($sp)
+ addi $5 $0 2
+ store $5 1($sp)
diff --git a/rva.asd b/rva.asd
index 65a4262..7fdb00b 100644
--- a/rva.asd
+++ b/rva.asd
@@ -4,16 +4,19 @@
(asdf:defsystem #:rva
;; :author ""
;; :license ""
- :version "0.1"
+ :version "0.3"
:homepage "https://github.com/bdunahu/rva"
:description "Assembler for the RISC-V[ECTOR] mini-ISA."
:source-control (:git "git@github.com:bdunahu/rva.git")
:depends-on (:uiop
- :clingon)
+ :clingon
+ :trivia)
:components ((:module "src"
:serial t
:components ((:file "package")
(:file "util")
+ (:file "lex")
+ (:file "parse")
(:file "main"))))
:long-description
#.(uiop:read-file-string
@@ -33,7 +36,9 @@
:serial t
:components ((:file "package")
(:file "main")
- (:file "util"))))
+ (:file "util")
+ (:file "lex")
+ (:file "parse"))))
:perform (test-op (o s) (uiop:symbol-call :rva-tests :test-rva)))
(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
diff --git a/src/lex.lisp b/src/lex.lisp
new file mode 100644
index 0000000..5b1457d
--- /dev/null
+++ b/src/lex.lisp
@@ -0,0 +1,108 @@
+(in-package #:lex)
+
+(define-condition lexer-error (error)
+ ((message :initarg :message
+ :initform nil
+ :reader message))
+ (:report (lambda (condition stream)
+ (format stream "~A" (message condition))))
+ (:documentation "Dedicated error for an invalid lex."))
+
+(defun file->tokens (file)
+ "Opens FILE and parses returns a list of tokens, or
+NIL if the file could not be opened."
+
+ (defun read-instr (lst tokens-so-far)
+ "Collects tokens in FILE into TOKENS-SO-FAR, splitting on a newline."
+ (let ((token (read-token)))
+ (cond ((null token) (reverse tokens-so-far))
+ ((eq token 'nl)
+ (cons (reverse tokens-so-far) (read-instr nil nil)))
+ (t (read-instr lst (cons token tokens-so-far))))))
+
+ (and (probe-file file)
+ (with-open-file (*standard-input* file :direction :input)
+ (remove nil (read-instr '() '())))))
+
+(defun read-token ()
+ "Reads *STANDARD-INPUT* and returns a token, or nil if the end
+of file has been reached.
+Whitespace, commas, colons, and parentheses are token delimiters.
+Comments start with a semi-colon ';' and all tokens after are ignored."
+ (let ((chr (read-char *standard-input* nil)))
+ (cond
+ ((null chr) chr)
+
+ ((char= chr #\linefeed) 'nl)
+
+ ((whitespace-char-p chr)
+ (read-token))
+
+ ((char= chr #\;)
+ (progn (read-line *standard-input* nil)
+ 'nl))
+
+ ((char= chr #\() 'left-paren)
+ ((char= chr #\)) 'right-paren)
+
+ ((char= chr #\:) 'colon)
+ ((char= chr #\$) 'dollar)
+
+ ((char= chr #\+) 'plus)
+ ((char= chr #\-) 'minus)
+
+ ((digit-char-p chr)
+ (read-immediate chr))
+
+ ((alpha-char-p chr)
+ (read-keyword chr))
+
+ (t (error 'lexer-error
+ :message
+ (format nil "LEX failled--~a is not a valid lexical symbol.~%" chr))))))
+
+(defun read-immediate (chr)
+ "Reads a sequence of digits, in base 2, 8, 10, or 16.. Throws
+`invalid-immediate-or-keyword' error if an alphabetic character is encountered."
+ ;; may be combined with read-keyword-helper
+ (defun read-immediate-helper (chrs-so-far)
+ (let ((chr (peek-char nil *standard-input* nil)))
+ (cond ((and (not (null chr)) (digit-char-p chr))
+ (read-immediate-helper (cons (read-char *standard-input* nil) chrs-so-far)))
+ ((and (not (null chr)) (alpha-char-p chr))
+ (error 'lexer-error
+ :message
+ (format nil "LEX failed--encountered ~a while reading immediate.~%" chr)))
+ (t (reverse chrs-so-far)))))
+
+ (let* ((next (peek-char nil *standard-input* nil))
+ (radix (cond ((null next) 10)
+ ((char= next #\b) 2)
+ ((char= next #\o) 8)
+ ((char= next #\x) 16)
+ ((alpha-char-p next) nil)
+ (t 10)))
+ (arg (list chr)))
+ (when (and (char= chr #\0) radix (not (= radix 10)))
+ (read-char *standard-input* nil)
+ (setq arg '()))
+ (parse-integer (coerce (read-immediate-helper arg) 'string) :radix radix)))
+
+(defun read-keyword (chr)
+ "Reads a sequence of alphabetic characters. Throws `invalid-immediate-or-keyword'
+error if a digit is encountered."
+ ;; may be combined with read-immediate-helper
+ (defun read-keyword-helper (chrs-so-far)
+ (let ((chr (peek-char nil *standard-input* nil)))
+ (cond ((and (not (null chr)) (alpha-char-p chr))
+ (read-keyword-helper (cons (read-char *standard-input* nil) chrs-so-far)))
+ ((and (not (null chr)) (digit-char-p chr))
+ (error 'lexer-error
+ :message
+ (format nil "LEX failed--encountered ~a while reading keyword.~%" chr)))
+ (t (reverse chrs-so-far)))))
+ (coerce (read-keyword-helper (list chr)) 'string))
+
+(defun whitespace-char-p (x)
+ (or (char= #\space x)
+ (not (graphic-char-p x))))
diff --git a/src/main.lisp b/src/main.lisp
index c85e392..f6e5754 100644
--- a/src/main.lisp
+++ b/src/main.lisp
@@ -35,14 +35,22 @@ _/_/ _/_/ "
(defun driver (cmd)
"Reads in a file and directs lexing, parsing, and binary emission."
(print-splash)
- (let ((args (clingon:command-arguments cmd))
- (parse? (not (clingon:getopt cmd :lex)))
- (emit? (not (clingon:getopt cmd :parse))))
+ (let* ((args (clingon:command-arguments cmd))
+ (file (car args))
+ (parse? (not (clingon:getopt cmd :lex)))
+ (emit? (not (clingon:getopt cmd :parse))))
(cond
;; complain about num arguments
- ((/= (length args) 1) (error "Wrong number of arguments."))
- ((not (util:asm-extension? (car args))) (error "The file is not an asm source code file."))))
- (error-cli "Nitimur in Vetitum"))
+ ((/= (length args) 1) (error "Wrong number of arguments.~%"))
+ ((not (util:asm-extension? file))
+ (error "The file is not an asm source code file.~%"))
+ (t (let ((tokens (lex:file->tokens file)))
+ (if tokens
+ (progn (pprint tokens)
+ (terpri))
+ (error "The file does not exist, or it could not be opened.~%"))
+ (format t "Nitimur in Vetitum~%"))))))
+
(defun cli/command ()
"Returns a clingon command."
diff --git a/src/package.lisp b/src/package.lisp
index 9d21293..3364856 100644
--- a/src/package.lisp
+++ b/src/package.lisp
@@ -1,7 +1,26 @@
-(defpackage #:rva
+helper(defpackage #:rva
(:use #:cl)
(:export #:main))
(defpackage #:util
(:use #:cl)
- (:export #:asm-extension?))
+ (:export #:asm-extension?
+ #:format-as-binary
+ #:type-r
+ #:type-i
+ #:type-j
+ #:label-loc))
+
+(defpackage #:lex
+ (:use #:cl)
+ (:export #:lexer-error
+ #:file->tokens
+ ;; exported for testing only
+ #:read-token))
+
+(defpackage #:parse
+ (:use #:cl)
+ (:export #:parser-error
+ #:tokens->ast
+ ;; exported for testing only
+ #:extract-label))
diff --git a/src/parse.lisp b/src/parse.lisp
new file mode 100644
index 0000000..3052583
--- /dev/null
+++ b/src/parse.lisp
@@ -0,0 +1,55 @@
+helper(in-package #:parse)
+
+(define-condition parser-error (error)
+ ((message :initarg :message
+ :initform nil
+ :reader message))
+ (:report (lambda (condition stream)
+ (format stream "~A" (message condition))))
+ (:documentation "Dedicated error for an invalid parse."))
+
+(defun tokens->ast (program)
+ "Given PROGRAM, which is a list of lists of symbols,
+filters out the labels and parses."
+ ;; TODO add directives
+ (let ((program (remove nil (mapcar #'extract-label program)))
+ (i 0))
+ (mapcar (lambda (l) (extract-instruction l i)) program)))
+
+(let ((i 0))
+ (defun extract-label (line)
+ "Given a series of tokens LINE, determines if LINE is
+in the form STRING {colon}. If it is, then it is treated as a
+label, and pushed onto the stack with the line index.
+
+Note that this function is intended to be called using mapcar,
+so that labels can be added to a map and otherwise removed from
+processing."
+ (trivia:match line
+ ((list (and id (type string))
+ (satisfies (lambda (x) (equal x 'lex::colon))))
+ (progn (push (cons (read-from-string id) i) util:label-loc) nil))
+ (_ (progn (incf i) line)))))
+
+(defun extract-instruction (line i)
+ "Given instruction LINE, determines the expected type format and passes
+LINE and the index I to the the respective function."
+ ;; TODO add pseudo-ops (i.e., nop, mov, ...)
+ (let* ((type-map '((r-type . extract-r-type)
+ (i-type . extract-i-type)
+ (j-type . extract-j-type)))
+ (keyword (car line))
+ (type-fn (cdr (assoc keyword type-map))))
+ (if type-fn
+ (funcall type-fn line i)
+ (error 'parser-error
+ (format nil "PARSE failed--~a is not a known keyword.~%" (keyword))))))
+
+(defun extract-r-type (line i)
+ 'r)
+
+(defun extract-i-type (line i)
+ 'i)
+
+(defun extract-j-type (line i)
+ 'j)
diff --git a/src/util.lisp b/src/util.lisp
index 87e4df9..5edee4a 100644
--- a/src/util.lisp
+++ b/src/util.lisp
@@ -3,3 +3,25 @@
(defun asm-extension? (file)
"Returns t if FILE is extended with .asm, nil otherwise."
(string= (pathname-type file) "asm"))
+
+;; TODO this won't work for negative numbers of odd sizes quite yet.
+(defun format-as-binary (num len)
+ "Formats NUM as a binary number, and pads to LEN with zeros."
+ (declare (type number num))
+ (declare (type (integer 0 *) len))
+ (format nil "~V,'0b" len num))
+
+(defparameter type-r
+ '(ADD SUB MUL QUOT REM SFTR SFTL AND OR NOT XOR ADDV SUBV MULV DIVV CMP CEV)
+ "R-type instructions.")
+
+(defparameter type-i
+ '(LOAD LOADV ADDI SUBI SFTRI SFTLI ANDI ORI XORI STORE STOREV)
+ "I-type instructions.")
+
+(defparameter type-j
+ '(JMP JRL JAL BEQ BGT BUF BOF PUSH POP)
+ "J-type instructions.")
+
+(defparameter label-loc '()
+ "A symbol table mapping label names to line indices.")
diff --git a/t/lex.lisp b/t/lex.lisp
new file mode 100644
index 0000000..7a20608
--- /dev/null
+++ b/t/lex.lisp
@@ -0,0 +1,100 @@
+(in-package #:rva-tests)
+
+(defmacro read-this (str &body body)
+ `(let ((*standard-input* (make-string-input-stream ,str)))
+ ,@body))
+
+(def-suite lex-tests
+ :description "Test functions exported from the lexer."
+ :in all-tests)
+
+(in-suite lex-tests)
+
+(test read-token-reads-eof
+ (read-this ""
+ (is (not (lex:read-token)))))
+
+(test read-token-reads-nl
+ (read-this "
+"
+ (is (eq (lex:read-token) 'lex::nl))))
+
+(test read-token-reads-left-paren
+ (read-this "("
+ (is (eq (lex:read-token) 'lex::left-paren))))
+
+(test read-token-reads-right-paren
+ (read-this ")"
+ (is (eq (lex:read-token) 'lex::right-paren))))
+
+(test read-token-reads-left-paren
+ (read-this "$"
+ (is (eq (lex:read-token) 'lex::dollar))))
+
+(test read-token-reads-plus
+ (read-this "+"
+ (is (eq (lex:read-token) 'lex::plus))))
+
+(test read-token-reads-minus
+ (read-this "-"
+ (is (eq (lex:read-token) 'lex::minus))))
+
+(test read-token-ignores-space
+ (read-this " ("
+ (is (eq (lex:read-token) 'lex::left-paren))))
+
+(test read-token-ignores-tab
+ (read-this " ("
+ (is (eq (lex:read-token) 'lex::left-paren))))
+
+(test read-token-ignores-comment
+ (read-this "; this is a comment
+("
+ (is (eq (lex:read-token) 'lex::nl))))
+
+(test read-token-immediate-zero
+ (read-this "0"
+ (is (= (lex:read-token) 0))))
+
+(test read-token-immediate-all-digits
+ (read-this "123456789"
+ (is (= (lex:read-token) 123456789))))
+
+(test read-token-immediate-binary
+ (read-this "0b00101010"
+ (is (= (lex:read-token) 42))))
+
+(test read-token-immediate-octal
+ (read-this "0o052"
+ (is (= (lex:read-token) 42))))
+
+(test read-token-immediate-hexadecimal
+ (read-this "0x200"
+ (is (= (lex:read-token) 512))))
+
+(test read-token-immediate-invalid-immediate
+ (handler-case
+ (progn (read-this "0v0" (lex:read-token))
+ (fail))
+ (lex:lexer-error ())))
+
+;; do we want a custom error for this too?
+(test read-token-immediate-radix
+ (handler-case
+ (progn (read-this "0x" (lex:read-token))
+ (fail))
+ (sb-int:simple-parse-error ())))
+
+(test read-token-keyword-single
+ (read-this "a"
+ (is (string= (lex:read-token) "a"))))
+
+(test read-token-keyword-add
+ (read-this "addi"
+ (is (string= (lex:read-token) "addi"))))
+
+(test read-token-immediate-invalid-keyword
+ (handler-case
+ (progn (read-this "sub0" (lex:read-token))
+ (fail))
+ (lex:lexer-error ())))
diff --git a/t/parse.lisp b/t/parse.lisp
new file mode 100644
index 0000000..bd1310f
--- /dev/null
+++ b/t/parse.lisp
@@ -0,0 +1,26 @@
+(in-package #:rva-tests)
+
+(def-suite parse-tests
+ :description "Test functions exported from the parser."
+ :in all-tests)
+
+(in-suite parse-tests)
+
+(test extract-label-is-a-label
+ (is (not (parse:extract-label '("LOOP" lex::colon)))))
+
+(test extract-label-not-a-label-one
+ (let ((lst '("NICE" "TRY")))
+ (is (equal lst
+ (parse:extract-label lst)))))
+
+(test extract-label-not-a-label-two
+ (let ((lst '("LOOP" lex::colon lex::colon)))
+ (is (equal lst
+ (parse:extract-label lst)))))
+
+(test extract-line-invalid-type
+ (handler-case
+ (progn (parse:tokens->ast '(("foo" LEX::DOLLAR)))
+ (fail))
+ (lex:parser-error ())))
diff --git a/t/util.lisp b/t/util.lisp
index ef59fbb..c2dafab 100644
--- a/t/util.lisp
+++ b/t/util.lisp
@@ -14,3 +14,15 @@
(test asm-extension?-returns-true-obvious-case
(is (util:asm-extension? "quux.asm")))
+
+(test format-as-binary-unsigned-no-pad
+ (is (string= (util:format-as-binary 0 0)
+ "0")))
+
+(test format-as-binary-unsigned-no-pad-fourty-two
+ (is (string= (util:format-as-binary 42 0)
+ "101010")))
+
+(test format-as-binary-unsigned-pad-fourty-two
+ (is (string= (util:format-as-binary 42 10)
+ "0000101010")))