www.digitalmars.com         C & C++   DMDScript  

digitalmars.dip.ideas - Flags enum

reply Quirin Schroll <qs.il.paperinik gmail.com> writes:
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
next sibling parent reply monkyyy <crazymonkyyy gmail.com> writes:
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
parent Nick Treleaven <nick geany.org> writes:
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
prev sibling next sibling parent Basile B. <b2.temp gmx.com> writes:
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
prev sibling parent Walter Bright <newshound2 digitalmars.com> writes:
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