Variables

EZ uses two keywords for declaring variables: mut for mutable values and const for immutable values.

Mutable Variables (mut)

Use mut to declare variables that can be reassigned:

mut x int = 10
mut name string = "Alice"
mut isActive bool = true

// Reassignment is allowed
x = 20
name = "Bob"
isActive = false

Immutable Constants (const)

Use const to declare values that cannot change:

const PI float = 3.14159
const MAX_SIZE int = 100
const APP_NAME string = "MyApp"

// This would cause an error:
// PI = 3.14  // Error! Cannot reassign const

Type Inference

EZ has full type inference. The type of every variable is known at compile time, but explicit type annotations are optional when the compiler can determine the type from the initializer.

// Inferred from literals
mut x = 42                    // Inferred: int
mut name = "Alice"            // Inferred: string
mut pi = 3.14                 // Inferred: float
mut flag = true               // Inferred: bool

// Explicit annotations are always accepted
mut y int = 42                // Explicit: int

// Inferred from function return type
do sum(a int, b int) -> int {
    return a + b
}
mut result = sum(1, 2)        // Inferred: int

// Inferred from struct literal
const p = Point{x: 1, y: 2}  // Inferred: Point

// Inferred from built-in constructors
mut val = new(Person)         // Inferred: ^Person (pointer)
mut dup = copy(val)           // Inferred: Person

// Multiple return values
do divide(a, b int) -> (int, int) {
    return a / b, a % b
}
mut quotient, remainder = divide(10, 3)  // Both inferred: int

Explicit type annotations are never required but can be used for clarity or documentation.

Declaring Without a Value

Variables can be declared without assigning a value, as long as a type annotation is provided. The variable is automatically zero-initialized:

mut x int       // 0
mut y i8        // 0
mut name string // ""
mut flag bool   // false

const Foo struct {
    name string
    age int
    active bool
}

mut f Foo       // Foo{name: "", age: 0, active: false}

This works with any type that has a zero value (see Default Zero Values).


Type Annotations

EZ is statically typed — you can declare the type explicitly:

mut count int = 0
mut price float = 19.99
mut message string = "Hello"
mut letter char = 'A'
mut enabled bool = true

Primitive Types

Integers

mut age int = 25
mut negative int = -100
mut zero int = 0

Sized Integers

EZ supports explicitly sized integers:

// Signed integers
mut small i8 = -128
mut medium i32 = -100000
mut large i64 = -9223372036854775808

// Unsigned integers (cannot be negative)
mut byte_val u8 = 255
mut word u32 = 4294967295
mut big u64 = 18446744073709551615

Numeric Separators

Use underscores for readability in large numbers:

mut million int = 1_000_000
mut billion int = 7_800_000_000
mut hex int = 0xFF_FF
mut octal int = 0o777
mut binary int = 0b1111_0000

Floats

mut pi float = 3.14159
mut temperature float = -40.5
mut percentage float = 0.85

Strings

mut greeting string = "Hello, World!"
mut empty string = ""
mut multiword string = "This is a sentence."

Characters

mut letter char = 'A'
mut digit char = '5'
mut symbol char = '@'
mut newline char = '\n'

Booleans

mut isValid bool = true
mut hasError bool = false

Arrays

// Dynamic arrays
mut numbers [int] = {1, 2, 3, 4, 5}
mut names [string] = {"Alice", "Bob", "Charlie"}
mut empty [int] = {}

// Fixed-size arrays (must use const because their size is fixed at checktime)
const DAYS [string, 7] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
const PRIMES [int, 5] = {2, 3, 5, 7, 11}

Maps

mut scores map[string:int] = {"math": 95, "english": 88}

// Maps must be declared with mut
mut ages map[string:int] = {
    "Alice": 30,
    "Bob": 25
}

Empty Collections

Use {} for empty arrays and {:} for empty maps:

// Empty array
mut numbers [int] = {}

// Empty map - use {:} to distinguish from empty array
mut scores map[string:int] = {:}

The {:} syntax is necessary because {} alone is ambiguous — it could be an empty array or an empty map. The colon : signals that it’s a map (just like {"key": value} uses : for key-value pairs).

do main() {
    // Empty map, then add items
    mut users map[string:int] = {:}
    users["Alice"] = 25
    users["Bob"] = 30

    println("Users: ${users}")  // {"Alice": 25, "Bob": 30}
    println("Count: ${len(users)}")  // 2
}

Blank Identifier

The underscore _ serves as a blank identifier for discarding unwanted values in multi-value assignments.

Basic Usage

// Discard the second return value
mut result, _ = divide_with_remainder(10, 3)
println(result)  // 3

Multiple Discards

// Keep only the middle value
mut _, middle, _ = get_three_values()

Common Use Case: Ignoring Errors

do read_file(path string) -> (string, Error) {
    // Returns (content, Error)
    return "file content", nil
}

do main() {
    // When you're confident there's no error:
    mut content, _ = read_file("config.ez")
    println(content)
}

Rules

  • _ can appear multiple times in the same assignment
  • Values assigned to _ are evaluated but not stored
  • _ cannot be read from (it’s write-only)

Return Value Handling

All return values must be handled. If a function returns a value, you must either assign it to a variable or explicitly discard it with _. Silently ignoring a return value is not permitted.

Fallible Functions

Functions returning (T, Error) tuples are fallible functions. They require destructuring — single-variable assignment is a compile error:

// Correct: handle the error
mut content, err = io.read_file("data.txt")
if err != nil {
    panic(err)
}

// Correct: explicitly discard the error
mut content, _ = io.read_file("data.txt")

// Wrong: single-var assignment from a fallible function
mut content = io.read_file("data.txt")  // error: use destructuring for fallible functions

Variable Scope

Variables are scoped to their containing block:

do main() {
    mut x int = 10

    if x > 5 {
        mut y int = 20  // y only exists in this block
        println(x + y)
    }

    // println(y)  // Error! y is not in scope
}

Shadowing

Inner scopes can shadow outer variables:

do main() {
    mut x int = 10
    println(x)  // 10

    if true {
        mut x int = 20  // shadows outer x
        println(x)   // 20
    }

    println(x)  // 10 (original x)
}

Mutability

The const keyword indicates complete immutability:

const arr [int, 3] = {1, 2, 3}
arr[0] = 10  // ERROR: Cannot modify const
arr = {4, 5, 6}  // ERROR: Cannot reassign const

The mut keyword allows modification and ensures the value itself is mutable:

mut arr [int] = {1, 2, 3}
arr[0] = 10  // OK
arr = {4, 5, 6}  // OK

When assigning from a function return or other source, mut automatically makes the value mutable:

do get_data() -> [int] {
    return {1, 2, 3}
}

mut arr = get_data()  // arr is mutable
arrays.append(arr, 4)  // OK - can modify directly

const fixed = get_data()  // fixed is immutable
arrays.append(fixed, 4)   // ERROR - cannot modify const

Shared Data with ref()

When you need multiple variables to share the same data, use ref():

do main() {
    mut arr [int] = {1, 2, 3}

    // mut ref is mutable - can modify through the reference
    mut r1 = ref(arr)
    arrays.append(r1, 4)  // OK - modifies arr

    // const ref is read-only - can read but not modify
    const r2 = ref(arr)
    mut val = r2[0]      // OK - can read
    arrays.append(r2, 5)  // ERROR - cannot modify through const ref

    // const ref sees changes made to the original
    arrays.append(arr, 6)
    println(r2[4])        // Prints 6 - r2 sees the change
}

ref() Mutability Rules

Reference DeclarationSourceAllowed?
mut r = ref(x)mut xYes
const r = ref(x)mut xYes (read-only view)
const r = ref(x)const xYes
mut r = ref(x)const xNo — cannot get a mutable reference to a const source. Use copy(x) instead.

addr() Constraints

addr() requires a stable address: a variable, struct field, array index, or pointer dereference. Literals, call results, and arithmetic expressions are rejected:

mut x = 42
mut p = addr(x)           // OK: variable
mut p2 = addr(point.x)    // OK: struct field
mut p3 = addr(arr[0])     // OK: array index

// mut p4 = addr(42)       // Error: literal
// mut p5 = addr(foo())    // Error: call result
// mut p6 = addr(x + 1)   // Error: expression

Explicit Copying with copy()

Use copy() to create a deep copy of any value:

mut a = Person{name: "Bob", age: 25}
mut b = copy(a)  // b is an independent deep copy
b.age = 26
println(a.age)  // 25 - unchanged

Compound Assignment

mut count int = 10

count += 5   // count = 15
count -= 3   // count = 12
count *= 2   // count = 24
count /= 4   // count = 6
count %= 5   // count = 1

Increment and Decrement

mut i int = 0

i++  // i = 1
i++  // i = 2
i--  // i = 1

Type Conversion

Convert between types explicitly:

// String to number
mut str string = "42"
mut num int = int(str)
mut decimal float = float("3.14")

// Number to string
mut n int = 100
mut s string = string(n)

// Float to int (truncates)
mut pi float = 3.14159
mut whole int = int(pi)  // 3

// Int to float
mut x int = 42
mut y float = float(x)  // 42.0

See Also

  • Types — all available types and type conversions
  • Arrays — array declarations and const vs mut arrays
  • Maps — map declarations and the {:} empty map syntax
  • Functions — using variables as function parameters
  • Structs — struct variable declarations
  • Modules — importing and using modules