Skip to content

Debugging tips

Although strictly-typed functional languages like OCaml greatly reduce the number of bugs in your programs, you will still make mistakes and have to debug them. Debugging functional languages can be challenging; here are some tips we've found to be useful.

printf-style debugging

printf-style debugging is highly effective if you do it right. The wrong way is to add printf statements to otherwise functional code:

let x = ... in
Printf.printf "x = %s\n" x;
let y = ... in
  ...

Even though this can work, you are embedding imperative code inside functional code, and you can get into tricky syntax issues as a result. A better way to do it is like this:

let x = ... in
let _ = Printf.printf "x = %s\n" x in
let y = ... in
  ...

This also embeds imperative code, but uses functional syntax, so there won't be any syntax issues.

Note

If the surrounding code is imperative (like a begin expression), the exact opposite criterion applies: favor the naked Printf.printf ... instead of the let _ = Printf.printf ... idiom.

Also, you should add %! at the end of the printf format string:

let x = ... in
let _ = Printf.printf "x = %s\n%!" x in
let y = ... in
  ...

This makes OCaml flush the string as soon as the printf executes. If you don't do that, sometimes error messages before a crash can be lost.

Finally, for debugging, consider writing your printfs unindented this way:

    let x = ... in
let _ = Printf.printf "DEBUG: x = %s\n%!" x in
    let y = ... in
      ...

This way, it's easy to spot these lines, which is good because you are going to want to delete them once debugging is done.

Using the REPL

"REPL" means "Read-Eval-Print-Loop" and refers to the interactive interpreter (what you get into if you type ocaml or utop at a terminal prompt). You might think that it's hard to use a REPL in a large, multi-file project like the compiler assignments, but we've got you covered!

Typing make repl in any directory will bring up a REPL in which all the modules of the program are accessible. This allows you to interactively test your code. Some kinds of code are easier to test in this way than others, but don't underestimate how useful this can be, especially when trying to track down a tricky bug. All you need to do is to open the relevant module:

# open Some_Compiler_Pass_That_Isnt_Working;;

and you can play with all the functions in that module.

Insert debugging code into tricky algorithms

Some passes involve tricky algorithms. In those cases, it's a good idea to define a debug variable at the top of the file:

let debug = ref false  (* change to `true` when you need debug output *)

and then put debug code into the tricky algorithm that prints out the inputs and outputs:

let tricky_algorithm weird_inputs =
  if !debug then
    Printf.printf "INPUT: %s\n" (string_of_weird_inputs weird_inputs);
  ... (* rest of algorithm *)

To do this, you need to be able to convert the algorithm inputs/output to strings. Most of the data structures you will be using can be easily converted to S-expressions, and there is a print_sexp function in the Utils module that will print an S-expression in a readable fashion (this is discussed in the next section). This is extremely useful for tracking down algorithm errors.