Skip to content

Conversation

@obi1kenobi
Copy link
Member

@obi1kenobi obi1kenobi commented Nov 2, 2025

Include implied and elaborated (henceforth, implied) bounds in the rustdoc JSON representations of associated types, impl Trait types, and generic type parameters. Implemented as a new implied_bounds: Vec<GenericBound> field, alongside the existing bounds: Vec<GenericBound> field that stores explicit (syntactic) bounds. An attempt at implementing #143197, based on the feedback and discussion in #143559 (comment).

Rustdoc JSON distingushes between explicit bounds specified together with the generic parameter definition versus ones given via where clauses (which we do not change). The design of this PR considers implied bounds an inherent property of the generic type parameter, and does not distinguish the source of the implied bound between the two sets of explicit bounds. I believe this simplifies the design and implementation without hurting any use existing or realistic future use case.

Per T-rustdoc feedback, the implementation is JSON-only and aims to minimize changes to clean and elsewhere. This is intended to mitigate possible performance impact on rustdoc as a whole, while providing this data to JSON-based workloads like cargo-semver-checks.

Please see below for what this PR does, what is deferred to future PRs, and what open questions remain.

Please also note that half the line count is the test suite, and a bunch more is just "adjust dozens of pattern match sites to use a slightly different shape," so this PR isn't as large as it seems 😅

Recommended review order:

  • Get a sense of the overall API:
    • src/rustdoc-json-types/lib.rs
    • src/librustdoc/clean/types.rs
    • src/librustdoc/clean/mod.rs
  • Review how implied bounds are computed
    • src/librustdoc/json/implied_bounds.rs
    • src/librustdoc/json/conversions.rs
  • Review remainder of implementation, which is predominantly plumbing
  • Test suite

Work deferred to future PRs

  • Implied outlives bounds. Just like T: 'a can be an implied bound, 'a: 'b can be an implied bound too. The former is implemented in this PR. To limit scope, I intend to open a separate PR for the latter after this one is merged.
  • Implied bounds for RTN. I'm open to implementing this too, but again I wanted to limit scope for this PR. RTN isn't stable yet anyway, so I felt rustdoc JSON support for its implied bounds could wait.
  • Precise implied bounds analysis for extern opaques. Currently, we don't seem to have sufficient metadata about opaques coming from other crates to determine e.g. whether the opaque is implied to be Sized at its use site. Fixing this seemed like a big lift for a somewhat uncommon case, so I felt it's best tackled later — perfect being the enemy of good enough and all.

Open questions for this PR

  • DocContext use outside of clean. Per T-rustdoc recommendations, this implementation moves all implied bounds computation into the JSON component. In turn that means the JSON-side code needs to do some cleaning of the clauses that represent those implied bounds. To avoid duplication, the current implementation reuses the clean code — but that requires a DocContext which is not otherwise available in the JSON component.
    • Currently, we construct a DocContext when needed — which may be often, and may not be a trivial cost. I'm not sure!
    • Instead, we could duplicate the code we need from clean into the JSON component, adjusting it to not need DocContext. This isn't great either.
    • Instead, we could put a DocContext inside JsonRenderer somehow. This isn't great either, because JsonRenderer is passed around by immutable reference, while clean uses &mut DocContext. We may need either RefCell or Mutex or lots of plumbing to replace &JsonRenderer with &mut JsonRenderer.
    • Possibly other options? I'd love reviewer feedback on this!
  • List of stable lang-item traits. I wasn't able to find a way to get a list of stable lang-item traits that we should consider for implied bounds analysis. We currently have a hard-coded list of them, which may be brittle as new lang-item traits are stabilized. If there's a better way to do this, I'm all ears.

Included tests

  • Generic type params, associated types, TAITs, RPIT (both regular and in async), ATPIT/GAT cases, and assorted DST/sizedness edge cases.
  • Implied bounds both on and originating from trait associated item constraints: both equality and constraints.
  • Fn/FnMut/FnOnce and AsyncFn/AsyncFnMut/AsyncFnOnce implied bounds preserve full parenthesized signatures.
  • Implied Sized bounds as a result of Rust language rules:
    • Sized default when ?Sized is not present, such as in generic parameters, associated types, etc.
    • Sized requirement for function parameters, return values, and contents of slices and arrays.
    • Transitive Sized requirement when a possibly-DST struct or tuple (e.g. struct S<T: ?Sized>(T);) is used in a position that requires it be Sized, like a function parameter or return value.
    • No implied Sized if such a possibly-DST type is used behind indirection, like &/&mut/*const/*mut.
  • Checks that bounds appear either in bounds (explicit, syntactically-included bounds) or implied_bounds (implied or elaborated bounds), never both.

r? fmease

cc @aDotInTheVoid

@rustbot
Copy link
Collaborator

rustbot commented Nov 2, 2025

fmease is currently at their maximum review capacity.
They may take a while to respond.

@rustbot rustbot added A-rustdoc-json Area: Rustdoc JSON backend S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output. labels Nov 2, 2025
@obi1kenobi
Copy link
Member Author

obi1kenobi commented Nov 2, 2025

I'm still not particularly familiar with rustdoc or rustc internals, and I definitely don't have very good intuition for how things work yet. This PR is approximately 80h of me doing my best to figure things out on my own. I don't expect I got everything right—there are probably things that could be improved. But I did write lots of tests with all the edge cases I could think of, and I tried hard not to write anything egregiously wrong :)

Feedback is very welcome, as is advice on resolving the remaining TODOs. In particular, let me know if you have a preference between adding implied_outlives to generic lifetime parameters in PR vs in a future one.

@rust-log-analyzer

This comment has been minimized.

@aDotInTheVoid
Copy link
Member

i’ll take a look at the code later, but for now:

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rust-bors

This comment has been minimized.

rust-bors bot added a commit that referenced this pull request Nov 2, 2025
Add implied bounds to generic types, impl Trait, and assoc types.
@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Nov 2, 2025
@rust-bors
Copy link
Contributor

rust-bors bot commented Nov 2, 2025

☀️ Try build successful (CI)
Build commit: 972828a (972828af4644b91a03dc856d943eb07c56de72a0, parent: bd3ac0330018c23b111bbee176f32c377be7b319)

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (972828a): comparison URL.

Overall result: ❌✅ regressions and improvements - please read the text below

Benchmarking this pull request means it may be perf-sensitive – we'll automatically label it not fit for rolling up. You can override this, but we strongly advise not to, due to possible changes in compiler perf.

Next Steps: If you can justify the regressions found in this try perf run, please do so in sufficient writing along with @rustbot label: +perf-regression-triaged. If not, please fix the regressions and do another perf run. If its results are neutral or positive, the label will be automatically removed.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
0.4% [0.1%, 3.0%] 14
Regressions ❌
(secondary)
0.3% [0.1%, 0.4%] 20
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-0.1% [-0.2%, -0.1%] 2
All ❌✅ (primary) 0.4% [0.1%, 3.0%] 14

Max RSS (memory usage)

Results (primary 2.0%, secondary 1.6%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.0% [1.2%, 3.1%] 12
Regressions ❌
(secondary)
1.6% [1.2%, 2.7%] 9
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 2.0% [1.2%, 3.1%] 12

Cycles

Results (primary 2.6%, secondary -4.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.6% [2.6%, 2.6%] 1
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-4.2% [-4.6%, -3.9%] 2
All ❌✅ (primary) 2.6% [2.6%, 2.6%] 1

Binary size

Results (primary 1.1%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
1.1% [1.1%, 1.1%] 1
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 1.1% [1.1%, 1.1%] 1

Bootstrap: 474.434s -> 474.73s (0.06%)
Artifact size: 390.87 MiB -> 390.95 MiB (0.02%)

@rustbot rustbot added perf-regression Performance regression. and removed S-waiting-on-perf Status: Waiting on a perf run to be completed. labels Nov 2, 2025
Copy link
Member

@aDotInTheVoid aDotInTheVoid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've only looked at the rustdoc-json side, but it seems fine. I'm not comfortable reviewing the ty/clean side of this, so I'll leave that to fmease.

I've also not yet looked an the tests in detail. But they seem quite comprehensive, thanks.

Could you move all the implied-bounds-*.rs tests into a new implied-bounds/ folder?

View changes since this review

.map(|item| clean_impl_item(item, cx))
.collect::<Vec<_>>(),
clean_generics(impl_.generics, cx),
clean_generics(impl_.generics, did, cx),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B. This isn't tested, as rustdoc json doesn't inline. I'm pretty certain it's correct, but worth flagging.

@obi1kenobi
Copy link
Member Author

Could you move all the implied-bounds-*.rs tests into a new implied-bounds/ folder?

Yes, I was going to ask if you're okay with that actually. Happy to do it prior to merging; will leave as-is for now for continuity of review in case fmease has already started reviewing.

@bors
Copy link
Collaborator

bors commented Nov 9, 2025

☔ The latest upstream changes (presumably #139558) made this pull request unmergeable. Please resolve the merge conflicts.

@rust-log-analyzer

This comment has been minimized.

@fmease fmease added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jan 5, 2026
@rust-log-analyzer

This comment has been minimized.

@obi1kenobi
Copy link
Member Author

I made significant changes based on the preliminary feedback, and I've updated the PR description to match. I also significantly expanded the test coverage, and fixed several bugs and gaps in the implementation as a result.

This took a while — thanks for bearing with me!

@obi1kenobi obi1kenobi marked this pull request as ready for review January 26, 2026 00:26
@rustbot
Copy link
Collaborator

rustbot commented Jan 26, 2026

These commits modify tests/rustdoc-json.
rustdoc-json is a public (but unstable) interface.

Please ensure that if you've changed the output:

  • It's intentional.
  • The FORMAT_VERSION in src/librustdoc-json-types is bumped if necessary.

cc @aDotInTheVoid

rustdoc-json-types is a public (although nightly-only) API. If possible, consider changing src/librustdoc/json/conversions.rs; otherwise, make sure you bump the FORMAT_VERSION constant.

cc @CraftSpider, @aDotInTheVoid, @Enselic

Comment on lines +291 to +294
StructItem(s) => ItemEnum::Struct(from_clean_struct(s, owner_def_id, renderer)),
UnionItem(u) => ItemEnum::Union(from_clean_union(u, owner_def_id, renderer)),
StructFieldItem(f) => ItemEnum::StructField(f.into_json(renderer)),
EnumItem(e) => ItemEnum::Enum(e.into_json(renderer)),
EnumItem(e) => ItemEnum::Enum(from_clean_enum(e, owner_def_id, renderer)),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Processing here required the owner_def_id, so we moved away from .into_json() to separate functions, mirroring other cases like the existing from_clean_static().

The changes to function items et al below are analogous and also because of owner_def_id.

Comment on lines -359 to -392
impl FromClean<clean::Struct> for Struct {
fn from_clean(struct_: &clean::Struct, renderer: &JsonRenderer<'_>) -> Self {
let has_stripped_fields = struct_.has_stripped_entries();
let clean::Struct { ctor_kind, generics, fields } = struct_;

let kind = match ctor_kind {
Some(CtorKind::Fn) => StructKind::Tuple(renderer.ids_keeping_stripped(fields)),
Some(CtorKind::Const) => {
assert!(fields.is_empty());
StructKind::Unit
}
None => StructKind::Plain { fields: renderer.ids(fields), has_stripped_fields },
};

Struct {
kind,
generics: generics.into_json(renderer),
impls: Vec::new(), // Added in JsonRenderer::item
}
}
}

impl FromClean<clean::Union> for Union {
fn from_clean(union_: &clean::Union, renderer: &JsonRenderer<'_>) -> Self {
let has_stripped_fields = union_.has_stripped_entries();
let clean::Union { generics, fields } = union_;
Union {
generics: generics.into_json(renderer),
has_stripped_fields,
fields: renderer.ids(fields),
impls: Vec::new(), // Added in JsonRenderer::item
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't deletions per se — we just moved these to dedicated from_clean_struct() / from_clean_union() functions due to the change in function signature.

Comment on lines +457 to +463
// This path is only hit when we aren't going through `from_clean_generics()`,
// i.e. we're processing cases like HRTB binders or function-pointer generic params.
//
// Non-lifetime binders don't currently allow bounds on type parameters, and
// late-bound type params are rejected on function pointers. That leaves no
// place for implied bounds to come from, as of today's version of Rust.
implied_bounds: Vec::new(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the argument in this comment to be true, but I'd love it if reviewers tried to poke holes at it. Perhaps I've missed an edge case.

Comment on lines +164 to +166
let explicit_maybe_sized =
explicit_bounds.iter().any(|bound| is_maybe_sized_bound(bound, renderer));
let implicitly_sized = if *needs_sized_check && explicit_maybe_sized {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote this code this way for readability. In principle, we could use one more layer of nesting and put the explicit_maybe_sized computation after if *needs_sized_check, at the cost of some readability. It's possible the compiler is already doing such a reordering for us, if it can prove that is_maybe_sized_bound() is a pure function.

Happy to tweak this based on reviewer feedback.

Comment on lines +444 to +460
/// Returns `true` if a projection predicate on `proj_trait_ref` should be attached to `trait_ref`.
///
/// Most projections target the exact trait that defines the associated item, which is the
/// `proj_trait_ref == trait_ref` fast path. A small set of lang-item traits define their
/// associated items on a base trait while the surface traits are its supertraits (today: `FnOnce`
/// and `AsyncFnOnce` vs `Fn`/`FnMut` and `AsyncFn`/`AsyncFnMut`). We remap those projections to the
/// supertrait so implied bounds carry the full signature; this list reflects the current language
/// surface and should be extended if new lang-item traits gain this shape.
fn projection_applies_to_trait_ref<'tcx>(
tcx: TyCtxt<'tcx>,
proj_trait_ref: ty::TraitRef<'tcx>,
trait_ref: ty::Binder<'tcx, ty::TraitRef<'tcx>>,
) -> bool {
let trait_ref = trait_ref.skip_binder();
if proj_trait_ref == trait_ref {
return true;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure whether an analogous case is possible outside of lang items. To be honest, this was at the end of chasing an edge case within an edge case, and I reached the complexity limit of how many things I could keep in mind at once.

Flagging for discussion with folks smarter than me.

Comment on lines +875 to +879
let Some(local_parent) = parent.as_local() else {
// Cross-crate TAITs don't carry the parent WF info in metadata,
// so we can't infer outlives bounds here.
return Vec::new();
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inside of this might be unreachable? We're inside the let Some(local_def_id) = opaque_def_id.as_local() arm so I'd expect the parent is also local.

I'm not positive about this, so I wrote this defensively. If reviewers agree it's unreachable, I'm happy to replace it with an assertion instead.

@obi1kenobi
Copy link
Member Author

obi1kenobi commented Jan 26, 2026

Let's see if I have permissions for this or not...

@bors try @rust-timer queue

EDIT: womp womp 😢

@rust-timer
Copy link
Collaborator

Insufficient permissions to issue commands to rust-timer.

@rust-bors
Copy link
Contributor

rust-bors bot commented Jan 26, 2026

@obi1kenobi: 🔑 Insufficient privileges: not in try users

@rust-timer
Copy link
Collaborator

Insufficient permissions to issue commands to rust-timer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-rustdoc-json Area: Rustdoc JSON backend perf-regression Performance regression. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants