Proposal
I'm proposing to add a new function annotation (or pragma) to mark functions that should never be executed at run time. I call the annotation @ctonly
in this proposal, but I'm notoriously bad at naming things, so I'm open to naming suggestions.
I have a draft PR with a (slightly simplified) implementation here: https://github.com/dlang/dmd/pull/20858
Motivation
My motivation is two-fold.
- We have some functions that we know we intend to only use during compile time. Yet it’s really easy to write
auto v = f(x);
instead ofenum v = f(x);
, so the function call shifts to run time. We could hope for some constant fold/partial evaluation optimization to kick in the later stages of the compilation, such that it still gets pre-computed at compile time. But why should we rely on that, while D gives us all the controls? Markingf
as@ctonly
will easily uncover this unintended usages. - If a function is marked
@ctonly
, backends can skip codegen for that function (currently I’ve only implemented that for LDC in my local branch, but the implementation is really straightforward). That might seem like a micro optimization at first glance, but if a@ctonly
function is templated and instantiated widely, code generation cost for it becomes non-trivial.
Implementation Ideas
Basically, what we need is to check is that @ctonly
functions are never called outside from CTFE context and that we never take addresses of these functions.
New function attribute
Let’s add a new function attribute @ctonly
to mark CT only functions.
This doesn’t require mangling change, since the feature is essentially frontend-only. It also doesn’t make much sense to have both @ctonly
and non @ctonly
versions of a function with the same signature, so I suggest to not count the new attribute when comparing function attributes for equality.
This is implemented in the first commit of the PR.
Basic check rules
- In semantic check of
CallExp
, make sure that if the callee function is@ctonly
, either the caller function is also@ctonly
or the call is in CTFE context. - Fail on taking the address of
@ctonly
functions.
Basic rules are implemented in the second commit, and the third commit adds some tests to showcase accepted and rejected code snippets.
Basic rules limitations
Though this works in simple cases, unfortunately there is at least one case, where it doesn’t work as I would like. Consider this code:
int f(int x) @ctonly { return x + 1; }
enum a = map!f([1, 2, 3]).array;
There is nothing wrong with it, f
is only called during compile time. But with only the basic rules, the compiler fails to understand that. The problem with the basic rules is they require explicit @ctonly
everywhere. Which I think is good for the most part. Except for this case with templates from the standard library (or any other library actually).
There is a way to write MapResult
implementation such that it checks if a function it gets via its template parameter is @ctonly
and adjusts all callers to also be @ctonly
... but that’s going to result in a lot of code duplication and, probably more importantly, will require changes to the stdlib.
So what if we enable inference for template instance functions?
Additional rules
- Add an extra bit marking if
@ctonly
was inferred. - If
f
is@ctonly
andf
is called fromg
andg
is a template instance function andf
is an alias parameter somewhere ing
's instantiation stack, markg
as inferred@ctonly
with an inference reasonf
. - If
f
is inferred@ctonly
with reasonr
andf
is called fromg
andg
is a template instance function andr
is an alias parameter somewhere ing
's instantiation stack, markg
as inferred@ctonly
with an inference reasonr
.
A hackier version of these rules is implemented in the fourth commit. In particular I only look at the first level of template parameters for initial inference step. And don’t look at template parameters at all for subsequent steps, instead promoting further as long as the caller is a template instance function. This is suboptimal and could be improved.