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 = 3const BASE: i32 = 10const 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 - 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 = 100const 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 = 42val 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 = 0counter = counter + 1counter = 42 mut flag: bool = falseflag = 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
Syntax
Basic Syntax
val name: Type = value // Immutablemut 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 variableThis prevents accidental modification and makes code easier to reason about.
Explicit Mutability
Use mut when you need to modify a variable:
mut x: i32 = 10x = 20 // OK: x is mutablex = x + 5 // OK: can updateWhy Immutable by Default?
- Safety: Prevents accidental modification
- Clarity: Easy to see which variables change
- Reasoning: Easier to understand code flow
- Optimization: Compiler can optimize better
Variable Reassignment
Mutable Variable Assignment
mut x: i32 = 0x = 10 // Simple assignmentx = x + 5 // Update based on current valuex = x * 2 // Arithmetic updateType-Safe Reassignment
Reassigned values must match the variable's type:
mut x: i32 = 10x = 20 // OK: i32// x = 3.14 // Error: expected i32, found f64// x = true // Error: expected i32, found boolAssignment with Expressions
mut result: i32 = 0result = add(5, 3) // Function call resultresult = if x > 0 { 1 } else { 0 } // Conditional (Phase 1)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 destructuringval (x, y) = get_point() // Struct destructuring, binds each named field by its own nameval Point { x, y } = point // Array destructuring, binds positionallyval [a, b, c] = triple // Array destructuring with a trailing rest: `rest` is a fresh `[T; N - 2]` arrayval [first, second, ..rest] = numbers // A bare `..` ignores the remainder; `_` discards a single elementval [head, ..] = numbersval [_, 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, a struct-field store, or a by-value call
argument:
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 = 5val b: i32 = a // a is COPIEDval 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 movedval ok: bool = a == b // reading a here is fineConditional moves don't leak. A move that only happens inside one branch of an
if/while/for does not invalidate the binding on paths that never ran that
branch:
val msg: string = "hi"if ready { val r: u64 = consume(msg) // moves msg only on this path}val n: u64 = msg.len() // OK, the move above was conditionalMove tracking covers every non-
Copytype:string, the collections (Vec,HashMap,BTreeMap), and any struct without@derive(Copy). Scalars, arrays and tuples ofCopyelements, 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.
Type Annotations
Type annotations are optional when the type can be inferred from the initializer. Numeric literal inference is fully implemented:
val x: i32 = 42 // Explicit annotationval pi: f64 = 3.14159 // Explicit annotationval 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 i32Non-numeric types (bool, string, struct) require an explicit annotation or a typed initializer. Function parameters and return types always require explicit 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}Shadowing vs. Reassignment:
- Shadowing creates a new variable (can have different type in Phase 1)
- Reassignment modifies existing variable (must have same type)
// Shadowing (Phase 1 feature for type change)val x: i32 = 5val x: f64 = 3.14 // New variable, different type // Reassignment (Phase 1)mut x: i32 = 5x = 10 // Same variable, must be same typeInitialization
Required Initialization
Variables must be initialized when declared (Phase 1):
val x: i32 = 42 // OK: initializedmut y: i32 = 0 // OK: initialized// val z: i32 // Error: missing initializerInitialization with Expressions
val x: i32 = 10 + 20 // Arithmeticval y: i32 = add(5, 3) // Function callval z: i32 = if true { 1 } else { 0 } // Conditional (Phase 1)Uninitialized Variables (Phase 1+)
Future phases may support uninitialized variables with explicit type:
// Not yet implementedval x: i32 // Declared but not initialized// Use of x here would be an errorx = 42 // Initialize before useCommon 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 possibleval x: i32 = 10val y: i32 = x * 2 // Only use mut when necessarymut counter: i32 = 0counter = counter + 12. Declare Variables Close to Use
// Good: declare near usagefunc calculate() -> i32 { val x: i32 = get_x() val y: i32 = get_y() return x + y} // Bad: declare far from usagefunc 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 namesval user_count: i32 = 42val is_valid: bool = trueval max_retries: i32 = 3 // Bad: unclear namesval n: i32 = 42val f: bool = trueval x: i32 = 34. Initialize with Meaningful Values
// Good: meaningful initializationmut error_count: i32 = 0mut is_complete: bool = false // Avoid: magic numbers without contextmut x: i32 = 42 // What does 42 mean?5. Group Related Variables
// Good: related variables togetherval width: i32 = 10val height: i32 = 20val area: i32 = width * heightCommon Mistakes
Assigning to Immutable Variable
val x: i32 = 10// x = 20 // Error: cannot assign to immutable variable // Fix: use mutmut y: i32 = 10y = 20 // OKType Mismatch in Reassignment
mut x: i32 = 10// x = 3.14 // Error: expected i32, found f64 // Fix: ensure types matchmut y: f64 = 10.0y = 3.14 // OKUsing Uninitialized Variable
// Error: uninitialized variables not allowed (Phase 1)// val x: i32// return x // Fix: initializeval x: i32 = 0return xShadowing Instead of Reassignment
mut x: i32 = 10val x: i32 = 20 // Creates new variable (shadowing), doesn't reassign // If you meant reassignment:mut y: i32 = 10y = 20 // Reassigns existing variableReferences
- 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