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


suite.lisp (13626 bytes)

1 ;;; suite.lisp -- run a vendored R7RS test suite (tests/r7rs/r7rs-tests.scm, chibi-scheme)
2 ;;; on the chip. Loaded by host.lisp.
3 ;;;
4 ;;; The host splits the file into top-level forms and sends each one as a single line.
5 ;;; tests/r7rs/shim.scm defines `test`, `test-assert`, test-begin and test-end on the chip
6 ;;; as procedures that print <<pass>> or <<fail VALUE>>. tests/r7rs/skip.scm names what
7 ;;; cannot run yet, with a reason. Every skipped test is counted. A form that fails before
8 ;;; all of its tests have reported counts the missing ones as failures.
9 
10 (in-package :lfl)
11 
12 (defun split-forms (text)
13   "Top-level data of TEXT as strings. Understands ; #| |# #; comments, strings, #\\ chars, |sym|."
14   (let ((forms '()) (i 0) (n (length text)))
15     (labels ((peek (k) (if (< (+ i k) n) (char text (+ i k)) #\Nul))
16              (delim-p (c) (or (member c '(#\Space #\Tab #\Newline #\Return #\( #\) #\" #\;)) (char= c #\Nul)))
17              (skip-ws ()
18                (loop
19                  (cond ((>= i n) (return))
20                        ((member (peek 0) '(#\Space #\Tab #\Newline #\Return #\Page)) (incf i))
21                        ((char= (peek 0) #\;) (loop until (or (>= i n) (char= (peek 0) #\Newline)) do (incf i)))
22                        ((and (char= (peek 0) #\#) (char= (peek 1) #\|))
23                         (incf i 2)
24                         (let ((depth 1))
25                           (loop while (and (< i n) (plusp depth))
26                                 do (cond ((and (char= (peek 0) #\|) (char= (peek 1) #\#)) (decf depth) (incf i 2))
27                                          ((and (char= (peek 0) #\#) (char= (peek 1) #\|)) (incf depth) (incf i 2))
28                                          (t (incf i))))))
29                        ((and (char= (peek 0) #\#) (char= (peek 1) #\;)) (incf i 2) (skip-ws) (datum))
30                        (t (return)))))
31              (datum ()
32                "Skip one datum starting at i."
33                (let ((c (peek 0)))
34                  (cond ((member c '(#\' #\` #\,))
35                         (incf i) (when (char= (peek 0) #\@) (incf i)) (skip-ws) (datum))
36                        ((char= c #\() (incf i)
37                         (loop (skip-ws)
38                               (cond ((>= i n) (return))
39                                     ((char= (peek 0) #\)) (incf i) (return))
40                                     (t (datum)))))
41                        ((char= c #\)) (incf i))      ; stray close: consume
42                        ((char= c #\") (incf i)
43                         (loop until (or (>= i n) (char= (peek 0) #\"))
44                               do (incf i (if (char= (peek 0) #\\) 2 1)))
45                         (incf i))
46                        ((char= c #\|) (incf i)
47                         (loop until (or (>= i n) (char= (peek 0) #\|))
48                               do (incf i (if (char= (peek 0) #\\) 2 1)))
49                         (incf i))
50                        ((and (char= c #\#) (char= (peek 1) #\\))
51                         (incf i 3)                  ; #\ plus at least one character
52                         (loop until (delim-p (peek 0)) do (incf i)))
53                        ((and (char= c #\#) (member (peek 1) '(#\( )))
54                         (incf i) (datum))
55                        ((and (char= c #\#) (char-equal (peek 1) #\u) (char= (peek 2) #\8) (char= (peek 3) #\())
56                         (incf i 3) (datum))
57                        (t (loop until (delim-p (peek 0)) do (incf i)))))))
58       (loop
59         (skip-ws)
60         (when (>= i n) (return))
61         (let ((start i)) (datum) (push (subseq text start i) forms))))
62     (nreverse forms)))
63 
64 (defun one-line (form)
65   "FORM on one line: newlines outside strings become spaces, inside strings \\n; comments dropped."
66   (with-output-to-string (o)
67     (let ((i 0) (n (length form)) (in-str nil) (prev-space nil))
68       (loop while (< i n) do
69         (let ((c (char form i)))
70           (cond (in-str
71                  (cond ((char= c #\\) (write-char c o) (incf i) (when (< i n) (write-char (char form i) o)))
72                        ((char= c #\") (setf in-str nil) (write-char c o))
73                        ((char= c #\Newline) (write-string "\\n" o))
74                        (t (write-char c o))))
75                 ((and (char= c #\#) (< (1+ i) n) (char= (char form (1+ i)) #\\))
76                  (write-string "#\\" o) (incf i 2) (when (< i n) (write-char (char form i) o)) (setf prev-space nil))
77                 ((char= c #\") (setf in-str t) (write-char c o) (setf prev-space nil))
78                 ((char= c #\;) (loop while (and (< i n) (char/= (char form i) #\Newline)) do (incf i)) (decf i))
79                 ((member c '(#\Space #\Tab #\Newline #\Return))
80                  (unless prev-space (write-char #\Space o)) (setf prev-space t))
81                 (t (write-char c o) (setf prev-space nil))))
82         (incf i)))))
83 
84 (defparameter *test-heads* '("test" "test-assert" "test-error" "test-values" "test-numeric-syntax"
85                              "test-precision" "test-read-error" "test-write-syntax"))
86 
87 (defun form-head (form)
88   (let* ((s (string-left-trim "(" (string-trim '(#\Space #\Newline) form)))
89          (e (or (position-if (lambda (c) (member c '(#\Space #\Newline #\( #\)))) s) (length s))))
90     (subseq s 0 e)))
91 
92 (defun nested-tests (form)
93   "Heads of the test calls inside FORM, in order."
94   (let ((heads '()) (start 0))
95     (loop
96       (let ((p (search "(test" form :start2 start)))
97         (unless p (return (nreverse heads)))
98         (let ((h (form-head (subseq form p))))
99           (when (member h *test-heads* :test #'string=) (push h heads)))
100         (setf start (1+ p))))))
101 
102 (defun read-skips (path)
103   "skip.scm entries: (section NAME REASON) (head NAME REASON) (match SUBSTRING REASON)
104 (literal KIND REASON) (nonlatin1 \"\" REASON)."
105   (with-open-file (s path)
106     (let ((*read-eval* nil))
107       (loop for e = (read s nil) while e
108             collect (list (intern (string-upcase (symbol-name (first e))) :keyword) (second e) (third e))))))
109 
110 (defun tokens (form)
111   "Identifier-like tokens of FORM (a crude split on delimiters)."
112   (let ((out '()) (start nil))
113     (loop for i from 0 to (length form)
114           for c = (if (< i (length form)) (char form i) #\Space)
115           do (if (member c '(#\Space #\Newline #\Tab #\( #\) #\' #\"))
116                  (when start (push (subseq form start i) out) (setf start nil))
117                  (unless start (setf start i))))
118     out))
119 
120 (defun defined-names (form)
121   "Names a skipped setup form would have defined: the name after define / define-syntax, or
122 every token of a define-record-type."
123   (let ((head (form-head form)) (toks (reverse (tokens form))))
124     (cond ((string= head "define-record-type")
125            ;; type, constructor, predicate, and each field's accessor/modifier -- not field names
126            (let ((f (handler-case (let ((*readtable* (copy-readtable nil)) (*read-eval* nil))
127                                     (setf (readtable-case *readtable*) :preserve)
128                                     (read-from-string form))
129                       (error () nil))))
130              (if (and (consp f) (>= (length f) 4))
131                  (mapcar #'princ-to-string
132                          (remove-if-not #'symbolp
133                                         (append (list (second f) (if (consp (third f)) (first (third f)) (third f)) (fourth f))
134                                                 (loop for fld in (nthcdr 4 f) when (consp fld) append (cdr fld)))))
135                  (cdr toks))))
136           ((member head '("define" "define-syntax" "define-values") :test #'string=)
137            (list (string-left-trim "(" (second toks))))
138           (t '()))))
139 
140 (defvar *skipped-names* '() "(name . reason) for definitions that were skipped.")
141 
142 (defun literal-kind (tok)
143   "Which number literal TOK is: :bignum (beyond 31 bits), :complex, :float, :ratio, :prefix, or NIL."
144   (let ((digits (some #'digit-char-p tok)) (n (length tok)))
145     (cond ((zerop n) nil)
146           ((and (every (lambda (c) (or (digit-char-p c) (find c "+-"))) tok) digits
147                 (let ((v (ignore-errors (parse-integer tok)))) (and v (not (<= -1073741824 v 1073741823)))))
148            :bignum)
149           ((and (> n 1) (char= (char tok 0) #\#) (find (char-downcase (char tok 1)) "eixbod")) :prefix)
150           ((member tok '("+inf.0" "-inf.0" "+nan.0" "-nan.0") :test #'string=) :float)
151           ((and (char-equal (char tok (1- n)) #\i) (or digits (member tok '("+i" "-i") :test #'string=))
152                 (every (lambda (c) (or (digit-char-p c) (find (char-downcase c) "+-./eianf"))) tok))
153            :complex)
154           ((and digits (every (lambda (c) (or (digit-char-p c) (find c "+-/"))) tok) (find #\/ tok)
155                 (> (count-if #'digit-char-p tok) 1))
156            :ratio)
157           ((and digits (every (lambda (c) (or (digit-char-p c) (find (char-downcase c) "+-.e"))) tok)
158                 (or (find #\. tok) (find #\e tok)) (digit-char-p (char tok (if (find (char tok 0) "+-.") (min 1 (1- n)) 0))))
159            :float)
160           ((and (> n 1) (char= (char tok 0) #\.) (digit-char-p (char tok 1))) :float))))
161 
162 (defun skip-reason (skips section form heads)
163   (let ((toks (tokens form)))
164     (dolist (sn *skipped-names*)
165       (when (member (car sn) toks :test #'string=)
166         (return-from skip-reason (format nil "uses ~a, skipped: ~a" (car sn) (cdr sn))))))
167   (dolist (e skips)
168     (destructuring-bind (kind what reason) e
169       (when (ecase kind
170               (:section (string= what section))
171               (:head (or (string= what (form-head form)) (member what heads :test #'string=)))
172               (:match (search what form))
173               (:nonlatin1 (some (lambda (c) (> (char-code c) 255)) form))
174               (:literal (some (lambda (tk) (eq (literal-kind tk) (intern (string-upcase what) :keyword)))
175                               (tokens form))))
176         (return reason)))))
177 
178 (defun run-suite (dir)
179   "Run DIR/r7rs-tests.scm with DIR/shim.scm and DIR/skip.scm. Write DIR/results.txt."
180   (let* ((text (with-open-file (s (here (concatenate 'string dir "r7rs-tests.scm")) :external-format :utf-8)
181                  (let ((str (make-string (file-length s)))) (subseq str 0 (read-sequence str s)))))
182          (forms (split-forms text))
183          (skips (read-skips (here (concatenate 'string dir "skip.scm"))))
184          (section "") (pass 0) (fail 0) (skip 0) (setup-skipped 0)
185          (reasons (make-hash-table :test 'equal)) (failures '()))
186     (let ((*log* nil))
187       (dolist (l (file-lines (concatenate 'string dir "shim.scm"))) (lisp-eval l)))
188     (dolist (form forms)
189       (let* ((head (form-head form)) (heads (nested-tests form)) (k (length heads)))
190         (when (string= head "test-begin")
191           (let ((q (position #\" form))) (when q (setf section (subseq form (1+ q) (position #\" form :start (1+ q)))))))
192         (let ((reason (skip-reason skips section form heads)))
193           (cond (reason
194                  (when (zerop k)
195                    (dolist (nm (defined-names form)) (push (cons nm reason) *skipped-names*)))
196                  (if (zerop k) (incf setup-skipped) (incf skip k))
197                  (incf (gethash reason reasons 0) (max k 0)))
198                 (t
199                  (let* ((line (one-line form))
200                         (out (handler-case (lisp-eval line :timeout 60)
201                                (error (e) (format nil "HOST ERROR ~a" e))))
202                         (p (count-substr "<<pass>>" out)))
203                    (incf pass p)
204                    (when (> k p)
205                      (incf fail (- k p))
206                      (push (format nil "[~a] ~a~%    => ~a" section
207                                    (subseq line 0 (min 160 (length line)))
208                                    (subseq out 0 (min 160 (length out))))
209                            failures))
210                    (when (search "HOST ERROR" out)
211                      (let ((rest (reduce #'+ (mapcar (lambda (f) (length (nested-tests f)))
212                                                      (cdr (member form forms))))))
213                        (incf fail rest)
214                        (push (format nil "ABORTED: the chip stopped answering; ~d later tests counted as failed" rest) failures))
215                      (return))))))))
216     (let ((total (+ pass fail skip)))
217       (with-open-file (o (here (concatenate 'string dir "results.txt")) :direction :output :if-exists :supersede)
218         (format o "r7rs suite: ~d passed, ~d failed, ~d skipped, ~d total~%" pass fail skip total)
219         (format o "setup forms skipped: ~d~%~%skipped tests by reason:~%" setup-skipped)
220         (maphash (lambda (r c) (format o "  ~4d  ~a~%" c r)) reasons)
221         (format o "~%failures:~%~{~a~%~}" (reverse failures)))
222       (format t "~&r7rs suite: ~d passed, ~d failed, ~d skipped, ~d total~%" pass fail skip total))))
223 
224 (defun count-substr (sub s)
225   (loop with start = 0 for p = (search sub s :start2 start) while p count t do (setf start (1+ p))))
226 
227 (defun analyze (path)
228   "Host only: list form heads and the longest one-line form."
229   (let* ((text (with-open-file (s path :external-format :utf-8)
230                  (let ((str (make-string (file-length s)))) (subseq str 0 (read-sequence str s)))))
231          (forms (split-forms text)) (counts (make-hash-table :test 'equal)) (tests 0))
232     (dolist (f forms)
233       (incf (gethash (form-head f) counts 0))
234       (incf tests (length (nested-tests f))))
235     (format t "~d top-level forms, ~d test calls, longest line ~d~%" (length forms) tests
236             (reduce #'max (mapcar (lambda (f) (length (one-line f))) forms)))
237     (maphash (lambda (h c) (format t "  ~4d ~a~%" c h)) counts)))