13 Variables

A variable is a storage location identified by a name. Every variable has a type and holds a value of that type.

Grammar: See grammar.ebnf § EXPRESSIONS (let_expr, assignment, binding_pattern)

13.1 Bindings

A let binding introduces an identifier into the current scope. Every let binding shall have an initializer expression. Uninitialized variables are not permitted.

let x = 42;                  // mutable
let name: str = "Alice";     // mutable, with type annotation
let $timeout = 30s;          // immutable ($ prefix)

let y: int;                  // error: binding requires initializer

Bindings are mutable by default. The $ prefix marks a binding as immutable (see Clause 12).

Type annotations are optional; types are inferred when omitted. When a type annotation is present, the annotated type shall match the inferred type.

13.2 Mutability

Bindings without $ prefix are mutable:

let x = 0;
x = x + 1;       // OK: mutable binding

let $y = 10;
$y = 20;         // error: cannot assign to immutable binding '$y'

The $ prefix marks a binding as immutable. See Constants for details.

13.2.1 Cannot Reassign

  • Immutable bindings ($-prefixed)
  • Function parameters (except self — see 13.5)
  • Loop variables
@add (a: int, b: int) -> int = {
    a = 10;  // error: cannot assign to parameter
    a + b
}

for item in items do
    item = other  // error: cannot assign to loop variable

13.3 Scope

Bindings are visible from declaration to end of enclosing block.

{
    let x = 10;
    let y = x + 5;  // x visible
    y
}
// x, y not visible

13.3.1 Shadowing

Bindings may shadow earlier bindings with the same name. Shadowing can change mutability:

{
    let x = 10;           // mutable
    let $x = x + 5;       // immutable, shadows outer x
    $x                    // 15
}

{
    let $x = 10;          // immutable
    {
        let x = $x * 2;   // mutable, shadows outer $x
        x = x + 1;        // OK: inner x is mutable
        x
    }
}

The $ prefix shall match between definition and usage within the same binding scope.

13.4 Destructuring

Patterns destructure composite values. The $ prefix applies to individual bindings:

let { x, y } = point;                  // both mutable
let { $x, y } = point;                 // x immutable, y mutable
let { x: px, y: py } = point;          // rename, both mutable
let (a, b) = pair;                     // both mutable
let ($a, $b) = pair;                   // both immutable
let [head, ..tail] = list;             // head mutable, tail mutable
let [$head, ..tail] = list;            // head immutable, tail mutable
let { position: { x, y } } = entity;   // nested destructure

Pattern shall match value structure.

13.5 Function Parameters

Parameters are immutable bindings scoped to the function body:

@add (a: int, b: int) -> int = a + b;

Parameters cannot be reassigned regardless of $ prefix, with one exception: self is a mutable binding in method bodies. self may be reassigned and its fields may be mutated. When a method mutates self, the modified value is implicitly propagated back to the caller through desugaring. See 11.11.

13.6 Assignment Semantics

Assignment is value copy. Ori uses value semantics: after x = y, x and y hold independent values. No aliasing exists between them.

let a = [1, 2, 3];
let b = a;         // b is an independent copy
b[0] = 99;         // does not affect a

NOTE The compiler may use copy-on-write (COW) or reference counting (ARC) internally to defer physical copying until mutation occurs. This is an optimization that does not affect observable semantics.

Assignment to an immutable binding ($-prefixed) is a compile-time error. Assignment to a function parameter or loop variable is a compile-time error. See 13.2.1.

13.6.1 Field assignment

Field assignment x.field = v desugars to x = { ...x, field: v }. The binding x shall be mutable.

13.6.2 Index assignment

Index assignment x[i] = v desugars to x = x.updated(key: i, value: v). The binding x shall be mutable.

13.6.3 Compound assignment

Compound assignment x op= y desugars to x = x op y. See 7.4.3.

13.7 Drop Ordering

A value is dropped (its Drop implementation is called, if any) when its binding goes out of scope. Within a block, values are dropped in reverse declaration order:

{
    let a = acquire_a();  // dropped third
    let b = acquire_b();  // dropped second
    let c = acquire_c();  // dropped first
    result
}

Drop order for function parameters after the body returns is implementation-defined.

The built-in function drop_early(value:) explicitly drops a value before the end of its scope. After drop_early(value: x), the binding x is inaccessible; any subsequent use is a compile-time error. drop_early works on both mutable and immutable bindings — it concerns ownership, not mutability.

See Clause 21 for details on the ARC memory model.

13.8 Blank Identifier

The underscore _ used alone in a pattern is the blank identifier. It matches any value and does not create a binding.

let _ = compute();       // evaluates compute(), discards result
match x { _ -> 0 }       // matches anything
let (_, b) = pair;       // discards first element

In let _ = expr, the expression is fully evaluated (including side effects and any Drop implementation of the result), but no binding is created.

Multiple _ patterns may appear in the same pattern, unlike named bindings which shall be unique.

See Clause 15 for pattern matching rules.