Skip to content

rustdoc-json: Show implied outlives-bounds #142226

@obi1kenobi

Description

@obi1kenobi
Member

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.

cc @aDotInTheVoid

@rustbot label A-rustdoc-json

Activity

added
needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.
T-rustdocRelevant to the rustdoc team, which will review and decide on the PR/issue.
on Jun 9, 2025
workingjubilee

workingjubilee commented on Jun 9, 2025

@workingjubilee
Member

Hm. Does rustdoc have access to the implicit implications that are added in MIR borrowck?

These bounds are not added to the `ParamEnv` of the affected item itself. For lexical
region resolution they are added using [`fn OutlivesEnvironment::from_normalized_bounds`].
Similarly, during MIR borrowck we add them using
[`fn UniversalRegionRelationsBuilder::add_implied_bounds`].

changed the title [-]rustdoc-json: Show implicit bounds on generics in functions, impls, and types[/-] [+]rustdoc-json: Show implied bounds on generics in functions, impls, and types[/+] on Jun 9, 2025
added
C-feature-requestCategory: A feature request, i.e: not implemented / a PR.
and removed
needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.
on Jun 9, 2025
fmease

fmease commented on Jun 9, 2025

@fmease
Member

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.

The only kind of bound that can be implied are outlives-bounds (region outlives-bounds 'a: 'b and type outlives-bounds T: '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.

added
A-lifetimesArea: Lifetimes / regions
A-implied-boundsArea: Implied bounds / inferred outlives-bounds
on Jun 9, 2025
fmease

fmease commented on Jun 9, 2025

@fmease
Member

(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:

pub struct Ref<'a, T: 'a + ?Sized>(pub &'a T);

Then the following change (let's call it "v1 → v2") is breaking:

- pub struct Ref<'a, T: 'a + ?Sized>(pub &'a T);
+ pub struct Ref<'a, T: ?Sized>(pub &'a T);

even though Ref/v2 still has an (explicit) implied-bound of T: '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 from upstream::Ref<'r, dyn Trait + 'r> to upstream::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:

trait Trait {}
impl Trait for &i32 {}

fn accept(_: upstream::Ref<'_, dyn Trait>) {}
//                             ^^^^^^^^^ With upstream v1 this means: `dyn Trait + 'r`
//                                       With upstream v2 this means: `dyn Trait + 'static` (!)

fn main() {
    let local = 0i32;
    accept(upstream::Ref(&&local)); // OK with upstream v1
                                    // ERR with upstream v2: `local` does not live long enough
}

[it] would implicitly change its meaning of that type […]

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.

changed the title [-]rustdoc-json: Show implied bounds on generics in functions, impls, and types[/-] [+]rustdoc-json: Show implied outlives-bounds[/+] on Jun 9, 2025
fmease

fmease commented on Jun 9, 2025

@fmease
Member

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:

pub struct Type<'r, T: Iterator>(&'r T::Item);

Here, we imply <T as Iterator>::Item: 'r.

obi1kenobi

obi1kenobi commented on Jun 9, 2025

@obi1kenobi
MemberAuthor

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 in cargo-semver-checks.

But yes, I was hoping implied and explicit bounds would be shown separately.

obi1kenobi

obi1kenobi commented on Jun 9, 2025

@obi1kenobi
MemberAuthor

Hm. Does rustdoc have access to the implicit implications that are added in MIR borrowck?

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

fmease commented on Jun 9, 2025

@fmease
Member

That only applies to lifetime bounds though, right?

To type-outlives-bounds T: 'a, yes.

Trait bound elaborations of the form

  1. ɛ → Sized unless ?Sized present (sized elaboration) or
  2. SubtraitSubtrait + Supertrait (supertrait elaboration) or
  3. TraitAlias... (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

workingjubilee commented on Jun 9, 2025

@workingjubilee
Member

@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

aDotInTheVoid commented on Jun 9, 2025

@aDotInTheVoid
Member

Rustdoc can totally pull the info from tcx/ty: #142264

The actual limitations are:

  • This makes the output much more normalized, which might be worse for HTML
  • It's unclear if this lets you differentiate between implied and user-written bounds.

Edit: I think both of these go away if we implement it a slightly different way: #142264 (comment)

added 2 commits that reference this issue on Jun 10, 2025
added a commit that references this issue on Jun 10, 2025
added a commit that references this issue on Jun 10, 2025
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-implied-boundsArea: Implied bounds / inferred outlives-boundsA-lifetimesArea: Lifetimes / regionsA-rustdoc-jsonArea: Rustdoc JSON backendC-feature-requestCategory: A feature request, i.e: not implemented / a PR.T-rustdocRelevant to the rustdoc team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @obi1kenobi@fmease@aDotInTheVoid@workingjubilee@rustbot

        Issue actions

          rustdoc-json: Show implied outlives-bounds · Issue #142226 · rust-lang/rust