Enums

Enums

Enums define a closed set of named variants. In their simplest form, each variant is an integer discriminant — equivalent to a C++ enum class. Variants can also carry structured data (ADT enums), making them Kairo’s mechanism for type-safe tagged unions. Dispatch on variants with match. See Control Flow for the complete match syntax.


Plain Enums

A plain enum is a set of named integer constants:

enum Direction {
    North,
    East,
    South,
    West,
}

var dir = Direction::North

Variants are comma-separated and scoped to the enum name. The trailing comma is optional.

Discriminant values

Variants are assigned sequential integers starting from 0 by default. Explicit values can be assigned with =:

enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    InternalError = 500,
    ServiceUnavailable,   // 501
}

Discriminant expressions must be compile-time constants. Duplicate values are a compile error.

Compound expressions are valid for bit flags:

enum FileMode derives u8 {
    Read    = 1 << 0,   // 1
    Write   = 1 << 1,   // 2
    Execute = 1 << 2,   // 4
    RW      = (Read | Write),       // 3
    All     = (Read | Write | Execute),  // 7
}

var perms: FileMode = .RW
if perms & FileMode::Read != 0 {
    // has read permission
}

Underlying Type

The default underlying type is u32. The compiler promotes to a wider unsigned integer if the number of variants exceeds what u32 can represent, though a warning is emitted for enums exceeding 2^32 variants.

Specify an explicit underlying type with derives followed by an integer type:

enum Opcode derives u8 {
    Nop  = 0x00,
    Load = 0x01,
    Store = 0x02,
    Halt = 0xFF,
}

Only integer types (u8, u16, u32, u64, i8, i16, i32, i64, usize, isize) are permitted. The compiler errors if any discriminant does not fit in the specified type.

Note

derives on an enum specifies the underlying integer type. This is distinct from derives on a class, which declares inheritance.


ADT Enums

Variants can carry structured data. Each payload is defined as a set of named fields inside braces:

enum <T> ParseResult {
    Success { value: T, bytes_consumed: i32 },
    Error   { message: string, position: i32 },
    EndOfInput,
}

Variants without a payload (EndOfInput above) are plain discriminants. Variants with a payload carry an anonymous struct alongside the discriminant. A single enum can freely mix both.

Construction

ADT variants are constructed with the variant name followed by brace-delimited field assignment, matching struct aggregate initialization:

var result = ParseResult<f64>::Success { value: 3.14, bytes_consumed: 4 }
var err = ParseResult<f64>::Error { message: "unexpected token", position: 12 }
var eof = ParseResult<f64>::EndOfInput

Field names are required. The order of fields in the initializer does not need to match the declaration order.

When the enum type can be inferred from context, the .Variant shorthand works:

var result: ParseResult<f64> = .Success { value: 3.14, bytes_consumed: 4 }

Destructuring with match

Use match to dispatch on variants and extract payload fields. Destructured fields use var or const to control mutability of the bound variable:

match result {
    case .Success(var value, var bytes_consumed) {
        std::println(f"parsed {value} from {bytes_consumed} bytes")
    }
    case .Error(const message, const position) {
        std::println(f"error at {position}: {message}")
        // message = "other"  // compile error: message is const
    }
    case .EndOfInput {
        std::println("nothing left to parse")
    }
}

Field names in the destructuring pattern must match the names in the variant declaration. You can omit fields you do not need the compiler does not require exhaustive field extraction:

match result {
    case .Error(const message) {
        // only message is bound, position is ignored
        log_error(message)
    }
    // ...
    default { }
}

See Control Flow for the full match syntax, including guards (where), ranges, and expression form.

match as an expression

ADT enums work in expression-form match. Each branch must produce the same type:

var description = match result {
    case .Success(var value, var bytes_consumed) {
        f"parsed {value} ({bytes_consumed} bytes)"
    }
    case .Error(var message, var position) {
        f"error at {position}: {message}"
    }
    case .EndOfInput {
        "end of input"
    }
}

See Control Flow for the full expression-form rules.

Memory layout

An ADT enum stores a discriminant tag plus storage sized to the largest variant’s payload. The tag is the same integer type as the underlying type (default u32). A variant with no payload consumes no extra storage beyond the tag.

ParseResult<f64> layout:
  [tag: u32] [payload: max(sizeof Success, sizeof Error)] [padding]

The compiler generates a destructor that switches on the tag to destroy the active variant’s fields. Copy and move operations follow the same pattern.


Generic Enums

Enums with payloads can be generic. Type parameters are declared in angle brackets before the enum name:

enum <K, V> LookupResult {
    Found    { key: K, value: V },
    NotFound { key: K },
    Error    { message: string },
}

var r = LookupResult<string, i32>::Found { key: "count", value: 42 }

Plain enums (no payloads) cannot be generic there is nothing for a type parameter to parameterize.

Constrain type parameters with impl or derives bounds:

enum <T impl Serializable> CacheEntry {
    Valid   { data: T, expires: i64 },
    Expired { last_data: T },
    Empty,
}

See Bounds for the full constraint system.


Shorthand Syntax

When the compiler can infer the enum type from context, the .Variant shorthand is available in construction, match cases, comparison, and assignment:

var dir: Direction = .North

match dir {
    case .North { go_up() }
    case .South { go_down() }
    case .East  { go_right() }
    case .West  { go_left() }
}

if dir == .North { ... }

dir = .South

The fully qualified form (Direction::North) always works and is required when the type cannot be inferred.


Comparison and Assignment

Enum values support equality comparison and assignment. Ordering operators (<, >, <=, >=) are not available by default extend them if needed:

var a = Direction::North
var b = Direction::South

if a == b { ... }    // ok
if a != b { ... }    // ok
// if a < b { ... }  // compile error: no ordering defined

For ADT enums, equality compares the tag only by default. Extend op == to include payload comparison if needed. See Operators for operator extension.


Extends

Enums cannot contain methods in their body. Use extend blocks to add behavior, matching the same pattern as structs:

enum LogLevel {
    Debug,
    Info,
    Warning,
    Error,
    Fatal,
}

extend LogLevel {
    fn is_severe(self) const -> bool {
        return self == .Error || self == .Fatal
    }

    fn prefix(self) const -> string {
        match self {
            case .Debug   { return "[DEBUG]" }
            case .Info    { return "[INFO]" }
            case .Warning { return "[WARN]" }
            case .Error   { return "[ERROR]" }
            case .Fatal   { return "[FATAL]" }
        }
    }

    static fn from_string(s: string) -> LogLevel {
        match s {
            case "debug"   { return .Debug }
            case "info"    { return .Info }
            case "warning" { return .Warning }
            case "error"   { return .Error }
            case "fatal"   { return .Fatal }
            default        { return .Info }
        }
    }
}

What extends can add

AllowedNot allowed
MethodsConstructors
Static functionsDestructors
Comparison / arithmetic operatorsCopy / move assignment
fn op as (type conversion)

Interface conformance

Enums can implement interfaces through extend ... impl:

interface Loggable {
    fn to_log_string(self) const -> string
}

extend LogLevel impl Loggable {
    fn to_log_string(self) const -> string {
        return self.prefix()
    }
}

The extend block and the enum definition must be in the same file. See Extends for the full extend system and Interfaces for interface declarations.


No Associated Data in Variants (Plain Enums Only)

For plain enums without the { field: Type } payload syntax, variants are strictly integer constants. If you need per-variant data, use an ADT enum.

For untagged memory overlays where you manage the active member yourself, use Unions.


No Inheritance

Enums do not support inheriting from other enums. The derives keyword on an enum is reserved exclusively for specifying the underlying type.


Casting

Plain enum values can be cast to their underlying integer type and back:

var dir = Direction::North
var raw = dir as u32           // 0

var back = raw as Direction    // Direction::North

Casting an integer to an enum that has no matching discriminant is undefined behavior.

ADT enums cannot be cast to integers directly they carry payloads that have no integer representation. Cast the tag explicitly if you need the discriminant value.

See Casting for the full casting model.


Forward Declarations

Enums can be forward-declared when the underlying type is specified:

enum Opcode derives u8    // forward declaration size is known

fn decode(op: Opcode) { ... }

enum Opcode derives u8 {
    Nop  = 0x00,
    Load = 0x01,
    Store = 0x02,
    Halt = 0xFF,
}

Without an explicit underlying type, forward declaration is not permitted the compiler needs the size, which depends on the variant count and payload sizes.


Summary

// Plain enum
enum Direction {
    North, East, South, West,
}

// Explicit discriminants + underlying type
enum Opcode derives u8 {
    Nop  = 0x00,
    Load = 0x01,
    Store = 0x02,
    Halt = 0xFF,
}

// Bit flags
enum FileMode derives u8 {
    Read    = 1 << 0,
    Write   = 1 << 1,
    Execute = 1 << 2,
    All     = (Read | Write | Execute),
}

// ADT enum with payloads
enum <K, V> LookupResult {
    Found    { key: K, value: V },
    NotFound { key: K },
    Error    { message: string },
}

// Construction
var r = LookupResult<string, i32>::Found { key: "count", value: 42 }

// Match with destructuring
match r {
    case .Found(var key, var value) {
        std::println(f"{key} = {value}")
    }
    case .NotFound(var key) {
        std::println(f"{key} not found")
    }
    case .Error(var message) {
        std::println(f"error: {message}")
    }
}

// Extended with methods
extend Direction {
    fn opposite(self) -> Direction {
        match self {
            case .North { return .South }
            case .South { return .North }
            case .East  { return .West }
            case .West  { return .East }
        }
    }
}