[Pre-RFC] Generic Keywords

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 a Future.

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 Futures, 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.

I'd rather move the "genericness" into the normal angle brackets.

  • const fn, <?const> or where ?const means the function can be run in const or non-const contexts. The body of the function can only call other const or ?const functions.
  • <C = ?const> or <C> ... where C = ?const is same as above, but the implementation can change depending on the constness parameter C. Any <?const> functions called within must be passed C.
  • async fn, <async> or where async means always async or "async-only".
  • <?async> or where ?async means the function can be run sync or async. The body of the function can only call other !async or ?async functions.
  • <A = ?async> or <A> ... where A = ?async is the same as above, but the implementation can change depending on the asyncness parameter A. Any <?async> or <async> functions within must be passed A.
/// Can be run at compile-time OR at runtime. This is regular rust.
const fn foo() {}
// Becomes
fn foo<?const>() {}
// or
fn foo() where ?const {}

/// Can be called at compile-time OR runtime, with a named generic
fn bar<C = ?const>() {
// or
fn bar<C>() where C = ?const {
    // something that will be run at runtime or at compile-time
    const_fn::<C>();

    // `C` is a special kind of boolean/enum that can only be used
    // in certain contexts
    if C == const {
        // something that will be run only at compile-time
    } else {
        // something that will be run only at runtime
    }
    // etc
}
/// Returns a Future<Output=()>
async fn baz() {}

// Returns a Future<Output=()> or ().
fn qux<A = ?async>() {
// or
fn qux<A>() where A = ?async {
    // something that will be run synchronously or asynchronously
    quux::<A>().await;

    if A == async {
        // something that will be run only if the function is asynchronous.
        // it's the only place where <async> functions can
        // be awaited
        
        something().await
    } else {
        // something that will be run only if the function is synchronous.
    }
    // etc
}

I'd rather keep the function keywords in their current position, so the weirdness doesn't get too out of control and refactorings are kept straightforward and easy.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.