Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] ctonly: new annotation to mark functions that are intended to be only used during compile time. #20858

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

yanok
Copy link
Contributor

@yanok yanok commented Feb 13, 2025

This is mostly a RFC and an invitation for discussion, the implementation is hacky and I guess I need to go via the official feature proposal process before the implementation could be merge (please advise!).

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.

For now it's completely ignored by the compiler.

I chose not to include it into mangling, since that's supposed to
be a front-end only thing. Backends are free to omit `ctonly` functions
in codegen, and you should never want to take apart `ctonly` version
from non-`ctonly` version during linking.

Following that logic, I also chose **not** to check `@ctonly` in
`attributesEqual`, so functions that only differ in `@ctonly`ness will
result in a compilation error. I think that's reasonable.

Please let me know if I missed anything, it seems a new attribute needs
to be added in many places, I might have missed something.
The goal of `@ctonly` annotation is to mark functions that are _never_
called during run time.

I believe, to achieve that goal, we need two things:
 1. Obviously, check that `@ctonly` functions are called from CTFE
    context or from other `@ctonly` functions.
 2. Addresses of `@ctonly` functions are never taken.

This commit implements this. As for disabling taking the address of the
function, the implementation is more restrictive, it forbids using
`@ctonly` function as lvalue completely. That probably could be made
less restrictive, for example, it's probably totally fine to return
`@ctonly` function by reference, as long as it stays inside CTFE
context...

This implementation works _almost_ like I want it to work, requiring
_all_ `@ctonly` functions to be _explicitly_ marked. There is an
annoying case though: templates. Consider this example:
```D
int f(int x) @ctonly { return x + 1; }
enum a = map!f([1, 2, 3]).array;
```

This is a perfectly valid code, but this implementation will complain
about `@ctonly` function `f` being called from non-`@ctonly` function
`MapResult!(f, ...).front`.
some test cases for `@ctonly` annotation.

`ctonly_simple_map.d` shows how templates could be `@ctonly` aware even
with this simple implementation.
It would be nice to be able to use templates from the standard library
with `@ctonly` functions, without having to update the template to be `@ctonly` aware.

To make it work we need to implement the following rules:
 1. If a `@ctonly` function `f` is called from a template instance function **and**
    `f`is a template alias parameter somewhere in the instantiation stack,
    mark calling function as `@ctonly`-inferred because of `f`.
 2. If a `@ctonly`-inferred function (with infer reason `f`) is called
    from a template instance function **and** `f` is still in the
    instantiation stack, mark calling function `@ctonly`-inferred
    because of `f`.
 3. Otherwise apply rules from the basic implementation.

In this commit I've tried to implement this, but had to take some
shortcuts.

1. It turned out to be pretty complicated to check if a function comes
   from an alias parameter... I think I've implemented it, but the code
   is ugly. I'm open to suggestions.
2. In rule (1) I don't check the full instantiation stack, just the
   top-level instantiation.
3. In rule (2) I don't check if `f` is still a template parameter at
   all, just that we are still in a template instantiation (such that we
   never infer `@ctonly` for non-templated functions). This could cause
   spurious infers which will result in unclear errors like "`@ctonly` `f`
   calls non `@ctonly` `g`", but in the code none of them is `@ctonly`.
   This could be fixed by either printing the whole promotion chain or
   by actually implementing checking the instantiation stack.
@yanok yanok requested a review from ibuclaw as a code owner February 13, 2025 12:42
@dlang-bot
Copy link
Contributor

Thanks for your pull request and interest in making D better, @yanok! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the coverage diff by visiting the details link of the codecov check)
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

Please see CONTRIBUTING.md for more information.


If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment.

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + dmd#20858"

@yanok
Copy link
Contributor Author

yanok commented Feb 13, 2025

@thewilsonator FYI

@thewilsonator thewilsonator added Review:Needs Changelog A changelog entry needs to be added to /changelog Review:Needs Spec PR A PR updating the language specification needs to be submitted to dlang.org labels Feb 13, 2025
@thewilsonator
Copy link
Contributor

I still think this would be better as a pragma than an attribute.

@rikkimax
Copy link
Contributor

This must not be inferred. It prevents codegen, it'll result in some major surprises.

Since you didn't use it, I assume you are not aware of the skipCodegen flag for functions.
https://github.com/dlang/dmd/blob/8c0967fe7c6303e740eeb1acac52c1fbc6f71d42/compiler/src/dmd/func.d#L120C10-L120C21

It's used to specify that a function will not be codegen'd and with that may be allowed to do stuff like GC.

f.skipCodegen = true;

Also we have a way of detecting CTFE usage __ctfe, which would be a better attribute identifier @__ctfe instead.

But yes attribute good. Oh and this needs to go through the DIP process.
https://forum.dlang.org/group/dip.ideas

@yanok yanok marked this pull request as draft February 13, 2025 13:32
@yanok
Copy link
Contributor Author

yanok commented Feb 13, 2025

I still think this would be better as a pragma than an attribute.

Personally I'm fine either way, but using an attribute seems a bit more consistent. I'll do a DIP and we'll see what will be the outcome.

@yanok
Copy link
Contributor Author

yanok commented Feb 13, 2025

This must not be inferred. It prevents codegen, it'll result in some major surprises.

Sure, it can't and must not be inferred in general. That's the basic rules, everything must be marked @ctonly (or @__ctfe as you suggest) explicitly.

However, there is this case with passing such functions as template parameters. There is absolutely no harm in inferring that MapResult!(fun, arr).front is @ctonly if fun is @ctonly, while it allows some morally valid examples to work, that would otherwise be rejected.

I think for the feature to be really useful, we need to allow this limited kind of inference. But I'm open to other suggestions.

Since you didn't use it, I assume you are not aware of the skipCodegen flag for functions. https://github.com/dlang/dmd/blob/8c0967fe7c6303e740eeb1acac52c1fbc6f71d42/compiler/src/dmd/func.d#L120C10-L120C21
It's used to specify that a function will not be codegen'd and with that may be allowed to do stuff like GC.

Thanks for the pointer! Yes, it looks like I must set it.

Also we have a way of detecting CTFE usage __ctfe, which would be a better attribute identifier @__ctfe instead.

Uh, ok. I'm notably bad at naming things, I just had to give it some name :) (it was @ctfe first, but then I decided @ctonly communicates the intent better). I'm totally open to rename it as people think will be more suitable.

But yes attribute good. Oh and this needs to go through the DIP process. https://forum.dlang.org/group/dip.ideas

Thanks for the link! I'll start it. Converting the PR into a draft for now then.

@yanok
Copy link
Contributor Author

yanok commented Feb 13, 2025

I've posted my proposal to the dip.ideas forum: https://forum.dlang.org/post/[email protected]

@rikkimax
Copy link
Contributor

However, there is this case with passing such functions as template parameters. There is absolutely no harm in inferring that MapResult!(fun, arr).front is @ctonly if fun is @ctonly, while it allows some morally valid examples to work, that would otherwise be rejected.

I think for the feature to be really useful, we need to allow this limited kind of inference. But I'm open to other suggestions.

If this is appropriate, it should already be handled by skipCodegen.

A lot of work has gone into that flag to make it work right.

Also we have a way of detecting CTFE usage __ctfe, which would be a better attribute identifier @__ctfe instead.

Uh, ok. I'm notably bad at naming things, I just had to give it some name :) (it was @ctfe first, but then I decided @ctonly communicates the intent better). I'm totally open to rename it as people think will be more suitable.

Those types of names have been floated before.

I am prioritising keeping the delta to a minimum where there is no benefit to having more changes.

@yanok
Copy link
Contributor Author

yanok commented Feb 15, 2025

If this is appropriate, it should already be handled by skipCodegen.

Hm... I'm not entirely sure, but I think it won't. The two things are indeed related but they are not the same (kinda dual in a way). AFAIU, skipCodegen flag analysis looks at all the call-sites for a function and if they are all under CTFE, concludes that the function can have skipCodegen flag. @ctonly works in the other direction: the user marks a function to be only available in CTFE, so all the caller functions must either do the call inside some construction that triggers CTFE or be a @ctonly function itself.

But the interaction with skipCodegen implementation is indeed an interesting topic.

Regarding the naming, as I said before, I don't have strong opinions here. Another person suggested to use __ctfe on the forum thread, so for now it looks like the winning candidate. I'm also happy to make the diff smaller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Review:Needs Changelog A changelog entry needs to be added to /changelog Review:Needs Spec PR A PR updating the language specification needs to be submitted to dlang.org
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants