Case Study 2: From Pascal to Other Languages — How Scope Rules Compare

Introduction

One of the most powerful reasons to learn scope and parameter passing in Pascal first is that Pascal makes every rule explicit and enforced at compile time. Other languages make different trade-offs — sometimes for convenience, sometimes for performance, sometimes for historical reasons. In this case study, we compare Pascal's scope model with three other languages: Python (function scope with closures), C (block scope with pointers), and JavaScript (hoisting and closures). We will see that each language's behavior is a variation on the principles we learned in this chapter.

This case study is designed to be read even if you have no experience with these other languages. The code examples are simple enough to follow from the comments and explanations alone.


Scope Comparison 1: Variable Visibility

Pascal: Strict Lexical Block Scope

program PascalScope;

  procedure Demo;
  var
    X: Integer;
  begin
    X := 10;
    WriteLn(X);  { Legal: X is in scope }
  end;

begin
  Demo;
  { WriteLn(X); }  { COMPILE ERROR: X does not exist here }
end.

Pascal is strict: a variable declared inside a procedure is visible only inside that procedure. No exceptions. The compiler enforces this at compile time, and there is no way to "reach into" another scope.

Python: Function Scope (Not Block Scope)

def demo():
    x = 10
    if True:
        y = 20    # y is scoped to the FUNCTION, not the if-block!
    print(y)      # Legal! prints 20. Surprises many people.

demo()
# print(x)       # Error: x is not defined (function scope works)

Python has function scope, not block scope. A variable created inside an if, for, or while block is still visible throughout the entire function. This is different from Pascal, where each nested block creates a new scope level. This Python behavior surprises many programmers who come from languages with block scope.

C: Block Scope (Like Pascal, But With Braces)

#include <stdio.h>

void demo() {
    int x = 10;
    if (1) {
        int y = 20;  /* y is scoped to this { } block */
        printf("%d\n", y);  /* Legal */
    }
    /* printf("%d\n", y); */  /* ERROR: y is not in scope */
}

int main() {
    demo();
    /* printf("%d\n", x); */  /* ERROR: x is not in scope */
    return 0;
}

C has block scope similar to Pascal, but with a key difference: in C (since C99), you can declare variables inside any { } block, including inside if statements and for loops. Pascal's scope units are procedures, functions, and the main program — you cannot create a new scope with just begin..end.

JavaScript: Hoisting and var vs. let

function demo() {
    console.log(x);   // undefined (not an error! — x is "hoisted")
    var x = 10;
    console.log(x);   // 10

    if (true) {
        let y = 20;   // let has block scope (modern JS)
        var z = 30;   // var has function scope (old JS)
    }
    // console.log(y);  // ERROR: y is not defined (block scoped by let)
    console.log(z);    // 30 (var is function-scoped, not block-scoped)
}

JavaScript's var keyword has function scope (like Python) plus a behavior called hoisting: the declaration is moved to the top of the function, but the assignment stays in place. This means using a var variable before its declaration line gives undefined instead of an error. Modern JavaScript's let and const keywords have block scope (like C/Pascal), which is a direct acknowledgment that block scope is easier to reason about.

Summary Table: Scope Units

Language Scope unit Block-scoped? Compile-time enforcement?
Pascal Procedure/function Yes (but only proc/func blocks) Yes, always
Python Function No — if/for do not create scope Runtime (NameError)
C { } block Yes Yes, always
JavaScript (var) Function No — if/for do not create scope No (undefined, not error)
JavaScript (let) { } block Yes Yes (ReferenceError)

Takeaway: Pascal's compile-time enforcement means scope bugs are caught before the program runs. In Python and JavaScript (var), scope bugs can lurk until a specific code path is executed.


Scope Comparison 2: Variable Shadowing

Pascal: Shadowing Is Silent But Deterministic

program PascalShadow;
var
  X: Integer;
  procedure P;
  var X: Integer;
  begin
    X := 99;          { Modifies local X }
  end;
begin
  X := 1;
  P;
  WriteLn(X);         { Prints 1 — global X unchanged }
end.

Pascal allows shadowing without any warning by default. The compiler does not complain. The behavior is completely deterministic — the innermost declaration always wins.

Python: Shadowing With a Twist (global Keyword)

x = 1

def p():
    x = 99    # Creates a NEW local x — shadows global x
    print(x)  # 99

p()
print(x)      # 1 — global unchanged

def q():
    global x  # Explicitly says "I want the global x"
    x = 99

q()
print(x)      # 99 — global IS changed

Python's rule: if you assign to a variable inside a function, Python treats it as local — unless you use the global keyword to explicitly opt in to modifying the global. This is Python's solution to the shadowing problem, but it creates its own confusion: reading a global is automatic, but writing to it requires global. Many beginners are baffled by this asymmetry.

Pascal's approach is different: if you want to modify an outer variable, you either access it directly (it is in scope) or pass it as a var parameter. There is no global keyword because there is no ambiguity — the compiler knows from the declarations which variable you mean.

C: Shadowing With Block Granularity

#include <stdio.h>

int x = 1;

void p() {
    int x = 99;       /* Shadows global x */
    printf("%d\n", x); /* 99 */
    {
        int x = 42;   /* Shadows the x above! Block-level shadow. */
        printf("%d\n", x); /* 42 */
    }
    printf("%d\n", x); /* 99 again — inner block's x is gone */
}

int main() {
    p();
    printf("%d\n", x); /* 1 — global unchanged */
    return 0;
}

C allows shadowing at any block level, which means you can have three different x variables in a single function. This is more fine-grained than Pascal but also more confusing.

JavaScript: Shadowing and the Temporal Dead Zone

let x = 1;

function p() {
    let x = 99;        // Shadows outer x
    console.log(x);    // 99
}

function q() {
    console.log(x);    // ReferenceError! (temporal dead zone)
    let x = 42;        // This shadows outer x, but the line above
                        // tries to use x before the let declaration
}

p();
q();  // Crashes

JavaScript's let has a "temporal dead zone" — the variable exists from the start of its block but cannot be accessed until the let statement. This is different from Pascal, where a variable is accessible from the first statement in its block (after declarations).

Takeaway: Pascal's approach to shadowing is the simplest and most predictable. There are no special keywords, no temporal dead zones, no assignment-vs-read asymmetries. If you understand shadowing in Pascal, you will recognize and handle it in any language.


Scope Comparison 3: Parameter Passing

Pascal: Three Explicit Modes

procedure Demo(A: Integer; var B: Integer; const C: Integer);
begin
  A := A + 1;     { Modifies local copy }
  B := B + 1;     { Modifies caller's variable }
  { C := C + 1; } { COMPILE ERROR — const is read-only }
end;

Pascal makes the parameter mode visible in the declaration. When you read the code, you know immediately which parameters can be modified.

Python: "Object Reference" (Neither Value Nor Reference)

def demo(a, b):
    a = a + 1       # Rebinds local name a — does NOT change caller's variable
    b.append(4)     # Modifies the object b refers to — DOES change caller's list

x = 5
y = [1, 2, 3]
demo(x, y)
print(x)            # 5 — unchanged
print(y)            # [1, 2, 3, 4] — changed!

Python passes "object references" — which means reassigning the parameter name does not affect the caller, but mutating the object it refers to does. There is no var keyword to make this explicit. You must know whether the object is mutable (lists, dictionaries) or immutable (integers, strings, tuples) to predict the behavior.

This is one of Python's most confusing aspects for beginners. Pascal's explicit var mode would eliminate the confusion entirely.

C: Values and Pointers

void demo(int a, int *b) {
    a = a + 1;       /* Modifies local copy */
    *b = *b + 1;     /* Modifies what b points to — changes caller's variable */
}

int main() {
    int x = 5, y = 10;
    demo(x, &y);     /* Pass x by value, y by address */
    printf("%d %d\n", x, y);  /* 5 11 */
    return 0;
}

C does not have var parameters. Instead, you pass a pointer (the address of a variable) and dereference it manually. This is more error-prone than Pascal's var — you must remember the * on every access and the & at every call site.

Pascal's var is essentially a pointer that the compiler manages for you. You get the power of pass-by-reference without the danger of raw pointer manipulation.

JavaScript: Always By Value (But Objects Are Reference Values)

function demo(a, b) {
    a = a + 1;         // Local rebind — no effect on caller
    b.push(4);         // Mutates the object — affects caller
    b = [99];          // Local rebind — does NOT affect caller's reference
}

let x = 5;
let y = [1, 2, 3];
demo(x, y);
console.log(x);        // 5
console.log(y);         // [1, 2, 3, 4]

JavaScript, like Python, passes object references by value. There is no way to write a "swap" function that swaps two primitive variables — you would need to wrap them in objects. In Pascal, var parameters make swap trivial.

Summary Table: Parameter Passing

Language Mechanism Can modify caller's variable? Explicit in declaration?
Pascal (value) Copy No Yes (no keyword)
Pascal (var) Address Yes Yes (var keyword)
Pascal (const) Compiler's choice No (enforced) Yes (const keyword)
Python Object reference Only via mutation No
C (value) Copy No Yes (no *)
C (pointer) Address (manual) Yes Yes (* in type)
JavaScript Value (incl. references) Only via mutation No

Scope Comparison 4: Nested Functions and Closures

program PascalNested;

  procedure Outer;
  var Counter: Integer;

    procedure Increment;
    begin
      Counter := Counter + 1;  { Accesses enclosing scope directly }
    end;

  begin
    Counter := 0;
    Increment;
    Increment;
    WriteLn(Counter);  { 2 }
  end;

begin
  Outer;
end.

Pascal's nested procedures can access variables from enclosing scopes. The compiler uses static links in stack frames to make this work at runtime. Increment is not visible outside Outer.

Python: Closures (With nonlocal for Writing)

def outer():
    counter = 0

    def increment():
        nonlocal counter     # Required to MODIFY enclosing variable
        counter += 1

    increment()
    increment()
    print(counter)           # 2

outer()

Python supports nested functions (closures), but requires the nonlocal keyword to modify (not just read) a variable from an enclosing scope. This is similar to Python's global keyword asymmetry. Pascal does not have this limitation — Counter in the example above is simply in scope and fully accessible.

C: No Nested Functions (Standard C)

Standard C does not support nested functions. To achieve similar behavior, you must pass data through parameters or use global variables. (GCC has a non-standard extension for nested functions, but it is not portable.)

This is a case where Pascal is more expressive than C. Pascal's nested subprograms provide natural encapsulation that C requires workarounds to achieve.

JavaScript: Closures Are Central

function outer() {
    let counter = 0;

    function increment() {
        counter++;    // Closures can read AND write — no special keyword needed
    }

    increment();
    increment();
    console.log(counter);  // 2
}

outer();

JavaScript closures work similarly to Pascal's nested procedures but are even more flexible — you can return a closure from a function, and it retains access to the enclosing scope's variables even after the enclosing function has returned. This is called "closing over" the variables, and the variables persist on the heap rather than the stack.

Pascal's nested procedures cannot outlive their enclosing scope (because Pascal manages everything on the stack), but they provide the same lexical-scope access mechanism.


The Big Picture

Feature Pascal Python C JavaScript
Scope type Lexical, block Lexical, function Lexical, block Lexical, block (let) / function (var)
Compile-time checking Full None (dynamic) Full Partial
Explicit param modes Yes (3 modes) No Partial (pointer syntax) No
Nested functions Yes Yes No (standard) Yes
Shadowing protection Warnings available No warnings Warnings available Strict mode helps

The core lesson: Understanding scope, the call stack, and parameter passing in Pascal gives you a rock-solid mental model. When you encounter Python's global/nonlocal keywords, C's pointer syntax, or JavaScript's hoisting behavior, you will recognize them as different solutions to the same underlying problems that Pascal solves with clean, explicit syntax.


Discussion Questions

  1. Python's global and nonlocal keywords do not exist in Pascal. Why not? What feature of Pascal makes them unnecessary?

  2. C does not have var parameters — it uses pointers instead. What are two advantages of Pascal's var approach over C's pointer approach?

  3. JavaScript's var keyword has function scope, while let has block scope. The JavaScript community now recommends always using let. What does this tell us about the value of block scope?

  4. If you were designing a new programming language today, which scope and parameter-passing rules would you choose? Would you follow Pascal, Python, C, JavaScript, or some combination? Justify your answer.

  5. Consider the claim: "Learning Pascal's strict scope rules first makes it easier to learn other languages' scope rules later, but learning Python's loose scope rules first makes it harder to learn Pascal's." Do you agree or disagree? Why?