The Same Precedence Mistake, Four Times, Two Months Apart
I made the same operator-precedence mistake four separate times over six weeks. This isn't a story about learning a rule — it's about admitting the rule alone was never going to be enough.
I made basically the same operator-precedence mistake four times between early June and mid-July, across four different exercises. By the second time it happened I could already recite the actual rule back correctly, out loud, no hesitation. That's the part I keep chewing on more than any one of the four bugs individually — knowing the rule apparently isn't the same thing as applying it while I'm actually mid-line, writing code.
The pattern
The actual rule, as far as I've pieced it together: ==, !=, and the shift operators << and >> bind tighter than the bitwise operators &, |, and ^. Backwards from how I instinctively want to read it — I kept assuming all of these sit at roughly the same weight, like + and -, and you just go left to right. They don't. a & b == c isn't "AND them, then compare," even though that's how it sounds out loud. It's a & (b == c), because == grabs b and c first. Same story with shifts: x & y << 2 binds as x & (y << 2), not (x & y) << 2.
None of this is obscure. It's sitting right there in the precedence table, and I could say the rule back correctly basically any time someone asked me to. Saying it back and actually applying it mid-expression turned out to be two different skills, apparently, because I kept getting it wrong anyway.
Where it kept showing up
June 5 — the first one. I wrote a & b == c, expecting it to mean (a & b) == c. It actually parses as a & (b == c). This is the one that taught me the rule exists in the first place — everything after this was supposed to just be me applying it, not learning it over again.
June 10 — five days later, a practice round. I was pulling a 3-bit field out of a register, trying to write "mask it out, then shift the result down to bit 0," and wrote something shaped like reg & (7U << 4) >> 4. I'd parenthesized (7U << 4) on purpose, so I thought I'd covered myself. What I hadn't parenthesized was the reg & (...) part as a whole, so the trailing >> 4 grabbed the mask literal instead of the result of the AND — which, best I can tell, quietly turned my shift back into the plain number 7 and left reg ANDed against entirely the wrong three bits. Being careful about one set of parentheses and not the ones around it ended up worse than not bothering with any of it, because it looked deliberate.
July 5 — almost a month later, a chapter test. Checking whether x = 12 is a power of two, I wrote if (x != 0 && (x & (x - 1) == 0)). I knew the trick was supposed to be (x & (x - 1)) == 0 — something about a power of two only having one bit set, so ANDing it against one less than itself clears that bit and leaves zero — but I wrote the AND and the == right next to each other and let precedence decide the grouping, which by this point is exactly the failure mode I should've been watching for. It took two tries before I actually wrote it right, which felt like its own warning sign — a slip is one thing, needing two tries to fix the same rule is starting to look like a habit.
July 9 — four days after that, a different chapter test. head + 1 % 8, meant to advance a circular index. A different precedence pair this time — % over +, not a bitwise case — but the same underlying thing: writing the expression the way it sounds instead of the way it groups. head + 1 % 8 parses as head + (1 % 8), which is just head + 1 with extra steps — the modulo never actually touches head at all. I logged it as another instance of the pattern, then almost talked myself out of it: 1 % 8 is 1 no matter how you group it, since 1 is already smaller than 8, so surely this exact shape can't produce a wrong answer regardless of precedence. That argument sounded clean enough that I nearly struck the entry from my own mistake log. Good thing I checked it against an actual index instead of trusting my own reasoning: at head = 7, the last valid slot in an 8-wide buffer, head + 1 % 8 comes out to 8 — one past the end of the buffer — while the (head + 1) % 8 I actually meant comes out to 0, the correct wraparound. The bug was real. What almost got struck from the log wasn't a false alarm; it was a second precedence mistake, stacked right on top of the first one, this time in my own defense of it.
The fix
What I'm trying to do now is parenthesize the whole bitwise sub-expression, every time, whether or not I feel confident about precedence in that particular moment:
(x & (x - 1)) == 0;
(reg & mask) >> shift_amount;
(head + 1) % 8;Not "parenthesize it when it looks ambiguous" — that's exactly the judgment call I kept getting wrong, since none of these four actually looked ambiguous to me at the time I wrote them. Just parenthesize it always, as a reflex, regardless of how sure I feel. All four incidents above would have been caught by an extra pair of parentheses that costs nothing and doesn't even really help anyone who already knows the rule — they're just insurance against whatever version of me is tired, or rushing, or mid-test.
Four times on the same rule tells me this was probably never really a knowledge gap — I could already state the precedence table correctly right after the first one. It's closer to a reading habit, built up over years of reading + and - and every other roughly-equal-weight operator left to right without a second thought, and I don't think a habit like that gets erased just because I understood the rule once. Or four times.
I want to say the parenthesize-everything thing is the fix and I'm done with this mistake now, but I've told myself some version of that three times already before writing this post, so I'm not pretending this time is obviously different just because I said it with more conviction. Maybe it is different. I genuinely don't know yet — I think I only find out the next time I'm tired or rushing during a timed test and see whether my hands still default to treating & and == like they weigh the same thing. What I do believe is that reciting the rule was never enough on its own — I could recite it fine after June 5th, and that didn't stop July 5th or July 9th. It also didn't stop me from nearly talking myself out of July 9th being real — the same left-to-right instinct that causes the bug is perfectly capable of producing a plausible-sounding argument for why it isn't one. Whatever fixes this has to be dumber and more automatic than "remember the rule." Closer to a reflex I don't have to think about, if I can get there.
Notes from readers
Comments — via GitHub