Skip to main content

Scoping and Shadowing

Zynx uses traditional lexical scoping with block structure. This document explains how scopes, lifetimes, and shadowing interact, and how that relates to the borrow rules enforced by the compiler.

Blocks and Scope

Variables declared within a block ({ ... }) are local to that block. They are automatically cleaned up when the scope ends.

fn main() {
let outer = 1;
{
let inner = 2;
// `outer` and `inner` are both accessible here
}
// Only `outer` is accessible here; `inner` is out of scope
}

Key points:

  • Scope is introduced by blocks ({ ... }), functions, and some control-flow constructs.
  • Values are dropped at the end of the scope that owns them.
  • References must not outlive the scope of the values they reference (see Borrowing Rules below).

Shadowing

Zynx supports variable shadowing, allowing you to reuse a name within the same or a nested scope. The new declaration “shadows” the previous one until the current scope ends.

fn main() {
let x = 10;
{
let x = 20; // Shadows the outer `x`
// `x` is 20 here
}
// `x` is 10 here again
}

Shadowing is commonly used for:

  • Transformations: Converting a value while keeping the same logical name.

    fn main() {
    let input = "42";
    let input = parse_int(input); // `input` is now an i32
    }
  • Unwrapping: Safely handling optional types or error unions.

    fn main() {
    let res: i32! = compute();
    if res {
    // Inside this block, `res` is a shadowed `i32`
    os.write("Result: {res}\n");
    }
    }

When shadowing, prefer small scopes and clear transformations so that it remains obvious which binding of a name is in use.

Borrowing Rules and Scope

Zynx enforces strict borrowing rules to ensure memory safety. These rules are evaluated with respect to lexical scopes:

  • Immutable borrows: Multiple const &T references are allowed at the same time.
  • Mutable borrows: Only one mutable reference &T is allowed at a time, and it cannot coexist with any immutable borrows.
  • Lifetimes: References must not outlive the values they refer to; the referenced value must remain in scope and not be moved before all borrows end.
fn main() {
let x = 10;

{
let r: const &i32 = &x; // immutable borrow
// `x` cannot be mutably borrowed here while `r` is live
os.write("{r}\n");
} // `r` goes out of scope here

let m: &i32 = &x; // now allowed: new borrow after previous one ended
os.write("{m}\n");
}

Shadowing interacts with borrowing only through scopes: each shadowed binding has its own lifetime. Once the inner binding goes out of scope, the outer binding becomes visible again and can be borrowed independently, as long as the borrow rules above are respected.

See also