Cella Documentation 0.1.0-dev Home Source

Enums

An enum is a type whose values come from a fixed list of cases. A case can be a plain name, like a C enum constant, or it can hold values of its own, called its payload. This chapter covers declaring enums, creating their values, testing which case a value is and converting enums to and from integers. Taking payloads apart is mostly the job of match, covered in Pattern matching.

Declaring an enum

An enum declaration lists its cases, one per line or separated by commas on one line:

Direction: enum{	north	east	south	west} Suit: enum { clubs, diamonds, hearts, spades } main: fun{	val heading = Direction.east	val trump = Suit.hearts	printf("%d %d\n", i32(heading == Direction.east), i32(trump != Suit.spades))}
Output
1 1

A case is written Type.case. Two values of a plain enum compare with == and !=; they have no ordering, so < and the other comparisons are errors. Case names are camelCase by convention.

An enum needs at least one case, and each case name can appear only once:

Direction: enum{	northCase 'north' is declared more than once in 'Direction'	south	northCase 'north' is declared more than once in 'Direction'} Nothing: enum'Nothing' needs at least one case{} main: fun{	val a = Direction.north < Direction.southNo operation defined for '<' between 'Direction' and 'Direction'}

Case values

Every case has an integer value. The first case is 0 and each case after it is one more than the case before. A case can set its own value with =, and the cases after it count on from there:

Status: enum{	ok	notReady	timeout	lost = -1	busy = 100	retry} main: fun{	printf("%d %d %d %d %d %d\n", i32(Status.ok), i32(Status.notReady), i32(Status.timeout), i32(Status.lost),		i32(Status.busy), i32(Status.retry))}
Output
0 1 2 -1 100 101

A value can be any constant integer expression. It can use constants and earlier cases of the same enum by their bare names, and cases of other enums once they're converted to an integer:

base: const i32 = 40 Level: enum{	none	low = base / 8	high = low * 3	top = i32(Direction.west) + high} Direction: enum { north, east, south, west } main: fun{	printf("%d %d %d\n", i32(Level.low), i32(Level.high), i32(Level.top))}
Output
5 15 18

The value has to be known when the program is compiled, and it has to be an integer. Two enums whose values depend on each other are an error:

Dynamic: enum{	zero	other = runtime()The value of 'other' must be a constant} Mixed: enum{	zero	half = 1.5The value of 'half' must be an integer} First: enum { zero, a = i32(Second.b) }'First' depends on its own values Second: enum { zero, b = i32(First.a) }'Second' depends on its own values main: fun{}

Two cases with the same value are two names for the same case. Here stop and halt are interchangeable:

Signal: enum{	go	stop	halt = 1} main: fun{	printf("%d\n", i32(Signal.stop == Signal.halt))}
Output
1

The tag type

An enum is stored as an integer, called its tag. Without a tag type, the compiler picks the smallest integer type that holds every case value. Writing an integer type in parentheses after enum sets the tag type yourself:

Small: enum { a, b, c } Signed: enum { zero, low = -50 } Wide: enum { zero, big = 70000 } Code: enum(u16){	none	first = 100} main: fun{	printf("%d %d %d %d\n", i32(sizeOf(Small)), i32(sizeOf(Signed)), i32(sizeOf(Wide)), i32(sizeOf(Code)))}
Output
1 1 4 2

The tag type can be any integer type. With a tag type, every case value must fit in it, and a typed value must convert to it without loss:

Byte: enum(u8){	low	high = 300'300' doesn't fit in 'u8'} Counted: enum(u8){	zero	last = 255	past'past' is 256, which doesn't fit in 'u8'} Typed: enum(u8){	zero	wide = 5u16Cannot convert type 'u16' to 'u8'} Fraction: enum(f32)'f32' isn't an integer type{	zero} main: fun{}

The default case

The case with the value 0 is the enum's default. A variable declared without a value starts as that case, and so does an enum field of a record built with T():

Mode: enum{	active = 1	idle = 0	paused = 2} Settings: rec{	mode: Mode	level: i32} main: fun{	var mode: Mode	val settings = Settings()	printf("%d %d\n", i32(mode == Mode.idle), i32(settings.mode == Mode.idle))}
Output
1 1

A case with the value 0 is required. Only an ext enum, covered in Calling C, and the tagless enums at the end of this chapter can do without one:

Rank: enum'Rank' needs a case with the value 0{	one = 1	two = 2} main: fun{}

Payloads

A case can hold values. Its payload is written like a parameter list, with a name and a type for each value. A case with a payload is created by calling it with one argument per value:

Shape: enum{	empty	circle(radius: f64)	rect(width: f64, height: f64)} area: fun(shape: Shape) -> f64 => match shape{	empty => 0.0	circle(val r) => 3.0 * r * r	rect(val w, val h) => w * h} main: fun{	val shapes = [Shape.circle(2.0), Shape.rect(3.0, 4.0), Shape.empty]	for shape in shapes		printf("%.1f\n", area(shape))}
Output
12.0
12.0
0.0

The match in area picks the arm for the case and binds the payload's values to r, w and h. Pattern matching covers this in full.

A case must be given exactly the values it holds, and a case without a payload takes no parentheses:

Shape: enum{	empty	circle(radius: f64)	rect(width: f64, height: f64)} main: fun{	val a = Shape.circle'Shape.circle' holds 1 value: radius	val b = Shape.rect(1.0)'Shape.rect' holds 2 values: width, height	val c = Shape.empty()'Shape.empty' holds no values	val d = Shape.circle(true)Cannot convert type 'bool' to 'f64'}

Each payload value needs a name, and names can't repeat within a case. A case can't hold its own enum directly, since the value would have to contain itself; it can hold a pointer to one instead:

Effect: enum{	none	hit(i32)Expected ':' and a type} Heal: enum{	none	heal(amount: i32, amount: i32)Payload 'amount' is declared more than once in 'heal'Payload 'amount' is declared more than once in 'heal'} List: enum{	end	link(value: i32, next: List)Payload 'next' of type 'List' causes a cycle in the memory layout} main: fun{}

Values of an enum with payloads have no ==; test them with is or match instead. Such an enum is stored as its tag followed by room for its largest payload:

Effect: enum{	none	hit(amount: i32)	scale(factor: f64)} main: fun{	printf("%d %d\n", i32(sizeOf(Effect)), i32(alignOf(Effect)))}
Output
16 8

If the case with the value 0 holds values, the enum has no default, and a variable of it must be given a value before it's read:

Effect: enum{	hit(amount: i32)	none} main: fun{	var effect: Effect	val copy = effectCannot use uninitialized values}

Cases that share a value must also hold the same payload types:

Effect: enum{	none	hit(amount: i32) = 1'hit' and 'miss' share the value 1 but have different payloads	miss(chance: f32) = 1'hit' and 'miss' share the value 1 but have different payloads} main: fun{}

Bare case names

Where the compiler already knows which enum it needs, a case can be written without the type name. That covers typed declarations, arguments, return values, assignments, array elements and comparisons with a value of the enum:

Team: enum { red, blue } Effect: enum{	none	hit(amount: i32)} opposite: fun(team: Team) -> Team{	if team == red		ret blue 	ret red} strength: fun(effect: Effect) -> i32 => match effect{	none => 0	hit(val n) => n} main: fun{	var team: Team = red	team = opposite(team)	val effects: array[Effect, 2] = [hit(5), none]	printf("%d %d %d\n", i32(team), strength(effects[0]), strength(hit(7)))}
Output
1 5 7

Without an expected type, a bare case name is an error. A name that could also mean a variable or function of the expected type is ambiguous, and has to be written in full:

Team: enum { red, blue } blue: val Team = Team.red main: fun{	val team = redCannot infer the type of 'red'	val same = Team.red == blue'blue' is ambiguous}

Testing for a case

value is case is true when the value is that case. For a case with a payload, the payload goes in parentheses: _ accepts any value, and val name binds the value to a name that can be used where the test is known to be true:

Effect: enum{	none	hit(amount: i32)	heal(amount: i32)} main: fun{	val effect = Effect.hit(9)	printf("%d %d %d\n", i32(effect is none), i32(effect is hit(_)), i32(effect is Effect.heal(_))) 	if effect is hit(val n) && n > 5		printf("big hit %d\n", n)}
Output
0 1 0
big hit 9

The case after is can be bare or qualified. is accepts every pattern match does; Pattern matching lists them.

Converting to and from integers

An enum without payloads converts to any integer type with T(x), which gives its case value. An integer converts back with the enum's name:

Code: enum{	none	first = 100	second} main: fun{	val code = Code.second	printf("%d %d\n", i32(code), u8(code)) 	val raw: u16 = 100	val back = Code(raw)	printf("%d\n", i32(back == Code.first))}
Output
101 101
1

These conversions are never implicit. A constant that matches no case is a compile error, and a value that matches no case at run time panics:

Code: enum{	none	first = 100} main: fun{	val bad = Code(50)'Code' has no case with the value 50} number: fun -> i32 => Code.firstCannot convert type 'Code' to 'i32'
Code: enum{	none	first = 100} main: fun{	val raw = 50	val _ = Code(raw)}
Output
Panic at main.ce:12:10: 'Code' has no case with this value

An enum with payloads has no conversion to or from integers. An enum can't declare its own conversions to or from integer types either, since those are built in (Conversions covers declared conversions):

Effect: enum{	none	hit(amount: i32)} Color: enum{	red	green 	as: op(self) -> i32 => 0Conversion to 'i32' is built in} main: fun{	val n = i32(Effect.none)No constructor for 'i32' accepts argument of type 'Effect'}

Methods

An enum can declare methods, static functions, properties and static fields, as a record does; Methods and properties covers them. Inside a method, self is the enum value, so match self is the usual way to look at it, and a mut self method can replace self with another case:

Effect: enum{	none	hit(amount: i32)	heal(amount: i32) 	strong: fun(amount: i32) -> Effect => hit(amount * 2) 	size: fun(self) -> i32 => match self	{		none => 0		hit(val n) => n		heal(val n) => n	} 	isHarmful: get(self) -> bool => self is hit(_) 	double: fun(mut self)	{		self = match self		{			none => none			hit(val n) => hit(n * 2)			heal(val n) => heal(n * 2)		}	}} main: fun{	var effect = Effect.strong(3)	effect.double()	printf("%d %d\n", effect.size(), i32(effect.isHarmful))}
Output
12 1

An enum can't declare fields, constructors or a destructor, and a method can't share a name with a case:

Effect: enum{	none	hit(amount: i32) 	count: i32Cannot declare fields in enums 	new: opCannot declare constructors in enums	{	}} Shot: enum{	miss	hit(amount: i32)'hit' is declared more than once in 'Shot' 	hit: fun -> Shot => miss'hit' is declared more than once in 'Shot'} main: fun{}

Owned enums

A plain enum copies like an integer. own enum declares an enum whose values can't be copied, only moved, the way own rec does for records. After a move, the old variable can't be used:

Ticket: own enum{	open(id: i32)	closed} main: fun{	val ticket = Ticket.open(1)	val moved = ticket	val again = ticketCannot use moved valuesMoved on line 12}

An enum whose payloads hold owning values, such as a record with a destructor, must be declared own enum ("Cannot store owning values in enums that aren't 'own'"). Ownership and destructors explains moves.

Generic enums

An enum can take type parameters, written after its name. Each use names the type, or lets the compiler infer it:

Maybe[T]: enum{	nothing	just(value: T)} half: fun(n: i32) -> Maybe[i32]{	if n % 2 != 0		ret nothing 	ret just(n / 2)} main: fun{	val results = [half(8), half(3)]	for result in results	{		match result		{			just(val v) => printf("%d\n", v)			nothing => printf("odd\n")		}	}}
Output
4
odd

Generics covers type parameters and their constraints, including [T: enum](generics.md#enum) for generic code over enums without payloads.

Tagless enums

enum match T declares an enum that stores a value of type T and nothing else. Its cases sort the values of T into groups: a case lists the values it stands for after =, and one case written = else takes every other value. A case can have a payload written as a name with no type, which is the value itself, and the else case must have one:

Socket: enum match i32{	invalid = -1	open(handle) = else} Parity: enum match u8{	even = 0, 2, 4, 6	odd = 1, 3, 5, 7	large(value) = else} describe: fun(p: Parity) -> i32 => match p{	even => 0	odd => 1	large(val v) => i32(v)} main: fun{	val raw = 7	val socket = Socket(raw)	match socket	{		open(val h) => printf("handle %d\n", h)		invalid => printf("invalid\n")	} 	printf("%d %d %d\n", describe(Parity(3)), describe(Parity(40)), i32(sizeOf(Socket)))	printf("%d %d\n", i32(Socket.invalid), i32(socket))}
Output
handle 7
1 40 4
-1 7

A tagless enum takes no more room than T, and converting between it and T never changes the value. Converting from T can't fail, since the else case accepts everything the other cases don't. A case with a payload can be created with a value, which has to be one that case stands for; a constant that isn't is a compile error, and a value that isn't panics at run time:

Socket: enum match i32{	invalid = -1	open(handle) = else} main: fun{	val bad: Socket = open(-1)'Socket.open' can't hold -1}
Socket: enum match i32{	invalid = -1	open(handle) = else} main: fun{	val raw = -1	val _: Socket = open(raw)}
Output
Panic at main.ce:12:18: 'Socket.open' can't hold this value

T can be an integer type, char, bool, a pointer type or a function type. For pointer and function types the only value a case can list is null, which makes an enum for pointers that may be null:

Node: rec{	value: i32} NodeRef: enum match ptr[Node]{	none = null	some(node) = else} read: fun(n: NodeRef) -> i32 => match n{	some(val p) => p.value	none => -1} main: fun{	var node = Node(5)	val found: NodeRef = some(@node)	val missing: NodeRef = none	printf("%d %d %d\n", read(found), read(missing), i32(sizeOf(NodeRef)))}
Output
5 -1 8

A case without a payload needs a value, the values must be constants, and no value can be listed twice. The else case is needed unless the listed values cover every value of T, and there can be only one:

Repeats: enum match u8{	low = 1, 22 is listed more than once	high = 2, 32 is listed more than once	rest(value) = else} Missing: enum match u16'Missing' needs an 'else' case{	low = 1} Unvalued: enum match i32{	zeroCase 'zero' needs a value	rest(value) = else} Floats: enum match f32Cannot declare 'enum match f32'{	any(value) = else} Pointers: enum match ptr[i32]{	zero = 0Only 'null' can be listed for 'ptr[i32]'	other(value) = else} main: fun{}

A tagless enum has no tag type, so enum(u8) match i32 is an error, and listing several values or writing else is only allowed in an enum match.