(Lack of) Polymorphization can lead to an unnecessarily recursive type & make compilation fail #77664
Labels
A-suggestion-diagnostics
Area: Suggestions generated by the compiler applied by `cargo fix`
A-trait-system
Area: Trait system
C-enhancement
Category: An issue proposing an enhancement or a PR with one.
C-optimization
Category: An issue highlighting optimization opportunities or PRs implementing such
D-terse
Diagnostics: An error or lint that doesn't give enough information about the problem at hand.
I-monomorphization
Issue: An error at monomorphization time.
T-compiler
Relevant to the compiler team, which will review and decide on the PR/issue.
Main issue related to the "polymorphization" (or lack thereof) problem: #46477
Minimal repro:
fails with:
The issue seems to stem from the fact that the closure created inside
foo
,|| ()
, despite not usingf
and thus not having anything of typeF
inside, has nevertheless a type that is infected with all the generic parameters in scope. So this leads to that closure being generic over the type parameterF
.So, the initial call
foo::<F = fn {initial}>
causes Rust to go and monomorphizefoo
for that givenF
, which requires monomorphizing the next call tofoo()
, and because of the above, whereby the closure is infected with theF
type parameter, it will end up monomorphizing something likefoo::<F = Closure<fn {initial}>>
. And that call, in and on itself, leads to monomorphizing the next call,foo::<F = Closure<Closure<fn {initial}>>>
, and so on, …, ad infinitum, causing the type recursion limit to be reached.Granted, we could say that this is an issue with "badly done" recursion, and so we may ask:
Why report this
I think it is worth doing it for several reasons:
Mainly, because it causes a compilation error! And the error message is not very helpful 😬
For instance, the first time I've encountered this issue, it is definitely an obscure one, and it's taken me a while to figure out what the root cause for the compilation error was.
Thus, having this be available may serve for future reference for other people, rather than the linked canonical issue Instantiate fewer copies of a closure inside a generic function #46477, or specific problems that some people had with iterator adaptors.
This is not "intended behavior" in that replacing a capture-less closure with an isomorphic function item avoids the compilation error!
Another odd thing is that the compilation error happens at usage site rather than at definition site, which I think has also been part of the confusion (I was getting
cargo check
to pass fine, and the compilation errors only happened withcargo test
calling that function). Worse, even a call site, if it can be proven unreachable by the compiler, can trigger dead code elimination and the whole recursive-monomorphization to be skipped altogether. That is, the following code compiles fine:This issue is also here to raise awareness about the semver implications of Rust supporting polymorphization: it is not only a matter of binary size and code bloat (as mentioned in Instantiate fewer copies of a closure inside a generic function #46477), it is actually a matter of compilation erroring or not. That means that if some version of the compiler were to support polymorphization and effectively make this code compile, then regressing with that polymorphization functionality would be breaking change⚠️ .
Finally, this is also to suggest a workaround for those stumbling upon it.
The workaround
::with_locals
Some of you may have figured it by now, given how the
fn()...
item was not infected by the typeF
.Similarly, in the case of a stateful closure, especially one actually capturing
f
, not even polymorphization would solve the issue. The real way to solve it would be introduce type erasure, similar tofn()
, but for closures. That is,dyn Fn...
:Less easy cases
For the
FnOnce()
people, this will either requireBox
ing, or to use an internal helper function with a runtime-checkedFnOnce
-ness:Note that the "fallibility" of the
FnMut()
would be solved with unsized locals, and/or, equivalently, with aRefMove<'_, T>
,StackBox<'_, T>
,Own<'_, T>
,&'_ own T
owning reference type (instanced withT = dyn FnOnce()
).Finally, If the closure happens to return a value, then care should be take not to wrap the return type.
That is,
will also fail since we hit the same issue as before but this time with
R -> (R, i32) -> ((R, i32), i32) -> …
The workaround, is to (ab)use the mutability of the closure to return the value elsewhere:
The text was updated successfully, but these errors were encountered: