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

  1. 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 var declaration in one procedure) and the symptom (zero total on receipt) are far apart.

  2. Global loop variables create hidden coupling. Using a global I as 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.

  3. The call stack reveals the truth. Drawing the stack frames showed immediately that there were two Total variables: the correct one in the wrong frame, and the zero in the right frame.

  4. 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.

  5. Compile-time safety is free. Using const parameters in Approach 2 means the compiler will catch any accidental modification of input data — a class of bugs that simply cannot occur.


Your Turn

  1. 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.
  2. Apply only the fix for Bug 2 (remove the local Total declaration). Rerun and verify the total is now correct.
  3. Add local I variables to fix Bugs 1 and 3. Run again.
  4. Implement Approach 2 from scratch. Compare the readability of the two versions.
  5. 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?)