Skip to content

Instantly share code, notes, and snippets.

@vlaaad

vlaaad/fdeps.clj Secret

Last active August 4, 2020 03:13
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vlaaad/0350b61e127e82165d195b490999ec0a to your computer and use it in GitHub Desktop.
Save vlaaad/0350b61e127e82165d195b490999ec0a to your computer and use it in GitHub Desktop.
Function call tree tracer that abuses function internals
(ns fdeps
(:import [java.lang.reflect Field Modifier]
[clojure.lang Var RT]))
(defn fdeps
([val]
(fdeps val {}))
([val m]
(reduce
(fn [acc var]
(let [acc (update acc val (fnil conj #{}) var)
var-val @var]
(if (contains? acc var-val)
acc
(fdeps var-val acc))))
m
(some->> val
class
.getDeclaredFields
(keep (fn [^Field f]
(or (and (identical? Var (.getType f))
(Modifier/isPublic (.getModifiers f))
(Modifier/isStatic (.getModifiers f))
(-> f .getName (.startsWith "const__"))
(.get f val))
nil)))))))
(defn f->sym [f]
(symbol (Compiler/demunge (.getName (type f)))))
(def ^:dynamic *calls*)
(defn track-calls [f args]
(let [*calls (atom [])
ret (binding [*calls* *calls]
(.applyTo f (RT/seq args)))]
{:form (->> args
(map #(or (some-> % meta ::f f->sym) %))
(cons (f->sym f)))
:ret ret
:calls @*calls}))
(defn trace [f]
(let [deps (fdeps f)
wrap-fn (fn [f]
(with-meta (fn [& args]
(let [result (track-calls f args)]
(swap! *calls* conj result)
(:ret result)))
{::f f}))
vars (vec (apply clojure.set/union (vals deps)))
old-vals (zipmap vars (map #(.getRawRoot %) vars))]
(try
(doseq [v vars]
(alter-var-root v wrap-fn))
(track-calls f nil)
(finally
(doseq [[var val] old-vals]
(.bindRoot var val))))))
@vlaaad
Copy link
Author

vlaaad commented Aug 29, 2019

Usage:

(defn factorial [n]
  (case n 
    (0 1) 1
    (* n (factorial (dec n)))))

(clojure.pprint/pprint (trace #(factorial 5)))
; => 
{:form (fdeps/eval94876/fn--94877),
 :ret 120,
 :calls
 [{:form (fdeps/factorial 5),
   :ret 120,
   :calls
   [{:form (fdeps/factorial 4),
     :ret 24,
     :calls
     [{:form (fdeps/factorial 3),
       :ret 6,
       :calls
       [{:form (fdeps/factorial 2),
         :ret 2,
         :calls
         [{:form (fdeps/factorial 1), :ret 1, :calls []}]}]}]}]}]}

(defn fib [n]
  (case n
    (0 1) 1
    (+ (fib (dec n)) (fib (dec (dec n))))))

(clojure.pprint/pprint (trace #(fib 4)))
; =>
{:form (fdeps/eval94990/fn--94991),
 :ret 5,
 :calls
 [{:form (fdeps/fib 4),
   :ret 5,
   :calls
   [{:form (fdeps/fib 3),
     :ret 3,
     :calls
     [{:form (fdeps/fib 2),
       :ret 2,
       :calls
       [{:form (fdeps/fib 1), :ret 1, :calls []}
        {:form (fdeps/fib 0), :ret 1, :calls []}]}
      {:form (fdeps/fib 1), :ret 1, :calls []}]}
    {:form (fdeps/fib 2),
     :ret 2,
     :calls
     [{:form (fdeps/fib 1), :ret 1, :calls []}
      {:form (fdeps/fib 0), :ret 1, :calls []}]}]}]}

Pros:

  • you don't have to edit/re-evaluate your function to debug it
  • very dynamic (no macros, uses reflection)

Cons:

  • can't trace every form, only ones that dereference vars (won't work with direct linking)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment