Mike Parker hints D management changes coming, I write about static assert and platform porting

Posted 2023-02-20

See community announcements for Mike's post, also Steve did a blog on attribute inference debugging. I write on static assert porting patterns here.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

Static assert patterns arguably harmful for porting

A somewhat common pattern in D code looks like this:

void doSomething() {
   version(Windows)
       Windows_Version();
   else version(linux)
       Linux_Version();
   else static assert(0);
}

This has the advantage of when you decided to port to a new platform, the compiler will tell you any time you missed a spot, ensuring you implemented everything on the new platform. But it also has the disadvantage of requiring you to implement everything on the new platform before it lets you do anything.

For small things that you can port quickly and need everything, this works fine. But realistically, a lot of libraries can be useful with partial ports and doing a complete port can take a significant amount of time, meaning this pattern can do harm in the long run as it complicates incremental porting efforts.

To take two concrete examples, this pattern is used in druntime and my own simpledisplay.d. simpledisplay has a few parts that use different approaches because I actually haven't found a way I completely like yet. The oldest way is to have version mixin templates for different platform implementations. The idea there was to group everything together for a platform, but the downside is the interface is then scattered. So, then I started added a separate interface that forwards to the impl. But this felt silly since it meant writing the function signatures several times, so newer functions got written with the version / else static assert pattern. This worked ok for a while, but when I wanted to update the Mac OS implementation and get it building again, it was a daunting task - I couldn't test window creation without implementing everything just to let it compile!

Instead of actually implementing it, I started using a helper function like this:

void featureNotImplemented()() {
        version(allow_unimplemented_features)
                throw new NotYetImplementedException();
        else
                static assert(0);
}

This lets me at least compile and run the program, while still having the option to get the compiler to check it. But, in practice, the static assert isn't that useful since I'll just run a search for featureNotImplemented in the code and work my way down... which means simply doing throw new NotYetImplementedException(); works about just as well anyway.

I'm generally happier throwing the exception than static asserting in cases like this.

However, there is a downside: it can only be used inside a function. If you are selectively declaring something, it won't work:

version(linux)
  enum SomeValue = 1;
else version(Windows)
  enum SomeValue = 2;
else
  throw new NotYetImplementedException(); // not gonna work! this isn't a declaration

This is where druntime comes in: it has blocks of declarations like this, and it uses static assert to try to cover its platforms comprehensively. A random sample, from core.sys.posix.time:

version (CRuntime_Glibc)
{
    time_t timegm(tm*); // non-standard
}
else version (Darwin)
{
    time_t timegm(tm*); // non-standard
}
else version (FreeBSD)
{
    time_t timegm(tm*); // non-standard
}
else version (NetBSD)
{
    time_t timegm(tm*); // non-standard
}
else version (OpenBSD)
{
    time_t timegm(tm*); // non-standard
}
else version (DragonFlyBSD)
{
    time_t timegm(tm*); // non-standard
}
else version (Solaris)
{
    time_t timegm(tm*); // non-standard
}
else version (CRuntime_Bionic)
{
    // Not supported.
}
else version (CRuntime_Musl)
{
    time_t timegm(tm*);
}
else version (CRuntime_UClibc)
{
    time_t timegm(tm*);
}
else
{
    static assert(false, "Unsupported platform");
}

And then a bit further down the same file:

version (linux)
{
    enum CLOCK_MONOTONIC          = 1;
}
else version (FreeBSD)
{   // time.h
    enum CLOCK_MONOTONIC         = 4;
}
else version (NetBSD)
{
    // time.h
    enum CLOCK_MONOTONIC         = 3;
}
else version (OpenBSD)
{
    // time.h
    enum CLOCK_MONOTONIC         = 3;
}
else version (DragonFlyBSD)
{   // time.h
    enum CLOCK_MONOTONIC         = 4;
}
else version (Darwin)
{
    // No CLOCK_MONOTONIC defined
}
else version (Solaris)
{
    enum CLOCK_MONOTONIC = 4;
}
else
{
    static assert(0);
}

This kind of thing appears over many dozens of times in the core.sys namespace and several hundred times throughout all of druntime. Same thing here: it makes sure you remember to check everything, but it also blocks using a partial build and makes porting druntime to a new platform feel completely daunting.

I think the static assert here might better just changed out for just an empty line; if you try to use a declaration that doesn't exist, you will get a compile error anyway, and if you don't use it, you are then not blocked from incremental use. Though if you did want to search for missing things, an empty line isn't easy to find, so I'd suggest using a standard comment that is easily searched for.

My general feeling after using it for many years is that static assert should be to sanity check some declaration that already exists, and not to ensure that a symbol does exist. Since if you use it, you'll know that it doesn't exist. And if you don't use it, you don't care.