sci

Steven Lombardi 2025-09-14T15:48:25.773049Z

Yeah if we're interpreting Clojure code, how are we supposed to handle case*, I don't see it in the Clojure docs as one of the supported "special forms". Do we just roll our own implementation that matches the case doc string and call it good?

Steven Lombardi 2025-09-14T15:53:34.866819Z

I'm going off this page, FYI https://clojure.org/reference/special_forms

borkdude 2025-09-14T16:22:43.608059Z

according to

user=> (keys clojure.lang.Compiler/specials)
(& monitor-exit case* try reify* finally loop* do letfn* if clojure.core/import* new deftype* let* fn* recur set! . var quote catch throw monitor-enter def)
case* is a special symbol

Steven Lombardi 2025-09-14T16:28:13.689999Z

Seems to be a private token for the compiler. Not so much an API stable construct. https://clojurians.slack.com/archives/C03S1KBA2/p1757865364019049?thread_ts=1757865364.019049&cid=C03S1KBA2

borkdude 2025-09-14T16:29:24.915919Z

it depends on how far you want to go in Clojure compatibility. Some tools assume there is something like case* which is why I adopted it in SCI, else some programs just won't run in bb

borkdude 2025-09-14T16:29:31.414499Z

programs like tools.analyzer-like stuff

Steven Lombardi 2025-09-14T16:31:28.947689Z

Interesting. But if I'm interpreting Clojure source, and I'm written in pure Clojure, and I want to support as many hosts as possible, maybe it's best to not know case* exists?

Steven Lombardi 2025-09-14T16:32:36.485919Z

Just draw the line at clojure.core/case and implement it custom instead of macroexpanding?

borkdude 2025-09-14T16:33:25.080009Z

it depends what your goals are

borkdude 2025-09-14T16:34:16.476799Z

I used to just treat case as a special form, but got hit by hit when running programs in bb. That's why I changed it. Maybe for other stuff it's fine not to do it.

Steven Lombardi 2025-09-14T16:34:58.942869Z

I'm using interpretation as a form of tracing, but I don't support tracing inside clojure.core

borkdude 2025-09-14T16:36:10.202029Z

Then it sounds fine not to go beyond case. FWIW, in bb/SCI the case macro just expands in the same form but replaces case with case* ;)

Steven Lombardi 2025-09-14T16:39:38.144129Z

Oh wait, even easier. I can just wrap case* in a variadic fn. Something that I can call apply on. Then I get the compiler's implementation for free without rolling my own.

2025-09-13T21:32:08.586749Z

This is a simplified example and does not work for all fn calls. But shows how recursion can potentially be controlled.

(def ctx 
  (let [max-cnt 500]
    (sci/init {:namespaces {'clojure.core
                            {'fn ^:sci/macro
                             (fn [_form _env args & body]
                               `(let [cnt# (volatile! ~max-cnt)]
                                 (fn* ~args
                                       (if (< (vswap! cnt# dec) 0)
                                         (throw (ex-info (str "Max iteration reached " ~max-cnt) {:max ~max-cnt}))
                                         ~@body))))}}})))

(sci/eval-string* ctx "((fn [i] (if (zero? i) :done (recur (dec i)))) 499)") ;=> :done
(sci/eval-string* ctx "((fn [i] (if (zero? i) :done (recur (dec i)))) 500)") ;=> throws
However, fn* can also be called directly and not sure what we can do about that
(sci/eval-string* ctx "((fn* [i] (if (zero? i) :done-a-lot-more-than-500 (recur (dec i)))) 1000)")

borkdude 2025-09-13T21:33:35.094119Z

you can prevent people calling fn* directly with :deny '[fn*]

borkdude 2025-09-13T21:34:08.699619Z

but not sure if fn will still work in that case

borkdude 2025-09-13T21:34:10.088559Z

I guess not

2025-09-13T21:34:19.421039Z

No it doesn't

(sci/eval-string "((fn [a] a) 1)" {:deny ['fn*]}) ;=> clojure.lang.ExceptionInfo: fn* is not allowed! [at :1:2]

borkdude 2025-09-13T21:34:36.432529Z

well, that's something that can be fixed

borkdude 2025-09-13T21:34:40.942129Z

pretty easily

2025-09-13T21:35:12.095789Z

ok cool, i have been mostly approaching it from a user perspective so I thought it would not be that easy

borkdude 2025-09-13T21:35:50.549869Z

I've done a similar thing for loop and core macros that expand to loop like doseq

2025-09-13T21:36:34.590189Z

Yeah I remember. I think together it would tackle the infinite loop "problem"

borkdude 2025-09-13T21:36:36.279269Z

user=> (sci/eval-string "(doseq [i [1 2 3]] i)" {:deny '[loop]})
nil

borkdude 2025-09-13T21:37:14.635239Z

hmm:

user=> (sci/eval-string "(doseq [i [1 2 3]] i)" {:deny '[loop loop*]})
Execution error (ExceptionInfo) at sci.impl.utils/throw-error-with-location (utils.cljc:47).
loop* is not allowed!
I guess that needs another fix for loop*

2025-09-13T21:37:45.799899Z

Cool, I have no idea, but i am happy to hear you think it is doable

2025-09-13T21:38:24.891669Z

So babashka and others can have the fast version and people caring about this safety can have a version with a counter

borkdude 2025-09-13T21:38:41.928179Z

yes, it's fixable. I have some special cases here where I check if it's not this specific symbol: https://github.com/babashka/sci/blob/6758ba028da559c536a06becbbedade7b0ba6448/src/sci/impl/utils.cljc#L170

borkdude 2025-09-13T21:40:21.306779Z

I think loop* wasn't part of SCI before

borkdude 2025-09-13T21:40:24.778139Z

got introduced later

2025-09-13T21:40:40.205789Z

So I am guessing you will change something at the analyzer level?

borkdude 2025-09-13T21:41:03.233479Z

the deny logic checks if it's not that specific symbol with identical?

2025-09-13T21:41:48.511849Z

it got introduced here i think https://github.com/babashka/sci/blob/master/CHANGELOG.md#0637-2022-12-20

borkdude 2025-09-13T21:42:15.055669Z

yep

borkdude 2025-09-13T21:42:34.066799Z

issue(s) welcome of course

2025-09-13T21:44:16.248499Z

I can definitely create one with this context. Will do so in the next two days

2025-09-13T21:53:09.658159Z

I had some examples lying around https://github.com/babashka/sci/issues/1002 Didn't test with loop* can look into that later

borkdude 2025-09-13T21:56:26.098529Z

loop* and loop is a similar case, we can lump that together in this issue if you mention it there

2025-09-13T22:04:51.417659Z

I added an example for loop* and let* . For some reason case* is not a problem

borkdude 2025-09-13T22:08:53.539199Z

interesting, looks like denying case* doesn't work:

user=> (sci/eval-string "(macroexpand '(case 1 2 3))" {:deny '[case*]})
(case* 1 2 3)

borkdude 2025-09-13T22:09:08.845339Z

please mention it in the issue as well

borkdude 2025-09-13T22:10:06.253179Z

oh wait, it does work, I was macroexpanding

borkdude 2025-09-13T22:10:15.505279Z

user=> (sci/eval-string "(case* 1 2 3))" {:deny '[case*]})
Execution error (ExceptionInfo) at sci.impl.utils/throw-error-with-location (utils.cljc:47).
case* is not allowed!

borkdude 2025-09-13T22:10:39.424289Z

in SCI case doesn't expand to case* , it's simply an alias

borkdude 2025-09-13T22:11:37.601929Z

afk now