Skip to content

Commit 927f221

Browse files
authored
CLJS-3260: and/or optimization as compiler pass (#94)
Introduce and/or optimization as a compiler pass. Tests for all the various interesting cases. No serious compiler perf regression, examine the codegen for persistent data structures looks good.
1 parent d82b10a commit 927f221

File tree

4 files changed

+223
-28
lines changed

4 files changed

+223
-28
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
;; Copyright (c) Rich Hickey. All rights reserved.
2+
;; The use and distribution terms for this software are covered by the
3+
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
4+
;; which can be found in the file epl-v10.html at the root of this distribution.
5+
;; By using this software in any fashion, you are agreeing to be bound by
6+
;; the terms of this license.
7+
;; You must not remove this notice, or any other, from this software.
8+
9+
(ns cljs.analyzer.passes.and-or)
10+
11+
(def simple-ops
12+
#{:var :js-var :local :invoke :const :host-field :host-call :js :quote})
13+
14+
(defn ->expr-env [ast]
15+
(assoc-in ast [:env :context] :expr))
16+
17+
(defn simple-op? [ast]
18+
(contains? simple-ops (:op ast)))
19+
20+
(defn simple-test-expr?
21+
[{:keys [op] :as ast}]
22+
(boolean
23+
(and (simple-op? ast)
24+
('#{boolean seq}
25+
(or (:tag ast)
26+
(when (#{:local :var} op)
27+
(-> ast :info :tag)))))))
28+
29+
(defn single-binding-let? [ast]
30+
(and (= :let (:op ast))
31+
(= 1 (count (-> ast :bindings)))))
32+
33+
(defn no-statements? [let-ast]
34+
(= [] (-> let-ast :body :statements)))
35+
36+
(defn returns-if? [let-ast]
37+
(= :if (-> let-ast :body :ret :op)))
38+
39+
(defn simple-test-binding-let? [ast]
40+
(and (single-binding-let? ast)
41+
(no-statements? ast)
42+
(simple-test-expr? (-> ast :bindings first :init))
43+
(returns-if? ast)))
44+
45+
(defn test=then? [if-ast]
46+
;; remove :env, if same, local will differ only by
47+
;; :context (:expr | :statement)
48+
(= (dissoc (:test if-ast) :env)
49+
(dissoc (:then if-ast) :env)))
50+
51+
(defn test=else? [if-ast]
52+
;; remove :env, if same, local will differ only by
53+
;; :context (:expr | :statement)
54+
(= (dissoc (:test if-ast) :env)
55+
(dissoc (:else if-ast) :env)))
56+
57+
(defn simple-and? [ast]
58+
(and (simple-test-binding-let? ast)
59+
(test=else? (-> ast :body :ret))))
60+
61+
(defn simple-or? [ast]
62+
(and (simple-test-binding-let? ast)
63+
(test=then? (-> ast :body :ret))))
64+
65+
(defn optimizable-and? [ast]
66+
(and (simple-and? ast)
67+
(simple-test-expr? (-> ast :body :ret :then))))
68+
69+
(defn optimizable-or? [ast]
70+
(and (simple-or? ast)
71+
(simple-test-expr? (-> ast :body :ret :else))))
72+
73+
(defn optimize-and [ast]
74+
{:op :js
75+
:env (:env ast)
76+
:segs ["((" ") && (" "))"]
77+
:args [(-> ast :bindings first :init)
78+
(->expr-env (-> ast :body :ret :then))]
79+
:form (:form ast)
80+
:children [:args]
81+
:tag 'boolean})
82+
83+
(defn optimize-or [ast]
84+
{:op :js
85+
:env (:env ast)
86+
:segs ["((" ") || (" "))"]
87+
:args [(-> ast :bindings first :init)
88+
(->expr-env (-> ast :body :ret :else))]
89+
:form (:form ast)
90+
:children [:args]
91+
:tag 'boolean})
92+
93+
(defn optimize [env ast _]
94+
(cond
95+
(optimizable-and? ast) (optimize-and ast)
96+
(optimizable-or? ast) (optimize-or ast)
97+
:else ast))

src/main/clojure/cljs/analyzer.cljc

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
no-warn with-warning-handlers wrapping-errors]]
1515
[cljs.env.macros :refer [ensure]]))
1616
#?(:clj (:require [cljs.analyzer.impl :as impl]
17+
[cljs.analyzer.passes.and-or :as and-or]
1718
[cljs.env :as env :refer [ensure]]
1819
[cljs.externs :as externs]
1920
[cljs.js-deps :as deps]
@@ -26,6 +27,7 @@
2627
[clojure.tools.reader :as reader]
2728
[clojure.tools.reader.reader-types :as readers])
2829
:cljs (:require [cljs.analyzer.impl :as impl]
30+
[cljs.analyzer.passes.and-or :as and-or]
2931
[cljs.env :as env]
3032
[cljs.reader :as edn]
3133
[cljs.tagged-literals :as tags]
@@ -4194,8 +4196,8 @@
41944196
tag (assoc :tag tag))))))
41954197

41964198
(def default-passes
4197-
#?(:clj [infer-type check-invoke-arg-types ns-side-effects]
4198-
:cljs [infer-type check-invoke-arg-types]))
4199+
#?(:clj [infer-type and-or/optimize check-invoke-arg-types ns-side-effects]
4200+
:cljs [infer-type and-or/optimize check-invoke-arg-types]))
41994201

42004202
(defn analyze* [env form name opts]
42014203
(let [passes *passes*

src/main/clojure/cljs/core.cljc

+4-26
Original file line numberDiff line numberDiff line change
@@ -872,22 +872,8 @@
872872
([] true)
873873
([x] x)
874874
([x & next]
875-
(core/let [forms (concat [x] next)]
876-
(core/cond
877-
(every? #(simple-test-expr? &env %)
878-
(map #(cljs.analyzer/no-warn (cljs.analyzer/analyze &env %)) forms))
879-
(core/let [and-str (core/->> (repeat (count forms) "(~{})")
880-
(interpose " && ")
881-
(#(concat ["("] % [")"]))
882-
(apply core/str))]
883-
(bool-expr `(~'js* ~and-str ~@forms)))
884-
885-
(typed-expr? &env x '#{boolean})
886-
`(if ~x (and ~@next) false)
887-
888-
:else
889-
`(let [and# ~x]
890-
(if and# (and ~@next) and#))))))
875+
`(let [and# ~x]
876+
(if and# (and ~@next) and#))))
891877

892878
(core/defmacro or
893879
"Evaluates exprs one at a time, from left to right. If a form
@@ -897,16 +883,8 @@
897883
([] nil)
898884
([x] x)
899885
([x & next]
900-
(core/let [forms (concat [x] next)]
901-
(if (every? #(simple-test-expr? &env %)
902-
(map #(cljs.analyzer/no-warn (cljs.analyzer/analyze &env %)) forms))
903-
(core/let [or-str (core/->> (repeat (count forms) "(~{})")
904-
(interpose " || ")
905-
(#(concat ["("] % [")"]))
906-
(apply core/str))]
907-
(bool-expr `(~'js* ~or-str ~@forms)))
908-
`(let [or# ~x]
909-
(if or# or# (or ~@next)))))))
886+
`(let [or# ~x]
887+
(if or# or# (or ~@next)))))
910888

911889
(core/defmacro nil? [x]
912890
`(coercive-= ~x nil))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
;; Copyright (c) Rich Hickey. All rights reserved.
2+
;; The use and distribution terms for this software are covered by the
3+
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
4+
;; which can be found in the file epl-v10.html at the root of this distribution.
5+
;; By using this software in any fashion, you are agreeing to be bound by
6+
;; the terms of this license.
7+
;; You must not remove this notice, or any other, from this software.
8+
9+
(ns cljs.analyzer-pass-tests
10+
(:require [cljs.analyzer :as ana]
11+
[cljs.analyzer.passes.and-or :as and-or]
12+
[cljs.analyzer-tests :as ana-tests :refer [analyze]]
13+
[cljs.compiler :as comp]
14+
[cljs.compiler-tests :as comp-tests :refer [compile-form-seq emit]]
15+
[cljs.env :as env]
16+
[clojure.string :as string]
17+
[clojure.test :as test :refer [deftest is testing]]))
18+
19+
(deftest test-and-or-code-gen-pass
20+
(testing "and/or optimization code gen pass"
21+
(let [expr-env (assoc (ana/empty-env) :context :expr)
22+
ast (->> `(and true false)
23+
(analyze expr-env))
24+
code (with-out-str (emit ast))]
25+
(is (= code "((true) && (false))")))
26+
(let [expr-env (assoc (ana/empty-env) :context :expr)
27+
ast (analyze expr-env
28+
`(and true (or true false)))
29+
code (with-out-str (emit ast))]
30+
(is (= code "((true) && (((true) || (false))))")))
31+
(let [expr-env (assoc (ana/empty-env) :context :expr)
32+
ast (analyze expr-env
33+
`(or true (and false true)))
34+
code (with-out-str (emit ast))]
35+
(is (= code "((true) || (((false) && (true))))")))
36+
(let [expr-env (assoc (ana/empty-env) :context :expr)
37+
local (gensym)
38+
ast (analyze expr-env
39+
`(let [~local true]
40+
(and true (or ~local false))))
41+
code (with-out-str (emit ast))]
42+
(is (= code
43+
(string/replace
44+
"(function (){var $SYM = true;\nreturn ((true) && ((($SYM) || (false))));\n})()"
45+
"$SYM" (str local)))))))
46+
47+
(deftest test-and-or-local
48+
(testing "and/or optimizable with boolean local"
49+
(let [expr-env (assoc (ana/empty-env) :context :expr)
50+
ast (->> `(let [x# true]
51+
(and x# true false))
52+
(analyze expr-env))
53+
code (with-out-str (emit ast))]
54+
(is (= 2 (count (re-seq #"&&" code)))))))
55+
56+
(deftest test-and-or-boolean-fn-arg
57+
(testing "and/or optimizable with boolean fn arg"
58+
(let [arg (with-meta 'x {:tag 'boolean})
59+
ast (analyze (assoc (ana/empty-env) :context :expr)
60+
`(fn [~arg]
61+
(and ~arg false false)))
62+
code (with-out-str (emit ast))]
63+
(is (= 2 (count (re-seq #"&&" code)))))))
64+
65+
(deftest test-and-or-boolean-var
66+
(testing "and/or optimizable with boolean var"
67+
(let [code (env/with-compiler-env (env/default-compiler-env)
68+
(compile-form-seq
69+
'[(ns foo.bar)
70+
(def baz true)
71+
(defn woz []
72+
(and baz false))]))]
73+
(is (= 1 (count (re-seq #"&&" code)))))))
74+
75+
(deftest test-and-or-js-boolean-var
76+
(testing "and/or optimizable with js boolean var"
77+
(let [code (env/with-compiler-env (env/default-compiler-env)
78+
(compile-form-seq
79+
'[(ns foo.bar)
80+
(defn baz []
81+
(and ^boolean js/woz false))]))]
82+
(is (= 1 (count (re-seq #"&&" code)))))))
83+
84+
(deftest test-and-or-host-call
85+
(testing "and/or optimizable with host call"
86+
(let [code (env/with-compiler-env (env/default-compiler-env)
87+
(compile-form-seq
88+
'[(ns foo.bar)
89+
(defn bar [x]
90+
(and ^boolean (.woz x) false))]))]
91+
(is (= 1 (count (re-seq #"&&" code)))))))
92+
93+
(deftest test-and-or-host-field
94+
(testing "and/or optimizable with host field"
95+
(let [code (env/with-compiler-env (env/default-compiler-env)
96+
(compile-form-seq
97+
'[(ns foo.bar)
98+
(defn bar [x]
99+
(and ^boolean (.-woz x) false))]))]
100+
(is (= 1 (count (re-seq #"&&" code)))))))
101+
102+
(deftest test-core-predicates
103+
(testing "and/or optimizable with core predicates"
104+
(let [code (env/with-compiler-env (env/default-compiler-env)
105+
(comp/with-core-cljs {}
106+
(fn []
107+
(compile-form-seq
108+
'[(ns foo.bar)
109+
(defn bar []
110+
(and (even? 1) false))]))))]
111+
(is (= 1 (count (re-seq #"&&" code)))))))
112+
113+
(comment
114+
(test/run-tests)
115+
116+
(require '[clojure.pprint :refer [pprint]])
117+
118+
)

0 commit comments

Comments
 (0)