;;; -*- Mode:LISP; Package:USER; Base:10; Readtable:CL; Fonts:(CPTFONT CPTFONTB) -*- 1;;; APROPOS2FILE -- use APROPOS listings to compare LISP worlds or ;;; environments. Saves info on each variable in all or selected ;;; package(s) that has a value or function binding. Use two such ;;; listings (e.g., from different revs of systems) to compare the ;;; symbol usages. Detects variables and functions that are missing ;;; from the first or added with the second version.* (defvar *apropos2list* nil "Apropos symbol main stack") (defvar *apropos2list1* nil "Apropos symbol comparison stack 1") (defvar *apropos2list2* nil "Apropos symbol comparison stack 2") (defun apropos2file(&optional default-pathname pkg &aux (filename (or (pathname-name default-pathname) "APROPOS"))) "Processes and saves output from (APROPOS \"\" PKG ...) for use in comparing symbols in different world loads/environments. APROPOS output is saved in file indicated by DEFAULT-PATHNAME; defaults to file APROPOS with current defaults. Output .TEXT file has APROPOS listing. Output .LIST file is suitable for processing by APROPOS2FILE-COMPARE. It contains a sorted list containing an entry for each interesting symbol. PKG defaults to NIL, meaning all packages; a package or list of packages may be specified. If PKG is T, then APROPOS is not run; rather, the previously saved .TEXT file indicated by DEFAULT-PATHNAME is re-read and processed. (This is useful if things crap out.)" (declare(values listfile)) (setq default-pathname (make-pathname :name filename :defaults default-pathname)) (let((textfile (make-pathname :type :text :defaults default-pathname)) (listfile (make-pathname :type "LIST" :defaults default-pathname)) (reprocess-p (eq pkg t)) (*print-pretty* nil) (*print-circle* nil) (*readtable* si:initial-common-lisp-readtable)) (unless reprocess-p ;;Write out APROPOS output (format t "~%>>> Save APROPOS output in ~a" (namestring textfile)) (with-open-file(standard-output textfile :direction :output) (apropos "" pkg :inherited nil :inheritors nil))) ;;Read back in, save interesting stuff on list (pkg, symbol, description) (format t "~%>>> ~:[P~;Rep~]rocess APROPOS output~:*~:[~; from ~a~]" reprocess-p (namestring textfile)) (setq *apropos2list* nil) (with-open-file(in textfile) (do ((line (readline in :eof) (readline in :eof))) ((eq line :eof)) (when (and line (> (string-length line) 0)) ;;Read tokens, trapping erros (multiple-value-bind (entry errorp) (catch-error (multiple-value-bind(sym desc) (with-input-from-string(ln line) (values (read ln nil :eof) (readline ln :eof))) ;;Got OK tokens (when (and sym (neq sym :eof) (stringp desc) (> (string-length desc) 0)) (push (list (package-name (symbol-package sym)) (symbol-name sym) desc (fboundp sym) (boundp sym)) *apropos2list*)))) (and (null entry) errorp (format t "%>>> Preceding error while processing ~a" line))) ))) ;;Sort list of good stuff (format t "~%>>> Sort intermediate result") (setq *apropos2list* (sort *apropos2list* #'(lambda (x y) (cond ((string-lessp (first x) (first y)) t) ((string-equal (first x) (first y)) (string-lessp (second x) (second y))) (t nil))))) ;;Write LISP list of good stuff (format t "~%>>> Save resulting list in ~a" (namestring listfile)) (with-open-file(out listfile :direction :output) (print *apropos2list* out)) listfile)) ;;; Use this if you want to sort an APROPOS .TEXT file by symbol (defun apropos2sort(&optional default-pathname &aux (filename (or (pathname-name default-pathname) "APROPOS"))) (setq default-pathname (make-pathname :name filename :defaults default-pathname)) (let((textfile (make-pathname :type :text :defaults default-pathname)) (listfile (make-pathname :type "LIST" :defaults default-pathname)) (*readtable* si:initial-common-lisp-readtable)) ;;Rewrite text dump (with-open-file(in listfile) (setq *apropos2list* (read in))) (with-open-file(out textfile :direction :output) (dolist (elt *apropos2list*) (format out "~&~{~a:~a ~a~2*~}" elt))))) ;;; Use this to read/load back in an APROPOS .LIST file (defun apropos2load(pathname) (let((*readtable* si:initial-common-lisp-readtable)) (with-open-file(in pathname) (read in)))) ;;; Define states (used to save comparison information) (defstruct (state (:type list)) pkg missing-pkg added-pkg missing-vars added-vars missing-funs added-funs) (defun apropos2print-state(state &optional (old 'old) (new 'new)) (declare(ignore old)) (let*((pkg (car state)) (missing-pkg (state-missing-pkg state)) (added-pkg (state-added-pkg state)) (missing-vars (state-missing-vars state)) (missing-funs (state-missing-funs state)) (added-vars (state-added-vars state)) (added-funs (state-added-funs state)) (changed-pkg (or missing-vars missing-funs added-vars added-funs))) (format t "~2&*** ~a package " pkg) (cond (missing-pkg (format t "missing from ~a" new)) (added-pkg (format t "added to ~a" new)) (changed-pkg (format t "some changes:~%") (format t "~@[~%--Missing vars: ~~{~45a ?~&~}~~]" missing-vars) (format t "~@[~%--Missing funs: ~~{~45a ?~&~}~~]" missing-funs) (format t "~@[~%--Added vars: ~~{~45a ?~&~}~~]" added-vars) (format t "~@[~%--Added funs: ~~{~45a ?~&~}~~]" added-funs)) (t (format t "no changes"))))) ;;; Define entries (input line entries from .LIST file) (defstruct (entry (:type list)) pkg name desc fboundp boundp) ;;; Utility routines for processing states from entries (defmacro exhaust-missing(entries state) `(let((state ,state)) (do*((rest ,entries (cdr rest)) (entry (first rest) (first rest)) (name (entry-name entry) (entry-name entry))) ((string-not-equal (state-pkg state) (entry-pkg entry)) (setq ,entries rest)) (when (entry-boundp entry) (push name (state-missing-vars state))) (when (entry-fboundp entry) (push name (state-missing-funs state)))))) (defmacro exhaust-added(entries state) `(let((state ,state)) (do*((rest ,entries (cdr rest)) (entry (first rest) (first rest)) (name (entry-name entry) (entry-name entry))) ((string-not-equal (state-pkg state) (entry-pkg entry)) (setq ,entries rest)) (when (entry-boundp entry) (push name (state-added-vars state))) (when (entry-fboundp entry) (push name (state-added-funs state)))))) (defun apropos2state(oldname old newname new &optional debug &aux state) (format t "~%+++Compare ~s to ~s~%" oldname newname) (macrolet((dump(state) `(let((oldstate ,state)) (when oldstate (apropos2print-state oldstate oldname newname)) (setq ,state (make-state))))) ;;Loop on pairs of packages (do*((oldstuff (car old) (car old)) (newstuff (car new) (car new)) (oldpkg (entry-pkg oldstuff) (entry-pkg oldstuff)) (newpkg (entry-pkg newstuff) (entry-pkg newstuff))) (nil) ;;Dump pending state, if any (dump state) (cond ((null (and old new)) ;;One or both exhausted (when debug (format t "~%Premature end of list")) (let((entries (remove-duplicates (or old new) :key #'entry-pkg :test #'string-equal))) (when debug (format t "~%???pkgs left are ~s" entries)) (dolist(entry entries) (setf (state-pkg state) (entry-pkg entry)) (if old (setf (state-missing-pkg state) t) (setf (state-added-pkg state) t)) (dump state))) ;;All done (return)) ;;At beginning of package(s) ((string-lessp oldpkg newpkg) ;;Current pkg is missing! (when debug (format t "~&Package ~s is missing" oldpkg)) (setf (state-pkg state) oldpkg) (setf (state-missing-pkg state) t) (exhaust-missing old (make-state :pkg oldpkg))) ((string-greaterp oldpkg newpkg) ;;New pkg is added! (when debug (format t "~&Package ~s is added" oldpkg)) (setf (state-pkg state) newpkg) (setf (state-added-pkg state) newpkg) (exhaust-added new (make-state :pkg newpkg))) (t ;;We have the same package! (setf (state-pkg state) oldpkg) (do*((olds old (cdr olds)) (news new (cdr news)) (oldstuff (car olds) (car olds)) (newstuff (car news) (car news)) (oldname (entry-name oldstuff) (entry-name oldstuff)) (newname (entry-name newstuff) (entry-name newstuff))) (nil) (setq old olds) (setq new news) (when debug (format t "~%---Compare entry ~~s to~&~~s" oldstuff newstuff)) (cond ((string-not-equal oldpkg (entry-pkg oldstuff)) ;;End of old package (when debug(format t "~%Done with old ~a" oldpkg)) (exhaust-added new state) (return)) ((string-not-equal newpkg (entry-pkg newstuff)) ;;End of new package (when debug (format t "~%Done with new ~a" newpkg)) (setq new news) (exhaust-missing old state) (return)) ;;Same package -- same symbol? ((string-lessp oldname newname) (when (entry-fboundp oldstuff) (push oldname (state-missing-funs state))) (when (entry-boundp oldstuff) (push oldname (state-missing-vars state))) (push newstuff news)) ((string-greaterp oldname newname) (when (entry-fboundp newstuff) (push newname (state-added-funs state))) (when (entry-boundp newstuff) (push newname (state-added-vars state))) (push oldstuff olds)) (t ;;Same symbol...any diff? (when debug (format t "~&---Same symbol ~s!" oldname)) (when (entry-fboundp oldstuff) (unless (entry-fboundp newstuff) (push oldname (state-missing-funs state)))) (when (entry-boundp oldstuff) (unless (entry-boundp newstuff) (push oldname (state-missing-vars state)))) (when (entry-fboundp newstuff) (unless (entry-fboundp oldstuff) (push newname (state-added-funs state)))) (when (entry-boundp newstuff) (unless (entry-boundp oldstuff) (push newname (state-added-vars state)))))))))))) ;;; Compare two saved listings (defun apropos2file-compare(name1 name2 &optional filename just-print) (declare(values complist)) (setq name1 (make-pathname :type "LIST" :defaults (merge-pathnames name1 name2))) (setq name2 (make-pathname :type "LIST" :defaults (merge-pathnames name2 name1))) (unless just-print (format t "~%>>> Loading ~a" (namestring name1)) (setq *apropos2list1* (apropos2load name1)) (format t "~%>>> Loading ~a" (namestring name2)) (setq *apropos2list2* (apropos2load name2))) (let((tag1 (pathname-name name1)) (tag2 (pathname-name name2)) (stream (if filename (open (setq filename (make-pathname :defaults (merge-pathnames filename name1) :type "TEXT")) :direction :output) standard-output))) (with-open-stream(standard-output stream) (apropos2state tag1 *apropos2list1* tag2 *apropos2list2*)) filename)) ;;; Testing code (defvar able) (defvar able1) (defvar able2) (defvar baker) (defvar charlie) (defvar delta) (defun init() (setq able1 (list (make-entry :pkg "ABLE" :name "DO-SOMETHING" :fboundp t) (make-entry :pkg "ABLE" :name "DO-SOMETHING-ELSE" :fboundp t))) (setq able2 (list (make-entry :pkg "ABLE" :name "SOMETHING" :boundp t) (make-entry :pkg "ABLE" :name "SOMETHING-DONE" :fboundp t) (make-entry :pkg "ABLE" :name "SOMETHING-ELSE" :boundp t))) (setq able (append able1 able2)) (setq baker (loop for orig in able as entry = (copy-list orig) do (setf (entry-pkg entry) "BAKER") collect entry)) (setq charlie (list (make-entry :pkg "CHARLIE" :name "JUNK" :boundp t) (make-entry :pkg "CHARLIE" :name "JUNK2" :boundp t))) (setq delta (list (make-entry :pkg "DELTA" :NAME "DOIT" :fboundp t) (make-entry :pkg "DELTA" :name "DOIT2" :fboundp t)))) (defun test1() (init) (let((testold (copy-tree (append able baker charlie))) (testnew (copy-tree (append able charlie delta)))) (apropos2state 'old testold 'new testnew))) (defun test2() (init) (let((testold (copy-tree (append able1 ;;SOMETHING added (cdr able2) ;;ZEALOUSLY-LEFT-OUT missing (ncons (make-entry :pkg "ABLE" :name "ZEALOUSLY-LEFT-OUT" :fboundp t)) baker (ncons (make-entry :pkg "BAKER" :name "ZEALOUSLY-LEFT-OUT" :boundp t)) ;;packages BOOBLE, BOOBLE2 missing (list (make-entry :pkg "BOOBLE" :name "SOME-STUFF" :FBOUNDP T) (make-entry :pkg "BOOBLE" :name "SOME-STUFF2" :FBOUNDP T) (make-entry :pkg "BOOBLE2" :name "SOME-STUFF" :FBOUNDP T)) charlie))) (testnew (copy-tree (append able baker charlie ;;ZOO-CHARLIE missing (list (make-entry :pkg "CHARLIE" :name "ZOO-CHARLIE-AND THE-HORSE-HE-RODE-IN-ON,WHEN I WAS JUST A BOY" :fboundp t) (make-entry :pkg "CHARLIE" :name "ZOO-CHARLIE2-AND THE-HORSE-HE-RODE-IN-ON,WHEN I WAS JUST A BOY" :fboundp t)) delta)))) (apropos2state 'old testold 'new testnew)))