Exploring language support
Language support for CTFE is rather varied. From the most utter basic of constant expansion such as parsing of a number literal. To the more complex of having a GUI toolkit running.
Because of how varied CTFE support can be, it must be classified. The LISP family languages are a great starting point in everything from simple to complex designs. The D programing language on the other hand, can be more familiar because of its C family origination. Unlike the C/C++ where the macro preprocessor was used, D does not have this. However in some ways the macro pre-processor in C/C++ could be considered a form of CTFE.
The D programming language
To fully grasp the usage of CTFE in a codebase. Let’s start off with a language that has a semi decent support. For this the D programming language will be used. D has good CTFE support but it does have limitations which are intentional.
D has meta-programming support which has been described as compile time arguments. A particularly unique language feature D has is known as a mixin template. A mixin template is a template that instead of creates a new type, can be effectively be thought of as output AST in a given context1.
A mixin template should not be confused with a normal template. Where a normal template effectively creates a new symbol uniquely to the arguments given2.
To further make use of CTFE code, it can be useful to take input in some form or another. That does not rely on hard coding into the file the values. There are two known ways for this, string imports and using a tool such as Bin2D[^Bin2DGithub].
String imports have the syntax of enum string thefile = import("file");. The search path for the file must be provided to the compiler via the -J flag or for dub via stringImportPaths property.
Bin2D is a very useful tool in that it can generate a file that contains many directories or files as byte arrays. Alternatively it can also export at runtime on request. However because of it being an external tool it does require a preprocess action which can be slower.
Developing CTFE’able code
To develop D code compatible with compile time, there is one main restriction. All required information to execute must be passed in. There is no global data in any form including static variables within a function.
As a result of this restriction the pure attribute is in heavy use by the author in his code bases. The pure attribute reflects the same restrictions that CTFE comprises of. No access to global data. Most of the time it would also be wise to add @safe. Where by removing the direct use of pointers.
An exception to the rule of no global data is constants. A constant such as enum is well known by the compiler, as such it can be utilized. However the opposite of this rule is no external code such as extern(C) functions. An example of a CTFE’able function would be factorial.
1 size_t factorial(size_t n) pure {
2 assert(n < 11);
3
4 if (n == 0)
5 return 1;
6 else
7 return n * factorial(n - 1);
8 }
9
10 static assert(factorial(5) == 120);
As shown by the code sample for a factorial callable at compile time it must execute and output the value of 120 during compilation. We know that it must by the static assert statement on line 10.
The pure attribute is listed on the right of the statement declaration before the opening bracket. For those unfamiliar with D this has the same effect as it being of the left. However contested. Some consider it good practice for attributes on the right to refer to the function and the left return type. Further, size_t is used as the data type. This is an unsigned integer dependent upon the word size of the resulting binary. Such as for 32bit, uint.
During optimised targets (-release) assert at compile time (not static version) should also be assumed to work and work like a static assert.
The declaration of size_t is an alias within two version statements. One for x86 the other for x86_64.
Lastly, there is two different types of asserts in D. First the normal runtime based assert and secondly the compile time static assert. The static assert is a declaration at compile time that must be true to compile. Whereas during a debug build an assert will throw an exception if it is not true. This is useful for contract based programming.
However this is not the only type of static statement, there is another: Static if! Static if is just like a regularly if statement except it can conditionally include code based upon the statement at runtime. This is comparable to the #if or #ifdef macro in C/C++. However do note, this is part of the language and not part of a preprocessor like in C/C++ 3.
Going forward it is necessary to discern the difference between runnable at runtime and not. Not all code that is written with CTFE in mind should be ever ran during runtime. This produces three groups of code. 1. Runnable during runtime 2. Runnable during compile time 3. Runnable during runtime and compile time
The first is the most obvious, in most languages you write for this predominately. If manipulation of types is required, runtime based reflection is used. In D however this is not possible comparatively without explicit compile time knowledge and expectation of this. Second, to execute at compile time is mostly what this book is about. However it would be useless if it couldn’t produce code to be executed during runtime. This is the third group.
Third, to execute at both compile time and runtime. To execute during runtime, information is passed in during compilation. While this may include the most basic generic like functionality found in Java. For all all intents and purposes it is not for this book. That is categories under the first, runtime based. This is because while it does change code generation, it does not produce code specifically based upon the input. Code that uses this functionality include ranges and generics for e.g. all three string types (string, dstring and wstring).
Lisp family of languages
The LISP family languages cover a large variety. Common-LISP, Dylan and Converge are great examples. They each have some form of support for CTFE. They base their implementation upon macros. Common-LISP is the most basic of all the support. The macro support evaluating new code based upon the call. But expands out in a forced inline. While this is fairly powerful, Dylan provides much more access to the compiler. Dylan’s macros are meant for direct extension to the language for both statements, definitions and operators. What is unique about Dylan is many features that would be considered on a type is moved into macros. Lastly in Converge, CTFE is thought of as compile-time meta-programming. This is different in other LISP languages primarily in that it gives full access to the AST at that macros entry point. This is by using a “CEI” object. This is considered the main method at CTFE to interact with the compiler.