sci

2025-09-16T08:33:37.010669Z

@borkdude Following up on the sandbox discussion. You mentioned re-definitions of vars do not propagate Maybe I found a bug then or i misunderstand what you mean by that ๐Ÿงต

2025-09-16T08:34:46.305609Z

(require '[sci.core :as sci])

(def orig-ctx (sci/init {}))
(sci/eval-string* orig-ctx "(defn hello [] :ok) (hello)") ;=> :ok

(def forked-ctx (sci/fork orig-ctx))
(sci/eval-string* forked-ctx "(defn hello [] :bad) (hello)") ;=> :bad

;; Redefinition of `hello` now also in `orig-ctx`
(sci/eval-string* orig-ctx "(hello)") ;=> :bad
I would expect/like if sci/fork would isolate the var changes only to forked-ctx

2025-09-16T08:40:52.609639Z

It also works the other way:

(sci/eval-string* orig-ctx "(def hello (constantly :random))")
(sci/eval-string* forked-ctx "(hello)") ;=> :random

2025-09-16T08:44:33.668019Z

For new vars the isolation works

(sci/eval-string* orig-ctx "(def b 1)")
 (sci/eval-string* forked-ctx "b") ;=> clojure.lang.ExceptionInfo: Could not resolve symbol: b 

borkdude 2025-09-16T08:49:46.762009Z

In the original context you can make the var immutable by adding :sci/built-in metadata. Then in the forked context the user cannot redefine it. Or you can just add the hello is a direct function to prevent re-definition.

2025-09-16T08:51:17.182729Z

in more dynamic context that is difficult I think. I'm curious why this is the default and not by default the immutable version

borkdude 2025-09-16T08:52:10.910209Z

the reason is that vars like in Clojure are just mutable objects. I didn't change this default. If you share mutable objects, you can mutate them, unless you don't expose them as mutable.

borkdude 2025-09-16T08:52:24.322769Z

I clarified that in the previous discussion as well

borkdude 2025-09-16T08:53:13.530449Z

maybe we should document that caveat though, but it's unlikely that this will change

borkdude 2025-09-16T08:53:30.813579Z

perhaps sci/fork can have an option to copy all the vars into new objects, which is more expensive

2025-09-16T08:53:37.673419Z

Yeah probably I missed that and it makes sense of course that you followed the clojure path. I was thinking though if it would be possible to forward all changes to the ctx so that a fork is like fork of a clojure map. But maybe that is not desirable for other reasons

2025-09-16T08:55:00.902869Z

> perhaps sci/fork can have an option to copy all the vars into new objects, which is more expensive yeah that's probably what I can do myself as well, but that would be less efficient

borkdude 2025-09-16T08:55:12.363369Z

I made clojure core etc immutable by default. you can only change them with sci/enable-unrestricted-access! (which turns off sandboxing basically, which isn't a good option for nested SCI usages, so I'll have to revisit this)

borkdude 2025-09-16T08:57:00.457299Z

e.g. in babashka, when you use SCI, you can alter clojure.core as well, because enable-unrestricted-access is enabled globally. I'll have to change this and make it part of the context instead I think

2025-09-16T09:00:21.264469Z

I would like to learn more about how the vars are implemented to understand the tradeoffs better. Do you have some pointers of which namespaces in Sci I should look at?

borkdude 2025-09-16T09:00:42.161869Z

Vars are implemented in the same way as JVM Clojure basically

borkdude 2025-09-16T09:01:10.241289Z

look for deftype Var for example

2025-09-16T09:02:38.211249Z

Ah yeah thanks I will study it a bit

borkdude 2025-09-16T09:23:53.279259Z

I pushed some (JVM only) pseudo-code here to fork vars as well in sci/fork: https://github.com/babashka/sci/commit/4b423a347c251b41c841d996c5206c1c9c68ec15

borkdude 2025-09-16T09:24:02.935439Z

it will even clone clojure.core

borkdude 2025-09-16T09:25:10.374199Z

takes about 2ms:

user=> (time (sci/fork ctx1 {:clone-vars true}))
"Elapsed time: 2.322375 msecs"

borkdude 2025-09-16T09:25:41.964279Z

user=> (time (dotimes [i 10000] (sci/fork ctx1 {:clone-vars true})))
"Elapsed time: 259.077834 msecs"
nil

2025-09-16T09:26:31.323129Z

Nice! That's fast enough I think

borkdude 2025-09-16T09:26:45.246659Z

0,02ms actually. yes pretty fast

2025-09-16T11:38:52.042209Z

I'm getting also getting good results at 0.1ms, but with a much bigger context (and maybe a slower machine, Mac M2)

borkdude 2025-09-16T11:42:34.673609Z

btw I'm not sure if cloning clojure.core is a good idea since vars like sci/out don't line up anymore which probably messes up printing etc

2025-09-16T11:43:26.284709Z

Ah yeah, so some extra filtering would be needed to get it right for every context

borkdude 2025-09-16T11:44:15.319859Z

oh it does work:

user=> (sci/binding [sci/out *out*] (sci/eval-string* (sci/fork (sci/init {}) {:clone-vars true}) "(prn :dude)"))
"dude" 

borkdude 2025-09-16T11:44:53.916969Z

I guess it works since prn is cloned but still uses the old *out*

borkdude 2025-09-16T11:45:23.122459Z

yes, this is a problem:

user=> (sci/binding [sci/out *out*] (sci/eval-string* (sci/fork (sci/init {}) {:clone-vars true}) "*out*"))
#object[sci.impl.vars.SciUnbound 0x259f0fdb "Unbound: #'clojure.core/*out*"]

2025-09-16T11:45:24.209939Z

makes sense. Just like the cloning of a var with an atom would still be propagated

2025-09-16T11:46:22.618389Z

ok so needs some extra experimentation at least

2025-09-16T11:47:05.965189Z

I understand the var problem a bit better now

2025-09-16T11:47:25.075549Z

my mental model was simplifying it a bit ๐Ÿ˜