www.digitalmars.com         C & C++   DMDScript  

digitalmars.dip.ideas - Final Enumerations

reply Quirin Schroll <qs.il.paperinik gmail.com> writes:




An enumeration is not a fixed set of values. It’s a selection of 
values of another type. Add a kind of enumeration type that is a 
fixed set of values.



In Java, enums are fixed sets.


them](https://github.com/dotnet/csharplang/blob/main/proposa
s/closed-enums.md). (This link will break if a decision on the proposal is
made, [permalink](https://github.com/dotnet/csharplang/blob/fac2b6993a2980132a15eaf5189dec7e109ff2af/proposals/closed-enums.md).)



The reason why such enumerations are valuable is because it can 
be ensured at compile-time that a `switch` handles all values.

Since D has `final` as a keyword, and it mirrors `final switch`, 
why not add `final enum` as a kind of enumeration such that the 
compiler enures values of that type are not outside the range of 
values defined.

Values of final enum type implicitly convert to a type if their 
base type does. That handles the conversion to the base type 
itself. That means a `final enum Option : bool { no, yes }` would 
not suffer from the implicit conversion of `0` or `1` to it, but 
`Option.no` and `Option.yes` would convert to `0` and `1` 
implicitly.





No grammar change is needed. `final enum` is already valid, 
however, `final` currently does not change the semantics of 
enumerations.

Technically, this is a breaking change. Either breakage (which is 
likely rare in practice) is acceptable here or adding this in an 
edition solves the breakage problem.



The usual way to create values of a final enum would be to refer 
to the enumeration directly.

Using a base-type value for the enum without an explicit cast is 
an error. The explicit cast is ` safe` only if VRP can prove that 
the base-type value will, in fact, be one of the enum values. 
Otherwise, it is ` system`; the compiler may add an assertion 
that the value is in range. (If the range of an enum is a 
contiguous section of a built-in integral type, that check is 
cheap, on the order of checking if indices are in range.)

Final enum types are types like `bool` and pointers for which it 
is undefined behavior if they end up having a value not listed in 
the enumeration. Producing such a value falls into the same 
category as a `bool` with a bit pattern other than `0x0` or 
`0x1`, or a non-null pointer that does not point to an object of 
the appropriate type. This affects `union` types when they’re 
members.

In D, enums can have a wide range of built-in types, including 
integers, floats, slice types (includes strings), pointers 
(including function pointers), class types, and associative array 
types. Checking that a run-time value is equal to one of these 
can be expensive. If that is deemed inappropriate for a 
core-language feature, it can be an error for any types for which 
it is more expensive than simple value comparisons.

A final enum always has a value equivalent to its `init` with the 
default value of its base type. Such a value must exist and it 
must be named (it need not be unique). This value must be handled 
by `final switch`, either by referring to `init` or an 
enumeration constant equal to it (often named `none` or 
`unknown`). (This restriction could be lifted, and if a final 
enum lacks a value equal to `init`, it has a hidden enumeration 
constant named `init` with the default value.)

If a non-final `switch` statement handles all cases of a value of 
final enum type, having a `default:` case is an error 
(unreachable code), except if the default case is, effectively, 
just an `assert(0)`. Such `switch` statement must be a `final 
switch` or the default marked as unreachable.

The only pain point about the notion and keyword: A final enum is 
not final in the sense of inheritance.





```d
final enum E0 { x, y, z }
final enum E1 : E0 { a, b } // good: `E1.a = E0.x` and `E1.b = 
E0.y`
final enum E2 : E0 { a, b, c, d } // Error: implicit initializer 
to `d` is out of range of `E0`
final enum E3 : E0 { p = E0.a, q = E0.z } // good, need not be 
contiguous

E1 e1 = E1.a;
E0 e0 = e1; // good, E1 is a sub type of E0
e1 = cast(E1)e0; // run-time check will succeed, since e0 is in 
fact E1.a
e0 = E0.z;
e1 = cast(E1)e0; // run-time check will fail, since e0 > E1.max
int x = 12;
e0 = x & 0x3; // good: VRP can prove E0.min ≤ `x & 0x3` ≤ E0.max 
and E0 is contiguous

// If VRP isn’t good enough to prove it’s valid, error in  safe 
code:
{ E3 e3 = x ? E0.x : E0.z; }

// If VRP isn’t good enough to prove it’s valid, run-time check 
succeeds …
{ E3 e3 = cast(E3)(x ? E0.x : E0.z); }
// … but it has to check values individually, since E3 isn’t 
contiguous.
```


```d
// E0.init is cast(E0)int.init which succeeds and is E0.x
// E1.init is E1.a for the same reason
// E2 is invalid
// E3.init is E3.p for the same reason

// restriction intact:
final enum E4 : int { y = 1 } // Error, no option for int.init
// restriction lifted:
final enum E4 : int { y = 1 } // Okay, equivalent to { init = 
int.init, y = 1 }

final enum E5 { init, y } // Good
final enum E6 { x, y, init } // Okay
final enum E7 { y = 1, init } // Okay
final enum E8 { y = 1, init = 0xFF } // Okay

// restriction intact:
final enum E9 : char { x } // Error, no option for char.init, 
which is 0xFF
// restriction lifted:
final enum E9 : char { x } // Okay, equivalent to { x = 0, init = 
char.init } because char.init is 0xFF
```



```d
E1 e1;
final switch (e1) { case E1.a, E1.b: } // Okay
switch (e1) { case E.a: default: } // Okay, default handles E.b
switch (e1) { case E1.a, E1.b: default: } // Error, all cases 
handled, default unreachable
switch (e1) { case E1.a, E1.b: default: assert(0); } // Okay, 
default is unreachable, but marked as such

E4 e4; // Assume restriction lifted
final switch (e4) { case E4.y: } // Error: init not handled
final switch (e4) { case E4.y, E4.init: } // Okay
switch (e4) { case E4.y: default: } // Okay, default handles init
```
Oct 15
next sibling parent reply Nick Treleaven <nick geany.org> writes:
On Wednesday, 15 October 2025 at 11:34:57 UTC, Quirin Schroll 
wrote:
 A final enum always has a value equivalent to its `init` with 
 the default value of its base type. Such a value must exist and 
 it must be named (it need not be unique). This value must be 
 handled by `final switch`, either by referring to `init` or an 
 enumeration constant equal to it (often named `none` or 
 `unknown`). (This restriction could be lifted, and if a final 
 enum lacks a value equal to `init`, it has a hidden enumeration 
 constant named `init` with the default value.)
Why is this needed? Non-final enum has `init` set to the first member value. I understand maybe wanting to support one of the other members being the `init` value, but why would it have to be the same value as the base type's `init`?
Oct 16
parent Quirin Schroll <qs.il.paperinik gmail.com> writes:
On Thursday, 16 October 2025 at 16:13:48 UTC, Nick Treleaven 
wrote:
 On Wednesday, 15 October 2025 at 11:34:57 UTC, Quirin Schroll 
 wrote:
 A final enum always has a value equivalent to its `init` with 
 the default value of its base type. Such a value must exist 
 and it must be named (it need not be unique). This value must 
 be handled by `final switch`, either by referring to `init` or 
 an enumeration constant equal to it (often named `none` or 
 `unknown`). (This restriction could be lifted, and if a final 
 enum lacks a value equal to `init`, it has a hidden 
 enumeration constant named `init` with the default value.)
Why is this needed? Non-final enum has `init` set to the first member value. I understand maybe wanting to support one of the other members being the `init` value, but why would it have to be the same value as the base type's `init`?
You’re right. I actually didn’t know (and didn’t check) that an enum’s `init` is its first value; I thought it would be copying the underlying type’s `init`. Since that’s the case, all the `init` stuff isn’t needed. Good, because I didn’t like it. Making an enum final shouldn’t change what its `init` is.
Oct 16
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Wednesday, 15 October 2025 at 11:34:57 UTC, Quirin Schroll 
wrote:




 An enumeration is not a fixed set of values. It’s a selection 
 of values of another type. Add a kind of enumeration type that 
 is a fixed set of values.

 [...]
I would rather see stricter rules enforced for existing `enum` types (at least in ` safe` code), but I understand there are practical reasons that may not be feasible. If this is the best we can do, then I support it.
Oct 16
parent Quirin Schroll <qs.il.paperinik gmail.com> writes:
On Thursday, 16 October 2025 at 17:01:40 UTC, Paul Backus wrote:
 On Wednesday, 15 October 2025 at 11:34:57 UTC, Quirin Schroll 
 wrote:




 An enumeration is not a fixed set of values. It’s a selection 
 of values of another type. Add a kind of enumeration type that 
 is a fixed set of values.

 [...]
I would rather see stricter rules enforced for existing `enum` types (at least in ` safe` code), but I understand there are practical reasons that may not be feasible. If this is the best we can do, then I support it.
Whether `cast(EnumType)42` makes sense depends entirely on your intention. I don’t think is a great idea to take if off people’s plates.
Oct 16