Introduction
There has been some discussion around about how to handle keyword generics, and this is my proposal, with two candidates.
The keywords on a function declaration context nowadays mean:
-
const
: compile-time or runtime. -
async
: Turn the return into aFuture
.
How would they work?
The objective of generic keywords is to be able to use the same function on different places and different behavior, which effectively means selective conditional compilation depending on the caller's context. Thus, this is a 0-cost abstraction. Functions with generic keywords will be recursively monomorphized on-demand, like with regular generics.
On generic constant functions, non-constant functions can only be called inside a generic scope, which triggers the conditional compilation. On generic asynchronous functions, it's mostly the same with the exception that Future
s, the return value of non-generic asynchronous functions, can only be awaited inside generic scopes. Awaiting generic asynchronous functions will only return a Future
inside asynchronous functions/blocks and will execute synchronously otherwise, so this all can be type-checked.
Type-checking
My uneducated guess is that generic scopes can easily be type-checked since the compiler, for every call to the function, only has to apply the conditional compilation (if it hasn't yet) and type-check the last expression. I am not sure in which compilation step conditional compilation happens, but if it's done before the MIR, this should be trivial to type-check. Elsewhere, the generic keyword conditional compilation would have to be done beforehand. Function monomorphization happens during the MIR generation, so during the HIR/THIR type-checking, the evaluations of a function could be cached and used to construct the MIR, annotating whether a function needs to be monomorphized over a keyword. This would remove redundant steps and mitigate the added compilation time this supposes.
POC Syntax #1
/// Can be run at compile-time OR at runtime. This is regular rust.
const fn foo() -> {}
/// Can be called at compile-time OR runtime, with generics
const<A> fn bar() -> {
// something that will be run at runtime or at compile-time
const_fn();
// this borrows from the `inline_const` experimental feature
const<A> {
// something that will be run at compile-time
}
const<A> else {
// something that will be run at runtime
}
// etc
}
/// Returns a Future<Output=()>
async fn baz() {}
// Returns a Future<Output=()> or ().
async<A> fn qux() {
// something that will be run synchronously or asynchronously
quux.await<A>;
async<A> {
// something that will be run if the function is asynchronous.
// it's the only place where non-generic async functions can
// be awaited
}.await;
async<A> else {
// something that will be run if the function is synchronous.
}
// etc
}
POC Syntax #2
/// Can be run at compile-time OR at runtime. This is regular rust.
const fn foo() -> {}
/// Can be called at compile-time OR runtime. This is still regular rust
const fn bar() -> {
// something that will be run at runtime or at compile-time
const_fn();
// cfg conditional compilation meta attribute
#[cfg(const)]
{
// something that will be run at compile-time
}
#[cfg(not(const))]
{
// something that will be run at runtime. It's the only place
// where non-const functions can be called.
}
// etc
}
/// Returns a Future<Output=()>
async fn baz() {}
// Returns a Future<Output=()> or (). The difference is that `?`
async? fn qux() {
// something that will be run synchronously or asynchronously
// depending on the caller's context.
quux.await;
// cfg conditional compilation meta attribute
#[cfg(async)]
{
// something that will be run if the function is asynchronous.
// it's the only place where non-generic async functions can
// be awaited.
}
#[cfg(not(async))]
{
// something that will be run if the function is synchronous.
}
// etc
}
Personal opinion
I do prefer the #1 over the #2, but the latter is much more cohesive within current rust, and adds very little syntax (just the async?
notation). The #1 proposal is more pleasant for me, but adds new (and somewhat confusing) meanings to the keywords (which increases the learning curve) and depends on the unstable feature inline_const
.
However, the #2 feature adds a magic meaning to #[cfg(not?(const|async))]
, and it is the ability to "clear the x" and be able to call functions that otherwise would not be callable (the whole point of the RFC).
Conclusion
The #1 proposal adds new concepts and numerous syntax, but in my opinion is pleasant to use. On the other hand, the #2 proposal requires little syntactic changes to accomplish the same purpose.