Bug 19399 – Different Conversion Rules for Same Value and Type -- Enum

Status
RESOLVED
Resolution
INVALID
Severity
critical
Priority
P1
Component
dmd
Product
D
Version
D2
Platform
Other
OS
All
Creation time
2018-11-14T00:22:34Z
Last change time
2020-06-26T09:07:59Z
Keywords
pull
Assigned to
No Owner
Creator
Sprink
See also
https://issues.dlang.org/show_bug.cgi?id=10560

Comments

Comment #0 by sprink.noreply — 2018-11-14T00:22:34Z
void foo(byte v) { writeln("byte ", v); } void foo(int v) { writeln("int ", v); } enum A : int { a = 127, b = 128, // shh just ignore this } void main() { A v = A.a; foo(A.a); // byte 127 foo(v); // int 127 should be byte 127 } Per https://issues.dlang.org/show_bug.cgi?id=10560 the desired type to call is "byte". So the bug should be fixed so that both the function call the foo(byte) overload.
Comment #1 by eiderdaus — 2018-11-14T12:25:09Z
This looks like not-a-bug to me: VRP examines literals (A.a) and concludes that A.a fits in a byte. But VRP will not deduce from the code flow that v contains some value that fits in a byte. The line `b = 128,` can be removed, the example will produce the same output then on DMD64 D Compiler v2.083.0: The literal A.a goes to foo(byte) and A v goes to foo(int) even when v == A.a.
Comment #2 by david.eckardt — 2019-02-14T11:22:16Z
This looks like a bug to me: int f(ubyte x) { return x.sizeof; } int f(ushort x) { return x.sizeof; } enum E: ushort {a = 10} static assert(E.a.sizeof == E.sizeof); // succeeds static assert(E.a.sizeof == f(E.a)); // fails, f(ubyte) is called E.a.sizeof can be 1 or 2 depending on how it is used. This is obviously an error. My guess is that for a function call “E.a” is treated as if it was the numeric literal “10”, then typeof(10) is used to pick the function from the overloads.
Comment #3 by david.eckardt — 2019-02-14T11:29:11Z
(In reply to David Eckardt from comment #2) > int f(ubyte x) { return x.sizeof; } > int f(ushort x) { return x.sizeof; } > > enum E: ushort {a = 10} > > static assert(E.a.sizeof == E.sizeof); // succeeds > static assert(E.a.sizeof == f(E.a)); // fails, f(ubyte) is called Extended the example: int f(ubyte x) { return x.sizeof; } int f(ushort x) { return x.sizeof; } enum E: ushort {a = 10} immutable y = E.a; static assert(E.sizeof == 2); // succeeds static assert(E.a.sizeof == E.sizeof); // succeeds static assert(y.sizeof == E.sizeof); // succeeds static assert(E.a.sizeof == f(E.a)); // fails, f(ubyte) is called
Comment #4 by david.eckardt — 2019-02-15T13:20:27Z
The wrong overloaded function is picked with arrays as well. An array of integers can be implicitly cast to char[]. This should not happen. int f(const char[] str) {return 1;} int f(const void[] data, int type_id) {return 2;} assert(f("abc") == 1); // As expected. assert(f("abc", 5) == 2); // As expected. assert(f([1, 2, 3]) == 1); // Compiles - an array of integers matches f(char[])?! f([1, 2, 300]); // Error: None of the overloads of `f` matches, as expected enum e = [4, 5, 6]; static assert(is(typeof(e) == int[])); // So we do have ints here... assert(f(e) == 1); // ... and int[] matches f(char[])?!? immutable a = e; f(a, 7); // Compiles f(a); // Error: None of the overloads of `f` matches, as expected
Comment #5 by simen.kjaras — 2019-02-15T14:11:20Z
I started out thinking this was not a bug, since enum values are basically inserted verbatim into the code. And indeed, this is exactly what should happen in cases like this: int fun(ubyte) { return 0; } int fun(ulong) { return 1; } enum M = 10; // Exactly equivalent to foo(10); - will call the ubyte overload. static assert(fun(M) == 0); However, this intuition breaks down when the enum has a specified type: enum ulong N = 10; // Will always call the ulong overload. static assert(fun(N) == 1); Both of these cases are working correctly. The error is with enums with curly brackets. If no type is specified for the enum, the behavior is correct: enum O { O = 10 } // Exactly equivalent to foo(10); - will call the ubyte overload. static assert(fun(O.O) == 0); However, when the type is explicitly specified, the error occurs: enum P : ulong { P = 10 } // Should call the ulong overload, but type information is discarded and the ubyte overload is called instead. This assert will fail. static assert(fun(P.P) == 1); VRP, as pointed out by Simon in comment 1, is what causes the conversions, even in the array examples in comment 4. Simply put, the compiler prefers to interpret a number as an int, but will try other types if they are a better fit. The only issue is that VRP is used when a type is explicitly specified for an enum with braces.
Comment #6 by david.eckardt — 2019-02-15T15:01:25Z
I see two more issues here. 1. For both suffix-less integer literals and `enum name = value` style (i.e. implicit type, no braces) `typeof` yields `int` although they may be coerced into a different type. The same applies for `sizeof` and `alignof. This can cause subtle bugs which are hard to track down (sorry, I am a bit upset because it just happened to me). `typeof`, `sizeof` and the like should be consistent here. As long as they are not, the way of working around type coercion and avoiding bugs is to use `cast(typeof(expression))expression`, which needs an explanation or the next person looking at the code will have a hard time trying to understanding what it is for. 2. Enums and literals of integer arrays such as `[1, 2, 3]` should not match `char[]`. Even `ubyte[]` cannot be implicitly cast to `char[]`.
Comment #7 by dlang-bot — 2019-06-27T13:12:26Z
@RazvanN7 created dlang/dmd pull request #10099 "Fix Issues 19399 and 10560 - Different Conversion Rules for Same Value and Type Enum" fixing this issue: - Fix Issues 19399 and 10560 - Different Conversion Rules for Same Value and Type Enum https://github.com/dlang/dmd/pull/10099
Comment #8 by bugzilla — 2020-06-26T08:17:11Z
Here's how function overload resolution works: f(ubyte); f(ushort); enum E : ushort { a = 10; } f(E.a); // which is called? There are 4 matching levels, 1 is worst and 4 is best: 1. no match 2. implicit conversion 3. conversion to const 4. exact match E.a can be implicitly converted to a ubyte. E.a can also be implicitly converted to ushort. Both are match level 2. To disambiguate this, we now move to "partial ordering". For partial ordering, we ignore what the argument is. We only look at the function parameters. We ask the question "can the parameters of f(ubyte) be used to call the f(ushort)?" The answer is yes, because ubyte can be implicitly converted to ushort. Then we ask "can the parameters of f(ushort) be used to call f(ubyte)?" The answer is no, because ushort cannot be implicitly converted to ubyte. Therefore, f(ubyte) is "more specialized", and the more specialized function is selected. Partial matching is a powerful method, and is far simpler than the C++ method of having a couple pages of match levels which nobody understands. Tellingly, C++ switch from match levels to partial ordering for template overload resolution. D benefited by this experience and uses partial ordering for both functions and templates. The compiler is correctly implementing the language semantics.
Comment #9 by bugzilla — 2020-06-26T08:21:37Z
(In reply to Sprink from comment #0) > void foo(byte v) { writeln("byte ", v); } > void foo(int v) { writeln("int ", v); } > > enum A : int { > a = 127, > b = 128, // shh just ignore this > } > > void main() > { > A v = A.a; > foo(A.a); // byte 127 > foo(v); // int 127 should be byte 127 > } foo(A.a) is passing an integer literal of type enum and value 127. 127 can be implicitly converted to both byte and int, so foo(byte) and foo(int) both match with conversion level 2. Partial ordering selects foo(byte). foo(v) is passing an integer variable of type enum with a base type of int. This cannot be implicitly converted to byte, but it can be implicitly converted to int. Hence, foo(int) is selected. Partial ordering does not come into play. Note that the compiler explicitly does NOT do data flow analysis in the front end to determine that v is 127. The compiler is working correctly as the language is designed.
Comment #10 by bugzilla — 2020-06-26T08:30:22Z
(In reply to Simen Kjaeraas from comment #5) > I started out thinking this was not a bug, since enum values are basically > inserted verbatim into the code. And indeed, this is exactly what should > happen in cases like this: > > int fun(ubyte) { return 0; } > int fun(ulong) { return 1; } > > enum M = 10; > // Exactly equivalent to foo(10); - will call the ubyte overload. > static assert(fun(M) == 0); Correct, because M is typed as an int and matches with conversion to both fun(ubyte) and fun(ulong), then partial ordering picks fun(ubyte). > However, this intuition breaks down when the enum has a specified type: > > enum ulong N = 10; > // Will always call the ulong overload. > static assert(fun(N) == 1); Correct, because here N is typed as a ulong, and so will pick fun(ulong) which is an exact match, while fun(ubyte) is a match with conversion. > Both of these cases are working correctly. The error is with enums with > curly brackets. If no type is specified for the enum, the behavior is > correct: > > enum O { > O = 10 > } > // Exactly equivalent to foo(10); - will call the ubyte overload. > static assert(fun(O.O) == 0); Yes, because O is an enum that converts to both ubyte and ulong, then partial ordering picks f(ubyte). > However, when the type is explicitly specified, the error occurs: > > enum P : ulong { > P = 10 > } > // Should call the ulong overload, but type information is discarded and the > ubyte overload is called instead. This assert will fail. > static assert(fun(P.P) == 1); ulong is NOT the type. The type is an enum that is implicitly convertible to its base type, which is ulong. The match to both functions is by conversion, which is resolved by partial ordering, again picking fun(ubyte). > VRP, as pointed out by Simon in comment 1, is what causes the conversions, > even in the array examples in comment 4. Simply put, the compiler prefers to > interpret a number as an int, but will try other types if they are a better > fit. The only issue is that VRP is used when a type is explicitly specified > for an enum with braces. No, this is not what is happening here. The compiler is working correctly as specified by the language.
Comment #11 by bugzilla — 2020-06-26T08:39:45Z
(In reply to David Eckardt from comment #6) > I see two more issues here. Please file them as two separate issues, as they are not applicable here.
Comment #12 by simen.kjaras — 2020-06-26T09:07:59Z
(In reply to Walter Bright from comment #10) > (In reply to Simen Kjaeraas from comment #5) > > However, when the type is explicitly specified, the error occurs: > > > > enum P : ulong { > > P = 10 > > } > > // Should call the ulong overload, but type information is discarded and the > > ubyte overload is called instead. This assert will fail. > > static assert(fun(P.P) == 1); > > ulong is NOT the type. The type is an enum that is implicitly convertible to > its base type, which is ulong. > > The match to both functions is by conversion, which is resolved by partial > ordering, again picking fun(ubyte). Agreed on all points. The problem may instead be said to be with intuition - it seems natural that two conversions are involved when converting P.P to an int: first the enum is converted to its base type, then that is converted to int. Furthermore, two conversions seems like a worse match than a single conversion. I acknowledge that's not how it works or is specified to work, but I can see expectations being different.