Case Study 1: Debugging a Scope Disaster
The Scenario
You have been hired as a teaching assistant for an introductory programming course. A student named Jordan has submitted the following program for a homework assignment. The program is supposed to manage a simple inventory: it reads item quantities, calculates totals, applies a discount if applicable, and prints a receipt. Jordan says, "It compiles fine, but the output is completely wrong. I have been staring at it for hours."
Your task: find every scope-related bug, explain what is going wrong, trace the call stack at key moments, and fix each issue.
Jordan's Buggy Program
program InventoryReport;
{ Jordan's submission — "Why doesn't this work?!" }
var
Items: array[1..10] of Integer;
Prices: array[1..10] of Real;
Count: Integer;
Total: Real;
Discount: Real;
I: Integer;
procedure ReadInventory;
begin
Write('How many items (1-10)? ');
ReadLn(Count);
for I := 1 to Count do { BUG 1: uses global I }
begin
Write('Quantity for item ', I, ': ');
ReadLn(Items[I]);
Write('Price for item ', I, ': ');
ReadLn(Prices[I]);
end;
end;
procedure CalculateTotal;
var
Total: Real; { BUG 2: shadows global Total }
begin
Total := 0;
for I := 1 to Count do { BUG 3: uses global I }
Total := Total + (Items[I] * Prices[I]);
WriteLn('Subtotal: ', Total:0:2);
end;
procedure ApplyDiscount;
begin
if Total > 100.0 then { BUG 4: reads global Total, which is 0 }
Discount := 0.10
else
Discount := 0.0;
Total := Total - (Total * Discount); { BUG 5: global Total is still 0 }
end;
procedure PrintReceipt;
var
I: Integer; { This one is correct — local I }
begin
WriteLn('=== RECEIPT ===');
for I := 1 to Count do
begin
WriteLn('Item ', I, ': ', Items[I], ' x $',
Prices[I]:0:2, ' = $',
(Items[I] * Prices[I]):0:2);
end;
WriteLn('Total: $', Total:0:2);
if Discount > 0 then
WriteLn('Discount: ', (Discount * 100):0:0, '%');
WriteLn('Final: $', Total:0:2); { BUG 6: same as Total — discount was applied to 0 }
end;
begin
ReadInventory;
CalculateTotal;
ApplyDiscount;
PrintReceipt;
end.
Bug Analysis
Bug 1: Global Loop Variable in ReadInventory
Location: ReadInventory, line for I := 1 to Count do
Problem: ReadInventory does not declare its own loop variable I. It uses the global I. While this happens to work in isolation, it means that ReadInventory modifies a global variable as a side effect. If any other code between calls depends on the value of I, it will find an unexpected value (the value of Count after the loop, or Count + 1 depending on the compiler).
Severity: Medium. Works in this particular call sequence but creates fragile coupling.
Fix: Declare var I: Integer; locally inside ReadInventory.
Bug 2: Shadowed Total in CalculateTotal
Location: CalculateTotal, line var Total: Real;
Problem: This is the show-stopping bug. CalculateTotal declares a local variable Total that shadows the global Total. The procedure carefully accumulates the correct subtotal — then prints it — but stores it in the local copy. When CalculateTotal returns, its stack frame is destroyed, and the local Total vanishes. The global Total is still 0.0 (it was never assigned).
Impact: Every subsequent procedure that reads the global Total (that is, ApplyDiscount and PrintReceipt) sees 0.0.
Severity: Critical. This is the primary cause of the wrong output.
Fix: Remove the local var Total: Real; declaration so that CalculateTotal operates on the global Total. Alternatively (and better), redesign so that CalculateTotal is a function that returns the total through its return value.
Bug 3: Global Loop Variable in CalculateTotal
Location: CalculateTotal, line for I := 1 to Count do
Problem: Same as Bug 1 — uses global I instead of a local loop counter.
Severity: Medium.
Fix: Declare var I: Integer; locally (note: the current local var section only has Total; add I to it).
Bug 4: ApplyDiscount Reads Uninitialized Global Total
Location: ApplyDiscount, line if Total > 100.0 then
Problem: Because of Bug 2, the global Total is 0.0. The condition Total > 100.0 is always false, so Discount is always set to 0.0, regardless of the actual subtotal.
Severity: Critical (consequence of Bug 2).
Fix: Once Bug 2 is fixed, this code will read the correct value.
Bug 5: ApplyDiscount Computes Discount on Zero
Location: ApplyDiscount, line Total := Total - (Total * Discount);
Problem: Even if Discount were set to 0.10, the computation would be 0.0 - (0.0 * 0.10) = 0.0. The discount is being applied to the zero-valued global Total.
Severity: Critical (consequence of Bug 2).
Fix: Once Bug 2 is fixed, this computation will be correct.
Bug 6: PrintReceipt Shows Wrong Final Amount
Location: PrintReceipt, line WriteLn('Final: $', Total:0:2);
Problem: Since the global Total is 0.0 and the discount was applied to 0.0, the receipt shows $0.00 as the total and final amount. The line-item calculations (which are computed inline) will actually show correct per-item amounts, creating a confusing receipt that lists items correctly but totals to zero.
Severity: Critical (consequence of Bug 2).
Fix: Cascading fix from Bug 2.
Stack Trace at Key Moments
Moment 1: Inside CalculateTotal, After Accumulation
Suppose the user entered 2 items: Item 1 quantity 3 at $20.00, Item 2 quantity 2 at $35.00.
+--------------------------------------------+
| Frame: CalculateTotal |
| Local Total: 130.00 (correct!) |
| (no local I — using global) |
+--------------------------------------------+
| Frame: main |
| Items[1]: 3 Prices[1]: 20.00 |
| Items[2]: 2 Prices[2]: 35.00 |
| Count: 2 |
| Total: 0.00 ← STILL ZERO |
| Discount: ??? (uninitialized) |
| I: 3 (from the for loop) |
+--------------------------------------------+
The critical insight: there are two variables named Total. The local one in CalculateTotal's frame has the correct value (130.00). The global one in main's frame is still 0.00. When CalculateTotal returns, the local frame is destroyed — and the correct value goes with it.
Moment 2: Inside ApplyDiscount
+--------------------------------------------+
| Frame: ApplyDiscount |
| (no local variables) |
| Reads global Total: 0.00 |
| Sets global Discount: 0.0 |
+--------------------------------------------+
| Frame: main |
| Total: 0.00 |
| Discount: 0.0 |
+--------------------------------------------+
ApplyDiscount correctly tests Total > 100.0, but the global Total is 0.00, so the discount is 0.0. The "fix" Total := Total - (Total * Discount) computes 0.0 - (0.0 * 0.0) = 0.0. No change.
Moment 3: Inside PrintReceipt
+--------------------------------------------+
| Frame: PrintReceipt |
| Local I: (used for loop) |
| Reads global Total: 0.00 |
| Reads global Discount: 0.0 |
+--------------------------------------------+
| Frame: main |
| Total: 0.00 |
| Discount: 0.0 |
+--------------------------------------------+
The receipt prints each line item correctly (because those are computed inline from Items[I] * Prices[I]), but the total line says $0.00.
The Fixed Program
Here is the corrected version with two approaches: a minimal fix and a proper redesign.
Approach 1: Minimal Fix (Remove Shadowing, Add Local Loop Variables)
program InventoryReportFixed1;
var
Items: array[1..10] of Integer;
Prices: array[1..10] of Real;
Count: Integer;
Total: Real;
Discount: Real;
procedure ReadInventory;
var
I: Integer; { FIX: local loop variable }
begin
Write('How many items (1-10)? ');
ReadLn(Count);
for I := 1 to Count do
begin
Write('Quantity for item ', I, ': ');
ReadLn(Items[I]);
Write('Price for item ', I, ': ');
ReadLn(Prices[I]);
end;
end;
procedure CalculateTotal;
var
I: Integer; { FIX: local loop variable }
{ FIX: removed "Total: Real" — no longer shadows global }
begin
Total := 0;
for I := 1 to Count do
Total := Total + (Items[I] * Prices[I]);
WriteLn('Subtotal: $', Total:0:2);
end;
procedure ApplyDiscount;
begin
if Total > 100.0 then
Discount := 0.10
else
Discount := 0.0;
Total := Total - (Total * Discount);
end;
procedure PrintReceipt;
var
I: Integer;
begin
WriteLn('=== RECEIPT ===');
for I := 1 to Count do
begin
WriteLn('Item ', I, ': ', Items[I], ' x $',
Prices[I]:0:2, ' = $',
(Items[I] * Prices[I]):0:2);
end;
if Discount > 0 then
begin
WriteLn('Subtotal: $', (Total / (1 - Discount)):0:2);
WriteLn('Discount: ', (Discount * 100):0:0, '%');
end;
WriteLn('Total: $', Total:0:2);
end;
begin
ReadInventory;
CalculateTotal;
ApplyDiscount;
PrintReceipt;
end.
Approach 2: Proper Redesign (Explicit Parameters, No Global Coupling)
program InventoryReportFixed2;
const
MAX_ITEMS = 10;
type
TQuantities = array[1..MAX_ITEMS] of Integer;
TPrices = array[1..MAX_ITEMS] of Real;
procedure ReadInventory(var Items: TQuantities; var Prices: TPrices;
var Count: Integer);
var
I: Integer;
begin
Write('How many items (1-', MAX_ITEMS, ')? ');
ReadLn(Count);
for I := 1 to Count do
begin
Write('Quantity for item ', I, ': ');
ReadLn(Items[I]);
Write('Price for item ', I, ': ');
ReadLn(Prices[I]);
end;
end;
function CalculateTotal(const Items: TQuantities; const Prices: TPrices;
const Count: Integer): Real;
var
I: Integer;
RunningTotal: Real;
begin
RunningTotal := 0;
for I := 1 to Count do
RunningTotal := RunningTotal + (Items[I] * Prices[I]);
CalculateTotal := RunningTotal;
end;
function GetDiscount(const Subtotal: Real): Real;
begin
if Subtotal > 100.0 then
GetDiscount := 0.10
else
GetDiscount := 0.0;
end;
procedure PrintReceipt(const Items: TQuantities; const Prices: TPrices;
const Count: Integer; const Subtotal, DiscountRate: Real);
var
I: Integer;
FinalTotal: Real;
begin
WriteLn('=== RECEIPT ===');
for I := 1 to Count do
WriteLn('Item ', I, ': ', Items[I], ' x $',
Prices[I]:0:2, ' = $',
(Items[I] * Prices[I]):0:2);
WriteLn('Subtotal: $', Subtotal:0:2);
if DiscountRate > 0 then
WriteLn('Discount: ', (DiscountRate * 100):0:0, '%');
FinalTotal := Subtotal - (Subtotal * DiscountRate);
WriteLn('Total: $', FinalTotal:0:2);
end;
var
Items: TQuantities;
Prices: TPrices;
Count: Integer;
Subtotal, DiscountRate: Real;
begin
ReadInventory(Items, Prices, Count);
Subtotal := CalculateTotal(Items, Prices, Count);
WriteLn('Subtotal: $', Subtotal:0:2);
DiscountRate := GetDiscount(Subtotal);
PrintReceipt(Items, Prices, Count, Subtotal, DiscountRate);
end.
Lessons Learned
-
Shadowing is the most dangerous scope bug. The program compiles, runs, and even partially works — but a single shadowed variable causes cascading failures. The root cause (a
vardeclaration in one procedure) and the symptom (zero total on receipt) are far apart. -
Global loop variables create hidden coupling. Using a global
Ias a loop counter in multiple procedures works by accident — if the procedures are never interleaved. The moment someone adds a call within a loop, the bug appears. -
The call stack reveals the truth. Drawing the stack frames showed immediately that there were two
Totalvariables: the correct one in the wrong frame, and the zero in the right frame. -
The redesigned version is self-documenting. In Approach 2, every procedure's header tells you exactly what data it needs and what it produces. You never have to read the implementation to understand the data flow.
-
Compile-time safety is free. Using
constparameters in Approach 2 means the compiler will catch any accidental modification of input data — a class of bugs that simply cannot occur.
Your Turn
- Compile and run Jordan's original buggy program with the test data (2 items: quantity 3 at $20.00, quantity 2 at $35.00). Verify that the total shows $0.00.
- Apply only the fix for Bug 2 (remove the local
Totaldeclaration). Rerun and verify the total is now correct. - Add local
Ivariables to fix Bugs 1 and 3. Run again. - Implement Approach 2 from scratch. Compare the readability of the two versions.
- Identify one additional design improvement that even Approach 2 could benefit from. (Hint: what happens if the user enters a count greater than 10 or less than 1?)