host.lisp (13814 bytes)
1 ;;; host.lisp -- host side: a Lisp-to-Forth compiler and a serial driver for the 2 ;;; Mecrisp Forth that runs in QEMU. Run with: sbcl --script host.lisp <command> 3 ;;; 4 ;;; compile FILE print the Forth that FILE's defuns compile to 5 ;;; load send lisp.fs and the compiled prims.lisp, enter the target REPL 6 ;;; demo scripted transcript of all three layers 7 ;;; ktest [FILE] run the kernel word tests (default forth-tests.txt) on a fresh chip 8 ;;; suite run the vendored chibi-scheme R7RS suite (tests/r7rs/), write results.txt 9 ;;; test [FILE] run the R7RS conformance subset (default r7rs-tests.scm) on the chip 10 ;;; 11 ;;; The compiler keeps every local on the Forth data stack and tracks the stack 12 ;;; layout at compile time, so a variable reference becomes dup / over / n pick. 13 14 (require :sb-bsd-sockets) 15 (defpackage :lfl (:use :cl)) 16 (in-package :lfl) 17 18 ;;; ---------- compiler ---------- 19 20 (defvar *out*) 21 (defvar *current* nil) 22 (defparameter *void-words* '(print cr) 23 "Forth words that push nothing.") 24 25 (defun emit (&rest toks) (dolist (tok toks) (push tok *out*))) 26 (defun word (sym) (string-downcase (symbol-name sym))) 27 28 (defun compile-expr (x env) 29 "Compile X with ENV, the compile-time stack top first (NIL marks a temporary). 30 Return T when the code leaves one value on the stack." 31 (cond ((integerp x) (emit (princ-to-string x)) t) 32 ((null x) (emit "0") t) 33 ((symbolp x) 34 (let ((n (position x env))) 35 (case n 36 ((nil) (emit (word x)) (not (member x *void-words*))) 37 (0 (emit "dup") t) 38 (1 (emit "over") t) 39 (t (emit (format nil "~d pick" n)) t)))) 40 ((eq (car x) 'if) 41 (destructuring-bind (c a b) (cdr x) 42 (compile-expr c env) (emit "if") (compile-expr a env) 43 (emit "else") (compile-expr b env) (emit "then") t)) 44 ((eq (car x) 'forth) (apply #'emit (cdr x)) t) 45 (t (let ((e env)) 46 (dolist (a (cdr x)) 47 (unless (compile-expr a e) (error "~s pushes nothing" a)) 48 (push nil e))) 49 (compile-expr (if (eq (car x) *current*) 'recurse (car x)) 50 (make-list (length (cdr x)) :initial-element :arg))))) 51 52 (defun compile-defun (form) 53 "(defun name (params) body...) -> one line of Forth." 54 (destructuring-bind (name params &rest body) (cdr form) 55 (let ((*current* name) (*out* nil) (env (reverse params))) 56 (emit ":" (word name)) 57 (loop for (b . more) on body 58 do (when (and (compile-expr b env) more) (emit "drop"))) 59 (loop repeat (length params) do (emit "nip")) 60 (emit ";") 61 (format nil "~{~a~^ ~}" (reverse *out*))))) 62 63 (defun compile-form (form) 64 "Return the Forth lines for one top-level form." 65 (ecase (car form) 66 (defun (list (compile-defun form))) 67 (defprims (loop for (name fw) in (cdr form) 68 collect (format nil "' ~a defprim ~a" (word fw) (word name)))))) 69 70 (defun read-forms (path) 71 (with-open-file (s path) 72 (loop for f = (read s nil) while f collect f))) 73 74 (defun compile-file-to-forth (path) 75 (mapcan #'compile-form (read-forms path))) 76 77 ;;; ---------- serial transport (QEMU -serial tcp:127.0.0.1:PORT,server,nowait) ---------- 78 79 (defvar *sock*) 80 (defvar *io*) 81 (defvar *log* t "Echo the conversation to stdout.") 82 83 (defun connect (&optional (port (parse-integer (or (sb-ext:posix-getenv "PORT") "4444")))) 84 (loop repeat 150 85 do (handler-case 86 (let ((s (make-instance 'sb-bsd-sockets:inet-socket :type :stream :protocol :tcp))) 87 (sb-bsd-sockets:socket-connect s #(127 0 0 1) port) 88 (setf *sock* s 89 *io* (sb-bsd-sockets:socket-make-stream 90 s :input t :output t :element-type 'character 91 :buffering :none :external-format :latin-1)) 92 (return t)) 93 (sb-bsd-sockets:socket-error () (sleep 0.2))) 94 finally (error "QEMU serial port ~d not reachable" port))) 95 96 (defun read-until (done-p timeout) 97 "Read chars until (funcall done-p text) or TIMEOUT seconds. Return the text." 98 (let ((acc (make-array 0 :element-type 'character :adjustable t :fill-pointer 0)) 99 (deadline (+ (get-internal-real-time) (* timeout internal-time-units-per-second))) 100 (fd (sb-bsd-sockets:socket-file-descriptor *sock*))) 101 (loop 102 (when (funcall done-p acc) (return (remove #\Return acc))) 103 (let ((left (/ (- deadline (get-internal-real-time)) internal-time-units-per-second))) 104 (when (<= left 0) (error "timeout after ~as; target said: ~s" timeout acc)) 105 ;; Drain the stream's own buffer before sleeping on the fd. 106 (let ((c (read-char-no-hang *io* nil :eof))) 107 (cond ((eq c :eof) (error "serial connection closed")) 108 (c (vector-push-extend c acc)) 109 (t (sb-sys:wait-until-fd-usable fd :input left)))))))) 110 111 (defun ends-with (suffix s) 112 (let ((n (length s)) (m (length suffix))) 113 (and (>= n m) (string= suffix s :start2 (- n m))))) 114 115 (defun send (line) 116 "Send LINE one char at a time, waiting for the target's echo of each. QEMU's USART 117 has a one-byte receive register and Mecrisp polls it, so a burst loses characters." 118 (loop for c across line 119 do (write-char c *io*) (finish-output *io*) 120 (read-until (lambda (s) (find c s)) 2)) 121 (write-char #\Newline *io*) (finish-output *io*) 122 (show line)) 123 124 (defun show (text) 125 (when *log* (write-string text) (finish-output))) 126 127 (defun forth (line &key (timeout 5)) 128 "Send one line to the Forth interpreter and wait for its ok." 129 (send line) 130 (let ((reply (read-until (lambda (s) (or (ends-with " ok." s) 131 (search "not found." s) 132 (search "Stack" s))) 133 timeout))) 134 (show reply) (show (string #\Newline)) 135 (unless (ends-with " ok." reply) (error "Forth error: ~a" reply)) 136 (when (search "Redefine" reply) (error "~a: a word was redefined; later words may bind to the wrong one" reply)) 137 reply)) 138 139 (defun comment-start (l) 140 "Position of a Forth \\ comment word in L: a backslash at the start or after a space, 141 followed by a space or the end of the line. A backslash inside code (e.g. in .\" #\\\") stays." 142 (loop for i from 0 below (length l) 143 when (and (char= (char l i) #\\) 144 (or (zerop i) (char= (char l (1- i)) #\Space)) 145 (or (= i (1- (length l))) (char= (char l (1+ i)) #\Space))) 146 return i)) 147 148 (defun forth-lines (lines) 149 (dolist (l lines) 150 (let ((l (string-trim " " (subseq l 0 (comment-start l))))) 151 (unless (string= l "") (forth l))))) 152 153 (defun lisp-prompt (&key (timeout 5)) 154 "Read up to the target Lisp's prompt. Return the text before it." 155 (let ((reply (read-until (lambda (s) (ends-with "> " s)) timeout))) 156 (show reply) 157 (string-trim '(#\Newline #\Space) (subseq reply 0 (- (length reply) 2))))) 158 159 (defun lisp-eval (expr &key (timeout 60)) 160 "Send EXPR to the target Lisp REPL. Return the printed result." 161 (send expr) 162 (lisp-prompt :timeout timeout)) 163 164 ;;; ---------- commands ---------- 165 166 (defparameter *here* (directory-namestring (or *load-truename* "./"))) 167 (defun here (name) 168 "NAME relative to this file's directory, unless NAME is already absolute." 169 (if (and (plusp (length name)) (char= (char name 0) #\/)) name (concatenate 'string *here* name))) 170 171 (defun load-target () 172 "Layer 2 + 3: send lisp.fs, then the Forth that prims.lisp compiles to." 173 (forth "" :timeout 30) 174 (forth-lines (with-open-file (s (here "lisp.fs")) 175 (loop for l = (read-line s nil) while l collect l))) 176 (forth-lines (compile-file-to-forth (here "prims.lisp"))) 177 (forth "unused .")) 178 179 (defun file-lines (name) 180 "Lines of NAME that are not blank and not ; comments." 181 (with-open-file (s (here name)) 182 (loop for l = (read-line s nil) while l 183 for tl = (string-trim " " l) 184 unless (or (string= tl "") (char= (char tl 0) #\;)) collect tl))) 185 186 (defun enter-lisp () 187 "Enter the chip REPL and load prelude.scm, one form per line." 188 (send "lisp") 189 (lisp-prompt) 190 (let ((*log* nil)) 191 (dolist (form (split-forms (with-open-file (s (here "prelude.scm")) 192 (let ((str (make-string (file-length s)))) (subseq str 0 (read-sequence str s)))))) 193 (let* ((l (one-line form)) (r (lisp-eval l :timeout 30))) 194 (when (or (search "unbound variable" r) (search "not a " r) (search "too deep" r) (search "out of memory" r)) 195 (error "prelude: ~a gave ~a" (subseq l 0 (min 80 (length l))) r)))))) 196 197 (defun ktest (name) 198 "Layer 3: run NAME (lines of INPUT ==> OUTPUT) on a freshly booted kernel. Return failures." 199 (let ((pass 0) (fail 0)) 200 (forth "") 201 (with-open-file (s (here name)) 202 (loop for l = (read-line s nil) while l 203 for tl = (string-trim " " l) 204 unless (or (string= tl "") (char= (char tl 0) #\\)) 205 do (let* ((k (search "==>" tl)) 206 (in (string-trim " " (subseq tl 0 k))) 207 (want (and k (string-trim " " (subseq tl (+ k 3)))))) 208 (send in) 209 (let* ((r (read-until (lambda (s) (or (ends-with " ok." s) (search "not found." s) 210 (search "underflow" s))) 211 10)) 212 (r (string-trim '(#\Space #\Newline) (if (ends-with " ok." r) (subseq r 0 (- (length r) 4)) r)))) 213 (if (or (null want) (string= r want)) 214 (incf pass) 215 (progn (incf fail) (format t "~&FAIL ~a => ~s (want ~s)~%" in r want))))))) 216 (format t "~&kernel tests: ~d passed, ~d failed, ~d total~%" pass fail (+ pass fail)) 217 fail)) 218 219 (defun conformance (name) 220 "Run NAME: lines of EXPR ==> WANT, compared as printed text. Return the failure count." 221 (let ((pass 0) (fail 0)) 222 (dolist (l (file-lines name)) 223 (let* ((k (search "==>" l)) 224 (e (string-trim " " (subseq l 0 k))) 225 (want (if k (string-trim " " (subseq l (+ k 3))) nil)) 226 (got (lisp-eval e :timeout 60))) 227 (if (or (null want) (string= got want)) 228 (incf pass) 229 (progn (incf fail) (format t "FAIL ~a => ~a (want ~a)~%" e got want))))) 230 (format t "~%conformance: ~d passed, ~d failed, ~d total~%" pass fail (+ pass fail)) 231 fail)) 232 233 (defun demo () 234 (connect) 235 (format t "~%=== Layer 1: host Lisp compiles Lisp to Forth and runs it on the chip ===~%") 236 (forth "" :timeout 30) 237 (dolist (line (compile-file-to-forth (here "demo.lisp"))) 238 (format t "[compiled] ~a~%" line) 239 (forth line)) 240 (flet ((check (line want) 241 (let ((r (forth line))) 242 (unless (search want r) (error "layer 1: ~a gave ~a" line r))))) 243 (check "7 sq ." "49") (check "10 fact ." "3628800")) 244 (format t "~%=== Layer 2: load the Forth Lisp (core lisp.fs + prims compiled from prims.lisp) ===~%") 245 (let ((*log* nil)) (load-target)) 246 (format t "~a~%" (forth "unused .")) 247 (format t "~%=== Layer 3: Lisp (host) -> Forth (chip) -> Lisp (chip) ===~%") 248 (enter-lisp) 249 (let ((bad 0)) 250 (loop for (e want) in 251 '(("(+ 1 2)" "3") 252 ("(define (sq x) (* x x))" "sq") 253 ("(sq 7)" "49") 254 ("(define (fact n) (if (< n 2) 1 (* n (fact (- n 1)))))" "fact") 255 ("(fact 7)" "5040") 256 ("(cons 1 (cons 2 '()))" "(1 2)") 257 ("(car '(a b c))" "a") 258 ("(define (my-map f l) (if (null? l) '() (cons (f (car l)) (my-map f (cdr l)))))" "my-map") 259 ("(my-map sq '(1 2 3 4))" "(1 4 9 16)") 260 ("((lambda (x y) (- x y)) 10 3)" "7") 261 ("(map fact '(1 2 3 4 5))" "(1 2 6 24 120)") 262 ("(let loop ((i 0)) (if (< i 10000) (loop (+ i 1)) i))" "10000") 263 ("(eq? (car '(a)) 'a)" "#t") 264 ("(< 2 1)" "#f")) 265 do (let ((v (lisp-eval e))) 266 (format t "~a => ~a~a~%" e v (if (string= v want) "" (progn (incf bad) " <-- WRONG"))))) 267 (send "(forth)") (forth "") 268 (format t "(forth) => back at the Forth prompt~%") 269 (format t "~a~%" (forth "unused .")) 270 (if (zerop bad) 271 (format t "~%SUCCESS: all three layers answered as expected.~%") 272 (progn (format t "~%FAILED: ~d wrong answers.~%" bad) (sb-ext:exit :code 1))))) 273 274 (load (here "suite.lisp")) 275 276 (defun main (args) 277 (let ((cmd (first args))) 278 (cond ((equal cmd "compile") 279 (format t "~{~a~%~}" (compile-file-to-forth (second args)))) 280 ((equal cmd "load") 281 (connect) (load-target) (enter-lisp)) 282 ((equal cmd "demo") (demo)) 283 ((equal cmd "forth") 284 (connect) (let ((*log* nil)) (load-target)) 285 (dolist (e (cdr args)) (forth e))) 286 ((equal cmd "ktest") 287 (connect) 288 (let ((*log* nil)) (unless (zerop (ktest (or (second args) "forth-tests.txt"))) (sb-ext:exit :code 1)))) 289 ((equal cmd "suite") 290 (connect) (let ((*log* nil)) (load-target) (enter-lisp)) 291 (run-suite "tests/r7rs/")) 292 ((equal cmd "test") 293 (connect) (let ((*log* nil)) (load-target) (enter-lisp)) 294 (unless (zerop (conformance (or (second args) "r7rs-tests.scm"))) (sb-ext:exit :code 1))) 295 ((equal cmd "eval") 296 (connect) (let ((*log* nil)) (load-target) (enter-lisp)) 297 (dolist (e (cdr args)) (format t "~a => ~a~%" e (lisp-eval e)))) 298 (t (format t "usage: host.lisp compile FILE | load | demo | ktest [FILE] | suite | test [FILE] | eval EXPR... | forth LINE...~%"))))) 299 300 (handler-case (main (cdr sb-ext:*posix-argv*)) 301 (error (e) (format t "~&FAILED: ~a~%" e) (sb-ext:exit :code 1)))