enums and version/static if/"inheritance"

Witold Baryluk witold.baryluk+d at gmail.com
Sun Jul 28 22:20:47 UTC 2024


I do often develop some low level codes, often interacting with 
Linux kernel, or some embedded systems. Where unfortunately some 
definitions of various constants and interfaces vary vastly 
between architectures or oses, or compilation options.

My today complain would be with `enums`.

A good context is just checking various `mman.d` files in phobos, 
where a number of top level `enum` values are defined using mix 
of version and static ifs, or even a bit of logic for fallback.

Unfortunately this only works for top level enum values. It does 
not work for named enum types.

A simple example

```d
enum FOO {
   A = 5,
   B = 6,
version (x86_64) {
   C = 7,
} else version (AArch64) {
   C = 17,
} else {
   static assert(0);
}
   version E = 9,
}

// static assert(checkUniqueValues!FOO);
```

Is simply not supported (not even that surprising considering 
that enum members are separated by comma, not a semicolon).


Same with `static if`.

Doing `enum FooBase { ... }  version () ... enum FOO : FooBase { 
... }` will not work (it does something else that expected)

One will say, how about this:

```d
enum FOO {
   A = 5,
   B = 6,
   C = ((){ version (x86_64) return 7; else return 17; })(),
   E = 9,
}


// static assert(checkUniqueValues!FOO);
```

That is unfortunately not good enough, and very verbose. There 
are often multiple values that need to have these branching 
structure, and now one is forced to repeat it multiple time.

And even worse, this only allows defining enum members that only 
differ in value. One cannot use this method to decide if enum 
member/value exists or not (or maybe put @deprecated or other UDA 
on them conditionally).

Example might be on linux x86_64 one should only use 
`HUGE_TLB_2MB` and `HUGE_TLB_1GB`, but on ppc64, different values 
are allowed, like only `HUGE_TLB_16MB` is allowed. In linux uapi 
/ libc all values are defined on all archs, but using wrong ones 
will definitively will cause a runtime error, and it would be 
preferably that library just hides unsupported ones by default.

Another example might be enum with some values hidden because 
they are broken / deprecated, but some special `version` could be 
used to enabled if one knows what they are doing (Example on 
linux might be some syscall numbers, which are deprecated or 
essentially broken, and superseded by better interfaces, example 
might be `tkill`, or `renameat`).


And last consideration, again for `mmap` flags.

There is a set of mmap flags defined by posix (the actual values 
might different between systems  and even archs). And a bunch of 
extra flags supported on various systems. In Phobos these is done 
by just having top level enum values (not part of any enum type), 
and importing proper modules with these values.

This makes impossible to write "type-safe" wrappers around `mmap` 
(lets assume we are calling Linux kernel directly so we can 
ignore all glibc / musl stuff, and we can invent a new 
interface), and flags must be just int.

In ideal world, one would `import ...linux.mman : mmap`, and it 
would have signature:

```d
module ...linux.mman;

version (X86_64) {
void* mmap(void* p, size_t, MMapProt prot, MMapFlags flags, int 
fd, off_t off) {
   return cast(void*)syscall!(Syscall.MMAP)(cast(ulong)p, 
cast(uint)prot, ...)
}
}
```

where for example MMapFlags would be (hypothetically):

```d
module ...linux.mman;

enum MMapFlags : imported!"...posix.mman").MMapFlags {
   // some extra linux specific flags
   MAP_DROPPABLE = 0x08,
   MAP_UNINITIALIZED = 0x4000000,
}
```

and common ones:

```d
module ...posix.mman;

enum MMapFlags {
   version (linux) {
     MAP_PRIVATE = 0x02,
   }
   // ...
}
```


So only options are:

1. Redefine entire struct of the enum (including comments, and 
expressions, i.e. if some enum member values are expression, like 
`MAP_HUGE_1GB = 30U << HUGETLB_FLAG_ENCODE_SHIFT,`) for every 
possibly combination of system, architecture, and in some cases 
even libc. This does not scale well.

2. Create some kind of string DSL, CTFEs and `mixin`s to build 
the full enum definition, something like this possible.

```d
mixin(buildEnum(`
   A 5
   B 6
   C 7 if x86_64
   C 17 if AArch64
   E 9
`));
```

And then provide some reflection based enum inheritance (where 
enum member values or their presence can be also be steered by 
some static ifs / version logic). Surely doable, but not very 
clean.

But it is pretty ugly and limiting, not to mention probably does 
not play well with most IDEs.


Maybe something like this would be interesting too.

```d
enum FOO {
   A = 5,
   B = 6,
   mixin("C = 7")
} else version (AArch64) {
   C = 17,
} else {
   static assert(0);
}
   version E = 9,
}

// static assert(checkUniqueValues!FOO);
```

3. Use typedef, to create new distinct type for these various 
flag constants, and use that.

Maybe

```d

import std.typecons;

alias Foo = Typedef!int;

enum Foo B = 6;

```


But then the actual structure and discoverability is lost. Plus 
if one has two classes of values both having `B`, they will 
clash, unless one puts some prefixes, or puts them in different 
modules, which is a big limitation.

I am not advocating for extending language necessarily, as I am 
sure library solution could be worked out too. Still it is a bit 
strange that one cannot easily use `version` and `static if` in 
`enum`s, where they can use it in almost every other place in the 
language.



More information about the Digitalmars-d mailing list