sci 2025-09-13

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?

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

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

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

programs like tools.analyzer-like stuff

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?

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

it depends what your goals are

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.

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

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* ;)

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.

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)")

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

but not sure if fn will still work in that case

I guess not

No it doesn't

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

well, that's something that can be fixed

pretty easily

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

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

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

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

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*

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

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

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

I think loop* wasn't part of SCI before

got introduced later

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

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

issue(s) welcome of course

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

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

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

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

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

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

please mention it in the issue as well

oh wait, it does work, I was macroexpanding

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!

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