Implementing a simpleminded REPL from scratch
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
.