carbon-lang icon indicating copy to clipboard operation
carbon-lang copied to clipboard

How do we indicate that functions should be callable during compilation?

Open zygoloid opened this issue 4 years ago • 9 comments

In https://github.com/carbon-language/carbon-lang/issues/498, we had a question of whether you can call unannotated functions in a type context:

fn g() -> Type { return Int; }
fn f(x: g()) -> Int { return x; }

The current resolution of #498 suggests that functions would need an explicit annotation in order to be able to be called in this context, perhaps analogous to C++'s constexpr. This seems to be easily justified by our code evolution goal.

However, given that we want to support forms of compile-time programming similar to the above, we will need some syntax to indicate the intent to allow uses of g() in constant evaluation, along with everything that implies -- in particular, imposing constraints on future evolution of the function. So what syntax should we use? Some options:

  • A prefix keyword (maybe constexpr or perhaps simply const)
  • A ! somewhere in the function declaration (fn! or g!)
  • An attribute (this would require that we define an attribute syntax, of course)

There are probably many others.

zygoloid avatar Aug 13 '21 20:08 zygoloid

There's also a semantic question: should a function that is annotated this way be evaluatable only during constant evaluation (like C++'s consteval) or usable both during constant evaluation and at runtime (like C++'s constexpr)? Both options seem to have significant merit.

Given how central constant evaluation is to Carbon's semantics, I think we can easily justify a punctuation syntax here. Perhaps something like a ! for constexpr semantics and a !! for consteval semantics? (Either after fn or after the function name would seem OK to me.)

zygoloid avatar Aug 13 '21 20:08 zygoloid

Do we need both semantics?

I understand the historical reasons why C++ needed them, but I'm wondering if there is a way to avoid that in Carbon.

chandlerc avatar Aug 13 '21 22:08 chandlerc

I think we need:

  • a way to write functions that can be shared by compile-time execution and runtime execution (for example, if I want to use the same type during compile-time evaluation and at runtime, I don't want to implement the type twice)
  • a way to write functions that do things that cannot be done at runtime (for example, metaprogramming activities might involve reflection and code generation that we can't support at runtime)

Assuming both situations require a marker, I think we can get away with using the same marker for both, and reject programs that (transitively) use compile-time-only constructs from runtime functions. I think then the question would be: do we want an explicit, declarative statement of intent in code to indicate whether a function is usable at runtime, in the same way we want an explicit, declarative statement of intent in code to indicate whether a function is usable at compile time?

I think the answer might reasonably be no. The only way we wouldn't be able to work this out for ourselves is if a function is intended for compile-time use only but the implementation happens to not use any compile-time-only functionality yet, which seems like a rare situation (quite unlike the situation of a runtime-only function that just happens to not use any runtime-only functionality yet, which is common). We could provide a mechanism to address that narrow issue (such as a compile-time-only operation that happens to be a no-op) if we think it's worthwhile, and then just have a single "usable at compile time" marker that allows runtime use only if the function doesn't transitively depend on anything that's compile-time-only.

zygoloid avatar Aug 13 '21 23:08 zygoloid

We triage inactive PRs and issues in order to make it easier to find active work. If this issue should remain active or becomes active again, please comment or remove the inactive label. The long term label can also be added for issues which are expected to take time. This issue is labeled inactive because the last activity was over 90 days ago.

github-actions[bot] avatar Nov 12 '21 01:11 github-actions[bot]

While not urgent, reading this it seems like we're close to a decision here?

Specifically, if we don't try to model both semantics (and allow a future proposal to do a no-op compile-time-only operation if it comes up), then we only need one syntax. @zygoloid had suggested ! as reasonable, on the fn or after the function name.

Do folks have specific reasons to prefer one position or another?

Do folks have specific concerns with using ! in this context?

chandlerc avatar Oct 22 '22 02:10 chandlerc

I have a mild preference for fn! F(...) rather than fn F!(...). I think the ! is easier to see in that position, and reads as being part of the introducer rather than part of the introduced name / parameters, which I think is logical.

zygoloid avatar Apr 11 '23 21:04 zygoloid

I think there are two things we could say here:

  • "constexpr" or "comptime_allowed": Allow this function to be called either during compilation or runtime.
  • "consteval" or "comptime_only": This function may only be called during compilation. In both cases, this is in contrast to unannotated functions that may only be called at runtime.

In the first "constexpr" case, a function with the annotation would be during-compilation-only if all its parameters are compile-time-only (declared with :!), as a result of our "we select the comptime version of a function based on whether the arguments are comptime values" decision.

In the second "consteval" case, parameters that aren't marked compile-time-only (declared with :!) wouldn't be usable in type expressions, or wouldn't be allowed at all.

It seems to me that it is going to be common to want an easy way to say "this function should be available at both runtime and compile-time because it performs a pure computation and will be needed in both contexts," and so I think we need a way to say "constexpr."

There is also a use case for "this function computes a type or something else like an initializer that we want to be sure is comptime_only." I'm open to that being "constexpr + all parameters are :!" or a separate annotation.

josh11b avatar Sep 20 '23 20:09 josh11b

I would like to bring up a big issue with the consteval model in C++20. I attempted to properly describe it here: https://github.com/chromium/subspace/issues/266#issuecomment-1595810173

The tl;dr is that type_traits run at compile time but they were designed to make decisions about runtime. "Does this function exist" at compile time is really trying to ask "Will this function exist at runtime, such that I can compile a call to it here" and consteval basically breaks type_trait usage because it's not available at runtime.

Rust side steps this whole thing because a) const things are always avail at runtime too, there's no consteval. b) compile-time-only stuff is modelled through macros instead which do not interact with things like traits/types.

I think these footguns should be known before copying them from C++ at least.

danakj avatar Sep 22 '23 01:09 danakj

We are likely going to have compile-time-only values, like types, and compile-time-only functions (for interop with C++ consteval). In fact, I think we are going to lean more on compile-time functions than macros where we can. So we will definitely need to be careful to make the APIs that query about functions support maintaining that information.

josh11b avatar Sep 22 '23 01:09 josh11b