mauricioszabo / atom-chlorine

An Atom plugin to integrate with Socket-REPL over Clojure, ClojureScript, ClojureCLR, Joker, Babashka, Clojerl, Lumo and Plank
MIT License
176 stars 17 forks source link

Add support for using *1, *2, *3, and *e #229

Closed hovsater closed 3 years ago

hovsater commented 3 years ago

When you evaluate a block, it would be nice to be able to use *1, *2 and *3 and *e respectively to reference return values and the last exception.

Taken from the Clojure docs:

Several special vars are available when using the REPL:

1, 2, 3 - hold the result of the last three expressions that were evaluated e - holds the result of the last exception.

It seems to be supported by most REPLs.

(+ 1 1) ; When evaluated returns 2
(println *1) ; When evaluated prints 2 and returns nil (given the prior form was evaluated first).

I'd be happy to contribute a small amount to make this happen if you're interested in pursuing it. 🙂

mauricioszabo commented 3 years ago

No, it's not really possible. A single "eval top block" can issue one to three commands to the REPL, so the value of these vars are not bound to last evaluation. Also, there's no *e on most REPLs - most exceptions are captured and serialized for Chlorine to capture...

I'll probably add an entry on the FAQ regarding these vars :)

hovsater commented 3 years ago

@mauricioszabo I see, that makes sense. 🙂 So am I right in assuming Chlorine does one-off evaluations to the Socket REPL instead of having an ongoing connection to it and that's why it's really not technically possible to achieve that?

For instance, doing echo '(+ 1 1) (println "The value is" *1)' | nc 0 5555 seems to print the correct output:

$ echo '(+ 1 1) (println "The value is" *1)' | nc 0 5555
user=> 2
The value is 2
nil

but I'm not familiar with how Chlorine actually send the blocks when you evaluate them, so perhaps, this isn't feasible.

mauricioszabo commented 3 years ago

No, it does have an ongoing connection. The problem is that when you evaluate anything, Chlorine tries to detect:

  1. Does this editor have a filename associated to it?
  2. Does it have a ns form?

If 1 is true, it'll send a command to set the filename (binding *1 to it); If 2 is true, it'll send a command to set the namespace (again, binding *1) Then, it'll send the eval command (once again, binding *1).

Now, if 1 and 2 are true (which they are, most of the time), everytime you send an evaluation you'll have *1 and *2 bound to nil and the current namespace (that's what the REPL returns when you set a filename and namespace). This means that "original *1" (the one that contains the evaluation result) is now *3.

You can check this yourself by trying to eval something, then evaluating *3. You'll see that it indeed contains the result of last eval; and that *1 probably contains nil, and *2 contains an instance to clojure.lang.Namespace

image

mauricioszabo commented 3 years ago

BTW, a simple correction: Chlorine will try to detect the ns FIRST then the filename. The reason is that when it detects the filename it also sets the right row, so if we send the in-ns command first it'll mess the current row

hovsater commented 3 years ago

I see. That makes sense. So, it "works" but not in the way you would expect, making it less useful? I tried out the example and it worked just like you said it would. However, the example below didn't set *1, *2 and *3 to anything, which made me a bit confused again.

Screenshot 2020-12-29 at 13 20 49

I assume *1, *2 and *3 are set to nil due to what you described earlier, but given I have no filename and no namespace, what commands are being run before the actual block I'm trying to evaluate?

EDIT:

Nvm, I just realised that evaluating the actual vars will obviously change the value of them again 🤦 My bad! This makes sense. Using the vars will be highly indeterministic, which make them less useful.

mauricioszabo commented 3 years ago

Do you have a ns form on this editor? This could explain why *2 is bound to nothing.

As for the nil in *3, it's probably because you eval'ed *1 first (then making it nil). Also, you don't have a filename but Chlorine will still try to set the current row, binding it to nil

The current code that does handle evals in Chlorine starts here: https://github.com/mauricioszabo/repl-tooling/blob/master/src/repl_tooling/repl_client/clojure.cljs#L95-L96

and goes to here: https://github.com/mauricioszabo/repl-tooling/blob/master/src/repl_tooling/repl_client/clojure.cljs#L40-L48

hovsater commented 3 years ago

Thanks for taking the time to explain this so throughly. It's really appreciated. 🙂

hovsater commented 3 years ago

Sorry for bringing this up again but when I woke up this morning I realised I didn't fully understand what you meant yesterday.

Given that you said

If 1 is true, it'll send a command to set the filename (binding 1 to it); If 2 is true, it'll send a command to set the namespace (again, binding 1) Then, it'll send the eval command (once again, binding *1).

So *1 will always point to last expression evaluated.

Now, if 1 and 2 are true (which they are, most of the time), everytime you send an evaluation you'll have 1 and 2 bound to nil and the current namespace (that's what the REPL returns when you set a filename and namespace). This means that "original 1" (the one that contains the evaluation result) is now 3.

So how come *1 isn't the value of whatever was being evaluated? Are there expressions being evaluated after our own code has been evaluated? My understanding was that our own expression was the last thing being evaluated.

mauricioszabo commented 3 years ago

So how come *1 isn't the value of whatever was being evaluated

It is - but the problem is that when you evaluate something in Chlorine, it'll send other commands before your eval. So it'll be re-bind to the result of previous command - for example, the "set filename and/or row"

hovsater commented 3 years ago

@mauricioszabo perhaps I misunderstand how the REPL bind the dynamic variables, but anything before my eval shouldn't be a problem since *1 is bound to the last result. I'm sorry if I'm asking a lot of questions, just trying to wrap my head around this.

mauricioszabo commented 3 years ago

Ok, so the thing is: if you don't have a ns form on your editor, Chlorine will try to run a code to set the current row. Let's suppose this code is called (set-row <some-number>). So, if you're on the line 20 of the editor, this is what will be run:

; Evaling (+ 1 2)
(set-row 19) ; Binds *1 to nil

(do
(+ 1 2)); Binds *2 to nil, and *1 to 3

Now, on the second evaluation, if you eval *1:

(set-row 19) ; Binds *1 to nil, *2 to 3, and *3 to nil

(do
*1) ; *1 is nil, so it'll bind *1 to nil, *2 to nil, and *3 to 3

set-row is called with 19 because of the do form

hovsater commented 3 years ago

@mauricioszabo thanks a lot, I finally get it! 🎉 Really appreciate you taking the time to explain this in detail. 🙂