Skip to content

CLJS-3260: and/or optimization as compiler pass #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1e46444
(wip) remove macro optimizations
swannodette May 11, 2021
90559ae
(wip) outlining the problem
swannodette May 11, 2021
ab4b08b
(wip) switch to analyzer ns
swannodette May 12, 2021
f74cb0b
(wip)
swannodette May 12, 2021
ff53839
(wip)
swannodette May 12, 2021
85d341d
(wip)
swannodette May 12, 2021
466f74c
(wip)
swannodette May 12, 2021
4e975f8
(wip)
swannodette May 12, 2021
97e6985
(wip)
swannodette May 12, 2021
8ebd991
(wip) pass
swannodette May 12, 2021
522695f
(wip) less trivial
swannodette May 12, 2021
aeb0896
(wip) another interesting case
swannodette May 12, 2021
4ba58e6
(wip) reorg
swannodette May 12, 2021
5575fb7
(wip) fix pass order
swannodette May 12, 2021
b886d3f
test explicitly tagged vars
swannodette May 12, 2021
1ad8670
(wip) buggy case
swannodette May 12, 2021
12b363e
(wip) add the other required tests
swannodette May 12, 2021
6e41b2f
(wip) a bit more info on the issue
swannodette May 12, 2021
1ed822e
(wip) handle var case, pass runs after type inference so local and fn…
swannodette May 12, 2021
c15d098
(wip) typo / some cleanup
swannodette May 12, 2021
a3be520
add nested examples
swannodette May 12, 2021
dc76e04
compound bool expr w/ bool local test
swannodette May 12, 2021
2417999
add boolean var and js boolea var tests
swannodette May 12, 2021
87b4e4b
use and-or pass test helper
swannodette May 12, 2021
e46b585
cleanup, host calls & fields
swannodette May 12, 2021
ec2cd49
fix up passes helper, and-or needs infer-type to work, minor tweaks
swannodette May 12, 2021
f650142
remove helper wire in pass, one failing runtime test
swannodette May 12, 2021
7ac2727
need extra parens, cljs-2585 regression fixed
swannodette May 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/main/cljs/cljs/analyzer/passes/and_or.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be bound by
;; the terms of this license.
;; You must not remove this notice, or any other, from this software.

(ns cljs.analyzer.passes.and-or)

(def simple-ops
#{:var :js-var :local :invoke :const :host-field :host-call :js :quote})

(defn ->expr-env [ast]
(assoc-in ast [:env :context] :expr))

(defn simple-op? [ast]
(contains? simple-ops (:op ast)))

(defn simple-test-expr?
[{:keys [op] :as ast}]
(boolean
(and (simple-op? ast)
('#{boolean seq}
(or (:tag ast)
(when (#{:local :var} op)
(-> ast :info :tag)))))))

(defn single-binding-let? [ast]
(and (= :let (:op ast))
(= 1 (count (-> ast :bindings)))))

(defn no-statements? [let-ast]
(= [] (-> let-ast :body :statements)))

(defn returns-if? [let-ast]
(= :if (-> let-ast :body :ret :op)))

(defn simple-test-binding-let? [ast]
(and (single-binding-let? ast)
(no-statements? ast)
(simple-test-expr? (-> ast :bindings first :init))
(returns-if? ast)))

(defn test=then? [if-ast]
;; remove :env, if same, local will differ only by
;; :context (:expr | :statement)
(= (dissoc (:test if-ast) :env)
(dissoc (:then if-ast) :env)))

(defn test=else? [if-ast]
;; remove :env, if same, local will differ only by
;; :context (:expr | :statement)
(= (dissoc (:test if-ast) :env)
(dissoc (:else if-ast) :env)))

(defn simple-and? [ast]
(and (simple-test-binding-let? ast)
(test=else? (-> ast :body :ret))))

(defn simple-or? [ast]
(and (simple-test-binding-let? ast)
(test=then? (-> ast :body :ret))))

(defn optimizable-and? [ast]
(and (simple-and? ast)
(simple-test-expr? (-> ast :body :ret :then))))

(defn optimizable-or? [ast]
(and (simple-or? ast)
(simple-test-expr? (-> ast :body :ret :else))))

(defn optimize-and [ast]
{:op :js
:env (:env ast)
:segs ["((" ") && (" "))"]
:args [(-> ast :bindings first :init)
(->expr-env (-> ast :body :ret :then))]
:form (:form ast)
:children [:args]
:tag 'boolean})

(defn optimize-or [ast]
{:op :js
:env (:env ast)
:segs ["((" ") || (" "))"]
:args [(-> ast :bindings first :init)
(->expr-env (-> ast :body :ret :else))]
:form (:form ast)
:children [:args]
:tag 'boolean})

(defn optimize [env ast _]
(cond
(optimizable-and? ast) (optimize-and ast)
(optimizable-or? ast) (optimize-or ast)
:else ast))
6 changes: 4 additions & 2 deletions src/main/clojure/cljs/analyzer.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
no-warn with-warning-handlers wrapping-errors]]
[cljs.env.macros :refer [ensure]]))
#?(:clj (:require [cljs.analyzer.impl :as impl]
[cljs.analyzer.passes.and-or :as and-or]
[cljs.env :as env :refer [ensure]]
[cljs.externs :as externs]
[cljs.js-deps :as deps]
Expand All @@ -26,6 +27,7 @@
[clojure.tools.reader :as reader]
[clojure.tools.reader.reader-types :as readers])
:cljs (:require [cljs.analyzer.impl :as impl]
[cljs.analyzer.passes.and-or :as and-or]
[cljs.env :as env]
[cljs.reader :as edn]
[cljs.tagged-literals :as tags]
Expand Down Expand Up @@ -4194,8 +4196,8 @@
tag (assoc :tag tag))))))

(def default-passes
#?(:clj [infer-type check-invoke-arg-types ns-side-effects]
:cljs [infer-type check-invoke-arg-types]))
#?(:clj [infer-type and-or/optimize check-invoke-arg-types ns-side-effects]
:cljs [infer-type and-or/optimize check-invoke-arg-types]))

(defn analyze* [env form name opts]
(let [passes *passes*
Expand Down
30 changes: 4 additions & 26 deletions src/main/clojure/cljs/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -872,22 +872,8 @@
([] true)
([x] x)
([x & next]
(core/let [forms (concat [x] next)]
(core/cond
(every? #(simple-test-expr? &env %)
(map #(cljs.analyzer/no-warn (cljs.analyzer/analyze &env %)) forms))
(core/let [and-str (core/->> (repeat (count forms) "(~{})")
(interpose " && ")
(#(concat ["("] % [")"]))
(apply core/str))]
(bool-expr `(~'js* ~and-str ~@forms)))

(typed-expr? &env x '#{boolean})
`(if ~x (and ~@next) false)

:else
`(let [and# ~x]
(if and# (and ~@next) and#))))))
`(let [and# ~x]
(if and# (and ~@next) and#))))

(core/defmacro or
"Evaluates exprs one at a time, from left to right. If a form
Expand All @@ -897,16 +883,8 @@
([] nil)
([x] x)
([x & next]
(core/let [forms (concat [x] next)]
(if (every? #(simple-test-expr? &env %)
(map #(cljs.analyzer/no-warn (cljs.analyzer/analyze &env %)) forms))
(core/let [or-str (core/->> (repeat (count forms) "(~{})")
(interpose " || ")
(#(concat ["("] % [")"]))
(apply core/str))]
(bool-expr `(~'js* ~or-str ~@forms)))
`(let [or# ~x]
(if or# or# (or ~@next)))))))
`(let [or# ~x]
(if or# or# (or ~@next)))))

(core/defmacro nil? [x]
`(coercive-= ~x nil))
Expand Down
118 changes: 118 additions & 0 deletions src/test/clojure/cljs/analyzer_pass_tests.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be bound by
;; the terms of this license.
;; You must not remove this notice, or any other, from this software.

(ns cljs.analyzer-pass-tests
(:require [cljs.analyzer :as ana]
[cljs.analyzer.passes.and-or :as and-or]
[cljs.analyzer-tests :as ana-tests :refer [analyze]]
[cljs.compiler :as comp]
[cljs.compiler-tests :as comp-tests :refer [compile-form-seq emit]]
[cljs.env :as env]
[clojure.string :as string]
[clojure.test :as test :refer [deftest is testing]]))

(deftest test-and-or-code-gen-pass
(testing "and/or optimization code gen pass"
(let [expr-env (assoc (ana/empty-env) :context :expr)
ast (->> `(and true false)
(analyze expr-env))
code (with-out-str (emit ast))]
(is (= code "((true) && (false))")))
(let [expr-env (assoc (ana/empty-env) :context :expr)
ast (analyze expr-env
`(and true (or true false)))
code (with-out-str (emit ast))]
(is (= code "((true) && (((true) || (false))))")))
(let [expr-env (assoc (ana/empty-env) :context :expr)
ast (analyze expr-env
`(or true (and false true)))
code (with-out-str (emit ast))]
(is (= code "((true) || (((false) && (true))))")))
(let [expr-env (assoc (ana/empty-env) :context :expr)
local (gensym)
ast (analyze expr-env
`(let [~local true]
(and true (or ~local false))))
code (with-out-str (emit ast))]
(is (= code
(string/replace
"(function (){var $SYM = true;\nreturn ((true) && ((($SYM) || (false))));\n})()"
"$SYM" (str local)))))))

(deftest test-and-or-local
(testing "and/or optimizable with boolean local"
(let [expr-env (assoc (ana/empty-env) :context :expr)
ast (->> `(let [x# true]
(and x# true false))
(analyze expr-env))
code (with-out-str (emit ast))]
(is (= 2 (count (re-seq #"&&" code)))))))

(deftest test-and-or-boolean-fn-arg
(testing "and/or optimizable with boolean fn arg"
(let [arg (with-meta 'x {:tag 'boolean})
ast (analyze (assoc (ana/empty-env) :context :expr)
`(fn [~arg]
(and ~arg false false)))
code (with-out-str (emit ast))]
(is (= 2 (count (re-seq #"&&" code)))))))

(deftest test-and-or-boolean-var
(testing "and/or optimizable with boolean var"
(let [code (env/with-compiler-env (env/default-compiler-env)
(compile-form-seq
'[(ns foo.bar)
(def baz true)
(defn woz []
(and baz false))]))]
(is (= 1 (count (re-seq #"&&" code)))))))

(deftest test-and-or-js-boolean-var
(testing "and/or optimizable with js boolean var"
(let [code (env/with-compiler-env (env/default-compiler-env)
(compile-form-seq
'[(ns foo.bar)
(defn baz []
(and ^boolean js/woz false))]))]
(is (= 1 (count (re-seq #"&&" code)))))))

(deftest test-and-or-host-call
(testing "and/or optimizable with host call"
(let [code (env/with-compiler-env (env/default-compiler-env)
(compile-form-seq
'[(ns foo.bar)
(defn bar [x]
(and ^boolean (.woz x) false))]))]
(is (= 1 (count (re-seq #"&&" code)))))))

(deftest test-and-or-host-field
(testing "and/or optimizable with host field"
(let [code (env/with-compiler-env (env/default-compiler-env)
(compile-form-seq
'[(ns foo.bar)
(defn bar [x]
(and ^boolean (.-woz x) false))]))]
(is (= 1 (count (re-seq #"&&" code)))))))

(deftest test-core-predicates
(testing "and/or optimizable with core predicates"
(let [code (env/with-compiler-env (env/default-compiler-env)
(comp/with-core-cljs {}
(fn []
(compile-form-seq
'[(ns foo.bar)
(defn bar []
(and (even? 1) false))]))))]
(is (= 1 (count (re-seq #"&&" code)))))))

(comment
(test/run-tests)

(require '[clojure.pprint :refer [pprint]])

)