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