sci

https://github.com/babashka/SCI - also see #babashka and #nbb
Sam Ritchie 2021-01-07T17:37:55.222Z

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

Sam Ritchie 2021-01-07T17:38:03.222300Z

(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 Ritchie 2021-01-07T17:38:09.222500Z

@borkdude I had this test -

Sam Ritchie 2021-01-07T17:38:46.223200Z

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

borkdude 2021-01-07T17:39:55.223600Z

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

Sam Ritchie 2021-01-07T17:42:10.223800Z

sg!

Sam Ritchie 2021-01-07T18:03:23.224500Z

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

Sam Ritchie 2021-01-07T18:25:37.224900Z

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

Sam Ritchie 2021-01-07T18:25:40.225200Z

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

borkdude 2021-01-07T18:31:50.226400Z

@sritchie09 What is the error?

Sam Ritchie 2021-01-07T18:34:28.226700Z

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

Sam Ritchie 2021-01-07T18:38:01.228600Z

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

borkdude 2021-01-07T18:39:29.229100Z

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

borkdude 2021-01-07T18:39:54.229400Z

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

borkdude 2021-01-07T18:40:25.229900Z

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

Sam Ritchie 2021-01-07T18:40:26.230Z

that is bound in self-hosted cljs only

Sam Ritchie 2021-01-07T18:40:44.230400Z

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

borkdude 2021-01-07T18:41:00.230900Z

@sritchie09 you can just call eval inside sci

Sam Ritchie 2021-01-07T18:41:01.231Z

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

Sam Ritchie 2021-01-07T18:41:08.231200Z

bind it inside sci

borkdude 2021-01-07T18:41:20.231700Z

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

Sam Ritchie 2021-01-07T18:41:28.231900Z

@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 Ritchie 2021-01-07T18:41:36.232200Z

they are calling (compile-fn f)

borkdude 2021-01-07T18:41:49.232600Z

so you want to forbid eval ?

borkdude 2021-01-07T18:42:10.233300Z

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

Sam Ritchie 2021-01-07T18:42:18.233700Z

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 Ritchie 2021-01-07T18:42:32.234100Z

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

Sam Ritchie 2021-01-07T18:42:34.234300Z

let me link

Sam Ritchie 2021-01-07T18:43:42.235100Z

`*mode*` is dynamic here

Sam Ritchie 2021-01-07T18:44:15.235700Z

and then compile-native calls eval internally

Sam Ritchie 2021-01-07T18:44:36.236Z

but compile-native is defined outside of sci

Sam Ritchie 2021-01-07T18:45:19.236800Z

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

borkdude 2021-01-07T18:45:29.237200Z

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

borkdude 2021-01-07T18:45:36.237600Z

depending on the mode

Sam Ritchie 2021-01-07T18:45:54.238200Z

this latter snippet has the code for both “modes”

Sam Ritchie 2021-01-07T18:47:22.240200Z

“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

borkdude 2021-01-07T18:47:33.240500Z

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

borkdude 2021-01-07T18:47:52.240800Z

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

Sam Ritchie 2021-01-07T18:48:30.241400Z

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 Ritchie 2021-01-07T18:49:02.242200Z

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.

borkdude 2021-01-07T18:49:28.242700Z

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

Sam Ritchie 2021-01-07T18:50:34.243300Z

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 Ritchie 2021-01-07T18:51:01.243900Z

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

borkdude 2021-01-07T18:51:25.244300Z

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

borkdude 2021-01-07T18:51:38.244700Z

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

Sam Ritchie 2021-01-07T18:52:57.245500Z

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

borkdude 2021-01-07T18:52:58.245600Z

so that's ok, I guess

Sam Ritchie 2021-01-07T18:53:24.246500Z

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

borkdude 2021-01-07T18:53:55.247700Z

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] ...)

borkdude 2021-01-07T18:54:44.248400Z

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

Sam Ritchie 2021-01-07T18:55:25.249100Z

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

Sam Ritchie 2021-01-07T18:55:41.249600Z

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

Sam Ritchie 2021-01-07T18:55:50.250200Z

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

borkdude 2021-01-07T18:55:50.250300Z

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

Sam Ritchie 2021-01-07T18:55:57.250600Z

yup, that is a better design

borkdude 2021-01-07T18:56:51.251300Z

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

borkdude 2021-01-07T18:57:40.252300Z

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 Ritchie 2021-01-07T18:58:34.252600Z

(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 Ritchie 2021-01-07T18:59:22.253500Z

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

borkdude 2021-01-07T19:01:39.253800Z

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

Sam Ritchie 2021-01-07T19:02:39.254300Z

yup

Sam Ritchie 2021-01-07T19:02:47.254700Z

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

Sam Ritchie 2021-01-07T19:03:09.255400Z

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

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

Sam Ritchie 2021-01-07T19:03:22.255700Z

is that a no-no?

borkdude 2021-01-07T19:03:51.256300Z

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.

borkdude 2021-01-07T19:04:18.256500Z

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

Sam Ritchie 2021-01-07T19:05:07.257400Z

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

Sam Ritchie 2021-01-07T19:05:18.257700Z

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

borkdude 2021-01-07T19:08:35.258400Z

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

borkdude 2021-01-07T19:10:30.259100Z

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

borkdude 2021-01-07T19:12:50.260600Z

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