diff --git a/lib/chibi/repl.scm b/lib/chibi/repl.scm index 0c97e28a..db6f3b6f 100644 --- a/lib/chibi/repl.scm +++ b/lib/chibi/repl.scm @@ -1,5 +1,5 @@ ;; repl.scm - friendlier repl with line editing and signal handling -;; Copyright (c) 2012 Alex Shinn. All rights reserved. +;; Copyright (c) 2012-2013 Alex Shinn. All rights reserved. ;; BSD-style license: http://synthcode.com/license.txt ;;> A user-friendly REPL with line editing and signal handling. @@ -165,6 +165,110 @@ ;;> @item{@scheme|{@exit}| - exit the REPL} ;;> ] +(define-record-type Repl + (make-repl + in out escape module env meta-env make-prompt history-file history raw?) + repl? + (in repl-in repl-in-set!) + (out repl-out repl-out-set!) + (escape repl-escape repl-escape-set!) + (module repl-module repl-module-set!) + (env repl-env repl-env-set!) + (meta-env repl-meta-env repl-meta-env-set!) + (make-prompt repl-make-prompt repl-make-prompt-set!) + (history-file repl-history-file repl-history-file-set!) + (history repl-history repl-history-set!) + (raw? repl-raw? repl-raw?-set!)) + +(define (repl/import-aux rp args meta continue only?) + (let* ((mod-name (cadr args)) + (mod+imps (eval `(resolve-import ',mod-name) (repl-meta-env rp)))) + (cond + ((pair? mod+imps) + (guard + (exn + (else + (print-exception exn (current-error-port)) + (warn "error loading module:" mod-name) + (continue rp))) + (let ((env (if only? (make-environment) (repl-env rp))) + (imp-env + (module-env + (eval `(load-module ',(car mod+imps)) (repl-meta-env rp))))) + (%import env imp-env (cdr mod+imps) #f) + (repl-env-set! rp env) + (continue rp)))) + (else + (warn "couldn't find module:" mod-name) + (continue rp))))) + +(define (repl/import rp args meta continue) + (repl/import-aux rp args meta continue #f)) + +(define (repl/import-only rp args meta continue) + (repl/import-aux rp args meta continue #t)) + +(define (repl/in rp args meta continue) + (cond + ((null? (cdr args)) + (repl-module-set! rp #f) + (repl-env-set! rp (interaction-environment))) + ((eval `(load-module ',(cadr args)) (repl-meta-env rp)) + => (lambda (m) + (repl-module-set! rp (cadr args)) + (repl-env-set! rp (module-env m)))) + (else + (warn "couldn't find module:" (cadr args)))) + (continue rp)) + +(define (repl/meta rp args meta continue) + (cond + ((null? (cdr args)) + (warn "usage: @meta ") + (continue rp)) + ((and (symbol? (cadr args)) + (eqv? (repl-escape rp) (string-ref (symbol->string (cadr args)) 0))) + (meta rp (cdr args) (lambda _ (continue rp)))) + (else + (eval (cadr args) (repl-meta-env rp)) + (continue rp)))) + +(define (repl/meta-module-is rp args meta continue) + (cond + ((null? (cdr args)) + (warn "usage: @meta ")) + ((eval `(load-module ',(cadr args)) (repl-meta-env rp)) + => (lambda (m) (repl-meta-env-set! rp (module-env m)))) + (else + (warn "couldn't find module:" (cadr args)))) + (continue rp)) + +(define (repl/help rp args meta continue) + (let ((out (repl-out rp))) + (cond + ((null? (cdr args)) + (display "Try @help []\n" out)) + ((null? (cddr args)) + (let* ((failed (list 'failed)) + (val (guard (exn (else (print-exception exn) failed)) + (eval (second args) (repl-env rp)))) + (mod (and (procedure? val) (containing-module val)))) + (cond + (mod + (write val out) (newline out) (newline out) + (print-module-binding-docs (car mod) (second args) out)) + ((not (eq? val failed)) + (describe val out))))) + (else + (guard (exn (else (print-exception exn (current-error-port)))) + (print-module-binding-docs (third args) (second args) out)))) + (continue rp))) + +(define (repl/exit rp args meta continue) + ;; To exit the repl simply don't call continue. + #f) + +;; Utility to read all objects from a port accumulated into a list. (define (read/ss/all port) (let loop ((l '())) (let ((x (read/ss port))) @@ -172,21 +276,74 @@ (reverse l) (loop (cons x l)))))) -(define (repl . o) +(define (string->sexp-list str) + (call-with-input-string str read/ss/all)) + +(define (repl/eval rp expr-list) + (let ((out (repl-out rp))) + (guard (exn (else (print-exception exn out))) + (let ((thread + (make-thread + (lambda () + ;; The inner guard in the child thread catches errors + ;; from eval. + (guard (exn (else (print-exception exn out))) + (for-each + (lambda (expr) + (call-with-values (lambda () (eval expr (repl-env rp))) + (lambda res-list + (cond + ((not (or (null? res-list) + (equal? res-list (list (if #f #f))))) + (write/ss (car res-list) out) + (for-each + (lambda (res) + (write-char #\space out) + (write/ss res out)) + (cdr res-list)) + (newline out)))))) + expr-list)))))) + ;; 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 "\nInterrupt\n" out) + (thread-terminate! thread)) + (lambda () (thread-join! (thread-start! thread)))))))) + +(define (repl/eval-string rp str) + (repl/eval + rp + (guard (exn (else (print-exception exn (current-error-port)))) + ;; Ugly wrapper to account for the implicit state mutation + ;; implied by the #!fold-case read syntax. + (let ((in (repl-in rp)) + (in2 (open-input-string str))) + (set-port-fold-case! in2 (port-fold-case? in)) + (set-port-line! in2 (port-line in)) + (let ((expr-list (read/ss/all in2))) + (set-port-fold-case! in (port-fold-case? in2)) + expr-list))))) + +(define (keywords->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 (cond - ((memq 'environment: o) => - (lambda (x) - (if module - (error "The module: and environment: keyword arguments should not both be given.")) - (cadr x))) - (module - (module-env - (if (module? module) module (load-module module)))) - (else (interaction-environment)))) + (env + (cond + ((memq 'environment: o) => + (lambda (x) + (if module + (error (string-append "The module: and environment: keyword " + "arguments should not both be given."))) + (cadr x))) + (module + (module-env + (if (module? module) module (load-module module)))) + (else (interaction-environment)))) (make-prompt (cond ((memq 'make-prompt: o) => cadr) @@ -206,179 +363,71 @@ (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 (make-prompt module)) - (line - (cond - (raw? - (display prompt out) - (flush-output out) - (read-line/complete-sexp in)) - (else - (edit-line in out - 'prompt: prompt - 'history: history - 'complete?: buffer-complete-sexp? - 'completion: (make-sexp-buffer-completer)))))) + '("emacs" "dumb"))))) + (meta-env (cond ((memq 'meta: o) => cadr) + (else (module-env (load-module '(meta))))))) + (make-repl + in out escape module env meta-env make-prompt history-file history raw?))) + +(define (repl/edit-line rp) + (let ((prompt ((repl-make-prompt rp) (repl-module rp))) + (in (repl-in rp)) + (out (repl-out rp))) + (cond + ((repl-raw? rp) + (display prompt out) + (flush-output out) + (read-line/complete-sexp in)) + (else + (edit-line in out + 'prompt: prompt + 'history: (repl-history rp) + 'complete?: buffer-complete-sexp? + 'completion: (make-sexp-buffer-completer)))))) + +(define repl-commands + `((import . ,repl/import) + (import-only . ,repl/import-only) + (in . ,repl/in) + (meta . ,repl/meta) + (meta-module-is . ,repl/meta-module-is) + (? . ,repl/help) + (h . ,repl/help) + (help . ,repl/help) + (exit . ,repl/exit))) + +(define (repl . o) + (let ((rp (keywords->repl o))) + (let lp ((rp rp)) + (let ((line (repl/edit-line rp))) (cond ((or (not line) (eof-object? line))) ((equal? line "") - (history-reset! history) - (lp module env meta-env)) + (history-reset! (repl-history rp)) + (lp rp)) (else - (history-commit! history line) + (history-commit! (repl-history rp) line) (cond - ((and (> (string-length line) 0) (eqv? escape (string-ref line 0))) - (let meta ((env env) - (line (substring line 1 (string-length line))) + ((and (> (string-length line) 1) + (eqv? (repl-escape rp) (string-ref line 0))) + ;; @ escaped command + (let meta ((rp rp) + (args (string->sexp-list (substring line 1))) (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) - (guard - (exn - (else - (print-exception exn (current-error-port)) - (fail "error loading module:" mod-name))) - (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))))) - ((config) - (display "Note: @config has been renamed @meta\n" out) - (continue module env meta-env)) - ((meta) - (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))))) - ((? h help) - (let* ((x (read/ss in)) - (y (read/ss in))) - (cond - ((eof-object? x) - (display "Try @help []\n" out)) - ((eof-object? y) - (let* ((failed (list 'failed)) - (val (guard (exn - (else - (print-exception exn) - failed)) - (eval x env))) - (mod (and (procedure? val) - (containing-module val)))) - (cond - (mod - (write val out) (newline out) (newline out) - (print-module-binding-docs (car mod) x out)) - ((not (eq? val failed)) - (describe val out))))) - (else - (guard (exn - (else - (print-exception exn (current-error-port)))) - (print-module-binding-docs y x out)))) - (continue module env meta-env))) - ((exit)) - (else - (fail "unknown repl command:" op)))))))) + (cond + ((null? args) + (warn "empty repl command") + (continue rp)) + ((assq (car args) repl-commands) + => (lambda (x) ((cdr x) rp args meta continue))) + (else + (warn "unknown repl command" (car args)) + (continue rp))))) (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-list - (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-list (read/ss/all in2))) - (set-port-fold-case! in (port-fold-case? in2)) - expr-list)))) - (thread - (make-thread - (lambda () - ;; The inner guard in the child thread - ;; catches errors from eval. - (guard - (exn - (else - (print-exception exn (current-output-port)))) - (for-each - (lambda (expr) - (call-with-values - (lambda () - (eval (list 'begin expr) env)) - (lambda res-list - (cond - ((not (or (null? res-list) - (equal? res-list - (list (if #f #f))))) - (write/ss (car res-list) out) - (for-each - (lambda (res) - (write-char #\space out) - (write/ss res out)) - (cdr res-list)) - (newline out)))))) - expr-list)))))) - ;; 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 "\nInterrupt\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)))))) + ;; Normal expression to eval. + (repl/eval-string rp line) + (lp rp))))))) + ;; Update the history file on completion. + (if (repl-history-file rp) + (call-with-output-file (repl-history-file rp) + (lambda (out) (write (history->list (repl-history rp)) out)))))) diff --git a/lib/chibi/repl.sld b/lib/chibi/repl.sld index 90a3836f..881765d6 100644 --- a/lib/chibi/repl.sld +++ b/lib/chibi/repl.sld @@ -5,5 +5,5 @@ (chibi ast) (chibi modules) (chibi doc) (chibi string) (chibi io) (chibi process) (chibi term edit-line) - (srfi 1) (srfi 18) (srfi 38) (srfi 95) (srfi 98)) + (srfi 1) (srfi 9) (srfi 18) (srfi 38) (srfi 95) (srfi 98)) (include "repl.scm"))