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 type has its own literal syntax and rules governing valid values and their interpretation at compile time.
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:
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):
Byte := 0xFF
Address := 0x1F4A
LowercaseHex := 0xabcdef
UppercaseHex := 0xABCDEF
Literal Limits vs Runtime Behavior:
Integer literals must fit within a 64-bit signed integer range (-9223372036854775808 to 9223372036854775807). This is a compile-time restriction on what values you can write directly in your code.
At runtime, integer values use arbitrary precision arithmetic and can grow beyond 64-bit limits through computation. However, integers exceeding 64-bit range 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.
Pi := 3.14159
Half := 0.5
Explicit := 12.34f64 # Explicit bit-depth suffix
Scientific notation expresses very large or small numbers using exponents:
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 +)
Float literals must include a decimal point (1.0 is valid, but 1 is an integer). A final decimal point without digits is invalid (1. is a syntax error). All floats are 64-bit (IEEE 754 double precision); the f64 suffix 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:
#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:
# 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
Float operations follow IEEE 754 semantics. Operations that would produce NaN (like 0.0 / 0.0, Inf - Inf, or Sqrt(-1.0)) return NaN values rather than failing. NaN propagates through arithmetic operations.
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):
LetterA := 'a' # Printable ASCII character
Space := ' '
Tab := '\t' # Escape sequence
LetterA := 0o61 # Hexadecimal notation: 0oXX (97 decimal = 'a')
char32 literals represent Unicode code points:
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 |
Numeric character notation works as follows:
0oXXforchar(hexadecimal notation,0o00to0oFFfor values 0-255)0uXXXXXXforchar32(hexadecimal notation,0u000000to0u10ffff)
Character literals cannot be empty or contain multiple characters.
String Literals¶
String literals represent text sequences and support interpolation for embedding expressions. Basic strings use double quotes:
Greeting := "Hello, World!"
Empty := ""
WithEscapes := "Line 1\nLine 2\tTabbed"
String interpolation embeds expressions using curly braces:
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:
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."
The compiler ignores empty interpolants:
Text1 := "ab{}cd" # Same as "abcd"
Text2 := "ab{
}cd" # Same as "abcd" (newline ignored)
Curly braces must be escaped ("\{ \}") to appear as literal characters in strings. The string type is an alias for []char (array of UTF-8 code units). Since UTF-8 code units are single bytes, strings are byte sequences rather than Unicode character sequences. For example, "José".Length returns 5 (5 code units/bytes, not 4 characters, since é takes 2 code units).
String-array equivalence:
Test1 := logic{"abc" = array{'a', 'b', 'c'}} # True
Test2 := logic{"" = array{}} # True
The compiler removes comments from strings:
Text1 := "abc<#comment#>def" # Same as "abcdef"
Boolean Literals¶
The logic type has two literal values:
IsReady := true
IsComplete := false
Use boolean values with the query operator ? or in comparisons:
if (IsReady?):
StartGame()
if (IsComplete = true):
ShowResults()
The logic{} expression creates boolean values from failable expressions (see Failure for details on failable expressions):
# 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:
# 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):
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:
/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
_
The Modules chapter covers path literals in detail.
Identifiers and References¶
Identifiers serve as references to values, whether they are 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.
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 does not 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:
(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:
(X, Y) # Two-element tuple
(1, "hello", true) # Mixed-type tuple
Tuples can be accessed using function-call syntax with a single integer argument:
point := (10, 20)
x := point(0) # Access first element
y := point(1) # Access second element
Write tuple types as follows:
tuple(int,int)
tuple(int,string,logic)
While the compiler accepts single-element tuple types like tuple(int), there is currently no syntax to construct a single-element tuple value.
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:
Player.Health # Access field
Config.MaxPlayers # Access nested value
math.Sqrt(16.0) # Access module function
Point.X # Access struct field
Member access can be chained, creating paths through nested structures:
Game.Players[0].Inventory.Items[0].Name
Computed Access¶
Square brackets provide computed access to elements, whether for arrays, maps, or other indexable structures. Verse evaluates the expression within brackets to determine which element to access:
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). Use regular parentheses Func() 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:
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:
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:
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:
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:
# 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:
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:
for (Index -> Item : Collection) {
Print("Item at {Index} is {Item}")
}
Since for expressions are themselves expressions, they produce array values and compose with other expressions. Verse evaluates the body of a for expression for each successful iteration, and these evaluations determine the value of the expression as a whole.
Loop¶
Loop expressions provide indefinite iteration, continuing until explicitly terminated through failure or other control flow:
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, regardless of what expressions appear in its body. This value has no practical use—loops are typically used 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:
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:
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:
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:
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, Verse calls the function Inc() once to determine the index, then reads that location, increments it, and stores the result 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:
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:
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 success or failure into a value:
# 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:
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:
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 evaluate 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.
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
Verse does not evaluate the comparison chain A <= B <= C as (A <= B) <= C. Instead, it is 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:
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:
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:
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:
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 are typically used for their side effects. The left-hand side must be a valid LValue—something that can be assigned to.
Verse supports complex LValues, allowing updates deep within data structures:
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:
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:
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:
# 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:
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:
# 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:
Result := MultiplyByTen(2; 3) # Evaluates 2 (discards it), calls Multiply(3)
Result = 30
Commas separate distinct arguments in the standard way:
Sum := Add(10, 20) # Two separate arguments
Sum = 30
Semicolons are not allowed in parameter lists - you must use commas:
# 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:
# 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:
# 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:
# 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:
# 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, you can separate elements with commas or semicolons (but not mixed):
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:
# 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:
Result := {
Temp := ComputeIntermediate()
Adjustment := CalculateAdjustment(Temp)
Temp + Adjustment
}
Compound expressions create new scopes for variables, allowing local bindings that do not affect the enclosing scope:
block:
X := 10 # Local to this block
Y := 20
X + Y
# X and Y no longer accessible
You can separate expressions within a compound using 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:
{ 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:
NumArray := array{1, 2, 3, 4, 5}
Empty := array{}
Mixed := array{1, "two", 3.0} # Mixed types if allowed
You can also construct arrays using indented syntax for clarity with longer lists:
Colors := array:
"red"
"green"
"blue"
"yellow"