Skip to content

brine debug makes a debug build and starts gdb (or lldb) on it. On x86-64 Windows and Linux and on AArch64 Linux, debug builds are native code with DWARF debug info: you break at file.br:line or at a Brine function such as Point.norm, step through Brine lines, and see every local by its Brine name and type. With gdb, values print the way Brine prints them ("origin", [1, 2, 3], Shape.Rect(w=2, h=3)).

shapes.br
struct Point:
x: float
y: float
fn norm(self) -> float:
return sqrt(self.x * self.x + self.y * self.y)
fn main():
let name = "origin"
let xs = [1, 2, 3]
let p = Point(x=3.0, y=4.0)
let n = p.norm()
print(name, xs, n)
Terminal window
brine debug shapes.br

A session on Windows with gdb 17.2, with the commands typed after each (gdb) prompt (gdb’s [New Thread ...] lines are left out):

Built shapes [debug] in 0.10s
Reading symbols from C:\Users\you\AppData\Local\Temp\brine-cache\d708f6e0b6b1cd6e\debug\shapes.exe...
(gdb) break shapes.br:13
Breakpoint 1 at 0x1400019e4: file shapes.br, line 13.
(gdb) run
Starting program: C:\Users\you\AppData\Local\Temp\brine-cache\d708f6e0b6b1cd6e\debug\shapes.exe
Thread 1 hit Breakpoint 1, main () at shapes.br:13
13 print(name, xs, n)
(gdb) info locals
n = 5
p = {x = 3, y = 4}
xs = [1, 2, 3]
name = "origin"

Methods are named Type.method, so you can break on them directly:

(gdb) break Point.norm
Breakpoint 2 at 0x14000176f: file shapes.br, line 6.
(gdb) run
Thread 1 hit Breakpoint 2, Point.norm (self=...) at shapes.br:6
6 return sqrt(self.x * self.x + self.y * self.y)
(gdb) bt
#0 Point.norm (self=...) at shapes.br:6
#1 0x00007ff70c6419d4 in main () at shapes.br:12
#2 0x00007ff70c6418f5 in main ()
#3 0x00007ff70c6413ba in __tmainCRTStartup ()
#4 0x00007ff70c641080 in mainCRTStartup ()
(gdb) info args
self = {x = 3, y = 4}
(gdb) finish
0x00007ff70c6419d4 in main () at shapes.br:12
12 let n = p.norm()
Value returned is $1 = 5

Frame #1 main is your fn main; the second main below it is the runtime’s C entry point, which starts the thread pool and calls yours.

Terminal window
brine debug # the current project
brine debug shapes.br # a single file
brine debug shapes.br -- a b c # pass arguments to the program

brine debug builds with the debug profile (the same build as brine build --debug), then starts:

Debugger found Command Brine runs
gdb with Python gdb -q -x <dir>/brine-gdb.py --args <exe> [args]: loads the pretty-printers, which also skip the C runtime when stepping
gdb without Python gdb -q -ex "skip -gfi */brine_rt.c" -ex "skip -gfi */brine_rt.h" --args <exe> [args]: steps skip the runtime, values print as their C layout
lldb (no gdb) lldb -O "settings set target.process.thread.step-avoid-regexp ^(brine_|br_)" -- <exe> [args]

It looks for gdb first, then lldb, on your PATH. Without either it stops after the build and says where the executable is. brine doctor shows which debugger it found.

Terminal window
brine build --debug
Built hello v0.1.0 [debug] in 0.97s
Output target/debug/hello.exe
Platform Debug build
x86-64 Windows (COFF), x86-64 Linux (ELF) native code from the x86-64 backend, with DWARF
AArch64 Linux (ELF) native code from the AArch64 backend, with DWARF
macOS, other platforms C at -O0 -g with #line directives (below)

A native debug build carries:

  • Lines: every Brine line has an address, so break file.br:line, step, next and finish work in Brine terms.
  • Functions under their Brine names: main, area, Point.norm, Shape.area. Functions from imported modules keep their plain names (area from geometry.br is area, at geometry.br:2). Each instance of a generic function has the generic’s name (largest).
  • Variables: every local and parameter by its Brine name and type, in the scope where it’s declared.
  • Unwind info, so bt and profilers walk through Brine frames.

Debug code skips the optimizations and keeps each value in its stack slot (no register allocation, no cached values), so what print shows is always the variable’s current value. Checks still run: a debug build panics exactly where a dev or release build would.

To stop gdb lldb
at a line break shapes.br:13 b shapes.br:13
in a function break main, break area b main
in a method break Point.norm b Point.norm
in every function matching a pattern rbreak ^Point\. breakpoint set -r '^Point\.'
on any panic break brine_panic_at b brine_panic_at

Every panic (overflow, a bad index, a failed assert, panic(...), unwrap() on None) goes through the runtime function brine_panic_at, which receives the Brine location and the message:

(gdb) break brine_panic_at
(gdb) run
Thread 1 hit Breakpoint 1, brine_panic_at (loc=0x7ff6a6ee306c <br_ccd1c7eaaac8cf230> "panic.br:4:11", msg=0x5ff920 "index 5 is out of bounds for an array of length 3") at C:\Users\you\AppData\Local\Temp\brine-cache\rt-2fdf516b1c19bd8c\brine_rt.c:142
142 br_lock(&br_io_mu);
(gdb) frame 3
#3 0x00007ff6a6ec1902 in main () at panic.br:4
4 print(xs[i + 5])

runtime/brine_gdb.py teaches gdb to print Brine values the way print does. brine debug writes it next to the debug build as brine-gdb.py and loads it when gdb has Python support.

Type Without the printers With them
str 0x7ff70c661010 <br_sb9eae4fe785a14cf> "origin"
[int] 0x80000040 [1, 2, 3]
{str: int} a pointer {"ann": 31, "bob": 27}
tuple a C struct (1, "two")
enum a C struct Shape.Rect(w=2, h=3)
Option[int] a C struct Some(1)
(gdb) info locals
pair = (1, "two")
ages = {"ann": 31, "bob": 27}
found = Some(1)
s = Shape.Rect(w=2, h=3)

Structs keep gdb’s own layout ({x = 3, y = 4}). At most 200 elements or bytes of a value are shown.

To use the printers with gdb started by hand (or from an IDE), load the file from the Brine source tree, or the copy brine debug wrote:

Terminal window
gdb -x path/to/brine_gdb.py --args target/debug/hello

or, inside gdb, source path/to/brine_gdb.py. They also make step skip the runtime’s C functions.

lldb reads the same DWARF, so lines, functions and variables work as in gdb:

Terminal window
lldb target/debug/hello
(lldb) b shapes.br:13
(lldb) run
(lldb) frame variable
(lldb) b Point.norm
(lldb) thread step-over

There are no lldb formatters yet: strings, arrays, maps and enums show as their C layout (pointers and structs). brine debug sets step-avoid-regexp so stepping doesn’t enter runtime functions (brine_*, br_*).

Where there is no native backend (macOS, and any other platform that builds through C), and whenever BRINE_BACKEND=c is set, a debug build is C compiled at -O0 -g, with #line directives that point back at the Brine source. Breakpoints by file and line, stepping and backtraces still work, but:

  • variables show under their C names: the Brine name and a number, like name_0, xs_1, p_3, n_4, next to the compiler’s temporaries (v0, v1, …);
  • values don’t always match the Brine variable at a given line, because the C code keeps them in temporaries;
  • the pretty-printers don’t apply to those C variables.
Terminal window
BRINE_BACKEND=c brine build --debug shapes.br -o shapes_c

Prefer a native debug build wherever there is one.

BRINE_LEAKCHECK=1 makes a program print how many strings and arrays are still allocated when it exits. Memory is reference counted and freed at scope exit, so the answer should always be 0:

Terminal window
BRINE_LEAKCHECK=1 target/dev/hello
Hello, brine!
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
brine: 0 live objects at exit

Brine’s own test suite requires 0. A non-zero count is a bug in Brine, not in your program; please report it with the program.