Skip to content

Commit

Permalink
Use type inference
Browse files Browse the repository at this point in the history
  • Loading branch information
r0man committed May 22, 2020
1 parent e0e9d7d commit 3221c5b
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 91 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.org
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
* Changelog
** Unreleased

- [[https://github.com/r0man/sablono/pull/213][#213]] Use type inference
- [[https://github.com/r0man/sablono/pull/209][#209]] Remove om.next dependency

** 0.8.6
Expand Down
173 changes: 105 additions & 68 deletions src/sablono/compiler.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
(ns sablono.compiler
(:require [cljs.compiler :as cljs]
(:require [cljs.analyzer :as ana]
[cljs.compiler :as cljs]
[clojure.set :as set]
[sablono.normalize :as normalize]
[sablono.util :refer :all])
;; TODO: Fix emit-constant exception for JSValue.
Expand All @@ -9,11 +11,29 @@
(:import cljs.tagged_literals.JSValue))

(defprotocol ICompile
(compile-react [this] "Compile a Clojure data structure into a React fn call."))
(compile-react [this env]
"Compile a Clojure data structure into code that returns a React element."))

(defprotocol IJSValue
(to-js [x]))

(def ^:private primitive-types
"The set of primitive types that can be handled by React."
#{'clj-nil 'js/React.Element 'number 'string 'sablono.html.Element})

(defn- primitive-type?
"Return true if `tag` is a primitive type that can be handled by
React, otherwise false. "
[tags]
(and (not (empty? tags)) (set/subset? tags primitive-types)))

(defn infer-tag
"Infer the tag of `form` using `env`."
[env form]
(when env
(when-let [tags (ana/infer-tag env (ana/no-warn (ana/analyze env form)))]
(if (set? tags) tags (set [tags])))))

(defn fragment?
"Returns true if `tag` is the fragment tag \"*\", otherwise false."
[tag]
Expand Down Expand Up @@ -85,12 +105,12 @@

(defn compile-react-element
"Render an element vector as a HTML element."
[element]
[element env]
(let [[tag attrs content] (normalize/element element)]
`(~(compile-constructor tag)
~(compile-tag tag)
~(compile-attrs attrs)
~@(if content (compile-react content)))))
~@(if content (compile-react content env)))))

(defn- unevaluated?
"True if the expression has not been evaluated."
Expand All @@ -99,6 +119,14 @@
(and (seq? expr)
(not= (first expr) `quote))))

(defmacro interpret-maybe
"Macro that wraps `expr` with a call to
`sablono.interpreter/interpret` if the inferred return type is not a
primitive React type."
[expr]
(if (primitive-type? (infer-tag &env expr))
expr `(sablono.interpreter/interpret ~expr)))

(defn- form-name
"Get the name of the supplied form."
[form]
Expand All @@ -110,82 +138,83 @@
(defmulti compile-form
"Pre-compile certain standard forms, where possible."
{:private true}
form-name)
(fn [form env] (form-name form)))

(defmethod compile-form "case"
[[_ v & cases]]
[[_ v & cases] env]
`(case ~v
~@(doall (mapcat
(fn [[test hiccup]]
(if hiccup
[test (compile-html hiccup)]
[(compile-html test)]))
[test (compile-html hiccup env)]
[(compile-html test env)]))
(partition-all 2 cases)))))

(defmethod compile-form "cond"
[[_ & clauses]]
[[_ & clauses] env]
`(cond ~@(mapcat
(fn [[check expr]] [check (compile-html expr)])
(fn [[check expr]]
[check (compile-html expr env)])
(partition 2 clauses))))

(defmethod compile-form "condp"
[[_ f v & cases]]
[[_ f v & cases] env]
`(condp ~f ~v
~@(doall (mapcat
(fn [[test hiccup]]
(if hiccup
[test (compile-html hiccup)]
[(compile-html test)]))
[test (compile-html hiccup env)]
[(compile-html test env)]))
(partition-all 2 cases)))))

(defmethod compile-form "do"
[[_ & forms]]
`(do ~@(butlast forms) ~(compile-html (last forms))))
[[_ & forms] env]
`(do ~@(butlast forms) ~(compile-html (last forms) env)))

(defmethod compile-form "let"
[[_ bindings & body]]
`(let ~bindings ~@(butlast body) ~(compile-html (last body))))
[[_ bindings & body] env]
`(let ~bindings ~@(butlast body) ~(compile-html (last body) env)))

(defmethod compile-form "let*"
[[_ bindings & body]]
`(let* ~bindings ~@(butlast body) ~(compile-html (last body))))
[[_ bindings & body] env]
`(let* ~bindings ~@(butlast body) ~(compile-html (last body) env)))

(defmethod compile-form "letfn*"
[[_ bindings & body]]
`(letfn* ~bindings ~@(butlast body) ~(compile-html (last body))))
[[_ bindings & body] env]
`(letfn* ~bindings ~@(butlast body) ~(compile-html (last body) env)))

(defmethod compile-form "for"
[[_ bindings body]]
`(~'into-array (for ~bindings ~(compile-html body))))
[[_ bindings body] env]
`(~'into-array (for ~bindings ~(compile-html body env))))

(defmethod compile-form "if"
[[_ condition & body]]
`(if ~condition ~@(for [x body] (compile-html x))))
[[_ condition & body] env]
`(if ~condition ~@(for [x body] (compile-html x env))))

(defmethod compile-form "if-not"
[[_ bindings & body]]
`(if-not ~bindings ~@(doall (for [x body] (compile-html x)))))
[[_ bindings & body] env]
`(if-not ~bindings ~@(doall (for [x body] (compile-html x env)))))

(defmethod compile-form "if-some"
[[_ bindings & body]]
`(if-some ~bindings ~@(doall (for [x body] (compile-html x)))))
[[_ bindings & body] env]
`(if-some ~bindings ~@(doall (for [x body] (compile-html x env)))))

(defmethod compile-form "when"
[[_ bindings & body]]
`(when ~bindings ~@(doall (for [x body] (compile-html x)))))
[[_ bindings & body] env]
`(when ~bindings ~@(doall (for [x body] (compile-html x env)))))

(defmethod compile-form "when-not"
[[_ bindings & body]]
`(when-not ~bindings ~@(doall (for [x body] (compile-html x)))))
[[_ bindings & body] env]
`(when-not ~bindings ~@(doall (for [x body] (compile-html x env)))))

(defmethod compile-form "when-some"
[[_ bindings & body]]
`(when-some ~bindings ~@(butlast body) ~(compile-html (last body))))
[[_ bindings & body] env]
`(when-some ~bindings ~@(butlast body) ~(compile-html (last body) env)))

(defmethod compile-form :default
[expr]
[expr env]
(if (:inline (meta expr))
expr `(sablono.interpreter/interpret ~expr)))
expr `(interpret-maybe ~expr)))

(defn- not-hint?
"True if x is not hinted to be the supplied type."
Expand Down Expand Up @@ -225,7 +254,7 @@

(defn- element-compile-strategy
"Returns the compilation strategy to use for a given element."
[[tag attrs & content :as element]]
[[tag attrs & content :as element] env]
(cond
;; e.g. [:span "foo"]
(every? literal? element)
Expand All @@ -239,6 +268,11 @@
(and (literal? tag) (not-implicit-map? attrs))
::literal-tag-and-no-attributes

;; e.g. [:span (attrs)], return type of `attrs` is a map
(and (literal? tag)
(= '#{cljs.core/IMap} (infer-tag env attrs)))
::literal-tag-and-hinted-attributes

;; e.g. [:span ^:attrs y]
(and (literal? tag) (attrs-hint? attrs))
::literal-tag-and-hinted-attributes
Expand All @@ -261,41 +295,42 @@
"Returns an unevaluated form that will render the supplied vector as a HTML
element."
{:private true}
element-compile-strategy)
(fn [element env]
(element-compile-strategy element env)))

(defmethod compile-element ::all-literal
[element]
(compile-react-element (eval element)))
[element env]
(compile-react-element (eval element) env))

(defmethod compile-element ::literal-tag-and-attributes
[[tag attrs & content]]
[[tag attrs & content] env]
(let [[tag attrs _] (normalize/element [tag attrs])]
`(~(compile-constructor tag)
~(compile-tag tag)
~(compile-attrs attrs)
~@(map compile-html content))))
~@(map #(compile-html % env) content))))

(defmethod compile-element ::literal-tag-and-no-attributes
[[tag & content]]
(compile-element (apply vector tag {} content)))
[[tag & content] env]
(compile-element (apply vector tag {} content) env))

(defmethod compile-element ::literal-tag-and-inline-content
[[tag & content]]
(compile-element (apply vector tag {} content)))
[[tag & content] env]
(compile-element (apply vector tag {} content) env))

(defmethod compile-element ::literal-tag-and-hinted-attributes
[[tag attrs & content]]
[[tag attrs & content] env]
(let [[tag tag-attrs _] (normalize/element [tag])
attrs-sym (gensym "attrs")]
`(let [~attrs-sym ~attrs]
(apply ~(compile-constructor tag)
~(compile-tag tag)
~(compile-merge-attrs tag-attrs attrs-sym)
~(when-not (empty? content)
(mapv compile-html content))))))
(mapv #(compile-html % env) content))))))

(defmethod compile-element ::literal-tag
[[tag attrs & content]]
[[tag attrs & content] env]
(let [[tag tag-attrs _] (normalize/element [tag])
attrs-sym (gensym "attrs")]
`(let [~attrs-sym ~attrs]
Expand All @@ -306,43 +341,45 @@
~(compile-attrs tag-attrs))
(if (map? ~attrs-sym)
~(when-not (empty? content)
(mapv compile-html content))
(mapv #(compile-html % env) content))
~(when attrs
(mapv compile-html (cons attrs-sym content))))))))
(mapv #(compile-html % env) (cons attrs-sym content))))))))

(defmethod compile-element :default
[element]
[element env]
`(sablono.interpreter/interpret
[~(first element)
~@(for [x (rest element)]
(if (vector? x)
(compile-element x)
(compile-element x env)
x))]))

(defn compile-html
"Pre-compile data structures into HTML where possible."
[content]
(cond
(vector? content) (compile-element content)
(literal? content) content
(hint? content String) content
(hint? content Number) content
:else (compile-form content)))
([content]
(compile-html content nil))
([content env]
(cond
(vector? content) (compile-element content env)
(literal? content) content
(hint? content String) content
(hint? content Number) content
:else (compile-form content env))))

(extend-protocol ICompile
clojure.lang.IPersistentVector
(compile-react [this]
(compile-react [this env]
(if (element? this)
(compile-react-element this)
(compile-react (seq this))))
(compile-react-element this env)
(compile-react (seq this) env)))
clojure.lang.ISeq
(compile-react [this]
(map compile-react this))
(compile-react [this env]
(map #(compile-react % env) this))
Object
(compile-react [this]
(compile-react [this env]
this)
nil
(compile-react [this]
(compile-react [this env]
nil))

(defn- to-js-map
Expand Down
8 changes: 5 additions & 3 deletions src/sablono/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
"Compile the Hiccup `form`. Always produces code that evaluates to
React elements."
[form]
(compiler/compile-html form))
(compiler/compile-html form &env))

(defmacro html
"Compile the Hiccup `form`. Produces code that evaluates to React
elements when running under ClojureScript environment, or
om.dom.Element records when running under Clojure."
[form]
(if (cljs-env? &env)
(compiler/compile-html form)
(compiler/compile-html form &env)
`(interpreter/interpret ~form)))

(defmacro html-expand
Expand All @@ -37,7 +37,9 @@
"Define a function, but wrap its output in an implicit html macro."
[name & fdecl]
(let [[fhead fbody] (split-with #(not (or (list? %) (vector? %))) fdecl)
wrap-html (fn [[args & body]] `(~args (html ~@body)))]
wrap-html (fn [[args & body]] `(~args (html ~@body)))
tag (if (cljs-env? &env) 'js/React.Element 'sablono.html.Element)
name (vary-meta name assoc :tag tag)]
`(defn ~name
~@fhead
~@(if (vector? (first fbody))
Expand Down
Loading

1 comment on commit 3221c5b

@r0man
Copy link
Owner Author

@r0man r0man commented on 3221c5b May 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 3221c5b Previous: 4cf489b Ratio
compile-tag-only-sablono 435830 ops/sec (±2.28%) 392090 ops/sec (±0.87%) 0.90
compile-tag-only-react 441071 ops/sec (±4.79%) 388595 ops/sec (±0.86%) 0.88
compile-tag-only-reagent 315949 ops/sec (±1.36%) 311565 ops/sec (±0.73%) 0.99
compile-tag-only-uix 986539 ops/sec (±2.31%) 1095319 ops/sec (±1.13%) 1.11
compile-class-attribute-sablono 344139 ops/sec (±2.53%) 290154 ops/sec (±1.07%) 0.84
compile-class-attribute-react 341839 ops/sec (±2.26%) 292491 ops/sec (±0.81%) 0.86
compile-class-attribute-reagent 180345 ops/sec (±12.64%) 202398 ops/sec (±0.72%) 1.12
compile-class-attribute-uix 675983 ops/sec (±1.73%) 584525 ops/sec (±0.58%) 0.86
compile-class-and-id-attributes-sablono 254076 ops/sec (±1.63%) 231864 ops/sec (±0.70%) 0.91
compile-class-and-id-attributes-react 242392 ops/sec (±1.56%) 228083 ops/sec (±0.76%) 0.94
compile-class-and-id-attributes-reagent 142982 ops/sec (±1.31%) 151385 ops/sec (±1.25%) 1.06
compile-class-and-id-attributes-uix 414418 ops/sec (±1.12%) 403546 ops/sec (±0.91%) 0.97
compile-nested-literals-sablono 39347 ops/sec (±1.47%) 40210 ops/sec (±0.93%) 1.02
compile-nested-literals-react 38268 ops/sec (±2.09%) 40718 ops/sec (±0.60%) 1.06
compile-nested-literals-reagent 27910 ops/sec (±1.67%) 31463 ops/sec (±0.70%) 1.13
compile-nested-literals-uix 61801 ops/sec (±2.03%) 59632 ops/sec (±0.92%) 0.96
interpret-attributes-sablono 61132 ops/sec (±13.42%) 104144 ops/sec (±1.43%) 1.70
interpret-attributes-react 278793 ops/sec (±2.55%) 250704 ops/sec (±0.65%) 0.90
interpret-attributes-reagent 182794 ops/sec (±2.78%) 182936 ops/sec (±0.54%) 1.00
interpret-attributes-uix 152603 ops/sec (±8.26%) 173888 ops/sec (±4.32%) 1.14
interpret-hinted-attributes-sablono 73043 ops/sec (±2.39%) 108642 ops/sec (±0.82%) 1.49
interpret-hinted-attributes-react 299015 ops/sec (±2.34%) 252697 ops/sec (±0.81%) 0.85
interpret-hinted-attributes-reagent 167495 ops/sec (±1.78%) 184297 ops/sec (±0.77%) 1.10
interpret-hinted-attributes-uix 155588 ops/sec (±2.56%) 176823 ops/sec (±0.90%) 1.14
compile-attributes-children-sablono 169560 ops/sec (±2.53%) 157224 ops/sec (±0.92%) 0.93
compile-attributes-children-react 151668 ops/sec (±1.27%) 156996 ops/sec (±1.25%) 1.04
compile-attributes-children-reagent 103591 ops/sec (±1.27%) 118990 ops/sec (±0.75%) 1.15
compile-attributes-children-uix 286151 ops/sec (±2.22%) 265129 ops/sec (±0.97%) 0.93
compile-when-form-sablono 86708 ops/sec (±2.31%) 76215 ops/sec (±0.91%) 0.88
compile-when-form-react 85912 ops/sec (±2.49%) 76460 ops/sec (±1.19%) 0.89
compile-when-form-reagent 58018 ops/sec (±1.53%) 61772 ops/sec (±1.21%) 1.06
compile-when-form-uix 119248 ops/sec (±1.49%) 118875 ops/sec (±0.56%) 1.00

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.