;; 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) (call-with-input-string (buffer->string buf) (lambda (in) (let lp () (if (not (eof-object? (read/ss in))) (lp)))))) (define module? vector?) (define (module-env mod) (vector-ref mod 1)) (define (all-exports env) (let lp ((env env) (res '())) (if (not env) res (lp (environment-parent env) (append (env-exports env) res))))) (define (make-sexp-buffer-completer) (buffer-make-completer (lambda (buf word) (let ((len (string-length word))) (sort (filter (lambda (w) (and (>= (string-length w) len) (equal? word (substring w 0 len)))) (map symbol->string (all-exports (interaction-environment))))))))) ;;> 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|{@import }| - import the @var{} in the @scheme{interaction-environment}, useful if the @scheme{import} binding is not available} ;;> @item{@scheme|{@import-only }| - replace the @scheme{interaction-environment} with the given @var{}} ;;> @item{@scheme|{@in []}| - switch to @var{}, or the @scheme{interaction-environment} if @var{} is not specified} ;;> @item{@scheme|{@meta }| - evaluate @var{} in the @scheme{(meta)} module} ;;> @item{@scheme|{@meta-module-is }| - switch the meta module to @var{}} ;;> @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 (load-module module))) (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) (meta-env (module-env (load-module '(meta))))) (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? 'completion: (make-sexp-buffer-completer)))))) (cond ((or (not line) (eof-object? line))) ((equal? line "") (lp module env meta-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 meta-env)) (call-with-input-string line (lambda (in) (let ((op (read/ss in))) (case op ((import import-only) (let* ((mod-name (read in)) (mod+imps (eval `(resolve-import ',mod-name) meta-env))) (if (pair? mod+imps) (let ((env (if (eq? op 'import-only) (let ((env (make-environment))) (interaction-environment env) env) env)) (imp-env (vector-ref (eval `(load-module ',(car mod+imps)) meta-env) 1))) (%import env imp-env (cdr mod+imps) #f) (continue module env meta-env)) (fail "couldn't find module:" mod-name)))) ((in) (let ((name (read/ss in))) (cond ((eof-object? name) (continue #f (interaction-environment) meta-env)) ((eval `(load-module ',name) meta-env) => (lambda (m) (continue name (module-env m) meta-env))) (else (fail "couldn't find module:" name))))) ((meta config) (if (eq? op 'config) (display "Note: @config has been renamed @meta\n" out)) (let ((expr (read/ss in))) (cond ((and (symbol? expr) (eqv? escape (string-ref (symbol->string expr) 0))) (meta meta-env (substring line 6 (string-length line)) (lambda _ (continue module env meta-env)))) (else (eval expr meta-env) (continue module env meta-env))))) ((meta-module-is) (let ((name (read/ss in))) (cond ((eval `(load-module ',name) meta-env) => (lambda (m) (lp module env (module-env m)))) (else (fail "couldn't find module:" name))))) ((exit)) (else (fail "unknown repl command:" op)))))))) (else ;; The outer guard in the parent thread catches read ;; errors and errors in the repl logic itself. (guard (exn (else (print-exception exn (current-error-port)))) (let* ((expr (call-with-input-string line (lambda (in2) ;; Ugly wrapper to account for the ;; implicit state mutation implied by ;; the #!fold-case read syntax. (set-port-fold-case! in2 (port-fold-case? in)) (let ((expr (read/ss in2))) (set-port-fold-case! in (port-fold-case? in2)) expr)))) (thread (make-thread (lambda () ;; The inner guard in the child thread ;; catches errors from eval. (guard (exn (else (print-exception exn (current-output-port)))) (let ((res (eval expr env))) (cond ((not (eq? res (if #f #f))) (write/ss res) (newline))))))))) ;; If an interrupt occurs while the child thread is ;; still running, terminate it, otherwise wait for it ;; to complete. (with-signal-handler signal/interrupt (lambda (n) (display "Interrupt\n" (current-error-port)) (thread-terminate! thread)) (lambda () (thread-join! (thread-start! thread)))))) ;; Loop whether there were errors or interrupts or not. (lp module env meta-env))))))) (if history-file (call-with-output-file history-file (lambda (out) (write (history->list history) out))))))