[racket] racket/sandbox, read-eval-print-loop, and #%top-interaction

From: Greg Hendershott (greghendershott at gmail.com)
Date: Tue Feb 11 15:29:17 EST 2014

So I got a bug report that Typed Racket type info is appearing twice,
in the REPL for my emacs racket-mode:
https://github.com/greghendershott/racket-mode/issues/21

All the relevant code is there, but I'll do a quick recap in this post.

Given this sample program:

#lang racket/base
(require racket/sandbox)

;; 1. Run this.
;; 2. In the resulting REPL, enter "1".
;; Result: The type information is printed twice:
;;     - : Integer [more precisely: One]
;;     - : Integer [more precisely: One]
;;     1

(call-with-trusted-sandbox-configuration
 (lambda ()
   (parameterize ([current-namespace (make-empty-namespace)]
                  [sandbox-input (current-input-port)]
                  [sandbox-output (current-output-port)]
                  [sandbox-error-output (current-error-port)])
     (define e (make-evaluator 'typed/racket)) ;; [^note]
     (parameterize ([current-eval e])
       (read-eval-print-loop)))))

;; [note]: Same behavior instead using make-module-evaluator:
;;   (define e (make-module-evaluator '(module m typed/racket 1)))

You get this kind of interaction.

> 1
- : Integer [generalized from One]
- : Integer [generalized from One]
1

Commenting out the sandbox-output param, so that output goes nowhere,
would give you this instead:

> 1
1

IOW the REPL prints the result, but Typed Racket's #%top-interaction
prints the type through a `display` side-effect. (Which is reasonable,
and what any similar #%top-interaction would do, IIUC.)

After lots of code spelunking and experimentation, I discovered that
using an alternative to read-eval-print-loop gets the desired
behavior:

(define (alternative-read-eval-print-loop)
  (let repl-loop ()
    ;; This prompt catches all error escapes, including from read and print.
    (call-with-continuation-prompt
     ;; 1. proc
     (lambda ()
       (let ([v ((current-prompt-read))])
         (unless (eof-object? v)
           (call-with-values
               (lambda ()
                 ;; This prompt catches escapes during evaluation.
                 ;; Unlike the outer prompt, the handler prints
                 ;; the results.
                 (call-with-continuation-prompt
                  (lambda ()
                    ((current-eval) v) ;; <== JUST DO THIS, NOT THE FOLLOWING...
                    ;; (let ([w (cons '#%top-interaction v)])
                    ;;   ((current-eval) (if (syntax? v)
                    ;;                       (namespace-syntax-introduce
                    ;;                        (datum->syntax #f w v))
                    ;;                       w)))
                    )))
             (lambda results (for-each (current-print) results)))
           ;; Abort to loop. (Calling `repl-loop' directory would not
be a tail call.)
           (abort-current-continuation (default-continuation-prompt-tag)))))
     ;; 2. prompt-tag
     (default-continuation-prompt-tag)
     ;; 3. handler
     (lambda args (repl-loop)))))

After puzzling why, finally I noticed in sandbox.rkt a reference to
#%top-interaction that I'd overlooked previously:

https://github.com/plt/racket/blob/533a2f21f864a25e27e744ef2fef04343e1d02e4/pkgs/sandbox-lib/racket/sandbox.rkt#L858

~~~~~~~~~~~~~~~~~~~

So: I _think_ I've found a reasonable work-around -- use my
alternative-read-eval-print-loop with racket/sandbox.

However I wanted to share this in case any Racket core devs might want
to advise, "Yeah, no, you really don't want to do that, and here's
why...".  Anyone?

Posted on the users mailing list.