Search code examples
d

Compile time initialization of an associative array


According to D Language Reference static initialization of associative arrays an associative array (AA) can be initialized this way:

immutable long[string] aa = [
  "foo": 5,
  "bar": 10,
  "baz": 2000
];

void main()
{
  import std.stdio : writefln;
  writefln("(aa = %s)", aa);
}

However the example doesn't compile with a reasonable recent DMD:

$ dmd --version
DMD64 D Compiler v2.083.0
Copyright (C) 1999-2018 by The D Language Foundation, All Rights Reserved written by Walter Bright
$ dmd -de -w so_003.d
so_003.d(3): Error: non-constant expression ["foo":5L, "bar":10L, "baz":2000L]

A bit of googling seems to indicate this is a long standing bug (?) in the language:

So I know how to work around that with a static constructor. However considering the issue have existed already about 10 years is this in practice turned into a feature ?

In fact that just a prelude to my actual question:

Is it possible to initialize an associative array in compile time ?

In the example below I can initialize module level string[] doubleUnits with a generator function that is run in compile-time (with CTFE) as proofed by pragma(msg). And I can initialize int[string] doubleUnitMap in run-time. But how I can initialize the AA in compile-time ?

import std.stdio : writefln;

immutable char[] units = ['a', 'b', 'c'];

immutable string[] doubleUnits = generateDoubleUnits(units);
pragma(msg, "compile time: ", doubleUnits);

string[] generateDoubleUnits(immutable char[] units)
pure
{
  import std.format : format;

  string[] buffer;
  foreach(unit; units) {
    buffer ~= format("%s%s", unit, unit);
  }

  return buffer;
}

immutable int[string] doubleUnitMap;
// pragma(msg) below triggers the following compilation error:
// Error: static variable doubleUnitMap cannot be read at compile time
//        while evaluating pragma(msg, "compile time: ", doubleUnitMap)
// pragma(msg, "compile time: ", doubleUnitMap);

shared static this() {
  doubleUnitMap = generateDoubleUnitMap(units);
}

int[string] generateDoubleUnitMap(immutable char[] units)
pure
{
  import std.format : format;

  int[string] buffer;
  foreach(unit; units) {
    string key = format("%s%s", unit, unit);
    buffer[key] = 1;
  }

  return buffer;
}

void main()
{
  writefln("(doubleUnits = %s)", doubleUnits);
  writefln("(doubleUnitMap = %s)", doubleUnitMap);
}

Solution

  • It is not possible to do the built-in AAs initialized at compile time because the compiler is ignorant of the runtime format. It knows the runtime interface and it knows the compile time memory layout... but the runtime memory layout is delegated to the library, so the compiler doesn't know how to form it. Hence the error.

    But, it you were to implement your own AA type implementation, then you can write the CTFE code to lay it out and then the compiler could make it at compile time.

    Many years ago, this was proposed as a fix - replace the built-in magic implementation with a library AA that happens to fit the compiler's interface. Then it could do it all. The problem was library types cannot express all the magic the built in associative arrays do. I don't remember the exact problems, but I think it was about const and other attribute interaction.

    But that said, even if it failed for a 100% replacement, your own implementation of a 90% replacement may well be good enough for you. The declarations will look different - MyAA!(string, int) instead of string[int], and the literals for it are different (though possibly makeMyAA(["foo" : 10]); a helper ctfe function that takes a built-in literal and converts it to your format), but the usage will be basically the same thanks to operator overloading.

    Of course, implementing your own AA can be a bit of code and maybe not worth it, but it is the way to make it work if CT initialization is a must have.

    (personally I find the static constructor to be plenty good enough...)