Language / enum Types

enum Types

An enum defines a tagged union type: a value that is exactly one of several named variants, optionally carrying a payload.

Use enums to model:

  • finite state machines (connection state, parser state),
  • protocol messages and events,
  • and any API where “exactly one of these cases” is the core invariant.

If your goal is “a function can fail with one of several error shapes”, prefer typed errors (typed errors) over enums.

Notes#

What works end-to-end today (parser → checker → lowering → codegen):

  • Enum declarations with:
  • unit variants (A),
  • tuple variants (Data(int) and Pair(int, int)),
  • and an optional trailing comma after the last variant.
  • Construction:
  • unit variants as values: E::A,
  • tuple variants as calls: E::Data(7).
  • type-directed shorthand:
  • when the expected type is an enum E, unit variants may be written as A (sugar for E::A),
  • when the expected type is an enum E, tuple variants may be written as Data(7) (sugar for E::Data(7)).
  • Function signatures:
  • enums may be used in parameter lists and return types,
  • error-producing functions may return enums (-> E | ErrorType...), and call()? works when the success type is an enum.
  • match expression over enums:
  • patterns are restricted to enum variants (E::A, E::Data(x)) and may use shorthand (A, Data(x)) when the scrutinee type is the enum,
  • binders may be names or _,
  • no guards (if ...) yet,
  • and expression-form matches must still be exhaustive in the current subset:
  • either by explicit arm coverage for every variant,
  • or by a single final wildcard _ arm that covers all remaining unmatched variants.
  • ?? over ordinary two-variant enums:
  • for a named enum with exactly two declared variants, value ?? fallback treats the first declared variant as the “success” arm,
  • if that first variant is unit, the expression yields that enum value,
  • if that first variant carries exactly one payload, the expression yields that payload,
  • and if the enum value is the second declared variant, the fallback expression is evaluated.
  • match statement over ordinary enum values:
  • enum variant arms must use the qualified form E::Variant(...),
  • bare identifiers remain reserved for binder-style arms,
  • and both expression-form and statement-form ordinary enum matches now allow one final _ catch-all arm, which must cover at least one remaining variant.
  • Generic enums (monomorphized):
  • enum Name(T, ...) { ... } declarations are supported in module-set builds that run monomorphization,
  • instantiated enums behave like ordinary enums once referenced (including construction and match),
  • callers typically introduce a local type alias for the instantiated enum (for example type R = Result(int, string);) and then use R::Ok(...) / R::Err(...) as constructors and patterns.
  • impl blocks on enums:
  • enums may have impl blocks (including generic impl EnumName(T, ...)),
  • static methods are callable as EnumName.method(...) (including through type aliases for instantiated generic enums),
  • instance methods are callable as value.method(...) when the first parameter is a receiver (self: &EnumName / mut self: &EnumName),
  • the special constructor method used by new Type(...) is for struct types; enums do not support constructor methods in the Supported forms.

Not implemented yet (or not yet stable/documented):

  • Guards in enum match arms (E::A if cond => ...).
  • A stable ABI story for passing/returning enums across the C99 boundary (do not assume an enum layout until it is specified in abi libsilk).

When the compiler rejects an enum construct in the Supported forms, the most common error is E2002 (“unsupported expression in the Supported forms”). Type mismatches inside enum constructors or match arms are E2001 (“type mismatch”).

Surface Syntax#

Enum declarations introduce a nominal type and its variants:

enum RecvJob {
  Msg(Job),
  Cancelled,
  Timeout,
}

Rules:

  • Variant names are identifiers and must be unique within the enum.
  • Variant names may not be the reserved optional constructors Some / None.
  • An enum must declare at least one variant.
  • A variant is either:
  • a unit variant (no payload): Cancelled,
  • or a tuple variant with one or more payload element types: Msg(Job), Pair(int, int).
  • A trailing comma after the last variant is permitted.

Construction#

Unit variants#

Unit variants are constructed as values using Enum::Variant (or, in type-directed contexts, just Variant):

enum E {
  A,
  B,
}

fn main () -> int {
  let x: E = E::A;
  let y: E = A;
  return 0;
}

Notes:

  • E::A() and A() are invalid in the Supported forms (unit variants are not callable).
  • A unit variant is an ordinary value of its enum type. It may initialize a binding or aggregate field, be returned or passed to a function, and be assigned to an existing mutable binding or struct field. These value uses accept both the qualified E::A form and the type-directed A shorthand.

Two-Variant ?? Shortcut#

When a named enum declares exactly two variants, the ?? operator may be used as a compact success/fallback form.

Rules in the Supported forms:

  • declaration order matters: the first declared variant is the “success” case, and the second declared variant triggers the fallback,
  • if the first variant is unit, value ?? fallback yields that enum value,
  • if the first variant carries exactly one payload, value ?? fallback yields that payload,
  • if the first variant carries more than one payload element, use match instead,
  • the second variant may be unit or may carry payloads; its payload is ignored by the ?? form and the fallback expression runs.
  • the right-hand side is still an expression in the current grammar; terminal statements such as return, break, and continue are not accepted there yet.

Examples:

enum Ready {
  Go,
  Stop,
}

fn require_ready (r: Ready) -> Ready {
  return r ?? Ready::Go;
}

enum ParsedPort {
  Port(int),
  Invalid,
}

fn port_or_default (p: ParsedPort) -> int {
  return p ?? 80;
}

Tuple variants#

Tuple variants are constructed using Enum::Variant(<args...>) where the argument count and types match the variant’s declared payload element types:

enum E {
  Data(int),
  Pair(int, int),
  Empty,
}

fn main () -> int {
  let a: E = E::Data(7);
  let b: E = Data(7);
  let b: E = E::Pair(1, 2);
  let c: E = E::Empty;
  return 0;
}

Notes:

  • E::Data by itself is not a value in the Supported forms (tuple variants must be constructed with (...)).
  • If a tuple-variant constructor argument has the wrong type, you get E2001.
  • If the argument count does not match the variant definition, the compiler currently rejects the construct with E2002.

Generic enums (instantiation via alias)#

When an enum is generic, callers typically alias an instantiation and then use that alias as the qualifier for constructors:

enum Result(T, E) {
  Ok(T),
  Err(E),
}

type R = Result(int, int);

fn main () -> int {
  let x: R = R::Ok(123);
  return match x {
    R::Ok(v) => v,
    R::Err(_) => 0,
  };
}

Namespaced enums (packages)#

Across packages, enums and variants may be referenced with :: qualification. For example, if util defines enum Mode { Inc, Dec }, an importer can write:

  • util::Mode as the type name, and
  • util::Mode::Inc / util::Mode::Dec as the constructors and patterns.

See packages imports exports for module-set rules and for how package imports seed qualified type names.

Matching#

Enum values are typically consumed via match expressions. The match expression rules are defined in flow match; this section focuses on the enum-specific subset.

Patterns#

Enum patterns are variant patterns:

  • Unit variant: E::A
  • Tuple variant: E::Data(x), E::Pair(a, b)
  • Tuple binder omission: E::Pair(_, b) (underscore binder ignores that element)

Shorthand:

  • When the scrutinee type is the enum E, the qualifier may be omitted:
  • Unit variant: A
  • Tuple variant: Data(x), Pair(a, b)

For instantiated generic enums, the qualifier E may be a type alias (for example type R = Result(int, string); then R::Ok(v) / R::Err(e)).

Binders:

  • introduce a name scoped to that arm only, and
  • shadow outer bindings of the same name (because they create a new binding in the arm’s environment).

Exhaustiveness#

In the Supported forms, enum exhaustiveness is split by match form:

  • Expression form:
  • either there is exactly one arm per enum variant,
  • or a single final wildcard arm (_ => ...) covers the remaining unmatched variants,
  • explicit variant arms must not repeat the same variant,
  • and if _ is used, it may appear at most once and must be the final arm.
  • Statement form over ordinary enum values:
  • either there is exactly one arm per enum variant,
  • or a single final wildcard arm (_ => ...) covers the remaining unmatched variants,
  • explicit variant arms must not repeat the same variant,
  • and if _ is used, it may appear at most once and must be the final arm.

If a match is not exhaustive:

  • expression form currently reports E2002, and
  • statement form without full coverage uses the checker’s missing-arm path when it reaches that analysis.

Example: unit enum match#

enum E {
  A,
  B,
}

fn main () -> int {
  let v: E = E::A;

  let x: int = match v {
    E::A => 10,
    E::B => 20,
  };

  if x != 10 {
    return 1;
  }
  return 0;
}

Example: unit enum match with final wildcard#

enum State {
  Ready,
  Busy,
  Closed,
}

fn score (s: State) -> int {
  return match s {
    Ready => 10,
    _ => 20,
  };
}

Example: tuple enum match (payload binders)#

enum E {
  Pair(int, int),
  Empty,
}

fn main () -> int {
  let v: E = E::Pair(1, 2);

  let x: int = match v {
    E::Pair(a, _) => a,
    E::Empty => 0,
  };

  if x != 1 {
    return 1;
  }
  return 0;
}

Example: struct payload enum match#

struct Job {
  id: int,
}

enum RecvJob {
  Msg(Job),
  Cancelled,
}

fn main () -> int {
  let j: Job = Job{ id: 5 };
  let evt: RecvJob = RecvJob::Msg(j);

  let rc: int = match evt {
    RecvJob::Msg(job) => job.id,
    RecvJob::Cancelled => 0,
  };

  if rc != 5 {
    return 1;
  }
  return 0;
}

Representation#

Enums are values. In the current IR-backed lowering, an enum value is lowered to scalar slots as:

  1. a u64 tag (variant index in declaration order, starting at 0), and
  2. a payload region that includes a distinct slot range for each variant’s payload elements, in variant declaration order.

Conceptually:

(u64 tag,
 payload slots for variant 0,
 payload slots for variant 1,
 ...)

Only the active variant’s payload region is meaningful for a given value; other payload regions are unspecified.

This representation is an implementation detail and is expected to evolve (for example, toward a tag + max-payload “union-style” layout) as the compiler and ABI mature.

Common Pitfalls#

  • Forgetting parentheses: E::Data(7) is valid, but E::Data is not a value in the Supported forms (error E2002).
  • Calling a unit variant: E::A is a value; E::A() is rejected (E2002).
  • Wrong binder count: E::Pair(a) does not match Pair(int, int) (E2002).
  • Non-exhaustive matches: you must list every variant (error E2002 in the Supported forms).
  • Assuming enum equality is defined: use match to inspect the tag/payload; the backend does not define ==/!= over enums yet.

Source repository · Edit this page · View Markdown