Beyond parameterizing with integrals, parameterizing with values in general would be great.
For example, in C++ the absence of parameterizing with strings has been a long-standing issue, and there are crazy meta-template programming work-arounds based on spelling stuff character by character, leading to:
constant_string<'H', 'e', 'l', 'l', 'o'>::cat< constant_string<' ', 'w', 'o', 'r', 'l', 'd'> >::type
The huge push for constexpr in C++11, and the various extensions brought by C++14 and C++17 clearly show the interest of programmers for more compile-time computation; mainly for two reasons:
- Free Performance: a constexpr
Regex::new would have the performance of regex! (without the hassle); build.rs somewhat fulfills part of this gap, but regex! exists because it is much more readable than defining all your Regexes in a file and referencing them throughout the project.
- Better Types: it has been brought on already that the branching factor of
BTree was not configurable until integral parameterization existed; beyond that, though, it would be valuable to parameterize a “pin-handler” by the address/width/mode of its pin (0/1/2/3 is not as readable, so enums?). I believe today it can be somewhat approximated by using a dedicated type providing the necessary constants by implementing a dedicated trait. For single-value arguments, it’s quite heavy-handed.
Beyond C++, in Scala they actually went all the way down with something like Rust procedural macros… hard to detect whether you should rebuild or not when that depends on the content of a database though.
However, I must admit that in terms of implementation, it seems like a really tricky thing to do.
Specifically, the C++ way (constexpr) has led to specific “evaluation code” popping in compilers to allow evaluation of an ever-increasing range of expressions. It seems hardly tenable to be honest, it means implementing features twice (one for constexpr and one for IR) yet guaranteeing that both results match. It also seems quite difficult to “go down”: what of low-level code such as calls into C, arbitrary memory manipulations, syscalls and inline assembly? Some of those are the building bricks of most collections (starting with Vec), and therefore most code remains inaccessible to this.
The other strategy could be quite similar to plugins: just compile and execute. LLVM can JIT code, the lowering to IR has to be implemented anyway, and since the compiler knows the in-memory representation of the result type, it should be able to “convert back” the raw memory of the result to an “AST value”. It solves all calls into C/memory/assembly issues, providing you can execute the code.
Unfortunately, executing the code gets hard in the case of cross-compilation. The result of std::mem::size_of for example depends on the triplet; it means that you would need to JIT for the target’s triplet, and execute it on the target… which is quite at odds with cross-compiling.
Beyond, there is the issue of deciding whether function should be explicitly annotated (better backward compatibility, but annoying when authors forget, leading to re-implementations) or implicitly guessed (easy re-use, but potential backward compatibility trap).