Add coverage, get to near 100
[sheep] / src / main / func.lisp
1 ; Copyright 2022 Frank Duncan (frank@consxy.com) under AGPL3.  See distributed LICENSE.txt.
2 (in-package #:sheep-func)
3
4 (defvar *doc*)
5 (defvar *prev-line*)
6 (defun peek () (car *doc*))
7 (defun next () (setf *prev-line* (pop *doc*)))
8 (defun more () (not (not *doc*)))
9 (defun prev-line () *prev-line*)
10
11 (defvar *keywords*)
12
13 (defun add-keyword (type)
14  (setf *keywords* (remove-duplicates (cons type *keywords*) :test #'string=)))
15
16 (defun fire-error (msg) (error (make-instance 'sheep:validation-failure :msg msg)))
17
18 (defun expect-blank-line ()
19  (let
20   ((prev (prev-line)))
21   (when (string/= "" (next)) (fire-error (format nil "Expected blank line after: ~A" prev)))))
22
23 (defun verify-next-line (&key optional)
24  (cond
25   ((and optional (not (more))) t)
26   ((not (more)) (fire-error (format nil "Expected line after: ~A" (prev-line))))
27   ((cl-ppcre:scan " $" (peek)) (fire-error (format nil "Can't end line with a space: ~A" (peek))))
28   ((< 120 (length (peek))) (fire-error (format nil "Longer than 120 chars: ~A" (peek))))))
29
30 (defun decompose-type (type-line)
31  (labels
32   ((decompose-symbol (atom)
33     (cond
34      ((cl-ppcre:scan " " atom) (fire-error (format nil "Symbols had spaces in it: ~A" atom)))
35      ((cl-ppcre:scan ":.*[A-Z]" atom) (fire-error (format nil "Keyword symbols must all be lower case: ~A" atom)))
36      ((cl-ppcre:scan ":.*" atom) (list :keyword atom))
37      ((cl-ppcre:scan "[a-z]" atom) (fire-error (format nil "Type symbols must all be upper case: ~A" atom)))
38      (t (list :type atom))))
39    (asterisk-type (atom)
40     (when (cl-ppcre:scan "\\*$" atom)
41      (list :asterisk (list (decompose-symbol (subseq atom 0 (1- (length atom))))))))
42    (symbol-type (def)
43     (list :symbol (list (decompose-symbol def))))
44    (or-type (def)
45     (when (cl-ppcre:scan "\\|" def)
46      (when (cl-ppcre:scan "[\\(\\)]" def) (fire-error (format nil "Or types can't have lists in them: ~A" def)))
47      (when (cl-ppcre:scan "[^ ]\\|" def)
48       (fire-error (format nil "All or pipes must be prefaced by spaces: ~A" def)))
49      (when (cl-ppcre:scan "\\|[^ ]" def)
50       (fire-error (format nil "All or pipes must be concluded by spaces: ~A" def)))
51      (list :or (mapcar #'decompose-symbol (cl-ppcre:split " \\| " def)))))
52    (list-type (def)
53     (when
54      (cl-ppcre:scan "^\\(.*\\)$" def)
55      (when (cl-ppcre:scan "\\|" def) (fire-error (format nil "List types can't have | in them: ~A" def)))
56      (when (cl-ppcre:scan "^\\(.*[\\(\\)].*\\)$" def)
57       (fire-error (format nil "List types can't have sublists: ~A" def)))
58      (when (cl-ppcre:scan "  " def) (fire-error (format nil "Lists can be seperated by only one space: ~A" def)))
59      (list
60       :list
61       (mapcar #'decompose-symbol (cl-ppcre:split " " (subseq def 1 (1- (length def)))))))))
62   (let
63    ((type-scanner (cl-ppcre:create-scanner "^  ([^ :]+): (.+)$")))
64    (when (not (cl-ppcre:scan type-scanner type-line))
65     (fire-error (format nil "Type line did not match \"  TYPE: type-definition\": ~A" type-line)))
66    (cl-ppcre:register-groups-bind (type def) (type-scanner type-line)
67     (let
68      ((decomposed-def (or (list-type def) (or-type def) (asterisk-type def) (symbol-type def))))
69      ; This error should never be fired off, as the above regexes match everything
70      (when (not decomposed-def) (fire-error (format nil "Couldn't figure out how to decompose: ~A" def)))
71      (list
72       type
73       decomposed-def
74       (remove
75        nil
76        (mapcar
77         (lambda (symb-def) (when (eql :type (car symb-def)) (cadr symb-def)))
78         (cadr decomposed-def)))))))))
79
80 (defun parse-types (types)
81  (if
82   (string/= "ARGUMENTS AND VALUES:" (peek))
83   (multiple-value-bind (processed-types args-to-be-explained) (process-types types)
84    (expect-blank-line)
85    (values (list :types processed-types) args-to-be-explained))
86   (values nil types)))
87
88 (defun process-types (remaining-types &optional processed-types args-to-be-explained)
89  (verify-next-line)
90  (cond
91   ((string= "" (peek)) (values processed-types (append args-to-be-explained remaining-types)))
92   ((not remaining-types)
93    (fire-error (format nil "Ran out of types to talk about, but got a non empty line: ~A" (peek))))
94   (t
95    (let
96     ((decomposed (decompose-type (peek))))
97     (if (string/= (car decomposed) (car remaining-types))
98      (process-types (cdr remaining-types) processed-types (append args-to-be-explained (list (car remaining-types))))
99      (progn
100       (next)
101       (process-types
102        (append (cdr remaining-types) (third decomposed))
103        (append processed-types (list (list (car decomposed) (second decomposed))))
104        args-to-be-explained)))))))
105
106 (defun description->paragraphs ()
107  (verify-next-line :optional t)
108  (let
109   ((next-line (next)))
110   (cond
111    ((not next-line) (list "")) ; Can be last section
112    ((and (string= "" next-line) (not (more))) (fire-error "Can't end with empty line"))
113    ((cl-ppcre:scan "^  [^ ].+" next-line)
114     (let
115      ((rest-of-description (description->paragraphs)))
116      (cons
117       (format nil "~A~A~A"
118        (subseq next-line 2 (length next-line))
119        (if (and (car rest-of-description) (string/= "" (car rest-of-description))) " " "")
120        (car rest-of-description))
121       (cdr rest-of-description))))
122    ((string= "" next-line)
123     (if (string= "EXAMPLES:" (peek))
124      (list "")
125      (cons "" (description->paragraphs))))
126    (t (fire-error (format nil "Got unexpected line, requires blank lines or start with two spaces: ~S" next-line))))))
127
128 (defun parse-description ()
129  (when (string/= "DESCRIPTION:" (next)) (fire-error (format nil "Expected DESCRIPTION: instead of: ~A" (prev-line))))
130  (expect-blank-line)
131  (let
132   ((paragraphs (description->paragraphs)))
133   (list :description (mapcar #'handle-text paragraphs))))
134
135 (defun process-examples ()
136  (when (more)
137   (verify-next-line :optional t)
138   (cons
139    (let
140     ((example-scanner (cl-ppcre:create-scanner "^  ([^ ].+) => (.+)$"))
141      (next-line (next)))
142     (if (not (cl-ppcre:scan example-scanner next-line))
143      (fire-error (format nil "Example line does not match \"  example => result\": ~A" next-line))
144      (cl-ppcre:register-groups-bind (example result) (example-scanner next-line)
145       (list example result))))
146    (process-examples))))
147
148 (defun parse-examples ()
149  ; This error should never be fired off, as the only way description ends is if it ran into EXAMPLES
150  (when (string/= "EXAMPLES:" (next)) (fire-error (format nil "Expected EXAMPLES: instead of: ~A" (prev-line))))
151  (expect-blank-line)
152  (list :examples (process-examples)))
153
154 ; For formatting of things like types in there
155 (defun handle-text (text)
156  (labels
157   ((inject-keywords (text remaining-keywords)
158     (if
159      (not remaining-keywords)
160      (list text)
161      (apply #'append
162       (mapcar
163        (lambda
164         (text-item)
165         (cond
166          ((not (stringp text-item)) (list text-item))
167          ((not (cl-ppcre:scan (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item)) (list text-item))
168          (t
169           (let
170            ((split-text (cl-ppcre:split (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item :limit 1000)))
171            (apply #'append
172             (list (car split-text))
173             (mapcar (lambda (ti) (list (list :keyword (car remaining-keywords)) ti)) (cdr split-text)))))))
174        (inject-keywords text (cdr remaining-keywords)))))))
175   (list :text (inject-keywords text *keywords*))))
176 ; (map
177 ; (list :text text))
178
179 (defun process-argument-and-values (args-to-be-explained)
180  (verify-next-line)
181  (cond
182   ((string= "" (peek))
183    (when args-to-be-explained
184     (fire-error (format nil "Unexplained arguments left: ~A" args-to-be-explained))))
185   ((not args-to-be-explained) (fire-error (format nil "No arguments left, but next line isn't empty: ~A" (peek))))
186   (t
187    (labels
188     ((decompose-arg (arg-line)
189       (let
190        ((arg-scanner (cl-ppcre:create-scanner "^  ([^ :]+): (.+)$")))
191        (when (not (cl-ppcre:scan arg-scanner arg-line))
192         (fire-error (format nil "Argument line did not match \"  TYPE: desc\": ~A" arg-line)))
193        (cl-ppcre:register-groups-bind (arg desc) (arg-scanner arg-line)
194         (list arg desc)))))
195     (let
196      ((decomposed (decompose-arg (next))))
197      (when
198       (string/= (car args-to-be-explained) (car decomposed))
199       (fire-error (format nil "Expected a description for ~A but got one for ~A"
200                    (car args-to-be-explained)
201                    (car decomposed))))
202      (cons
203       (list
204        (car decomposed)
205        (handle-text (cadr decomposed)))
206       (process-argument-and-values (cdr args-to-be-explained))))))))
207
208 (defun parse-arguments-and-values (args-to-be-explained)
209  (when (string/= "ARGUMENTS AND VALUES:" (next))
210   (fire-error (format nil "Expected ARGUMENTS AND VALUES: instead of: ~A" (prev-line))))
211  (expect-blank-line)
212  (let
213   ((processed-args-and-values (process-argument-and-values (remove-duplicates args-to-be-explained :test #'string=))))
214   (expect-blank-line)
215   (list :arguments-and-values processed-args-and-values)))
216
217 (defun parse-header (func)
218  (verify-next-line)
219  (let*
220   ((func-name (symbol-name func))
221    (scanner (cl-ppcre:create-scanner (format nil "~A(.*) => (.*)$" func-name))))
222   (when (not (cl-ppcre:scan scanner (peek)))
223    (fire-error (format nil "First line of ~A did not match: ~A {ARGS}* => {RESULT}*, ~A" func func-name (peek))))
224   (cl-ppcre:register-groups-bind (args result) (scanner (next))
225    (let
226     ((ast-of-start
227       (list
228        func-name
229        (mapcar
230         (lambda (arg)
231          (cond
232           ((cdr (assoc arg '(("&optional" . :&optional) ("&key" . &key) ("&rest" . &rest)) :test #'string=)))
233           ((cl-ppcre:scan "[a-z]" arg)
234            (fire-error (format nil "Argument in ~A should be all upper case: ~S" func-name arg)))
235           (t arg)))
236         (cdr (cl-ppcre:split " " args)))
237        (mapcar
238         (lambda (arg)
239          (cond
240           ((cl-ppcre:scan "[a-z]" arg)
241            (fire-error (format nil "Result in ~A should be all upper case: ~A" func-name arg)))
242           (t arg)))
243         (cl-ppcre:split ", " result)))))
244     (add-keyword func-name)
245     (expect-blank-line)
246     (values
247      (cons :function ast-of-start)
248      (remove-if-not #'stringp (append (second ast-of-start) (third ast-of-start))))))))
249
250 (defun internal-doc->ast (func doc)
251  (let
252   ((*doc* (cl-ppcre:split "\\n" doc :limit 1000))
253    (*prev-line* nil)
254    (*keywords* nil))
255   (multiple-value-bind (header types) (parse-header func)
256    (mapcar #'add-keyword types)
257    (cons header
258     (multiple-value-bind (types args-to-be-defined) (parse-types types)
259      (mapcar #'add-keyword args-to-be-defined)
260      (append
261       (when types (list types))
262       (list
263        (when args-to-be-defined (parse-arguments-and-values args-to-be-defined))
264        (parse-description))
265       (when (more) (list (parse-examples)))))))))
266
267 (defun doc->ast (func) (internal-doc->ast func (documentation func 'function)))
268
269 (defun format-text (text)
270  (format nil "~{~A~}"
271   (mapcar
272    (lambda (text)
273     (cond
274      ((stringp text) text)
275      ((and (listp text) (eql :keyword (car text))) (format nil "_~(~A~)_" (cadr text)))
276      ; This error should never get fired, as it would only if there were a bug in the conversion code
277      (t (fire-error (format nil "Don't know how to convert text: ~S" text)))))
278    (cadr text))))
279
280 (defun format-header (header)
281  (format nil "## Function **~A**
282
283 #### Syntax:
284
285 **~(~A~)** ~{_~(~A~)_ ~}=> ~{_~(~A~)_~^, ~}
286
287 "
288   (second header)
289   (second header)
290   (third header)
291   (fourth header)))
292
293 (defun format-types (types)
294  (flet
295   ((recompose-type (type)
296     (case (car type)
297      (:list (format nil "(~{~(~A~)~^ ~})" (mapcar #'cadr (cadr type))))
298      (:or (format nil "~{~(~A~)~^ | ~}" (mapcar #'cadr (cadr type))))
299      (:asterisk (format nil "~(~A~)*" (cadr (car (cadr type)))))
300      (:symbol (format nil "~(~A~)" (cadr (car (cadr type))))))))
301   (if (not types)
302    ""
303    (format nil "~{~A~%~}~%"
304     (mapcar
305      (lambda (type) (format nil "```~(~A~)::= ~A```  " (car type) (recompose-type (cadr type))))
306      (cadr types))))))
307
308 (defun format-args-and-values (args-and-values)
309  (format nil "#### Arguments and Values:~%~%~{~A~%~}~%"
310   (mapcar
311    (lambda (arg-value) (format nil "_~(~A~)_---~A  " (car arg-value) (format-text (cadr arg-value))))
312    (cadr args-and-values))))
313
314 (defun format-description (description)
315  (format nil "#### Description:~%~%~{~A~%~^~%~}" (mapcar #'format-text (cadr description))))
316
317 (defun format-examples (examples)
318  (if (not examples)
319   ""
320   (format nil "~%#### Examples:~%~%~{~A~%~}"
321    (mapcar
322     (lambda (example) (format nil "```~A``` => ```~A```  " (car example) (cadr example)))
323     (cadr examples)))))
324
325 (defun ast->md (ast)
326  (flet
327   ((get-section (name) (find name ast :key #'car)))
328   (format nil "~A~A~A~A~A"
329    (format-header (get-section :function))
330    (format-types (get-section :types))
331    (if
332     (get-section :arguments-and-values)
333     (format-args-and-values (get-section :arguments-and-values))
334     "")
335    (format-description (get-section :description))
336    (format-examples (get-section :examples)))))
337
338 (defun ast->category-name (ast)
339  (declare (ignore ast))
340  "function")
341
342 (defun ast->short-name (ast)
343  (format nil "~(~A~)" (second (find :function ast :key #'car))))
344
345 (defun ast->link (ast)
346  (format nil "function-~(~A~)" (second (find :function ast :key #'car))))
347
348 (defun ast->short-desc (ast)
349  (format-text (car (cadr (find :description ast :key #'car)))))