Skip to content

Commit 09ff331

Browse files
mfikesswannodette
authored andcommitted
CLJS-2866: Predicate-induced type inference
Look for if tests that look like the simple application of a predicate to a local, and in that case, map certain predicates to the tags that satisfying that predicate implies. For example a (string? x) test implies that x has the tag string in the then branch of an if. Likewise check for the use of satisfies? and instance? and induce tags accordingly based on the type argument. For core predicates that delegate to satisfies? or instance?, we have hard-coded entries in the table that essentially cause things to behave as if the predicate were inlined. For example, (counted? x) behaves just like (satisfies? ICounted x). We only induce tags if no tag previously exists (or a tag exists but it is the special any tag.) This ensures that we don't override type hints, or generally revise existing logic. (We really only induce a tag as a last resort.) Arrange the code so that we can add more complicated induced tags in the future (perhaps allowing for logical connectives in the test, for example.)
1 parent 289014c commit 09ff331

5 files changed

Lines changed: 167 additions & 5 deletions

File tree

src/main/cljs/cljs/core.cljs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9960,7 +9960,7 @@ reduces them without incurring seq initialization"
99609960

99619961
; Use the new, more efficient, IPrintWithWriter interface when possible.
99629962
(satisfies? IPrintWithWriter obj)
9963-
(-pr-writer obj writer opts)
9963+
(-pr-writer ^any obj writer opts)
99649964

99659965
(or (true? obj) (false? obj))
99669966
(-write writer (str obj))

src/main/clojure/cljs/analyzer.cljc

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"The namespace of the constants table as a symbol."
6969
'cljs.core.constants)
7070

71+
(def ^:private identity-counter (atom 0))
72+
73+
(defn- add-identity [m]
74+
(assoc m :identity (swap! identity-counter inc)))
75+
7176
#?(:clj
7277
(def transit-read-opts
7378
(try
@@ -1440,14 +1445,122 @@
14401445
:form form}
14411446
(var-ast env sym)))
14421447

1448+
(def ^:private predicate->tag
1449+
'{
1450+
;; Base values
1451+
cljs.core/nil? clj-nil
1452+
cljs.core/undefined? clj-nil
1453+
cljs.core/false? boolean
1454+
cljs.core/true? boolean
1455+
cljs.core/zero? number
1456+
cljs.core/infinite? number
1457+
1458+
;; Base types
1459+
cljs.core/boolean? boolean
1460+
cljs.core/string? string
1461+
cljs.core/char? string
1462+
cljs.core/number? number
1463+
cljs.core/integer? number
1464+
cljs.core/float? number
1465+
cljs.core/double? number
1466+
cljs.core/array? array
1467+
cljs.core/seq? seq
1468+
1469+
;; JavaScript types
1470+
cljs.core/regexp? js/RegExp
1471+
1472+
;; Types
1473+
cljs.core/keyword? cljs.core/Keyword
1474+
cljs.core/var? cljs.core/Var
1475+
cljs.core/symbol? cljs.core/Symbol
1476+
cljs.core/volatile? cljs.core/Volatile
1477+
cljs.core/delay? cljs.core/Delay
1478+
cljs.core/reduced? cljs.core/Reduced
1479+
1480+
;; Protocols
1481+
cljs.core/map-entry? cljs.core/IMapEntry
1482+
cljs.core/reversible? cljs.core/IReversible
1483+
cljs.core/uuid? cljs.core/IUUID
1484+
cljs.core/tagged-literal? cljs.core/ITaggedLiteral
1485+
cljs.core/iterable? cljs.core/IIterable
1486+
cljs.core/cloneable? cljs.core/ICloneable
1487+
cljs.core/inst? cljs.core/Inst
1488+
cljs.core/counted? cljs.core/ICounted
1489+
cljs.core/indexed? cljs.core/IIndexed
1490+
cljs.core/coll? cljs.core/ICollection
1491+
cljs.core/set? cljs.core/ISet
1492+
cljs.core/associative? cljs.core/IAssociative
1493+
cljs.core/ifind? cljs.core/IFind
1494+
cljs.core/sequential? cljs.core/ISequential
1495+
cljs.core/sorted? cljs.core/ISorted
1496+
cljs.core/reduceable cljs.core/IReduce
1497+
cljs.core/map? cljs.core/IMap
1498+
cljs.core/list? cljs.core/IList
1499+
cljs.core/record? cljs.core/IRecord
1500+
cljs.core/vector? cljs.core/IVector
1501+
cljs.core/chunked-seq? cljs.core/IChunkedSeq
1502+
cljs.core/ifn? cljs.core/IFn
1503+
1504+
;; Composites
1505+
cljs.core/seqable? #{cljs.core/ISeqable array string}
1506+
cljs.core/ident? #{cljs.core/Keyword cljs.core/Symbol}
1507+
})
1508+
1509+
(defn- simple-predicate-induced-tag
1510+
"Look for a predicate-induced tag when the test expression is a simple
1511+
application of a predicate to a local, as in (string? x)."
1512+
[env test]
1513+
(when (and (list? test)
1514+
(== 2 (count test))
1515+
(every? symbol? test))
1516+
(let [analyzed-fn (no-warn (analyze (assoc env :context :expr) (first test)))]
1517+
(when (= :var (:op analyzed-fn))
1518+
(when-let [tag (predicate->tag (:name analyzed-fn))]
1519+
(let [sym (last test)]
1520+
(when (and (nil? (namespace sym))
1521+
(get-in env [:locals sym]))
1522+
[sym tag])))))))
1523+
1524+
(defn- type-check-induced-tag
1525+
"Look for a type-check-induced tag when the test expression is the use of
1526+
satisfies? or instance? on a local, as in (satisfies? ICounted x)."
1527+
[env test]
1528+
(when (and (list? test)
1529+
(== 3 (count test))
1530+
(every? symbol? test))
1531+
(let [analyzed-fn (no-warn (analyze (assoc env :context :expr) (first test)))]
1532+
(when (= :var (:op analyzed-fn))
1533+
(when ('#{cljs.core/satisfies? cljs.core/instance?} (:name analyzed-fn))
1534+
(let [analyzed-type (no-warn (analyze (assoc env :context :expr) (second test)))
1535+
tag (:name analyzed-type)
1536+
sym (last test)]
1537+
(when (and (= :var (:op analyzed-type))
1538+
(nil? (namespace sym))
1539+
(get-in env [:locals sym]))
1540+
[sym tag])))))))
1541+
1542+
(defn- add-predicate-induced-tags
1543+
"Looks at the test and adds any tags which are induced by virtue
1544+
of the predicate being satisfied. For exmaple in (if (string? x) x :bar)
1545+
the local x in the then branch must be of string type."
1546+
[env test]
1547+
(let [[local tag] (or (simple-predicate-induced-tag env test)
1548+
(type-check-induced-tag env test))]
1549+
(cond-> env
1550+
local (update-in [:locals local :tag] (fn [prev-tag]
1551+
(if (or (nil? prev-tag)
1552+
(= 'any prev-tag))
1553+
tag
1554+
prev-tag))))))
1555+
14431556
(defmethod parse 'if
14441557
[op env [_ test then else :as form] name _]
14451558
(when (< (count form) 3)
14461559
(throw (error env "Too few arguments to if")))
14471560
(when (> (count form) 4)
14481561
(throw (error env "Too many arguments to if")))
14491562
(let [test-expr (disallowing-recur (analyze (assoc env :context :expr) test))
1450-
then-expr (allowing-redef (analyze env then))
1563+
then-expr (allowing-redef (analyze (add-predicate-induced-tags env test) then))
14511564
else-expr (allowing-redef (analyze env else))]
14521565
{:env env :op :if :form form
14531566
:test test-expr :then then-expr :else else-expr
@@ -2105,7 +2218,8 @@
21052218
:variadic? (:variadic? init-expr)
21062219
:max-fixed-arity (:max-fixed-arity init-expr)
21072220
:method-params (map :params (:methods init-expr))})
2108-
be)]
2221+
be)
2222+
be (add-identity be)]
21092223
(recur (conj bes be)
21102224
(assoc-in env [:locals name] be)
21112225
(next bindings))))

src/main/clojure/cljs/compiler.cljc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
:else d))))
8787

8888
(defn hash-scope [s]
89-
#?(:clj (System/identityHashCode s)
89+
#?(:clj (or (:identity s) (System/identityHashCode s))
9090
:cljs (hash-combine (-hash ^not-native (:name s))
9191
(shadow-depth s))))
9292

src/test/cljs/cljs/inference_test.cljs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
(ns cljs.inference-test
1010
(:require-macros [cljs.inference-util])
11-
(:require [cljs.test :refer [deftest]]
11+
(:require [cljs.test :refer [deftest is]]
1212
[cljs.pprint]))
1313

1414
(deftest test-cljs-2825
@@ -78,3 +78,13 @@
7878
(uuid? nil)
7979
(tagged-literal? nil)
8080
(cljs.pprint/float? nil)))
81+
82+
(deftest cljs-2866-test
83+
;; Here we are testing that in the JavaScript emitted,
84+
;; the gensym generated for curr is being passed to dec
85+
(is (zero? ((fn [x]
86+
(while (pos? @x)
87+
(let [curr @x]
88+
(when (number? curr)
89+
(reset! x (dec curr)))))
90+
@x) (atom 1)))))

src/test/clojure/cljs/analyzer_tests.clj

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,44 @@
231231
(:tag (analyze test-env '(if x "foo" 1)))))
232232
'#{number string})))
233233

234+
(deftest if-induced-inference
235+
(is (= (a/no-warn
236+
(e/with-compiler-env test-cenv
237+
(:tag (a/analyze test-env '(let [x ^any []] (if (nil? x) x :kw))))))
238+
'#{clj-nil cljs.core/Keyword}))
239+
(is (= (a/no-warn
240+
(e/with-compiler-env test-cenv
241+
(:tag (a/analyze test-env '(let [x ^any []] (if (boolean? x) x :kw))))))
242+
'#{boolean cljs.core/Keyword}))
243+
(is (= (a/no-warn
244+
(e/with-compiler-env test-cenv
245+
(:tag (a/analyze test-env '(let [x ^any []] (if (number? x) x :kw))))))
246+
'#{number cljs.core/Keyword}))
247+
(is (= (a/no-warn
248+
(e/with-compiler-env test-cenv
249+
(:tag (a/analyze test-env '(let [x ^any []] (if (double? x) x :kw))))))
250+
'#{number cljs.core/Keyword}))
251+
(is (= (a/no-warn
252+
(e/with-compiler-env test-cenv
253+
(:tag (a/analyze test-env '(let [x ^any []] (if (float? x) x :kw))))))
254+
'#{number cljs.core/Keyword}))
255+
(is (= (a/no-warn
256+
(e/with-compiler-env test-cenv
257+
(:tag (a/analyze test-env '(let [x ^any []] (if (integer? x) x :kw))))))
258+
'#{number cljs.core/Keyword}))
259+
(is (= (a/no-warn
260+
(e/with-compiler-env test-cenv
261+
(:tag (a/analyze test-env '(let [x ^any []] (if (seq? x) x :kw))))))
262+
'#{seq cljs.core/Keyword}))
263+
(is (= (a/no-warn
264+
(e/with-compiler-env test-cenv
265+
(:tag (a/analyze test-env '(let [x ^any []] (if (array? x) x :kw))))))
266+
'#{array cljs.core/Keyword}))
267+
(is (= (a/no-warn
268+
(e/with-compiler-env test-cenv
269+
(:tag (a/analyze test-env '(let [x ^any []] (if (seqable? x) x :kw))))))
270+
'#{cljs.core/ISeqable array string cljs.core/Keyword})))
271+
234272
(deftest method-inference
235273
(is (= (e/with-compiler-env test-cenv
236274
(:tag (analyze test-env '(.foo js/bar))))

0 commit comments

Comments
 (0)