Enums

    Enums in Cyrus define a type that can be one of several variants. Each variant is tagged and may optionally carry data (payload). Enums are central for modeling sum types, error handling, and finite-state values.

    Cyrus supports:

    • Unit variants (no payload)
    • Tuple variants (tuple payload)
    • Struct variants (named-field payload)
    • Valued variants (scalar/constant-like)

    Basic Enum Declaration

    Use the enum keyword with a named variant list:

    enum Color {
        Red,
        Green,
        Black,
        Custom(int, int, int)
    }
    

    Construct values either with the enum name or using the short .Variant syntax:

    var c1 = Color.Red;
    var c2 = Color.Custom(1, 2, 3);
    
    var c3: Color = .Green;
    

    Variant Kinds

    Unit Variants

    Unit variants are simple tags representing distinct states without any extra data.

    enum ConnectionState {
        Disconnected,
        Connecting,
        Connected,
        Failed
    }
    
    fn is_online(state: ConnectionState) bool {
        switch (state) {
            case .Connected => {
                return true;
            }
            default => {
                return false;
            }
        }
    }
    

    Tuple Variants

    Tuple variants carry unnamed positional data, like a typed tuple attached to the variant.

    enum Task {
        Compute(struct {
            id: uint64,
            priority: int
        }),
        Delay(uint32),
        Range((int, int))
    }
    
    fn process_task(t: Task) {
        switch (t) {
            case .Compute(req) => {
                printf("Compute(id: %llu, priority: %d)\n", req.id, req.priority);
            }
            case .Delay(ms) => {
                printf("Delay(ms: %u)\n", ms);
            }
            case .Range(bounds) => {
                printf("Range(start: %d, end: %d)\n", bounds.0, bounds.1);
            }
            default => {
                printf("Unknown Task\n");
            }
        }
    }
    
    pub fn main() {
        var t = Task.Compute(struct { id: 1024, priority: 5 });
        process_task(t);
    
        t = Task.Range((0, 100));
        process_task(t);
    
        t = Task.Delay(500);
        process_task(t);
    }
    

    Struct Variants

    Struct variants define an inline struct layout directly inside the variant declaration. Instead of wrapping an existing type, the variant itself acts as a schema, providing named, typed fields.

    enum Error {
        NotFound = struct { id: 1uint32, msg: "not found" },
    
        Custom {
            id: uint32,
            msg: const char*,
            extra: const char*
        }
    }
    
    pub fn main() {
        const err = Error.Custom { id: 2, msg: "custom error message", extra: null };
    
        switch (err) {
            case .NotFound => {
                printf("not found\n");
            }
            case .Custom { id, msg, .. } => {
                printf("id: %d, %s\n", id, msg);
            }
        }
    }
    
    • Custom { ... } is a struct variant with named payload fields.
    • NotFound here uses a valued struct payload (see below).

    Valued Variants (Scalar / Constant-like)

    Valued variants are a special case where each variant has an associated constant value. This is typically used for scalar enums.

    type Kind = enum { Unknown = 0, Known = 1 };
    
    import std::libc{printf};
    
    enum ScalarEnum {
        A = 10,
        B = 20,
        C = 30
    }
    
    fn display_scalar_enum(value: ScalarEnum) {
        switch (value) {
            case .A(a) => {
                printf("a(%d) ", a);
            }
            case .B(b) => {
                printf("b(%d) ", b);
            }
            case .C(c) => {
                printf("c(%d) ", c);
            }
        }
    }
    
    pub fn main() {
        const x: ScalarEnum = .A;
    
        display_scalar_enum(x);
        display_scalar_enum(.B);
        display_scalar_enum(.C);
    }
    

    The compiler infers that all ScalarEnum variants are integer-valued and lowers the enum as a compact integral type (int32 by default) in codegen.

    Valued variants are also allowed to carry richer payloads (e.g., NotFound = struct { ... } in the previous example).

    Tag Type and Scalar Enums

    You can explicitly specify the underlying tag type of an enum:

    enum ScalarEnum(uint32) {
        A = 10,
        B = 20,
        C = 30
    }
    

    For unnamed scalar enums:

    const a: enum(bool) { A = true, B = false } = .A;
    

    The tag type controls the ABI-level representation of the variant tag, which can be useful for FFI and low-level layout constraints.

    Named vs Unnamed Enums

    Cyrus supports both named and unnamed enums.

    Named Enums

    Named enums define a reusable type:

    enum Option<T> {
        Some(T),
        None
    }
    
    pub fn main() {
        var opt: Option<int> = .None;
        display_option(opt);
    
        opt = .Some(10);
        display_option(opt);
    }
    

    Note: Generics (<T>) are introduced in detail in a later section.

    Unnamed Enums

    Unnamed enums can be declared inline at the use-site:

    const mode: enum { Off = 0, On = 1 } = .On;
    
    switch (mode) {
        case .Off(x) => { /* ... */ }
        case .On(x) => { /* ... */ }
    }
    

    This is useful for one-off tagged values where defining a top-level name would be overkill.

    Heterogeneous Variants

    Cyrus enums are “sum types” that allow each variant to define its own unique payload structure. You can freely combine unit variants, valued variants, tuple variants, and struct variants in a single declaration.

    import std::libc{printf};
    
    enum Color {
        Red,                     // Unit variant
        Green = "green!",        // Valued variant (scalar/constant)
        Custom(uint, uint, uint) // Tuple variant
    }
    
    pub fn main() {
        const color = Color.Green;
    
        switch (color) {
            case .Red => {
                printf("red\n");
            }
            case .Green(value) => {
                printf("%s\n", value);
            }
            case .Custom(a, b, c) => {
                printf("custom(%d, %d, %d)\n", a, b, c);
            }
        }
    }
    
    • Variant Autonomy: The compiler manages the memory layout, ensuring enough space for the largest possible payload while tracking the current active variant via a hidden tag.
    • Unified Matching: The switch statement handles these heterogeneous payloads seamlessly, providing the appropriate bindings based on the variant's specific definition.
    • Type Safety: The compiler ensures that you only attempt to access payloads that exist for the specific variant you are matching.

    Matching on Enums

    switch is the primary way to deconstruct and inspect enums. Pattern syntax depends on the variant kind.

    import std::libc{printf};
    
    type Kind = enum { Unknown = 0, Known = 1 };
    
    fn display_kind(value: Kind) {
        switch (value) {
            case .Unknown(x) => {
                printf("%d", x);
            }
            case .Known(x) => {
                printf("%d", x);
            }
        }
    }
    
    pub fn main() {
        const x: Kind = .Unknown;
    
        display_kind(.Known);
        display_kind(.Unknown);
        display_kind(x);
    }
    
    • Unit variants: case .Variant => { ... }
    • Tuple variants: case .Variant(payload) => { ... }
    • Struct variants: case .Variant { field1, field2 } => { ... }
    • Valued/scalar variants: case .Variant(value) => { ... }

    default can be used as a catch-all arm when not all variants are listed.

    For struct variants, patterns support ignoring, eliding, and renaming fields:

    switch (err) {
        // Ignore a specific exported field
        case .Custom { id, msg, extra: _ } => {
            // 'id' and 'msg' are bound; 'extra' is ignored
        }
    }
    
    switch (err) {
        // Ignore the rest of the fields
        case .Custom { id, msg, .. } => {
            // Only 'id' and 'msg' are bound
        }
    }
    
    switch (err) {
        // Rename a field locally
        case .Custom { id, msg, extra: msg2 } => {
            msg2; // VALID: bound to the 'extra' field
            extra; // ERROR: not found
        }
    }