Repository navigation
const if to supress instantiation/codegen of unreachable branches #3582
Description
Currently there's a suboptimal interaction between const asserts and const generics.
A more general function that is valid for all N cannot call a function that asserts at compile time that N fulfills some conditions even if the call is on a branch that's dead for the invalid N.
I.e. this does not compile:
#![feature(inline_const)]
// method in std
fn method_with_precondition<const N: usize>() {
const { assert!(N > 0) };
}
// user method, no way to opt out of asserts for N = 0
fn generic_caller_method<const N: usize>() -> fn() {
if N > 0 {
panic_on_zero::<N>
} else {
|| {} // fallback
}
}
fn main() {
let _fun = foo::<0>();
}error[E0080]: evaluation of `panic_on_zero::<0>::{constant#0}` failed
--> src/main.rs:5:13
|
5 | const { assert!(N > 0) };
| ^^^^^^^^^^^^^^ the evaluated program panicked at 'assertion failed: N > 0', src/main.rs:5:13
|
= note: this error originates in the macro `assert` (in Nightly builds, run with -Z macro-backtrace for more info)
This blocks / requires unpleasant choices in several T-libs features that want to check for N != 0 or similar constraints:
- Tracking Issue for
Iterator::array_chunksrust#100450 - Tracking Issue for slice::array_chunks rust#74985
- Tracking Issue for slice::array_windows rust#75027
- Tracking Issue for split_array rust#90091 - although this one needs generic_const_exprs before we can even worry about callers
- Tracking Issue for
Iterator::map_windows(featureiter_map_windows) rust#87155
If const-eval in the dead branches is expensive it might also help compile perf.
There are efforts (rust-lang/rust#99682) to move monomorphization-time errors to check time and also apply those checks to dead code (rust-lang/rust#112879), which will make it even more difficult to discharge compile-time obligations imposed by callees.
Currently the language does not seem to have any way to select different code paths in generic contexts without also instantiating those paths. Even generic_const_exprs does not offer this unless it gets extended rust-lang/project-const-generics#26
Therefore it would be useful if we could write the case above as
fn foo<const N: usize>() -> fn() {
const if N > 0 {
panic_on_zero::<N>
} else {
|| {} // fallback
}
}so that panic_on_zero::<0> will never be instantiated.
Activity
Jules-Bertholet commentedon Mar 7, 2024
I would note pattern types as an alternative; you could constrain an input
constparameter with a pattern (egconst N: usize in 1..), and combine this with trait machinery to select an impl based on the value of theconst:Lokathor commentedon Mar 7, 2024
That is significantly harder to understand/review.
the8472 commentedon Mar 7, 2024
That looks like the same kind of unification as the const expr issue that I linked to.
oli-obk commentedon Mar 7, 2024
I'm a big proponent of pattern types, but I have to agree this is not a good use case for it.
On the topic at hand, do you want a
generic_const_exprslike scheme where this shows up in where bounds and needs to have where bounds all the way up to where it is monomorphic? Or do you want this to be a post monomorphization error explicitly? Or well, in the case of the example, a way to avoid post monomorphization errors?the8472 commentedon Mar 7, 2024
Oh, having the issue written like this (returning a function pointer) I now realize that anonymous consts + function pointers might help, if the optimizer plays along.
I.e.
But this is not as ergonomic as a real const-if because it means callers needs to hide the dead branches inside functions that get selected by a const block.
At least the playground doesn't complain.
Edit: #3582 (comment) says that this will stop working in the future.
Jules-Bertholet commentedon Mar 7, 2024
I fully agree, but if pattern types are added to the language for other reasons anyway, then perhaps the ergonomics improvement alone wouldn't justify a separate dedicated
const if. (I'm not arguing for this claim, just raising the possibility.)I would also note, that with pattern types as const generic parameters, much of what is now implemented as post-mono assertions could become pre-mono checks, eg:
A hypothetical
const matchcould then improve the ergonomics:Lokathor commentedon Mar 7, 2024
Well, once there's const match then there's not a strong argument against const if, since it would seem that const if is a strict subset of const match.
the8472 commentedon Mar 7, 2024
Imo that's just restating the problem. In this context pattern types are ~isomorphic to
generic_const_exprswithConditional<{(1..).contains(N)}>. And here too impls may very well only exist for some pattern of N and not all N, meaning more-generic caller faces the same issue.And const-match is the ~same solution to the problem.
Jules-Bertholet commentedon Mar 7, 2024
The advantage of the pattern types approach specifically is that the compiler can do exhaustiveness, disjointness etc checking, so rust-lang/project-const-generics#26 is potentially tractable even for impls involving them. With arbitrary
generic_const_exprs, you need a full SMT solver or post-mono errorsoli-obk commentedon Mar 7, 2024
I think even if we come up with a perfect
wherebound based system, having an escape hatch that you explicitly need to opt into is good.It may be easier on the compiler implementation side to require a
const ifexpression to be of function pointer type and its branches to only be path expressions. So basically sugar forwhere, due to the magical nature of
rustc_arg_required_constbeing an explicit hack (that we already have for legacy SIMD reasons) where the argument must be const, we can do expressions without triggeringgeneric_const_exprschecks.the8472 commentedon Mar 7, 2024
Would that mean less work for the compiler compared to #3582 (comment)
oli-obk commentedon Mar 7, 2024
that only works because rust-lang/rust#112879 hasn't landed in forever because it's such a massive perf regression
the8472 commentedon Mar 7, 2024
I'll cite that as another reason to have const-if then.
tgross35 commentedon Mar 8, 2024
This seems like a component of something more generally usable, along the lines of Zig's
comptimetype operations. That is, if we support branching and pruning on const generics, it would be cohesive to allow behavior based on generic types (if T == u32 { ... }) - which we may or may not want.Pattern types do indeed make selecting behavior more complicated, but something like a pattern type in the where bounds does seem more consistent with generic types if the intent is simply that compiling fails in certain conditions.
8 remaining items
the8472 commentedon May 8, 2024
rust-lang/rust#122785 to prove that it's useful. I have not yet added more because I wasn't sure how shaky that foundation is.
Would that also be true for crate-private methods?
The cases I have in mind involve generic callees.
I could imagine a case where someone wants to use it as a sort of late
cfg. E.g. to select code-paths based onsize_ofor other compiletime calculations and not having to deal with asserts from the unused impls could be useful. But if that does arise I guess we could tell them to add a phantom generic param.RalfJung commentedon May 8, 2024
Lol that landed a day after rust-lang/rust#122568. ^^
@saethlin's rust-lang/rust#123272 landed later. I can't tell how much of an effect rust-lang/rust#121421 had without that later fix. It may be worth trying to revert rust-lang/rust#122785 to see if it still makes a difference -- it should not make a difference in terms of how much we codegen, and I would be somewhat surprised if just the traversal of "mentioned" items makes such a difference.
Yes. The initial reachability analysis is necessarily conservative and there's no good way to exclude any monomorphic function from being considered. We can do our best to fix issues like rust-lang/rust#119214 but it's not possible to fix them all (without completely re-thinking how we handle monomorphic items that "may or may not" be needed by downstream crates).
As I said, I think we should actually do item collection starting with all monomorphic items as roots, and
constblocks in monomorphic items should be considered likeconstitems (which we also always evaluate). The only way I see to fix the problem of "we sometimes do evaluate them and sometimes don't" (rust-lang/rust#122814) is to always evaluate them.... until polymorphization optimizes away the pram and codegen's the function anyway. ;)
the8472 commentedon May 9, 2024
rust-lang/rust#124907 the difference is tiny now
RalfJung commentedon May 9, 2024
Thanks for checking!
CAD97 commentedon May 26, 2024
I think we'd prefer to avoid a situation where
const if condition { … }andif (const { condition }) { … }are both valid but have subtly different guarantees, especially if common compilation modes skip monomorphization of dead arms of the latter as an optimization. Avoiding this is one of two reasons I typically spell this asstatic ifinstead.The second is that there's a desire for a construct which guards not just post-mono asserts but also pre-mono bounds. In an ideal future, I'd like to be able to have a function declare a pre-mono bound of e.g.
where const { N > 0 }and still be able to call it from a function without that bound by handling the fallback case. Translating the pattern refinement types example:Further overloading the meaning of
staticisn't great, but neither is overloading the meaning ofconstto include controlling code instantiation.const matchas used above functions by rebinding a new, more tightly bound const name.static ifwould function by a kind of refinement typing, introducing additional generic bounds to the controlled scope.This is wishful thinking, to be sure, but despite the details being hairy, the base concept is straightforward (and basically just a port of the C++ constexpr if semantics to Rust generics).
the8472 commentedon May 30, 2024
Yes, this is critical for the libs usecase. Without it we can't promote a const assert to a bound later on.
bstrie commentedon Jul 18, 2024
I don't think we need to burden either
constorstaticwith this unfortunate overload. For that matter, we don't need to burdenifwith the complexity of handling both branching and code instantiation.We already have
wherefor this use case, so I'd suggest it makes more sense just to allowwhereblocks in these positions for selective code instantiation:This seems like a natural and straightforward extension that doesn't conflict with the existing semantics of
where.cramertj commentedon Jan 17, 2025
Personally, I'd expect the code following
whereto allow type bounds, not just const bounds, but obviously that triggers all the typical issues with specialization.e.g.
CAD97 commentedon Jan 17, 2025
My current thinking is also that an expression-level
where, if allowed, should syntactically take a where-clause the same as item-levelwhere, even if (most) type bounds aren't specializable this way. However, ifwhere constis a thing, that opens up the fairly natural looking composed syntax of:deline-oss commentedon Jan 17, 2025
sorry for my ignorance but why we need new construct for pruning branches at compile-time when we could reuse existing inline consts like:
it seems everyone mentioning some new ways to achieve it but no one explained why simplest solution wont cut it
RalfJung commentedon Jan 17, 2025
We generally guarantee that all items mentioned in a function get monomorphized. We could add special hacks that exempt items mentioned inside a
if const, but that would be exactly that -- a hack, giving magic meaning to a composition of two constructs that neither construct has in isolation. It would also be confusing since your example would in fact not hit that case: you are not usingif constbutif or (const, const)(if we replace||by prefix syntax). We could extend the hack further but it should be clear that this is not going to produce a nice end state.deline-oss commentedon Jan 17, 2025
sorry it was badly crafted example, it would prune only based on
if const {}, not arbitraryif const {} || const {}, edited.It seems like there are actually 2 use-cases:
const {}(i think zig has this feature where it doesnt typecheck or even emit ir for comptime known false branches)bstrie commentedon Mar 20, 2025
I want to ask a clarifying question to make sure we're all on the same page here.
So far it looks like many here are focusing on the use case of monomorphization/const generics.
But I think a few others are also thinking about providing a general Zig-style method for conditionally compiling code (skipping all typechecking of untaken branches in the process) based on arbitrary compile-time evaluation (
const fnor otherwise).So the question is, does anyone foresee any problem with having a single mechanism that provides both?
In particular, are there any problems with allowing this feature to accept the result of evaluating arbitrary constant expressions, rather than specifically const generics?
bstrie commentedon Mar 22, 2025
@cramertj Can you elaborate on what issues with specialization that example would cause? AIUI this feature wouldn't impact trait resolution, so I don't see what the problem is with allowing the generic metadata to instantiate differently based on the concrete type.
cramertj commentedon Mar 22, 2025
@bstrie #3582 (comment)
One of the easiest specialization soundness demos is rust-lang/rust#40582:
This prints "specialization", when it should panic. The lifetime of the
Rbeing produced ('static) does not match that of the input ref. My understanding is thathelperis monomorphized in a way that does not respect the lifetime bounds on the specialized impl offrom_ref, so it chooses the specialized impl (which should not be applicable) rather than the default impl.Inline
wherehas, I think, the same problem-- it just allows us to remove the default impl ofFromRef:My understanding is that when
helperis monomorphized, it will incorrectly ignore lifetime bounds and will choose thewherebranch rather than theelsebranch.