src/base.lisp @ 6385b2a6b82a

Fix exit code
author Steve Losh <steve@stevelosh.com>
date Wed, 19 Dec 2018 10:16:08 -0500
parents a08b75bd86ad
children 674260595163
(in-package :cacl)

;;;; Config -------------------------------------------------------------------
(defparameter *undo-limit* 30)


;;;; State --------------------------------------------------------------------
(defvar *running* nil)
(defvar *stack* nil)
(defvar *previous* nil)
(defvar *commands* nil)


;;;; Stack --------------------------------------------------------------------
(defun push! (&rest objects)
  (dolist (o objects)
    (push (if (floatp o)
            (coerce o 'double-float)
            o)
          *stack*)))

(defun pop! ()
  (assert *stack* () "Cannot pop empty stack")
  (pop *stack*))

(defun pop-all! ()
  (prog1 *stack* (setf *stack* nil)))


(defmacro with-args (symbols &body body)
  `(let (,@(iterate (for symbol :in (reverse symbols))
                    (collect `(,symbol (pop!)))))
     ,@body))

(defmacro with-read-only-args (symbols &body body)
  `(let (,@(iterate (for i :from 0)
                    (for symbol :in (reverse symbols))
                    (collect `(,symbol (nth ,i *stack*)))))
     ,@body))


;;;; Undo ---------------------------------------------------------------------
(defun save-stack ()
  (unless (eql *stack* (car *previous*))
    (push *stack* *previous*))
  (setf *previous* (subseq *previous* 0 (min (1+ *undo-limit*)
                                             (length *previous*)))))

(defun save-thunk (thunk)
  (push thunk *previous*))

(defun undo ()
  (assert (cdr *previous*) () "Cannot undo any further")
  ;; The first element in *previous* is the current stack, so remove it.
  (pop *previous*)
  (let ((top (car *previous*)))
    (etypecase top
      (list nil)
      (function (funcall top)
                (pop *previous*)))
    (setf *stack* (car *previous*))))


;;;; Misc ---------------------------------------------------------------------
(defun sh (command &key (input "") output)
  (uiop:run-program command
                    :output (when output :string)
                    :input (make-string-input-stream input)))

(defun pbcopy (object)
  (sh '("pbcopy") :input (aesthetic-string object))
  (values))

(defun pbpaste ()
  (values (sh '("pbpaste") :output t)))

(defgeneric ref% (object key))

(defmethod ref% ((object hash-table) key)
  (gethash key object))

(defmethod ref% ((object vector) key)
  (aref object key))

(defun ref (object &rest keys)
  (recursively ((object object)
                (keys keys))
    (if (null keys)
      object
      (recur (ref% object (first keys)) (rest keys)))))


;;;; Help ---------------------------------------------------------------------
(defun first-letter (command)
  (let ((ch (aref (symbol-name command) 0)))
    (if (alphanumericp ch)
      ch
      #\!)))

(defun partition-commands (commands)
  (mapcar (lambda (letter-and-commands)
            (sort (second letter-and-commands) #'string<))
          (sort (hash-table-contents (group-by #'first-letter commands))
                #'char< :key #'first)))

(defun print-version ()
  (format t "CACL v0.0.0 (~A)~%"
          #+sbcl 'sbcl
          #+ccl 'ccl
          #+ecl 'ecl
          #+abcl 'abcl))

(defun print-help ()
  (terpri)
  (format t "CACL is an RPN calculator written in Common Lisp.~@
          ~@
          The current stack is displayed above the prompt (the top is at the right).~@
          ~@
          Forms are read from standard input with the standard Common Lisp READ function.~@
          This means you can put multiple things on one line if you want, like this:~@
          ~%    1 2 +~@
          ~@
          What happens when a form is read depends on the form:~@
          ~@
          * Numbers are pushed onto the stack.~@
          * Symbols run commands.~@
          * Quoted forms are pushed onto the stack.~@
          ~@
          Type `commands` for a list of available commands.~@
          ~@
          To get help for a particular command, push its symbol onto the stack~@
          and run the `doc` command:~@
          ~%    'float doc~@
          "))

(defun print-commands ()
  (terpri)
  (format t "AVAILABLE COMMANDS:~@
             ~(~{~{~A~^ ~}~%~}~)~%"
          (partition-commands *commands*)))


;;;; Commands -----------------------------------------------------------------
(defgeneric command (symbol))

(defmethod command ((symbol symbol))
  (error "Unknown command ~S" symbol))


(defgeneric command-documentation (symbol))

(defmethod command-documentation (object)
  (flet ((friendly-type (object)
           (let ((type (type-of object)))
             (if (consp type) (first type) type))))
    (error "Cannot retrieve documentation for ~S ~S"
           (friendly-type object) object)))

(defmethod command-documentation ((symbol symbol))
  (error "Unknown command ~S" symbol))


(defmacro define-command% (symbol args read-only &body body)
  (multiple-value-bind (forms declarations documentation)
      (parse-body body :documentation t)
    `(progn
       (defmethod command ((symbol (eql ',symbol)))
         (,(if read-only 'with-read-only-args 'with-args) ,args
           ,@declarations
           ,@forms))
       (defmethod command-documentation ((symbol (eql ',symbol)))
         ,(or documentation "No documentation provided"))
       (pushnew ',symbol *commands*))))

(defmacro define-command (symbol-or-symbols args &body body)
  (let ((read-only (member '&read-only args))
        (args (remove '&read-only args)))
    `(progn ,@(iterate
                (for symbol :in (ensure-list symbol-or-symbols))
                (collect `(define-command% ,symbol ,args ,read-only ,@body))))))

(defmacro define-simple-command
    (symbols argument-count &optional (lisp-function (first symbols)))
  (let ((args (make-gensym-list argument-count "ARG")))
    `(define-command ,symbols ,args
       (push! (,lisp-function ,@args)))))

(defmacro define-constant-command (symbol value)
  `(define-command ,symbol ()
     (push! ,value)))


;;;; Commands/IO --------------------------------------------------------------
(define-command pbc (&read-only x)
  "Copy the top element of the stack to the system clipboard.

  The item will remain on the stack.

  "
  (pbcopy x))

(define-command pbp ()
  "Push the contents of the system clipboard onto the stack as a string."
  (push! (pbpaste)))

(define-command file (path)
  "Push the contents of `path` onto the stack as a string."
  (push! (read-file-into-string path)))

(defun curl% (url)
  (let ((body (drakma:http-request url)))
    (etypecase body
      (string body)
      (vector (flexi-streams:octets-to-string body)))))

(define-command curl (url)
  "Retrieve `url` and push its contents onto the stack as a string."
  (push! (curl% url)))


;;;; Commands/Stack -----------------------------------------------------------
(define-command (clear cl) ()
  "Clear the entire stack."
  (pop-all!))

(define-command (print p) (&read-only item)
  "Print `item`.  It will remain on the stack."
  (princ (structural-string item))
  (terpri)
  (force-output))

(define-command (pprint pp) (&read-only item)
  "Pretty print `item`.  It will remain on the stack."
  (pprint item)
  (terpri)
  (force-output))

(define-command (dup d) (x)
  "Duplicate the top element of the stack."
  (push! x x))

(define-command pop ()
  "Pop the top element of the stack."
  (pop!))

(define-command (length len) (item)
  (push! (length item)))

(define-command (swap x) (x y)
  "Exchange the top two elements of the stack."
  (push! y x))

(define-command (reverse rev) ()
  "Reverse the stack."
  (setf *stack* (reverse *stack*)))

(define-command (hist history) ()
  (let ((*read-default-float-format* 'double-float))
    (flet ((print-entry (e)
             (typecase e
               (list (print-stack e))
               (t (prin1 e) (terpri)))))
      (mapc #'print-entry (reverse *previous*))))
  (terpri))

(define-command count ()
  "Push the length of the stack."
  (push! (length *stack*)))

(define-command (undo un) ()
  (undo)
  (throw :do-not-add-undo-state nil))


;;;; Commands/Objects ---------------------------------------------------------
(define-command ref (object key)
  (push! (ref object key)))


;;;; Commands/System ----------------------------------------------------------
(define-command doc (symbol)
  "Print the documentation for the symbol at the top of the stack."
  (format t "~A: ~A~%" symbol (command-documentation symbol)))

(define-command help ()
  "Print some basic help information."
  (print-help))

(define-command commands ()
  "Print a list of available commands."
  (print-commands))

(define-command reload ()
  "Reload the entire CACL system from Quicklisp."
  (funcall (read-from-string "ql:quickload") :cacl))

(define-command (quit q) ()
  "Quit CACL."
  (setf *running* nil))

(define-command version ()
  "Print the version and host Lisp."
  (print-version))

(define-command nop ()
  "Do nothing.")


;;;; Special Forms ------------------------------------------------------------
(defgeneric special-form (symbol &rest body))

(defmacro define-special-form (symbol arguments &rest body)
  (let ((args (gensym "ARGUMENTS")))
    `(defmethod special-form ((symbol (eql ',symbol)) &rest ,args)
       (destructuring-bind ,arguments ,args
         ,@body))))

(define-special-form quote (value)
  (push! value))


;;;; REPL ---------------------------------------------------------------------
(defmacro with-errors-handled (&body body)
  (with-gensyms (old-stack)
    `(let ((,old-stack *stack*))
       (handler-case (progn ,@body)
         (error (e)
           (format t "~A: ~A~%" (type-of e) e)
           (setf *stack* ,old-stack))))))


(defun read-input ()
  (let ((*read-default-float-format* 'double-float)
        (line (read-line *standard-input* nil :eof nil)))
    (if (eq :eof line)
      (setf *running* nil)
      (read-all-from-string line))))

(defun handle-input (input)
  (with-errors-handled
    (catch :do-not-add-undo-state
      (etypecase input
        ((or number string) (push! input))
        (symbol (command input))
        (cons (apply 'special-form input)))
      (save-stack))))

(defun handle-all-input ()
  (mapc #'handle-input (read-input)))


(defun render-stack-item (object)
  (typecase object
    (string (structural-string (str:prune 20 object :ellipsis "…")))
    (t (write-to-string object :pretty t :lines 1 :level 2 :right-margin 20 :length 20))))

;; (defgeneric render-stack-item (object))

;; (defmethod render-stack-item ((object t))
;;   (princ-to-string object))

;; (defmethod render-stack-item ((string string))
;;   (-<> string
;;     (str:replace-all (string #\newline) "⏎ " <>)
;;     (str:prune 15 <> :ellipsis "…")
;;     structural-string))

;; (defmethod render-stack-item ((hash-table hash-table))
;;   "{…}")

;; (defmethod render-stack-item ((array array))
;;   "#(…)")


(defun print-stack (&optional (stack *stack*))
  (write-char #\()
  (let ((*read-default-float-format* 'double-float))
    (format t "~{~A~^ ~}" (mapcar #'render-stack-item (reverse stack))))
  (write-char #\))
  (terpri)
  (force-output))

(defun print-prompt ()
  (princ "? ")
  (force-output))


(defun run ()
  (setf *running* t
        *stack* nil
        *previous* (list nil))
  (let ((*package* (find-package :cacl)))
    (iterate (while *running*)
             (progn
               (terpri)
               (print-stack)
               (print-prompt)
               (handle-all-input))))
  (values))


;;;; Command Line -------------------------------------------------------------
(adopt:define-interface *ui*
  "[OPTIONS] [VALUES...]"
  (format nil "CACL is a TUI RPN calculator written (and configurable) in Common Lisp.~@
    ~@
    Documentation about the command-line options to the CACL binary follows.  ~
    For information about how to use CACL itself type help when running in interactive mode.")

  ((help) "display help and exit"
   :long "help"
   :short #\h
   :reduce (constantly t))

  ((rcfile) "path to the custom initialization file (default ~/.caclrc)"
   :long "rcfile"
   :parameter "PATH"
   :initial-value "~/.caclrc"
   :reduce #'adopt:newest)

  ((rcfile no-rcfile) "disable loading of any rcfile"
   :long "no-rcfile"
   :reduce (constantly nil))

  ((interactive mode) "run in interactive mode (the default)"
   :long "interactive"
   :short #\i
   :initial-value 'interactive
   :reduce (constantly 'interactive))

  ((batch mode) "run in batch processing mode"
   :long "batch"
   :short #\b
   :reduce (constantly 'batch))

  ((inform) "print informational message at startup (the default)"
   :long "inform"
   :initial-value t
   :reduce (constantly t))

  ((no-inform inform) "suppress printing of informational message at startup"
   :long "no-inform"
   :reduce (constantly nil)))


(defun toplevel ()
  ;; ccl clobbers the pprint dispatch table when dumping an image, no idea why
  (set-pprint-dispatch 'hash-table 'losh:pretty-print-hash-table)
  (multiple-value-bind (arguments options) (adopt:parse-options *ui*)
    (when (gethash 'help options)
      (adopt:print-usage *ui*)
      (adopt:exit 0))
    (when (gethash 'inform options)
      (print-version))
    (when-let ((rc (gethash 'rcfile options)))
      (load rc :if-does-not-exist nil))
    (when arguments
      (cerror "Ignore them" "Unrecognized command-line arguments: ~S" arguments))
    (run)))