Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Debugging

Build with -g to debug a Pepper program in gdb, lldb, Visual Studio or WinDbg. The compiler then describes the program to the debugger, which can:

  • stop at a file:line breakpoint;
  • step through the source one statement at a time;
  • show a backtrace with the source line of every Pepper frame;
  • print parameters, locals, module-scope variables and struct fields.
pepper build -g main.pep -o main        # Linux: DWARF in the executable
pepper build -g main.pep -o main        # macOS: DWARF, and main.dSYM beside it
pepper build -g main.pep -o main.exe    # Windows: CodeView, and main.pdb beside it

On macOS the linker leaves the DWARF in the object files, so -g also runs dsymutil (from Xcode’s command line tools) to gather it into main.dSYM. lldb finds the bundle beside the program; so do the program’s own backtraces, which read their source lines from it. Without dsymutil the program still builds, and its backtraces name functions but not lines.

-g and -O work together. Optimized code still steps and shows a backtrace, and a call the optimizer inlined still appears as its own frame. A variable the optimizer removed shows as optimized out.

A session

This program builds an array of points and adds up the first one:

# Build with `-g` to debug this program: break on line 18 and print `first`.
struct Point {
    var x: int = 0;
    var y: int = 0;
}

total(p: Point): int {
    return p.x + p.y;
}

main(): int {
    var points = Array<Point>();
    var p = Point();
    p.x = 1;
    p.y = 2;
    points.append(p);
    var first = points[0];
    return total(first) - 3;
}

Stop on line 18, after first is assigned, and look around. In gdb:

$ pepper build -g debugging.pep -o debugging
$ gdb ./debugging
(gdb) break debugging.pep:18
(gdb) run
Breakpoint 1, main () at debugging.pep:18
18	    return total(first) - 3;
(gdb) info locals
points = 0x5555555592f0
p = 0x5555555592c0
first = 0x5555555592c0
(gdb) print *first
$1 = {__refcount = 3, __weakcount = 1, x = 1, y = 2}
(gdb) step
total (p=0x5555555592c0) at debugging.pep:8
8	    return p.x + p.y;
(gdb) backtrace
#0  total (p=0x5555555592c0) at debugging.pep:8
#1  main () at debugging.pep:18

lldb takes the same steps with b debugging.pep:18, r, frame variable and bt. In Visual Studio, open the executable as a project (File > Open > Project/Solution), set a breakpoint in the source file and start debugging. The .pdb beside the executable is found automatically.

What the debugger sees

  • Functions under the names you wrote: total, Point.length, Array<int>.append. Methods the compiler writes for a struct, such as __acquire and __release, are marked artificial and placed on the struct’s line. A closure is named for where it is written: closure at main.pep:14.

  • Structs. A variable of a struct type holds a reference, so the debugger shows an address. Dereference it (print *first) to see the fields. The fields include the __refcount and __weakcount head, which helps when chasing an ownership bug.

  • Numbers under their Pepper names (int, ulong, u8).

  • Enums. A plain enum shows its case by name. An enum with payloads shows as a struct holding its tag, __tag, whose value names the case (Rect), and the payload fields of every case, each named Case__field (Rect__width), which a debugger expression can name: print shape.Rect__width. With Pepper’s formatters loaded (see below) it shows only its case and that case’s values instead. A value enum ([Value], such as Optional and Result) is that struct itself, held inline, with no reference counts.

  • Tuples as a struct with members 0, 1, and so on.

  • Trait values as the object they refer to and its vtable.

  • Generics. Each instantiation is its own type, such as Array<long>.

  • Module-scope variables under their names.

  • String, Array and Optional read naturally once the debugger loads Pepper’s formatters (see below): a String as its text, an Array as its count and elements, an Optional as its case, Empty or Value, and its value. A String and an Array also show their reference counts as [ref count] (strong references) and [weak count].

  • Other enums with payloads read the same way with the formatters: as their case and that case’s values, such as Rect with width and height.

Stepping through a static block

A static block unrolls its compile-time loops into one pass per element, and the debugger steps through those passes as it would a loop that runs: it stops on the for (or while) line at the start of each pass, then on each line of the body. A compile-time if stops once on its line, then in the branch it kept. The compile-time variables (the loop’s field, a var of the block) are locals of each pass, so the debugger shows which pass it is in:

# Build with `-g` and step through `total`: each pass of the `static`
# block's loop stops on the `for` line, then on its body, with `field`
# holding the pass's member.
struct Point {
    var x: int = 1;
    var y: int = 2;
    var z: int = 3;
}

total(p: Point): int {
    var sum = 0;
    static (sum, p) {
        for (var field in reflect.properties<Point>()) {
            sum = sum + field.get(p);
        }
    }
    return sum;
}

main(): int {
    return total(Point()) - 6;
}
$ pepper build -g debugging-static.pep -o debugging-static
$ gdb ./debugging-static
(gdb) break total
(gdb) run
(gdb) next
11	    var sum = 0;
(gdb) next
13	        for (var field in reflect.properties<Point>()) {
(gdb) next
14	            sum = sum + field.get(p);
(gdb) print field
$1 = {name = 0x55555559073f "x", type_name = 0x555555590a57 "int", index = 0, is_last = false, is_const = false, is_property = false}
(gdb) next
13	        for (var field in reflect.properties<Point>()) {
(gdb) next
14	            sum = sum + field.get(p);
(gdb) print field.name
$2 = 0x5555555909ba "y"

To step through the code each pass emits instead, with the compile-time values filled in, build with --debug-static=expanded. Beside the program it writes debugging-static.static.pep, the source with the block expanded as --emit-static writes it, and the debugger shows that file for the block’s code:

$ pepper build -g --debug-static=expanded debugging-static.pep -o debugging-static
$ gdb ./debugging-static
(gdb) break debugging-static.static.pep:18
(gdb) run
Breakpoint 1, total (p=0x55555559c6b0) at debugging-static.static.pep:18
18	            sum = sum + p.y;
(gdb) print field.name
$1 = 0x5555555909ba "y"

The rest of the function, and the program, stays in its own files.

A breakpoint on a line of the body stops in every pass. A member or case shows as a structure of what reflect reads from it (name, index, is_last, and so on); a list does not show. The stop on the header is a nop the compiler adds only to unoptimized -g builds; optimized code steps through the body’s lines without it.

Formatters for the runtime’s types

The runtime ships formatters for each debugger in runtime/debuggers/. The sessions below stop in a program holding var name = "pepper";, an Array<int> values of 3 and 4, and var found = Optional<int>.Value(7);.

  • gdb. On Linux a -g program carries gdb_printers.py in its .debug_gdb_scripts section. gdb loads it from directories on its auto-load safe path, so add the program’s directory to it, on the command line or in ~/.gdbinit:

    gdb -iex "add-auto-load-safe-path ." ./debugging
    
    (gdb) print name
    $1 = "pepper" = {[ref count] = 1, [weak count] = 1}
    (gdb) print values
    $2 = Array<int> of count 2 = {[ref count] = 1, [weak count] = 1, [0] = 3, [1] = 4}
    (gdb) print found
    $3 = Value = {value = 7}
    
  • lldb. Import lldb_formatters.py, once per session or in ~/.lldbinit:

    (lldb) command script import runtime/debuggers/lldb_formatters.py
    (lldb) frame variable name *values found
    (String *) name = 0x00005555555592c0 "pepper"
    (Array<int>) *values = count=2 {
      [ref count] = 1
      [weak count] = 1
      [0] = 3
      [1] = 4
    }
    (Optional<int>) found = Value {
      value = 7
    }
    
  • Visual Studio and WinDbg. A -g program’s .pdb carries pepper.natvis, and an entry for each of the program’s enums with payloads, so nothing needs loading.

Building with -g

Debug info changes the code each file compiles to, so -g builds keep their own objects in the build cache, apart from builds without it. With -g, editing a file recompiles the files that import it, even when the edit only moves a declaration: their code records the line numbers of the generics they instantiate from it.

Backtraces without a debugger

A program can print its own stack: a panic does in a build without -O, and the debug module’s capture() and print_backtrace() do on request. debug.output, debug.is_debugger_attached and debug.breakpoint talk to an attached debugger. See Backtraces and the debugger.