Skip to content

Variables and Constants

Variables are mutable by default. The mut keyword is accepted but optional:

do compute() -> (i64, Error) { return 1, nil }
do main() {
mut a i64 = 42 // mutable
b i64 = 42 // mutable (default, `mut` is optional)
name = "hello" // mutable, type inferred
result, err = compute() // mutable, multi-return inferred
println(a)
println(b)
println(name)
println(result)
}

Variables declared this way:

  • Can be reassigned after declaration
  • Are scoped to their containing block

A declaration with a type annotation but no value is allowed and zero-initializes the variable (0, 0.0, "", false, and so on). Because a dropped = value is easy to miss, the compiler emits W1004 in that case:

do main() {
count i64 // warning[W1004]: 'count' declared with no value — defaults to 0
total i64 = 0 // no warning; the zero value is explicit
println(count)
println(total)
}

Like all warnings, W1004 can be suppressed with -q W1004 or -q all. const declarations must always have a value (E2011).

Constants are declared using the const keyword:

const Point struct {
x i64
y i64
}
const PI f64 = 3.14159
const MAX_SIZE i64 = 100
const origin = Point{x: 0, y: 0}
do main() {
println(PI)
println(MAX_SIZE)
println(origin)
}

At file scope, const declarations of primitive types (integer and float types, string, bool, char) and arrays require explicit type annotations. Struct literals and enum values can still be inferred. Inside function bodies, type inference works for all const declarations.

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

do main() {
mut arr [i64] = {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. There is no need to use copy() to obtain a mutable version:

import @arrays
do get_data() -> [i64] {
return {1, 2, 3}
}
do main() {
mut arr = get_data() // arr is mutable
arrays.append(arr, 4) // OK - can modify directly
const fixed [i64, 3] = {1, 2, 3} // fixed is immutable
// arrays.append(fixed, 4) // ERROR - cannot modify const
println(arr)
println(fixed)
}

Variables and constants are block-scoped. A block is delimited by braces {}.

Inner scopes may declare variables that shadow outer scope variables:

// x is 10 here
do main() {
mut x i64 = 10
if true {
mut x i64 = 20 // Shadows outer x
// x is 20 here
}
}

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 the blank identifier _. Silently ignoring a return value is not permitted.

Some functions return a (T, Error) tuple; these are fallible functions. They require destructuring. Assigning the result to a single variable is a compile-time error (E3089); the compiler forces you to make the choice — inspect the error or explicitly discard it — visible in the code:

import @io
do main() {
// Correct: handle the error
mut content, err = io.read_file("data.txt")
if err != nil {
println(err)
}
// Correct: explicitly discard the error when you know it won't fail
mut other, _ = io.read_file("data.txt")
// Wrong: single-var assignment from a fallible function
// mut third = io.read_file("data.txt") // error[E3089]: use destructuring for fallible functions
println(content)
println(other)
}

The blank identifier _ discards a return value you don’t need:

import @json
do get_pair() -> (i64, i64) { return 1, 2 }
do get_triple() -> (i64, i64, i64) { return 1, 2, 3 }
do main() {
// Discard the second return value
mut value, _ = get_pair()
// Discard multiple values
const _, middle, _ = get_triple()
// Discard error intentionally
mut text string = "1"
mut data, _ = json.decode(text)
println(value)
println(middle)
}

The blank identifier:

  • Cannot be read from (it discards the value)
  • Can be used multiple times in the same assignment
  • Works with both mut and const declarations
  • Makes it explicit that you are intentionally ignoring a value