Skip to content

What are the guarantees around which constants (and callees) in a function get monomorphized? #122301

Open

Description

@the8472

Currently this compiles in stable.

struct Foo<T, const N: usize>(T);

impl<T, const N: usize> Foo<T, N> {
    const BAR: () = if N == 0 {
        panic!()
    };
}

struct Invoke<T, const N: usize>(T);

impl<T, const N: usize> Invoke<T, N> {
    const FUN: fn() = if N != 0 {
        || Foo::<T, N>::BAR
    } else {
        || {}
    };
}

fn main() {
    Invoke::<(), 0>::FUN();
}

This is a useful property¹ but if the panic path were not completely excluded this could lead to monomorphization-time errors which are undesirable since they only show up in cargo build and not in cargo check. But not as undesirable as #107503 where the errors are optimization-dependent.

Still, @RalfJung indicated that the current behavior might not be intentional so I'm filing this issue for clarification.

(Also see rust-lang/rfcs#3582 for more discussion of the same question.)


¹actual use could look like this:

// library-provided function:

/// Fails to compile if N == 0
fn chunks<const N: usize>(foo: Foo) -> &[[u8; N]] {
   const {
       assert!(N > 0)!
   }

   todo!()
}

// user-code, generic over all N
fn library_user_code<const N: usize>(foo: Foo) ->  {

   // this could be generated by a `const_if!()` macro or use inline const blocks
   struct ConstIf< const N: usize>;

   impl<const N: usize> ConstIf<N> {
     const FUN: fn() = if N != 0 {
         |foo| chunks::<N>(foo)
     } else {
         |foo| { /* fallback code here */ }
     };
   }
   
   ConstIf::<N>::FUN(foo)
}

Activity

  1. added
    T-langRelevant to the language team
    A-const-genericsArea: const generics (parameters and arguments)
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    on Mar 10, 2024
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Mar 10, 2024
  3. RalfJung commented on Mar 10, 2024

    @RalfJung
    Member

    Cc @rust-lang/wg-const-eval

    Ultimately this is a t-lang question though so might need a t-lang design meeting or some such thing.

  4. RalfJung commented on Mar 10, 2024

    @RalfJung
    Member

    In fact you don't need closures, this builds as well:

    struct S<T>(T);
    impl<T> S<T> {
        const C: () = panic!();
    }
    
    const fn bar<T>() { S::<T>::C }
    
    struct Invoke<T, const N: usize>(T);
    
    impl<T, const N: usize> Invoke<T, N> {
        const FUN: () = if N != 0 {
            bar::<T>() // not called for N == 0, and hence not monomorphized
        } else {
            ()
        };
    }
    
    fn main() {
        let _val = Invoke::<(), 0>::FUN;
    }
  5. RalfJung commented on Mar 10, 2024

    @RalfJung
    Member

    This is a useful property

    Your example doesn't quite seem to have the right shape for that use-case; could you flesh out the example a bit more to make this more clear?

  6. the8472 commented on Mar 10, 2024

    @the8472
    MemberAuthor

    could you flesh out the example a bit more

    Added it to the top post.

  7. RalfJung commented on Mar 10, 2024

    @RalfJung
    Member

    Makes sense, thanks!

    Note that without generic_const_exprs such a const_if! macro will be very limited.

  8. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Mar 10, 2024
  9. added 2 commits that reference this issue on Mar 15, 2024
  10. added 2 commits that reference this issue on Mar 17, 2024
  11. added a commit that references this issue on Mar 17, 2024
  12. 5 remaining items

  13. RalfJung commented on Mar 22, 2024

    @RalfJung
    Member
  14. the8472 commented on Mar 22, 2024

    @the8472
    MemberAuthor

    This one shouldn't be optimization-dependent though, so I hope there's no conflict between them.

  15. RalfJung commented on Mar 22, 2024

    @RalfJung
    Member

    It's not opt-dependent, but it's also unclear how we want to resolve the opt-dependent issue. Some proposals involve also walking all items "mentioned" in a const. That would be in direct conflict with your goal here I think. To be clear I think that's a weakness of those proposals. But if that turns out to be the only viable strategy then we'll have to decide what we want more: using const tricks to control what gets monomorphized, or not having optimization-dependent errors.

  16. RalfJung commented on Mar 25, 2024

    @RalfJung
    Member

    One crucial part of this construction is that everything involved is generic. If somewhere in the two "branches" you end up calling a monomorphic function, then that may have its constants evaluated even if it is in the "dead" branch -- or it may not, it depends on which functions are deemed cross-crate-inlinable. That's basically what #122814 is about.

  17. scottmcm commented on Apr 3, 2024

    @scottmcm
    Member

    I would really like to have a const if language feature that people can use to communicate to us that they're relying on it, and thus that we can ensure actually does keep doing what they want.

    (Just speaking for me, not for Lang as a whole.)

  18. RalfJung commented on Apr 3, 2024

    @RalfJung
    Member

    Yeah it would be nice to have that. Currently I don't see how to implement it though.

  19. changed the title [-]Should dead code in const be monomorphized?[/-] [+]Provide better control of which constants (and callees) in a function get monomorphized[/+] on May 8, 2024
  20. RalfJung commented on May 8, 2024

    @RalfJung
    Member

    I would really like to have a const if language feature that people can use to communicate to us that they're relying on it, and thus that we can ensure actually does keep doing what they want.

    That's rust-lang/rfcs#3582.

    This issue here is about whether we want to stably guarantee something about the current syntax that can stand-in for the desired feature, if somewhat crudely.

    I think what we'd need is that in a const item, only the functions that are actually called get monomorphized. Which seems like a reasonable promise to make? But the reachable root computation is extremely involved, and it does statically traverse const items (see the updated comments in #122769), and we have bugs like #119214, so -- @the8472 I don't know if your guarantee actually holds today, but I suspect not, though it's kind of a bug.

    That said, even if we had const if, I am not sure what #119214 would do to that. Nothing good, probably. reachable can't evaluate constants, it has to go entirely based on syntactic appearance, so everything in both arms of the if would be "reachable". And once #119214 is resolved we can maybe make the guarantee without new syntax (though new syntax may still be nice).

  21. RalfJung commented on May 8, 2024

    @RalfJung
    Member

    #119214 can only affect monomorphic functions.

    I don't think we can provide any guarantees that a monomorphic function won't be codegen'd and thus have its consts evaluated. Even if there is a const if or with the trick from this issue, monomorphic functions can always end up being collected under some odd conditions. The best we can do is encode their MIR instead of codegen'ing them so that if we are wrong and they are needed, a downstream crate can do the codegen. But with the current strategy of not encoding the MIR for (sufficiently big, i.e. non-cross-crate-inlineable) monomorphic functions but always encoding their compiled form, fundamentally we have to conservatively compile them if they may be needed, and then fundamentally we have to evaluate their consts and stop compilation if any of them panics. (That's why I suggested in #122814 that we just always evaluate all consts in monomorphic functions, and in functions they "mention".)

    But... I think if we disregard monomorphic functions, then I think it is true that in a const item we'll only monomorphize what actually gets called. We may very much overapproximate what is "reachable", and hence encode MIR for things, but the only way we start eval'ing the consts in a generic function is if it gets codegen'd (which can only happen if it gets mentioned in a codegen'd function, which is not the case here), or const-eval'd.

    Well, and there's #122828. But that can only happen when the function in question is called from somewhere that we compute optimized_mir on. And in your example @the8472 I assume chunks only ever gets called via that FUN constant. We don't run the inliner on const item bodies so this can't happen either.

    So... yeah I think your pattern is safe. But there's so many moving parts here it's really hard to say. That said, guaranteeing that your pattern is safe is exactly as hard as guaranteeing that an const if is safe, the difficulty is the same in both cases. const if is just syntactically nicer, but no easier to implement.

  22. changed the title [-]Provide better control of which constants (and callees) in a function get monomorphized[/-] [+]What are the guarantees around which constants (and callees) in a function get monomorphized?[/+] on May 8, 2024
  23. RalfJung commented on May 8, 2024

    @RalfJung
    Member

    So concretely, I think that if a function is

    • generic (in a type or a const -- lifetimes are not enough)
    • and only ever mentioned in a const

    (both of which should be true for your pattern)
    then we will not ever consider it in the monomorphization collector, and therefore the only way it can ever be considered is by const eval, which will only consider function items that actually occur in the given execution.

    That said, there is this thing called polymorphization, and I have absolutely no idea how it works. If a function gets polymorphized, does it behave basically like a monomorphic function despite being generic? In that case you'd have to further make sure that your function does not get polymorphized. I don't even know which functions we can polymorphize. There does not seem to be a tracking issue for polymorphization, and it does seem to be nightly-only.

  24. lcnr commented on May 9, 2024

    @lcnr
    Contributor

    That said, there is this thing called polymorphization, and I have absolutely no idea how it works. If a function gets polymorphized, does it behave basically like a monomorphic function despite being generic? In that case you'd have to further make sure that your function does not get polymorphized. I don't even know which functions we can polymorphize. There does not seem to be a tracking issue for polymorphization, and it does seem to be nightly-only.

    I think it's fine to ignore polymorphization in the decision here. It should not change what gets monomorphized but instead only merges different monomorphic instances in one polymorphic one. It's the same as the linker/llvm deduplicating equivalent functions, which is something we already have to support

  25. added
    I-lang-radarItems that are on lang's radar and will need eventual work or consideration.
    and removed
    I-lang-nominatedNominated for discussion during a lang team meeting.
    on Jan 26, 2025
  26. traviscross commented on Feb 24, 2025

    @traviscross
    Contributor

    As a possible minimization of this and of what @RalfJung said, for my own notes when we pick this up:

    fn f<const N: usize>() {
        const {
            if N > 0 {
                // The function `g` is generic over a const or type
                // parameter.
                const fn g<const N: usize>() { const { assert!(N > 0); } }
                // And it's only ever mentioned in a `const`.
                g::<N>()
            }
        }
    }
    
    fn main() {
        _ = f::<0>();
        _ = f::<1>();
    }

    Playground link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-const-genericsArea: const generics (parameters and arguments)C-discussionCategory: Discussion or questions that doesn't represent real issues.I-lang-radarItems that are on lang's radar and will need eventual work or consideration.T-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions