-
Notifications
You must be signed in to change notification settings - Fork 13.6k
Description
Implied bounds are a serious and underappreciated SemVer hazard:
- They are public API, but not obvious nor written down explicitly in the location where they are public API.
- They are "infectious", behaving similarly to auto-traits, and therefore able to cause a "spooky action at a distance" style of breakage.
Consider the following example:
pub fn example<'a, T>(value: Wrapper<'a, T>) {}
pub struct Wrapper<'a, T>(Inner<'a, T>);
struct Inner<'a, T>(&'a i64, T);
impl<'a, T> Wrapper<'a, T> { ... }
impl<'a, T> Inner<'a, T> { ... }
Noting that Inner
is a private type, let's say it sees the following change:
- struct Inner<'a, T>(&'a i64, T);
+ struct Inner<'a, T: 'a>(&'a T);
The new T: 'a
bound propagates in "infectious" fashion from Inner
to Wrapper
to example
, causing breaking changes in the public API of both Wrapper
and example
.
Today, neither the HTML nor the JSON outputs of rustdoc show implicit bounds. Despite the breaking change, the rustdoc HTML and JSON outputs will be identical before and after this change, making it completely invisible to both consumers of the API and the maintainers who wish to not needlessly break their users.
The issue is not specific to "outlives" bounds — it also appears with T: ?Sized
bounds and (I believe) any kind of trait or lifetime bound that is valid in Rust. This issue requests information on all kinds of implicit bounds.
cargo-semver-checks
would like to be able to analyze changes to bounds and flag SemVer hazards. The lack of implicit bounds data means that we currently cannot do so in either direction:
- We cannot see implicit bounds, so SemVer breakage caused by implicit bounds is invisible to us.
- We cannot report breakage due to additions of an explicit bound, because the bound may have already been implied.
- We cannot claim that the removal of an explicit bound in trait associated items is breaking, since (I believe) the bound may still be implied.
Exposing rustc's implicit bounds data in rustdoc JSON would resolve this problem for cargo-semver-checks
. Unrelatedly, as a user of rustdoc HTML, I'd also love to see implicit bounds in HTML as well.
@rustbot label A-rustdoc-json
Activity
workingjubilee commentedon Jun 9, 2025
Hm. Does rustdoc have access to the implicit implications that are added in MIR borrowck?
rust/src/doc/rustc-dev-guide/src/traits/implied-bounds.md
Lines 42 to 45 in c31cccb
[-]rustdoc-json: Show implicit bounds on generics in functions, impls, and types[/-][+]rustdoc-json: Show implied bounds on generics in functions, impls, and types[/+]fmease commentedon Jun 9, 2025
The only kind of bound that can be implied are outlives-bounds (region outlives-bounds
'a: 'b
and type outlives-boundsT: 'a
).I'm not sure what the SemVer hazards around
?Sized
are but they're not a form of implied bound,Sized
would come closer to that notion.fmease commentedon Jun 9, 2025
(To be clear I'm very much in favor of this feature)
There's one important detail I'd like to mention: Explicit (i.e., user-written) outlives-bounds should be differentiated from implied outlives-bounds in the rustdoc JSON output because they aren't 100% the same. Consider the following API:
Then the following change (let's call it "v1 → v2") is breaking:
even though
Ref
/v2 still has an (explicit) implied-bound ofT: 'a
. Why is it breaking?Well, if a downstream user wrote
upstream::Ref<'r, dyn Trait>
for any lifetime'r
(incl.upstream::Ref<dyn Trait>
where'r
is simply'_
) in a signature (i.e., not in a fn or const body), going from upstream v1 to v2 would implicitly change its meaning fromupstream::Ref<'r, dyn Trait + 'r>
toupstream::Ref<'r, dyn Trait + 'static>
which is a different semantic type and can thus lead to actual breakages down the line. E.g., the following code would no longer compile:That's because object lifetime defaulting doesn't (and likely never will) consider implied outlives-bounds, only explicit ones.
For CSC (...) to catch that, explicit "
T: 'a
"s must be differentiable from implied "T: 'a
"s.[-]rustdoc-json: Show implied bounds on generics in functions, impls, and types[/-][+]rustdoc-json: Show implied outlives-bounds[/+]fmease commentedon Jun 9, 2025
I've dropped the on generics from the title "[…] implied bounds on generics […]" because implied outlives-bounds may not only "belong" to "generics" (i.e., generic parameters) but also to "alias types" (most notably, projections (associated type paths)). Consider:
Here, we imply
<T as Iterator>::Item: 'r
.obi1kenobi commentedon Jun 9, 2025
Thanks @fmease — good points all around.
I actually never put 2 and 2 together to figure out the different behavior with respect to
dyn Trait
and implied lifetime bounds, so that's super useful to know and I'll make sure we flag it correctly. That only applies to lifetime bounds though, right?The implied
Iterator::Item: 'r
bound is also a great example, I'll have to think about how we can handle that incargo-semver-checks
.But yes, I was hoping implied and explicit bounds would be shown separately.
obi1kenobi commentedon Jun 9, 2025
I was chatting with @BoxyUwU at RustWeek and at the end of our conversation we believed that rustdoc should in principle have access to everything it might need here. I don't recall the nuances though, so I'm hoping we didn't miss anything.
fmease commentedon Jun 9, 2025
To type-outlives-bounds
T: 'a
, yes.Trait bound elaborations of the form
Sized
unless?Sized
present (sized elaboration) orSubtrait
→Subtrait + Supertrait
(supertrait elaboration) orTraitAlias
→...
(trait alias expansion)are not considered implied bounds in rustc's impl / typical rustc dev jargon, if that's what you're alluding to.
workingjubilee commentedon Jun 9, 2025
@obi1kenobi I have no reason to disagree with Boxy's assessment, I only am noting that if something should work "in principle", then the basis may be "well, in principle, rustdoc really ought to work a very different way than it currently does".
It does seem like we should be able to print out all the implied and elaborated bounds for an item, though, doesn't it?
aDotInTheVoid commentedon Jun 9, 2025
Rustdoc can totally pull the info from tcx/ty: #142264
The actual limitations are:
Edit: I think both of these go away if we implement it a slightly different way: #142264 (comment)
clean_ty_generics
#142275Rollup merge of rust-lang#142275 - aDotInTheVoid:gen-ty-of, r=fmease
Rollup merge of rust-lang#142275 - aDotInTheVoid:gen-ty-of, r=fmease
Rollup merge of rust-lang#142275 - aDotInTheVoid:gen-ty-of, r=fmease
Unrolled build for #142275
Rollup merge of #142275 - aDotInTheVoid:gen-ty-of, r=fmease