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
// Inferred from array literal
mut arr = {1, 2, 3} // Inferred: [int]
// 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.
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)
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
}
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
Example Program
do main() {
// Constants
const TAX_RATE float = 0.08
const STORE_NAME string = "EZ Mart"
// Variables
mut subtotal float = 0.0
mut itemCount int = 0
// Add items
mut prices [float] = {9.99, 24.99, 4.99, 14.99}
for_each price in prices {
subtotal += price
itemCount++
}
mut tax float = subtotal * TAX_RATE
mut total float = subtotal + tax
println("=== ${STORE_NAME} ===")
println("Items: ${itemCount}")
println("Subtotal: $${subtotal}")
println("Tax (${TAX_RATE * 100}%): $${tax}")
println("Total: $${total}")
}