<< Back to previous view

[NREPL-53] Middleware linearization is nondeterministically wrong Created: 02/May/14  Updated: 30/Sep/14

Status: Open
Project: tools.nrepl
Component/s: None
Affects Version/s: 0.2.3
Fix Version/s: None

Type: Defect Priority: Critical
Reporter: Gary Fredericks Assignee: Chas Emerick
Resolution: Unresolved Votes: 0
Labels: None

clojure 1.6.0


I've had trouble getting my middleware ordered correctly, and have wittled it down to the following example which seems to only fail in some processes. I.e., it is deterministic within a particular jvm, but across jvms the result can change. My guess is that this has to do with hashing vars.

This code prints true (signifying a correct result) roughly 1/3 of the time and false otherwise.

(ns chas.core
  (:require [clojure.tools.nrepl.middleware :as mid]

(def wrap-foo identity)
(mid/set-descriptor! #'wrap-foo
                     {:expects #{#'clojure.tools.nrepl.middleware.session/session "eval"}})

(def wrap-bar identity)
(mid/set-descriptor! #'wrap-bar
                     {:requires #{#'clojure.tools.nrepl.middleware.session/session},
                      :expects #{"describe" #'clojure.tools.nrepl.middleware.pr-values/pr-values},
                      :handles {"stacktrace" {}}})

(defn -main
   ;; should return true, since #'wrap-foo belongs later in the stack than
   ;; #'clojure.tools.nrepl.middleware.session/session due to its :expects
   ;; clause
   (->> [#'wrap-foo
        (filter #{#'clojure.tools.nrepl.middleware.session/session #'wrap-foo})
        (= [#'clojure.tools.nrepl.middleware.session/session #'wrap-foo]))))

Comment by Gary Fredericks [ 02/May/14 10:35 PM ]

Interesting followup observation I made with Alan Malloy in IRC: if we remove "eval" from the :expects clause, it seems to return consistently true. On the other hand if we instead add #'clojure.tools.nrepl.middleware.interruptible-eval/interruptible-eval to the middleware list, suddenly it returns consistently false.

Comment by Gary Trakhman [ 04/Jun/14 3:35 PM ]

I should add my own experience report with this, hope to dig in to it someday soon: https://groups.google.com/d/msg/clojure/nUBBbYZHuTE/ScLBH-A2HkoJ

In my case, it was incorrectness, not indeterminism.

Comment by Chas Emerick [ 19/Aug/14 1:59 PM ]

Half of the middleware linearization stuff is too clever, the other half is really dumb. :-/

I'd love a patch for this. Absent one, I'll have to dig into it, but I'm not going to hold up 0.2.4 any further for that opportunity to come around.

Comment by Gary Fredericks [ 19/Aug/14 2:12 PM ]

Is this a fair summary of the requirements?

1) Should be deterministic
2) Should only return a result that respects the :expects and :requires clauses – if this is impossible it should throw an exception explaining why

Comment by Chas Emerick [ 19/Aug/14 2:58 PM ]

Yes, that's fair. I might add (3) try to not break anything significant that might rely upon undocumented behaviour, but I doubt that's helpful.

Comment by Gary Fredericks [ 25/Sep/14 9:29 PM ]

I've written generative tests for this, but test.check requires clojure 1.4 (while nrepl tests run on 1.2).

If I wrap them in code that NOOPs for old clojures, I imagine they will they get run in CI? Is that a reasonable approach?

Comment by Colin Jones [ 26/Sep/14 7:50 AM ]

That approach sounds great to me.

Comment by Gary Fredericks [ 26/Sep/14 10:06 AM ]

Given I've coded it that way, is there an easy way for me to run the tests without tweaking the pom.xml to specify 1.4?

Comment by Gary Fredericks [ 30/Sep/14 10:11 PM ]

Okay I think I finally tracked this down.

Basically the conj-sorted function was not a valid way of doing a topological sort.

I replaced it with an actual topological sort, that still pushes independent middlewares toward the end.

The generative tests are now passing, as are the rest of the tests.

At some point I'll circle back to my original problem and see if that's solved.

I still don't fully understand all of the linearization code, so it's possible that the problem is more subtle than this.

The fix is on a github branch here. I might want to wait on another release of test.check before making a patch, since it should have some new features that make the generative tests better (in particular I'd be able to remove the dependency on my test.chuck utility library, which obviously shouldn't be in the tools.nrepl codebase).

[NREPL-59] Tracking source form positions in eval Created: 19/Jun/14  Updated: 30/Sep/14

Status: Open
Project: tools.nrepl
Component/s: None
Affects Version/s: 0.2.3
Fix Version/s: None

Type: Enhancement Priority: Major
Reporter: Bozhidar Batsov Assignee: Chas Emerick
Resolution: Unresolved Votes: 0
Labels: enhancement


Dev tools writers using nREPL's eval op face the following problem - using the `eval` op you cannot pass information regarding the filename and the file position of the form you're evaluating and therefore afterwards you can't use find its definition. Because of this most CIDER users reload their source buffers all the time with `load-file`. Another implication of this problem is that exception backtraces have meaningless information about the position of the problem (usually something like `NO_FILE:1`). I've noticed that LightTable has a custom middleware for this, ritz used to have one as well, probably others too. Now I'm contemplating doing something similar for CIDER, but it seems to me this is basic functionality that should be supported out-of-the-box.

I'd like to propose that the default eval middleware be augmented to support some form of source form tracking to spare tool writers from having to reinvent the wheel.

Comment by Chas Emerick [ 19/Aug/14 1:50 PM ]

I agree that this should be baked in. `:line` and `:column` are some optional slots eval could accept (but should not require), with the obvious effect on the reader passed along to the compiler. This may be trickier than it sounds, especially if you're aiming to keep things at parity for both Clojure and ClojureScript (as code to be eval'd is read well before it hits interruptible_eval, in piggieback).

In any case, patch quite welcome.

Comment by Bozhidar Batsov [ 22/Aug/14 2:57 PM ]

`:filepath` should be another optional slot. I don't do much ClojureScript (and I don't know much about piggieback) so getting something working for Clojure would be a good enough start for me. I'll try to cook some patch soon.

Comment by Bozhidar Batsov [ 03/Sep/14 9:58 AM ]

Chas, can you give me some pointers about how to approach the task. I don't think we can use `clojure.main/repl` for this and I'm not sure what makes sense and what doesn't. One implementation of the idea can be found here https://github.com/pallet/ritz/blob/develop/nrepl-middleware/src/ritz/nrepl/middleware/tracking_eval.clj but I'm not sure whether it's good or bad.

Seems to me there should be a simpler way to provide a context for the evaluation, but I'm far from an expert in the area.

Comment by Chas Emerick [ 03/Sep/14 12:45 PM ]

Now I remember what I was planning the last time I thought about it: the easiest way AFAICT would be to use load-file, and simply pad out the top-level form being evaluated. So if you had this file:

(ns foo.bar
  (:require clojure.set))

(defn x [] 5)

    (defn y [] 6)

and the user loads `y`, you'd send this in a load-file message with appropriate file path information:

(ns foo.bar)

    (defn y [] 6)

load-file turns this into a call like (clojure.lang.Compiler/load "(ns foo.bar)\n\n\n\n\n (defn y [] 6)" "foo/bar.clj" "bar.clj". That doesn't change *ns*, and gives you proper metadata.

I've only skimmed Ritz's implementation, but it does a lot of clever things, and is tied to Clojure. Perhaps there's reasons why my (perhaps hacky?) approach isn't suitable in certain situations, but it should be pretty future-proof, and should work transparently with ClojureScript/piggieback. I'd rather dummy up "files" to be loaded than overriding LineNumberingPushbackReader method impls, etc.

Comment by Bozhidar Batsov [ 04/Sep/14 8:07 AM ]

Your idea sounds great to me. I guess the only question is whether the clients should take care of this or nREPL when passed extra arguments to the eval op.

Comment by Chas Emerick [ 04/Sep/14 10:08 AM ]

All of the source-file-aware stuff is in load-file, so I'm inclined to put the burden on the client. Even if an op accepted the necessary info, the client would still need to provide it (target namespace + line and column information); going from there to a dummied-up ns form and the corresponding whitespace is so straightforward, I'd rather not complicate the op interface(s).

Comment by Bozhidar Batsov [ 04/Sep/14 10:15 AM ]

Roger that! I've just implemented this in cider and it works great. I'll be closing this. Thanks for all your help!

Comment by Bozhidar Batsov [ 04/Sep/14 10:27 AM ]

P.S. You should probably mention this technique in the README for posterity's sake.

Comment by Bozhidar Batsov [ 04/Sep/14 10:27 AM ]

P.S. You should probably mention this technique in the README for posterity's sake.

Comment by Chas Emerick [ 04/Sep/14 10:33 AM ]

True. Really, the repo should have a separate file for implementors, as there's a lot of stuff in the source and tribal knowledge that isn't documented anywhere....

Comment by Bozhidar Batsov [ 09/Sep/14 9:17 AM ]

Btw, looking at this ticket https://github.com/clojure-emacs/cider/issues/781 I'm not sure we can use just (ns ns-name) at the start of the dummy files as it seems this overrides the namespace definition. Perhaps we should always insert the complete ns declaration from the source file in question? Or is there some other smart trick I'm not aware of that'll help avoid this ns clobbering.

Comment by Chas Emerick [ 09/Sep/14 2:00 PM ]

load-file only changes *ns* for the duration of the loading process. If it clobbered *ns* otherwise, then loading entire files would change it in the REPL (which doesn't happen)…

Comment by Bozhidar Batsov [ 10/Sep/14 9:28 AM ]

Are we talking about the same thing? My concern is that swapping:

(ns foo
   (:require [bar :as b]))


(ns foo)

changes the ns definition (not the ns itself) in a manner in which you're no longer referring to bar as b anymore.

Comment by Vitalie Spinu [ 16/Sep/14 4:36 AM ]

Another solution is to allow the `eval` middleware to accept a token that will be appended to the temp file that eval uses for evaluation. Then the client can split the file path and retrive the metadata back. You can even allow for a full path of a temp file as an argument. This should be trivial to implement.

Comment by Bozhidar Batsov [ 16/Sep/14 4:45 AM ]

The problem with the solution you propose is that *eval* is implemented in terms of Clojure's built *clojure.main/repl* and we obviously can't change it to suit our needs. Don't know about you, but I definitely don't want *eval* to have to be implemented in terms of some custom evaluation logic developed specially for nREPL.

The current solution works well enough and requires zero middleware changes. The only question is does the temp file need the complete ns declaration of the original source file or not.

Comment by Vitalie Spinu [ 16/Sep/14 4:56 AM ]

I have just looked at the code and it looks like you are right. I am now confused. Where do "tmp/form-init34343" files come from? I thought they are handled in nREPL.

Comment by Bozhidar Batsov [ 16/Sep/14 5:00 AM ]

It comes from Clojure itself (or rather its built-in REPL evaluation). These tmp filenames appeared in Clojure 1.6. In older Clojure releases the var metadata had just *NO_FILE* for *:file*.

Comment by Chas Emerick [ 16/Sep/14 8:03 AM ]

ns declarations are strictly additive AFAICT:

user=> (ns foo (:require [clojure.set :as set]))
foo=> set/intersection
#<set$intersection clojure.set$intersection@3e4db6a>
foo=> (ns foo)
foo=> set/intersection
#<set$intersection clojure.set$intersection@3e4db6a>

And using load-file, which is effectively what the load-file op duplicates:

chas@t440p:~$ cat > foo.clj
(ns foo (:require [clojure.set :as set]))
chas@t440p:~$ cat > foo2.clj
(ns foo)
chas@t440p:~$ lein repl
nREPL server started on port 40749 on host - nrepl://

user=> (load-file "foo.clj")
user=> (in-ns 'foo)
#<Namespace foo>
foo=> set/intersection
#<set$intersection clojure.set$intersection@2fcecc1>
foo=> (in-ns 'user)
#<Namespace user>
user=> (load-file "foo2.clj")
user=> (in-ns 'foo)
#<Namespace foo>
foo=> set/intersection
#<set$intersection clojure.set$intersection@2fcecc1>

The load-file op appears to work properly outside of this use case as well, since I can (from cider) remove a :require clause from a file, reload it, and its alias remains in place.

If you seem to need to include the full ns declaration, then something else is amiss somewhere...

Comment by Bozhidar Batsov [ 16/Sep/14 9:51 AM ]

I couldn't reproduce the problem some people are reporting, so I'm inclined to believe you. I've just merged a patch that uses the full ns declarations all the time, but I may revert it in light of your comment. I wonder if this behaviour changes in different Clojure versions.

Comment by Chas Emerick [ 16/Sep/14 9:58 AM ]

Using the full ns declaration isn't necessarily bad, but there is the edge case of people who add :reload and/or :reload-all options to their :require, etc clauses. In such cases, sending a single top-level expr to be loaded will then cause other namespaces to be reloaded, perhaps with side effects (if those other namespaces have impolite top-level forms themselves).

Re: version differences, I'm pretty sure that's not an issue either:

chas@t440p:~/dev/pdfts$ java -cp ~/Downloads/clojure-1.0.0.jar clojure.main
Clojure 1.0.0-
user=> *clojure-version*
{:major 1, :minor 0, :incremental 0, :qualifier ""}
user=> (load-file "foo.clj")
user=> (in-ns 'foo)
#<Namespace foo>
foo=> set/intersection
#<set$intersection__5909 clojure.set$intersection__5909@5b0abc94>
foo=> (in-ns 'user)
#<Namespace user>
user=> (load-file "foo2.clj")
user=> (in-ns 'foo)
#<Namespace foo>
foo=> set/intersection
#<set$intersection__5909 clojure.set$intersection__5909@5b0abc94>

I could believe that there's some bug/oddity with the implementation of the load-file op in this case, but the underlying mechanism is stable and behaves non-destructively.

Comment by Bozhidar Batsov [ 22/Sep/14 12:22 AM ]

So, we've finally tracked down the problem - load-file returns for "ns" either "user" or ":main" for some reason, regardless of the namespace of the file you're loading. This sounds like a bug to me; I'll find another ticket.

Comment by Bozhidar Batsov [ 30/Sep/14 11:20 AM ]

We found one more related problem. Seems that load-file in piggieback discards the return value of the evaluation. More details can be found here. I think the behaviour for Clojure and ClojureScript load-file should be unified.

Generated at Tue Sep 30 23:04:23 CDT 2014 using JIRA 4.4#649-r158309.