Skip to content

Structs and Enums

Structs and enums represent Verse's value-oriented type system, providing lightweight alternatives to classes for simple data aggregation and fixed sets of named values. Unlike classes with their object-oriented features, structs and enums focus on simplicity, immutability, and value semantics.

Structs bundle related data without methods or inheritance, perfect for mathematical types, configuration data, and simple records. Enums define fixed sets of named constants, replacing magic numbers with meaningful names and providing compile-time safety through exhaustive pattern matching.

Together, structs and enums complement classes and interfaces by offering simpler, more constrained type constructors optimized for specific use cases.

Structs

Structs provide lightweight data containers without the object-oriented features of classes. They're value types optimized for simple data aggregation, making them perfect for mathematical types, data transfer objects, and any scenario where you need a simple bundle of related values without behavior.

Structs group related data with minimal overhead:

vector2 := struct:
    X : float = 0.0
    Y : float = 0.0

color := struct:
    R : int = 0
    G : int = 0
    B : int = 0
    A : int = 255  # Alpha channel

damage_info := struct:
    Amount : int = 0
    Type : damage_type = damage_type.Physical
    Source : ?character = false
    IsCritical : logic = false

All struct fields are public and immutable by default. Structs cannot have methods, constructors, or participate in inheritance hierarchies. This simplicity makes them efficient and predictable.

Construction

Creating struct instances uses the same archetype syntax as classes:

Origin := vector2{}  # Uses defaults: (0.0, 0.0)
PlayerPos := vector2{X := 100.0, Y := 250.0}
RedColor := color{R := 255}  # Other channels default to 0/255

# Structs are values - assignment creates a copy
NewPos := PlayerPos
# NewPos is a separate instance with the same values

Since structs are value types, assigning a struct to a variable creates a copy of all its data. This differs from classes, which use reference semantics.

Comparison

Structs with all comparable fields support equality comparison:

vector3i := struct:
    X : int = 0
    Y : int = 0
    Z : int = 0

Origin := vector3i{}
UnitX := vector3i{X := 1}

if (Origin = vector3i{}):  # Succeeds - all fields match
    Print("At origin")

if (Origin = UnitX):  # Fails - X fields differ
    Print("Same position")

Comparison happens field by field, succeeding only if all corresponding fields are equal.

Persistable Structs

Structs can be marked as persistable for use with Verse's persistence system:

player_stats := struct<persistable>:
    HighScore : int = 0
    GamesPlayed : int = 0
    WinRate : float = 0.0

# Can be used in persistent storage
PlayerData : weak_map(player, player_stats) = map{}

Once published, persistable structs cannot be modified, ensuring data compatibility across game updates.

Enums

Enums define types with a fixed set of named values, perfect for representing states, types, or any concept with a known, finite set of alternatives. They make code more readable by replacing magic numbers with meaningful names and provide compile-time safety by restricting values to the defined set.

An enum lists all possible values for a type:

game_state := enum:
    MainMenu
    Playing
    Paused
    GameOver

damage_type := enum:
    Physical
    Fire
    Ice
    Lightning
    Poison

direction := enum:
    North
    East
    South
    West

Each value in the enum becomes a named constant of that enum type. The compiler ensures that variables of an enum type can only hold one of these defined values. Enums can even be empty:

placeholder := enum{}  # Valid but rarely useful

Enums introduce both a type and a set of values, and it's crucial to distinguish between them:

status := enum:
    Active
    Inactive

# status is the TYPE
# status.Active and status.Inactive are VALUES

CurrentStatus:status = status.Active  # OK - value of type status

You cannot use the enum type where a value is expected:

# ERROR: Cannot use type as value
BadAssignment:status = status  # Compile error
set CurrentStatus = status     # Compile error

# CORRECT: Use enum values
GoodAssignment:status = status.Active  # OK
set CurrentStatus = status.Inactive    # OK

This distinction prevents confusion and ensures type safety. The enum type defines what values are possible, while enum values are the actual constants you use in your code.

Restrictions

Enums have specific syntactic requirements that keep their usage clear and unambiguous:

Enums must be direct right-hand side of definitions:

# Valid
Priority := enum:
    Low
    Medium
    High

# Invalid - cannot use enum in expressions
Result := -enum{A, B}      # Compile error
Value := enum{X, Y} + 1    # Compile error

Enums must be module or class-level definitions:

# Valid
MyEnum := enum:
    Value1
    Value2

# Invalid - cannot define local enums
ProcessData():void =
    LocalEnum := enum{A, B}  # Compile error - no local enums

These restrictions ensure enums remain stable, referenceable definitions throughout your codebase rather than ephemeral local values.

Using Enums

Enums provide type-safe alternatives to error-prone string or integer constants:

var CurrentState:game_state = game_state.MainMenu

ProcessInput(Input:string):void =
    case (CurrentState):
        game_state.MainMenu =>
            if (Input = "Start"):
                set CurrentState = game_state.Playing
        game_state.Playing =>
            if (Input = "Pause"):
                set CurrentState = game_state.Paused
        game_state.Paused =>
            if (Input = "Resume"):
                set CurrentState = game_state.Playing
            else if (Input = "Quit"):
                set CurrentState = game_state.MainMenu
        game_state.GameOver =>
            if (Input = "Restart"):
                set CurrentState = game_state.MainMenu

The case expression with enums provides powerful pattern matching with exhaustiveness checking that ensures you handle all possible values correctly.

Open vs Closed Enums

Enums can be marked as open or closed, fundamentally affecting how they can evolve and how they interact with pattern matching:

# Closed enum - cannot add values after publication
day_of_week := enum<closed>:  # <closed> is the default
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday

# Open enum - can add new values after publication
weapon_type := enum<open>:
    Sword
    Bow
    Staff
    # Can add Wand, Dagger, etc. in updates

Closed enums (the default) commit to a fixed set of values forever. This allows the compiler to verify that case expressions handle all possibilities exhaustively. Use closed enums for truly fixed sets: days of the week, cardinal directions, fundamental game states.

Open enums allow new values to be added in future versions. This flexibility comes at a cost: case expressions cannot be exhaustive since future values might exist. Use open enums for extensible sets: item types, enemy types, damage types, or any content that may grow.

Exhaustiveness

The interaction between enum types and case expressions follows sophisticated rules that prevent bugs while enabling both safety and flexibility. Understanding these rules is essential for working with enums effectively.

Closed Enums with Full Coverage:

When your case expression handles every value in a closed enum, no wildcard is needed:

day := enum:
    Monday
    Tuesday
    Wednesday

# Exhaustive - all values covered
GetDayType(D:day):string =
    case (D):
        day.Monday => "Weekday"
        day.Tuesday => "Weekday"
        day.Wednesday => "Weekday"
    # No wildcard needed - all values handled

Adding a wildcard when all cases are covered triggers an unreachable code warning:

# Warning: unreachable wildcard
GetDayType(D:day):string =
    case (D):
        day.Monday => "Weekday"
        day.Tuesday => "Weekday"
        day.Wednesday => "Weekday"
        _ => "Unknown"  # WARNING: unreachable - all values already matched

Closed Enums with Partial Coverage:

If you don't match all values, you must either provide a wildcard or be in a <decides> context:

day := enum:
    Monday
    Tuesday
    Wednesday
    Thursday

# With wildcard - OK
GetWeekStartWildCard(D:day):string =
    case (D):
        day.Monday => "Week start"
        _ => "Mid-week"

# Without wildcard but in <decides> context - OK
GetWeekStartDecides(D:day)<decides>:string =
    case (D):
        day.Monday => "Week start"
        # Missing other days causes failure

# Without either - COMPILE ERROR
# GetWeekStartBad(D:day):string =
#    case (D):
#        day.Monday => "Week start"
#        # ERROR: Missing cases and no wildcard

Open Enums Always Require Wildcard or <decides>:

Open enums can have new values added after publication, so they can never be exhaustive:

weapon := enum<open>:
    Sword
    Bow
    Staff

# Must have wildcard - OK
GetWeaponClassWildCard(W:weapon):string =
    case (W):
        weapon.Sword => "Melee"
        weapon.Bow => "Ranged"
        weapon.Staff => "Magic"
        _ => "Unknown"  # REQUIRED - future values may exist

# In <decides> context without wildcard - OK
GetWeaponClassDecides(W:weapon)<decides>:string =
    case (W):
        weapon.Sword => "Melee"
        weapon.Bow => "Ranged"
        weapon.Staff => "Magic"
        # Can fail for unknown (future) values

# Without either - COMPILE ERROR
# GetWeaponClassBad(W:weapon):string =
#    case (W):
#        weapon.Sword => "Melee"
#        weapon.Bow => "Ranged"
#        weapon.Staff => "Magic"
#        # ERROR: Open enum requires wildcard or <decides>

Even if you match all currently defined values in an open enum, you still need a wildcard or <decides> context because new values might be added in future versions.

Summary of Exhaustiveness Rules:

Enum Type Case Coverage Wildcard Context Result
Closed Full No Any βœ“ Valid - exhaustive
Closed Full Yes Any ⚠ Warning - unreachable wildcard
Closed Partial Yes Any βœ“ Valid
Closed Partial No <decides> βœ“ Valid - unmatched values fail
Closed Partial No Non-<decides> βœ— Error - missing cases
Open Any Yes Any βœ“ Valid
Open Any No <decides> βœ“ Valid - unmatched values fail
Open Any No Non-<decides> βœ— Error - open enum needs wildcard

These rules ensure that closed enums provide safety through exhaustiveness while open enums require explicit handling of unknown values.

Unreachable Case Detection

The compiler actively detects unreachable cases in case expressions, helping you identify dead code and logic errors:

Duplicate cases are flagged as unreachable:

status := enum:
    Active
    Inactive
    Pending

# ERROR: Duplicate case is unreachable
GetStatusCode(S:status):int =
    case (S):
        status.Active => 1
        status.Inactive => 2
        status.Pending => 3
        status.Pending => 4  # ERROR: unreachable - already matched above

Cases after wildcards are always unreachable:

# ERROR: Case after wildcard
GetStatusCode(S:status):int =
    case (S):
        status.Active => 1
        _ => 0  # Wildcard matches everything
        status.Inactive => 2  # ERROR: unreachable - wildcard already matched

These errors prevent logic bugs where you think you're handling specific cases but the code will never execute.

The @ignore_unreachable Attribute

Sometimes you intentionally want unreachable casesβ€”for testing, migration, or defensive programming. The @ignore_unreachable attribute suppresses unreachable warnings and errors for specific cases:

status := enum:
    Active
    Inactive

ProcessStatus(S:status):int =
    case (S):
        status.Active => 1
        status.Inactive => 2
        @ignore_unreachable status.Inactive => 3  # No error
        @ignore_unreachable _ => 0  # No unreachable warning

This attribute only affects cases it's applied to. Other unreachable cases without the attribute still produce errors:

ProcessStatus(S:status):int =
    case (S):
        status.Active => 1
        status.Inactive => 2
        @ignore_unreachable status.Inactive => 3  # Suppressed
        status.Active => 4  # ERROR: still unreachable without attribute

Use @ignore_unreachable sparingly, primarily during refactoring or when maintaining multiple code paths for testing purposes.

Explicit Qualification

Enumerators can collide with identifiers in parent scopes. When this happens, you can use explicit qualification to disambiguate:

# Top level 'Start'
Start:int = 0

# Enum wants to use 'Start' as enumerator
game_state := enum:
    (game_state:)Start  # Explicit qualification avoids collision
    Playing
    Paused

# Now both are accessible
OuterStart:int = Start             # References the int
StateStart:game_state = game_state.Start  # References the enum value

The syntax (enum_name:)enumerator explicitly qualifies the enumerator, preventing conflicts with outer-scope symbols.

Using Reserved Words as Enum Values:

Qualification also allows you to use reserved words and keywords as enum values, which would otherwise cause errors:

# Using reserved words as enum values
keyword_enum := enum:
    (keyword_enum:)public    # OK: reserved word qualified
    (keyword_enum:)for       # OK: keyword qualified
    (keyword_enum:)class     # OK: reserved word qualified
    Regular                  # Normal enum value

# Without qualification - errors
# bad_enum := enum:
#    public    # Error 3532: reserved word
#    for       # Error 3514: reserved keyword

This is particularly useful when modeling language constructs, access levels, or any domain where reserved words make natural value names.

Self-Referential Enum Values:

You can even use the enum's own name as a value when qualified:

recursive_enum := enum:
    (recursive_enum:)recursive_enum  # OK: qualified with enum name
    OtherValue

# Without qualification - error
# bad_recursive := enum:
  #  bad_recursive  # Error 3532: shadows the type name

Attributes on Enums

Enums support custom attributes, both on the enum type itself and on individual enumerators:

# Define my_attribute by inheriting from the attribute class
@attribscope_enum
my_attribute := class(attribute):
    MyMetaData:string = "I'm default metadata"
    # category<constructor>(Name:string)<computes> := my_attribute{}

    # Apply to enum and enumerators
@my_attribute()
game_state := enum:
    @my_attribute(MyMetaData = "Initial")
    MainMenu

    @my_attribute(MyMetaData = "Active")
    Playing

    @my_attribute(MyMetaData = "Paused")
    Paused

Attributes must be marked with the appropriate scopes (@attribscope_enum for enum types, @attribscope_enumerator for individual values) or the compiler will reject them. This provides metadata capabilities for reflection, serialization, or custom tooling.

Comparison

Enum values are fully comparable, meaning they support both equality (=) and inequality (<>) operators. This makes them ideal for state tracking and conditional logic:

CurrentWeapon := weapon_type.Sword
if (CurrentWeapon = weapon_type.Sword):
    PlaySwordAnimation()

PreviousState := game_state.Playing
if (CurrentState <> PreviousState):
    OnStateChanged(PreviousState, CurrentState)

Enum values from the same enum type can be compared, while values from different enum types are always unequal:

letters := enum:
    A, B, C

numbers := enum:
    One, Two, Three

Test()<decides>:letters =
    letters.A = letters.A    # Succeeds - same value
    letters.A <> letters.B   # Succeeds - different values
    letters.A <> numbers.One # Succeeds - different enum types

Because enums are comparable, they can be used as map keys, stored in sets, and used with generic functions that require comparable types:

# Enums as map keys
StateIDs:[game_state]int = map{
    game_state.Menu => 0,
    game_state.Playing => 1,
    game_state.Paused => 2
}

# In generic functions
FindState(States:[]game_state, Target:game_state)<decides>:int =
    for (State:States, GameState->ID : StateIDs):
        if (State = Target):
            ID
    -1