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:linebreakpoint; - 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__acquireand__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__refcountand__weakcounthead, 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 namedCase__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 asOptionalandResult) 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,ArrayandOptionalread naturally once the debugger loads Pepper’s formatters (see below): aStringas its text, anArrayas its count and elements, anOptionalas its case,EmptyorValue, and its value. AStringand anArrayalso 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
Rectwithwidthandheight.
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
-gprogram carriesgdb_printers.pyin its.debug_gdb_scriptssection. 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
-gprogram’s.pdbcarriespepper.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.