digitalmars.dip.ideas - Flags enum
- Quirin Schroll (75/75) Jun 27 Defining an `enum` type whose members have specific bits set is a
- monkyyy (21/97) Jul 02 ```d
- Nick Treleaven (57/70) Jul 03 `foo.e|foo.c` is not a value of `foo`, so the cast shouldn't be
- Basile B. (6/82) Jul 09 I don't think this is a full progress, only a half. I see that
- Walter Bright (2/2) Jul 13 I agree that defining flags can be a bit tedious, and we can do better.
Defining an `enum` type whose members have specific bits set is a common task. Therefore, the language should make that easy to do correctly and hard to do incorrectly. The main purpose is to make defining and maintaining flags correctly easy. In particular, for normal enums, adding flags anywhere except the end is error prone, as subsequent values need to be updated. In the I propose to add an attribute ` flags` that can be applied to `enum` types only. A ` flags enum` only different from normal a normal enum on the definition side. In particular on the definition side: * It must have some unsigned integer type as its underlying type, the default is `uint`. * It is an error to have the first member unassigned. This is because some flag enums have a neutral element with value `0`, and for others, that makes no sense and they have the first member with value `1`. The language should not make assumptions; requiring the programmer to write `= 0` or `= 1` explicitly is not a big ask. * It is an error to assign `0` to any member except the first. * If the first member is `0`, there must be at least one other member, and the second member must be unassigned, which gives it the value `1`. * Members with explicit values must have an [`OrExpression`](https://dlang.org/spec/expression.html#OrExpression) of pairwise different, previously defined constants (possibly just one constant). * Members without initializers, generally speaking, progress in powers of 2. * As a special exception, the last member may be assigned the underlying type’s `max` (either as `uint.max` or `-1`), for the purpose of expressing an invalid non-zero value. In particular, members without initializers follow these rules: * If it is the second member and the first member has value `0`, it has value `1`. * If the previous member is has no initializer, it has value double the previous member. * If the previous member has a non-zero non-power-of-2 value, consider the member before that. All in all, this means that power-of-2 members have their values implicitly assigned, except for possible doppelgängers, and members with an initializer are ignored for the purpose of value progression as their purpose is to be an abbreviation for prepackaged options. If a flags enum has an invalid value, it is its `init`; otherwise the `init` is zero, even if a neutral option does not exist. ```d flags enum WindowOptions : ubyte { empty = 0, titleBar, // 1 statusBar, // 2 progressBar = statusBar, // doppelgänger minimizeButton, // 4 maximizeButton, // 8 closeButton, // 16 standardButtons = minimizeButton | maximizeButton | closeButton, // prepackaged combo defaultButtons = standardButtons, helpButton, // 32 dialogButtons = closeButton | helpButton, allButtons = defaultButtons | dialogButtons, // closeButton overlaps, but okay invalid = -1 } ``` ```d flags enum Options : ubyte { invalid = -1, // error, can only start with 1 or 0, -1 goes to the end b = 2, // error: can’t skip 1 c = 6, // error, can’t assign values directly } ```
Jun 27
On Thursday, 27 June 2024 at 19:03:01 UTC, Quirin Schroll wrote:Defining an `enum` type whose members have specific bits set is a common task. Therefore, the language should make that easy to do correctly and hard to do incorrectly. The main purpose is to make defining and maintaining flags correctly easy. In particular, for normal enums, adding flags anywhere except the end is error prone, as subsequent values need to be updated. In the I propose to add an attribute ` flags` that can be applied to `enum` types only. A ` flags enum` only different from normal a normal enum on the definition side. In particular on the definition side: * It must have some unsigned integer type as its underlying type, the default is `uint`. * It is an error to have the first member unassigned. This is because some flag enums have a neutral element with value `0`, and for others, that makes no sense and they have the first member with value `1`. The language should not make assumptions; requiring the programmer to write `= 0` or `= 1` explicitly is not a big ask. * It is an error to assign `0` to any member except the first. * If the first member is `0`, there must be at least one other member, and the second member must be unassigned, which gives it the value `1`. * Members with explicit values must have an [`OrExpression`](https://dlang.org/spec/expression.html#OrExpression) of pairwise different, previously defined constants (possibly just one constant). * Members without initializers, generally speaking, progress in powers of 2. * As a special exception, the last member may be assigned the underlying type’s `max` (either as `uint.max` or `-1`), for the purpose of expressing an invalid non-zero value. In particular, members without initializers follow these rules: * If it is the second member and the first member has value `0`, it has value `1`. * If the previous member is has no initializer, it has value double the previous member. * If the previous member has a non-zero non-power-of-2 value, consider the member before that. All in all, this means that power-of-2 members have their values implicitly assigned, except for possible doppelgängers, and members with an initializer are ignored for the purpose of value progression as their purpose is to be an abbreviation for prepackaged options. If a flags enum has an invalid value, it is its `init`; otherwise the `init` is zero, even if a neutral option does not exist. ```d flags enum WindowOptions : ubyte { empty = 0, titleBar, // 1 statusBar, // 2 progressBar = statusBar, // doppelgänger minimizeButton, // 4 maximizeButton, // 8 closeButton, // 16 standardButtons = minimizeButton | maximizeButton | closeButton, // prepackaged combo defaultButtons = standardButtons, helpButton, // 32 dialogButtons = closeButton | helpButton, allButtons = defaultButtons | dialogButtons, // closeButton overlaps, but okay invalid = -1 } ``` ```d flags enum Options : ubyte { invalid = -1, // error, can only start with 1 or 0, -1 goes to the end b = 2, // error: can’t skip 1 c = 6, // error, can’t assign values directly } ``````d struct flag{ int i=1; alias i this; auto opBinary(string s:"+")(int a){ assert(a==1); return flag(i*2); } } enum foo{ a=flag(1),b,c,d,e } void main(){ foo f; f=cast(foo)(foo.e|foo.c); import std; f.i.writeln; } ``` good luck getting such a thing merged into phoboes but this could be a lib solution
Jul 02
On Tuesday, 2 July 2024 at 17:50:13 UTC, monkyyy wrote:struct flag{ int i=1; alias i this; auto opBinary(string s:"+")(int a){ assert(a==1); return flag(i*2); } } enum foo{ a=flag(1),b,c,d,e }Nice!void main(){ foo f; f=cast(foo)(foo.e|foo.c);`foo.e|foo.c` is not a value of `foo`, so the cast shouldn't be used. However the op could be a valid `flag` with an overload for `|` - I renamed `flag` to `Flags` and added that below. Also: * We can support no flags set too. * I disallow setting `Flags` to an integer to prevent accidents, instead use `Flags.mask`. * `Flags.mask(-1)` can be used as an invalid value / all bits set. * I check that the ctor and `opBinary(1)` are not called unless `i` is a multiple of 2 * I check that `opBinary(1)` doesn't overflow `i` ```d struct Flags{ import core.bitop; uint i = 0; alias this = get; uint get() => i; // a must be a multiple of 2 this(uint a) { assert(popcnt(a) == 1); i = a; } static Flags mask(uint a) { Flags f; f.i = a; return f; } // for use inside enum type after a ctor call auto opBinary(string op:"+")(uint a) { assert(a == 1); assert(popcnt(i) == 1); assert(i < 0x80000000); // avoid overflow return mask(i * 2); } auto opBinary(string op: "|")(Flags rhs) => mask(i | rhs.i); auto opBinary(string op: "&")(Flags rhs) => mask(i & rhs.i); } enum Foo { none = Flags(), a = Flags(1), b, c, d, e, m = c | e, invalid = Flags.mask(-1) } void main(){ import std.exception; assert(Foo.none == 0); Flags f = 2; assertThrown!Error(f = Foo(3)); // >1 bit set assertThrown!Error(f = Foo.none + 1); // 0 bits set assertThrown!Error(f = Foo.m + 1); // >1 bit set assertThrown!Error(f = Foo.mask(0x80000000) + 1); // overflow static assert(!__traits(compiles, f = 5)); assert(Foo.m == 20); assert(Foo.invalid & Foo.b); assert((Foo.e & Foo.c) == 0); assert(Foo.m & Foo.c); } ```
Jul 03
On Thursday, 27 June 2024 at 19:03:01 UTC, Quirin Schroll wrote:Defining an `enum` type whose members have specific bits set is a common task. Therefore, the language should make that easy to do correctly and hard to do incorrectly. The main purpose is to make defining and maintaining flags correctly easy. In particular, for normal enums, adding flags anywhere except the end is error prone, as subsequent values need to be updated. In the I propose to add an attribute ` flags` that can be applied to `enum` types only. A ` flags enum` only different from normal a normal enum on the definition side. In particular on the definition side: * It must have some unsigned integer type as its underlying type, the default is `uint`. * It is an error to have the first member unassigned. This is because some flag enums have a neutral element with value `0`, and for others, that makes no sense and they have the first member with value `1`. The language should not make assumptions; requiring the programmer to write `= 0` or `= 1` explicitly is not a big ask. * It is an error to assign `0` to any member except the first. * If the first member is `0`, there must be at least one other member, and the second member must be unassigned, which gives it the value `1`. * Members with explicit values must have an [`OrExpression`](https://dlang.org/spec/expression.html#OrExpression) of pairwise different, previously defined constants (possibly just one constant). * Members without initializers, generally speaking, progress in powers of 2. * As a special exception, the last member may be assigned the underlying type’s `max` (either as `uint.max` or `-1`), for the purpose of expressing an invalid non-zero value. In particular, members without initializers follow these rules: * If it is the second member and the first member has value `0`, it has value `1`. * If the previous member is has no initializer, it has value double the previous member. * If the previous member has a non-zero non-power-of-2 value, consider the member before that. All in all, this means that power-of-2 members have their values implicitly assigned, except for possible doppelgängers, and members with an initializer are ignored for the purpose of value progression as their purpose is to be an abbreviation for prepackaged options. If a flags enum has an invalid value, it is its `init`; otherwise the `init` is zero, even if a neutral option does not exist. ```d flags enum WindowOptions : ubyte { empty = 0, titleBar, // 1 statusBar, // 2 progressBar = statusBar, // doppelgänger minimizeButton, // 4 maximizeButton, // 8 closeButton, // 16 standardButtons = minimizeButton | maximizeButton | closeButton, // prepackaged combo defaultButtons = standardButtons, helpButton, // 32 dialogButtons = closeButton | helpButton, allButtons = defaultButtons | dialogButtons, // closeButton overlaps, but okay invalid = -1 } ``` ```d flags enum Options : ubyte { invalid = -1, // error, can only start with 1 or 0, -1 goes to the end b = 2, // error: can’t skip 1 c = 6, // error, can’t assign values directly } ```I don't think this is a full progress, only a half. I see that the idea is to make the bitsets based on enum members more safe but then why not introduce a native bitset type ? For example that propostion will not lead to lift the code of OrExp, AndExp, XorExp, etc.
Jul 09
I agree that defining flags can be a bit tedious, and we can do better. That's a big reason why I proposed bitfields.
Jul 13