-
-
Notifications
You must be signed in to change notification settings - Fork 14.4k
Add implied bounds to generic types, impl Trait, and assoc types. #148379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
|
|
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 |
This comment has been minimized.
This comment has been minimized.
|
i’ll take a look at the code later, but for now: @bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Add implied bounds to generic types, impl Trait, and assoc types.
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (972828a): comparison URL. Overall result: ❌✅ regressions and improvements - please read the text belowBenchmarking 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 @bors rollup=never Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
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.
CyclesResults (primary 2.6%, secondary -4.2%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeResults (primary 1.1%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Bootstrap: 474.434s -> 474.73s (0.06%) |
There was a problem hiding this 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?
src/librustdoc/clean/inline.rs
Outdated
| .map(|item| clean_impl_item(item, cx)) | ||
| .collect::<Vec<_>>(), | ||
| clean_generics(impl_.generics, cx), | ||
| clean_generics(impl_.generics, did, cx), |
There was a problem hiding this comment.
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.
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. |
|
☔ The latest upstream changes (presumably #139558) made this pull request unmergeable. Please resolve the merge conflicts. |
b35832e to
43cc2c3
Compare
This comment has been minimized.
This comment has been minimized.
43cc2c3 to
85d31c2
Compare
This comment has been minimized.
This comment has been minimized.
85d31c2 to
e411e2e
Compare
|
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! |
|
These commits modify Please ensure that if you've changed the output:
rustdoc-json-types is a public (although nightly-only) API. If possible, consider changing |
e411e2e to
658536c
Compare
| 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)), |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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(), |
There was a problem hiding this comment.
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.
| 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 { |
There was a problem hiding this comment.
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.
| /// 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | ||
| }; |
There was a problem hiding this comment.
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.
|
Let's see if I have permissions for this or not... @bors try @rust-timer queue EDIT: womp womp 😢 |
|
Insufficient permissions to issue commands to rust-timer. |
|
@obi1kenobi: 🔑 Insufficient privileges: not in try users |
|
Insufficient permissions to issue commands to rust-timer. |
Include implied and elaborated (henceforth, implied) bounds in the rustdoc JSON representations of associated types,
impl Traittypes, and generic type parameters. Implemented as a newimplied_bounds: Vec<GenericBound>field, alongside the existingbounds: 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
whereclauses (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
cleanand elsewhere. This is intended to mitigate possible performance impact on rustdoc as a whole, while providing this data to JSON-based workloads likecargo-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:
src/rustdoc-json-types/lib.rssrc/librustdoc/clean/types.rssrc/librustdoc/clean/mod.rssrc/librustdoc/json/implied_bounds.rssrc/librustdoc/json/conversions.rsWork deferred to future PRs
T: 'acan be an implied bound,'a: 'bcan 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.Sizedat 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
DocContextuse outside ofclean. 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 thecleancode — but that requires aDocContextwhich is not otherwise available in the JSON component.DocContextwhen needed — which may be often, and may not be a trivial cost. I'm not sure!cleaninto the JSON component, adjusting it to not needDocContext. This isn't great either.DocContextinsideJsonRenderersomehow. This isn't great either, becauseJsonRendereris passed around by immutable reference, whilecleanuses&mut DocContext. We may need eitherRefCellorMutexor lots of plumbing to replace&JsonRendererwith&mut JsonRenderer.Included tests
Fn/FnMut/FnOnceandAsyncFn/AsyncFnMut/AsyncFnOnceimplied bounds preserve full parenthesized signatures.Sizedbounds as a result of Rust language rules:Sizeddefault when?Sizedis not present, such as in generic parameters, associated types, etc.Sizedrequirement for function parameters, return values, and contents of slices and arrays.Sizedrequirement when a possibly-DST struct or tuple (e.g.struct S<T: ?Sized>(T);) is used in a position that requires it beSized, like a function parameter or return value.Sizedif such a possibly-DST type is used behind indirection, like&/&mut/*const/*mut.bounds(explicit, syntactically-included bounds) orimplied_bounds(implied or elaborated bounds), never both.r? fmease
cc @aDotInTheVoid