new annotation or pragma to mark functions that are intended to be only used during compile time

Ilya ilya.yanok at gmail.com
Sun Feb 23 16:33:48 UTC 2025


On Thursday, 20 February 2025 at 19:44:37 UTC, Steven 
Schveighoffer wrote:
> It has not gone anywhere, but if we were to consider a language 
> change, I'd want to evaluate whether this is a better option, 
> since it's a de-facto standard.

Ok, let me take a step back. What I want to do is give users a 
way to say "this function is used only at compile time and you 
can skip codegen for it".

You say "this is already `assert(__ctfe)` for it, just use it". 
The problem with `assert(__ctfe)` is it *doesn't* guarantee 
codegen can be skipped. See the example above. Same goes for the 
suggested `in(__ctfe)` function contract.

Yes, we can skip codegen for a function starting with 
`assert(__ctfe)`, **if** we replace all calls to this function 
with `assert(__ctfe)` **and** we are not in release build. I 
don't quite like it:
  1. It sounds more complicated than my approach
  2. More importantly, it only works in debug builds. To make it 
work with release builds, we need to keep `assert(__ctfe)` in 
release builds (as you suggest below). But that's a breaking 
change and a pretty dangerous one. Imagine you have a function 
`f` that starts with `assert(__ctfe)` that is almost always 
called under CTFE. Almost. But there is a rare code path that 
runs `f` at run time, that code path is not covered by any tests 
and happens roughly once a month in prod. What happens today? We 
hit `f` at run time, we run it, it might be much slower than we 
expected, we might even miss some deadline... but it works. If we 
replace calls to `f` with `assert(false)`, once that code path is 
triggered, it just crashes.

> At the root this is code generation. Do you want to generate 
> code or not? Semantic already has to run for CTFE.

Oh, when I say "semantics" I mean "program meaning", not refer to 
a semantic analysis pass in the compiler. Sorry for confusion.

Yes, I want to skip code generation. Just skipping code 
generation is pretty easy, I actually started with that, I can 
add an UDA and hack our backend (LDC) to skip codegen for marked 
functions. But I found that linker errors are not the nicest 
feedback when I was trying to make my annotations work. They 
usually give you a pretty good hint, yes. But there is only a 
function name, and it's mangled, and you have to wait until _all_ 
the compilations are done, so you can run the linker.

So I thought I can do better, and make a more reasonable error 
message much faster with a bit of static analysis.

> `assert(__ctfe)` (and `assert(!__ctfe)`) could be in the same 
> vein as `assert(0)` -- that is, it always evaluates even in 
> release builds.

Yes, they could. But that's a breaking change, see above.

>> This works with runtime assert, but will be rejected by the 
>> check.
>
> Yes, this is a problem with the idea to make it a compiler 
> error.
>
> However, the compiler still does not have to generate code for 
> `ctonly`, even if we don't make it an error. It can just 
> replace the call with an `assert(__ctfe)` (or one that produces 
> a nice message).

See above. That only works in debug build. Or we can to enable 
`assert(__ctfe)` in release, which is a breaking change.

But even putting that aside, I actually *like* it being a 
compiler error (I'm obviously biased, because of my types/static 
analysis background).

I was experimenting with our code base and my workflow was like 
this:
1. Find a good candidate (a function with lots of instances in 
object files but no matching symbols in the linked binary).
2. Mark it with `@ctonly` and look at compiler errors one by one.
3. There are essentially two cases:
    - the caller function is RT, so need to add an `enum` to force 
CTFE. This case could be caught by `assert(__ctfe)` **if** there 
is test coverage
    - the caller function could be CT-only itself. This will never 
be caught with `assert(__ctfe)`.

>> That won't work the same way `assert(__ctfe)` works today, see 
>> the example above, so you are proposing to change the 
>> semantics of `assert(__ctfe)`.
>
> Yes, you are right. Then again, your attribute has the same 
> issue, no?

No, it's a brand new attribute, with a brand new meaning. Yes, it 
will compile-time reject some code that will work with 
`assert(__ctfe)`. But that's normal, we don't (and can't) promise 
that it is equivalent to `assert(__ctfe)`.

> I think with replacing any runtime calls to a ctfe-only 
> function with an assert, we have basically the best that can be 
> had.

See above, either it only works in debug or we need the breaking 
change.

> In general we should prefer solutions that don't require 
> changing syntax/semantics:
> - no need to update IDEs/LSP
> - no need to change any semantics

... but you are proposing to change `assert(__ctfe)` semantics :)

> - no new attributes to worry about
> - if we hook onto a de-facto standard, then existing code is 
> upgraded automatically.

Ok, I see two totally reasonable concerns here:
1. Tooling backward compatibility (even though LSPs usually 
either use the compiler directly or derive from it, I see your 
point here).
2. Upgrading the existing code.

Regarding 1. It doesn't have to be a full-fledged function 
attribute. Could be an UDA or pragma, so tools that don't know 
about it (pretty much everything) will simply ignore it.

Regarding 2. I don't think automatic upgrade is possible or even 
desirable. Since the two things are not strictly equivalent. 
Being said that, `assert(__ctfe)` is still a great signal that a 
function is CT-only. So, what we could do is provide a migration 
tool that would try adding new attributes to such functions, run 
the compiler, and if the compiler says "yes", we can do the 
change.



More information about the dip.ideas mailing list