www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - bit-level logic operations on enums

reply d coder <dlang.coder gmail.com> writes:
Greetings

Currently DMD allows me to bit-or two enum variables, but not bit-and. For
example in the following code, DMD is OK with line 6, but gives an error
for line 9. I do not see any logic why DMD does that. Is it a bug or is it
intended.

BTW if I declare all a,b,c and d as "enum BING_e", then DMD gives errors
for both line 6 and 9.

Regards
- Puneet


enum BING_e: byte {NONE_BING = 0b00, FOO_BING = 0b01, // 1
    BAR_BING = 0b10, BOTH_BING = 0b11}                // 2
void main() {                                         // 3
  BING_e a = BING_e.FOO_BING;                         // 4
  BING_e b = BING_e.BAR_BING;                         // 5
  BING_e c = a | b;                                   // 6
  import std.stdio;                                   // 7
  writeln(c);                                         // 8
  BING_e d = a & b;                                   // 9
  import std.stdio;                                   // 10
  writeln(d);                                         // 11
}                                                     // 12
Mar 01 2013
next sibling parent reply FG <home fgda.pl> writes:
On 2013-03-01 13:56, d coder wrote:
 Currently DMD allows me to bit-or two enum variables, but not bit-and. For
 example in the following code, DMD is OK with line 6, but gives an error
 for line 9. I do not see any logic why DMD does that. Is it a bug or is it
 intended.
Strange. | and ^ work but & fails. The promotion from byte to int is unnecessary in any of those 3 ops. It works correctly for non-enums: byte x, y, z = 1; z = x | y; z = x ^ y; z = x & y; z = x >> y; // all OK z = x << y; z = x + y; z = x * y; z = x / y; // can't convert int to byte Not sure why byte enum would be handled any differently than simple byte.
Mar 01 2013
parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 01 Mar 2013 08:38:53 -0500, FG <home fgda.pl> wrote:

 Not sure why byte enum would be handled any differently than simple byte.
An enum is guaranteed by the compiler to be one of the specified values. The simple accounting kept by the compiler to prove whether one type will fit in another doesn't include enums, that is just prohibited, even if it seems like it should result in a valid enum value. -Steve
Mar 01 2013
prev sibling next sibling parent reply "Era Scarecrow" <rtcvb32 yahoo.com> writes:
On Friday, 1 March 2013 at 12:57:14 UTC, d coder wrote:
 Greetings

 Currently DMD allows me to bit-or two enum variables, but not 
 bit-and. For example in the following code, DMD is OK with line 
 6, but gives an error for line 9. I do not see any logic why 
 DMD does that. Is it a bug or is it intended.

 BTW if I declare all a,b,c and d as "enum BING_e", then DMD 
 gives errors for both line 6 and 9.

 enum BING_e: byte {NONE_BING = 0b00, FOO_BING = 0b01, // 1
     BAR_BING = 0b10, BOTH_BING = 0b11}                // 2
 void main() {                                         // 3
   BING_e a = BING_e.FOO_BING;                         // 4
   BING_e b = BING_e.BAR_BING;                         // 5
   BING_e c = a | b;                                   // 6
   import std.stdio;                                   // 7
   writeln(c);                                         // 8
   BING_e d = a & b;                                   // 9
   import std.stdio;                                   // 10
   writeln(d);                                         // 11
 }                                                     // 12
To do the binary operators it converts the flags to numbers, however you have to cast it for it to become a enum again, however this is unsafe as you could accidentally hold a value that has no name; //with your code BING_e c = cast(BING_e) (a | 0x10); //suppose to be 0b10 If you want to work with flags I have a implementation that I think is very good and simple. Note: If you have a flag that covers multiple bits like BOTH_BING, it must match ALL the bits to qualify. https://github.com/rtcvb32/Side-Projects/blob/master/flags.d
Mar 01 2013
parent "Era Scarecrow" <rtcvb32 yahoo.com> writes:
On Friday, 1 March 2013 at 13:41:27 UTC, Era Scarecrow wrote:
  To do the binary operators it converts the flags to numbers,
s/flags/enum(s)
Mar 01 2013
prev sibling parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 01 Mar 2013 07:56:21 -0500, d coder <dlang.coder gmail.com> wrote:

 Greetings

 Currently DMD allows me to bit-or two enum variables, but not bit-and.  
 For
 example in the following code, DMD is OK with line 6, but gives an error
 for line 9. I do not see any logic why DMD does that. Is it a bug or is  
 it
 intended.
It is a bug that line 6 is not an error: BING_e c = a | b; // OK (bug!) writeln(typeof(a | b).stringof); // int BING_e c2 = 0b11; // ERROR, cannot assign int value to enum BING_e (should be the message for 2 lines above as well) D does not (or at least should not) allow assignments of integers back to enumeration types. You have to cast. Any time an enumeration is involved in a math operation, it becomes an integer (as my code shows). The "correct" way to do this is not to store the result of an operation with flags as an enum, but rather as a uint (or ushort or ubyte). Then check flags using the enum. In other words, c and d in your code should be ubytes, not BING_e. The largest drawback to this approach is that typechecking for enums is gone when calling functions -- your function has to take a uint, not the enum, and random bits, other enum bits, etc can be passed in. Of course, technically, this is possible anyway, and often times certain flag sets are invalid. Impossible for the compiler to verify these things for you, you might want to use an in clause for the function to verify the bitset is correct. -Steve
Mar 01 2013
parent reply FG <home fgda.pl> writes:
On 2013-03-01 20:13, Steven Schveighoffer wrote:
 D does not (or at least should not) allow assignments of integers back to
 enumeration types.  You have to cast.
or use normal int enum instead of byte enum, if you have the space for it.
Mar 01 2013
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 01 Mar 2013 14:32:11 -0500, FG <home fgda.pl> wrote:

 On 2013-03-01 20:13, Steven Schveighoffer wrote:
 D does not (or at least should not) allow assignments of integers back  
 to
 enumeration types.  You have to cast.
or use normal int enum instead of byte enum, if you have the space for it.
It still fails if int is the enum base. The issue is not whether the bit pattern will fit in the base type, the issue is assigning an enum from int. -Steve
Mar 01 2013
parent reply FG <home fgda.pl> writes:
On 2013-03-01 20:56, Steven Schveighoffer wrote:
 or use normal int enum instead of byte enum, if you have the space for it.
It still fails if int is the enum base. The issue is not whether the bit pattern will fit in the base type, the issue is assigning an enum from int.
What fails? Are we talking something compiler-version-specific here? enum BING_e { ... } ... // rest the same as in original post BING_e c = a & b; // this works for me BING_e d = a & 1; // fails, and it should fail
Mar 01 2013
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 01 Mar 2013 15:33:32 -0500, FG <home fgda.pl> wrote:

 On 2013-03-01 20:56, Steven Schveighoffer wrote:
 or use normal int enum instead of byte enum, if you have the space for  
 it.
It still fails if int is the enum base. The issue is not whether the bit pattern will fit in the base type, the issue is assigning an enum from int.
What fails? Are we talking something compiler-version-specific here? enum BING_e { ... } ... // rest the same as in original post BING_e c = a & b; // this works for me BING_e d = a & 1; // fails, and it should fail
My test code was incorrect. I had commented out the line that assigned c, and replaced with an assignment to an int (to test something else). In fact, any math on two or more enums is assignable back to an enum (for int or larger). I don't understand why this should be valid for int, but not for any other type. These make no sense, even in terms of bitfield flags, but the compiler accepts them: a << b; a * b; According to the spec, enums are integer promoted based on their base-type. But int isn't promoted, so apparently that enum is left alone. Then, when determining the type of an enum operation, if the two operands are of the same enum type (meaning, they haven't gone through any integer promotion), the result is the same enum type. So this unintuitively results in: enum A : int { a } enum B : byte { b } A a; B b; writeln(typeof(a | a).stringof); // => A writeln(typeof(b | b).stringof); // => int (byte is promoted to int) writeln(typeof(a | b).stringof); // => int writeln(typeof(a | 1).stringof); // => int writeln(typeof(b | 1).stringof); // => int So I guess it works according to the spec, but the spec makes no sense to me. I don't understand why int or uint (or larger) can be used as bitfields, but smaller types cannot. Nor do I understand why arbitrary math operations between enums are valid (can't think of a use case), while arbitrary math between enum and int is not. I would push for one of 4 behaviors, which make some sense to me: 1. Math operations between two enum values of the same type ALWAYS result in the same enum type regardless of base type (like int-based enums) 2. Math operations between two enum values of the same type ALWAYS result in the base type (full-scale enforcement of "enum only contains values from the identified list") 3. Operations between two enums or between an enum and an int, that result at compile-time in a valid member of the enum result in the enum type. Otherwise, the operation is converted to the base type. 4. Enums can be assigned any value from its base type, or any value implicitly convertible to that base type. As it stands now, the compiler makes a half-assed attempt to prevent invalid enum values, but fails miserably in some cases, and is overzealous in others, which is in fact worse than either one alone! At this point, you can't say anything about enums that is always valid except the manifest-constant usage of them. TDPL is puzzlingly silent on enum math. -Steve
Mar 01 2013
parent reply "Era Scarecrow" <rtcvb32 yahoo.com> writes:
On Friday, 1 March 2013 at 21:27:25 UTC, Steven Schveighoffer 
wrote:
 Then, when determining the type of an enum operation, if the 
 two operands are of the same enum type (meaning, they haven't 
 gone through any integer promotion), the result is the same 
 enum type.

 So this unintuitively results in:

 enum A : int { a }
 enum B : byte { b }

 A a;
 B b;
 writeln(typeof(a | a).stringof); // => A
 writeln(typeof(b | b).stringof); // => int (byte is promoted to 
 int)
 writeln(typeof(a | b).stringof); // => int
 writeln(typeof(a | 1).stringof); // => int
 writeln(typeof(b | 1).stringof); // => int
The only way the first makes sense is the compiler knows that it's the same value and is a no-op, thereby doesn't lose it's enum state; At which case b|b should do the same thing. However I'm quite sure the following is true, as otherwise it wouldn't make sense (Unless it knows the enum can only ever be 0 and doesn't matter?) writeln(typeof(a | A.a).stringof); // => int writeln(typeof(A.a | a).stringof); // => int
 I would push for one of 4 behaviors, which make some sense to 
 me:

 1. Math operations between two enum values of the same type 
 ALWAYS result in the same enum type regardless of base type 
 (like int-based enums)
Isn't this the same (or close enough) as 4?
 2. Math operations between two enum values of the same type 
 ALWAYS result in the base type (full-scale enforcement of "enum 
 only contains values from the identified list")
 3. Operations between two enums or between an enum and an int, 
 that result at compile-time in a valid member of the enum 
 result in the enum type.  Otherwise, the operation is converted 
 to the base type.
It might be okay to do it against compile-time known values as it can then confirm if the value is even possible, but this seems inconsistent, and can then break code if the enums values change.
 4. Enums can be assigned any value from its base type, or any 
 value implicitly convertible to that base type.
Then 'final switch' won't work, nor could you know if the value was valid at any point and enums would be easy to abuse.
 As it stands now, the compiler makes a half-assed attempt to 
 prevent invalid enum values, but fails miserably in some cases, 
 and is overzealous in others, which is in fact worse than 
 either one alone!  At this point, you can't say anything about 
 enums that is always valid except the manifest-constant usage 
 of them.

 TDPL is puzzlingly silent on enum math.
Regardless it should likely be consistent with how it handles math and binary operators and enums.
Mar 01 2013
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 01 Mar 2013 17:24:22 -0500, Era Scarecrow <rtcvb32 yahoo.com>  
wrote:

 On Friday, 1 March 2013 at 21:27:25 UTC, Steven Schveighoffer wrote:
 Then, when determining the type of an enum operation, if the two  
 operands are of the same enum type (meaning, they haven't gone through  
 any integer promotion), the result is the same enum type.

 So this unintuitively results in:

 enum A : int { a }
 enum B : byte { b }

 A a;
 B b;
 writeln(typeof(a | a).stringof); // => A
 writeln(typeof(b | b).stringof); // => int (byte is promoted to int)
 writeln(typeof(a | b).stringof); // => int
 writeln(typeof(a | 1).stringof); // => int
 writeln(typeof(b | 1).stringof); // => int
The only way the first makes sense is the compiler knows that it's the same value and is a no-op, thereby doesn't lose it's enum state; At which case b|b should do the same thing. However I'm quite sure the following is true, as otherwise it wouldn't make sense (Unless it knows the enum can only ever be 0 and doesn't matter?) writeln(typeof(a | A.a).stringof); // => int writeln(typeof(A.a | a).stringof); // => int
I was sure too. It's not. Any operation between A types results in A. writeln(typeof(a + a).stringof); // => A writeln(typeof(a << a).stringof); // => A writeln(typeof(a * a).stringof); // => A
 I would push for one of 4 behaviors, which make some sense to me:

 1. Math operations between two enum values of the same type ALWAYS  
 result in the same enum type regardless of base type (like int-based  
 enums)
Isn't this the same (or close enough) as 4?
It would make math between enums and integers type as integers, which would then not be assignable back to the enum. In fact, this is exactly how it works for ints. Not so for smaller types. I'm just going for consistency.
 2. Math operations between two enum values of the same type ALWAYS  
 result in the base type (full-scale enforcement of "enum only contains  
 values from the identified list")
 3. Operations between two enums or between an enum and an int, that  
 result at compile-time in a valid member of the enum result in the enum  
 type.  Otherwise, the operation is converted to the base type.
It might be okay to do it against compile-time known values as it can then confirm if the value is even possible, but this seems inconsistent, and can then break code if the enums values change.
I'm ok with breaking code if the result is a more consistent enum feature.
 4. Enums can be assigned any value from its base type, or any value  
 implicitly convertible to that base type.
Then 'final switch' won't work, nor could you know if the value was valid at any point and enums would be easy to abuse.
You can already do this. See above. -Steve
Mar 01 2013
next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 01 Mar 2013 18:00:57 -0500, Steven Schveighoffer  
<schveiguy yahoo.com> wrote:

 I was sure too.  It's not.  Any operation between A types results in A.

 writeln(typeof(a + a).stringof); // => A
 writeln(typeof(a << a).stringof); // => A
 writeln(typeof(a * a).stringof); // => A
I should add that in my test code (not the code posted previously), A is defined as: enum A { a = 2 } So the zero theory is out. All those result in a non-member value. -Steve
Mar 01 2013
prev sibling parent reply "Era Scarecrow" <rtcvb32 yahoo.com> writes:
On Friday, 1 March 2013 at 23:00:58 UTC, Steven Schveighoffer 
wrote:
 I was sure too.  It's not.  Any operation between A types 
 results in A.

 writeln(typeof(a + a).stringof); // => A
 writeln(typeof(a << a).stringof); // => A
 writeln(typeof(a * a).stringof); // => A
It has to be a bug, as it doesn't make sense for it to be legal.
Mar 01 2013
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 01 Mar 2013 19:22:14 -0500, Era Scarecrow <rtcvb32 yahoo.com>  
wrote:

 On Friday, 1 March 2013 at 23:00:58 UTC, Steven Schveighoffer wrote:
 I was sure too.  It's not.  Any operation between A types results in A.

 writeln(typeof(a + a).stringof); // => A
 writeln(typeof(a << a).stringof); // => A
 writeln(typeof(a * a).stringof); // => A
It has to be a bug, as it doesn't make sense for it to be legal.
Read the spec, carefully. It's implemented as designed. http://dlang.org/type.html Implicit conversion: "A enum can be implicitly converted to its base type, but going the other way requires an explicit conversion." But for integer promotion: "If a enum has as a base type one of the types in the left column, it is converted to the type in the right column." The left column referred to does not have anything 32-bit or above in it besides dchar. Remember that. Later on, regarding arithmetic operations: If one or both of the operand types is an enum after undergoing the [Integer promotion] conversions, the result type is: * If the operands are the same type, the result will be the (sic) that type. * If one operand is an enum and the other is the base type of that enum, the result is the base type. * If the two operands are different enums, the result is the closest base type common to both. A base type being closer means there is a shorter sequence of conversions to base type to get there from the original type. Note that if no integer promotion occurs (i.e. base type is 32 bits or above and not dchar), and you are adding A with A, both operands are of the same type, so "the result will be that type." Just bad design IMO. It should be fixed. -Steve
Mar 01 2013
parent "Era Scarecrow" <rtcvb32 yahoo.com> writes:
On Saturday, 2 March 2013 at 00:36:19 UTC, Steven Schveighoffer 
wrote:
 Read the spec, carefully.  It's implemented as designed.

 http://dlang.org/type.html

 Implicit conversion: "A enum can be implicitly converted to its 
 base type, but going the other way requires an explicit 
 conversion."

 But for integer promotion:

 "If a enum has as a base type one of the types in the left 
 column, it is converted to the type in the right column."

 The left column referred to does not have anything 32-bit or 
 above in it besides dchar.  Remember that.

 Later on, regarding arithmetic operations:

 If one or both of the operand types is an enum after undergoing 
 the  conversions, the result type is:
 * If the operands are the same type, the result will be the 
 (sic) that type.
 * If one operand is an enum and the other is the base type of  
 that enum, the result is the base type.
 * If the two operands are different enums, the result is the 
 closest base type common to both. A base type being closer 
 means there is a shorter sequence of conversions to base type 
 to get there from the original type.
 Note that if no integer promotion occurs (I.e. base type is 32 
 bits or above and not dchar), and you are adding A with A, both 
 operands are of the same type, so "the result will be that 
 type."
I've heard more than once the spec changing to fit an implementation. Still seems wrong. The enum's converted (not promoted) to it's numeric version and cannot be converted back. The details quoted are regarding integer promotion. If it's intended to be that way (enums + math/binary operators = same enum type) then the spec is flat out wrong. Following the simple logic and feature for 'final switch' that it's guaranteed to handles ALL cases and combinations of what the enum could be (thus there's no default); but it can't handle the cases presented. An enum (unless forcibly cast) should always be valid; So anything that could potentially modify the enum can only result in it's base type.
 Just bad design IMO.  It should be fixed.
Agreed, needs to be fixed.
Mar 01 2013