rustdoc JSON: Add implied bounds to generic types, impl Trait, and assoc types.#148379
rustdoc JSON: Add implied bounds to generic types, impl Trait, and assoc types.#148379obi1kenobi wants to merge 1 commit intorust-lang:mainfrom
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.
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.
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.
|
Finished benchmarking commit (0e2f486): comparison URL. Overall result: ❌ regressions - 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 1.1%, secondary -1.2%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (primary -3.4%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeThis benchmark run did not return any relevant results for this metric. Bootstrap: 473.061s -> 470.796s (-0.48%) |
|
Thanks for taking a look @fmease! As you likely remember, this is my first PR that reaches this deep into rustc internals concerned with type-checking and such, so I appreciate the pointers to other places where I should look to reuse code from. For example, I missed the visitors, even though in retrospect it seems obvious such a system had to exist 😅 I'll do a cleanup pass aiming to fix up the issues you've flagged so far, and hopefully that'll make the PR as a whole easier to review! |
|
@rustbot label -S-waiting-on-review +S-waiting-on-author |
There was a problem hiding this comment.
From #148379 (comment):
trait MaybeUnsized<T: ?Sized> { // T can be unsized here fn by_ref(_: &T); // implied `where T: Sized` here; // we want to show that as an implied bound fn by_value(_: T); }
Now I understand why you're recursing into HIR/middle types to find out whether they "require Sized".
This is kinda funny, I never thought of this as an "implied bound", and neither does rustc to be fair. What's actually going on in rustc / Rust is that we require all locals (incl. fn params) to be Sized (w/ exceptions like let _: [u8]; which is legal). However, in trait fns without body, rustc doesn't check that at the def-site, maybe call it an oversight.
My immediate reaction to recursing into Ty to find out whether it "requires Sized" in a trait fn w/o body, is let's not. This is a lot of complexity that possibly has false positives, needs maintenance and doesn't have much gain (I'll clarify later).
To be clear, I guess I'm fine with calling the lack of Sized checking in such cases "implied bounds". However, there's a much simpler way to express this: Just add $ty: Sized for all params of type $ty in trait fns w/o body. This is 100% correct: So for by_ref in the example above, add &T: Sized (trivially true but that's fine); for by_value, add T: Sized (if the arg ty is Box<T>, it's Box<T>: Sized, if it's str it's str: Sized or for<'_delay> str: Sized, etc.).
doesn't have much gain
I don't know if you really want these bounds to be "normalized" / as "simple" as possible (so [`&T: Sized`] → [], [`Adt<T>: Sized`] → [`T: Sized`] (depends on the def of Adt)) for C-S-C. I don't think that's valuable since C-S-C needs to (eventually?) know anyway that changing e.g. pub fn f<T: ?Sized>(_: &T) where &T: Sized {} to pub fn f<T: ?Sized>(_: &T) {} is not a breaking change.
Edit: If you only care about paramater types if they reference type parameters, you don't actually need to add $ParamTy: Sized for all param types $ParamTy. See #148379 (comment). I think you only need to do so for bare type parameters T since well-formedness checking takes care of the other cases.
There was a problem hiding this comment.
Thanks for the super detailed and very prompt feedback, I really appreciate it! Also thanks for bearing with me being a n00b :)
My plan is to (1) follow your "let's not" advice and (2) try to descope this PR so it's easier on both of us. Injecting non-normalized bounds like &T: Sized would be great, but they aren't crucial to have right now for c-s-c. We can easily start without them, and I think we should.
I have some travel coming up, so I'll push as many fixes as I can before I fly out but I might not manage to get everything. I'm happy to get more partial reviews if you have time, and otherwise I'll just pick up where I left off after I get home.
| ty: Ty<'tcx>, | ||
| target_param: ParamTy, | ||
| allow_unsized: bool, | ||
| ) -> bool { |
There was a problem hiding this comment.
Irrespective of the fact that I think this whole fn probably shouldn't exist, I have some questions re. its impl
(minor) For my understanding, the name of param_requires_sized_in_ty is slightly misleading, isn't it? It's more like param_implicitly_requires_sized_in_ty as it decides whether we should add an "implied" T: Sized?
I get why T → true and &T → false (1: T: Sized only if T: Sized, 2: &T: Sized trivially true). However, let's look at other type constructors.
To repeat myself, while it's true that Rust doesn't require the fn params of trait fn w/o body to be Sized, it still requires all types to be well-formed (ignoring a bunch known bugs and some other tiny exceptions). E.g., slice and array ty ctors always require their type argument to be Sized, so [T] and [T; N] are illegal if T doesn't impl Sized, that's also true in trait fn w/o body, meaning compilation would stop early and never reach this code. As you know, for ADTs type arguments aren't necessarily required to be Sized, so Adt<T> (we're inside a trait Trait<T: ?Sized>) is either illegal or it isn't depending on the definition of Adt.
However, you're actually recursing for Slice and Array (walking their argument) and recursing for structs (visiting their fields). Unless I'm blindingly missing something, that shouldn't be necessary at all. Given [T], this fn will never return true since the type checker would've already rejected this type if T needed Sized (it indeed isn't "implied"). Given Struct<T>, same deal. It should never return true.
(the same should apply to opaque tys btw)
tests/rustdoc-json/implied-bounds/bounded-generic-type-alias-impl-trait.rs
Show resolved
Hide resolved
1479c8e to
e4e998c
Compare
There was a problem hiding this comment.
Cleaned up a bunch based on preliminary feedback from fmease, including:
- all test cases
cargo checkindependently, and have constrained generics - no more HIR walking to compute extra "implied" bounds on our own
- reusing more
cleancode
This cut quite a bit of code and complexity as a result.
Time permitting, I'd love another review pass! I'll be travelling for a little while and I look forward to coming back to this as soon as I'm back.
@rustbot label -S-waiting-on-author +S-waiting-on-review
This comment has been minimized.
This comment has been minimized.
e4e998c to
95ae8cd
Compare
This comment has been minimized.
This comment has been minimized.
|
I'm back from my trip and ready to pick this back up, pending reviewer availability ofc 👍 |
This comment has been minimized.
This comment has been minimized.
95ae8cd to
36749ca
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
I try to get to this before the end of the week & I'll try my hardest to get this PR to ship sooner than later 🤞 Don't hesitate to ping me. |
|
Thank you, I appreciate it! I'll try to keep it free of merge conflicts in the meantime. |
36749ca to
825939e
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
View all comments
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