Mutability¶
Values in Verse are immutable by default: once created, a value never changes. Mutation is available but opt-in and visible. You declare a variable with var, change it with set, and the effect system records both in the signature of any function that does so.
The distinction reaches further than whether a value can change. It also determines how data is shared between functions, which is why structs and classes behave differently when you mutate them.
The Pure Foundation¶
In Verse's pure fragment, computation happens without side effects. Values are created but never modified. Functions transform inputs into outputs without changing anything along the way. This is not a limitation — it is a powerful foundation that makes code predictable and composable.
point := struct<computes>:
X:float = 0.0
Y:float = 0.0
# These values are eternal - Origin will always be (0, 0)
Origin := point{}
UnitX := point{X := 1.0}
UnitY := point{Y := 1.0}
Distance(P1:point, P2:point)<reads>:float =
DX := P2.X - P1.X
DY := P2.Y - P1.Y
Sqrt(DX * DX + DY * DY)
Distance(Origin, UnitX) = 1.0
In this pure world, equality means structural equality — two values are equal if they have the same shape and content. For primitive types and structs, this happens automatically. For classes, which have identity beyond their content, equality requires more careful consideration.
# A recursive class needs an explicit structural comparison
linked_list := class:
Value:int = 0
Next:?linked_list = false
# Custom equality check for structural comparison
Equals(Other:linked_list)<computes><decides>:void =
Self.Value = Other.Value
# Both have no next, or both have next and those are equal
if (Self.Next?):
Tmp := Self.Next?
OtherNext := Other.Next?
Tmp.Equals[OtherNext]
else:
not Other.Next?
List1 := linked_list{Value := 1, Next := option{linked_list{Value := 2}}}
List2 := linked_list{Value := 1, Next := option{linked_list{Value := 2}}}
List1.Equals[List2] # Same shape and content
not List1.Equals[linked_list{Value := 1}] # A shorter list is not equal
A function marked <computes> always produces the same output for the same input, with no hidden dependencies. That is what makes it safe to cache, reorder, or run in parallel.
Introducing Mutation¶
Mutation enters through two keywords: var and set. The var annotation declares that a variable can be reassigned. The set keyword performs that reassignment. Together, they provide controlled mutation with clear visibility.
Score:int = 100 # Immutable: cannot be reassigned
var Health:float = 100.0 # Mutable: the type annotation is required
set Health = 75.0
Health = 75.0
Every use of var and set has implications for effects. Reading from a var variable requires the <reads> effect. Using set requires both <reads> and <writes> effects. This is not bureaucracy — it is transparency. The effects make mutation visible in function signatures, so callers know when functions might observe or modify state.
Requirements for var Declarations¶
Mutable variable declarations have strict requirements that prevent common errors:
var Health:float = 100.0 # A var needs both an explicit type and a value
# var Health := 100.0 # ERROR: := cannot be used with var
# var Health:float # ERROR: no initial value in a local scope
# var Health # ERROR: neither a type nor a value
# var (var Health):int = 0 # ERROR: var cannot be nested inside var
Health = 100.0
The type inference syntax := cannot be used with var. You must explicitly declare the type.
In local scopes (functions, control flow blocks), every var declaration requires an initial value. However, when declaring mutable fields in classes or interfaces, the initial value can be omitted and provided during instantiation (see the Classes and Interfaces chapter for details).
A declaration that supplies neither a type nor a value is not a declaration at all, and the var keyword cannot be nested within itself.
var Declarations as Expressions¶
Variable declarations with var can be used as expressions, evaluating to their initial value:
X := (var Y:int = 42) # X is 42, and Y is declared and mutable
X = 42
set Y = 43
Y = 43
# set (var Z:int = 0) = 1 # ERROR: cannot use set on a value
A var declaration cannot be the target of set. Since var declarations return their initial value as an expression result, you cannot use set on them - set requires a mutable variable, not a value.
set with Block Expressions¶
The set statement can use block expressions, which allows complex computations and side effects:
var X:int = 0
var Y:int = 1
set X = block:
set Y = X # Side effect: Y becomes 0
2 # Block result: X becomes 2
X = 2 and Y = 0
This pattern is useful when the new value requires intermediate computations or when you need multiple side effects during assignment.
Verse evaluates the left-hand side of set before the block executes, and assigns the block's return value. This can lead to confusing behavior in certain cases:
# Confusing: Setting the same variable inside the block
var X:int = 0
set X = block:
set X = 5 # X temporarily becomes 5
2 # But X will be set to 2 (the block result)
X = 2 # The inner set was overwritten!
# Confusing: Modifying index variables used in array access
var Xs:[]int = array{10, 20, 30}
var Index:int = 1
set Xs[Index] = block:
set Index = 2 # Index changes, but does not affect which element is set
99
Xs[1] = 99 # Element at original Index (1) was modified, not Xs[2]
Index = 2 # Index is now 2, but too late to affect the assignment
To avoid confusion, it is best to avoid modifying the target variable or any variables used in the target expression inside the block.
Scope and Redeclaration Restrictions¶
Verse does not allow variable shadowing. Once an identifier is declared, you cannot redeclare it with := anywhere in the same scope or any nested scope. This is more restrictive than many languages that allow inner scopes to shadow outer scope variables.
var Count:int = 0
Step:int = 1
# Count := 1 # ERROR: Count exists; write `set Count = 1` instead
# Count := Step # ERROR: the same, though it looks like an assignment
if (SomeCondition?):
set Count += Step
# Count := 2 # ERROR: no shadowing, not even in a nested scope
# var Count:int = 2 # ERROR: nor with a nested var declaration
Count = 0
Use set Count = Step to assign to an existing mutable variable. If you need multiple identifiers with similar purposes, use descriptive names (e.g., InitialHealth, CurrentHealth) or use qualified names to create separate scopes (see the Modules and Paths chapter for details on qualified names and disambiguation).
Deep vs Shallow Mutability¶
Verse's approach to mutability differs significantly between structs and classes, reflecting their different roles in the language.
Struct Mutability: Deep and Structural¶
When you declare a struct variable with var, you are declaring the entire structure as mutable — the variable itself and all its nested fields, recursively. This deep mutability means you can modify any part of the structure tree.
player_stats := struct<computes>:
Level:int = 1
Position:point = point{}
Inventory:[]string = array{}
# Immutable struct variable - nothing can change
Stats1:player_stats = player_stats{}
# set Stats1.Level = 2 # ERROR: cannot modify an immutable struct
# Mutable struct variable - everything can change, however deeply nested
var Stats2:player_stats = player_stats{}
set Stats2.Level = 2
set Stats2.Position.X = 100.0
set Stats2.Inventory += array{"Sword"}
Stats2.Level = 2
Stats2.Position.X = 100.0
Stats2.Inventory = array{"Sword"}
When you assign one struct variable to another, Verse performs a deep copy. The two variables become independent, each with their own copy of the data. Changes to one do not affect the other.
var Original:player_stats = player_stats{Level := 5}
var Copy:player_stats = Original
set Copy.Level = 10
Original.Level = 5 # unchanged, they are independent copies
This deep-copy semantics extends to all value types: structs, arrays, maps, and tuples. When you pass a struct to a function, the function receives its own copy. When you store a struct in a container, the container holds a copy. This prevents aliasing and makes reasoning about struct mutations local and predictable.
# Arrays also have value semantics - assignments create copies
var Original:[]int = array{1, 2, 3}
var Copy:[]int = Original
set Copy[0] = 999
Original[0] = 1 # unchanged, they are independent copies
Copy[0] = 999
Class Mutability: Reference Semantics¶
Classes behave differently. They have reference semantics — when you assign a class instance, you are sharing a reference to the same object, not creating a copy. The var annotation on a class variable only affects whether that variable can be reassigned to reference a different object. It does not affect the mutability of the object's fields.
game_character := class:
Name:string = "Hero"
var Health:float = 100.0 # This field is always mutable
MaxHealth:float = 100.0 # This field is always immutable
# Immutable variable, but mutable fields can still change
Player1:game_character = game_character{}
# set Player1 = game_character{} # ERROR: cannot reassign a non-var variable
set Player1.Health = 50.0
# Mutable variable allows reassignment, but not more field mutation
var Player2:game_character = Player1 # Same object
set Player2 = game_character{Name := "Villain"}
set Player2.Health = 75.0
# set Player2.MaxHealth = 200.0 # ERROR: MaxHealth is still not var
Player2.Name = "Villain"
Player2.Health = 75.0
Player1.Health = 50.0 # Untouched: Player2 now refers to another object
The key insight: for classes, the class definition determines field mutability at definition time, not at variable declaration time. A var field is always mutable, regardless of how you access it. A non-var field is always immutable, even if accessed through a var variable.
Collection Mutability: Arrays and Maps¶
Arrays and maps follow struct semantics—they are values, not references. When you copy a collection, you get an independent copy. Mutations to one copy do not affect the other.
Basic Array Mutation¶
Mutable arrays allow element replacement:
var Nums:[]int = array{0, 1}
set Nums[0] = 42
Nums[0] = 42
Nums[1] = 1 # Unchanged
You cannot add elements beyond the array's current length:
var A:[]int = array{0}
not (set A[1] = 1) # Fails - index out of bounds
# Must use concatenation: set A = A + array{1}
Basic Map Mutation¶
Mutable maps allow both updating existing keys and adding new keys:
var Scores:[string]int = map{"Alice" => 1}
set Scores["Alice"] = 42 # Updates the existing key
set Scores["Bob"] = 100 # Adds a new key
Scores = map{"Alice" => 42, "Bob" => 100}
Looking up a non-existent key does not add it:
M:[int]int = map{}
not (M[0] = 0) # Key does not exist, comparison fails
M = map{} # M is still empty, the lookup did not add the key
Deleting Keys from Maps¶
Verse does not have a direct "delete" or "remove" operation for maps. To remove keys, create a new map that excludes the unwanted keys by iterating over the original map:
var Scores:[string]int = map{"Alice" => 100, "Bob" => 85, "Charlie" => 92}
# Remove "Bob" by creating a new map without that key
var NewScores:[string]int = map{}
for (Name->Score:Scores):
if (Name <> "Bob"):
set NewScores[Name] = Score
set Scores = NewScores
# Scores now only contains Alice and Charlie
Scores["Alice"] = 100
Scores["Charlie"] = 92
This pattern can be wrapped in a helper function for reusability. See the Control Flow chapter for more details on for loops.
Nested Collection Mutation¶
Collections can be nested, and set works through multiple levels:
# A map of arrays
var Data:[int][]int = map{}
set Data[666] = array{42} # Sets a whole array as the value of a key
set Data[666][0] = 1234 # Reaches into the nested array
Data = map{666 => array{1234}}
# An array of maps
var Grid:[][int]int = array{map{}}
set Grid[0][1234] = 4321 # Adds a key to the nested map
Grid[0] = map{1234 => 4321}
# An array of arrays
var Matrix:[][]int = array{array{1234}}
set Matrix[0][0] = 42
Matrix = array{array{42}}
All nested levels should exist to use set, if any of the higher levels do not exist, the entire set will fail.
var Grid:[string][]int = map{"apples" => array{1, 2, 3, 4}}
set Grid["bananas"] = array{} # OK - no nesting, just adds a new key
set Grid["apples"][2] = 7 # OK - changes nested element 3 to 7
not (set Grid["oranges"][0] = 10) # Fails - the "oranges" key does not exist
Grid["apples"] = array{1, 2, 7, 4}
Value Semantics for Collections¶
Extracting a value from a mutable collection creates an independent copy:
var X:[][int]int = array{map{42 => 1122}}
# Y gets a copy of the map, not a reference
Y := X[0]
set X[0][0] = 111
X[0] = map{42 => 1122, 0 => 111}
Y = map{42 => 1122} # Unchanged
set X[0] = map{42 => 4242} # Replacing the element does not affect Y either
Y = map{42 => 1122}
This is different from class reference semantics—collections copy, classes share.
Collections with Mutable Values¶
When collections contain classes or structs with mutable fields, you can mutate through the collection:
C := my_class{}
set C.X[0] = 42 # Through a var field that is itself a collection
C.X[0] = 42
var M:[int]my_class = map{0 => C}
set M[0].X[0] = 99 # Through a collection, into a var field
C.X[0] = 99 # The map holds the same object, not a copy of it
A map of a value type behaves differently: constructed from a var, it does not track changes to the source variable:
var I:int = 42
M:[int]int = map{0 => I}
M[0] = 42
set I = 0
M[0] = 42 # Still 42! Map has a copy of the value
Arrays of Structs: Independent Copies¶
When you store structs in an array, each element is an independent copy:
S := my_struct{I := 88}
var A:[]my_struct = array{S, S} # All three start out at 88
# Mutating one does not affect the others
set A[0].I = 99
A[0].I = 99 # Changed
A[1].I = 88 # Unchanged
S.I = 88 # Unchanged
Arrays of Classes: Shared References¶
Arrays of classes behave very differently—all references to the same object share mutations:
C := my_class{}
var A:[]my_class = array{C, C} # Both elements reference the same object
# Mutating through one affects every reference
set A[0].I = 30
A[1].I = 30 # Changed!
C.I = 30 # Changed here too
# Replacing an element breaks the sharing for that element
set A[1] = my_class{}
A[0].I = 30 # Still the original object
A[1].I = 20 # A new object with the default value
This is a critical distinction: structs in collections are copies, classes in collections are shared references.
Compound Assignment Operators¶
Verse supports compound assignment operators that combine arithmetic with mutation:
var S:my_struct = my_struct{}
set S.A += 10
S.A = 20
set S.A -= 3
S.A = 17
set S.A *= 4
S.A = 68
Available compound operators:
set +=- Addition assignment (int, float, string, array)set -=- Subtraction assignment (int, float)set *=- Multiplication assignment (int, float)set /=- Division assignment (float only)
Do note that set /= does not work with integers because integer division is failable.
Compound assignments work anywhere regular assignment does:
var Score:int = 100
set Score += 50
set Score *= 2
Score = 300
var Data:[]int = array{1, 2, 3}
set Data += array{4, 5} # Array concatenation
Data = array{1, 2, 3, 4, 5}
var Nums:[][]int = array{array{1}}
set Nums[0][0] *= 42
Nums[0][0] = 42
Array concatenation with += works on struct fields, nested fields, and collection values, just like regular set does:
my_struct := struct<computes>:
X:[]int = array{}
my_nested := struct<computes>:
Inner:my_struct = my_struct{}
# Append to a struct field
var S:my_struct = my_struct{}
set S.X += array{1, 2, 3}
S.X = array{1, 2, 3}
# Append to a nested struct field
var N:my_nested = my_nested{}
set N.Inner.X += array{10, 20}
N.Inner.X = array{10, 20}
# Append to a map value
var M:[int][]int = map{}
set M[42] = array{}
set M[42] += array{1}
set M[42] += array{2}
M[42] = array{1, 2}
Mutating Parametric Containers¶
Compound assignment works when the element type is a class type parameter, so generic containers can be written without special-casing:
queue(t:type) := class:
var Contents<private>:[]t = ()
Push<public>(Arg:t)<transacts>:void =
set Contents += array. Arg # polymorphic +=
Pop<public>()<transacts><decides>:t =
Result := Contents[Contents.Length - 1]
set Contents = for (Key->Val:Contents; Key <> Contents.Length - 1). Val
Result
Q := queue(int){}
Q.Push(1)
Q.Push(2)
Q.Pop[] = 2
Q.Pop[] = 1
not Q.Pop[] # the queue is empty again
Assignment into an element of a parametric container also works: set X.Contents[0] = ....
Tuple Mutability: Replacement Only¶
Tuples can be replaced entirely but individual elements cannot be mutated:
var T:tuple(int, int) = (10, 20)
# The whole tuple can be replaced
set T = (30, 40)
T(0) = 30
T(1) = 40
# set T(0) = 70 # ERROR: tuple elements cannot be mutated
This restriction applies even when the tuple is mutable. You must replace the entire tuple to change its contents.
Map Ordering and Mutation¶
Maps preserve insertion order, and this order is maintained through mutations.
New Keys Append to End¶
var M:[int]int = map{2 => 2}
set M[1] = 1 # Appends to end
set M[0] = 0 # Appends to end
# Iteration follows insertion order, not key order
Keys := for (Key->Value : M). Key
Keys = array{2, 1, 0}
Updating Existing Keys Preserves Position¶
Map equality considers both keys/values and order, which is what makes the position of an updated key observable:
var M:[string]int = map{"a" => 3, "b" => 1, "c" => 2}
# Mutating a value keeps the key in place
set M["a"] = 0
M = map{"a" => 0, "b" => 1, "c" => 2} # Same keys, values and order: equal
# The same pairs in another order are a different map
M <> map{"b" => 1, "c" => 2, "a" => 0}
Critical Mutability Restrictions¶
Verse imposes several important restrictions on where and how mutation can occur. These are not arbitrary—they prevent unsound behaviors and maintain type safety.
Cannot Mutate Immutable Class Fields¶
Classes might contain unique pointers or other resources that cannot be safely cloned. Therefore, you cannot mutate immutable fields of a class instance:
classX := class:
X:int = 20 # No var, so immutable for the life of the object
var C:classX = classX{}
set C = classX{} # OK: the variable can be pointed at another instance
# set C.X = 30 # ERROR: the field itself still cannot be mutated
C.X = 20
This restriction applies even when the class instance itself is mutable. Only var fields of classes can be mutated.
Only Structs Allow Field Mutation¶
Only structs marked <computes> (pure structs) allow field mutation through a variable:
my_mutable_struct := struct<computes>{M:int = 0, J:float = 3.0}
# my_mutable_struct := struct{M:int = 0} # ERROR on the set below: not <computes>
var S:my_mutable_struct = my_mutable_struct{}
Old := S # Makes a copy of the struct
set S.M = 1 # Makes a copy of the struct, but updates `M` in the process
S.M = 1
not (Old = S) # Structs do not pass as references
When a new struct is constructed, Verse assigns it the updated value and copies other fields. If there are other places referencing the old struct, they will not have the updated values (unlike classes).
This restriction ensures that only predictable, effect-free structs can be mutated.
Cannot Mutate Through Immutable Class Fields¶
When mutating nested structures, you cannot mutate through an immutable field of a class (a field not declared with var):
inner := struct<computes>{Value:int = 0}
locked := class{Data:inner = inner{}} # Immutable field
unlocked := class{var Data:inner = inner{}} # Mutable field
box := struct<computes>{L:locked, U:unlocked}
var B:box = box{L := locked{}, U := unlocked{}}
set B.U.Data.Value = 10 # OK: the path runs through a var field
# set B.L.Data.Value = 10 # ERROR: L.Data is not declared with var
B.U.Data.Value = 10
The error occurs because L.Data is an immutable field (not declared with var). The mutation path must be var all the way down, however deeply nested it is.
Mutability of the index is a separate matter, and does not help: it is the array that must be var to allow element mutation.
var I:int = 2 # A mutable index changes nothing
var A:[]int = array{5, 6, 7}
set A[I] = 42
A[2] = 42
# B:[]int = array{5, 6, 7}
# set B[I] = 42 # ERROR: B is not var, so no element of it can be mutated
Identity and Uniqueness¶
The <unique> specifier gives classes identity-based equality. Without it, classes can't be compared for equality at all (you'd need to write custom comparison methods). With it, equality means identity — two references are equal only if they refer to the exact same object.
unique_item := class<unique>:
var Count:int = 0
Item1:unique_item = unique_item{}
Item2:unique_item = Item1 # Same object
Item3:unique_item = unique_item{} # Different object, identical contents
Item1 = Item2
not (Item1 = Item3)
This identity-based equality is crucial for game objects that need distinct identities even when their data is identical. Two monsters might have the same stats, but they are still different monsters.