Enums
An enum is a named type with a fixed set of variants. Each variant can be
a bare unit (like a C enum member), or it can carry a payload, which turns the
type into a tagged union. A value of an enum type always knows which variant
it currently is. Use match, if let, and let ... else to branch on the
variant and pull the payload back out.
Unlike type aliases, which are structural, enum
is nominal: two enums with identically-shaped variants are still different
types.
Contents
- Declaration
- Construction
.Discriminantis- Enum and int conversion
matchif let/let else- Changing a payload
- Payloads cannot hold containers
unsafe: unchecked payload access- Generic enums
- Built-in
OptionandResult - Built-in game enums
Declaration
Discriminants (the integer tag backing each variant) auto-number from 0. An
explicit = N resets the counter, so the next unannotated variant continues
from N + 1. Two variants that resolve to the same value are a
WS064 error.
enum Color { Red, Green, Blue } // 0, 1, 2
enum Status { Idle = 0, Running = 5, Done } // 0, 5, 6
enum Shape {
Empty, // unit variant
Circle(float), // positional payload
Rect(float, float),
Box { w: float, h: float }, // named payload
}
A single enum can freely mix unit, positional, and named variants. Whichever shape a variant is declared with is the shape it must be constructed and matched with (see Construction below).
Construction
Construct a variant with its qualified Enum.Variant path: a unit variant is
the bare path, a positional-payload variant takes (...), and a
named-payload variant takes { ... } (the shorthand { x, y } works in a
value position too, same as a record literal):
enum Shape { Empty, Circle(float), Box { w: float, h: float } }
let c = Shape.Empty
let s = Shape.Circle(5.0)
let b = Shape.Box { w: 1.0, h: 2.0 }
Using the wrong bracket form for a variant’s declared shape, such as
Shape.Circle { } for a positional variant or Shape.Box(1.0, 2.0) for a
named one, is a WS065 error. This keeps a variant’s shape
unambiguous for exhaustiveness checking and the LSP’s fill action.
The prelude variants
Some, None, Ok, and Err (the built-in Option/Result
variants) are available bare, with no Option. / Result. qualifier:
let o = Some(42) // T inferred = int
let n: Option<int> = None // annotation fixes T when payload can't
None and Err carry no payload to infer their type parameters from, so a
bare let n = None has nothing to pin T. Give the binding a type
annotation (let n: Option<int> = None) or that’s a WS063
error.
.Discriminant
.Discriminant yields the variant’s tag as an int. On a variant path
(Enum.Variant.Discriminant) it’s a compile-time constant that costs no
gates. On a value it reads the tag currently stored in that value:
enum Shape { Empty, Circle(float), Rect(float, float) }
out d = Shape.Circle.Discriminant // compile-time int, = 1
enum Shape { Empty, Circle(float), Rect(float, float) }
static var s: Shape = Shape.Circle(5.0)
out matches = s.Discriminant == Shape.Rect.Discriminant // runtime read vs const
.Discriminant (and match) on a value that isn’t an enum is a
WS066 error.
is
value is Enum.Variant asks whether the value currently holds that variant and
yields a bool. It is shorthand for the discriminant comparison above:
enum Shape { Empty, Circle(float), Rect(float, float) }
static var s: Shape = Shape.Circle(5.0)
in ready: bool
out round = s is Shape.Circle // same gates as the compare above
out other = !(s is Shape.Circle) // negate with `!`
out go = s is Shape.Rect && ready // binds like `==`
An enum has no ==: comparing two values would have to compare every payload
slot too, so s == Shape.Empty is a WS004 error. Test the
variant with is, or compare .Discriminants.
The test compares tags only, so a payload variant is tested by its tag and binds
nothing. Use match or if let to get a payload
back out. The right side must name a variant of the same enum: s is v
(two values) and s is Other.Empty (a different enum) are errors.
A test whose two sides are both compile-time constants folds away, so testing
a const value costs no gates.
Enum and int conversion
Two spellings move between an enum value and its integer tag.
value.ToInt() is an exact alias for .Discriminant: it yields the tag as
an int, on both a value and a variant path, and folds to the same
compile-time constant when the receiver is a variant path.
Enum.FromInt(n) goes the other way. It builds a value of Enum whose tag is
the int n, with every payload slot defaulted to its zero value. Here n
may be a runtime int, not just a constant. It sets only the tag, so it is
meaningful mainly for unit-only enums (C-like enums and the built-in game
enums). For a payload-carrying variant the payload reads back as zero. An n
that matches no variant’s discriminant leaves a value that no match arm
covers except a wildcard _.
enum Shape { Empty, Circle(float), Rect(float, float) }
in tag: int
static var s: Shape = Shape.Rect(1.0, 2.0)
// ToInt is Discriminant by another name.
out same = s.ToInt() == s.Discriminant // always true
out circle = Shape.Circle.ToInt() // compile-time int, = 1
// FromInt rebuilds a value from a (possibly runtime) tag.
let rebuilt = Shape.FromInt(tag)
out kind = match rebuilt {
Empty => 0,
Circle(r) => 1,
Rect(w, h) => 2,
}
EnumToInt(value) and IntToEnum(value, wrap?) are the gate-backed
equivalents of .ToInt() and Enum.FromInt(n), for routing through the game’s
“Enum to Integer” / “Integer to Enum” gates.
EnumToInt(value) requires an enum argument (an int or any non-enum
is a type error) and yields an int. When value is a compile-time-known enum
(a variant literal, or a const value) it folds to the discriminant literal
and emits no gate. A runtime enum value instead routes through the real
EnumToInt gate, fed by the value’s tag. For Wirescript’s record enums the
runtime gate only reads the tag, but it is emitted so game/native enums go
through the real gate.
IntToEnum(value) is the reverse. Its result is an enum whose concrete
type comes from the use site (the annotated target or output type), the same
as FromInt and null. With no enum-typed context it can’t tell which enum
the integer names, and that is an error. A constant value folds to the enum
record directly, and a runtime value routes through the real IntToEnum gate.
The optional wrap clamps an out-of-range tag into range.
enum Shape { Empty, Circle(float), Rect(float, float) }
in tag: int
static var s: Shape = Shape.Rect(1.0, 2.0)
// EnumToInt is the gate-backed equivalent of ToInt / Discriminant.
out folded = EnumToInt(Shape.Circle(1.0)) // compile-time: folds to 1, no gate
out live = EnumToInt(s) // runtime: routes through the gate
// IntToEnum is the gate-backed equivalent of FromInt. The result's enum
// type comes from the annotated target.
let back: Shape = IntToEnum(tag) // runtime int -> Shape via the gate
out kind = match back {
Empty => 0,
Circle(r) => 1,
Rect(w, h) => 2,
}
match
match branches on an enum value’s variant, binding any payload as it goes.
It works both as an expression (arms are values, comma-separated, and it
compiles to a Select tree) and as a statement (arms are blocks, and it
compiles to a Branch/Union tree). Arm patterns use bare variant names.
The scrutinee’s enum type is already known, so there’s no Enum. qualifier.
Expression form
enum Shape { Empty, Circle(float), Rect(float, float), Box { w: float, h: float } }
static var s: Shape = Shape.Circle(5.0)
out area = match s {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
Box { w, h } => w * h,
Empty => 0.0,
}
The arms’ result types follow the same
widening join as an if-then-else expression.
An arm can also be a block that ends in return (A => { return 0 }), which
leaves the enclosing mod or handler instead of giving the match a value. A
block arm that can end any other way is WS095. So is a block arm of a value
match outside an exec context (a top-level out or let), where it never runs.
An arm may be a record literal, which chooses a record per field:
A => { x: 1, y: 2 }. An arm written {} or { name } is a block, as in the
statement form.
Statement form
Statement arms are blocks (no commas between them) and require exec context:
enum Shape { Empty, Circle(float) }
static var s: Shape = Shape.Circle(5.0)
var lastArea: float = 0.0
on Loaded() {
match s {
Circle(r) => { lastArea = r * r }
Empty => { lastArea = 0.0 }
}
}
Exhaustiveness
A match must cover every variant of the scrutinee’s enum, or be capped with
a _ wildcard arm. An uncovered variant is a WS054 error
that names the missing pattern(s) in its message. An arm that can never run
because an earlier arm already covers everything it would match is a
WS061 warning. A _ after arms for every variant is not
one: it runs for a tag that names no variant, such as Enum.FromInt(n) with
n out of range.
enum Shape { Empty, Circle(float), Rect(float, float) }
static var s: Shape = Shape.Circle(5.0)
out area = match s {
Circle(r) => 3.14159 * r * r,
_ => 0.0,
}
Nested patterns
A pattern can nest into a variant’s payload, including into another enum, and exhaustiveness checking follows it down:
enum Opt { Some(int), None }
enum Tree { Leaf(int), Node(Opt) }
static var t: Tree = Tree.Leaf(0)
out val = match t {
Node(Some(x)) => x,
Node(None) => 0,
Leaf(n) => n,
}
A named-payload pattern can ignore the fields it doesn’t need with ..:
Box { w, .. } binds only w and drops h.
A payload that is an int, float, bool or string can be matched by a
literal, a range or a string pattern, as a match on a single
value is:
enum Cmd { Say(string), Move(int), Quit }
static var c: Cmd = Cmd.Quit
out text = match c {
Say("") => "(silent)",
Say("/" .. command) => "command ${command}",
Say(words) => words,
Move(0) => "stay",
Move(_) => "move",
Quit => "bye",
}
Alternatives and guards
A | B => ... runs one arm for either variant, and builds it once. An
alternative cannot bind a payload (WS121), since only one of
them matches; use _ in its place. pattern if condition => ... runs the arm
only when the condition, which can read the pattern’s bindings, is also true;
otherwise matching goes on with the next arm. A guarded arm covers nothing for
WS054, since its guard may be false.
enum Shape { Empty, Circle(float), Rect(float, float) }
static var s: Shape = Shape.Empty
out kind = match s {
Circle(r) if r > 10.0 => "big round",
Circle(_) | Rect(_, _) => "shape",
Empty => "nothing",
}
Enums at ports
An enum value is its tag plus a slot per payload field, and a port pin carries a
single wire. So only an enum with no payloads crosses a port, and only a
top-level in or out port. It travels as its tag (the .Discriminant), and
on the other side the name is the enum value again, ready to match, compare
with is, or store. A built-in game enum has no payloads, so it crosses the same
way. An array or map of such an enum crosses a top-level port as one column of
tags, and each element reads back as the enum. An out of one is set like any
other output: a default, emit name = value, or an out in a handler.
enum Mode { Idle, Run }
in mode: Mode
in ease: EasingFunction
var last: EasingFunction = EasingFunction.Linear
var n: int = 0
in go: exec
out seen: Mode = Mode.Idle
on go {
last = ease
n = match mode { Idle => 0, Run => 1 }
emit seen = mode
}
out current: EasingFunction = last
Everything else is a WS049 error: an enum with a payload at
any port, and any enum inside a record or tuple port, or at a chip parameter
or output. A mod parameter is fine (a mod is inlined, so nothing crosses a
port), and a chip parameter written *E wires each part. To bring an enum
with payloads in from outside the unit, pass its parts as plain values and build
it inside:
enum Shape { Empty, Circle(float), Rect(float, float) }
var s: Shape = Shape.Empty
in setCircle: exec
in radius: float
on setCircle {
s = Shape.Circle(radius)
}
out area = match s {
Circle(r) => r * r,
Rect(w, h) => w * h,
Empty => 0.0,
}
To send an enum with payloads out, expose a derived scalar, such as
.Discriminant or a match result.
A match scrutinee can be any enum value in storage, or a let that aliases
one. An array element, a map value, and a record field all match directly
(match arr[i] { .. }, or let e = h.shape first). The one spelling that
does not is m.get(k). That method returns a {Value, Found} result record
rather than the value, so matching it is the WS066
“requires an enum scrutinee” error, which names that shape. Subscript it
(m[k]) or read the member (m.get(k).Value).
if let / let else
These are single-variant refutable binds, shorthand for a match with one
real arm.
if let PATTERN = scrutinee { ... } runs the then block (with the pattern’s
bindings in scope) only when the scrutinee is that variant; the else is
optional:
enum Opt { Some(int), None }
static var o: Opt = Opt.Some(5)
var result: int = 0
on Loaded() {
if let Some(x) = o {
result = x
} else {
result = -1
}
}
let PATTERN = scrutinee else { ... } binds into the surrounding scope
instead of a nested block, so its else is required to diverge: it
must end in return / emit, or be an if/match whose arms all diverge.
This guarantees the binding is always available after the statement. A non-diverging else is a WS062
error.
enum Opt { Some(int), None }
mod unwrapOr(v: Opt, fallback: int) -> int {
let Some(x) = v else { return fallback }
return x
}
static var o: Opt = Opt.Some(5)
var result: int = 0
on Loaded() {
result = unwrapOr(o, -1)
}
Changing a payload
A payload field has no direct write. s.field = v on an enum value is a
WS007 error, because an enum value is stored as its tag
plus one slot per variant field, and the surface field name names no slot.
There are two ways to change one, and which you want depends on whether the value is already the variant you are writing.
Destructure and assign the capture. A match arm, an if let, or a
let ... else binds each capture directly to the matched value’s payload
slot, so writing the capture writes the payload in place. The other
fields, and the tag, are left alone. Reading a capture reads that slot live
too, so a call in the arm that writes the matched value is visible through the
capture:
type Track = { origin: vector, direction: vector }
enum Nav {
Idle,
Moving { track: Track, label: string, active: bool },
}
var nav: Nav = Nav.Moving {
track: { origin: Vec(4.0, 4.0, 4.0), direction: Vec(0.707, 0.707, 0.0) },
label: "start",
active: false,
}
in go: exec
on go {
if let Moving { track, label, active } = nav {
track = { origin: Vec(69.0, 69.0, 69.0), direction: Vec(1.0, 0.0, 0.0) }
label = "moved"
active = true
}
}
The write costs one Var_Set per scalar field, and one per leaf for a
record-typed field, so track above costs two, one for origin and one for
direction. That is the same price as writing a record var. Because the
assignment sits inside the arm, it only runs when the value is that variant,
which makes an in-place payload write safe.
The scrutinee has to be writable storage for this: a var, a static var,
a *T parameter, or a var-backed record field. Captures off a let, a
const, or a by-value parameter stay read-only, since there is no storage gate
behind their slots for a write to land on.
A container element is not storage. arr[i], m[k], and any field reached
through one (hs[0].e) read the element out by value, so a capture off them is
read-only too and writing it is a WS007 error. To change an
element’s payload, copy it into a var, mutate that, and store it back:
enum Slot { Empty, Full { label: string, ready: bool } }
var slots: Slot[]
var cur: Slot = Slot.Empty
in go3: exec
on go3 {
slots.push(Slot.Full { label: "start", ready: false })
cur = slots[0]
if let Full { label, ready } = cur {
label = "moved"
ready = true
}
slots[0] = cur
}
Rebuild the whole variant. Assigning the enum itself sets the tag and every slot of the new variant at once, and is the only option when the value is changing variant:
enum Nav2 { Idle, Moving { label: string, active: bool } }
var nav2: Nav2 = Nav2.Idle
in go2: exec
on go2 {
nav2 = Nav2.Moving { label: "moved", active: true }
}
Reading works the same way round: pull a payload field out through a match
or if let capture, not through value.field.
Payloads cannot hold containers
A payload field cannot be an array or a map, and neither can a record used as
one. The declaration is a WS069 error:
enum Loadout {
Empty,
Carrying { items: int[] }, // WS069
}
A payload slot is a single storage gate, and it is filled by constructing
the variant. There is no way to construct a container into one. A declaration
initializer bakes an initial value, and an array has none. A runtime
Loadout.Carrying { items: [1, 2, 3] } has no gate that assigns a whole array.
The slot would still be allocated, so a captured items.push(...) compiled to a
real gate writing storage that nothing ever filled, and the value read back
empty. Rejecting the declaration prevents that.
The same applies to a generic enum instantiated with a container, when the
parameter is one a variant stores, so Option<int[]> is a WS069 at
the annotation. A parameter no variant stores is unaffected.
Keep the container in its own var and put something scalar in the payload
that refers to it, such as an index, a key, or a length:
var items: int[]
enum Loadout {
Empty,
Carrying { first: int, count: int },
}
var loadout: Loadout = Loadout.Empty
in pickUp: exec
on pickUp {
items.push(7)
loadout = Loadout.Carrying { first: 0, count: items.length() }
}
unsafe: unchecked payload access
unsafe <value>.<Variant>.<field> reads or writes one payload slot without
proving the value is that variant. It costs no gate: the access resolves
straight to the slot.
enum Job { Idle, Running { label: string, ticks: int } }
var job: Job = Job.Running { label: "start", ticks: 0 }
var seen: string = ""
in tick: exec
on tick {
unsafe job.Running.ticks = unsafe job.Running.ticks + 1
seen = unsafe job.Running.label
}
The variant is required and is what selects the slot, so a field two variants
share is never ambiguous. A record-typed payload keeps projecting
(unsafe job.Running.spec.width). An unknown variant is a
WS060, an unknown field a WS010, a missing segment a
WS070.
The tag is asserted, never tested and never written:
- Reading a variant the value is not returns that slot’s stale contents, with no error and no default.
- Writing sets the slot only. If
jobisIdle,unsafe job.Running.ticks = 5fillsRunning’s slot whilejobstaysIdle, and a latermatchstill takes theIdlearm.
Prefer a destructure, which tests the tag first. Use unsafe when you have
already established the variant.
unsafe is contextual, so a variable, parameter, or mod named unsafe keeps
working. (The tree-sitter grammar cannot express that and treats the word as a
keyword, so an editor using it flags such a name even though it compiles.)
Generic enums
An enum can take type parameters, instantiated per use the same way as a generic type alias:
enum Box<T> { Value(T), Empty }
static var b: Box<int> = Box.Value(42)
out val = match b {
Value(x) => x,
Empty => 0,
}
Built-in Option and Result
Wirescript ships Option<T> and Result<T, E> as prelude enums. They need no
declaration, and their variants are usable bare (Some/None/Ok/Err,
see Construction above). They’re built in as if declared:
enum Option<T> { Some(T), None }
enum Result<T, E> { Ok(T), Err(E) }
Don’t redeclare them. The prelude already registers both names, so
an enum Option<T> { ... } of your own is a WS013
duplicate-declaration error.
static var maybe: Option<int> = Some(7)
static var missing: Option<int> = None
out found = match maybe { Some(x) => x, None => -1 }
static var r: Result<int, string> = Ok(200)
out status = match r {
Ok(code) => code,
Err(msg) => -1,
}
Built-in game enums
A handful of enum types are built into the compiler with no enum
declaration at all. EasingFunction, Direction, ColorSpace,
DisplayTextJustification, TextTypeface, DisplayTextEasing, and
EasingDirection are the ones shipping today. They are not hand-maintained:
the compiler discovers them from the game’s own config enums (the same ones
that back a gate’s settings-menu fields), so the exact set and its variants
track whatever build the compiler was generated against.
BrickMaterial (Plastic, Glass, TranslucentPlastic, Glow, Metal,
Hologram) is built in the same way from the game’s brick material enum. It is
the value @material
takes rather than a gate setting. Its members are bare only as that argument
(@material(Glow)); everywhere else write them qualified, BrickMaterial.Glow.
Type is what GetValueType and .getType()
give: the kind of value on a wire (Bool, Int, Float, Color, Vector,
Rotator, Quat, String, Enum, Object, Brick, Component, Exec).
It is built from the game’s wire-kind enum, with the member names Wirescript
uses for its types. Its members are bare only as the arms of a match on a
Type; everywhere else write them qualified, Type.Int.
A built-in game enum behaves like an ordinary unit-only enum (see
Declaration above): construct a variant with its qualified
path, store it in a var or static var, and read .Discriminant:
static var mode: EasingFunction = EasingFunction.Bounce
out disc = mode.Discriminant
The enum value is the default representation, and .Discriminant gives back
the integer the game’s own schema assigns that member. This is the only way a
built-in game enum differs from a user-declared one. A user enum’s
discriminants auto-number from 0, but a built-in game enum’s discriminant is
the real schema value. It round-trips through saved component data, and a
renumbered tag would write the wrong value to the game.
A built-in enum value also passes directly as the matching gate config argument:
in t: float
let eased = Easing(0.0, 1.0, t, function = EasingFunction.Bounce, direction = EasingDirection.InOut)
out result = eased
The older bare-name form (an unqualified member name) still works side by side with the enum-qualified form, and both set the same config field:
in t: float
let eased = Easing(0.0, 1.0, t, function = Bounce, direction = InOut)
out result = eased