Debugging with IEx

You already know that IEx is the go-to utility to play with Elixir code. It also has a secret and dark second life as a debugger. It isn’t fancy, but it lets you get into a running program and examine the environment.

You enter the debugger when running Elixir code hits a breakpoint. There are two ways of creating a breakpoint. One works by adding calls into the code you want to debug. The other is initiated from inside IEx. We’ll look at both using the following (buggy) code:

tooling/buggy/lib/buggy.ex
 defmodule​ Buggy ​do
 def​ parse_header(
  <<
  format::integer-16,
  tracks::integer-16,
» division::integer-16
  >>
  ) ​do
 
  IO.puts ​"​​format: ​​#{​format​}​​"
  IO.puts ​"​​tracks: ​​#{​tracks​}​​"
  IO.puts ​"​​division: ​​#{​decode(division)​}​​"
 end
 
 def​ decode(<< 1::1, beats::15 >>) ​do
 "​​♩ = ​​#{​beats​}​​"
 end
 
 def​ decode(<< 0::1, fps::7, beats::8 >>) ​do
 "​​#{​-fps​}​​ fps, ​​#{​beats​}​​/frame"
 end
 end

This code is supposed to decode the data part of a MIDI header frame. This contains three 16-bit fields: the format, the number of tracks, and the time division. This last field comes in one of two formats:

images/midiheader.png

The parse_header/1 function splits the overall header into the three fields, and the decode/1 function works out which type of time division we have.

Let’s run it, using a sample header I extracted from a MIDI file.

 $ iex -S mix
 iex>​ header = << 0, 1, 0, 8, 0, 120 >>
 <<0, 1, 0, 8, 0, 120>>
 iex>​ Buggy.parse_header header
 format: 1
 tracks: 8
 **​ (FunctionClauseError) no function clause matching in Buggy.decode/1
 iex>

Oh no! That was totally unexpected. It looks like we’re not passing the correct value to decode. Let’s use the debugger to find out what’s going on.

Injecting Breakpoints Using IEx.pry

We can add a breakpoint to our source code using the pry function. For example, to stop our code just before we call decode we could write this:

 def​ parse_header(
  <<
  format::integer-16,
  tracks::integer-16,
  division::integer-16
  >>
 ) ​do
 
»require​ IEx; IEx.pry
  IO.puts ​"​​format: ​​#{​format​}​​"
  IO.puts ​"​​tracks: ​​#{​tracks​}​​"
  IO.puts ​"​​division: ​​#{​decode(division)​}​​"
 end

(We need the require because pry is a macro.)

Let’s try the code now:

 $ iex -S mix
 iex>​ Buggy.parse_header << 0, 1, 0, 8, 0, 120 >>
 Break reached: Buggy.parse_header/1 (lib/buggy.ex:11)
 
  9:
» ​10: require IEx; IEx.pry
  ​11: IO.puts "format: #{format}"
 
 pry> binding
 [division: 120, format: 1, tracks: 8]
 iex>​ continue()
 format: 1
 tracks: 8
 **​ (FunctionClauseError) no function clause matching in Buggy.decode/1

We reached the breakpoint, and IEx entered pry mode. It showed us the function we were in as well as the source lines surrounding the breakpoint.

At this point, IEx is running in the context of this function, so a call to binding shows the local variables. The value in the division function is 120, but that isn’t matching either of the parameters to decode.

Aha! decode is expecting a binary, not an integer. Let’s fix our code:

 def​ parse_header(
  <<
  format::integer-16,
  tracks::integer-16,
» division::bits-16
  >>
 ) ​do
 ...

The pry call is still in there, so let’s recompile and try again:

 iex>​ r Buggy
 {:reloaded, Buggy, [Buggy]}
 iex>​ Buggy.parse_header << 0, 1, 0, 8, 0, 120 >>
 Break reached: Buggy.parse_header/1 (lib/buggy.ex:12)
 
  10: ) do
  11:
  12: require IEx; IEx.pry
  13: IO.puts "format: #{format}"
  14: IO.puts "tracks: #{tracks}"
 
 pry> binding
 [division: <<0, 120>>, format: 1, tracks: 8]
 pry> continue
 format: 1
 tracks: 8
 division: 0 fps, 120/frame
 :ok

Now the division is a binary, and when we continue the code runs and outputs the header fields. Except…it’s parsing the time division as if it were the SMPTE version, and not the beats/quarter note version.

Setting Breakpoints with Break

The second way to create a breakpoint doesn’t involve any code changes. Instead, you can use the break! command inside IEx to add a breakpoint on any public function. Let’s remove the call to pry and run the code again. Inside IEx we’ll add a breakpoint on the decode function:

 iex>​ ​require​ IEx
 IEx
 iex>​ break! Buggy.decode/1
 1
 iex>​ breaks
 
  ID Module.function/arity Pending stops
 ---- ----------------------- ---------------
  1 Buggy.decode/1 1
 
 iex>​ Buggy.parse_header << 0, 1, 0, 8, 0, 120 >>
 format: 1
 tracks: 8
 Break reached: Buggy.decode/1 (lib/buggy.ex:21)
 
  19: end
  20:
  21: def decode(<< 0::1, fps::7, beats::8 >>) do
  22: "#{-fps} fps, #{beats}/frame"
  23: end
 
 pry> binding
 [division: <<0, 120>>, format: 1, tracks: 8]

We hit the breakpoint, and we are indeed matching the wrong version of the decode function when we pass it 0000000001111000. Ah, that’s because I’m discriminating based on the value of the top bit, and I got it the wrong way around: the SMPTE version should be

 def​ decode(<< 1::1, fps::7, beats::8 >>) ​do

and the beats version should be

 def​ decode(<< 0::1, beats::15 >>) ​do

There’s lots more functionality in the debugger. You can start by getting help for IEx.break/4.

Does This Seem a Little Artificial?

I have a confession to make. The only time I use the Elixir breakpoint facility is when I work on this section of the book. If I have to add code to the source to break in the middle of a function, then I can just raise an exception there instead to get the information I need. And the fact that I can only break at public functions from inside IEx means that I can’t get the kind of granularity I need to diagnose issues, because 90% of my functions are private.

However, I’m an old curmudgeon—my favorite editor is a card punch. Don’t let my lack of enthusiasm stop you from trying the debugger.