Fork me on GitHub
#sci
<
2021-01-07
>
Sam Ritchie17:01:55

Might have found an interesting diff in sci from clj to cljs…

Sam Ritchie17:01:03

(defn eval [form]
  (sci/eval-form (sci/fork es/context) form))

(is (= o/identity-operator
         (eval
          '(do (require '[sicmutils.operator :as o])
               o/identity-operator)))
      "can sci internally require namespaces?")

Sam Ritchie17:01:46

in clj, that that passes; in cljs it fails, since the eval form returns a var, not a resolved var

borkdude17:01:55

@sritchie09 I'm having dinner right now, but feel free to post an issue with the desired outcome

Sam Ritchie18:01:23

(whoops, this was from an error of mine 🙂 I was fiddling around and accidentally removed my own deref on the value)

Sam Ritchie18:01:37

ah, okay, this is more the error I was anticipating with eval:

Sam Ritchie18:01:40

(let [m {:bindings {'make (fn [form] (cljs.core/eval form))}}]
  (sci/eval-form (sci/init m) '(make `(fn [x#] (* x# 12)))))

borkdude18:01:50

@sritchie09 What is the error?

Sam Ritchie18:01:28

Execution error (Error) at (<cljs repl>:1).
cljs.core/*eval* not bound

Sam Ritchie18:01:01

another discovery (as you hint at and document in the README): if I want to use dynamic variables to toggle behavior (like certain simplifier paths, or a compile mode), then instead of a Clojure dynamic variable:

(def ^:dynamic *mode* :sci)
I can use
(def mode
  (sci/new-dynamic-var '*mode* :sci))
which will work both in and out of the SCI environment

borkdude18:01:29

yes, that will work in both envs, but in the outside env you need to deref explicitly

borkdude18:01:54

cljs.core/*eval* not bound
this is a CLJS problem, not a sci problem

borkdude18:01:25

you care calling cljs.core/eval here, not something inside sci

Sam Ritchie18:01:26

that is bound in self-hosted cljs only

Sam Ritchie18:01:44

yeah, that’s right. can I define a function outside of sci that, when invoked inside of sci, will call sci’s eval?

borkdude18:01:00

@sritchie09 you can just call eval inside sci

Sam Ritchie18:01:01

oh… I guess I can bind the actual “eval” to the dynamic variable

Sam Ritchie18:01:08

bind it inside sci

borkdude18:01:20

that's not needed, it already works inside sci without config

Sam Ritchie18:01:28

@borkdude the problem is I don’t want the user to do that, this is a few levels deeper than what the user would interact with

Sam Ritchie18:01:36

they are calling (compile-fn f)

borkdude18:01:49

so you want to forbid eval ?

borkdude18:01:10

you can either do this through :deny or just override clojure.core/eval with something else

Sam Ritchie18:01:18

no, sorry, I want that to be fine and possible - but I want the user to be able to toggle a compilation mode inside SCI, either “sci” or “eval” mode

Sam Ritchie18:01:32

or, what I actually wanted to do was make ‘eval’ the default inside of sci

Sam Ritchie18:01:42

`*mode*` is dynamic here

Sam Ritchie18:01:15

and then compile-native calls eval internally

Sam Ritchie18:01:36

but compile-native is defined outside of sci

Sam Ritchie18:01:19

so it captures cljs.core/eval… what would be excellent is if I could write this in some way that eval resolved to the sci eval inside of sci

borkdude18:01:29

ok, so you want eval to be bound to either sci eval or clojure eval - right?

borkdude18:01:36

depending on the mode

Sam Ritchie18:01:54

this latter snippet has the code for both “modes”

Sam Ritchie18:01:22

“sci mode” is a little different from what you just stated (sorry I’m being confusing here!) “sci mode” means, “use sci/eval-form “. “native mode” means, “call eval”. the reason this distinction matters is that for sci/eval-form, I provide a context with function substitutions - #(Math/sin %) for sin, etc - and with eval, I have to do the postwalk-replace manually before calling eval

borkdude18:01:33

what you can do is (sci/eval-string ctx "eval"), this will get you the eval function inside sci

borkdude18:01:52

but the way you implemented it here is pretty much the same I think

Sam Ritchie18:01:30

this is what I had meant yesterday about “calling sci from inside sci”… I see now that I am technically not calling sci INSIDE sci, I am bringing in some external fn that, externally, used SCI

Sam Ritchie18:01:02

so probably I should just not worry, since the behavior will be identical. But what I HAVE learned here is that I should be defining any dynamic variables the SCI Way, if I want them usable outside and inside SCI.

borkdude18:01:28

that's true, sci by design doesn't want to have anything to do with Clojure vars

Sam Ritchie18:01:34

I wonder if, as a style thing, I should just NOT import any dynamic variable now, so they’re not even available in SCI-mode of sicmutils

Sam Ritchie18:01:01

since they’re not useful if they can’t be rebound (that is until I convert them in the way we’ve described)

borkdude18:01:25

Clojure itself has dynamic vars that aren't usually re-bound by the user, if that's what you mean?

borkdude18:01:38

e.g. *1 etc are usually only set by tooling, not by the user

Sam Ritchie18:01:57

oh, interesting, let me check if I can do that

borkdude18:01:58

so that's ok, I guess

Sam Ritchie18:01:24

(defn rebound [f]
  (binding [*mode* :testing]
    (f)))

borkdude18:01:55

if you want them to be re-bound by the user, you will have to invoke (sci/eval-form ...) within a (sci/binding [the-sci-dyn-var 12] ...)

borkdude18:01:44

ahh, I think I see your problem. you want a dynvar that can be re-bound in both clojure and sci expressions?

Sam Ritchie18:01:25

yeah, I want the user to, at a min, be able to look at the var and see its binding

Sam Ritchie18:01:41

if I DON’T resolve the dynamic var when I declare the sci context this works great

Sam Ritchie18:01:50

(eval
 '(do (require '[sicmutils.expression.compile :as c])
      (c/rebound (fn [] @c/*mode*))))

borkdude18:01:50

that can be accomplished by exposing the implementation detail using a function

Sam Ritchie18:01:57

yup, that is a better design

borkdude18:01:51

what I sometimes also do is have both a clojure and sci var and synchronize them "just in time" in the places where it matters

borkdude18:01:40

e.g. if I want to include Clojure functions that depend on *out*, I include them in sci as:

(fn [arg] (binding [*out* @sci/out] (clojure-function arg))

Sam Ritchie18:01:34

(defn sci-ns
  "Given a map of symbol => var, returns a map of symbol => var with:

  - any pair removed whose value is a macro (tagged with `:macro true` metadata)
  - all other values resolved"
  [sym->var]
  (letfn [(process [[sym var]]
            (cond
              ;; Inside SCI, macros are replaced by rewritten-as-functions
              ;; versions of themselves, with additional slots for `&form` and
              ;; `&env`.
              (macro? var)
              (if-let [sci-macro (macros/all sym)]
                [[sym sci-macro]]
                [])

              ;; Keep dynamic variables as unresolved vars, so that they can
              ;; at least be inspected (at which point they'll reveal any
              ;; rebindings applied by the system)
              (dynamic? var) [[sym var]]

              ;; by default, the SCI environment holds values, not the vars
              ;; that they were attached to in non-SCI land.
              :else [[sym @var]]))]
    (into {} (mapcat process) sym->var)))

Sam Ritchie18:01:22

this was my final thing. {'sicmutils.env (sci-ns (ns-bindings 'sicmutils.env))} is a good value for :namespaces

borkdude19:01:39

so (dynamic? var) [[sym var]] includes a normal Clojure var inside the sci namespaces?

Sam Ritchie19:01:47

which will then change its binding, if an external thing rebinds it

Sam Ritchie19:01:09

ie deref-ing it inside sci reflects changes from functions that do stuff like this:

(defn rebound [f]
  (binding [*mode* :testing]
    (f)))

Sam Ritchie19:01:22

is that a no-no?

borkdude19:01:51

The reason sci doesn't want to have anything to do with Clojure vars, is that creating vars pollutes the global runtime. I'm not sure if sci works properly when you include Clojure vars. Function calls work, because vars implement IFn, but they work differently when just using them as values.

borkdude19:01:18

But if it works, it works. I've never tried this

Sam Ritchie19:01:07

this is for inspection-only, and still a little weird (since it’s a thing you have to deref to inspect)…

Sam Ritchie19:01:18

the better solution is to expose any modifications to dynamic vars via fns

borkdude19:01:35

Functions are nice, but if you do want to use dynvar, this is how I would do it:

$ clj
Clojure 1.10.1
user=> (def ^:dynamic *dyn-var* 12)
#'user/*dyn-var*
user=> (binding [*dyn-var* 13] *dyn-var*)
13
user=> (def sci-dyn-var (sci/copy-var *dyn-var* (sci/create-ns 'user)))
#'user/sci-dyn-var
user=> (sci/binding [sci-dyn-var *dyn-var*] (sci/eval-string "*dyn-var*" {:bindings {'*dyn-var* sci-dyn-var}}))
12
user=> (sci/binding [sci-dyn-var *dyn-var*] (sci/eval-string "(binding [*dyn-var* 13] *dyn-var*)" {:bindings {'*dyn-var* sci-dyn-var}}))
13

borkdude19:01:30

the downside of this is that *dyn-var* and sci-dyn-var need manual syncing

borkdude19:01:50

if you need to for example read the var in a function that is used both in and out sci, you will probably re-define that function using another (clojure.core/binding []) around it in the sci config, to sync back the value set by the user