1.1--- a/lisp/ffi/btrfs.asd Mon Oct 16 19:33:42 2023 -0400
1.2+++ b/lisp/ffi/btrfs.asd Mon Oct 16 22:25:50 2023 -0400
1.3@@ -9,7 +9,7 @@
1.4 :maintainer "ellis <ellis@rwest.io>"
1.5 :homepage "https://nas-t.net"
1.6 :bug-tracker "https://lab.rwest.io/comp/core/issues"
1.7- :depends-on (:macs :sxp)
1.8+ :depends-on (:std :sxp)
1.9 :in-order-to ((test-op (test-op "btrfs/tests")))
1.10 :components ((:file "btrfs/btrfs")))))
1.11
2.1--- a/lisp/lib/cli.asd Mon Oct 16 19:33:42 2023 -0400
2.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2.3@@ -1,14 +0,0 @@
2.4-(defsystem "cli"
2.5- :version "0.1.0"
2.6- :author "ellis <ellis@rwest.io>"
2.7- :description "command-line ui framework"
2.8- :bug-tracker "https://lab.rwest.io/ellis/macs/issues"
2.9- :source-control (:hg "https://lab.rwest.io/ellis/macs")
2.10- :depends-on (:macs :sxp)
2.11- :in-order-to ((test-op (test-op "cli/tests")))
2.12- :components ((:file "cli/cli")))
2.13-
2.14-(defsystem :cli/tests
2.15- :depends-on (:macs :rt :cli)
2.16- :components ((:file "cli/tests"))
2.17- :perform (test-op (op c) (uiop:symbol-call '#:rt '#:do-tests :cli)))
3.1--- a/lisp/lib/cli/cli.lisp Mon Oct 16 19:33:42 2023 -0400
3.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
3.3@@ -1,781 +0,0 @@
3.4-;;; cli.lisp --- cli programming api and utils
3.5-
3.6-;; This package contains a simple api and macros for building lisp CLI
3.7-;; programs.
3.8-
3.9-;;; Commentary:
3.10-
3.11-;; - inspired by: clingon, uiop
3.12-
3.13-;; Basic assumptions at runtime:
3.14-;; - running in a POSIX-compliant shell
3.15-;; - output stream supports UTF-8
3.16-
3.17-;; TODO 2023-10-14: install-ast, install-thunk, proc-args, etc should
3.18-;; return IR types - CLI-IR THUNK and CLI-IR respectively.
3.19-
3.20-;; TODO 2023-10-14: rename cli-ast to cli-ir, install-ast to
3.21-;; install-ir, etc.
3.22-
3.23-;;; Code:
3.24-(pkg:defpkg :cli
3.25- (:use :cl :sym :cond :fu :str :ana :fmt :log)
3.26- (:import-from :ana :alet)
3.27- (:import-from :uiop :println)
3.28- (:import-from :sb-posix :filename-designator)
3.29- (:import-from :sb-ext :parse-native-namestring)
3.30- (:shadowing-import-from :sb-ext :exit)
3.31- (:export
3.32- :*argv*
3.33- :init-args
3.34- :cli-arg0
3.35- :cli-args
3.36- :command-line-args
3.37- :*cli-group-separator*
3.38- :*cli-opt-kinds*
3.39- :global-opt-p
3.40- :exec-path-list
3.41- :argp
3.42- :$val
3.43- :$args
3.44- :$argc
3.45- :$opts
3.46- :$optc
3.47- :make-shorty
3.48- :with-cli-handlers
3.49- :completing-read
3.50- :make-prompt!
3.51- :defmain
3.52- :main
3.53- :with-cli
3.54- :make-cli
3.55- ;; opt-parsers
3.56- :make-opt-parser
3.57- :parse-bool-opt
3.58- :parse-str-opt
3.59- :parse-form-opt
3.60- :parse-list-opt
3.61- :parse-sym-opt
3.62- :parse-key-opt
3.63- :parse-num-opt
3.64- :parse-str-opt
3.65- :parse-file-opt
3.66- :parse-dir-opt
3.67- :make-opts
3.68- :make-cmds
3.69- :active-opts
3.70- :active-cmds
3.71- :proc-args
3.72- :make-cli-node
3.73- :make-cli-ast
3.74- :proc-args
3.75- :parse-args
3.76- :debug-opts
3.77- :do-cmd
3.78- :do-opt
3.79- :call-opt
3.80- :call-cmd
3.81- :apply-cmd
3.82- :print-help
3.83- :print-version
3.84- :print-usage
3.85- :handle-unknown-argument
3.86- :handle-missing-argument
3.87- :handle-invalid-argument
3.88- :cli-opt
3.89- :cli-val
3.90- :cli-cmd-args
3.91- :cli-cmd
3.92- :cli-cwd
3.93- :find-cmd
3.94- :find-opt
3.95- :find-short-opt
3.96- :install-ast
3.97- ;; :gen-cli-thunk
3.98- :install-thunk
3.99- :cli
3.100- :cli-equal
3.101- :defopt
3.102- :defcmd
3.103- :define-cli
3.104- ;; ast types
3.105- :opt
3.106- :cmd
3.107- :arg
3.108- :cli-name
3.109- :cli-opts
3.110- :cli-cmds
3.111- :cli-thunk
3.112- :cli-description
3.113- :cli-version
3.114- :cli-usage))
3.115-
3.116-(in-package :cli)
3.117-
3.118-(defun cli-arg0 () (car sb-ext:*posix-argv*))
3.119-(defun cli-args () (cdr sb-ext:*posix-argv*))
3.120-
3.121-(declaim (inline exec-path-list))
3.122-(defun exec-path-list ()
3.123- (let ((var (sb-posix:getenv "PATH")))
3.124- (mapcar #'directory
3.125- (loop for i = 0 then (1+ j)
3.126- as j = (position #\: var :start i)
3.127- collect (subseq var i j)
3.128- while j))))
3.129-
3.130-(defparameter *cli-group-separator*
3.131- "--"
3.132- "A marker specifying the end of a unique group of CLI args.")
3.133-
3.134-;; uiop:command-line-arguments
3.135-
3.136-;;; Macros
3.137-(defmacro argp (arg &optional (args (cli-args)))
3.138- "Test for presence of ARG in ARGS. Return the tail of
3.139-ARGS starting from the position of ARG."
3.140- `(member ,arg ,args :test #'string=))
3.141-
3.142-(defmacro make-shorty (name)
3.143- "Return the first char of symbol or string NAME."
3.144- `(character (aref (if (stringp ,name) ,name (symbol-name ,name)) 0)))
3.145-
3.146-;; (defun treat-as-argument (condition)
3.147-;; "A handler which can be used to invoke the `treat-as-argument' restart"
3.148-;; (invoke-restart (find-restart 'treat-as-argument condition)))
3.149-
3.150-;; (defun discard-argument (condition)
3.151-;; "A handler which can be used to invoke the `discard-argument' restart"
3.152-;; (invoke-restart (find-restart 'discard-argument condition)))
3.153-
3.154-(defmacro with-cli-handlers (form)
3.155- "A wrapper which handles common cli errors that may occur during
3.156-evaluation of FORM."
3.157- `(handler-case ,form
3.158- (sb-sys:interactive-interrupt ()
3.159- (format *error-output* "~&(:SIGINT)~&")
3.160- (exit :code 130))
3.161- (error (c)
3.162- (format *error-output* "~&~A~&" c)
3.163- (exit :code 1))))
3.164-
3.165-(defun init-args () (setq *argv* (cons (cli-arg0) (cli-args))))
3.166-
3.167-(defmacro with-cli (slots cli &body body)
3.168- "Like with-slots with some extra bindings."
3.169- ;; (with-gensyms (cli-body)
3.170- ;; (let ((cli-body (mapcar (lambda (x) ()) cli-body)
3.171- `(progn
3.172- (init-args)
3.173- (setf (cli-cwd ,cli) (sb-posix:getcwd))
3.174- (with-slots ,slots (parse-args ,cli *argv* :compile t)
3.175- ,@body)))
3.176-
3.177-;;; Prompts
3.178-(declaim (inline completing-read))
3.179-(defun completing-read (prompt collection
3.180- &key (history nil) (default nil)
3.181- (key nil) (test nil))
3.182-
3.183- "A simplified COMPLETING-READ for common-lisp.
3.184-
3.185-The Emacs completion framework includes a function called
3.186-`completing-read' which prompts the user for input from the
3.187-mini-buffer. It is a very flexible interface which can be used to read
3.188-user input programatically. This is incredibly useful for building
3.189-data entry interfaces -- for example see the `make-prompt!' macro.
3.190-
3.191-Obviously writing a completion framework is out-of-scope, but we can
3.192-simulate one by embedding a DSL in our prompters if we choose. For
3.193-example, perhaps we treat a single '?' character as a request from the
3.194-user to list valid options while continue waiting for input."
3.195- (princ prompt)
3.196- ;; ensure we empty internal buffer
3.197- (finish-output)
3.198- (let* ((coll (symbol-value collection))
3.199- (r (if coll
3.200- (find (read-line) coll :key key :test test)
3.201- (or (read-line) default))))
3.202- (prog1
3.203- r
3.204- (setf (symbol-value history) (push r history)))))
3.205-
3.206-(defmacro make-prompt! (var &optional prompt)
3.207- "Generate a 'prompter' from list or variable VAR and optional
3.208-PROMPT string.
3.209-
3.210-This isn't an ideal solution as it does in fact expose a dynamic
3.211-variable (VAR-prompt-history). We should generate accessors and
3.212-keep the variables within lexical scope of the generated
3.213-closure."
3.214- (with-gensyms (s p h)
3.215- `(let ((,s (if (boundp ',var) (symbol-value ',var)
3.216- (progn
3.217- (defvar ,(symb var) nil)
3.218- ',(symb var))))
3.219- (,p (when (stringp ,prompt) ,prompt)) ;; prompt string
3.220- (,h ',(symb var '-prompt-history))) ;; history symbol
3.221- (defvar ,(symb var '-prompt-history) nil)
3.222- (defun ,(symb var '-prompt) ()
3.223- ,(format nil "Prompt for a value from `~A', use DEFAULT if non-nil
3.224-and no value is provided by user, otherwise fallback to the `car'
3.225-of `~A-PROMPT-HISTORY'." var var)
3.226- (completing-read
3.227- (format nil "~A [~A]: "
3.228- (or ,p ">")
3.229- (car (symbol-value ,h)))
3.230- ,s :history ,h :default nil)))))
3.231-
3.232-(defmacro define-cli-constant (name cli &optional doc)
3.233- `(define-constant ,name ,cli ,@doc :test #'cli-equal))
3.234-
3.235-(defvar *default-cli-def* 'defparameter)
3.236-
3.237-(defmacro defcmd (name &body body)
3.238- `(defun ,name ($args $opts)
3.239- (declare (ignorable $args $opts))
3.240- (let (($argc (length $args))
3.241- ($optc (length $opts)))
3.242- (declare (ignorable $argc $optc))
3.243- ,@body)))
3.244-
3.245-(defmacro defopt (name &body body)
3.246- `(defun ,name ($val)
3.247- (declare (ignorable $val))
3.248- ,@body))
3.249-
3.250-(declaim (inline walk-cli-slots))
3.251-(defun walk-cli-slots (cli)
3.252- "Walk the plist CLI, performing actions as necessary based on the slot
3.253-keys."
3.254- (loop for kv in (group cli 2)
3.255- when (eql :thunk (car kv))
3.256- return (let ((th (cdr kv)))
3.257- (if (or (functionp th) (symbolp th)) (funcall th) (compile nil (lambda () th)))))
3.258- cli)
3.259-
3.260-(defmacro define-cli (name &body body)
3.261- "Define a symbol NAME bound to a top-level CLI object."
3.262- (declare (type symbol name))
3.263- `(,*default-cli-def* ,name (apply #'make-cli t (walk-cli-slots ',body))))
3.264-
3.265-(defmacro defmain (ret &body body)
3.266- "Define a main function in the current package which returns RET.
3.267-
3.268-Note that this macro does not export the defined function and requires
3.269-`cli:main' to be an external symbol."
3.270- `(progn
3.271- (declaim (type stream output))
3.272- (defun main (&key (output *standard-output*))
3.273- "Run the top-level function and print to OUTPUT."
3.274- (let ((*standard-output* output))
3.275- (with-cli-handlers
3.276- (progn ,@body ,ret))))))
3.277-
3.278-;;; Utils
3.279-(defvar *argv*)
3.280-
3.281-(defun make-cli (kind &rest slots)
3.282- "Creates a new CLI object of the given kind."
3.283- (declare (type (member :opt :cmd :cli t) kind))
3.284- (apply #'make-instance
3.285- (cond
3.286- ((eql kind :cli) 'cli)
3.287- ((eql kind :opt) 'cli-opt)
3.288- ((eql kind :cmd) 'cli-cmd)
3.289- (t 'cli))
3.290- slots))
3.291-
3.292-;; RESEARCH 2023-09-12: closed over hash-table with short/long flags
3.293-;; to avoid conflicts. if not, need something like a flag-function
3.294-;; slot at class allocation.
3.295-(defmacro make-opts (&body opts)
3.296- `(map 'vector
3.297- (lambda (x)
3.298- (etypecase x
3.299- (string (make-cli :opt :name x))
3.300- (list (apply #'make-cli :opt x))
3.301- (t (make-cli :opt :name (format nil "~(~A~)" x) :global t))))
3.302- (walk-cli-slots ',opts)))
3.303-
3.304-(defmacro make-cmds (&body opts)
3.305- `(map 'vector
3.306- (lambda (x)
3.307- (etypecase x
3.308- (string (make-cli :cmd :name x))
3.309- (list (apply #'make-cli :cmd x))
3.310- (t (make-cli :cmd :name (format nil "~(~A~)" x)))))
3.311- (walk-cli-slots ',opts)))
3.312-
3.313-(defun long-opt-p (str)
3.314- (and (char= (aref str 0) (aref str 1) #\-)
3.315- (> (length str) 2)))
3.316-
3.317-(defun short-opt-p (str)
3.318- (and (char= (aref str 0) #\-)
3.319- (not (char= (aref str 1) #\-))
3.320- (> (length str) 1)))
3.321-
3.322-(defun opt-group-p (str)
3.323- (string= str *cli-group-separator*))
3.324-
3.325-(defun opt-prefix-eq (ch str)
3.326- (char= (aref str 0) ch))
3.327-
3.328-(defun gen-thunk-ll (origin args)
3.329- (let ((a0 (list (symb '$a 0) origin)))
3.330- (group
3.331- (nconc (loop for i from 1 for a in args nconc (list (symb '$a i) a)) a0 )
3.332- 2)))
3.333-
3.334-;; TODO 2023-10-06:
3.335-;; (defmacro gen-cli-thunk (pvars &rest thunk)
3.336-;; "Generate and return a function based on THUNK suitable for the :thunk
3.337-;; slot of cli objects with pandoric bindings PVARS.")
3.338-
3.339-;;; Protocol
3.340-(defgeneric push-cmd (cmd place))
3.341-
3.342-(defgeneric push-opt (opt place))
3.343-
3.344-(defgeneric pop-cmd (place))
3.345-
3.346-(defgeneric pop-opt (place))
3.347-
3.348-(defgeneric find-cmd (self name &optional active))
3.349-
3.350-(defgeneric find-opt (self name &optional active))
3.351-
3.352-(defgeneric active-cmds (self))
3.353-
3.354-(defgeneric active-opts (self &optional global))
3.355-
3.356-(defgeneric find-short-opt (self ch))
3.357-
3.358-(defgeneric call-opt (self arg))
3.359-
3.360-(defgeneric do-opt (self))
3.361-
3.362-(defgeneric call-cmd (self args opts))
3.363-
3.364-(defgeneric parse-args (self args &key &allow-other-keys)
3.365- (:documentation "Parse list of strings ARGS using SELF.
3.366-
3.367-A list of the same length as ARGS is returned containing 'cli-ast'
3.368-objects: (OPT . (or char string)) (CMD . string) NIL"))
3.369-
3.370-(defgeneric do-cmd (self)
3.371- (:documentation "Run the command SELF with args parsed at runtime."))
3.372-
3.373-(defgeneric print-help (self &optional stream)
3.374- (:documentation "Format cli SELF as a helpful string."))
3.375-
3.376-(defgeneric print-version (self &optional stream)
3.377- (:documentation "Print the version of SELF."))
3.378-
3.379-(defgeneric print-usage (self &optional stream)
3.380- (:documentation "Format cli SELF as a useful string."))
3.381-
3.382-(defgeneric handle-unknown-argument (self arg)
3.383- (:documentation "Handle an unknown argument."))
3.384-
3.385-(defgeneric handle-missing-argument (self arg)
3.386- (:documentation "Handle a missing argument."))
3.387-
3.388-(defgeneric handle-invalid-argument (self arg)
3.389- (:documentation "Handle an invalid argument."))
3.390-
3.391-(defgeneric cli-equal (a b))
3.392-
3.393-(defun default-thunk (cli) (lambda (x) (declare (ignore x)) (print-help cli)))
3.394-
3.395-(defvar *cli-opt-kinds* '(bool str form list sym key num file dir))
3.396-
3.397-(defun cli-opt-kind-p (s)
3.398- (declare (type symbol s))
3.399- (find s *cli-opt-kinds*))
3.400-
3.401-(defmacro make-opt-parser (kind-spec &body body)
3.402- "Return a KIND-opt-parser function based on KIND-SPEC which is either a
3.403-symbol from *cli-opt-kinds* or a list, and optional BODY which
3.404-is a list of handlers for the opt-val."
3.405- (let* ((kind (if (consp kind-spec) (car kind-spec) kind-spec))
3.406- (super (when (consp kind-spec) (cadr kind-spec)))
3.407- (fn-name (symb 'parse- kind '-opt)))
3.408- ;; thread em
3.409- (let ((fn1 (when (not (eql 'nil super)) (symb 'parse- super '-opt))))
3.410- `(progn
3.411- (defun ,fn-name ($val)
3.412- "Parse the cli-opt-val $VAL."
3.413- ;; do stuff
3.414- (when (not (eql ',fn1 'nil)) (setq $val (funcall ',fn1 $val)))
3.415- ,@body)))))
3.416-
3.417-(make-opt-parser bool $val)
3.418-
3.419-(make-opt-parser (str bool) (when (stringp $val) $val))
3.420-
3.421-(make-opt-parser (form str) (read-from-string $val))
3.422-
3.423-(make-opt-parser (list form) (when (listp $val) $val))
3.424-
3.425-(make-opt-parser (sym form) (when (symbolp $val) $val))
3.426-
3.427-(make-opt-parser (key form) (when (keywordp $val) $val))
3.428-
3.429-(make-opt-parser (num form) (when (numberp $val) $val))
3.430-
3.431-(make-opt-parser (file str)
3.432- (when $val (parse-native-namestring $val nil *default-pathname-defaults* :as-directory nil)))
3.433-
3.434-(make-opt-parser (dir str)
3.435- (when $val (sb-ext:parse-native-namestring $val nil *default-pathname-defaults* :as-directory t)))
3.436-
3.437-;;; Objects
3.438-(defclass cli-opt ()
3.439- ;; note that cli-opts can have a nil or unbound name slot
3.440- ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string)
3.441- (kind :initarg :kind :initform 'boolean :accessor cli-opt-kind :type cli-opt-kind-p)
3.442- (thunk :initform #'default-thunk :initarg :thunk :type function-lambda-expression :accessor cli-thunk)
3.443- (val :initarg :val :initform nil :accessor cli-val :type form)
3.444- (global :initarg :global :initform nil :accessor global-opt-p :type boolean)
3.445- (description :initarg :description :accessor cli-description :type string)
3.446- (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean))
3.447- (:documentation "CLI option"))
3.448-
3.449-(defmethod handle-unknown-argument ((self cli-opt) arg))
3.450-(defmethod handle-missing-argument ((self cli-opt) arg))
3.451-(defmethod handle-invalid-argument ((self cli-opt) arg))
3.452-
3.453-(defmethod initialize-instance :after ((self cli-opt) &key)
3.454- (with-slots (name thunk) self
3.455- (unless (stringp name) (setf name (format nil "~(~A~)" name)))
3.456- (when (symbolp thunk) (setf thunk (funcall (compile nil `(lambda () ,(symbol-function thunk))))))
3.457- self))
3.458-
3.459-(defmethod install-thunk ((self cli-opt) (lambda function) &optional compile)
3.460- "Install THUNK into the corresponding slot in cli-cmd SELF."
3.461- (let ((%thunk (if compile (compile nil lambda) lambda)))
3.462- (setf (cli-thunk self) %thunk)
3.463- self))
3.464-
3.465-(defmethod print-object ((self cli-opt) stream)
3.466- (print-unreadable-object (self stream :type t)
3.467- (format stream "~A :global ~A :val ~A"
3.468- (cli-name self)
3.469- (global-opt-p self)
3.470- (cli-val self))))
3.471-
3.472-(defmethod print-usage ((self cli-opt) &optional stream)
3.473- (format stream " -~(~{~A~^/--~}~)~A~A"
3.474- (if-let ((n (cli-name self)))
3.475- (list (make-shorty n) n)
3.476- 'dyn)
3.477- (if (global-opt-p self) "* " " ")
3.478- (if-let ((d (and (slot-boundp self 'description) (cli-description self))))
3.479- (format stream ": ~A" d)
3.480- "")))
3.481-
3.482-(defmethod cli-equal ((a cli-opt) (b cli-opt))
3.483- (with-slots (name global kind) a
3.484- (with-slots ((bn name) (bg global) (bk kind)) b
3.485- (and (string= name bn)
3.486- (eql global bg)
3.487- (eql kind bk)))))
3.488-
3.489-(defmethod call-opt ((self cli-opt) arg)
3.490- (funcall (compile nil (cli-thunk self)) arg))
3.491-
3.492-(defmethod do-opt ((self cli-opt))
3.493- (call-opt self (cli-val self)))
3.494-
3.495-(defclass cli-cmd ()
3.496- ;; name slot is required and must be a string
3.497- ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string)
3.498- (opts :initarg :opts :initform (make-array 0 :element-type 'cli-opt)
3.499- :accessor cli-opts :type (vector cli-opt))
3.500- (cmds :initarg :cmds :initform (make-array 0 :element-type 'cli-cmd)
3.501- :accessor cli-cmds :type (vector cli-cmd))
3.502- (thunk :initform #'default-thunk :initarg :thunk :accessor cli-thunk :type function-lambda-expression)
3.503- (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean)
3.504- (description :initarg :description :accessor cli-description :type string)
3.505- (args :initform nil :initarg :args :accessor cli-cmd-args))
3.506- (:documentation "CLI command"))
3.507-
3.508-(defmethod initialize-instance :after ((self cli-cmd) &key)
3.509- (with-slots (name cmds opts thunk) self
3.510- (unless (stringp name) (setf name (format nil "~(~A~)" name)))
3.511- (unless (vectorp cmds) (setf cmds (funcall (compile nil `(lambda () ,cmds)))))
3.512- (unless (vectorp opts) (setf opts (funcall (compile nil `(lambda () ,opts)))))
3.513- (when (symbolp thunk) (setf thunk (symbol-function thunk)))
3.514- self))
3.515-
3.516-(defmethod print-object ((self cli-cmd) stream)
3.517- (print-unreadable-object (self stream :type t)
3.518- (format stream "~A :opts ~A :cmds ~A :args ~A"
3.519- (cli-name self)
3.520- (length (cli-opts self))
3.521- (length (cli-cmds self))
3.522- (length (cli-cmd-args self)))))
3.523-
3.524-(defmethod print-usage ((self cli-cmd) &optional stream)
3.525- (with-slots (opts cmds) self
3.526- (format stream "~(~A~) ~A~A~A"
3.527- (cli-name self)
3.528- (if-let ((d (and (slot-boundp self 'description) (cli-description self))))
3.529- (format nil ": ~A" d)
3.530- "")
3.531- (if (null opts)
3.532- ""
3.533- (format nil "~{~% ~A~^~}" (loop for o across opts collect (print-usage o nil))))
3.534- (if (null cmds)
3.535- ""
3.536- (format nil "~% ~{! ~A~}" (loop for c across cmds collect (print-usage c nil)))))))
3.537-
3.538-(defmethod push-cmd ((self cli-cmd) (place cli-cmd))
3.539- (vector-push self (cli-cmds place)))
3.540-
3.541-(defmethod push-opt ((self cli-opt) (place cli-cmd))
3.542- (vector-push self (cli-opts place)))
3.543-
3.544-(defmethod pop-cmd ((self cli-cmd))
3.545- (vector-pop (cli-cmds self)))
3.546-
3.547-(defmethod pop-opt ((self cli-opt))
3.548- (vector-pop (cli-opts self)))
3.549-
3.550-(defmethod cli-equal ((a cli-cmd) (b cli-cmd))
3.551- (with-slots (name opts cmds) a
3.552- (with-slots ((bn name) (bo opts) (bc cmds)) b
3.553- (and (string= name bn)
3.554- (if (and (null opts) (null bo))
3.555- t
3.556- (unless (member nil (loop for oa across opts
3.557- for ob across bo
3.558- collect (cli-equal oa ob)))
3.559- t))
3.560- (if (and (null cmds) (null bc))
3.561- t
3.562- (unless (member nil (loop for ca across cmds
3.563- for cb across bc
3.564- collect (cli-equal ca cb)))
3.565- t))))))
3.566-
3.567-;; typically when starting from a top-level `cli', the global
3.568-;; `cli-opts' will be parsed first, followed by the first command
3.569-;; found. If a command is found, the tail of the list is passed as
3.570-;; arguments to this function, which can pass additonal arguments to
3.571-;; nested commands.
3.572-
3.573-;; TODO 2023-09-12: Parsing restarts at the `*cli-group-separator*'
3.574-;; if present, or stops at EOI.
3.575-
3.576-(declaim (inline %make-cli-node))
3.577-(defstruct (cli-node (:constructor %make-cli-node)) kind form)
3.578-
3.579-(defun make-cli-node (kind form)
3.580- (%make-cli-node :kind kind :form form))
3.581-
3.582-(declaim (inline %make-cli-ast))
3.583-(defstruct (cli-ast (:constructor %make-cli-ast)) ast)
3.584-
3.585-(defun make-cli-ast (nodes)
3.586- (%make-cli-ast :ast nodes))
3.587-
3.588-(defmethod find-cmd ((self cli-cmd) name &optional active)
3.589- (when-let ((c (find name (cli-cmds self) :key #'cli-name :test #'string=)))
3.590- (if active
3.591- ;; maybe issue warning here? report to user
3.592- (when (cli-lock-p c) c)
3.593- c)))
3.594-
3.595-(defmethod active-cmds ((self cli-cmd))
3.596- (remove-if-not #'cli-lock-p (cli-cmds self)))
3.597-
3.598-
3.599-(defmethod find-opt ((self cli-cmd) name &optional active)
3.600- (when-let ((o (find name (cli-opts self) :key #'cli-name :test #'string=)))
3.601- (if active
3.602- (when (cli-lock-p o) o)
3.603- o)))
3.604-
3.605-(defun active-global-opt-p (opt)
3.606- "Return non-nil if OPT is active at runtime and global."
3.607- (when (and (cli-lock-p opt) (global-opt-p opt)) t))
3.608-
3.609-(defmethod active-opts ((self cli-cmd) &optional global)
3.610- (remove-if-not
3.611- (if global
3.612- #'active-global-opt-p
3.613- #'cli-lock-p)
3.614- (cli-opts self)))
3.615-
3.616-(defmethod find-short-opt ((self cli-cmd) ch)
3.617- (find ch (cli-opts self) :key #'cli-name :test #'opt-prefix-eq))
3.618-
3.619-(defmethod proc-args ((self cli-cmd) args)
3.620- "process ARGS into an ast. Each element of the ast is a node with a
3.621-:kind slot, indicating the type of node and a :form slot which stores
3.622-a value.
3.623-
3.624-For now we parse group separators '--' and insert a nil into the tree,
3.625-this will likely change to generating a new branch in the ast as it
3.626-should be."
3.627- (make-cli-ast
3.628- (loop
3.629- for a in args
3.630- if (= (length a) 1) collect (make-cli-node 'arg a)
3.631- ;; SHORT OPT
3.632- else if (short-opt-p a)
3.633- collect (if-let ((o (find-short-opt self (aref a 1))))
3.634- (progn
3.635- (setf (cli-val o) t)
3.636- (make-cli-node 'opt o))
3.637- (make-cli-node 'arg a))
3.638-
3.639- ;; LONG OPT
3.640- else if (long-opt-p a)
3.641- ;; what we actually want to do is consume the next sequence of args - TBD
3.642- collect (if-let ((o (find-opt self (string-trim "-" a))))
3.643- (progn
3.644- (setf (cli-val o) (string-trim "-" a))
3.645- (make-cli-node 'opt o))
3.646- (make-cli-node 'arg a))
3.647- ;; OPT GROUP
3.648- else if (opt-group-p a)
3.649- collect nil
3.650- ;; CMD
3.651- else if (find-cmd self a)
3.652- ;; TBD
3.653- collect (make-cli-node 'cmd (find-cmd self a))
3.654- ;; ARG
3.655- else collect (make-cli-node 'arg a))))
3.656-
3.657-(defmethod install-ast ((self cli-cmd) (ast cli-ast))
3.658- "Install the given AST, recursively filling in value slots."
3.659- (with-slots (cmds opts) self
3.660- ;; we assume all nodes in the ast have been validated and the ast
3.661- ;; itself is consumed. validation is performed in proc-args.
3.662-
3.663- ;; before doing anything else we lock SELF, which should remain
3.664- ;; locked for the full runtime duration.
3.665- (setf (cli-lock-p self) t)
3.666- (loop named install
3.667- for (node . tail) on (cli-ast-ast ast)
3.668- unless (null node)
3.669- do
3.670- (with-slots (kind form) node
3.671- (case kind
3.672- ;; opts
3.673- (opt
3.674- (let ((name (cli-name form))
3.675- (val (cli-val form)))
3.676- (when-let ((o (find-opt self name)))
3.677- (setf (cli-val o) val
3.678- (cli-lock-p o) t))))
3.679- ;; when we encounter a command we recurse over the tail
3.680- (cmd
3.681- (when-let ((c (find-cmd self (cli-name form))))
3.682- (setf (cli-lock-p c) t)
3.683- ;; handle the rest of the AST
3.684- (install-ast c (make-cli-ast tail))
3.685- (return-from install)))
3.686- (arg (push-arg form self)))))
3.687- (setf (cli-cmd-args self) (nreverse (cli-cmd-args self)))
3.688- self))
3.689-
3.690-(defmethod install-thunk ((self cli-cmd) (lambda function) &optional compile)
3.691- "Install THUNK into the corresponding slot in cli-cmd SELF."
3.692- (let ((%thunk (if compile (compile nil lambda) lambda)))
3.693- (setf (cli-thunk self) %thunk)
3.694- self))
3.695-
3.696-(defmethod push-arg (arg (self cli-cmd))
3.697- (push arg (cli-cmd-args self)))
3.698-
3.699-(defmethod parse-args ((self cli-cmd) args &key (compile nil))
3.700- "Parse ARGS and return the updated object SELF.
3.701-
3.702-ARGS is assumed to be a valid cli-ast (list of cli-nodes), unless
3.703-COMPILE is t, in which case a list of strings is assumed."
3.704- (with-slots (opts cmds) self
3.705- (let ((args (if compile (proc-args self args) args)))
3.706- (print (install-ast self args)))))
3.707-
3.708-;; warning: make sure to fill in the opt and cmd slots with values
3.709-;; from the top-level args before doing a command.
3.710-(defmethod call-cmd ((self cli-cmd) args opts)
3.711- ;; TODO 2023-09-12: handle args/env
3.712- (funcall (cli-thunk self) args opts))
3.713-
3.714-(defmethod do-cmd ((self cli-cmd))
3.715- (call-cmd self (cli-cmd-args self) (cli-opts self)))
3.716-
3.717-(defclass cli (cli-cmd)
3.718- ;; name slot defaults to *package*, must be string
3.719- ((name :initarg :name :initform (string-downcase (package-name *package*)) :accessor cli-name :type string)
3.720- (version :initarg :version :initform "0.1.0" :accessor cli-version :type string)
3.721- ;; TODO 2023-10-11: look into pushd popd - wd-stack?
3.722- (cwd :initarg :cwd :initform (sb-posix:getcwd) :type string :accessor cli-cwd
3.723- :documentation "working directory of the top-level CLI."))
3.724- (:documentation "CLI"))
3.725-
3.726-(defmethod print-usage ((self cli) &optional stream)
3.727- (iprintln (format nil "usage: ~A [global] <command> [<arg>]~%" (cli-name self)) 2 stream))
3.728-
3.729-(defmethod print-version ((self cli) &optional stream)
3.730- (println (cli-version self) stream))
3.731-
3.732-(defmethod print-help ((self cli) &optional stream)
3.733- (println (format nil "~A v~A" (cli-name self) (cli-version self)) stream)
3.734- (print-usage self stream)
3.735- (iprintln (cli-description self) 2 stream)
3.736- ;; (terpri stream)
3.737- (iprintln "options:" 2 stream)
3.738- (with-slots (opts cmds) self
3.739- (unless (null opts)
3.740- (loop for o across opts
3.741- do (iprintln (print-usage o) 4 stream)))
3.742- ;; (terpri stream)
3.743- (iprintln "commands:" 2 stream)
3.744- (unless (null cmds)
3.745- (loop for c across cmds
3.746- do (iprintln (print-usage c) 4 stream)))))
3.747-
3.748-(defmethod cli-equal :before ((a cli) (b cli))
3.749- "Return T if A is the same cli object as B.
3.750-
3.751-Currently this function is intended only for instances of the CLI
3.752-class and is used as a specialized EQL for DEFINE-CONSTANT."
3.753- (with-slots (version) a
3.754- (with-slots ((bv version)) b
3.755- (string= version bv))))
3.756-
3.757-;; same as cli-cmd method, default is to compile though
3.758-(defmethod parse-args ((self cli) (args list) &key (compile t))
3.759- "Parse list of string arguments ARGS and return the updated object SELF."
3.760- (with-slots (opts cmds) self
3.761- (let ((args (if compile (proc-args self args) args)))
3.762- (install-ast self args))))
3.763-
3.764-(declaim (inline debug-opts))
3.765-(defun debug-opts (cli)
3.766- (let ((o (active-opts cli))
3.767- (a (cli-cmd-args cli))
3.768- (c (active-cmds cli)))
3.769- (debug! (cli-cwd cli) o a c)))
3.770-
3.771-(declaim (inline solop))
3.772-(defun solop (self)
3.773- (and (= 0 (length (active-opts self t)) (length (active-cmds self)))))
3.774-
3.775-(defmethod do-cmd ((self cli))
3.776- (if (solop self)
3.777- (call-cmd self (cli-cmd-args self) (cli-opts self))
3.778- (progn
3.779- (loop for o across (active-opts self t)
3.780- do (do-opt o))
3.781- (loop for c across (active-cmds self)
3.782- do (do-cmd c)))))
3.783-
3.784-(provide :cli)
4.1--- a/lisp/lib/cli/tests.lisp Mon Oct 16 19:33:42 2023 -0400
4.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
4.3@@ -1,46 +0,0 @@
4.4-;; we should be able to call this from the body of the test, but we
4.5-;; get an undefined-function error for 'MACS.RT::MAKE-PROMPT!'
4.6-(defpkg :cli/tests
4.7- (:use :cl :rt :cli))
4.8-
4.9-(defsuite :cli)
4.10-(in-suite :cli)
4.11-(unless *compile-tests*
4.12- (deftest cli-prompt ()
4.13- "Test MACS.CLI prompts"
4.14- (make-prompt! tpfoo "testing: ")
4.15- (defvar tcoll nil)
4.16- (defvar thist nil)
4.17- (let ((*standard-input* (make-string-input-stream
4.18- (format nil "~A~%~A~%" "foobar" "foobar"))))
4.19- ;; prompts
4.20- (is (string= (tpfoo-prompt) "foobar"))
4.21- (is (string= "foobar"
4.22- (cli:completing-read "nothing: " tcoll :history thist :default "foobar"))))))
4.23-
4.24-(defparameter *opts* (cli:make-opts (:name foo :global t :description "bar")
4.25- (:name bar :description "foo")))
4.26-
4.27-(defparameter *cmd1* (make-cli :cmd :name "holla" :opts *opts* :description "cmd1 description"))
4.28-(defparameter *cmd2* (make-cli :cmd :name "ayo" :cmds #(*cmd1*) :opts *opts* :description "cmd1 description"))
4.29-(defparameter *cmds* (cli:make-cmds (:name "baz" :description "baz" :opts *opts*)))
4.30-
4.31-(defparameter *cli* (make-cli t :opts *opts* :cmds *cmds* :description "test cli"))
4.32-
4.33-(deftest cli ()
4.34- "test MACS.CLI OOS."
4.35- (let ((cli *cli*))
4.36- (is (eq (make-shorty "test") #\t))
4.37- (is (equalp (proc-args cli '("-f" "baz" "--bar" "fax")) ;; not eql
4.38- (make-cli-ast
4.39- (list (make-cli-node 'opt (find-short-opt cli #\f))
4.40- (make-cli-node 'cmd (find-cmd cli "baz"))
4.41- (make-cli-node 'opt (find-opt cli "bar"))
4.42- (make-cli-node 'arg "fax")))))
4.43- (is (parse-args cli '("--bar" "baz" "-f" "yaks")))
4.44- (is (stringp
4.45- (with-output-to-string (s)
4.46- (print-version cli s)
4.47- (print-usage cli s)
4.48- (print-help cli s))))
4.49- (is (string= "foobar" (parse-str-opt "foobar")))))
5.1--- a/lisp/lib/rt.asd Mon Oct 16 19:33:42 2023 -0400
5.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
5.3@@ -1,14 +0,0 @@
5.4-(defsystem :rt
5.5- :version "0.1.0"
5.6- :author "ellis <ellis@rwest.io>"
5.7- :description "regression test framework"
5.8- :bug-tracker "https://lab.rwest.io/ellis/macs/issues"
5.9- :source-control (:hg "https://lab.rwest.io/ellis/macs")
5.10- :depends-on (:macs :sxp)
5.11- :in-order-to ((test-op (test-op "rt/tests")))
5.12- :components ((:file "rt/rt")))
5.13-
5.14-(defsystem :rt/tests
5.15- :depends-on (:macs :rt)
5.16- :components ((:file "rt/tests"))
5.17- :perform (test-op (op c) (uiop:symbol-call '#:rt '#:do-tests :rt)))
6.1--- a/lisp/lib/rt/rt.lisp Mon Oct 16 19:33:42 2023 -0400
6.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
6.3@@ -1,665 +0,0 @@
6.4-;;; rt.lisp --- regression testing
6.5-
6.6-;; Regression Testing framework. inspired by PCL, the original CMUCL
6.7-;; code, and the SBCL port.
6.8-
6.9-;;; Commentary:
6.10-
6.11-;; - :rt https://www.merl.com/publications/docs/TR91-04.pdf Chapter 1
6.12-;; - :com.gigamonkeys.test https://github.com/gigamonkey/monkeylib-test-framework
6.13-;; - :sb-rt https://github.com/sbcl/sbcl/blob/master/contrib/sb-rt/rt.lisp
6.14-
6.15-;; This package is intended to provide a modernized Lisp testing
6.16-;; library with features found in some of the test frameworks listed
6.17-;; below.
6.18-
6.19-;; - :it.bese.fiveam https://github.com/lispci/fiveam
6.20-;; - :try https://github.com/melisgl/try
6.21-;; - :rove https://github.com/fukamachi/rove
6.22-
6.23-;;; TODO:
6.24-#|
6.25-
6.26-- [ ] benchmark support: do-bench, test-count,
6.27-
6.28-- [ ] fixtures api
6.29-
6.30-- [ ] profiling
6.31-|#
6.32-;;; Code:
6.33-#+x86-64
6.34-(eval-when (:compile-toplevel :load-toplevel :execute)
6.35- (require 'sb-sprof))
6.36-
6.37-(pkg:defpkg :rt
6.38- (:use
6.39- :cl :sxp
6.40- :sym :list :cond :readtables :fu :fmt :log :ana :pan :sb-aprof
6.41- #+x86-64 :sb-sprof)
6.42- (:nicknames :rt)
6.43- (:export
6.44- :*default-test-opts*
6.45- :*compile-tests*
6.46- :*catch-test-errors*
6.47- :*test-suffix*
6.48- :*default-test-suite-name*
6.49- :*test-suite*
6.50- :*test-suite-list*
6.51- ;; TODO 2023-09-04: :*test-profiler-list* not yet
6.52- :*testing*
6.53- :test-suite-designator
6.54- :check-suite-designator
6.55- :make-test
6.56- :make-suite
6.57- :test-name=
6.58- :do-test
6.59- :do-tests
6.60- :continue-testing
6.61- :with-test-env
6.62- :ensure-suite
6.63- :test-fixture
6.64- :fixture-prototype
6.65- :make-fixture-prototype
6.66- :make-fixture
6.67- :with-fixture
6.68- :test-result
6.69- :test-pass-p
6.70- :test-fail-p
6.71- :test-skip-p
6.72- :test-failed
6.73- :fail!
6.74- :is
6.75- :signals
6.76- :deftest
6.77- :defsuite
6.78- :in-suite
6.79- :eval-test
6.80- :compile-test
6.81- :locked-tests
6.82- :push-test
6.83- :pop-test
6.84- :delete-test
6.85- :find-test
6.86- :do-suite
6.87- :test-object
6.88- :test
6.89- :test-fixture
6.90- :test-suite
6.91- :test-name
6.92- :tests
6.93- :test-form
6.94- :test-results))
6.95-
6.96-(in-package :rt)
6.97-(in-readtable *macs-readtable*)
6.98-
6.99-;;; Vars
6.100-(defvar *default-test-opts* '(optimize sb-c::instrument-consing))
6.101-(defvar *compile-tests* t
6.102- "When nil do not compile tests. With a value of t, tests are compiled
6.103-with default optimizations else the value is used to configure
6.104-compiler optimizations.")
6.105-(defvar *catch-test-errors* t "When non-nil, cause errors in a test to be caught.")
6.106-(defvar *test-suffix* "-test" "A suffix to append to every `test' defined with `deftest'.")
6.107-(defvar *test-suite-list* nil "List of available `test-suite' objects.")
6.108-(defvar *test-suite* nil "A 'test-suite-designator' which identifies the current `test-suite'.")
6.109-(eval-when (:compile-toplevel :load-toplevel :execute)
6.110- (defvar *default-test-suite-name* "default"))
6.111-(declaim (type (or stream boolean string) *test-input*))
6.112-(defvar *test-input* nil "When non-nil, specifies an input stream or buffer for `*testing*'.")
6.113-(defvar *default-bench-count* 100 "Default number of iterations to repeat a bench test for. This value is
6.114-used when the slot value of :BENCH is t.")
6.115-(defvar *testing* nil "Testing state var.")
6.116-
6.117-;;; Utils
6.118-(eval-when (:compile-toplevel :load-toplevel :execute)
6.119- (defun make-test (&rest slots)
6.120- (apply #'make-instance 'test slots))
6.121- (defun make-suite (&rest slots)
6.122- (apply #'make-instance 'test-suite slots)))
6.123-
6.124-;; TODO 2023-09-04: optimize
6.125-(declaim (inline do-tests))
6.126-(defun do-tests (&optional (suite *test-suite*) (output *standard-output*))
6.127- (if (pathnamep output)
6.128- (with-open-file (stream output :direction :output)
6.129- (do-suite (ensure-suite suite) :stream stream))
6.130- (do-suite (ensure-suite suite) :stream output)))
6.131-
6.132-;; this assumes that *test-suite* is re-initialized correctly to the
6.133-;; correct test-suite object.
6.134-(defun continue-testing ()
6.135- (if-let ((test *testing*))
6.136- (throw '%in-test test)
6.137- (do-suite *test-suite*)))
6.138-
6.139-;; NOTE 2023-09-01: `pushnew' does not return an indication of whether
6.140-;; place is changed - it returns place. This is functionally sound but
6.141-;; means that if we want to do something else in the event that place
6.142-;; is unchanged, we run into some friction,
6.143-;; https://stackoverflow.com/questions/56228832/adapting-common-lisp-pushnew-to-return-success-failure
6.144-(defun spush (item lst &key (test #'equal))
6.145- "Substituting `push'"
6.146- (declare (type function test))
6.147- (cond
6.148- ((null lst) (push item lst))
6.149- ((list lst)
6.150- (if-let ((found (member item lst
6.151- :test test)))
6.152- (progn
6.153- (rplaca found item)
6.154- lst)
6.155- (push item lst)))
6.156- #|(or nil '(t (cons item lst)))|#))
6.157-
6.158-;; FIX 2023-08-31: spush, replace with `add-test' method.
6.159-;; (declaim (inline normalize-test-name))
6.160-(defun normalize-test-name (a)
6.161- "Return the normalized `test-suite-designator' of A."
6.162- (etypecase a
6.163- (string a)
6.164- (symbol (symbol-name a))
6.165- (test-object (test-name a))
6.166- (t (format nil "~A" a))))
6.167-
6.168-(defun test-name= (a b)
6.169- "Return t if A and B are similar `test-suite-designator's."
6.170- (let ((a (normalize-test-name a))
6.171- (b (normalize-test-name b)))
6.172- (string= a b)))
6.173-
6.174-;; (declaim (inline assert-suite ensure-suite))
6.175-(defun ensure-suite (name)
6.176- (if-let ((ok (member name *test-suite-list* :test #'test-name=)))
6.177- (car ok)
6.178- (when (or (eq name t) (null name)) (make-suite :name *default-test-suite-name*))))
6.179-
6.180-(defun check-suite-designator (suite) (check-type suite test-suite-designator))
6.181-
6.182-(defun assert-suite (name)
6.183- (check-suite-designator name)
6.184- (assert (ensure-suite name)))
6.185-
6.186-(declaim (inline test-opt-key-p test-opt-valid-p))
6.187-(defun test-opt-key-p (k)
6.188- "Test if K is a `test-opt-key'."
6.189- (member k '(:profile :save :stream)))
6.190-
6.191-(defun test-opt-valid-p (f)
6.192- "Test if F is a valid `test-opt' form. If so, return F else nil."
6.193- (when (test-opt-key-p (car f))
6.194- f))
6.195-
6.196-;;; Conditions
6.197-(define-condition test-failed (error)
6.198- ((reason :accessor fail-reason :initarg :reason :initform "unknown")
6.199- (name :accessor fail-name :initarg :name)
6.200- (form :accessor fail-form :initarg :form))
6.201- (:documentation "Signaled when a test fails.")
6.202- (:report (lambda (c s)
6.203- (format s "The following expression failed: ~S~%~A."
6.204- (fail-form c)
6.205- (fail-reason c)))))
6.206-
6.207-;;; Protocol
6.208-(defgeneric eval-test (self)
6.209- (:documentation "Eval a `test'."))
6.210-
6.211-(defgeneric compile-test (self &key &allow-other-keys)
6.212- (:documentation "Compile a `test'."))
6.213-
6.214-(defgeneric locked-tests (self)
6.215- (:documentation "Return a list of locked tests in `test-suite' object SELF."))
6.216-
6.217-(defgeneric push-test (self place)
6.218- (:documentation
6.219- "Push `test' SELF to the value of slot ':tests' in `test-suite' object PLACE."))
6.220-
6.221-(defgeneric pop-test (self)
6.222- (:documentation
6.223- "Pop the first `test' from the slot-value of ':tests' in `test-suite' object SELF."))
6.224-
6.225-(defgeneric push-result (self place)
6.226- (:documentation
6.227- "Push object SELF to the value of slot ':results' in object PLACE."))
6.228-
6.229-(defgeneric pop-result (self)
6.230- (:documentation
6.231- "Pop the first `test-result' from the slot-value of ':tests' from object SELF."))
6.232-
6.233-(defgeneric push-fixture (self place)
6.234- (:documentation
6.235- "Push object SELF to the value of slot ':results' in object PLACE."))
6.236-
6.237-(defgeneric delete-test (self &key &allow-other-keys)
6.238- (:documentation "Delete `test' object specified by `test-object' SELF and optional keys."))
6.239-
6.240-(defgeneric find-test (self name &key &allow-other-keys)
6.241- (:documentation "Find `test' object specified by name and optional keys."))
6.242-
6.243-(defgeneric do-test (self &optional test)
6.244- (:documentation
6.245- "Run `test' SELF, printing results to `*standard-output*'. The second
6.246-argument is an optional fixture.
6.247-
6.248-SELF can also be a `test-suite', in which case the TESTS slot is
6.249-queried for the value of TEST. If TEST is not provided, pops the car
6.250-from TESTS."))
6.251-
6.252-(defgeneric do-suite (self &key &allow-other-keys)
6.253- (:documentation
6.254- "Perform actions on `test-suite' object SELF with optional keys."))
6.255-
6.256-;;;; Results
6.257-(deftype result-tag ()
6.258- '(or (member :pass :fail :skip) null))
6.259-
6.260-(declaim (inline %make-test-result))
6.261-(defstruct (test-result (:constructor %make-test-result)
6.262- (:conc-name tr-))
6.263- (tag nil :type result-tag :read-only t)
6.264- (form nil :type form))
6.265-
6.266-(defun make-test-result (tag &optional form)
6.267- (%make-test-result :tag tag :form form))
6.268-
6.269-(defmethod test-pass-p ((res test-result))
6.270- (when (eq :pass (tr-tag res)) t))
6.271-
6.272-(defmethod test-fail-p ((res test-result))
6.273- (when (eq :fail (tr-tag res)) t))
6.274-
6.275-(defmethod test-skip-p ((res test-result))
6.276- (when (eq :skip (tr-tag res)) t))
6.277-
6.278-(defmethod print-object ((self test-result) stream)
6.279- (print-unreadable-object (self stream)
6.280- (format stream "~A ~A"
6.281- (tr-tag self)
6.282- (tr-form self))))
6.283-
6.284-;;; Objects
6.285-(defclass test-object ()
6.286- ((name :initarg :name :initform (required-argument) :type string :accessor test-name)
6.287- #+nil (cached :initarg :cache :allocation :class :accessor test-cached-p :type boolean))
6.288- (:documentation "Super class for all test-related objects."))
6.289-
6.290-(defmethod print-object ((self test-object) stream)
6.291- "test"
6.292- (print-unreadable-object (self stream :type t :identity t)
6.293- (format stream "~A"
6.294- (test-name self))))
6.295-
6.296-;;;; Tests
6.297-;; HACK 2023-08-31: inherit sxp?
6.298-
6.299-(defclass test (test-object)
6.300- ((fn :type symbol :accessor test-fn)
6.301- (bench :type (or boolean fixnum) :accessor test-bench :initform nil :initarg :bench)
6.302- (profile :type list :accessor test-profile :initform nil :initarg :profile)
6.303- (args :type list :accessor test-args :initform nil :initarg :args)
6.304- (decl :type list :accessor test-decl :initform nil :initarg :decl)
6.305- (form :initarg :form :initform nil :type function-lambda-expression :accessor test-form)
6.306- (doc :initarg :doc :type string :accessor test-doc)
6.307- (lock :initarg :lock :type boolean :accessor test-lock-p)
6.308- (persist :initarg :persist :initform nil :type boolean :accessor test-persist-p)
6.309- (results :initarg :results :type (array test-result) :accessor test-results))
6.310- (:documentation "Test class typically made with `deftest'."))
6.311-
6.312-(defmethod test-bench-p ((self test))
6.313- (when (test-bench self) t))
6.314-
6.315-(defmethod get-bench-count ((self test))
6.316- (when-let ((v (test-bench self)))
6.317- (cond
6.318- ((typep v 'fixnum) v)
6.319- ((eq v t) *default-bench-count*)
6.320- ;; unknown value
6.321- (t nil))))
6.322-
6.323-(defmethod initialize-instance ((self test) &key name)
6.324- ;; (debug! "building test" name)
6.325- (setf (test-fn self)
6.326- (make-symbol
6.327- (format nil "~A~A"
6.328- name
6.329- (gensym *test-suffix*))))
6.330- (setf (test-lock-p self) t)
6.331- ;; TODO 2023-09-21: we should count how many checks are in the :form
6.332- ;; slot and infer the array dimensions.
6.333- (setf (test-results self) (make-array 0 :element-type 'test-result))
6.334- (call-next-method))
6.335-
6.336-(defmethod print-object ((self test) stream)
6.337- (print-unreadable-object (self stream :type t :identity t)
6.338- (format stream "~A :fn ~A :args ~A :persist ~A"
6.339- (test-name self)
6.340- (test-fn self)
6.341- (test-args self)
6.342- (test-persist-p self))))
6.343-
6.344-;; TODO 2023-09-01: use sxp?
6.345-;; (defun validate-form (form))
6.346-
6.347-(defmethod push-result ((self test-result) (place test))
6.348- (with-slots (results) place
6.349- (push self results)))
6.350-
6.351-(defmethod pop-result ((self test))
6.352- (pop (test-results self)))
6.353-
6.354-(defmethod eval-test ((self test))
6.355- `(progn ,@(test-form self)))
6.356-
6.357-(defmethod compile-test ((self test) &key declare &allow-other-keys)
6.358- (compile
6.359- (test-fn self)
6.360- `(lambda ()
6.361- ,@(when declare `((declare ,declare)))
6.362- ,@(test-form self))))
6.363-
6.364-(defun fail! (form &optional fmt &rest args)
6.365- (let ((reason (and fmt (apply #'format nil fmt args))))
6.366- (with-simple-restart (ignore-fail "Continue testing.")
6.367- (error 'test-failed :reason reason :form form))))
6.368-
6.369-(defmacro with-test-env (self &body body)
6.370- `(catch '%in-test
6.371- (setf (test-lock-p ,self) t)
6.372- (let* ((*testing* ,self)
6.373- (bail nil)
6.374- r)
6.375- (block bail
6.376- ,@body
6.377- (setf (test-lock-p ,self) bail))
6.378- r)))
6.379-
6.380-(defmethod do-test ((self test) &optional fx)
6.381- (declare (ignorable fx))
6.382- (with-test-env self
6.383- (debug! "running test: " *testing*)
6.384- (flet ((%do ()
6.385- (if-let ((opt *compile-tests*))
6.386- ;; RESEARCH 2023-08-31: with-compilation-unit?
6.387- (progn
6.388- (when (eq opt t) (setq opt *default-test-opts*))
6.389- ;; TODO 2023-09-21: handle failures here
6.390- (funcall (compile-test self :declare opt))
6.391- (setf r (make-test-result :pass (test-fn self))))
6.392- (progn
6.393- (eval-test self)
6.394- (setf r (make-test-result :pass (test-name self)))))))
6.395- (if *catch-test-errors*
6.396- (handler-bind
6.397- ((style-warning #'muffle-warning)
6.398- (error
6.399- #'(lambda (c)
6.400- (setf bail t)
6.401- (setf r (make-test-result :fail c))
6.402- (return-from bail r))))
6.403- (%do))
6.404- (%do)))))
6.405-
6.406-(defmacro bench (iter &body body)
6.407- `(loop for i from 1 to ,iter
6.408- do ,@body))
6.409-
6.410-(defmethod do-bench ((self test) &optional fx)
6.411- (declare (ignorable fx))
6.412- (with-test-env self
6.413- (flet ((%do ()
6.414- (if-let ((opt *compile-tests*))
6.415- (progn
6.416- (when (eq opt t) (setq opt *default-test-opts*))
6.417- ;; TODO 2023-09-21: handle failures here
6.418- (let ((fn (compile-test self :declare opt)))
6.419- (bench (test-bench self) (funcall fn)))
6.420- (setf r (make-test-result :pass (test-fn self))))
6.421- (progn
6.422- (bench (test-bench self) (eval-test self))
6.423- (setf r (make-test-result :pass (test-name self)))))))
6.424- (if *catch-test-errors*
6.425- (handler-bind
6.426- ((style-warning #'muffle-warning)
6.427- (error
6.428- #'(lambda (c)
6.429- (setf bail t)
6.430- (setf r (make-test-result :fail c))
6.431- (return-from bail r))))
6.432- (%do))
6.433- (%do)))))
6.434-
6.435-;;;; Fixtures
6.436-
6.437-;; Our fixtures are just closures - with a pandoric environment. You
6.438-;; might call it a domain-specific object protocol.
6.439-
6.440-;; You can build fixtures inside a test or use the push-fixture
6.441-;; method on a `test-suite' object.
6.442-
6.443-(deftype fixture () 'form)
6.444-
6.445-(declaim (inline %make-fixture-prototype))
6.446-(defstruct (fixture-prototype (:constructor %make-fixture-prototype)
6.447- (:conc-name fxp))
6.448- (kind :empty :type keyword)
6.449- (form nil :type form))
6.450-
6.451-(defun make-fixture-prototype (kind form)
6.452- (%make-fixture-prototype :kind kind :form form))
6.453-
6.454-(defmacro make-fixture (letargs &body ds)
6.455- (let ((letargs (let-binding-transform letargs)))
6.456- `(let (,@letargs)
6.457- (dlambda ,@ds))))
6.458-
6.459-(defmacro with-fixture ((var fx) &body body)
6.460- `(let ((,var ,fx))
6.461- ,@body))
6.462-
6.463-;;;; Suites
6.464-(defclass test-suite (test-object)
6.465- ((tests :initarg :set :initform nil :type list :accessor tests
6.466- :documentation "test-suite tests")
6.467- (results :initarg :results :initform nil :type list :accessor test-results
6.468- :documentation "test-suite results")
6.469- (stream :initarg :stream :initform *standard-output* :type stream :accessor test-stream)
6.470- (fixtures :initarg :fixtures :initform nil :type list :accessor test-fixtures))
6.471- (:documentation "A class for collections of related `test' objects."))
6.472-
6.473-(defmethod print-object ((self test-suite) stream)
6.474- (print-unreadable-object (self stream :type t :identity t)
6.475- (format stream "~A [~d+~d:~d:~d:~d]"
6.476- (test-name self)
6.477- (count t (map-tests self (lambda (x) (not (test-bench-p x)))))
6.478- (count t (map-tests self #'test-bench-p))
6.479- (count t (map-tests self #'test-lock-p))
6.480- (count t (map-tests self #'test-persist-p))
6.481- (length (test-results self)))))
6.482-
6.483-;; (defmethod reinitialize-instance ((self test-suite) &rest initargs &key &allow-other-keys))
6.484-
6.485-(deftype test-suite-designator ()
6.486- "Either nil, a symbol, a string, or a `test-suite' object."
6.487- '(or null symbol string test-suite test keyword))
6.488-
6.489-(defmethod map-tests ((self test-suite) function)
6.490- (mapcar function (tests self)))
6.491-
6.492-(defmethod push-test ((self test) (place test-suite))
6.493- (push self (tests place)))
6.494-
6.495-(defmethod pop-test ((self test-suite))
6.496- (pop (tests self)))
6.497-
6.498-(defmethod push-result ((self test-result) (place test-suite))
6.499- (with-slots (results) place
6.500- (push self results)))
6.501-
6.502-(defmethod pop-result ((self test-suite))
6.503- (pop (test-results self)))
6.504-
6.505-(defmethod find-test ((self test-suite) name &key (test #'test-name=))
6.506- (declare (type (or string symbol) name)
6.507- (type function test))
6.508- (find name (the list (tests self)) :test test))
6.509-
6.510-(defmethod do-test ((self test-suite) &optional test)
6.511- (push-result
6.512- (if test
6.513- (do-test (find-test self (test-name test)))
6.514- (do-test (pop-test self)))
6.515- self))
6.516-
6.517-;; HACK 2023-09-01: find better method of declaring failures from
6.518-;; within the body of `deftest'.
6.519-(defmethod do-suite ((self test-suite) &key stream)
6.520- (when stream (setf (test-stream self) stream))
6.521- (with-slots (name stream) self
6.522- (format stream "in suite ~x with ~A/~A tests:~%"
6.523- name
6.524- (count t (tests self)
6.525- :key (lambda (x) (or (test-lock-p x) (test-persist-p x))))
6.526- (length (tests self)))
6.527- ;; loop over each test, calling `do-test' if locked or persistent
6.528- (map-tests self
6.529- (lambda (x)
6.530- (when (or (test-lock-p x) (test-persist-p x))
6.531- (let ((res (do-test x)))
6.532- (push-result res self)
6.533- (format stream "~@[~<~%~:;~:@(~S~) ~>~]~%" res)))))
6.534- ;; compare locked vs expected
6.535- (let ((locked (remove-if #'null (map-tests self (lambda (x) (when (test-lock-p x) x)))))
6.536- (fails
6.537- ;; collect if locked test not expected
6.538- (loop for r in (test-results self)
6.539- unless (test-pass-p r)
6.540- collect r)))
6.541- (if (null locked)
6.542- (format stream "~&No tests failed.~%")
6.543- (progn
6.544- ;; RESEARCH 2023-09-04: print fails ??
6.545- (format stream "~&~A out of ~A ~
6.546- total tests failed: ~
6.547- ~:@(~{~<~% ~1:;~S~>~
6.548- ~^, ~}~)."
6.549- (length locked)
6.550- (length (tests self))
6.551- locked)
6.552- (unless (null fails)
6.553- (format stream "~&~A unexpected failures: ~
6.554- ~:@(~{~<~% ~1:;~S~>~
6.555- ~^, ~}~)."
6.556- (length fails)
6.557- fails))))
6.558- ;; close stream
6.559- (finish-output stream)
6.560- ;; return values (PASS? LOCKED)
6.561- (values (not fails) locked))))
6.562-
6.563-;;; Checks
6.564-(flet ((%test (val form)
6.565- (let ((r
6.566- (if val
6.567- (make-test-result :pass form)
6.568- (make-test-result :fail form))))
6.569- (debug! r)
6.570- r)))
6.571- (defmacro is (test &rest args)
6.572- "The DWIM Check.
6.573-
6.574-(is (= 1 1) :test 100) ;=> #S(TEST-RESULT :TAG :PASS :FORM (= 1 1))
6.575-If TEST returns a truthy value, return a PASS test-result, else return
6.576-a FAIL. The TEST is parameterized by ARGS which is a plist or nil.
6.577-
6.578-If ARGS is nil, TEST is bound to to the RESULT slot of the test-result
6.579-and evaluated 'as-is'.
6.580-
6.581-(nyi!)
6.582-ARGS may contain the following keywords followed by a corresponding
6.583-value:
6.584-
6.585-:EXPECTED
6.586-
6.587-:TIMEOUT
6.588-
6.589-:THEN
6.590-
6.591-All other values are treated as let bindings.
6.592-"
6.593- (with-gensyms (form)
6.594- `(if ,(null args)
6.595- (if *testing*
6.596- (push-result (funcall ,#'%test ,test ',test) *testing*)
6.597- (funcall ,#'%test ,test ',test))
6.598- (macrolet ((,form (test) `(let ,,(group args 2) ,,test)))
6.599- ;; TODO 2023-09-21: does this work...
6.600- (if *testing*
6.601- (push-result (funcall ,#'%test (,form ,test) ',test) *testing*)
6.602- (funcall ,#'%test (,form ,test) ',test)))))))
6.603-
6.604-(defmacro signals (condition-spec &body body)
6.605- "Generates a passing TEST-RESULT if body signals a condition of type
6.606-CONDITION-SPEC. BODY is evaluated in a block named NIL, CONDITION-SPEC
6.607-is not evaluated."
6.608- (let ((block-name (gensym)))
6.609- (destructuring-bind (condition &optional reason-control &rest reason-args)
6.610- (ensure-list condition-spec)
6.611- `(block ,block-name
6.612- (handler-bind ((,condition (lambda (c)
6.613- ;; ok, body threw condition
6.614- ;; TODO 2023-09-05: result collectors
6.615- ;; (add-result 'test-passed
6.616- ;; :test-expr ',condition)
6.617- (return-from ,block-name (make-test-result :pass ',body)))))
6.618- (block nil
6.619- ,@body))
6.620- (fail!
6.621- ',condition
6.622- ,@(if reason-control
6.623- `(,reason-control ,@reason-args)
6.624- `("Failed to signal a ~S" ',condition)))
6.625- (return-from ,block-name nil)))))
6.626-
6.627-;;; Macros
6.628-(defmacro deftest (name props &body body)
6.629- "Build a test with NAME, parameterized by LAMBDA-LIST and with a test form of BODY."
6.630- (destructuring-bind (pr doc dec fn)
6.631- (multiple-value-bind (forms dec doc)
6.632- ;; parse body with docstring allowed
6.633- (sb-int:parse-body
6.634- (or body) t)
6.635- `(,props ',doc ',dec ',forms))
6.636- ;; TODO 2023-09-21: parse plist
6.637- `(let ((obj (make-test
6.638- :name ',(format nil "~A" name)
6.639- ;; note: we could leave these unbound if we want,
6.640- ;; personal preference
6.641- :form ,fn
6.642- ,@(when-let ((v (getf pr :persist))) `(:persist ,v))
6.643- ,@(when-let ((v (getf pr :args))) `(:args ,v))
6.644- ,@(when-let ((v (getf pr :bench))) `(:bench ,v))
6.645- ,@(when-let ((v (getf pr :profile))) `(:profile ,v))
6.646- ,@(when doc `(:doc ,doc))
6.647- ,@(when dec `(:decl ,dec)))))
6.648- (push-test obj *test-suite*)
6.649- obj)))
6.650-
6.651-(defmacro defsuite (suite-name &rest props)
6.652- "Define a `test-suite' with provided keys. The object returned can be
6.653-enabled using the `in-suite' macro, similiar to the `defpackage' API."
6.654- (check-type suite-name (or symbol string))
6.655- `(eval-when (:compile-toplevel :load-toplevel :execute)
6.656- (let ((obj (make-suite
6.657- :name (format nil "~A" ',suite-name)
6.658- ,@(when-let ((v (getf props :stream))) `(:stream ,v)))))
6.659- (setq *test-suite-list* (spush obj *test-suite-list* :test #'test-name=))
6.660- obj)))
6.661-
6.662-(defmacro in-suite (name)
6.663- "Set `*test-suite*' to the `test-suite' referred to by symbol
6.664-NAME. Return the `test-suite'."
6.665- (assert-suite name)
6.666- `(setf *test-suite* (ensure-suite ',name)))
6.667-
6.668-(provide :rt)
7.1--- a/lisp/lib/rt/tests.lisp Mon Oct 16 19:33:42 2023 -0400
7.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
7.3@@ -1,17 +0,0 @@
7.4-;;; rt/tests.lisp
7.5-(defpkg :rt/tests
7.6- (:use :cl :rt))
7.7-(defsuite :rt)
7.8-(in-suite :rt)
7.9-(deftest rt (:bench 100 :profile t :persist nil)
7.10- (is (typep (make-fixture-prototype :empty nil) 'fixture-prototype))
7.11- (with-fixture (fx (make-fixture ((a 1) (b 2))
7.12- (:+ () (+ (incf a) (incf b)))
7.13- (:- () (- (decf a) (decf b)))
7.14- (t () 0)))
7.15- (is (= 5 (funcall fx :+)))
7.16- (is (= 7 (funcall fx :+)))
7.17- (is (= 5 (funcall fx :-)))
7.18- (is (= 0 (funcall fx))))
7.19- (signals (error t) (test-form (make-instance 'test-result))))
7.20-
8.1--- a/lisp/lib/sxp.asd Mon Oct 16 19:33:42 2023 -0400
8.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
8.3@@ -1,27 +0,0 @@
8.4-;;; sxp.asd
8.5-(defsystem "sxp"
8.6- :version "0.1.0"
8.7- :author "ellis <ellis@rwest.io>"
8.8- :maintainer "ellis <ellis@rwest.io>"
8.9- :depends-on ("macs")
8.10- :description "S-eXPressions"
8.11- :homepage "https://rwest.io/sxp"
8.12- :bug-tracker "https://lab.rwest.io/comp/sxp/issues"
8.13- :source-control "https://lab.rwest.io/comp/sxp"
8.14- :license "WTF"
8.15- :in-order-to ((test-op (test-op :sxp/tests)))
8.16- :components ((:file "sxp/sxp")))
8.17-
8.18-(defmethod perform :after ((op load-op) (c (eql (find-system :sxp))))
8.19- (pushnew :sxp *features*))
8.20-
8.21-(defsystem "sxp/tests"
8.22- :depends-on ("sxp" "rt" "uiop")
8.23- :components ((:file "sxp/tests"))
8.24- :perform (test-op (op c)
8.25- (uiop:symbol-call '#:rt '#:do-tests)))
8.26-
8.27-(defsystem "sxp/bench"
8.28- :depends-on ("sxp" "uiop" "sb-sprof" "flamegraph")
8.29- :components ((:file "sxp/bench"))
8.30- :perform (test-op (op c) (uiop:symbol-call '#:sxp-bench '#:run-bench)))
9.1--- a/lisp/lib/sxp/bench.lisp Mon Oct 16 19:33:42 2023 -0400
9.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
9.3@@ -1,49 +0,0 @@
9.4-(require :sb-sprof)
9.5-(defpackage :sxp-bench
9.6- (:use :cl :sxp :sb-ext :sb-unix)
9.7- (:export :run-bench :*bench-input-file* :*bench-input-string* :*bench-input-object*
9.8- :*bench-output-directory* :*bench-iterations* :*bench-report-file* ;:*bench-flamegraph-file*
9.9- ))
9.10-(in-package :sxp-bench)
9.11-(declaim
9.12- (type (or string pathname) *bench-input-file* *bench-output-directory* *bench-report-file*)
9.13- (type string *bench-input-string*)
9.14- (type sxp *bench-input-object*)
9.15- (type integer *bench-iterations*))
9.16-(defparameter *bench-input-file* "tests.sxp")
9.17-(defparameter *bench-input-string* (uiop:read-file-string *bench-input-file*))
9.18-(defparameter *bench-input-object* (make-instance 'sxp))
9.19-(read-sxp-string *bench-input-object* *bench-input-string*)
9.20-
9.21-(defparameter *bench-output-directory* "/tmp/sxp-bench")
9.22-(defparameter *bench-iterations* 1000)
9.23-(defparameter *bench-report-file* "bench.sxp")
9.24-;; (defparameter *bench-flamegraph-file* "bench.stack")
9.25-(defmacro bench (&body body)
9.26- `(loop for i from 1 to *bench-iterations*
9.27- do ,@body))
9.28-
9.29-(defun rbench (fn input)
9.30- (let ((res))
9.31- (bench (call-with-timing (lambda (&rest x) (push (cons i x) res)) fn input))
9.32- (nreverse res)))
9.33-
9.34-(defun wbench (fn)
9.35- (let ((res))
9.36- (bench (let ((out (make-pathname :name (format nil "~d.sxp" i) :directory *bench-output-directory*)))
9.37- (call-with-timing (lambda (&rest x) (push (cons i x) res)) fn *bench-input-object* out :if-exists :supersede)))
9.38- (nreverse res)))
9.39-
9.40-(defun run-bench (&optional rpt)
9.41- (when (probe-file *bench-output-directory*)
9.42- (sb-ext:delete-directory *bench-output-directory* :recursive t))
9.43- (sb-unix:unix-mkdir *bench-output-directory* #o777)
9.44- (let ((rres (sb-sprof:with-profiling (:sample-interval 0.001) (rbench #'sxp:read-sxp-file *bench-input-file*)))
9.45- (wres (sb-sprof:with-profiling (:sample-interval 0.001) (wbench #'sxp:write-sxp-file))))
9.46- (if rpt
9.47- (progn
9.48- (format t "Writing output to ~s" *bench-report-file*)
9.49- (uiop:with-output-file (out *bench-report-file* :if-exists :supersede :if-does-not-exist :create)
9.50- (print `(,@rres ,@wres) out)))
9.51- (print (list rres wres))))
9.52- (terpri))
10.1--- a/lisp/lib/sxp/sxp.lisp Mon Oct 16 19:33:42 2023 -0400
10.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
10.3@@ -1,158 +0,0 @@
10.4-;;; sxp.lisp --- S-eXPressions
10.5-
10.6-;; sxp is a unified S-Expression data format
10.7-
10.8-;;; Code:
10.9-(pkg:defpkg :sxp
10.10- (:use :cl :sb-mop :sym :fu)
10.11- (:import-from :uiop :read-file-forms :slurp-stream-forms :with-output-file)
10.12- ;; TODO: hot-patch readtables into sxp classes/parsers
10.13- (:import-from :readtables :defreadtable :in-readtable)
10.14- (:export
10.15- :sxp-fmt-designator
10.16- :form :formp :sxp-error :sxp-fmt-error :sxp-syntax-error :reader :writer :fmt
10.17- :wrap :wrap! :wrap-from-string! :unwrap :unwrap! :unwrap-or
10.18- :sxpp :build-ast :load-ast :ast
10.19- :define-macro :define-fmt :read-sxp-file :write-sxp-file
10.20- :read-sxp-string :write-sxp-string :read-sxp-stream :write-sxp-stream
10.21- :make-sxp :sxp :formp :form
10.22- :wrap-object :unwrap-object))
10.23-
10.24-(in-package :sxp)
10.25-
10.26-(defun formp (form)
10.27- (or (consp form) (atom form)))
10.28-
10.29-(deftype form ()
10.30- '(satisfies formp))
10.31-
10.32-;;; Conditions
10.33-(define-condition sxp-error (error) ())
10.34-
10.35-(define-condition sxp-fmt-error (sxp-error)
10.36- ((format-control :initarg :format-control :reader format-control)
10.37- (format-arguments :initarg :format-arguments :reader format-arguments))
10.38- (:report (lambda (c s)
10.39- (apply 'format s (format-control c) (format-arguments c)))))
10.40-
10.41-(define-condition sxp-syntax-error (sxp-error) ())
10.42-
10.43- ;;; Protocol
10.44-(defgeneric wrap (self form))
10.45-(defgeneric wrap! (self form))
10.46-(defgeneric wrap-from-string! (self str))
10.47-(defgeneric unwrap (self))
10.48-(defgeneric unwrap! (self))
10.49-(defgeneric unwrap-or (self lambda))
10.50-(defgeneric sxpp (self form))
10.51-
10.52-(defgeneric write-sxp-stream (self stream &key pretty case))
10.53-(defgeneric read-sxp-stream (self stream))
10.54-
10.55-(defgeneric build-ast (self &key &allow-other-keys)
10.56- (:documentation "build the sxp representation of SELF and store it in the :ast
10.57-slot. The :ast slot is always ignored."))
10.58-
10.59-(defgeneric load-ast (self)
10.60- (:documentation "load the object SELF from the :ast slot."))
10.61-
10.62-;;; Objects
10.63-(defclass sxp ()
10.64- ((ast :initarg :ast :type form :accessor ast))
10.65- (:documentation "Dynamic class representing a SXP form."))
10.66-
10.67-(defmethod wrap! ((self sxp) form) (setf (slot-value self 'ast) (ignore-errors form)))
10.68-
10.69-(defmethod wrap-from-string! ((self sxp) str) (setf (slot-value self 'ast) (ignore-errors (read str))))
10.70-
10.71-(defmethod wrap ((self sxp) form) (setf (slot-value self 'ast) form))
10.72-
10.73-(defmethod unwrap ((self sxp)) (slot-value self 'ast))
10.74-
10.75-(defmethod unwrap! ((self sxp)) (ignore-errors (slot-value self 'ast)))
10.76-
10.77-(defmethod unwrap-or ((self sxp) else-fn)
10.78- (if (slot-unbound 'sxp self 'ast)
10.79- (slot-value self 'ast)
10.80- (if (null (slot-value self 'ast))
10.81- (funcall else-fn))))
10.82-
10.83-(defmethod write-sxp-stream ((self sxp) stream &key (pretty *print-pretty*) (case :downcase))
10.84- (write (ast self)
10.85- :stream stream
10.86- :pretty pretty
10.87- :case case))
10.88-
10.89-(defmethod read-sxp-stream ((self sxp) stream)
10.90- (setf (ast self) (slurp-stream-forms stream :count nil)))
10.91-
10.92-;; (defsetf unwrap ) (defsetf wrap )
10.93-
10.94-;;; Functions
10.95-(defun read-sxp-file (file)
10.96- (make-instance 'sxp :ast (read-file-forms file)))
10.97-
10.98-(defun write-sxp-file (sxp file &optional &key if-exists)
10.99- (with-output-file (out file) :if-exists if-exists
10.100- (write-sxp-stream sxp out)))
10.101-
10.102-(defun read-sxp-string (self str) (with-input-from-string (s str) (read-sxp-stream self s)))
10.103-
10.104-(defun write-sxp-string (sxp)
10.105- (let ((ast (ast sxp)))
10.106- (if (> (length ast) 1)
10.107- (write-to-string ast)
10.108- (write-to-string (car ast)))))
10.109-
10.110-(defun make-sxp (&rest form) (make-instance 'sxp :ast form))
10.111-
10.112-(deftype sxp-fmt-designator () '(member :canonical :collapsed))
10.113-
10.114-(defun unwrap-object (obj &key (slots t) (methods nil)
10.115- (indirect nil) (tag nil)
10.116- (unboundp nil) (nullp nil)
10.117- (exclude nil))
10.118- "Build and return a new `form' from OBJ by traversing the class
10.119-definition. This differs from the generic function `unwrap' which
10.120-always uses the ast slot as an internal buffer. We can also call this
10.121-on any class instance (doesn't need to subclass `sxp').
10.122-
10.123-SLOTS specifies the slots to be included in the output. If the value
10.124-is t, all slots are included. The ast slot is not included by default,
10.125-but this behavior may change in future revisions.
10.126-
10.127-When INDIRECT is non-nil, also include methods which indirectly
10.128-specialize on OBJ.
10.129-
10.130-When TAG is non-nil, return a cons where car is TAG and cdr is the
10.131-output. If TAG is t, use the class-name symbol."
10.132- (declare (type standard-object obj)
10.133- (type (or list boolean) slots)
10.134- (type (or list boolean) methods)
10.135- (type boolean indirect)
10.136- (type list exclude))
10.137- (unless (or slots methods)
10.138- (error "Required one missing key arg: SLOTS or METHODS"))
10.139- (let* ((class (class-of obj))
10.140- (res (when tag (list (if (eq t tag) (class-name class) tag)))))
10.141- (block unwrap
10.142- (when-let ((slots (when slots
10.143- (list-class-slots class slots exclude))))
10.144- (let ((slot-vals (list-slot-values-using-class class obj (remove-if #'null slots) nullp unboundp)))
10.145- (if methods
10.146- (push slot-vals res)
10.147- (return-from unwrap (push slot-vals res)))))
10.148- (when-let ((methods (when methods (list-class-methods class methods indirect))))
10.149- (push methods res)))
10.150- (flatten res)))
10.151-
10.152-(defun wrap-object (class form)
10.153- "Given a CLASS prototype and an input FORM, return a new instance of
10.154-CLASS. FORM is assumed to be the finalized lisp object which has
10.155-already passed through `read' -- not a string or file-stream for
10.156-example."
10.157- (declare (type class class)
10.158- (type form form)))
10.159-
10.160-;; (defmacro define-fmt ())
10.161-;; (defmacro define-macro ())
11.1--- a/lisp/lib/sxp/tests.lisp Mon Oct 16 19:33:42 2023 -0400
11.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
11.3@@ -1,38 +0,0 @@
11.4-;;; tests.lisp --- SEXP tests
11.5-(pkg:defpkg :sxp.tests
11.6- (:use :cl :sxp :macs :rt)
11.7- (:export :*test-file* :*test-string*))
11.8-
11.9-(in-package :sxp.tests)
11.10-(in-readtable *macs-readtable*)
11.11-(declaim
11.12- (type (or string pathname) *test-file*)
11.13- (type string *test-string*))
11.14-(defparameter *test-file* "tests.sxp")
11.15-(defparameter *test-string* "(FOO 'BAR `(\"test\" ,BAZ ,@QUX) 123 0.0123 1/3 `(,A1 ,A2))")
11.16-
11.17-(defsuite :sxp)
11.18-(in-suite :sxp)
11.19-
11.20-(deftest forms ()
11.21- (is (formp nil))
11.22- (is (formp t))
11.23- (is (formp 3.14))
11.24- (is (formp "string"))
11.25- (is (formp (mapc #`(',a1) '(a))))
11.26- (is (formp ())))
11.27-
11.28-(deftest sxp-file ()
11.29- (let ((f (read-sxp-file *test-file*)))
11.30- (is (equal (unwrap f) (unwrap f)))))
11.31-
11.32-(deftest sxp-string ()
11.33- (let ((f (make-instance 'sxp)))
11.34- (is (formp (read-sxp-string f *test-string*)))
11.35- (is (equalp (read-from-string (write-sxp-string f)) (read-from-string *test-string*)))))
11.36-
11.37-(deftest sxp-stream ()
11.38- (let ((f (make-instance 'sxp)))
11.39- (with-input-from-string (s *test-string*)
11.40- (read-sxp-stream f s))
11.41- (is (write-sxp-stream f nil))))
12.1--- a/lisp/lib/sxp/tests.sxp Mon Oct 16 19:33:42 2023 -0400
12.2+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
12.3@@ -1,42 +0,0 @@
12.4-; skip me maybe
12.5-;; this file does not contain quote characters.
12.6-(edges-1
12.7-(
12.8-(1389.886593 1341.567282)
12.9-(1383.122623 1339.369530)
12.10-)
12.11-(
12.12-(1383.122623 1339.369530)
12.13-(1387.706464 1325.261939)
12.14-)
12.15-(
12.16-(1387.706464 1325.261939)
12.17-(1394.470360 1327.459664)
12.18-)
12.19-(
12.20-(1394.470360 1327.459664)
12.21-(1389.886593 1341.567282)
12.22-)
12.23-) ; edges end
12.24-
12.25-(edges-2
12.26-( ( 1.1 2.2 ) (2.2 3.3) )
12.27-( ( 2.2 3.3 ) (3.3 3.3) )
12.28-( ( 3.3 3.3 ) (1.1 2.2) )
12.29-) ; end edges of triangle room
12.30-
12.31-(= 4 4)
12.32-(= 5 4)
12.33-(> 4.0 54.0)
12.34-(= 4 s)
12.35-(= (= 4 4) (> 5 4))
12.36-(not (= 3 3))
12.37-(not 4)
12.38-(if (= 4 4) 42 666)
12.39-(if (= 4.0 4.0) (42))
12.40-(+ 4 4)
12.41-(+ 5.0 6.5)
12.42-(- 4 5)
12.43-(^ 2 3)
12.44-(^ 3 2)
12.45-(^ 3 (+ 2 1))
13.1--- a/lisp/std.asd Mon Oct 16 19:33:42 2023 -0400
13.2+++ b/lisp/std.asd Mon Oct 16 22:25:50 2023 -0400
13.3@@ -2,6 +2,14 @@
13.4 (defsystem :std
13.5 :pathname "std"
13.6 :class :package-inferred-system
13.7- :depends-on (:std/all)
13.8+ :defsystem-depends-on (:asdf-package-system)
13.9+ :depends-on (:std/pkg :std/all)
13.10 :in-order-to ((test-op (test-op "std/tests")))
13.11 :perform (test-op (o c) (symbol-call :rt :do-tests :std)))
13.12+
13.13+(defsystem :std/tests
13.14+ :pathname "std"
13.15+ :depends-on (:std :rt)
13.16+ :components ((:file "tests")))
13.17+
13.18+(asdf:register-system-packages "std/all" '(:std))
14.1--- a/lisp/std/alien.lisp Mon Oct 16 19:33:42 2023 -0400
14.2+++ b/lisp/std/alien.lisp Mon Oct 16 22:25:50 2023 -0400
14.3@@ -19,9 +19,9 @@
14.4 ;; represented by objects of type ALIEN-VALUE.
14.5
14.6 ;;; Code:
14.7-(pkg:defpkg :std/alien
14.8+(uiop:define-package :std/alien
14.9 (:nicknames :alien)
14.10- (:use :cl :sb-vm :sb-ext :sb-c :str :sym :fu)
14.11+ (:use :cl :sb-vm :sb-ext :sb-c :std/base :std/fu)
14.12 (:use-reexport :sb-alien)
14.13 (:export
14.14 :copy-c-string
15.1--- a/lisp/std/all.lisp Mon Oct 16 19:33:42 2023 -0400
15.2+++ b/lisp/std/all.lisp Mon Oct 16 22:25:50 2023 -0400
15.3@@ -1,14 +1,14 @@
15.4-(defpkg :std/all
15.5+;; TODO replace with defpkg
15.6+(uiop:define-package :std/all
15.7 (:nicknames :std)
15.8 (:use-reexport
15.9- :pkg
15.10- :named-readtables
15.11- :alien
15.12- :ana
15.13- :pan
15.14- :log
15.15- :str
15.16- :sym
15.17- :list
15.18- :fmt
15.19- :fs))
15.20+ :std/base
15.21+ :std/ana
15.22+ :std/pan
15.23+ :std/log
15.24+ :std/fu
15.25+ :std/fmt
15.26+ :std/sxp
15.27+ :std/cli
15.28+ :std/alien
15.29+ :std/thread))
16.1--- a/lisp/std/ana.lisp Mon Oct 16 19:33:42 2023 -0400
16.2+++ b/lisp/std/ana.lisp Mon Oct 16 22:25:50 2023 -0400
16.3@@ -1,9 +1,9 @@
16.4 ;;; ana.lisp --- anaphoric macros
16.5
16.6 ;;; Code:
16.7-(defpackage :std/ana
16.8- (:use :cl :readtables :fu)
16.9+(uiop:define-package :std/ana
16.10 (:nicknames :ana)
16.11+ (:use :cl :std/named-readtables :std/fu)
16.12 (:export
16.13 #:alambda
16.14 #:nlet-tail
16.15@@ -15,7 +15,7 @@
16.16 #:this
16.17 #:self))
16.18
16.19-(in-package :macs.ana)
16.20+(in-package :std/ana)
16.21
16.22 (in-readtable :std)
16.23
17.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
17.2+++ b/lisp/std/base.lisp Mon Oct 16 22:25:50 2023 -0400
17.3@@ -0,0 +1,6 @@
17.4+(uiop:define-package :std/base
17.5+ (:use-reexport :cl :std/named-readtables :std/pkg :std/str :std/sym :std/list :std/cond))
17.6+
17.7+
17.8+
17.9+
18.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
18.2+++ b/lisp/std/cli.lisp Mon Oct 16 22:25:50 2023 -0400
18.3@@ -0,0 +1,782 @@
18.4+;;; cli.lisp --- cli programming api and utils
18.5+
18.6+;; This package contains a simple api and macros for building lisp CLI
18.7+;; programs.
18.8+
18.9+;;; Commentary:
18.10+
18.11+;; - inspired by: clingon, uiop
18.12+
18.13+;; Basic assumptions at runtime:
18.14+;; - running in a POSIX-compliant shell
18.15+;; - output stream supports UTF-8
18.16+
18.17+;; TODO 2023-10-14: install-ast, install-thunk, proc-args, etc should
18.18+;; return IR types - CLI-IR THUNK and CLI-IR respectively.
18.19+
18.20+;; TODO 2023-10-14: rename cli-ast to cli-ir, install-ast to
18.21+;; install-ir, etc.
18.22+
18.23+;;; Code:
18.24+(uiop:define-package :std/cli
18.25+ (:nicknames :cli)
18.26+ (:use :cl :std/base :std/fu :std/ana :std/fmt :std/log)
18.27+ (:import-from :std/ana :alet)
18.28+ (:import-from :uiop :println)
18.29+ (:import-from :sb-posix :filename-designator)
18.30+ (:import-from :sb-ext :parse-native-namestring)
18.31+ (:shadowing-import-from :sb-ext :exit)
18.32+ (:export
18.33+ :*argv*
18.34+ :init-args
18.35+ :cli-arg0
18.36+ :cli-args
18.37+ :command-line-args
18.38+ :*cli-group-separator*
18.39+ :*cli-opt-kinds*
18.40+ :global-opt-p
18.41+ :exec-path-list
18.42+ :argp
18.43+ :$val
18.44+ :$args
18.45+ :$argc
18.46+ :$opts
18.47+ :$optc
18.48+ :make-shorty
18.49+ :with-cli-handlers
18.50+ :completing-read
18.51+ :make-prompt!
18.52+ :defmain
18.53+ :main
18.54+ :with-cli
18.55+ :make-cli
18.56+ ;; opt-parsers
18.57+ :make-opt-parser
18.58+ :parse-bool-opt
18.59+ :parse-str-opt
18.60+ :parse-form-opt
18.61+ :parse-list-opt
18.62+ :parse-sym-opt
18.63+ :parse-key-opt
18.64+ :parse-num-opt
18.65+ :parse-str-opt
18.66+ :parse-file-opt
18.67+ :parse-dir-opt
18.68+ :make-opts
18.69+ :make-cmds
18.70+ :active-opts
18.71+ :active-cmds
18.72+ :proc-args
18.73+ :make-cli-node
18.74+ :make-cli-ast
18.75+ :proc-args
18.76+ :parse-args
18.77+ :debug-opts
18.78+ :do-cmd
18.79+ :do-opt
18.80+ :call-opt
18.81+ :call-cmd
18.82+ :apply-cmd
18.83+ :print-help
18.84+ :print-version
18.85+ :print-usage
18.86+ :handle-unknown-argument
18.87+ :handle-missing-argument
18.88+ :handle-invalid-argument
18.89+ :cli-opt
18.90+ :cli-val
18.91+ :cli-cmd-args
18.92+ :cli-cmd
18.93+ :cli-cwd
18.94+ :find-cmd
18.95+ :find-opt
18.96+ :find-short-opt
18.97+ :install-ast
18.98+ ;; :gen-cli-thunk
18.99+ :install-thunk
18.100+ :cli
18.101+ :cli-equal
18.102+ :defopt
18.103+ :defcmd
18.104+ :define-cli
18.105+ ;; ast types
18.106+ :opt
18.107+ :cmd
18.108+ :arg
18.109+ :cli-name
18.110+ :cli-opts
18.111+ :cli-cmds
18.112+ :cli-thunk
18.113+ :cli-description
18.114+ :cli-version
18.115+ :cli-usage))
18.116+
18.117+(in-package :std/cli)
18.118+
18.119+(defun cli-arg0 () (car sb-ext:*posix-argv*))
18.120+(defun cli-args () (cdr sb-ext:*posix-argv*))
18.121+
18.122+(declaim (inline exec-path-list))
18.123+(defun exec-path-list ()
18.124+ (let ((var (sb-posix:getenv "PATH")))
18.125+ (mapcar #'directory
18.126+ (loop for i = 0 then (1+ j)
18.127+ as j = (position #\: var :start i)
18.128+ collect (subseq var i j)
18.129+ while j))))
18.130+
18.131+(defparameter *cli-group-separator*
18.132+ "--"
18.133+ "A marker specifying the end of a unique group of CLI args.")
18.134+
18.135+;; uiop:command-line-arguments
18.136+
18.137+;;; Macros
18.138+(defmacro argp (arg &optional (args (cli-args)))
18.139+ "Test for presence of ARG in ARGS. Return the tail of
18.140+ARGS starting from the position of ARG."
18.141+ `(member ,arg ,args :test #'string=))
18.142+
18.143+(defmacro make-shorty (name)
18.144+ "Return the first char of symbol or string NAME."
18.145+ `(character (aref (if (stringp ,name) ,name (symbol-name ,name)) 0)))
18.146+
18.147+;; (defun treat-as-argument (condition)
18.148+;; "A handler which can be used to invoke the `treat-as-argument' restart"
18.149+;; (invoke-restart (find-restart 'treat-as-argument condition)))
18.150+
18.151+;; (defun discard-argument (condition)
18.152+;; "A handler which can be used to invoke the `discard-argument' restart"
18.153+;; (invoke-restart (find-restart 'discard-argument condition)))
18.154+
18.155+(defmacro with-cli-handlers (form)
18.156+ "A wrapper which handles common cli errors that may occur during
18.157+evaluation of FORM."
18.158+ `(handler-case ,form
18.159+ (sb-sys:interactive-interrupt ()
18.160+ (format *error-output* "~&(:SIGINT)~&")
18.161+ (exit :code 130))
18.162+ (error (c)
18.163+ (format *error-output* "~&~A~&" c)
18.164+ (exit :code 1))))
18.165+
18.166+(defun init-args () (setq *argv* (cons (cli-arg0) (cli-args))))
18.167+
18.168+(defmacro with-cli (slots cli &body body)
18.169+ "Like with-slots with some extra bindings."
18.170+ ;; (with-gensyms (cli-body)
18.171+ ;; (let ((cli-body (mapcar (lambda (x) ()) cli-body)
18.172+ `(progn
18.173+ (init-args)
18.174+ (setf (cli-cwd ,cli) (sb-posix:getcwd))
18.175+ (with-slots ,slots (parse-args ,cli *argv* :compile t)
18.176+ ,@body)))
18.177+
18.178+;;; Prompts
18.179+(declaim (inline completing-read))
18.180+(defun completing-read (prompt collection
18.181+ &key (history nil) (default nil)
18.182+ (key nil) (test nil))
18.183+
18.184+ "A simplified COMPLETING-READ for common-lisp.
18.185+
18.186+The Emacs completion framework includes a function called
18.187+`completing-read' which prompts the user for input from the
18.188+mini-buffer. It is a very flexible interface which can be used to read
18.189+user input programatically. This is incredibly useful for building
18.190+data entry interfaces -- for example see the `make-prompt!' macro.
18.191+
18.192+Obviously writing a completion framework is out-of-scope, but we can
18.193+simulate one by embedding a DSL in our prompters if we choose. For
18.194+example, perhaps we treat a single '?' character as a request from the
18.195+user to list valid options while continue waiting for input."
18.196+ (princ prompt)
18.197+ ;; ensure we empty internal buffer
18.198+ (finish-output)
18.199+ (let* ((coll (symbol-value collection))
18.200+ (r (if coll
18.201+ (find (read-line) coll :key key :test test)
18.202+ (or (read-line) default))))
18.203+ (prog1
18.204+ r
18.205+ (setf (symbol-value history) (push r history)))))
18.206+
18.207+(defmacro make-prompt! (var &optional prompt)
18.208+ "Generate a 'prompter' from list or variable VAR and optional
18.209+PROMPT string.
18.210+
18.211+This isn't an ideal solution as it does in fact expose a dynamic
18.212+variable (VAR-prompt-history). We should generate accessors and
18.213+keep the variables within lexical scope of the generated
18.214+closure."
18.215+ (with-gensyms (s p h)
18.216+ `(let ((,s (if (boundp ',var) (symbol-value ',var)
18.217+ (progn
18.218+ (defvar ,(symb var) nil)
18.219+ ',(symb var))))
18.220+ (,p (when (stringp ,prompt) ,prompt)) ;; prompt string
18.221+ (,h ',(symb var '-prompt-history))) ;; history symbol
18.222+ (defvar ,(symb var '-prompt-history) nil)
18.223+ (defun ,(symb var '-prompt) ()
18.224+ ,(format nil "Prompt for a value from `~A', use DEFAULT if non-nil
18.225+and no value is provided by user, otherwise fallback to the `car'
18.226+of `~A-PROMPT-HISTORY'." var var)
18.227+ (completing-read
18.228+ (format nil "~A [~A]: "
18.229+ (or ,p ">")
18.230+ (car (symbol-value ,h)))
18.231+ ,s :history ,h :default nil)))))
18.232+
18.233+(defmacro define-cli-constant (name cli &optional doc)
18.234+ `(define-constant ,name ,cli ,@doc :test #'cli-equal))
18.235+
18.236+(defvar *default-cli-def* 'defparameter)
18.237+
18.238+(defmacro defcmd (name &body body)
18.239+ `(defun ,name ($args $opts)
18.240+ (declare (ignorable $args $opts))
18.241+ (let (($argc (length $args))
18.242+ ($optc (length $opts)))
18.243+ (declare (ignorable $argc $optc))
18.244+ ,@body)))
18.245+
18.246+(defmacro defopt (name &body body)
18.247+ `(defun ,name ($val)
18.248+ (declare (ignorable $val))
18.249+ ,@body))
18.250+
18.251+(declaim (inline walk-cli-slots))
18.252+(defun walk-cli-slots (cli)
18.253+ "Walk the plist CLI, performing actions as necessary based on the slot
18.254+keys."
18.255+ (loop for kv in (group cli 2)
18.256+ when (eql :thunk (car kv))
18.257+ return (let ((th (cdr kv)))
18.258+ (if (or (functionp th) (symbolp th)) (funcall th) (compile nil (lambda () th)))))
18.259+ cli)
18.260+
18.261+(defmacro define-cli (name &body body)
18.262+ "Define a symbol NAME bound to a top-level CLI object."
18.263+ (declare (type symbol name))
18.264+ `(,*default-cli-def* ,name (apply #'make-cli t (walk-cli-slots ',body))))
18.265+
18.266+(defmacro defmain (ret &body body)
18.267+ "Define a main function in the current package which returns RET.
18.268+
18.269+Note that this macro does not export the defined function and requires
18.270+`cli:main' to be an external symbol."
18.271+ `(progn
18.272+ (declaim (type stream output))
18.273+ (defun main (&key (output *standard-output*))
18.274+ "Run the top-level function and print to OUTPUT."
18.275+ (let ((*standard-output* output))
18.276+ (with-cli-handlers
18.277+ (progn ,@body ,ret))))))
18.278+
18.279+;;; Utils
18.280+(defvar *argv*)
18.281+
18.282+(defun make-cli (kind &rest slots)
18.283+ "Creates a new CLI object of the given kind."
18.284+ (declare (type (member :opt :cmd :cli t) kind))
18.285+ (apply #'make-instance
18.286+ (cond
18.287+ ((eql kind :cli) 'cli)
18.288+ ((eql kind :opt) 'cli-opt)
18.289+ ((eql kind :cmd) 'cli-cmd)
18.290+ (t 'cli))
18.291+ slots))
18.292+
18.293+;; RESEARCH 2023-09-12: closed over hash-table with short/long flags
18.294+;; to avoid conflicts. if not, need something like a flag-function
18.295+;; slot at class allocation.
18.296+(defmacro make-opts (&body opts)
18.297+ `(map 'vector
18.298+ (lambda (x)
18.299+ (etypecase x
18.300+ (string (make-cli :opt :name x))
18.301+ (list (apply #'make-cli :opt x))
18.302+ (t (make-cli :opt :name (format nil "~(~A~)" x) :global t))))
18.303+ (walk-cli-slots ',opts)))
18.304+
18.305+(defmacro make-cmds (&body opts)
18.306+ `(map 'vector
18.307+ (lambda (x)
18.308+ (etypecase x
18.309+ (string (make-cli :cmd :name x))
18.310+ (list (apply #'make-cli :cmd x))
18.311+ (t (make-cli :cmd :name (format nil "~(~A~)" x)))))
18.312+ (walk-cli-slots ',opts)))
18.313+
18.314+(defun long-opt-p (str)
18.315+ (and (char= (aref str 0) (aref str 1) #\-)
18.316+ (> (length str) 2)))
18.317+
18.318+(defun short-opt-p (str)
18.319+ (and (char= (aref str 0) #\-)
18.320+ (not (char= (aref str 1) #\-))
18.321+ (> (length str) 1)))
18.322+
18.323+(defun opt-group-p (str)
18.324+ (string= str *cli-group-separator*))
18.325+
18.326+(defun opt-prefix-eq (ch str)
18.327+ (char= (aref str 0) ch))
18.328+
18.329+(defun gen-thunk-ll (origin args)
18.330+ (let ((a0 (list (symb '$a 0) origin)))
18.331+ (group
18.332+ (nconc (loop for i from 1 for a in args nconc (list (symb '$a i) a)) a0 )
18.333+ 2)))
18.334+
18.335+;; TODO 2023-10-06:
18.336+;; (defmacro gen-cli-thunk (pvars &rest thunk)
18.337+;; "Generate and return a function based on THUNK suitable for the :thunk
18.338+;; slot of cli objects with pandoric bindings PVARS.")
18.339+
18.340+;;; Protocol
18.341+(defgeneric push-cmd (cmd place))
18.342+
18.343+(defgeneric push-opt (opt place))
18.344+
18.345+(defgeneric pop-cmd (place))
18.346+
18.347+(defgeneric pop-opt (place))
18.348+
18.349+(defgeneric find-cmd (self name &optional active))
18.350+
18.351+(defgeneric find-opt (self name &optional active))
18.352+
18.353+(defgeneric active-cmds (self))
18.354+
18.355+(defgeneric active-opts (self &optional global))
18.356+
18.357+(defgeneric find-short-opt (self ch))
18.358+
18.359+(defgeneric call-opt (self arg))
18.360+
18.361+(defgeneric do-opt (self))
18.362+
18.363+(defgeneric call-cmd (self args opts))
18.364+
18.365+(defgeneric parse-args (self args &key &allow-other-keys)
18.366+ (:documentation "Parse list of strings ARGS using SELF.
18.367+
18.368+A list of the same length as ARGS is returned containing 'cli-ast'
18.369+objects: (OPT . (or char string)) (CMD . string) NIL"))
18.370+
18.371+(defgeneric do-cmd (self)
18.372+ (:documentation "Run the command SELF with args parsed at runtime."))
18.373+
18.374+(defgeneric print-help (self &optional stream)
18.375+ (:documentation "Format cli SELF as a helpful string."))
18.376+
18.377+(defgeneric print-version (self &optional stream)
18.378+ (:documentation "Print the version of SELF."))
18.379+
18.380+(defgeneric print-usage (self &optional stream)
18.381+ (:documentation "Format cli SELF as a useful string."))
18.382+
18.383+(defgeneric handle-unknown-argument (self arg)
18.384+ (:documentation "Handle an unknown argument."))
18.385+
18.386+(defgeneric handle-missing-argument (self arg)
18.387+ (:documentation "Handle a missing argument."))
18.388+
18.389+(defgeneric handle-invalid-argument (self arg)
18.390+ (:documentation "Handle an invalid argument."))
18.391+
18.392+(defgeneric cli-equal (a b))
18.393+
18.394+(defun default-thunk (cli) (lambda (x) (declare (ignore x)) (print-help cli)))
18.395+
18.396+(defvar *cli-opt-kinds* '(bool str form list sym key num file dir))
18.397+
18.398+(defun cli-opt-kind-p (s)
18.399+ (declare (type symbol s))
18.400+ (find s *cli-opt-kinds*))
18.401+
18.402+(defmacro make-opt-parser (kind-spec &body body)
18.403+ "Return a KIND-opt-parser function based on KIND-SPEC which is either a
18.404+symbol from *cli-opt-kinds* or a list, and optional BODY which
18.405+is a list of handlers for the opt-val."
18.406+ (let* ((kind (if (consp kind-spec) (car kind-spec) kind-spec))
18.407+ (super (when (consp kind-spec) (cadr kind-spec)))
18.408+ (fn-name (symb 'parse- kind '-opt)))
18.409+ ;; thread em
18.410+ (let ((fn1 (when (not (eql 'nil super)) (symb 'parse- super '-opt))))
18.411+ `(progn
18.412+ (defun ,fn-name ($val)
18.413+ "Parse the cli-opt-val $VAL."
18.414+ ;; do stuff
18.415+ (when (not (eql ',fn1 'nil)) (setq $val (funcall ',fn1 $val)))
18.416+ ,@body)))))
18.417+
18.418+(make-opt-parser bool $val)
18.419+
18.420+(make-opt-parser (str bool) (when (stringp $val) $val))
18.421+
18.422+(make-opt-parser (form str) (read-from-string $val))
18.423+
18.424+(make-opt-parser (list form) (when (listp $val) $val))
18.425+
18.426+(make-opt-parser (sym form) (when (symbolp $val) $val))
18.427+
18.428+(make-opt-parser (key form) (when (keywordp $val) $val))
18.429+
18.430+(make-opt-parser (num form) (when (numberp $val) $val))
18.431+
18.432+(make-opt-parser (file str)
18.433+ (when $val (parse-native-namestring $val nil *default-pathname-defaults* :as-directory nil)))
18.434+
18.435+(make-opt-parser (dir str)
18.436+ (when $val (sb-ext:parse-native-namestring $val nil *default-pathname-defaults* :as-directory t)))
18.437+
18.438+;;; Objects
18.439+(defclass cli-opt ()
18.440+ ;; note that cli-opts can have a nil or unbound name slot
18.441+ ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string)
18.442+ (kind :initarg :kind :initform 'boolean :accessor cli-opt-kind :type cli-opt-kind-p)
18.443+ (thunk :initform #'default-thunk :initarg :thunk :type function-lambda-expression :accessor cli-thunk)
18.444+ (val :initarg :val :initform nil :accessor cli-val :type form)
18.445+ (global :initarg :global :initform nil :accessor global-opt-p :type boolean)
18.446+ (description :initarg :description :accessor cli-description :type string)
18.447+ (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean))
18.448+ (:documentation "CLI option"))
18.449+
18.450+(defmethod handle-unknown-argument ((self cli-opt) arg))
18.451+(defmethod handle-missing-argument ((self cli-opt) arg))
18.452+(defmethod handle-invalid-argument ((self cli-opt) arg))
18.453+
18.454+(defmethod initialize-instance :after ((self cli-opt) &key)
18.455+ (with-slots (name thunk) self
18.456+ (unless (stringp name) (setf name (format nil "~(~A~)" name)))
18.457+ (when (symbolp thunk) (setf thunk (funcall (compile nil `(lambda () ,(symbol-function thunk))))))
18.458+ self))
18.459+
18.460+(defmethod install-thunk ((self cli-opt) (lambda function) &optional compile)
18.461+ "Install THUNK into the corresponding slot in cli-cmd SELF."
18.462+ (let ((%thunk (if compile (compile nil lambda) lambda)))
18.463+ (setf (cli-thunk self) %thunk)
18.464+ self))
18.465+
18.466+(defmethod print-object ((self cli-opt) stream)
18.467+ (print-unreadable-object (self stream :type t)
18.468+ (format stream "~A :global ~A :val ~A"
18.469+ (cli-name self)
18.470+ (global-opt-p self)
18.471+ (cli-val self))))
18.472+
18.473+(defmethod print-usage ((self cli-opt) &optional stream)
18.474+ (format stream " -~(~{~A~^/--~}~)~A~A"
18.475+ (if-let ((n (cli-name self)))
18.476+ (list (make-shorty n) n)
18.477+ 'dyn)
18.478+ (if (global-opt-p self) "* " " ")
18.479+ (if-let ((d (and (slot-boundp self 'description) (cli-description self))))
18.480+ (format stream ": ~A" d)
18.481+ "")))
18.482+
18.483+(defmethod cli-equal ((a cli-opt) (b cli-opt))
18.484+ (with-slots (name global kind) a
18.485+ (with-slots ((bn name) (bg global) (bk kind)) b
18.486+ (and (string= name bn)
18.487+ (eql global bg)
18.488+ (eql kind bk)))))
18.489+
18.490+(defmethod call-opt ((self cli-opt) arg)
18.491+ (funcall (compile nil (cli-thunk self)) arg))
18.492+
18.493+(defmethod do-opt ((self cli-opt))
18.494+ (call-opt self (cli-val self)))
18.495+
18.496+(defclass cli-cmd ()
18.497+ ;; name slot is required and must be a string
18.498+ ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string)
18.499+ (opts :initarg :opts :initform (make-array 0 :element-type 'cli-opt)
18.500+ :accessor cli-opts :type (vector cli-opt))
18.501+ (cmds :initarg :cmds :initform (make-array 0 :element-type 'cli-cmd)
18.502+ :accessor cli-cmds :type (vector cli-cmd))
18.503+ (thunk :initform #'default-thunk :initarg :thunk :accessor cli-thunk :type function-lambda-expression)
18.504+ (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean)
18.505+ (description :initarg :description :accessor cli-description :type string)
18.506+ (args :initform nil :initarg :args :accessor cli-cmd-args))
18.507+ (:documentation "CLI command"))
18.508+
18.509+(defmethod initialize-instance :after ((self cli-cmd) &key)
18.510+ (with-slots (name cmds opts thunk) self
18.511+ (unless (stringp name) (setf name (format nil "~(~A~)" name)))
18.512+ (unless (vectorp cmds) (setf cmds (funcall (compile nil `(lambda () ,cmds)))))
18.513+ (unless (vectorp opts) (setf opts (funcall (compile nil `(lambda () ,opts)))))
18.514+ (when (symbolp thunk) (setf thunk (symbol-function thunk)))
18.515+ self))
18.516+
18.517+(defmethod print-object ((self cli-cmd) stream)
18.518+ (print-unreadable-object (self stream :type t)
18.519+ (format stream "~A :opts ~A :cmds ~A :args ~A"
18.520+ (cli-name self)
18.521+ (length (cli-opts self))
18.522+ (length (cli-cmds self))
18.523+ (length (cli-cmd-args self)))))
18.524+
18.525+(defmethod print-usage ((self cli-cmd) &optional stream)
18.526+ (with-slots (opts cmds) self
18.527+ (format stream "~(~A~) ~A~A~A"
18.528+ (cli-name self)
18.529+ (if-let ((d (and (slot-boundp self 'description) (cli-description self))))
18.530+ (format nil ": ~A" d)
18.531+ "")
18.532+ (if (null opts)
18.533+ ""
18.534+ (format nil "~{~% ~A~^~}" (loop for o across opts collect (print-usage o nil))))
18.535+ (if (null cmds)
18.536+ ""
18.537+ (format nil "~% ~{! ~A~}" (loop for c across cmds collect (print-usage c nil)))))))
18.538+
18.539+(defmethod push-cmd ((self cli-cmd) (place cli-cmd))
18.540+ (vector-push self (cli-cmds place)))
18.541+
18.542+(defmethod push-opt ((self cli-opt) (place cli-cmd))
18.543+ (vector-push self (cli-opts place)))
18.544+
18.545+(defmethod pop-cmd ((self cli-cmd))
18.546+ (vector-pop (cli-cmds self)))
18.547+
18.548+(defmethod pop-opt ((self cli-opt))
18.549+ (vector-pop (cli-opts self)))
18.550+
18.551+(defmethod cli-equal ((a cli-cmd) (b cli-cmd))
18.552+ (with-slots (name opts cmds) a
18.553+ (with-slots ((bn name) (bo opts) (bc cmds)) b
18.554+ (and (string= name bn)
18.555+ (if (and (null opts) (null bo))
18.556+ t
18.557+ (unless (member nil (loop for oa across opts
18.558+ for ob across bo
18.559+ collect (cli-equal oa ob)))
18.560+ t))
18.561+ (if (and (null cmds) (null bc))
18.562+ t
18.563+ (unless (member nil (loop for ca across cmds
18.564+ for cb across bc
18.565+ collect (cli-equal ca cb)))
18.566+ t))))))
18.567+
18.568+;; typically when starting from a top-level `cli', the global
18.569+;; `cli-opts' will be parsed first, followed by the first command
18.570+;; found. If a command is found, the tail of the list is passed as
18.571+;; arguments to this function, which can pass additonal arguments to
18.572+;; nested commands.
18.573+
18.574+;; TODO 2023-09-12: Parsing restarts at the `*cli-group-separator*'
18.575+;; if present, or stops at EOI.
18.576+
18.577+(declaim (inline %make-cli-node))
18.578+(defstruct (cli-node (:constructor %make-cli-node)) kind form)
18.579+
18.580+(defun make-cli-node (kind form)
18.581+ (%make-cli-node :kind kind :form form))
18.582+
18.583+(declaim (inline %make-cli-ast))
18.584+(defstruct (cli-ast (:constructor %make-cli-ast)) ast)
18.585+
18.586+(defun make-cli-ast (nodes)
18.587+ (%make-cli-ast :ast nodes))
18.588+
18.589+(defmethod find-cmd ((self cli-cmd) name &optional active)
18.590+ (when-let ((c (find name (cli-cmds self) :key #'cli-name :test #'string=)))
18.591+ (if active
18.592+ ;; maybe issue warning here? report to user
18.593+ (when (cli-lock-p c) c)
18.594+ c)))
18.595+
18.596+(defmethod active-cmds ((self cli-cmd))
18.597+ (remove-if-not #'cli-lock-p (cli-cmds self)))
18.598+
18.599+
18.600+(defmethod find-opt ((self cli-cmd) name &optional active)
18.601+ (when-let ((o (find name (cli-opts self) :key #'cli-name :test #'string=)))
18.602+ (if active
18.603+ (when (cli-lock-p o) o)
18.604+ o)))
18.605+
18.606+(defun active-global-opt-p (opt)
18.607+ "Return non-nil if OPT is active at runtime and global."
18.608+ (when (and (cli-lock-p opt) (global-opt-p opt)) t))
18.609+
18.610+(defmethod active-opts ((self cli-cmd) &optional global)
18.611+ (remove-if-not
18.612+ (if global
18.613+ #'active-global-opt-p
18.614+ #'cli-lock-p)
18.615+ (cli-opts self)))
18.616+
18.617+(defmethod find-short-opt ((self cli-cmd) ch)
18.618+ (find ch (cli-opts self) :key #'cli-name :test #'opt-prefix-eq))
18.619+
18.620+(defmethod proc-args ((self cli-cmd) args)
18.621+ "process ARGS into an ast. Each element of the ast is a node with a
18.622+:kind slot, indicating the type of node and a :form slot which stores
18.623+a value.
18.624+
18.625+For now we parse group separators '--' and insert a nil into the tree,
18.626+this will likely change to generating a new branch in the ast as it
18.627+should be."
18.628+ (make-cli-ast
18.629+ (loop
18.630+ for a in args
18.631+ if (= (length a) 1) collect (make-cli-node 'arg a)
18.632+ ;; SHORT OPT
18.633+ else if (short-opt-p a)
18.634+ collect (if-let ((o (find-short-opt self (aref a 1))))
18.635+ (progn
18.636+ (setf (cli-val o) t)
18.637+ (make-cli-node 'opt o))
18.638+ (make-cli-node 'arg a))
18.639+
18.640+ ;; LONG OPT
18.641+ else if (long-opt-p a)
18.642+ ;; what we actually want to do is consume the next sequence of args - TBD
18.643+ collect (if-let ((o (find-opt self (string-trim "-" a))))
18.644+ (progn
18.645+ (setf (cli-val o) (string-trim "-" a))
18.646+ (make-cli-node 'opt o))
18.647+ (make-cli-node 'arg a))
18.648+ ;; OPT GROUP
18.649+ else if (opt-group-p a)
18.650+ collect nil
18.651+ ;; CMD
18.652+ else if (find-cmd self a)
18.653+ ;; TBD
18.654+ collect (make-cli-node 'cmd (find-cmd self a))
18.655+ ;; ARG
18.656+ else collect (make-cli-node 'arg a))))
18.657+
18.658+(defmethod install-ast ((self cli-cmd) (ast cli-ast))
18.659+ "Install the given AST, recursively filling in value slots."
18.660+ (with-slots (cmds opts) self
18.661+ ;; we assume all nodes in the ast have been validated and the ast
18.662+ ;; itself is consumed. validation is performed in proc-args.
18.663+
18.664+ ;; before doing anything else we lock SELF, which should remain
18.665+ ;; locked for the full runtime duration.
18.666+ (setf (cli-lock-p self) t)
18.667+ (loop named install
18.668+ for (node . tail) on (cli-ast-ast ast)
18.669+ unless (null node)
18.670+ do
18.671+ (with-slots (kind form) node
18.672+ (case kind
18.673+ ;; opts
18.674+ (opt
18.675+ (let ((name (cli-name form))
18.676+ (val (cli-val form)))
18.677+ (when-let ((o (find-opt self name)))
18.678+ (setf (cli-val o) val
18.679+ (cli-lock-p o) t))))
18.680+ ;; when we encounter a command we recurse over the tail
18.681+ (cmd
18.682+ (when-let ((c (find-cmd self (cli-name form))))
18.683+ (setf (cli-lock-p c) t)
18.684+ ;; handle the rest of the AST
18.685+ (install-ast c (make-cli-ast tail))
18.686+ (return-from install)))
18.687+ (arg (push-arg form self)))))
18.688+ (setf (cli-cmd-args self) (nreverse (cli-cmd-args self)))
18.689+ self))
18.690+
18.691+(defmethod install-thunk ((self cli-cmd) (lambda function) &optional compile)
18.692+ "Install THUNK into the corresponding slot in cli-cmd SELF."
18.693+ (let ((%thunk (if compile (compile nil lambda) lambda)))
18.694+ (setf (cli-thunk self) %thunk)
18.695+ self))
18.696+
18.697+(defmethod push-arg (arg (self cli-cmd))
18.698+ (push arg (cli-cmd-args self)))
18.699+
18.700+(defmethod parse-args ((self cli-cmd) args &key (compile nil))
18.701+ "Parse ARGS and return the updated object SELF.
18.702+
18.703+ARGS is assumed to be a valid cli-ast (list of cli-nodes), unless
18.704+COMPILE is t, in which case a list of strings is assumed."
18.705+ (with-slots (opts cmds) self
18.706+ (let ((args (if compile (proc-args self args) args)))
18.707+ (print (install-ast self args)))))
18.708+
18.709+;; warning: make sure to fill in the opt and cmd slots with values
18.710+;; from the top-level args before doing a command.
18.711+(defmethod call-cmd ((self cli-cmd) args opts)
18.712+ ;; TODO 2023-09-12: handle args/env
18.713+ (funcall (cli-thunk self) args opts))
18.714+
18.715+(defmethod do-cmd ((self cli-cmd))
18.716+ (call-cmd self (cli-cmd-args self) (cli-opts self)))
18.717+
18.718+(defclass cli (cli-cmd)
18.719+ ;; name slot defaults to *package*, must be string
18.720+ ((name :initarg :name :initform (string-downcase (package-name *package*)) :accessor cli-name :type string)
18.721+ (version :initarg :version :initform "0.1.0" :accessor cli-version :type string)
18.722+ ;; TODO 2023-10-11: look into pushd popd - wd-stack?
18.723+ (cwd :initarg :cwd :initform (sb-posix:getcwd) :type string :accessor cli-cwd
18.724+ :documentation "working directory of the top-level CLI."))
18.725+ (:documentation "CLI"))
18.726+
18.727+(defmethod print-usage ((self cli) &optional stream)
18.728+ (iprintln (format nil "usage: ~A [global] <command> [<arg>]~%" (cli-name self)) 2 stream))
18.729+
18.730+(defmethod print-version ((self cli) &optional stream)
18.731+ (println (cli-version self) stream))
18.732+
18.733+(defmethod print-help ((self cli) &optional stream)
18.734+ (println (format nil "~A v~A" (cli-name self) (cli-version self)) stream)
18.735+ (print-usage self stream)
18.736+ (iprintln (cli-description self) 2 stream)
18.737+ ;; (terpri stream)
18.738+ (iprintln "options:" 2 stream)
18.739+ (with-slots (opts cmds) self
18.740+ (unless (null opts)
18.741+ (loop for o across opts
18.742+ do (iprintln (print-usage o) 4 stream)))
18.743+ ;; (terpri stream)
18.744+ (iprintln "commands:" 2 stream)
18.745+ (unless (null cmds)
18.746+ (loop for c across cmds
18.747+ do (iprintln (print-usage c) 4 stream)))))
18.748+
18.749+(defmethod cli-equal :before ((a cli) (b cli))
18.750+ "Return T if A is the same cli object as B.
18.751+
18.752+Currently this function is intended only for instances of the CLI
18.753+class and is used as a specialized EQL for DEFINE-CONSTANT."
18.754+ (with-slots (version) a
18.755+ (with-slots ((bv version)) b
18.756+ (string= version bv))))
18.757+
18.758+;; same as cli-cmd method, default is to compile though
18.759+(defmethod parse-args ((self cli) (args list) &key (compile t))
18.760+ "Parse list of string arguments ARGS and return the updated object SELF."
18.761+ (with-slots (opts cmds) self
18.762+ (let ((args (if compile (proc-args self args) args)))
18.763+ (install-ast self args))))
18.764+
18.765+(declaim (inline debug-opts))
18.766+(defun debug-opts (cli)
18.767+ (let ((o (active-opts cli))
18.768+ (a (cli-cmd-args cli))
18.769+ (c (active-cmds cli)))
18.770+ (debug! (cli-cwd cli) o a c)))
18.771+
18.772+(declaim (inline solop))
18.773+(defun solop (self)
18.774+ (and (= 0 (length (active-opts self t)) (length (active-cmds self)))))
18.775+
18.776+(defmethod do-cmd ((self cli))
18.777+ (if (solop self)
18.778+ (call-cmd self (cli-cmd-args self) (cli-opts self))
18.779+ (progn
18.780+ (loop for o across (active-opts self t)
18.781+ do (do-opt o))
18.782+ (loop for c across (active-cmds self)
18.783+ do (do-cmd c)))))
18.784+
18.785+(provide :cli)
19.1--- a/lisp/std/cond.lisp Mon Oct 16 19:33:42 2023 -0400
19.2+++ b/lisp/std/cond.lisp Mon Oct 16 22:25:50 2023 -0400
19.3@@ -3,7 +3,6 @@
19.4 ;;; Code:
19.5 (defpackage :std/cond
19.6 (:use :cl)
19.7- (:nicknames :cond)
19.8 (:export
19.9 #:nyi!
19.10 #:required-argument
19.11@@ -27,7 +26,7 @@
19.12 #:invalid-argument-p
19.13 #:unwind-protect-case))
19.14
19.15-(in-package :cond)
19.16+(in-package :std/cond)
19.17
19.18 (defmacro nyi! (&optional comment)
19.19 `(prog1
20.1--- a/lisp/std/fmt.lisp Mon Oct 16 19:33:42 2023 -0400
20.2+++ b/lisp/std/fmt.lisp Mon Oct 16 22:25:50 2023 -0400
20.3@@ -3,11 +3,11 @@
20.4 ;;; Code:
20.5 (defpackage :std/fmt
20.6 (:nicknames :fmt)
20.7- (:use :cl :str :fu :list)
20.8+ (:use :std/base :std/fu)
20.9 (:import-from :uiop :println)
20.10 (:export :printer-status :fmt-row :fmt-sxhash :iprintln :fmt-tree))
20.11
20.12-(in-package :fmt)
20.13+(in-package :std/fmt)
20.14
20.15 (defun iprintln (x &optional (n 2) stream)
20.16 (println (format nil "~A~A" (make-string n :initial-element #\Space) x) stream))
21.1--- a/lisp/std/fs.lisp Mon Oct 16 19:33:42 2023 -0400
21.2+++ b/lisp/std/fs.lisp Mon Oct 16 22:25:50 2023 -0400
21.3@@ -13,4 +13,4 @@
21.4 (:use :cl :str :cond :fu)
21.5 (:export))
21.6
21.7-(in-package :fs)
21.8+(in-package :std/fs)
22.1--- a/lisp/std/fu.lisp Mon Oct 16 19:33:42 2023 -0400
22.2+++ b/lisp/std/fu.lisp Mon Oct 16 22:25:50 2023 -0400
22.3@@ -3,7 +3,7 @@
22.4 ;;; Code:
22.5 (defpackage :std/fu
22.6 (:nicknames :fu)
22.7- (:use :cl :sb-mop :sb-c :named-readtables :sym :list :cond)
22.8+ (:use :cl :sb-mop :sb-c :std/named-readtables :std/sym :std/list :std/cond)
22.9 (:export
22.10 :until
22.11 #:mkstr
22.12@@ -54,7 +54,7 @@
22.13 :merge! :sort!
22.14 :list-slot-values-using-class :list-class-methods :list-class-slots :list-indirect-slot-methods))
22.15
22.16-(in-package :fu)
22.17+(in-package :std/fu)
22.18
22.19 ;;; Misc
22.20 (defmacro until (condition &body body)
23.1--- a/lisp/std/list.lisp Mon Oct 16 19:33:42 2023 -0400
23.2+++ b/lisp/std/list.lisp Mon Oct 16 22:25:50 2023 -0400
23.3@@ -2,14 +2,20 @@
23.4
23.5 ;;; Code:
23.6 (defpackage :std/list
23.7- (:nicknames :list)
23.8 (:use :cl)
23.9+ (:shadowing-import-from
23.10+ :sb-int
23.11+ :ensure-list :recons :memq :assq :ensure-list
23.12+ :proper-list-of-length-p :proper-list-p :singleton-p)
23.13 (:export
23.14- #:ensure-car
23.15- #:ensure-cons
23.16- :let-binding-transform))
23.17+ :ensure-car
23.18+ :ensure-cons
23.19+ :let-binding-transform
23.20+ ;; reexports
23.21+ :ensure-list :recons :memq :assq :ensure-list
23.22+ :proper-list-of-length-p :proper-list-p :singleton-p))
23.23
23.24-(in-package :list)
23.25+(in-package :std/list)
23.26
23.27 ;; (reexport-from :sb-int
23.28 ;; :include '(:recons :memq :assq :ensure-list :proper-list-of-length-p :proper-list-p
24.1--- a/lisp/std/log.lisp Mon Oct 16 19:33:42 2023 -0400
24.2+++ b/lisp/std/log.lisp Mon Oct 16 22:25:50 2023 -0400
24.3@@ -19,12 +19,12 @@
24.4 ;;; Code:
24.5 (defpackage :std/log
24.6 (:nicknames :log)
24.7- (:use :cl :str :fmt :sym :fu)
24.8+ (:use :std/base :std/fu)
24.9 (:export :*log-level* :log-level-designator :log-timestamp-source
24.10 :log! :warn! :info! :debug! :trace! :dbg!
24.11 :debug-p))
24.12
24.13-(in-package :log)
24.14+(in-package :std/log)
24.15
24.16 (deftype log-level-designator () '(member :warn :debug :info :trace))
24.17 (declaim (type (or boolean log-level-designator) *log-level*))
25.1--- a/lisp/std/named-readtables.lisp Mon Oct 16 19:33:42 2023 -0400
25.2+++ b/lisp/std/named-readtables.lisp Mon Oct 16 22:25:50 2023 -0400
25.3@@ -12,7 +12,7 @@
25.4 ;; behavior (using standard) versus your source code (custom).
25.5
25.6 ;;; Code:
25.7-(pkg:defpkg :std/named-readtables
25.8+(defpackage :std/named-readtables
25.9 (:nicknames :named-readtables)
25.10 (:use :cl)
25.11 (:export
25.12@@ -37,7 +37,7 @@
25.13 #:readtable-does-not-exist
25.14 #:parse-body))
25.15
25.16-(in-package :named-readtables)
25.17+(in-package :std/named-readtables)
25.18 (pushnew :named-readtables *features*)
25.19
25.20 (defmacro without-package-lock ((&rest package-names) &body body)
26.1--- a/lisp/std/pan.lisp Mon Oct 16 19:33:42 2023 -0400
26.2+++ b/lisp/std/pan.lisp Mon Oct 16 22:25:50 2023 -0400
26.3@@ -3,7 +3,7 @@
26.4 ;;; Code:
26.5 (defpackage :std/pan
26.6 (:nicknames :pan)
26.7- (:use :cl :named-readtables :fu :ana)
26.8+ (:use :cl :std/named-readtables :std/fu :std/ana)
26.9 (:export
26.10 #:pandoriclet
26.11 #:pandoriclet-get
26.12@@ -15,7 +15,7 @@
26.13 #:plambda
26.14 #:pandoric-eval))
26.15
26.16-(in-package :pan)
26.17+(in-package :std/pan)
26.18 (in-readtable :std)
26.19
26.20 (defun pandoriclet-get (letargs)
27.1--- a/lisp/std/pkg.lisp Mon Oct 16 19:33:42 2023 -0400
27.2+++ b/lisp/std/pkg.lisp Mon Oct 16 22:25:50 2023 -0400
27.3@@ -15,7 +15,7 @@
27.4 #:package-definition-form #:parse-defpkg-form
27.5 #:ensure-package))
27.6
27.7-(in-package :pkg)
27.8+(in-package :std/pkg)
27.9
27.10 (eval-when (:load-toplevel :compile-toplevel :execute)
27.11 (defun find-package* (package-designator &optional (error t))
28.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
28.2+++ b/lisp/std/rt.lisp Mon Oct 16 22:25:50 2023 -0400
28.3@@ -0,0 +1,665 @@
28.4+;;; rt.lisp --- regression testing
28.5+
28.6+;; Regression Testing framework. inspired by PCL, the original CMUCL
28.7+;; code, and the SBCL port.
28.8+
28.9+;;; Commentary:
28.10+
28.11+;; - :rt https://www.merl.com/publications/docs/TR91-04.pdf Chapter 1
28.12+;; - :com.gigamonkeys.test https://github.com/gigamonkey/monkeylib-test-framework
28.13+;; - :sb-rt https://github.com/sbcl/sbcl/blob/master/contrib/sb-rt/rt.lisp
28.14+
28.15+;; This package is intended to provide a modernized Lisp testing
28.16+;; library with features found in some of the test frameworks listed
28.17+;; below.
28.18+
28.19+;; - :it.bese.fiveam https://github.com/lispci/fiveam
28.20+;; - :try https://github.com/melisgl/try
28.21+;; - :rove https://github.com/fukamachi/rove
28.22+
28.23+;;; TODO:
28.24+#|
28.25+
28.26+- [ ] benchmark support: do-bench, test-count,
28.27+
28.28+- [ ] fixtures api
28.29+
28.30+- [ ] profiling
28.31+|#
28.32+;;; Code:
28.33+#+x86-64
28.34+(eval-when (:compile-toplevel :load-toplevel :execute)
28.35+ (require 'sb-sprof))
28.36+
28.37+(defpackage :std/rt
28.38+ (:use
28.39+ :std/base :std/sxp :std/fu :std/fmt :std/log :std/ana :std/pan
28.40+ :std/list :std/sym :std/cond :std/str
28.41+ :sb-aprof #+x86-64 :sb-sprof)
28.42+ (:nicknames :rt)
28.43+ (:export
28.44+ :*default-test-opts*
28.45+ :*compile-tests*
28.46+ :*catch-test-errors*
28.47+ :*test-suffix*
28.48+ :*default-test-suite-name*
28.49+ :*test-suite*
28.50+ :*test-suite-list*
28.51+ ;; TODO 2023-09-04: :*test-profiler-list* not yet
28.52+ :*testing*
28.53+ :test-suite-designator
28.54+ :check-suite-designator
28.55+ :make-test
28.56+ :make-suite
28.57+ :test-name=
28.58+ :do-test
28.59+ :do-tests
28.60+ :continue-testing
28.61+ :with-test-env
28.62+ :ensure-suite
28.63+ :test-fixture
28.64+ :fixture-prototype
28.65+ :make-fixture-prototype
28.66+ :make-fixture
28.67+ :with-fixture
28.68+ :test-result
28.69+ :test-pass-p
28.70+ :test-fail-p
28.71+ :test-skip-p
28.72+ :test-failed
28.73+ :fail!
28.74+ :is
28.75+ :signals
28.76+ :deftest
28.77+ :defsuite
28.78+ :in-suite
28.79+ :eval-test
28.80+ :compile-test
28.81+ :locked-tests
28.82+ :push-test
28.83+ :pop-test
28.84+ :delete-test
28.85+ :find-test
28.86+ :do-suite
28.87+ :test-object
28.88+ :test
28.89+ :test-fixture
28.90+ :test-suite
28.91+ :test-name
28.92+ :tests
28.93+ :test-form
28.94+ :test-results))
28.95+
28.96+(in-package :std/rt)
28.97+(in-readtable :std)
28.98+
28.99+;;; Vars
28.100+(defvar *default-test-opts* '(optimize sb-c::instrument-consing))
28.101+(defvar *compile-tests* t
28.102+ "When nil do not compile tests. With a value of t, tests are compiled
28.103+with default optimizations else the value is used to configure
28.104+compiler optimizations.")
28.105+(defvar *catch-test-errors* t "When non-nil, cause errors in a test to be caught.")
28.106+(defvar *test-suffix* "-test" "A suffix to append to every `test' defined with `deftest'.")
28.107+(defvar *test-suite-list* nil "List of available `test-suite' objects.")
28.108+(defvar *test-suite* nil "A 'test-suite-designator' which identifies the current `test-suite'.")
28.109+(eval-when (:compile-toplevel :load-toplevel :execute)
28.110+ (defvar *default-test-suite-name* "default"))
28.111+(declaim (type (or stream boolean string) *test-input*))
28.112+(defvar *test-input* nil "When non-nil, specifies an input stream or buffer for `*testing*'.")
28.113+(defvar *default-bench-count* 100 "Default number of iterations to repeat a bench test for. This value is
28.114+used when the slot value of :BENCH is t.")
28.115+(defvar *testing* nil "Testing state var.")
28.116+
28.117+;;; Utils
28.118+(eval-when (:compile-toplevel :load-toplevel :execute)
28.119+ (defun make-test (&rest slots)
28.120+ (apply #'make-instance 'test slots))
28.121+ (defun make-suite (&rest slots)
28.122+ (apply #'make-instance 'test-suite slots)))
28.123+
28.124+;; TODO 2023-09-04: optimize
28.125+(declaim (inline do-tests))
28.126+(defun do-tests (&optional (suite *test-suite*) (output *standard-output*))
28.127+ (if (pathnamep output)
28.128+ (with-open-file (stream output :direction :output)
28.129+ (do-suite (ensure-suite suite) :stream stream))
28.130+ (do-suite (ensure-suite suite) :stream output)))
28.131+
28.132+;; this assumes that *test-suite* is re-initialized correctly to the
28.133+;; correct test-suite object.
28.134+(defun continue-testing ()
28.135+ (if-let ((test *testing*))
28.136+ (throw '%in-test test)
28.137+ (do-suite *test-suite*)))
28.138+
28.139+;; NOTE 2023-09-01: `pushnew' does not return an indication of whether
28.140+;; place is changed - it returns place. This is functionally sound but
28.141+;; means that if we want to do something else in the event that place
28.142+;; is unchanged, we run into some friction,
28.143+;; https://stackoverflow.com/questions/56228832/adapting-common-lisp-pushnew-to-return-success-failure
28.144+(defun spush (item lst &key (test #'equal))
28.145+ "Substituting `push'"
28.146+ (declare (type function test))
28.147+ (cond
28.148+ ((null lst) (push item lst))
28.149+ ((list lst)
28.150+ (if-let ((found (member item lst
28.151+ :test test)))
28.152+ (progn
28.153+ (rplaca found item)
28.154+ lst)
28.155+ (push item lst)))
28.156+ #|(or nil '(t (cons item lst)))|#))
28.157+
28.158+;; FIX 2023-08-31: spush, replace with `add-test' method.
28.159+;; (declaim (inline normalize-test-name))
28.160+(defun normalize-test-name (a)
28.161+ "Return the normalized `test-suite-designator' of A."
28.162+ (etypecase a
28.163+ (string a)
28.164+ (symbol (symbol-name a))
28.165+ (test-object (test-name a))
28.166+ (t (format nil "~A" a))))
28.167+
28.168+(defun test-name= (a b)
28.169+ "Return t if A and B are similar `test-suite-designator's."
28.170+ (let ((a (normalize-test-name a))
28.171+ (b (normalize-test-name b)))
28.172+ (string= a b)))
28.173+
28.174+;; (declaim (inline assert-suite ensure-suite))
28.175+(defun ensure-suite (name)
28.176+ (if-let ((ok (member name *test-suite-list* :test #'test-name=)))
28.177+ (car ok)
28.178+ (when (or (eq name t) (null name)) (make-suite :name *default-test-suite-name*))))
28.179+
28.180+(defun check-suite-designator (suite) (check-type suite test-suite-designator))
28.181+
28.182+(defun assert-suite (name)
28.183+ (check-suite-designator name)
28.184+ (assert (ensure-suite name)))
28.185+
28.186+(declaim (inline test-opt-key-p test-opt-valid-p))
28.187+(defun test-opt-key-p (k)
28.188+ "Test if K is a `test-opt-key'."
28.189+ (member k '(:profile :save :stream)))
28.190+
28.191+(defun test-opt-valid-p (f)
28.192+ "Test if F is a valid `test-opt' form. If so, return F else nil."
28.193+ (when (test-opt-key-p (car f))
28.194+ f))
28.195+
28.196+;;; Conditions
28.197+(define-condition test-failed (error)
28.198+ ((reason :accessor fail-reason :initarg :reason :initform "unknown")
28.199+ (name :accessor fail-name :initarg :name)
28.200+ (form :accessor fail-form :initarg :form))
28.201+ (:documentation "Signaled when a test fails.")
28.202+ (:report (lambda (c s)
28.203+ (format s "The following expression failed: ~S~%~A."
28.204+ (fail-form c)
28.205+ (fail-reason c)))))
28.206+
28.207+;;; Protocol
28.208+(defgeneric eval-test (self)
28.209+ (:documentation "Eval a `test'."))
28.210+
28.211+(defgeneric compile-test (self &key &allow-other-keys)
28.212+ (:documentation "Compile a `test'."))
28.213+
28.214+(defgeneric locked-tests (self)
28.215+ (:documentation "Return a list of locked tests in `test-suite' object SELF."))
28.216+
28.217+(defgeneric push-test (self place)
28.218+ (:documentation
28.219+ "Push `test' SELF to the value of slot ':tests' in `test-suite' object PLACE."))
28.220+
28.221+(defgeneric pop-test (self)
28.222+ (:documentation
28.223+ "Pop the first `test' from the slot-value of ':tests' in `test-suite' object SELF."))
28.224+
28.225+(defgeneric push-result (self place)
28.226+ (:documentation
28.227+ "Push object SELF to the value of slot ':results' in object PLACE."))
28.228+
28.229+(defgeneric pop-result (self)
28.230+ (:documentation
28.231+ "Pop the first `test-result' from the slot-value of ':tests' from object SELF."))
28.232+
28.233+(defgeneric push-fixture (self place)
28.234+ (:documentation
28.235+ "Push object SELF to the value of slot ':results' in object PLACE."))
28.236+
28.237+(defgeneric delete-test (self &key &allow-other-keys)
28.238+ (:documentation "Delete `test' object specified by `test-object' SELF and optional keys."))
28.239+
28.240+(defgeneric find-test (self name &key &allow-other-keys)
28.241+ (:documentation "Find `test' object specified by name and optional keys."))
28.242+
28.243+(defgeneric do-test (self &optional test)
28.244+ (:documentation
28.245+ "Run `test' SELF, printing results to `*standard-output*'. The second
28.246+argument is an optional fixture.
28.247+
28.248+SELF can also be a `test-suite', in which case the TESTS slot is
28.249+queried for the value of TEST. If TEST is not provided, pops the car
28.250+from TESTS."))
28.251+
28.252+(defgeneric do-suite (self &key &allow-other-keys)
28.253+ (:documentation
28.254+ "Perform actions on `test-suite' object SELF with optional keys."))
28.255+
28.256+;;;; Results
28.257+(deftype result-tag ()
28.258+ '(or (member :pass :fail :skip) null))
28.259+
28.260+(declaim (inline %make-test-result))
28.261+(defstruct (test-result (:constructor %make-test-result)
28.262+ (:conc-name tr-))
28.263+ (tag nil :type result-tag :read-only t)
28.264+ (form nil :type form))
28.265+
28.266+(defun make-test-result (tag &optional form)
28.267+ (%make-test-result :tag tag :form form))
28.268+
28.269+(defmethod test-pass-p ((res test-result))
28.270+ (when (eq :pass (tr-tag res)) t))
28.271+
28.272+(defmethod test-fail-p ((res test-result))
28.273+ (when (eq :fail (tr-tag res)) t))
28.274+
28.275+(defmethod test-skip-p ((res test-result))
28.276+ (when (eq :skip (tr-tag res)) t))
28.277+
28.278+(defmethod print-object ((self test-result) stream)
28.279+ (print-unreadable-object (self stream)
28.280+ (format stream "~A ~A"
28.281+ (tr-tag self)
28.282+ (tr-form self))))
28.283+
28.284+;;; Objects
28.285+(defclass test-object ()
28.286+ ((name :initarg :name :initform (required-argument) :type string :accessor test-name)
28.287+ #+nil (cached :initarg :cache :allocation :class :accessor test-cached-p :type boolean))
28.288+ (:documentation "Super class for all test-related objects."))
28.289+
28.290+(defmethod print-object ((self test-object) stream)
28.291+ "test"
28.292+ (print-unreadable-object (self stream :type t :identity t)
28.293+ (format stream "~A"
28.294+ (test-name self))))
28.295+
28.296+;;;; Tests
28.297+;; HACK 2023-08-31: inherit sxp?
28.298+
28.299+(defclass test (test-object)
28.300+ ((fn :type symbol :accessor test-fn)
28.301+ (bench :type (or boolean fixnum) :accessor test-bench :initform nil :initarg :bench)
28.302+ (profile :type list :accessor test-profile :initform nil :initarg :profile)
28.303+ (args :type list :accessor test-args :initform nil :initarg :args)
28.304+ (decl :type list :accessor test-decl :initform nil :initarg :decl)
28.305+ (form :initarg :form :initform nil :type function-lambda-expression :accessor test-form)
28.306+ (doc :initarg :doc :type string :accessor test-doc)
28.307+ (lock :initarg :lock :type boolean :accessor test-lock-p)
28.308+ (persist :initarg :persist :initform nil :type boolean :accessor test-persist-p)
28.309+ (results :initarg :results :type (array test-result) :accessor test-results))
28.310+ (:documentation "Test class typically made with `deftest'."))
28.311+
28.312+(defmethod test-bench-p ((self test))
28.313+ (when (test-bench self) t))
28.314+
28.315+(defmethod get-bench-count ((self test))
28.316+ (when-let ((v (test-bench self)))
28.317+ (cond
28.318+ ((typep v 'fixnum) v)
28.319+ ((eq v t) *default-bench-count*)
28.320+ ;; unknown value
28.321+ (t nil))))
28.322+
28.323+(defmethod initialize-instance ((self test) &key name)
28.324+ ;; (debug! "building test" name)
28.325+ (setf (test-fn self)
28.326+ (make-symbol
28.327+ (format nil "~A~A"
28.328+ name
28.329+ (gensym *test-suffix*))))
28.330+ (setf (test-lock-p self) t)
28.331+ ;; TODO 2023-09-21: we should count how many checks are in the :form
28.332+ ;; slot and infer the array dimensions.
28.333+ (setf (test-results self) (make-array 0 :element-type 'test-result))
28.334+ (call-next-method))
28.335+
28.336+(defmethod print-object ((self test) stream)
28.337+ (print-unreadable-object (self stream :type t :identity t)
28.338+ (format stream "~A :fn ~A :args ~A :persist ~A"
28.339+ (test-name self)
28.340+ (test-fn self)
28.341+ (test-args self)
28.342+ (test-persist-p self))))
28.343+
28.344+;; TODO 2023-09-01: use sxp?
28.345+;; (defun validate-form (form))
28.346+
28.347+(defmethod push-result ((self test-result) (place test))
28.348+ (with-slots (results) place
28.349+ (push self results)))
28.350+
28.351+(defmethod pop-result ((self test))
28.352+ (pop (test-results self)))
28.353+
28.354+(defmethod eval-test ((self test))
28.355+ `(progn ,@(test-form self)))
28.356+
28.357+(defmethod compile-test ((self test) &key declare &allow-other-keys)
28.358+ (compile
28.359+ (test-fn self)
28.360+ `(lambda ()
28.361+ ,@(when declare `((declare ,declare)))
28.362+ ,@(test-form self))))
28.363+
28.364+(defun fail! (form &optional fmt &rest args)
28.365+ (let ((reason (and fmt (apply #'format nil fmt args))))
28.366+ (with-simple-restart (ignore-fail "Continue testing.")
28.367+ (error 'test-failed :reason reason :form form))))
28.368+
28.369+(defmacro with-test-env (self &body body)
28.370+ `(catch '%in-test
28.371+ (setf (test-lock-p ,self) t)
28.372+ (let* ((*testing* ,self)
28.373+ (bail nil)
28.374+ r)
28.375+ (block bail
28.376+ ,@body
28.377+ (setf (test-lock-p ,self) bail))
28.378+ r)))
28.379+
28.380+(defmethod do-test ((self test) &optional fx)
28.381+ (declare (ignorable fx))
28.382+ (with-test-env self
28.383+ (debug! "running test: " *testing*)
28.384+ (flet ((%do ()
28.385+ (if-let ((opt *compile-tests*))
28.386+ ;; RESEARCH 2023-08-31: with-compilation-unit?
28.387+ (progn
28.388+ (when (eq opt t) (setq opt *default-test-opts*))
28.389+ ;; TODO 2023-09-21: handle failures here
28.390+ (funcall (compile-test self :declare opt))
28.391+ (setf r (make-test-result :pass (test-fn self))))
28.392+ (progn
28.393+ (eval-test self)
28.394+ (setf r (make-test-result :pass (test-name self)))))))
28.395+ (if *catch-test-errors*
28.396+ (handler-bind
28.397+ ((style-warning #'muffle-warning)
28.398+ (error
28.399+ #'(lambda (c)
28.400+ (setf bail t)
28.401+ (setf r (make-test-result :fail c))
28.402+ (return-from bail r))))
28.403+ (%do))
28.404+ (%do)))))
28.405+
28.406+(defmacro bench (iter &body body)
28.407+ `(loop for i from 1 to ,iter
28.408+ do ,@body))
28.409+
28.410+(defmethod do-bench ((self test) &optional fx)
28.411+ (declare (ignorable fx))
28.412+ (with-test-env self
28.413+ (flet ((%do ()
28.414+ (if-let ((opt *compile-tests*))
28.415+ (progn
28.416+ (when (eq opt t) (setq opt *default-test-opts*))
28.417+ ;; TODO 2023-09-21: handle failures here
28.418+ (let ((fn (compile-test self :declare opt)))
28.419+ (bench (test-bench self) (funcall fn)))
28.420+ (setf r (make-test-result :pass (test-fn self))))
28.421+ (progn
28.422+ (bench (test-bench self) (eval-test self))
28.423+ (setf r (make-test-result :pass (test-name self)))))))
28.424+ (if *catch-test-errors*
28.425+ (handler-bind
28.426+ ((style-warning #'muffle-warning)
28.427+ (error
28.428+ #'(lambda (c)
28.429+ (setf bail t)
28.430+ (setf r (make-test-result :fail c))
28.431+ (return-from bail r))))
28.432+ (%do))
28.433+ (%do)))))
28.434+
28.435+;;;; Fixtures
28.436+
28.437+;; Our fixtures are just closures - with a pandoric environment. You
28.438+;; might call it a domain-specific object protocol.
28.439+
28.440+;; You can build fixtures inside a test or use the push-fixture
28.441+;; method on a `test-suite' object.
28.442+
28.443+(deftype fixture () 'form)
28.444+
28.445+(declaim (inline %make-fixture-prototype))
28.446+(defstruct (fixture-prototype (:constructor %make-fixture-prototype)
28.447+ (:conc-name fxp))
28.448+ (kind :empty :type keyword)
28.449+ (form nil :type form))
28.450+
28.451+(defun make-fixture-prototype (kind form)
28.452+ (%make-fixture-prototype :kind kind :form form))
28.453+
28.454+(defmacro make-fixture (letargs &body ds)
28.455+ (let ((letargs (let-binding-transform letargs)))
28.456+ `(let (,@letargs)
28.457+ (dlambda ,@ds))))
28.458+
28.459+(defmacro with-fixture ((var fx) &body body)
28.460+ `(let ((,var ,fx))
28.461+ ,@body))
28.462+
28.463+;;;; Suites
28.464+(defclass test-suite (test-object)
28.465+ ((tests :initarg :set :initform nil :type list :accessor tests
28.466+ :documentation "test-suite tests")
28.467+ (results :initarg :results :initform nil :type list :accessor test-results
28.468+ :documentation "test-suite results")
28.469+ (stream :initarg :stream :initform *standard-output* :type stream :accessor test-stream)
28.470+ (fixtures :initarg :fixtures :initform nil :type list :accessor test-fixtures))
28.471+ (:documentation "A class for collections of related `test' objects."))
28.472+
28.473+(defmethod print-object ((self test-suite) stream)
28.474+ (print-unreadable-object (self stream :type t :identity t)
28.475+ (format stream "~A [~d+~d:~d:~d:~d]"
28.476+ (test-name self)
28.477+ (count t (map-tests self (lambda (x) (not (test-bench-p x)))))
28.478+ (count t (map-tests self #'test-bench-p))
28.479+ (count t (map-tests self #'test-lock-p))
28.480+ (count t (map-tests self #'test-persist-p))
28.481+ (length (test-results self)))))
28.482+
28.483+;; (defmethod reinitialize-instance ((self test-suite) &rest initargs &key &allow-other-keys))
28.484+
28.485+(deftype test-suite-designator ()
28.486+ "Either nil, a symbol, a string, or a `test-suite' object."
28.487+ '(or null symbol string test-suite test keyword))
28.488+
28.489+(defmethod map-tests ((self test-suite) function)
28.490+ (mapcar function (tests self)))
28.491+
28.492+(defmethod push-test ((self test) (place test-suite))
28.493+ (push self (tests place)))
28.494+
28.495+(defmethod pop-test ((self test-suite))
28.496+ (pop (tests self)))
28.497+
28.498+(defmethod push-result ((self test-result) (place test-suite))
28.499+ (with-slots (results) place
28.500+ (push self results)))
28.501+
28.502+(defmethod pop-result ((self test-suite))
28.503+ (pop (test-results self)))
28.504+
28.505+(defmethod find-test ((self test-suite) name &key (test #'test-name=))
28.506+ (declare (type (or string symbol) name)
28.507+ (type function test))
28.508+ (find name (the list (tests self)) :test test))
28.509+
28.510+(defmethod do-test ((self test-suite) &optional test)
28.511+ (push-result
28.512+ (if test
28.513+ (do-test (find-test self (test-name test)))
28.514+ (do-test (pop-test self)))
28.515+ self))
28.516+
28.517+;; HACK 2023-09-01: find better method of declaring failures from
28.518+;; within the body of `deftest'.
28.519+(defmethod do-suite ((self test-suite) &key stream)
28.520+ (when stream (setf (test-stream self) stream))
28.521+ (with-slots (name stream) self
28.522+ (format stream "in suite ~x with ~A/~A tests:~%"
28.523+ name
28.524+ (count t (tests self)
28.525+ :key (lambda (x) (or (test-lock-p x) (test-persist-p x))))
28.526+ (length (tests self)))
28.527+ ;; loop over each test, calling `do-test' if locked or persistent
28.528+ (map-tests self
28.529+ (lambda (x)
28.530+ (when (or (test-lock-p x) (test-persist-p x))
28.531+ (let ((res (do-test x)))
28.532+ (push-result res self)
28.533+ (format stream "~@[~<~%~:;~:@(~S~) ~>~]~%" res)))))
28.534+ ;; compare locked vs expected
28.535+ (let ((locked (remove-if #'null (map-tests self (lambda (x) (when (test-lock-p x) x)))))
28.536+ (fails
28.537+ ;; collect if locked test not expected
28.538+ (loop for r in (test-results self)
28.539+ unless (test-pass-p r)
28.540+ collect r)))
28.541+ (if (null locked)
28.542+ (format stream "~&No tests failed.~%")
28.543+ (progn
28.544+ ;; RESEARCH 2023-09-04: print fails ??
28.545+ (format stream "~&~A out of ~A ~
28.546+ total tests failed: ~
28.547+ ~:@(~{~<~% ~1:;~S~>~
28.548+ ~^, ~}~)."
28.549+ (length locked)
28.550+ (length (tests self))
28.551+ locked)
28.552+ (unless (null fails)
28.553+ (format stream "~&~A unexpected failures: ~
28.554+ ~:@(~{~<~% ~1:;~S~>~
28.555+ ~^, ~}~)."
28.556+ (length fails)
28.557+ fails))))
28.558+ ;; close stream
28.559+ (finish-output stream)
28.560+ ;; return values (PASS? LOCKED)
28.561+ (values (not fails) locked))))
28.562+
28.563+;;; Checks
28.564+(flet ((%test (val form)
28.565+ (let ((r
28.566+ (if val
28.567+ (make-test-result :pass form)
28.568+ (make-test-result :fail form))))
28.569+ (debug! r)
28.570+ r)))
28.571+ (defmacro is (test &rest args)
28.572+ "The DWIM Check.
28.573+
28.574+(is (= 1 1) :test 100) ;=> #S(TEST-RESULT :TAG :PASS :FORM (= 1 1))
28.575+If TEST returns a truthy value, return a PASS test-result, else return
28.576+a FAIL. The TEST is parameterized by ARGS which is a plist or nil.
28.577+
28.578+If ARGS is nil, TEST is bound to to the RESULT slot of the test-result
28.579+and evaluated 'as-is'.
28.580+
28.581+(nyi!)
28.582+ARGS may contain the following keywords followed by a corresponding
28.583+value:
28.584+
28.585+:EXPECTED
28.586+
28.587+:TIMEOUT
28.588+
28.589+:THEN
28.590+
28.591+All other values are treated as let bindings.
28.592+"
28.593+ (with-gensyms (form)
28.594+ `(if ,(null args)
28.595+ (if *testing*
28.596+ (push-result (funcall ,#'%test ,test ',test) *testing*)
28.597+ (funcall ,#'%test ,test ',test))
28.598+ (macrolet ((,form (test) `(let ,,(group args 2) ,,test)))
28.599+ ;; TODO 2023-09-21: does this work...
28.600+ (if *testing*
28.601+ (push-result (funcall ,#'%test (,form ,test) ',test) *testing*)
28.602+ (funcall ,#'%test (,form ,test) ',test)))))))
28.603+
28.604+(defmacro signals (condition-spec &body body)
28.605+ "Generates a passing TEST-RESULT if body signals a condition of type
28.606+CONDITION-SPEC. BODY is evaluated in a block named NIL, CONDITION-SPEC
28.607+is not evaluated."
28.608+ (let ((block-name (gensym)))
28.609+ (destructuring-bind (condition &optional reason-control &rest reason-args)
28.610+ (ensure-list condition-spec)
28.611+ `(block ,block-name
28.612+ (handler-bind ((,condition (lambda (c)
28.613+ ;; ok, body threw condition
28.614+ ;; TODO 2023-09-05: result collectors
28.615+ ;; (add-result 'test-passed
28.616+ ;; :test-expr ',condition)
28.617+ (return-from ,block-name (make-test-result :pass ',body)))))
28.618+ (block nil
28.619+ ,@body))
28.620+ (fail!
28.621+ ',condition
28.622+ ,@(if reason-control
28.623+ `(,reason-control ,@reason-args)
28.624+ `("Failed to signal a ~S" ',condition)))
28.625+ (return-from ,block-name nil)))))
28.626+
28.627+;;; Macros
28.628+(defmacro deftest (name props &body body)
28.629+ "Build a test with NAME, parameterized by LAMBDA-LIST and with a test form of BODY."
28.630+ (destructuring-bind (pr doc dec fn)
28.631+ (multiple-value-bind (forms dec doc)
28.632+ ;; parse body with docstring allowed
28.633+ (sb-int:parse-body
28.634+ (or body) t)
28.635+ `(,props ',doc ',dec ',forms))
28.636+ ;; TODO 2023-09-21: parse plist
28.637+ `(let ((obj (make-test
28.638+ :name ',(format nil "~A" name)
28.639+ ;; note: we could leave these unbound if we want,
28.640+ ;; personal preference
28.641+ :form ,fn
28.642+ ,@(when-let ((v (getf pr :persist))) `(:persist ,v))
28.643+ ,@(when-let ((v (getf pr :args))) `(:args ,v))
28.644+ ,@(when-let ((v (getf pr :bench))) `(:bench ,v))
28.645+ ,@(when-let ((v (getf pr :profile))) `(:profile ,v))
28.646+ ,@(when doc `(:doc ,doc))
28.647+ ,@(when dec `(:decl ,dec)))))
28.648+ (push-test obj *test-suite*)
28.649+ obj)))
28.650+
28.651+(defmacro defsuite (suite-name &rest props)
28.652+ "Define a `test-suite' with provided keys. The object returned can be
28.653+enabled using the `in-suite' macro, similiar to the `defpackage' API."
28.654+ (check-type suite-name (or symbol string))
28.655+ `(eval-when (:compile-toplevel :load-toplevel :execute)
28.656+ (let ((obj (make-suite
28.657+ :name (format nil "~A" ',suite-name)
28.658+ ,@(when-let ((v (getf props :stream))) `(:stream ,v)))))
28.659+ (setq *test-suite-list* (spush obj *test-suite-list* :test #'test-name=))
28.660+ obj)))
28.661+
28.662+(defmacro in-suite (name)
28.663+ "Set `*test-suite*' to the `test-suite' referred to by symbol
28.664+NAME. Return the `test-suite'."
28.665+ (assert-suite name)
28.666+ `(setf *test-suite* (ensure-suite ',name)))
28.667+
28.668+(provide :rt)
29.1--- a/lisp/std/str.lisp Mon Oct 16 19:33:42 2023 -0400
29.2+++ b/lisp/std/str.lisp Mon Oct 16 22:25:50 2023 -0400
29.3@@ -14,13 +14,12 @@
29.4 ;; unicode< unicode> unicode= unicode-equal
29.5 ;; unicode<= unicode>=))
29.6
29.7-(defpackage :std/str
29.8+(uiop:define-package :std/str
29.9 (:use :cl :uiop/driver :sb-unicode)
29.10- (:nicknames :str)
29.11 (:export
29.12 #:string-designator))
29.13
29.14-(in-package :str)
29.15+(in-package :std/str)
29.16
29.17 ;; (mapc (lambda (s) (export s)) sb-unicode-syms)
29.18 ;; (reexport-from
29.19@@ -32,5 +31,3 @@
29.20 or a character."
29.21 `(or symbol string character))
29.22 ;;; TODO 2023-08-27: camel snake kebab
29.23-
29.24-;;; format recipes
30.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
30.2+++ b/lisp/std/sxp.lisp Mon Oct 16 22:25:50 2023 -0400
30.3@@ -0,0 +1,159 @@
30.4+;;; sxp.lisp --- S-eXPressions
30.5+
30.6+;; sxp is a unified S-Expression data format
30.7+
30.8+;;; Code:
30.9+(defpackage :std/sxp
30.10+ (:use :cl :sb-mop :std/sym :std/fu)
30.11+ (:nicknames :sxp)
30.12+ (:import-from :uiop :read-file-forms :slurp-stream-forms :with-output-file)
30.13+ ;; TODO: hot-patch readtables into sxp classes/parsers
30.14+ (:import-from :named-readtables :defreadtable :in-readtable)
30.15+ (:export
30.16+ :sxp-fmt-designator
30.17+ :form :formp :sxp-error :sxp-fmt-error :sxp-syntax-error :reader :writer :fmt
30.18+ :wrap :wrap! :wrap-from-string! :unwrap :unwrap! :unwrap-or
30.19+ :sxpp :build-ast :load-ast :ast
30.20+ :define-macro :define-fmt :read-sxp-file :write-sxp-file
30.21+ :read-sxp-string :write-sxp-string :read-sxp-stream :write-sxp-stream
30.22+ :make-sxp :sxp :formp :form
30.23+ :wrap-object :unwrap-object))
30.24+
30.25+(in-package :std/sxp)
30.26+
30.27+(defun formp (form)
30.28+ (or (consp form) (atom form)))
30.29+
30.30+(deftype form ()
30.31+ '(satisfies formp))
30.32+
30.33+;;; Conditions
30.34+(define-condition sxp-error (error) ())
30.35+
30.36+(define-condition sxp-fmt-error (sxp-error)
30.37+ ((format-control :initarg :format-control :reader format-control)
30.38+ (format-arguments :initarg :format-arguments :reader format-arguments))
30.39+ (:report (lambda (c s)
30.40+ (apply 'format s (format-control c) (format-arguments c)))))
30.41+
30.42+(define-condition sxp-syntax-error (sxp-error) ())
30.43+
30.44+ ;;; Protocol
30.45+(defgeneric wrap (self form))
30.46+(defgeneric wrap! (self form))
30.47+(defgeneric wrap-from-string! (self str))
30.48+(defgeneric unwrap (self))
30.49+(defgeneric unwrap! (self))
30.50+(defgeneric unwrap-or (self lambda))
30.51+(defgeneric sxpp (self form))
30.52+
30.53+(defgeneric write-sxp-stream (self stream &key pretty case))
30.54+(defgeneric read-sxp-stream (self stream))
30.55+
30.56+(defgeneric build-ast (self &key &allow-other-keys)
30.57+ (:documentation "build the sxp representation of SELF and store it in the :ast
30.58+slot. The :ast slot is always ignored."))
30.59+
30.60+(defgeneric load-ast (self)
30.61+ (:documentation "load the object SELF from the :ast slot."))
30.62+
30.63+;;; Objects
30.64+(defclass sxp ()
30.65+ ((ast :initarg :ast :type form :accessor ast))
30.66+ (:documentation "Dynamic class representing a SXP form."))
30.67+
30.68+(defmethod wrap! ((self sxp) form) (setf (slot-value self 'ast) (ignore-errors form)))
30.69+
30.70+(defmethod wrap-from-string! ((self sxp) str) (setf (slot-value self 'ast) (ignore-errors (read str))))
30.71+
30.72+(defmethod wrap ((self sxp) form) (setf (slot-value self 'ast) form))
30.73+
30.74+(defmethod unwrap ((self sxp)) (slot-value self 'ast))
30.75+
30.76+(defmethod unwrap! ((self sxp)) (ignore-errors (slot-value self 'ast)))
30.77+
30.78+(defmethod unwrap-or ((self sxp) else-fn)
30.79+ (if (slot-unbound 'sxp self 'ast)
30.80+ (slot-value self 'ast)
30.81+ (if (null (slot-value self 'ast))
30.82+ (funcall else-fn))))
30.83+
30.84+(defmethod write-sxp-stream ((self sxp) stream &key (pretty *print-pretty*) (case :downcase))
30.85+ (write (ast self)
30.86+ :stream stream
30.87+ :pretty pretty
30.88+ :case case))
30.89+
30.90+(defmethod read-sxp-stream ((self sxp) stream)
30.91+ (setf (ast self) (slurp-stream-forms stream :count nil)))
30.92+
30.93+;; (defsetf unwrap ) (defsetf wrap )
30.94+
30.95+;;; Functions
30.96+(defun read-sxp-file (file)
30.97+ (make-instance 'sxp :ast (read-file-forms file)))
30.98+
30.99+(defun write-sxp-file (sxp file &optional &key if-exists)
30.100+ (with-output-file (out file) :if-exists if-exists
30.101+ (write-sxp-stream sxp out)))
30.102+
30.103+(defun read-sxp-string (self str) (with-input-from-string (s str) (read-sxp-stream self s)))
30.104+
30.105+(defun write-sxp-string (sxp)
30.106+ (let ((ast (ast sxp)))
30.107+ (if (> (length ast) 1)
30.108+ (write-to-string ast)
30.109+ (write-to-string (car ast)))))
30.110+
30.111+(defun make-sxp (&rest form) (make-instance 'sxp :ast form))
30.112+
30.113+(deftype sxp-fmt-designator () '(member :canonical :collapsed))
30.114+
30.115+(defun unwrap-object (obj &key (slots t) (methods nil)
30.116+ (indirect nil) (tag nil)
30.117+ (unboundp nil) (nullp nil)
30.118+ (exclude nil))
30.119+ "Build and return a new `form' from OBJ by traversing the class
30.120+definition. This differs from the generic function `unwrap' which
30.121+always uses the ast slot as an internal buffer. We can also call this
30.122+on any class instance (doesn't need to subclass `sxp').
30.123+
30.124+SLOTS specifies the slots to be included in the output. If the value
30.125+is t, all slots are included. The ast slot is not included by default,
30.126+but this behavior may change in future revisions.
30.127+
30.128+When INDIRECT is non-nil, also include methods which indirectly
30.129+specialize on OBJ.
30.130+
30.131+When TAG is non-nil, return a cons where car is TAG and cdr is the
30.132+output. If TAG is t, use the class-name symbol."
30.133+ (declare (type standard-object obj)
30.134+ (type (or list boolean) slots)
30.135+ (type (or list boolean) methods)
30.136+ (type boolean indirect)
30.137+ (type list exclude))
30.138+ (unless (or slots methods)
30.139+ (error "Required one missing key arg: SLOTS or METHODS"))
30.140+ (let* ((class (class-of obj))
30.141+ (res (when tag (list (if (eq t tag) (class-name class) tag)))))
30.142+ (block unwrap
30.143+ (when-let ((slots (when slots
30.144+ (list-class-slots class slots exclude))))
30.145+ (let ((slot-vals (list-slot-values-using-class class obj (remove-if #'null slots) nullp unboundp)))
30.146+ (if methods
30.147+ (push slot-vals res)
30.148+ (return-from unwrap (push slot-vals res)))))
30.149+ (when-let ((methods (when methods (list-class-methods class methods indirect))))
30.150+ (push methods res)))
30.151+ (flatten res)))
30.152+
30.153+(defun wrap-object (class form)
30.154+ "Given a CLASS prototype and an input FORM, return a new instance of
30.155+CLASS. FORM is assumed to be the finalized lisp object which has
30.156+already passed through `read' -- not a string or file-stream for
30.157+example."
30.158+ (declare (type class class)
30.159+ (type form form)))
30.160+
30.161+;; (defmacro define-fmt ())
30.162+;; (defmacro define-macro ())
31.1--- a/lisp/std/sym.lisp Mon Oct 16 19:33:42 2023 -0400
31.2+++ b/lisp/std/sym.lisp Mon Oct 16 22:25:50 2023 -0400
31.3@@ -3,9 +3,8 @@
31.4 ;; inspired by alexandria/symbols.lisp
31.5
31.6 ;;; Code:
31.7-(pkg:defpkg :std/sym
31.8- (:use :cl :str :sb-int)
31.9- (:nicknames :sym)
31.10+(uiop:define-package :std/sym
31.11+ (:use :cl :std/str :sb-int)
31.12 (:export
31.13 #:ensure-symbol
31.14 #:format-symbol
31.15@@ -17,7 +16,7 @@
31.16 #:with-unique-names
31.17 #:symbolicate))
31.18
31.19-(in-package :sym)
31.20+(in-package :std/sym)
31.21
31.22 ;; (reexport-from :sb-int
31.23 ;; :include '(:with-unique-names :symbolicate :package-symbolicate :keywordicate :gensymify*))
32.1--- a/lisp/std/tests.lisp Mon Oct 16 19:33:42 2023 -0400
32.2+++ b/lisp/std/tests.lisp Mon Oct 16 22:25:50 2023 -0400
32.3@@ -5,24 +5,10 @@
32.4 ;; TODO: fix false positives when using (eval-test)
32.5
32.6 ;;; Code:
32.7-(defpackage :std/tests
32.8- (:use
32.9- :cl
32.10- :readtables
32.11- :str
32.12- :fmt
32.13- :sym
32.14- :list
32.15- :cond
32.16- :log
32.17- :fu
32.18- :ana
32.19- :pan
32.20- :fs
32.21- :alien
32.22- :thread
32.23- :rt))
32.24-
32.25+(uiop:define-package :std/tests
32.26+ (:use :cl :std/all :std/rt)
32.27+ (:use-reexport :std/tests/sxp))
32.28+
32.29 (in-package :std/tests)
32.30
32.31 (in-readtable :std)
32.32@@ -49,8 +35,8 @@
32.33 "Test STD.SYM"
32.34 ;; gensyms
32.35 (is (not (equalp (make-gensym 'a) (make-gensym 'a))))
32.36- (is (eq (ensure-symbol 'tests :macs.tests) 'tests))
32.37- (is (eq 'macs.tests::foo (format-symbol :macs.tests "~A" 'foo)))
32.38+ (is (eq (ensure-symbol 'tests :std/tests) 'tests))
32.39+ (is (eq 'std/tests::foo (format-symbol :std/tests "~A" 'foo)))
32.40 (is (eq (make-keyword 'fizz) :fizz)))
32.41
32.42 ;;;; TODO
32.43@@ -101,7 +87,7 @@
32.44 "#))
32.45 ;; with plist option
32.46 (is (string=
32.47- #.(fmt:fmt-tree nil '(sk-project :name "foobar" :path "/a/b/c.asd" :vc :hg) :layout :down :plist t)
32.48+ #.(std/fmt:fmt-tree nil '(sk-project :name "foobar" :path "/a/b/c.asd" :vc :hg) :layout :down :plist t)
32.49 #"SK-PROJECT
32.50 ├─ :NAME
32.51 │ ╰─ "foobar"
32.52@@ -129,3 +115,62 @@
32.53 (is (= 0 (funcall p nil)))
32.54 (is (= 1 (funcall p 1)))
32.55 (is (= 1 b c)))))
32.56+
32.57+;;; RT
32.58+(defsuite :rt)
32.59+(in-suite :rt)
32.60+(deftest rt (:bench 100 :profile t :persist nil)
32.61+ (is (typep (make-fixture-prototype :empty nil) 'fixture-prototype))
32.62+ (with-fixture (fx (make-fixture ((a 1) (b 2))
32.63+ (:+ () (+ (incf a) (incf b)))
32.64+ (:- () (- (decf a) (decf b)))
32.65+ (t () 0)))
32.66+ (is (= 5 (funcall fx :+)))
32.67+ (is (= 7 (funcall fx :+)))
32.68+ (is (= 5 (funcall fx :-)))
32.69+ (is (= 0 (funcall fx))))
32.70+ (signals (error t) (test-form (make-instance 'test-result))))
32.71+
32.72+;;; CLI
32.73+(defsuite :cli)
32.74+(in-suite :cli)
32.75+(unless *compile-tests*
32.76+ (deftest cli-prompt ()
32.77+ "Test MACS.CLI prompts"
32.78+ (make-prompt! tpfoo "testing: ")
32.79+ (defvar tcoll nil)
32.80+ (defvar thist nil)
32.81+ (let ((*standard-input* (make-string-input-stream
32.82+ (format nil "~A~%~A~%" "foobar" "foobar"))))
32.83+ ;; prompts
32.84+ (is (string= (tpfoo-prompt) "foobar"))
32.85+ (is (string= "foobar"
32.86+ (cli:completing-read "nothing: " tcoll :history thist :default "foobar"))))))
32.87+
32.88+(defparameter *opts* (cli:make-opts (:name foo :global t :description "bar")
32.89+ (:name bar :description "foo")))
32.90+
32.91+(defparameter *cmd1* (make-cli :cmd :name "holla" :opts *opts* :description "cmd1 description"))
32.92+(defparameter *cmd2* (make-cli :cmd :name "ayo" :cmds #(*cmd1*) :opts *opts* :description "cmd1 description"))
32.93+(defparameter *cmds* (cli:make-cmds (:name "baz" :description "baz" :opts *opts*)))
32.94+
32.95+(defparameter *cli* (make-cli t :opts *opts* :cmds *cmds* :description "test cli"))
32.96+
32.97+(deftest cli ()
32.98+ "test MACS.CLI OOS."
32.99+ (let ((cli *cli*))
32.100+ (is (eq (make-shorty "test") #\t))
32.101+ (is (equalp (proc-args cli '("-f" "baz" "--bar" "fax")) ;; not eql
32.102+ (make-cli-ast
32.103+ (list (make-cli-node 'opt (find-short-opt cli #\f))
32.104+ (make-cli-node 'cmd (find-cmd cli "baz"))
32.105+ (make-cli-node 'opt (find-opt cli "bar"))
32.106+ (make-cli-node 'arg "fax")))))
32.107+ (is (parse-args cli '("--bar" "baz" "-f" "yaks")))
32.108+ (is (stringp
32.109+ (with-output-to-string (s)
32.110+ (print-version cli s)
32.111+ (print-usage cli s)
32.112+ (print-help cli s))))
32.113+ (is (string= "foobar" (parse-str-opt "foobar")))))
32.114+
33.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
33.2+++ b/lisp/std/tests/bench.lisp Mon Oct 16 22:25:50 2023 -0400
33.3@@ -0,0 +1,49 @@
33.4+(require :sb-sprof)
33.5+(defpackage :sxp-bench
33.6+ (:use :cl :sxp :sb-ext :sb-unix)
33.7+ (:export :run-bench :*bench-input-file* :*bench-input-string* :*bench-input-object*
33.8+ :*bench-output-directory* :*bench-iterations* :*bench-report-file* ;:*bench-flamegraph-file*
33.9+ ))
33.10+(in-package :sxp-bench)
33.11+(declaim
33.12+ (type (or string pathname) *bench-input-file* *bench-output-directory* *bench-report-file*)
33.13+ (type string *bench-input-string*)
33.14+ (type sxp *bench-input-object*)
33.15+ (type integer *bench-iterations*))
33.16+(defparameter *bench-input-file* "tests.sxp")
33.17+(defparameter *bench-input-string* (uiop:read-file-string *bench-input-file*))
33.18+(defparameter *bench-input-object* (make-instance 'sxp))
33.19+(read-sxp-string *bench-input-object* *bench-input-string*)
33.20+
33.21+(defparameter *bench-output-directory* "/tmp/sxp-bench")
33.22+(defparameter *bench-iterations* 1000)
33.23+(defparameter *bench-report-file* "bench.sxp")
33.24+;; (defparameter *bench-flamegraph-file* "bench.stack")
33.25+(defmacro bench (&body body)
33.26+ `(loop for i from 1 to *bench-iterations*
33.27+ do ,@body))
33.28+
33.29+(defun rbench (fn input)
33.30+ (let ((res))
33.31+ (bench (call-with-timing (lambda (&rest x) (push (cons i x) res)) fn input))
33.32+ (nreverse res)))
33.33+
33.34+(defun wbench (fn)
33.35+ (let ((res))
33.36+ (bench (let ((out (make-pathname :name (format nil "~d.sxp" i) :directory *bench-output-directory*)))
33.37+ (call-with-timing (lambda (&rest x) (push (cons i x) res)) fn *bench-input-object* out :if-exists :supersede)))
33.38+ (nreverse res)))
33.39+
33.40+(defun run-bench (&optional rpt)
33.41+ (when (probe-file *bench-output-directory*)
33.42+ (sb-ext:delete-directory *bench-output-directory* :recursive t))
33.43+ (sb-unix:unix-mkdir *bench-output-directory* #o777)
33.44+ (let ((rres (sb-sprof:with-profiling (:sample-interval 0.001) (rbench #'sxp:read-sxp-file *bench-input-file*)))
33.45+ (wres (sb-sprof:with-profiling (:sample-interval 0.001) (wbench #'sxp:write-sxp-file))))
33.46+ (if rpt
33.47+ (progn
33.48+ (format t "Writing output to ~s" *bench-report-file*)
33.49+ (uiop:with-output-file (out *bench-report-file* :if-exists :supersede :if-does-not-exist :create)
33.50+ (print `(,@rres ,@wres) out)))
33.51+ (print (list rres wres))))
33.52+ (terpri))
34.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
34.2+++ b/lisp/std/tests/sxp.lisp Mon Oct 16 22:25:50 2023 -0400
34.3@@ -0,0 +1,39 @@
34.4+;;; std/tests/sxp.lisp --- SXP tests
34.5+(defpackage :std/tests/sxp
34.6+ (:use :cl :sxp :std/base :std/fu :std/rt)
34.7+ (:export :*sxp-test-file* :*sxp-test-string*))
34.8+
34.9+(in-package :std/tests/sxp)
34.10+(in-readtable :std)
34.11+(declaim
34.12+ (type (or string pathname) *sxp-test-file*)
34.13+ (type string *sxp-test-string*))
34.14+(defparameter *sxp-test-file* "tests.sxp")
34.15+(defparameter *sxp-test-string* "(FOO 'BAR `(\"test\" ,BAZ ,@QUX) 123 0.0123 1/3 `(,A1 ,A2))")
34.16+
34.17+(defsuite :sxp)
34.18+(in-suite :sxp)
34.19+
34.20+(deftest forms ()
34.21+ (is (formp nil))
34.22+ (is (formp t))
34.23+ (is (formp 3.14))
34.24+ (is (formp "string"))
34.25+ (is (formp (mapc #`(',a1) '(a))))
34.26+ (is (formp ())))
34.27+
34.28+(deftest sxp-file ()
34.29+ (let ((f (read-sxp-file *sxp-test-file*)))
34.30+ (is (equal (unwrap f) (unwrap f)))))
34.31+
34.32+(deftest sxp-string ()
34.33+ (let ((f (make-instance 'sxp)))
34.34+ (is (formp (read-sxp-string f *sxp-test-string*)))
34.35+ (is (equalp (read-from-string (write-sxp-string f)) (read-from-string *sxp-test-string*)))))
34.36+
34.37+(deftest sxp-stream ()
34.38+ (let ((f (make-instance 'sxp)))
34.39+ (with-input-from-string (s *sxp-test-string*)
34.40+ (read-sxp-stream f s))
34.41+ (with-output-to-string (s)
34.42+ (is (write-sxp-stream f s)))))
35.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
35.2+++ b/lisp/std/tests/tests.sxp Mon Oct 16 22:25:50 2023 -0400
35.3@@ -0,0 +1,42 @@
35.4+; skip me maybe
35.5+;; this file does not contain quote characters.
35.6+(edges-1
35.7+(
35.8+(1389.886593 1341.567282)
35.9+(1383.122623 1339.369530)
35.10+)
35.11+(
35.12+(1383.122623 1339.369530)
35.13+(1387.706464 1325.261939)
35.14+)
35.15+(
35.16+(1387.706464 1325.261939)
35.17+(1394.470360 1327.459664)
35.18+)
35.19+(
35.20+(1394.470360 1327.459664)
35.21+(1389.886593 1341.567282)
35.22+)
35.23+) ; edges end
35.24+
35.25+(edges-2
35.26+( ( 1.1 2.2 ) (2.2 3.3) )
35.27+( ( 2.2 3.3 ) (3.3 3.3) )
35.28+( ( 3.3 3.3 ) (1.1 2.2) )
35.29+) ; end edges of triangle room
35.30+
35.31+(= 4 4)
35.32+(= 5 4)
35.33+(> 4.0 54.0)
35.34+(= 4 s)
35.35+(= (= 4 4) (> 5 4))
35.36+(not (= 3 3))
35.37+(not 4)
35.38+(if (= 4 4) 42 666)
35.39+(if (= 4.0 4.0) (42))
35.40+(+ 4 4)
35.41+(+ 5.0 6.5)
35.42+(- 4 5)
35.43+(^ 2 3)
35.44+(^ 3 2)
35.45+(^ 3 (+ 2 1))
36.1--- a/lisp/std/thread.lisp Mon Oct 16 19:33:42 2023 -0400
36.2+++ b/lisp/std/thread.lisp Mon Oct 16 22:25:50 2023 -0400
36.3@@ -8,12 +8,12 @@
36.4
36.5 ;;; Code:
36.6 (defpackage :std/thread
36.7- (:use :cl :macs.alien :sb-thread)
36.8+ (:use :std/base :std/alien :sb-thread)
36.9 (:nicknames :thread)
36.10 (:export
36.11 :print-thread-info :print-thread-message-top-level :thread-support-p))
36.12
36.13-(in-package :thread)
36.14+(in-package :std/thread)
36.15
36.16 ;; (reexport-from :sb-thread
36.17 ;; :include '(:main-thread
37.1--- a/readme.org Mon Oct 16 19:33:42 2023 -0400
37.2+++ b/readme.org Mon Oct 16 22:25:50 2023 -0400
37.3@@ -5,238 +5,67 @@
37.4 *** btrfsutil
37.5 * lisp
37.6 #+begin_src lisp :results silent
37.7- (let ((asds '("lisp/macs/macs.asd" "lisp/lib/cli.asd" "lisp/lib/rt.asd"
37.8- "lisp/lib/rdb.asd" "lisp/lib/sxp.asd" "lisp/lib/organ/organ.asd" "lisp/lib/skel/skel.asd"
37.9- "lisp/ffi/rocksdb.asd" "lisp/ffi/btrfs.asd" "lisp/ffi/uring.asd")))
37.10+ (let ((asds '("lisp/std.asd" "lisp/lib/rdb.asd" "lisp/lib/organ/organ.asd" "lisp/lib/skel/skel.asd"
37.11+ "lisp/ffi/rocksdb.asd" "lisp/ffi/btrfs.asd" "lisp/ffi/uring.asd")))
37.12 (mapc (lambda (x) (asdf:load-asd (pathname x))) asds))
37.13- (asdf:load-system :macs)
37.14- (asdf:load-system :rt)
37.15+ (asdf:load-system :std)
37.16 #+end_src
37.17-** macs
37.18-*** pkg
37.19+** std
37.20 *** tests
37.21-#+begin_src lisp :package pkg.tests :results output replace :exports results
37.22- (asdf:load-system :cli)
37.23- (asdf:load-system :macs/tests)
37.24- (in-package :macs.tests)
37.25- (load "lisp/macs/tests.lisp")
37.26- (setq *log-level* :debug)
37.27- (rt:do-tests :macs)
37.28+#+begin_src lisp :package std/tests :results output replace :exports results
37.29+ (load "lisp/std/tests.lisp")
37.30+ (load "lisp/std/tests/sxp.lisp")
37.31+ (rt:do-tests :named-readtables)
37.32+ (rt:do-tests :std)
37.33+ (rt:do-tests :rt)
37.34+ (rt:do-tests :cli)
37.35+ (in-package :std/tests/sxp)
37.36+ (setq *sxp-test-file* "lisp/std/tests/tests.sxp")
37.37+ (rt:do-tests :sxp)
37.38 #+end_src
37.39 #+RESULTS:
37.40 #+begin_example
37.41-in suite MACS with 11/11 tests:
37.42-:DEBUG @ 224.94667
37.43-; running test:
37.44-; #<TEST PAN :fn PAN-test292 :args NIL :persist NIL {1004BEDAD3}>
37.45-:DEBUG @ 224.95667
37.46-; #<PASS (= 0 (FUNCALL P NIL))>
37.47-:DEBUG @ 224.95667
37.48-; #<PASS (= 1 (FUNCALL P 1))>
37.49-:DEBUG @ 224.95667
37.50-; #<PASS (= 1 B C)>
37.51-#<PASS PAN-TEST292>
37.52-:DEBUG @ 224.95667
37.53-; running test:
37.54-; #<TEST ANA :fn ANA-test291 :args NIL :persist NIL {1004BEB593}>
37.55-:DEBUG @ 224.95667
37.56-; #<PASS (= 8 (AIF (+ 2 2) (+ IT IT)))>
37.57-#<PASS ANA-TEST291>
37.58-:DEBUG @ 224.95667
37.59-; running test:
37.60-; #<TEST FMT :fn FMT-test290 :args NIL :persist NIL {1004BE9D93}>
37.61-:DEBUG @ 224.96
37.62-; #<PASS (STRING= (FORMAT NIL | 1 | 2 | 3 |~%) (FMT-ROW '(1 2 3)))>
37.63-:DEBUG @ 224.96
37.64-; #<PASS (STRING= (FMT-SXHASH (SXHASH T)) (FMT-SXHASH (SXHASH T)))>
37.65-:DEBUG @ 224.96
37.66-; #<PASS (STRING= FOOBAR
37.67- ├─ :A
37.68- ├─ :B
37.69- ├─ C
37.70- ╰─ D
37.71-
37.72- FOOBAR
37.73- ├─ :A
37.74- ├─ :B
37.75- ├─ C
37.76- ╰─ D
37.77-)>
37.78-:DEBUG @ 224.96
37.79-; #<PASS (STRING= SK-PROJECT
37.80- ├─ :NAME
37.81- │ ╰─ "foobar"
37.82- ├─ :PATH
37.83- │ ╰─ "/a/b/c.asd"
37.84- ╰─ :VC
37.85- ╰─ :HG
37.86-
37.87- SK-PROJECT
37.88- ├─ :NAME
37.89- │ ╰─ "foobar"
37.90- ├─ :PATH
37.91- │ ╰─ "/a/b/c.asd"
37.92- ╰─ :VC
37.93- ╰─ :HG
37.94-)>
37.95-#<PASS FMT-TEST290>
37.96-:DEBUG @ 224.96
37.97-; running test:
37.98-; #<TEST ALIEN :fn ALIEN-test289 :args NIL :persist NIL {1004BE8283}>
37.99-:DEBUG @ 224.96333
37.100-; #<PASS (= 0 (FOREIGN-INT-TO-INTEGER 0 4))>
37.101-:DEBUG @ 224.96333
37.102-; #<PASS (= 1 (BOOL-TO-FOREIGN-INT T))>
37.103-#<PASS ALIEN-TEST289>
37.104-:DEBUG @ 224.96333
37.105-; running test:
37.106-; #<TEST THREAD :fn THREAD-test288 :args NIL :persist NIL {1004B87423}>
37.107-:DEBUG @ 224.96667
37.108-; #<PASS (STRINGP (PRINT-THREAD-INFO NIL))>
37.109-#<PASS THREAD-TEST288>
37.110-:DEBUG @ 224.96667
37.111-; running test:
37.112-; #<TEST REEXPORT :fn REEXPORT-test287 :args NIL :persist NIL {1004A27883}>
37.113-#<PASS REEXPORT-TEST287>
37.114-:DEBUG @ 224.96667
37.115-; running test:
37.116-; #<TEST COND :fn COND-test286 :args NIL :persist NIL {1004A265D3}>
37.117-#<PASS COND-TEST286>
37.118-:DEBUG @ 224.96667
37.119-; running test:
37.120-; #<TEST LOG :fn LOG-test285 :args NIL :persist NIL {1004A25323}>
37.121-:DEBUG @ 224.96667
37.122-; test
37.123-; DEBUG
37.124-:DEBUG @ 224.96667
37.125-; test
37.126-; DEBUG
37.127-:DEBUG @ 224.97
37.128-; test
37.129-; DEBUG
37.130-:DEBUG @ 224.97
37.131-; #<PASS (DEBUG! test *LOG-LEVEL*)>
37.132-#<PASS LOG-TEST285>
37.133-:DEBUG @ 224.97
37.134-; running test:
37.135-; #<TEST LIST :fn LIST-test284 :args NIL :persist NIL {1004A23F13}>
37.136-:DEBUG @ 224.97333
37.137-; #<PASS (EQ (ENSURE-CAR '(0)) (ENSURE-CAR 0))>
37.138-:DEBUG @ 224.97333
37.139-; #<PASS (EQ (ENSURE-CAR '(NIL)) (ENSURE-CAR NIL))>
37.140-:DEBUG @ 224.97333
37.141-; #<PASS (NOT (EQ (ENSURE-CONS 0) (ENSURE-CONS 0)))>
37.142-:DEBUG @ 224.97333
37.143-; #<PASS (EQUAL (ENSURE-CONS 0) (ENSURE-CONS 0))>
37.144-#<PASS LIST-TEST284>
37.145-:DEBUG @ 224.97333
37.146-; running test:
37.147-; #<TEST STR :fn STR-test283 :args NIL :persist NIL {1004A21F63}>
37.148-:DEBUG @ 224.97667
37.149-; #<PASS (TYPEP test 'STRING-DESIGNATOR)>
37.150-:DEBUG @ 224.97667
37.151-; #<PASS (TYPEP 'TEST 'STRING-DESIGNATOR)>
37.152-:DEBUG @ 224.97667
37.153-; #<PASS (TYPEP C 'STRING-DESIGNATOR)>
37.154-:DEBUG @ 224.97667
37.155-; #<PASS (NOT (TYPEP 0 'STRING-DESIGNATOR))>
37.156-#<PASS STR-TEST283>
37.157-:DEBUG @ 224.97667
37.158-; running test:
37.159-; #<TEST SYM :fn SYM-test282 :args NIL :persist NIL {1004A20453}>
37.160-:DEBUG @ 224.98
37.161-; #<PASS (NOT (EQUALP (MAKE-GENSYM 'A) (MAKE-GENSYM 'A)))>
37.162-:DEBUG @ 224.98
37.163-; #<PASS (EQ (ENSURE-SYMBOL 'TESTS MACS.TESTS) 'TESTS)>
37.164-:DEBUG @ 224.98
37.165-; #<PASS (EQ 'FOO (FORMAT-SYMBOL MACS.TESTS ~A 'FOO))>
37.166-:DEBUG @ 224.98
37.167-; #<PASS (EQ (MAKE-KEYWORD 'FIZZ) FIZZ)>
37.168-#<PASS SYM-TEST282>
37.169+in suite NAMED-READTABLES with 1/1 tests:
37.170+#<PASS READTABLES-TEST1041>
37.171+No tests failed.
37.172+in suite STD with 10/10 tests:
37.173+#<PASS PAN-TEST1051>
37.174+#<PASS ANA-TEST1050>
37.175+#<PASS FMT-TEST1049>
37.176+#<PASS ALIEN-TEST1048>
37.177+#<PASS THREAD-TEST1047>
37.178+#<PASS COND-TEST1046>
37.179+#<PASS LOG-TEST1045>
37.180+#<PASS LIST-TEST1044>
37.181+#<PASS STR-TEST1043>
37.182+#<PASS SYM-TEST1042>
37.183+No tests failed.
37.184+in suite RT with 1/1 tests:
37.185+#<PASS RT-TEST1052>
37.186+No tests failed.
37.187+in suite CLI with 1/1 tests:
37.188+#<PASS CLI-TEST1053>
37.189+No tests failed.
37.190+in suite SXP with 4/4 tests:
37.191+#<PASS SXP-STREAM-TEST1057>
37.192+#<PASS SXP-STRING-TEST1056>
37.193+#<PASS SXP-FILE-TEST1055>
37.194+#<PASS FORMS-TEST1054>
37.195 No tests failed.
37.196 #+end_example
37.197 ** lib
37.198-*** TODO cli
37.199-#+begin_src lisp :package cli.tests :results output replace :exports results
37.200- (asdf:load-system :cli)
37.201- (asdf:load-system :cli/tests)
37.202- (in-package :cli.tests)
37.203- (load "lisp/lib/cli/tests.lisp")
37.204- (setq *log-level* :debug)
37.205- (rt:do-tests :cli)
37.206-#+end_src
37.207-*** TODO rt
37.208-#+begin_src lisp :package rt.tests :results output replace :exports results
37.209- (asdf:load-system :rt/tests)
37.210- (in-package :rt.tests)
37.211- (load "lisp/lib/cli/tests.lisp")
37.212- (setq *log-level* :debug)
37.213- (do-tests :rt)
37.214-#+end_src
37.215 *** rdb
37.216 **** tests
37.217 #+begin_src lisp :package rdb.tests :results output replace :exports results
37.218 (asdf:load-system :rdb/tests)
37.219- (in-package :rdb.tests)
37.220+ (in-package :rdb/tests)
37.221 (load "lisp/lib/rdb/tests.lisp")
37.222- (setq *log-level* :debug)
37.223- (do-tests :rdb)
37.224+ (setq log:*log-level* :debug)
37.225+ (rt:do-tests :rdb)
37.226 #+end_src
37.227 #+RESULTS:
37.228 : in suite RDB with 0/0 tests:
37.229 : No tests failed.
37.230-*** sxp
37.231-**** tests
37.232-#+begin_src lisp :package sxp.tests :results output replace :exports results
37.233- (asdf:load-system :sxp/tests)
37.234- (load "lisp/lib/sxp/tests.lisp")
37.235- (in-package :sxp.tests)
37.236- (let ((*default-pathname-defaults* #.#P"./lisp/lib/sxp/")
37.237- (log:*log-level* :debug))
37.238- (do-tests :sxp))
37.239-#+end_src
37.240-#+RESULTS:
37.241-#+begin_example
37.242-in suite SXP with 4/4 tests:
37.243-:DEBUG @ 10905.557
37.244-; running test:
37.245-; #<TEST SXP-STREAM :fn SXP-STREAM-test1461 :args NIL :persist NIL {1002F41C33}>
37.246-((foo 'bar `("test" ,baz ,@qux) 123 0.0123 1/3 `(,a1 ,a2))):DEBUG @ 10905.563
37.247-; #<PASS (WRITE-SXP-STREAM F NIL)>
37.248-#<PASS SXP-STREAM-TEST1461>
37.249-:DEBUG @ 10905.563
37.250-; running test:
37.251-; #<TEST SXP-STRING :fn SXP-STRING-test1460 :args NIL :persist NIL {1002DEF703}>
37.252-:DEBUG @ 10905.566
37.253-; #<PASS (FORMP (READ-SXP-STRING F *TEST-STRING*))>
37.254-:DEBUG @ 10905.566
37.255-; #<PASS (EQUALP (READ-FROM-STRING (WRITE-SXP-STRING F))
37.256- (READ-FROM-STRING *TEST-STRING*))>
37.257-#<PASS SXP-STRING-TEST1460>
37.258-:DEBUG @ 10905.566
37.259-; running test:
37.260-; #<TEST SXP-FILE :fn SXP-FILE-test1459 :args NIL :persist NIL {1002DEDBF3}>
37.261-:DEBUG @ 10905.57
37.262-; #<PASS (EQUAL (UNWRAP F) (UNWRAP F))>
37.263-#<PASS SXP-FILE-TEST1459>
37.264-:DEBUG @ 10905.57
37.265-; running test:
37.266-; #<TEST FORMS :fn FORMS-test1458 :args NIL :persist NIL {1002DEC3C3}>
37.267-:DEBUG @ 10905.577
37.268-; #<PASS (FORMP NIL)>
37.269-:DEBUG @ 10905.577
37.270-; #<PASS (FORMP T)>
37.271-:DEBUG @ 10905.577
37.272-; #<PASS (FORMP 3.14)>
37.273-:DEBUG @ 10905.577
37.274-; #<PASS (FORMP string)>
37.275-:DEBUG @ 10905.577
37.276-; #<PASS (FORMP (MAPC (LAMBDA (A1) `(',A1)) '(A)))>
37.277-:DEBUG @ 10905.577
37.278-; #<PASS (FORMP NIL)>
37.279-#<PASS FORMS-TEST1458>
37.280-No tests failed.
37.281-#+end_example
37.282 *** organ
37.283 **** tests
37.284 #+begin_src lisp :package organ.tests :results output replace :exports results