Implementing a simpleminded REPL from scratch

Tagged as lisp, clim, repl

Written on 2022-04-25 by Daniel 'jackdaniel' KochmaƄski

We will start with a very simple (and conforming!) implementation:

(defun rep ()
  (format t "~&~a> " (package-name *package*))
  (shiftf +++ ++ + - (read *standard-input* nil '%quit))
  (when (eq - '%quit)
    (throw :exit "bye!"))
  (shiftf /// // / (multiple-value-list (eval -)))
  (shiftf *** ** * (first /))
  (format t "~&~{ ~s~^~%~}~%" /))

(defun repl ()
  (catch :exit
    (loop (handler-case (rep)
            (condition (c)
              (format *error-output* "~&~a~%~a~%" (class-name (class-of c)) c))))))

Starting this REPL in McCLIM is simple:

(with-output-to-drawing-stream (stream :clx-ttf nil
                                       :text-style (make-text-style :fix nil nil)
                                       :scroll-bars :vertical)
  (let ((*standard-input* stream)
        (*standard-output* stream)
        (*error-output* stream))
    (unwind-protect (repl)
      (close stream))))

Now we may add a graphical debugger:

(defun repl ()
  (catch :exit
    (loop
      (clim-debugger:with-debugger ()
        (with-simple-restart (abort "Return to CLIM's top level.")
          (rep))))))

And create a "real" application frame:

(define-application-frame a-repl ()
  ()
  (:pane :interactor :text-style (make-text-style :fix nil nil)))

(defmethod run-frame-top-level ((frame a-repl) &rest args)
  (declare (ignore args))
  (let ((*standard-input* (frame-standard-input frame))
        (*standard-output* (frame-standard-output frame))
        (*error-output* (frame-error-output frame))
        (*query-io* (frame-query-io frame)))
    (unwind-protect (repl)
      (frame-exit frame))))

(find-application-frame 'a-repl :width 800 :height 600)

Now let's estabilish a context where graphics land below the prompt:

(defun rep ()
  (format t "~&~a> " (package-name *package*))
  (shiftf +++ ++ + - (read *standard-input* nil '%quit))
  (when (eq - '%quit)
    (throw :exit "bye!"))
  (with-room-for-graphics (t :first-quadrant nil)
    (shiftf /// // / (multiple-value-list (eval -))))
  (shiftf *** ** * (first /))
  (format t "~&~{ ~s~^~%~}~%" /))

> (in-package clim-user)
> (draw-rectangle* *standard-output* 10 10 90 90 :ink +dark-blue+)

Let's allow interleaving commands with forms for evaluation:

(defun rep ()
  (multiple-value-bind (command-or-form ptype)
      (accept 'command-or-form :prompt (package-name *package*))
    (when (presentation-subtypep ptype 'command)
      (with-application-frame (frame)
        (return-from rep (execute-frame-command frame command-or-form))))
    (shiftf +++ ++ + - command-or-form)
    (when (eq - '%quit)
      (throw :exit "bye!"))
    (with-room-for-graphics (t :first-quadrant nil)
      (shiftf /// // / (multiple-value-list (eval -))))
    (shiftf *** ** * (first /))
    (format t "~&~{ ~s~^~%~}~%" /)))

(defmethod run-frame-top-level ((frame a-repl) &rest args)
  (declare (ignore args))
  (let ((*standard-input* (frame-standard-input frame))
        (*standard-output* (frame-standard-output frame))
        (*error-output* (frame-error-output frame))
        (*query-io* (frame-query-io frame))
        (*command-dispatchers* '(#\,)))
    (unwind-protect (repl)
      (frame-exit frame))))

A fancy inspector would be nice:

(define-presentation-type result () :inherit-from t)

(define-a-repl-command com-inspect-result ((result 'result :gesture :select))
  (clouseau:inspect result))

(defun rep ()
  (multiple-value-bind (command-or-form ptype)
      (accept 'command-or-form :prompt (package-name *package*))
    (when (presentation-subtypep ptype 'command)
      (with-application-frame (frame)
        (return-from rep (execute-frame-command frame command-or-form))))
    (shiftf +++ ++ + - command-or-form)
    (when (eq - '%quit)
      (throw :exit "bye!"))
    (with-room-for-graphics (t :first-quadrant nil)
      (shiftf /// // / (multiple-value-list (eval -))))
    (shiftf *** ** * (first /))
    (format-textual-list / (lambda (object stream)
                             (format stream " ")
                             (present object 'result :stream stream))
                         :separator #\newline)
    (terpri)))

That and much more is implemented in the system clim-listener. The purpose of this blog post is to show how easy it is to build a sketchy application to help with daily tasks.

Cheers!

P.S. For the inspector and for the debugger load systems clouseau and clim-debugger.