From b7481f7d25b45fd06d7ebf37d76e98b63b9e577b Mon Sep 17 00:00:00 2001 From: davidnolen Date: Sat, 25 Jan 2025 11:51:36 -0500 Subject: [PATCH 1/5] * externs parsing comments * doc externs-map * add info helper, doc * update eval comments --- src/main/clojure/cljs/externs.clj | 37 ++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/main/clojure/cljs/externs.clj b/src/main/clojure/cljs/externs.clj index d86aa91ef..07ab13c86 100644 --- a/src/main/clojure/cljs/externs.clj +++ b/src/main/clojure/cljs/externs.clj @@ -124,6 +124,8 @@ (when (> (.getChildCount node) 0) (parse-extern-node (.getFirstChild node)))) +;; Handle: +;; Math.hypot = function(...) {...}; (defmethod parse-extern-node Token/ASSIGN [^Node node] (when (> (.getChildCount node) 0) (let [ty (get-var-info node) @@ -253,7 +255,20 @@ externs (index-externs (parse-externs externs-file)))) defaults sources)))) -(def externs-map (memoize externs-map*)) +(def ^{:doc "Returns a map of externs in the form: + + {foo {bar {baz {} + woz {...}} + + JavaScript var information is not held in the map itself, but on the + symbols in the map. See the helper `info` for grabbing the metadata. + The metadata map matches the layout of var info of the ClojureScript + analyzer: :file & :line, method info, :ret-tag, :tag, :doc, etc. + are all available. + + See also `parse-externs`."} + externs-map + (memoize externs-map*)) (defn ns-match? [ns-segs var-segs] (or @@ -313,6 +328,16 @@ (parse-externs (resource->source-file rsrc)) (:module desc))})))) +(defn info + "Helper for grabbing var info from an externs map. + + See `externs-map`" + [externs props] + (-> externs + (get-in (butlast props)) + (find (last props)) + first meta)) + (comment (require '[clojure.java.io :as io] '[cljs.closure :as closure] @@ -354,8 +379,7 @@ [(closure/js-source-file "goog/date/date.js" (io/input-stream (io/resource "goog/date/date.js")))] {}) - (get-in '[goog date month]) - ) + (get-in '[goog date month])) (pprint (analyze-goog-file "goog/date/date.js" 'goog.date.month)) @@ -385,7 +409,7 @@ (-> (filter (fn [s] - (= "externs.zip//webkit_dom.js" (.getName s))) + (= "externs.zip//whatwg_console.js" (.getName s))) (default-externs)) first parse-externs index-externs (find 'console) first meta) @@ -393,7 +417,7 @@ (-> (filter (fn [s] - (= "externs.zip//webkit_dom.js" (.getName s))) + (= "externs.zip//whatwg_console.js" (.getName s))) (default-externs)) first parse-externs index-externs (get-in '[Console prototype]) @@ -402,8 +426,11 @@ (require '[clojure.java.io :as io] '[cljs.closure :as cc]) + ;; react.ext.js needs to be available (-> (cc/js-source-file nil (io/file "react.ext.js")) parse-externs index-externs (get 'React) (find 'Component) first meta) + + (-> (info (externs-map) '[Number isNaN]) :ret-tag) ;; => boolean ) From fee443cc711eb5b720006144786d38b8d85255df Mon Sep 17 00:00:00 2001 From: davidnolen Date: Sat, 25 Jan 2025 12:24:44 -0500 Subject: [PATCH 2/5] * copy over min/max logic from Clojure --- src/main/cljs/cljs/core.cljs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/cljs/cljs/core.cljs b/src/main/cljs/cljs/core.cljs index dcdf23c31..ef2818486 100644 --- a/src/main/cljs/cljs/core.cljs +++ b/src/main/cljs/cljs/core.cljs @@ -2771,14 +2771,22 @@ reduces them without incurring seq initialization" (defn ^number max "Returns the greatest of the nums." ([x] x) - ([x y] (cljs.core/max x y)) + ([x y] + (cond + (.isNaN js/Number x) x + (.isNaN js/Number y) y + :else (cljs.core/max x y))) ([x y & more] (reduce max (cljs.core/max x y) more))) (defn ^number min "Returns the least of the nums." ([x] x) - ([x y] (cljs.core/min x y)) + ([x y] + (cond + (.isNaN js/Number x) x + (.isNaN js/Number y) y + :else (cljs.core/min x y))) ([x y & more] (reduce min (cljs.core/min x y) more))) From 9609c26afafd79d2531d39b91ee5e39e846db382 Mon Sep 17 00:00:00 2001 From: davidnolen Date: Sat, 25 Jan 2025 12:25:12 -0500 Subject: [PATCH 3/5] * wip add TODO --- src/test/clojure/cljs/externs_parsing_tests.clj | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/test/clojure/cljs/externs_parsing_tests.clj b/src/test/clojure/cljs/externs_parsing_tests.clj index effad773d..a698bf866 100644 --- a/src/test/clojure/cljs/externs_parsing_tests.clj +++ b/src/test/clojure/cljs/externs_parsing_tests.clj @@ -7,7 +7,10 @@ ;; You must not remove this notice, or any other, from this software. (ns cljs.externs-parsing-tests - (:require [cljs.closure :as closure] + (:require [cljs.analyzer :as ana] + [cljs.closure :as closure] + [cljs.compiler :as comp] + [cljs.env :as env] [cljs.externs :as externs] [clojure.java.io :as io] [clojure.test :as test :refer [deftest is]]) @@ -45,8 +48,16 @@ (find 'HTMLDocument) first meta)] (is (= 'Document (:super info))))) +;; TODO: +;; analyze (.isNaN js/NaN 1) +;; node :tag should be js/Boolean + (comment + ;; js/Boolean + (env/with-compiler-env (env/default-compiler-env) + (ana/js-tag '[Number isNaN] :ret-tag)) + (externs/parse-externs (externs/resource->source-file (io/resource "goog/object/object.js"))) From 19677dbcef4702c65b1c1f584800dd56400c1114 Mon Sep 17 00:00:00 2001 From: davidnolen Date: Mon, 27 Jan 2025 21:23:37 -0500 Subject: [PATCH 4/5] * add js-global? helper to analyzer * in host call check for this case - if js global change the type resolution via externs * add some comment eval exprs for context --- src/main/clojure/cljs/analyzer.cljc | 19 ++++++++++---- src/main/clojure/cljs/compiler.cljc | 2 +- src/main/clojure/cljs/externs.clj | 1 + .../clojure/cljs/externs_parsing_tests.clj | 25 ++++++++++++++++--- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index 8c61c4586..ba8ff4240 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -3542,6 +3542,12 @@ (list* '. dot-form) " with classification " (classify-dot-form dot-form)))))) +(defn js-global? + "Return true if the expr is a JS global" + [expr] + (and (= 'js (:ns expr)) + (some? (get-in (get-externs) [(-> (:name expr) name symbol)])))) + (defn analyze-dot [env target field member+ form] (let [v [target field member+] {:keys [dot-action target method field args]} (build-dot-form v) @@ -3550,11 +3556,14 @@ form-meta (meta form) target-tag (:tag targetexpr) prop (or field method) - tag (or (:tag form-meta) - (and (js-tag? target-tag) - (vary-meta (normalize-js-tag target-tag) - update-in [:prefix] (fnil conj '[Object]) prop)) - nil)] + tag (if (js-global? targetexpr) + ;; we have a known global, don't treat as instance of some type + (with-meta 'js {:prefix [(-> targetexpr :name name symbol) prop]}) + (or (:tag form-meta) + (and (js-tag? target-tag) + (vary-meta (normalize-js-tag target-tag) + update-in [:prefix] (fnil conj '[Object]) prop)) + nil))] (when (and (not= 'constructor prop) (not (string/starts-with? (str prop) "cljs$")) (not (-> prop meta :protocol-prop))) diff --git a/src/main/clojure/cljs/compiler.cljc b/src/main/clojure/cljs/compiler.cljc index b96c09b36..632cc75de 100644 --- a/src/main/clojure/cljs/compiler.cljc +++ b/src/main/clojure/cljs/compiler.cljc @@ -641,7 +641,7 @@ (defn safe-test? [env e] (let [tag (ana/infer-tag env e)] - (or (#{'boolean 'seq} tag) (truthy-constant? e)))) + (or ('#{boolean seq js/Boolean} tag) (truthy-constant? e)))) (defmethod emit* :if [{:keys [test then else env unchecked]}] diff --git a/src/main/clojure/cljs/externs.clj b/src/main/clojure/cljs/externs.clj index 07ab13c86..71619e25c 100644 --- a/src/main/clojure/cljs/externs.clj +++ b/src/main/clojure/cljs/externs.clj @@ -432,5 +432,6 @@ (get 'React) (find 'Component) first meta) + (info (externs-map) '[Number]) (-> (info (externs-map) '[Number isNaN]) :ret-tag) ;; => boolean ) diff --git a/src/test/clojure/cljs/externs_parsing_tests.clj b/src/test/clojure/cljs/externs_parsing_tests.clj index a698bf866..1d5a51698 100644 --- a/src/test/clojure/cljs/externs_parsing_tests.clj +++ b/src/test/clojure/cljs/externs_parsing_tests.clj @@ -48,16 +48,35 @@ (find 'HTMLDocument) first meta)] (is (= 'Document (:super info))))) -;; TODO: -;; analyze (.isNaN js/NaN 1) -;; node :tag should be js/Boolean +(deftest test-number-infer-test + (let [cenv (env/default-compiler-env) + aenv (ana/empty-env)] + (is (= (env/with-compiler-env cenv + (:tag (ana/analyze aenv '(.isNaN js/Number 1)))) + 'js/Boolean)))) + +;; TODO: js/subtle.crypto (comment + (externs/info + (::ana/externs @(env/default-compiler-env)) + '[Number]) + + (externs/info + (::ana/externs @(env/default-compiler-env)) + '[Number isNaN]) + ;; js/Boolean (env/with-compiler-env (env/default-compiler-env) (ana/js-tag '[Number isNaN] :ret-tag)) + ;; js + (let [cenv (env/default-compiler-env) + aenv (ana/empty-env)] + (->> (env/with-compiler-env cenv + (:tag (ana/analyze aenv '(.isNaN js/Number 1)))))) + (externs/parse-externs (externs/resource->source-file (io/resource "goog/object/object.js"))) From 67b53695f90e314e9720cb16ff9849f60a0f6d7d Mon Sep 17 00:00:00 2001 From: davidnolen Date: Mon, 27 Jan 2025 21:29:29 -0500 Subject: [PATCH 5/5] * add info example --- src/main/clojure/cljs/externs.clj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/clojure/cljs/externs.clj b/src/main/clojure/cljs/externs.clj index 71619e25c..a6d460eab 100644 --- a/src/main/clojure/cljs/externs.clj +++ b/src/main/clojure/cljs/externs.clj @@ -330,6 +330,8 @@ (defn info "Helper for grabbing var info from an externs map. + Example: + (info externs '[Number isNaN]) See `externs-map`" [externs props]