# Notes on issue 57374 Github issue [#57374](https://github.com/rust-lang/rust/issues/57374). Since the Return of the Leak Check™, the flag `-Zno-leak-check` needs to be passed so that the regular Universes code runs, causing this issue. ```rust #![feature(nll)] fn main() { let x: fn(&'static ()) = |_| {}; let y: for<'a> fn(&'a ()) = x; } ``` ```rust error: higher-ranked subtype error --> src/main.rs:18:33 | 18 | let x: fn(&'static ()) = |_| {}; | ^^^^^^^^^^^^^^^ error: aborting due to previous error ``` The following points are ordered in the order I've chronologically looked at them during my investigations, not by importance. #### 1. The span points at the type of `x`. The error is the invalid assignment of `x` to `y` but the chosen span does not show that. The piece of code responsible for finding the span here is: `find_outlives_blame_span` from the [error_reporting module](https://github.com/rust-lang/rust/blob/f47ec2ad5b6887b3d400aee49e2294bd27733d18/src/librustc_mir/borrow_check/nll/region_infer/error_reporting/mod.rs#L699), but it delegates most of that work to `best_blame_constraint` [here](https://github.com/rust-lang/rust/blob/f47ec2ad5b6887b3d400aee49e2294bd27733d18/src/librustc_mir/borrow_check/nll/region_infer/error_reporting/mod.rs#L62-L146). We're looking for the best span to blame for the fact that `'_#20r: '_#0r`. `'_#20r` is the bound universal region we're checking: a variable whose origin is a Placeholder in U7. We start with the constraint path between the two regions, via `find_constraint_paths_between_regions`. Here, we have this path: ```shell [ "(\'_#20r: \'_#4r) due to Single(bb0[8]) - category: Assignment", "(\'_#4r: \'_#6r) due to All(...) - category: TypeAnnotation", "(\'_#6r: \'_#0r) due to All(...) - category: BoringNoLocation" ] ``` - `bb0[8]` is the assignment `_3 = _1`. - the type of `_3` is `for<'a> fn(&ReLateBound(DebruijnIndex(0), BrNamed(crate0:DefIndex(1:10), 'a)) ()) // "y"` - the type of `_1` is `fn(&'_#4r ()) // "x"` More info about the other variables: - `'_#4r` has an `Existential` NLLRegionVariableOrigin - `'_#6r` has an `Existential` NLLRegionVariableOrigin We then walk this chain of constraints backwards, choosing the "best span to cite", using a few factors. Here, the constraint category is the most important one: the `BoringNoLocation` is ignored, and we have a `TypeAnnotation` "before" the `Assignment` so this span is considered the best. (As this category is found first, the SCC of the later constraints doesn't matter) #### 2. The span can be incorrect in other similar cases. In similar looking code, the structure of the constraint path would be the same, with the late `TypeAnnotation` causing to show the wrong span. For example, as an argument in a function call, like so: ```rust #![feature(nll)] fn main() { let x: fn(&'static ()) = |_| {}; f(x); } fn f(y: for<'a> fn(&'a ())) { } ``` Resulting in the same span as before: ```rust error: higher-ranked subtype error --> src/main.rs:18:33 | 18 | let x: fn(&'static ()) = |_| {}; | ^^^^^^^^^^^^^^^ error: aborting due to previous error ``` Here, we have this path: ```shell [ "(\'_#9r: \'_#6r) due to Single(bb0[9]) - category: CallArgument", "(\'_#6r: \'_#4r) due to Single(bb0[8]) - category: Boring", "(\'_#4r: \'_#7r) due to All(...) - category: TypeAnnotation", "(\'_#7r: \'_#0r) due to All(...) - category: BoringNoLocation" ] ``` It's possible similar looking code causing different categories of constraints would be similarly showing the wrong span: - when "very interesting" constraint categories are late in the chain: like the `TypeAnnotation` in these examples, but the same applies to `Return` and `Yield`. - when other "less interesting" constraint categories are early in the chain before the "very interesting" ones (`Cast`, `ClosureBounds`, etc.) It may be interesting to try to find other examples where these less interesting categories other than `Assignment` and `CallArgument` show up early like here ? #### 3. The higher-ranked subtype error itself With the leak check, or AST borrowck, the error is pointing at the assignment: ```rust error[E0308]: mismatched types --> src/main.rs:39:33 | 39 | let y: for<'a> fn(&'a ()) = x; | ^ expected concrete lifetime, found bound lifetime parameter 'a | = note: expected type `for<'a> fn(&'a ())` found type `fn(&'static ())` ``` AST borrowck finds this type error as a `RegionsPlaceholderMismatch` (ultimately, "one type is more general than the other"), while the leak-check as a `RegionsOverlyPolymorphic` (ultimately, "expected concrete lifetime, found bound lifetime parameter 'a"). With NLLs, MIR borrowck will call `nll::compute_regions`, which will want to solve the region constraints. `RegionInferenceContext::solve()` will check the universal regions (`check_universal_regions`), which will check the bound universal regions (`check_bound_universal_region`) for the placeholders. Checking `'_#20r` here will first find the error element `Location(bb0[0])` (which seems to be `StorageLive(_1)` ?), as the first `Location` element contained in the SCC. This seems interesting, all the SCC elements wouldn't have the same "error region" -- but likely the same starting and ending points in the constraint paths, all leading to the erroneous constraint ? This would make sense that we take the first SCC element here, if all paths lead to Rome. In any case, here are SCC 18's 17 elements: ```shell [ Location(bb0[0]), Location(bb0[1]), Location(bb0[2]), Location(bb0[3]), Location(bb0[4]), Location(bb0[5]), Location(bb0[6]), Location(bb0[7]), Location(bb0[8]), Location(bb0[9]), Location(bb0[10]), Location(bb0[11]), Location(bb0[12]), Location(bb0[13]), Location(bb0[14]), RootUniversalRegion('_#0r), RootUniversalRegion('_#1r) ] ``` The 17 "error regions" for these elements: ```shell [ '_#0r, '_#0r, '_#0r, '_#3r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#0r, '_#1r ] ``` `'_#3r` looks different so let's trace this instead, similarly to how we traced `'_#0r` earlier. `'_#3r` is coming from `Location(bb0[3])` — that is, the statement `_1 = move _2 as fn(&'_#3r ()) (ClosureFnPointer);` which involves `_1` a part of our erroneous assignment `_3 = _1;`. Like `'_#4r` and `'_#6r`, it has an `Existential` NLLRegionVariableOrigin. The span found by `find_outlives_blame_span` for region `'_#3r`, `bb0[3]`, is a bit different this time, but still wrong: ```rust let x: fn(&'static ()) = |_| {}; ^^^^^^ ``` The constraint path between `'_#20r` and `'_#3r` is: ```shell [ "(\'_#20r: \'_#4r) due to Single(bb0[8]) - category: Assignment", "(\'_#4r: \'_#3r) due to Single(bb0[3]) - category: Assignment" ] ``` and `bb0[8]`, as before, would be the one we want. Once again, we have `bb0[8]` at the beginning of the constraint path, just like the constraint path explored in part 1. These 2 different paths lead to what appears to be the erroneous assigment. In this case, there are only `Assignment`s in the constraint path, and the reason this span was chosen was because of [this test](https://github.com/rust-lang/rust/blob/a21d82438b72a0394a670b6bfe95e3e62ee08802/src/librustc_mir/borrow_check/nll/region_infer/error_reporting/mod.rs#L131): `'_#3r` is in SCC 1 and not 18. (Anecdote, but this is the error region with the shortest constraint path between the free region and the error region, maybe that's an interesting property ? Here: - `'_#20r` -> `'_#0r`: length 3 - `'_#20r` -> `'_#3r`: length 2 - `'_#20r` -> `'_#1r`: length 4) So, some questions I have left, assuming the previous analysis is somewhat on the right track: - What is [this](https://github.com/rust-lang/rust/blob/a21d82438b72a0394a670b6bfe95e3e62ee08802/src/librustc_mir/borrow_check/nll/region_infer/mod.rs#L1294-L1300) trying to find ? Is it picking the error element and error region we expect here (if all these paths would lead to the expected error here, the answer would be yes) ? Do we still want to use `find_outlives_blame_span` ? If so, how do we want it to work to return the beginning of the path here, when it was tailored to work backwards from the end of path ? Do higher-ranked errors require forwards traversal ? - What is the error we want to emit for higher-rank subtype errors ? How to find the data it would need (without extracting it from the MIR itself in this function) ? The `nice_region_error`s could be probably be used but I don't know where to find the information they need ( for example, the "Expected/Found" errors). Is it still stored at this point in the compilation pipeline, and if so, where ? ##### Note: The similar following example shows that as long as a lifetime is specified in `x`'s type', the example code will fail (except, say, `'_` which should be in this context similar to the forall we need here -- maybe surprising that it can mean both existential and universal depending on the context but it makes sense). It might be interesting for a rustc regression test ? ```rust #![feature(nll)] fn f<'a>() { let x: fn(&'a ()) = |_| {}; let y: for<'b> fn(&'b ()) = x; } fn main() {} ``` --- #### 4. Survey of tests with higher-ranked subtype errors To have a better understanding on how `check_bound_universal_region` currently behaves, here's a summary of what rustc's existing tests errors look like. There are 3 such errors in the tests. ##### 4.1. `universe_violation.rs` Very interesting because [it looks similar](https://github.com/rust-lang/rust/blob/906deae0790bd18681b937fe9a141a3c26cf1855/src/test/ui/nll/relate_tys/universe-violation.rs) to the code in the issue. Minimized and made to look even more similar: ```rust fn make_it() -> fn(&'static ()) { panic!() } fn main() { let a: fn(&'static ()) = make_it(); let b: fn(&()) = a; } ``` ```rust error: higher-ranked subtype error --> src/main.rs:40:22 | 40 | let b: fn(&()) = a; ^ ``` Interestingly, the span is correct, even with a constraint path is very similar to path explored in part 3, without `TypeAnnotation`s: ```shell [ "(\'_#17r: \'_#2r) due to Single(bb2[3]) - category: Assignment", "(\'_#2r: \'_#0r) due to Single(bb0[1]) - category: Assignment" ] ``` (Note: no constraint path from `'_#17r` to the error regions from the 15 elements in SCC 17 contain a `TypeAnnotation`) The correct span is displayed here, because of the "unusual" case [here](https://github.com/rust-lang/rust/blob/a21d82438b72a0394a670b6bfe95e3e62ee08802/src/librustc_mir/borrow_check/nll/region_infer/error_reporting/mod.rs#L138-L145): the search failed and everything is indeed in the same SCC 17 (as mentioned before, this wasn't the case for the code in the issue). ##### 4.2. `relate_tys/hr-fn-aaa-as-aba.rs foo()` Exploring this doesn't matter much: there's only one span in the running here, all the constraints for this error are coming from a single MIR statement. ```rust #![feature(nll)] fn make_it() -> for<'a> fn(&'a u32, &'a u32) -> &'a u32 { panic!() } fn foo() { let a: for<'a, 'b> fn(&'a u32, &'b u32) -> &'a u32 = make_it(); } fn main() { } ``` ```rust error: higher-ranked subtype error --> src/main.rs:39:58 | 39 | let a: for<'a, 'b> fn(&'a u32, &'b u32) -> &'a u32 = make_it(); | ^^^^^^^^^ ``` Again, no `TypeAnnotation` in the constraint path: ```shell [ "(\'_#27r: \'_#28r) due to Single(bb0[1]) - category: Assignment", "(\'_#28r: \'_#26r) due to Single(bb0[1]) - category: Assignment" ] ``` `'_#27r` is in SCC 20, and `'_#26r` in SCC 19. ##### 4.3. `relate_tys/hr-fn-aaa-as-aba.rs bar()` This is turning the previous `foo()` test code in a pattern, going through a slightly different code path. ```rust #![feature(nll)] fn make_it() -> for<'a> fn(&'a u32, &'a u32) -> &'a u32 { panic!() } fn bar() { let _: for<'a, 'b> fn(&'a u32, &'b u32) -> &'a u32 = make_it(); } fn main() { } ``` ```rust error: higher-ranked subtype error --> src/main.rs:39:12 | 39 | let _: for<'a, 'b> fn(&'a u32, &'b u32) -> &'a u32 = make_it(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` This behaves a bit like the code in the issue, pointing to the type, I wonder if that's intended ? In any case there's only one span in the running, like `foo()`, but this time it's chosen reluctantly: ```shell [ "(\'_#14r: \'_#15r) due to All(...) - category: BoringNoLocation", "(\'_#15r: \'_#13r) due to All(...) - category: BoringNoLocation" ] ``` The first constraint is picked because of the "unusual" case [again](https://github.com/rust-lang/rust/blob/a21d82438b72a0394a670b6bfe95e3e62ee08802/src/librustc_mir/borrow_check/nll/region_infer/error_reporting/mod.rs#L138-L145) but for different reasons: the search failed because no `ConstraintCategory` was of interest, all the `BoringNoLocation`s are ignored (not that it would matter much here, but they were in different SCCs -- 10 and 9 respectively -- while the linked comment mostly references the unusual case for a single SCC).