You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
What are the guarantees around which constants (and callees) in a function get monomorphized? #122301
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.
// library-provided function:/// Fails to compile if N == 0fnchunks<constN:usize>(foo:Foo) -> &[[u8;N]]{const{assert!(N > 0)!
}todo!()}// user-code, generic over all Nfnlibrary_user_code<constN:usize>(foo:Foo) -> {// this could be generated by a `const_if!()` macro or use inline const blocksstructConstIf<constN:usize>;impl<constN:usize>ConstIf<N>{constFUN:fn() = ifN != 0{
|foo| chunks::<N>(foo)}else{
|foo| {/* fallback code here */}};}ConstIf::<N>::FUN(foo)}
Reacted by Ricardo Fernández Serrata and José Luis Cruz
In fact you don't need closures, this builds as well:
structS<T>(T);impl<T>S<T>{constC:() = panic!();}constfnbar<T>(){S::<T>::C}structInvoke<T,constN:usize>(T);impl<T,constN:usize>Invoke<T,N>{constFUN:() = ifN != 0{bar::<T>()// not called for N == 0, and hence not monomorphized}else{()};}fnmain(){let _val = Invoke::<(),0>::FUN;}
Note that we are currently also exploring various ways to evaluate more constants, which requires some amount of monomorphization in dead code. This is done to combat the problem of optimization-level-dependent build failures:
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.
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.
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.
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
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.
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).
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.
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
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.
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
As a possible minimization of this and of what @RalfJung said, for my own notes when we pick this up:
fnf<constN:usize>(){const{ifN > 0{// The function `g` is generic over a const or type// parameter.constfng<constN:usize>(){const{assert!(N > 0);}}// And it's only ever mentioned in a `const`.g::<N>()}}}fnmain(){
_ = f::<0>();
_ = f::<1>();}
Activity
RalfJung commentedon Mar 10, 2024
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.
RalfJung commentedon Mar 10, 2024
In fact you don't need closures, this builds as well:
RalfJung commentedon Mar 10, 2024
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?
the8472 commentedon Mar 10, 2024
Added it to the top post.
RalfJung commentedon Mar 10, 2024
Makes sense, thanks!
Note that without generic_const_exprs such a
const_if!macro will be very limited.add test for rust-lang#122301 to cover behavior that's on stable
add test for rust-lang#122301 to cover behavior that's on stable
Rollup merge of rust-lang#122572 - the8472:test-const-deadness, r=Ral…
Rollup merge of rust-lang#122572 - the8472:test-const-deadness, r=Ral…
Auto merge of rust-lang#122653 - matthiaskrgr:rollup-28h37ym, r=matth…
5 remaining items
RalfJung commentedon Mar 22, 2024
Note that we are currently also exploring various ways to evaluate more constants, which requires some amount of monomorphization in dead code. This is done to combat the problem of optimization-level-dependent build failures:
the8472 commentedon Mar 22, 2024
This one shouldn't be optimization-dependent though, so I hope there's no conflict between them.
RalfJung commentedon Mar 22, 2024
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
consttricks to control what gets monomorphized, or not having optimization-dependent errors.RalfJung commentedon Mar 25, 2024
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.
scottmcm commentedon Apr 3, 2024
I would really like to have a
const iflanguage 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.)
RalfJung commentedon Apr 3, 2024
Yeah it would be nice to have that. Currently I don't see how to implement it though.
[-]Should dead code in const be monomorphized?[/-][+]Provide better control of which constants (and callees) in a function get monomorphized[/+]RalfJung commentedon May 8, 2024
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
constitem, 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.reachablecan't evaluate constants, it has to go entirely based on syntactic appearance, so everything in both arms of theifwould be "reachable". And once #119214 is resolved we can maybe make the guarantee without new syntax (though new syntax may still be nice).RalfJung commentedon May 8, 2024
#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 ifor 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
constitem 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
chunksonly ever gets called via thatFUNconstant. We don't run the inliner onconstitem 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 ifis safe, the difficulty is the same in both cases.const ifis just syntactically nicer, but no easier to implement.[-]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?[/+]RalfJung commentedon May 8, 2024
So concretely, I think that if a function is
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.
lcnr commentedon May 9, 2024
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
traviscross commentedon Feb 24, 2025
As a possible minimization of this and of what @RalfJung said, for my own notes when we pick this up:
Playground link