skip to content

Why can a backwards array scan using an unsigned index never terminate when its loop guard is `i >= 0`?

level: seniorimportance: nice to knowfreq 30%

answer

  1. what values can the index type even hold?
  2. a guard that is always true tests nothing
  3. compute 0 - 1 in modular arithmetic
  4. all-ones pattern: unsigned max, signed -1
  5. empty array wraps before the loop starts

basics

~20 s

An unsigned integer cannot hold a negative value, so the guard i >= 0 is always true. When i reaches 0, decrementing wraps it to the maximum unsigned value — the bit pattern of -1 — and the scan runs past the array forever.

solid answer

~50 s

The guard is a tautology: an unsigned type's range starts at zero, so `i >= 0` can never be false and the loop's exit condition tests nothing. The scan processes indices n-1 down to 0 correctly, but the final decrement computes 0 - 1 in modular unsigned arithmetic, yielding 2^32 - 1 — the same all-ones bit pattern that means -1 in two's complement, which is exactly why -1 "becomes" the largest value in the room. The next iteration indexes far out of bounds. The same representational fact poisons mixed comparisons: where a signed operand is implicitly converted to unsigned, `-1 < length` becomes `4294967295 < length`, silently false. Fixes: use a signed index wide enough for the range, or restructure the guard as `while i > 0` indexing `a[i - 1]`, or count remaining elements downward — and check the empty-array case, where `length - 1` wraps immediately.

code

pseudocode · 4 lines
pseudocode
i = length(samples) - 1        // i is a 32-bit unsigned integer
while i >= 0:                  // exit condition: can it ever be false?
    checksum = checksum XOR samples[i]
    i = i - 1                  // what happens when i is 0?

go deeper

for a junior

Be ready to state the core fact: an unsigned type starts at zero, so i >= 0 is always true, and decrementing 0 wraps to the maximum value instead of reaching -1.

for a middle

An interviewer expects the mechanics: modular unsigned arithmetic, why the all-ones pattern is both unsigned max and signed -1, and at least one correct restructuring of the countdown loop.

for a senior

Demonstrate lived diagnosis: recognize the near-2^n index in a crash as the fingerprint, name the empty-container wrap as the second bug in the same line, and rank fixes by which eliminate the hazard rather than patch one comparison.

for a principal

Own the policy angle: decide whether the codebase standardizes on signed sizes and indexes or on homogeneous unsigned arithmetic, and back the rule with lint gates at API boundaries — the bug class is cheaper to forbid than to keep re-finding.

## The tautological guard An unsigned n-bit integer represents 0 .. 2^n - 1 and nothing else. A guard of the form `i >= 0` over such a type compares against the bottom of the representable range, so it is true for every value the variable can hold — the compiler could replace it with `true` without changing behavior (and some toolchains warn precisely because of that). The loop's only exit path is gone before the first iteration runs. ## Where the values actually go ``` i = length(samples) - 1 // i is a 32-bit unsigned integer while i >= 0: checksum = checksum XOR samples[i] i = i - 1 ``` The scan is perfectly correct from n-1 down to 0. The failure is the *final* decrement: unsigned arithmetic is defined modulo 2^n, so `0 - 1` yields 2^32 - 1 = 4,294,967,295. That is the all-ones bit pattern — the very pattern that reads as **-1** in two's complement. Nothing overflowed in the hardware's eyes; the subtraction did exactly what modular arithmetic promises. The next iteration indexes `samples[4294967295]`, and the loop never ends on its own. Note the second landmine in the first line: for an **empty array**, `length - 1` performs the same wrap before the loop even starts, so the guard-restructuring fix must handle n = 0, not just the countdown. ## The same pattern, the comparison version The deeper representational fact — signed -1 and unsigned maximum share one bit pattern — also corrupts comparisons between a signed counter and an unsigned length field. In languages that implicitly convert mixed operands, the signed side is converted to unsigned, so: ``` i = -1 // signed sentinel: "not found" if i < length(buffer): // length is unsigned ... // -1 converts to 4294967295: comparison is false ``` The branch silently evaluates as `4294967295 < length` — almost always false — and the sentinel sails through logic that visibly checks for it. Ecosystems split three ways on this exact point: C and C++ perform the implicit conversion (the classic source of the bug), Go and Rust reject mixed signed/unsigned comparisons at compile time, and Java sidesteps the category by offering no unsigned primitive types at all. The disagreement is a signal that the trap is real enough to design a type system around. ## Recognizing it in production Symptoms that should trigger this hypothesis: - A loop that "cannot exit" whose guard reads as obviously sensible. - A crash or bounds fault at an index near 4 billion (or 65535, or 2^64 - 1 — the width names the culprit). - A `>= 0` or `< length` check that a debugger shows evaluating the "wrong" way with a -1 in play. The near-2^n magnitude is the fingerprint: it says a small negative quantity was read through an unsigned lens. ## Fixes, ranked by robustness 1. **Use a signed index** wide enough for every legal index (a 64-bit signed type covers any 32-bit length). The guard then means what it says. 2. **Restructure the countdown**: `while i > 0`, operate on `a[i - 1]`, decrement — every value of `i` stays in 0..n and zero is the natural exit. Handles the empty array for free. 3. **Count remaining elements** instead of positions: `remaining = n; while remaining > 0: process(a[remaining - 1]); remaining = remaining - 1`. 4. Casting inside the guard is the weakest fix — it patches one comparison, leaves the wrap on decrement in place, and still misses n = 0. In review, the senior-level habit is to treat any arithmetic that can cross zero on an unsigned type — countdowns, `length - 1`, size differences like `a - b` — as suspect by construction, and to keep index types signed or comparisons homogeneous at API boundaries.

  • How would you rewrite the countdown so an unsigned index is actually safe?
    Guard on `while i > 0` and operate on `a[i - 1]`, decrementing afterwards: i takes values n down to 1, every index touched is in range, zero exits the loop, and an empty array never enters it. Alternatively keep a `remaining` count. Both keep unsigned arithmetic away from crossing zero, which is the actual hazard.
  • Why does a comparison like -1 < length go wrong when length is unsigned?
    In languages that implicitly unify mixed operands, the signed -1 is converted to unsigned, and its all-ones bit pattern reads as the maximum unsigned value — 4,294,967,295 at 32 bits. The comparison silently becomes max < length, which is false, so sentinel checks pass through untriggered. Some type systems forbid the mixed comparison outright for exactly this reason.
  • What symptom in a crash report points you at this bug family?
    An out-of-bounds index whose magnitude sits just under a power of two — around 4.29 billion, 65535, or 2^64 - 1. That fingerprint says a small negative quantity was computed and then read through an unsigned lens of that width, so I would go hunting for countdowns, length - 1 on possibly-empty containers, and mixed signed/unsigned comparisons.

It is a car odometer rolled backwards: turn it below zero and it does not show a minus sign — it shows 999999. The loop keeps driving because the dashboard can never display a negative number.

saying these in an interview costs you the question

  • Expects the loop to stop once i goes negative — unsigned values cannot be negative
  • Thinks -1 stays -1 when compared against an unsigned value
  • Calls the wrap on 0 - 1 random corruption instead of defined modular arithmetic
  • Proposes a cast in the guard without handling the empty-array wrap
  • Believes the compiler will reject a tautological guard rather than possibly just warning

context