Blocks and Scope

A scope is a region of source code within which a name refers to a specific binding.

Scope-Creating Constructs

The following constructs create scopes:

ConstructScope contains
Function bodyParameters and body expression
run(...)All bindings within the sequence
for loopLoop variable and body
match armPattern bindings and arm expression
if branchesEach branch expression
loop(...)Body expression
with...inCapability binding and in expression
LambdaParameters and body expression

Lexical Scoping

Ori uses lexical scoping. A name refers to the binding in the innermost enclosing scope that declares that name.

run(
    let x = 10,
    run(
        let y = x + 5,  // x visible from outer scope
        y,
    ),
    // y not visible here
    x,
)

Names are resolved at the point of use by searching outward through enclosing scopes. If no binding is found, the compiler reports an error.

Visibility

A binding is visible from its declaration to the end of its enclosing scope.

run(
    // x not yet visible
    let x = 10,
    let y = x + 5,  // x visible
    y,
)
// x, y not visible

Bindings in run(...) are visible to all subsequent expressions in the sequence:

run(
    let a = 1,
    let b = a + 1,  // a visible
    let c = b + 1,  // a and b visible
    c,
)

No Hoisting

Bindings are not hoisted. A name cannot be used before its declaration:

run(
    let y = x + 1,  // error: x not declared
    let x = 10,
    y,
)

Shadowing

A binding may shadow an earlier binding with the same name. The new binding hides the previous one within its scope.

run(
    let x = 10,
    let x = x + 5,  // shadows, x is now 15
    x,
)

Shadowing applies to all bindings, including function parameters:

@increment (x: int) -> int = run(
    let x = x + 1,  // shadows parameter
    x,
)

The shadowed binding becomes inaccessible; there is no way to refer to it.

Lambda Capture

Lambdas capture variables from enclosing scopes by value.

run(
    let base = 10,
    let add_base = (x) -> x + base,  // captures base = 10
    add_base(5),  // returns 15
)

Capture Semantics

Capture is a snapshot at lambda creation time:

run(
    let mut x = 10,
    let f = () -> x * 2,  // captures x = 10
    x = 20,
    f(),  // returns 20, not 40
)

Lambdas cannot mutate captured bindings:

run(
    let mut x = 0,
    let inc = () -> x = x + 1,  // error: cannot mutate outer scope
    inc(),
)

This restriction prevents side effects through closures.

Nested Scopes

Scopes may be nested to arbitrary depth. Inner scopes can access bindings from all enclosing scopes:

run(
    let a = 1,
    run(
        let b = 2,
        run(
            let c = a + b,  // both visible
            c,
        ),
    ),
)

Each scope is independent; bindings in one branch do not affect another:

if condition then
    run(let x = 1, x)
else
    run(let x = 2, x)  // different x, no conflict