From 9696bac5bd47abbc8741631989448db48ee017db Mon Sep 17 00:00:00 2001 From: David Nolen Date: Thu, 21 Nov 2024 14:19:49 -0800 Subject: [PATCH] Add specs to analyzer, add spec based tests (#238) * clojure.spec specs for the AST based on the AST reference * unit tests for all the AST node types + spec assertions * fix minor cases where the AST diverges from the AST reference --- deps.edn | 3 +- src/main/clojure/cljs/analyzer.cljc | 26 +- src/test/clojure/cljs/analyzer/spec_tests.clj | 288 +++++++++++++++ src/test/clojure/cljs/analyzer/specs.cljc | 328 ++++++++++++++++++ 4 files changed, 637 insertions(+), 8 deletions(-) create mode 100644 src/test/clojure/cljs/analyzer/spec_tests.clj create mode 100644 src/test/clojure/cljs/analyzer/specs.cljc diff --git a/deps.edn b/deps.edn index 8925f7e20..9a8f42b00 100644 --- a/deps.edn +++ b/deps.edn @@ -13,7 +13,8 @@ :main-opts ["-i" "src/test/cljs_cli/cljs_cli/test_runner.clj" "-e" "(cljs-cli.test-runner/-main)"]} :compiler.test {:extra-paths ["src/test/cljs" "src/test/cljs_build" "src/test/cljs_cp" - "src/test/clojure" "src/test/self"]} + "src/test/clojure" "src/test/self"] + :extra-deps {org.clojure/spec.alpha {:mvn/version "0.5.238"}}} :compiler.test.run {:main-opts ["-i" "src/test/clojure/cljs/test_runner.clj" "-e" "(cljs.test-runner/-main)"]} :runtime.test.build {:extra-paths ["src/test/cljs"] diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index 376956d8e..887ae349a 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -1880,7 +1880,12 @@ (assoc locals e {:name e :line (get-line e env) - :column (get-col e env)}) + :column (get-col e env) + ;; :local is required for {:op :local ...} nodes + ;; but previously we had no way to figure this out + ;; for `catch` locals, by adding it here we can recover + ;; it later + :local :catch}) locals) catch (when cblock (disallowing-recur (analyze (assoc catchenv :locals locals) cblock))) @@ -2143,6 +2148,7 @@ {:line line :column column}) param {:op :binding :name name + :form name :line line :column column :tag tag @@ -2205,8 +2211,10 @@ shadow (or (handle-symbol-local name (get locals name)) (get-in env [:js-globals name])) fn-scope (:fn-scope env) - name-var {:name name - :op :binding + name-var {:op :binding + :env env + :form name + :name name :local :fn :info {:fn-self-name true :fn-scope fn-scope @@ -2326,8 +2334,10 @@ (let [ret-tag (-> n meta :tag) fexpr (no-warn (analyze env (n->fexpr n))) be (cond-> - {:name n - :op :binding + {:op :binding + :name n + :form n + :env env :fn-var true :line (get-line n env) :column (get-col n env) @@ -2416,7 +2426,9 @@ col (get-col name env) shadow (or (handle-symbol-local name (get-in env [:locals name])) (get-in env [:js-globals name])) - be {:name name + be {:op :binding + :name name + :form name :line line :column col :init init-expr @@ -2425,7 +2437,6 @@ :shadow shadow ;; Give let* bindings same shape as var so ;; they get routed correctly in the compiler - :op :binding :env {:line line :column col} :info {:name name :shadow shadow} @@ -2565,6 +2576,7 @@ (throw (error env "Wrong number of args to quote"))) (let [expr (analyze-const env x)] {:op :quote + :literal? true :expr expr :env env :form form diff --git a/src/test/clojure/cljs/analyzer/spec_tests.clj b/src/test/clojure/cljs/analyzer/spec_tests.clj new file mode 100644 index 000000000..57134c703 --- /dev/null +++ b/src/test/clojure/cljs/analyzer/spec_tests.clj @@ -0,0 +1,288 @@ +;; 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.spec-tests + (:require [cljs.analyzer :as ana] + [cljs.analyzer.api :as ana-api :refer [no-warn]] + [cljs.compiler.api :as comp-api] + [cljs.analyzer-tests :refer [analyze ns-env]] + [cljs.analyzer.specs :as a] + [clojure.test :as test :refer [deftest is]] + [clojure.spec.alpha :as s]) + (:import [java.io StringReader])) + +(deftest test-binding + (let [node (analyze ns-env '(let [x 1] x)) + binding (-> node :bindings first)] + (is (= :binding (:op binding))) + (is (s/valid? ::a/node binding)))) + +(deftest test-case + (let [let-node (no-warn (analyze ns-env '(case x 1 :foo 2 :bar))) + node (-> let-node :body :ret)] + (is (= :case (:op node))) + (is (s/valid? ::a/node node)) + (let [nodes (-> node :nodes) + case-node (first nodes)] + (is (= :case-node (:op case-node))) + (is (s/valid? ::a/node case-node)) + (let [case-tests (:tests case-node) + case-test (first case-tests) + case-then (:then case-node)] + (is (= :case-test (:op case-test))) + (is (s/valid? ::a/node case-test)) + (is (= :case-then (:op case-then))) + (is (s/valid? ::a/node case-then)))))) + +(deftest test-const + (is (s/valid? ::a/node (analyze ns-env 1))) + (is (s/valid? ::a/node (analyze ns-env 1.2))) + (is (s/valid? ::a/node (analyze ns-env true))) + (is (s/valid? ::a/node (analyze ns-env "foo"))) + (let [node (analyze ns-env [])] + (is (= :vector (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env [1 2 3]))) + (is (s/valid? ::a/node (analyze ns-env {}))) + (let [node (analyze ns-env {1 2 3 4})] + (is (= :map (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env #{}))) + (let [node (analyze ns-env #{1 2 3})] + (is (= :set (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-def + (let [node (no-warn (analyze ns-env '(def x)))] + (is (= :def (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env '(def x 1)))) + (is (s/valid? ::a/node (analyze ns-env '(def x (fn []))))) + (is (s/valid? ::a/node (analyze ns-env '(def x (fn [y] y)))))) + +(deftest test-defn + (is (s/valid? ::a/node (analyze ns-env '(defn x [])))) + (is (s/valid? ::a/node (analyze ns-env '(defn x [] 1)))) + (is (s/valid? ::a/node (analyze ns-env '(defn x [y] y))))) + +(deftest test-defrecord + (let [node (no-warn (analyze ns-env '(defrecord A []))) + body (:body node)] + (is (= :defrecord (-> body :statements first :ret :op))) + (is (s/valid? ::a/node node)))) + +(deftest test-deftype + (let [node (no-warn (analyze ns-env '(deftype A [])))] + (is (= :deftype (-> node :statements first :op))) + (is (s/valid? ::a/node node)))) + +(deftest test-do + (let [node (analyze ns-env '(do))] + (is (= :do (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env '(do 1)))) + (is (s/valid? ::a/node (analyze ns-env '(do 1 2 3))))) + +(deftest test-fn + (let [node (no-warn (analyze ns-env '(fn [])))] + (is (= :fn (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env '(fn [] 1)))) + (is (s/valid? ::a/node (analyze ns-env '(fn [x])))) + (is (s/valid? ::a/node (analyze ns-env '(fn [x] 1))))) + +(deftest test-fn-method + (let [node (analyze ns-env '(fn ([]) ([x] x))) + methods (:methods node) + fn0 (first methods) + fn1 (second methods)] + (is (= :fn-method (:op fn0))) + (is (s/valid? ::a/node fn0)) + (is (= :fn-method (:op fn1))) + (is (s/valid? ::a/node fn1)))) + +(deftest test-host-call + (let [node (analyze ns-env '(.substring "foo" 0 1))] + (is (= :host-call (:op node))) + (is (s/valid? ::a/node node))) + (let [node (analyze ns-env '(. "foo" (substring 0 1)))] + (is (= :host-call (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-host-field + (let [node (analyze ns-env '(.-length "foo"))] + (is (= :host-field (:op node))) + (is (s/valid? ::a/node node))) + (let [node (analyze ns-env '(. "foo" -length))] + (is (= :host-field (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-if + (let [node (analyze ns-env '(if true true))] + (is (= :if (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env '(if true true false))))) + +(deftest test-invoke + (let [node (no-warn (analyze ns-env '(count "foo")))] + (is (= :invoke (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-js + (let [node (analyze ns-env '(js* "~{}" 1))] + (is (= :js (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-js-array + (let [node (analyze ns-env + (ana-api/with-state (ana-api/empty-state) + (first (ana-api/forms-seq (StringReader. "#js [1 2 3]")))))] + (is (= :js-array (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-js-object + (let [node (analyze ns-env + (ana-api/with-state (ana-api/empty-state) + (first (ana-api/forms-seq (StringReader. "#js {:foo 1 :bar 2}")))))] + (is (= :js-object (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-js-var + (let [node (analyze ns-env 'js/String)] + (is (= :js-var (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-let + (let [node (analyze ns-env '(let []))] + (is (= :let (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env '(let [x 1])))) + (is (s/valid? ::a/node (analyze ns-env '(let [x 1] x))))) + +(deftest test-letfn + (let [node (analyze ns-env '(letfn [(foo [] (bar)) (bar [] (foo))]))] + (is (= :letfn (:op node))) + (is (s/valid? ::a/node node)))) + +;; list, no longer needed, subsumed by :quote + +(deftest test-local + (let [node (analyze ns-env '(fn [x] x)) + fn-method (-> node :methods first) + body (-> fn-method :body) + ret (:ret body)] + (is (= :local (:op ret))) + (is (s/valid? ::a/node node)))) + +(deftest test-loop + (let [node (analyze ns-env '(loop []))] + (is (= :loop (:op node))) + (is (s/valid? ::a/node node))) + (let [node (analyze ns-env '(loop [x 1] x))] + (is (s/valid? ::a/node node))) + (let [node (analyze ns-env '(loop [x 1] (recur (inc x))))] + (is (s/valid? ::a/node node))) + (let [node (no-warn + (analyze ns-env + '(loop [x 100] + (if (pos? x) + (recur (dec x)) + x))))] + (is (s/valid? ::a/node node)))) + +(deftest test-map + (let [node (no-warn (analyze ns-env '{:foo 1 :bar 2}))] + (is (= :map (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-new + (let [node (no-warn (analyze ns-env '(new String)))] + (is (= :new (:op node))) + (is (s/valid? ::a/node node))) + (is (s/valid? ::a/node (analyze ns-env '(new js/String)))) + (is (s/valid? ::a/node (no-warn (analyze ns-env '(String.))))) + (is (s/valid? ::a/node (analyze ns-env '(js/String.))))) + +(deftest test-no-op + (let [node (binding [ana/*unchecked-if* true] + (no-warn (analyze ns-env '(set! *unchecked-if* false))))] + (is (= :no-op (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-ns + (let [node (no-warn + (binding [ana/*cljs-ns* 'cljs.user] + (analyze ns-env '(ns foo (:require [goog.string])))))] + (is (= :ns (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-ns* + (let [node (no-warn + (binding [ana/*cljs-ns* 'cljs.user] + (analyze ns-env '(ns* (:require '[goog.string])))))] + (is (= :ns* (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-quote + (let [node (analyze ns-env ''(1 2 3))] + (is (= :quote (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-recur + (let [node (no-warn (analyze ns-env '(fn [x] (recur (inc x)))))] + (is (s/valid? ::a/node node)))) + +(deftest test-set + (let [node (analyze ns-env #{1 2 3})] + (is (= :set (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-set! + (let [node (no-warn (analyze ns-env '(set! x 1)))] + (is (= :set! (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-the-var + (let [node (comp-api/with-core-cljs {} + #(analyze ns-env '(var first)))] + (is (= :the-var (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-throw + (let [node (no-warn (analyze ns-env '(throw (js/Error. "foo"))))] + (is (= :throw (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-try + (let [node (no-warn (analyze ns-env '(try 1 (catch :default e) (finally))))] + (is (= :try (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-var + (let [node (no-warn (analyze ns-env '(fn [] x))) + fn-method (-> node :methods first) + body (-> fn-method :body) + ret (:ret body)] + (is (= :var (:op ret))) + (is (s/valid? ::a/node node)))) + +(deftest test-vector + (let [node (no-warn (analyze ns-env '[1 2]))] + (is (= :vector (:op node))) + (is (s/valid? ::a/node node)))) + +(deftest test-with-meta + (let [node (analyze ns-env ^{:meta 2} {:foo 1})] + (is (= :with-meta (:op node))) + (is (s/valid? ::a/node node)))) + +(comment + + (test/run-tests) + + ) diff --git a/src/test/clojure/cljs/analyzer/specs.cljc b/src/test/clojure/cljs/analyzer/specs.cljc new file mode 100644 index 000000000..ec5079bf9 --- /dev/null +++ b/src/test/clojure/cljs/analyzer/specs.cljc @@ -0,0 +1,328 @@ +;; 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.specs + (:require [clojure.spec.alpha :as s])) + +(s/def ::op keyword?) +(s/def ::form any?) +(s/def ::env map?) +(s/def ::context #{:expr :return :statement}) + +(defmulti node :op) +(s/def ::node (s/multi-spec node :op)) + +(s/def ::test ::node) +(s/def ::then ::node) +(s/def ::else ::node) + +;; TODO: :tag +(s/def ::base + (s/keys + :req-un [::op ::env ::form])) + +(s/def ::name symbol?) +(s/def :cljs.analyzer.specs.binding/local + #{:arg :catch :fn :let :letfn :loop :field}) +(s/def ::variadic? boolean?) +(s/def ::init ::node) +(s/def ::shadow + (s/or :nil nil? + :node ::node)) + +(defmethod node :binding [_] + (s/merge + ::base + (s/keys + :req-un [::name :cljs.analyzer.specs.binding/local] + :opt-un [::variadic? ::init ::shadow]))) + +(s/def ::nodes (s/* ::node)) +(s/def ::default ::node) + +(defmethod node :case [_] + (s/merge ::base + (s/keys + :req-un [::test ::nodes ::default]))) + +(defmethod node :case-node [_] + (s/keys + :req-un [::op ::env ::tests ::then])) + +(defmethod node :case-test [_] + (s/merge ::base + (s/keys + :req-un [::test]))) + +(defmethod node :case-then [_] + (s/merge ::base + (s/keys + :req-un [::then]))) + +(s/def ::literal? boolean?) +(s/def ::val any?) + +(defmethod node :const [_] + (s/merge ::base + (s/keys + :req-un [::val] + ;; ::literal? is required in the AST REF, but we don't actually use it + ;; should check tools.analyzer + :opt-un [::literal?]))) + +(defmethod node :def [_] + (s/merge ::base + (s/keys + :req-un [::name] + :opt-un [::init ::the-var]))) + +(s/def ::body ::node) +(s/def ::t symbol?) + +(defmethod node :defrecord [_] + (s/merge ::base + (s/keys + :req-un [::t ::body]))) + +(defmethod node :deftype [_] + (s/merge ::base + (s/keys + :req-un [::t ::body]))) + +(s/def ::statements (s/* ::node)) +(s/def ::ret ::node) +(s/def ::body? boolean?) + +(defmethod node :do [_] + (s/merge ::base + (s/keys + :req-un [::statements ::ret] + :opt-un [::body?]))) + +(s/def ::local ::node) +(s/def ::max-fixed-arity int?) +(s/def ::methods (s/+ ::node)) + +(defmethod node :fn [_] + (s/merge ::base + (s/keys + :req-un [::variadic? ::max-fixed-arity ::methods] + :opt-un [::local]))) + +(s/def ::fixed-arity int?) +(s/def ::params (s/* ::node)) + +(defmethod node :fn-method [_] + (s/merge ::base + (s/keys + :req-un [::fixed-arity ::params ::body]))) + +(s/def ::method symbol?) +(s/def ::target ::node) +(s/def ::args (s/* ::node)) + +(defmethod node :host-call [_] + (s/merge ::base + (s/keys + :req-un [::method ::target ::args]))) + +(s/def ::field symbol?) + +(defmethod node :host-field [_] + (s/merge ::base + (s/keys + :req-un [::field ::target]))) + +(defmethod node :if [_] + (s/merge ::base + (s/keys + :req-un [::test ::then] + :opt-un [::else]))) + +(s/def ::fn ::node) + +(defmethod node :invoke [_] + (s/merge ::base + (s/keys + :req-un [::fn ::args]))) + +(s/def ::code string?) + +(defmethod node :js [_] + (s/merge ::base + (s/keys + :opt-un [::code]))) + +(defmethod node :js-array [_] + (s/merge ::base + (s/keys + :req-un [::items]))) + +(defmethod node :js-object [_] + (s/merge ::base + (s/keys + :req-un [::vals]))) + +(s/def ::ns symbol?) + +(defmethod node :js-var [_] + (s/merge ::base + (s/keys + :req-un [::ns ::name]))) + +(s/def ::bindings (s/* ::node)) + +(defmethod node :let [_] + (s/merge ::base + (s/keys + :req-un [::bindings ::body]))) + +(defmethod node :letfn [_] + (s/merge ::base + (s/keys + :req-un [::bindings ::body]))) + +(s/def ::items (s/* ::node)) + +;; TODO: not in ast-ref +(defmethod node :list [_] + (s/merge ::base + (s/keys + :req-un [::items]))) + +(defmethod node :local [_] + (s/merge ::base + (s/keys + :req-un [:cljs.analyzer.specs.binding/local ::name]))) + +(defmethod node :loop [_] + (s/merge ::base + (s/keys + :req-un [::bindings ::body]))) + +(s/def ::vals (s/* ::node)) + +(defmethod node :map [_] + (s/merge ::base + (s/keys :req-un [::keys ::vals]))) + +(s/def ::class ::node) + +(defmethod node :new [_] + (s/merge ::base + (s/keys + :req-un [::class ::args]))) + +(defmethod node :no-op [_] + (s/keys + :req-un [::env ::op])) + +(defmethod node :ns [_] + ::base) + +(defmethod node :ns* [_] + ::base) + +(s/def ::expr ::node) + +(defmethod node :quote [_] + (s/merge ::base + (s/keys + :req-un [::expr ::literal?]))) + +(s/def ::exprs (s/* ::node)) + +(defmethod node :recur [_] + (s/merge ::base + (s/keys + :req-un [::exprs]))) + +(defmethod node :set [_] + (s/merge ::base + (s/keys + :req-un [::items]))) + +(defmethod node :set! [_] + (s/merge ::base + (s/keys + :req-un [::target ::val]))) + +(s/def ::var ::node) +(s/def ::sym ::node) +(s/def ::meta map?) + +(defmethod node :the-var [_] + (s/merge ::base + (s/keys + :opt-un [::var ::sym ::meta]))) + +(s/def ::the-var ::node) + +(s/def ::exception ::node) + +(defmethod node :throw [_] + (s/merge ::base + (s/keys + :req-un [::exception]))) + +(s/def ::catch ::node) +(s/def ::finally ::node) + +(defmethod node :try [_] + (s/merge ::base + (s/keys + :req-un [::body ::catch ::name ::finally]))) + +(defmethod node :var [_] + (s/merge ::base + (s/keys + :req-un [::ns ::name]))) + +(s/def ::meta ::node) + +(defmethod node :vector [_] + (s/merge ::base + (s/keys + :req-un [::items]))) + +(defmethod node :with-meta [_] + (s/merge ::base + (s/keys + :req-un [::meta ::expr]))) + +(comment + + (s/valid? ::node 1) + (s/valid? ::node + {:op :const + :env {} + :form 1 + :literal? true + :val 1}) + + (s/explain-data ::node + {:op :if + :env {} + :form '(if true true false) + :test {:op :const + :env {} + :form true + :literal? true + :val true} + :then {:op :const + :env {} + :form true + :literal? true + :val true} + :else {:op :const + :env 1 + :form false + :literal? true + :val false}}) + + )