A curiosity journal of math, physics, programming, astronomy, and more.

Clojure-flavored JavaScript

While ClojureScript is my preferred means of writing JavaScript, for simple cases like the one-off scripts I occasionally use for blog posts, it produces far more code than the few kilobytes I need. Advanced and minified compilation of (js/console.log "Hello World") may be "only" 1.4 kilobytes, but making any use of Clojure's persistent collections rapidly inflates build size: console logging an empty vector in place of a string produces a weighty 91 kilobytes of minified code. Persistent collections are one of the best parts of Clojure, but for small scripts I can manage without them. One thing I wanted to see if I could keep, however, was the Lisp syntax, including Clojure's basic macros like ->, doto, and if-let. To do that, I created a kind of poor-man's ClojureScript, what I call "Clojure-flavored JavaScript". So far I've only implemented as much as I need, but here are the rudiments.

After reading a form, ClojureScript performs an analysis phase before compiling the final JavaScript, but for my use, macro expansion is sufficient:

(clojure.walk/macroexpand-all
  '(when-let [a Math/PI]
     (doto js/document
       (.appendChild nil))))
(let*
 [temp22260 Math/PI]
 (if
  temp22260
  (do
   (let*
    [a temp22260]
    (let*
     [G__22261 js/document]
     (. G__22261 appendChild nil)
     G__22261)))))

To generate JavaScript code, we can walk a macro-expanded form and handle whatever we find, which beside atoms like symbols can also include Clojure's various special forms (such as let*, fn*, and do), whatever clojure.core functions are used in expanded macros (such as first and seq, used by destructuring logic), and whatever functions we choose to use (such as str for string concatenation). Here we'll only implement enough of a compiler to compile the above form, so we'll only deal with a few atoms and special forms.

ClojureScript compilation uses an emits function to write compiled JavaScript code to *out*, so we'll follow a similar pattern:

(defn emits [& ss]
  (doseq [s ss]
    (cond
      (seq? s)    (apply emits s)
      (string? s) (.write *out* s))))

When generating symbols, we need to account for the Math namespace and special characters that JavaScript doesn't allow in names. Here I'm only handling #, which is present in symbols created by macros, and -, which is conventionally used in Lisp to separate words.

(defn emit-symbol [s]
  (let [s (if (= "Math" (namespace s))
            (str (namespace s) "." (name s))
            (name s))]
    (emits
      (-> (name s)
        (clojure.string/replace "#" "_HASH_")
        (clojure.string/replace "-" "_")))))

Keywords we'll emit as strings:

(defn emit-keyword [k]
  (emits "'" (name k) "'"))

Here's a function to handle the forms we've defined so far:

(defn emit [form]
  (cond
    (nil? form)     (emits "null")
    (number? form)  (emits (str form))
    (string? form)  (emits "'" (clojure.string/replace form "'" "\\'") "'")
    (symbol? form)  (emit-symbol form)
    (keyword? form) (emit-keyword form)))

To compile the example above we need to handle special forms and function invocations. Let's create create an emit-invoke multimethod that dispatches on the first form in a list:

(defmulti emit-invoke first)

By default, we'll just invoke whatever function name is given:

(defmethod emit-invoke :default [[f & args]]
  (emits (name f) "(")
  (doseq [arg (butlast args)]
    (emit arg)
    (emits ", "))
  (emit (last args))
  (emits ")"))
#object[clojure.lang.MultiFn 0x6c43ae35 "clojure.lang.MultiFn@6c43ae35"]

Add emit-invoke to the original emit function:

(defn emit [form]
  (cond
    (nil? form)     (emits "null")
    (number? form)  (emits (str form))
    (string? form)  (emits "'" (clojure.string/replace form "'" "\\'") "'")
    (symbol? form)  (emit-symbol form)
    (keyword? form) (emit-keyword form)
    (seq? form)     (emit-invoke form)))

And here's a function to carry out the whole compilation pipeline:

(defn compile-to-javascript [form]
  (with-out-str
    (emit (clojure.walk/macroexpand-all form))))

Let's compile with what we have to see how close we are:

(compile-to-javascript
  '(when-let [a Math/PI]
     (doto js/document
       (.appendChild nil))))
"let*(, if(temp22343, do(let*(, let*(, .(G__22344, appendChild, null), G__22344)))))"

We need to define some special forms. Here's if, do, let*, and .:

(defmethod emit-invoke 'if [[_ pred then else]]
  (emits "((")
  (emit pred)
  (emits ") ? ")
  (emit then)
  (emits " : ")
  (emit else)
  (emits ")"))
#object[clojure.lang.MultiFn 0x6c43ae35 "clojure.lang.MultiFn@6c43ae35"]
(defmethod emit-invoke 'do [[_ & body]]
  (emits "(() => {\n")
  (doseq [form (butlast body)]
    (emit form)
    (emits ";\n"))
  (emits "return ")
  (emit (last body))
  (emits ";\n})()"))
#object[clojure.lang.MultiFn 0x6c43ae35 "clojure.lang.MultiFn@6c43ae35"]
(defmethod emit-invoke 'let* [[_ bindings & body]]
  (emits "(() => {\n")
  (doseq [[k v] (partition 2 bindings)]
    (if (= '_ k)
      (do
        (emit v)
        (emits ";\n"))
      (do
        (emits "var ")
        (emit k)
        (emits " = ")
        (emit v)
        (emits ";\n"))))
  (doseq [b (butlast body)]
    (emit b)
    (emits ";\n"))
  (emits "return ")
  (emit (last body))
  (emits ";\n})()"))
#object[clojure.lang.MultiFn 0x6c43ae35 "clojure.lang.MultiFn@6c43ae35"]
(defmethod emit-invoke '. [[_ target method & args]]
  (emit target)
  (emits "." (name method) "(")
  (when (seq args)
    (doseq [arg (butlast args)]
      (emit arg)
      (emits ", "))
    (emit (last args)))
  (emits ")"))
#object[clojure.lang.MultiFn 0x6c43ae35 "clojure.lang.MultiFn@6c43ae35"]

Now compiling gives us:

(compile-to-javascript
  '(when-let [a Math/PI]
     (doto js/document
       (.appendChild nil))))
"(() => {\nvar temp22489 = Math.PI;\nreturn ((temp22489) ? (() => {\nreturn (() => {\nvar a = temp22489;\nreturn (() => {\nvar G__22490 = document;\nG__22490.appendChild(null);\nreturn G__22490;\n})();\n})();\n})() : null);\n})()"

Generated symbol names and unnecessary parentheses and semicolons can be cleaned up by a minifier such as Google's Closure Compiler:

(:out
  (sh "./node_modules/google-closure-compiler/cli.js"
      :in (compile-to-javascript
            '(when-let [a Math/PI]
               (doto js/document
                 (.appendChild nil))))))
"(()=>{if(Math.PI){var a=document;a.appendChild(null)}else a=null;return a})();\n"

While equivalent hand-written JavaScript could easily be smaller, the goal here is merely to be competitive.

To make a fully fledged Clojure-flavored JavaScript, you'll need to implement few more special forms, as well as handle core functions such as str and +.

In my brief experience using such a compiler, I have found it slightly dizzying to write Clojure code that only relies on JavaScript functions and mutative idioms, but so far the dizziness has been tolerable for small experiments.