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)).
A session
Section titled “A session”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)brine debug shapes.brA 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.10sReading symbols from C:\Users\you\AppData\Local\Temp\brine-cache\d708f6e0b6b1cd6e\debug\shapes.exe...(gdb) break shapes.br:13Breakpoint 1 at 0x1400019e4: file shapes.br, line 13.(gdb) runStarting program: C:\Users\you\AppData\Local\Temp\brine-cache\d708f6e0b6b1cd6e\debug\shapes.exe
Thread 1 hit Breakpoint 1, main () at shapes.br:1313 print(name, xs, n)(gdb) info localsn = 5p = {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.normBreakpoint 2 at 0x14000176f: file shapes.br, line 6.(gdb) runThread 1 hit Breakpoint 2, Point.norm (self=...) at shapes.br:66 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 argsself = {x = 3, y = 4}(gdb) finish0x00007ff70c6419d4 in main () at shapes.br:1212 let n = p.norm()Value returned is $1 = 5Frame #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.
brine debug
Section titled “brine debug”brine debug # the current projectbrine debug shapes.br # a single filebrine debug shapes.br -- a b c # pass arguments to the programbrine 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.
Debug builds
Section titled “Debug builds”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,nextandfinishwork in Brine terms. - Functions under their Brine names:
main,area,Point.norm,Shape.area. Functions from imported modules keep their plain names (areafromgeometry.brisarea, atgeometry.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
btand 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.
Breakpoints
Section titled “Breakpoints”| 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) runThread 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:142142 br_lock(&br_io_mu);(gdb) frame 3#3 0x00007ff6a6ec1902 in main () at panic.br:44 print(xs[i + 5])The gdb pretty-printers
Section titled “The gdb pretty-printers”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 localspair = (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:
gdb -x path/to/brine_gdb.py --args target/debug/helloor, 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:
lldb target/debug/hello(lldb) b shapes.br:13(lldb) run(lldb) frame variable(lldb) b Point.norm(lldb) thread step-overThere 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_*).
Through C: #line directives
Section titled “Through C: #line directives”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.
BRINE_BACKEND=c brine build --debug shapes.br -o shapes_cPrefer a native debug build wherever there is one.
Leak checking
Section titled “Leak checking”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:
BRINE_LEAKCHECK=1 target/dev/helloHello, brine![0, 1, 4, 9, 16, 25, 36, 49, 64, 81]brine: 0 live objects at exitBrine’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.