SCUA

Manual

Enums

An enum is a closed set of named variants. It's the type you reach for when a value is one of a fixed handful of things: a direction, an input event, an AI state. Because the set is closed, the compiler can check that a match handles every case.

enum Dir {
  North,
  East,
  South,
  West,
}
print(`North = {Dir.North}, West = {Dir.West}`)

Run it:

$ scua dir.scua
North = 0, West = 3

You construct a variant through the enum name, Dir.North, so the reader always sees which type it is. Each enum is its own type; mixing two different enums is a compile error.

#Payload-free variants are integers

A variant that carries no data is backed by an integer, auto-numbered from 0. That makes enums cheap and lets them index arrays directly, which is handy for per-direction step tables, sprite lookups, or turn order.

You can set explicit discriminants. A bare variant continues from the previous one plus one:

enum Key {
  Escape = 27,
  Space  = 32,
  Enter,
}
print(`Escape = {Key.Escape}, Space = {Key.Space}, Enter = {Key.Enter}`)
$ scua key.scua
Escape = 27, Space = 32, Enter = 33

Enter got 33 because it follows Space = 32.

When you genuinely need to cross the integer boundary, for a C library, the wire, or a save file, the conversion is explicit. .to_int() goes out, Enum.from_int(n) comes back:

enum Dir { North, East, South, West }
print(`East index = {Dir.East.to_int()}`)
let restored = Dir.from_int(2)
print(`from_int(2) is South? {restored == Dir.South}`)
$ scua convert.scua
East index = 1
from_int(2) is South? true

#Variants that carry data

A variant can carry a positional payload. As soon as one does, the whole enum is a tagged sum type: the data rides with the tag, so there's no separate "when it's this state, that field means something" table to keep in sync.

enum Input {
  Quit,
  Key(int),
  Click(int),
}
fn act(e)
  match e
    Input.Quit     -> return "bye"
    Input.Key(c)   -> return `pressed key {c}`
    Input.Click(b) -> return `clicked button {b}`
  end
end
print(act(Input.Quit))
print(act(Input.Key(65)))
print(act(Input.Click(2)))
$ scua input.scua
bye
pressed key 65
clicked button 2

A payload can have several fields, and you can name them. The names are documentation. You still match by position:

enum AIState {
  Idle,
  Chase(target: int, dist: int),   -- names are documentation, matched positionally
}
match AIState.Chase(42, 3)
  AIState.Idle        -> print("idle")
  AIState.Chase(t, d) -> print(`chase {t} at range {d}`)
end
$ scua chase.scua
chase 42 at range 3

#Matching and exhaustiveness

match discriminates the variants, and because an enum is closed, the compiler checks the match is exhaustive. Drop a variant and it names the gap:

enum Dir { North, East, South, West }
fn label(d)
  match d
    Dir.North -> return "up"
    Dir.East  -> return "right"
    Dir.South -> return "down"
  end
end
print(label(Dir.North))
$ scua label.scua
scua: label.scua:3: type error: match on Dir is not exhaustive — missing: West

That check is the main reason to use an enum over a set of loose constants: add a variant, and every match that needs attention turns into a compile error. See pattern matching for the full set of arm shapes.

#A sharp edge: evolving a persisted enum

There is no versioning or reserve story for enums yet. That matters the moment an enum value is saved or sent over the wire.

Say an older build knows three variants and matches them exhaustively. A newer build adds a fourth and sends a value carrying it. When the old build receives that value and matches on it, none of its arms fit, and it hits a runtime fault:

enum Tool {
  Sword,
  Bow,
  Staff,
}
fn describe(t)
  match t
    Tool.Sword -> return "melee"
    Tool.Bow   -> return "ranged"
    Tool.Staff -> return "magic"
  end
end
-- A value arrives carrying a discriminant this build doesn't know.
let newer = Tool.from_int(3)
print(describe(newer))
$ scua evolve.scua
scua: evolve.scua:10: match: no arm matched (got 3)
  at describe (line 10)
  at main (line 15)

Until there's a proper versioning mechanism, treat any enum that crosses a version boundary with care: pin its discriminants, only add variants at the end, and when you match values that might come from a newer build, include a _ arm to handle the unknown case instead of faulting.

#See also