Skip to content
volt
GitHub

Enums and match

A plain enum is a set of named values. The backing integer type is optional (the smallest one that fits by default), and a variant can pick its value.

use std::io;
enum level: u8 {
DEBUG, // 0
INFO, // 1
WARN = 10,
ERROR, // 11
}
fn main() -> void {
val l = level::WARN;
std::println("{} {} {}", l, l as i32, level::ERROR as i32);
}
// expect: WARN 10 11

Enums compare with == and print their variant’s name.

Variants can carry data: one value, a tuple, or a tuple with named elements.

use std::io;
enum event {
KEY: u8,
CLICK: (x: i32, y: i32),
RESIZE: (i32, i32),
QUIT,
}
fn main() -> void {
val events: event[] = { event::KEY('a'), event::CLICK(10, 20), .RESIZE(800, 600), .QUIT };
for (e) in events {
std::println(e);
}
}
// expect: KEY(97)
// expect: CLICK(10, 20)
// expect: RESIZE(800, 600)
// expect: QUIT

.QUIT is short for event::QUIT wherever the type is already known.

A variant’s payload is also a field named after it, to read, change or borrow where the value is known to hold that variant. It’s checked: reading it from another variant stops the program (with a message in a debug build, a trap in a release one). @discriminant(x) says which variant x holds.

use std::io;
enum shape {
CIRCLE: i32,
RECT: (w: i32, h: i32),
}
fn main() -> void {
var s = shape::RECT(2, 3);
std::println("{} {}", s.RECT.w * s.RECT.h, @discriminant(s));
s.RECT.h = 4;
std::println(s);
}
// expect: 6 1
// expect: RECT(2, 4)

Enums can be generic too:

use std::io;
<T: type>
enum tree {
LEAF: T,
EMPTY,
}
fn main() -> void {
val t: tree<str> = tree<str>::LEAF("x");
std::println(t);
}
// expect: LEAF(x)

match compares a value against patterns, top to bottom. It’s an expression, and it has to cover every case: list every variant, or end with default.

use std::io;
enum event {
KEY: u8,
CLICK: (x: i32, y: i32),
QUIT,
}
fn describe(e: event) -> str {
return match (e) {
.KEY(k) if k == 'q' => "quit key", // a guard
.KEY(k) => "a key",
.CLICK(x, y) => "a click",
.QUIT => "quit",
};
}
fn main() -> void {
std::println("{} {} {}", describe(event::KEY('q')), describe(event::CLICK(1, 2)), describe(.QUIT));
}
// expect: quit key a click quit

Missing a case is a compile error:

enum light { RED, YELLOW, GREEN }
fn next(l: light) -> light {
return match (l) {
.RED => light::GREEN,
.GREEN => light::YELLOW,
};
}
// error: YELLOW

Integers, ranges, strings, booleans and tuples match too, and null matches an empty optional; _ matches anything, and a name binds the value (often with a guard).

use std::io;
fn size(n: i32) -> str {
return match (n) {
0 => "none",
1..=9 => "a few",
x if x < 0 => "negative",
default => "lots",
};
}
fn main() -> void {
std::println("{} {} {} {}", size(0), size(4), size(-2), size(99));
val pair = (2, true);
match (pair) {
(0, _) => std::println("zero"),
(n, true) => std::println("{} and true", n),
default => std::println("other"),
}
}
// expect: none a few negative lots
// expect: 2 and true

p1 | p2 => body matches either pattern. Alternatives can bind a name, as long as each one binds it, and count toward covering every case:

use std::io;
enum shape {
CIRCLE: f64,
SQUARE: f64,
POINT,
}
fn kind(c: u8) -> str {
return match (c) {
'(' | ')' | '[' | ']' => "bracket",
'0'..='9' | '_' => "digit or _",
default => "other",
};
}
fn size(s: shape) -> f64 {
return match (s) {
.CIRCLE(r) | .SQUARE(r) => r,
.POINT => 0.0,
};
}
fn main() -> void {
std::println("{} {} {}", kind(']'), kind('5'), size(shape::SQUARE(2.5)));
}
// expect: bracket digit or _ 2.5

The alternatives sit at the top of an arm: (0 | 1, x) inside a tuple isn’t one. Each alternative is its own arm with the same guard and body, so a guard runs once for each alternative that matches, and the body is compiled once for each: keep a long body in a function the arm calls.

[...] matches a slice, an array or a str (its bytes) by its elements: [] is empty, [x] has one, [first, ..rest] has at least one and binds the others as a slice, [.., last] reads from the end, and [a, b, ..] needs at least two. Each element is any pattern, so literals, nested tuples and x& (a reference to the element in place) work too. A match on a slice has to handle every length: the arms below cover 0, 1, and 2 or more.

use std::io;
fn describe(xs: i32[..]) -> void {
match (xs) {
[] => std::println("empty"),
[x] => std::println("just {}", x),
[0, ..rest] => std::println("0, then {} more", rest.len),
[first, .., last] => std::println("{} to {}", first, last),
}
}
fn sum(xs: i32[..]) -> i32 {
match (xs) {
[] => return 0,
[x, ..rest] => return x + sum(rest),
}
}
fn main() -> void {
val xs: i32[] = { 3, 1, 4, 1, 5 };
describe(xs[0..0]);
describe(xs[0..1]);
describe(xs[..]);
std::println("{}", sum(xs[..]));
}
// expect: empty
// expect: just 3
// expect: 3 to 5
// expect: 14

A binding copies the payload. .V(x&) binds a reference to the payload in place instead, to read a large value without copying it, or to change it. A match on a reference (r: slot&) matches what it refers to, so x& there changes the caller’s value.

use std::io;
enum slot {
FULL: i32,
FREE,
}
fn main() -> void {
var s = slot::FULL(1);
match (s) {
.FULL(n&) => { *n += 10; },
.FREE => {},
}
std::println(s);
}
// expect: FULL(11)

Error sets and trait unions are matched the same way.