Skip to content

Commit 5549175

Browse files
authored
Refactor has-extern? / js-tag (#246)
* fix up tests so they don't throw if no warnings * resolve-extern, which can be used by both has-extern? and js-tag, returns both resolved prefix and var info * remove various hacks around extern resolution (Number, Window, prototype etc.), resolve-extern handles everything * undefined is a ref cycle, special case * change normalize-js-tag so it marks the ctor prop * add normalize-unresolved-prefix to fix up the cases we can't find * add impl unit tests * more unit test - can finally resolve crypto.subtle, verify type inference as well * add lift-tag-to-js helper * in resolve-var add the ctor to the tag to track later, this also lets the extra information flow * in analyze-dot check to see if the target is a constructor - if it is use that as tag instead, Function is not useful * test assertion that we can figure out the return even if a extern js ctor is bound to a local * add compiler test case for inferring return of Number.isNaN * add non-ctor inference for array, string, boolean and number, will be useful later * add js/isNaN test * add isArray extern test * don't return raised js/Foo types for boolean, number, string * remove ^boolean hint from array? ass test * remove hint for make-array, add test * remove hints for isFinite and isSafeInteger, tests * can infer distinct?, add test * remove various ^boolean cases no longer needed * FIXME comments about dubious ^boolean cases * move ^boolean hint from special-symbol? to contains? where it belongs, test case * goog.object/containsKey type inference doesn't work for reason, leave a trail for later * goog.string/contains does work, add test * remove hint from NaN? * FIXME note about re-matches, another dubious case of ^boolean hints
1 parent 90a40f6 commit 5549175

File tree

8 files changed

+350
-79
lines changed

8 files changed

+350
-79
lines changed

src/main/cljs/cljs/core.cljs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@
243243
[x]
244244
(coercive-= x nil))
245245

246-
(defn ^boolean array?
246+
(defn array?
247247
"Returns true if x is a JavaScript array."
248248
[x]
249249
(if (identical? *target* "nodejs")
@@ -444,7 +444,7 @@
444444

445445
(declare apply)
446446

447-
(defn ^array make-array
447+
(defn make-array
448448
"Construct a JavaScript array of the specified dimensions. Accepts ignored
449449
type argument for compatibility with Clojure. Note that there is no efficient
450450
way to allocate multi-dimensional arrays in JavaScript; as such, this function
@@ -1055,8 +1055,8 @@
10551055
(bit-xor (-hash o) 0)
10561056

10571057
(number? o)
1058-
(if ^boolean (js/isFinite o)
1059-
(if-not ^boolean (.isSafeInteger js/Number o)
1058+
(if (js/isFinite o)
1059+
(if-not (.isSafeInteger js/Number o)
10601060
(hash-double o)
10611061
(js-mod (Math/floor o) 2147483647))
10621062
(case o
@@ -2355,7 +2355,7 @@ reduces them without incurring seq initialization"
23552355
"Returns true if n is a JavaScript number with no decimal part."
23562356
[n]
23572357
(and (number? n)
2358-
(not ^boolean (js/isNaN n))
2358+
(not (js/isNaN n))
23592359
(not (identical? n js/Infinity))
23602360
(== (js/parseFloat n) (js/parseInt n 10))))
23612361

@@ -2432,7 +2432,7 @@ reduces them without incurring seq initialization"
24322432
(or (identical? x js/Number.POSITIVE_INFINITY)
24332433
(identical? x js/Number.NEGATIVE_INFINITY)))
24342434

2435-
(defn contains?
2435+
(defn ^boolean contains?
24362436
"Returns true if key is present in the given collection, otherwise
24372437
returns false. Note that for numerically indexed collections like
24382438
vectors and arrays, this tests if the numeric key is within the
@@ -2462,12 +2462,12 @@ reduces them without incurring seq initialization"
24622462
(contains? coll k))
24632463
(MapEntry. k (get coll k) nil))))
24642464

2465-
(defn ^boolean distinct?
2465+
(defn distinct?
24662466
"Returns true if no two of the arguments are ="
24672467
([x] true)
24682468
([x y] (not (= x y)))
24692469
([x y & more]
2470-
(if (not (= x y))
2470+
(if (not (= x y))
24712471
(loop [s #{x y} xs more]
24722472
(let [x (first xs)
24732473
etc (next xs)]
@@ -8351,6 +8351,7 @@ reduces them without incurring seq initialization"
83518351
(if (identical? node root)
83528352
nil
83538353
(set! root node))
8354+
;; FIXME: can we figure out something better here?
83548355
(if ^boolean (.-val added-leaf?)
83558356
(set! count (inc count)))
83568357
tcoll))
@@ -8372,6 +8373,7 @@ reduces them without incurring seq initialization"
83728373
(if (identical? node root)
83738374
nil
83748375
(set! root node))
8376+
;; FIXME: can we figure out something better here?
83758377
(if ^boolean (.-val removed-leaf?)
83768378
(set! count (dec count)))
83778379
tcoll)))
@@ -10562,6 +10564,7 @@ reduces them without incurring seq initialization"
1056210564
(pr-writer (meta obj) writer opts)
1056310565
(-write writer " "))
1056410566
(cond
10567+
;; FIXME: can we figure out something better here?
1056510568
;; handle CLJS ctors
1056610569
^boolean (.-cljs$lang$type obj)
1056710570
(.cljs$lang$ctorPrWriter obj obj writer opts)
@@ -10576,7 +10579,7 @@ reduces them without incurring seq initialization"
1057610579
(number? obj)
1057710580
(-write writer
1057810581
(cond
10579-
^boolean (js/isNaN obj) "##NaN"
10582+
(js/isNaN obj) "##NaN"
1058010583
(identical? obj js/Number.POSITIVE_INFINITY) "##Inf"
1058110584
(identical? obj js/Number.NEGATIVE_INFINITY) "##-Inf"
1058210585
:else (str_ obj)))
@@ -11942,7 +11945,7 @@ reduces them without incurring seq initialization"
1194211945
(fn [x y]
1194311946
(cond (pred x y) -1 (pred y x) 1 :else 0)))
1194411947

11945-
(defn ^boolean special-symbol?
11948+
(defn special-symbol?
1194611949
"Returns true if x names a special form"
1194711950
[x]
1194811951
(contains?
@@ -12175,6 +12178,8 @@ reduces them without incurring seq initialization"
1217512178
Object
1217612179
(findInternedVar [this sym]
1217712180
(let [k (munge (str_ sym))]
12181+
;; FIXME: this shouldn't need ^boolean due to GCL library analysis,
12182+
;; but not currently working
1217812183
(when ^boolean (gobject/containsKey obj k)
1217912184
(let [var-sym (symbol (str_ name) (str_ sym))
1218012185
var-meta {:ns this}]
@@ -12268,7 +12273,7 @@ reduces them without incurring seq initialization"
1226812273
(when (nil? NS_CACHE)
1226912274
(set! NS_CACHE (atom {})))
1227012275
(let [ns-str (str_ ns)
12271-
ns (if (not ^boolean (gstring/contains ns-str "$macros"))
12276+
ns (if (not (gstring/contains ns-str "$macros"))
1227212277
(symbol (str_ ns-str "$macros"))
1227312278
ns)
1227412279
the-ns (get @NS_CACHE ns)]
@@ -12292,7 +12297,7 @@ reduces them without incurring seq initialization"
1229212297
[x]
1229312298
(instance? goog.Uri x))
1229412299

12295-
(defn ^boolean NaN?
12300+
(defn NaN?
1229612301
"Returns true if num is NaN, else false"
1229712302
[val]
1229812303
(js/isNaN val))
@@ -12321,6 +12326,7 @@ reduces them without incurring seq initialization"
1232112326
[s]
1232212327
(if (string? s)
1232312328
(cond
12329+
;; FIXME: another cases worth thinking about
1232412330
^boolean (re-matches #"[\x00-\x20]*[+-]?NaN[\x00-\x20]*" s) ##NaN
1232512331
^boolean (re-matches
1232612332
#"[\x00-\x20]*[+-]?(Infinity|((\d+\.?\d*|\.\d+)([eE][+-]?\d+)?)[dDfF]?)[\x00-\x20]*"

src/main/clojure/cljs/analyzer.cljc

Lines changed: 105 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -980,10 +980,10 @@
980980
(defn normalize-js-tag [x]
981981
;; if not 'js, assume constructor
982982
(if-not (= 'js x)
983-
(with-meta 'js
984-
{:prefix (conj (->> (string/split (name x) #"\.")
985-
(map symbol) vec)
986-
'prototype)})
983+
(let [props (->> (string/split (name x) #"\.") (map symbol))
984+
[xs y] ((juxt butlast last) props)]
985+
(with-meta 'js
986+
{:prefix (vec (concat xs [(with-meta y {:ctor true})]))}))
987987
x))
988988

989989
(defn ->type-set
@@ -1030,46 +1030,89 @@
10301030
boolean Boolean
10311031
symbol Symbol})
10321032

1033-
(defn has-extern?*
1033+
(defn resolve-extern
1034+
"Given a foreign js property list, return a resolved js property list and the
1035+
extern var info"
1036+
([pre]
1037+
(resolve-extern pre (get-externs)))
10341038
([pre externs]
1035-
(let [pre (if-some [me (find
1036-
(get-in externs '[Window prototype])
1037-
(first pre))]
1038-
(if-some [tag (-> me first meta :tag)]
1039-
(into [tag 'prototype] (next pre))
1040-
pre)
1041-
pre)]
1042-
(has-extern?* pre externs externs)))
1043-
([pre externs top]
1039+
(resolve-extern pre externs externs {:resolved []}))
1040+
([pre externs top ret]
10441041
(cond
1045-
(empty? pre) true
1042+
(empty? pre) ret
10461043
:else
10471044
(let [x (first pre)
10481045
me (find externs x)]
10491046
(cond
1050-
(not me) false
1047+
(not me) nil
10511048
:else
10521049
(let [[x' externs'] me
1053-
xmeta (meta x')]
1054-
(if (and (= 'Function (:tag xmeta)) (:ctor xmeta))
1055-
(or (has-extern?* (into '[prototype] (next pre)) externs' top)
1056-
(has-extern?* (next pre) externs' top)
1057-
;; check base type if it exists
1058-
(when-let [super (:super xmeta)]
1059-
(has-extern?* (into [super] (next pre)) externs top)))
1060-
(recur (next pre) externs' top))))))))
1050+
info' (meta x')
1051+
ret (cond-> ret
1052+
;; we only care about var info for the last property
1053+
;; also if we already added it, don't override it
1054+
;; because we're now resolving type information
1055+
;; not instance information anymore
1056+
;; i.e. [console] -> [Console] but :tag is Console _not_ Function vs.
1057+
;; [console log] -> [Console prototype log] where :tag is Function
1058+
(and (empty? (next pre))
1059+
(not (contains? ret :info)))
1060+
(assoc :info info'))]
1061+
;; handle actual occurrences of types, i.e. `Console`
1062+
(if (and (or (:ctor info') (:iface info')) (= 'Function (:tag info')))
1063+
(or
1064+
;; then check for "static" property
1065+
(resolve-extern (next pre) externs' top
1066+
(update ret :resolved conj x))
1067+
1068+
;; first look for a property on the prototype
1069+
(resolve-extern (into '[prototype] (next pre)) externs' top
1070+
(update ret :resolved conj x))
1071+
1072+
;; finally check the super class if there is one
1073+
(when-let [super (:super info')]
1074+
(resolve-extern (into [super] (next pre)) externs top
1075+
(assoc ret :resolved []))))
1076+
1077+
(or
1078+
;; If the tag of the property isn't Function or undefined,
1079+
;; try to resolve it similar to the super case above,
1080+
;; this handles singleton cases like `console`
1081+
(let [tag (:tag info')]
1082+
(when (and tag (not (contains? '#{Function undefined} tag)))
1083+
;; check prefix first, during cljs.externs parsing we always generate prefixes
1084+
;; for tags because of types like webCrypto.Crypto
1085+
(resolve-extern (into (or (-> tag meta :prefix) [tag]) (next pre)) externs top
1086+
(assoc ret :resolved []))))
1087+
1088+
;; assume static property
1089+
(recur (next pre) externs' top
1090+
(update ret :resolved conj x))))))))))
1091+
1092+
(defn normalize-unresolved-prefix
1093+
[pre]
1094+
(cond-> pre
1095+
(< 1 (count pre))
1096+
(cond->
1097+
(-> pre pop peek meta :ctor)
1098+
(-> pop
1099+
(conj 'prototype)
1100+
(conj (peek pre))))))
1101+
1102+
(defn has-extern?*
1103+
[pre externs]
1104+
(boolean (resolve-extern pre externs)))
10611105

10621106
(defn has-extern?
10631107
([pre]
10641108
(has-extern? pre (get-externs)))
10651109
([pre externs]
10661110
(or (has-extern?* pre externs)
1067-
(when (= 1 (count pre))
1068-
(let [x (first pre)]
1069-
(or (get-in externs (conj '[Window prototype] x))
1070-
(get-in externs (conj '[Number] x)))))
10711111
(-> (last pre) str (string/starts-with? "cljs$")))))
10721112

1113+
(defn lift-tag-to-js [tag]
1114+
(symbol "js" (str (alias->type tag tag))))
1115+
10731116
(defn js-tag
10741117
([pre]
10751118
(js-tag pre :tag))
@@ -1078,12 +1121,13 @@
10781121
([pre tag-type externs]
10791122
(js-tag pre tag-type externs externs))
10801123
([pre tag-type externs top]
1081-
(when-let [[p externs' :as me] (find externs (first pre))]
1082-
(let [tag (-> p meta tag-type)]
1083-
(if (= (count pre) 1)
1084-
(when tag (symbol "js" (str (alias->type tag tag))))
1085-
(or (js-tag (next pre) tag-type externs' top)
1086-
(js-tag (into '[prototype] (next pre)) tag-type (get top tag) top)))))))
1124+
(when-let [tag (get-in (resolve-extern pre externs) [:info tag-type])]
1125+
(case tag
1126+
;; don't lift these, analyze-dot will raise them for analysis
1127+
;; representing these types as js/Foo is a hassle as it widens the
1128+
;; return types unnecessarily i.e. #{boolean js/Boolean}
1129+
(boolean number string) tag
1130+
(lift-tag-to-js tag)))))
10871131

10881132
(defn dotted-symbol? [sym]
10891133
(let [s (str sym)]
@@ -1274,8 +1318,9 @@
12741318
(assoc shadowed-by-local :op :local))
12751319

12761320
:else
1277-
(let [pre (->> (string/split (name sym) #"\.") (map symbol) vec)]
1278-
(when (and (not (has-extern? pre))
1321+
(let [pre (->> (string/split (name sym) #"\.") (map symbol) vec)
1322+
res (resolve-extern (->> (string/split (name sym) #"\.") (map symbol) vec))]
1323+
(when (and (not res)
12791324
;; ignore exists? usage
12801325
(not (-> sym meta ::no-resolve)))
12811326
(swap! env/*compiler* update-in
@@ -1284,10 +1329,12 @@
12841329
{:name sym
12851330
:op :js-var
12861331
:ns 'js
1287-
:tag (with-meta (or (js-tag pre) (:tag (meta sym)) 'js) {:prefix pre})}
1332+
:tag (with-meta (or (js-tag pre) (:tag (meta sym)) 'js)
1333+
{:prefix pre
1334+
:ctor (-> res :info :ctor)})}
12881335
(when-let [ret-tag (js-tag pre :ret-tag)]
12891336
{:js-fn-var true
1290-
:ret-tag ret-tag})))))
1337+
:ret-tag ret-tag})))))
12911338
(let [s (str sym)
12921339
lb (handle-symbol-local sym (get locals sym))
12931340
current-ns (-> env :ns :name)]
@@ -2585,12 +2632,12 @@
25852632
:children [:expr]}))
25862633

25872634
(def js-prim-ctor->tag
2588-
'{js/Object object
2589-
js/String string
2590-
js/Array array
2591-
js/Number number
2635+
'{js/Object object
2636+
js/String string
2637+
js/Array array
2638+
js/Number number
25922639
js/Function function
2593-
js/Boolean boolean})
2640+
js/Boolean boolean})
25942641

25952642
(defn prim-ctor?
25962643
"Test whether a tag is a constructor for a JS primitive"
@@ -3543,13 +3590,25 @@
35433590
(list* '. dot-form) " with classification "
35443591
(classify-dot-form dot-form))))))
35453592

3593+
;; this only for a smaller set of types that we want to infer
3594+
;; we don't generally want to consider function for example, these
3595+
;; specific cases are ones we either try to optimize or validate
3596+
(def ^{:private true}
3597+
tag->js-prim-ctor
3598+
'{string js/String
3599+
array js/Array
3600+
number js/Number
3601+
boolean js/Boolean})
3602+
35463603
(defn analyze-dot [env target field member+ form]
35473604
(let [v [target field member+]
35483605
{:keys [dot-action target method field args]} (build-dot-form v)
35493606
enve (assoc env :context :expr)
35503607
targetexpr (analyze enve target)
35513608
form-meta (meta form)
3552-
target-tag (:tag targetexpr)
3609+
target-tag (as-> (:tag targetexpr) $
3610+
(or (some-> $ meta :ctor lift-tag-to-js)
3611+
(tag->js-prim-ctor $ $)))
35533612
prop (or field method)
35543613
tag (or (:tag form-meta)
35553614
(and (js-tag? target-tag)
@@ -3581,7 +3640,8 @@
35813640
(let [pre (-> tag meta :prefix)]
35823641
(when-not (has-extern? pre)
35833642
(swap! env/*compiler* update-in
3584-
(into [::namespaces (-> env :ns :name) :externs] pre) merge {}))))
3643+
(into [::namespaces (-> env :ns :name) :externs]
3644+
(normalize-unresolved-prefix pre)) merge {}))))
35853645
(case dot-action
35863646
::access (let [children [:target]]
35873647
{:op :host-field

src/main/clojure/cljs/compiler.cljc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,8 @@
641641

642642
(defn safe-test? [env e]
643643
(let [tag (ana/infer-tag env e)]
644-
(or (#{'boolean 'seq} tag) (truthy-constant? e))))
644+
(or ('#{boolean seq} (ana/js-prim-ctor->tag tag tag))
645+
(truthy-constant? e))))
645646

646647
(defmethod emit* :if
647648
[{:keys [test then else env unchecked]}]

0 commit comments

Comments
 (0)