Recently Written · git

forthlisp

Scheme on a from-scratch Forth on from-scratch machine code: core (host Lisp, Scheme in Forth, R7RS harness)

git clone https://github.com/equwal/forthlisp

Log | Files | Refs


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)))