How can I keep a REPL alive during `read-eval-print-loop` + `with-handlers` + a contract violation?

I have a program that starts a REPL via (read-eval-print-loop) that happens to be dynamically inside a with-handlers clause. And I noticed something equivalent to the following that might be related to error-escape-handler. In both of the following two cases, enter (+ 1 #f) at the REPL, which generates a contract failure:

  1. racket -e '(read-eval-print-loop)' [Notice the REPL keeps going.]
  2. racket -e '(with-handlers ([exn:fail? (const 1)]) (read-eval-print-loop))' [Notice the 1 is returned and the REPL dies.]

Now, you may say "that's perfectly obvious behavior," and it is. But I would love to have a way for, say, only contract failures (exn:fail:contract?), or really any failure at my discretion, to behave the usual way and not kill the REPL-inside-with-handlers, so that the REPL state is unaffected like it would normally be. How can I do this?

I got this working, but there might be a better way:

;; file.rktl
(let ([this-eval (current-eval)])
  (define handler (uncaught-exception-handler))
  (define (pls-exit e)
    (parameterize ([error-escape-handler (λ () (exit 1))])
      (handler e)))
  (parameterize ([current-eval (λ (v)
                                 (with-handlers ([exn:fail:contract? handler]
                                                 [(const #t) pls-exit])
                                   (this-eval v)))])
    (read-eval-print-loop)))

$ racket -f file.rktl
> (+ 1 1)
2
> (+ #f)
+: contract violation
  expected: number?
  given: #f
  context...:
   body of top-level
...
> (raise 1)
uncaught exception: 1
$

1 Like

Perhaps this is better? Idk.

(let ([this-eval (current-eval)])
  (define (the-eval v)
    (call-with-exception-handler
     (λ (e)
       (cond
         [(exn:fail:contract? e) e]
         [else
          (error-escape-handler (λ () (exit 1)))
          e]))
     (λ () (this-eval v))))

  (parameterize ([current-eval the-eval])
    (read-eval-print-loop)))

1 Like

Sorawee's solutions are interesting, and I haven't fully digested them, but I ended up with a slightly simpler solution: run the (read-eval-print-loop) outside of the with-handlers :slight_smile:

The with-handlers is handy for my programs commands to report errors as possible bugs; however, for the REPL command, I can just let things be.

The read-eval-print-loop function keeps going after exceptions because

  • it wraps evaluation with a default prompt, and
  • the default unhandled-exception handler aborts to the default prompt.

If you want to escape further than the default prompt for a certain exceptions, then call-with-continuation-prompt is the right idea. For the kinds of exceptions where you want to stop the REPL, escape to an enclosing prompt. For other kinds of exceptions, just pass them along to the enclosing exception handler, which likely invokes the uncaught-exception handler:

(let ([tag (make-continuation-prompt-tag)])
  (call-with-continuation-prompt
   (lambda ()
     (call-with-exception-handler
      (λ (e)
        (cond
          [(exn:fail:contract? e)
           (abort-current-continuation tag e)]
          [else e]))
      (λ () (read-eval-print-loop))))
   tag
   (lambda (e) (raise e))))
2 Likes