anmonteiro / lumo

Fast, cross-platform, standalone ClojureScript environment
Eclipse Public License 1.0
1.88k stars 84 forks source link

Eval broken in a script after initial tick #296

Open dundalek opened 6 years ago

dundalek commented 6 years ago

Hi, I am experimenting with enhancing a cljs repl and I need to pass the user input through a custom function to modify behavior. I am trying to leverage Lumo for that. I am using lumo.core.eval in a script and am running into a strange behavior. Here is a distilled down repro case repro.cljs:

(ns repro.core
  (:require [cljs.reader]
            [lumo.core]))

(defn my-eval []
  (-> "(+ 2 3)"
      (cljs.reader/read-string)
      (lumo.core/eval)
      (pr-str)
      (js/console.log)))

(my-eval)
(js/setTimeout my-eval 1)

I run it with lumo repro.cljs and expect this output:

5
5

Actual output in Lumo 1.8.0-beta (on Ubuntu Linux 16.04):

5
<embedded>:6453
lumo.repl.eval=function(a){switch(arguments.length){case 1:return lumo.repl.eval.cljs$core$IFn$_invoke$arity$1(arguments[0]);case 2:return lumo.repl.eval.cljs$core$IFn$_invoke$arity$2(arguments[0],arguments[1]);default:throw Error(["Invalid arity: ",cljs.core.str.cljs$core$IFn$_invoke$arity$1(arguments.length)].join(""));}};lumo.repl.eval.cljs$core$IFn$_invoke$arity$1=function(a){return lumo.repl.eval.cljs$core$IFn$_invoke$arity$2(a,cljs.core._STAR_ns_STAR_.name)};
                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ^

TypeError: Cannot read property 'name' of null
    at Function.lumo.repl.eval.cljs$core$IFn$_invoke$arity$1 (<embedded>:6453:462)
    at lumo.core.eval (<embedded>:6576:214)
    at Timeout.repro$core$my_eval [as _onTimeout] (evalmachine.<anonymous>:3:62)
    at ontimeout (timers.js:469:11)
    at tryOnTimeout (timers.js:304:5)
    at Timer.listOnTimeout (timers.js:264:5)

The first invocation works but the second one which is deferred throws. Seems like a bug but maybe I just need to set up things differently?

anmonteiro commented 6 years ago

Thanks for the report. The issue seems to be coming from here, if you wanna dig deeper: https://github.com/anmonteiro/lumo/blob/dc2f4f7fa21bec272b4d3c50e8b1d7a71a8d450c/src/cljs/snapshot/lumo/repl.cljs#L860

dundalek commented 6 years ago

Awesome, thanks for the directions @anmonteiro. I did some digging and found out that some bindings were getting missing. Then I noticed the execute functions did re-binding in every call so I tried out following workaround to capture the values and rebind them:

(ns repro.core
  (:require [cljs.reader]
            [lumo.repl]
            [cljs.env :as env]
            [cljs.js :as cljs]))

(def ns *ns*)
(def compiler env/*compiler*)
(def eval-fn cljs/*eval-fn*)
(def load-fn cljs/*load-fn*)

(defn my-eval []
  (binding [cljs/*eval-fn* eval-fn
            cljs/*load-fn* load-fn]
    (-> "(+ 2 3)"
        (cljs.reader/read-string)
        (lumo.repl/eval ns compiler)
        (pr-str)
        (js/console.log))))

(my-eval)
(js/setTimeout my-eval 1)

Now it works as expected. I assume that fix would be to do something similar inside repl/eval function.

anmonteiro commented 6 years ago

I'm not certain it would work if we do the same inside the lumo.repl/eval function. I'm pretty sure the bindings are lost in the async setTimeout call, so they wouldn't be there anyway.

arichiardi commented 6 years ago

Long time ago @darwin came up with a library for solving this exact problem. I cannot find it now but I will link it if I do.

dundalek commented 6 years ago

I did some more digging today and found out that lumo.repl/execute-text works even inside setTimeout:

(ns repro.core
  (:require [cljs.reader]
            [lumo.repl]))

(defn my-eval []
  (-> "(+ 2 3)"
      (lumo.repl/execute-text {:expression? true})))

(my-eval)
(js/setTimeout my-eval 1)

It seems the difference is that it first sets the state with set-session-state-for-session-id! and then sets the necessary bindings.

arichiardi commented 6 years ago

I don't know if still relevant, but the project is https://github.com/binaryage/cljs-zones and it basically does exactly that: save context somewhere before giving up control and then restoring it back afterwards.