new annotation or pragma to mark functions that are intended to be only used during compile time
Ilya
ilya.yanok at gmail.com
Thu Feb 13 14:56:42 UTC 2025
# 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.
1. 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 of `enum 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? Marking `f` as `@ctonly` will easily uncover this
unintended usages.
2. 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
1. 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.
2. 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:
```cpp
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
1. Add an extra bit marking if `@ctonly` was inferred.
2. If `f` is `@ctonly` **and** `f` is called from `g` **and** `g`
is a template instance function **and** `f` is an alias parameter
somewhere in `g`'s instantiation stack, mark `g` as inferred
`@ctonly` with an inference reason `f`.
3. If `f` is inferred `@ctonly` with reason `r` **and** `f` is
called from `g` **and** `g` is a template instance function
**and** `r` is an alias parameter somewhere in `g`'s
instantiation stack, mark `g` as inferred `@ctonly` with an
inference reason `r`.
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.
More information about the dip.ideas
mailing list