rust-debugger
rdbg debugs from the command line: one paused process per project; breakpoints
and state carry across calls. Needs a debug build (the default cargo build).
Run rdbg with no arguments for the full command list; REFERENCE.md beside
this file adds examples. Install:
curl -fsSL https://azimi.me/rust-debugger-skill/install.sh | sh (also sets up
codelldb), plus rust-analyzer on PATH.
When to reach for it (and when not)
Read first. Every launch rebuilds the target, so a wasted debugging detour is
expensive. Launch only for a runtime question at a place you can name:
- a wrong value where you need the real inputs/flow that produced it;
- an unexpected branch, type, or state that reading can't pin down;
- a panic —
rdbg debug --paniclands on the culprit frame in one call; - testing a fix live with
set --then continuebefore editing + rebuilding.
Don't launch when a quick read already points at the fix; when the output is missing (nothing fires to break on — finding the absent check is a reading task); or to iterate on a candidate fix — re-run the narrowed test instead.
Stay cheap: one session with several breakpoints, or one trace, beats
re-launching. If 2–3 probes haven't localized it, go back to reading.
Fix once, don't churn: after ~2–3 edit→test cycles you're guessing — break at
the failing assertion and compare actual vs expected values, or set the
suspect value and continue to validate the hypothesis without editing.
Tap, don't walk — hard rules. The debugger aims your reading; it rarely hands
you the fix. Break at the sink, read which path fired (and which sinks show
0 hits), bt to the deciding frame, then read that code. Every losing
benchmark run broke one of these while sure it was "making progress":
btnamed afile:line→ STOP; don't launch again. Read it; the fix is in that frame or its caller — don't break into a callee the backtrace names to "confirm" a return value.- Budget 2 launches (a
--cargotracecounts — it rebuilds tsz). Before a 3rd, name the runtime fact you still lack and can't get by reading; if you can't, read. 0 hits/NOT BOUND→ the emit is elsewhere: READ the upstream gate. A named wrong diagnostic (aTSxxxxcode) is agreptask — find the emit site by reading, then launch once; don't relaunch guessing sinks.evalcan't run Rust methods — a type's display name,.len(), any method value fails. Break inside the formatter/interner lookup and read its inputs, or read the code. A failedevalon an opaqueArc/TypeIdis not a "flaky debugger." Never adddbg!/eprintln!(forbidden here, and it rebuilds tsz); userdbg varsfor all locals instead of re---capture.- State only what the output shows — cite the command+output or say "unknown."
A raw test
FAILED/exited 101with no>>> STOP/hit-count ran to completion without pausing; it does not mean the line didn't run (rdbg breaksshows whether a breakpoint bound). Degraded tooling (DWARF error, breakpoints won't bind) → stop and read.
tsz: wrong or extra diagnostics (the main use here)
tsz diagnostics funnel through a few sinks: CheckerState::error,
CheckerContext::push_diagnostic (context/diagnostic_push.rs), and
emit_render_request. For a wrong or extra (false-positive)
diagnostic, break the sinks and trace the emit backward to the deciding
code instead of grepping emit sites:
rdbg launch --cargo crates/tsz-checker --test <stem> --break-fn error --break-fn push_diagnostic -- <test_fn>
rdbg eval code # `code` at an error stop; `diag.code` at push_diagnostic
rdbg continue --until 'code == <code>' # ONLY if the current stop isn't already the emit
rdbg bt # walk back to the deciding frame
rdbg frame <n> ; rdbg vars # the types/flags that produced it
<stem> is crates/tsz-checker/tests/<stem>.rs; words after -- filter to
the failing #[test] name. Check the current stop's code before
continue --until — it resumes first and tests only later stops.
- Unhit sinks are reported at exit (
bound, 0 hits= wrong path, try the next sink;NOT BOUND= bad name). Qualified names (Type::method) don't bind — use the base name, or--break <file>:<line>inside the function. - A missing diagnostic means an upstream gate wrongly accepted — the sink
shows
0 hits, and that0 hitsis usually the whole clue. Read that gate to see why it passed; this is normally solved by reading, not stepping. Break there only to confirm one specific value. - The same pattern works a layer down: break a
tsz-solverrelation or evaluation function to see the actualTypeIds and flags at the decision. - vs
tsz-tracing:TSZ_LOGtraces show the breadth of what solver/checker did; rdbg answers a pointed question with concrete values and lets you mutate state to test a hypothesis. rdbg downwhen finished;.rdbg/is gitignored; use rdbg instead of temporarydbg!/println!(forbidden here anyway).
Targets and sessions
rdbg launch --cargo <dir> --bin app --break src/x.rs:88 -- --threads 4
rdbg launch --cargo <dir> --lib --break src/lib.rs:42 -- my_test # #[test] in src/
rdbg launch --cargo <dir> --test t --break tests/t.rs:12 -- case # tests/t.rs
rdbg trace --cargo <dir> --lib --break src/lib.rs:42 --capture a,b -- my_test
--lib for a #[test] inside the library (the common case); --test <name>
only for an integration test file tests/<name>.rs. Add --panic to also stop
where a panic is raised, or --break-fn <name> for a function breakpoint.
trace runs through every hit and returns a table in one call — use it to
watch a value evolve without stepping.
Command crib
rdbg break src/x.rs:42 [--if "i == 5" | --hit 3 | --log "i={i}"]
rdbg break --fn crate::f | --panic ; rdbg watch var ; rdbg breaks
rdbg continue [--until 'sum >= 100'] # condition re-checked at each stop
rdbg step over|in|out ; rdbg until src/x.rs:99 ; rdbg restart
rdbg vars ; rdbg eval a.b c ; rdbg set a.b = 8 [--then continue]
rdbg bt ; rdbg frame <n>|up|down ; rdbg list ; rdbg state
rdbg where <Name> ; rdbg def|hover|refs <file> <line> <col>
rdbg stop ; rdbg down # end session; stop the daemon
Notes
eval/set/conditions take variable paths and simple comparisons; codelldb adds comparison/arithmetic/field eval. Rust method calls can't run — break inside instead.- Panicking test, one call:
rdbg debug --cargo <dir> --lib --panic -- <test>returns the message, first user frame with args and locals, and a backtrace. - Debug the debug build;
--releasehas little to inspect.rdbg down(or 30 minutes idle) releases the paused process.