From 0c75f9b5c3dc345a21756a9b825a5230443e08f8 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 22:58:53 +0100 Subject: [PATCH 01/14] sembr src/diagnostics/lintstore.md --- src/diagnostics/lintstore.md | 66 +++++++++++++++++------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/src/diagnostics/lintstore.md b/src/diagnostics/lintstore.md index 90b62b350..cda975a9a 100644 --- a/src/diagnostics/lintstore.md +++ b/src/diagnostics/lintstore.md @@ -3,15 +3,14 @@ This page documents some of the machinery around lint registration and how we run lints in the compiler. -The [`LintStore`] is the central piece of infrastructure, around which -everything rotates. The `LintStore` is held as part of the [`Session`], and it +The [`LintStore`] is the central piece of infrastructure, around which everything rotates. +The `LintStore` is held as part of the [`Session`], and it gets populated with the list of lints shortly after the `Session` is created. ## Lints vs. lint passes -There are two parts to the linting mechanism within the compiler: lints and -lint passes. Unfortunately, a lot of the documentation we have refers to both -of these as just "lints." +There are two parts to the linting mechanism within the compiler: lints and lint passes. +Unfortunately, a lot of the documentation we have refers to both of these as just "lints." First, we have the lint declarations themselves, and this is where the name and default lint level and other metadata come from. @@ -24,10 +23,11 @@ like all macros). we lint against direct declarations without the use of the macro. Lint declarations don't carry any "state" - they are merely global identifiers -and descriptions of lints. We assert at runtime that they are not registered -twice (by lint name). +and descriptions of lints. +We assert at runtime that they are not registered twice (by lint name). -Lint passes are the meat of any lint. Notably, there is not a one-to-one +Lint passes are the meat of any lint. +Notably, there is not a one-to-one relationship between lints and lint passes; a lint might not have any lint pass that emits it, it could have many, or just one -- the compiler doesn't track whether a pass is in any way associated with a particular lint, and frequently @@ -44,36 +44,33 @@ and all lints are registered. There are three 'sources' of lints: * internal lints: lints only used by the rustc codebase -* builtin lints: lints built into the compiler and not provided by some outside - source -* `rustc_interface::Config`[`register_lints`]: lints passed into the compiler - during construction +* builtin lints: lints built into the compiler and not provided by some outside source +* `rustc_interface::Config`[`register_lints`]: lints passed into the compiler during construction -Lints are registered via the [`LintStore::register_lint`] function. This should -happen just once for any lint, or an ICE will occur. +Lints are registered via the [`LintStore::register_lint`] function. +This should happen just once for any lint, or an ICE will occur. -Once the registration is complete, we "freeze" the lint store by placing it in -an `Arc`. +Once the registration is complete, we "freeze" the lint store by placing it in an `Arc`. Lint passes are registered separately into one of the categories -(pre-expansion, early, late, late module). Passes are registered as a closure +(pre-expansion, early, late, late module). +Passes are registered as a closure -- i.e., `impl Fn() -> Box`, where `dyn X` is either an early or late -lint pass trait object. When we run the lint passes, we run the closure and -then invoke the lint pass methods. The lint pass methods take `&mut self` so -they can keep track of state internally. +lint pass trait object. +When we run the lint passes, we run the closure and then invoke the lint pass methods. +The lint pass methods take `&mut self` so they can keep track of state internally. #### Internal lints -These are lints used just by the compiler or drivers like `clippy`. They can be -found in [`rustc_lint::internal`]. +These are lints used just by the compiler or drivers like `clippy`. +They can be found in [`rustc_lint::internal`]. An example of such a lint is the check that lint passes are implemented using -the `declare_lint_pass!` macro and not by hand. This is accomplished with the -`LINT_PASS_IMPL_WITHOUT_MACRO` lint. +the `declare_lint_pass!` macro and not by hand. +This is accomplished with the `LINT_PASS_IMPL_WITHOUT_MACRO` lint. Registration of these lints happens in the [`rustc_lint::register_internals`] -function which is called when constructing a new lint store inside -[`rustc_lint::new_lint_store`]. +function which is called when constructing a new lint store inside [`rustc_lint::new_lint_store`]. #### Builtin Lints @@ -83,19 +80,18 @@ Often the first provides the definitions for the lints themselves, and the latter provides the lint pass definitions (and implementations), but this is not always true. -The builtin lint registration happens in -the [`rustc_lint::register_builtins`] function. +The builtin lint registration happens in the [`rustc_lint::register_builtins`] function. Just like with internal lints, this happens inside of [`rustc_lint::new_lint_store`]. #### Driver lints These are the lints provided by drivers via the `rustc_interface::Config` -[`register_lints`] field, which is a callback. Drivers should, if finding it -already set, call the function currently set within the callback they add. The -best way for drivers to get access to this is by overriding the -`Callbacks::config` function which gives them direct access to the `Config` -structure. +[`register_lints`] field, which is a callback. +Drivers should, if finding it +already set, call the function currently set within the callback they add. +The best way for drivers to get access to this is by overriding the +`Callbacks::config` function which gives them direct access to the `Config` structure. ## Compiler lint passes are combined into one pass @@ -105,8 +101,8 @@ of lint passes. Instead, we have a single lint pass of each variety (e.g., individual lint passes; this is because then we get the benefits of static over dynamic dispatch for each of the (often empty) trait methods. -Ideally, we'd not have to do this, since it adds to the complexity of -understanding the code. However, with the current type-erased lint store +Ideally, we'd not have to do this, since it adds to the complexity of understanding the code. +However, with the current type-erased lint store approach, it is beneficial to do so for performance reasons. [`LintStore`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint/struct.LintStore.html From 496964235a4ac5d2e42f78ccc302197f92bc26bb Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:05:39 +0100 Subject: [PATCH 02/14] use the more standard column length --- ci/sembr/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/sembr/src/main.rs b/ci/sembr/src/main.rs index 4038f112d..a9b4d182b 100644 --- a/ci/sembr/src/main.rs +++ b/ci/sembr/src/main.rs @@ -15,7 +15,7 @@ struct Cli { /// Modify files that do not comply overwrite: bool, /// Applies to lines that are to be split - #[arg(long, default_value_t = 100)] + #[arg(long, default_value_t = 80)] line_length_limit: usize, } From df96b2d974e826ec1a7ea4358553552e492ab521 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:07:32 +0100 Subject: [PATCH 03/14] sembr src/solve/candidate-preference.md --- src/solve/candidate-preference.md | 58 +++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/solve/candidate-preference.md b/src/solve/candidate-preference.md index 22ec3162b..56fc86502 100644 --- a/src/solve/candidate-preference.md +++ b/src/solve/candidate-preference.md @@ -1,16 +1,23 @@ # Candidate preference -There are multiple ways to prove `Trait` and `NormalizesTo` goals. Each such option is called a [`Candidate`]. If there are multiple applicable candidates, we prefer some candidates over others. We store the relevant information in their [`CandidateSource`]. +There are multiple ways to prove `Trait` and `NormalizesTo` goals. +Each such option is called a [`Candidate`]. +If there are multiple applicable candidates, we prefer some candidates over others. +We store the relevant information in their [`CandidateSource`]. -This preference may result in incorrect inference or region constraints and would therefore be unsound during coherence. Because of this, we simply try to merge all candidates in coherence. +This preference may result in incorrect inference or region constraints and would therefore be unsound during coherence. +Because of this, we simply try to merge all candidates in coherence. ## `Trait` goals -Trait goals merge their applicable candidates in [`fn merge_trait_candidates`]. This document provides additional details and references to explain *why* we've got the current preference rules. +Trait goals merge their applicable candidates in [`fn merge_trait_candidates`]. +This document provides additional details and references to explain *why* we've got the current preference rules. ### `CandidateSource::BuiltinImpl(BuiltinImplSource::Trivial))` -Trivial builtin impls are builtin impls which are known to be always applicable for well-formed types. This means that if one exists, using another candidate should never have fewer constraints. We currently only consider `Sized` - and `MetaSized` - impls to be trivial. +Trivial builtin impls are builtin impls which are known to be always applicable for well-formed types. +This means that if one exists, using another candidate should never have fewer constraints. +We currently only consider `Sized` - and `MetaSized` - impls to be trivial. This is necessary to prevent a lifetime error for the following pattern @@ -50,7 +57,8 @@ where ### `CandidateSource::ParamEnv` Once there's at least one *non-global* `ParamEnv` candidate, we prefer *all* `ParamEnv` candidates over other candidate kinds. -A where-bound is global if it is not higher-ranked and doesn't contain any generic parameters. It may contain `'static`. +A where-bound is global if it is not higher-ranked and doesn't contain any generic parameters. +It may contain `'static`. We try to apply where-bounds over other candidates as users tends to have the most control over them, so they can most easily adjust them in case our candidate preference is incorrect. @@ -68,7 +76,8 @@ fn foo<'a, T: Trait<'a>>() { } ``` -We also need this as shadowed impls can result in currently ambiguous solver cycles: [trait-system-refactor-initiative#76]. Without preference we'd be forced to fail with ambiguity +We also need this as shadowed impls can result in currently ambiguous solver cycles: [trait-system-refactor-initiative#76]. +Without preference we'd be forced to fail with ambiguity errors if the where-bound results in region constraints to avoid incompleteness. ```rust trait Super { @@ -94,7 +103,9 @@ fn overflow() { } ``` -This preference causes a lot of issues. See [#24066]. Most of the +This preference causes a lot of issues. +See [#24066]. +Most of the issues are caused by preferring where-bounds over impls even if the where-bound guides type inference: ```rust trait Trait { @@ -167,7 +178,10 @@ where #### Why no preference for global where-bounds -Global where-bounds are either fully implied by an impl or unsatisfiable. If they are unsatisfiable, we don't really care what happens. If a where-bound is fully implied then using the impl to prove the trait goal cannot result in additional constraints. For trait goals this is only useful for where-bounds which use `'static`: +Global where-bounds are either fully implied by an impl or unsatisfiable. +If they are unsatisfiable, we don't really care what happens. +If a where-bound is fully implied then using the impl to prove the trait goal cannot result in additional constraints. +For trait goals this is only useful for where-bounds which use `'static`: ```rust trait A { @@ -181,13 +195,15 @@ where x.test(); } ``` -More importantly, by using impls here we prevent global where-bounds from shadowing impls when normalizing associated types. There are no known issues from preferring impls over global where-bounds. +More importantly, by using impls here we prevent global where-bounds from shadowing impls when normalizing associated types. +There are no known issues from preferring impls over global where-bounds. #### Why still consider global where-bounds Given that we just use impls even if there exists a global where-bounds, you may ask why we don't just ignore these global where-bounds entirely: we use them to weaken the inference guidance from non-global where-bounds. -Without a global where-bound, we currently prefer non-global where bounds even though there would be an applicable impl as well. By adding a non-global where-bound, this unnecessary inference guidance is disabled, allowing the following to compile: +Without a global where-bound, we currently prefer non-global where bounds even though there would be an applicable impl as well. +By adding a non-global where-bound, this unnecessary inference guidance is disabled, allowing the following to compile: ```rust fn check(color: Color) where @@ -209,7 +225,9 @@ impl From for f32 { ### `CandidateSource::AliasBound` -We prefer alias-bound candidates over impls. We currently use this preference to guide type inference, causing the following to compile. I personally don't think this preference is desirable 🤷 +We prefer alias-bound candidates over impls. +We currently use this preference to guide type inference, causing the following to compile. +I personally don't think this preference is desirable 🤷 ```rust pub trait Dyn { type Word: Into; @@ -254,7 +272,9 @@ fn foo<'a, T: Trait<'a>>() { ### `CandidateSource::BuiltinImpl(BuiltinImplSource::Object(_))` -We prefer builtin trait object impls over user-written impls. This is **unsound** and should be remoed in the future. See [#57893](https://github.com/rust-lang/rust/issues/57893) and [#141347](https://github.com/rust-lang/rust/pull/141347) for more details. +We prefer builtin trait object impls over user-written impls. +This is **unsound** and should be remoed in the future. +See [#57893](https://github.com/rust-lang/rust/issues/57893) and [#141347](https://github.com/rust-lang/rust/pull/141347) for more details. ## `NormalizesTo` goals @@ -336,7 +356,7 @@ Even if the trait goal was proven via an impl, we still prefer `ParamEnv` candid #### We prefer "orphaned" where-bounds We add "orphaned" `Projection` clauses into the `ParamEnv` when normalizing item bounds of GATs and RPITIT in `fn check_type_bounds`. -We need to prefer these `ParamEnv` candidates over impls and other where-bounds. +We need to prefer these `ParamEnv` candidates over impls and other where-bounds. ```rust #![feature(associated_type_defaults)] trait Foo { @@ -355,7 +375,8 @@ I don't fully understand the cases where this preference is actually necessary a #### We prefer global where-bounds over impls -This is necessary for the following to compile. I don't know whether anything relies on it in practice 🤷 +This is necessary for the following to compile. +I don't know whether anything relies on it in practice 🤷 ```rust trait Id { type This; @@ -423,7 +444,8 @@ where #### RPITIT `type_of` cycles -We currently have to avoid impl candidates if there are where-bounds to avoid query cycles for RPITIT, see [#139762]. It feels desirable to me to stop relying on auto-trait leakage of during RPITIT computation to remove this issue, see [#139788]. +We currently have to avoid impl candidates if there are where-bounds to avoid query cycles for RPITIT, see [#139762]. +It feels desirable to me to stop relying on auto-trait leakage of during RPITIT computation to remove this issue, see [#139788]. ```rust use std::future::Future; @@ -457,8 +479,8 @@ where #### Trait definition cannot use associated types from always applicable impls The `T: Trait` assumption in the trait definition prevents it from normalizing -`::Assoc` to `T` by using the blanket impl. This feels like a somewhat -desirable constraint, if not incredibly so. +`::Assoc` to `T` by using the blanket impl. +This feels like a somewhat desirable constraint, if not incredibly so. ```rust trait Eq {} @@ -486,4 +508,4 @@ impl Trait for T { [#24066]: https://github.com/rust-lang/rust/issues/24066 [#133044]: https://github.com/rust-lang/rust/issues/133044 [#139762]: https://github.com/rust-lang/rust/pull/139762 -[#139788]: https://github.com/rust-lang/rust/issues/139788 \ No newline at end of file +[#139788]: https://github.com/rust-lang/rust/issues/139788 From ec0f998c763f59f1c9519570629f57d45df5f028 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:20:20 +0100 Subject: [PATCH 04/14] improve solve/candidate-preference.md --- src/solve/candidate-preference.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/solve/candidate-preference.md b/src/solve/candidate-preference.md index 56fc86502..86ff3c84e 100644 --- a/src/solve/candidate-preference.md +++ b/src/solve/candidate-preference.md @@ -32,7 +32,7 @@ where { // Elaborating the `&'a str: Trait` where-bound results in a // `&'a str: Sized` where-bound. We do not want to prefer this - // over the builtin impl. + // over the builtin impl. is_sized(x); } ``` @@ -77,7 +77,7 @@ fn foo<'a, T: Trait<'a>>() { ``` We also need this as shadowed impls can result in currently ambiguous solver cycles: [trait-system-refactor-initiative#76]. -Without preference we'd be forced to fail with ambiguity +Without preference, we'd be forced to fail with ambiguity errors if the where-bound results in region constraints to avoid incompleteness. ```rust trait Super { @@ -98,7 +98,7 @@ where fn overflow() { // We can use the elaborated `Super` where-bound // to prove the where-bound of the `T: Trait` implementation. This currently results in - // overflow. + // overflow. let x: ::TraitAssoc; } ``` @@ -106,7 +106,7 @@ fn overflow() { This preference causes a lot of issues. See [#24066]. Most of the -issues are caused by preferring where-bounds over impls even if the where-bound guides type inference: +issues are caused by preferring where-bounds over impls even, if the where-bound guides type inference: ```rust trait Trait { fn call_me(&self, x: T) {} @@ -180,8 +180,8 @@ where Global where-bounds are either fully implied by an impl or unsatisfiable. If they are unsatisfiable, we don't really care what happens. -If a where-bound is fully implied then using the impl to prove the trait goal cannot result in additional constraints. -For trait goals this is only useful for where-bounds which use `'static`: +If a where-bound is fully implied, then using the impl to prove the trait goal cannot result in additional constraints. +For trait goals, this is only useful for where-bounds which use `'static`: ```rust trait A { @@ -195,7 +195,7 @@ where x.test(); } ``` -More importantly, by using impls here we prevent global where-bounds from shadowing impls when normalizing associated types. +More importantly, by using impls here, we prevent global where-bounds from shadowing impls when normalizing associated types. There are no known issues from preferring impls over global where-bounds. #### Why still consider global where-bounds From 2b448e4dbab2ac98762bc6c57210f70cf86e600f Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:23:31 +0100 Subject: [PATCH 05/14] avoid inline external links --- src/solve/candidate-preference.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/solve/candidate-preference.md b/src/solve/candidate-preference.md index 86ff3c84e..944c8316b 100644 --- a/src/solve/candidate-preference.md +++ b/src/solve/candidate-preference.md @@ -274,7 +274,7 @@ fn foo<'a, T: Trait<'a>>() { We prefer builtin trait object impls over user-written impls. This is **unsound** and should be remoed in the future. -See [#57893](https://github.com/rust-lang/rust/issues/57893) and [#141347](https://github.com/rust-lang/rust/pull/141347) for more details. +See [#57893] and [#141347] for more details. ## `NormalizesTo` goals @@ -509,3 +509,5 @@ impl Trait for T { [#133044]: https://github.com/rust-lang/rust/issues/133044 [#139762]: https://github.com/rust-lang/rust/pull/139762 [#139788]: https://github.com/rust-lang/rust/issues/139788 +[#57893]: https://github.com/rust-lang/rust/issues/57893 +[#141347]: https://github.com/rust-lang/rust/pull/141347 From fda1a7b47b2dbc6e1788fe43a88103482310049c Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:26:53 +0100 Subject: [PATCH 06/14] add date-check marker to recent section --- src/solve/candidate-preference.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/solve/candidate-preference.md b/src/solve/candidate-preference.md index 944c8316b..7fc39269d 100644 --- a/src/solve/candidate-preference.md +++ b/src/solve/candidate-preference.md @@ -476,6 +476,7 @@ where } ``` + #### Trait definition cannot use associated types from always applicable impls The `T: Trait` assumption in the trait definition prevents it from normalizing From c24ae84c31e76418175a53b5e0c184e0a1476e60 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:27:31 +0100 Subject: [PATCH 07/14] sembr src/borrow-check/region-inference/member-constraints.md --- .../region-inference/member-constraints.md | 128 ++++++++++-------- 1 file changed, 72 insertions(+), 56 deletions(-) diff --git a/src/borrow-check/region-inference/member-constraints.md b/src/borrow-check/region-inference/member-constraints.md index 2fe0a7b58..75891b419 100644 --- a/src/borrow-check/region-inference/member-constraints.md +++ b/src/borrow-check/region-inference/member-constraints.md @@ -1,17 +1,18 @@ # Member constraints A member constraint `'m member of ['c_1..'c_N]` expresses that the -region `'m` must be *equal* to some **choice regions** `'c_i` (for -some `i`). These constraints cannot be expressed by users, but they -arise from `impl Trait` due to its lifetime capture rules. Consider a -function such as the following: +region `'m` must be *equal* to some **choice regions** `'c_i` (for some `i`). +These constraints cannot be expressed by users, but they +arise from `impl Trait` due to its lifetime capture rules. +Consider a function such as the following: ```rust,ignore fn make(a: &'a u32, b: &'b u32) -> impl Trait<'a, 'b> { .. } ``` Here, the true return type (often called the "hidden type") is only -permitted to capture the lifetimes `'a` or `'b`. You can kind of see +permitted to capture the lifetimes `'a` or `'b`. +You can kind of see this more clearly by desugaring that `impl Trait` return type into its more explicit form: @@ -23,7 +24,8 @@ fn make(a: &'a u32, b: &'b u32) -> MakeReturn<'a, 'b> { .. } Here, the idea is that the hidden type must be some type that could have been written in place of the `impl Trait<'x, 'y>` -- but clearly such a type can only reference the regions `'x` or `'y` (or -`'static`!), as those are the only names in scope. This limitation is +`'static`!), as those are the only names in scope. +This limitation is then translated into a restriction to only access `'a` or `'b` because we are returning `MakeReturn<'a, 'b>`, where `'x` and `'y` have been replaced with `'a` and `'b` respectively. @@ -31,8 +33,8 @@ replaced with `'a` and `'b` respectively. ## Detailed example To help us explain member constraints in more detail, let's spell out -the `make` example in a bit more detail. First off, let's assume that -you have some dummy trait: +the `make` example in a bit more detail. +First off, let's assume that you have some dummy trait: ```rust,ignore trait Trait<'a, 'b> { } @@ -49,8 +51,8 @@ fn make(a: &'a u32, b: &'b u32) -> MakeReturn<'a, 'b> { ``` What happens in this case is that the return type will be `(&'0 u32, &'1 u32)`, -where `'0` and `'1` are fresh region variables. We will have the following -region constraints: +where `'0` and `'1` are fresh region variables. +We will have the following region constraints: ```txt '0 live at {L} @@ -67,11 +69,11 @@ return tuple is constructed to where it is returned (in fact, `'0` and `'1` might have slightly different liveness sets, but that's not very interesting to the point we are illustrating here). -The `'a: '0` and `'b: '1` constraints arise from subtyping. When we -construct the `(a, b)` value, it will be assigned type `(&'0 u32, &'1 +The `'a: '0` and `'b: '1` constraints arise from subtyping. +When we construct the `(a, b)` value, it will be assigned type `(&'0 u32, &'1 u32)` -- the region variables reflect that the lifetimes of these -references could be made smaller. For this value to be created from -`a` and `b`, however, we do require that: +references could be made smaller. +For this value to be created from `a` and `b`, however, we do require that: ```txt (&'a u32, &'b u32) <: (&'0 u32, &'1 u32) @@ -82,35 +84,39 @@ which means in turn that `&'a u32 <: &'0 u32` and hence that `'a: '0` Note that if we ignore member constraints, the value of `'0` would be inferred to some subset of the function body (from the liveness -constraints, which we did not write explicitly). It would never become +constraints, which we did not write explicitly). +It would never become `'a`, because there is no need for it too -- we have a constraint that -`'a: '0`, but that just puts a "cap" on how *large* `'0` can grow to -become. Since we compute the *minimal* value that we can, we are happy -to leave `'0` as being just equal to the liveness set. This is where -member constraints come in. +`'a: '0`, but that just puts a "cap" on how *large* `'0` can grow to become. +Since we compute the *minimal* value that we can, we are happy +to leave `'0` as being just equal to the liveness set. +This is where member constraints come in. ## Choices are always lifetime parameters At present, the "choice" regions from a member constraint are always lifetime parameters from the current function. As of March 2026, this falls out from the placement of impl Trait, though in the future it may not -be the case. We take some advantage of this fact, as it simplifies the current -code. In particular, we don't have to consider a case like `'0 member of ['1, +be the case. +We take some advantage of this fact, as it simplifies the current code. +In particular, we don't have to consider a case like `'0 member of ['1, 'static]`, in which the value of both `'0` and `'1` are being inferred and hence -changing. See [rust-lang/rust#61773][#61773] for more information. +changing. +See [rust-lang/rust#61773][#61773] for more information. [#61773]: https://github.com/rust-lang/rust/issues/61773 ## Applying member constraints -Member constraints are a bit more complex than other forms of -constraints. This is because they have a "or" quality to them -- that +Member constraints are a bit more complex than other forms of constraints. +This is because they have a "or" quality to them -- that is, they describe multiple choices that we must select from. E.g., in our example constraint `'0 member of ['a, 'b, 'static]`, it might be -that `'0` is equal to `'a`, `'b`, *or* `'static`. How can we pick the -correct one? What we currently do is to look for a *minimal choice* --- if we find one, then we will grow `'0` to be equal to that minimal -choice. To find that minimal choice, we take two factors into +that `'0` is equal to `'a`, `'b`, *or* `'static`. +How can we pick the correct one? + What we currently do is to look for a *minimal choice* +-- if we find one, then we will grow `'0` to be equal to that minimal choice. +To find that minimal choice, we take two factors into consideration: lower and upper bounds. ### Lower bounds @@ -121,30 +127,34 @@ apply member constraints, we've already *computed* the lower bounds of `'0` because we computed its minimal value (or at least, the lower bounds considering everything but member constraints). -Let `LB` be the current value of `'0`. We know then that `'0: LB` must -hold, whatever the final value of `'0` is. Therefore, we can rule out +Let `LB` be the current value of `'0`. +We know then that `'0: LB` must hold, whatever the final value of `'0` is. +Therefore, we can rule out any choice `'choice` where `'choice: LB` does not hold. -Unfortunately, in our example, this is not very helpful. The lower -bound for `'0` will just be the liveness set `{L}`, and we know that -all the lifetime parameters outlive that set. So we are left with the -same set of choices here. (But in other examples, particularly those +Unfortunately, in our example, this is not very helpful. +The lower bound for `'0` will just be the liveness set `{L}`, and we know that +all the lifetime parameters outlive that set. +So we are left with the same set of choices here. +(But in other examples, particularly those with different variance, lower bound constraints may be relevant.) ### Upper bounds The *upper bounds* are those lifetimes that *must outlive* `'0` -- i.e., that `'0` must be *smaller* than. In our example, this would be -`'a`, because we have the constraint that `'a: '0`. In more complex -examples, the chain may be more indirect. +`'a`, because we have the constraint that `'a: '0`. +In more complex examples, the chain may be more indirect. We can use upper bounds to rule out members in a very similar way to -lower bounds. If UB is some upper bound, then we know that `UB: +lower bounds. +If UB is some upper bound, then we know that `UB: '0` must hold, so we can rule out any choice `'choice` where `UB: 'choice` does not hold. In our example, we would be able to reduce our choice set from `['a, -'b, 'static]` to just `['a]`. This is because `'0` has an upper bound +'b, 'static]` to just `['a]`. +This is because `'0` has an upper bound of `'a`, and neither `'a: 'b` nor `'a: 'static` is known to hold. (For notes on how we collect upper bounds in the implementation, see @@ -153,39 +163,45 @@ of `'a`, and neither `'a: 'b` nor `'a: 'static` is known to hold. ### Minimal choice After applying lower and upper bounds, we can still sometimes have -multiple possibilities. For example, imagine a variant of our example -using types with the opposite variance. In that case, we would have -the constraint `'0: 'a` instead of `'a: '0`. Hence the current value -of `'0` would be `{L, 'a}`. Using this as a lower bound, we would be +multiple possibilities. +For example, imagine a variant of our example +using types with the opposite variance. +In that case, we would have the constraint `'0: 'a` instead of `'a: '0`. +Hence the current value of `'0` would be `{L, 'a}`. +Using this as a lower bound, we would be able to narrow down the member choices to `['a, 'static]` because `'b: -'a` is not known to hold (but `'a: 'a` and `'static: 'a` do hold). We -would not have any upper bounds, so that would be our final set of choices. +'a` is not known to hold (but `'a: 'a` and `'static: 'a` do hold). +We would not have any upper bounds, so that would be our final set of choices. In that case, we apply the **minimal choice** rule -- basically, if -one of our choices if smaller than the others, we can use that. In -this case, we would opt for `'a` (and not `'static`). +one of our choices if smaller than the others, we can use that. +In this case, we would opt for `'a` (and not `'static`). This choice is consistent with the general 'flow' of region propagation, which always aims to compute a minimal value for the -region being inferred. However, it is somewhat arbitrary. +region being inferred. +However, it is somewhat arbitrary. ### Collecting upper bounds in the implementation In practice, computing upper bounds is a bit inconvenient, because our -data structures are setup for the opposite. What we do is to compute +data structures are setup for the opposite. +What we do is to compute the **reverse SCC graph** (we do this lazily and cache the result) -- -that is, a graph where `'a: 'b` induces an edge `SCC('b) -> -SCC('a)`. Like the normal SCC graph, this is a DAG. We can then do a -depth-first search starting from `SCC('0)` in this graph. This will -take us to all the SCCs that must outlive `'0`. +that is, a graph where `'a: 'b` induces an edge `SCC('b) -> SCC('a)`. +Like the normal SCC graph, this is a DAG. +We can then do a depth-first search starting from `SCC('0)` in this graph. +This will take us to all the SCCs that must outlive `'0`. One wrinkle is that, as we walk the "upper bound" SCCs, their values -will not yet have been fully computed. However, we **have** already +will not yet have been fully computed. +However, we **have** already applied their liveness constraints, so we have some information about -their value. In particular, for any regions representing lifetime +their value. +In particular, for any regions representing lifetime parameters, their value will contain themselves (i.e., the initial -value for `'a` includes `'a` and the value for `'b` contains `'b`). So -we can collect all of the lifetime parameters that are reachable, +value for `'a` includes `'a` and the value for `'b` contains `'b`). +So we can collect all of the lifetime parameters that are reachable, which is precisely what we are interested in. From 539bbfb6258ff8653cac58808ad42f0c6af2a086 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:35:04 +0100 Subject: [PATCH 08/14] whitespace --- src/borrow-check/region-inference/member-constraints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borrow-check/region-inference/member-constraints.md b/src/borrow-check/region-inference/member-constraints.md index 75891b419..21a56da9b 100644 --- a/src/borrow-check/region-inference/member-constraints.md +++ b/src/borrow-check/region-inference/member-constraints.md @@ -114,7 +114,7 @@ is, they describe multiple choices that we must select from. E.g., in our example constraint `'0 member of ['a, 'b, 'static]`, it might be that `'0` is equal to `'a`, `'b`, *or* `'static`. How can we pick the correct one? - What we currently do is to look for a *minimal choice* +What we currently do is to look for a *minimal choice* -- if we find one, then we will grow `'0` to be equal to that minimal choice. To find that minimal choice, we take two factors into consideration: lower and upper bounds. From fa09a2c4d56e6c6432d34fdf557ab28a45943a82 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:36:09 +0100 Subject: [PATCH 09/14] sembr src/early-late-parameters.md --- src/early-late-parameters.md | 70 ++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/early-late-parameters.md b/src/early-late-parameters.md index 7651dee4a..f1e6a79d7 100644 --- a/src/early-late-parameters.md +++ b/src/early-late-parameters.md @@ -11,7 +11,8 @@ introduced: [Intermingled parameter lists] and [Intermingled parameter lists, ta ## What does it mean to be "early" bound or "late" bound -Every function definition has a corresponding ZST that implements the `Fn*` traits known as a [function item type][function_item_type]. This part of the chapter will talk a little bit about the "desugaring" of function item types as it is useful context for explaining the difference between early bound and late bound generic parameters. +Every function definition has a corresponding ZST that implements the `Fn*` traits known as a [function item type][function_item_type]. +This part of the chapter will talk a little bit about the "desugaring" of function item types as it is useful context for explaining the difference between early bound and late bound generic parameters. Let's start with a very trivial example involving no generic parameters: @@ -51,7 +52,8 @@ impl Fn<(T,)> for FooFnItem { } ``` -Note that the function item type `FooFnItem` is generic over some type parameter `T` as defined on the function `foo`. However, not all generic parameters defined on functions are also defined on the function item type as demonstrated here: +Note that the function item type `FooFnItem` is generic over some type parameter `T` as defined on the function `foo`. +However, not all generic parameters defined on functions are also defined on the function item type as demonstrated here: ```rust fn foo<'a, T: Sized>(a: &'a T) -> &'a T { # a @@ -74,9 +76,10 @@ Generic parameters not all being defined on the function item type means that th 1. Naming the function (e.g. `let a = foo;`) the arguments for `FooFnItem` are provided. 2. Calling the function (e.g. `a(&10);`) any parameters defined on the builtin impl are provided. -This two-step system is where the early vs late naming scheme comes from, early bound parameters are provided in the *earliest* step (naming the function), whereas late bound parameters are provided in the *latest* step (calling the function). +This two-step system is where the early vs late naming scheme comes from, early bound parameters are provided in the *earliest* step (naming the function), whereas late bound parameters are provided in the *latest* step (calling the function). -Looking at the desugaring from the previous example we can tell that `T` is an early bound type parameter and `'a` is a late bound lifetime parameter as `T` is present on the function item type but `'a` is not. See this example of calling `foo` annotated with where each generic parameter has an argument provided: +Looking at the desugaring from the previous example we can tell that `T` is an early bound type parameter and `'a` is a late bound lifetime parameter as `T` is present on the function item type but `'a` is not. +See this example of calling `foo` annotated with where each generic parameter has an argument provided: ```rust fn foo<'a, T: Sized>(a: &'a T) -> &'a T { # a @@ -98,13 +101,15 @@ my_func(&String::new()); ### Higher ranked function pointers and trait bounds -A generic parameter being late bound allows for more flexible usage of the function item. For example if we have some function `foo` with an early bound lifetime parameter and some function `bar` with a late bound lifetime parameter `'a` we would have the following builtin `Fn` impls: +A generic parameter being late bound allows for more flexible usage of the function item. +For example if we have some function `foo` with an early bound lifetime parameter and some function `bar` with a late bound lifetime parameter `'a` we would have the following builtin `Fn` impls: ```rust,ignore impl<'a> Fn<(&'a String,)> for FooFnItem<'a> { /* ... */ } impl<'a> Fn<(&'a String,)> for BarFnItem { /* ... */ } ``` -The `bar` function has a strictly more flexible signature as the function item type can be called with a borrow with *any* lifetime, whereas the `foo` function item type would only be callable with a borrow with the same lifetime on the function item type. We can show this by simply trying to call `foo`'s function item type multiple times with different lifetimes: +The `bar` function has a strictly more flexible signature as the function item type can be called with a borrow with *any* lifetime, whereas the `foo` function item type would only be callable with a borrow with the same lifetime on the function item type. +We can show this by simply trying to call `foo`'s function item type multiple times with different lifetimes: ```rust // The `'a: 'a` bound forces this lifetime to be early bound. @@ -125,9 +130,12 @@ f(&String::new()); f(&String::new()); ``` -In this example we call `foo`'s function item type twice, each time with a borrow of a temporary. These two borrows could not possible have lifetimes that overlap as the temporaries are only alive during the function call, not after. The lifetime parameter on `foo` being early bound requires all callers of `f` to provide a borrow with the same lifetime, as this is not possible the borrow checker errors. +In this example we call `foo`'s function item type twice, each time with a borrow of a temporary. +These two borrows could not possible have lifetimes that overlap as the temporaries are only alive during the function call, not after. +The lifetime parameter on `foo` being early bound requires all callers of `f` to provide a borrow with the same lifetime, as this is not possible the borrow checker errors. -If the lifetime parameter on `foo` was late bound this would be able to compile as each caller could provide a different lifetime argument for its borrow. See the following example which demonstrates this using the `bar` function defined above: +If the lifetime parameter on `foo` was late bound this would be able to compile as each caller could provide a different lifetime argument for its borrow. +See the following example which demonstrates this using the `bar` function defined above: ```rust # fn foo<'a: 'a>(b: &'a String) -> &'a String { b } @@ -143,7 +151,8 @@ b(&String::new()); b(&String::new()); ``` -This is reflected in the ability to coerce function item types to higher ranked function pointers and prove higher ranked `Fn` trait bounds. We can demonstrate this with the following example: +This is reflected in the ability to coerce function item types to higher ranked function pointers and prove higher ranked `Fn` trait bounds. +We can demonstrate this with the following example: ```rust // The `'a: 'a` bound forces this lifetime to be early bound. fn foo<'a: 'a>(b: &'a String) -> &'a String { b } @@ -170,7 +179,8 @@ fn higher_ranked_fn_ptr() { } ``` -In both of these cases the borrow checker errors as it does not consider `foo_fn_item` to be callable with a borrow of any lifetime. This is due to the fact that the lifetime parameter on `foo` is early bound, causing `foo_fn_item` to have a type of `FooFnItem<'_>` which (as demonstrated by the desugared `Fn` impl) is only callable with a borrow of the same lifetime `'_`. +In both of these cases the borrow checker errors as it does not consider `foo_fn_item` to be callable with a borrow of any lifetime. +This is due to the fact that the lifetime parameter on `foo` is early bound, causing `foo_fn_item` to have a type of `FooFnItem<'_>` which (as demonstrated by the desugared `Fn` impl) is only callable with a borrow of the same lifetime `'_`. ### Turbofishing in the presence of late bound parameters @@ -188,14 +198,16 @@ fn foo<'a>(b: &'a u32) -> &'a u32 { b } let f /* : FooFnItem */ = foo::<'static>; ``` -The above example errors as the lifetime parameter `'a` is late bound and so cannot be instantiated as part of the "naming a function" step. If we make the lifetime parameter early bound we will see this code start to compile: +The above example errors as the lifetime parameter `'a` is late bound and so cannot be instantiated as part of the "naming a function" step. +If we make the lifetime parameter early bound we will see this code start to compile: ```rust fn foo<'a: 'a>(b: &'a u32) -> &'a u32 { b } let f /* : FooFnItem<'static> */ = foo::<'static>; ``` -What the current implementation of the compiler aims to do is error when specifying lifetime arguments to a function that has both early *and* late bound lifetime parameters. In practice, due to excessive breakage, some cases are actually only future compatibility warnings ([#42868](https://github.com/rust-lang/rust/issues/42868)): +What the current implementation of the compiler aims to do is error when specifying lifetime arguments to a function that has both early *and* late bound lifetime parameters. +In practice, due to excessive breakage, some cases are actually only future compatibility warnings ([#42868](https://github.com/rust-lang/rust/issues/42868)): - When the amount of lifetime arguments is the same as the number of early bound lifetime parameters a FCW is emitted instead of an error - An error is always downgraded to a FCW when using method call syntax @@ -287,7 +299,8 @@ Foo::inherent_function::<'static, 'static, 'static>(&(), &()); free_function::<'static, 'static, 'static>(&(), &()); ``` -Even when specifying enough lifetime arguments for both the late and early bound lifetime parameter, these arguments are not actually used to annotate the lifetime provided to late bound parameters. We can demonstrate this by turbofishing `'static` to a function while providing a non-static borrow: +Even when specifying enough lifetime arguments for both the late and early bound lifetime parameter, these arguments are not actually used to annotate the lifetime provided to late bound parameters. +We can demonstrate this by turbofishing `'static` to a function while providing a non-static borrow: ```rust struct Foo; @@ -302,7 +315,8 @@ This compiles even though the `&String::new()` function argument does not have a ### Liveness of types with late bound parameters -When checking type outlives bounds involving function item types we take into account early bound parameters. For example: +When checking type outlives bounds involving function item types we take into account early bound parameters. +For example: ```rust fn foo(_: T) {} @@ -315,9 +329,11 @@ fn bar() { } ``` -As the type parameter `T` is early bound, the desugaring of the function item type for `foo` would look something like `struct FooFnItem`. Then in order for `FooFnItem: 'static` to hold we must also require `T: 'static` to hold as otherwise we would wind up with soundness bugs. +As the type parameter `T` is early bound, the desugaring of the function item type for `foo` would look something like `struct FooFnItem`. +Then in order for `FooFnItem: 'static` to hold we must also require `T: 'static` to hold as otherwise we would wind up with soundness bugs. -Unfortunately, due to bugs in the compiler, we do not take into account early bound *lifetimes*, which is the cause of the open soundness bug [#84366](https://github.com/rust-lang/rust/issues/84366). This means that it's impossible to demonstrate a "difference" between early/late bound parameters for liveness/type outlives bounds as the only kind of generic parameters that are able to be late bound are lifetimes which are handled incorrectly. +Unfortunately, due to bugs in the compiler, we do not take into account early bound *lifetimes*, which is the cause of the open soundness bug [#84366](https://github.com/rust-lang/rust/issues/84366). +This means that it's impossible to demonstrate a "difference" between early/late bound parameters for liveness/type outlives bounds as the only kind of generic parameters that are able to be late bound are lifetimes which are handled incorrectly. Regardless, in theory the code example below *should* demonstrate such a difference once [#84366](https://github.com/rust-lang/rust/issues/84366) is fixed: ```rust @@ -341,17 +357,20 @@ fn bar<'b>() { ### Must be a lifetime parameter -Type and Const parameters are not able to be late bound as we do not have a way to support types such as `dyn for Fn(Box)` or `for fn(Box)`. Calling such types requires being able to monomorphize the underlying function which is not possible with indirection through dynamic dispatch. +Type and Const parameters are not able to be late bound as we do not have a way to support types such as `dyn for Fn(Box)` or `for fn(Box)`. +Calling such types requires being able to monomorphize the underlying function which is not possible with indirection through dynamic dispatch. ### Must not be used in a where clause -Currently when a generic parameter is used in a where clause it must be early bound. For example: +Currently when a generic parameter is used in a where clause it must be early bound. +For example: ```rust # trait Trait<'a> {} fn foo<'a, T: Trait<'a>>(_: &'a String, _: T) {} ``` -In this example the lifetime parameter `'a` is considered to be early bound as it appears in the where clause `T: Trait<'a>`. This is true even for "trivial" where clauses such as `'a: 'a` or those implied by wellformedness of function arguments, for example: +In this example the lifetime parameter `'a` is considered to be early bound as it appears in the where clause `T: Trait<'a>`. +This is true even for "trivial" where clauses such as `'a: 'a` or those implied by wellformedness of function arguments, for example: ```rust fn foo<'a: 'a>(_: &'a String) {} fn bar<'a, T: 'a>(_: &'a T) {} @@ -375,9 +394,12 @@ f(&String::new()); At *some point* during type checking an error should be emitted for this code as `String` does not implement `Trait` for any lifetime. -If the lifetime `'a` were late bound then this becomes difficult to check. When naming `foo` we do not know what lifetime should be used as part of the `T: Trait<'a>` trait bound as it has not yet been instantiated. When coercing the function item type to a function pointer we have no way of tracking the `String: Trait<'a>` trait bound that must be proven when calling the function. +If the lifetime `'a` were late bound then this becomes difficult to check. +When naming `foo` we do not know what lifetime should be used as part of the `T: Trait<'a>` trait bound as it has not yet been instantiated. +When coercing the function item type to a function pointer we have no way of tracking the `String: Trait<'a>` trait bound that must be proven when calling the function. -If the lifetime `'a` is early bound (which it is in the current implementation in rustc), then the trait bound can be checked when naming the function `foo`. Requiring parameters used in where clauses to be early bound gives a natural place to check where clauses defined on the function. +If the lifetime `'a` is early bound (which it is in the current implementation in rustc), then the trait bound can be checked when naming the function `foo`. +Requiring parameters used in where clauses to be early bound gives a natural place to check where clauses defined on the function. Finally, we do not require lifetimes to be early bound if they are used in *implied bounds*, for example: ```rust @@ -388,11 +410,13 @@ f(&String::new()); f(&String::new()); ``` -This code compiles, demonstrating that the lifetime parameter is late bound, even though `'a` is used in the type `&'a T` which implicitly requires `T: 'a` to hold. Implied bounds can be treated specially as any types introducing implied bounds are in the signature of the function pointer type, which means that when calling the function we know to prove `T: 'a`. +This code compiles, demonstrating that the lifetime parameter is late bound, even though `'a` is used in the type `&'a T` which implicitly requires `T: 'a` to hold. +Implied bounds can be treated specially as any types introducing implied bounds are in the signature of the function pointer type, which means that when calling the function we know to prove `T: 'a`. ### Must be constrained by argument types -It is important that builtin impls on function item types do not wind up with unconstrained generic parameters as this can lead to unsoundness. This is the same kind of restriction as applies to user written impls, for example the following code results in an error: +It is important that builtin impls on function item types do not wind up with unconstrained generic parameters as this can lead to unsoundness. +This is the same kind of restriction as applies to user written impls, for example the following code results in an error: ```rust trait Trait { type Assoc; From efa85fca1b0645278c580ebe01a2fb78ccb43135 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Mon, 23 Mar 2026 23:51:11 +0100 Subject: [PATCH 10/14] improve early-late-parameters.md --- src/early-late-parameters.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/early-late-parameters.md b/src/early-late-parameters.md index f1e6a79d7..d78d5331b 100644 --- a/src/early-late-parameters.md +++ b/src/early-late-parameters.md @@ -37,7 +37,7 @@ The builtin impls for the `FnMut`/`FnOnce` traits as well as the impls for `Copy A slightly more complicated example would involve introducing generic parameters to the function: ```rust -fn foo(a: T) -> T { +fn foo(a: T) -> T { # a /* snip */ } @@ -73,7 +73,7 @@ impl<'a, T: Sized> Fn<(&'a T,)> for FooFnItem { The lifetime parameter `'a` from the function `foo` is not present on the function item type `FooFnItem` and is instead introduced on the builtin impl solely for use in representing the argument types. Generic parameters not all being defined on the function item type means that there are two steps where generic arguments are provided when calling a function. -1. Naming the function (e.g. `let a = foo;`) the arguments for `FooFnItem` are provided. +1. Naming the function (e.g. `let a = foo;`) the arguments for `FooFnItem` are provided. 2. Calling the function (e.g. `a(&10);`) any parameters defined on the builtin impl are provided. This two-step system is where the early vs late naming scheme comes from, early bound parameters are provided in the *earliest* step (naming the function), whereas late bound parameters are provided in the *latest* step (calling the function). @@ -99,10 +99,10 @@ my_func(&String::new()); ## Differences between early and late bound parameters -### Higher ranked function pointers and trait bounds +### Higher ranked function pointers and trait bounds A generic parameter being late bound allows for more flexible usage of the function item. -For example if we have some function `foo` with an early bound lifetime parameter and some function `bar` with a late bound lifetime parameter `'a` we would have the following builtin `Fn` impls: +For example, if we have some function `foo` with an early bound lifetime parameter and some function `bar` with a late bound lifetime parameter `'a`, we would have the following builtin `Fn` impls: ```rust,ignore impl<'a> Fn<(&'a String,)> for FooFnItem<'a> { /* ... */ } impl<'a> Fn<(&'a String,)> for BarFnItem { /* ... */ } @@ -130,12 +130,12 @@ f(&String::new()); f(&String::new()); ``` -In this example we call `foo`'s function item type twice, each time with a borrow of a temporary. -These two borrows could not possible have lifetimes that overlap as the temporaries are only alive during the function call, not after. +In this example, we call `foo`'s function item type twice, each time with a borrow of a temporary. +These two borrows could not possibly have lifetimes that overlap as the temporaries are only alive during the function call, not after. The lifetime parameter on `foo` being early bound requires all callers of `f` to provide a borrow with the same lifetime, as this is not possible the borrow checker errors. -If the lifetime parameter on `foo` was late bound this would be able to compile as each caller could provide a different lifetime argument for its borrow. -See the following example which demonstrates this using the `bar` function defined above: +If the lifetime parameter on `foo` was late bound, this would be able to compile as each caller could provide a different lifetime argument for its borrow. +See the following example, which demonstrates this using the `bar` function defined above: ```rust # fn foo<'a: 'a>(b: &'a String) -> &'a String { b } @@ -172,14 +172,14 @@ fn higher_ranked_trait_bound() { fn higher_ranked_fn_ptr() { let bar_fn_item = bar; let fn_ptr: for<'a> fn(&'a String) -> &'a String = bar_fn_item; - + let foo_fn_item = foo::<'_>; // errors let fn_ptr: for<'a> fn(&'a String) -> &'a String = foo_fn_item; } ``` -In both of these cases the borrow checker errors as it does not consider `foo_fn_item` to be callable with a borrow of any lifetime. +In both of these cases, the borrow checker errors as it does not consider `foo_fn_item` to be callable with a borrow of any lifetime. This is due to the fact that the lifetime parameter on `foo` is early bound, causing `foo_fn_item` to have a type of `FooFnItem<'_>` which (as demonstrated by the desugared `Fn` impl) is only callable with a borrow of the same lifetime `'_`. ### Turbofishing in the presence of late bound parameters @@ -208,7 +208,7 @@ let f /* : FooFnItem<'static> */ = foo::<'static>; What the current implementation of the compiler aims to do is error when specifying lifetime arguments to a function that has both early *and* late bound lifetime parameters. In practice, due to excessive breakage, some cases are actually only future compatibility warnings ([#42868](https://github.com/rust-lang/rust/issues/42868)): -- When the amount of lifetime arguments is the same as the number of early bound lifetime parameters a FCW is emitted instead of an error +- When the amount of lifetime arguments is the same as the number of early bound lifetime parameters, a FCW is emitted instead of an error - An error is always downgraded to a FCW when using method call syntax To demonstrate this we can write out the different kinds of functions and give them both a late and early bound lifetime: @@ -330,7 +330,7 @@ fn bar() { ``` As the type parameter `T` is early bound, the desugaring of the function item type for `foo` would look something like `struct FooFnItem`. -Then in order for `FooFnItem: 'static` to hold we must also require `T: 'static` to hold as otherwise we would wind up with soundness bugs. +Then, in order for `FooFnItem: 'static` to hold, we must also require `T: 'static` to hold as otherwise we would wind up with soundness bugs. Unfortunately, due to bugs in the compiler, we do not take into account early bound *lifetimes*, which is the cause of the open soundness bug [#84366](https://github.com/rust-lang/rust/issues/84366). This means that it's impossible to demonstrate a "difference" between early/late bound parameters for liveness/type outlives bounds as the only kind of generic parameters that are able to be late bound are lifetimes which are handled incorrectly. From 4e291aa76d95859f852391e449a816288da16fc1 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Tue, 24 Mar 2026 00:03:31 +0100 Subject: [PATCH 11/14] sembr src/parallel-rustc.md --- src/parallel-rustc.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/parallel-rustc.md b/src/parallel-rustc.md index f83aaa639..2fa266338 100644 --- a/src/parallel-rustc.md +++ b/src/parallel-rustc.md @@ -11,7 +11,8 @@ Tracking issue: As of November 2024, most of the rust compiler is now parallelized. -- The codegen part is executed concurrently by default. You can use the `-C +- The codegen part is executed concurrently by default. + You can use the `-C codegen-units=n` option to control the number of concurrent tasks. - The parts after HIR lowering to codegen such as type checking, borrowing checking, and mir optimization are parallelized in the nightly version. @@ -31,17 +32,19 @@ The following sections are kept for now but are quite outdated. ## Code generation During monomorphization the compiler splits up all the code to -be generated into smaller chunks called _codegen units_. These are then generated by -independent instances of LLVM running in parallel. At the end, the linker -is run to combine all the codegen units together into one binary. This process -occurs in the [`rustc_codegen_ssa::base`] module. +be generated into smaller chunks called _codegen units_. +These are then generated by independent instances of LLVM running in parallel. +At the end, the linker +is run to combine all the codegen units together into one binary. +This process occurs in the [`rustc_codegen_ssa::base`] module. [`rustc_codegen_ssa::base`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_codegen_ssa/base/index.html ## Data structures The underlying thread-safe data-structures used in the parallel compiler -can be found in the [`rustc_data_structures::sync`] module. These data structures +can be found in the [`rustc_data_structures::sync`] module. +These data structures are implemented differently depending on whether `parallel-compiler` is true. | data structure | parallel | non-parallel | @@ -68,14 +71,18 @@ are implemented differently depending on whether `parallel-compiler` is true. ### WorkerLocal -[`WorkerLocal`] is a special data structure implemented for parallel compilers. It -holds worker-locals values for each thread in a thread pool. You can only +[`WorkerLocal`] is a special data structure implemented for parallel compilers. +It holds worker-locals values for each thread in a thread pool. +You can only access the worker local value through the `Deref` `impl` on the thread pool it -was constructed on. It panics otherwise. +was constructed on. +It panics otherwise. `WorkerLocal` is used to implement the `Arena` allocator in the parallel -environment, which is critical in parallel queries. Its implementation is -located in the [`rustc_data_structures::sync::worker_local`] module. However, +environment, which is critical in parallel queries. +Its implementation is +located in the [`rustc_data_structures::sync::worker_local`] module. +However, in the non-parallel compiler, it is implemented as `(OneThread)`, whose `T` can be accessed directly through `Deref::deref`. @@ -85,7 +92,8 @@ can be accessed directly through `Deref::deref`. ## Parallel iterator The parallel iterators provided by the [`rayon`] crate are easy ways to -implement parallelism. In the current implementation of the parallel compiler +implement parallelism. +In the current implementation of the parallel compiler we use a custom [fork][rustc-rayon] of `rayon` to run tasks in parallel. Some iterator functions are implemented to run loops in parallel @@ -142,15 +150,17 @@ When a query `foo` is evaluated, the cache table for `foo` is locked. start evaluating. - If there *is* another query invocation for the same key in progress, we release the lock, and just block the thread until the other invocation has - computed the result we are waiting for. **Cycle error detection** in the parallel - compiler requires more complex logic than in single-threaded mode. When + computed the result we are waiting for. + **Cycle error detection** in the parallel + compiler requires more complex logic than in single-threaded mode. + When worker threads in parallel queries stop making progress due to interdependence, the compiler uses an extra thread *(named deadlock handler)* to detect, remove and report the cycle error. The parallel query feature still has implementation to do, most of which is -related to the previous `Data Structures` and `Parallel Iterators`. See [this -open feature tracking issue][tracking]. +related to the previous `Data Structures` and `Parallel Iterators`. +See [this open feature tracking issue][tracking]. ## Rustdoc From 3946ce9b9afa442efa1f2be088a302ec49876551 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Tue, 24 Mar 2026 00:06:00 +0100 Subject: [PATCH 12/14] improve parallel-rustc.md --- src/parallel-rustc.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/parallel-rustc.md b/src/parallel-rustc.md index 2fa266338..68be36a0c 100644 --- a/src/parallel-rustc.md +++ b/src/parallel-rustc.md @@ -64,7 +64,7 @@ are implemented differently depending on whether `parallel-compiler` is true. or the authoring of persistent documentation covering the specific of the invariants, the atomicity, and the lock orderings. -- On the other hand, we still need to figure out what other invariants +- On the other hand, we still need to figure out what other invariants during compilation might not hold in parallel compilation. [`rustc_data_structures::sync`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_data_structures/sync/index.html @@ -93,10 +93,10 @@ can be accessed directly through `Deref::deref`. The parallel iterators provided by the [`rayon`] crate are easy ways to implement parallelism. -In the current implementation of the parallel compiler +In the current implementation of the parallel compiler, we use a custom [fork][rustc-rayon] of `rayon` to run tasks in parallel. -Some iterator functions are implemented to run loops in parallel +Some iterator functions are implemented to run loops in parallel when `parallel-compiler` is true. | Function(Omit `Send` and `Sync`) | Introduction | Owning Module | @@ -154,8 +154,8 @@ When a query `foo` is evaluated, the cache table for `foo` is locked. **Cycle error detection** in the parallel compiler requires more complex logic than in single-threaded mode. When - worker threads in parallel queries stop making progress due to interdependence, - the compiler uses an extra thread *(named deadlock handler)* to detect, remove and + worker threads in parallel queries stop making progress due to interdependence, + the compiler uses an extra thread *(named deadlock handler)* to detect, remove and report the cycle error. The parallel query feature still has implementation to do, most of which is From 50a4342c368d55f62fc0e7439e34427598e518c2 Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Tue, 24 Mar 2026 00:09:52 +0100 Subject: [PATCH 13/14] sembr src/tests/ci.md --- src/tests/ci.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tests/ci.md b/src/tests/ci.md index 516119449..5581b7eb9 100644 --- a/src/tests/ci.md +++ b/src/tests/ci.md @@ -109,11 +109,13 @@ The live results can be seen on [the GitHub Actions workflows page]. At any given time, at most a single `auto` build is being executed. Find out more in [Merging PRs serially with bors](#merging-prs-serially-with-bors). -Normally, when an auto job fails, the whole CI workflow immediately ends. However, it can be useful to +Normally, when an auto job fails, the whole CI workflow immediately ends. +However, it can be useful to create auto jobs that are "non-blocking", or optional, to test them on CI for some time before blocking -merges on them. This can be useful if those jobs can be flaky. +merges on them. +This can be useful if those jobs can be flaky. -To do that, prefix such a job with `optional-`, and set `continue_on_error: true` for it in [`jobs.yml`]. +To do that, prefix such a job with `optional-`, and set `continue_on_error: true` for it in [`jobs.yml`]. [platform tiers]: https://forge.rust-lang.org/release/platform-support.html#rust-platform-support [auto]: https://github.com/rust-lang/rust/tree/automation/bors/auto From 01b1f09d685408764b89aea5beea116a2327105f Mon Sep 17 00:00:00 2001 From: Tshepang Mbambo Date: Tue, 24 Mar 2026 00:14:20 +0100 Subject: [PATCH 14/14] more area to click --- src/parallel-rustc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parallel-rustc.md b/src/parallel-rustc.md index 68be36a0c..c4e066306 100644 --- a/src/parallel-rustc.md +++ b/src/parallel-rustc.md @@ -94,7 +94,7 @@ can be accessed directly through `Deref::deref`. The parallel iterators provided by the [`rayon`] crate are easy ways to implement parallelism. In the current implementation of the parallel compiler, -we use a custom [fork][rustc-rayon] of `rayon` to run tasks in parallel. +we use [a custom fork of `rayon`][rustc-rayon] to run tasks in parallel. Some iterator functions are implemented to run loops in parallel when `parallel-compiler` is true.