(ns riddley.walk (:refer-clojure :exclude [macroexpand]) (:require [riddley.compiler :as cmp])) (defn macroexpand "Expands both macros and inline functions. Optionally takes a `special-form?` predicate which identifies first elements of expressions that shouldn't be macroexpanded, and honors local bindings." ([x] (macroexpand x nil)) ([x special-form?] (cmp/with-base-env (if (seq? x) (let [frst (first x)] (if (or (and special-form? (special-form? frst)) (contains? (cmp/locals) frst)) ;; might look like a macro, but for our purposes it isn't x (let [x' (macroexpand-1 x)] (if-not (identical? x x') (macroexpand x' special-form?) ;; if we can't macroexpand any further, check if it's an inlined function (if-let [inline-fn (and (seq? x') (symbol? (first x')) (-> x' meta ::transformed not) (or (-> x' first resolve meta :inline-arities not) ((-> x' first resolve meta :inline-arities) (count (rest x')))) (-> x' first resolve meta :inline))] (let [x'' (with-meta (apply inline-fn (rest x')) (meta x'))] (macroexpand ;; unfortunately, static function calls can look a lot like what we just ;; expanded, so prevent infinite expansion (if (= '. (first x'')) (with-meta (concat (butlast x'') [(if (instance? clojure.lang.IObj (last x'')) (with-meta (last x'') (merge (meta (last x'')) {::transformed true})) (last x''))]) (meta x'')) x'') special-form?)) x'))))) x)))) ;;; (def ^:private special-forms (into #{} (keys (. clojure.lang.Compiler specials)))) (defn- special-meta [[op & body]] (list* (vary-meta op assoc ::special true) body)) (defn- do-handler [f [_ & body]] (list* 'do (doall (map f body)))) (defn- fn-handler [f x] (let [prelude (take-while (complement sequential?) x) remainder (drop (count prelude) x) remainder (if (vector? (first remainder)) (list remainder) remainder) body-handler (fn [x] (cmp/with-lexical-scoping (doseq [arg (first x)] (cmp/register-arg arg)) (doall (list* (first x) (map f (rest x))))))] (cmp/with-lexical-scoping ;; register a local for the function, if it's named (when-let [nm (second prelude)] (cmp/register-local nm (list* 'fn* nm (map #(take 1 %) remainder)))) (concat prelude (if (seq? (first remainder)) (doall (map body-handler remainder)) [(body-handler remainder)]))))) (defn- def-handler [f x] (let [[_ n & r] x] (cmp/with-lexical-scoping (cmp/register-local n '()) (list* 'def (f n) (doall (map f r)))))) (defn- let-bindings [f x recursive?] (let [pairs (partition-all 2 x)] (when recursive? (doall (map (fn [[k v]] (cmp/register-local k nil)) pairs))) (->> pairs (mapcat (fn [[k v]] (let [[k v] [k (f v)]] (cmp/register-local k v) [k v]))) vec))) (defn- reify-handler [f x] (let [[_ classes & fns] x] (list* 'reify* classes (doall (map (fn [[nm args & body]] (cmp/with-lexical-scoping (doseq [arg args] (cmp/register-arg arg)) (list* nm args (doall (map f body))))) fns))))) (defn- deftype-handler [f x] (let [[_ type resolved-type args _ interfaces & fns] x] (cmp/with-lexical-scoping (doseq [arg args] (cmp/register-arg arg)) (list* 'deftype* type resolved-type args :implements interfaces (doall (map (fn [[nm args & body]] (cmp/with-lexical-scoping (doseq [arg args] (cmp/register-arg arg)) (list* nm args (doall (map f body))))) fns)))))) (defn- let-handler ([f x] (let-handler f x nil)) ([f x recursive?] (cmp/with-lexical-scoping (doall (list* (first x) (let-bindings f (second x) recursive?) (map f (drop 2 x))))))) (defn- letfn-handler [f x] (let-handler f x true)) (defn- case-handler [f [_ ge shift mask default imap switch-type check-type skip-check]] (let [prefix ['case* ge shift mask] suffix [switch-type check-type skip-check]] (concat prefix [(f default)] [(let [m (->> imap (map (fn [[k [idx form]]] [k [idx (f form)]])) (into {}))] (if (every? number? (keys m)) (into (sorted-map) m) m))] suffix))) (defn- catch-handler [f x] (let [[_ type var & body] x] (cmp/with-lexical-scoping (when var (cmp/register-arg (with-meta var (merge (meta var) {:tag type})))) (list* 'catch type var (doall (map f body)))))) (defn- try-handler [f x] (let [[_ & body] x] (list* 'try (doall (map #(f % :try-clause? true) body))))) (defn- dot-handler [f x] (let [[_ hostexpr mem-or-meth & remainder] x] (list* '. (f hostexpr) (if (seq? mem-or-meth) (list* (first mem-or-meth) (doall (map f (rest mem-or-meth)))) (f mem-or-meth)) (doall (map f remainder))))) (defn walk-exprs "A walk function which only traverses valid Clojure expressions. The `predicate` describes whether the sub-form should be transformed. If it returns true, `handler` is invoked, and returns a transformed form. Unlike `clojure.walk`, if the handler is called, the rest of the sub-form is not walked. The handler function is responsible for recursively calling `walk-exprs` on the form it is given. Macroexpansion can be halted by defining a set of `special-form?` which will be left alone. Including `fn`, `let`, or other binding forms can break local variable analysis, so use with caution. The :try-clause? option indicates that a `try` clause is being walked. The special forms `catch` and `finally` are only special in `try` clauses." ([predicate handler x] (walk-exprs predicate handler nil x)) ([predicate handler special-form? x & {:keys [try-clause?]}] (cmp/with-base-env (let [x (try (macroexpand x special-form?) (catch ClassNotFoundException _ x)) walk-exprs' (partial walk-exprs predicate handler special-form?) x' (cond (and (seq? x) (= 'var (first x)) (predicate x)) (handler (eval x)) (and (seq? x) (= 'quote (first x)) (not (predicate x))) x (predicate x) (handler x) (seq? x) (if (or (and (not try-clause?) (#{'catch 'finally} (first x))) (not (contains? special-forms (first x)))) (doall (map walk-exprs' x)) ((condp = (first x) 'do do-handler 'def def-handler 'fn* fn-handler 'let* let-handler 'loop* let-handler 'letfn* letfn-handler 'case* case-handler 'try try-handler 'catch catch-handler 'reify* reify-handler 'deftype* deftype-handler '. dot-handler #(doall (map %1 %2))) walk-exprs' (special-meta x))) (instance? java.util.Map$Entry x) (clojure.lang.MapEntry. (walk-exprs' (key x)) (walk-exprs' (val x))) (or (set? x) (vector? x)) (into (empty x) (map walk-exprs' x)) (instance? clojure.lang.IRecord x) x (map? x) (into (empty x) (map walk-exprs' x)) ;; special case to handle clojure.test (and (symbol? x) (-> x meta :test)) (vary-meta x update-in [:test] walk-exprs') :else x)] (if (instance? clojure.lang.IObj x') (with-meta x' (merge (meta x) (meta x'))) x'))))) ;;; (defn macroexpand-all "Recursively macroexpands all forms, preserving the &env special variables." [x] (walk-exprs (constantly false) nil x)) (defn special-form? "Given sym, a symbol produced by walk-exprs, returns true if sym is a special form." [x] (when (symbol? x) (::special (meta x))))