Variables and Constants

Global constants (const)

Declared at the top level of a file (outside any function). Always require an initial value. Type is inferred from the value. Cannot be mutated.

const MAX = 100
const GREETING = "Hello"
const PI_APPROX = 314

Constants may reference expressions involving other constants:

const BASE = 10
const LIMIT = BASE * 5   // LIMIT = 50

Attempting to redeclare a constant with the same name is a TypeError. Declaring a const inside a function is a SyntaxError.

Local variables (let)

Declared inside a function body. Immutable by default — the variable cannot be reassigned after its initial binding.

def main() {
    let x = 42
    let name = "Alice"
    let greeting = "Hello, " + name
}

An optional type annotation must match the inferred type:

let x: Int = 42      // valid
let x: Int = ""      // TypeError: mismatched types

Variables cannot be declared without an initial value (use let mut for that):

def f() {
    let a;        // SyntaxError: missing value
}

Redeclaring the same name in the same function is a TypeError. There is no shadowing.

Declaring a let at the top level is a SyntaxError.

Mutable local variables (let mut)

Allows reassignment after the initial binding. The type is fixed at declaration.

def main() {
    let mut count = 0
    count = count + 1
    count = 99
}

Assigning a different type is a TypeError:

def main() {
    let mut a = 1
    a = ""    // TypeError: cannot assign Str to Int
}

Empty let mut declarations

let mut may be declared with a type annotation but no initial value. The variable must be assigned before it is read:

def f(condition: Bool) Int {
    let mut result: Int
    if condition {
        result = 1
    } else {
        result = 2
    }
    return result    // valid: both branches assign
}

Reading an uninitialised variable is a TypeError. An immutable let cannot use the empty declaration form.

Compound assignment operators

Shorthand for variable = variable OP value. The variable must be mut.

OperatorExampleEquivalent
+=a += 3a = a + 3
-=a -= 3a = a - 3
*=a *= 3a = a * 3
/=a /= 3a = a / 3
%=a %= 3a = a % 3
def main() {
    let mut a = 1
    a += 1    // 2
    a -= 1    // 1
    a *= 4    // 4
    a /= 2    // 2
    print(a.Str())
}

+= also works on Str variables (string concatenation):

def main() {
    let mut s = "hello"
    s += " world"
    print(s)    // hello world
}

Scoping rules

Oxynium does not have block-level scoping. A variable declared inside an if, while, or for block is accessible in the enclosing function after that block. Redeclaring a name from an outer scope inside an inner block is a TypeError:

def main() {
    let a = 1
    if true {
        let a = 2    // TypeError: duplicate declaration
    }
}

For-loop variables persist after the loop ends, holding the last values seen:

def main() {
    for c, i in "abc" {}
    // c = 'c', i = 3 — last values from the loop
    print(i.Str() + c.Str())    // 3c
}

Field mutation through immutable variables

The let / let mut distinction controls whether the variable binding can be reassigned. Fields on a class instance can still be mutated through a non-mut variable:

class Point { x: Int, y: Int }

def main() {
    let p = new Point { x: 0, y: 0 }
    p.x = 5    // valid: mutating a field, not reassigning p
}