DoTrace - Provides a trace/1 macro to trace function calls at runtime, mimicking Clojure's dotrace output

I saw this LinkedIn post:

*Can your programming language do this?

This is a macro in Clojure called `dotrace`. When you surround a piece of code with it, it pretty prints the call tree along with its inputs/outputs, which is incredibly useful for debugging recursive functions.

Many other languages cannot do this with user-level code.

Clojure can because its syntax is “just” data structures and can be processed and rewritten on the fly by code of your own choosing. Like `dotrace`!

Macros effectively extend the compiler into userland, and it’s one of the reasons Lisp is so easy to fall in love with it.*

I don’t know about Lisp, but here’s an attempt in Elixir:

iex> defmodule Math do
       def factorial(0), do: 1
       def factorial(n) when n > 0, do: n * factorial(n - 1)
     end
iex> require DoTrace
iex> DoTrace.trace(Math.factorial(5))
TRACE t1028: (factorial [5])
TRACE t1092: | (factorial [4])
TRACE t1156: | | (factorial [3])
TRACE t1220: | | | (factorial [2])
TRACE t1284: | | | | (factorial [1])
TRACE t1348: | | | | | (factorial [0])
TRACE t1348: | | | | | | => 1
TRACE t1284: | | | | | => 1
TRACE t1220: | | | | => 2
TRACE t1156: | | | => 6
TRACE t1092: | | => 24
TRACE t1028: | => 120
120
8 Likes

Very cool feature! I would like to suggest few ideas:

  1. Add the module name and arity to trace logs
  2. Use :owl package to generate links that opens specific function source code
  3. Add option to disable trace log prefix to ensure output is clear
  4. In other case I would use :owl again to have borderless table with prefix as first column and log as the second one.

Look that in my case the output looks like:

TRACE t515: (factorial [5])
TRACE t643: | (factorial [4])
TRACE t771: | | (factorial [3])
TRACE t899: | | | (factorial [2])
TRACE t1027: | | | | (factorial [1])
TRACE t1155: | | | | | (factorial [0])
TRACE t1155: | | | | | | => 1
TRACE t1027: | | | | | => 1
TRACE t899: | | | | => 2
TRACE t771: | | | => 6
TRACE t643: | | => 24
TRACE t515: | => 120

t before number is rather useless I think, but it’s not important. Some integers have 4 digits and some only 3. Because of that the output could be more or less harder to read in some cases.

# version 1 (with option to disable prefix set to true - default?)
(Math.factorial/1 [5])
| (Math.factorial/1 [4])
| | (Math.factorial/1 [3])
| | | (Math.factorial/1 [2])
| | | | (Math.factorial/1 [1])
| | | | | (Math.factorial/1 [0])
| | | | | | => 1
| | | | | => 1
| | | | => 2
| | | => 6
| | => 24
| => 120

# version 2 (with option to disable prefix set to false)
TRACE t515:  (Math.factorial/1 [5])
TRACE t643:  | (Math.factorial/1 [4])
TRACE t771:  | | (Math.factorial/1 [3])
TRACE t899:  | | | (Math.factorial/1 [2])
TRACE t1027: | | | | (Math.factorial/1 [1])
TRACE t1155: | | | | | (Math.factorial/1 [0])
TRACE t1155: | | | | | | => 1
TRACE t1027: | | | | | => 1
TRACE t899:  | | | | => 2
TRACE t771:  | | | => 6
TRACE t643:  | | => 24
TRACE t515:  | => 120

Edit: Oh, looks like there is a bug with pipes:

iex> 5 |> Math.factorial() |> IO.puts() |> DoTrace.trace()
** (UndefinedFunctionError) function IO.puts/0 is undefined or private. Did you mean:

      * puts/1
      * puts/2

    (elixir 1.19.0-rc.0) IO.puts()
    iex:13: (file)
3 Likes

That looks great, but why is it not shaped as a custom backend to Kernel.dbg/2?

4 Likes

…skill issues? :joy::person_shrugging:

I don’t know enough yet. It was only meant as a proof of concept, hence the version number 0.1.0.

1 Like

Nah, c’mon. I just wanted to suggest that embedding into an existing ecosystem (especially if provided by the language core team) might save you few keystrokes, because Kernel.dbg/2 might actually offload a ton of work from you, providing a common mechanism for fancy output and/or real debugging with pry.

Still, it looks already very good, but a bit alien, that’s what was my humble comment about :slight_smile:

2 Likes

Alien?! Ever since I started with Elixir I kept wondering why don’t we get execution chains printed like that. They are much more intuitive to me, especially when there is recursion involved.

1 Like

It’s good, no worries. I consider this a proof of concept, and not a very good one at that. It looks alien because of the Process registry for storing the trace depth, and the spawning of asynchronous Tasks. This brings with it the Process.sleep(10), where 10 is arbitrary, so that this doesn’t fail:

 if Process.alive?(task.pid) do ... else ... end

I might get back to this at some point, when I learn more. I don’t know if this is a “proper” way of achieving the goal. If some “wizard” out there finds it worthwhile to develop further, feel free to send a PR–or to request the GitHub repo to be taken off my hands! @dimitarvp :wink:

1 Like

My wizarding qualities are sadly very limited lately but I can give you a review on a next prototype when you have it. I also haven’t done macros in a while.

1 Like

Sure. To me too. It does not make them elixirish, though. I prefer meerkats to mere cats, and yet this fact alone does not make a meerkat a less stranger pet.

1 Like

No macros no fun, if I recall this verse by Bob Marley correctly.

2 Likes

Well, if only Common LISP had the OTP runtime. The LISP folks practically beat most of the computer science game ages ago, including allow one to easily devise business-specific DSLs that compile to quite well optimised LISP.

3 Likes

Robert Virding thought so a while ago too. LFE for the rescue.

2 Likes

“It is an established fact that John McCarthy shared alien tech with the world in 1958 when he introduced us to Lisp. We continue that great tradition.”

This website is hilarious! :joy:

2 Likes

Robert is the best, yeah. Only the strongest sense of humor might make a person survive through writing the whole OTP in bare C language. Back then, even Prolog was prohibited, let alone LISP.

2 Likes

I’m aware, but I doubt it’s as well integrated as Elixir is at this point in time.

Eh. Define “integrated” please. AFAICT, LFE is fully functional, it’s just nobody likes parentheses anymore.

Ecto, Ash, Phoenix + LiveView, any framework that needs Elixir macros.

Give me the incantation that I must put somewhere and let it all work. Without that I’m not doing any other career shifts (with the only exception that’s becoming more and more likely with time being Rust, because the supply in Elixir’s job market looks to have diminished severely in the last two years).

Never was that for me. I can tolerate and even get used to and love almost any syntax as long as the compile-time guarantees and runtime quality are both excellent.

No LISP ever had that. Maybe Racket these days, but there’s not many volunteers and their parallelism story was progressing at a snail’s pace last time I checked them out (2-ish years ago).

1 Like