A test that passed last week fails today. How do you find what changed without guessing?
The build was green on Friday and red on Monday, and nobody remembers touching the counter. Reading a week of commits one by one feels thorough and mostly produces guesses. There is a shorter path: shrink the failure until one command repeats it, read the first error in its output, and let Git search the history for you.
The first habit is a minimal reproduction: shrink the failing flow until a single command fails every time you run it. When make all fails, try make sim alone; when that fails, you have one stage, one log, and one message instead of three. Never debug from the biggest failing command when a smaller one fails too.
The second habit is reading the first error, not the last. A log ends with the stop and begins with the cause: fix the first complaint, rerun, and watch how many later lines vanish with it. The troubleshooting table from the previous lesson is organized this way, one stage at a time.
The third habit is bisecting with Git, which means a binary search through history: Git checks out commits for you, runs your failing command on each, and halves the range until one commit remains. You supply the range and the test; the search does the walking. The rest of this lesson watches one run of that search.
The figure below shows the history this lesson searches: five commits, with the newest at the right failing. Bisect tests the middle commit first, then halves toward the side the verdict points at. Look at the badges above the boxes: the middle goes first, and the first bad commit is the one every later failure traces back to.
Keep the verdict rule beside you while you read the run below. Git runs your command on a checked-out commit and reads only its exit status: 0 means good and anything nonzero means bad. Two verdicts are enough to corner one bad commit among five, because each test throws away half the range.
The history below holds five commits, from the baseline to the failing head. Read the short messages: only one of them touches the counter, but the search does not know that yet.
# captured terminal output
$ git log --oneline
6ffa46c c5 readme
ed1c07c c4 docs again
ed7e36f c3 tidy counter
7af7bb3 c2 docs
d025be2 c1 baselineThe output above lists the range bisect will search: five commits ending at the failing head. Commit hashes in your own run will differ, and the date line was left out of this transcript.
Before searching, confirm the failure repeats with one command at the head:
# captured terminal output
$ make sim > /dev/null 2>&1; echo "head: make sim exit status $?"
head: make sim exit status 2The output above was measured on macOS. Exit status 2 from make sim is the same run-time failure signature as case 7 of the troubleshooting table: everything compiles and the testbench reports a mismatch. A repeatable one-command failure is the entry ticket for bisect; without it the search has nothing reliable to test.
# captured terminal output
$ git bisect start HEAD $(git rev-list --max-parents=0 HEAD) > /dev/null && git bisect run make sim 2>&1 | grep -v -E '^(iverilog|cd build|VCD|ERROR|FATAL|make:|mkdir)|Time:|finish called|^$' | grep -v 'Date:' | cut -c1-100; git bisect reset > /dev/null 2>&1
running 'make' 'sim'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[7af7bb3ee49578147cdc81d01e4edf10679a4612] c2 docs
running 'make' 'sim'
PASS: mini_controller
ed7e36fef72fa1b0dc79785f59f055debcec7f53 is the first bad commit
commit ed7e36fef72fa1b0dc79785f59f055debcec7f53
Author: You <you@example.com>
c3 tidy counter
rtl/mini_controller.sv | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commitThe output above was measured on macOS. Bisect checked out the middle commit, c3, and ran make sim there: that run failed, so the bad change sits in the older half. It then tested c2 and saw the PASS verdict, so the good side ends at c2 and c3 is reported as the first bad commit. Two tests cornered one commit out of five, which matches the arithmetic: each test halves the range, so five commits take about two or three tests (illustrative). The offending change replaced the increment 4'd1 with 4'd2: a counts-by-two change like the one First Simulation makes on purpose.
It halves toward the good end and tests c2 next. The failing c3 means the bad change sits in the range c1 to c3, and c2 is the middle of that range. A PASS at c2 leaves only c3 standing, which is exactly how this run ends after two tests.
Cause this lesson's bug yourself. Change the counter increment from 4'd1 to 4'd2, run make sim, and confirm the exit status 2 you saw above. The failure lines match the count-by-two run in First Simulation, where the same change breaks every counting edge. Restore the increment and rerun to green before you continue: bisect needs a clean tree, and this edit has served its purpose.
Bisecting with a test that is flaky. A test that sometimes passes on broken code sends the search down the wrong half, and the verdict names an innocent commit. Run your one command twice before trusting it, and only bisect a failure that repeats every time.
A history where one commit changes many things. Bisect points at the commit, not the line: a commit that tidies the counter, renames signals and edits the docs in one step still needs reading. Keep commits small so the first bad commit is also a small story.
Continuing to edit while bisecting. Uncommitted edits travel with every checkout the search makes, so every verdict tests your edits plus the commit. Commit or stash your work first, and leave the tree alone until the search reports.
Rule: repeat it with one command, shrink it, then ask history what changed.
It tests the middle first, then halves the range with every verdict: a bad verdict keeps the older half, a good verdict keeps the newer half. At most three tests for six commits (illustrative): the first test rules out three commits, the second rules out most of the rest, and the third isolates the one.
make all fails. Running the steps alone, lint passes and make sim fails. What is your reproduction, and why not debug from make all?The reproduction is make sim: one command that fails every time. make all mixes lint, simulation and synthesis into one log, so its output hides which stage complains. Shrink to the smallest failing step, then read that step's first error.
At the first error: a file is missing from the compile line, which is case 2 of the troubleshooting table. The last lines only report that the build stopped; they vanish once the missing file is added. Fix causes in order, never stops.
Shrink a red build to one command that fails every time before changing anything.
Read the first error: later lines report the stop, only the first reports the cause.
Bisect halves history with every test, so the first bad commit takes a handful of runs, not a week of reading.