From 700903ee7cfa4a68a80aca6281d20294b8f5f47b Mon Sep 17 00:00:00 2001
From: Oleksandr Yakushev <alex@bytopia.org>
Date: Wed, 14 Aug 2024 16:09:44 +0300
Subject: [PATCH 1/2] [trace] Add orchard.trace

---
 src/orchard/trace.clj                 | 136 ++++++++++++++
 test/orchard/trace_test.clj           | 248 ++++++++++++++++++++++++++
 test/orchard/trace_test/sample_ns.clj |  25 +++
 3 files changed, 409 insertions(+)
 create mode 100644 src/orchard/trace.clj
 create mode 100644 test/orchard/trace_test.clj
 create mode 100644 test/orchard/trace_test/sample_ns.clj

diff --git a/src/orchard/trace.clj b/src/orchard/trace.clj
new file mode 100644
index 00000000..a2d8fa3c
--- /dev/null
+++ b/src/orchard/trace.clj
@@ -0,0 +1,136 @@
+(ns orchard.trace
+  "Faster and prettier reimplementation of `clojure.tools.trace` with unnecessary
+  parts removed. Used for tracing function invocations and their results."
+  (:require [clojure.string :as string]
+            [orchard.print :as print]))
+
+;;;; Internals
+
+(defmacro ^:private appendn
+  "Add all strings in `args` to the given StringBuilder `sb`."
+  [sb & args]
+  (let [sbsym (with-meta (gensym "sb") {:tag 'StringBuilder})]
+    `(let [~sbsym ~sb]
+       ~@(map (fn [x] `(.append ~sbsym ~x)) args))))
+
+(defn- funcall-to-string
+  "Print the invokation of a function and arguments to a string."
+  ([fname args]
+   (funcall-to-string fname args ""))
+  ([fname, args, ^String outer-prefix]
+   (let [max-args 20 ;; Most args we want to display before truncating.
+         res (StringBuilder.)
+         argn (bounded-count (inc max-args) args)
+         more-than-max-args? (> argn max-args)
+         argn (min argn max-args)]
+     (appendn res outer-prefix "(" fname)
+     (dotimes [i argn]
+       (let [arg (nth args i)]
+         (appendn res " " arg)))
+     (when more-than-max-args?
+       (.append res " ..."))
+     (.append res ")")
+     (.toString res))))
+
+(def ^:private ^:dynamic *depth* 0)
+
+(defn- trace-indent []
+  (string/join (repeat *depth* "│ ")))
+
+(defn- trace-fn-call [name f args]
+  ;; Good defaults for orchard.print.
+  (binding [*print-length* 100
+            *print-level* 5
+            print/*max-atom-length* 150
+            print/*max-total-length* 10000]
+    (println (trace-indent))
+    (println
+     (funcall-to-string (str name) (map print/print-str args)
+                        (trace-indent)))
+    (let [value (binding [*depth* (inc *depth*)]
+                  (apply f args))
+          res-prefix "└─→ "
+          value-str (print/print-str value)]
+      (binding [*depth* (inc *depth*)]
+        (println (trace-indent)))
+      (println (str (trace-indent) res-prefix value-str))
+      value)))
+
+(defn- resolve-var ^clojure.lang.Var [v]
+  (if (var? v) v (resolve v)))
+
+;;;; Public API
+
+(def ^:private traced-vars (atom #{}))
+(def ^:private traced-nses (atom #{}))
+
+(defn traceable?
+  "Return true if the given var can be traced."
+  [v]
+  (let [v (resolve-var v)]
+    (and (ifn? @v) (not (:macro (meta v))))))
+
+(defn traced?
+  "Return true if the given var is currently traced."
+  [v]
+  (let [v (resolve-var v)]
+    (contains? (meta v) ::traced)))
+
+(defn trace-var*
+  "If the specified Var holds an IFn and is not marked as a macro, its
+  contents is replaced with a version wrapped in a tracing call;
+  otherwise nothing happens. Can be undone with `untrace-var*`."
+  [v]
+  (let [v (resolve-var v)
+        ns (.ns v)
+        s  (.sym v)]
+    (when (and (traceable? v) (not (traced? v)))
+      (let [f @v
+            vname (symbol (str ns "/" s))]
+        (swap! traced-vars conj v)
+        (alter-var-root v #(fn tracing-wrapper [& args]
+                             (trace-fn-call vname % args)))
+        (alter-meta! v assoc ::traced f)
+        v))))
+
+(defn untrace-var*
+  "Reverses the effect of `trace-var*` for the given Var, replacing the traced
+  function with the original, untraced version. No-op for non-traced Vars."
+  [v]
+  (let [v (resolve-var v)
+        f (::traced (meta v))]
+    (when f
+      (alter-var-root v (constantly (::traced (meta v))))
+      (alter-meta! v dissoc ::traced)
+      (swap! traced-vars disj v)
+      v)))
+
+(defn trace-ns*
+  "Trace all Vars in the given namespace. Can be undone with `untrace-ns*`. `ns`
+  should be a namespace object or a symbol.
+
+  No-op for clojure.core and orchard.trace."
+  [ns]
+  (let [ns (the-ns ns)]
+    (when-not ('#{clojure.core orchard.trace} (.name ns))
+      (->> (ns-interns ns)
+           vals
+           (filter (comp fn? var-get))
+           (run! trace-var*))
+      (swap! traced-nses conj ns))))
+
+(defn untrace-ns*
+  "Untrace all Vars in the given namespace."
+  [ns]
+  (let [ns (the-ns ns)]
+    (->> (ns-interns ns)
+         vals
+         (filter (comp fn? var-get))
+         (run! untrace-var*))
+    (swap! traced-nses disj ns)))
+
+(defn untrace-all
+  "Reverses the effect of tracing for all already traced vars and namespaces."
+  []
+  (run! untrace-ns* @traced-nses)
+  (run! untrace-var* @traced-vars))
diff --git a/test/orchard/trace_test.clj b/test/orchard/trace_test.clj
new file mode 100644
index 00000000..674e478a
--- /dev/null
+++ b/test/orchard/trace_test.clj
@@ -0,0 +1,248 @@
+(ns orchard.trace-test
+  (:require
+   [clojure.test :as t :refer [is are deftest]]
+   [orchard.trace :as sut]
+   [orchard.trace-test.sample-ns :as sample-ns]))
+
+(deftest funcall-to-string-test
+  (are [args expected] (= expected (#'sut/funcall-to-string "foo" args "|||  "))
+    []                                   "|||  (foo)"
+    (map str (range 10 20))              "|||  (foo 10 11 12 13 14 15 16 17 18 19)"
+    (map str (range 10 30))              "|||  (foo 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29)"
+    ["bar" "baz" (apply str (range 20))] "|||  (foo bar baz 012345678910111213141516171819)"
+    ["bar" "baz" (apply str (range 25))] "|||  (foo bar baz 0123456789101112131415161718192021222324)"
+    [(apply str (range 25)) "bar" "baz"] "|||  (foo 0123456789101112131415161718192021222324 bar baz)"
+    [(apply str (range 50)) "bar" "baz"] "|||  (foo 012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 bar baz)"))
+
+(def arg1 "dasdajlksjdalsjdlajasdasasdasdasdsdads")
+(def arg2 "28347029384-asdaasdassdasd128-08-asd2834209")
+(def expected-trace-result
+  "
+(orchard.trace-test.sample-ns/qux \"dasdajlksjdalsjdlajasdasasdasdasdsdads\" \"28347029384-asdaasdassdasd128-08-asd2834209\")
+│ 
+│ (orchard.trace-test.sample-ns/foo \"dasdajlksjdalsjdlajasdasasdasdasdsdads\" \"28347029384-asdaasdassdasd128-08-asd2834209\")
+│ │ 
+│ │ (orchard.trace-test.sample-ns/bar \"dasdajlksjdalsjdlajasdasasdasdasdsdads\")
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/baz 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ...)
+│ │ │ │ 
+│ │ │ └─→ nil
+│ │ │ 
+│ │ └─→ \"dasdajlksjdalsjdlajasdasasdasdasdsdadshello\"
+│ │ 
+│ └─→ \"dasdajlksjdalsjdlajasdasasdasdasdsdadshello9024382dsa-80-821dsadssadsaadsa-48392074382\"
+│ 
+└─→ \"dasdajlksjdalsjdlajasdasasdasdasdsdadshello9024382dsa-80-821dsadssadsaadsa-48392074382\"
+")
+(def expected-trace-result-no-foo
+  "
+(orchard.trace-test.sample-ns/qux \"dasdajlksjdalsjdlajasdasasdasdasdsdads\" \"28347029384-asdaasdassdasd128-08-asd2834209\")
+│ 
+│ (orchard.trace-test.sample-ns/bar \"dasdajlksjdalsjdlajasdasasdasdasdsdads\")
+│ │ 
+│ │ (orchard.trace-test.sample-ns/baz 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ...)
+│ │ │ 
+│ │ └─→ nil
+│ │ 
+│ └─→ \"dasdajlksjdalsjdlajasdasasdasdasdsdadshello\"
+│ 
+└─→ \"dasdajlksjdalsjdlajasdasasdasdasdsdadshello9024382dsa-80-821dsadssadsaadsa-48392074382\"
+")
+(def vars [#'sample-ns/baz #'sample-ns/bar #'sample-ns/foo #'sample-ns/qux])
+
+(deftest basic-test
+  (run! sut/trace-var* vars)
+  (is (= expected-trace-result (with-out-str (sample-ns/qux arg1 arg2))))
+
+  (run! sut/untrace-var* vars)
+  (is (= "" (with-out-str (sample-ns/qux arg1 arg2))))
+
+  (run! sut/trace-var* vars)
+  (sut/untrace-var* #'sample-ns/foo)
+  (is (= expected-trace-result-no-foo (with-out-str (sample-ns/qux arg1 arg2))))
+
+  (sut/trace-ns* 'orchard.trace-test.sample-ns)
+  (is (= expected-trace-result (with-out-str (sample-ns/qux arg1 arg2))))
+
+  (sut/untrace-ns* 'orchard.trace-test.sample-ns)
+  (is (= "" (with-out-str (sample-ns/qux arg1 arg2))))
+
+  (sut/trace-ns* 'orchard.trace-test.sample-ns)
+  (sut/untrace-all)
+  (is (= "" (with-out-str (sample-ns/qux arg1 arg2)))))
+
+(deftest fibo-test
+  (sut/trace-ns* 'orchard.trace-test.sample-ns)
+
+  (is (= "
+(orchard.trace-test.sample-ns/fibo 5)
+│ 
+│ (orchard.trace-test.sample-ns/fibo 3)
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ 
+│ │ └─→ 1
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 2)
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 0)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ └─→ 2
+│ │ 
+│ └─→ 3
+│ 
+│ (orchard.trace-test.sample-ns/fibo 4)
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 2)
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 0)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ └─→ 2
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 3)
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 2)
+│ │ │ │ 
+│ │ │ │ (orchard.trace-test.sample-ns/fibo 0)
+│ │ │ │ │ 
+│ │ │ │ └─→ 1
+│ │ │ │ 
+│ │ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ │ 
+│ │ │ │ └─→ 1
+│ │ │ │ 
+│ │ │ └─→ 2
+│ │ │ 
+│ │ └─→ 3
+│ │ 
+│ └─→ 5
+│ 
+└─→ 8
+"
+         (with-out-str (sample-ns/fibo 5))))
+
+  (is (= "
+(orchard.trace-test.sample-ns/fibo 5)
+│ 
+│ (orchard.trace-test.sample-ns/fibo 3)
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ 
+│ │ └─→ 1
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 2)
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 0)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ └─→ 2
+│ │ 
+│ └─→ 3
+│ 
+│ (orchard.trace-test.sample-ns/fibo 4)
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 2)
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 0)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ └─→ 2
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo 3)
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ 
+│ │ │ └─→ 1
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo 2)
+│ │ │ │ 
+│ │ │ │ (orchard.trace-test.sample-ns/fibo 0)
+│ │ │ │ │ 
+│ │ │ │ └─→ 1
+│ │ │ │ 
+│ │ │ │ (orchard.trace-test.sample-ns/fibo 1)
+│ │ │ │ │ 
+│ │ │ │ └─→ 1
+│ │ │ │ 
+│ │ │ └─→ 2
+│ │ │ 
+│ │ └─→ 3
+│ │ 
+│ └─→ 5
+│ 
+└─→ 8
+"
+         (with-out-str (sample-ns/fibo 5))))
+
+  (is (= "
+(orchard.trace-test.sample-ns/fibo2 0 1 10)
+│ 
+│ (orchard.trace-test.sample-ns/fibo2 1 1 9)
+│ │ 
+│ │ (orchard.trace-test.sample-ns/fibo2 1 2 8)
+│ │ │ 
+│ │ │ (orchard.trace-test.sample-ns/fibo2 2 3 7)
+│ │ │ │ 
+│ │ │ │ (orchard.trace-test.sample-ns/fibo2 3 5 6)
+│ │ │ │ │ 
+│ │ │ │ │ (orchard.trace-test.sample-ns/fibo2 5 8 5)
+│ │ │ │ │ │ 
+│ │ │ │ │ │ (orchard.trace-test.sample-ns/fibo2 8 13 4)
+│ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ (orchard.trace-test.sample-ns/fibo2 13 21 3)
+│ │ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ │ (orchard.trace-test.sample-ns/fibo2 21 34 2)
+│ │ │ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ │ │ (orchard.trace-test.sample-ns/fibo2 34 55 1)
+│ │ │ │ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ │ │ │ (orchard.trace-test.sample-ns/fibo2 55 89 0)
+│ │ │ │ │ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ │ │ │ └─→ 89
+│ │ │ │ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ │ │ └─→ 89
+│ │ │ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ │ └─→ 89
+│ │ │ │ │ │ │ │ 
+│ │ │ │ │ │ │ └─→ 89
+│ │ │ │ │ │ │ 
+│ │ │ │ │ │ └─→ 89
+│ │ │ │ │ │ 
+│ │ │ │ │ └─→ 89
+│ │ │ │ │ 
+│ │ │ │ └─→ 89
+│ │ │ │ 
+│ │ │ └─→ 89
+│ │ │ 
+│ │ └─→ 89
+│ │ 
+│ └─→ 89
+│ 
+└─→ 89
+"
+         (with-out-str (sample-ns/fibo2 0 1 10)))))
diff --git a/test/orchard/trace_test/sample_ns.clj b/test/orchard/trace_test/sample_ns.clj
new file mode 100644
index 00000000..82ab8b6e
--- /dev/null
+++ b/test/orchard/trace_test/sample_ns.clj
@@ -0,0 +1,25 @@
+(ns orchard.trace-test.sample-ns
+  (:require clojure.string))
+
+(defn baz [& _]
+  nil)
+
+(defn bar [a]
+  (apply baz (range))
+  (str a "hello"))
+
+(defn foo [a b]
+  (str (bar a) (clojure.string/reverse b)))
+
+(defn qux [a b]
+  (foo a b))
+
+(defn fibo [n]
+  (if (<= n 1)
+    1
+    (+ (fibo (- n 2)) (fibo (- n 1)))))
+
+(defn fibo2 [a b n]
+  (if (= n 0)
+    b
+    (fibo2 b (+ a b) (dec n))))

From a0265da48bdede132793860b92aee74b7a4e291a Mon Sep 17 00:00:00 2001
From: Oleksandr Yakushev <alex@bytopia.org>
Date: Wed, 14 Aug 2024 16:21:53 +0300
Subject: [PATCH 2/2] Update CHANGELOG

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad6cf8b8..f8d88305 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## master (unreleased)
 
 * [#282](https://github.com/clojure-emacs/orchard/issues/282): Inspector: don't crash when inspecting internal classes.
+* [#284](https://github.com/clojure-emacs/orchard/issues/284): Add orchard.trace.
 
 ## 0.26.2 (2024-07-19)