Embedding cherry for runtime CLJS compilation

Cherry is a new, experimental ClojureScript compiler. One of the main ideas behind cherry is to reduce friction between ClojureScript and JS tooling. It can be used as a standalone compiler directly as an NPM package without a JVM.

Since v0.0.1, cherry can be embedded into an existing vanilla CLJS or shadow-cljs project. To facilitate this, cherry exposes the cherry.embed namespace. It contains the following:

An example shadow-cljs project:

deps.edn:

{:aliases {:cherry {:extra-deps {io.github.squint-cljs/cherry
                                 {:git/sha "4e948708cb20ab0a1a892c30fe87842a2efcc380"}}}
           :shadow {:extra-deps {thheller/shadow-cljs {:mvn/version "2.22.9"}}
                    :main-opts ["-m" "shadow.cljs.devtools.cli"]}}}

package.json:

{"type": "module"}

shadow-cljs.edn

{:deps true
 :builds
 {:cli
  {:target :esm
   :runtime :node
   :output-dir "out"
   :modules {:eval {:init-fn my.cherry/init}}
   :build-hooks [(shadow.cljs.build-report/hook
                  {:output-to "report.html"})]}}}

src/my/cherry.cljs:

(ns my.cherry
  (:require [cherry.embed :as cherry]))

(cherry/preserve-ns 'cljs.core)
(cherry/preserve-ns 'clojure.string)

(defn init []
  (let [[_ expr] (.slice js/process.argv 2)]
    (cherry/eval-string expr)))

Note that when you don't want to "preserve" an entire namespace, you can just (defn ^:export foo []) a function or value in your CLJS namespace.

When compiling this with clojure -M:cherry:shadow release cli, we get a single out/eval.js file of about 550kb of which roughly 330kb comes from preserving the cljs.core namespace.

We can invoke it with:

$ node out/eval.js -e '(do (prn :hello) (js/console.log (clojure.string/join , [1 2 3])))'
:hello
123

Note that you can enable eval in your CLJS/shadow app with:

(set! *eval* cherry/eval-form)

Full example:

(ns my-shadow.app
  (:require [cherry.embed :as cherry]))

(cherry/preserve-ns 'cljs.core)

(set! *eval* cherry/eval-form)

(def x (eval '(+ 1 2 3)))

Since cherry is a compiler, the code generally runs faster than with SCI which is an interpreter. For many cases SCI is fast enough, but numerical computations in a hot loop isn't one of its strenghts:

$ npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 566.194958 msecs"

With cherry:

$ node out/eval.js -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 8.812375 msecs"

The execution time is similar to self-hosted ClojureScript:

$ plk -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000"Elapsed time: 9.894542 msecs"

Cherry is still experimental and looking for feedback. Please try it out and join the Clojurians Slack channel for discussion.

Published: 2023-03-30

Tagged: clojure cherry

Archive