From 0b4fdd39aed50fcd8fa1faf17fd3c5e260979a35 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 20:04:13 +0100 Subject: [PATCH 01/20] reflow src/tests/directives.md --- src/tests/directives.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/tests/directives.md b/src/tests/directives.md index ed6c8b9f93..ab878b88ea 100644 --- a/src/tests/directives.md +++ b/src/tests/directives.md @@ -274,9 +274,11 @@ For example: - A test with the `//@ edition: 2015..2021` directive can be run under the 2015 and the 2018 edition, so the upper bound is exclusive just like in Rust (note that there's no equivalent to Rust's `..=` where the upper bound is inclusive). - However, CI will only run the test with the lowest edition in the range (which is 2015 in this example). + However, + CI will only run the test with the lowest edition in the range (which is 2015 in this example). - A test with the `//@ edition: 2018..` directive will run under 2018 edition or greater. - However, CI will only run the test with the lowest edition in the range (which is 2018 in this example). + However, + CI will only run the test with the lowest edition in the range (which is 2018 in this example). You can also force `./x test` to use a specific edition by passing the `-- --edition=` argument. However, tests with the `//@ edition` directive will clamp the value passed to the argument. @@ -300,7 +302,8 @@ Asked in #### Test-suite-specific directives -The test suites [`rustdoc-html`][rustdoc-html-tests], [`rustdoc-js`/`rustdoc-js-std`][rustdoc-js-tests] +The test suites [`rustdoc-html`][rustdoc-html-tests], +[`rustdoc-js`/`rustdoc-js-std`][rustdoc-js-tests] and [`rustdoc-json`][rustdoc-json-tests] each feature an additional set of directives whose basic syntax resembles the one of compiletest directives but which are ultimately read and checked by separate tools. From 3a7c2de741f921194621fdfa52811257309f499d Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 20:06:29 +0100 Subject: [PATCH 02/20] improve tests/directives.md --- src/tests/directives.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/directives.md b/src/tests/directives.md index ab878b88ea..7d8fc182ba 100644 --- a/src/tests/directives.md +++ b/src/tests/directives.md @@ -303,10 +303,10 @@ Asked in #### Test-suite-specific directives The test suites [`rustdoc-html`][rustdoc-html-tests], -[`rustdoc-js`/`rustdoc-js-std`][rustdoc-js-tests] +[`rustdoc-js`/`rustdoc-js-std`][rustdoc-js-tests], and [`rustdoc-json`][rustdoc-json-tests] each feature an additional set of directives whose basic -syntax resembles the one of compiletest directives but which are ultimately read and checked by -separate tools. +syntax resembles one of the compiletest directives, +but which are ultimately read and checked by separate tools. For more information, please read their respective chapters as linked above. [rustdoc-html-tests]: ../rustdoc-internals/rustdoc-html-test-suite.md From 01f3215334a9a4458dfb6fdd2993d04d6587dc0b Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 20:26:28 +0100 Subject: [PATCH 03/20] that made me think files should have this name --- src/rustdoc-internals/rustdoc-html-test-suite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rustdoc-internals/rustdoc-html-test-suite.md b/src/rustdoc-internals/rustdoc-html-test-suite.md index a88e60c183..76105c6c9e 100644 --- a/src/rustdoc-internals/rustdoc-html-test-suite.md +++ b/src/rustdoc-internals/rustdoc-html-test-suite.md @@ -3,7 +3,7 @@ This page is about the test suite named `rustdoc-html` used to test the HTML output of `rustdoc`. For other rustdoc-specific test suites, see [Rustdoc test suites]. -Each test file in this test suite is simply a Rust source file `file.rs` sprinkled with +Each test file in this test suite is simply a Rust source file sprinkled with so-called *directives* located inside normal Rust code comments. These come in two flavors: *Compiletest* and *HtmlDocCk*. From b3ed5cefafd19b7904abd8b68406b4fee8013527 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 20:27:01 +0100 Subject: [PATCH 04/20] reflow src/rustdoc-internals/rustdoc-html-test-suite.md --- src/rustdoc-internals/rustdoc-html-test-suite.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rustdoc-internals/rustdoc-html-test-suite.md b/src/rustdoc-internals/rustdoc-html-test-suite.md index 76105c6c9e..13b618e7d4 100644 --- a/src/rustdoc-internals/rustdoc-html-test-suite.md +++ b/src/rustdoc-internals/rustdoc-html-test-suite.md @@ -145,8 +145,8 @@ Checks that the directory given by `PATH` contains exactly `ENTRIES`. ## Compiletest Directives (Brief) As mentioned in the introduction, you also have access to [compiletest directives]. -Most importantly, they allow you to register auxiliary crates and -to pass flags to the `rustdoc` binary under test. +Most importantly, +they allow you to register auxiliary crates and to pass flags to the `rustdoc` binary under test. It's *strongly recommended* to read that chapter if you don't know anything about them yet. Here are some details that are relevant to this test suite specifically: @@ -177,9 +177,9 @@ the original negative check failing. **Note**: Please avoid the use of *negated* checks! -**Tip**: If you can't avoid it, please **always** pair it with an analogous positive check in the -immediate vicinity, so people changing "the shape" have a chance to notice and to update the -negated check! +**Tip**: If you can't avoid it, +please **always** pair it with an analogous positive check in the immediate vicinity, +so people changing "the shape" have a chance to notice and to update the negated check! ## Limitations From cb8fe789349fa378142511e1123345ecee635bef Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 21:04:41 +0100 Subject: [PATCH 05/20] sembr src/traits/canonicalization.md --- src/traits/canonicalization.md | 159 +++++++++++++++++---------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/src/traits/canonicalization.md b/src/traits/canonicalization.md index 4bd56a020f..97d138ec43 100644 --- a/src/traits/canonicalization.md +++ b/src/traits/canonicalization.md @@ -4,52 +4,52 @@ > [Next-gen trait solving Canonicalization chapter](../solve/canonicalization.md). > It is suggested to reorganize these contents in the future. -Canonicalization is the process of **isolating** an inference value -from its context. It is a key part of implementing -[canonical queries][cq], and you may wish to read the parent chapter +Canonicalization is the process of **isolating** an inference value from its context. +It is a key part of implementing [canonical queries][cq], +and you may wish to read the parent chapter to get more context. Canonicalization is really based on a very simple concept: every [inference variable](../type-inference.md#vars) is always in one of -two states: either it is **unbound**, in which case we don't know yet -what type it is, or it is **bound**, in which case we do. So to -isolate some data-structure T that contains types/regions from its +two states: either it is **unbound**, in which case we don't know yet what type it is, +or it is **bound**, in which case we do. +So to isolate some data-structure T that contains types/regions from its environment, we just walk down and find the unbound variables that appear in T; those variables get replaced with "canonical variables", -starting from zero and numbered in a fixed order (left to right, for -the most part, but really it doesn't matter as long as it is +starting from zero and numbered in a fixed order (left to right, for the most part, +but really it doesn't matter as long as it is consistent). [cq]: ./canonical-queries.md -So, for example, if we have the type `X = (?T, ?U)`, where `?T` and -`?U` are distinct, unbound inference variables, then the canonical -form of `X` would be `(?0, ?1)`, where `?0` and `?1` represent these -**canonical placeholders**. Note that the type `Y = (?U, ?T)` also -canonicalizes to `(?0, ?1)`. But the type `Z = (?T, ?T)` would -canonicalize to `(?0, ?0)` (as would `(?U, ?U)`). In other words, the -exact identity of the inference variables is not important – unless +So, for example, if we have the type `X = (?T, ?U)`, where `?T` and `?U` are distinct, +unbound inference variables, then the canonical +form of `X` would be `(?0, ?1)`, where `?0` and `?1` represent these **canonical placeholders**. +Note that the type `Y = (?U, ?T)` also canonicalizes to `(?0, ?1)`. +But the type `Z = (?T, ?T)` would canonicalize to `(?0, ?0)` (as would `(?U, ?U)`). +In other words, the exact identity of the inference variables is not important – unless they are repeated. -We use this to improve caching as well as to detect cycles and other -things during trait resolution. Roughly speaking, the idea is that if -two trait queries have the same canonical form, then they will get -the same answer. That answer will be expressed in terms of the -canonical variables (`?0`, `?1`), which we can then map back to the +We use this to improve caching as well as to detect cycles and other things during trait resolution. +Roughly speaking, the idea is that if two trait queries have the same canonical form, +then they will get +the same answer. +That answer will be expressed in terms of the canonical variables (`?0`, +`?1`), which we can then map back to the original variables (`?T`, `?U`). ## Canonicalizing the query To see how it works, imagine that we are asking to solve the following trait query: `?A: Foo<'static, ?B>`, where `?A` and `?B` are unbound. -This query contains two unbound variables, but it also contains the -lifetime `'static`. The trait system generally ignores all lifetimes -and treats them equally, so when canonicalizing, we will *also* +This query contains two unbound variables, but it also contains the lifetime `'static`. +The trait system generally ignores all lifetimes and treats them equally, +so when canonicalizing, we will *also* replace any [free lifetime](../appendix/background.md#free-vs-bound) with a -canonical variable (Note that `'static` is actually a _free_ lifetime -variable here. We are not considering it in the typing context of the whole -program but only in the context of this trait reference. Mathematically, we -are not quantifying over the whole program, but only this obligation). +canonical variable (Note that `'static` is actually a _free_ lifetime variable here. +We are not considering it in the typing context of the whole +program but only in the context of this trait reference. +Mathematically, we are not quantifying over the whole program, but only this obligation). Therefore, we get the following result: ```text @@ -62,12 +62,11 @@ Sometimes we write this differently, like so: for { ?0: Foo<'?1, ?2> } ``` -This `for<>` gives some information about each of the canonical -variables within. In this case, each `T` indicates a type variable, -so `?0` and `?2` are types; the `L` indicates a lifetime variable, so -`?1` is a lifetime. The `canonicalize` method *also* gives back a -`CanonicalVarValues` array OV with the "original values" for each -canonicalized variable: +This `for<>` gives some information about each of the canonical variables within. + In this case, each `T` indicates a type variable, +so `?0` and `?2` are types; the `L` indicates a lifetime variable, so `?1` is a lifetime. +The `canonicalize` method *also* gives back a +`CanonicalVarValues` array OV with the "original values" for each canonicalized variable: ```text [?A, 'static, ?B] @@ -79,8 +78,8 @@ We'll need this vector OV later, when we process the query response. Once we've constructed the canonical query, we can try to solve it. To do so, we will wind up creating a fresh inference context and -**instantiating** the canonical query in that context. The idea is that -we create a substitution S from the canonical form containing a fresh +**instantiating** the canonical query in that context. +The idea is that we create a substitution S from the canonical form containing a fresh inference variable (of suitable kind) for each canonical variable. So, for our example query: @@ -94,22 +93,24 @@ the substitution S might be: S = [?A, '?B, ?C] ``` -We can then replace the bound canonical variables (`?0`, etc) with -these inference variables, yielding the following fully instantiated +We can then replace the bound canonical variables (`?0`, etc) with these inference variables, +yielding the following fully instantiated query: ```text ?A: Foo<'?B, ?C> ``` -Remember that substitution S though! We're going to need it later. +Remember that substitution S though! +We're going to need it later. -OK, now that we have a fresh inference context and an instantiated -query, we can go ahead and try to solve it. The trait solver itself is -explained in more detail in [another section](../solve/the-solver.md), but +OK, now that we have a fresh inference context and an instantiated query, +we can go ahead and try to solve it. +The trait solver itself is explained in more detail in [another section](../solve/the-solver.md), +but suffice to say that it will compute a [certainty value][cqqr] (`Proven` or -`Ambiguous`) and have side-effects on the inference variables we've -created. For example, if there were only one impl of `Foo`, like so: +`Ambiguous`) and have side-effects on the inference variables we've created. +For example, if there were only one impl of `Foo`, like so: [cqqr]: ./canonical-queries.md#query-response @@ -119,29 +120,29 @@ where X: 'a { ... } ``` -then we might wind up with a certainty value of `Proven`, as well as -creating fresh inference variables `'?D` and `?E` (to represent the +then we might wind up with a certainty value of `Proven`, +as well as creating fresh inference variables `'?D` and `?E` (to represent the parameters on the impl) and unifying as follows: - `'?B = '?D` - `?A = Vec` - `?C = ?E` -We would also accumulate the region constraint `?E: '?D`, due to the -where clause. +We would also accumulate the region constraint `?E: '?D`, due to the where clause. -In order to create our final query result, we have to "lift" these -values out of the query's inference context and into something that -can be reapplied in our original inference context. We do that by -**re-applying canonicalization**, but to the **query result**. +In order to create our final query result, +we have to "lift" these values out of the query's inference context and into something that +can be reapplied in our original inference context. +We do that by **re-applying canonicalization**, but to the **query result**. ## Canonicalizing the query result As discussed in [the parent section][cqqr], most trait queries wind up with a result that brings together a "certainty value" `certainty`, a -result substitution `var_values`, and some region constraints. To -create this, we wind up re-using the substitution S that we created -when first instantiating our query. To refresh your memory, we had a query +result substitution `var_values`, and some region constraints. +To create this, we wind up re-using the substitution S that we created +when first instantiating our query. +To refresh your memory, we had a query ```text for { ?0: Foo<'?1, ?2> } @@ -160,10 +161,10 @@ If we "refresh" S with the latest results, we get: S = [Vec, '?D, ?E] ``` -These are precisely the new values for the three input variables from -our original query. Note though that they include some new variables -(like `?E`). We can make those go away by canonicalizing again! We don't -just canonicalize S, though, we canonicalize the whole query response QR: +These are precisely the new values for the three input variables from our original query. +Note though that they include some new variables (like `?E`). +We can make those go away by canonicalizing again! +We don't just canonicalize S, though, we canonicalize the whole query response QR: ```text QR = { @@ -187,20 +188,20 @@ Canonical(QR) = for { } ``` -(One subtle point: when we canonicalize the query **result**, we do not -use any special treatment for free lifetimes. Note that both -references to `'?D`, for example, were converted into the same -canonical variable (`?1`). This is in contrast to the original query, -where we canonicalized every free lifetime into a fresh canonical -variable.) +(One subtle point: when we canonicalize the query **result**, +we do not use any special treatment for free lifetimes. +Note that both references to `'?D`, for example, were converted into the same +canonical variable (`?1`). +This is in contrast to the original query, +where we canonicalized every free lifetime into a fresh canonical variable.) Now, this result must be reapplied in each context where needed. ## Processing the canonicalized query result -In the previous section we produced a canonical query result. We now have -to apply that result in our original context. If you recall, way back in the -beginning, we were trying to prove this query: +In the previous section we produced a canonical query result. +We now have to apply that result in our original context. +If you recall, way back in the beginning, we were trying to prove this query: ```text ?A: Foo<'static, ?B> @@ -223,11 +224,11 @@ for { } ``` -We now want to apply that response to our context. Conceptually, how -we do that is to (a) instantiate each of the canonical variables in +We now want to apply that response to our context. +Conceptually, how we do that is to (a) instantiate each of the canonical variables in the result with a fresh inference variable, (b) unify the values in -the result with the original values, and then (c) record the region -constraints for later. Doing step (a) would yield a result of +the result with the original values, and then (c) record the region constraints for later. +Doing step (a) would yield a result of ```text { @@ -247,13 +248,13 @@ Step (b) would then unify: ?B with ?C ``` -And finally the region constraint of `?C: 'static` would be recorded -for later verification. +And finally the region constraint of `?C: 'static` would be recorded for later verification. (What we *actually* do is a mildly optimized variant of that: Rather -than eagerly instantiating all of the canonical values in the result -with variables, we instead walk the vector of values, looking for -cases where the value is just a canonical variable. In our example, -`values[2]` is `?C`, so that means we can deduce that `?C := ?B` and -`'?D := 'static`. This gives us a partial set of values. Anything for -which we do not find a value, we create an inference variable.) +than eagerly instantiating all of the canonical values in the result with variables, +we instead walk the vector of values, looking for +cases where the value is just a canonical variable. +In our example, +`values[2]` is `?C`, so that means we can deduce that `?C := ?B` and `'?D := 'static`. +This gives us a partial set of values. +Anything for which we do not find a value, we create an inference variable.) From a1b2a5225d88b34d22f2efff60ee596a9af559aa Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 21:22:02 +0100 Subject: [PATCH 06/20] improve traits/canonicalization.md --- src/traits/canonicalization.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/traits/canonicalization.md b/src/traits/canonicalization.md index 97d138ec43..7564fbe907 100644 --- a/src/traits/canonicalization.md +++ b/src/traits/canonicalization.md @@ -13,7 +13,7 @@ Canonicalization is really based on a very simple concept: every [inference variable](../type-inference.md#vars) is always in one of two states: either it is **unbound**, in which case we don't know yet what type it is, or it is **bound**, in which case we do. -So to isolate some data-structure T that contains types/regions from its +So, to isolate some data-structure T that contains types/regions from its environment, we just walk down and find the unbound variables that appear in T; those variables get replaced with "canonical variables", starting from zero and numbered in a fixed order (left to right, for the most part, @@ -22,13 +22,13 @@ consistent). [cq]: ./canonical-queries.md -So, for example, if we have the type `X = (?T, ?U)`, where `?T` and `?U` are distinct, +So, for example, if we have the type `X = (?T, ?U)`, where `?T` and `?U` are distinct unbound inference variables, then the canonical form of `X` would be `(?0, ?1)`, where `?0` and `?1` represent these **canonical placeholders**. Note that the type `Y = (?U, ?T)` also canonicalizes to `(?0, ?1)`. But the type `Z = (?T, ?T)` would canonicalize to `(?0, ?0)` (as would `(?U, ?U)`). -In other words, the exact identity of the inference variables is not important – unless -they are repeated. +In other words, the exact identity of the inference variables is not important, +unless they are repeated. We use this to improve caching as well as to detect cycles and other things during trait resolution. Roughly speaking, the idea is that if two trait queries have the same canonical form, @@ -47,8 +47,8 @@ The trait system generally ignores all lifetimes and treats them equally, so when canonicalizing, we will *also* replace any [free lifetime](../appendix/background.md#free-vs-bound) with a canonical variable (Note that `'static` is actually a _free_ lifetime variable here. -We are not considering it in the typing context of the whole -program but only in the context of this trait reference. +We are not considering it in the typing context of the whole program, +but only in the context of this trait reference. Mathematically, we are not quantifying over the whole program, but only this obligation). Therefore, we get the following result: @@ -63,7 +63,7 @@ for { ?0: Foo<'?1, ?2> } ``` This `for<>` gives some information about each of the canonical variables within. - In this case, each `T` indicates a type variable, +In this case, each `T` indicates a type variable, so `?0` and `?2` are types; the `L` indicates a lifetime variable, so `?1` is a lifetime. The `canonicalize` method *also* gives back a `CanonicalVarValues` array OV with the "original values" for each canonicalized variable: @@ -142,13 +142,13 @@ with a result that brings together a "certainty value" `certainty`, a result substitution `var_values`, and some region constraints. To create this, we wind up re-using the substitution S that we created when first instantiating our query. -To refresh your memory, we had a query +To refresh your memory, we had a query: ```text for { ?0: Foo<'?1, ?2> } ``` -for which we made a substutition S: +We made a substutition S for it: ```text S = [?A, '?B, ?C] @@ -199,7 +199,7 @@ Now, this result must be reapplied in each context where needed. ## Processing the canonicalized query result -In the previous section we produced a canonical query result. +In the previous section, we produced a canonical query result. We now have to apply that result in our original context. If you recall, way back in the beginning, we were trying to prove this query: @@ -228,7 +228,7 @@ We now want to apply that response to our context. Conceptually, how we do that is to (a) instantiate each of the canonical variables in the result with a fresh inference variable, (b) unify the values in the result with the original values, and then (c) record the region constraints for later. -Doing step (a) would yield a result of +Doing step (a) would yield this result: ```text { @@ -248,7 +248,7 @@ Step (b) would then unify: ?B with ?C ``` -And finally the region constraint of `?C: 'static` would be recorded for later verification. +And finally, the region constraint of `?C: 'static` would be recorded for later verification. (What we *actually* do is a mildly optimized variant of that: Rather than eagerly instantiating all of the canonical values in the result with variables, From 423a48d2217a65bd35812b9ff70ecd21984ae9ff Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 21:23:09 +0100 Subject: [PATCH 07/20] reflow src/traits/canonicalization.md --- src/traits/canonicalization.md | 42 +++++++++++++++------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/traits/canonicalization.md b/src/traits/canonicalization.md index 7564fbe907..d87e3b3031 100644 --- a/src/traits/canonicalization.md +++ b/src/traits/canonicalization.md @@ -6,25 +6,23 @@ Canonicalization is the process of **isolating** an inference value from its context. It is a key part of implementing [canonical queries][cq], -and you may wish to read the parent chapter -to get more context. +and you may wish to read the parent chapter to get more context. Canonicalization is really based on a very simple concept: every [inference variable](../type-inference.md#vars) is always in one of two states: either it is **unbound**, in which case we don't know yet what type it is, or it is **bound**, in which case we do. -So, to isolate some data-structure T that contains types/regions from its -environment, we just walk down and find the unbound variables that +So, to isolate some data-structure T that contains types/regions from its environment, +we just walk down and find the unbound variables that appear in T; those variables get replaced with "canonical variables", starting from zero and numbered in a fixed order (left to right, for the most part, -but really it doesn't matter as long as it is -consistent). +but really it doesn't matter as long as it is consistent). [cq]: ./canonical-queries.md So, for example, if we have the type `X = (?T, ?U)`, where `?T` and `?U` are distinct -unbound inference variables, then the canonical -form of `X` would be `(?0, ?1)`, where `?0` and `?1` represent these **canonical placeholders**. +unbound inference variables, then the canonical form of `X` would be `(?0, +?1)`, where `?0` and `?1` represent these **canonical placeholders**. Note that the type `Y = (?U, ?T)` also canonicalizes to `(?0, ?1)`. But the type `Z = (?T, ?T)` would canonicalize to `(?0, ?0)` (as would `(?U, ?U)`). In other words, the exact identity of the inference variables is not important, @@ -32,11 +30,9 @@ unless they are repeated. We use this to improve caching as well as to detect cycles and other things during trait resolution. Roughly speaking, the idea is that if two trait queries have the same canonical form, -then they will get -the same answer. +then they will get the same answer. That answer will be expressed in terms of the canonical variables (`?0`, -`?1`), which we can then map back to the -original variables (`?T`, `?U`). +`?1`), which we can then map back to the original variables (`?T`, `?U`). ## Canonicalizing the query @@ -44,8 +40,8 @@ To see how it works, imagine that we are asking to solve the following trait query: `?A: Foo<'static, ?B>`, where `?A` and `?B` are unbound. This query contains two unbound variables, but it also contains the lifetime `'static`. The trait system generally ignores all lifetimes and treats them equally, -so when canonicalizing, we will *also* -replace any [free lifetime](../appendix/background.md#free-vs-bound) with a +so when canonicalizing, +we will *also* replace any [free lifetime](../appendix/background.md#free-vs-bound) with a canonical variable (Note that `'static` is actually a _free_ lifetime variable here. We are not considering it in the typing context of the whole program, but only in the context of this trait reference. @@ -94,8 +90,7 @@ S = [?A, '?B, ?C] ``` We can then replace the bound canonical variables (`?0`, etc) with these inference variables, -yielding the following fully instantiated -query: +yielding the following fully instantiated query: ```text ?A: Foo<'?B, ?C> @@ -107,8 +102,7 @@ We're going to need it later. OK, now that we have a fresh inference context and an instantiated query, we can go ahead and try to solve it. The trait solver itself is explained in more detail in [another section](../solve/the-solver.md), -but -suffice to say that it will compute a [certainty value][cqqr] (`Proven` or +but suffice to say that it will compute a [certainty value][cqqr] (`Proven` or `Ambiguous`) and have side-effects on the inference variables we've created. For example, if there were only one impl of `Foo`, like so: @@ -140,8 +134,8 @@ We do that by **re-applying canonicalization**, but to the **query result**. As discussed in [the parent section][cqqr], most trait queries wind up with a result that brings together a "certainty value" `certainty`, a result substitution `var_values`, and some region constraints. -To create this, we wind up re-using the substitution S that we created -when first instantiating our query. +To create this, +we wind up re-using the substitution S that we created when first instantiating our query. To refresh your memory, we had a query: ```text @@ -190,8 +184,8 @@ Canonical(QR) = for { (One subtle point: when we canonicalize the query **result**, we do not use any special treatment for free lifetimes. -Note that both references to `'?D`, for example, were converted into the same -canonical variable (`?1`). +Note that both references to `'?D`, for example, +were converted into the same canonical variable (`?1`). This is in contrast to the original query, where we canonicalized every free lifetime into a fresh canonical variable.) @@ -252,8 +246,8 @@ And finally, the region constraint of `?C: 'static` would be recorded for later (What we *actually* do is a mildly optimized variant of that: Rather than eagerly instantiating all of the canonical values in the result with variables, -we instead walk the vector of values, looking for -cases where the value is just a canonical variable. +we instead walk the vector of values, +looking for cases where the value is just a canonical variable. In our example, `values[2]` is `?C`, so that means we can deduce that `?C := ?B` and `'?D := 'static`. This gives us a partial set of values. From b455b5d70ac29fc47353796f51e0e6385562a65c Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 21:26:03 +0100 Subject: [PATCH 08/20] fix sembr tool fail --- src/traits/canonicalization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traits/canonicalization.md b/src/traits/canonicalization.md index d87e3b3031..d1cd1a2cef 100644 --- a/src/traits/canonicalization.md +++ b/src/traits/canonicalization.md @@ -21,8 +21,8 @@ but really it doesn't matter as long as it is consistent). [cq]: ./canonical-queries.md So, for example, if we have the type `X = (?T, ?U)`, where `?T` and `?U` are distinct -unbound inference variables, then the canonical form of `X` would be `(?0, -?1)`, where `?0` and `?1` represent these **canonical placeholders**. +unbound inference variables, then the canonical form of `X` would be `(?0, ?1)`, +where `?0` and `?1` represent these **canonical placeholders**. Note that the type `Y = (?U, ?T)` also canonicalizes to `(?0, ?1)`. But the type `Z = (?T, ?T)` would canonicalize to `(?0, ?0)` (as would `(?U, ?U)`). In other words, the exact identity of the inference variables is not important, From f28b457f7301ddea123f8221f11c0f804682dde9 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 21:31:49 +0100 Subject: [PATCH 09/20] sembr src/type-inference.md --- src/type-inference.md | 249 +++++++++++++++++++++--------------------- 1 file changed, 123 insertions(+), 126 deletions(-) diff --git a/src/type-inference.md b/src/type-inference.md index 196fcdd919..0a37dfbd99 100644 --- a/src/type-inference.md +++ b/src/type-inference.md @@ -1,7 +1,6 @@ # Type inference -Type inference is the process of automatic detection of the type of an -expression. +Type inference is the process of automatic detection of the type of an expression. It is what allows Rust to work with fewer or no type annotations, making things easier for users: @@ -13,55 +12,52 @@ fn main() { } ``` -Here, the type of `things` is *inferred* to be `Vec<&str>` because of the value -we push into `things`. +Here, +the type of `things` is *inferred* to be `Vec<&str>` because of the value we push into `things`. -The type inference is based on the standard Hindley-Milner (HM) type inference -algorithm, but extended in various ways to accommodate subtyping, region +The type inference is based on the standard Hindley-Milner (HM) type inference algorithm, +but extended in various ways to accommodate subtyping, region inference, and higher-ranked types. ## A note on terminology -We use the notation `?T` to refer to inference variables, also called -existential variables. +We use the notation `?T` to refer to inference variables, also called existential variables. -We use the terms "region" and "lifetime" interchangeably. Both refer to -the `'a` in `&'a T`. +We use the terms "region" and "lifetime" interchangeably. +Both refer to the `'a` in `&'a T`. -The term "bound region" refers to a region that is bound in a function -signature, such as the `'a` in `for<'a> fn(&'a u32)`. A region is -"free" if it is not bound. +The term "bound region" refers to a region that is bound in a function signature, +such as the `'a` in `for<'a> fn(&'a u32)`. +A region is "free" if it is not bound. ## Creating an inference context -You create an inference context by doing something like -the following: +You create an inference context by doing something like the following: ```rust,ignore let infcx = tcx.infer_ctxt().build(); // Use the inference context `infcx` here. ``` -`infcx` has the type `InferCtxt<'tcx>`, the same `'tcx` lifetime as on -the `tcx` it was built from. +`infcx` has the type `InferCtxt<'tcx>`, the same `'tcx` lifetime as on the `tcx` it was built from. -The `tcx.infer_ctxt` method actually returns a builder, which means -there are some kinds of configuration you can do before the `infcx` is -created. See `InferCtxtBuilder` for more information. +The `tcx.infer_ctxt` method actually returns a builder, +which means there are some kinds of configuration you can do before the `infcx` is +created. +See `InferCtxtBuilder` for more information. ## Inference variables The main purpose of the inference context is to house a bunch of -**inference variables** – these represent types or regions whose precise -value is not yet known, but will be uncovered as we perform type-checking. +**inference variables** – these represent types or regions whose precise value is not yet known, +but will be uncovered as we perform type-checking. -If you're familiar with the basic ideas of unification from H-M type -systems, or logic languages like Prolog, this is the same concept. If -you're not, you might want to read a tutorial on how H-M type -inference works, or perhaps this blog post on -[unification in the Chalk project]. +If you're familiar with the basic ideas of unification from H-M type systems, +or logic languages like Prolog, this is the same concept. +If you're not, you might want to read a tutorial on how H-M type +inference works, or perhaps this blog post on [unification in the Chalk project]. [Unification in the Chalk project]: http://smallcultfollowing.com/babysteps/blog/2017/03/25/unification-in-chalk-part-1/ @@ -69,30 +65,29 @@ All told, the inference context stores five kinds of inference variables (as of March 2023): - Type variables, which come in three varieties: - - General type variables (the most common). These can be unified with any - type. + - General type variables (the most common). + These can be unified with any type. - Integral type variables, which can only be unified with an integral type, and arise from an integer literal expression like `22`. - - Float type variables, which can only be unified with a float type, and - arise from a float literal expression like `22.0`. + - Float type variables, which can only be unified with a float type, + and arise from a float literal expression like `22.0`. - Region variables, which represent lifetimes, and arise all over the place. - Const variables, which represent constants. -All the type variables work in much the same way: you can create a new -type variable, and what you get is `Ty<'tcx>` representing an -unresolved type `?T`. Then later you can apply the various operations -that the inferencer supports, such as equality or subtyping, and it -will possibly **instantiate** (or **bind**) that `?T` to a specific -value as a result. +All the type variables work in much the same way: you can create a new type variable, +and what you get is `Ty<'tcx>` representing an +unresolved type `?T`. +Then later you can apply the various operations that the inferencer supports, +such as equality or subtyping, and it +will possibly **instantiate** (or **bind**) that `?T` to a specific value as a result. -The region variables work somewhat differently, and are described -below in a separate section. +The region variables work somewhat differently, and are described below in a separate section. ## Enforcing equality / subtyping -The most basic operations you can perform in the type inferencer is -**equality**, which forces two types `T` and `U` to be the same. The -recommended way to add an equality constraint is to use the `at` +The most basic operations you can perform in the type inferencer is **equality**, +which forces two types `T` and `U` to be the same. +The recommended way to add an equality constraint is to use the `at` method, roughly like so: ```rust,ignore @@ -100,71 +95,72 @@ infcx.at(...).eq(t, u); ``` The first `at()` call provides a bit of context, i.e. why you are -doing this unification, and in what environment, and the `eq` method -performs the actual equality constraint. +doing this unification, and in what environment, +and the `eq` method performs the actual equality constraint. -When you equate things, you force them to be precisely equal. Equating -returns an `InferResult` – if it returns `Err(err)`, then equating +When you equate things, you force them to be precisely equal. +Equating returns an `InferResult` – if it returns `Err(err)`, then equating failed, and the enclosing `TypeError` will tell you what went wrong. -The success case is perhaps more interesting. The "primary" return -type of `eq` is `()` – that is, when it succeeds, it doesn't return a -value of any particular interest. Rather, it is executed for its -side-effects of constraining type variables and so forth. However, the -actual return type is not `()`, but rather `InferOk<()>`. The -`InferOk` type is used to carry extra trait obligations – your job is -to ensure that these are fulfilled (typically by enrolling them in a -fulfillment context). See the [trait chapter] for more background on that. +The success case is perhaps more interesting. +The "primary" return type of `eq` is `()` – that is, when it succeeds, it doesn't return a +value of any particular interest. +Rather, it is executed for its side-effects of constraining type variables and so forth. +However, the actual return type is not `()`, but rather `InferOk<()>`. +The `InferOk` type is used to carry extra trait obligations – your job is +to ensure that these are fulfilled (typically by enrolling them in a fulfillment context). +See the [trait chapter] for more background on that. [trait chapter]: traits/resolution.md -You can similarly enforce subtyping through `infcx.at(..).sub(..)`. The same -basic concepts as above apply. +You can similarly enforce subtyping through `infcx.at(..).sub(..)`. +The same basic concepts as above apply. ## "Trying" equality -Sometimes you would like to know if it is *possible* to equate two -types without error. You can test that with `infcx.can_eq` (or -`infcx.can_sub` for subtyping). If this returns `Ok`, then equality -is possible – but in all cases, any side-effects are reversed. +Sometimes you would like to know if it is *possible* to equate two types without error. + You can test that with `infcx.can_eq` (or `infcx.can_sub` for subtyping). +If this returns `Ok`, then equality is possible – but in all cases, any side-effects are reversed. -Be aware, though, that the success or failure of these methods is always -**modulo regions**. That is, two types `&'a u32` and `&'b u32` will -return `Ok` for `can_eq`, even if `'a != 'b`. This falls out from the -"two-phase" nature of how we solve region constraints. +Be aware, though, that the success or failure of these methods is always **modulo regions**. +That is, two types `&'a u32` and `&'b u32` will return `Ok` for `can_eq`, even if `'a != 'b`. + This falls out from the "two-phase" nature of how we solve region constraints. ## Snapshots -As described in the previous section on `can_eq`, often it is useful -to be able to do a series of operations and then roll back their -side-effects. This is done for various reasons: one of them is to be -able to backtrack, trying out multiple possibilities before settling -on which path to take. Another is in order to ensure that a series of -smaller changes take place atomically or not at all. +As described in the previous section on `can_eq`, +often it is useful to be able to do a series of operations and then roll back their +side-effects. +This is done for various reasons: one of them is to be able to backtrack, +trying out multiple possibilities before settling +on which path to take. +Another is in order to ensure that a series of smaller changes take place atomically or not at all. To allow for this, the inference context supports a `snapshot` method. -When you call it, it will start recording changes that occur from the -operations you perform. When you are done, you can either invoke -`rollback_to`, which will undo those changes, or else `confirm`, which -will make them permanent. Snapshots can be nested as long as you follow -a stack-like discipline. - -Rather than use snapshots directly, it is often helpful to use the -methods like `commit_if_ok` or `probe` that encapsulate higher-level +When you call it, it will start recording changes that occur from the operations you perform. +When you are done, you can either invoke `rollback_to`, +which will undo those changes, or else `confirm`, which +will make them permanent. +Snapshots can be nested as long as you follow a stack-like discipline. + +Rather than use snapshots directly, +it is often helpful to use the methods like `commit_if_ok` or `probe` that encapsulate higher-level patterns. ## Subtyping obligations -One thing worth discussing is subtyping obligations. When you force -two types to be a subtype, like `?T <: i32`, we can often convert those -into equality constraints. This follows from Rust's rather limited notion -of subtyping: so, in the above case, `?T <: i32` is equivalent to `?T = i32`. +One thing worth discussing is subtyping obligations. +When you force two types to be a subtype, like `?T <: i32`, we can often convert those +into equality constraints. +This follows from Rust's rather limited notion of subtyping: so, +in the above case, `?T <: i32` is equivalent to `?T = i32`. -However, in some cases we have to be more careful. For example, when -regions are involved. So if you have `?T <: &'a i32`, what we would do -is to first "generalize" `&'a i32` into a type with a region variable: -`&'?b i32`, and then unify `?T` with that (`?T = &'?b i32`). We then -relate this new variable with the original bound: +However, in some cases we have to be more careful. +For example, when regions are involved. +So if you have `?T <: &'a i32`, +what we would do is to first "generalize" `&'a i32` into a type with a region variable: +`&'?b i32`, and then unify `?T` with that (`?T = &'?b i32`). +We then relate this new variable with the original bound: ```text &'?b i32 <: &'a i32 @@ -173,73 +169,74 @@ relate this new variable with the original bound: This will result in a region constraint (see below) of `'?b: 'a`. One final interesting case is relating two unbound type variables, -like `?T <: ?U`. In that case, we can't make progress, so we enqueue -an obligation `Subtype(?T, ?U)` and return it via the `InferOk` -mechanism. You'll have to try again when more details about `?T` or -`?U` are known. +like `?T <: ?U`. + In that case, we can't make progress, so we enqueue an obligation `Subtype(?T, +?U)` and return it via the `InferOk` +mechanism. +You'll have to try again when more details about `?T` or `?U` are known. ## Region constraints -Regions are inferenced somewhat differently from types. Rather than -eagerly unifying things, we simply collect constraints as we go, but -make (almost) no attempt to solve regions. These constraints have the -form of an "outlives" constraint: +Regions are inferenced somewhat differently from types. +Rather than eagerly unifying things, we simply collect constraints as we go, but +make (almost) no attempt to solve regions. +These constraints have the form of an "outlives" constraint: ```text 'a: 'b ``` -Actually the code tends to view them as a subregion relation, but it's the same -idea: +Actually the code tends to view them as a subregion relation, but it's the same idea: ```text 'b <= 'a ``` -(There are various other kinds of constraints, such as "verifys"; see -the [`region_constraints`] module for details.) +(There are various other kinds of constraints, +such as "verifys"; see the [`region_constraints`] module for details.) -There is one case where we do some amount of eager unification. If you have an -equality constraint between two regions +There is one case where we do some amount of eager unification. +If you have an equality constraint between two regions ```text 'a = 'b ``` -we will record that fact in a unification table. You can then use -[`opportunistic_resolve_var`] to convert `'b` to `'a` (or vice -versa). This is sometimes needed to ensure termination of fixed-point -algorithms. +we will record that fact in a unification table. +You can then use [`opportunistic_resolve_var`] to convert `'b` to `'a` (or vice +versa). +This is sometimes needed to ensure termination of fixed-point algorithms. [`region_constraints`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_infer/infer/region_constraints/index.html [`opportunistic_resolve_var`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_infer/infer/region_constraints/struct.RegionConstraintCollector.html#method.opportunistic_resolve_var ## Solving region constraints -Region constraints are only solved at the very end of -typechecking, once all other constraints are known and -all other obligations have been proven. There are two -ways to solve region constraints right now: lexical and -non-lexical. Eventually there will only be one. +Region constraints are only solved at the very end of typechecking, +once all other constraints are known and +all other obligations have been proven. +There are two ways to solve region constraints right now: lexical and +non-lexical. +Eventually there will only be one. An exception here is the leak-check which is used during trait solving -and relies on region constraints containing higher-ranked regions. Region +and relies on region constraints containing higher-ranked regions. +Region constraints in the root universe (i.e. not arising from a `for<'a>`) must -not influence the trait system, as these regions are all erased during -codegen. +not influence the trait system, as these regions are all erased during codegen. -To solve **lexical** region constraints, you invoke -[`resolve_regions_and_report_errors`]. This "closes" the region -constraint process and invokes the [`lexical_region_resolve`] code. Once -this is done, any further attempt to equate or create a subtyping +To solve **lexical** region constraints, you invoke [`resolve_regions_and_report_errors`]. + This "closes" the region constraint process and invokes the [`lexical_region_resolve`] code. +Once this is done, any further attempt to equate or create a subtyping relationship will yield an ICE. -The NLL solver (actually, the MIR type-checker) does things slightly -differently. It uses canonical queries for trait solving which use -[`take_and_reset_region_constraints`] at the end. This extracts all of the -outlives constraints added during the canonical query. This is required -as the NLL solver must not only know *what* regions outlive each other, -but also *where*. Finally, the NLL solver invokes [`get_region_var_infos`], +The NLL solver (actually, the MIR type-checker) does things slightly differently. +It uses canonical queries for trait solving which use +[`take_and_reset_region_constraints`] at the end. +This extracts all of the outlives constraints added during the canonical query. +This is required as the NLL solver must not only know *what* regions outlive each other, +but also *where*. +Finally, the NLL solver invokes [`get_region_var_infos`], providing all region variables to the solver. [`resolve_regions_and_report_errors`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_trait_selection/traits/struct.ObligationCtxt.html#method.resolve_regions_and_report_errors @@ -249,8 +246,8 @@ providing all region variables to the solver. ## Lexical region resolution -Lexical region resolution is done by initially assigning each region -variable to an empty value. We then process each outlives constraint -repeatedly, growing region variables until a fixed-point is reached. +Lexical region resolution is done by initially assigning each region variable to an empty value. +We then process each outlives constraint repeatedly, +growing region variables until a fixed-point is reached. Region variables can be grown using a least-upper-bound relation on the region lattice in a fairly straightforward fashion. From 1e5733f9071d3780d6ec7c3296a9645690c0cbbc Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 21:46:06 +0100 Subject: [PATCH 10/20] improve type-inference.md --- src/type-inference.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/type-inference.md b/src/type-inference.md index 0a37dfbd99..aaedb2b825 100644 --- a/src/type-inference.md +++ b/src/type-inference.md @@ -51,7 +51,8 @@ See `InferCtxtBuilder` for more information. ## Inference variables The main purpose of the inference context is to house a bunch of -**inference variables** – these represent types or regions whose precise value is not yet known, +**inference variables**. +These represent types or regions whose precise value is not yet known, but will be uncovered as we perform type-checking. If you're familiar with the basic ideas of unification from H-M type systems, @@ -77,7 +78,7 @@ All told, the inference context stores five kinds of inference variables All the type variables work in much the same way: you can create a new type variable, and what you get is `Ty<'tcx>` representing an unresolved type `?T`. -Then later you can apply the various operations that the inferencer supports, +You can then later apply the various operations that the inferencer supports, such as equality or subtyping, and it will possibly **instantiate** (or **bind**) that `?T` to a specific value as a result. @@ -119,12 +120,12 @@ The same basic concepts as above apply. ## "Trying" equality Sometimes you would like to know if it is *possible* to equate two types without error. - You can test that with `infcx.can_eq` (or `infcx.can_sub` for subtyping). +You can test that with `infcx.can_eq` (or `infcx.can_sub` for subtyping). If this returns `Ok`, then equality is possible – but in all cases, any side-effects are reversed. Be aware, though, that the success or failure of these methods is always **modulo regions**. That is, two types `&'a u32` and `&'b u32` will return `Ok` for `can_eq`, even if `'a != 'b`. - This falls out from the "two-phase" nature of how we solve region constraints. +This falls out from the "two-phase" nature of how we solve region constraints. ## Snapshots @@ -156,8 +157,8 @@ This follows from Rust's rather limited notion of subtyping: so, in the above case, `?T <: i32` is equivalent to `?T = i32`. However, in some cases we have to be more careful. -For example, when regions are involved. -So if you have `?T <: &'a i32`, +An example is when regions are involved. +So, if you have `?T <: &'a i32`, what we would do is to first "generalize" `&'a i32` into a type with a region variable: `&'?b i32`, and then unify `?T` with that (`?T = &'?b i32`). We then relate this new variable with the original bound: @@ -170,7 +171,7 @@ This will result in a region constraint (see below) of `'?b: 'a`. One final interesting case is relating two unbound type variables, like `?T <: ?U`. - In that case, we can't make progress, so we enqueue an obligation `Subtype(?T, +In that case, we can't make progress, so we enqueue an obligation `Subtype(?T, ?U)` and return it via the `InferOk` mechanism. You'll have to try again when more details about `?T` or `?U` are known. @@ -226,7 +227,7 @@ constraints in the root universe (i.e. not arising from a `for<'a>`) must not influence the trait system, as these regions are all erased during codegen. To solve **lexical** region constraints, you invoke [`resolve_regions_and_report_errors`]. - This "closes" the region constraint process and invokes the [`lexical_region_resolve`] code. +This "closes" the region constraint process and invokes the [`lexical_region_resolve`] code. Once this is done, any further attempt to equate or create a subtyping relationship will yield an ICE. From 033b61044dcdf5dd66bd70256e34212955f93f01 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 21:47:05 +0100 Subject: [PATCH 11/20] reflow src/type-inference.md --- src/type-inference.md | 68 ++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/src/type-inference.md b/src/type-inference.md index aaedb2b825..31f2f4b198 100644 --- a/src/type-inference.md +++ b/src/type-inference.md @@ -16,8 +16,7 @@ Here, the type of `things` is *inferred* to be `Vec<&str>` because of the value we push into `things`. The type inference is based on the standard Hindley-Milner (HM) type inference algorithm, -but extended in various ways to accommodate subtyping, region -inference, and higher-ranked types. +but extended in various ways to accommodate subtyping, region inference, and higher-ranked types. ## A note on terminology @@ -42,23 +41,21 @@ let infcx = tcx.infer_ctxt().build(); `infcx` has the type `InferCtxt<'tcx>`, the same `'tcx` lifetime as on the `tcx` it was built from. The `tcx.infer_ctxt` method actually returns a builder, -which means there are some kinds of configuration you can do before the `infcx` is -created. +which means there are some kinds of configuration you can do before the `infcx` is created. See `InferCtxtBuilder` for more information. ## Inference variables -The main purpose of the inference context is to house a bunch of -**inference variables**. +The main purpose of the inference context is to house a bunch of **inference variables**. These represent types or regions whose precise value is not yet known, but will be uncovered as we perform type-checking. If you're familiar with the basic ideas of unification from H-M type systems, or logic languages like Prolog, this is the same concept. -If you're not, you might want to read a tutorial on how H-M type -inference works, or perhaps this blog post on [unification in the Chalk project]. +If you're not, you might want to read a tutorial on how H-M type inference works, +or perhaps this blog post on [unification in the Chalk project]. [Unification in the Chalk project]: http://smallcultfollowing.com/babysteps/blog/2017/03/25/unification-in-chalk-part-1/ @@ -76,11 +73,10 @@ All told, the inference context stores five kinds of inference variables - Const variables, which represent constants. All the type variables work in much the same way: you can create a new type variable, -and what you get is `Ty<'tcx>` representing an -unresolved type `?T`. +and what you get is `Ty<'tcx>` representing an unresolved type `?T`. You can then later apply the various operations that the inferencer supports, -such as equality or subtyping, and it -will possibly **instantiate** (or **bind**) that `?T` to a specific value as a result. +such as equality or subtyping, +and it will possibly **instantiate** (or **bind**) that `?T` to a specific value as a result. The region variables work somewhat differently, and are described below in a separate section. @@ -88,8 +84,7 @@ The region variables work somewhat differently, and are described below in a sep The most basic operations you can perform in the type inferencer is **equality**, which forces two types `T` and `U` to be the same. -The recommended way to add an equality constraint is to use the `at` -method, roughly like so: +The recommended way to add an equality constraint is to use the `at` method, roughly like so: ```rust,ignore infcx.at(...).eq(t, u); @@ -100,12 +95,12 @@ doing this unification, and in what environment, and the `eq` method performs the actual equality constraint. When you equate things, you force them to be precisely equal. -Equating returns an `InferResult` – if it returns `Err(err)`, then equating -failed, and the enclosing `TypeError` will tell you what went wrong. +Equating returns an `InferResult` – if it returns `Err(err)`, then equating failed, +and the enclosing `TypeError` will tell you what went wrong. The success case is perhaps more interesting. -The "primary" return type of `eq` is `()` – that is, when it succeeds, it doesn't return a -value of any particular interest. +The "primary" return type of `eq` is `()` – that is, when it succeeds, +it doesn't return a value of any particular interest. Rather, it is executed for its side-effects of constraining type variables and so forth. However, the actual return type is not `()`, but rather `InferOk<()>`. The `InferOk` type is used to carry extra trait obligations – your job is @@ -130,18 +125,15 @@ This falls out from the "two-phase" nature of how we solve region constraints. ## Snapshots As described in the previous section on `can_eq`, -often it is useful to be able to do a series of operations and then roll back their -side-effects. +often it is useful to be able to do a series of operations and then roll back their side-effects. This is done for various reasons: one of them is to be able to backtrack, -trying out multiple possibilities before settling -on which path to take. +trying out multiple possibilities before settling on which path to take. Another is in order to ensure that a series of smaller changes take place atomically or not at all. To allow for this, the inference context supports a `snapshot` method. When you call it, it will start recording changes that occur from the operations you perform. When you are done, you can either invoke `rollback_to`, -which will undo those changes, or else `confirm`, which -will make them permanent. +which will undo those changes, or else `confirm`, which will make them permanent. Snapshots can be nested as long as you follow a stack-like discipline. Rather than use snapshots directly, @@ -151,16 +143,16 @@ patterns. ## Subtyping obligations One thing worth discussing is subtyping obligations. -When you force two types to be a subtype, like `?T <: i32`, we can often convert those -into equality constraints. +When you force two types to be a subtype, like `?T <: i32`, +we can often convert those into equality constraints. This follows from Rust's rather limited notion of subtyping: so, in the above case, `?T <: i32` is equivalent to `?T = i32`. However, in some cases we have to be more careful. An example is when regions are involved. So, if you have `?T <: &'a i32`, -what we would do is to first "generalize" `&'a i32` into a type with a region variable: -`&'?b i32`, and then unify `?T` with that (`?T = &'?b i32`). +what we would do is to first "generalize" `&'a i32` into a type with a region variable: `&'?b i32`, +and then unify `?T` with that (`?T = &'?b i32`). We then relate this new variable with the original bound: ```text @@ -172,15 +164,14 @@ This will result in a region constraint (see below) of `'?b: 'a`. One final interesting case is relating two unbound type variables, like `?T <: ?U`. In that case, we can't make progress, so we enqueue an obligation `Subtype(?T, -?U)` and return it via the `InferOk` -mechanism. +?U)` and return it via the `InferOk` mechanism. You'll have to try again when more details about `?T` or `?U` are known. ## Region constraints Regions are inferenced somewhat differently from types. -Rather than eagerly unifying things, we simply collect constraints as we go, but -make (almost) no attempt to solve regions. +Rather than eagerly unifying things, we simply collect constraints as we go, +but make (almost) no attempt to solve regions. These constraints have the form of an "outlives" constraint: ```text @@ -204,8 +195,7 @@ If you have an equality constraint between two regions ``` we will record that fact in a unification table. -You can then use [`opportunistic_resolve_var`] to convert `'b` to `'a` (or vice -versa). +You can then use [`opportunistic_resolve_var`] to convert `'b` to `'a` (or vice versa). This is sometimes needed to ensure termination of fixed-point algorithms. [`region_constraints`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_infer/infer/region_constraints/index.html @@ -214,10 +204,8 @@ This is sometimes needed to ensure termination of fixed-point algorithms. ## Solving region constraints Region constraints are only solved at the very end of typechecking, -once all other constraints are known and -all other obligations have been proven. -There are two ways to solve region constraints right now: lexical and -non-lexical. +once all other constraints are known and all other obligations have been proven. +There are two ways to solve region constraints right now: lexical and non-lexical. Eventually there will only be one. An exception here is the leak-check which is used during trait solving @@ -228,8 +216,8 @@ not influence the trait system, as these regions are all erased during codegen. To solve **lexical** region constraints, you invoke [`resolve_regions_and_report_errors`]. This "closes" the region constraint process and invokes the [`lexical_region_resolve`] code. -Once this is done, any further attempt to equate or create a subtyping -relationship will yield an ICE. +Once this is done, +any further attempt to equate or create a subtyping relationship will yield an ICE. The NLL solver (actually, the MIR type-checker) does things slightly differently. It uses canonical queries for trait solving which use From 1954e528cc083e7f0da9a5485854bbc67df33a4a Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 22:50:03 +0100 Subject: [PATCH 12/20] double space is very common... I do not remember seeing more --- ci/sembr/src/main.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ci/sembr/src/main.rs b/ci/sembr/src/main.rs index 56761cbb07..10b41e7703 100644 --- a/ci/sembr/src/main.rs +++ b/ci/sembr/src/main.rs @@ -122,9 +122,8 @@ fn comply(content: &str) -> String { }; let mut newly_split_lines = line.split_inclusive(&*REGEX_SPLIT); let first = newly_split_lines.next().unwrap().trim_end().to_owned(); - let mut remaining: Vec<_> = newly_split_lines - .map(|portion| format!("{:indent$}{}", "", portion.trim_end())) - .collect(); + let mut remaining: Vec<_> = + newly_split_lines.map(|portion| format!("{:indent$}{}", "", portion.trim())).collect(); let mut new_lines = Vec::new(); new_lines.push(first); new_lines.append(&mut remaining); @@ -244,13 +243,14 @@ some code. block ``` sentence with *italics* should not be ignored. truly. git log main.. compiler - foo. bar. baz o? whatever r? @reviewer r? @reviewer ~? diagnostic the queries that we do, as well as the **query DAG**. The + +trim double space. or many spaces. in this next sentence "; let expected = " # some. heading @@ -276,9 +276,6 @@ some code. block sentence with *italics* should not be ignored. truly. git log main.. compiler - foo. - bar. - baz o? whatever r? @reviewer @@ -287,6 +284,10 @@ r? @reviewer the queries that we do, as well as the **query DAG**. The + +trim double space. +or many spaces. +in this next sentence "; assert_eq!(expected, comply(original)); } From efc3da48b80c2cea3a1a30416f0cb7a9f5101bc1 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 22:59:11 +0100 Subject: [PATCH 13/20] binding not needed --- ci/sembr/src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ci/sembr/src/main.rs b/ci/sembr/src/main.rs index 10b41e7703..6125f2261c 100644 --- a/ci/sembr/src/main.rs +++ b/ci/sembr/src/main.rs @@ -121,11 +121,10 @@ fn comply(content: &str) -> String { line.find(|ch: char| !ch.is_whitespace()).unwrap() }; let mut newly_split_lines = line.split_inclusive(&*REGEX_SPLIT); - let first = newly_split_lines.next().unwrap().trim_end().to_owned(); + let mut new_lines = Vec::new(); + new_lines.push(newly_split_lines.next().unwrap().trim_end().to_owned()); let mut remaining: Vec<_> = newly_split_lines.map(|portion| format!("{:indent$}{}", "", portion.trim())).collect(); - let mut new_lines = Vec::new(); - new_lines.push(first); new_lines.append(&mut remaining); new_content.splice(new_n..=new_n, new_lines.clone()); new_n += new_lines.len() - 1; From 8e6bb4cee9795046762577ae55675809b553958a Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 22:59:52 +0100 Subject: [PATCH 14/20] sembr src/ty-module/early-binder.md --- src/ty-module/early-binder.md | 37 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/ty-module/early-binder.md b/src/ty-module/early-binder.md index d83d73c955..e5b43f3837 100644 --- a/src/ty-module/early-binder.md +++ b/src/ty-module/early-binder.md @@ -10,11 +10,20 @@ fn main() { } ``` -When type checking `main` we cannot just naively look at the return type of `foo` and assign the type `T` to the variable `c`, The function `main` does not define any generic parameters, `T` is completely meaningless in this context. More generally whenever an item introduces (binds) generic parameters, when accessing types inside the item from outside, the generic parameters must be instantiated with values from the outer item. +When type checking `main` we cannot just naively look at the return type of `foo` and assign the type `T` to the variable `c`, The function `main` does not define any generic parameters, +`T` is completely meaningless in this context. +More generally whenever an item introduces (binds) generic parameters, when accessing types inside the item from outside, +the generic parameters must be instantiated with values from the outer item. -In rustc we track this via the [`EarlyBinder`] type, the return type of `foo` is represented as an `EarlyBinder` with the only way to access `Ty` being to provide arguments for any generic parameters `Ty` might be using. This is implemented via the [`EarlyBinder::instantiate`] method which discharges the binder returning the inner value with all the generic parameters replaced by the provided arguments. +In rustc we track this via the [`EarlyBinder`] type, +the return type of `foo` is represented as an `EarlyBinder` with the only way to access `Ty` being to provide arguments for any generic parameters `Ty` might be using. +This is implemented via the [`EarlyBinder::instantiate`] method which discharges the binder returning the inner value with all the generic parameters replaced by the provided arguments. -To go back to our example, when type checking `main` the return type of `foo` would be represented as `EarlyBinder(T/#0)`. Then, because we called the function with `i32, u128` for the generic arguments, we would call `EarlyBinder::instantiate` on the return type with `[i32, u128]` for the args. This would result in an instantiated return type of `i32` that we can use as the type of the local `c`. +To go back to our example, +when type checking `main` the return type of `foo` would be represented as `EarlyBinder(T/#0)`. +Then, because we called the function with `i32, u128` for the generic arguments, we would call `EarlyBinder::instantiate` on the return type with `[i32, +u128]` for the args. +This would result in an instantiated return type of `i32` that we can use as the type of the local `c`. Here are some more examples: @@ -45,12 +54,14 @@ fn bar(foo: Foo) { } ``` -In the compiler the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), at some point during type checking `bar` we will wind up calling `FieldDef::ty(x, &[u32, f32])` in order to obtain the type of `foo.x`. +In the compiler the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), at some point during type checking `bar` we will wind up calling `FieldDef::ty(x, &[u32, +f32])` in order to obtain the type of `foo.x`. -**Note on indices:** It is a bug if the index of a `Param` does not match what the `EarlyBinder` binds. For -example, if the index is out of bounds or the index of a lifetime corresponds to a type parameter. +**Note on indices:** It is a bug if the index of a `Param` does not match what the `EarlyBinder` binds. +For example, +if the index is out of bounds or the index of a lifetime corresponds to a type parameter. These sorts of errors are caught earlier in the compiler during name resolution where we disallow references -to generics parameters introduced by items that should not be nameable by the inner item. +to generics parameters introduced by items that should not be nameable by the inner item. [`FieldDef::ty`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.FieldDef.html#method.ty [field_def_ty_src]: https://github.com/rust-lang/rust/blob/44d679b9021f03a79133021b94e6d23e9b55b3ab/compiler/rustc_middle/src/ty/mod.rs#L1421-L1426 @@ -59,7 +70,8 @@ to generics parameters introduced by items that should not be nameable by the in --- -As mentioned previously when _outside_ of an item, it is important to instantiate the `EarlyBinder` with generic arguments before accessing the value inside, but the setup for when we are conceptually inside of the binder already is a bit different. +As mentioned previously when _outside_ of an item, it is important to instantiate the `EarlyBinder` with generic arguments before accessing the value inside, +but the setup for when we are conceptually inside of the binder already is a bit different. For example: ```rust @@ -68,9 +80,14 @@ impl Trait for Vec { } ``` -When constructing a `Ty` to represent the `b` parameter's type we need to get the type of `Self` on the impl that we are inside. This can be acquired by calling the [`type_of`] query with the `impl`'s `DefId`, however, this will return a `EarlyBinder` as the impl block binds generic parameters that may have to be discharged if we are outside of the impl. +When constructing a `Ty` to represent the `b` parameter's type we need to get the type of `Self` on the impl that we are inside. +This can be acquired by calling the [`type_of`] query with the `impl`'s `DefId`, however, +this will return a `EarlyBinder` as the impl block binds generic parameters that may have to be discharged if we are outside of the impl. -The `EarlyBinder` type provides an [`instantiate_identity`] function for discharging the binder when you are "already inside of it". This is effectively a more performant version of writing `EarlyBinder::instantiate(GenericArgs::identity_for_item(..))`. Conceptually this discharges the binder by instantiating it with placeholders in the root universe (we will talk about what this means in the next few chapters). In practice though it simply returns the inner value with no modification taking place. +The `EarlyBinder` type provides an [`instantiate_identity`] function for discharging the binder when you are "already inside of it". +This is effectively a more performant version of writing `EarlyBinder::instantiate(GenericArgs::identity_for_item(..))`. +Conceptually this discharges the binder by instantiating it with placeholders in the root universe (we will talk about what this means in the next few chapters). +In practice though it simply returns the inner value with no modification taking place. [`type_of`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/context/struct.TyCtxt.html#method.type_of [`instantiate_identity`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/type.EarlyBinder.html#method.instantiate_identity From 41a9b706de0616e8b60e182995e31bd6456e18cc Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 23:08:12 +0100 Subject: [PATCH 15/20] improve ty-module/early-binder.md --- src/ty-module/early-binder.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/ty-module/early-binder.md b/src/ty-module/early-binder.md index e5b43f3837..016ac9de84 100644 --- a/src/ty-module/early-binder.md +++ b/src/ty-module/early-binder.md @@ -1,6 +1,9 @@ # `EarlyBinder` and instantiating parameters -Given an item that introduces a generic parameter `T`, whenever we refer to types inside of `foo` (i.e. the return type or argument types) from outside of `foo` we must take care to handle the generic parameters defined on `foo`. As an example: +Given an item that introduces a generic parameter `T`, +whenever we refer to types inside of `foo` (i.e. the return type or argument types) from outside of `foo`, +we must take care to handle the generic parameters defined on `foo`. +As an example: ```rust,ignore fn foo(a: T, _b: U) -> T { a } @@ -10,9 +13,11 @@ fn main() { } ``` -When type checking `main` we cannot just naively look at the return type of `foo` and assign the type `T` to the variable `c`, The function `main` does not define any generic parameters, -`T` is completely meaningless in this context. -More generally whenever an item introduces (binds) generic parameters, when accessing types inside the item from outside, +When type checking `main`, +we cannot just naively look at the return type of `foo` and assign the type `T` to the variable `c`. +The function `main` does not define any generic parameters, +and `T` is completely meaningless in this context. +More generally, whenever an item introduces (binds) generic parameters, when accessing types inside the item from outside, the generic parameters must be instantiated with values from the outer item. In rustc we track this via the [`EarlyBinder`] type, @@ -20,7 +25,8 @@ the return type of `foo` is represented as an `EarlyBinder` with the only wa This is implemented via the [`EarlyBinder::instantiate`] method which discharges the binder returning the inner value with all the generic parameters replaced by the provided arguments. To go back to our example, -when type checking `main` the return type of `foo` would be represented as `EarlyBinder(T/#0)`. +when type checking `main`, +the return type of `foo` would be represented as `EarlyBinder(T/#0)`. Then, because we called the function with `i32, u128` for the generic arguments, we would call `EarlyBinder::instantiate` on the return type with `[i32, u128]` for the args. This would result in an instantiated return type of `i32` that we can use as the type of the local `c`. @@ -44,7 +50,7 @@ struct Foo { .. } -fn bar(foo: Foo) { +fn bar(foo: Foo) { // the type of `foo`'s `x` field before instantiating it would be: // `EarlyBinder(Vec)` // we then instantiate the binder with `[u32, f32]` as those are the @@ -54,7 +60,7 @@ fn bar(foo: Foo) { } ``` -In the compiler the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), at some point during type checking `bar` we will wind up calling `FieldDef::ty(x, &[u32, +In the compiler, the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), and at some point during type checking `bar`, we will wind up calling `FieldDef::ty(x, &[u32, f32])` in order to obtain the type of `foo.x`. **Note on indices:** It is a bug if the index of a `Param` does not match what the `EarlyBinder` binds. @@ -80,14 +86,15 @@ impl Trait for Vec { } ``` -When constructing a `Ty` to represent the `b` parameter's type we need to get the type of `Self` on the impl that we are inside. -This can be acquired by calling the [`type_of`] query with the `impl`'s `DefId`, however, +When constructing a `Ty` to represent the `b` parameter's type, we need to get the type of `Self` on the impl that we are inside of. +This can be acquired by calling the [`type_of`] query with the `impl`'s `DefId`. However, this will return a `EarlyBinder` as the impl block binds generic parameters that may have to be discharged if we are outside of the impl. The `EarlyBinder` type provides an [`instantiate_identity`] function for discharging the binder when you are "already inside of it". This is effectively a more performant version of writing `EarlyBinder::instantiate(GenericArgs::identity_for_item(..))`. -Conceptually this discharges the binder by instantiating it with placeholders in the root universe (we will talk about what this means in the next few chapters). -In practice though it simply returns the inner value with no modification taking place. +Conceptually, this discharges the binder by instantiating it with placeholders in the root universe +(and we will talk about what this means in the next few chapters). +In practice, though, it simply returns the inner value with no modification taking place. [`type_of`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/context/struct.TyCtxt.html#method.type_of [`instantiate_identity`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/type.EarlyBinder.html#method.instantiate_identity From 6521d7f06be9ef87fa9485bf7528a7ef1d5e4baa Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 23:10:36 +0100 Subject: [PATCH 16/20] sembr src/ty-module/early-binder.md --- src/ty-module/early-binder.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ty-module/early-binder.md b/src/ty-module/early-binder.md index 016ac9de84..d46f8a5fb9 100644 --- a/src/ty-module/early-binder.md +++ b/src/ty-module/early-binder.md @@ -17,7 +17,8 @@ When type checking `main`, we cannot just naively look at the return type of `foo` and assign the type `T` to the variable `c`. The function `main` does not define any generic parameters, and `T` is completely meaningless in this context. -More generally, whenever an item introduces (binds) generic parameters, when accessing types inside the item from outside, +More generally, whenever an item introduces (binds) generic parameters, +when accessing types inside the item from outside, the generic parameters must be instantiated with values from the outer item. In rustc we track this via the [`EarlyBinder`] type, @@ -27,7 +28,8 @@ This is implemented via the [`EarlyBinder::instantiate`] method which discharges To go back to our example, when type checking `main`, the return type of `foo` would be represented as `EarlyBinder(T/#0)`. -Then, because we called the function with `i32, u128` for the generic arguments, we would call `EarlyBinder::instantiate` on the return type with `[i32, +Then, because we called the function with `i32, u128` for the generic arguments, +we would call `EarlyBinder::instantiate` on the return type with `[i32, u128]` for the args. This would result in an instantiated return type of `i32` that we can use as the type of the local `c`. @@ -60,7 +62,8 @@ fn bar(foo: Foo) { } ``` -In the compiler, the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), and at some point during type checking `bar`, we will wind up calling `FieldDef::ty(x, &[u32, +In the compiler, the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), and at some point during type checking `bar`, we will wind up calling `FieldDef::ty(x, +&[u32, f32])` in order to obtain the type of `foo.x`. **Note on indices:** It is a bug if the index of a `Param` does not match what the `EarlyBinder` binds. @@ -76,7 +79,8 @@ to generics parameters introduced by items that should not be nameable by the in --- -As mentioned previously when _outside_ of an item, it is important to instantiate the `EarlyBinder` with generic arguments before accessing the value inside, +As mentioned previously when _outside_ of an item, +it is important to instantiate the `EarlyBinder` with generic arguments before accessing the value inside, but the setup for when we are conceptually inside of the binder already is a bit different. For example: @@ -86,8 +90,10 @@ impl Trait for Vec { } ``` -When constructing a `Ty` to represent the `b` parameter's type, we need to get the type of `Self` on the impl that we are inside of. -This can be acquired by calling the [`type_of`] query with the `impl`'s `DefId`. However, +When constructing a `Ty` to represent the `b` parameter's type, +we need to get the type of `Self` on the impl that we are inside of. +This can be acquired by calling the [`type_of`] query with the `impl`'s `DefId`. +However, this will return a `EarlyBinder` as the impl block binds generic parameters that may have to be discharged if we are outside of the impl. The `EarlyBinder` type provides an [`instantiate_identity`] function for discharging the binder when you are "already inside of it". From 8996541f6ca6dea8ba5cc2d1fffc307700bc43b5 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 23:13:55 +0100 Subject: [PATCH 17/20] reflow src/ty-module/early-binder.md --- src/ty-module/early-binder.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ty-module/early-binder.md b/src/ty-module/early-binder.md index d46f8a5fb9..d1c311827c 100644 --- a/src/ty-module/early-binder.md +++ b/src/ty-module/early-binder.md @@ -29,8 +29,7 @@ To go back to our example, when type checking `main`, the return type of `foo` would be represented as `EarlyBinder(T/#0)`. Then, because we called the function with `i32, u128` for the generic arguments, -we would call `EarlyBinder::instantiate` on the return type with `[i32, -u128]` for the args. +we would call `EarlyBinder::instantiate` on the return type with `[i32, u128]` for the args. This would result in an instantiated return type of `i32` that we can use as the type of the local `c`. Here are some more examples: @@ -62,9 +61,9 @@ fn bar(foo: Foo) { } ``` -In the compiler, the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), and at some point during type checking `bar`, we will wind up calling `FieldDef::ty(x, -&[u32, -f32])` in order to obtain the type of `foo.x`. +In the compiler, the `instantiate` call for this is done in [`FieldDef::ty`] ([src][field_def_ty_src]), +and at some point during type checking `bar`, +we will wind up calling `FieldDef::ty(x, &[u32, f32])` in order to obtain the type of `foo.x`. **Note on indices:** It is a bug if the index of a `Param` does not match what the `EarlyBinder` binds. For example, From 149b9d526b6daef98e9bcfba9a4fc9bf07f3f56a Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 23:14:32 +0100 Subject: [PATCH 18/20] sembr src/traits/resolution.md --- src/traits/resolution.md | 194 ++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 96 deletions(-) diff --git a/src/traits/resolution.md b/src/traits/resolution.md index 394d800b5f..1ae5bde54d 100644 --- a/src/traits/resolution.md +++ b/src/traits/resolution.md @@ -3,15 +3,15 @@ This chapter describes the general process of _trait resolution_ and points out some non-obvious things. -**Note:** This chapter (and its subchapters) describe how the trait -solver **currently** works. However, we are in the process of -designing a new trait solver. If you'd prefer to read about *that*, +**Note:** This chapter (and its subchapters) describe how the trait solver **currently** works. +However, we are in the process of designing a new trait solver. +If you'd prefer to read about *that*, see [*this* subchapter](./chalk.md). ## Major concepts -Trait resolution is the process of pairing up an impl with each -reference to a trait. So, for example, if there is a generic function like: +Trait resolution is the process of pairing up an impl with each reference to a trait. +So, for example, if there is a generic function like: ```rust,ignore fn clone_slice(x: &[T]) -> Vec { ... } @@ -26,9 +26,10 @@ let v: Vec = clone_slice(&[1, 2, 3]) it is the job of trait resolution to figure out whether there exists an impl of (in this case) `isize : Clone`. -Note that in some cases, like generic functions, we may not be able to -find a specific impl, but we can figure out that the caller must -provide an impl. For example, consider the body of `clone_slice`: +Note that in some cases, like generic functions, we may not be able to find a specific impl, +but we can figure out that the caller must +provide an impl. +For example, consider the body of `clone_slice`: ```rust,ignore fn clone_slice(x: &[T]) -> Vec { @@ -39,57 +40,55 @@ fn clone_slice(x: &[T]) -> Vec { } ``` -The line marked `(*)` is only legal if `T` (the type of `*e`) -implements the `Clone` trait. Naturally, since we don't know what `T` -is, we can't find the specific impl; but based on the bound `T:Clone`, +The line marked `(*)` is only legal if `T` (the type of `*e`) implements the `Clone` trait. +Naturally, since we don't know what `T` is, +we can't find the specific impl; but based on the bound `T:Clone`, we can say that there exists an impl which the caller must provide. -We use the term *obligation* to refer to a trait reference in need of -an impl. Basically, the trait resolution system resolves an obligation -by proving that an appropriate impl does exist. +We use the term *obligation* to refer to a trait reference in need of an impl. +Basically, +the trait resolution system resolves an obligation by proving that an appropriate impl does exist. During type checking, we do not store the results of trait selection. -We simply wish to verify that trait selection will succeed. Then -later, at codegen time, when we have all concrete types available, we -can repeat the trait selection to choose an actual implementation, which -will then be generated in the output binary. +We simply wish to verify that trait selection will succeed. +Then later, at codegen time, when we have all concrete types available, we +can repeat the trait selection to choose an actual implementation, +which will then be generated in the output binary. ## Overview Trait resolution consists of three major parts: -- **Selection**: Deciding how to resolve a specific obligation. For - example, selection might decide that a specific obligation can be +- **Selection**: Deciding how to resolve a specific obligation. + For example, selection might decide that a specific obligation can be resolved by employing an impl which matches the `Self` type, or by using a parameter bound (e.g. `T: Trait`). In the case of an impl, selecting one - obligation can create *nested obligations* because of where clauses - on the impl itself. It may also require evaluating those nested - obligations to resolve ambiguities. + obligation can create *nested obligations* because of where clauses on the impl itself. + It may also require evaluating those nested obligations to resolve ambiguities. -- **Fulfillment**: The fulfillment code is what tracks that obligations - are completely fulfilled. Basically it is a worklist of obligations - to be selected: once selection is successful, the obligation is +- **Fulfillment**: The fulfillment code is what tracks that obligations are completely fulfilled. + Basically it is a worklist of obligations to be selected: once selection is successful, + the obligation is removed from the worklist and any nested obligations are enqueued. Fulfillment constrains inference variables. -- **Evaluation**: Checks whether obligations holds without constraining - any inference variables. Used by selection. +- **Evaluation**: Checks whether obligations holds without constraining any inference variables. + Used by selection. ## Selection -Selection is the process of deciding whether an obligation can be -resolved and, if so, how it is to be resolved (via impl, where clause, etc). -The main interface is the `select()` function, which takes an obligation -and returns a `SelectionResult`. There are three possible outcomes: +Selection is the process of deciding whether an obligation can be resolved and, +if so, how it is to be resolved (via impl, where clause, etc). +The main interface is the `select()` function, +which takes an obligation and returns a `SelectionResult`. +There are three possible outcomes: -- `Ok(Some(selection))` – yes, the obligation can be resolved, and - `selection` indicates how. If the impl was resolved via an impl, - then `selection` may also indicate nested obligations that are required - by the impl. +- `Ok(Some(selection))` – yes, the obligation can be resolved, and `selection` indicates how. + If the impl was resolved via an impl, + then `selection` may also indicate nested obligations that are required by the impl. -- `Ok(None)` – we are not yet sure whether the obligation can be - resolved or not. This happens most commonly when the obligation - contains unbound type variables. +- `Ok(None)` – we are not yet sure whether the obligation can be resolved or not. + This happens most commonly when the obligation contains unbound type variables. - `Err(err)` – the obligation definitely cannot be resolved due to a type error or because there are no impls that could possibly apply. @@ -97,42 +96,42 @@ and returns a `SelectionResult`. There are three possible outcomes: The basic algorithm for selection is broken into two big phases: candidate assembly and confirmation. -Note that because of how lifetime inference works, it is not possible to -give back immediate feedback as to whether a unification or subtype -relationship between lifetimes holds or not. Therefore, lifetime -matching is *not* considered during selection. This is reflected in -the fact that subregion assignment is infallible. This may yield -lifetime constraints that will later be found to be in error (in +Note that because of how lifetime inference works, +it is not possible to give back immediate feedback as to whether a unification or subtype +relationship between lifetimes holds or not. +Therefore, lifetime matching is *not* considered during selection. +This is reflected in the fact that subregion assignment is infallible. +This may yield lifetime constraints that will later be found to be in error (in contrast, the non-lifetime-constraints have already been checked -during selection and can never cause an error, though naturally they -may lead to other errors downstream). +during selection and can never cause an error, +though naturally they may lead to other errors downstream). ### Candidate assembly **TODO**: Talk about _why_ we have different candidates, and why it needs to happen in a probe. -Searches for impls/where-clauses/etc that might -possibly be used to satisfy the obligation. Each of those is called -a candidate. To avoid ambiguity, we want to find exactly one -candidate that is definitively applicable. In some cases, we may not -know whether an impl/where-clause applies or not – this occurs when +Searches for impls/where-clauses/etc that might possibly be used to satisfy the obligation. +Each of those is called a candidate. +To avoid ambiguity, we want to find exactly one candidate that is definitively applicable. +In some cases, we may not know whether an impl/where-clause applies or not – this occurs when the obligation contains unbound inference variables. The subroutines that decide whether a particular impl/where-clause/etc applies to a particular obligation are collectively referred to as the process of _matching_. For `impl` candidates , this amounts to unifying the impl header (the `Self` type and the trait arguments) -while ignoring nested obligations. If matching succeeds then we add it -to a set of candidates. There are other rules when assembling candidates for -built-in traits such as `Copy`, `Sized`, and `CoerceUnsized`. +while ignoring nested obligations. +If matching succeeds then we add it to a set of candidates. +There are other rules when assembling candidates for built-in traits such as `Copy`, +`Sized`, and `CoerceUnsized`. -Once this first pass is done, we can examine the set of candidates. If -it is a singleton set, then we are done: this is the only impl in -scope that could possibly apply. Otherwise, we can **winnow** down the set -of candidates by using where clauses and other conditions. Winnowing uses -`evaluate_candidate` to check whether the nested obligations may apply. -If this still leaves more than 1 candidate, we use ` fn candidate_should_be_dropped_in_favor_of` -to prefer some candidates over others. +Once this first pass is done, we can examine the set of candidates. +If it is a singleton set, then we are done: this is the only impl in +scope that could possibly apply. +Otherwise, we can **winnow** down the set of candidates by using where clauses and other conditions. +Winnowing uses `evaluate_candidate` to check whether the nested obligations may apply. +If this still leaves more than 1 candidate, +we use ` fn candidate_should_be_dropped_in_favor_of` to prefer some candidates over others. If this reduced set yields a single, unambiguous entry, we're good to go, @@ -140,8 +139,8 @@ otherwise the result is considered ambiguous. #### Winnowing: Resolving ambiguities -But what happens if there are multiple impls where all the types -unify? Consider this example: +But what happens if there are multiple impls where all the types unify? +Consider this example: ```rust,ignore trait Get { @@ -161,24 +160,26 @@ impl Get for Box { } ``` -What happens when we invoke `get(&Box::new(1_u16))`, for example? In this -case, the `Self` type is `Box` – that unifies with both impls, -because the first applies to all types `T`, and the second to all -`Box`. In order for this to be unambiguous, the compiler does a *winnowing* -pass that considers `where` clauses -and attempts to remove candidates. In this case, the first impl only -applies if `Box : Copy`, which doesn't hold. After winnowing, +What happens when we invoke `get(&Box::new(1_u16))`, for example? +In this case, the `Self` type is `Box` – that unifies with both impls, +because the first applies to all types `T`, and the second to all `Box`. +In order for this to be unambiguous, +the compiler does a *winnowing* pass that considers `where` clauses +and attempts to remove candidates. +In this case, the first impl only applies if `Box : Copy`, which doesn't hold. +After winnowing, then, we are left with just one candidate, so we can proceed. #### `where` clauses -Besides an impl, the other major way to resolve an obligation is via a -where clause. The selection process is always given a [parameter -environment] which contains a list of where clauses, which are -basically obligations that we can assume are satisfiable. We will iterate -over that list and check whether our current obligation can be found -in that list. If so, it is considered satisfied. More precisely, we -want to check whether there is a where-clause obligation that is for +Besides an impl, the other major way to resolve an obligation is via a where clause. +The selection process is always given a [parameter +environment] which contains a list of where clauses, +which are basically obligations that we can assume are satisfiable. +We will iterate over that list and check whether our current obligation can be found +in that list. +If so, it is considered satisfied. +More precisely, we want to check whether there is a where-clause obligation that is for the same trait (or some subtrait) and which can match against the obligation. [parameter environment]: ../typing-parameter-envs.md @@ -201,12 +202,13 @@ fn foo(x: X) { } ``` -In the body of `foo`, clearly we can use methods of `A1`, `A2`, or `B` -on variable `x`. The line marked `(*)` will incur an obligation `X: A1`, -while the line marked `(#)` will incur an obligation `X: B`. Meanwhile, +In the body of `foo`, clearly we can use methods of `A1`, `A2`, or `B` on variable `x`. +The line marked `(*)` will incur an obligation `X: A1`, +while the line marked `(#)` will incur an obligation `X: B`. +Meanwhile, the parameter environment will contain two where-clauses: `X : A2` and `X : B`. -For each obligation, then, we search this list of where-clauses. The -obligation `X: B` trivially matches against the where-clause `X: B`. +For each obligation, then, we search this list of where-clauses. +The obligation `X: B` trivially matches against the where-clause `X: B`. To resolve an obligation `X:A1`, we would note that `X:A2` implies that `X:A1`. ### Confirmation @@ -214,8 +216,7 @@ To resolve an obligation `X:A1`, we would note that `X:A2` implies that `X:A1`. _Confirmation_ unifies the output type parameters of the trait with the values found in the obligation, possibly yielding a type error. -Suppose we have the following variation of the `Convert` example in the -previous section: +Suppose we have the following variation of the `Convert` example in the previous section: ```rust,ignore trait Convert { @@ -230,24 +231,25 @@ let y: char = x.convert(); // NOTE: `y: char` now! ``` Confirmation is where an error would be reported because the impl specified -that `Target` would be `usize`, but the obligation reported `char`. Hence the -result of selection would be an error. +that `Target` would be `usize`, but the obligation reported `char`. +Hence the result of selection would be an error. -Note that the candidate impl is chosen based on the `Self` type, but -confirmation is done based on (in this case) the `Target` type parameter. +Note that the candidate impl is chosen based on the `Self` type, +but confirmation is done based on (in this case) the `Target` type parameter. ### Selection during codegen -As mentioned above, during type checking, we do not store the results of trait -selection. At codegen time, we repeat the trait selection to choose a particular -impl for each method call. This is done using `fn codegen_select_candidate`. +As mentioned above, during type checking, we do not store the results of trait selection. +At codegen time, we repeat the trait selection to choose a particular impl for each method call. +This is done using `fn codegen_select_candidate`. In this second selection, we do not consider any where-clauses to be in scope because we know that each resolution will resolve to a particular impl. -One interesting twist has to do with nested obligations. In general, in codegen, +One interesting twist has to do with nested obligations. +In general, in codegen, we only need to figure out which candidate applies, and we do not care about nested obligations, -as these are already assumed to be true. Nonetheless, we *do* currently fulfill all of them. +as these are already assumed to be true. +Nonetheless, we *do* currently fulfill all of them. That is because it can sometimes inform the results of type inference. That is, we do not have the full substitutions in terms of the type variables -of the impl available to us, so we must run trait selection to figure -everything out. +of the impl available to us, so we must run trait selection to figure everything out. From 9fdf975068886b32b3f7e17f75c96d8e769ac8f3 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 23:27:59 +0100 Subject: [PATCH 19/20] improve traits/resolution.md --- src/traits/resolution.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/traits/resolution.md b/src/traits/resolution.md index 1ae5bde54d..77793bdd3e 100644 --- a/src/traits/resolution.md +++ b/src/traits/resolution.md @@ -67,9 +67,9 @@ Trait resolution consists of three major parts: It may also require evaluating those nested obligations to resolve ambiguities. - **Fulfillment**: The fulfillment code is what tracks that obligations are completely fulfilled. - Basically it is a worklist of obligations to be selected: once selection is successful, + Basically, it is a worklist of obligations to be selected: once selection is successful, the obligation is - removed from the worklist and any nested obligations are enqueued. + removed from the worklist, and any nested obligations are enqueued. Fulfillment constrains inference variables. - **Evaluation**: Checks whether obligations holds without constraining any inference variables. @@ -120,7 +120,7 @@ The subroutines that decide whether a particular impl/where-clause/etc applies to a particular obligation are collectively referred to as the process of _matching_. For `impl` candidates , this amounts to unifying the impl header (the `Self` type and the trait arguments) -while ignoring nested obligations. +while ignoring nested obligations. If matching succeeds then we add it to a set of candidates. There are other rules when assembling candidates for built-in traits such as `Copy`, `Sized`, and `CoerceUnsized`. From c31317aecf53ae6458510446246ca3b1d1e961c7 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Sat, 31 Oct 2026 23:28:18 +0100 Subject: [PATCH 20/20] reflow src/traits/resolution.md --- src/traits/resolution.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/traits/resolution.md b/src/traits/resolution.md index 77793bdd3e..5d4c8a2b75 100644 --- a/src/traits/resolution.md +++ b/src/traits/resolution.md @@ -27,8 +27,7 @@ it is the job of trait resolution to figure out whether there exists an impl of (in this case) `isize : Clone`. Note that in some cases, like generic functions, we may not be able to find a specific impl, -but we can figure out that the caller must -provide an impl. +but we can figure out that the caller must provide an impl. For example, consider the body of `clone_slice`: ```rust,ignore @@ -51,8 +50,8 @@ the trait resolution system resolves an obligation by proving that an appropriat During type checking, we do not store the results of trait selection. We simply wish to verify that trait selection will succeed. -Then later, at codegen time, when we have all concrete types available, we -can repeat the trait selection to choose an actual implementation, +Then later, at codegen time, when we have all concrete types available, +we can repeat the trait selection to choose an actual implementation, which will then be generated in the output binary. ## Overview @@ -68,8 +67,7 @@ Trait resolution consists of three major parts: - **Fulfillment**: The fulfillment code is what tracks that obligations are completely fulfilled. Basically, it is a worklist of obligations to be selected: once selection is successful, - the obligation is - removed from the worklist, and any nested obligations are enqueued. + the obligation is removed from the worklist, and any nested obligations are enqueued. Fulfillment constrains inference variables. - **Evaluation**: Checks whether obligations holds without constraining any inference variables. @@ -101,8 +99,8 @@ it is not possible to give back immediate feedback as to whether a unification o relationship between lifetimes holds or not. Therefore, lifetime matching is *not* considered during selection. This is reflected in the fact that subregion assignment is infallible. -This may yield lifetime constraints that will later be found to be in error (in -contrast, the non-lifetime-constraints have already been checked +This may yield lifetime constraints that will later be found to be in error (in contrast, +the non-lifetime-constraints have already been checked during selection and can never cause an error, though naturally they may lead to other errors downstream). @@ -126,8 +124,8 @@ There are other rules when assembling candidates for built-in traits such as `Co `Sized`, and `CoerceUnsized`. Once this first pass is done, we can examine the set of candidates. -If it is a singleton set, then we are done: this is the only impl in -scope that could possibly apply. +If it is a singleton set, +then we are done: this is the only impl in scope that could possibly apply. Otherwise, we can **winnow** down the set of candidates by using where clauses and other conditions. Winnowing uses `evaluate_candidate` to check whether the nested obligations may apply. If this still leaves more than 1 candidate, @@ -176,8 +174,7 @@ Besides an impl, the other major way to resolve an obligation is via a where cla The selection process is always given a [parameter environment] which contains a list of where clauses, which are basically obligations that we can assume are satisfiable. -We will iterate over that list and check whether our current obligation can be found -in that list. +We will iterate over that list and check whether our current obligation can be found in that list. If so, it is considered satisfied. More precisely, we want to check whether there is a where-clause obligation that is for the same trait (or some subtrait) and which can match against the obligation.