Enums

Enums

Enums define a type with a fixed set of named variants.

Defining an Enum

enum Color { Red, Green, Blue }

Each variant is automatically assigned an integer value starting from 0:

  • Red = 0
  • Green = 1
  • Blue = 2

Explicit values and integer decoding

Enums without payloads can assign integer literals to variants:

enum Kind { Data = 21, Next, Heartbeat = 7, Unknown = -1 }

The default representation is i32. Choose a different integer type after the enum name to control its size and signedness:

enum Color u8 {
    RED = 1,
    GREEN = 2,
    BLUE = 3
}

Color occupies one byte, including when stored in arrays and class fields. The supported types are i8, u8, i16, u16, i32, u32, i64, and u64. The representation remains an implementation detail of the enum's storage: Color is still a distinct type from u8.

An omitted value is zero for the first variant, or one greater than the preceding variant (Next is 22 above). Negative and hexadecimal literals are supported. Values must be unique and fit in the chosen integer type; an automatic increment past its maximum is an error. Unsigned enums reject negative values. Both explicit values and custom integer types require an enum without payloads.

Read a value with _convert<i32>(Kind.Data). To decode an integer, use _enum_from_int<Kind>(value), which returns std.Option<Kind> and requires the standard library:

using std;
 
/* Decode a protocol identifier, using zero for an unknown identifier. */
function read_kind(value: u8) i32 {
    return match _enum_from_int<Kind>(value) {
        Option.Some(kind) => match kind {
            Kind.Data => 1,
            Kind.Next => 2,
            Kind.Heartbeat => 3,
            Kind.Unknown => 4
        },
        Option.None => 0
    };
}

Decoding compares the original integer before narrowing, so an oversized integer cannot wrap into a valid variant. _convert<Kind>(value) is not allowed.

Using Enums

Variables

Declare variables with an enum type and access variants using dot notation:

var c: Color = Color.Green;

An enum declared in a module can be named through its module path, the same way a module's classes and functions can, or by its bare name after a using:

using std;
 
var mode = std.io.FileMode.Write;   // through the module path
var file = std.io.File();
file.open("notes.txt", mode);

Comparison

Enum values can be compared with == and !=:

function is_red(c: Color) bool {
    return c == Color.Red;
}
 
function main() i32 {
    var c: Color = Color.Blue;
    if (c != Color.Red) {
        return 1;
    }
    return 0;
}

Enums as Function Parameters

Enums can be passed to and returned from functions:

enum Direction { Up, Down, Left, Right }
 
function opposite(d: Direction) Direction {
    if (d == Direction.Up) { return Direction.Down; }
    if (d == Direction.Down) { return Direction.Up; }
    if (d == Direction.Left) { return Direction.Right; }
    return Direction.Left;
}
 
function main() i32 {
    var dir: Direction = Direction.Up;
    var opp: Direction = opposite(dir);
    if (opp == Direction.Down) {
        return 1;
    }
    return 0;
}

Multiple Enums

You can define multiple enums in the same file:

enum Status { Pending, Running, Completed }
enum Priority { Low, Medium, High }
 
function main() i32 {
    var s: Status = Status.Running;
    var p: Priority = Priority.High;
    return 0;
}

Enums in Classes

Enums can be used as field types in classes:

enum TaskStatus { Pending, InProgress, Done }
 
class Task {
    var status: TaskStatus;
 
    init() {
        this.status = TaskStatus.Pending;
    }
 
    method start() void {
        this.status = TaskStatus.InProgress;
    }
 
    method complete() void {
        this.status = TaskStatus.Done;
    }
 
    method is_done() bool {
        return this.status == TaskStatus.Done;
    }
}

Pattern Matching with Enums

Combine enums with match expressions for clean branching:

enum Color { Red, Green, Blue }
 
function to_rgb(c: Color) i32 {
    return match c {
        Color.Red => 0xFF0000,
        Color.Green => 0x00FF00,
        Color.Blue => 0x0000FF,
        _ => 0
    };
}

See Match Expressions for more details.

Payload-Carrying Variants

Variants can carry data, turning an enum into a tagged union:

enum Shape {
    Circle(f64),          // radius
    Rect(f64, f64),       // width, height
    Empty
}
 
var c = Shape.Circle(2.5);
var r = Shape.Rect(3.0, 4.0);

The only way to reach a payload is a match with a destructuring pattern, which binds each payload position to a fresh immutable local (use _ to skip a position):

function area(s: ref Shape) f64 {
    return match s {
        Shape.Circle(r) => 3.14 * r * r,
        Shape.Rect(w, h) => w * h,
        Shape.Empty => 0.0
    };
}

Matches on enums are checked for exhaustiveness: every variant must be covered, or a _ arm must be present. The error names the missing variants.

Rules for payload enums:

  • They are value types with the same ownership rules as classes: pass by ref to borrow, pass by value to move, returned by value (moved to the caller).
  • == / != are not defined on them — match is the eliminator.
  • Payload types may be primitives, pointers, other enums, interfaces, and classes — including classes that own heap memory (String, Vec<T>, anything with deinit). Reference payloads borrow their referents and cannot outlive them. Arrays and recursive payloads without indirection are rejected.
  • Payload-free enums are unchanged: plain i32 values with == comparison, usable in extern "C" signatures. Payload enums cannot cross the C boundary yet.

Ownership and Drops

Sun never implicitly copies a compound value, and payload enums follow that rule throughout:

  • Construction moves. Holder.Hold(owner) moves owner into the enum; using owner afterwards is a use-after-move error.
  • Assignment moves and drops. var b = a; and b = a; move a (it cannot be used again). Overwriting a variable or field that already holds a payload drops the old payload first.
  • Drops are automatic. When an enum whose payload owns resources goes out of scope (block end, loop iteration, return, break/continue, or an exception unwinding through the frame), its live payload is dropped exactly once — the compiler synthesizes a per-enum drop routine that switches on the tag.
  • Moving a payload consumes its enum. If any reachable arm takes ownership of a compound payload, the match consumes the owned enum and gives its compound payloads to the selected arm's bindings. Bindings can move into a result, another variable, or a by-value argument. Remaining payloads, including _ positions, are dropped when the arm exits. The original enum cannot be used again.
  • Inspection borrows automatically. If no reachable arm takes ownership of a payload, the bindings borrow in place and the enum remains available. Matching an explicit reference always borrows and rejects payload moves. Scalar payloads are read by value.
var maybe = Option.Some(String(alloc, "hello"));
var text = match maybe {
    Option.Some(s) => s,
    Option.None => String(alloc, "fallback")
};
// text now owns the string; maybe has been consumed.

To inspect without consuming:

function length(maybe: const ref Option<String>) i64 {
    return match maybe {
        Option.Some(s) => s.length(),
        Option.None => 0
    };
}

Generic Enums

Enums can take type parameters, making reusable shapes like options and results expressible:

enum Option<T> { Some(T), None }
// _Result<T, E> is builtin; It is independent of the standard library.
 
function find(x: i32) Option<i32> {
    if (x > 0) { return Option.Some(x * 2); }  // T inferred from the argument
    return Option.None;                        // T from the return type
}
 
var a = Option.Some(21);           // Option<i32>, inferred
var b: Option<f64> = Option.None;  // type arguments from the annotation

Type arguments are inferred from payload arguments where possible; a bare unit variant like Option.None takes them from the expected type (a variable annotation, the function return type, or an assigned field), and it is an error when no context determines them. Each specialization is a distinct type — Option<i32> and Option<f64> coexist independently — and matches use the generic name in patterns:

match a {
    Option.Some(v) => v,
    Option.None => 0
}

Not yet supported: explicit type arguments in expression position (Option<i32>.Some(5) — use inference or an annotation), nested patterns, and arrays or globals of payload enums. These are planned follow-ups.

Trailing Commas

Trailing commas are allowed in enum definitions:

enum Days {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}