From ae507e5c0a38cc31a096f3804a8ad6f66d4307bb Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 31 Jul 2023 17:23:43 +0200 Subject: [PATCH] [nop] Housekeeping --- README.md | 3 + src/taoensso/truss.cljc | 124 +++++++++++++++------------------ src/taoensso/truss/impl.cljc | 123 ++++++++++++++------------------ test/taoensso/truss_tests.cljc | 7 +- 4 files changed, 116 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index c3d0fd1..700c320 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ See for intro and usage: ;; :line 9, ;; :column 11, ;; :file "examples.cljc"}} + +;; Assert inside collections using `:in`: +(have string? :in ["don't" "panic"]) ``` That's everything most users will need to know, but see the [documentation](#documentation) below for more! diff --git a/src/taoensso/truss.cljc b/src/taoensso/truss.cljc index 5e51ef6..03e6b3b 100644 --- a/src/taoensso/truss.cljc +++ b/src/taoensso/truss.cljc @@ -5,43 +5,64 @@ (comment (require '[taoensso.encore :as enc])) -;;;; +;;;; CLJ-865 + +#?(:clj + (defmacro keep-callsite + "The long-standing CLJ-865 means that it's not possible for an inner + macro to access the `&form` metadata of a wrapping outer macro. This + means that wrapped macros lose calsite info, etc. + + This util offers a workaround for macro authors: + (defmacro inner [] (meta &form)) + (defmacro outer [] (keep-callsite `(inner))) + (outer) => {:keys [line column ...]}" + + {:added "v1.8.0 (2022-12-13)"} + [form] `(with-meta ~form (meta ~'&form)))) #?(:clj (defn- clj-865-workaround "Experimental undocumented alternative CLJ-865 workaround that allows more precise control than `keep-callsite`." - [macro-amp-form args] + [macro-form args] (let [[a0 & an] args] - (if-let [given-amp-form (and (map? a0) (get a0 :&form))] - [given-amp-form an] - [macro-amp-form args])))) + (if-let [macro-form* (and (map? a0) (get a0 :&form))] + [macro-form* an] + [macro-form args])))) + +(comment (clj-865-workaround '() [{:&form "a"} "b"])) ;;;; Core API #?(:clj (defmacro have - "Takes a pred and one or more vals. Tests pred against each val, - trapping errors. If any pred test fails, throws a detailed assertion error. - Otherwise returns input val/vals for convenient inline-use/binding. + "Takes a (fn pred [x]) => truthy, and >=1 vals. + Tests pred against each val,trapping errors. + + If any pred test fails, throws a detailed `ExceptionInfo`. + Otherwise returns input val/s for convenient inline-use/binding. - Respects *assert* value so tests can be elided from production for zero - runtime costs. + Respects `*assert*`, so tests can be elided from production if desired + (meaning zero runtime cost). Provides a small, simple, flexible feature subset to alternative tools like clojure.spec, core.typed, prismatic/schema, etc. - ;; Will throw a detailed error message on invariant violation: - (fn my-fn [x] (str/trim (have string? x))) + Examples: - You may attach arbitrary debug info to assertion violations like: - `(have string? x :data {:my-arbitrary-debug-info \"foo\"})` + (defn my-trim [x] (str/trim (have string? x))) - Re: use of Truss assertions within other macro bodies: - Due to CLJ-865, call site information (e.g. line number) of - outer macro will unfortunately be lost. + ;; Attach arb optional info to violations using `:data`: + (have string? x + :data {:my-arbitrary-debug-info \"foo\"}) - See `keep-callsite` util for a workaround. + ;; Assert inside collections using `:in`: + (have string? :in [\"foo\" \"bar\"]) + + Regarding use within other macros: + Due to CLJ-865, callsite info like line number of outer macro + will be lost. See `keep-callsite` for workaround. See also `have?`, `have!`." {:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])} @@ -52,10 +73,10 @@ #?(:clj (defmacro have? - "Like `have` but returns `true` on successful tests. In particular, this - can be handy for use with :pre/:post conditions. Compare: - (fn my-fn [x] {:post [(have nil? %)]} nil) ; {:post [nil]} FAILS - (fn my-fn [x] {:post [(have? nil? %)]} nil) ; {:post [true]} passes as intended" + "Like `have` but returns `true` on successful tests. + Handy for `:pre`/`:post` conditions. Compare: + ((fn my-fn [] {:post [(have nil? %)]} nil)) ; {:post [nil ]} FAILS + ((fn my-fn [] {:post [(have? nil? %)]} nil)) ; {:post [true]} passes as intended" {:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])} [& args] (let [[&form args] (clj-865-workaround &form args) @@ -64,8 +85,8 @@ #?(:clj (defmacro have! - "Like `have` but ignores *assert* value (so can never be elided). Useful - for important conditions in production (e.g. security checks)." + "Like `have` but ignores `*assert*` value (so can never be elided!). + Useful for important conditions in production (e.g. security checks)." {:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])} [& args] (let [[&form args] (clj-865-workaround &form args) @@ -74,12 +95,11 @@ #?(:clj (defmacro have!? - "Specialized cross between `have?` and `have!`. Not used often but can be - handy for semantic clarification and/or to improve multi-val performance - when the return vals aren't necessary. - - **WARNING**: Do NOT use in :pre/:post conds since those are ALWAYS subject - to *assert*, directly contradicting the intention of the bang (`!`) here." + "Returns `true` on successful tests, and ignores `*assert*` value + (so can never be elided!). + + **WARNING**: Do NOT use in `:pre`/`:post` conditions since those always + respect `*assert*`, contradicting the intention of the bang (`!`) here." {:arglists '([x] [pred (:in) x] [pred (:in) x & more-xs])} [& args] (let [[&form args] (clj-865-workaround &form args) @@ -91,33 +111,28 @@ (macroexpand '(have a)) (macroexpand '(have? [:or nil? string?] "hello")) - (enc/qb 1e5 + (enc/qb 1e6 ; [260.08 294.62] (with-error-fn nil (have? string? 5)) (with-error-fn (fn [_] :truss/error) (have? string? 5))) (have string? (range 1000))) (comment - ;; HotSpot is great with these: - (enc/qb 1e4 + (enc/qb 1e6 ; [37.97 46.3 145.57 131.99 128.65] (string? "a") (have? "a") (have string? "a" "b" "c") (have? [:or nil? string?] "a" "b" "c") (have? [:or nil? string?] "a" "b" "c" :data "foo")) - ;; [ 5.59 26.48 45.82 ] ; 1st gen (macro form) - ;; [ 3.31 13.48 36.22 ] ; 2nd gen (fn form) - ;; [0.82 1.75 7.57 27.05 ] ; 3rd gen (lean macro form) - ;; [0.4 0.47 1.3 1.77 1.53] ; 4th gen (macro preds) - (enc/qb 1e4 + (enc/qb 1e6 ; [75.73 75.88] (have string? :in ["foo" "bar" "baz"]) (have? string? :in ["foo" "bar" "baz"])) (macroexpand '(have string? 5)) (macroexpand '(have string? 5 :data "foo")) - (macroexpand '(have string? 5 :data (enc/get-env))) - (let [x :x] (have string? 5 :data (enc/get-env))) + (macroexpand '(have string? 5 :data (enc/get-locals))) + (let [x :x] (have string? 5 :data (enc/get-locals))) (have string? 5) (have string? 5 :data {:a "a"}) @@ -125,7 +140,7 @@ ((fn [x] (let [a "a" b "b"] - (have string? x :data {:env (enc/get-env)}))) 5) + (have string? x :data {:env (enc/get-locals)}))) 5) (do (set! *assert* false) @@ -149,29 +164,6 @@ ;;;; Utils -#?(:clj - (defmacro keep-callsite - "CLJ-865 unfortunately means that it's currently not possible - for an inner macro to access the &form metadata of an outer macro. - - This means that inner macros lose call site information like the - line number of the outer macro. - - This util offers a workaround for macro authors: - - (defmacro my-macro1 [x] `(truss/have ~x)) ; W/o call site info - (defmacro my-macro2 [x] (keep-callsite `(truss/have ~x))) ; With call site info" - - {:added "v1.8.0 (2022-12-13)"} - [& body] `(with-meta (do ~@body) (meta ~'&form)))) - -(comment - (defmacro my-macro1 [x] `(have ~x)) - (defmacro my-macro2 [x] (keep-callsite `(have ~x))) - - (my-macro1 nil) - (my-macro2 nil)) - (defn get-data "Returns current value of dynamic assertion data." [] impl/*data*) @@ -197,13 +189,13 @@ ;;;; Deprecated -(defn get-dynamic-assertion-data +(defn ^:no-doc get-dynamic-assertion-data {:deprecated "v1.7.0 (2022-11-16)" :doc "Prefer `get-data`"} [] impl/*data*) #?(:clj - (defmacro with-dynamic-assertion-data + (defmacro ^:no-doc with-dynamic-assertion-data {:deprecated "v1.7.0 (2022-11-16)" :doc "Prefer `with-data`"} [data & body] `(binding [impl/*data* ~data] ~@body))) diff --git a/src/taoensso/truss/impl.cljc b/src/taoensso/truss/impl.cljc index 3877ef0..e39b365 100644 --- a/src/taoensso/truss/impl.cljc +++ b/src/taoensso/truss/impl.cljc @@ -16,20 +16,17 @@ ;; - Allows Encore to depend on Truss (esp. nb for back-compatibility wrappers). ;; - Allows Truss to be entirely dependency free. -#?(:clj (defmacro if-cljs [then else] (if (:ns &env) then else))) #?(:clj (defmacro catching - "Cross-platform try/catch/finally." - ;; Very unfortunate that CLJ-1293 has not yet been addressed ([try-expr ] `(catching ~try-expr ~'_ nil)) ([try-expr error-sym catch-expr] - `(if-cljs - (try ~try-expr (catch js/Error ~error-sym ~catch-expr)) - (try ~try-expr (catch Throwable ~error-sym ~catch-expr)))) + (if (:ns &env) + `(try ~try-expr (catch js/Error ~error-sym ~catch-expr)) + `(try ~try-expr (catch Throwable ~error-sym ~catch-expr)))) ([try-expr error-sym catch-expr finally-expr] - `(if-cljs - (try ~try-expr (catch js/Error ~error-sym ~catch-expr) (finally ~finally-expr)) - (try ~try-expr (catch Throwable ~error-sym ~catch-expr) (finally ~finally-expr)))))) + (if (:ns &env) + `(try ~try-expr (catch js/Error ~error-sym ~catch-expr) (finally ~finally-expr)) + `(try ~try-expr (catch Throwable ~error-sym ~catch-expr) (finally ~finally-expr)))))) (defn rsome [pred coll] (reduce (fn [acc in] (when-let [p (pred in)] (reduced p))) nil coll)) (defn revery? [pred coll] (reduce (fn [acc in] (if (pred in) true (reduced nil))) true coll)) @@ -45,15 +42,15 @@ (defn #?(:clj ks-nnil? :cljs ^boolean ks-nnil?) [ks m] (revery? #(some? (get m %)) ks))) #?(:clj - (defn get-source [form env] - (let [{:keys [line column file]} (meta form) + (defn get-source [macro-form macro-env] + (let [{:keys [line column file]} (meta macro-form) file - (if-not (:ns env) - *file* ; Compiling clj - (or ; Compiling cljs - (when-let [url (and file (try (io/resource file) (catch Throwable _ nil)))] - (try (.getPath (io/file url)) (catch Throwable _ nil)) - (do (str url))) + (if-not (:ns macro-env) + *file* ; Compiling Clj + (or ; Compiling Cljs + (when-let [url (and file (catching (io/resource file)))] + (catching (.getPath (io/file url))) + (do (str url))) file))] {:ns (str *ns*) @@ -67,30 +64,31 @@ (comment (io/resource "taoensso/truss.cljc")) #?(:clj - (let [resolve-clj clojure.core/resolve - resolve-cljs - (when-let [ns (find-ns 'cljs.analyzer.api)] - (when-let [v (ns-resolve ns 'resolve)] @v))] - - (defn resolve-var - #?(:clj ([sym] (resolve-clj sym))) - ([env sym] - (when (symbol? sym) - (if (:ns env) - (when resolve-cljs (resolve-cljs env sym)) - (do (resolve-clj env sym)))))))) - -(comment (resolve-var nil 'string?)) - -#?(:clj - (defn- var->sym [cljs? v] - (let [m (if cljs? v (meta v))] - (symbol (str (:ns m)) (name (:name m)))))) + (defn- var-info + "Returns ?{:keys [var meta ns name ...]} for given symbol." + [macro-env sym] + (when (symbol? sym) + (if (:ns macro-env) + (let [ns (find-ns 'cljs.analyzer.api) + v (ns-resolve ns 'resolve)] ; Don't cache! + (@v macro-env sym)) ; ?{:keys [meta ns name ...]} + + (when-let [v (resolve macro-env sym)] + (let [m (meta v)] + {:var v + :meta + (if-let [x (get m :arglists)] + (assoc m :arglists `'~x) ; Quote + (do m)) + + :ns (get m :ns) + :name (get m :name)})))))) #?(:clj - (defn resolve-sym - #?(:clj ([sym] (when-let [v (resolve-var sym)] (var->sym false v)))) - ([env sym] (when-let [v (resolve-var env sym)] (var->sym (:ns env) v))))) + (defn resolve-sym [macro-env sym] + (when (symbol? sym) + (when-let [{var-ns :ns, var-name :name} (var-info macro-env sym)] + (symbol (str var-ns) (name var-name)))))) (comment (resolve-sym nil 'string?)) @@ -118,12 +116,12 @@ #?(:clj (defn- safe-pred-form? "Returns true for common preds that can't throw." - [env pred-form] + [macro-env pred-form] (or (keyword? pred-form) (map? pred-form) (set? pred-form) - (when-let [rsym (resolve-sym env pred-form)] + (when-let [rsym (resolve-sym macro-env pred-form)] (contains? safe-pred-forms rsym))))) (comment (safe-pred-form? nil 'nil?)) @@ -131,21 +129,22 @@ #?(:clj (defn parse-pred-form "Returns {:keys [pred rsym safe?]}" - [env pred-form] + [macro-env pred-form] (cond - (= pred-form ::some?) (parse-pred-form env `some?) + (= pred-form ::some?) (parse-pred-form macro-env `some?) (not (vector? pred-form)) - {:pred-form pred-form - :rsym (resolve-sym env pred-form) - :safe? (safe-pred-form? env pred-form)} + {:pred-form pred-form + :rsym (resolve-sym macro-env pred-form) + :safe? (safe-pred-form? macro-env pred-form)} :else (let [[type a1 a2 a3] pred-form num-args (dec (count pred-form))] (when (or (< num-args 1) (> num-args 3)) - (throw (ex-info "Truss special predicates should have 1≤n≤3" - {:pred-form pred-form}))) + (throw + (ex-info "Truss special predicates should have 1≤n≤3" + {:pred-form pred-form}))) (case type :set= {:pred-form `(fn [~'x] (= (ensure-set ~'x) (ensure-set ~a1)))} @@ -168,9 +167,9 @@ (:and :or :not) ; Composition (let [;;; Support recursive expansion - {a1 :pred-form, sf-a1? :safe?} (when a1 (parse-pred-form env a1)) - {a2 :pred-form, sf-a2? :safe?} (when a2 (parse-pred-form env a2)) - {a3 :pred-form, sf-a3? :safe?} (when a3 (parse-pred-form env a3)) + {a1 :pred-form, sf-a1? :safe?} (when a1 (parse-pred-form macro-env a1)) + {a2 :pred-form, sf-a2? :safe?} (when a2 (parse-pred-form macro-env a2)) + {a3 :pred-form, sf-a3? :safe?} (when a3 (parse-pred-form macro-env a3)) sf-a1 (when a1 (if sf-a1? a1 `(safe-pred ~a1))) sf-a2 (when a2 (if sf-a2? a2 `(safe-pred ~a2))) @@ -212,16 +211,6 @@ (parse-pred-form nil [:and 'string? 'seq]) (parse-pred-form nil [:and 'integer? [:and 'number? 'pos? 'int?]])]) -;; #?(:clj -;; (defn- fast-pr-str -;; "Combination `with-out-str`, `pr`. Ignores *print-dup*." -;; [x] -;; (let [w (java.io.StringWriter.)] -;; (print-method x w) -;; (.toString w)))) - -;; (comment (enc/qb 1e5 (pr-str {:a :A}) (fast-pr-str {:a :A}))) - (defn- error-message ;; Temporary, to support Clojure 1.9 ;; Clojure 1.10+ now has `ex-message` @@ -242,16 +231,6 @@ arg-val (if undefn-arg? 'truss/undefined-arg arg) arg-type (if undefn-arg? 'truss/undefined-arg (type arg)) - ;; arg-str - ;; (cond - ;; undefn-arg? "" - ;; (nil? arg) "" - ;; :else - ;; (binding [*print-readably* false - ;; *print-length* 3] - ;; #?(:clj (fast-pr-str arg) - ;; :cljs (pr-str arg)))) - ?err (cond (identical? -dummy-error ?err) nil @@ -261,7 +240,7 @@ msg_ (delay - (let [;arg-form (if (nil? arg-form) 'nil arg-form) + (let [; arg-form (if (nil? arg-form) 'nil arg-form) msg (str "Invariant failed at " ns-sym (when ?line (str "[" ?line (when ?column (str "," ?column)) "]")) ": " diff --git a/test/taoensso/truss_tests.cljc b/test/taoensso/truss_tests.cljc index c3006b7..bbb4e10 100644 --- a/test/taoensso/truss_tests.cljc +++ b/test/taoensso/truss_tests.cljc @@ -105,9 +105,10 @@ :cljs 'cljs.core/string?)))]) (testing "Special preds" - [(is (= nil (have [:or nil? string?] nil))) - (is (= "hello" (have [:or nil? string?] "hello"))) - (is (= "hello" (have [:or pos? string?] "hello"))) + [(is (= nil (have [:or nil? string?] nil))) + (is (= "hello" (have [:or nil? string?] "hello"))) + (is (= "hello" (have [:or pos? string?] "hello"))) + (is (= ["a" "b"] (have [:or pos? string?] "a" "b"))) (is (throws? :common {:arg {:value -5}} (have [:or pos? string?] -5)))