doc/list-exports: Executables shouldn't have language-specific names.
[sod] / doc / list-exports
diff --git a/doc/list-exports b/doc/list-exports
new file mode 100755 (executable)
index 0000000..a90c0d5
--- /dev/null
@@ -0,0 +1,880 @@
+#! /bin/sh
+":"; ### -*-lisp-*-
+":"; CL_SOURCE_REGISTRY=$(pwd)/build/src/:; export CL_SOURCE_REGISTRY
+":"; exec cl-launch -X -l "sbcl cmucl" -s asdf -i "(sod-exports::main)" -- "$0" "$@" || exit 1
+
+(cl:defpackage #:sod-exports
+  (:use #:common-lisp
+       #+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))
+       (let ((thing (cadr symbols)))
+         (if (atom thing) (list thing) thing))
+       (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)
+                               (char= (char (symbol-name arg) 0) #\&))
+                         do (setf state arg)
+                       else if (member state '(:mandatory &rest))
+                         collect (instify arg)
+                       else if (member state '(&optional &aux))
+                         collect (instify (dig arg '(0)))
+                       else if (eq state '&key)
+                         collect (instify (dig arg '(0 1)))
+                       else
+                         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
+         (symbolicate 'c- kind '-type)
+         (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))))
+      (and export
+          (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))))
+      (and export
+          (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)
+  (: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))
+    (sort (loop for s being the symbols of package
+               when (or (not (exported-symbol-p s))
+                        (gethash s externs))
+                 collect s)
+         #'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))
+         (let ((name (symbol-name symbol)))
+           (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))
+    (and (consp store)
+        (eq (car store) 'funcall)
+        (consp (cdr store)) (consp (cadr store))
+        (eq (caadr store) 'function)
+        (let ((func (cadadr store)))
+          (and (consp func) (consp (cdr func))
+               (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)
+                (eql (eql-specializer-object spec) what))))
+       (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))
+    (when (fboundp symbol)
+      (push (cond ((macro-function symbol) :macro)
+                 ((typep (fdefinition symbol) 'generic-function)
+                  :generic)
+                 (t :function))
+           things)
+      (etypecase (ignore-errors (fdefinition (list 'setf symbol)))
+       (generic-function (push :setf-generic things))
+       (function (push :setf-function things))
+       (null)))
+    (when (find-class symbol nil)
+      (push :class things))
+    (when (or (specialized-on-p #'sod:expand-c-type-spec 0 symbol)
+             (specialized-on-p #'sod:expand-c-type-form 0 symbol))
+      (push :c-type things))
+    (when (or (specialized-on-p #'sod-parser:expand-parser-spec 1 symbol)
+             (specialized-on-p #'sod-parser:expand-parser-form 1 symbol))
+      (push :parser things))
+    (when (get symbol 'optparse::opthandler)
+      (push :opthandler things))
+    (when (get symbol 'optparse::optmacro)
+      (push :optmacro things))
+    (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
+                           (sort (mapcan (lambda (sym)
+                                           (multiple-value-bind
+                                               (symbol foundp)
+                                               (find-symbol
+                                                (symbol-name sym)
+                                                package)
+                                             (and foundp (list symbol))))
+                                         (cdr assoc))
+                                 #'string< :key #'symbol-name))))
+             (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.
+  (loop with best = (package-name package)
+       with best-length = (length best)
+       for name in (package-nicknames package)
+       for name-length = (length name)
+       when (< name-length best-length)
+         do (setf best name
+                  best-length name-length)
+       finally (return best)))
+
+(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)
+        (and (eq sym symbol)
+             (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)
+                  (member char '(#\% #\+ #\- #\* #\/ #\= #\[ #\] #\?))))
+            name)
+      (string-downcase name)
+      (with-output-to-string (out)
+       (write-char #\| out)
+       (map nil (lambda (char)
+                  (when (or (char= char #\|)
+                            (char= char #\\))
+                    (write-char #\\ out))
+                  (write-char char out))
+            name)
+       (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"
+           (and exportp (eq pkg package))
+           (cond ((keywordp symbol) "")
+                 ((eq pkg nil) "#")
+                 (t (downcase-or-escape (best-package-name pkg))))
+           (or exportp (null pkg))
+           (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
+                                 (typep class 'interesting-class)
+                                 (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)
+                  (dolist (super (class-direct-superclasses class))
+                    (push class (gethash super subs))
+                    (walk-up super))
+                  (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)
+                      (mapcar (lambda (class)
+                                (pretty-symbol-name (class-name class)
+                                                    package))
+                              (remove super
+                                      (class-direct-superclasses this))))
+              (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)
+                 `(flet ((,',func (it) ,expr))
+                    (let ((,',l (,',func ,',l))
+                          (,',r (,',func ,',r)))
+                      ,@body)))
+               (update (expr)
+                 `(flet ((,',func (it) ,expr))
+                    (psetf ,',l (,',func ,',l)
+                           ,',r (,',func ,',r))))
+               (compare (expr)
+                 `(cond ((let ((left ,',l) (right ,',r)) ,expr)
+                         (return-from ,',block t))
+                        ((let ((right ,',l) (left ,',r)) ,expr)
+                         (return-from ,',block nil))))
+               (typesw (&rest clauses)
+                 (labels ((iter (clauses)
+                            (if (null clauses)
+                                'nil
+                                (destructuring-bind (type &rest body)
+                                    (car clauses)
+                                  (if (eq type t)
+                                      `(progn ,@body)
+                                      `(if (typep ,',l ',type)
+                                           (if (typep ,',r ',type)
+                                               (progn ,@body)
+                                               (return-from ,',block t))
+                                           (if (typep ,',r ',type)
+                                               (return-from ,',block nil)
+                                               ,(iter (cdr clauses)))))))))
+                   (iter clauses))))
+       (let ((,l ,left) (,r ,right))
+        (block ,block
+          ,@body)))))
+
+(defun order-specializers (la lb)
+  "Return whether specializers LA should be sorted before LB."
+
+  (deep-compare (la lb)
+    ;; 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)
+                  (dolist (method (generic-function-methods func))
+                    (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))
+               (multiple-value-bind (name knownp)
+                   (function-name-core (generic-function-name func))
+                 (when (and knownp
+                            (or (not (eq (symbol-package name) package))
+                                (gethash name externs)))
+                   (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
+              (format t "~A~%" (pretty-symbol-name name package)))
+             ((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
+                      (lambda (spec)
+                        (etypecase spec
+                          (class
+                           (let ((name (class-name spec)))
+                             (if (eq name t) "t"
+                                 (pretty-symbol-name name package))))
+                          (eql-specializer
+                           (let ((obj (eql-specializer-object spec)))
+                             (format nil "(eql ~A)"
+                                     (if (symbolp obj)
+                                         (pretty-symbol-name obj package)
+                                         obj))))))
+                      (method-specializers method))
+                     (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
+                              (mapcar #'slot-definition-name
+                                      (class-direct-slots class)))
+                             (exported (remove-if
+                                        (lambda (sym)
+                                          (or (not (symbol-package sym))
+                                              (and (not (exported-symbol-p
+                                                         sym))
+                                                   (eq (symbol-package sym)
+                                                       package))))
+                                        slot-names)))
+                        (and exported
+                             (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))
+  (dolist (assoc (sort (categorize-symbols paths package) #'string<
+                      :key (lambda (assoc)
+                             (file-namestring (car assoc)))))
+    (when (cdr assoc)
+      (format t "~A~%" (file-namestring (car assoc)))
+      (dolist (def (cdr assoc))
+       (let ((sym (car def)))
+         (format t "  ~A~@[~48T~{~(~A~)~^ ~}~]~%"
+                 (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~^, ~}~%"
+             (mapcar (lambda (name) (pretty-symbol-name name package))
+                     names))
+      (dolist (assoc alist)
+       (format t "~2T~A: ~{~A~^, ~}~%"
+               (pretty-symbol-name (car assoc) package)
+               (mapcar (lambda (name) (pretty-symbol-name name package))
+                       (cdr assoc))))
+      (terpri)))
+
+  ;; Report on classes and generic functions.
+  (format t "Classes:~%")
+  (analyse-classes package)
+  (terpri)
+  (format t "Methods:~%")
+  (analyse-generic-functions package)
+  (terpri))
+
+(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"))
+        (sod-frontend (asdf:find-system "sod-frontend"))
+        (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
+                  :if-does-not-exist :create)
+    (report-project-symbols)))
+
+#+interactive (main)
+
+;;;----- That's all, folks --------------------------------------------------