Skip to content

Commit a373039

Browse files
mfikesdnolen
authored andcommitted
CLJS-2873: Improved inference for loop / recur
1 parent f6bdfe1 commit a373039

2 files changed

Lines changed: 98 additions & 12 deletions

File tree

src/main/clojure/cljs/analyzer.cljc

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,41 @@
891891
'prototype)})
892892
x))
893893

894+
(defn ->type-set
895+
"Ensures that a type tag is a set."
896+
[t]
897+
(if #?(:clj (set? t)
898+
:cljs (cljs-set? t))
899+
t
900+
#{t}))
901+
902+
(defn canonicalize-type [t]
903+
"Ensures that a type tag is either nil, a type symbol, or a non-singleton
904+
set of type symbols, absorbing clj-nil into seq and all types into any."
905+
(cond
906+
(symbol? t) t
907+
(empty? t) nil
908+
(== 1 (count t)) (first t)
909+
(contains? t 'any) 'any
910+
(contains? t 'seq) (let [res (disj t 'clj-nil)]
911+
(if (== 1 (count res))
912+
'seq
913+
res))
914+
:else t))
915+
916+
(defn add-types
917+
"Produces a union of types."
918+
([] 'any)
919+
([t1] t1)
920+
([t1 t2]
921+
(if (or (nil? t1)
922+
(nil? t2))
923+
'any
924+
(-> (set/union (->type-set t1) (->type-set t2))
925+
canonicalize-type)))
926+
([t1 t2 & ts]
927+
(apply add-types (add-types t1 t2) ts)))
928+
894929
(def alias->type
895930
'{object Object
896931
string String
@@ -1953,7 +1988,8 @@
19531988
fixed-arity (count params')
19541989
recur-frame {:protocol-impl (:protocol-impl env)
19551990
:params params
1956-
:flag (atom nil)}
1991+
:flag (atom nil)
1992+
:tags (atom [])}
19571993
recur-frames (cons recur-frame *recur-frames*)
19581994
body-env (assoc env :context :return :locals locals)
19591995
body-form `(do ~@body)
@@ -2230,36 +2266,51 @@
22302266
(analyze-let-body* env context exprs)))
22312267

22322268
(defn analyze-let
2233-
[encl-env [_ bindings & exprs :as form] is-loop]
2269+
[encl-env [_ bindings & exprs :as form] is-loop widened-tags]
22342270
(when-not (and (vector? bindings) (even? (count bindings)))
22352271
(throw (error encl-env "bindings must be vector of even number of elements")))
22362272
(let [context (:context encl-env)
22372273
op (if (true? is-loop) :loop :let)
2274+
bindings (if widened-tags
2275+
(vec (mapcat
2276+
(fn [[name init] widened-tag]
2277+
[(vary-meta name assoc :tag widened-tag) init])
2278+
(partition 2 bindings)
2279+
widened-tags))
2280+
bindings)
22382281
[bes env] (analyze-let-bindings encl-env bindings op)
22392282
recur-frame (when (true? is-loop)
2240-
{:params bes :flag (atom nil)})
2283+
{:params bes
2284+
:flag (atom nil)
2285+
:tags (atom (mapv :tag bes))})
22412286
recur-frames (if recur-frame
22422287
(cons recur-frame *recur-frames*)
22432288
*recur-frames*)
22442289
loop-lets (cond
22452290
(true? is-loop) *loop-lets*
22462291
(some? *loop-lets*) (cons {:params bes} *loop-lets*))
22472292
expr (analyze-let-body env context exprs recur-frames loop-lets)
2248-
children [:bindings :body]]
2249-
{:op op
2250-
:env encl-env
2251-
:bindings bes
2252-
:body (assoc expr :body? true)
2253-
:form form
2254-
:children children}))
2293+
children [:bindings :body]
2294+
nil->any (fnil identity 'any)]
2295+
(if (and is-loop
2296+
(not widened-tags)
2297+
(not= (mapv nil->any @(:tags recur-frame))
2298+
(mapv (comp nil->any :tag) bes)))
2299+
(recur encl-env form is-loop @(:tags recur-frame))
2300+
{:op op
2301+
:env encl-env
2302+
:bindings bes
2303+
:body (assoc expr :body? true)
2304+
:form form
2305+
:children children})))
22552306

22562307
(defmethod parse 'let*
22572308
[op encl-env form _ _]
2258-
(analyze-let encl-env form false))
2309+
(analyze-let encl-env form false nil))
22592310

22602311
(defmethod parse 'loop*
22612312
[op encl-env form _ _]
2262-
(analyze-let encl-env form true))
2313+
(analyze-let encl-env form true nil))
22632314

22642315
(defmethod parse 'recur
22652316
[op env [_ & exprs :as form] _ _]
@@ -2279,6 +2330,10 @@
22792330
(not add-implicit-target-object?))
22802331
(warning :protocol-impl-recur-with-target env {:form (:form (first exprs))}))
22812332
(reset! (:flag frame) true)
2333+
(swap! (:tags frame) (fn [tags]
2334+
(mapv (fn [tag expr]
2335+
(add-types tag (:tag expr)))
2336+
tags exprs)))
22822337
(assoc {:env env :op :recur :form form}
22832338
:frame frame
22842339
:exprs exprs

src/test/clojure/cljs/analyzer_tests.clj

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,37 @@
269269
(:tag (a/analyze test-env '(let [x ^any []] (if (seqable? x) x :kw))))))
270270
'#{cljs.core/ISeqable array string cljs.core/Keyword})))
271271

272+
(deftest loop-recur-inference
273+
(is (= (a/no-warn
274+
(e/with-compiler-env test-cenv
275+
(:tag (analyze test-env '(loop [x "a"] x)))))
276+
'string))
277+
(is (= (a/no-warn
278+
(e/with-compiler-env test-cenv
279+
(:tag (analyze test-env '(loop [x 10]
280+
(if (pos? x)
281+
(dec x)
282+
x))))))
283+
'number))
284+
(is (= (a/no-warn
285+
(e/with-compiler-env test-cenv
286+
(:tag (analyze test-env '((fn [p?]
287+
(loop [x nil]
288+
(if (p? x)
289+
x
290+
(recur (str x)))))
291+
11)))))
292+
'#{string clj-nil}))
293+
(is (= (a/no-warn
294+
(e/with-compiler-env test-cenv
295+
(:tag (analyze test-env '((fn [^string x]
296+
(loop [y x]
297+
(if (= "x" y)
298+
y
299+
(recur 1))))
300+
"a")))))
301+
'#{number string})))
302+
272303
(deftest method-inference
273304
(is (= (e/with-compiler-env test-cenv
274305
(:tag (analyze test-env '(.foo js/bar))))

0 commit comments

Comments
 (0)