Expressions
Everything is an expression. This design principle sets Verse apart from many other languages where statements and expressions are distinct concepts. Every piece of code you write produces a value, even constructs you might expect to be purely side-effecting. This creates a programming model where code can be composed and combined in ways that feel natural and predictable.
Primary Expressions
Everything starts with primary expressions—the atomic units from which more complex expressions are built. These include literals, identifiers, parenthesized expressions, and the tuple construct that provides lightweight data aggregation.
Basic Values
Literals are source code representations of constant values. Verse provides literals for all its primitive types: integers, floats, characters, strings, booleans, and functions. Each literal type has specific syntax rules that determine what values can be expressed and how they're interpreted.
<!--versetest point := struct{X:float, Y:float} Condition:logic = true --> <!-- 01 -->
Result := if (Condition?) then 42 else 3.14 # Integer and float literals
array{1, 2, 3} # Integer literals in array construction
point{X:=0.0, Y:=1.0} # Float literals in object construction
Integer Literals
Integer literals represent whole numbers and can be written in two formats:
Decimal notation uses standard digits:
<!--versetest--> <!-- 02 -->
Count := 42
Negative := -17
Zero := 0
Large := 9223372036854775807 # Maximum 64-bit signed integer literals
Hexadecimal notation uses the 0x prefix followed by hex digits
(0-9, a-f, A-F):
<!--versetest--> <!-- 03 -->
Byte := 0xFF
Address := 0x1F4A
LowercaseHex := 0xabcdef
UppercaseHex := 0xABCDEF
Integer literals must fit within a 64-bit signed integer range
(-9223372036854775808 to 9223372036854775807). At runtime,
integer values are arbitrary precision and can grow past the values
that can be written as literals. However, integers exceeding 64-bit
have limited support (e.g., cannot be used in string interpolation
or persisted).
Float Literals
Floating-point literals represent decimal numbers, they must include a
decimal point and in some cases the f64 suffix.
<!--versetest--> <!-- 04 -->
Pi := 3.14159
Half := 0.5
Explicit := 12.34f64 # Explicit bit-depth suffix
Scientific notation is used for very large or small numbers using exponents:
<!--versetest--> <!-- 05 -->
Large := 1.0e10 # 10,000,000,000 (sign optional)
Small := 1.0e-5 # 0.00001
WithSign := 2.5e+3 # 2,500 (explicit + sign)
Compact := 1.5e2 # 150 (no sign defaults to +)
Some rules:
- Must have decimal point:
1.0is valid,1is an integer - Final decimal point without digits is invalid:
1.is a syntax error - All floats are 64-bit (IEEE 754 double precision); the
f64suffix is optional - Unary operators work as with integers:
-1.0,+1.0
Overflow and Underflow Behavior:
Float literals outside the IEEE 754 double-precision range produce compile-time errors:
<!--versetest--> <!-- 06 -->
#TooBig := 1.7976931348623159e+308 # Compile error: literal overflow
Maximum := 1.7976931348623158e+308 # OK: Maximum finite float
However, runtime float arithmetic follows standard IEEE 754 semantics:
<!--versetest--> <!-- 666 -->
# Runtime overflow produces infinity
Large := 1.0e308
Overflow := Large * 10.0 # Overflow produces infinity
# Division by zero produces infinity
PosInf := 1.0 / 0.0
NegInf := -1.0 / 0.0
# Underflow produces denormalized numbers or zero
Small := 1.0e-320
Smaller := Small / 1.0e10 # Underflows gracefully
Operations that would produce NaN (like 0.0 / 0.0 or Inf - Inf)
cause runtime errors rather than producing NaN values.
Character Literals
Character literals represent individual text units. Verse has two character types with different literal syntax:
char literals represent UTF-8 code units (single bytes, 0-255):
<!--versetest--> <!-- 07 -->
LetterA := 'a' # Printable ASCII character
Space := ' '
Tab := '\t' # Escape sequence
Hex := 0o61 # Hex notation: 0oXX (97 decimal = 'a')
char32 literals represent Unicode code points:
<!--versetest--> <!-- 08 -->
Emoji := '😀' # Non-ASCII automatically char32
Accented := 'é'
ChineseChar := '好'
HexUnicode := 0u1f600 # Hex notation: 0uXXXXX (😀)
Type inference from literals:
- ASCII characters (
U+0000toU+007F):'a'has typechar - Non-ASCII characters:
'😀'has typechar32 - No implicit conversion between
charandchar32
Escape sequences work in both char and strings:
| Escape | Meaning | Codepoint |
|---|---|---|
\t |
Tab | U+0009 |
\n |
Newline | U+000A |
\r |
Carriage return | U+000D |
\" |
Double quote | U+0022 |
\' |
Single quote | U+0027 |
\\ |
Backslash | U+005C |
\{ |
Left brace (string interpolation) | U+007B |
\} |
Right brace (string interpolation) | U+007D |
\< |
Less than | U+003C |
\> |
Greater than | U+003E |
\& |
Ampersand | U+0026 |
\# |
Hash | U+0023 |
\~ |
Tilde | U+007E |
Hex notation work as follows:
0oXXforchar(two hex digits,0o00to0off)0uXXXXXforchar32(up to six hex digits,0u00000to0u10ffff)
Character literals can not be empty or have multiple characters.
String Literals
String literals represent text sequences and support interpolation for embedding expressions. Basic strings use double quotes:
<!--versetest--> <!-- 09 -->
Greeting := "Hello, World!"
Empty := ""
WithEscapes := "Line 1\nLine 2\tTabbed"
String interpolation embeds expressions using curly braces:
<!--versetest Format(D:float, ?Decimals:int):string="" --> <!-- 10 -->
Name := "Alice"
Age := 30
# Simple interpolation
Message := "Hello, {Name}!" # "Hello, Alice!"
# Expression interpolation
Info := "Age next year: {Age + 1}" # "Age next year: 31"
# Function calls
Score := 100
Text := "Score: {ToString(Score)}" # "Score: 100"
# Function calls with named arguments
Distance := 5.5
Formatted := "Distance: {Format(Distance, ?Decimals:=2)}"
Multi-line strings can span multiple lines using interpolation braces for continuation:
<!--versetest--> <!-- 11 -->
LongMessage := "This is a multi-line {
}string that continues across {
}multiple lines."
# Result: "This is a multi-line string that continues across multiple lines."
OtherMessage := "Another message{
} with some empty{
} spaces."
# Result := "Another message with some empty spaces."
Empty interpolants are ignored:
<!--versetest--> <!-- 12 -->
Text1 := "ab{}cd" # Same as "abcd"
Text2 := "ab{
}cd" # Same as "abcd" (newline ignored)
Special rules:
- Curly braces must be escaped:
"\{ \}"for literal braces stringis an alias for[]char(array of UTF-8 code units)- Strings are sequences of UTF-8 bytes, not Unicode characters
"José".Length = 5(5 bytes, not 4 characters - é takes 2 bytes)
String-array equivalence:
<!--versetest--> <!-- 13 -->
Test1 := logic{"abc" = array{'a', 'b', 'c'}} # True
Test2 := logic{"" = array{}} # True
Comments in strings are removed:
<!--versetest--> <!-- 14 -->
Text1 := "abc<#comment#>def" # Same as "abcdef"
Boolean Literals
The logic type has two literal values:
<!--versetest--> <!-- 15 -->
IsReady := true
IsComplete := false
Boolean values are used with the query operator ? or in comparisons:
<!--versetest StartGame():void = {} ShowResults():void = {} IsReady:logic = true IsComplete:logic = false --> <!-- 16 -->
if (IsReady?):
StartGame()
if (IsComplete = true):
ShowResults()
The logic{} expression creates boolean values from failable expressions (see Failure for details on failable expressions):
<!--versetest Operation()<computes><decides>:void = {} Optional:?int = option{1} X:int = 1 Y:int = 1 --> <!-- 17 -->
# Converts <decides> expression to logic value
Success := logic{Operation[]} # true if succeeds, false if fails
HasValue := logic{Optional?} # true if optional has value
IsEqual := logic{X = Y} # true if equal, false otherwise
The logic{} expression requires at least a superficial possibility of failure. Pure expressions without <decides> effect cause errors:
<!--versetest--> <!-- 18 -->
# ERROR: logic{0} has no decides effect
# ERROR: logic{} is empty
Valid := logic{false?} # OK: false? can fail
Multiple expressions inside logic{} can be separated by semicolons or commas (see Semicolons vs Commas for details):
<!--versetest--> <!-- 19 -->
Result1 := logic{true?; true?} # Semicolon separator
Result2 := logic{true?, true?} # Comma separator
Path Literals
Path literals identify modules and packages using a hierarchical naming scheme:
<!--NoCompile--> <!-- 21 -->
/Verse.org/Verse # Standard library path
/YourGame/Player/Inventory # Custom module path
/user@example.com/MyModule # Personal namespace
Path syntax follows specific rules:
- Starts with
/ - Contains label (alphanumeric,
.,-) - Identifiers must start with letter or
_
Path literals are covered in detail in the Modules chapter.
Identifiers and References
Identifiers serve as references to values, whether they're constants, variables, functions, or types. An identifier consists of:
- First character: Letter (A-Z, a-z) or underscore (
_) - Subsequent characters: Letters, digits (0-9), or underscores
- Reserved: Single underscore
_cannot be used as an identifier
Identifiers are case-sensitive and use only ASCII characters—Unicode characters are not supported in identifiers.
<!--NoCompile--> <!-- 22 -->
int # Reference to the int type
GetValue # Reference to a function
Counter # Reference to a variable
my_class # Reference to a class
_private # Leading underscore allowed
variable123 # Digits allowed after first character
# Invalid identifiers:
# 123invalid # Cannot start with digit
# my-variable # Hyphen not allowed
# café # Unicode not supported
# _ # Single underscore is reserved
The language doesn't syntactically distinguish between different kinds of identifiers (types, functions, variables)—the context determines how each identifier is used.
Parentheses and Grouping
Parentheses serve dual purposes: they group expressions to control evaluation order, and they create tuple expressions. A parenthesized expression simply evaluates to the value of its contents, allowing you to override the default operator precedence or improve readability:
<!--versetest A:int = 1 B:int = 2 C:int = 3 X:int = 5 Y:int = 10 Positive:string = "positive" Negative:string = "negative" --> <!-- 23 -->
(A + B) * C # Group addition before multiplication
if (X > 0 and Y > 0) then Positive else Negative
Tuples
Tuples provide a way to group two or more values with little ceremony. The syntax distinguishes between parentheses used for grouping and those used for tuple construction through the presence of commas:
<!--versetest X:int = 5 Y:int = 10 --> <!-- 24 -->
(X, Y) # Two-element tuple
(1, "hello", true) # Mixed-type tuple
Tuples can be accessed using function-call syntax with a single integer argument:
<!--versetest--> <!-- 25 -->
point := (10, 20)
x := point(0) # Access first element
y := point(1) # Access second element
Tuple types are written:
<!--versetest GetPoint():tuple(int,int) = (10, 20) GetData():tuple(int,string,logic) = (42, "hello", true) <# --> <!-- 26 -->
tuple(int,int)
tuple(int,string,logic)
<!-- #> -->
While the type of an unary element can be accepted by the compiler,
tuple(int), there is currently no syntax to write a tuple of one element.
Postfix Operations
Postfix operations are operations that follow their operand and can be chained together. This creates a left-to-right reading order that feels natural and allows for intuitive composition.
Member Access
The dot operator provides access to members of objects, modules, and other structured values. Member access expressions evaluate to the value of the specified member:
<!--NoCompile--> <!-- 27 -->
Player.Health # Access field
Config.MaxPlayers # Access nested value
math.Sqrt(16.0) # Access module function
Point.X # Access struct field
<!-- math.Sqrt may not compile ... I don't really care to fix it. -->
Member access can be chained, creating paths through nested structures:
<!--versetest item := class{Name:string = "Sword"} inventory := class{Items:[]item = array{item{}}} player_type := class{Inventory:inventory = inventory{}} game := class{Players:[]player_type = array{player_type{}}} M()<decides>:void = Game:game = game{} Game.Players[0].Inventory.Items[0].Name <# --> <!-- 28 -->
Game.Players[0].Inventory.Items[0].Name
<!-- #> -->
Computed Access
Square brackets provide computed access to elements, whether for arrays, maps, or other indexable structures. The expression within brackets is evaluated to determine which element to access:
<!--versetest ComputeIndex():int = 0 M()<decides>:void = Array:[]int = array{1, 2, 3} Map:[string]int = map{"key" => 42} Matrix:[][]int = array{array{1, 2}, array{3, 4}} Row:int = 0 Col:int = 1 Data:[]int = array{10, 20, 30} Array[0] Map["key"] Matrix[Row][Col] Data[ComputeIndex()] <# --> <!-- 29 -->
Array[0] # Array indexing
Map["key"] # Map lookup
Matrix[Row][Col] # Nested indexing
Data[ComputeIndex()] # Dynamic index computation
<!-- #> -->
The square bracket syntax Func[] is required for calling
functions that may fail (those with the <decides> effect). Regular
parentheses Func() are used for functions that always succeed. Array
indexing also uses [] because it can fail when the index is out of bounds.
GetValue()<decides>:int = ...
GetData():int = ...
# Must use [] for functions that may fail
if (X := GetValue[]):
Print("Got: {X}")
# Must use () for functions that always succeed
Y := GetData()
# ERROR: Cannot use () for failable functions
# Z := GetValue() # Compile error!
Function Calls
Function calls use parentheses with comma-separated arguments. The language treats function calls as expressions that evaluate to the function's return value:
<!--versetest Sqrt(X:int):float = 4.0 MaxOf(A:int, B:int):int = if (A > B) then A else B Initialize():void = {} GetData():int = 42 Transform():int = 10 Process(X:int, Y:int)<decides>:void = {} M()<decides>:void = A:int = 5 B:int = 10 Sqrt(16) MaxOf(A, B) Initialize() Process[GetData(), Transform()] <# --> <!-- 30 -->
Sqrt(16) # Single argument
MaxOf(A, B) # Multiple arguments
Initialize() # No arguments
Process[GetData(), Transform()] # Nested calls, outer call may fail
<!-- #> -->
Object Construction
Object construction uses a distinctive brace syntax to indicates the
creation of a new instance. The syntax requires explicit field
initialization using the := operator:
<!--versetest point := struct{ X:int, Y:int } player := struct{Name:string, Level:int, Health:int} config := struct { MaxPlayers:int, Difficulty:string, EnablePvP:logic } --> <!-- 31 -->
point{X:=10, Y:=20}
player{Name:="Hero", Level:=1, Health:=100}
config{
MaxPlayers := 16,
EnablePvP := true,
Difficulty := "normal"
}
The use of := for field initialization reinforces that these are
binding operations—you're binding values to fields at construction
time. Object constructors can be nested, creating complex
initialization expressions:
<!--versetest point:=struct{ X:int, Y:int} inventory:=struct{Capacity:int} player:=struct{ Position:point, Inventory:inventory} config:=struct{Difficulty:string} game_state:=struct{Player:player, Settings:config} --> <!-- 32 -->
Game := game_state{
Player := player{
Position := point{X:=0, Y:=0},
Inventory := inventory{Capacity:=20}
},
Settings := config{Difficulty:="hard"}
}
Control Flow as Expressions
One of Verse's distinctive features is that control flow constructs are expressions, not statements. This means that if-expressions, loops, and case expressions all produce values that can be used in larger expressions.
Conditional
The if-then-else construct is an expression that evaluates to one of two values based on a condition:
<!--versetest ComputeA():int=1 ComputeB():int=1 X:int = 5 Condition:logic = true --> <!-- 33 -->
Result := if (X > 0) then "positive" else "negative"
Value := if (Condition=true) then ComputeA() else ComputeB()
The else clause can be omitted, though this affects the type of the expression. Verse supports multiple syntactic forms for if-expressions, including parenthesized conditions and indented bodies:
<!--versetest Condition:logic = true Value1:int = 42 Value2:int = 100 --> <!-- 34 -->
# Standard form
if (Condition?) then Value1 else Value2
# Indented form
if:
Condition?
then:
Value1
else:
Value2
For
For expressions iterate over collections and produce values. The basic form iterates over elements:
<!--versetest Process(Item:int):void={} Collection:[]int = array{1, 2, 3} --> <!-- 35 -->
for (Item : Collection) { Process(Item) }
An extended form provides access to both index and item--in the case
of a Map, indices are not limited to integers:
<!--versetest Collection:[]int = array{1, 2, 3} --> <!-- 36 -->
for (Index -> Item : Collection) {
Print("Item at {Index} is {Item}")
}
Since for expressions are themseleves expressions, they produce array values and compose with other expressions. The body of a for expression is evaluated for each successful iteration, and the expression as a whole has a value determined by these evaluations.
Loop
Loop expressions provide indefinite iteration, continuing until explicitly terminated through failure or other control flow:
<!--versetest GetNext():int=1 Done(Value:int)<computes><decides>:void={} Process(Value:int):void={} M():void= loop { Value := GetNext() if (Done[Value]) then break Process(Value) } <# --> <!-- 37 -->
loop {
Value := GetNext()
if (Done[Value]) then break
Process(Value)
}
<!-- #> -->
The loop construct can use indented syntax for clarity.
A loop expression produces a value of type true (the
top type in Verse's type system), regardless of what expressions appear in
its body. This value is currently not useful for practical purposes—you
typically use loops for their side effects rather than their return value.
Result := loop:
ProcessData()
if (ShouldStop[]):
break
# Result has type 'true' (and returns `true`)
Case
Case expressions provide multi-way branching based on value matching:
<!--versetest color := enum: Red Yellow Green Other Color:color = color.Red --> <!-- 38 -->
Description := case(Color) {
color.Red => "Danger",
color.Yellow => "Warning",
color.Green => "Safe",
_ => "Unknown"
}
The _ pattern serves as a catch-all, ensuring the case expression is
exhaustive. Case expressions evaluate to the value of the matched
branch, making them useful for value computation as well as control
flow.
Binary Operations
Binary expressions follow a carefully designed precedence hierarchy that balances mathematical conventions with programming practicality.
Assignment and Binding
At the lowest precedence level, assignment operators bind values to
identifiers. The := operator creates immutable bindings, while set = performs mutable assignment:
<!--versetest--> <!-- 39 -->
X := 42 # Immutable binding
Y := X * 2 # Binding to computed value
Z := W := 10 # Right-associative chaining
Assignment operators are right-associative, meaning that a := b := c
groups as a := (b := c). This allows for natural chaining of
assignments while maintaining clarity about evaluation order.
Compound assignments provide shorthand for common update patterns:
<!--versetest F()<transacts>:void= var Counter :int = 0 var Total :int = 0 Factor:=2 set Counter += 1 set Total *= Factor <# --> <!-- 40 -->
set Counter += 1 # Equivalent to: set Counter = Counter + 1
set Total *= Factor # Equivalent to: set Total = Total * Factor
<!-- #> -->
Compound assignment operators evaluate the left-hand side expression only once** which is observable when the expression has side effects:
<!--versetest assert: var TestArray:[]int = array{10, 20, 30, 40, 50} var Index:int = 0 Inc():int = set Index += 1 Index
# Compound assignment: Inc() called ONCE
set TestArray[Inc()] += 1
# Verify: Index = 1 (Inc called once)
Index = 1
# TestArray[1] = 20 + 1 = 21
TestArray[1] = 21
-->
var Index:int = 0
Inc():int =
set Index += 1
Index
# Compound assignment calls Inc() one
set Array[Inc()] += 1
# Result: Array[1] = Array[1] + 1
# Expanded form would call Inc() twice
# set Array[Inc()] = Array[Inc()] + 1
# Result: Array[1] = Array[2] + 1 (different!)
In the compound assignment set Array[Inc()] += 1, the function Inc()
is called once to determine the index, then that location is read,
incremented, and stored back.
Range Expressions
The range operator (..) creates integer ranges for iteration in
for loops. Ranges are inclusive on both ends and can only appear
directly in for loop iteration clauses:
<!--versetest End()<computes>:int=10 Count:int=10 Start:int=1 Process(I:int):void={} F():void= for (I := 1..10): for (J := I..(I+10)): for (K:= J..End()) {} <# --> <!-- 41 -->
1..10 # Range from 1 to 10 (inclusive)
Start..End # Variable-defined range
for (I := 0..Count): # Must use := syntax, not :
Process(I)
<!-- #> -->
Ranges are not first-class values. They cannot be stored in variables
or used outside of for loop iteration clauses. See the Range
Operator Restrictions
section for details.
Logical Operations
Logical operators combine boolean values with short-circuit
evaluation. Their result is either success or failure. Verse uses
keyword operators (and, or, not) rather than symbols, improving
readability:
<!--versetest ProcessQuadrant()<computes>:void = {} Validated:logic= true UseDefault()<computes><decides>:void = {} IsReady()<computes><decides>:void = {} Wait()<computes>:void = {} M()<transacts>:void = X:int = 5 Y:int = 10 if (X > 0 and Y > 0) then ProcessQuadrant() Result := logic{Validated? or UseDefault[]} if (not IsReady[]) then Wait() <# --> <!-- 42 -->
if (X > 0 and Y > 0) then ProcessQuadrant()
Result := logic{Validated? or UseDefault[]}
if (not IsReady[]) then Wait()
<!-- #> -->
The precedence ensures that and binds tighter than or, matching
mathematical logic conventions, the logic{} expression turns succes
or failure into a value:
<!--NoCompile--> <!-- 43 -->
# Evaluates as: (ExpA and ExpB) or (ExpC and ExpD)
Condition := logic{ExpA and ExpB or ExpC and ExpD}
Important: Variable bindings do not escape from logical operations.
When you use := inside and, or, or not expressions, those
bindings are only evaluated for short-circuit control flow and are not
accessible afterward:
<!--NoCompile--> <!-- 998 -->
Arr:[]int = array{10, 20}
# ERROR: Bindings in logical operations are NOT accessible
if ((X := Arr[0]) and (Y := Arr[1])):
# X and Y are not bound here - this will cause a compilation error!
Z := X + Y
# Simple if binding DOES work
if (X := Arr[0]):
# OK: X is accessible here
Y := X + 1
Comparison Operations
Comparison operators also either succeed or fail and can be chained for range checking:
<!--versetest InRange():void={} Value:int = 50 X:int = 75 Minimum:int = 0 Maximum:int = 100 A:int = 5 B:int = 10 --> <!-- 44 -->
if (0 <= Value <= 100) then InRange()
IsValid := logic{X > Minimum and X < Maximum}
Same := logic{A = B}
Different := logic{A <> B}
All comparison operators have the same precedence and are evaluated left-to-right. Crucially, comparison operators return their left operand when the comparison succeeds, and comparison chains have special syntax that checks all adjacent pairs.
<!--versetest assert: X := 0 < 10 X = 0 # Returns left operand (0)
Value:int = 50
Result := 0 <= Value <= 100
Result = 0 # Chain returns leftmost operand (0)
# Chain checks BOTH comparisons
Value2:int = 75
not(10 <= Value2 <= 50) # Fails because 75 > 50
<# --> <!-- 999 -->
X := 0 < 10
# X equals 0 (the left operand)
0 <= Value <= 100
# Special chain syntax that checks BOTH:
# - 0 <= Value (lower bound)
# - Value <= 100 (upper bound)
# Returns 0 (leftmost operand) if both succeed
<!-- #> -->
The comparison chain A <= B <= C is not evaluated as (A <= B) <= C.
Instead, it's special syntax that checks both A <= B and B <= C, while
returning the leftmost operand (A) on success. This enables natural
mathematical notation for ranges without requiring and operators.
Arithmetic Operations
Arithmetic operations follow standard mathematical precedence, with multiplication and division binding tighter than addition and subtraction:
<!--versetest A:int = 1 B:int = 2 C:int = 3 --> <!-- 45 -->
Result := A + B * C # Multiplication first
Average := (A + B) / 2 # Parentheses override precedence
Integer division by zero fails and has the <decides> effect.
When dividing integers, X / Y can fail if Y is 0, allowing you to handle
this case safely:
<!--versetest X:int = 10 Y:int = 0 assert: not(Result := X / Y) --> <!-- 997 -->
if (Result := X / Y):
Print("Division succeeded")
else:
Print("Cannot divide by zero")
Float division by zero does not fail; it returns infinity according to IEEE 754 floating-point semantics.
Unary operators have the highest precedence among arithmetic operations:
<!--versetest Flag:logic = true Value:int = 1 X:int = 1 Y:int = 2 --> <!-- 46 -->
Negative := -Value
Inverted := logic{not Flag=true}
Result := -X * Y # Unary minus applies to x only
Set Expressions
While Verse emphasizes immutability, practical programming sometimes requires mutation. Set expressions provide mutation of variables and fields:
<!--versetest c := class { var Field:int = 0 } GetObj()<transacts>:c = c{} GetArr()<transacts>:[]int = array{1} GetMap()<transacts>:[string]string = map{ "hi" => "hp" } Element:int = 5 Value:int = 100 Index:int = 0 Key:string = "key" MappedValue:string = "value" assert: var X:int = 0 var Obj:c = GetObj() var Arr:[]int = GetArr() var Map:[string]string = GetMap()
set X = 10
set Obj.Field = Value
set Arr[Index] = Element
set Map[Key] = MappedValue
<# --> <!-- 47 -->
set X = 10 # Variable assignment
set Obj.Field = Value # Field assignment
set Arr[Index] = Element # Array element assignment
set Map[Key] = MappedValue # Map entry assignment
<!-- #> -->
Set expressions are themselves expressions that return the value being
assigned (the right-hand side). For example, set Obj.Field = Value
returns Value, not Obj. This allows chaining assignments:
set Y = set X = 5 # Both X and Y become 5
Though set expressions have a value, they're typically used for their side effects. The left-hand side must be a valid LValue—something that can be assigned to.
Complex LValues are supported, allowing updates deep within data structures:
<!--versetest item := class{Name:string = "Item"} inventory := class{var Items:[]item = array{item{}}} player := class{var Inventory:inventory = inventory{}} game := class{var Players:[]player = array{player{}}} M()<transacts><decides>:void = Game:game = game{} CurrentPlayer:int = 0 Slot:int = 0 NewItem:item = item{} set Game.Players[CurrentPlayer].Inventory.Items[Slot] = NewItem <# --> <!-- 48 -->
set Game.Players[CurrentPlayer].Inventory.Items[Slot] = NewItem
<!-- #> -->
Semicolons vs Commas
Verse uses semicolons and commas as separators in various contexts, but they have fundamentally different semantics in most situations. Understanding when each is appropriate is essential for writing correct Verse code.
Semicolons (within parentheses) create sequences - they evaluate expressions in order and return the value of the last expression:
<!--versetest assert: Result := (1; 2; 3) Result = 3 --> <!-- 49 -->
Result := (1; 2; 3) # Evaluates 1, then 2, then 3; returns 3
# Note: Parentheses are required
# Result := 1; 2 # ERROR: Not valid without parentheses
Commas (within parentheses) create tuples - they group multiple values into a single composite value:
<!--versetest--> <!-- 50 -->
Result := (1, 2, 3) # Creates a tuple of three elements
# Result = (1, 2, 3) (type: tuple(int, int, int))
# Note: Parentheses are required
# Result := 1, 2 # ERROR: Not valid without parentheses
Context-Specific Behavior
In expression contexts (like assignments), semicolons and commas require parentheses to create sequences and tuples. The distinction is clear when comparing parenthesized expressions:
<!--versetest--> <!-- 51 -->
# Semicolon: sequence (returns last value)
X := (0; 1) # X = 1, type is int
# Comma: tuple (groups values)
Y := (0, 1) # Y = (0, 1), type is tuple(int, int)
This applies to function return values as well:
<!--versetest--> <!-- 52 -->
GetInt():int = (1.0; 2) # Returns 2 (int)
GetTuple():tuple(float, int) = (1.0, 2) # Returns (1.0, 2)
Semicolons in argument position create a sequence that executes before the call, with only the last value passed as the argument:
<!--versetest Process(X:int):void={} LogEvent(S:string):int=1 --> <!-- 53 -->
# Semicolon executes side effects, then passes last value
Process(LogEvent("called"); 42) # Logs "called", then calls Process(42)
# Equivalent to:
LogEvent("called")
Process(42)
This pattern enables side effects in argument position:
<!--versetest MultiplyByTen(X:int):int = X * 10 --> <!-- 54 -->
Result := MultiplyByTen(2; 3) # Evaluates 2 (discards it), calls Multiply(3)
Result = 30
Commas separate distinct arguments in the standard way:
<!--versetest Add(A:int, B:int):int = A + B --> <!-- 55 -->
Sum := Add(10, 20) # Two separate arguments
Sum = 30
Semicolons are not allowed in parameter lists - you must use commas:
<!--versetest assert_semantic_error(3540): InvalidFunc(A:int; B:int):void = {} --> <!-- 56 -->
# VALID: Comma-separated parameters
ValidFunc(A:int, B:int):void = {}
# INVALID: Semicolon in parameters
# InvalidFunc(A:int; B:int):void = {}
In Specific Scopes
Within block expressions (braces), semicolons and commas are interchangeable as separators between definitions:
<!--versetest--> <!-- 57 -->
# In block scope, all three separators work:
block:
X:int = 0; Y:int = 0 # Semicolon separator
block:
X:int = 0, Y:int = 0 # Comma separator
block:
X:int = 0 # Newline separator (most common)
Y:int = 0
In logic{} constructor - both semicolons and commas work, but with
different semantics based on the construct's behavior:
<!--versetest--> <!-- 58 -->
# Both evaluate all expressions and return logic value
Result1 := logic{true?; true?} # Sequence of queries
Result2 := logic{true?, true?} # Also valid
In option{} constructor - follows the standard sequence vs tuple rule:
<!--versetest--> <!-- 59 -->
# Semicolon: sequence, wraps last value
Option1 := option{1; 2}? # 2
# Comma: tuple, wraps the tuple
Option2 := option{1, 2}? # (1, 2)
In for expressions - semicolon typically separates the iteration clause from filter conditions, while commas separate multiple conditions:
<!--versetest--> <!-- 60 -->
# Semicolon separates iteration from filter
for (X := 1..3; X <> 2) { X }
# Comma separates multiple filter conditions
for (X := 1..3, X <> 2) { X } # Same meaning in this context
In array{} constructors, elements can be separated by commas or
semicolons (but not mixed):
<!--versetest--> <!-- 61 -->
CommaArray := array{1, 2, 3} # Commas work
SemiArray := array{1; 2; 3} # Semicolons also work
# MixedArray := array{1, 2; 3} # ERROR: Cannot mix separators
Newlines as Separators
In addition to semicolons and commas, newlines can serve as separators in compound expressions and blocks. Newlines behave like semicolons - they create sequences:
<!--versetest--> <!-- 62 -->
# These are equivalent:
Result1 := (1; 2; 3)
Result2 := (
1
2
3
)
# Both return 3
Compound and Block Expressions
Compound expressions, delimited by braces, group multiple expressions into a single expression. The value of a compound expression is the value of its last sub-expression:
<!--versetest ComputeIntermediate():int=3 CalculateAdjustment(o:int):int=3 --> <!-- 63 -->
Result := {
Temp := ComputeIntermediate()
Adjustment := CalculateAdjustment(Temp)
Temp + Adjustment
}
Compound expressions create new scopes for variables, allowing local bindings that don't affect the enclosing scope:
<!--versetest--> <!-- 64 -->
block:
X := 10 # Local to this block
Y := 20
X + Y
# X and Y no longer accessible
Expressions within a compound can be separated by semicolons, commas, or newlines. Semicolons and newlines create sequences (returning the last value), while commas create tuples. See Semicolons vs Commas for the complete rules:
<!--versetest A:int = 1 B:int = 2 C:int = 3 M():void = X := { A; B; C } Y := { A, B, C } Z := { A B C } <# --> <!-- 65 -->
{ A; B; C } # Semicolon separation (returns C)
{ A, B, C } # Comma separation (returns tuple (A, B, C))
{ # Newline separation (returns C)
A
B
C
}
<!-- #> -->
Array Expressions
Array expressions create array values using the array keyword
followed by elements in braces:
<!--versetest--> <!-- 66 -->
NumArray := array{1, 2, 3, 4, 5}
Empty := array{}
Mixed := array{1, "two", 3.0} # Mixed types if allowed
Arrays can also be constructed using indented syntax for clarity with longer lists:
<!--versetest--> <!-- 67 -->
Colors := array:
"red"
"green"
"blue"
"yellow"