Thread overview
Flags enum
Jul 02
monkyyy
3 days ago
Basile B.
June 27

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 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.

@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
}
@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
}
July 02

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 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.

@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
}
@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
}
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

July 03

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
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);
}
3 days ago

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 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.

@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
}
@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.