rust-lang / rust-lang/rfcs

Partial SFINAE by elision of unevaluated const branches from type comparison checks

Open
#3,048 0 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Markdown
Stars
6.6k
Forks
1.7k
Avg merge
16h 14m
Merged PRs (30d)
1

Description

I realize plugging "SFINAE" into the title of the issue is likely to be a hot-button trigger, but I figured I wouldn't dance around the bush and just come out and admit that this RFC falls into the SFINAE category - which rust has somehow largely avoided even bring up let alone succumbing to. (I think part of the reason for that is that const was extremely limited in scope up until very recently, making it a moot discussion.)

I would like to propose that const expressions that contain compile-time conditional branches diverging predicated on a strictly (and exclusively) compile-time constant should be permitted to diverge, realizable via the elision of the the path not taken prior to the reconciling of the types.

I think this example of code that is currently not allowed will explain things best:

struct Foo;

enum CompileTimeConst {
    BranchA,
    BranchB,
}

impl Foo {
    const COMPILE_TIME_CONST: CompileTimeConst = CompileTimeConst::BranchA;

    fn foo() -> u8 {
        let foo = match Self::COMPILE_TIME_CONST {
            CompileTimeConst::BranchA => [0, 1, 2, 3],
            CompileTimeConst::BranchB => [1, 2, 3],
        };

        foo[0]
    }
}

(Note that the extremely contrived example intentionally does not change its signature or behavior depending on the compile-time constant, keeping the scope of the substitution intentionally private and isolated.)

This currently does not compile because the diverging branches of the match block are checked for matching return types prior to the elimination of the conditional altogether, but the logic is sound regardless of what value COMPILE_TIME_CONST takes.

I am not sure whether it would be the compiler's job in this case to validate that for all possible CompileTimeConst options the code remains valid (it does), or if it would suffice for it to only validate that for the currently chosen value of COMPILE_TIME_CONST the program is valid. Both would allow the diverging types to go through, although with the latter option given a different example it is very easy to wind up with code that is valid for the single and current compile-time constant value but not for other values, and the program could break when a different but valid value for COMPILE_TIME_CONST is chosen without its type having changed, e.g.

impl Foo {
    const COMPILE_TIME_CONST: CompileTimeConst = CompileTimeConst::BranchA;

    fn foo() -> u8 {
        let foo = match Self::COMPILE_TIME_CONST {
            CompileTimeConst::BranchA => [0, 1, 2, 3],
            CompileTimeConst::BranchB => [1, 2, 3],
        };

        foo[3]
    }
}

I am also curious if there are any of the following:

  • Smaller/more limited examples of const SFINAE that would make a better starting point,
  • Existing cases of const substitution that already allow type divergence in certain cases or in more limited scopes

With regards to actual implementation, I think the prefered approach would be to elide the branch not taken prior to performing the type checks, in which case the code becomes linear and the branch would be eliminated altogether, bypassing the issue entirely, i.e. reducing to the following code which does not have any diverging types:

struct Foo;

enum CompileTimeConst {
    BranchA,
    BranchB,
}

impl Foo {
    fn foo() -> u8 {
        let foo = [0, 1, 2, 3]; // BranchA
        foo[0]
    }
}

However another option exists which would avoid the need for a separate round of elision by simply passing the buck to the dead code optimizer by expanding rather than reducing the constant conditional. This has the advantage that would also address the "is it valid in all cases" caveat mentioned above, because the expanded code would then be equivalently type-checked prior to the substitution and the error would be revealed, but at the cost of spending time generating code that will be discarded:

impl Foo {
    const COMPILE_TIME_CONST: CompileTimeConst = CompileTimeConst::BranchA;

    fn foo() -> u8 {
        // Each of the `match` block cases has been expanded to the end of the scope, duplicating
        // the code if and as necessary to avoid any type divergence.
        match Self::COMPILE_TIME_CONST {
            CompileTimeConst::BranchA => {
                let foo = [0, 1, 2, 3];
                foo[3]
            }
            CompileTimeConst::BranchB => {
                let foo = [1, 2, 3];
                foo[3]
            }
        }
    }
}

Notice that here the potential out-of-bounds when accessing foo[3] contingent on the value of COMPILE_TIME_CONST would be caught since the code is first generated then elided.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The issue provides only Rust examples and no repository files, tests, or compiler entry points. Start by comparing the proposed branch-elision and branch-expansion approaches, then define whether every possible constant value must type-check. Done would require an agreed RFC direction and a specified, implementable model for const-conditional type checking.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.