Jump to page: 1 2
Thread overview
new annotation or pragma to mark functions that are intended to be only used during compile time
Feb 13
Ilya
Feb 13
user1234
6 days ago
Ilya
6 days ago
Ilya
1 day ago
Ilya
1 day ago
Hipreme
1 day ago
Ilya
23 hours ago
Ilya
3 hours ago
Hipreme
1 day ago
Paul Backus
23 hours ago
Ilya
February 13

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:

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.

February 13

On Thursday, 13 February 2025 at 14:56:42 UTC, Ilya wrote:

>

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:

int f(int x) @ctonly { return x + 1; }
enum a = map!f([1, 2, 3]).array;

This is not that a problem imo. I think @ctonly would only be used in a certain
way that is mixin(someCall(someArgs)), so if a few useless code still slips in the object files, well that's a pity, but that can get fixed later.

(That case is actually a problem of "attribute-transitivity")

>

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.

Overall that's a good idea. In the bug tracker we have that alternative that is "pure functions must be ctfe-able". Problem is that D purity is not strong, you can wrap a function in a delegate so that it appears to be pure to the compiler; so the obvious question is "@ctonly would mean strongly-pure" ?

Also as far as I understand the intent, it should be well denoted that the goal is to skip emiting useless code in the objects.

February 13

On Thursday, 13 February 2025 at 14:56:42 UTC, Ilya wrote:

>

[…]

The easiest and in fact backwards compatible solution for this is to detect in(__ctfe) as a function precondition contract and make the compiler not emit the function into the object file. If for some reason you don’t like that, I suggest re-using the __ctfe keyword for that purpose.

For the purpose of accidentally calling the function at runtime, it does the job reasonably well already.

Even more generally, it could be a compile-error if the a function with a statically false contract is called.

6 days ago

On Thursday, 13 February 2025 at 17:16:45 UTC, user1234 wrote:

> >
int f(int x) @ctonly { return x + 1; }
enum a = map!f([1, 2, 3]).array;

This is not that a problem imo. I think @ctonly would only be used in a certain
way that is mixin(someCall(someArgs)), so if a few useless

Oh, no :) I've taken the map example from the real code, we are very creative in our CTFE usage :)

So, yes, we have functions we want to mark @ctonly that we use with map and other high-order functions. And with no inference that's not possible without changing the stdlib.

>

code still slips in the object files, well that's a pity, but that can get fixed later.

No, it's not about some code slipping (in my example, map!f code will still be generated I believe), but rather not being able to turn some functions into @ctonly at all.

Yes, we could use staticMap instead of map, but there are tradeoffs.

>

Overall that's a good idea. In the bug tracker we have that alternative that is "pure functions must be ctfe-able". Problem is that D purity is not strong, you can wrap a function in a delegate so that it appears to be pure to the compiler; so the obvious question is "@ctonly would mean strongly-pure" ?

No, the obvious (even though stupid) example would be @ctonly function that is never used: it can do anything it wants. @ctonly means only that the function is never needed at runtime, that's it.

You can continue saying that if it can't be used at runtime, when probably it has to be usable at compile time, so it has to be ctfe-able. And then if ctfe-able implies strongly-pure (which I'm not sure is true), you can conclude that @ctonly implies (under the assumption of being used) strongly pure.

But that's not my goal.

>

Also as far as I understand the intent, it should be well denoted that the goal is to skip emiting useless code in the objects.

It's not so much about having this useless code in the objects, I think the linker does the good job with garbage collection usually. But I do care about the time spent in generation of this useless code which turned to be non-trivial. I think I wrote that in the Motivation section, but probably I could be more clear about that.

6 days ago

On Thursday, 13 February 2025 at 22:26:02 UTC, Quirin Schroll wrote:

>

The easiest and in fact backwards compatible solution for this is to detect in(__ctfe) as a function precondition contract and make the compiler not emit the function into the object

Oh, that's a brilliant idea actually! Thanks, I should have experimented with it before trying to introduce my own attribute. It seems like it would do almost what I want to achieve. The two missing bits would be:

  1. Static check, as you pointed out we could even generalize it to static contract violation.
  2. Skipping codegen of functions that have in(__ctfe) in preconditions.

Both seem like smaller changes in comparison to what I suggested, I really like that. They probably could also be submitted independently.

I have some reservations though:

  • I really-really-really need to skip codegen of these functions in the release build, that's really important for us. But I think contracts are ignored in release builds, would that even work?
  • (less important) Ideally I'd also like to skip codegen for high-order templates using these functions, like in the map example above. Probably if I implement skipping codegen via setting skipCodegen bit on the function, it will be propagated to the callers with the existing skipCodegen implementation, but I'm not entirely sure.
>

file. If for some reason you don’t like that, I suggest re-using the __ctfe keyword for that purpose.

Ok, I've already got the same suggestion on my PR, so I guess we have a leader :)

1 day ago

Hey, I did some measurements with my initial implementation and even marking the obviously CT-only things in our codebase gives ~7% compilation time improvement for some of the object files. That's not super exciting, but still something we'd like to get (we are annoyed by the long compilation times a lot).

What are the next steps? This thread is super quiet... Does that mean there is not enough interest in this? Should I try proceeding with the DIP PR? Or it doesn't make much sense?

1 day ago

On Thursday, 20 February 2025 at 13:55:33 UTC, Ilya wrote:

>

Hey, I did some measurements with my initial implementation and even marking the obviously CT-only things in our codebase gives ~7% compilation time improvement for some of the object files. That's not super exciting, but still something we'd like to get (we are annoyed by the long compilation times a lot).

What are the next steps? This thread is super quiet... Does that mean there is not enough interest in this? Should I try proceeding with the DIP PR? Or it doesn't make much sense?

I would be very happy to have that in D. That idea is super cool and I've never even thought that was a problem to be solved, thanks for your input!

I have a plenty of functions that are called in compilation time, and beyond reducing the object generation time, one would of course get reduced binary size, which is also fairly important to those which are targeting WASM.

1 day ago

On Thursday, 13 February 2025 at 14:56:42 UTC, Ilya wrote:

>

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.

This has been proposed before, and it doesn't require language changes:

void foo() {
   assert(__ctfe);
}

Basically, this will never work at runtime, only at compile time. It's already used in a lot of D code for things like this.

The compiler can take this hint as "do not optimize or generate object code for this".

The compiler can also decide at code-generation time to have an error if it has tried to call the function, or maybe the mark gets spread to the next level up?

It can be very straightforward -- if this is not the first runtime statement in the function, then it doesn't get the benefit. It has to be this form.

-Steve

1 day ago

On Thursday, 13 February 2025 at 14:56:42 UTC, Ilya wrote:

>

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.

In case you weren't aware, this exists in C++, and is called consteval.

1 day ago

On Thursday, 20 February 2025 at 15:30:59 UTC, Hipreme wrote:

>

I have a plenty of functions that are called in compilation time, and beyond reducing the object generation time, one would of course get reduced binary size, which is also fairly

TBH, usually the linker does a pretty good job at collecting non-referenced symbols, so I don't really expect to see any benefits wrt binary sizes here.

>

important to those which are targeting WASM.

Though I'm not sure how things work in WASM world... Do you have a linker?

« First   ‹ Prev
1 2