From 6163fb10ff55ec4e785cc41626c530f3bd481860 Mon Sep 17 00:00:00 2001 From: Mark Wooding Date: Tue, 6 Aug 2019 13:06:06 +0100 Subject: [PATCH] doc/list-exports.lisp: Document and lightly reformat. Much more pleasant. --- doc/list-exports.lisp | 399 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 372 insertions(+), 27 deletions(-) diff --git a/doc/list-exports.lisp b/doc/list-exports.lisp index 6663801..a90c0d5 100755 --- a/doc/list-exports.lisp +++ b/doc/list-exports.lisp @@ -8,22 +8,40 @@ #+cmu #:mop #+sbcl #:sb-mop)) +;; Load the target system so that we can poke about in it. (cl:in-package #:sod-exports) (eval-when (:compile-toplevel :load-toplevel :execute) (mapc #'asdf:load-system '(:sod :sod-frontend))) +;;;-------------------------------------------------------------------------- +;;; Miscelleneous utilities. + (defun symbolicate (&rest things) + "Concatenate the THINGS and turn the result into a symbol." (intern (apply #'concatenate 'string (mapcar #'string things)))) +;;;-------------------------------------------------------------------------- +;;; Determining the symbols exported by particular files. + (defun incomprehensible-form (head tail) + "Report an incomprehensible form (HEAD . TAIL)." (format *error-output* ";; incomprehensible: ~S~%" (cons head tail))) (defgeneric form-list-exports (head tail) + (:documentation + "Return a list of symbols exported by the form (HEAD . TAIL). + + This is called from `form-exports' below.") (:method (head tail) + "By default, a form exports nothing." (declare (ignore head tail)) nil)) (defmethod form-list-exports ((head (eql 'cl:export)) tail) + "Return the symbols exported by a toplevel `export' form. + + We can cope with (export 'SYMBOLS), where SYMBOLS is a symbol or a list." + (let ((symbols (car tail))) (if (and (consp symbols) (eq (car symbols) 'quote)) @@ -32,22 +50,52 @@ (incomprehensible-form head tail)))) (defmethod form-list-exports ((head (eql 'sod:definst)) tail) + "Return the symbols exported by a `form-list-exports' form. + + The syntax is: + + (definst CODE (STREAMVAR [[:export FLAG]]) ARGS + FORM*) + + If FLAG is non-nil, then we export `CODE-inst', `make-CODE-inst', and + `inst-ARG' for each argument ARG in the lambda-list ARGS. There are some + quirks in this lambda-list: + + * If we find a list (PUBLIC PRIVATE) where we expected an argument-name + symbol (but not a list), then the argument is PUBLIC. (PRIVATE is + used to name a slot in the class created by the macro, presumably + because PUBLIC on its own is a public symbol in some package.) + + * If we find a symbol %NAME, this means the same as the list (NAME + %NAME), only we recognize it even where the lambda-list syntax expects + a list." + (destructuring-bind (code (streamvar &key export) args &body body) tail (declare (ignore streamvar body)) + (and export (list* (symbolicate code '-inst) (symbolicate 'make- code '-inst) + (labels ((dig (tree path) + ;; Dig down into a TREE, following the PATH. Stop + ;; when we find an atom, or reach the end of the + ;; path. (if (or (atom tree) (null path)) tree (dig (nth (car path) tree) (cdr path)))) (cook (arg) + ;; Convert an ARG name which might start with `%'. (if (consp arg) (car arg) (let ((name (symbol-name arg))) (if (char= (char name 0) #\%) (intern (subseq name 1)) arg)))) (instify (arg) + ;; Convert ARG name into the `inst-ARG' accessor. (symbolicate 'inst- (cook arg)))) + + ;; Work through the lambda-list, keeping track of where we + ;; expect the argument symbols to be. (loop with state = :mandatory for arg in args if (and (symbolp arg) @@ -63,6 +111,14 @@ do (error "Confused by ~S." arg))))))) (defmethod form-list-exports ((head (eql 'sod::define-tagged-type)) tail) + "Return the symbols exported by a `define-tagged-type' form. + + This is a scummy internal macro in `c-types-impl.lisp'. The syntax is + + (define-tagged-type KIND DESCRIPTION) + + It exports `KIND' and `make-KIND'." + (destructuring-bind (kind what) tail (declare (ignore what)) (list kind @@ -70,6 +126,15 @@ (symbolicate 'make- kind '-type)))) (defmethod form-list-exports ((head (eql 'sod:defctype)) tail) + "Return the symbols exported by a `defctype' form. + + The syntax is: + + (defctype {NAME | (NAME SYNONYM*)} VALUE [[:export FLAG]]) + + If FLAG is non-nil, this form exports `c-type-NAME', `NAME', and all of + the `SYNONYM's." + (destructuring-bind (names value &key export) tail (declare (ignore value)) (let ((names (if (listp names) names (list names)))) @@ -77,6 +142,15 @@ (list* (symbolicate 'c-type- (car names)) names))))) (defmethod form-list-exports ((head (eql 'sod:define-simple-c-type)) tail) + "Return the symbols exported by a `define-simple-c-type' form. + + The syntax is: + + (define-simple-c-type {NAME | (NAME SYNONYM*)} TYPE [[:export FLAG]]) + + If FLAG is non-nil, this form exports `c-type-NAME', `NAME', and all of + the `SYNONYM's." + (destructuring-bind (names type &key export) tail (declare (ignore type)) (let ((names (if (listp names) names (list names)))) @@ -84,39 +158,69 @@ (list* (symbolicate 'c-type- (car names)) names))))) (defmethod form-list-exports ((head (eql 'cl:macrolet)) tail) + "Return the symbols expored by a toplevel `macrolet' form. + + Which are simply the symbols exported by its body." (mapcan #'form-exports (cdr tail))) (defmethod form-list-exports ((head (eql 'cl:eval-when)) tail) + "Return the symbols expored by a toplevel `eval-when' form. + + Which are simply the symbols exported by its body." + + ;; We don't bother checking when it'd actually be evaluated. (mapcan #'form-exports (cdr tail))) (defmethod form-list-exports ((head (eql 'cl:progn)) tail) + "Return the symbols expored by a toplevel `progn' form. + + Which are simply the symbols exported by its body." (mapcan #'form-exports tail)) (defgeneric form-exports (form) + (:documentation + "Return a list of symbols exported by a toplevel FORM.") (:method (form) nil) (:method ((form cons)) (form-list-exports (car form) (cdr form)))) -(defgeneric list-exports (thing)) +(defgeneric list-exports (thing) + (:documentation + "Return a list of symbols exported by THING.")) (defmethod list-exports ((stream stream)) + "Return a list of symbols exported by a STREAM. + + By reading it and analysing the forms." + (loop with eof = '#:eof for form = (read stream nil eof) until (eq form eof) when (consp form) nconc (form-exports form))) (defmethod list-exports ((path pathname)) + "Return a list of symbols exported by a directory PATHNAME. + + Return an alist of pairs (PATH . SYMBOL) listing each SYMBOL exported by a + PATH of the form PATHNAME/*.lisp." + (mapcar (lambda (each) (cons each (with-open-file (stream each) (list-exports stream)))) (directory (merge-pathnames path #p"*.lisp")))) (defmethod list-exports ((path string)) + "Return a list of symbols exported by a PATH string. + + By converting it into a pathname." + (list-exports (pathname path))) (defun list-exported-symbols (package) + "Return a sorted list of symbols exported by PACKAGE." (sort (loop for s being the external-symbols of package collect s) #'string< :key #'symbol-name)) (defun list-all-symbols (package) + "Return a sorted list of all symbols exported by or private to PACKAGE." (let ((externs (make-hash-table))) (dolist (sym (list-exported-symbols package)) (setf (gethash sym externs) t)) @@ -127,10 +231,23 @@ #'string< :key #'symbol-name))) (defun find-symbol-homes (paths package) + "Determine the `home' file for the symbols exported by PACKAGE. + + Return an alist of pairs (PATH . SYMBOL) listing each SYMBOL exported by a + PATH of the form PATHNAME/*.lisp where PATHNAME is a member of PATHS. Do + this by finding all the files and parsing them (somewhat superficially), + and cross-checking the result against the actual symbols exported by the + PACKAGE." + + ;; Building the alist is exactly what `list-exports' is for. The rest of + ;; this function is the cross-checking. (let* ((symbols (list-exported-symbols package)) (exports-alist (let ((*package* package)) (mapcan #'list-exports paths))) (homes (make-hash-table :test #'equal))) + + ;; Work through the alist recording where we found each symbol. Check + ;; that they're actually exported by poking at the package. (dolist (assoc exports-alist) (let ((home (car assoc))) (dolist (symbol (cdr assoc)) @@ -138,12 +255,24 @@ (unless (nth-value 1 (find-symbol name package)) (format *error-output* ";; unexported: ~S~%" symbol)) (setf (gethash name homes) home))))) + + ;; Check that all of the symbols exported by the package are accounted + ;; for in our alist. (dolist (symbol symbols) (unless (gethash (symbol-name symbol) homes) (format *error-output* ";; mysterious: ~S~%" symbol))) + + ;; We're done. exports-alist)) +;;;-------------------------------------------------------------------------- +;;; Determining the kinds of definitions attached to symbols. + (defun boring-setf-expansion-p (symbol) + "Return non-nil if SYMBOL has a trivial `setf' expansion. + + i.e., (setf (SYMBOL ...) ...) works by (funcall #'(setf SYMBOL) ...)." + (multiple-value-bind (temps args stores store fetch) (ignore-errors (get-setf-expansion (list symbol))) (declare (ignore temps args stores fetch)) @@ -156,6 +285,12 @@ (eq (car func) 'setf)))))) (defun specialized-on-p (func arg what) + "Check whether FUNC has a method specialized for the symbol WHAT. + + We assume FUNC is a (well-known) generic function. ARG is a small integer + identifying one of FUNC's mandatory arguments. Return non-nil if FUNC has + a method for which this ARG is `eql'-specialized on WHAT." + (some (lambda (method) (let ((spec (nth arg (method-specializers method)))) (and (typep spec 'eql-specializer) @@ -163,6 +298,27 @@ (generic-function-methods func))) (defun categorize (symbol) + "Determine what things SYMBOL is defined to do. + + Return a list of keywords: + + * :constant -- SYMBOL's value cell is `boundp' and `constantp' + * :variable -- SYMBOL's value cell is `boundp' but not `constantp' + * :macro -- SYMBOL's function cell is `macro-function' + * :generic -- SYMBOL's function cell is a `generic-function' + * :function -- SYMBOL's function cell is a non-generic `function' + * :setf-generic -- (setf SYMBOL) is a `generic-function' + * :setf-function -- (setf SYMBOL) is a non-generic `function' + * :class -- SYMBOL is `find-class' + * :c-type -- `expand-c-type-spec' or `expand-c-type-form' has a method + specialized on SYMBOL + * :parser -- `expand-parser-spec' or `expand-parser-form' has a method + specialized on SYMBOL + * :opthandler -- SYMBOL has an `opthandler' property + * :optmacro -- SYMBOL has an `optmacro' property + + categorizing the kinds of definitions that SYMBOL has." + (let ((things nil)) (when (boundp symbol) (push (if (constantp symbol) :constant :variable) things)) @@ -191,6 +347,11 @@ (nreverse things))) (defun categorize-symbols (paths package) + "Return a categorized list of the symbols exported by PACKAGE. + + Return an alist of PAIRS (PATH . SYMBOLS), for each PATH in PATHS, where + SYMBOLS is itself an alist (SYMBOL . KEYWORDS) listing the kinds of + definitions that SYMBOL has (see `categorize')." (mapcar (lambda (assoc) (let ((home (car assoc)) (symbols (delete-duplicates @@ -206,10 +367,13 @@ (cons home (mapcar (lambda (symbol) (cons symbol (categorize symbol))) symbols)))) - (find-symbol-homes paths package))) +;;;-------------------------------------------------------------------------- +;;; Reporting. + (defun best-package-name (package) + "Return a convenient name for PACKAGE." ;; We pick the shortest one. Strangely, there's no `find minimal thing ;; according to this valuation' function in Common Lisp. @@ -225,6 +389,9 @@ (defvar charbuf-size 0) (defun exported-symbol-p (symbol &optional (package (symbol-package symbol))) + "Return whether SYMBOL is exported by PACKAGE. + + PACKAGE default's to the SYMBOL's home package, but may be different." (and package (multiple-value-bind (sym how) (find-symbol (symbol-name symbol) package) @@ -232,6 +399,12 @@ (eq how :external))))) (defun downcase-or-escape (name) + "Return a presentable form for a symbol or package name. + + If NAME consists only of uppercase letters and ordinary punctuation, then + return NAME in lowercase; otherwise wrap it in `|...|' and escape as + necessary." + (if (every (lambda (char) (or (upper-case-p char) (digit-char-p char) @@ -249,6 +422,13 @@ (write-char #\| out)))) (defun pretty-symbol-name (symbol package) + "Return a presentable form for SYMBOL, relative to PACKAGE. + + If SYMBOL is exported by PACKAGE then just write the SYMBOL's name + otherwise prefix the name with the SYMBOL's home package name, separated + joined with one or two colons. Uninterned symbols and keywords are also + printed specially." + (let ((pkg (symbol-package symbol)) (exportp (exported-symbol-p symbol))) (format nil "~:[~A:~:[:~;~]~;~2*~]~A" @@ -260,12 +440,17 @@ (downcase-or-escape (symbol-name symbol))))) (deftype interesting-class () + "The type of `interesting' classes, which might be user-defined." '(or standard-class structure-class #.(class-name (class-of (find-class 'condition))))) (defun analyse-classes (package) + "Print a report on the classes defined by PACKAGE." + + ;; Canonify PACKAGE into a package object. (setf package (find-package package)) + (let ((classes (mapcan (lambda (symbol) (let ((class (find-class symbol nil))) (and class @@ -273,6 +458,11 @@ (list class)))) (list-exported-symbols package))) (subs (make-hash-table))) + ;; CLASSES is a list of the `interesting' classes defined by (i.e., whose + ;; names are exported by) PACKAGE. SUBS maps a class to those of its + ;; direct subclasses which are relevant to our report. + + ;; Populate the SUBS table. (let ((done (make-hash-table))) (labels ((walk-up (class) (unless (gethash class done) @@ -282,7 +472,12 @@ (setf (gethash class done) t)))) (dolist (class classes) (walk-up class)))) + (labels ((walk-down (this super depth) + ;; Recursively traverse the class graph from THIS, recalling + ;; that our parent is SUPER, and that we are DEPTH levels + ;; down. + (format t "~v,0T~A~@[ [~{~A~^ ~}]~]~%" (* 2 depth) (pretty-symbol-name (class-name this) package) @@ -294,9 +489,41 @@ (dolist (sub (sort (copy-list (gethash this subs)) #'string< :key #'class-name)) (walk-down sub this (1+ depth))))) + + ;; Print the relevant fragment of the class graph. (walk-down (find-class t) nil 0)))) (defmacro deep-compare ((left right) &body body) + "Helper macro for traversing two similar objects in parallel. + + Specifically it's good at defining complex structural ordering relations, + answering the question: is the LEFT value strictly less than the RIGHT + value. + + Evaluate the BODY forms, maintaining a pair of `cursors', initially at the + LEFT and RIGHT values. + + The following local macros are defined to do useful things. + + * (focus EXPR . BODY) -- EXPR is an expression in terms of `it': advance + each of the cursors to the result of evaluating this expression, with + `it' bound to the current cursor value, and evaluate the BODY in the + resulting environment. + + * (update EXPR) -- as `focus', but mutate the cursors rather than + binding them. + + * (compare EXPR) -- EXPR is an expression in terms of the literal + symbols `left' and `right', which returns non-nil if it thinks `left' + is (strictly) less than `right' in some sense: evaluate this both ways + round, and return if LEFT is determined to be less than or greater + than RIGHT. + + * (typesw (TYPE . BODY)*) -- process each clause in turn: if the left + cursor value has TYPE, but the right does not, then LEFT is less than + RIGHT; if the right cursor value has TYPE but the left does not, then + LEFT is greater than RIGHT; otherwise, evaluate BODY." + (let ((block (gensym "BLOCK-")) (func (gensym "FUNC-")) (l (gensym "LEFT-")) (r (gensym "RIGHT-"))) `(macrolet ((focus (expr &body body) @@ -334,44 +561,94 @@ ,@body))))) (defun order-specializers (la lb) + "Return whether specializers LA should be sorted before LB." + (deep-compare (la lb) - (loop (typesw (null (return nil))) - (focus (car it) - (typesw (eql-specializer - (focus (eql-specializer-object it) - (typesw (keyword - (compare (string< left right))) - (symbol - (focus (package-name (symbol-package it)) - (compare (string< left right))) - (compare (string< left right))) - (t - (focus (with-output-to-string (out) - (prin1 it out) - (write-char #\nul)) - (compare (string< left right))))))) - (class - (focus (class-name it) - (focus (package-name (symbol-package it)) - (compare (string< left right))) - (compare (string< left right)))) - (t - (error "unexpected things")))) - (update (cdr it))))) + ;; Iterate over the two lists. The cursors advance down the spine, and + ;; we focus on each car in turn. + + (loop + (typesw (null (return nil))) + ;; If one list reaches the end, then it's lesser; if both, they're + ;; equal. + + (focus (car it) + ;; Examine the two specializers at this position. + + (typesw (eql-specializer + (focus (eql-specializer-object it) + ;; We found an `eql' specializer. Compare the objects. + + (typesw (keyword + ;; Keywords compare by name. + + (compare (string< left right))) + + (symbol + ;; Symbols compare by package and name. + + (focus (package-name (symbol-package it)) + (compare (string< left right))) + (compare (string< left right))) + + (t + ;; Compare two other objects by comparing their + ;; string representations. + + (focus (with-output-to-string (out) + (prin1 it out) + (write-char #\nul)) + (compare (string< left right))))))) + + (class + ;; We found a class, Compare the class names. + (focus (class-name it) + (focus (package-name (symbol-package it)) + (compare (string< left right))) + (compare (string< left right)))) + + (t + ;; We found some other kind of specializer that we don't + ;; understand. + + (error "unexpected things")))) + + ;; No joy with that pair of specializers: try the next. + (update (cdr it))))) (defun analyse-generic-functions (package) + "Print a report of the generic functions and methods defined by PACKAGE." + + ;; Canonify package into a package object. (setf package (find-package package)) + (flet ((function-name-core (name) + ;; Return the underlying name for a function NAME. Specifically, + ;; if NAME is (setf THING) then the core is THING; if NAME is a + ;; symbol then the core is simply NAME; otherwise we're confused. + ;; Return a second value to say whether we got the job done. + (typecase name (symbol (values name t)) ((cons (eql setf) t) (values (cadr name) t)) (t (values nil nil))))) + (let ((methods (make-hash-table)) (functions (make-hash-table)) (externs (make-hash-table))) + ;; EXTERNS is a set of the symbols exported by PACKAGE. FUNCTIONS and + ;; METHODS are sets of generic function names (not cores), and method + ;; objects, which we've decided are worth reporting. + + ;; Collect the EXTERNS symbols. (dolist (symbol (list-exported-symbols package)) (setf (gethash symbol externs) t)) + + ;; Collect the FUNCTIONS and METHODS. (dolist (symbol (list-exported-symbols package)) + + ;; Mark the generic functions and `setf'-functions named by exported + ;; symbols as interesting, along with all of their methods. (flet ((dofunc (func) (when (typep func 'generic-function) (setf (gethash func functions) t) @@ -379,11 +656,14 @@ (setf (gethash method methods) t))))) (dofunc (and (fboundp symbol) (fdefinition symbol))) (dofunc (ignore-errors (fdefinition (list 'setf symbol))))) + + ;; For symbols whose home package is PACKAGE, and which name a class, + ;; also collect functions with methods specialized on that class, and + ;; (only) the specialized methods. (when (eq (symbol-package symbol) package) (let ((class (find-class symbol nil))) (when class - (dolist - (func (specializer-direct-generic-functions class)) + (dolist (func (specializer-direct-generic-functions class)) (multiple-value-bind (name knownp) (function-name-core (generic-function-name func)) (when (and knownp @@ -392,20 +672,30 @@ (setf (gethash func functions) t) (dolist (method (specializer-direct-methods class)) (setf (gethash method methods) t))))))))) + + ;; Print the report. (let ((funclist nil)) + + ;; Gather the functions we've decided are interesting, and sort them. (maphash (lambda (func value) (declare (ignore value)) (push func funclist)) functions) (setf funclist (sort funclist (lambda (a b) + ;; Sort by the core symbols, and order the + ;; `setf' variant after the base version. (let ((core-a (function-name-core a)) (core-b (function-name-core b))) (if (eq core-a core-b) (and (atom a) (consp b)) (string< core-a core-b)))) :key #'generic-function-name)) + (dolist (function funclist) + ;; Print out each function in turn. + + ;; Print the header line. (let ((name (generic-function-name function))) (etypecase name (symbol @@ -413,10 +703,13 @@ ((cons (eql setf) t) (format t "(setf ~A)~%" (pretty-symbol-name (cadr name) package))))) + + ;; Report on the function's (interesting) methods. (dolist (method (sort (copy-list (generic-function-methods function)) #'order-specializers :key #'method-specializers)) + (when (gethash method methods) (format t "~2T~{~A~^ ~}~@[ [~{~(~S~)~^ ~}]~]~%" (mapcar @@ -436,13 +729,30 @@ (method-qualifiers method))))))))) (defun check-slot-names (package) + "Check that PACKAGE defines no slots whose names are exported symbols. + + This acts to discourage the use of `slot-value' by external callers. + Return two values: + + * an alist of entries (CLASS . SLOT-NAMES), listing for each offending + class, whose of its slot names which are either (a) external or (b) + from a foreign package; and + + * the distilled list of bad SLOT-NAMES." + + ;; Canonify PACKAGE into a package objects. (setf package (find-package package)) + (let* ((symbols (list-all-symbols package)) + + ;; Determine all of the named classes. (classes (mapcan (lambda (symbol) (when (eq (symbol-package symbol) package) (let ((class (find-class symbol nil))) (and class (list class))))) symbols)) + + ;; Build the main alist of offending classes and slots. (offenders (mapcan (lambda (class) (let* ((slot-names @@ -460,13 +770,22 @@ (list (cons (class-name class) exported))))) classes)) + + ;; Distill the bad slot names into a separate list. (bad-words (remove-duplicates (mapcan (lambda (list) (copy-list (cdr list))) offenders)))) + + ;; Done. (values offenders bad-words))) (defun report-symbols (paths package) + "Report on all of the symbols defined in PACKAGE by the files in PATHS." + + ;; Canonify PACKAGE to a package object. (setf package (find-package package)) + + ;; Print the breakdown of symbols by source file, with their purposes. (format t "~A~%Package `~(~A~)'~2%" (make-string 77 :initial-element #\-) (package-name package)) @@ -481,6 +800,8 @@ (pretty-symbol-name sym package) (cdr def)))) (terpri))) + + ;; Report on leaked slot names, if any are exported or foreign. (multiple-value-bind (alist names) (check-slot-names package) (when names (format t "Leaked slot names: ~{~A~^, ~}~%" @@ -492,6 +813,8 @@ (mapcar (lambda (name) (pretty-symbol-name name package)) (cdr assoc)))) (terpri))) + + ;; Report on classes and generic functions. (format t "Classes:~%") (analyse-classes package) (terpri) @@ -501,17 +824,31 @@ (export 'report-project-symbols) (defun report-project-symbols () + "Write to `*standard-output*' a report on all of the symbols in Sod." + (labels ((components (comp) + ;; Return the subcomponents of an ASDF component. + (asdf:component-children comp)) + (files (comp) + ;; Return a list of files needed by an ASDF component. + (sort (remove-if-not (lambda (comp) (typep comp 'asdf:cl-source-file)) (components comp)) #'string< :key #'asdf:component-name)) + (by-name (comp name) + ;; Find the subcomponent called NAME of an ASDF component. + (gethash name (asdf:component-children-by-name comp))) + (file-name (file) + ;; Return the pathname of an ASDF file component. + (slot-value file 'asdf/component:absolute-pathname))) + (let* ((sod (asdf:find-system "sod")) (parser-files (files (by-name sod "parser"))) (utilities (by-name sod "utilities")) @@ -519,13 +856,19 @@ (optparse (by-name sod "optparse")) (frontend (by-name sod-frontend "frontend")) (sod-files (set-difference (files sod) (list optparse utilities)))) + + ;; Report on the various major pieces of the project. (report-symbols (mapcar #'file-name sod-files) "SOD") (report-symbols (mapcar #'file-name (list frontend)) "SOD-FRONTEND") (report-symbols (mapcar #'file-name parser-files) "SOD-PARSER") (report-symbols (mapcar #'file-name (list optparse)) "OPTPARSE") (report-symbols (mapcar #'file-name (list utilities)) "SOD-UTILITIES")))) +;;;-------------------------------------------------------------------------- +;;; Command-line use. + (defun main () + "Write a report to `doc/SYMBOLS'." (with-open-file (*standard-output* #p"doc/SYMBOLS" :direction :output :if-exists :supersede @@ -533,3 +876,5 @@ (report-project-symbols))) #+interactive (main) + +;;;----- That's all, folks -------------------------------------------------- -- 2.11.0