Skip to content

Primitive Data Types

Verse provides a rich set of primitive types that cover fundamental programming needs. The numeric types int, float, and rational handle mathematical operations, counters, and measurements. The logic type represents boolean values for conditions and flags. Text is handled through char, char32, and string types for character data, player names, and messages. Two special types, any and void, serve unique roles in the type hierarchy as the supertype of all types and the empty type respectively.

Let's explore each primitive type in detail, starting with the numeric types that form the backbone of game logic.

Intrinsics

intrinsic functions are built-in operations provided directly by the runtime that cannot be implemented in pure Verse code. These functions receive special compiler treatment and form the foundation for many language features. Intrinsic functions are special because they:

  • Implemented by the runtime: Written in C++ or other native code, not Verse
  • Cannot be replicated in Verse: Require access to runtime internals or low-level operations
  • Receive compiler recognition: The compiler knows about them and may optimize their use

Examples include mathematical operations like Abs(), collection methods like Find(), and type conversions like ToString().

Most intrinsic functions cannot be referenced as first-class values. This means you can call them directly, but you cannot store them in variables or pass them as function arguments:

Result := Abs(-42)  # Returns 42

# Invalid: Cannot reference without calling
# F := Abs  # ERROR
# Invalid: Cannot pass as parameter
# ApplyFunction(Abs, -42)  # ERROR

This restriction exists because intrinsics often require special calling conventions or optimizations that don't fit the standard function model. If you need to pass intrinsic functionality around, wrap it in a regular function or nested function.

Integers

The int type represents integer, non-fractional values. An int can contain a positive number, a negative number, or zero. Supported integers range from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807, inclusive. Literals (numbers that can be written as constants in code) are limited in size.

You can include int values within your code as literals.

A :int= -42                    # civilian size
#B := 42424242424242424242424242424242424242424242424242 # scary numbers...
                               # ...can be computed but not written as literals

AnswerToTheQuestion :int= 42   # A variable that never changes
CoinsPerQuiver :int= 100       # A quiver costs this many coins
ArrowsPerQuiver :int= 15       # A quiver contains this many arrows

# Mutable variables (see Mutability chapter for details on var and set)
var Coins :int= 225           # The player currently has 225 coins
var Arrows :int= 3            # The player currently has 3 arrows
var TotalPurchases :int= 0    # Track total purchases

You can use the four basic math operations with integers: + for addition, - for subtraction, * for multiplication, and / for division.

var C :int= (-MyInt + MyHugeInt - 2) * 3   # arithmetic
set C += 1                                 # like saying, set C = C + 1
set C *= 2                                 # like saying, set C = C * 2

For integers, the operator / is failable, and the result is a rational type if it succeeds.

Rationals

The rational type represents exact fractions as ratios of integers. Unlike int or float, you cannot write a rational literal directly—rationals are created through integer division using the / operator.

X := 7 / 3    # X has type rational, representing exactly 7÷3

Rationals provide exact arithmetic without the precision loss of floating-point numbers, making them ideal for game logic requiring precise fractional calculations (resource distribution, turn-based systems, probability calculations).

Integer division with / produces a rational value. Division by zero fails:

Half := 5 / 2           # rational: exactly 5/2
Third := 10 / 3         # rational: exactly 10/3
Quarter := 1 / 4        # rational: exactly 1/4

if (not (1 / 0)):
    # Division by zero fails

Rationals are automatically reduced to lowest terms for equality comparisons:

# All these are equal - reduced to 5/2
(5 / 2) = (10 / 4)      # true
(5 / 2) = (15 / 6)      # true
(10 / 4) = (15 / 6)     # true

This normalization ensures that mathematically equivalent rationals compare as equal regardless of how they were constructed.

Negative signs are normalized to the numerator:

(1 / -3) = (-1 / 3)     # true: negative moves to numerator
(-1 / -3) = (1 / 3)     # true: double negative becomes positive

This canonical form simplifies equality checking and ensures consistent behavior.

An important property: int is a subtype of rational. This means any integer can be used where a rational is expected:

ProcessRational(X:rational):rational = X

# Can pass integers directly
ProcessRational(5) = 5/1     # 5 is implicitly 5/1 (rational)
ProcessRational(0) = 0/1     # 0 is implicitly 0/1 (rational)

However, you cannot return a rational where an int is expected—that would be a narrowing conversion:

# Invalid: Cannot narrow rational to int
# BadFunction(X:rational):int = X  # ERROR 3510

Whole number rationals equal their integer equivalents:

(2 / 1) = 2             # true
2 = (2 / 1)             # true
(4 / 2) = 2             # true: 4/2 reduces to 2/1, equals 2
(9 / 3) = 3             # true: 9/3 reduces to 3/1, equals 3

This enables seamless mixing of integer and rational values in calculations.

Two functions convert rationals to integers:

  • Floor — rounds toward negative infinity (down on number line)
  • Ceil — rounds toward positive infinity (up on number line)
# Positive rationals
Floor(5 / 2)= 2         # 2.5 → 2 (down)
Ceil(5 / 2) = 3         # 2.5 → 3 (up)

# Negative rationals - note direction!
Floor((-5) / 2) = -3    # -2.5 → -3 (toward negative infinity)
Ceil((-5) / 2) = -2     # -2.5 → -2 (toward positive infinity)

# With negative denominator
Floor(5 / -2) = -3      # Same as (-5)/2
Ceil(5 / -2) = -2       # Same as (-5)/2

# Both negative
Floor((-5) / -2) = 2    # 2.5 → 2
Ceil((-5) / -2) = 3     # 2.5 → 3

Floor rounds toward negative infinity, not toward zero. This matches mathematical convention but differs from truncation. When the argument is a rational, Floor does not fail, but if passed a float it is a decides function.

Rationals can be used as parameter and return types:

# Function returning rational
Half(X:int)<computes><decides>:rational = X / 2

# Use the result
if (Result := Half[7]):
    Floor(Result) = 3   # 7/2 = 3.5, Floor gives 3
    Ceil(Result) = 4    # 7/2 = 3.5, Ceil gives 4

Because int is a subtype of rational, you cannot overload based solely on these types:

# Invalid: Cannot distinguish int from rational
# ProcessValue(X:int):void = {}
# ProcessValue(X:rational):void = {}  # Error!

The compiler sees int as more specific than rational, so the signatures would be ambiguous.

Rationals excel at resource distribution and fairness calculations:

# Fair resource distribution
DistributeResources(TotalGold:int, NumPlayers:int)<decides>:int =
    GoldPerPlayer := TotalGold / NumPlayers
    Floor(GoldPerPlayer)  # Each player gets whole gold pieces or we fail

# Item affordability calculation
Coins:int = 225
CoinsPerQuiver:int = 100
ArrowsPerQuiver:int = 15

if (NumberOfQuivers := Floor(Coins / CoinsPerQuiver)):
    TotalArrows:int = NumberOfQuivers * ArrowsPerQuiver
    # Player can afford 2 quivers = 30 arrows

Floats

The float type represents all non-integer numerical values. It can hold large values and precise fractions, such as 1.0, -50.5, and 3.14159. A float is an IEEE 64-bit float, which means it can contain a positive or negative number that has a decimal point in the range [-2^1024 + 1, … , 0, … , 2^1024 - 1], or has the value NaN (Not a Number). The implementation differs from the IEEE standard in the following ways:

  • There is only one NaN value.
  • NaN is equal to itself.
  • Every number is equal to itself.
  • 0 cannot be negative.

You can include float values within your code as literals:

A:float = 1.0
B := 2.14
MaxHealth : float = 100.0

var C:float = A + B
C = 3.14              # succeeds
set C -= 3.14
C = 0.0               # succeeds
# C = 0              # compile error; 0 is not a `float` literal

You can use the four basic math operations with floats: + for addition, - for subtraction, * for multiplication, and / for division. There are also combined operators for doing the basic math operations (addition, subtraction, multiplication, and division), and updating the value of a variable:

var CurrentHealth : float = 100.0
set CurrentHealth /= 2.0    # Halves the value of CurrentHealth
set CurrentHealth += 10.0   # Adds 10 to CurrentHealth
set CurrentHealth *= 1.5    # Multiplies CurrentHealth by 1.5

To convert an int to a float, multiply it by 1.0: MyFloat:=MyInt*1.0.

Mathematical Functions

Verse provides intrinsic mathematical functions for common numerical operations. These functions are optimized by the runtime and work with both int and float types.

The Abs() function returns the absolute value of a number—its distance from zero without regard to sign:

# Signatures
Abs(X:int):int
Abs(X:float):float
Abs(5)    # Returns 5
Abs(-5)   # Returns 5
Abs(0)    # Returns 0
Abs(3.14) # Returns 3.14

The Min() and Max() functions return the minimum or maximum of two values:

# Signatures
Min(A:int, B:int):int
Min(A:float, B:float):float
Max(A:int, B:int):int
Max(A:float, B:float):float
# NaN propagates through comparison
Max(NaN, 5.0)   # Returns NaN
Min(NaN, 5.0)   # Returns NaN
Max(NaN, NaN)   # Returns NaN

# Infinity handling
Max(Inf, 100.0)    # Returns Inf
Min(-Inf, 100.0)   # Returns -Inf
Max(-Inf, -Inf)    # Returns -Inf
Min(Inf, Inf)      # Returns Inf

Verse provides multiple rounding functions that convert floats to integers with different rounding strategies:

# Signatures
Floor(X:float)<reads><decides>:int   # Round down
Ceil(X:float)<reads><decides>:int    # Round up
Round(X:float)<reads><decides>:int   # Round to nearest even (IEEE-754)
Int(X:float)<reads><decides>:int     # Truncate toward zero

Round to nearest even (ties go to even):

Round[1.5]    # Returns 2 (tie: 1.5 rounds to even 2)
Round[0.5]    # Returns 0 (tie: 0.5 rounds to even 0)
Round[2.5]    # Returns 2 (tie: 2.5 rounds to even 2)
Round[-1.5]   # Returns -2 (tie: -1.5 rounds to even -2)
Round[-0.5]   # Returns 0 (tie: -0.5 rounds to even 0)

Round[1.4]    # Returns 1 (no tie, rounds down)
Round[1.6]    # Returns 2 (no tie, rounds up)

The "round to nearest even" strategy (also called banker's rounding) avoids bias when rounding many tie values.

Some additional mathematical functions:

# Signature
# Sqrt(X:float):float

# Negative inputs return NaN
Sqrt(-1.0)    # Returns NaN

# Special values
Sqrt(Inf)     # Returns Inf
Sqrt(NaN)     # Returns NaN

# Signature
# Pow(Base:float, Exponent:float):float

Pow(2.0, 3.0)     # Returns 8.0 (2³)
Pow(10.0, 2.0)    # Returns 100.0
Pow(4.0, 0.5)     # Returns 2.0 (square root)
Pow(2.0, -1.0)    # Returns 0.5 (reciprocal)

# Special cases
Pow(0.0, 0.0)     # Returns 1.0 (by convention)
Pow(NaN, 0.0)     # Returns 1.0 (0 exponent always 1)
Pow(1.0, NaN)     # Returns 1.0 (1 to any power is 1)

# Exp(X:float):float

Exp(0.0)      # Returns 1.0
Exp(1.0)      # Returns 2.718... (e)
Exp(-1.0)     # Returns 0.368... (1/e)

# Special values
Exp(-Inf)     # Returns 0.0
Exp(Inf)      # Returns Inf
Exp(NaN)      # Returns NaN

# Signature
# Ln(X:float):float

Ln(1.0)       # Returns 0.0
# Ln(2.718...)     # Returns 1.0 (ln(e) = 1)
Ln(10.0)      # Returns 2.302...

# Invalid inputs
Ln(-1.0)      # Returns NaN (negative)
Ln(0.0)       # Returns -Inf (log of zero)

# Special values
Ln(Inf)       # Returns Inf
Ln(NaN)       # Returns NaN

# Signature
# Log(Base:float, Value:float):float

Log(10.0, 100.0)   # Returns 2.0 (log₁₀(100) = 2)
Log(2.0, 8.0)      # Returns 3.0 (log₂(8) = 3)
Log(2.0, 2.0)      # Returns 1.0 (logₙ(n) = 1)

Verse provides standard trigonometric functions operating on radians:

# Signatures
# Sin(Angle:float):float
# Cos(Angle:float):float
# Tan(Angle:float):float

# Common angles (using PiFloat constant)
Sin(0.0)              # Returns 0.0
Sin(PiFloat / 2.0)    # Returns 1.0
Sin(PiFloat)          # Returns 0.0
Sin(-PiFloat / 2.0)   # Returns -1.0

Cos(0.0)              # Returns 1.0
Cos(PiFloat / 2.0)    # Returns 0.0
Cos(PiFloat)          # Returns -1.0

Tan(0.0)              # Returns 0.0
Tan(PiFloat / 4.0)    # Returns 1.0
Tan(-PiFloat / 4.0)   # Returns -1.0

# Special values
Sin(NaN)              # Returns NaN
Sin(Inf)              # Returns NaN

# Signatures
# ArcSin(X:float):float   # Returns angle in [-π/2, π/2]
# ArcCos(X:float):float   # Returns angle in [0, π]
# ArcTan(X:float):float   # Returns angle in [-π/2, π/2]
# ArcTan(Y:float, X:float):float  # Two-argument arctangent

# Inverse relationships
ArcSin(0.0)    # Returns 0.0
ArcSin(1.0)    # Returns π/2
ArcSin(-1.0)   # Returns -π/2

ArcCos(1.0)    # Returns 0.0
ArcCos(0.0)    # Returns π/2
ArcCos(-1.0)   # Returns π

ArcTan(0.0)    # Returns 0.0
ArcTan(1.0)    # Returns π/4
ArcTan(-1.0)   # Returns -π/4

# Verify inverse relationship
Angle := PiFloat / 6.0  # 30 degrees
Sin(ArcSin(Sin(Angle))) = Sin(Angle)  # True

# ArcTan(Y, X) returns angle of point (X, Y) from origin
ArcTan(1.0, 1.0)     # Returns π/4 (45 degrees)
ArcTan(1.0, 0.0)     # Returns π/2 (90 degrees)
ArcTan(0.0, 1.0)     # Returns 0.0 (0 degrees)
ArcTan(1.0, -1.0)    # Returns 3π/4 (135 degrees)
ArcTan(-1.0, -1.0)   # Returns -3π/4 (-135 degrees)

Hyperbolic functions are analogs of trigonometric functions for hyperbolas. They are useful in physics simulations, catenary curves, and certain mathematical models.

# Signatures
# Sinh(X:float):float    # Hyperbolic sine
# Cosh(X:float):float    # Hyperbolic cosine
# Tanh(X:float):float    # Hyperbolic tangent
# ArSinh(X:float):float  # Inverse hyperbolic sine
# ArCosh(X:float):float  # Inverse hyperbolic cosine
# ArTanh(X:float):float  # Inverse hyperbolic tangent

Sinh(0.0)     # Returns 0.0
Sinh(1.0)     # Returns 1.175...
Cosh(0.0)     # Returns 1.0
Cosh(1.0)     # Returns 1.543...
Tanh(0.0)     # Returns 0.0
Tanh(1.0)     # Returns 0.761...

# Special values
Sinh(-Inf)    # Returns -Inf
Sinh(Inf)     # Returns Inf
Cosh(-Inf)    # Returns Inf
Cosh(Inf)     # Returns Inf
Tanh(-Inf)    # Returns -1.0
Tanh(Inf)     # Returns 1.0

ArSinh(0.0)   # Returns 0.0
ArCosh(1.0)   # Returns 0.0
ArTanh(0.0)   # Returns 0.0

# Special values
ArSinh(-Inf)  # Returns -Inf
ArSinh(Inf)   # Returns Inf
ArCosh(Inf)   # Returns Inf
ArCosh(-1.0)  # Returns NaN (domain error)

For integer division with remainder, Verse provides Mod and Quotient. Both functions are failable—they fail when the divisor is zero.

# Signatures
# Mod(Dividend:int, Divisor:int)<decides>:int
# Quotient(Dividend:int, Divisor:int)<decides>:int

# Positive operands
Mod[15, 4]      # Returns 3
Quotient[15, 4] # Returns 3
# Relationship: 15 = 3*4 + 3

# Negative dividend
Mod[-15, 4]      # Returns 1
Quotient[-15, 4] # Returns -4
# Relationship: -15 = -4*4 + 1

# Negative divisor
Mod[-1, -2]      # Returns 1
Quotient[-1, -2] # Returns 1

# Division by zero fails
if (not Mod[10, 0]):
    Print("Cannot mod by zero")
if (not Quotient[10, 0]):
    Print("Cannot divide by zero")

The modulo result always satisfies:

Dividend = Quotient[Dividend, Divisor] * Divisor + Mod[Dividend, Divisor]

The sign of the result follows specific rules:

  • Mod result has the same sign as the divisor (Euclidean division)
  • Quotient adjusts accordingly to maintain the identity

There are also some utility functions:

# Signatures
# Sgn(X:int):int
# Sgn(X:float):float

Sgn(10)       # Returns 1
Sgn(0)        # Returns 0
Sgn(-5)       # Returns -1

Sgn(3.14)     # Returns 1.0
Sgn(0.0)      # Returns 0.0
Sgn(-2.71)    # Returns -1.0

# Special float values
Sgn(Inf)      # Returns 1.0
Sgn(-Inf)     # Returns -1.0
Sgn(NaN)      # Returns NaN

Lerp interpolates between two values:

# Signature
# Lerp(From:float, To:float, Parameter:float):float

Lerp(0.0, 10.0, 0.0)    # Returns 0.0 (0% = From)
Lerp(0.0, 10.0, 0.5)    # Returns 5.0 (50%)
Lerp(0.0, 10.0, 1.0)    # Returns 10.0 (100% = To)
Lerp(0.0, 10.0, 2.0)    # Returns 20.0 (extrapolation)
Lerp(10.0, 20.0, 0.3)   # Returns 13.0

# Works with negative ranges
Lerp(-10.0, 10.0, 0.5)  # Returns 0.0

The formula is: From + Parameter * (To - From)

IsFinite checks if a float is finite and returns true if the value is not NaN, Inf, or -Inf:

# Method on float values
# X.IsFinite():logic

(5.0).IsFinite[]      # Returns true
(0.0).IsFinite[]      # Returns true
(-100.0).IsFinite[]   # Returns true

not (Inf).IsFinite[]  # Returns false
not (-Inf).IsFinite[] # Returns false
not (NaN).IsFinite[]  # Returns false

# Useful for validation
# SafeCalculation(X:float, Y:float)<decides>:float =
#     X.IsFinite[] and Y.IsFinite[]
#     Result := X / Y
#     Result.IsFinite[]
#     Result

Verse provides constants for common mathematical values:

PiFloat # 3.14159265358979323846...
Inf     # Positive infinity
-Inf    # Negative infinity (negation of Inf)
NaN     # Not a Number

Booleans

The logic type represents the Boolean values true and false.

A:logic = true
B := false

# A = B          # fails
A?                # succeeds
# B?             # fails

true?             # succeeds
# false?         # fails

The logic type only supports query operations and comparison operations. Query expressions use the query operator ? to check if a logic value is true and fail if the logic value is false. For comparison operations, use the failable operator = to test if two logic values are the same, and <> to test for inequality.

Many programming languages find it idiomatic to use a type like logic to signal the success or failure of an operation. In Verse, we use success and failure instead for that purpose, whenever possible. The conditional only executes the then branch if the guard succeeds:

if (TargetLocked?):
    ShowTargetLockedIcon()

To convert an expression that has the <decides> effect to true on success or false on failure, use logic{ exp }:

GotIt := logic{GetRandomInt(0, Frequency) <> 0}   # if success
GotIt?                                            # then this succeeds
GotIt = false                                     # and this fails
not GotIt?                                        # and this fails too

Characters and Strings

Text is represented in terms of characters and strings. A char is a single UTF-8 code unit (not a full Unicode code point). A string is therefore an array of characters, written as []char. For convenience, the type alias string is provided for []char:

MyName :string = "Joseph"
MyAlterEgo := "José"

UTF-8 is used as the character encoding scheme. Each UTF-8 code unit is one byte. A Unicode code point may require between one and four code units. Code points with lower values use fewer bytes, while higher values require more.

For example:

  • "a" requires one byte ({0o61}),
  • "á" requires two bytes ({0oC3}{0oA1}),
  • "🐈" (cat emoji) requires four bytes ({0u1f408}).

Thus, strings are sequences of code units, not necessarily sequences of Unicode characters in the abstract sense.

Because strings are arrays of char, you can index into them with []. Indexing has the <decides> effect: it succeeds when the index is valid and fails otherwise.

TheLetterJ := MyName[0]     # succeeds
TheLetterJ = 'J'            # succeeds
# MyName[100]              # fails

The length of a string is the number of UTF-8 code units it contains, accessed via .Length. Note that this is not the same as the number of Unicode characters:

"José".Length = 5           # succeeds; 5 UTF-8 code units
"Jose".Length = 4           # succeeds; 4 UTF-8 code units

Because string is just []char, strings declared as var can be mutated:

var OuterSpaceFriend :string = "Glorblex"
set OuterSpaceFriend[0] = 'F'

Strings can be concatenated using the + operator:

MyAttemptAtFormatting := "My name is " + MyName + " but my alter ego is " + MyAlterEgo + "."

Verse also supports string interpolation for more readable formatting:

Formatting := "My name is {MyName} but my alter ego is {MyAlterEgo}."

Interpolation works for any value that has a ToString() function in scope.

Literal characters are written with single quotes. The type depends on whether the character falls within the ASCII range (U+0000U+007F) or not:

  • 'e' has type char,
  • 'é' has type char32.
A :char = 'e'                       # ok
B :char32 = 'é'                     # ok
# C :char = 'é'                     # error: type of 'é' is char32
# D :char32 = 'e'                   # error: type of 'e' is char

Character literals can also be written using numeric escape sequences:

E :char = 0o65                      # ok; same as 'e'
F :char32 = 0u00E9                  # ok; same as 'é'
  • char represents a single UTF-8 code unit (one byte, 0oXX).
  • char32 represents a full Unicode code point (0uXXXXX).

Hex notation:

  • 0oXX for char: two hex digits (0o00 to 0off)
  • 0uXXXXX for char32: up to six hex digits (0u00000 to 0u10ffff)

Unlike some languages, Verse does not allow implicit conversion between characters and integers.

Character escape sequences work in both character and string literals:

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 U+007B
\} Right brace U+007D
\< Less than U+003C
\> Greater than U+003E
\& Ampersand U+0026
\# Hash/pound U+0023
\~ Tilde U+007E

Examples:

Tab := '\t'
Newline := '\n'
Quote := '\"'
Brace := '\{'

Strings can be compared using the failable operators = (equality) and <> (inequality). Comparison is done by code point, and is case sensitive. Equality depends on exact code unit sequences, not visual appearance. Unicode allows multiple encodings for the same abstract character. For example, "é" may appear as the single code point {0u00E9}, or as the two-code-point sequence "e" ({0u0065}) plus a combining accent ({0u0301}). These two strings look the same, but they are not equal in Verse.

Checking whether a player has selected the correct item:

ExpectedItemInternalName :string = "RedPotion"
SelectedItemInternalName :string = "BluePotion"

if (SelectedItemInternalName = ExpectedItemInternalName):
    true
else:
    false

Padding a timer with leading zeros:

SecondsLeft :int = 30
SecondsString :string = ToString(SecondsLeft)    # convert int to string

var Combined :string = "Time Remaining: "
if (SecondsString.Length > 2):
    set Combined += "99"               # clamp to maximum
else if (SecondsString.Length < 2):
    set Combined += "0{SecondsString}" # pad with zero
else:
    set Combined += SecondsString

String interpolation supports complex expressions, not just simple variables:

# Expression interpolation
Age := 30
Message := "Next year: {Age + 1}"

# Function calls with named arguments
Distance := 5.5
Formatted := "Distance: {Format(Distance, ?Decimals:=2)}"

Strings can span multiple lines using interpolation braces for continuation:

LongMessage := "This is a multi-line{
}string that continues across{
}multiple lines."

Empty interpolants {} are ignored, which is useful for line continuation without adding content.

Since string is []char, strings and character arrays can be compared:

"abc" = array{'a', 'b', 'c'}    # Succeeds
"" = array{}                     # Succeeds - empty string equals empty array

Block comments within strings are removed during parsing:

Text := "abc<#this comment is removed#>def"    # Same as "abcdef"

ToString()

The ToString() function converts values to their string representations. It's polymorphic—multiple overloads exist for different types:

# Signatures
ToString(X:int):string
ToString(X:float):string
ToString(X:char):string
ToString(X:string):string  # Identity function

String interpolation implicitly calls ToString() on embedded values:

Age := 25
Score := 98.5

# These are equivalent:
Message1 := "Age: " + ToString(Age) + ", Score: " + ToString(Score)
Message2 := "Age: {Age}, Score: {Score}"
# Both produce: "Age: 25, Score: 98.5"

This makes ToString() essential for formatting output, even when you don't call it directly.

ToString() only works on primitive types. User-defined classes and structs don't have automatic string conversion.

ToDiagnostic()

The ToDiagnostic() function converts values to diagnostic string representations, useful for debugging and logging. While similar to ToString(), it may provide more detailed or implementation-specific information:

# Usage (exact signature depends on type)
DiagnosticText := ToDiagnostic(SomeValue)

ToDiagnostic() is primarily used for debugging output rather than user-facing strings. The exact format it produces may vary between VM implementations and is not guaranteed to be stable across versions.

Type type

The type type is a metatype - a type whose values are themselves types. Every Verse type can be used as a value of type type. This enables powerful generic programming through parametric functions, where types are parameters that can be passed around and constrained.

You can create variables and parameters that hold type values:

# Variable holding a type value
IntType:type = int
StringType:type = string
# Function that takes a type as parameter
CreateDefault(t:type):?t = false
# Usage
X:?int = CreateDefault(int)      # T = int, returns false
Y:?string = CreateDefault(string)  # T = string, returns false

All Verse types can be type values:

# Primitives
PrimitiveType:type = int

# User-defined types
MyClass := class {}
ClassType:type = MyClass

MyStruct := struct {Value:int}
StructType:type = MyStruct

# Collection types
ArrayType:type = []int
MapType:type = [string]int
TupleType:type = tuple(int, string)
OptionType:type = ?int

# Function types
FuncType:type = int->string

# Parametric types
generic_class(t:type) := class {Data:t}
ParametricType:type = generic_class(int)

# Metatypes
SubtypeValue:type = subtype(MyClass)

# Type literals
TypeLiteralValue:type = type{_(:int):string}

This universality makes type the foundation for Verse's generic programming - any type can be abstracted over.

Type Parameters

The most common use of type is in where clauses to create parametric (generic) functions:

# Identity function - works with any type
Identity(X:t where t:type):t = X

# Usage - type parameter inferred
Identity(42)        # t = int
Identity("hello")   # t = string
Identity(true)      # t = logic

The where t:type constraint means "t can be any Verse type." The type system infers t from the argument and ensures type safety throughout the function.

While where t:type accepts any type, you can use more specific constraints like subtype to limit which types are valid:

# Only accepts types that are subtypes of comparable
Sort(Items:[]t where t:subtype(comparable)):[]t =
    # Can use comparison operations because t is comparable
    ...

For comprehensive documentation on parametric functions, see the Functions chapter.

Type as First-Class Values

Unlike many languages where types only exist at compile time, Verse treats types as first-class values that can be computed, stored, and manipulated:

# Function that returns a type value
GetTypeForSize(Size:int):type =
    if (Size <= 8):
        int
    else:
        string

# Store type in data structure
TypeRegistry:[string]type = map{
    "Integer" => int,
    "Text" => string,
    "Flag" => logic
}

Passing types between functions:

# Helper function that takes a type parameter
CreateArray(ElementType:type, Size:int):[]ElementType =
    # This pattern works in some contexts
    ...

# Function that uses the helper
MakeIntArray():[]int =
    CreateArray(int, 10)

Returning Options of Type Parameters

A common pattern is to have functions return ?t where t is a type parameter, allowing the function to work with any type while potentially failing:

# Function that might produce a value of any type
MaybeValue(T:type, Condition:logic):?T =
    if (Condition):
        # Cannot construct T generically, return failure
        false
    else:
        false

# Specific usage
X:?int = MaybeValue(int, false)  # Returns false as ?int

This pattern is particularly useful for generic containers and factory functions that may or may not be able to produce a value.

Type Constraints

The type constraint in where clauses is the most permissive - it accepts any Verse type. For more specific requirements, Verse provides additional constraints:

# Most permissive: any type
Generic(X:t where t:type):t = X

# More specific: must be subtype of comparable
RequiresComparison(X:t where t:subtype(comparable))<decides>:void =
    X = X  # Can use = because t is comparable

# Even more specific: must be exact subtype
RequiresExactType(X:t, Y:u where t:type, u:subtype(t)):t =
    X  # Y is guaranteed to be compatible with t

The type system enforces these constraints at compile time, preventing invalid type usage.

Limitations

While type enables powerful abstractions, there are some limitations:

Cannot construct arbitrary types generically:

# Cannot do this - no way to construct a value of arbitrary type t
# MakeValue(T:type):T = ???  # What would this return for T=int? T=string?

Cannot inspect type structure at runtime:

# Cannot do this - no runtime type introspection
# GetFieldNames(T:type):[]string = ???

Type parameters must be inferred or explicit:

# Type parameter must be determinable from usage
Identity(X:t where t:type):t = X

# OK: t inferred from argument
Identity(42)

# ERROR: t cannot be inferred from no arguments
# MakeDefault(where t:type):t = ???

Any

The any type is the supertype of all types. Every type in the language is a subtype of any. Because of this, any itself supports very few operations: whatever functionality any provides must also be implemented by every other type. In practice, there is very little you can do directly with values of type any. Still, it is important to understand the type, because it sometimes arises when working with code that mixes different kinds of values, or when the type checker has no more precise type to assign.

One way any appears is when combining values that do not share a more specific supertype. For example:

Letters := enum:
    A
    B
    C

letter := class:
    Value : char
    Main(Arg : int) : void =
        X := if (Arg > 0) then:
            Letters.A
        else:
            letter{Value := 'D'}

In this example, X is assigned either a value of type Letters or of type letter. Since these two types are unrelated, the compiler assigns X the type any, which is their lowest common supertype.

A more useful role for any is as the type of a parameter that is required syntactically but not actually used. This pattern can arise when implementing interfaces that require a certain method signature.

FirstInt(X:int, :any) : int = X

Here, the second parameter is ignored. Because it can be any value of any type, it is given the type any.

In more general code, the same idea can be expressed using parametric types, making the function flexible while still precise:

First(X:t, :any where t:type) : t = X

This version works for any type t, returning a value of type t while discarding the unused argument of type any.

Void

The void type is the empty type. Unlike any, which contains all possible values, void contains none. It represents the absence of a value and is used in places where no result is returned.

Because void has no values, you can never construct or assign a value of type void. This makes it useful as a marker type in function signatures and control flow.

A function whose purpose is to perform an effect, rather than compute a value, has return type void.

LogMessage(Msg:string) : void =
    Print(Msg)

Here, LogMessage performs an action (printing) but does not return a result. The void return type makes that explicit.