Fork me on GitHub
#sci
<
2022-01-20
>
haywood14:01:12

Heya, is there an easy way to treat namespaces loaded into the context via something like this https://github.com/babashka/sci#copy-a-namespace like source code eval’d into the compiler so that it’s getting the sci versions of the clojure.core namespace etc?

borkdude14:01:28

can you explain with an example? I don't think I fully understood your question yet

haywood14:01:45

in the docs this function is provided as an example for how to load all of the functions in a library’s namespace

(reduce (fn [ns-map [var-name var]]
            (let [m (meta var)
                  no-doc (:no-doc m)
                  doc (:doc m)
                  arglists (:arglists m)]
              (if no-doc ns-map
                  (assoc ns-map var-name
                         (sci/new-var (symbol var-name) @var
                                      (cond-> {:ns fns
                                               :name (:name m)}
                                        (:macro m) (assoc :macro true)
                                        doc (assoc :doc doc)
                                        arglists (assoc :arglists arglists)))))))
          {}
          (ns-publics 'foobar))
Which creates a SciVar wrapper around the function, but when the function runs it’s not ‘inside’ the context of the sci compiler e.g. if the library code calls resolve it will be clojure.core/resolve and not the SCI version of it. But if I slurp that namespace and eval it with sci, everything works as intended. So I’m trying to work out if there is a way to rebind everything inside a namespace with these https://github.com/babashka/sci/blob/master/src/sci/impl/namespaces.cljc#L808 at the time I’m creating the SciVar around the value.

borkdude15:01:15

@haywood Perhaps this helps?

user=> (def ctx (sci/init {:namespaces {'user {'x 1 'foo patched-foo 'bar (fn [x] :bar)}}}))
#'user/ctx
user=> (def sci-resolve (sci/eval-string* ctx "resolve"))
#'user/sci-resolve
user=> (sci-resolve 'x)
1
user=> (defn patched-foo [x] ((sci-resolve 'bar) x))
#'user/patched-foo
user=> (def ctx (sci/merge-opts ctx {:namespaces {'user {'patched-foo patched-foo}}}))
#'ctx
user=> (sci/eval-string* ctx "(foo 1)")
:bar
You can patch your resolve using functions to use SCI's resolve from the context.

haywood15:01:57

yea definitely, that or wrap the eval-string* call with (with-redefs [resolve (sci/eval-form ctx 'resolve)] (sci/eval-string* ctx code-str))

borkdude15:01:21

would you like to give SCI access to your entire host, or just the opposite and prevent that?

borkdude15:01:12

as in, do you want to give access to Clojure's resolve or would you like to prevent this at all costs.

haywood15:01:56

ah, so not even a matter of prevention, but I want resolve to know about namespaces that were previously created via sci/eval-string*

borkdude15:01:38

yeah, then you'll need to do that with-redefs trick above

haywood15:01:01

or read the file’s source in as a string and eval it that way, not sure which approach is better

borkdude15:01:41

you could rewrite that function, if that is within reason, to do the right thing based on some dynamic var or so

haywood15:01:52

yea I think I might go that way longer term

borkdude15:01:39

(def ^:dynamic *resolver* clojure.core/resolve)
(binding [*resolver* sci-resolve] ...)

borkdude15:01:10

(defn foo [] ((*resolver* 'foo.bar)))

haywood15:01:40

do I have the correct mental model that when sci calls a SciVar’s wrapped function, the execution is basically external and can do whatever?

haywood15:01:57

(when the SciVar is created via the method above)

borkdude15:01:26

SCI just executes the provided function in the context and the behavior of that function doesn't change regardless of called within or outside of SCI

borkdude15:01:05

so if that function calls an HTTP service or launches a rocket, or resolves a clojure var, it doesn't matter

borkdude15:01:49

it's just a simple function call without any magic

haywood15:01:24

cool, for too long I was thinking it had the same restraints as code that was eval’d using sci

haywood15:01:32

or was working inside that context

borkdude15:01:26

in some cases it might indeed be easier to just evaluate that function's source with eval-string* indeed

borkdude15:01:38

then it will behave as the internal code

haywood15:01:04

I might submit a patch to the README for that section to doubly-clarify that

borkdude15:01:13

perhaps you can hack something together with source-fn

borkdude15:01:46

user=> (clojure.repl/source-fn 'inc)
"(defn inc\n  \"Returns a number one greater than num. Does not auto-promote\n  longs, will throw on overflow. See also: inc'\"\n  {:inline (fn [x] `(. clojure.lang.Numbers (~(if *unchecked-math* 'unchecked_inc 'inc) ~x)))\n   :added \"1.2\"}\n  [x] (. clojure.lang.Numbers (inc x)))"

haywood15:01:08

nice! yea I think there is something here

haywood15:01:58

how come this is commented out? https://github.com/babashka/sci/blob/master/src/sci/impl/namespaces.cljc#L1453 definitely generally usable vs a naive

(-> (ns-publics 'my-lib)
                          vals
                          first
                          meta
                          :file
                          slurp)

borkdude15:01:38

There is another version below adapted for SCI

1
borkdude15:01:55

$ clj
Clojure 1.11.0-alpha3
user=> (require '[sci.core :as sci])
nil
user=> (def constantly-source (clojure.repl/source-fn 'constantly))
#'user/constantly-source
user=> (def ctx (sci/init {}))
#'user/ctx
user=> (sci/eval-string* ctx (str "(in-ns 'foo) " constantly-source))
#'foo/constantly
user=> (sci/eval-string* ctx "((foo/constantly 1))")
1

borkdude15:01:03

^ a demo of how you could use source-fn

haywood15:01:59

very nice! thank you for clarifying some things that were tripping me up 🙂