Requesting input on some possible low-level changes



   From: Wolfgang Jenkner <wjenkner at inode>
   
   Why don't you find it enough to do (in RUN) something like
   
   (with-simple-restart 
    (macsyma-quit "Macsyma top-level")
    (handler-case
     (macsyma-top-level input-string batch-flag)
     (condition () (invoke-restart 'macsyma-quit))))
   
   This will certainly dispose of your item (6) (we should choose a more
   specific type of condition, though).
   
   Since everybody wants to keep TO_LISP() anyway, this leaves us just
   with the C-c issue, which seems to be system dependent (On both CLISP
   and SBCL C-c signals a condition, but on SBCL it is not handled by the
   form above, for some reason)

I predict great confusion about this C-c issue unless you clear away
the fog.  Pardon the pedantry that follows; I'm sure most readers of
this list know most of what follows, but the conclusions may be a
surprise: why one should never write (handler-case ... (condition
...)) and why an error handler shouldn't handle C-c.

Very few computer applications directly treat the keychord C-c in any
way special.  Rather, it is certain devices like teletype drivers,
pseudo teletype drivers, and programs like telnet and xterm that stand
in for fancy teletypes, that interpret certain keyboard events as an
instruction to send asynchronous signals to processes connected to
that teletype instead of threating the keychord as a character.  In
Unix land this C-c chord typically signals an asynchronous `signal'
called a SIGINT.  This `signal' has nothing to do with the Common Lisp
notion of signal.

Continuing in Unix land, the default action when a process receives a
SIGINT signal is for the process to terminate.  This is automatic if
the program takes no special action for SIGINT.  But most programs
with rich user interaction and important state (like Lisp!) field
SIGINT and interpret it as a request to interrupt.  In a Lisp
application, it is idiomatic for a SIGINT to enter the debugger in
such a way that invoking a continue restart will continue the
interrupted computation intact.

Now, suppose we coded this way:

   (with-simple-restart 
    (macsyma-quit "Macsyma top-level")
    (handler-case
     (macsyma-top-level input-string batch-flag)
     (ERROR () (invoke-restart 'macsyma-quit))))   

The question is what should happen if a C-c SIGINT occurs.  Should
receipt of SIGINT be interpreted as an error, or equivalently, should
SIGINT cause common-lisp:signal to be called to to signal some subtype
of the common-lisp:error condition?

I feel the answer is no.  It is reasonable for for an implementation
to signal some type of condition when SIGINT occurs, but it would be
unfortunate if that condition is a subtype of error.  The reason is
that idiomatic lisp code may execute locally inside handlers for
various subtypes of error, and even for error itself.  (This is what
the macro ignore-errors does.)  A usual intended meaning of SIGINT is
to interrupt the computation in a continuable way.  But if execution
accidentally happens to be inside an handler for cl:error (such as
ignore-errors) when the SIGINT is received, that handler will run even
if the user's intention was to interrupt with the possibility of
continuing.

Therefore, SIGINT should signal some subtype of condition disjoint
from error.  If the application _desires_ SIGINT to throw out of the
computation (e.g. back to the command loop) it can easily implement
that with a handler specialized on the condition type.  But if SIGINT
signals some subtype of cl:error, and if SIGINT happens while
execution is inside some very local error handler, that handler will
run for something that should not have been interpreted as an error
and the application won't get the chance to continue as if nothing
happened.

Implementations are all over the map:

  Allegro signals a subtype of error, and I feel that is a mistake.

  You claim with CMUCL the handler-case doesn't catch SIGINT
processing, and I verified by tracing cl:signal in an older CMUCL that
the SIGINT processor apparently doesn't use the condition system to
process SIGINT.  There is no requirement that it do so, although the
condition system feels like the appropriate language tool for
interfacing with things like asynchronous events.  The condition
system came fairly late to ANSI CL, so CMUCL's implementation choice
may have a large historical component.

   Apparently, they did quite a similar thing at
   
   telnet://maxima.franz.com
   
   (Tracing the macro WITH-SIMPLE-RESTART will show how they changed the
   source code).

I happen to be the one who wrote that code and brought up that server
at the request of rjf and others.  I'm happy to append the modified
listener wrapper here, although it isn't exactly what you want.  In
particular, you probably want an additional handler for the condition
that corresponds to SIGINT (and its evil analog on Windows), whether
or not that condition class is a subtype of cl:error.

(defun user::run (&optional (handle-errors t))	; handle-errors is for debugging
  "Run Maxima in its own package."
  (in-package "MAXIMA")
					; jfa new command-line communication
  (let ((input-string *standard-input*)
	(maxima_int_lisp_preload (maxima-getenv "MAXIMA_INT_LISP_PRELOAD"))
	(maxima_int_input_string (maxima-getenv "MAXIMA_INT_INPUT_STRING"))
	(batch-flag (maxima-getenv "MAXIMA_INT_BATCH_FLAG")))
    (if maxima_int_lisp_preload
	(load maxima_int_lisp_preload))
    (if maxima_int_input_string
	(setq input-string (make-string-input-stream maxima_int_input_string)))

    (catch 'to-lisp
      (set-pathnames)
      #+(or cmu clisp allegro)
      (loop
	(with-simple-restart (macsyma-quit "Maxima top-level")
	  (handler-bind
	      ((error (lambda (c)
			(when handle-errors
			  (format t "~@<~3iExecution signalled an error: ~_~a~:@>~&" c)
			  (invoke-restart 'macsyma-quit)))))
	    (macsyma-top-level input-string batch-flag))))
      #-(or cmu clisp allegro)
      (catch 'macsyma-quit
	(macsyma-top-level input-string batch-flag)))))

I'll suggest that, whatever someone implements, there should be
&optional or &key arguments to the REPL to control handling of errors.
That way the same REPL code that runs in a user image can be used by
developers who sometimes _need_ to see internal errors, and who need
to be able to interrupt and enter the debugger.  It's unwise to have
separate versions of the REPL loop because they can diverge, and it's
also unwise to release code to users where you can't instruct them how
to turn off automatic error handling in order to provide developers
with backtraces after failures that aren't somehow reproducible on
developer machines.

Finally, to return to this code fragment:

    (handler-case
     ...
     (condition () (invoke-restart 'macsyma-quit)))

One should _never_ write a handler-case for condition.  The reason is
that the user programmer cannot know what other code might signal
conditions in normal operation.  (The ANS has a warning about this
somewhere.)  The handler-case idiom above promiscuously grabs _every_
signalled condition and throws out of the computation.  In principle
you cannot know whether Lisp functions like car or cons might use
signal in their normal operation.  The Allegro compiler actually uses
signal to inform about code that must be compiled differently for
8-bit and 16-bit=character images.  The condition signalled is a
subtype of no public type more specific than condition, but the code
above would break the Allegro compiler (in certain obscure
circumstances unlikely to be encountered at maxima run time :-).

Once again, sorry for the pedantry...

Steven M. Haflich <smh@franz.com>
Franz Inc., Oakland CA