racket/collects/xrepl/xrepl.rkt
Eli Barzilay e311de0522 Improve some printouts.
Specifically the one from ,rr.  Also make ,switch syntax error more
friendly.  Also, test for the recent xrepl fix, including its printout.
2013-04-17 16:39:10 -04:00

1491 lines
66 KiB
Racket

#lang racket/base
;; ----------------------------------------------------------------------------
;; customization
(define toplevel-prefix (make-parameter "-")) ; when not in a module
(define saved-values-number (make-parameter 5))
(define saved-values-patterns (make-parameter '("^" "$~a")))
;; TODO: when there's a few more of these, make them come from the prefs
;; ----------------------------------------------------------------------------
(require racket/list racket/match scribble/text/wrap)
;; ----------------------------------------------------------------------------
;; utilities
(define home-dir (find-system-path 'home-dir))
(define-namespace-anchor anchor)
(define (here-namespace) (namespace-anchor->namespace anchor))
;; autoloads: avoid loading a ton of stuff to minimize startup penalty
(define autoloaded-specs (make-hasheq))
(define (autoloaded? sym) (hash-ref autoloaded-specs sym #f))
(define-syntax-rule (defautoload libspec id ...)
(begin (define (id . args)
(set! id (parameterize ([current-namespace (here-namespace)])
(dynamic-require 'libspec 'id)))
(hash-set! autoloaded-specs 'libspec #t)
(hash-set! autoloaded-specs 'id #t)
(apply id args))
...))
(defautoload racket/system system system*)
(defautoload racket/file file->string)
(defautoload setup/path-to-relative path->relative-string/setup)
(defautoload syntax/modcode get-module-code)
(defautoload racket/path find-relative-path)
;; similar, but just for identifiers
(define hidden-namespace (make-base-namespace))
(define initial-namespace (current-namespace))
;; when `racket/enter' initializes, it grabs the `current-namespace' to get
;; back to -- which means it should be instantiated in a top level namespace
;; rather than in (here-namespace); but if we use `initial-namespace' we
;; essentially rely on the user to not kill `enter!' (eg, (define enter! 4)).
;; the solution is to make a `hidden-namespace' where we store these bindings,
;; then instantiate needed modules in the initial namespace and immediately
;; attach the modules to the hidden one then use it, so changes to the binding
;; in `initial-namespace' doesn't affect us.
(define (make-lazy-identifier sym from)
(define id #f)
(λ () (or id (begin (parameterize ([current-namespace initial-namespace])
(namespace-require from))
(parameterize ([current-namespace hidden-namespace])
(namespace-attach-module initial-namespace from)
(namespace-require from)
(set! id (namespace-symbol->identifier sym))
id)))))
;; makes it easy to use meta-tools without user-namespace contamination
(define (eval-sexpr-for-user form)
(eval (namespace-syntax-introduce (datum->syntax #f form))))
;; If `mod' is a known module, return it; if it's a symbol and 'mod is
;; known, return 'mod; otherwise return #f. If `mode' is 'path, return
;; a path for modules that come from files and #f otherwise, and if it's
;; 'path/sym return a path for the same and a symbolic name for known
;; modules with that name.
(define (known-module mod [mode #f])
(define (known-top mod)
(and (not (eq? mode 'path))
(with-handlers ([exn:fail? (λ (_) #f)])
(module->imports mod)
(if (eq? mode 'path/sym) (cadr mod) mod))))
(match mod
[(list 'quote (? symbol?)) (known-top mod)]
[_ (or (with-handlers ([exn:fail? (λ (_) #f)])
(define r
(resolved-module-path-name
((current-module-name-resolver) mod #f #f #f)))
(if (not mode)
(and r mod)
;; sanity check that path results exists
(and (or (and (path? r) (file-exists? r))
(and (eq? mode 'path/sym) (symbol? r)))
r)))
;; for symbols, try also 'mod
(and (symbol? mod) (known-top `',mod)))]))
(define (module->path module)
(resolved-module-path-name ((current-module-name-resolver) module #f #f #f)))
(define (mpi->name mpi)
(resolved-module-path-name (module-path-index-resolve mpi)))
(define (->relname x)
(cond [(path-string? x) (path->relative-string/setup x)]
[x]))
(define (module-displayable-name mod)
(define (choose-path x)
;; choose the shortest from an absolute path, a relative path, and a
;; "~/..." path.
(if (not (complete-path? x)) ; shouldn't happen
x
(let* ([r (path->string (find-relative-path (current-directory) x))]
[h (path->string (build-path (string->path-element "~")
(find-relative-path home-dir x)))]
[best (if (< (string-length r) (string-length h)) r h)]
[best (if (< (string-length best) (string-length x)) best x)])
best)))
(define (get-prefix* path)
(define x (if (string? path) path (path->string path)))
(define y (->relname path))
(if (equal? x y)
(format "~s" (choose-path x))
(regexp-replace #rx"[.]rkt$" y "")))
(let loop ([mod mod])
(match mod
[(? symbol?) (symbol->string mod)]
[(list 'quote (? symbol? s)) (format "'~a" (loop s))]
[(list 'file (? string? s)) (loop (string->path s))]
[(or (? path?) (? string?)) (get-prefix* mod)]
[_ (error 'xrepl "internal error; ~v" mod)])))
(define (here-source) ; returns a path, a symbol, or #f (= not in a module)
(variable-reference->module-source
(eval (namespace-syntax-introduce
(datum->syntax #f `(,#'#%variable-reference))))))
(define (phase->name phase [fmt #f])
(define s
(case phase
[(0) #f] [(#f) "for-label"] [(1) "for-syntax"] [(-1) "for-template"]
[else (format "for-meta:~a" phase)]))
(cond [(not fmt) s] [s (format fmt s)] [else ""]))
;; true if (quote sym) is a known module name
(define (known-module-name? sym)
(and (symbol? sym)
(with-handlers ([exn? (λ (_) #f)]) (module->imports `',sym) #t)))
(define last-output-port #f)
(define last-error-port #f)
(define (maybe-new-output-ports)
(define-syntax-rule (maybe last cur)
(unless (eq? last cur)
(when last (flush-output last)) ; just in case
(set! last cur)
(flush-output last)
(port-count-lines! last)))
(maybe last-output-port (current-output-port))
(maybe last-error-port (current-error-port)))
(define (fresh-line [stderr? #f])
(maybe-new-output-ports)
(define port (if stderr? last-error-port last-output-port))
(flush-output port)
(define-values [line col pos] (port-next-location port))
(unless (eq? col 0) (newline)))
(define (zero-column!)
;; there's a problem whenever there's some printout followed by a read: the
;; cursor will at column zero, but the port counting will think that it's
;; still right after the printout; call this function in such cases to adjust
;; the column to 0.
(maybe-new-output-ports)
(define-values [line col pos] (port-next-location last-output-port))
(set-port-next-location! last-output-port line 0 pos))
;; wrapped output
(define-syntax-rule (with-wrapped-output body ...)
(do-xrepl-wrapped-output (λ () body ...)))
(define (do-xrepl-wrapped-output thunk)
(do-wrapped-output thunk #:indent-first -2 #:line-prefix #rx"^;+ *"))
;; maybe move this into scribble/text/wrap
(define (do-wrapped-output thunk
#:wrap-width [width (wrap-width)]
#:line-prefix [prefix-rx #f] ; not including spaces
#:indent-first [fst-indent 0] ; can be negative
#:split-word [split-word #f])
(define-values [ip op] (make-pipe))
(define widths
(cond [(fst-indent . > . 0) (cons (- width fst-indent) width)]
[(fst-indent . < . 0) (cons width (+ width fst-indent))]
[else (cons width width)]))
(define indents
(let ([spaces (make-bytes (abs fst-indent) (char->integer #\space))])
(cond [(fst-indent . > . 0) (cons spaces #"")]
[(fst-indent . < . 0) (cons #"" spaces)]
[else (cons #"" #"")])))
(define out (current-output-port))
(define (wrapper)
(define m (cond [(regexp-match #rx#"^(?:\n|[^\n]+)" ip) => car] [else #f]))
(when m ; #f => we're at the end
(if (equal? #"\n" m)
(newline out)
(let* ([i (cdar (regexp-match-positions #rx#"^ *" m))]
[p (regexp-match-positions prefix-rx m i)]
[i (if (and p (= (caar p) i)) (cdar p) i)]
[j (caar (regexp-match-positions #rx" *$" m))]
[widths (cons (- (car widths) i) (- (cdr widths) i))]
[lines (wrap-line (bytes->string/utf-8 (subbytes m i j))
widths split-word)])
(write-bytes m out 0 i)
(write-bytes (car indents) out)
(write-string (car lines) out)
(for ([l (in-list (cdr lines))])
(newline out)
(write-bytes m out 0 i)
(write-bytes (cdr indents) out)
(write-string l out))))
(wrapper)))
(define th (thread wrapper))
(parameterize ([current-output-port op]) (thunk))
(close-output-port op)
(thread-wait th))
;; ----------------------------------------------------------------------------
;; toplevel "," commands management
(struct command (names argline blurb desc handler))
(define commands (make-hasheq))
(define commands-list '()) ; for help displays, in definition order
(define current-command (make-parameter #f))
(define (register-command! names blurb argline desc handler)
(let* ([names (if (list? names) names (list names))]
[cmd (command names blurb argline desc handler)])
(for ([n (in-list names)])
(if (hash-ref commands n #f)
(error 'defcommand "duplicate command name: ~s" n)
(hash-set! commands n cmd)))
(set! commands-list (cons cmd commands-list))))
(define-syntax-rule (defcommand cmd+aliases argline blurb [desc ...]
body0 body ...)
(register-command! `cmd+aliases `argline `blurb `(desc ...)
(λ () body0 body ...)))
(define (cmderror fmt #:default-who [dwho #f] . args)
(let ([cmd (current-command)])
(raise-user-error (or (and cmd (string->symbol (format ",~a" cmd)))
dwho '???)
(apply format fmt args))))
;; returns first peeked non-space/tab char (#\return is considered space too)
(define string->list*
(let ([t (make-weak-hasheq)]) ; good for string literals
(λ (s) (hash-ref! t s (λ () (string->list s))))))
(define (skip-spaces/peek [skip " \t\r"])
(let ([skip (string->list* skip)])
(let loop ()
(let ([ch (peek-char)])
(if (memq ch skip) (begin (read-char) (loop)) ch)))))
(define (here-path [no-path eof])
(let ([x (here-source)]) (if (path? x) x no-path)))
(define (here-mod-or-eof)
(let ([x (here-source)])
(if (not x)
eof
(datum->syntax #f
(cond [(symbol? x) `',x]
[(path? x) (let ([s (path->string x)])
(if (absolute-path? x) `(file ,s) s))]
[else (error 'here-mod-or-eof "internal error: ~s" x)])))))
(define (getarg kind [flag 'req] #:default [dflt #f])
(unless (memq flag '(req opt list list+))
(error 'getarg "unknown flag: ~e" flag))
(define (argerror fmt . args)
(apply cmderror #:default-who 'getarg fmt args))
(define (missing) (argerror "missing ~a argument" kind))
(define (get read)
(define (get-one)
(cond [(eq? read read-line-arg) (read)]
[(eq? #\newline (skip-spaces/peek)) eof]
[else (read)]))
(define (get-list)
(let ([x (get-one)]) (if (eof-object? x) '() (cons x (get-list)))))
(define 0th (get-one))
(define 0th? (not (eof-object? 0th)))
(define 1st (if (and (not 0th?) dflt) (dflt) 0th))
(define 1st? (not (eof-object? 1st)))
(cond [1st? (if (memq flag '(list list+))
(cons 1st (if 0th? (get-list) '()))
1st)]
[(eq? flag 'opt) #f]
[(eq? flag 'list) '()]
[else (missing)]))
(define (read-string-arg)
(define ch (skip-spaces/peek " \t\r\n"))
(let* ([i (current-input-port)]
[m (if (eq? ch #\")
(let ([m (regexp-match #px#"((?:\\\\.|[^\"\\\\]+)+)\"" i)])
(and m (regexp-replace* #rx#"\\\\(.)" (cadr m) #"\\1")))
(cond [(regexp-match #px#"\\S+" i) => car] [else #f]))])
(if m (bytes->string/locale m) eof)))
(define (read-line-arg)
(regexp-replace* #px"^\\s+|\\s+$" (read-line) ""))
(define (symbolic-shorthand x)
;; convenience: symbolic requires that name a file turn to a `file'
;; require, and those that name a known module turn to a (quote sym)
(define dtm (if (syntax? x) (syntax->datum x) x))
(if (not (symbol? dtm))
x
(let* (;; try a file
[f (expand-user-path (symbol->string dtm))]
[f (and (file-exists? f) (path->string f))]
[f (and f (if (absolute-path? f) `(file ,f) f))]
;; try a quoted one if the above failed
[m (or f (and (known-module-name? dtm) `',dtm))]
[m (and m (if (syntax? x) (datum->syntax x m x) m))])
(or m x))))
(define (process-require req)
;; no verification of requires -- let the usual error happen if needed
(symbolic-shorthand req))
(define (process-module mod)
(or (known-module (symbolic-shorthand mod))
(cmderror "unknown module: ~s" mod)))
(define (translate arg convert)
(and arg (if (memq flag '(list list+)) (map convert arg) (convert arg))))
(let loop ([kind kind])
(case kind
[(line) (get read-line-arg)]
[(string) (get read-string-arg)]
[(path) (translate (loop 'string) expand-user-path)]
[(sexpr) (get read)]
[(syntax) (translate (get read-syntax) namespace-syntax-introduce)]
[(require) (translate (loop 'syntax) process-require)]
[(module) (translate (loop 'sexpr) process-module)]
[else (error 'getarg "unknown arg kind: ~e" kind)])))
(define (run-command cmd)
(parameterize ([current-command cmd])
(with-handlers ([void (λ (e)
(if (exn? e)
(eprintf "~a\n" (exn-message e))
(eprintf "~s\n" e)))])
((command-handler (or (hash-ref commands cmd #f)
(error "Unknown command:" cmd)))))))
(defcommand (help h ?) "[<command-name>]"
"display available commands"
["Lists known commands and their help; use with a command name to get"
"additional information for that command."]
(define arg (match (getarg 'sexpr 'opt) [(list 'unquote x) x] [x x]))
(define cmd
(and arg (hash-ref commands arg
(λ () (printf "*** Unknown command: `~s'\n" arg) #f))))
(define (show-cmd cmd indent)
(define names (command-names cmd))
(printf "~a~s" indent (car names))
(when (pair? (cdr names)) (printf " ~s" (cdr names)))
(printf ": ~a\n" (command-blurb cmd)))
(with-wrapped-output
(if cmd
(begin (show-cmd cmd "; ")
(printf "; usage: ,~a" arg)
(let ([a (command-argline cmd)]) (when a (printf " ~a" a)))
(printf "\n")
(for ([d (in-list (command-desc cmd))])
(printf "; ~a\n" d)))
(begin (printf "; Available commands:\n")
(for-each (λ (c) (show-cmd c "; ")) (reverse commands-list))))))
;; ----------------------------------------------------------------------------
;; generic commands
(defcommand (exit quit ex) "[<exit-code>]"
"exit racket"
["Optional argument specifies exit code."]
(cond [(getarg 'sexpr 'opt) => exit] [else (exit)]))
(define last-2dirs
(make-parameter (let ([d (current-directory)]) (cons d d))))
(define (report-directory-change [mode #f])
(define curdir (current-directory))
(define (report) ; remove last "/" and say where we are
(define-values [base name dir?] (split-path curdir))
(printf "; now in ~a\n" (if base (build-path base name) curdir)))
(cond [(not (equal? (car (last-2dirs)) curdir))
(last-2dirs (cons curdir (car (last-2dirs))))
(report)]
[else (case mode
[(pwd) (report)]
[(cd) (printf "; still in the same directory\n")])]))
(defcommand cd "[<path>]"
"change the current directory"
["Sets `current-directory'; expands user paths. With no arguments, goes"
"to your home directory. An argument of `-' indicates the previous"
"directory."]
(let* ([arg (or (getarg 'path 'opt) home-dir)]
[arg (if (equal? arg (string->path "-")) (cdr (last-2dirs)) arg)])
(if (directory-exists? arg)
(begin (current-directory arg) (report-directory-change 'cd))
(eprintf "; cd: no such directory: ~a\n" arg))))
(defcommand pwd #f
"display the current directory"
["Displays the value of `current-directory'."]
(report-directory-change 'pwd))
(defcommand (shell sh ls cp mv rm md rd git svn) "<shell-command>"
"run a shell command"
["`sh' runs a shell command (via `system'), the aliases run a few useful"
"unix commands. (Note: `ls' has some default arguments set.)"
"If the REPL is inside some module's namespace, the command can use $F"
"which is set to the full path to this module's source file."]
(let* ([arg (getarg 'line)]
[arg (if (equal? "" arg) #f arg)]
[cmd (current-command)])
(case cmd
[(ls) (set! cmd "ls -F")]
[(shell) (set! cmd 'sh)])
(let ([cmd (cond [(eq? 'sh cmd) #f]
[(symbol? cmd) (symbol->string cmd)]
[else cmd])]
[here (here-path #f)])
(putenv "F" (if here (path->string here) ""))
(unless (system (cond [(and (not cmd) (not arg)) (getenv "SHELL")]
[(not cmd) arg]
[(not arg) cmd]
[else (string-append cmd " " arg)]))
(eprintf "; (exit with an error status)\n"))
(when here (putenv "F" ""))
(void))))
(defcommand (edit e) "<file> ..."
"edit files in your $EDITOR"
["Runs your $EDITOR with the specified file/s. If no files are given, and"
"the REPL is currently inside a module, the file for that module is used."
"If $EDITOR is not set, the ,drracket will be used instead."]
(define env (let ([e (getenv "EDITOR")]) (and (not (equal? "" e)) e)))
(define exe (and env (find-executable-path env)))
(cond [(not env)
(printf "~a, using the ,drracket command.\n"
(if env
(string-append "$EDITOR ("env") not found in your path")
"no $EDITOR variable"))
(run-command 'drracket)]
[(not (apply system* exe (getarg 'path 'list #:default here-path)))
(eprintf "; (exit with an error status)\n")]
[else (void)]))
(define ->running-dr #f)
(define (->dr . xs) (unless ->running-dr (start-dr)) (->running-dr xs))
(define (start-dr)
(printf "; starting DrRacket...\n")
(define c (make-custodian))
(define ns ((dynamic-require 'racket/gui 'make-gui-namespace)))
(parameterize ([current-custodian c]
[current-namespace ns]
[exit-handler (λ (x)
(eprintf "; DrRacket shutdown.\n")
(set! ->running-dr #f)
(custodian-shutdown-all c))])
;; construct a kind of a fake sandbox to run drracket in
(define es
(eval '(begin (require racket/class racket/gui framework racket/file)
(define es (make-eventspace))
es)))
(define (E expr)
(parameterize ([current-custodian c]
[current-namespace ns]
[(eval 'current-eventspace ns) es])
(eval expr ns)))
(E '(begin
(define c (current-custodian))
(define-syntax-rule (Q expr ...)
(parameterize ([current-eventspace es])
(queue-callback
(λ () (parameterize ([current-custodian c]) expr ...)))))
;; problem: right after we read commands, readline will save a new
;; history in the prefs file which frequently collides with drr; so
;; make it use a writeback thing, with silent failures. (actually,
;; this is more likely a result of previously starting drr wrongly,
;; but keep this anyway.)
(let ([t (make-hasheq)] [dirty '()])
(preferences:low-level-get-preference
(λ (sym [dflt (λ () #f)])
(hash-ref t sym
(λ () (let ([r (get-preference sym dflt)])
(hash-set! t sym r)
r)))))
(preferences:low-level-put-preferences
(λ (prefs vals)
(Q (set! dirty (append prefs dirty))
(for ([pref (in-list prefs)] [val (in-list vals)])
(hash-set! t pref val)))))
(define (flush-prefs)
(set! dirty (remove-duplicates dirty))
(with-handlers ([void void])
(put-preferences dirty (map (λ (p) (hash-ref t p)) dirty))
(set! dirty '())))
(exit:insert-on-callback flush-prefs)
(define (write-loop)
(sleep (random 4))
(when (pair? dirty) (Q (flush-prefs)))
(write-loop))
(define th (thread write-loop))
(exit:insert-on-callback (λ () (Q (kill-thread th)))))
;; start it
(Q (dynamic-require 'drracket #f))
;; hide the first untitled window, so drr runs in "server mode"
(Q (dynamic-require 'drracket/tool-lib #f))
(define top-window
(let ([ch (make-channel)])
(Q (let ([r (get-top-level-windows)])
(channel-put ch (and (pair? r) (car r)))))
(channel-get ch)))
(Q (when top-window (send top-window show #f))
;; and avoid trying to open new windows in there
(send (group:get-the-frame-group) clear))
;; avoid being able to quit so the server stays running,
;; also hack: divert quitting into closing all group frames
(define should-exit? #f)
(exit:insert-can?-callback
(λ () (or should-exit?
(let ([g (group:get-the-frame-group)])
(when (send g can-close-all?) (send g on-close-all))
#f))))
(require drracket/tool-lib))) ; used as usual below
(define (new)
(E '(Q (drracket:unit:open-drscheme-window #f))))
(define open
(case-lambda
[() (E '(Q (handler:open-file)))]
[paths
(let ([paths (map path->string paths)])
(E `(Q (let ([f (drracket:unit:open-drscheme-window ,(car paths))])
(send f show #t)
,@(for/list ([p (in-list (cdr paths))])
`(begin (send f open-in-new-tab ,p)
(send f show #t)))))))]))
(define (quit)
(E `(Q (set! should-exit? #t) (exit:exit))))
(define (loop)
(define m (thread-receive))
(if (pair? m)
(let ([proc (case (car m) [(new) new] [(open) open] [(quit) quit]
[else (cmderror "unknown flag: -~a" (car m))])])
(if (procedure-arity-includes? proc (length (cdr m)))
(apply proc (cdr m))
(cmderror "bad number of arguments for the -~a flag" (car m))))
(error '->dr "internal error"))
(loop))
(define th (thread loop))
(set! ->running-dr (λ (xs) (thread-send th xs)))))
(defcommand (drracket dr drr) "[-flag] <file> ..."
"edit files in DrRacket"
["Runs DrRacket with the specified file/s. If no files are given, and"
"the REPL is currently inside a module, the file for that module is used."
"DrRacket is launched directly, without starting a new subprocess, and it"
"is kept running in a hidden window so further invocations are immediate."
"In addition to file arguments, the arguments can have a flag that"
"specifies one of a few operations for the running DrRacket:"
"* -new: opens a new editing window. This is the default when no files are"
" given and the REPL is not inside a module,"
"* -open: opens the specified file/s (or the current module's file). This"
" is the default when files are given or when inside a module."
"* -quit: exits the running instance. Quitting the application as usual"
" will only close the visible window, but it will still run in a hidden"
" window. This command should not be needed under normal circumstances."]
(define args (getarg 'path 'list #:default here-path))
(if (null? args)
(->dr 'new)
(let* ([cmd (let ([s (path->string (car args))])
(and (regexp-match? #rx"^-" s)
(string->symbol (substring s 1))))]
[args (if cmd (cdr args) args)])
(apply ->dr (or cmd 'open) args))))
;; ----------------------------------------------------------------------------
;; binding related commands
(defcommand (apropos ap) "<search-for> ..."
"look for a binding"
["Additional arguments restrict the shown matches. The search specs can"
"have symbols (which specify what to look for in bound names), and regexps"
"(for more complicated matches)."]
(let* ([look (map (λ (s) (cond [(symbol? s)
(regexp (regexp-quote (symbol->string s)))]
[(regexp? s) s]
[else (cmderror "bad search spec: ~e" s)]))
(getarg 'sexpr 'list))]
[look (and (pair? look)
(λ (str) (andmap (λ (rx) (regexp-match? rx str)) look)))]
[syms (map (λ (sym) (cons sym (symbol->string sym)))
(namespace-mapped-symbols))]
[syms (if look (filter (λ (s) (look (cdr s))) syms) syms)]
[syms (sort syms string<? #:key cdr)]
[syms (map car syms)])
(with-wrapped-output
(if (null? syms)
(printf "; No matches found")
(begin (printf "; Matches: ~s" (car syms))
(for ([s (in-list (cdr syms))]) (printf ", ~s" s))))
(printf ".\n"))))
(defcommand (describe desc id) "[<phase-number>] <identifier-or-module> ..."
"describe a (bound) identifier"
["For a bound identifier, describe where is it coming from; for a known"
"module, describe its imports and exports. You can use this command with"
"several identifiers. An optional numeric argument specifies phase for"
"identifier lookup."]
(define-values [try-mods? level ids/mods]
(let ([xs (getarg 'syntax 'list)])
(if (and (pair? xs) (number? (syntax-e (car xs))))
(values #f (syntax-e (car xs)) (cdr xs))
(values #t 0 xs))))
(with-wrapped-output
(for ([id/mod (in-list ids/mods)])
(define dtm (syntax->datum id/mod))
(define mod (and try-mods? (known-module dtm 'path/sym)))
(define bind
(cond [(identifier? id/mod) (identifier-binding id/mod level)]
[mod #f]
[else (cmderror "not an identifier or a known module: ~s" dtm)]))
(define bind? (or bind (not mod)))
(when bind? (describe-binding dtm bind level))
(when mod
(parameterize (;; without this the reported paths are wrong
[current-load-relative-directory
(and (path? mod)
(let-values ([(base name dir?) (split-path mod)])
(and (path? base) base)))])
(describe-module dtm mod bind?))))))
(define (describe-binding sym b level)
(define at-phase (phase->name level " (~a)"))
(cond
[(not b)
(printf "; `~s' is a toplevel (or unbound) identifier~a\n" sym at-phase)]
[(eq? b 'lexical)
(printf "; `~s' is a lexical identifier~a\n" sym at-phase)]
[(or (not (list? b)) (not (= 7 (length b))))
(cmderror "*** internal error, racket changed ***")]
[else
(define-values [src-mod src-id nominal-src-mod nominal-src-id
src-phase import-phase nominal-export-phase]
(apply values b))
(set! src-mod (->relname (mpi->name src-mod)))
(set! nominal-src-mod (->relname (mpi->name nominal-src-mod)))
(printf "; `~s' is a bound identifier~a,\n" sym at-phase)
(printf "; defined~a in ~a~a\n" (phase->name src-phase "-~a") src-mod
(if (not (eq? sym src-id)) (format " as `~s'" src-id) ""))
(printf "; required~a ~a\n" (phase->name import-phase "-~a")
(if (equal? src-mod nominal-src-mod)
"directly"
(format "through \"~a\"~a"
nominal-src-mod
(if (not (eq? sym nominal-src-id))
(format " where it is defined as `~s'" nominal-src-id)
""))))
(printf "~a" (phase->name nominal-export-phase "; (exported-~a)\n"))]))
(define (describe-module sexpr mod-path/sym also?)
(define get
(if (symbol? mod-path/sym)
(let ([spec `',mod-path/sym])
(λ (imp?) ((if imp? module->imports module->exports) spec)))
(let ([code (get-module-code mod-path/sym)])
(λ (imp?)
((if imp? module-compiled-imports module-compiled-exports) code)))))
(define (phase<? p1 p2)
(cond [(eq? p1 p2) #f]
[(or (eq? p1 0) (not p2)) #t]
[(or (eq? p2 0) (not p1)) #f]
[(and (> p1 0) (> p2 0)) (< p1 p2)]
[(and (< p1 0) (< p2 0)) (> p1 p2)]
[else (> p1 0)]))
(define (modname<? x y)
(cond [(and (string? x) (string? y)) (string<? x y)]
[(and (symbol? x) (symbol? y))
(string<? (symbol->string x) (symbol->string y))]
[(and (symbol? x) (string? y)) #t]
[(and (string? x) (symbol? y)) #f]
[else (error 'describe-module "internal error: ~s, ~s" x y)]))
(define imports
(filter-map
(λ (x)
(and (pair? (cdr x))
(cons (car x) (sort (map (λ (m) (->relname (mpi->name m))) (cdr x))
modname<?))))
(sort (get #t) phase<? #:key car)))
(define-values [val-exports stx-exports]
(let-values ([(vals stxs) (get #f)])
(define (get-directs l)
(filter-map
(λ (x)
(let ([directs (filter-map (λ (b) (and (null? (cadr b)) (car b)))
(cdr x))])
(and (pair? directs) (cons (car x) directs))))
(sort l phase<? #:key car)))
(values (get-directs vals) (get-directs stxs))))
(printf "; `~a' is~a a module,\n" sexpr (if also? " also" ""))
(let ([relname (->relname mod-path/sym)])
(printf "; ~a~a\n"
(if (symbol? relname) "defined directly as '" "located at ")
relname))
(if (null? imports)
(printf "; no imports.\n")
(for ([imps (in-list imports)])
(let ([phase (car imps)] [imps (cdr imps)])
(printf "; imports~a: ~a" (phase->name phase "-~a") (car imps))
(for ([imp (in-list (cdr imps))]) (printf ", ~a" imp))
(printf ".\n"))))
(define (show-exports exports kind)
(for ([exps (in-list exports)])
(let ([phase (car exps)]
[exps (sort (cdr exps) string<? #:key symbol->string)])
(printf "; direct ~a exports~a: ~a"
kind (phase->name phase "-~a") (car exps))
(for ([exp (in-list (cdr exps))]) (printf ", ~a" exp))
(printf ".\n"))))
(if (and (null? val-exports) (null? stx-exports))
(printf "; no direct exports.\n")
(begin (show-exports val-exports "value")
(show-exports stx-exports "syntax"))))
(define help-id (make-lazy-identifier 'help 'racket/help))
(defcommand doc "<any> ..."
"browse the racket documentation"
["Uses Racket's `help' to browse the documentation. (Note that this can be"
"used even in languages that don't have the `help' binding.)"]
(eval-sexpr-for-user `(,(help-id) ,@(getarg 'syntax 'list))))
;; ----------------------------------------------------------------------------
;; require/load commands
(defcommand (require req r) "<require-spec> ...+"
"require a module"
["The arguments are usually passed to `require', unless an argument"
"specifies an existing filename -- in that case, it's like using a"
"\"string\" or a (file \"...\") in `require'. (Note: this does not"
"work in subforms.)"]
(more-inputs #`(require #,@(getarg 'require 'list+)))) ; use *our* `require'
(define rr-modules (make-hash)) ; hash to remember reloadable modules
(define last-rr-modules '())
(defcommand (require-reloadable reqr rr) "<module> ..."
"require a module, make it reloadable"
["Same as ,require but the module is required in a way that makes it"
"possible to reload later. If it was already loaded then it is reloaded."
"Note that this is done by setting `compile-enforce-module-constants' to"
"#f, which prohibits some optimizations."]
(let ([ms (getarg 'module 'list)])
(when (pair? ms) (set! last-rr-modules ms)))
(when (null? last-rr-modules) (cmderror "missing module argument(s)"))
(parameterize ([compile-enforce-module-constants
(compile-enforce-module-constants)])
(compile-enforce-module-constants #f)
(for ([mod (in-list last-rr-modules)])
(define resolved ((current-module-name-resolver) mod #f #f #f))
(define path (resolved-module-path-name resolved))
(define disp (module-displayable-name mod))
(if (hash-ref rr-modules resolved #f)
;; reload
(begin (printf "; reloading ~a\n" disp)
(parameterize ([current-module-declare-name resolved])
(load/use-compiled path)))
;; require
(begin (hash-set! rr-modules resolved #t)
(printf "; requiring ~a\n" disp)
;; (namespace-require mod)
(eval #`(require #,mod)))))))
(define enter!-id (make-lazy-identifier 'enter! 'racket/enter))
(defcommand (enter en) "[<module>] [noisy?]"
"require a module and go into its namespace"
["Uses `enter!' to go into the module's namespace. A module name can"
"specify an existing file as with the ,require command. If no module is"
"given, and the REPL is already in some module's namespace, then `enter!'"
"is used with that module, causing it to reload if needed. (Note that this"
"can be used even in languages that don't have the `enter!' binding.)"]
(eval-sexpr-for-user `(,(enter!-id)
,(getarg 'module #:default here-mod-or-eof)
,@(getarg 'syntax 'list)
#:dont-re-require-enter)))
(defcommand (toplevel top) #f
"go back to the toplevel"
["Go back to the toplevel, same as ,enter with no arguments."]
(eval-sexpr-for-user `(,(enter!-id) #f)))
(defcommand (load ld) "<filename> ..."
"load a file"
["Uses `load' to load the specified file(s)."]
(more-inputs* (map (λ (name) #`(load #,name)) (getarg 'path 'list))))
;; ----------------------------------------------------------------------------
;; debugging commands
;; not useful: catches only escape continuations
;; (define last-break-exn (make-parameter #f))
;; (defcommand (continue cont) #f
;; "continue from a break"
;; ["Continue running from the last break."]
;; (if (last-break-exn)
;; ((exn:break-continuation (last-break-exn)))
;; (cmderror 'continue "no break exception to continue from")))
(define last-backtrace #f)
(defcommand (backtrace bt) #f
"see a backtrace of the last exception"
["Display the last exception with its backtrace."]
(printf "; ~a\n"
(regexp-replace* #rx"\n+" (or last-backtrace "(no backtrace)")
"\n; ")))
(define time-id
(make-lazy-identifier 'time* 'unstable/time))
(defcommand time "[<count>] <expr> ..."
"time an expression"
["Times execution of an expression, similar to `time' but prints a"
"little easier to read information. You can provide an initial number"
"that specifies how many times to run the expression -- in this case,"
"the expression will be executed that many times, extreme results are"
"removed (top and bottom 2/7ths), and the remaining results will be"
"averaged. Two garbage collections are triggered before each run; the"
"resulting value(s) are from the last run."]
(more-inputs #`(#,(time-id) #,@(getarg 'syntax 'list))))
(define trace-id (make-lazy-identifier 'trace 'racket/trace))
(defcommand (trace tr) "<function> ..."
"trace a function"
["Traces a function (or functions), using the `racket/trace' library."]
(eval-sexpr-for-user `(,(trace-id) ,@(getarg 'syntax 'list))))
(define untrace-id (make-lazy-identifier 'untrace 'racket/trace))
(defcommand (untrace untr) "<function> ..."
"untrace a function"
["Untraces functions that were traced with ,trace."]
(eval-sexpr-for-user `(,(untrace-id) ,@(getarg 'syntax 'list))))
(defautoload errortrace
profiling-enabled instrumenting-enabled clear-profile-results
output-profile-results execute-counts-enabled annotate-executed-file)
(defcommand (errortrace errt inst) "[<flag>]"
"errortrace instrumentation control"
["An argument is used to perform a specific operation:"
" + : turn errortrace instrumentation on (effective only for code that is"
" evaluated from now on)"
" - : turn it off (also only for future evaluations)"
" ? : show status without changing it"
"With no arguments, toggles instrumentation."]
(case (getarg 'sexpr 'opt)
[(#f) (if (autoloaded? 'errortrace)
(instrumenting-enabled (not (instrumenting-enabled)))
(instrumenting-enabled #t))]
[(-) (when (autoloaded? 'errortrace) (instrumenting-enabled #f))]
[(+) (instrumenting-enabled #t)]
[(?) (void)]
[else (cmderror "unknown subcommand")])
(if (autoloaded? 'errortrace)
(printf "; errortrace instrumentation is ~a\n"
(if (instrumenting-enabled) "on" "off"))
(printf "; errortrace not loaded\n")))
(define profile-id
(make-lazy-identifier 'profile 'profile))
(define (statistical-profiler)
(more-inputs #`(#,(profile-id) #,(getarg 'syntax))))
(define (errortrace-profiler)
(instrumenting-enabled #t)
(define flags (regexp-replace* #rx"[ \t]+" (getarg 'line) ""))
(for ([cmd (in-string (if (equal? "" flags)
(if (profiling-enabled) "*!" "+")
flags))])
(case cmd
[(#\+) (profiling-enabled #t) (printf "; profiling is on\n")]
[(#\-) (profiling-enabled #f) (printf "; profiling is off\n")]
[(#\*) (output-profile-results #f #t)]
[(#\#) (output-profile-results #f #f)]
[(#\!) (clear-profile-results) (printf "; profiling data cleared\n")]
[else (cmderror "unknown subcommand")])))
(defcommand (profile prof) "[<expr> | <flag> ...]"
"profiler control"
["Runs either the exact errortrace-based profiler, or the statistical one."
"* If a parenthesized expression is given, run the statistical profiler"
" while running it. This profiler requires no special setup and adds"
" almost no overhead, it samples stack traces as execution goes on."
"* Otherwise the errortrace profiler is used. This profiler produces"
" precise results, but like other errortrace uses, it must be enabled"
" before loading the code and it adds noticeable overhead. In this case,"
" an argument is used to determine a specific operation:"
" + : turn the profiler on (effective only for code that is evaluated"
" from now on)"
" - : turn the profiler off (also only for future evaluations)"
" * : show profiling results by time"
" # : show profiling results by counts"
" ! : clear profiling results"
" Multiple flags can be combined, for example \",prof *!-\" will show"
" profiler results, clear them, and turn it off."
"* With no arguments, turns the errortrace profiler on if it's off, and if"
" it's on it shows the collected results and clears them."
"Note: using no arguments or *any* of the flags turns errortrace"
" instrumentation on, even a \",prof -\". Use the ,errortrace command if"
" you want to turn instrumentation off."]
(if (memq (skip-spaces/peek) '(#\( #\[ #\{))
(statistical-profiler)
(errortrace-profiler)))
(defcommand execution-counts "<file> ..."
"execution counts"
["Enable errortrace instrumentation for coverage, require the file(s),"
"display the results, disables coverage, and disables instrumentation if"
"it wasn't previously turned on."]
(let ([files (getarg 'path 'list)]
[inst? (and (autoloaded? 'errortrace) (instrumenting-enabled))])
(more-inputs
(λ ()
(instrumenting-enabled #t)
(execute-counts-enabled #t))
#`(require #,@(map (λ (file) `(file ,(path->string file))) files))
(λ ()
(for ([file (in-list files)])
(annotate-executed-file file " 123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
(λ ()
(execute-counts-enabled #f)
(unless inst? (instrumenting-enabled #f))))))
(defautoload racket/sandbox
make-module-evaluator kill-evaluator call-with-trusted-sandbox-configuration
sandbox-coverage-enabled get-uncovered-expressions)
(defcommand (coverage cover) "<file>"
"coverage information via a sandbox"
["Runs the given file in a (trusted) sandbox, and annotates it with"
"uncovered expression information."]
(let ([file (getarg 'path)])
(sandbox-coverage-enabled) ; autoload it
(parameterize ([sandbox-coverage-enabled #t])
(define e
(call-with-trusted-sandbox-configuration
(λ () (make-module-evaluator file))))
(define uncovered
(map (λ (x) (let ([p (sub1 (syntax-position x))])
(cons p (+ p (syntax-span x)))))
(get-uncovered-expressions e #t)))
(kill-evaluator e)
(call-with-input-file file
(λ (inp)
;; this is a naive and inefficient solution, could be made efficient
;; using `mzlib/integer-set'
(let loop ()
(let* ([start (file-position inp)]
[line (read-line inp)]
[len (and (string? line) (string-length line))]
[end (and len (+ len start))]
[indent (and len (regexp-match-positions #px"\\S" line))]
[indent (and indent (caar indent))])
(when len
(displayln line)
(when indent
(string-fill! line #\space)
(for ([u (in-list uncovered)])
(when (and ((car u) . < . end)
((cdr u) . > . indent))
(for ([i (in-range (max (- (car u) start) indent)
(min (- (cdr u) start) len))])
(string-set! line i #\^))))
(displayln (regexp-replace #rx" +$" line "")))
(loop)))))))))
;; ----------------------------------------------------------------------------
;; namespace switching
(define default-namespace-name '*)
(define current-namespace-name (make-parameter default-namespace-name))
(define namespaces
(let* ([r (namespace-symbol->identifier '#%top-interaction)]
[r (identifier-binding r)]
[r (and r (mpi->name (caddr r)))]
[t (make-hasheq)])
(hash-set! t (current-namespace-name) (cons (current-namespace) r))
t))
(defcommand (switch-namespace switch) "[<name>] [? | - | ! [<init>]]"
"switch to a different repl namespace"
["Switch to the <name> namespace, creating it if needed. The <name> of a"
"namespace is a symbol or an integer; `*' indicates the initial namespace."
"These names are used only by this command, they're not bindings. A new"
"namespace is initialized using the name of the namespace if it names a"
"module, or using the same initial module that was used for the current"
"namespace."
"If `! <init>' is used, the new namespace will be created even if it"
"exists, using `<init>' as the initial module. If `!' is used without an"
"<init> to reset an existing namespace its initial module is used again,"
"and if it is used to create a new namespace, the initial module in current"
"namespace used."
"You can also use `-' and a name to drop the corresponding namespace"
"(allowing it to be garbage-collected), and `?' to list all known"
"namespaces."
"A few examples:"
" ,switch ! reset the current namespace"
" ,switch ! racket reset it using the `racket' language"
" ,switch r5rs switch to a new `r5rs' namespace, initializing it"
" with `r5rs'"
" ,switch foo switch to `foo', creating it if it doesn't exist"
" ,switch foo ! racket switch to newly made `foo', even if it exists"
" ,switch foo ! same, but using the same <init> as it was created"
" with, or as the current namespace if `foo' is new"
" ,switch ? list known namespaces and their initial modules"
" ,switch - r5rs drop the `r5rs' namespace"
"(Note that you can use `^' etc to communicate values between namespaces.)"]
(define (list-namespaces)
(printf "; namespaces and their languages:\n")
(define nss (sort (map (λ (x) (cons (format "~s" (car x)) (cddr x)))
(hash-map namespaces cons))
string<? #:key car))
(define maxlen (apply max (map (λ (x) (string-length (car x))) nss)))
(for ([x (in-list nss)])
(printf "; ~a:~a ~s\n"
(car x)
(make-string (- maxlen (string-length (car x))) #\space)
(cdr x))))
(define (delete name)
(when (eq? name default-namespace-name)
(cmderror "cannot drop the default namespace"))
(when (eq? name (current-namespace-name))
(cmderror "cannot drop the current namespace"))
(unless (hash-ref namespaces name #f)
(cmderror "unknown namespace name: ~s" name))
(hash-remove! namespaces name)
(printf "; namespace dropped: ~s\n" name))
(define (switch name force-reset? init)
(unless (or (not name) (symbol? name) (fixnum? name))
(cmderror "bad namespace name, must be symbol or fixnum"))
(define old-namespace (current-namespace))
;; if there's an <init>, then it must be forced
(let* ([name (or name (current-namespace-name))]
[init
(cond [init]
[(or force-reset? (not (hash-ref namespaces name #f)))
(when (eq? name default-namespace-name)
;; no deep reason for this, but might be usful to keep it
;; possible to ,en xrepl/xrepl to change options etc
(cmderror "cannot reset the default namespace"))
(cdr (or (hash-ref namespaces name #f)
(let ([k (known-module name)]) (and k (cons #f k)))
(hash-ref namespaces (current-namespace-name) #f)
;; just in case
(hash-ref namespaces default-namespace-name #f)))]
[else #f])])
(when init
(printf "; *** ~a `~s' namespace with ~a ***\n"
(if (hash-ref namespaces name #f)
"Resetting the" "Initializing a new")
name
(module-displayable-name init))
(current-namespace (make-base-empty-namespace))
(unless (known-module init)
(parameterize ([current-namespace old-namespace])
(dynamic-require init #f)) ; instantiate it if needed
(namespace-attach-module old-namespace init))
(namespace-require init)
(hash-set! namespaces name (cons (current-namespace) init))))
(when (and name (not (eq? name (current-namespace-name))))
(printf "; *** Switching to the `~s' namespace ***\n" name)
(let ([x (hash-ref namespaces (current-namespace-name))])
(unless (eq? (car x) old-namespace)
(printf "; (note: saving current namespace for `~s')\n"
(current-namespace-name))
(hash-set! namespaces (current-namespace-name)
(cons old-namespace (cdr x)))))
(current-namespace-name name)
(current-namespace (car (hash-ref namespaces name)))))
(define (syntax-error)
(cmderror "syntax error, see \",help ~s\"" (current-command)))
(match (getarg 'sexpr 'list)
[(list) (cmderror "what do you want to do?")]
[(list '?) (list-namespaces)]
[(list '? _ ...) (syntax-error)]
[(list '- name) (delete name)]
[(list '- _ ...) (syntax-error)]
[(list '!) (switch #f #t #f )]
[(list '! init) (switch #f #t init)]
[(list name) (switch name #f #f )]
[(list name '!) (switch name #t #f )]
[(list name '! init) (switch name #t init)]
[_ (syntax-error)]))
;; ----------------------------------------------------------------------------
;; syntax commands
(define current-syntax (make-parameter #f))
(defautoload racket/pretty pretty-write)
(defautoload macro-debugger/stepper-text expand/step-text)
(define not-in-base
(λ () (let ([base-stxs #f])
(unless base-stxs
(set! base-stxs ; all ids that are bound to a syntax in racket/base
(parameterize ([current-namespace hidden-namespace])
(let-values ([(vals stxs) (module->exports 'racket/base)])
(map (λ (s) (namespace-symbol->identifier (car s)))
(cdr (assq 0 stxs)))))))
(λ (id) (not (ormap (λ (s) (free-identifier=? id s)) base-stxs))))))
(define (macro-stepper . args)
(define-values [i o] (make-pipe))
(parameterize ([current-output-port o])
(thread (λ () (apply expand/step-text args) (close-output-port o))))
(let loop ()
(define l (read-line i))
(unless (eof-object? l)
;; hack: beautify the stepper's output -- remove empty line, indent code
(unless (equal? "" l)
(printf (if (regexp-match? #px"^[A-Z][a-z]+\\b" l)
"; ---- ~a ----\n" "; ~a\n")
l))
(loop))))
(defcommand (syntax stx st) "[<expr>] [<flag> ...]"
"set syntax object to inspect, and control it"
["With no arguments, will show the previously set (or expanded) syntax"
"additional arguments serve as an operation to perform:"
"- `^' sets the syntax from the last entered expression"
"- other sexprs set the current syntax explicitly"
"- `+' will `expand-once' the syntax and show the result (can be used again"
" for additional `expand-once' steps)"
"- `!' will `expand' the syntax and show the result"
"- `*' will use the syntax stepper to show expansion steps, leaving macros"
" from racket/base intact (does not change the currently set syntax)"
"- `**' similar to `*', but expanding everything"
"Note that you can specify several syntaxes and operations in a single"
"invocation."]
(define args (getarg 'syntax 'list))
(for ([stx (in-list (if (null? args) '(#f) args))])
(define (show/set label stx)
(printf "; ~a\n" label)
(current-syntax stx)
(display "; ") (pretty-write (syntax->datum stx)))
(define (cur) (or (current-syntax) (cmderror "no syntax set yet")))
(case (and stx (if (identifier? stx) (syntax-e stx) '--none--))
[(#f) (show/set "Current syntax:" (cur))]
[(^) (if (last-input-syntax)
(show/set "Using last expression:" (last-input-syntax))
(cmderror "no expression entered yet"))]
[(+) (show/set "expand-once ->" (expand-once (cur)))]
[(!) (show/set "expand ->" (expand (cur)))]
[(*) (printf "; Stepper:\n") (macro-stepper (cur) (not-in-base))]
[(**) (printf "; Stepper:\n") (macro-stepper (cur))]
[else
(if (syntax? stx)
(begin (printf "; Syntax set\n") (current-syntax stx))
(cmderror "internal error: ~s ~s" stx (syntax? stx)))])))
(defautoload macro-debugger/analysis/check-requires show-requires)
(defcommand (check-requires ckreq) "[<module>]"
"check the `require's of a module"
["Uses `macro-debugger/analysis/check-requires', see the docs for more"
"information."]
(define mod (getarg 'module #:default here-mod-or-eof))
(define rs (show-requires mod))
(with-wrapped-output
(for ([decision (in-list '(keep bypass drop))])
(define all (filter (λ (x) (eq? decision (car x))) rs))
(unless (null? all)
(define names (map cadr all))
;; doesn't print the phase number (third element of all members)
(printf "; ~a: ~a"
(string-titlecase (symbol->string decision)) (car names))
(for ([n (in-list (cdr names))]) (printf ", ~a" n))
(printf ".\n")))))
;; ----------------------------------------------------------------------------
;; dynamic log output control
(define current-log-receiver-thread (make-parameter #f))
(define global-logger (current-logger))
(defcommand log "<level>"
"control log output"
["Starts (or stops) logging events at the given level. The level should be"
"one of the valid racket logging levels, or #f for no logging. For"
"convenience, the level can also be #t (maximum logging) or an integer"
"(with 0 for no logging, and larger numbers for more logging output)."]
(define levels '(#f fatal error warning info debug))
(define level
(let ([l (getarg 'sexpr)])
(cond [(memq l levels) l]
[(memq l '(#f none -)) #f]
[(memq l '(#t all +)) (last levels)]
[(not (integer? l))
(cmderror "bad level, expecting one of: ~s" levels)]
[(<= l 0) #f]
[(< l (length levels)) (list-ref levels l)]
[else (last levels)])))
(cond [(current-log-receiver-thread) => kill-thread])
(when level
(let ([r (make-log-receiver global-logger level)])
(current-log-receiver-thread
(thread
(λ ()
(let loop ()
(match (sync r)
[(vector l m v name)
(display (format "; [~a] ~a~a\n"
l m (if v (format " ~.s" v) "")))
(flush-output)])
(loop))))))))
;; ----------------------------------------------------------------------------
;; meta evaluation hook
;; questionable value, (and need to display the resulting values etc)
#;
(defcommand meta "<expr>"
"meta evaluation"
["Evaluate the given expression where bindings are taken from the xrepl"
"module. This is convenient when you're in a namespace that does not have"
"a specific binding -- for example, you might be using a language that"
"doesn't have `current-namespace', so to get it, you can use"
"`,eval (current-namespace)'. The evaluation happens in the repl namespace"
"as usual, only the bindings are taken from the xrepl module -- so you can"
"use `^' to refer to the result of such an evaluation."]
(eval (datum->syntax #'here `(#%top-interaction . ,(getarg 'sexpr))))
(void))
;; ----------------------------------------------------------------------------
;; setup xrepl in the user's racketrc file
(define init-file (find-system-path 'init-file))
(defcommand install! #f
"install xrepl in your Racket init file"
["Installs xrepl in your Racket REPL initialization file. This is done"
"carefully: I will tell you about the change, and ask for permission."
"You can then edit the file if you want to; in your system, you can find it"
,(format "at \"~a\"." init-file)]
(define comment "The following line loads `xrepl' support")
(define expr "(require xrepl)")
(define dexpr "(dynamic-require 'xrepl #f)")
(define contents (if (file-exists? init-file) (file->string init-file) ""))
;; discard the newline for further input
(let loop () (when (byte-ready?) (read-byte)))
(define (look-for comment-rx expr)
(let ([m (regexp-match-positions
(format "(?<=\r?\n|^) *;+ *~a *\r?\n *~a *(?=\r?\n|$)"
comment-rx (regexp-quote expr))
contents)])
(and m (car m))))
(define existing? (look-for (regexp-quote comment) expr))
(define existing-readline?
(look-for "load readline support[^\r\n]*" "(require readline/rep)"))
(define (yes? question)
(define qtext (string->bytes/utf-8 (format "; ~a? " question)))
(define inp
(case (object-name (current-input-port))
[(readline-input)
(parameterize ([(dynamic-require
(collection-file-path "pread.rkt" "readline")
'readline-prompt)
qtext])
(read-line))]
[else (write-bytes qtext) (flush-output) (read-line)]))
(and (string? inp) (regexp-match? #px"^[[:space:]]*[yY]" inp)))
(cond
[existing?
(printf "; already installed, nothing to do\n")
(when existing-readline?
(printf "; (better to remove the readline loading, xrepl does that)"))]
[(let ([m (regexp-match
(string-append (regexp-quote expr) "|" (regexp-quote dexpr))
contents)])
(and m (begin (printf "; found \"~a\", ~a\n"
(car m) "looks like xrepl is already installed")
(not (yes? "should I continue anyway")))))]
[else
(when existing-readline?
(printf "; found a `readline' loading line\n")
(if (yes? "xrepl will already do that, ok to remove")
(set! contents (string-append
(substring contents 0 (car existing-readline?))
(substring contents (cdr existing-readline?))))
(printf "; it will be kept ~a\n"
"(you can edit the file and removing it later)")))
(printf "; writing new contents, with an added \"~a\"\n" expr)
(printf "; (if you want to load it conditionally, edit the file and\n")
(printf "; use \"~a\" instead, which is a plain expression)\n" dexpr)
(if (yes? "OK to continue")
(begin
(call-with-output-file* init-file #:exists 'truncate
(λ (o) (define new (regexp-replace #rx"(?:\r?\n)+$" contents ""))
(write-string new o)
(unless (equal? "" new) (write-string "\n\n" o))
(fprintf o ";; ~a\n~a\n" comment expr)))
(printf "; new contents written to ~a\n" init-file))
(printf "; ~a was not updated\n" init-file))])
(void))
;; ----------------------------------------------------------------------------
;; eval hook that keep track of recent evaluation results
;; saved interaction values (can be #f to disable saving)
(define saved-values (make-parameter '()))
(define (save-values! xs)
(let* ([xs (filter (λ (x) (not (void? x))) xs)] ; don't save void values
[xs (map (λ (x) (and x (make-weak-box x))) xs)]) ; save weakly
(unless (null? xs)
;; the order is last, 2nd-to-last, ..., same from prev interactions
;; the idea is that `^', `^^', etc refer to the values as displayed
(saved-values (append (reverse xs) (saved-values)))
(let ([n (saved-values-number)] [l (saved-values)])
(when (< n (length l)) (saved-values (take l n)))))))
(define last-saved-names+state (make-parameter '(#f #f #f)))
(define (get-saved-names)
(define last (last-saved-names+state))
(define last-num (cadr last))
(define last-ptrns (caddr last))
(define cur-num (saved-values-number))
(define cur-ptrns (saved-values-patterns))
(if (and (equal? last-num cur-num) (equal? last-ptrns cur-ptrns))
(car last)
(let ([new
(for*/list ([i (in-range 1 (add1 (saved-values-number)))]
[p (in-list cur-ptrns)])
(string->symbol
(cond
[(= 1 (string-length p)) (make-string i (string-ref p 0))]
[(regexp-match? #rx"^[^~]*~a[^~]*$" p) (format p i)]
[else (error 'saved-names "bad name pattern: ~e" p)])))])
(last-saved-names+state (list new cur-num cur-ptrns))
new)))
;; see comment at the top of this module for the below hair
(require xrepl/saved-values)
;; make saved values available through bindings, but avoid names that
;; already exist in the namespace (possibly from a previous initialization)
(define (initialize-namespace)
;; We might run into circularity problems, give up silently in that case
(when (with-handlers ([exn? (λ (_) #f)])
(namespace-attach-module (here-namespace) 'xrepl/saved-values)
(dynamic-require 'xrepl/saved-values (void))
#t)
;; Hack: wire in our parameter for expansions (see comment in saved-values)
(eval-sexpr-for-user `(,#'set-saved-values-param! ,saved-values))
(for ([sym (in-list (get-saved-names))])
(define id (namespace-symbol->identifier sym))
(unless (identifier-binding id)
(eval-sexpr-for-user
`(,#'require (,#'only-in ,#'xrepl/saved-values
[,#'saved-value-ref ,id])))))))
(require (for-syntax racket/base))
(define ((make-xrepl-evaluator orig) expr)
;; not useful: catches only escape continuations
;; (with-handlers ([exn:break? (λ (e) (last-break-exn e) (raise e))]) ...)
(if (saved-values)
(let ([results (call-with-values (λ () (orig expr)) list)])
(save-values! results)
(apply values results))
(orig expr)))
;; ----------------------------------------------------------------------------
;; capture ",..." and run the commands, use readline/rep when possible
(define get-prefix ; to show before the "> " prompt
(let ()
(define (get-prefix)
(let* ([x (here-source)]
[x (and x (module-displayable-name (if (symbol? x) `',x x)))]
[x (or x (toplevel-prefix))]
[x (let ([ph (namespace-base-phase)])
(if (eq? 0 ph) x (format "~a[~a]" x ph)))])
(if (eq? (current-namespace-name) default-namespace-name)
x (format "~a::~a" (current-namespace-name) x))))
(define last-directory #f)
(define last-namespace #f)
(define prefix #f)
(λ ()
(define curdir (current-directory))
(unless (and (equal? (current-namespace) last-namespace)
(equal? curdir last-directory))
(report-directory-change)
(initialize-namespace)
(set! prefix
(with-handlers
([exn? (λ (e)
(eprintf "; error during prompt calculation: ~a\n"
(exn-message e))
"[internal-error]")])
(get-prefix)))
(set! last-namespace (current-namespace))
(set! last-directory curdir))
prefix)))
;; the last non-command expression read
(define last-input-syntax (make-parameter #f))
(struct more-inputs (list)
#:constructor-name more-inputs* #:omit-define-syntaxes)
(define (more-inputs . inputs) (more-inputs* inputs))
(define (make-xrepl-reader orig)
(define (plain-reader prefix) ; a plain reader, without readline
(display prefix) (display "> ") (flush-output) (zero-column!)
(let ([in ((current-get-interaction-input-port))])
((current-read-interaction) (object-name in) in)))
(define RL ; no direct dependency on readline
(with-handlers ([exn? (λ (_) #f)])
(collection-file-path "pread.rkt" "readline")))
(define (make-readline-reader)
(let ([p (dynamic-require RL 'current-prompt)]
[r (dynamic-require RL 'read-cmdline-syntax)])
(λ (prefix) ; uses the readline prompt
(parameterize ([p (bytes-append (string->bytes/locale prefix) (p))])
(r)))))
(define reader
(case (object-name (current-input-port))
[(stdin)
(if (or (not (terminal-port? (current-input-port)))
(eq? 'windows (system-type))
(regexp-match? #rx"^dumb" (or (getenv "TERM") ""))
(not RL))
plain-reader
(with-handlers ([exn?
(λ (e)
(eprintf "; Warning: no readline support (~a)\n"
(exn-message e))
plain-reader)])
(dynamic-require 'readline/rep-start #f)
;; requiring readline should have changed the reader
(if (eq? (current-prompt-read)
(dynamic-require RL 'read-cmdline-syntax))
(make-readline-reader)
(begin (eprintf "; Warning: could not initialize readline\n")
plain-reader))))]
[(readline-input)
(eprintf "; Note: readline already loaded\n~a\n"
"; (better to let xrepl load it for you)")
(make-readline-reader)]
[else plain-reader]))
;; IO management
(port-count-lines! (current-input-port))
;; wrap the reader to get the command functionality
(define more-inputs '())
(define (reader-loop)
(parameterize ([saved-values #f])
(define from-queue? (pair? more-inputs))
(define input
(if from-queue?
(begin0 (car more-inputs) (set! more-inputs (cdr more-inputs)))
(begin (fresh-line) (reader (get-prefix)))))
(syntax-case input ()
[(uq cmd) (eq? 'unquote (syntax-e #'uq))
(let ([r (run-command (syntax->datum #'cmd))])
(cond [(void? r) (reader-loop)]
[(more-inputs? r)
(set! more-inputs (append (more-inputs-list r) more-inputs))
(reader-loop)]
[else (eprintf "; Warning: internal weirdness: ~s\n" r) r]))]
[_ (begin (unless from-queue? (last-input-syntax input)) input)])))
reader-loop)
;; ----------------------------------------------------------------------------
;; a display handler that omits stacktraces (making them available later)
(define ((make-xrepl-display-handler orig) str exn)
(fresh-line #t)
(define backtrace?
(parameterize ([current-error-port (open-output-string)])
(orig str exn)
(let* ([s (get-output-string (current-error-port))]
[s (regexp-replace* #rx"^\n+|\n+$" s "")]
[s (regexp-replace* #rx"\n\n+" s "\n")])
;; temporary hack: this is always on since it shows all fields,
;; so ",bt" is now really a generic "more info"
(and ; (not (equal? str s))
(begin (set! last-backtrace s) #t)))))
(define msg "[,bt for context]")
(parameterize ([current-output-port (current-error-port)])
(let* ([s (regexp-replace* #rx"^\n+|\n+$" str "")]
[s (regexp-replace* #rx"\n\n+" s "\n")]
[s (regexp-replace* #rx"\n [^\n]+\\.\\.\\.:(?:[^\n]+|\n )+" s "")]
[s (regexp-replace* #rx"\n" s "\n; ")]
[s (if backtrace?
(string-append s (if (regexp-match? #rx"\n" s) "\n; " " ") msg)
s)])
(with-wrapped-output (printf "; ~a\n" s)))))
;; ----------------------------------------------------------------------------
;; set up the xrepl environment
(provide setup-xrepl-environment)
(define (setup-xrepl-environment)
(define (tweak param maker) (param (maker (param))))
(tweak error-display-handler make-xrepl-display-handler)
(tweak current-eval make-xrepl-evaluator)
(tweak current-prompt-read make-xrepl-reader))