enum types
On this page
Note: All of this capability is implemented except calling a method that takes self by value on a value typed as the enum itself. Where it is shown, it is marked.
An enum declares a closed set of variants: a value of the enum is exactly one of them, and the whole set is written inside the declaration, so nothing elsewhere can add to it. That closedness is what buys an exhaustive match.
The variants may carry values of their own, or carry nothing at all (three forms below):
enum PhoneType {
Home, Work, Mobile;
}
enum Point {
pub struct XyPoint {
pub x f32;
pub y f32;
}
pub struct AngPoint {
pub dist f32;
pub angle f32;
}
}
A bare name is an empty struct variant. That one sentence is what makes a plain set of named symbols and a set of variant structs the same thing, and it is why there is no second construct to learn: Home above declares exactly what struct Home; would.
Two properties hold of every enum:
- An enum is discriminated: there is a hidden "tag" field at the beginning of the value, holding a number the compiler assigns to each variant, which is what tells at runtime which variant a value is.
- Every variant occupies the same size of memory — the size of the largest, plus the tag — so smaller variants are padded to fit. A value of the enum can therefore be held in a variable, copied, passed by value, and replaced in place by a different variant. Write @unsized to decline the padding.
Where you want a set of variants that other modules can add to, an enum is the wrong construct: use a trait and structs that declare is against it. That versatility costs something, and the cost is worth knowing: an open trait has no tag, so a value of it carries a pointer to type metadata and a match has to check the possibilities one at a time. A closed set is not only exhaustive, it is faster, because a tag is an index.
Declaration#
The simplest form is a name and its variants. A variant written as a struct carries fields of its own; one written as a bare name carries none. It is permissible for a struct variant to be empty of fields too, as the presence of the tag ensures the overall value is not empty.
An enum may also declare members in common: fields every variant has, and methods every variant answers. Neither is something Rust's enums offer, and both are what let a caller do useful work with a value whose variant it has not yet recovered.
enum Event {
pub time datetime; // a field every variant has
pub fn stamp(self &) datetime {time;} // a method with a body: a default
pub fn describe(self &) String; // a method without one: a requirement
pub struct ButtonEvent {
pub button u8;
pub pushed bool;
}
pub struct KeyEvent {
pub key Unicode;
}
pub struct QuitEvent;
}
All three variants above begin with the time field, ahead of any fields they declare themselves. A method with a body is a default, which each variant gets unless it declares that name itself; a method without one is a requirement, which every variant must implement, and calling it through a reference to the enum dispatches on the variant — see Method dispatch below.
Either way, what a variant has is its own copy of the method. It is called on a value, event.stamp(), or named through the variant, Event.KeyEvent.stamp. Naming it through the enum, Event.stamp, is an error: the enum keeps no copy of its methods, only of its static functions.
A common member may be written anywhere in the body. What makes a member common is its kind, not its position: a struct is a variant, and anything else is common. The layout still puts common fields first in every variant.
Variant forms#
A variant is declared in one of three forms, and one enum may mix them freely:
enum Token {
Number(f64); // declared as a type
Word(&str);
struct Op {pub prec i32; pub right bool;} // declared as a struct
End; // empty
}
- As a type: a name and a type in parentheses, Number(f64);. The variant holds that type itself. It has no field name, so there is no .value, or any name at all, to reach what it holds by: a pattern that binds it gives the contents, as case imm n Number binds an f64 (a copy, or a move of what moves, as the matched value allows; through a reference, a reference to it). The variant stays a type of its own, distinct from the type it holds: a Number is not an f64. It is built as any variant is, Number[1.5], and may pin its tag value after its type, Data(i64) = 7;. Core's Option and Result are declared this way: None; Some(T); and Ok(T); Error(E);.
- As a struct: struct Op {...} names its fields and may have methods. A binding gives the struct, whose tag is never a visible field.
- Empty: a bare name holds nothing, End;, and declares what struct End; would.
A variant declared as a type is told from a common field by its parentheses: Number(f64); declares a variant, and time datetime; a field every variant has, whatever either is called. An enum with fields in common names everything it holds, so none of its variants is declared as a type, and is refused where one is written (ErrorTypeVariant); declare it as a struct, whose field has a name. (A name and a tuple type, t (i32, i32);, is a field too.)
Tag values#
Each variant's tag value is assigned for you, ascending from zero in declaration order. You may write a value instead, which is what lines an enum up with the constants of a library written in another language. Numbering resumes from the last value written, so only the variants that need a particular value have to say so.
An enum may also name the integer type its tag values are laid out in. That fixes the tag's width, so a value too large for it is an error rather than a silent widening away from the layout you asked for. Without it the tag is as wide as the largest value needs — note that this follows the largest value, not the number of variants.
enum Colors u32 {
Red = 0xFF0000,
Green = 0x00FF00,
Blue = 0x0000FF;
}
A tag value may be negative, as some libraries' constants are. Each value must fit the named integer type the way any integer literal must fit its type, and a negative sign counts: -4 fits i32, while its bit pattern 0xFFFFFFFC does not, and a negative value fits no unsigned type. Numbering resumes from a negative value too, counting up. An enum that names no type and holds a negative value gets the narrowest signed tag that holds every value.
enum VkResult i32 {
Success = 0,
NotReady, // 1
ErrorOutOfHostMemory = -1,
ErrorOutOfDeviceMemory = -2,
ErrorDeviceLost = -4;
}
The tag field cannot be accessed, referenced or mutated: which variant a value holds is reached by pattern matching, and compared with ==.
For alignment reasons it may be desirable to place the tag somewhere other than first. To do this, write an anonymous field of type tag where it should go. tag means this only where a field's type is written, so it is not a reserved word and a field may still be named tag.
enum Aligned {
pub wide i64;
_ tag;
pub struct Small {pub n i32;}
pub struct Large {pub a i64; pub b i64;}
}
Initialization#
A value is built using a constructor for one of the variants: its name with its values in parentheses, with no new, and an empty variant's name alone:
mut event = Event.QuitEvent(timestamp); oddval = Event.KeyEvent(timestamp, 's'); imm phone = PhoneType.Work;
Notice that the constructor supplies a value for every common field, and never for the tag: that one is settled by which variant was named.
A generic variant takes its type arguments from where its value goes, as every generic does (Generics): imm none Option[i32] = None;. Where nothing says, they are written in brackets after the name, and the values after them: None[i32], Some[i32](5). Square brackets after a variant give type arguments and never values, so Some[5] and None[] are refused, naming the spelling that replaces them. In a pattern or after is a name stays a type: case is None[IoBuf].
An empty variant's name is a value, so a method can be called on it directly: with pub fn get(self &) i32 in Lamp, Lamp.Red.get() constructs Red and calls get on it, as (Lamp.Red).get() does. Only a method reads this way (one that takes self); a variant that holds values is not a value by its name alone, so Shape.Circle.area() is still refused for the self it lacks. A generic enum's empty variant has nothing to take its type arguments from as a receiver, so they are written: Pending.Waiting[i64].key().
Naming a variant#
A variant is a name of its enum, not of the module around it, so it is reached through the enum: Event.KeyEvent, PhoneType.Work. Two enums may therefore each declare a Quit without either crowding a namespace neither of them was asked to share.
Inside the enum's braces, every name the enum declares is bare: its variants, and its fields, methods and functions. That holds for a variant's own body too, since it is written there, so a method of one variant names another without the enum in front:
enum Traffic {
struct Stop {
fn next(self &) Traffic {Go(5);} // Traffic.Go
}
struct Go {
pub delay i32;
}
}
A name the variant declares itself comes first: a field of Go spelled like a sibling variant is, inside Go's methods, the field. One exception to the rule: a variant of a generic enum reaches the enum's methods through self, as self.name(), since it receives them only when the enum is instantiated. In a generic enum the enum's bare name, and each variant's, is the instance being defined, at the enum's current type arguments: inside enum Maybe[T], Maybe is Maybe[T] and a variant's No builds Maybe.No[T] (see Generics). A variant that an extension copies from its base (below) keeps what its names meant where it was written, so it still means the base's variants.
An enum's private members are its module's. A member not declared pub is private to the module that declares the enum, as a struct's is. So code written anywhere inside the enum's braces — the enum's methods, its static functions, and every variant's methods — reads and writes the private fields of the enum and of every variant, calls their private methods, and gives private fields their values when it builds a variant, through any value and not only through self, and so does every other function and type of the module. Outside the module they stay private. So pub means what it means everywhere: part of the enum's interface to the rest of the program, and not a way for one variant to reach another.
enum Gauge {
struct Low {
lo i32;
pub fn above(self &, h &High) bool {h.hi > lo;} // High's private field
}
struct High {
hi i32;
}
pub fn level(g &imm Gauge) i32 { // a static function
match g {
case imm l Low {l.lo;}
case imm h High {h.hi;}
}
}
}
(This sentence is the compiler's current reading and is still to be reviewed.) An anonymous function written inside the braces is not part of this: like one written in a struct's method, it belongs to the module.
A module that wants an enum's variants as names of its own says so with use, which names the enum and then, optionally, which variants:
use PhoneType; // Home, Work and Mobile, bare
use Event KeyEvent as Key, QuitEvent; // only these, one of them renamed
use Point {XyPoint, AngPoint}; // a list may be spread over lines in braces
use Colors but Blue; // every variant but one
After use PhoneType;, Work means PhoneType.Work. The module that declares the enum is no exception: it writes use like any other module to have the variants bare. A use folds variants and nothing else of the enum, and a variant it leaves out is still reached through the enum. The enum may be named through a module, use shapes.Colors;. A folded name must not be one the module already has for something else, whatever brought that one in; two enums that each have a Quit are both folded by renaming one or leaving it out. Nor may a module fold the same variant in twice itself: writing use Colors; twice is an error. The same variant arriving by a wildcard import is the name the module already has, so use Option; is fine beside the Some and None every module already has.
The names a use makes belong to the module that wrote it and are private to it. Written pub use, they are public names of the module: another module reaches them through it, shapes.Red, and an import of the module with .* brings them in bare. That is how Some, None, Ok and Error are bare in every program: the core library writes pub use Option; and pub use Result;, and every module imports it with .*.
A pattern needs no use. A pattern knows the enum of the value it is matched against, so it names that enum's variants bare, and looks there before it looks at the names in scope:
fn pushed(e &imm Event) bool {
match e {
case imm b ButtonEvent {b.pushed;} // Event.ButtonEvent, with no 'use Event;'
case is QuitEvent {false;}
else {false;}
}
}
The same holds in a bound if and in an is test. Because the matched enum is asked first, a use of another enum with a variant of the same name does not change what a pattern means. Pattern matching has the details, including how a generic enum's variant takes its type arguments from the value.
Copying#
A value may be passed around, by copy, just like any other value. This is possible because every variant is the same size — the size of the largest, plus the tag.
Because they are all one size, allocated memory holding one variant can be given a value of a different variant instead, and a pool allocator can hand out and reclaim variants of one enum interchangeably.
If any variant is a move type — it declares Move, has a finalizer, or holds a value that moves — the enum is a move type too, since a value of the enum may be that variant: it is moved rather than copied.
Finalizing#
A value typed as the enum is finalized at the end of its life as the variant it holds would be: the tag says which variant it is. An enum may declare its own finalizer, which runs for every variant, after the variant's own if it has one, and before the finalizers of the variant's fields:
enum Resource {
fn final(self &uni) {
log("released"); // runs for every variant, after its own
}
struct File {
pub handle Handle;
fn final(self &uni) {
close(handle); // runs first, for a File
}
}
struct Nothing {}
}
An enum with nothing to finalize in any variant costs nothing: dropping an Option[i32] does no work, while dropping an Option[String] finalizes the string when it holds one. An owning reference a variant holds is released as the enum value dies, so the owner held in a Some is let go with its Option.
Value Access#
A common field is readable from a value whose variant is not yet known, because every variant holds it at the same offset. To reach a variant's own fields, use pattern matching to establish which variant the value holds. Once it has been narrowed it is used like any other struct value. A name tested for a variant, if s is Circle, is that variant where the test holds, so s.radius reads the field there, until the name may hold another variant (Flow typing); a pattern that binds (if imm c Circle = s, or imm c Circle = s else {...}; past an early exit) gives the variant as a value of its own. A variant declared as a type has no field to read: its pattern binds what it holds.
Method dispatch#
Sometimes every variant should answer the same method with its own implementation. Declare the method on the enum without a body, and let each variant implement it:
enum Number {
pub fn double(self &mut Self);
pub struct Real {
pub n f32;
pub fn double(self &mut Self) {
n *= 2;
}
}
pub struct Complex {
pub r f32;
pub i f32;
pub fn double(self &mut Self) {
r *= 2;
i *= 2;
}
}
}
Calling number.double() on a reference to a Number invokes the implementation belonging to whichever variant the reference points at. The tag says which variant it is, that selects the variant's vtable, and the vtable holds the method.
The same goes for any other trait the enum complies with. A reference to a Number may be passed where a virtual reference to such a trait is expected, and the virtual reference it becomes carries the vtable of whichever variant it points at, chosen by the tag, exactly as a reference to that variant would. Narrowing it with is or match therefore names a variant, never the enum.
The enum need not comply as a whole for a match to use a trait. A match on an enum may name a trait, case imm m Measured, which matches every variant that complies with it and binds a virtual reference to the trait built from the variant, whose vtable the tag chooses (Trait patterns).
A method may also take self by value, and a variant's value calls its own copy:
enum Reading {
pub id i32;
pub fn get(self) i32 {id;}
pub struct Low {}
pub struct High {pub n i32;}
}
Reading.High(1, 4).get() // 1: High's own copy of get
Note: on a value typed as the enum itself, which variant's copy runs would have to be read from the tag the same way, and that is not built for a value. Calling such a method on a value of the enum is an error today; match to recover the variant, or declare the method with a reference receiver, which dispatches as above, or name the enum as the receiver.
One function over the whole enum#
The receiver a method names decides which it is. A method written with self & or self &Self belongs to the variants: it is copied into each, and inside a copy self is that variant. A method whose receiver names the enum — self &Lamp, self &mut Lamp, any other kind of reference, or by value self Lamp — is one function over the whole enum. It is not copied, nothing is dispatched on the tag, and inside it self is the enum, so match self, case is and self is Red work as they do anywhere else:
enum Lamp {
pub n i32 = 7;
Red;
Green;
Off;
pub fn describe(self &Lamp) i32 { // one function over the whole enum
match self {
case Red {10;}
case Green {20;}
case Off {30;}
}
}
pub fn next(self &mut Lamp) { // may store any variant through self
match self {
case Red {*self = Lamp.Green;}
case Green {*self = Lamp.Off;}
case Off {*self = Lamp.Red;}
}
}
pub fn get(self &) i32 {n;} // copied into each variant, as above
}
imm l Lamp = Lamp.Green;
l.describe() // 20
Lamp.Red.describe() // 10: a variant's value calls it, widened to Lamp
A method over the whole enum is called on a value of the enum or on a value of one of its variants, which is widened to the enum for the call, and by value it takes a copy. A variant's value cannot lend &mut Lamp, since the method could store another variant in it: declare the value as the enum, mut l Lamp = Lamp.Red. If a variant declares a method of the same name, a value typed as that variant calls the variant's own, and a value typed as the enum calls the enum's. Within one enum a name is declared once, so the two kinds cannot share a name there. A package that declares such a method defines it once, and an importer calls it. An enum that extends another does not inherit its base's methods over the base, since a value of the extension is not a value of the base. A generic enum's method over the whole enum, self &Option[T], is not available yet.
Complying with a trait#
An enum may say in its declaration that it complies with one or more open traits, exactly as a struct does, with is after its name. What the trait requires and what it gives then reach every variant: a default the variant does not declare becomes its own copy, and a requirement is met by the variant's own method or by one with a body the enum gives every variant.
trait Named {
pub fn label(self &) i32;
pub fn loud(self &) i32 {label() * 100;}
}
trait Measured {
fn size(self &) i32 {7;}
}
enum Shape is Named {
pub k i32;
pub fn label(self &) i32 {k;} // meets Named's requirement for every variant
pub struct Circle {
pub r i32;
pub fn label(self &) i32 {r * 10;}
}
pub struct Square is Measured { // a variant may name traits of its own
pub s i32;
}
}
fn loudOf(n &<Named) i32 {n.loud();}
fn shapeLoud(s &Shape) i32 {loudOf(s);} // passes the variant's vtable, chosen by the tag
loudOf(&Shape.Circle(1, 7)) // 7000: Circle's own label, Named's loud
loudOf(&Shape.Square(2, 3)) // 200: the enum's label, Named's loud
A variant may name traits of its own in the same way, beyond its enum: struct Square is Measured above. Its enum is its base without being written, and naming the enum in the list is an error, since a variant is already a member of the enum it is written inside.
An enum's list and a variant's name only open traits, and none of them is a base in the sense a struct's first one is: the enum lays out itself and every variant, discriminant first, so a trait either one names may not require fields. Nor may any is list name an enum, whether first or later: an enum's variants are all declared inside it, and nothing outside may join the set. An enum that extends another takes its base's traits with the variants it copies and names none of its own.
Comparing#
A value may be compared for equivalence, and not for order: which variant a value holds is what a comparison can answer, and declaration order is not a magnitude.
Where every variant is empty, the value is the tag, so the comparison is exactly a tag comparison:
if phone == PhoneType.Mobile { ... }
Where a variant carries fields, two values are equal when they hold the same variant and that variant's own == says their payloads are equal: the tags are compared first, and only if they match is the variant's == called. A variant that carries nothing is equal to another of its kind by its tag alone. != is the negation. A variant's == is written as any struct's is, taking another of the variant, by value or by reference:
enum Shape {
struct Circle {
pub radius f32;
pub fn `==`(self, other Circle) bool {radius == other.radius;}
}
struct Rect {
pub w f32;
pub h f32;
pub fn `==`(self &, other &Rect) bool {w == other.w and h == other.h;}
}
struct Dot; // equal to any other Dot
}
imm unit Shape = Shape.Circle(1.0);
unit == Shape.Circle(1.0) // true: the same variant, equal payloads
unit == Shape.Circle(2.0) // false: the same variant, a payload that differs
unit == Shape.Rect(1.0, 1.0) // false: a different variant
unit != Shape.Dot // true
The comparison is the enum's: it is written on a value of the enum, and a variant on its left is compared with the variant's own ==, which takes another of that variant.
Comparing with an empty variant asks the tag alone. When either operand is an empty variant, unit == Shape.Dot, unit != Shape.Dot, or the variant on the left, the other value either holds that variant or does not, and the comparison says which, whatever the enum's other variants carry and whether they declare a ==. It is the test unit is Dot in the form of a comparison. This holds for every enum, core's Option included: maybeInt == None and maybeInt != None need no == on Some. The empty variant takes the type of the other operand where it needs one, as it does wherever its enum is expected. Two values that both carry fields still compare as above, and are refused where a variant declares no ==.
A variant carries fields when it holds anything beyond the tag, and that includes the fields every variant of the enum has in common: an enum with pub time i32; compares the time through each variant's own ==, an empty variant's included, since the time is part of what it is.
Every variant that carries fields must declare a == of its own for the enum to be compared: an enum compares by what its variants say, and there is no comparison of a struct's contents to fall back on (deriving one is still to come). If one does not, comparing the enum is an error, and the message names the first variant without one and the fields it carries. Writing that variant's ==, or matching to recover the variant, is the remedy. That includes core's Option and Result, whose Some, Ok and Error declare none: two options holding values are matched, not compared, for now (an option against None is compared, as above).
The operands are only read, wherever they lie: comparing two values of an enum that moves moves neither, and both can be used afterwards.
Declining the padding#
Padding every variant to the largest costs memory, and whether to pay it for the convenience of a value is a choice only you can make. Write @unsized to decline it:
enum @unsized Shape {
pub kind i32;
pub struct Dot;
pub struct Polygon {pub points Array[Point, 64];}
}
Each variant then occupies only what it needs, and the enum has no single size. So no value of it may be held — not in a variable, not in a parameter, not in a field — and it is reached only by reference:
imm shape &Shape = &someDot;
Notice how a reference to a variant coerced automatically into a reference to the enum. Everything else is unchanged: the tag is still in the value, a common field is still readable through the reference, matching still recovers the variant, and dispatch still works. What is given up is the value semantics — holding one in a variable, copying it, passing it by value, and swapping a variant in place.
Most types that want this are graphs and trees, and those are usually better served by an open trait and structs that are-a it: once a type is reached by reference anyway, there is little left for a closed set to buy. So @unsized is for the case that is genuinely closed, wants exhaustive matching, and has variants whose sizes differ enough to matter.
Extending an enum#
An enum may be declared as an extension of another, adding variants to the ones the base declared:
enum Colors {
Red, Green, Blue;
}
enum RichColors extends Colors {
Cyan, Magenta;
}
The extension has its own copy of each of the base's variants, followed by the ones it adds. So RichColors has five variants, RichColors.Red, RichColors.Green, RichColors.Blue, RichColors.Cyan and RichColors.Magenta, each a type of its own. The copies come first and keep the base's tag values, and each carries whatever the base's variant does: its fields, and its methods.
A variant is a value of its own enum and of no other. Colors.Green is a Colors; the RichColors value is RichColors.Green:
imm plain Colors = Colors.Green; imm rich RichColors = RichColors.Green; // the extension's own Green imm added RichColors = RichColors.Magenta;
All five are names of RichColors, so use RichColors; makes all five bare. A module that also folds Colors in would have two of each name the two enums share, so it chooses with as or but. A pattern on a RichColors names all five bare with neither, and means the extension's own. So does a variant written inside RichColors's braces: a method of Cyan that writes Red builds RichColors.Red. A method a copy brought with it was written inside Colors's braces, so its bare Green is still Colors.Green, as its signature, written for Colors, expects.
No substitutability runs between a base enum and its extension, in either direction. They are two distinct types, and the compiler enforces it: neither a value nor a reference nor a virtual reference of one may stand where the other is expected, and neither may one's variant.
So a match on the base is still exhaustive over the base's own variants, which is the property the whole construct rests on: a value typed as the base can only ever hold one of the base's variants, so a match naming those needs no else. A match on the extension must account for the added variants as well.
This is worth saying outright because the intuition points the other way. Everywhere else in Cone a subtype is an abstraction and a value of the concrete type stands in for it; here, adding variants would make a supertype — every value of the base is a valid value of the extension, and no value of the extension is a valid value of the base. Cone does not take that direction, and the two types stay unrelated.
Generic enums#
A generic enum may extend and be extended. Since a generic enum is a family of types, what an enum extends is one of its instances, so the base is always written with its type arguments. A generic extension usually passes its own parameters on:
enum Pending[T] extends Option[T] {
Waiting;
}
Pending[i32] then has three variants: its copies of Option[i32]'s None and Some, holding an i32, and Waiting. They are built, named and matched as any generic enum's are:
imm p Pending[i32] = Pending.Some(5);
imm w Pending[i32] = Pending.Waiting;
match p {
case imm s Some {s;} // Pending[i32]'s Some, holding an i32
case is None {0;}
case is Waiting {-1;}
}
An enum that is not generic may extend one instance of a generic enum. Its copies are then ordinary variants, each holding what that instance's does:
enum IntOrWait extends Option[i32] {
Waiting;
}
imm n IntOrWait = IntOrWait.Some(9);
Everything said above holds instance by instance. Pending[i32] and Option[i32] are two types, and neither's values are the other's. Each is laid out on its own, so Option[&i32] is still a bare pointer while Pending[&i32], with a third variant, carries a tag. A generic base named with no type arguments, as in extends Option, is an error.
Extending an extension#
An extension may itself be extended, plain or generic. Each level copies the one below it, that level's own copies included, and adds its own variants:
enum RicherColors extends RichColors {
Gold;
}
RicherColors has six variants: its copies of RichColors's five, then Gold. Every copy keeps the tag value it had where it was first declared, and the whole chain shares the discriminant of the enum at its bottom. Each level is a type of its own, as a base and its extension always are, so a match on a RichColors is still exhaustive with no arm for Gold.
What an extension may declare#
Beside the variants it adds, an extension may declare methods with a body, static functions and statics of its own:
enum RichColors extends Colors {
Cyan, Magenta;
static shades i32 = 4;
pub fn palette() i32 {shades * 2;}
pub fn weight(self &) i32 {palette() + 1;}
}
A method with a body is given to every variant of the extension: the ones it adds, and its copies of the base's too, so RichColors.Red has a weight method. Inside each, self is that variant, as it is for a method declared on the base. The base's own variants are untouched: Colors.Red has no weight, and neither does Colors. If the base's variant already declared a method of that name itself, its copy keeps its own. A static function and a static stay the extension's own: bare inside its braces, and RichColors.palette() from outside.
Inside an extension's braces, its base's names are bare too, exactly as its own are: the base's static functions, statics and methods, whether the base is generic or not, in the extension's methods and in the variants it adds. Down a chain, every level's names are. So if Colors declares a static function make(), a method of RichColors or of Cyan calls it as make(), and Colors.make() still works too. Where a name is both the extension's and the base's — the copy of a variant is — the extension's is meant, as a nearer name always wins.
What cannot be given to the copies is declared on the base instead, whose members come along with the variants the extension copies:
- A method with no body, a requirement: the copies were written for the base, and have no body of their own to implement it in.
- A common field: it would change the layout of the copies, whose methods were written for the base's.
- The discriminant, which is the base's: an extension writes neither a tag field nor the integer type tag values are laid out in.
- An is, naming traits for the whole set: it would give the copies requirements they have no body to meet. The base's own is comes along with its variants, so an extension of an enum that is a trait is that trait too, copies and added variants alike. A variant the extension adds may still name traits of its own.
And an extension adds names to its base's, and never redeclares or overloads one. A name the base has — a method, a variant, a static, or one it inherits from further down a chain — means the same thing in every enum that extends it, so an extension declaring it again is an error, and so is an added variant taking the name of one the extension already has from its base.
The base's common members work through the extension exactly as through the base: a common field is readable from a value whose variant is not yet known, a default method is available, and a requirement dispatches on the variant — which an added variant must therefore implement, as the base's own variants do.
An extension is inside its base's privacy boundary. Its own methods and static functions, its added variants' methods, and the methods its copies brought with them, reach the private members of its own variants — the copies among them — and of the base's, as the base's own code does, even where the extension is declared in another module. From another module the reach runs one way: the base's code does not see what an extension adds, and two extensions of one base do not see each other's variants, copies included. In one module, all of them are the module's code, and see everything.
Each of the two enums is laid out on its own, since neither holds the other's variants. So extending an enum never changes the base: an Option-shaped enum of an empty variant and one holding a single reference stays a bare pointer, however many enums extend it. And the base never constrains the extension: an added variant may be larger than anything the base holds, and the two may differ about @unsized.
enum Packet {
pub id i32;
struct Ping;
struct Payload {pub a i64; pub b i64;}
}
enum @unsized MorePackets extends Packet {
struct Wide {pub a i64; pub b i64; pub c i64; pub d i64;} // larger than either
}
A tag value pinned by an added variant must still fit the discriminant the base lays its variants out in, sign included, and must not be one the set already holds. If you publish an enum others may extend, adding a variant to it in a later version shifts the values of every extension's own variants, since those are numbered after yours; writing the values out is how to keep them stable across that boundary.