skip to content

A running total prints zero after an edit added a declaration inside the loop block: how do you diagnose which binding is written?

level: middleimportance: should knowfreq 40%

answer

  1. two mentions, maybe two declarations
  2. resolve each mention outward
  3. compare the write and the read
  4. a fresh binding on every pass
  5. well-formed, so nothing is reported

basics

~20 s

Resolve each mention outward to its nearest declaration and compare them. The write inside the loop reached a new per-iteration binding the edit introduced, while the read after the loop still reaches the outer binding, which nothing ever wrote.

solid answer

~40 s

Treat it as a resolution question, not a control-flow question. Take the mention that writes `total` inside the loop and the mention that reads `total` after it, and for each one walk outward to the nearest enclosing declaration. If they land on different declarations, that is the defect, and no amount of staring at the loop condition will show it. The added declaration made a second binding, remade on every pass, so nothing accumulated anywhere the final read can see. Nothing is ill-formed here, so no diagnostic fires. The fix depends on intent: delete the inner declaration when the loop was meant to accumulate, so the mention writes through to the outer binding, or rename the inner name when it genuinely is a different quantity.

code

pseudocode · 7 lines
pseudocode
declare total = 0

for each section in sections
    declare total = section.amount  // the edit that broke it
    log(total)

print(total)   // 0 - the outer binding was never written

go deeper

for a junior

Know that a name declared inside a loop body is a new binding on every pass, so it cannot hold a value that carries from one pass to the next.

for a middle

Run the resolution walk as a procedure: take the write and the read, resolve each outward to its nearest declaration, and say whether the two land on the same one.

for a senior

Explain why the defect is invisible to the build and to a block-at-a-time reading, and say what you would turn on so the next such edit is reported when it is made.

for a principal

Weigh a codebase-wide ban on shadowing against the rename churn it costs, and decide which classes of silent, legal defects the build is expected to own.

## Read it as a resolution defect The symptom - a total that prints zero - invites a control-flow hunt: is the collection empty, is the condition inverted, does the body run at all? Those checks come back clean here, and they come back clean quickly, because the loop body visibly runs and visibly adds. The defect is one block boundary away. An edit added a declaration inside the loop body for a name the enclosing block already declared. Now two bindings share the spelling, and the two mentions that matter resolve to different ones: - the mention **inside** the loop resolves to the inner declaration, because resolution stops at the nearest enclosing block that declares the name; - the mention **after** the loop resolves to the outer declaration, because the inner block has ended and is no longer part of the search. The write and the read never met. The outer binding still holds its initial value. ## The diagnostic procedure 1. **Locate the two mentions.** One writes, one reads, and they are on opposite sides of the block boundary. 2. **Resolve each one outward.** From the mention, walk out through enclosing blocks to the first that declares the name. Write down which declaration each mention reached. 3. **Compare.** Same declaration means the shadow is not your defect and you go back to control flow. Different declarations means you have found it. 4. **Confirm from the shape.** A binding declared inside a loop body is created fresh on each pass, so no value carries from one pass to the next through it - which is exactly the "it adds but nothing accumulates" symptom. This takes less time than one round of instrumentation, and unlike printing values it *settles* the question rather than describing the symptom. ## Why the program stayed silent Nothing here is ill-formed, and that is the whole reason this defect survives review: - A nested block declaring a name an outer block also declares is an ordinary construct, not a duplicate declaration. - Every mention resolves to something, so there is no unresolved name. - The loop body is internally consistent: read on its own, every mention in it means the same thing. - Types agree, because both bindings hold the same kind of value. - The defect is only visible **across** the boundary, and nothing in the text draws attention to a boundary. The usual detection is not a diagnostic at all but a policy: turn on the build's shadowing report where the codebase has decided against shadowing, so an added declaration is flagged at the moment it is added rather than at the moment a total prints zero. ## Choosing the fix | intent of the edit | correct fix | what it makes the loop do | |---|---|---| | accumulate into the outer total | delete the inner declaration | the mention writes through to the one binding | | track a per-pass quantity | rename the inner name | two names for two genuinely different values | | both, deliberately | keep both with distinct names | accumulation and the per-pass value coexist readably | The fix to avoid is widening the outer declaration - hoisting it further out, or making it reachable from more of the program - in the hope that a wider name is harder to shadow. It is not, and it costs the readability the block structure was providing. ## What this question is really testing An interviewer asking it is checking three things at once: - whether the candidate reasons about **which declaration a mention binds to**, as a first-class question with a mechanical answer; - whether they know that a declaration inside a loop body produces a **fresh binding per pass**, so it cannot carry a running value; - whether they reach for a **procedure** before reaching for instrumentation - the resolution walk is deterministic, while printing values is a search. The weak answer starts adding print statements and finds the truth eventually. The strong answer names the two mentions, resolves each one, states that they land on different declarations, and only then decides which of the two fixes matches the intent. It usually ends with a sentence on prevention, because a defect that is legal, silent and introduced by an ordinary edit is one you want the build to catch rather than the next reader.

  • Why does the loop body look correct when you read it in isolation?
    Because inside the block the added declaration is the nearest one, so every mention there is consistent with every other. The defect only exists across the block boundary, where the read after the loop resolves to a different declaration than the write inside it. Reading one block at a time cannot show it.
  • Which fix do you prefer, deleting the inner declaration or renaming it?
    Delete it when the intent was to accumulate: the mention then writes through to the outer binding and the loop does what it looked like it did. Rename it when the inner value genuinely is a different quantity, such as a per-section subtotal, because two distinct names then describe what the code already computes.

saying these in an interview costs you the question

  • Blames the loop condition before checking which declaration each mention reaches
  • Says the build would have reported a duplicate name
  • Thinks the inner binding accumulates across iterations
  • Assumes a write and a later read must reach the same declaration
  • Hoists the declaration to the widest scope instead of finding the shadow