Thread overview
enum confusion
May 09, 2022
Don Allen
May 09, 2022
Ali Çehreli
May 10, 2022
Don Allen
May 11, 2022
Ali Çehreli
May 10, 2022
Salih Dincer
May 09, 2022

I've posted here before and then took a D hiatus. I'm working on the pre-hiatus project again, which involves porting about 9000 lines of working but ugly C I wrote 10 years ago, implementing a suite of personal financial management software.

Programming languages are an area of interest of mine and have been for a long time. In the years since I started writing code (over 60 years ago!) professionally and otherwise, I've used just about every language you can think of and some you can't. I considered a number of languages for this project (e.g, Rust, Scheme, Haskell, Nim, Zig) and rejected all for various reasons. So far, I've been mostly happy with my choice of D and much of the work is done, with the core functionality working. The code is more concise and readable than the C it came from. Performance is absolutely fine. I love the fast compile times (dmd on a FreeBSD system) and the ability to debug with gdb.

But ... you knew this was coming ... I find enums and/or their documentation to be a weak spot. As time permits, I'll have more to say about this, but I want to raise an initial issue here for comments.

The Language Reference has three subsections on enums -- Named Enums, Anonymous Enums and Manifest Constants. The first mention that I can find of when enums are evaluated and how/where they are stored occurs in the Manifest Constants subsection (17.3): "Manifest constants are not lvalues, meaning their address cannot be taken. They exist only in the memory of the compiler." This can be read to suggest that the other two types of enums described are lvalues, though the documentation doesn't say one way or the other. Finding this hard to believe, I wrote a little test code:

  1 import std.stdio;
  2
  3 enum Foo {
  4     bar
  5 }
  6
  7 int main(string[] args)
  8 {
  9     writefln("debug: %d\n", &(Foo.bar));
 10     return 0;
 11 }

Compiling results in

test1.d(9): Error: manifest constant bar cannot be modified

Foo.bar is not a manifest constant according to the Language Ref; it's a Named Enum as described in 17.1. 17.3 defines manifest constants as a special case of Anonymous Enums, those having a single member. But if you add another member to the Named Enum in my test:

  1 import std.stdio;
  2
  3 enum Foo {
  4     bar,
  5     baz
  6 }
  7
  8 int main(string[] args)
  9 {
 10     writefln("debug: %d\n", &(Foo.bar));
 11     return 0;
 12 }

making a Named Enum with multiple members -- certainly not what the documentation is calling a manifest constant -- and yet you get the same error message from the compiler, referring to 'bar' as a manifest constant.

This suggests to me that the compiler is doing what I expect it would and should do -- evaluating all enums at compile time, none of them lvalues -- whether named, anonymous or what the documentation calls manifest constants. It would appear that the only difference among the various enum flavors is whether a new type is created or not (named vs anonymous) and a little syntactic sugar allowing the omission of braces for single-member anonymous enums. There may be additional differences in how the various cases are treated by to!string that I can't discuss right now because I haven't looked at this carefully enough.

So my conclusion is that there's a disconnect between the Language Reference and what the compiler is actually doing and I think the problem is that documentation is incorrect, or misleading at best.

I'm happy to hear the observations of people who know this language better than I do. Perhaps I'm missing something here.

May 08, 2022
On 5/8/22 18:50, Don Allen wrote:

> I'm working on the
> pre-hiatus project again

Welcome back!

> So far, I've been mostly happy with my choice of D
> and much of the work is done, with the core functionality working. The
> code is more concise and readable than the C it came from. Performance
> is absolutely fine. I love the fast compile times (dmd on a FreeBSD
> system) and the ability to debug with gdb.

I am feeling panicky as DConf submission deadline is approaching (May 15, 2022). I would love to watch your report on this project.

> Named Enums,

Check.

> Anonymous Enums

What a strange feature. :)

> and Manifest Constants.

Check.

> "Manifest constants are not
> lvalues, meaning their address cannot be taken. They exist only in the
> memory of the compiler." This can be read to suggest that the other two
> types of enums described *are* lvalues

Yes, unclear wording but no, none of them are lvalues. Equivalent constructs that I can think of are 'static const' and 'static immutable'. Those would be evaluated at compile time but have addresses:

static const a = foo();
static immutable b = bar();

int foo() {
  return 42;
}

int bar() {
  return 43;
}

void main() {
  assert(&a != &b);
}

> evaluating all enums at compile time, none of them
> lvalues

YES! :)

Ali

P.S. While looking for traits related to rvalues and lvalues, I realized that at least isRvalueAssignable() and isLvalueAssignable() do not appear at the contents section on this page:

  https://dlang.org/phobos/std_traits.html

There are issues with Phobos documentation. :/
May 10, 2022
Ali --

Thanks for the helpful comments. I hope these documentation issues get fixed. I'll submit an issue on github and if I can find the time, will submit a pull request to fix the enum discussion in the Language Reference.

Speaking of documentation, your book has been invaluable as I've made my way through this project. It really is excellent.

/Don
May 10, 2022

On Monday, 9 May 2022 at 01:50:44 UTC, Don Allen wrote:

>

[...]
Foo.bar is not a manifest constant according to the Language Ref; it's a Named Enum as described in 17.1. 17.3 defines manifest constants as a special case of Anonymous Enums, those having a single member. But if you add another member to the Named Enum in my test:

  1 import std.stdio;
  2
  3 enum Foo {
  4     bar,
  5     baz
  6 }
  7
  8 int main(string[] args)
  9 {
 10     writefln("debug: %d\n", &(Foo.bar));
 11     return 0;
 12 }

making a Named Enum with multiple members -- certainly not what the documentation is calling a manifest constant -- and yet you get the same error message from the compiler, referring to 'bar' as a manifest constant.

Parentheses are there to make things easier for me. Same for the compiler. That is to avoid confusion or misunderstanding. We get the same result in the 8 examples below.

struct Foo {
  int bar;
  alias bar this;
  auto far() { return &bar; }
}
void main()
{
  Foo foo;
  // to direct
  "1: ".writeln(&foo.bar);
  (&foo.bar).writefln!"2. %s";
  "3: ".writeln(&(foo.bar));
  (&(foo.bar)).writefln!"4. %s";
  // by alias
  "5. ".writeln(&foo);
  (&foo).writefln!"6. %s";
  // by function
  "7: ".writeln(&(*foo.far));
  (&(*foo.far)).writefln!"8. %s";
}

We do similar things for a mathematical operation, or we avoid parentheses if they comply with arithmetic precedence rules.

In summary, for us humans, sometimes the definitions are different, but in machine language everything is the same.

SDB@79

May 11, 2022
On 5/10/22 05:16, Don Allen wrote:

> I'll submit an issue on github and if I can find the time, will
> submit a pull request to fix the enum discussion in the Language Reference.

Much appreciated! :)

> Speaking of documentation, your book has been invaluable as I've made my
> way through this project. It really is excellent.

Thank you! That means a lot to me.

Ali

May 11, 2022

On 5/8/22 9:50 PM, Don Allen wrote:

>

But ... you knew this was coming ... I find enums and/or their documentation to be a weak spot. As time permits, I'll have more to say about this, but I want to raise an initial issue here for comments.

There are 2 types of enum meaning -- one is like #define, and declares there's some value that only exists at compile-time. That's the manifest constant.

That is done by using enum kind of as a storage class (like const).

enum x = 5; // x is now a manifest constant of type int.
enum byte y = 5; // y is now a manifest constant of type byte.

writeln(&x); // error, not an lvalue, this is equivalent to:
writeln(&5);

The second type of enum meaning is to declare a type. This type can be an lvalue:

enum Foo {
   bar
}

Foo foo;
writeln(&foo); // ok

The symbol Foo now becomes a type, and can be used to denote an integer value that has only one value (Foo.bar). Though technically, it can be any int value via casting or math operations. You can also specify a base type if desired.

Some libraries use this as a crude typedef (with extra manifest constant properties).

An anonymous enum is a weird way of declaring a bunch of manifest constants, and use the "= last member + 1" feature for declaring them (if that's what you want). I don't think many people use them. Yes, I see that they are the same thing as manifest constants with one member, but I don't ever think of them that way.

You can also declare an enum with an identifier, but without any members and becomes a type without a default initializer. Mostly usable as a handy UDA tag.

enum dontSerialize;

struct S
{
   @dontSerialize string comment;
}

enum is definitely one of the stranger things in D.

-Steve