Variables
Variables store values that can be used and manipulated in your programs.
Compile-Time Constants
Use const to declare compile-time constants. Constants must have an explicit type
annotation and a constant expression as their value.
const MAX_RETRIES: i32 = 3
const BASE: i32 = 10
const DOUBLED: i32 = BASE * 2 // arithmetic on other consts is allowedKey points:
- Declared with
constkeyword - Type annotation is required
- RHS must be a constant expression: literals, arithmetic/unary/cast on literals,
or identifiers that refer to previously declared
constnames - Function calls and runtime values are not allowed as
constinitializers - An integer constant must have a value of its type: an initializer that overflows it or
divides by zero is a compile error on the initializer (
ConstHasNoValue) - No ownership or lifetime, consts do not participate in the borrow checker
- Module-level consts are visible to all functions regardless of source order
- Function-body consts are scoped to the enclosing function
Module-Level Constants
const LEARNING_RATE_SCALE: i32 = 100
const MAX_EPOCHS: i32 = 50
func train() -> i32 {
return MAX_EPOCHS * LEARNING_RATE_SCALE // 5000
}Function-Body Constants
func compute_threshold(base: i32) -> i32 {
const FACTOR: i32 = 4
return base * FACTOR
}Constant Expressions
Valid RHS forms for const:
| Form | Example |
|---|---|
| Integer literal | 42, 0xFF, 0b1010 |
| Float literal | 3.14, 1.0e-3 |
| Bool literal | true, false |
| String literal | "hello" |
| Unary op on const expr | -MAX, !FLAG |
| Binary op on const exprs | A * 2, A + B |
| Cast of const expr | BASE as f32 |
Another const name | DOUBLED (if DOUBLED is a const) |
Constants vs Variables
const | val | mut | |
|---|---|---|---|
| Keyword | const | val | mut |
| Mutable | no | no | yes |
| Scope | module or function | function/block | function/block |
| RHS | constant expr only | any expr | any expr |
| Ownership | none | yes | yes |
| LLVM emission | global constant | stack alloca | stack alloca |
Variable Declaration
Immutable Variables
Use val to declare immutable variables:
val x: i32 = 42
val name: string = "Neuro"
val pi: f64 = 3.14159Key points:
- Declared with
valkeyword - Cannot be reassigned after initialization
- Type annotation optional when type can be inferred from a numeric literal
- Must be initialized at declaration
Mutable Variables
Use mut to declare mutable variables:
mut counter: i32 = 0
counter = counter + 1
counter = 42
mut flag: bool = false
flag = trueKey points:
- Declared with
mutkeyword - Can be reassigned after initialization
- Reassigned value must match original type
- Type annotation optional when type can be inferred from a numeric literal
- A binding is one kind of assignment target. Fields, elements and tensor coordinates are targets too: see places
Syntax
Basic Syntax
val name: Type = value // Immutable
mut name: Type = value // MutableComponents:
valormutkeyword- Variable name (identifier)
- Type annotation (
: Type) - Initializer expression (
= value)
Examples
func variables() -> i32 {
// Immutable variables
val x: i32 = 10
val y: i32 = 20
val sum: i32 = x + y
// Mutable variables
mut counter: i32 = 0
counter = 5
counter = counter + 1
return counter
}Mutability
Immutable by Default
Variables are immutable by default in Neuro:
val x: i32 = 10
// x = 20 // Error: cannot assign to immutable variableA reader can tell from the declaration alone which bindings change.
Explicit Mutability
Use mut when you need to modify a variable:
mut x: i32 = 10
x = 20 // OK: x is mutable
x = x + 5 // OK: can updateVariable Reassignment
Mutable Variable Assignment
mut x: i32 = 0
x = 10 // Simple assignment
x = x + 5 // Update based on current value
x = x * 2 // Arithmetic updateType-Safe Reassignment
Reassigned values must match the variable's type:
mut x: i32 = 10
x = 20 // OK: i32
// x = 3.14 // Error: expected i32, found f64
// x = true // Error: expected i32, found boolAssignment with Expressions
mut result: i32 = 0
result = add(5, 3) // Function call result
result = if x > 0 { 1 } else { 0 } // Conditional
result = x * 2 + y // Complex expressionMultiple Reassignments
func counter() -> i32 {
mut count: i32 = 0
count = count + 1 // count = 1
count = count + 1 // count = 2
count = count + 1 // count = 3
return count
}Destructuring
A val or mut binding may take a pattern instead of a single name:
// Tuple destructuring
val (x, y) = get_point()
// Struct destructuring, binds each named field by its own name
val Point { x, y } = point
// Array destructuring, binds positionally
val [a, b, c] = triple
// Array destructuring with a trailing rest: `rest` is a fresh `[T; N - 2]` array
val [first, second, ..rest] = numbers
// A bare `..` ignores the remainder; `_` discards a single element
val [head, ..] = numbers
val [_, mid, _] = tripleA rest-less array pattern must bind every element; val [a, b] = arr where arr
has more than two elements is a compile error. Add a ..rest (or ..) to capture
the remainder. mut patterns make every binding mutable. Patterns nest, so an
element may itself be a tuple, struct, or array pattern.
Pattern Matching in Declarations
val Some(value) = optional else {
return 0
}The else branch runs when the value does not match the pattern; see
val-else for the full form,
including the optional |binding|.
Move Semantics (Ownership)
Every binding owns its value. For non-Copy types (string, the collections,
structs without @derive(Copy)), placing the value somewhere new moves ownership out
of the source binding, and the source becomes invalid. Reading a moved binding is a
compile error:
val s1: string = "Hello"
val s2: string = s1 // s1 is MOVED into s2
// val n: u64 = s1.len() // ERROR: use of moved value 's1'
val n: u64 = s2.len() // OK, s2 owns the value nowA move happens whenever a non-Copy value is handed to a new owner: a val/mut
initializer, an assignment, a return or a function's last expression, a struct-field
store, a by-value call argument, the left side of ??, the operand of ?, or a match
whose arm binds an owning payload by value. An if, match or block in one of those
positions moves whatever its arms yield. A value reached through a borrow (a &self
receiver's field, say) owns nothing to hand over, so moving it is an error:
func consume(s: string) -> u64 { s.len() }
val greeting: string = "Hi"
val len: u64 = consume(greeting) // greeting is moved into consume()
// val again = greeting.len() // ERROR: greeting was movedCopy scalars are never moved. i8..u64, f32/f64, and bool are
duplicated on assignment, so the source stays valid:
val a: i32 = 5
val b: i32 = a // a is COPIED
val c: i32 = a + b // both a and b still valid.clone() is the opt-out. When you need an independent copy of a non-Copy
value, clone it, the receiver is borrowed, not moved:
val a: string = "hello"
val b: string = a.clone() // a is NOT moved
val ok: bool = a == b // reading a here is fineA move in a branch counts after the branch. A move inside one arm of an if or
match does not affect the other arms, but past the if or match the binding may
already be gone, so reading it there is a use of a moved value. An arm that leaves
(return, break, continue, or a panic) is the exception, since no path past the
branch runs it:
val msg: string = "hi"
if ready {
val r: u64 = consume(msg) // moves msg on this path
}
// val n: u64 = msg.len() // COMPILE ERROR: use of moved value 'msg'
val other: string = "hey"
if !ready {
return consume(other) // moves other, but this path leaves
}
val m: u64 = other.len() // OK: every path that reaches here kept `other`Move tracking covers every non-
Copytype:string, the collections (Vec,HashMap,BTreeMap), any struct without@derive(Copy), and any array, tuple, enum, newtype or generic instance holding one of those. Scalars, aggregates built only out ofCopyparts, references, and@derive(Copy)structs are freely duplicable. A type with aDropimpl cannot beCopy.mutbindings that were moved can be revived by reassigning them a fresh value; amutbinding that still holds one has that value destroyed by the assignment that replaces it.
Type Annotations
Type annotations are optional when the initializer determines the type:
val x: i32 = 42 // Explicit annotation
val pi: f64 = 3.14159 // Explicit annotation
val flag: bool = true // Explicit annotation
val n = 100 // Inferred i32 (default for integer literals)
val ratio = 3.14 // Inferred f64 (default for float literals)
mut count = 0 // Inferred i32A bool, string or struct initializer gives the binding its own type (val p = Point { x: 1, y: 2 }
is a Point). An integer literal with nothing else to go on is i32, a float literal f64.
Function parameters and return types always need annotations.
Variable Scope
Function Scope
Variables are scoped to the function where they're declared:
func example() -> i32 {
val x: i32 = 10
return x // x is in scope
}
func other() -> i32 {
// return x // Error: x not in scope
return 0
}Block Scope
Variables are scoped to their enclosing block:
func blocks() -> i32 {
val x: i32 = 1
if true {
val y: i32 = 2 // y only exists in this block
// x and y both in scope
}
// Only x in scope here
// return y // Error: y not in scope
return x
}Shadowing
Inner scopes can shadow outer scope variables:
func shadowing() -> i32 {
val x: i32 = 1
if true {
val x: i32 = 2 // Shadows outer x
// Inner x is 2
}
// Outer x is still 1
return x // Returns 1
}The inner binding may have a different type. A later val or mut in the same block may
reuse a name too, and change its type:
func reparse() -> i32 {
val n = "42"
val n = n.len() as i32 // the initializer still reads the first `n`
return n // 2
}A shadowed binding is not dropped when it is shadowed. It stays owned, and alive under any
borrow taken of it, until its block ends. Its initializer runs before the new name exists, so
val x = x moves the outer x into the new one.
Reassignment is the other way to change a value. It keeps the same binding, so the new value must have the binding's type.
Initialization
Required Initialization
Variables must be initialized when declared:
val x: i32 = 42 // OK: initialized
mut y: i32 = 0 // OK: initialized
// val z: i32 // Error: missing initializerInitialization with Expressions
val x: i32 = 10 + 20 // Arithmetic
val y: i32 = add(5, 3) // Function call
val z: i32 = if true { 1 } else { 0 } // ConditionalCommon Patterns
Accumulators
func sum_to_n(n: i32) -> i32 {
mut sum: i32 = 0
for i in 1..=n {
sum = sum + i
}
return sum
}State Machines
func state_machine(input: i32) -> i32 {
mut state: i32 = 0
if input == 1 {
state = 1
} else if input == 2 {
state = 2
}
return state
}Conditional Initialization
func conditional_init(flag: bool) -> i32 {
val x: i32 = if flag {
42
} else {
0
}
return x
}Examples
Counter
func count_up() -> i32 {
mut counter: i32 = 0
counter = counter + 1 // 1
counter = counter + 1 // 2
counter = counter + 1 // 3
return counter
}Swap (Manual)
func swap_manual(a: i32, b: i32) -> i32 {
mut temp: i32 = a
mut first: i32 = b
mut second: i32 = temp
return first * 100 + second
}Running Total
func running_total(a: i32, b: i32, c: i32) -> i32 {
mut total: i32 = 0
total = total + a
total = total + b
total = total + c
return total
}Flag Toggle
func toggle_flag(initial: bool) -> i32 {
mut flag: bool = initial
flag = !flag
if flag {
return 1
} else {
return 0
}
}Best Practices
1. Prefer Immutable Variables
// Good: immutable when possible
val x: i32 = 10
val y: i32 = x * 2
// Only use mut when necessary
mut counter: i32 = 0
counter = counter + 12. Declare Variables Close to Use
// Good: declare near usage
func calculate() -> i32 {
val x: i32 = get_x()
val y: i32 = get_y()
return x + y
}
// Bad: declare far from usage
func calculate_bad() -> i32 {
val x: i32 = get_x()
// ... lots of code ...
val y: i32 = get_y()
return x + y
}3. Use Descriptive Names
// Good: clear names
val user_count: i32 = 42
val is_valid: bool = true
val max_retries: i32 = 3
// Bad: unclear names
val n: i32 = 42
val f: bool = true
val x: i32 = 34. Initialize with Meaningful Values
// Good: meaningful initialization
mut error_count: i32 = 0
mut is_complete: bool = false
// Avoid: magic numbers without context
mut x: i32 = 42 // What does 42 mean?5. Group Related Variables
// Good: related variables together
val width: i32 = 10
val height: i32 = 20
val area: i32 = width * heightCommon Mistakes
Assigning to Immutable Variable
val x: i32 = 10
// x = 20 // Error: cannot assign to immutable variable
// Fix: use mut
mut y: i32 = 10
y = 20 // OKType Mismatch in Reassignment
mut x: i32 = 10
// x = 3.14 // Error: expected i32, found f64
// Fix: ensure types match
mut y: f64 = 10.0
y = 3.14 // OKUsing Uninitialized Variable
// Error: uninitialized variables are not allowed
// val x: i32
// return x
// Fix: initialize
val x: i32 = 0
return xShadowing Instead of Reassignment
A val inside a block declares a new binding, even when an outer mut has the same name:
mut total: i32 = 0
if ready {
val total: i32 = 10 // a new binding that ends at this block's brace
}
return total // still 0
// If you meant reassignment, drop the `val`:
// total = 10References
- Types - Type system and type checking
- Functions - Local variables in functions
- Expressions - Variable initialization expressions
- Operators - Assignment operator
See Also
- Rust Book: Variables and Mutability
- Immutability