Skip to content

Instantly share code, notes, and snippets.

@yuhan0
Last active May 13, 2025 06:56
Show Gist options
  • Select an option

  • Save yuhan0/a87796eedd902996c53f4f17b39067a5 to your computer and use it in GitHub Desktop.

Select an option

Save yuhan0/a87796eedd902996c53f4f17b39067a5 to your computer and use it in GitHub Desktop.
(ns datastar.expr
(:require [clojure.string :as str]
[clojure.walk :as walk]))
;; The motivation here is to provide a *thin* syntactic veneer over constructing strings
;; representing Datastar expressions (basically JS snippets embedded in DOM attributes)
;;
;; Takes inspiration from Scriptjure, but targeted more specifically to the Datastar context,
;; and does *not* aim to implement Clojure-like truthiness / value semantics (cf. Squint, Clojurescript)
;; or to enable the writing of arbitrarily nested complex expressions (an antipattern)
;;
;; why: complex rendering / domain logic should be expressed server-side wherever possible,
;; or encapsulated in native JS script tags / webcomponents client-side.
;; On the other hand, Datastar signals are great for expressing declarative UI logic,
;; which ideally should consist of simple accessors and constructors.
;; This library merely provides a friendly API: (emit `sexpr)=>string for writing such simple expressions
;; using Clojure-like syntax, with as minimal abstraction as possible between the two.
;; NOTE: Datastar expressions can be thought of as being implicitly wrapped and executed in a constrained scope.
;; Unlike bare JS compile targets (Squint / Scriptjure), top-level expressions involving intermediate bindings
;; or multiple statements don't need to be wrapped in IIFEs, we can just emit them as ;-separated exprs.
;; We also have the implicit `evt` binding in scope for event handler attributes (data-on-click, etc.)
;; as well as undocumented(?) bindings `el` for the DOM node itself,
(defn- http-request-sugar?
"Truthy if expr is @post, @get etc."
[expr]
(and (seq? expr)
(= 'clojure.core/deref (first expr))
(symbol? (second expr))
(#{"get" "post" "put" "patch" "delete"}
(name (second expr)))))
(defn- jsfn? [expr]
(and (symbol? expr)
(= "js" (namespace expr))))
(defn- regex? [x]
(instance? java.util.regex.Pattern x))
(defn dispatch [x]
;; TODO how to handle
;; - sets (js/Set?)
;; - map-entries (outside of maps??)
;; - #uuid / #inst etc.
;; TODO what about sugar for
;; (.method obj) => obj.method()
;; (.-target evt) => evt.target
;; Just use (. obj method) / (. evt -target) for now
;; Also evt.target can just be used as a plain symbol (if known at compile time)
(cond (nil? x) nil
(boolean? x) ::bool
(number? x) ::num
(string? x) ::str
(regex? x) ::regex
(ident? x) ::ident
(vector? x) ::vec
(map? x) ::map
(seq? x) ;; NOTE: runtime syntax-quote produces seqs, (list? x) returns false
(if-some [[head] (seq x)]
(cond (simple-keyword? head) ::lookup ;; (:k x) => sugar for x['k']
(jsfn? head) ::jsfn ;; (js/foo.bar x y) => foo.bar(x,y)
(http-request-sugar? head) ::http-request ;; (@get x) => @get(x)
(ident? head) head ;; Everything else is a function call/special form
;; HOFs / fn literals in head position are not allowed, eg. ((if p + -) x y)
:else (throw (ex-info "Invalid head of list" {:head head})))
::empty-list)
:else (throw (ex-info "Not supported" {:data x :type (type x)}))))
;;# API
(defmulti emit
"Returns a Datastar expression as a string"
{:arglists '([expr])} #'dispatch)
(defn emit-many [& exprs]
(str/join ";" (map emit exprs)))
(comment
;; Prefer explicit fn calls over macros
;; - the following only saves a couple of keystrokes over emit-many
;; - loses namespace resolution (have to recreate via walk)
;; - no interpolation of runtime vals (more custom syntax needed / force call-sites to wrap in another macro layer)
(defmacro d* [& exprs]
(str/join ";" (map emit (walk/postwalk
(fn [e]
(if-let [v (and (symbol? e)
(resolve &env e))]
(symbol v)
e))
exprs))))
(let [x 3]
(d* (+ 1 2 x)))
;; => "1+2+x"
(let [x 3]
(emit-many `(+ 1 2 ~x)))
;; => "1+2+3"
,)
;;# Method definitions
;;## Helpers
(defn- simple? [x] ;; expressions that don't need parenthesizing
((some-fn nil? boolean? number? ident? string?) x))
(defn- wrap [x]
(if (simple? x)
(emit x)
;; TODO check if (emit x) is already wrapped?
;; Not as simple as (and (starts-with? "(") (ends-with? ")")
;; since they could belong to different paren groups
;; Probably not worth the couple of chars it would save
(str "(" (emit x) ")")))
(defn- emit-call [jsfn args]
(str jsfn "(" (str/join "," (map emit args)) ")"))
(defn- throw-arity [h n args]
(throw (ex-info (str "Emitted call to `" h "` must take at least " n " args")
{:args args})))
(defn emit-head
"Define an `emit` method for list exprs beginning with head."
([head min-arity method] ;; TODO max arity?
(.addMethod emit head
(fn [[_ & args]]
(if (< (count args) min-arity)
(throw-arity head min-arity args)
(apply method args)))))
([head method]
(.addMethod emit head
#(apply method (next %)))))
(defn emit-macroexpanded
"Emit calls to clojure macros by delegating to macroexpand"
[head]
(.addMethod emit head
(fn [expr] (emit (macroexpand expr)))))
(def direct-translations
"These can be directly replaced in any context eg. arguments to HOFs
where other translations need eta-expansion"
`{abs js/Math.abs
int? js/Number.isInteger
integer? js/Number.isInteger
NaN? js/Number.isNaN
parse-long js/parseInt
parse-double js/parseFloat
rand js/Math.random
pr-str js/JSON.stringify})
(defn- ->jsfn [expr]
(if (jsfn? expr) expr
(get direct-translations expr)))
;;## Passthrough
(defn- raw-str [& args]
(list ::raw (apply str args)))
(defmethod emit ::raw [[_ x]] x)
(defmethod emit ::jsfn [[f & args]]
(emit-call (name f) args))
(doseq [[f js] direct-translations]
(emit-head f #(emit-call (name js) %&)))
(comment
(emit (raw-str "blah"))
;; => "blah"
(emit `(js/JSON.stringify x))
;; => "JSON.stringify(x)"
(emit `(parse-long evt.target.value))
;; => "parseInt(evt.target.value)"
,)
;;## Primitives
(defmethod emit nil [_]
"null")
(defmethod emit ::bool [n]
(if n "true" "false"))
(defmethod emit ::num [n]
(str (if (ratio? n) (double n) n)))
(defmethod emit ::str [s]
(if (re-find #"['\\\r\n]" s )
(pr-str s)
(str \' s \')))
(defmethod emit ::regex [s]
;; wilfully ignoring the differences between jvm / js syntax
(str "/" s "/"))
(defmethod emit ::ident [x]
(let [x (direct-translations x x)
n (name x)]
(if (str/starts-with? "$" n)
n ;; FIXME what exactly are the rules around signal naming
(munge n))))
(comment
(map emit [1 2N 3M 4/5 6.0]) ;; => ("1" "2" "3" "0.8" "6.0")
;; These already stringify to valid JS, that's convenient
(map emit [##Inf ##-Inf ##NaN 1e-20]) ;; => ("Infinity" "-Infinity" "NaN" "1.0E-20")
(mapv (comp symbol emit)
[""
"/about.html"
"single'quotes"
"double\"quotes"
"triple'\"quotes"
"multi\nline"])
;; => [''
;; '/about.html'
;; "single'quotes"
;; 'double"quotes'
;; "triple'\"quotes"
;; "multi\nline"]
(emit :foo-bar) ;; => "foo_bar"
(emit :$foo) ;; => "$foo"
(emit :foo?) ;; => "foo_QMARK_"
;; all idents get stripped of their ns and emitted as plain js vars
;; TODO is this really a good idea
(map emit '[a :b c/d :e/f])
;; => ("a" "b" "d" "f")
,)
;;## Literal collections
(defn- parenthesize [osc strs]
(let [[open sep close] osc]
(str open (str/join sep strs) close)))
(defmethod emit ::vec [v]
(parenthesize "[,]" (map emit v)))
(defmethod emit ::map [m]
;; FIXME will these ever be emitted in a position where they get confused for block notation syntax? (WAT)
(parenthesize "{,}"
(for [[k v] m]
(if (simple? k)
(str (emit k) ":" (emit v))
(throw (ex-info "invalid key"
{:k k :m m}))))))
(comment
(mapv emit
[{}
{:a 1}
{:a 1 :b 2}
{:a {:x 1 :y 2} :b {:x 3 :y 4}}])
;; => ["{}" "{a:1}" "{a:1,b:2}" "{a:{x:1,y:2},b:{x:3,y:4}}"]
,)
;;## String interpolation
(defmethod emit `str [[_ & xs]]
(str "`"
(str/join
(for [x xs]
(if (or (char? x) (string? x)) x ;; FIXME how to escape backticks?
(str "${" (emit x) "}"))))
"`"))
(comment
(emit `(str url "?" query "=" val))
;; => "`${url}?${query}=${val}`"
;; TODO any different (perf, compatibility) vs
;; [url,'?',query,'=',val].join('')
,)
;;## Assignment
(emit-head `set!
(fn [b v] (str (emit b) "=" (wrap v))))
(emit-head `aset
(fn [a i v] (emit `(set! (aget ~a ~i) ~v))))
(emit-head `assoc!
(fn [m k v] (emit `(set! (get ~m ~k) ~v))))
(emit-head `new #(str "new " (emit-call (emit %1) %&)))
;; TODO update! conj!
(comment
(emit `(set! js/window.location (str "/" url)))
;; => "window.location=(`/${url}`)"
(mapv emit `[(set! $foo (inc $foo))
(aset $xs 0 123)
(assoc! $foo "bar" "baz")
(new js/Object)])
;; => ["$foo=($foo+1)" "$xs[0]=123" "$foo['bar']='baz'"]
,)
;; TODO sugar for += , *= etc?
;; `(js/+= $foo 2) => $foo+=2
;;## Control flow
(comment
;; No need to wrap multiple statements in an IIFE after all?
(emit-head `do
(fn ([] (emit nil))
([e] (emit e))
([e & body]
(str "(()=>{"
(str/join ";" (map emit (cons e body)))
"})()"))))
;; But what if the do-expr is nested eg.
;; (+ 1 (do (prn x) x)) => 1+console.log(x);x (invalid!)
;; We'd need to either hoist the side-effecting statements:
;; console.log(x);1+x
;; Which implies the need for some CFG / lifetime analysis (way out of scope)
;; or simply wrap them in an IIFE
;; 1+(()=>{console.log(x);return x})())
,)
;; Sequential operations (do a b c) can always be transpiled to comma operator since
;; - we don't need to handle early returns
;; - any newly introduced bindings must be wrapped in a `let`
(emit-head `do
(fn ([] (emit nil))
([x] (emit x))
([x & more]
(parenthesize "(,)"
(map emit (cons x more))))))
;; let-bindings are transpiled to nested curried lambdas,
;; this naturally handles dependencies between sequential bindings and variable shadowing (macroexpansion of as->, some-> etc)
;; without need for manual alpha-conversion or dependency analysis
;; Most JS engines should be able to optimize this to perform roughly the same as Squint's strategy of 0-arg IIFE + const-bindings
(emit-head `let*
(fn [bindings & body]
(loop [bvs (reverse (partition 2 bindings)) ; inside-out
res (emit `(do ~@body))]
(if-some [[[b v] & r] (seq bvs)]
(recur r
(str "(" (emit b) "=>" res ")(" (emit v) ")"))
res))))
(comment
;; Old version, didn't work outside top-level context
(emit-head `let* 1
(fn [bindings & body]
(str/join ";"
(or (seq (concat (for [[b v] (partition 2 bindings)]
;; FIXME should this be var / const instead
(str "let " (emit b) "=" (emit v)))
(map emit body)))
[(emit nil)]))))
,)
;;### conditionals
;; NOTE: follows js truthiness semantics - (if 0 yes no) => 0?yes:no => no
;; Must wrap explicitly if clj-like semantics needed:
;; (if (not (or (false? x) (nil? x))) yes no) => (!((false===x)||(null===x)))?yes:no
(emit-head `if 2
(let [truthy? #(or (true? %) (keyword? %))] ;; Somewhat of a hack to support :else clauses in cond
(fn ([c t] (if (truthy? c) (wrap t)
(str (wrap c) "&&" (wrap t))))
([c t e] (if (truthy? c) (wrap t)
(str (wrap c) "?" (wrap t) ":" (wrap e)))))))
(doseq [m `[when if-not when-not cond]]
(emit-macroexpanded m))
;;### Destructuring
;; sequential destructuring expands to (nth v idx nil)
;; in JS an out-of-bounds call to v[idx] => undefined
;; Properly handling this requires an IIFE ((r)=>r===undefined?r:nil)(v[idx])
;; Could also emit v[idx]??nil but that's a relatively new syntax addition
(emit-head `nth
(fn ([coll idx] (str (emit coll) "[" (emit idx) "]"))
([coll idx nf]
(let [r (gensym)]
(emit `(let* [~r (nth ~coll ~idx)]
(if (= ~r js/undefined)
~nf ~r)))))))
(comment
;; TODO map destructuring is trickier, probably need to roll our own or stub out those calls to PersistentArrayMap
(macroexpand '(clojure.core/let [{a :a} m] a))
;; => (let*
;; [map__89656 m
;; map__89656
;; (if (seq? map__89656)
;; (if (next map__89656)
;; (clojure.lang.PersistentArrayMap/createAsIfByAssoc
;; (to-array map__89656))
;; (if (seq map__89656)
;; (first map__89656)
;; clojure.lang.PersistentArrayMap/EMPTY))
;; map__89656)
;; a (get map__89656 :a)]
;; a)
,)
(defn- simple-bindings [[head bindings & body]]
;; HACK un-namespace bindings to allow let-macroexpansion
(let [un #(if (symbol? %) (symbol (name %)) %)]
(list* head
(vec (mapcat (fn [[b v]]
[(walk/postwalk un b) v])
(partition 2 bindings)))
body)))
(doseq [m `[let if-let when-let if-some when-some when-first]]
(defmethod emit m [expr]
(emit (macroexpand (simple-bindings expr)))))
(comment
(mapv emit
`[(do)
(do a)
(do a b)
(do a b c)])
;; => ["null" "a" "(a,b)" "(a,b,c)"]
(mapv emit
`[(let* [])
(let* [x 2])
(let* [x 2] x)
(let* [x 2, y (inc x)] (+ x y))])
;; => ["null" "(x=>null)(2)" "(x=>x)(2)" "(x=>(y=>x+y)(x+1))(2)"]
(emit
`(let [a 1
b (+ a 1)
c (+ a b)
d (+ b c)
e (+ c d)]
(+ a b c d e)))
;; => "(a=>(b=>(c=>(d=>(e=>a+b+c+d+e)(c+d))(b+c))(a+b))(a+1))(1)"
(emit
`(let [[a b] c]
(+ a b)))
;; => "(vec__90484=>(a=>(b=>a+b)((r90487=>(r90487===undefined)?null:r90487)(vec__90484[1])))((r90488=>(r90488===undefined)?null:r90488)(vec__90484[0])))(c)"
(mapv emit
`[#_(if)
#_(if a)
(if a b)
(if a b c)])
;; => ["a&&b" "a?b:c"]
(mapv emit
`[#_(when)
(when a)
(when a b)
(when a b c)])
;; => ["a&&(null)" "a&&(b)" "a&&((b,c))"]
(emit `(cond p x q y :else z))
;; => "p?x:(q?y:(z))"
(emit `(when-let [x c] (println x) x))
;; => "(temp__5823__auto__=>temp__5823__auto__&&((x=>(console.log(x),x))(temp__5823__auto__)))(c)"
;; Following best practice and gensym-ing syntax-quoted bindings also works, but output is noiser
(emit `(when-let [x# c] (println x#) x#))
;; => "(temp__5823__auto__=>temp__5823__auto__&&((x__92092__auto__=>(console.log(x__92092__auto__),x__92092__auto__))(temp__5823__auto__)))(c)"
,)
;;## Syntax sugar
(emit-head `aget (fn ([arr idx] (str (wrap arr) "[" (emit idx) "]"))
([arr idx & idxs]
(str/join (list* (wrap arr)
(for [i (cons idx idxs)]
(str "[" (emit i) "]")))))))
(emit-head `get (fn ([obj k] (str (wrap obj) "[" (emit k) "]"))
([obj k nf] (str (wrap obj) "[" (emit k) "]??" (wrap nf)))))
(defmethod emit ::lookup [[k m & nf]]
(emit `(get ~m ~k ~@nf)))
(defmethod emit ::http-request [[[_ verb] & args]]
;; args always interpolated via `str
(str "@" (name verb) "(" (emit (cons `str args)) ")"))
(emit-head '.
(fn [x m & args]
(let [n (cond (symbol? m) (name m)
(seq? m) (name (first m)))
args (if (seq? m) (rest m) args)]
(if (str/starts-with? n "-")
(str (emit x) "." (subs n 1))
(emit-call (str (emit x) "." n) args)))))
;; TODO dispatch (.method obj arg) => (. obj method arg)
(emit-macroexpanded '..)
(comment
(emit '(:a x)) ;; => "x['a']"
(emit '(:a x y)) ;; => "x['a']??(y)"
(emit `(@get "/foo/" $bar "?prop=" $baz)) ;; => "@get(`/foo/${$bar}?prop=${$baz}`)"
(emit `(@post "/endpoint")) ;; => "@post(`/endpoint`)"
(emit `(. a b c d)) ;; => "a.b(c,d)"
(emit `(. foo bar)) ;; => "foo.bar()"
(emit `(. foo -bar)) ;; => "foo.bar"
(emit `(.. a (b c) (d e f) -g h)) ;; => "a.b(c).d(e,f).g.h()"
(emit `(.. $title toUpperCase)) ;; => "$title.toUpperCase()"
,)
;;## Monoids (0+ arity)
(doseq [[m [unit binop]]
`{+ [0 "+"]
* [1 "*"]
and [true "&&"]
or [nil "||"]
;; NOTE: Unlike clojure, JS treats min/max as monoids with Inf / -Inf as the unit
min [##Inf js/Math.min]
max [##-Inf js/Math.max]}]
(emit-head m
(fn ([] unit)
([& args]
(cond (jsfn? binop) (emit-call (name binop) args)
(next args) (str/join binop (map wrap args))
:else (emit (first args))))))) ;; 1 arg => no need to wrap
;; Spread syntax
(emit-head `merge
(fn [& args]
(parenthesize "{,}"
(for [m args]
(str "..." (wrap m))))))
(emit-head `concat
(fn [& args]
(parenthesize "[,]"
(for [s args]
(str "..." (wrap s))))))
(comment
(mapv emit `[(and)
(and p)
(and p q)
(and (or p q) (or r s))])
;; => [true "p" "p&&q" "(p||q)&&(r||s)"]
(mapv emit `[(min) (min a) (min a b c)
(max) (max a) (max a b c)
(min upper (max x lower))])
;; => [##Inf "Math.min(a)" "Math.min(a,b,c)"
;; ##-Inf "Math.max(a)" "Math.max(a,b,c)"
;; "Math.min(upper,Math.max(x,lower))"]
(mapv emit `[(merge) (merge m1) (merge m1 m2)
(concat) (concat v1) (concat v1 v2)])
;; => ["{}" "{...m1}" "{...m1,...m2}"
;; "[]" "[...v1]" "[...v1,...v2]"]
;; TODO min-key/max-key?
,)
;;## Semigroups (1+ arity)
(doseq [[m binop]
`{bit-and "&"
bit-or "|"
bit-xor "^"}]
(emit-head m 1
(fn [& args] (str/join binop (map wrap args)))))
;;## Magmas (left associative)
(doseq [[m [binop unop]]
`{- ["-" "-"]
/ ["/" "1/"]}]
(emit-head m 1
(fn [& args]
(if (next args) (str/join binop (map wrap args))
(str unop (wrap (first args)))))))
;;## Binary relations (conjunctive)
(doseq [[r binop]
`{= "===" ;; NOTE: reference-based equality for collections
< "<"
<= "<="
> ">"
>= ">="}]
(emit-head r 1
(fn [& args]
#_(if (next args)
(str/join "&&"
(map (fn [[x y]]
(str "(" (wrap x) binop (wrap y) ")"))
(partition 2 1 args)))
(emit true))
(emit `(and ~@(map (fn [[x y]]
;; HACK to prevent unnecessary wrapping
(raw-str (wrap x) binop (wrap y)))
(partition 2 1 args)))))))
(comment
(emit `(= x y 10)) ;; => "(x===y)&&(y===10)"
(emit `(<= 1 x 20)) ;; => "(1<=x)&&(x<=20)"
(emit `(not= x y z)) ;; => "!((x===y)&&(y===z))"
,)
;;## More predicates
(emit-head `not #(str "!" (wrap %)))
(emit-head `not= #(emit `(not (= ~@%&))))
(emit-head `nil? #(emit `(= nil ~%)))
(emit-head `any? #(emit true))
(emit-head `some? #(emit `(not= nil ~%)))
(emit-head `false? #(emit `(= false ~%)))
(emit-head `true? #(emit `(= true ~%)))
(emit-head `type #(str "typeof " (wrap %)))
(emit-head `number? #(emit `(= (type ~%) "number")))
(emit-head `string? #(emit `(= (type ~%) "string")))
(emit-head `boolean? #(emit `(= (type ~%) "boolean")))
;; TODO nat-int? neg-int? pos-int? float? double? infinite?
;; not applicable:
;; keyword? symbol? ident? simple-keyword? simple-symbol? simple-ident? qualified-keyword? qualified-symbol? qualified-ident? special-symbol? tagged-literal?
;; uri? uuid? var? chunked-seq? delay? map-entry? record? reduced? volatile? inst? realized?
;; bound? thread-bound? bytes? class? decimal? future? ratio? rational? readetr-conditional?
;; unsure about these:
;; seqable? empty? associative? coll? counted? fn? ifn? indexed? list? map? reversible? seq? sequential? set? sorted? vector?
;; From cljs:
;; array? object?
;; cloneable? ifind? iterable? js-symbol? reduceable? regexp?
(comment
;; special-case the 2-arg call? (not= x y) => !(x===y) vs (x!==y)
(emit-head `not=
(fn [& args]
(if (= 2 (count args))
(str (emit (first args)) "!==" (emit (second args)))
(emit `(not (= ~@args))))))
(emit `(and x (not y))) ;; => "x&&(!y)"
(emit `(and (or a (not b))
(not (or c (not (not d))))))
;; => "(a||(!b))&&(!(c||(!(!d))))"
,)
;;## Threading macros
(doseq [macro `[-> ->> some-> some->> cond-> cond->>]]
(emit-macroexpanded macro))
(comment
(emit `(-> $count inc (/ 2) str))
;; => "`${($count+1)/2}`"
(emit `(some-> evt .-target .-value))
;; => "let G__80562=evt;let G__80562=(null===G__80562)?null:(G__80562.target);(null===G__80562)?null:(G__80562.value)"
,)
;;## Sequence ops (~ js Array)
(emit-head `count #(emit `(. ~% -length)))
(emit-head `alength #(emit `(. ~% -length)))
(emit-head `first #(emit `(aget ~% 0)))
(emit-head `second #(emit `(aget ~% 1)))
(emit-head `last #(emit `(aget ~% (dec (count ~%))))) ;; FIXME double-eval
(emit-head `subvec (fn [arr start end]
(emit `(. ~arr slice ~start ~end))))
(emit-head `take (fn [n xs] (emit `(. ~xs slice 0 ~n))))
(emit-head `drop (fn [n xs] (emit `(. ~xs slice ~n))))
(emit-head `next #(emit `(drop ~% 1)))
(emit-head `rest #(emit `(drop ~% 1)))
(emit-head `nnext #(emit `(drop ~% 2)))
;; shallow clone + op
(emit-head `reverse #(emit `(. (concat ~%) reverse)))
(emit-head `sort #(emit `(. (concat ~%) sort)))
(comment
(emit `(reverse xs))
;; => "[...xs].reverse()"
,)
;; Restricted fn syntax
(emit-head `fn*
(fn [params body-expr]
{:pre [(every? symbol? params)]}
(str "(" (str/join "," (map name params)) ")=>" (wrap body-expr))))
;; No destructuring for now
;; TODO un-namespace the param list like with `let etc.
(emit-head `fn #(emit `(fn* ~@%&)))
(defn- lift-fn [expr]
(or (->jsfn expr)
(cond (and (seq? expr)
(`#{fn* fn} (first expr))) expr
(symbol? expr) (let [x (gensym)] ;; eta-expand
(list 'fn* [x] (list expr x)))
:else (throw (ex-info "invalid function" {:expr expr})))))
(emit-head `map (fn [f xs] (emit `(. ~xs (map ~(lift-fn f))))))
(emit-head `mapcat (fn [f xs] (emit `(. ~xs (flatMap ~(lift-fn f))))))
(emit-head `filter (fn [p xs] (emit `(. ~xs (filter ~(lift-fn p))))))
(emit-head `complement (fn [f] (let [x (gensym)]
(emit `(fn* [~x] (not (~f ~x)))))))
(emit-head `remove (fn [p xs] (emit `(. ~xs (filter (complement ~p))))))
(comment
(emit `(map (fn [x] (+ x 2)) xs))
;; => "xs.map((x)=>(x+2))"
(emit `(->> xs (filter even?) (map inc)))
;; => "xs.filter((G__75369)=>((G__75369%2)===0)).map((G__75368)=>(G__75368+1))"
(emit `(->> [1 2 3] (map js/Math.abs)))
;; => "[1,2,3].map(Math.abs)"
(emit `(remove int? xs))
;; => "xs.filter((G__75374)=>(!(Number.isInteger(G__75374))))"
(emit `(map parse-long xs))
;; => "xs.map(parseInt)"
,)
;; From cheat sheet:
;; https://jafingerhut.github.io/cheatsheet/clojuredocs/cheatsheet-tiptip-cdocs-summary.html
;; seq vals keys rseq subseq rsubseq sequence
;; lazy-seq repeatedly iterate iteration
;; repeat range
;; file-seq line-seq resultset-seq re-seq tree-seq xml-seq iterator-seq enumeration-seq
;; keep keep-indexed
;; distinct filter remove take-nth for dedupe random-sample
;; cons conj concat lazy-cat mapcat cycle interleave interpose
;; rest nthrest next fnext nnext drop drop-while take-last for
;; take take-while butlast drop-last for
;; conj concat distinct flatten group-by partition partition-all partition-by split-at split-with filter remove replace shuffle partitionv partitionv-all splitv-at
;; reverse sort sort-by compare
;; map pmap map-indexed mapcat for replace seque
;; first second last rest next ffirst nfirst fnext nnext nth nthnext rand-nth when-first max-key min-key
;; zipmap into reduce reductions set vec into-array to-array-2d mapv filterv
;; apply
;; some filter
;; doseq dorun doall run!
;; realized?
;;## Collection ops (~ js Object)
(emit-head `keys #(emit `(js/Object.keys ~%)))
(emit-head `vals #(emit `(js/Object.values ~%)))
(emit-head `assoc #(parenthesize ["{..." "," "}"]
(cons (wrap %1)
(for [[k v] (partition 2 %&)]
(str (emit k) ":" (emit v))))))
(comment
(mapv emit `[(keys m)
(vals m)
(assoc m k v)
(assoc m k1 v1 k2 v2)
(get m k)
(get m k nf)])
;; => ["Object.keys(m)"
;; "Object.values(m)"
;; "{...m,k:v}"
;; "{...m,k1:v1,k2:v2}"
;; "m[k]"
;; "m[k]??nf"]
,)
;; Generic ops:
;; count bounded-count empty not-empty into conj
;; distinct? empty? every? not-every? some not-any?
;; sequential? associative? sorted? counted? reversible?
;; coll? list? vector? set? map? seq? record? map-entry?
;; Lists:
;; list list* first nth peek .indexOf .lastIndexOf cons conj rest pop
;; Vectors:
;; vector vec vector-of mapv filterv nth get peek .indexOf .lastIndexOf assoc assoc-in pop subvec replace conj rseq update update-in reduce-kv
;; Sets:
;; set hash-set contains? conj disj
;; clojure.set/union clojure.set/difference clojure.set/intersection clojure.set/select clojure.set/subset? clojure.set/superset?
;; Maps:
;; hash-map array-map zipmap bean frequencies group-by clojure.set/index
;; get get-in contains? find keys vals assoc assoc-in dissoc merge merge-with select-keys update update-in update-keys update-vals
;; reduce-kv key val
;; clojure.set/rename-keys clojure.set/map-invert
;; Sorted sets / maps:
;; sorted-set sorted-set-by sorted-map sorted-map-by
;; rseq subseq rsubseq
;;## Strings
(emit-head `subs #(emit `(. ~%1 (substring ~@%&))))
(emit-head `println #(emit `(js/console.log ~@%&)))
(emit-head `prn #(emit `(js/console.log ~@(map (partial list `pr-str) %&)))) ;; pr-str = JSON/stringify
(emit-head `re-pattern #(emit (if (string? %) (re-pattern %) `(new js/RegExp ~%))))
(defn- anchor-regex [re] (re-pattern (str "^" re "$")))
(emit-head `re-matches #(emit `(. ~(anchor-regex %1) exec ~%2)))
(emit-head `re-find #(emit `(some-> (. ~%1 exec ~%2) first)))
;; TODO re-groups re-seq
(doseq [[f m]
`{str/trim .trim
str/triml .trimLeft
str/trimr .trimRight
str/split .split
str/starts-with? .startsWith
str/ends-with? .endsWith
str/lower-case .toLowerCase
str/upper-case .toUpperCase
;; WARNING: the following don't match clojure.string semantics
;; returning -1 instead of nil for not-found
str/index-of .indexOf
str/last-index-of .lastIndexOf}]
(emit-head f
#(emit-call (str (emit %1) m) %&)))
(emit-head `str/join
(fn [sep coll] (emit `(. ~coll join ~sep))))
(comment
(emit `(println 1 2 3)) ;; => "console.log(1,2,3)"
(emit `(println 1 [2] {3 4})) ;; => "console.log(1,[2],{3:4})"
(emit `(prn 1 [2] {3 4})) ;; => "console.log(JSON.stringify(1),JSON.stringify([2]),JSON.stringify({3:4}))"
(emit `(subs s 1 2)) ;; => "s.substring(1,2)"
(emit `(re-pattern "a(b)c")) ;; => "/a(b)c/"
(emit `(re-matches #"a(b+)c" "abbc")) ;; => "/^a(b+)c$/.exec('abbc')"
(emit `(re-find #"a(b)c" "123abc456")) ;; => "let G__85089=/a(b)c/.exec('123abc456');(null===G__85089)?null:(G__85089[0])"
(mapv emit `[(str/trim " abc ")
(str/upper-case "abc")
(str/index-of "abcde" "cd")
(str/join "," ["foo" "bar"])])
;; => ["' abc '.trim()"
;; "'abc'.toUpperCase()"
;; "'abcde'.indexOf('cd')"
;; "['foo','bar'].join(',')"]
;; TODO match index-of semantics?
(doseq [[f m] `{str/index-of .indexOf
str/last-index-of .lastIndexOf}]
(emit-head f
(let [r (gensym "r")
m (symbol (subs (name m) 1))]
#(emit `(let [~r (. ~%1 ~m ~@%&)]
(if (< ~r 0) nil ~r))))))
(emit `(str/index-of "abcde" "cd"))
;; => "(r92963=>(r92963<0)?null:r92963)('abcde'.indexOf('cd'))"
,)
;; => nil
;;## Math ops
(emit-head `inc #(emit `(+ ~% 1)))
(emit-head `dec #(emit `(- ~% 1)))
;; These need a bit of attention
(emit-head `quot (fn [n d] (emit `(js/Math.trunc (/ ~n ~d))))) ;; towards zero
(emit-head `rem (fn [n d] (str (wrap n) "%" (wrap d))))
;; Prefer using rem over mod most of the time, unless the sign of the result is important
(emit-head `mod (fn [n d] {:pre [(simple? d)]} ;; HACK: prevent re-evaluating compound expr for divisor
(emit `(rem (+ (rem ~n ~d) ~d) ~d))))
(emit-head `even? #(emit `(= (rem ~% 2) 0)))
(emit-head `odd? #(emit `(not= (rem ~% 2) 0)))
(emit-head `zero? #(emit `(= 0 ~%)))
(emit-head `pos? #(emit `(< 0 ~%)))
(emit-head `neg? #(emit `(< ~% 0)))
(comment
(emit `(odd? x)) ;; => "x%2!==0"
(emit `(/ (rem (+ x (abs y)) (* 3 (inc z))) 2))
;; => "((x+(Math.abs(y)))%(3*(z+1)))/2"
(for [[n d] [[12 10] [-12 10]
[12 -10] [-12 -10]]]
[(quot n d) (mod n d) (rem n d)])
;; => ([1 2 2] [-1 8 -2]
;; [-1 -8 2] [1 -2 -2])
,)
;;## Bit twiddling
(emit-head `bit-not #(str "~" (wrap %)))
(doseq [[f binop] `{bit-shift-left "<<"
bit-shift-right ">>"
unsigned-bit-shift-right ">>>"}]
(emit-head f #(str (wrap %1) binop (wrap %2))))
(emit-head `bit-and-not #(emit `(bit-and ~%1 ~@(map (partial list `bit-not) %&))))
(emit-head `bit-clear #(emit `(bit-and ~%1 (bit-not (bit-shift-left 1 ~%2)))))
(emit-head `bit-set #(emit `(bit-or ~%1 (bit-shift-left 1 ~%2))))
(emit-head `bit-flip #(emit `(bit-xor ~%1 (bit-shift-left 1 ~%2))))
(emit-head `bit-test #(emit `(not (bit-and ~%1 (bit-shift-left 1 ~%2)))))
(comment
(mapv emit `[(bit-not b)
(bit-shift-left b n)
(bit-shift-right b n)
(unsigned-bit-shift-right b n)
(bit-and-not a b c)
(bit-clear a b)
(bit-set a b)
(bit-flip a b)
(bit-test a b)])
;; => ["~b" "b<<n" "b>>n" "b>>>n" "a&(~b)&(~c)" "a&(~(1<<b))" "a|(1<<b)" "a^(1<<b)" "!(a&(1<<b))"]
(mapv emit `[(bit-and (bit-or p q) (bit-or r s))
(bit-or (bit-and p q) (bit-and r s))
(= (bit-and (bit-not p) (bit-not q)) (bit-not (bit-or p q)))])
;; => ["(p|q)&(r|s)" "(p&q)|(r&s)" "((~p)&(~q))===(~(p|q))"]
,)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment