chibi-scheme/lib/chibi/repl.scm

186 lines
8.1 KiB
Scheme

;; repl.scm - friendlier repl with line editing and signal handling
;; Copyright (c) 2011 Alex Shinn. All rights reserved.
;; BSD-style license: http://synthcode.com/license.txt
;;> A user-friendly REPL with line editing and signal handling.
;;> The default REPL provided by chibi-scheme is very minimal,
;;> meant primarily to be small and work on any platform. This
;;> module provides an advanced REPL that handles vt100 line
;;> editing and signal handling, so that C-c will interrupt a
;;> computation and bring you back to the REPL prompt. To use
;;> this repl, run
;;> @command{chibi-scheme -mchibi.repl -e'(repl)'}
;;> from the command line or within Emacs.
(define (with-signal-handler sig handler thunk)
(let ((old-handler #f))
(dynamic-wind
(lambda () (set! old-handler (set-signal-action! sig handler)))
thunk
(lambda () (set-signal-action! sig old-handler)))))
(define (warn msg . args)
(let ((out (current-error-port)))
(display msg out)
(for-each (lambda (x) (write-char #\space out) (write x out)) args)
(newline out)))
(define (write-to-string x)
(call-with-output-string (lambda (out) (write x out))))
(define (buffer-complete-sexp? buf)
(guard (exn (else #f))
(call-with-input-string (buffer->string buf)
(lambda (in)
(let lp () (if (not (eof-object? (read/ss in))) (lp)))))
#t))
(define module? vector?)
(define (module-env mod) (vector-ref mod 1))
;;> Runs an interactive REPL. Repeatedly displays a prompt,
;;> then Reads an expression, Evaluates the expression, Prints
;;> the result then Loops. Terminates when the end of input is
;;> reached or the @scheme|{@exit}| command is given.
;;>
;;> Basic Emacs-style line editing with persistent history
;;> completion is provided. C-c can be used to interrupt the
;;> current computation and drop back to the prompt. The
;;> following keyword arguments customize the REPL:
;;>
;;> @itemlist[
;;> @item{@scheme{in:} - the input port (default @scheme{(current-input-port)})}
;;> @item{@scheme{out:} - the output port (default @scheme{(current-output-port)})}
;;> @item{@scheme{module:} - the initial module (default @scheme{(interaction-environment)})}
;;> @item{@scheme{escape:} - the command escape character (default @scheme|{#\@}|)}
;;> @item{@scheme{history:} - the initial command history}
;;> @item{@scheme{history-file:} - the file to save history to (default ~/.chibi-repl-history)}
;;> ]
;;>
;;> REPL commands in the style of @hyperlink["http://s48.org/"]{Scheme48}
;;> are available to control out-of-band properties. By default a command
;;> is written as an identifier beginning with an "@" character (which
;;> would not be a portable identifier), but this can be customized with
;;> the @scheme{escape:} keyword. The following commands are available:
;;>
;;> @itemlist[
;;> @item{@scheme|{@in [<module>]}| - switch to @var{<module>}, or the @scheme{interaction-environment} if @var{<module>} is not specified}
;;> @item{@scheme|{@config <expr>}| - evaluate @var{<expr>} in the @scheme{(config)} module}
;;> @item{@scheme|{@config-module-is <module>}| - switch the config module to @var{<module>}}
;;> @item{@scheme|{@exit}| - exit the REPL}
;;> ]
(define (repl . o)
(let* ((in (cond ((memq 'in: o) => cadr) (else (current-input-port))))
(out (cond ((memq 'out: o) => cadr) (else (current-output-port))))
(escape (cond ((memq 'escape: o) => cadr) (else #\@)))
(module (cond ((memq 'module: o) => cadr) (else #f)))
(env (if module
(module-env
(if (module? module)
module
(eval `(load-module ',module) *config-env*)))
(interaction-environment)))
(history-file
(cond ((memq 'history-file: o) => cadr)
(else (string-append (get-environment-variable "HOME")
"/.chibi-repl-history"))))
(history
(cond ((memq 'history: o) => cadr)
(else
(or (guard (exn (else #f))
(list->history
(call-with-input-file history-file read)))
(make-history)))))
(raw? (cond ((memq 'raw?: o) => cadr)
(else (member (get-environment-variable "TERM")
'("emacs" "dumb"))))))
(let lp ((module module) (env env) (config-env *config-env*))
(let* ((prompt
(string-append (if module (write-to-string module) "") "> "))
(line
(cond
(raw?
(display prompt out)
(flush-output out)
(read-line in))
(else
(edit-line in out
'prompt: prompt
'history: history
'complete?: buffer-complete-sexp?)))))
(cond
((or (not line) (eof-object? line)))
((equal? line "") (lp module env config-env))
(else
(history-commit! history line)
(cond
((and (> (string-length line) 0) (eqv? escape (string-ref line 0)))
(let meta ((env env)
(line (substring line 1 (string-length line)))
(continue lp))
(define (fail msg . args)
(apply warn msg args)
(continue module env config-env))
(call-with-input-string line
(lambda (in)
(let ((op (read/ss in)))
(case op
((in)
(let ((name (read/ss in)))
(cond
((eof-object? name)
(continue #f (interaction-environment) config-env))
((eval `(load-module ',name) config-env)
=> (lambda (m)
(continue name (module-env m) config-env)))
(else
(fail "couldn't find module:" name)))))
((config)
(let ((expr (read/ss in)))
(cond
((and
(symbol? expr)
(eqv? escape (string-ref (symbol->string expr) 0)))
(meta config-env
(substring line 6 (string-length line))
(lambda _ (continue module env config-env))))
(else
(eval expr config-env)
(continue module env config-env)))))
((config-module-is)
(let ((name (read/ss in)))
(cond
((eval `(load-module ',name) config-env)
=> (lambda (m) (lp module env (module-env m))))
(else
(fail "couldn't find module:" name)))))
((exit))
(else
(fail "unknown repl command:" op))))))))
(else
(guard
(exn
(else (print-exception exn (current-error-port))))
(let* ((expr (call-with-input-string line read/ss))
(thread
(make-thread
(lambda ()
(guard
(exn
(else (print-exception exn (current-error-port))))
(let ((res (eval expr env)))
(cond
((not (eq? res (if #f #f)))
(write/ss res)
(newline)))))))))
(with-signal-handler
signal/interrupt
(lambda (n)
(display "Interrupt\n" (current-error-port))
(thread-terminate! thread))
(lambda () (thread-join! (thread-start! thread))))))
(lp module env config-env)))))))
(if history-file
(call-with-output-file history-file
(lambda (out) (write (history->list history) out))))))