screwlisp proposes kittens

ansi cl interactive condition handling as browsing the gopher

| link | Last. 20260228T051704344Z |

Catk and I were once lambasted for gopher heresy by gopher.icu. Please consider this ansi common lisp condition system demo of gopher on lisp character streams a proud tradition. In particular here I am exposing deep, truly benthic common lisp condition system possibilities to the light of day. As I mentioned, the wonderful gopher someodd’s writing on the gopher prompted me to recast multifarious activities of mine as actually just being the gopher, continuous in protocol with the whole. As anticipated, by casting the ansi common lisp condition system interactive and non as being done by the gopher, it is straightforward in gopher terms to consider its otherwise subtle possibilities.

Here is a video of me “browsing the gopher” in a slime repl. In sbcl, since to my knowledge their language model slop commits were reverted.

Here are some links, to avoid peppering them through the article:

As an aside, I wrote zanzabar for some mediocre variety on foo bar baz. Zanzibar is an archipelago off eastern Africa, according to wikipedia.

The gist

This list “of gophermap items”:

((|0| "b" c)
 (|0| "b" c)
 (|1| "remap" #'try-gophering)
 )

Is read as a gophermap:

(define-condition gophermap-choose () ())

(as-gophermap
    ((|0| "b" c)
     (|0| "b" c)
     (|1| "remap" #'try-gophering)
     )
  (signal 'gophermap-choose))

and the need to choose from the gophermap is signalled. In the outermost function, 'gophermap-choose is just bound to #'error.

(handler-bind
    ((gophermap-choose #'error) ..)
  |...|
  )

where error will throw us into lisp’s interactive debugger, and we see the restarts corresponding to the list above:

Condition COMMON-LISP-USER::GOPHERMAP-CHOOSE was signalled.
   [Condition of type GOPHERMAP-CHOOSE]

Restarts:
 0: [1] remap
 1: [0] b
 2: [0] b
 3: [RETRY] Retry SLIME REPL evaluation request.
 4: [*ABORT] Return to SLIME's top level.
 5: [ABORT] Exit debugger, returning to top level.

Backtrace:
..

In reverse order, I guess. the line 0: [1] remap means

tapping the number 0 on your keyboard will perform the interactive restart named |1| which has the poorly written report, “remap”.

(pipes let me use numbers as symbols instead of numbers in common lisp)

1: [0] b and 2: [0] b similarly, except their restart is named by |0|.

We can see by capturing *debug-io* in a string that choosing 0 keeps nesting us back into this lisp interactive debugger ‘gophermap’, and eventually choosing a gopher itemtype zero restart prints its text:

CL-USER> (with-output-to-string
    (*debug-io*)
(try-gophering))

(DO "zanzabar" ON #<SB-IMPL::STRING-OUTPUT-STREAM {7F0F051DE9A3}>) 
"zanzabar"
CL-USER> (with-output-to-string
    (*debug-io*)
(try-gophering))

(DO #<FUNCTION TRY-GOPHERING>
    ON
 #<SB-IMPL::STRING-OUTPUT-STREAM {7F0F051DE9A3}>) 

(DO #<FUNCTION TRY-GOPHERING>
    ON
 #<SB-IMPL::STRING-OUTPUT-STREAM {7F0F051DE9A3}>) 

(DO "zanzabar" ON #<SB-IMPL::STRING-OUTPUT-STREAM {7F0F051DE9A3}>) 
"zanzabar"

where in particular the gopher itemtype 0 text-file output uses princ on the captured local value of the symbol - (|0| "b" c) - c here while gopher itemtype 1 (|1| "remap" #'try-gophering) runs the funcalls the local value of the symbol, which here is the function #'try-gophering. The sharp quote reader macro gets the function of a symbol. We can also see that I provide a string in the second position key, and that string is what the report is. In gopher parlance, the report is the gopher item description.

Selected source features

Each restart is added one at a time by this condition and these two lisp macros:

(define-condition gophermap-chose
    ()
  ((item-type :initarg :item-type
	      :reader item-type)
   (item-specifier :initarg :item-specifier
		   :reader item-specifier)
   (output-stream :initarg :to-stream
		  :reader output-stream)))

(defmacro gophermap
    ((char-sym item-description item-specifier) &body body)
  `(restart-case
       (progn ,@body)
     (,char-sym (stream)
       :report ,item-description
       :interactive (lambda () (list gopher-stream))
       :test (lambda (c) (typep c 'gophermap-choose))
       (print `(do ,,item-specifier on ,stream))
       (signal 'gophermap-chose
	       :item-type ',char-sym
	       :item-specifier ,item-specifier
	       :to-stream stream))))

(defmacro as-gophermap
    (list &body body)
  (if list
      `(gophermap ,(car list) (as-gophermap ,(cdr list) ,@body))
      `(progn ,@body)))

We see each restart-case is named by char-sym. I chose to use symbols matching the gopher itemtype single character identifiers. When :report gets a string, that string is what shows up in the interactive debugger seen at the start. Though :report can also take an arbitrary function of an output stream.

:interactive captures local variables from the signal context (being interactively debugged) that will be provided to the restart chosen by tapping a number key. Since the restart choice is a single keypress, there is no opportunity to code in what variables to pass to invoke-restart. :interactive evaluates a function of no arguments in the local context and uses them as the restart’s arguments.

:test - now, when the gophermap-choose condition interactive choice is made, the interactive restart signals my regrettable pun name gophermap-chose condition in which I used the same names- |0| for “do the gopher itemtype zero thing” and |1| for do the gopher itemtype one thing. When the available restarts of a condition are being computed, we can arbitrarily mask restarts by passing a function of one argument (the condition) to :test. In my case I just look whether the type is gophermap-choose for what goes in the “gophermap” interactive debugger, and whether the type is gophermap-chose for the non-interactive restarts that respond with the chosen item as the indicated gopher itemtype.

We can see the noninteractive gophermap-chose handler restart mechanic over here:

(defun try-gophering ()
  (let ((gopher-stream *debug-io*)
	(c "zanzabar"))
    (handler-bind
	((gophermap-choose #'error)
	 (gophermap-chose #'(lambda
				(c)
			      (let ((r (find-restart
					(item-type c)
					c)))
				(when
				    r
				  (invoke-restart r
						  (item-specifier c)
						  (output-stream c)))))))
      (itemtypes-01
	(as-gophermap
	    ((|0| "b" c)
	     (|0| "b" c)
	     (|1| "remap" #'try-gophering)
	     )
	  (signal 'gophermap-choose))))))

where itemtypes-01 provides restarts for the rfc1436 recommended startingpoint gopher client handling of itemtypes 0 and 1:

(defmacro itemtypes-01
    (&body body)
  `(restart-case
       (progn ,@body)
     (|0|
	 (specifier stream)
       :test (lambda (c) (typep c 'gophermap-chose))
       (terpri)
       (princ specifier stream) (values))
     (|1|
	 (specifier stream)
       :test (lambda (c) (typep c 'gophermap-chose))
       (terpri)
       (funcall specifier))))

and in gopher-stream and “c” we also saw local (i.e. lexical) values being captured by outer condition handlers. The local variable c was written in by a macro, but gopher-stream did something unexpected. The :interactive (lambda () (list gopher-stream)) in the restart of the macro gophermap captures the value of gopher-stream as debug-io, which shows up in the restart when gophermap-chose is signalled:

..
(,char-sym (stream)
       :report ,item-description
       :interactive (lambda () (list gopher-stream))
       :test (lambda (c) (typep c 'gophermap-choose))
       (print `(do ,,item-specifier on ,stream))
       (signal 'gophermap-chose
	       :item-type ',char-sym
	       :item-specifier ,item-specifier
	       :to-stream stream))
..

Passing the stream along more mundanely in a slot of the noninteractive condition being signalled.

This capture of *debug-io* results in the complementary bizarrity of capturing *standard-output*

CL-USER> (with-output-to-string
    (*standard-output*)
(try-gophering))
zanzabar
"
(DO #<FUNCTION TRY-GOPHERING>
    ON
 #<SYNONYM-STREAM :SYMBOL SWANK::*CURRENT-DEBUG-IO* {1001340453}>) 

(DO "zanzabar"
    ON
 #<SYNONYM-STREAM :SYMBOL SWANK::*CURRENT-DEBUG-IO* {1001340453}>) 
"

and *debug-io*:

CL-USER> (with-output-to-string
    (*debug-io*)
(try-gophering))

(DO #<FUNCTION TRY-GOPHERING>
    ON
 #<SB-IMPL::STRING-OUTPUT-STREAM {7F0F051DE9A3}>) 

(DO "zanzabar" ON #<SB-IMPL::STRING-OUTPUT-STREAM {7F0F051DE9A3}>) 
"zanzabar"

which admittedly have the opposite names to what you would want, but *standard-output* is the default output, so it had to be shown like this.

Conclusions

You can download the source another-gopher.lisp from my itch.io as a regular lisp file, since the concern here is the pure ansi common lisp condition system and not my ontological affairs.

It is practical and significant as such because it provides a gopher browsing experience of non-lexical computed restarts available to a signalled condition in the common lisp condition system.

New restarts for other gopher itemtypes, and handlers that know about them can be incrementally written and bound outside of the existing code, and potentially reuse the fundamental two gopher itemtypes provided.

In some run time contexts, one imagines a new handler option that would serve the gopher protocol itemtypes of the available common lisp restarts to an external gopher browser instead of slime’s debugger, allowing common lisp interactive debugging to be performed with lynx or dillo or emacs elpher-mode as the debugger interface.

On the other hand, one imagines exposing the whole gopher to slime’s debugger as a gopher browser.

screwlisp proposes kittens