Skip to content

Commit

Permalink
nrepl: try to understand interruptible-eval magic
Browse files Browse the repository at this point in the history
  • Loading branch information
darwin committed Sep 26, 2016
1 parent 51dd41d commit 448cace
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 36 deletions.
40 changes: 24 additions & 16 deletions src/nrepl/dirac/nrepl/piggieback.clj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
cljs.repl
[cljs.env :as env]
[cljs.analyzer :as ana]
[dirac.nrepl.state :refer [*cljs-repl-env* *cljs-compiler-env* *cljs-repl-options* *original-clj-ns*]]
[dirac.nrepl.state :as state]
[dirac.nrepl.driver :as driver]
[dirac.nrepl.version :refer [version]]
[dirac.nrepl.sessions :as sessions]
Expand Down Expand Up @@ -164,7 +164,7 @@
; environment now (instead of attempting to create one to
; begin with, because we can't reliably replicate what
; cljs.repl/repl* does in terms of options munging
(set! *cljs-compiler-env* env/*compiler*)
(set! state/*cljs-compiler-env* env/*compiler*)
; if the CLJS evaluated result is nil, then we can assume
; what was evaluated was a cljs.repl special fn (e.g. in-ns,
; require, etc)
Expand Down Expand Up @@ -203,18 +203,19 @@
Accepts all options usually accepted by e.g. cljs.repl/repl."
[repl-env & {:as options}]
; TODO I think we need a var to set! the compiler environment from the REPL environment after each eval
(log/trace "start-cljs-repl! call stack: " (helpers/get-printed-stack-trace))
(try
(let [init-code (nrepl/code (ns cljs.user
(:require [cljs.repl :refer-macros (source doc find-doc apropos dir pst)])))
nrepl-message (assoc nrepl-ieval/*msg* ::first-cljs-repl true)] ; initial cljs-repl-iteration is hadnled specially
(set! ana/*cljs-ns* 'cljs.user)
(run-single-cljs-repl-iteration nrepl-message init-code repl-env nil options) ; this will implicitly set! *cljs-compiler-env*
(set! *cljs-repl-env* repl-env)
(set! *cljs-repl-options* options)
(set! *original-clj-ns* *ns*) ; interruptible-eval is in charge of emitting the final :ns response in this context
(set! state/*cljs-repl-env* repl-env)
(set! state/*cljs-repl-options* options)
(set! state/*original-clj-ns* *ns*) ; interruptible-eval is in charge of emitting the final :ns response in this context
(set! *ns* (find-ns ana/*cljs-ns*)))
(catch Exception e
(set! *cljs-repl-env* nil)
(set! state/*cljs-repl-env* nil)
(throw e))))

;; mostly a copy/paste from interruptible-eval
Expand All @@ -239,28 +240,29 @@
; Clojure session (dynamic environment) is not in place, so we need to go
; through the `session` atom to access/update its vars. Same goes for load-file.
(defn evaluate! [nrepl-message]
(log/trace "evaluate! call stack: " (helpers/get-printed-stack-trace))
(let [{:keys [session transport ^String code]} nrepl-message]
; we append a :cljs/quit to every chunk of code evaluated so we can break out of cljs.repl/repl*'s loop,
; so we need to go a gnarly little stringy check here to catch any actual user-supplied exit
(if-not (.. code trim (endsWith ":cljs/quit"))
(let [repl-env (@session #'*cljs-repl-env*)
compiler-env (@session #'*cljs-compiler-env*)
options (@session #'*cljs-repl-options*)]
(let [repl-env (@session #'state/*cljs-repl-env*)
compiler-env (@session #'state/*cljs-compiler-env*)
options (@session #'state/*cljs-repl-options*)]
(run-single-cljs-repl-iteration nrepl-message code repl-env compiler-env options))
(let [actual-repl-env (@session #'*cljs-repl-env*)]
(let [actual-repl-env (@session #'state/*cljs-repl-env*)]
(reset! (:cached-setup actual-repl-env) :tear-down) ; TODO: find a better way
(cljs.repl/-tear-down actual-repl-env)
(sessions/remove-dirac-session-descriptor! session)
(swap! session assoc
#'*ns* (@session #'*original-clj-ns*)
#'*cljs-repl-env* nil
#'*cljs-compiler-env* nil
#'*cljs-repl-options* nil
#'*ns* (@session #'state/*original-clj-ns*)
#'state/*cljs-repl-env* nil
#'state/*cljs-compiler-env* nil
#'state/*cljs-repl-options* nil
#'ana/*cljs-ns* 'cljs.user)
(transport/send transport (response-for nrepl-message
:value "nil"
:printed-value 1
:ns (str (@session #'*original-clj-ns*))))))))
:ns (str (@session #'state/*original-clj-ns*))))))))

; struggled for too long trying to interface directly with cljs.repl/load-file,
; so just mocking a "regular" load-file call
Expand Down Expand Up @@ -559,10 +561,16 @@

(defn dirac-nrepl-middleware [next-handler]
(fn [nrepl-message]
; we are a middleware which is expected to be called in the context of clojure.tools.nrepl.middleware.interruptible-eval
; interruptible-eval does binding swapping specified by vars in sesssion
; on first call we install our extra bindings and let interruptible-eval store/restore it for us
; long story short: with each future invocation our dynamic vars in state namespace will be bound to values relevant to
; the session at hand
(sessions/install-bindings-if-needed! (:session nrepl-message))
(let [nrepl-message (logged-nrepl-message nrepl-message)]
(log/debug "dirac-nrepl-middleware:" (:op nrepl-message) (sessions/get-session-id (:session nrepl-message)))
(log/trace "received nrepl message:\n" (pprint nrepl-message))
(sessions/ensure-bindings! (:session nrepl-message))
(log/trace "dirac-nrepl-middleware call stack: " (helpers/get-printed-stack-trace))
(cond
(dirac-special-command? nrepl-message) (handle-dirac-special-command! nrepl-message)
(is-eval-cljs-quit-in-joined-session? nrepl-message) (issue-dirac-special-command! nrepl-message ":disjoin")
Expand Down
38 changes: 18 additions & 20 deletions src/nrepl/dirac/nrepl/sessions.clj
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
(ns dirac.nrepl.sessions
(:require [clojure.tools.logging :as log]
[cljs.analyzer :as ana]
[cljs.analyzer :as analyzer]
[dirac.logging :as logging]
[dirac.nrepl.state :refer [session-descriptors]]
[dirac.nrepl.state :refer [*cljs-repl-env*
*cljs-compiler-env*
*cljs-repl-options*
*original-clj-ns*]]
[dirac.nrepl.state :as state]
[clojure.tools.nrepl.middleware.interruptible-eval :as nrepl-ieval]
[dirac.backport.string :as backport-string]))

Expand All @@ -16,22 +12,24 @@
(-> session meta :id))

(defn dirac-session? [session]
(boolean (@session #'*cljs-repl-env*)))
(boolean (@session #'state/*cljs-repl-env*)))

(defn get-current-session []
{:post [%]}
(:session nrepl-ieval/*msg*))

; -- bindings magic ---------------------------------------------------------------------------------------------------------

(defn ensure-bindings! [session]
; ensure that bindings exist so cljs-repl can set!
(if-not (contains? @session #'*cljs-repl-env*)
(swap! session (partial merge {#'*cljs-repl-env* *cljs-repl-env*
#'*cljs-compiler-env* *cljs-compiler-env*
#'*cljs-repl-options* *cljs-repl-options*
#'*original-clj-ns* *original-clj-ns*
#'ana/*cljs-ns* ana/*cljs-ns*}))))
(defn install-bindings-if-needed! [session]
; this magic here expects interruptible-eval bindings jugggling
; https://github.com/clojure/tools.nrepl/blob/fcac1e13d6f80eb0db28773247a769c1dc9659b1/src/main/clojure/clojure/tools/nrepl/middleware/interruptible_eval.clj#L85
(when-not (contains? @session #'state/*cljs-repl-env*)
(log/debug "installing session bindings")
(swap! session (partial merge {#'state/*cljs-repl-env* state/*cljs-repl-env*
#'state/*cljs-compiler-env* state/*cljs-compiler-env*
#'state/*cljs-repl-options* state/*cljs-repl-options*
#'state/*original-clj-ns* state/*original-clj-ns*
#'analyzer/*cljs-ns* analyzer/*cljs-ns*}))))

; -- dirac sessions management ----------------------------------------------------------------------------------------------

Expand All @@ -41,7 +39,7 @@
:tag tag})

(defn find-dirac-session-descriptor [session]
(some #(if (= (:session %) session) %) @session-descriptors))
(some #(if (= (:session %) session) %) @state/session-descriptors))

(defn get-dirac-session-descriptor-transport [session-descriptor]
(:transport session-descriptor))
Expand All @@ -57,18 +55,18 @@
(log/trace transport (logging/pprint session))
(if-not (find-dirac-session-descriptor session)
(let [session-descriptor (make-dirac-session-descriptor session transport tag)]
(swap! session-descriptors concat (list session-descriptor)))
(swap! state/session-descriptors concat (list session-descriptor)))
(log/error "attempt to add duplicit session descriptor:\n" (logging/pprint session))))

(defn remove-dirac-session-descriptor! [session]
(log/debug "remove-dirac-session-descriptor!" (get-session-id session))
(log/trace (logging/pprint session))
(if-let [session-descriptor (find-dirac-session-descriptor session)]
(swap! session-descriptors #(remove #{session-descriptor} %))
(swap! state/session-descriptors #(remove #{session-descriptor} %))
(log/error "attempt to remove unknown session descriptor:\n" (logging/pprint session))))

(defn find-matching-dirac-session-descriptors [matcher]
(let [descriptors @session-descriptors
(let [descriptors @state/session-descriptors
descriptors-count (count descriptors)
match-result (fn [index descriptor]
(if (matcher descriptor index descriptors-count) descriptor))]
Expand All @@ -91,7 +89,7 @@
(prepare-dirac-session-descriptor-tag (find-dirac-session-descriptor session)))

(defn get-dirac-session-tags []
(get-dirac-session-descriptors-tags @session-descriptors))
(get-dirac-session-descriptors-tags @state/session-descriptors))

; -- joining sessions -------------------------------------------------------------------------------------------------------

Expand Down
2 changes: 2 additions & 0 deletions src/nrepl/dirac/nrepl/state.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

; we cannot pass nrepl-message info into all our functions,
; so we keep some global state around and various functions touch it at will
; also note that these get re-bound with each new nrepl message by clojure.tools.nrepl.middleware.interruptible-eval
; and each new middleware invocation has their own session-specific version

(def ^:dynamic *cljs-repl-env* nil) ; this is the var that is checked by the middleware to determine whether an active CLJS REPL is in flight
(def ^:dynamic *cljs-compiler-env* nil)
Expand Down

0 comments on commit 448cace

Please sign in to comment.