Enums

An enum defines a fixed set of named choices. Instead of using raw numbers like 0 = north, 1 = east — which nobody can read six months later — you give each option a name. The compiler then checks every comparison and conversion, so a typo becomes a compile error instead of a silent wrong answer. Plain enums are just names that can be compared, ordered, and converted to and from text. Their order matches their declaration order.

enum Direction {
  North,
  East,
  South,
  West
}

Enums that carry data

Sometimes different choices have different shapes of data. A Circle needs one number (radius); a Rect needs two (width and height). Struct enums let each variant carry its own fields, keeping all the shape kinds in one type while still letting you work with each variant naturally.

enum Shape {
  Circle {radius: float },
  Rect {width: float, height: float }
}

Methods that work on any variant

Write the same function name for each variant, each taking self of that variant's type. Loft automatically picks the right version at runtime based on which variant is actually stored in the variable. This lets you call shape.area() without caring which kind of shape it is.

fn area(self: Circle) -> float {
  PI * pow(self.radius, 2)
}
fn area(self: Rect) -> float {
  self.width * self.height
}

Polymorphic methods can return text just as well as numbers. Each variant can produce its own human-readable description.

fn describe(self: Circle) -> text {
  "circle with radius {self.radius}"
}
fn describe(self: Rect) -> text {
  "rect {self.width} by {self.height}"
}

Enum methods

A plain enum can have methods too. The method is written once, for the whole enum: the self parameter carries the current value, and a match on it picks the case. Here opposite() flips North↔South and East↔West.

fn opposite(self: Direction) -> Direction {
  match self {
    North => South,
    South => North,
    East => West,
    West => East
  }
}
fn main() {

Plain enum values are ordered by declaration position. North comes first (smallest), West comes last (greatest).

  d = Direction.East;
  assert(d == East, "Enum equality");
  assert(d != North, "Enum inequality");
  assert(d > North, "East declared after North, so it is greater");
  assert(Direction.West > South, "West declared last, so it is greatest");

Convert between text and enum in both directions.

  assert("{d}" == "East", "Format plain enum as text");
  assert("West" as Direction == West, "Parse text to enum");

Use a plain enum method like any other method.

  assert(d.opposite() == West, "opposite of East is West");
  assert(Direction.North.opposite() == South, "opposite of North is South");

Struct enum variants are constructed exactly like regular structs.

  c = Circle {radius: 1.0 };
  assert(c.radius == 1.0, "Circle radius field");
  r = Rect {width: 4.0, height: 5.0 };
  assert(r.width * r.height == 20.0, "Rect manual area");

Calling area() on a Circle runs the Circle version; on a Rect it runs the Rect version — chosen automatically at runtime.

  assert(round(c.area() * 1000) == round(PI * 1000), "Circle area = PI*r^2");
  assert(r.area() == 20.0, "Rect area = width*height");

describe() uses format strings with field access inside each variant's method.

  assert(c.describe() == "circle with radius 1", "describe circle: {c.describe()}");
  assert(r.describe() == "rect 4 by 5", "describe rect: {r.describe()}");

A method on the enum itself is the fallback

Calling area() on a value held as a Shape works like a match on its variant, and a match must cover every variant. So when only some variants have their own version, the call is refused unless the enum has one too: fn area(self: Shape) -> float { 0.0 } That version is the _ arm: a variant with its own area() still runs it, and every other variant runs this one. A method only some variants have is fine as long as nothing calls it on a value held as the enum. An empty-body stub for one variant also covers it: fn area(self: SomeVariant) -> float { } Give the stub a real body — { 0.0 } — when anything reads its result. An empty body hands back the type's default (0.0 for a float, 0 for an integer, "" for text), which is the same on both backends. That is a real value, not a marker: it is not null, so there is nothing to test for, and a caller cannot tell it from a computed 0.0.

Match expressions on enums

Match picks a code path based on the active variant. You must handle every variant, or include a _ wildcard arm that catches the rest.

  axis = match d {
    North | South => "vertical",
    East | West => "horizontal"
  };
  assert(axis == "horizontal", "or-pattern: East matches East|West");

When a variant has fields, name them inside braces to use them in the arm body.

  label = match c {
    Circle { radius } => "r={radius}",
    Rect { width, height } => "{width}x{height}"
  };
  assert(label == "r=1", "field destructuring in match arm");

Guard clauses

An arm can have an if guard after the pattern. If the guard fails, matching falls through to the next arm. Because the guard can fail, a guarded arm alone does not prove the variant is handled — you still need a wildcard _ or an unguarded arm for that variant.

  area = match c {
    Circle { radius } if radius > 0.0 => PI * radius * radius,
    _ => 0.0
  };
  assert(round(area * 1000) == round(PI * 1000), "guarded match on Circle");

Scalar match

Match also works on integers, text, floats, booleans, and characters. Arms can be literals, ranges, null, or _.

  grade = match 85 {
    90..=100 => "A",
    80..90   => "B",
    _        => "C"
  };
  assert(grade == "B", "range pattern 80..90 matches 85");

Or-patterns work on scalars too.

  kind = match 2 {
    1 | 2 | 3 => "low",
    _         => "high"
  };
  assert(kind == "low", "scalar or-pattern");

A null pattern matches when the value is absent — here a division by zero, which yields null (see the Integers page). The ?? null turns the reasoned null into a plain one; it is the value that is being matched either way.

  zero = 0;
  check = match 1 / zero ?? null {
    null => "absent",
    _    => "present"
  };
  assert(check == "absent", "null pattern matches div-by-zero when defended");

Character literals work in match arms.

  vowel = match 'e' {
    'a' | 'e' | 'i' | 'o' | 'u' => true,
    _ => false
  };
  assert(vowel, "character or-pattern");
}