emacs.d/elpa/racket-mode-20200115.2148/racket/namespace.rkt
2020-01-29 18:18:31 +01:00

270 lines
11 KiB
Racket

#lang at-exp racket/base
(require racket/contract
racket/file
racket/format
racket/function
racket/list
racket/match
syntax/modread
racket/path
syntax/parse
syntax/strip-context
syntax/stx
(only-in "error.rkt" display-exn)
"mod.rkt"
(only-in "util.rkt" display-commented))
(provide dynamic-require/some-namespace)
;; A composition of dynamic-require and module->namespace that tries
;; to tolerate syntax errors. It tries to return a namespace with at
;; least some identifiers from the file -- such as from module
;; languages, requires, and definitions.
;;
;; Motivation:
;;
;; https://github.com/greghendershott/racket-mode/issues/272
;;
;; You're working in #lang racket/base. You're partway through writing
;; a some expression, and realize you need to add (say)
;; with-module-reading-parameterization. You add syntax/modread to
;; your require.
;;
;; Now, you want to type with-m and hit TAB to complete. Plus after
;; that, you might want to C-. a.k.a. M-x racket-describe to read
;; docs.
;;
;; But you need to re-run, first, for the new require to take effect
;; and make the syntax/modread exports available.
;;
;; But if you re-run, your half-written expression results in a syntax
;; or runtime error. Now your REPL is just an empty racket/base.
;;
;; Annoying!
;;
;; Strategy: When dynamic-require fails, try again using a custom load
;; handler that rewrites the file -- "distill" it to a skeleton of
;; module forms, requires, and define-values. Try again using that.
;;
;; Note that it's important for the skeleton to include submodules,
;; because racket-mode lets you "enter" a submodule and work with
;; identifiers inside it (and only inside it).
(define is-skeleton
"[Due to errors, REPL is just module language, requires, and stub definitions]")
(define is-base
"[Due to errors, REPL is just racket/base]")
;; A composition of dynamic-require and module->namespace, but which
;; tries to tolerate errors in the source file and return _some_
;; namespace more useful than racket/base (if possible).
(define/contract (dynamic-require/some-namespace mod retry-as-skeletal?)
(-> mod? boolean? namespace?)
(parameterize ([current-load-relative-directory (mod-dir mod)]
[current-directory (mod-dir mod)])
(cond [(normal mod) => values]
[(and retry-as-skeletal?
(skeletal mod)) => (λ (ns)
(display-commented is-skeleton)
ns)]
[else (display-commented is-base)
(make-base-namespace)])))
(define/contract (normal mod)
(-> mod? (or/c #f namespace?))
(with-handlers ([exn:fail? (λ (e) (display-exn e) #f)])
(dynamic-require (mod-rmp mod) #f)
(module->namespace (mod-rmp mod))))
(define/contract (skeletal mod)
(-> mod? (or/c #f namespace?))
(with-handlers ([exn:fail? (const #f)]) ;don't show errors again
(parameterize ([current-load (make-load mod)]
;; Module is cached in old namespace, so for `load`
;; to be called, we need a fresh namespace.
[current-namespace (make-base-namespace)])
(dynamic-require (mod-rmp mod) #f)
(module->namespace (mod-rmp mod)))))
(define/contract (make-load mod)
(-> mod? any)
(define original-load (current-load))
(define special-path (build-path (mod-dir mod) (mod-file mod)))
(λ (path module-name)
(if (equal? path special-path)
(eval (skeleton (read-module-file path)))
(original-load path module-name))))
(define (read-module-file file) ;Path-String -> Syntax
(with-module-reading-parameterization
(λ ()
(parameterize ([read-accept-compiled #f])
(with-input-from-file file read-syntax)))))
(define no-op-expr #'(void))
(define no-op-def-val #''|Due to errors in source file, this value is from a "stub" define-values|)
(define (skeleton stx) ;Syntax -> Syntax
;; We got here because `stx` has either a syntax error or a runtime
;; error. If it has a syntax error, we can't `expand` it as whole.
;; Let's try to distill it to a skeleton of things that create
;; runtime, module-level bidings: requires and defines.
;;
;; To get #%require and define-values, we want to work with
;; fully-expanded syntax as much as possible. But we have to catch
;; syntax errors and replace each with #'(void). Also we want to
;; walk submodule forms for their bindings, but we can't expand a
;; submodule forms in isolation (that's a syntax error).
;;
;; So, the idea is to preserve the nested modules skeleton, and only
;; try to expand each of their module-level expressions to discover
;; bindings.
;;
;; Our final result should, as a whole, work with (eval (expand)).
(strip-context
;; Unlike expand-syntax-to-top-form, expand-to-top-form does
;; namespace-syntax-introduce before expanding to top form.
(let recur ([stx (expand-to-top-form stx)])
(syntax-parse stx
#:literal-sets (kernel-literals)
#:datum-literals (#%module-begin module+)
;; Note: A #lang file has #%module-begin even on initial read
;; and without calling `expand`. However, a (module) expression
;; file -- even when using with-module-reading-parameterization
;; -- doesn't. That only gets added by `expand`. But we can't
;; use `expand`. Anyway, it hardly matters as we're going to
;; remove everything interesting that a #%module-begin might
;; transform (IIUC). Just treat #%module-begin as begin.
[((~and mod (~or module module*)) name:id lang:expr . es)
#`(mod name lang . #,(stx-map recur #'es))]
[(#%module-begin . es)
#`(begin . #,(stx-map recur #'es))]
[(module+ name:id . es)
#`(module+ name . #,(stx-map recur #'es))]
[_
(let ([stx (with-handlers ([exn:fail:syntax? (const no-op-expr)])
(expand stx))])
(syntax-parse stx
#:literal-sets (kernel-literals)
[(begin . es) #`(begin . #,(stx-map recur #'es))]
[(#%require . _) stx]
[(define-values (id ...) . _) #`(define-values (id ...)
(values
#,@(stx-map (const no-op-def-val)
#'(id ...))))]
[_ no-op-expr]))]))))
(module+ test
(require rackunit
racket/set
version/utils)
;; A example of the transformation we do.
;;
;; Note: Prior to Racket 6.3, expansion of `require` with
;; non-existent modules seems to be a syntax error. So in this test,
;; use modules that actually exist in minimal Racket.
(check-equal? (syntax->datum
(skeleton
#'(module m racket/base
(#%module-begin
(require racket/pretty
racket/list)
(if) ;stx err
(/ 1 0) ;runtime err
(define foo 42)
(define-values (bar baz) (values 43 44))
(define (f x) (+ x 1))
(module* m #f
(require net/url)
(if) ;stx err
(/ 1 0)) ;runtime err
(module+ test
(require rackunit)
(if)) ;stx err
(module m typed/racket/base
(#%module-begin
(require racket/function)
(define id 42)
(if))))))) ;stx err
(let ([no-op-expr (syntax->datum no-op-expr)]
[no-op-def-val (syntax->datum no-op-def-val)])
`(module m racket/base
(begin
(begin (#%require racket/pretty) (#%require racket/list))
,no-op-expr
,no-op-expr
(define-values (foo) (values ,no-op-def-val))
(define-values (bar baz) (values ,no-op-def-val ,no-op-def-val))
(define-values (f) (values ,no-op-def-val))
(module* m #f
(#%require net/url)
(void)
(void))
(module+ test
(#%require rackunit)
,no-op-expr)
(module m typed/racket/base
(begin
(#%require racket/function)
(define-values (id) (values ,no-op-def-val))
,no-op-expr))))))
;; Helpers to write text or sexpr to a tempory .rkt file, then run
;; through dynamic-require/some-namespace and get the
;; namespace-mapped-symbols.
(define/contract (call-with-temporary-file v proc)
(-> any/c (-> mod? any/c) any/c)
(define file #f)
(dynamic-wind
(λ ()
(set! file (make-temporary-file "call-with-temporary-file-~a.rkt"))
(call-with-output-file file #:exists 'replace
(λ (out)
(cond [(string? v) (display v out)]
[else (write v out)]))))
(λ () (proc (->mod/existing file)))
(λ () (delete-file file))))
(define/contract (syms mod)
(-> mod? (listof symbol?))
(namespace-mapped-symbols
(parameterize ([current-namespace (make-base-empty-namespace)])
(dynamic-require/some-namespace mod #t))))
(define (do v)
(define op (open-output-string))
(define result (parameterize ([current-error-port op])
(call-with-temporary-file v syms)))
(check-match (get-output-string op)
(regexp (string-append (regexp-quote is-skeleton) "\n$")))
result)
;; Despite a syntax error and a runtime error, a binding provided by
;; a require is available in the namespace in both:
;; (a) A #lang file:
(check-not-false
(memq 'pretty-print (do @~a{#lang racket/base
(if)
(require racket/pretty)})))
;; (b) A module expression file:
(check-not-false
(memq 'pretty-print (do `(module m racket/base
(if)
(require racket/pretty)))))
;; Requiring exactly 1 binding adds exactly that symbol to the
;; namespace:
(check-equal? (set-subtract
(list->set
(do `(module m racket/base
(/ 1 0)
(require (only-in racket/pretty pretty-print)))))
(list->set
(do `(module n racket/base
(/ 1 0)))))
(set 'pretty-print)))