ExDebugger

Auto-annotates AST at various strategic places with debugging expressions to allow for fast iteration of debugging during development time.

Raison d’être

There are multiple ways of debugging an Elixir application:

  1. IO.puts/IO.inspect/Logger
  2. IEx.pry
  3. :debugger as explained here and here. There is also a VSCode plugin that automates the experience if you can get it to working. Likewise for emacs and for those who use IntelliJ Elixir
  4. Instrumenting:
  5. Tracing:

In my personal practice of doing TDD and getting to the bottom of a particular bug the above options are lacking:

In all the above cases, the main stumbling block on my part can be best summarized as that none of them are particularly fun to use in the context of a TDD-flow or quickly hacking something together. Debugging requires a certain focus and the shorter you can make the feedback loop the better.

As an experiment, I authored this library with the aim of shortening this feedback loop while keeping ceremony to a bear minimum and thus it seeks to:

  1. Be so simple it is accessible to every developer regardless as to their seniority level/experience. e.g.:
    1. All the obvious places in which change of state can occur are being annotated with hidden debugging statements to minimize clutter. These are:
      1. At the beginning of every def/defp
      2. At the end of every def/defp
      3. At every juncture inside every polyfurcating expression, e.g.:
        • case
        • if/else
        • cond
    2. All the hidden debugging expressions can be toggled on or off; allowing you to differentiate between development and production. As such:
      • you can selectively turn on/off places in your code base that are relevant to your TDD-flow and avoid clutter in the output you need to introspect
      • these hidden statements will not pose any additional overhead when running in production thus effectively allowing you to keep your entire code base intact. No more: “Oh, I merged in that commented-out IO.inspect into master”-type of nonsense.
  2. Be versatile so that it can accommodate finer granularity by giving you access to a macro that allows you to manually annotate alternative places in your code base while giving you access to the same benefits as enumerated under point 1 above.

I am personally using this in my projects to see how the flow works out from a practical stand point which may make me introduce extra features or potentially put a full halt to this project altogether. In the duration of this beta release, kindly feel free to experiment accordingly. Feedback is welcome.

Installation

def deps do
  [
    {:ex_debugger, "~> 0.1.2"}
  ]
end

Usage

config.exs:

import Config

config :ex_debugger, :debug_options_file, "#{File.cwd!()}/debug_options.exs"

debug_options.exs:

config :ex_debugger, :debug,
  capture: :stdout, #[:repo, :stdout, :both, :none]
  warn: false,
  all: false,
  "Elixir.HelloWorld": true

For auto-annotating your module with debugging statements:

hello_world.exs:

defmodule HelloWorld do
  use ExDebugger

  def hello(input) do
    if input == :world do
      "Hello World!"
    else
      "Hello Something Else: #{input}"
    end
  end
end

Effectively, this will manipulate the AST to look like this behind the scenes:

defmodule HelloWorld do
  use ExDebugger

  def hello(input) do
    if input == :world do
      "Hello World!" |> d(:if_statement, __ENV__, binding(), false)
    else
      "Hello Something Else: #{input}" |> d(:if_statement, __ENV__, binding(), false)
    end
    |> d(:def_output, __ENV__, binding(), false)
  end
end

Kindly consult the resources on ENV and binding() to appreciate the value they provide.

When running the following:

iex(1)> c("hello_world.exs")
iex(2)> HelloWorld.hello(:world)

you will see:

===================:if_statement======================
Piped Value: "Hello World!"
Bindings: [input: :world]

file: path_to_your_project/hello_world.exs:6
module: Elixir.HelloWorld
function: "&hello/1"

=============================================

===================:def_output_only======================
Piped Value: "Hello World!"
Bindings: [input: :world]

file: path_to_your_project/hello_world.exs:10
module: Elixir.HelloWorld
function: "&hello/1"

=============================================

This of course provided you have made the appropriate settings under debug_options.exs as depicted above.

In the case you need extra fine granularity then you can opt to do:

defmodule A do
  use ExDebugger.Manual
end

which will give you access to the macro dd\2,3 that can be used as follows:

defmodule HelloWorld do
  use ExDebugger.Manual

  def hello(input) do
    if input == :world do
      "Hello World!" |> dd(:investigation)
    else
      "Hello Something Else: #{input}"
    end
  end
end

This renders the following output:

iex(1)> c("hello_world.exs")
iex(2)> HelloWorld.hello(:world)
===================:investigation======================
Piped Value: "Hello World!"
Bindings: [input: :world]

file: path_to_project/hello_world.exs:6
module: Elixir.HelloWorld
function: "&hello/1"

=============================================

"Hello World!"

For manual debug, you need to configure debug_options.exs as follows:

config :ex_debugger, :manual_debug,
  capture: :stdout, #[:repo, :stdout, :both, :none]
  warn: false,
  all: false,
  "Elixir.HelloWorld": true

Both use ExDebugger and use ExDebugger.Manual can be used in tandem if need be.

Limitations

All usage is supported except for certain cases of defmacro __using__(_). Currently, the test scenarios involving defmacro __using__(_) have been forced to pass for the time being. If someone deeply cares about this, then feel free to submit advice/PR accordingly.

RoadMap

In the case that this library has good working potential, then the following are the features I would like to see implemented:

  1. Auto annotation of anonymous function cases and support for unless
  2. VSCode Extension:
    • Leveraging the persisted Debugging Events in ones project, walk through the code base while depicting change of state as a normal IDE debugger.
    • Allow for annotation of these persisted Debugging Events with comments to document certain observations.
    • Enable/Disable in the walk-through view certain Debugging Events by means of filters to eliminate clutter and/or to retain the essential subset that demonstrates the bug.
    • Persist this subset somehow for future replay purposes either in VSCode or either as a GIF. This may facilitate in communicating certain mistakes in the code base in one’s PR or issue tracker.

Contributing

In case you see potential in this library and would like to contribute then feel free to reach out.

docs