Last active
May 13, 2025 06:56
-
-
Save yuhan0/a87796eedd902996c53f4f17b39067a5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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