From 43327b5da6bc4ccbddebb935b1bcf362ccb8f993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Fri, 14 Feb 2025 11:06:06 +0000 Subject: [PATCH 1/9] switch polonius compare-mode to polonius=next --- src/tools/compiletest/src/runtest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index f283a625f977c..debf67e2741dd 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -1766,7 +1766,7 @@ impl<'test> TestCx<'test> { match self.config.compare_mode { Some(CompareMode::Polonius) => { - rustc.args(&["-Zpolonius"]); + rustc.args(&["-Zpolonius=next"]); } Some(CompareMode::NextSolver) => { rustc.args(&["-Znext-solver"]); From f4094ea25236bc4ba3d9b7b03d2674ba7ab92907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Mon, 24 Mar 2025 07:09:18 +0000 Subject: [PATCH 2/9] update test expectations for boring locals + dropckoutlives interactions The suboptimal error only appears with NLLs due to liveness differences where polonius cannot have as many boring locals. Sometimes this causes NLLs to emit a duplicate error as well. --- ...err => dropck-normalize-errors.nll.stderr} | 24 +++---- .../dropck-normalize-errors.polonius.stderr | 64 +++++++++++++++++++ tests/ui/drop/dropck-normalize-errors.rs | 6 +- ... => hir-wf-check-erase-regions.nll.stderr} | 8 +-- ...hir-wf-check-erase-regions.polonius.stderr | 39 +++++++++++ tests/ui/wf/hir-wf-check-erase-regions.rs | 6 +- 6 files changed, 129 insertions(+), 18 deletions(-) rename tests/ui/drop/{dropck-normalize-errors.stderr => dropck-normalize-errors.nll.stderr} (82%) create mode 100644 tests/ui/drop/dropck-normalize-errors.polonius.stderr rename tests/ui/wf/{hir-wf-check-erase-regions.stderr => hir-wf-check-erase-regions.nll.stderr} (91%) create mode 100644 tests/ui/wf/hir-wf-check-erase-regions.polonius.stderr diff --git a/tests/ui/drop/dropck-normalize-errors.stderr b/tests/ui/drop/dropck-normalize-errors.nll.stderr similarity index 82% rename from tests/ui/drop/dropck-normalize-errors.stderr rename to tests/ui/drop/dropck-normalize-errors.nll.stderr index 2bb5909c6b226..b008daa51a33e 100644 --- a/tests/ui/drop/dropck-normalize-errors.stderr +++ b/tests/ui/drop/dropck-normalize-errors.nll.stderr @@ -1,44 +1,44 @@ error[E0277]: the trait bound `NonImplementedStruct: NonImplementedTrait` is not satisfied in `ADecoder<'a>` - --> $DIR/dropck-normalize-errors.rs:15:28 + --> $DIR/dropck-normalize-errors.rs:19:28 | LL | fn make_a_decoder<'a>() -> ADecoder<'a> { | ^^^^^^^^^^^^ within `ADecoder<'a>`, the trait `NonImplementedTrait` is not implemented for `NonImplementedStruct` | help: this trait has no implementations, consider adding one - --> $DIR/dropck-normalize-errors.rs:7:1 + --> $DIR/dropck-normalize-errors.rs:11:1 | LL | trait NonImplementedTrait { | ^^^^^^^^^^^^^^^^^^^^^^^^^ note: required because it appears within the type `BDecoder` - --> $DIR/dropck-normalize-errors.rs:26:12 + --> $DIR/dropck-normalize-errors.rs:30:12 | LL | pub struct BDecoder { | ^^^^^^^^ note: required because it appears within the type `ADecoder<'a>` - --> $DIR/dropck-normalize-errors.rs:12:12 + --> $DIR/dropck-normalize-errors.rs:16:12 | LL | pub struct ADecoder<'a> { | ^^^^^^^^ = note: the return type of a function must have a statically known size error[E0277]: the trait bound `NonImplementedStruct: NonImplementedTrait` is not satisfied in `BDecoder` - --> $DIR/dropck-normalize-errors.rs:23:20 + --> $DIR/dropck-normalize-errors.rs:27:20 | LL | type Decoder = BDecoder; | ^^^^^^^^ within `BDecoder`, the trait `NonImplementedTrait` is not implemented for `NonImplementedStruct` | help: this trait has no implementations, consider adding one - --> $DIR/dropck-normalize-errors.rs:7:1 + --> $DIR/dropck-normalize-errors.rs:11:1 | LL | trait NonImplementedTrait { | ^^^^^^^^^^^^^^^^^^^^^^^^^ note: required because it appears within the type `BDecoder` - --> $DIR/dropck-normalize-errors.rs:26:12 + --> $DIR/dropck-normalize-errors.rs:30:12 | LL | pub struct BDecoder { | ^^^^^^^^ note: required by a bound in `Decode::Decoder` - --> $DIR/dropck-normalize-errors.rs:4:5 + --> $DIR/dropck-normalize-errors.rs:8:5 | LL | type Decoder; | ^^^^^^^^^^^^^ required by this bound in `Decode::Decoder` @@ -48,25 +48,25 @@ LL | type Decoder: ?Sized; | ++++++++ error[E0277]: the trait bound `NonImplementedStruct: NonImplementedTrait` is not satisfied - --> $DIR/dropck-normalize-errors.rs:27:22 + --> $DIR/dropck-normalize-errors.rs:31:22 | LL | non_implemented: ::Assoc, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `NonImplementedTrait` is not implemented for `NonImplementedStruct` | help: this trait has no implementations, consider adding one - --> $DIR/dropck-normalize-errors.rs:7:1 + --> $DIR/dropck-normalize-errors.rs:11:1 | LL | trait NonImplementedTrait { | ^^^^^^^^^^^^^^^^^^^^^^^^^ error[E0277]: the trait bound `NonImplementedStruct: NonImplementedTrait` is not satisfied - --> $DIR/dropck-normalize-errors.rs:15:28 + --> $DIR/dropck-normalize-errors.rs:19:28 | LL | fn make_a_decoder<'a>() -> ADecoder<'a> { | ^^^^^^^^^^^^ the trait `NonImplementedTrait` is not implemented for `NonImplementedStruct` | help: this trait has no implementations, consider adding one - --> $DIR/dropck-normalize-errors.rs:7:1 + --> $DIR/dropck-normalize-errors.rs:11:1 | LL | trait NonImplementedTrait { | ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/drop/dropck-normalize-errors.polonius.stderr b/tests/ui/drop/dropck-normalize-errors.polonius.stderr new file mode 100644 index 0000000000000..f8674b8e34a42 --- /dev/null +++ b/tests/ui/drop/dropck-normalize-errors.polonius.stderr @@ -0,0 +1,64 @@ +error[E0277]: the trait bound `NonImplementedStruct: NonImplementedTrait` is not satisfied in `ADecoder<'a>` + --> $DIR/dropck-normalize-errors.rs:19:28 + | +LL | fn make_a_decoder<'a>() -> ADecoder<'a> { + | ^^^^^^^^^^^^ within `ADecoder<'a>`, the trait `NonImplementedTrait` is not implemented for `NonImplementedStruct` + | +help: this trait has no implementations, consider adding one + --> $DIR/dropck-normalize-errors.rs:11:1 + | +LL | trait NonImplementedTrait { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +note: required because it appears within the type `BDecoder` + --> $DIR/dropck-normalize-errors.rs:30:12 + | +LL | pub struct BDecoder { + | ^^^^^^^^ +note: required because it appears within the type `ADecoder<'a>` + --> $DIR/dropck-normalize-errors.rs:16:12 + | +LL | pub struct ADecoder<'a> { + | ^^^^^^^^ + = note: the return type of a function must have a statically known size + +error[E0277]: the trait bound `NonImplementedStruct: NonImplementedTrait` is not satisfied in `BDecoder` + --> $DIR/dropck-normalize-errors.rs:27:20 + | +LL | type Decoder = BDecoder; + | ^^^^^^^^ within `BDecoder`, the trait `NonImplementedTrait` is not implemented for `NonImplementedStruct` + | +help: this trait has no implementations, consider adding one + --> $DIR/dropck-normalize-errors.rs:11:1 + | +LL | trait NonImplementedTrait { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +note: required because it appears within the type `BDecoder` + --> $DIR/dropck-normalize-errors.rs:30:12 + | +LL | pub struct BDecoder { + | ^^^^^^^^ +note: required by a bound in `Decode::Decoder` + --> $DIR/dropck-normalize-errors.rs:8:5 + | +LL | type Decoder; + | ^^^^^^^^^^^^^ required by this bound in `Decode::Decoder` +help: consider relaxing the implicit `Sized` restriction + | +LL | type Decoder: ?Sized; + | ++++++++ + +error[E0277]: the trait bound `NonImplementedStruct: NonImplementedTrait` is not satisfied + --> $DIR/dropck-normalize-errors.rs:31:22 + | +LL | non_implemented: ::Assoc, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `NonImplementedTrait` is not implemented for `NonImplementedStruct` + | +help: this trait has no implementations, consider adding one + --> $DIR/dropck-normalize-errors.rs:11:1 + | +LL | trait NonImplementedTrait { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 3 previous errors + +For more information about this error, try `rustc --explain E0277`. diff --git a/tests/ui/drop/dropck-normalize-errors.rs b/tests/ui/drop/dropck-normalize-errors.rs index 793122bd33d4b..2ade63f27c5f6 100644 --- a/tests/ui/drop/dropck-normalize-errors.rs +++ b/tests/ui/drop/dropck-normalize-errors.rs @@ -1,5 +1,9 @@ // Test that we don't ICE when computing the drop types for +//@ ignore-compare-mode-polonius (explicit revisions) +//@ revisions: nll polonius +//@ [polonius] compile-flags: -Zpolonius=next + trait Decode<'a> { type Decoder; } @@ -14,7 +18,7 @@ pub struct ADecoder<'a> { } fn make_a_decoder<'a>() -> ADecoder<'a> { //~^ ERROR the trait bound - //~| ERROR the trait bound + //[nll]~| ERROR the trait bound panic!() } diff --git a/tests/ui/wf/hir-wf-check-erase-regions.stderr b/tests/ui/wf/hir-wf-check-erase-regions.nll.stderr similarity index 91% rename from tests/ui/wf/hir-wf-check-erase-regions.stderr rename to tests/ui/wf/hir-wf-check-erase-regions.nll.stderr index 07304cd448ebb..dcade3aa36796 100644 --- a/tests/ui/wf/hir-wf-check-erase-regions.stderr +++ b/tests/ui/wf/hir-wf-check-erase-regions.nll.stderr @@ -1,5 +1,5 @@ error[E0277]: `&'a T` is not an iterator - --> $DIR/hir-wf-check-erase-regions.rs:7:21 + --> $DIR/hir-wf-check-erase-regions.rs:11:21 | LL | type IntoIter = std::iter::Flatten>; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&'a T` is not an iterator @@ -11,7 +11,7 @@ note: required by a bound in `std::iter::IntoIterator::IntoIter` --> $SRC_DIR/core/src/iter/traits/collect.rs:LL:COL error[E0277]: `&'a T` is not an iterator - --> $DIR/hir-wf-check-erase-regions.rs:7:5 + --> $DIR/hir-wf-check-erase-regions.rs:11:5 | LL | type IntoIter = std::iter::Flatten>; | ^^^^^^^^^^^^^ `&'a T` is not an iterator @@ -23,7 +23,7 @@ note: required by a bound in `Flatten` --> $SRC_DIR/core/src/iter/adapters/flatten.rs:LL:COL error[E0277]: `&'a T` is not an iterator - --> $DIR/hir-wf-check-erase-regions.rs:11:27 + --> $DIR/hir-wf-check-erase-regions.rs:15:27 | LL | fn into_iter(self) -> Self::IntoIter { | ^^^^^^^^^^^^^^ `&'a T` is not an iterator @@ -35,7 +35,7 @@ note: required by a bound in `Flatten` --> $SRC_DIR/core/src/iter/adapters/flatten.rs:LL:COL error[E0277]: `&T` is not an iterator - --> $DIR/hir-wf-check-erase-regions.rs:11:27 + --> $DIR/hir-wf-check-erase-regions.rs:15:27 | LL | fn into_iter(self) -> Self::IntoIter { | ^^^^^^^^^^^^^^ `&T` is not an iterator diff --git a/tests/ui/wf/hir-wf-check-erase-regions.polonius.stderr b/tests/ui/wf/hir-wf-check-erase-regions.polonius.stderr new file mode 100644 index 0000000000000..55728aa642b80 --- /dev/null +++ b/tests/ui/wf/hir-wf-check-erase-regions.polonius.stderr @@ -0,0 +1,39 @@ +error[E0277]: `&'a T` is not an iterator + --> $DIR/hir-wf-check-erase-regions.rs:11:21 + | +LL | type IntoIter = std::iter::Flatten>; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&'a T` is not an iterator + | + = help: the trait `Iterator` is not implemented for `&'a T` + = help: the trait `Iterator` is implemented for `&mut I` + = note: required for `Flatten>` to implement `Iterator` +note: required by a bound in `std::iter::IntoIterator::IntoIter` + --> $SRC_DIR/core/src/iter/traits/collect.rs:LL:COL + +error[E0277]: `&'a T` is not an iterator + --> $DIR/hir-wf-check-erase-regions.rs:11:5 + | +LL | type IntoIter = std::iter::Flatten>; + | ^^^^^^^^^^^^^ `&'a T` is not an iterator + | + = help: the trait `Iterator` is not implemented for `&'a T` + = help: the trait `Iterator` is implemented for `&mut I` + = note: required for `&'a T` to implement `IntoIterator` +note: required by a bound in `Flatten` + --> $SRC_DIR/core/src/iter/adapters/flatten.rs:LL:COL + +error[E0277]: `&'a T` is not an iterator + --> $DIR/hir-wf-check-erase-regions.rs:15:27 + | +LL | fn into_iter(self) -> Self::IntoIter { + | ^^^^^^^^^^^^^^ `&'a T` is not an iterator + | + = help: the trait `Iterator` is not implemented for `&'a T` + = help: the trait `Iterator` is implemented for `&mut I` + = note: required for `&'a T` to implement `IntoIterator` +note: required by a bound in `Flatten` + --> $SRC_DIR/core/src/iter/adapters/flatten.rs:LL:COL + +error: aborting due to 3 previous errors + +For more information about this error, try `rustc --explain E0277`. diff --git a/tests/ui/wf/hir-wf-check-erase-regions.rs b/tests/ui/wf/hir-wf-check-erase-regions.rs index 20cc1cfe73018..ef9132697efc8 100644 --- a/tests/ui/wf/hir-wf-check-erase-regions.rs +++ b/tests/ui/wf/hir-wf-check-erase-regions.rs @@ -1,6 +1,10 @@ // Regression test for #87549. //@ incremental +//@ ignore-compare-mode-polonius (explicit revisions) +//@ revisions: nll polonius +//@ [polonius] compile-flags: -Zpolonius=next + pub struct Table([Option; N]); impl<'a, T, const N: usize> IntoIterator for &'a Table { @@ -10,7 +14,7 @@ impl<'a, T, const N: usize> IntoIterator for &'a Table { fn into_iter(self) -> Self::IntoIter { //~^ ERROR `&'a T` is not an iterator - //~| ERROR `&T` is not an iterator + //[nll]~| ERROR `&T` is not an iterator unimplemented!() } } From a5adde8eaae4ad5d9510897b02117bf0a9f45d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Fri, 8 Aug 2025 15:14:17 +0000 Subject: [PATCH 3/9] simplify polonius=next Remove incomplete handling of kills during traversal for loan liveness to get to a simpler and actionable prototype. This handles the cases, on sufficiently simple examples, that were deferred from NLLs (NLL problem case 3, lending iterators), and is still a good step to put in people's hands without needing to wait for another full implementation. This is a practical cut in scope, but it also shows where are the areas of improvement, that we will explore in the future. --- .../src/polonius/constraints.rs | 4 +- .../src/polonius/liveness_constraints.rs | 12 +- .../src/polonius/loan_liveness.rs | 191 ++---------------- compiler/rustc_borrowck/src/polonius/mod.rs | 6 +- .../src/polonius/typeck_constraints.rs | 14 +- 5 files changed, 28 insertions(+), 199 deletions(-) diff --git a/compiler/rustc_borrowck/src/polonius/constraints.rs b/compiler/rustc_borrowck/src/polonius/constraints.rs index 50f59dd0dee67..5259575785955 100644 --- a/compiler/rustc_borrowck/src/polonius/constraints.rs +++ b/compiler/rustc_borrowck/src/polonius/constraints.rs @@ -7,9 +7,7 @@ use rustc_mir_dataflow::points::PointIndex; /// /// This models two sources of constraints: /// - constraints that traverse the subsets between regions at a given point, `a@p: b@p`. These -/// depend on typeck constraints generated via assignments, calls, etc. (In practice there are -/// subtleties where a statement's effect only starts being visible at the successor point, via -/// the "result" of that statement). +/// depend on typeck constraints generated via assignments, calls, etc. /// - constraints that traverse the CFG via the same region, `a@p: a@q`, where `p` is a predecessor /// of `q`. These depend on the liveness of the regions at these points, as well as their /// variance. diff --git a/compiler/rustc_borrowck/src/polonius/liveness_constraints.rs b/compiler/rustc_borrowck/src/polonius/liveness_constraints.rs index 6ab09f731c078..2ba72180d66a6 100644 --- a/compiler/rustc_borrowck/src/polonius/liveness_constraints.rs +++ b/compiler/rustc_borrowck/src/polonius/liveness_constraints.rs @@ -105,22 +105,14 @@ fn propagate_loans_between_points( }); } - let Some(current_live_regions) = live_regions.row(current_point) else { - // There are no constraints to add: there are no live regions at the current point. - return; - }; let Some(next_live_regions) = live_regions.row(next_point) else { // There are no constraints to add: there are no live regions at the next point. return; }; for region in next_live_regions.iter() { - if !current_live_regions.contains(region) { - continue; - } - - // `region` is indeed live at both points, add a constraint between them, according to - // variance. + // `region` could be live at the current point, and is live at the next point: add a + // constraint between them, according to variance. if let Some(&direction) = live_region_variances.get(®ion) { add_liveness_constraint( region, diff --git a/compiler/rustc_borrowck/src/polonius/loan_liveness.rs b/compiler/rustc_borrowck/src/polonius/loan_liveness.rs index 5cd265e0db92d..bdc3047e5ba01 100644 --- a/compiler/rustc_borrowck/src/polonius/loan_liveness.rs +++ b/compiler/rustc_borrowck/src/polonius/loan_liveness.rs @@ -1,27 +1,18 @@ -use std::collections::{BTreeMap, BTreeSet}; - use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet}; -use rustc_middle::mir::visit::Visitor; -use rustc_middle::mir::{ - Body, Local, Location, Place, Rvalue, Statement, StatementKind, Terminator, TerminatorKind, -}; -use rustc_middle::ty::{RegionVid, TyCtxt}; +use rustc_middle::ty::RegionVid; use rustc_mir_dataflow::points::PointIndex; use super::{LiveLoans, LocalizedOutlivesConstraintSet}; +use crate::BorrowSet; use crate::constraints::OutlivesConstraint; -use crate::dataflow::BorrowIndex; use crate::region_infer::values::LivenessValues; use crate::type_check::Locations; -use crate::{BorrowSet, PlaceConflictBias, places_conflict}; -/// Compute loan reachability, stop at kills, and trace loan liveness throughout the CFG, by +/// Compute loan reachability to approximately trace loan liveness throughout the CFG, by /// traversing the full graph of constraints that combines: /// - the localized constraints (the physical edges), /// - with the constraints that hold at all points (the logical edges). pub(super) fn compute_loan_liveness<'tcx>( - tcx: TyCtxt<'tcx>, - body: &Body<'tcx>, liveness: &LivenessValues, outlives_constraints: impl Iterator>, borrow_set: &BorrowSet<'tcx>, @@ -29,11 +20,6 @@ pub(super) fn compute_loan_liveness<'tcx>( ) -> LiveLoans { let mut live_loans = LiveLoans::new(borrow_set.len()); - // FIXME: it may be preferable for kills to be encoded in the edges themselves, to simplify and - // likely make traversal (and constraint generation) more efficient. We also display kills on - // edges when visualizing the constraint graph anyways. - let kills = collect_kills(body, tcx, borrow_set); - // Create the full graph with the physical edges we've localized earlier, and the logical edges // of constraints that hold at all points. let logical_constraints = @@ -59,15 +45,15 @@ pub(super) fn compute_loan_liveness<'tcx>( continue; } - // Record the loan as being live on entry to this point. - live_loans.insert(node.point, loan_idx); - - // Here, we have a conundrum. There's currently a weakness in our theory, in that - // we're using a single notion of reachability to represent what used to be _two_ - // different transitive closures. It didn't seem impactful when coming up with the - // single-graph and reachability through space (regions) + time (CFG) concepts, but in - // practice the combination of time-traveling with kills is more impactful than - // initially anticipated. + // Record the loan as being live on entry to this point if it reaches a live region + // there. + // + // This is an approximation of liveness (which is the thing we want), in that we're + // using a single notion of reachability to represent what used to be _two_ different + // transitive closures. It didn't seem impactful when coming up with the single-graph + // and reachability through space (regions) + time (CFG) concepts, but in practice the + // combination of time-traveling with kills is more impactful than initially + // anticipated. // // Kills should prevent a loan from reaching its successor points in the CFG, but not // while time-traveling: we're not actually at that CFG point, but looking for @@ -92,40 +78,20 @@ pub(super) fn compute_loan_liveness<'tcx>( // two-step traversal described above: only kills encountered on exit via a backward // edge are ignored. // - // In our test suite, there are a couple of cases where kills are encountered while - // time-traveling, however as far as we can tell, always in cases where they would be - // unreachable. We have reason to believe that this is a property of the single-graph - // approach (but haven't proved it yet): - // - reachable kills while time-traveling would also be encountered via regular - // traversal - // - it makes _some_ sense to ignore unreachable kills, but subtleties around dead code - // in general need to be better thought through (like they were for NLLs). - // - ignoring kills is a conservative approximation: the loan is still live and could - // cause false positive errors at another place access. Soundness issues in this - // domain should look more like the absence of reachability instead. - // - // This is enough in practice to pass tests, and therefore is what we have implemented - // for now. + // This version of the analysis, however, is enough in practice to pass the tests that + // we care about and NLLs reject, without regressions on crater, and is an actionable + // subset of the full analysis. It also naturally points to areas of improvement that we + // wish to explore later, namely handling kills appropriately during traversal, instead + // of continuing traversal to all the reachable nodes. // - // FIXME: all of the above. Analyze potential unsoundness, possibly in concert with a - // borrowck implementation in a-mir-formality, fuzzing, or manually crafting - // counter-examples. + // FIXME: analyze potential unsoundness, possibly in concert with a borrowck + // implementation in a-mir-formality, fuzzing, or manually crafting counter-examples. - // Continuing traversal will depend on whether the loan is killed at this point, and - // whether we're time-traveling. - let current_location = liveness.location_from_point(node.point); - let is_loan_killed = - kills.get(¤t_location).is_some_and(|kills| kills.contains(&loan_idx)); + if liveness.is_live_at(node.region, liveness.location_from_point(node.point)) { + live_loans.insert(node.point, loan_idx); + } for succ in graph.outgoing_edges(node) { - // If the loan is killed at this point, it is killed _on exit_. But only during - // forward traversal. - if is_loan_killed { - let destination = liveness.location_from_point(succ.point); - if current_location.is_predecessor_of(destination, body) { - continue; - } - } stack.push(succ); } } @@ -192,116 +158,3 @@ impl LocalizedConstraintGraph { physical_edges.chain(materialized_edges) } } - -/// Traverses the MIR and collects kills. -fn collect_kills<'tcx>( - body: &Body<'tcx>, - tcx: TyCtxt<'tcx>, - borrow_set: &BorrowSet<'tcx>, -) -> BTreeMap> { - let mut collector = KillsCollector { borrow_set, tcx, body, kills: BTreeMap::default() }; - for (block, data) in body.basic_blocks.iter_enumerated() { - collector.visit_basic_block_data(block, data); - } - collector.kills -} - -struct KillsCollector<'a, 'tcx> { - body: &'a Body<'tcx>, - tcx: TyCtxt<'tcx>, - borrow_set: &'a BorrowSet<'tcx>, - - /// The set of loans killed at each location. - kills: BTreeMap>, -} - -// This visitor has a similar structure to the `Borrows` dataflow computation with respect to kills, -// and the datalog polonius fact generation for the `loan_killed_at` relation. -impl<'tcx> KillsCollector<'_, 'tcx> { - /// Records the borrows on the specified place as `killed`. For example, when assigning to a - /// local, or on a call's return destination. - fn record_killed_borrows_for_place(&mut self, place: Place<'tcx>, location: Location) { - // For the reasons described in graph traversal, we also filter out kills - // unreachable from the loan's introduction point, as they would stop traversal when - // e.g. checking for reachability in the subset graph through invariance constraints - // higher up. - let filter_unreachable_kills = |loan| { - let introduction = self.borrow_set[loan].reserve_location; - let reachable = introduction.is_predecessor_of(location, self.body); - reachable - }; - - let other_borrows_of_local = self - .borrow_set - .local_map - .get(&place.local) - .into_iter() - .flat_map(|bs| bs.iter()) - .copied(); - - // If the borrowed place is a local with no projections, all other borrows of this - // local must conflict. This is purely an optimization so we don't have to call - // `places_conflict` for every borrow. - if place.projection.is_empty() { - if !self.body.local_decls[place.local].is_ref_to_static() { - self.kills - .entry(location) - .or_default() - .extend(other_borrows_of_local.filter(|&loan| filter_unreachable_kills(loan))); - } - return; - } - - // By passing `PlaceConflictBias::NoOverlap`, we conservatively assume that any given - // pair of array indices are not equal, so that when `places_conflict` returns true, we - // will be assured that two places being compared definitely denotes the same sets of - // locations. - let definitely_conflicting_borrows = other_borrows_of_local - .filter(|&i| { - places_conflict( - self.tcx, - self.body, - self.borrow_set[i].borrowed_place, - place, - PlaceConflictBias::NoOverlap, - ) - }) - .filter(|&loan| filter_unreachable_kills(loan)); - - self.kills.entry(location).or_default().extend(definitely_conflicting_borrows); - } - - /// Records the borrows on the specified local as `killed`. - fn record_killed_borrows_for_local(&mut self, local: Local, location: Location) { - if let Some(borrow_indices) = self.borrow_set.local_map.get(&local) { - self.kills.entry(location).or_default().extend(borrow_indices.iter()); - } - } -} - -impl<'tcx> Visitor<'tcx> for KillsCollector<'_, 'tcx> { - fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) { - // Make sure there are no remaining borrows for locals that have gone out of scope. - if let StatementKind::StorageDead(local) = statement.kind { - self.record_killed_borrows_for_local(local, location); - } - - self.super_statement(statement, location); - } - - fn visit_assign(&mut self, place: &Place<'tcx>, rvalue: &Rvalue<'tcx>, location: Location) { - // When we see `X = ...`, then kill borrows of `(*X).foo` and so forth. - self.record_killed_borrows_for_place(*place, location); - self.super_assign(place, rvalue, location); - } - - fn visit_terminator(&mut self, terminator: &Terminator<'tcx>, location: Location) { - // A `Call` terminator's return value can be a local which has borrows, so we need to record - // those as killed as well. - if let TerminatorKind::Call { destination, .. } = terminator.kind { - self.record_killed_borrows_for_place(destination, location); - } - - self.super_terminator(terminator, location); - } -} diff --git a/compiler/rustc_borrowck/src/polonius/mod.rs b/compiler/rustc_borrowck/src/polonius/mod.rs index 142ef8ba28eff..a9092b1981e1d 100644 --- a/compiler/rustc_borrowck/src/polonius/mod.rs +++ b/compiler/rustc_borrowck/src/polonius/mod.rs @@ -146,8 +146,8 @@ impl PoloniusContext { /// - converting NLL typeck constraints to be localized /// - encoding liveness constraints /// - /// Then, this graph is traversed, and combined with kills, reachability is recorded as loan - /// liveness, to be used by the loan scope and active loans computations. + /// Then, this graph is traversed, reachability is recorded as loan liveness, to be used by the + /// loan scope and active loans computations. /// /// The constraint data will be used to compute errors and diagnostics. pub(crate) fn compute_loan_liveness<'tcx>( @@ -182,8 +182,6 @@ impl PoloniusContext { // Now that we have a complete graph, we can compute reachability to trace the liveness of // loans for the next step in the chain, the NLL loan scope and active loans computations. let live_loans = compute_loan_liveness( - tcx, - body, regioncx.liveness_constraints(), regioncx.outlives_constraints(), borrow_set, diff --git a/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs b/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs index 1289b1899eb35..8106d1e4af068 100644 --- a/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs +++ b/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs @@ -47,9 +47,7 @@ pub(super) fn convert_typeck_constraints<'tcx>( tcx, body, stmt, - liveness, &outlives_constraint, - location, point, universal_regions, ) @@ -78,9 +76,7 @@ fn localize_statement_constraint<'tcx>( tcx: TyCtxt<'tcx>, body: &Body<'tcx>, stmt: &Statement<'tcx>, - liveness: &LivenessValues, outlives_constraint: &OutlivesConstraint<'tcx>, - current_location: Location, current_point: PointIndex, universal_regions: &UniversalRegions<'tcx>, ) -> LocalizedOutlivesConstraint { @@ -119,16 +115,8 @@ fn localize_statement_constraint<'tcx>( "there should be no common regions between the LHS and RHS of an assignment" ); - // As mentioned earlier, we should be tracking these better upstream but: we want to - // relate the types on entry to the type of the place on exit. That is, outlives - // constraints on the RHS are on entry, and outlives constraints to/from the LHS are on - // exit (i.e. on entry to the successor location). let lhs_ty = body.local_decls[lhs.local].ty; - let successor_location = Location { - block: current_location.block, - statement_index: current_location.statement_index + 1, - }; - let successor_point = liveness.point_from_location(successor_location); + let successor_point = current_point; compute_constraint_direction( tcx, outlives_constraint, From 13308988b051ebe014a061b9d595cbb8289348d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Mon, 16 Jun 2025 14:39:32 +0000 Subject: [PATCH 4/9] new impl fixes crash test --- tests/crashes/135646.rs | 7 ------- .../ui/nll/polonius/array-literal-index-oob-2024.rs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) delete mode 100644 tests/crashes/135646.rs create mode 100644 tests/ui/nll/polonius/array-literal-index-oob-2024.rs diff --git a/tests/crashes/135646.rs b/tests/crashes/135646.rs deleted file mode 100644 index 841ea5b81b41f..0000000000000 --- a/tests/crashes/135646.rs +++ /dev/null @@ -1,7 +0,0 @@ -//@ known-bug: #135646 -//@ compile-flags: -Zpolonius=next -//@ edition: 2024 - -fn main() { - &{ [1, 2, 3][4] }; -} diff --git a/tests/ui/nll/polonius/array-literal-index-oob-2024.rs b/tests/ui/nll/polonius/array-literal-index-oob-2024.rs new file mode 100644 index 0000000000000..2054a32e53553 --- /dev/null +++ b/tests/ui/nll/polonius/array-literal-index-oob-2024.rs @@ -0,0 +1,12 @@ +// This test used to ICE under `-Zpolonius=next` when computing loan liveness +// and taking kills into account during reachability traversal of the localized +// constraint graph. Originally from another test but on edition 2024, as +// seen in issue #135646. + +//@ compile-flags: -Zpolonius=next +//@ edition: 2024 +//@ check-pass + +fn main() { + &{ [1, 2, 3][4] }; +} From d4bbd681bbdb2b47717922e46bd022dbf2535403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Wed, 18 Jun 2025 09:01:57 +0000 Subject: [PATCH 5/9] turn expensive assert into debug assertion --- compiler/rustc_borrowck/src/polonius/typeck_constraints.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs b/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs index 8106d1e4af068..e4e52962bf7f9 100644 --- a/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs +++ b/compiler/rustc_borrowck/src/polonius/typeck_constraints.rs @@ -94,8 +94,8 @@ fn localize_statement_constraint<'tcx>( // - and that should be impossible in MIR // // When we have a more complete implementation in the future, tested with crater, etc, - // we can relax this to a debug assert instead, or remove it. - assert!( + // we can remove this assertion. It's a debug assert because it can be expensive. + debug_assert!( { let mut lhs_regions = FxHashSet::default(); tcx.for_each_free_region(lhs, |region| { @@ -183,6 +183,7 @@ fn localize_terminator_constraint<'tcx>( } } } + /// For a given outlives constraint and CFG edge, returns the localized constraint with the /// appropriate `from`-`to` direction. This is computed according to whether the constraint flows to /// or from a free region in the given `value`, some kind of result for an effectful operation, like From b172980d5c0de4a917e0f28ebd118f20889e6036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Thu, 26 Jun 2025 13:06:48 +0000 Subject: [PATCH 6/9] add some test cases for overlapping yielded items These are just some sanity checks to ensure NLLs, the polonius alpha analysis, and the datalog implementation behave the same on these common examples. --- ...nding-iterator-sanity-checks.legacy.stderr | 28 ++++++++ .../lending-iterator-sanity-checks.nll.stderr | 28 ++++++++ ...ing-iterator-sanity-checks.polonius.stderr | 26 +++++++ .../lending-iterator-sanity-checks.rs | 71 +++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 tests/ui/nll/polonius/lending-iterator-sanity-checks.legacy.stderr create mode 100644 tests/ui/nll/polonius/lending-iterator-sanity-checks.nll.stderr create mode 100644 tests/ui/nll/polonius/lending-iterator-sanity-checks.polonius.stderr create mode 100644 tests/ui/nll/polonius/lending-iterator-sanity-checks.rs diff --git a/tests/ui/nll/polonius/lending-iterator-sanity-checks.legacy.stderr b/tests/ui/nll/polonius/lending-iterator-sanity-checks.legacy.stderr new file mode 100644 index 0000000000000..fa201b89048af --- /dev/null +++ b/tests/ui/nll/polonius/lending-iterator-sanity-checks.legacy.stderr @@ -0,0 +1,28 @@ +error[E0499]: cannot borrow `*t` as mutable more than once at a time + --> $DIR/lending-iterator-sanity-checks.rs:19:19 + | +LL | fn use_live(t: &mut T) -> Option<(T::Item<'_>, T::Item<'_>)> { + | - let's call the lifetime of this reference `'1` +LL | let Some(i) = t.next() else { return None }; + | - first mutable borrow occurs here +LL | let Some(j) = t.next() else { return None }; + | ^ second mutable borrow occurs here +... +LL | Some((i, j)) + | ------------ returning this value requires that `*t` is borrowed for `'1` + +error[E0499]: cannot borrow `*t` as mutable more than once at a time + --> $DIR/lending-iterator-sanity-checks.rs:31:13 + | +LL | let i = t.next(); + | - first mutable borrow occurs here +... +LL | let j = t.next(); + | ^ second mutable borrow occurs here +LL | +LL | } + | - first borrow might be used here, when `i` is dropped and runs the destructor for type `Option<::Item<'_>>` + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0499`. diff --git a/tests/ui/nll/polonius/lending-iterator-sanity-checks.nll.stderr b/tests/ui/nll/polonius/lending-iterator-sanity-checks.nll.stderr new file mode 100644 index 0000000000000..fa201b89048af --- /dev/null +++ b/tests/ui/nll/polonius/lending-iterator-sanity-checks.nll.stderr @@ -0,0 +1,28 @@ +error[E0499]: cannot borrow `*t` as mutable more than once at a time + --> $DIR/lending-iterator-sanity-checks.rs:19:19 + | +LL | fn use_live(t: &mut T) -> Option<(T::Item<'_>, T::Item<'_>)> { + | - let's call the lifetime of this reference `'1` +LL | let Some(i) = t.next() else { return None }; + | - first mutable borrow occurs here +LL | let Some(j) = t.next() else { return None }; + | ^ second mutable borrow occurs here +... +LL | Some((i, j)) + | ------------ returning this value requires that `*t` is borrowed for `'1` + +error[E0499]: cannot borrow `*t` as mutable more than once at a time + --> $DIR/lending-iterator-sanity-checks.rs:31:13 + | +LL | let i = t.next(); + | - first mutable borrow occurs here +... +LL | let j = t.next(); + | ^ second mutable borrow occurs here +LL | +LL | } + | - first borrow might be used here, when `i` is dropped and runs the destructor for type `Option<::Item<'_>>` + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0499`. diff --git a/tests/ui/nll/polonius/lending-iterator-sanity-checks.polonius.stderr b/tests/ui/nll/polonius/lending-iterator-sanity-checks.polonius.stderr new file mode 100644 index 0000000000000..bf8546b63bf30 --- /dev/null +++ b/tests/ui/nll/polonius/lending-iterator-sanity-checks.polonius.stderr @@ -0,0 +1,26 @@ +error[E0499]: cannot borrow `*t` as mutable more than once at a time + --> $DIR/lending-iterator-sanity-checks.rs:19:19 + | +LL | let Some(i) = t.next() else { return None }; + | - first mutable borrow occurs here +LL | let Some(j) = t.next() else { return None }; + | ^ second mutable borrow occurs here +... +LL | } + | - first borrow might be used here, when `i` is dropped and runs the destructor for type `::Item<'_>` + +error[E0499]: cannot borrow `*t` as mutable more than once at a time + --> $DIR/lending-iterator-sanity-checks.rs:31:13 + | +LL | let i = t.next(); + | - first mutable borrow occurs here +... +LL | let j = t.next(); + | ^ second mutable borrow occurs here +LL | +LL | } + | - first borrow might be used here, when `i` is dropped and runs the destructor for type `Option<::Item<'_>>` + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0499`. diff --git a/tests/ui/nll/polonius/lending-iterator-sanity-checks.rs b/tests/ui/nll/polonius/lending-iterator-sanity-checks.rs new file mode 100644 index 0000000000000..ae8cc78957fef --- /dev/null +++ b/tests/ui/nll/polonius/lending-iterator-sanity-checks.rs @@ -0,0 +1,71 @@ +// Some sanity checks for lending iterators with GATs. This is just some non-regression tests +// ensuring the polonius alpha analysis, the datalog implementation, and NLLs agree in these common +// cases of overlapping yielded items. + +//@ ignore-compare-mode-polonius (explicit revisions) +//@ revisions: nll polonius legacy +//@ [polonius] compile-flags: -Z polonius=next +//@ [legacy] compile-flags: -Z polonius=legacy + +trait LendingIterator { + type Item<'a> + where + Self: 'a; + fn next(&mut self) -> Option>; +} + +fn use_live(t: &mut T) -> Option<(T::Item<'_>, T::Item<'_>)> { + let Some(i) = t.next() else { return None }; + let Some(j) = t.next() else { return None }; + //~^ ERROR cannot borrow `*t` as mutable more than once at a time + + // `i` is obviously still (use-)live here, but we called `next` again to get `j`. + Some((i, j)) +} + +fn drop_live(t: &mut T) { + let i = t.next(); + + // Now `i` is use-dead here, but we don't know if the iterator items have a `Drop` impl, so it's + // still drop-live. + let j = t.next(); + //~^ ERROR cannot borrow `*t` as mutable more than once at a time +} + +// But we can still manually serialize the lifetimes with scopes (or preventing the destructor from +// being called), so they're not overlapping. +fn manually_non_overlapping(t: &mut T) { + { + let i = t.next(); + } + + let j = t.next(); // i is dead + + drop(j); + let k = t.next(); // j is dead + + let k = std::mem::ManuallyDrop::new(k); + let l = t.next(); // we told the compiler that k is not drop-live +} + +// The cfg below is because there's a diagnostic ICE trying to explain the source of the error when +// using the datalog implementation. We're not fixing *that*, outside of removing the implementation +// in the future. +#[cfg(not(legacy))] // FIXME: remove this cfg when removing the datalog implementation +fn items_have_no_borrows(t: &mut T) +where + for<'a> T::Item<'a>: 'static, +{ + let i = t.next(); + let j = t.next(); +} + +fn items_are_copy(t: &mut T) +where + for<'a> T::Item<'a>: Copy, +{ + let i = t.next(); + let j = t.next(); +} + +fn main() {} From 48ebae9cef6fdb1af495e692a0811634f308e447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Thu, 26 Jun 2025 15:27:23 +0000 Subject: [PATCH 7/9] add NLL-like imprecision example This test showcases the same imprecision as NLLs, unlike the datalog implementation, when using reachability as a liveness approximation. --- .../flow-sensitive-invariance.nll.stderr | 36 +++++++++++++++++++ .../flow-sensitive-invariance.polonius.stderr | 36 +++++++++++++++++++ .../nll/polonius/flow-sensitive-invariance.rs | 34 ++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 tests/ui/nll/polonius/flow-sensitive-invariance.nll.stderr create mode 100644 tests/ui/nll/polonius/flow-sensitive-invariance.polonius.stderr create mode 100644 tests/ui/nll/polonius/flow-sensitive-invariance.rs diff --git a/tests/ui/nll/polonius/flow-sensitive-invariance.nll.stderr b/tests/ui/nll/polonius/flow-sensitive-invariance.nll.stderr new file mode 100644 index 0000000000000..5756148f4eb2a --- /dev/null +++ b/tests/ui/nll/polonius/flow-sensitive-invariance.nll.stderr @@ -0,0 +1,36 @@ +error: lifetime may not live long enough + --> $DIR/flow-sensitive-invariance.rs:20:17 + | +LL | fn use_it<'a, 'b>(choice: bool) -> Result, Invariant<'b>> { + | -- -- lifetime `'b` defined here + | | + | lifetime `'a` defined here +LL | let returned_value = create_invariant(); +LL | if choice { Ok(returned_value) } else { Err(returned_value) } + | ^^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b` + | + = help: consider adding the following bound: `'b: 'a` + = note: requirement occurs because of the type `Invariant<'_>`, which makes the generic argument `'_` invariant + = note: the struct `Invariant<'l>` is invariant over the parameter `'l` + = help: see for more information about variance + +error: lifetime may not live long enough + --> $DIR/flow-sensitive-invariance.rs:20:45 + | +LL | fn use_it<'a, 'b>(choice: bool) -> Result, Invariant<'b>> { + | -- -- lifetime `'b` defined here + | | + | lifetime `'a` defined here +LL | let returned_value = create_invariant(); +LL | if choice { Ok(returned_value) } else { Err(returned_value) } + | ^^^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'b` but it is returning data with lifetime `'a` + | + = help: consider adding the following bound: `'a: 'b` + = note: requirement occurs because of the type `Invariant<'_>`, which makes the generic argument `'_` invariant + = note: the struct `Invariant<'l>` is invariant over the parameter `'l` + = help: see for more information about variance + +help: `'a` and `'b` must be the same: replace one with the other + +error: aborting due to 2 previous errors + diff --git a/tests/ui/nll/polonius/flow-sensitive-invariance.polonius.stderr b/tests/ui/nll/polonius/flow-sensitive-invariance.polonius.stderr new file mode 100644 index 0000000000000..5756148f4eb2a --- /dev/null +++ b/tests/ui/nll/polonius/flow-sensitive-invariance.polonius.stderr @@ -0,0 +1,36 @@ +error: lifetime may not live long enough + --> $DIR/flow-sensitive-invariance.rs:20:17 + | +LL | fn use_it<'a, 'b>(choice: bool) -> Result, Invariant<'b>> { + | -- -- lifetime `'b` defined here + | | + | lifetime `'a` defined here +LL | let returned_value = create_invariant(); +LL | if choice { Ok(returned_value) } else { Err(returned_value) } + | ^^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b` + | + = help: consider adding the following bound: `'b: 'a` + = note: requirement occurs because of the type `Invariant<'_>`, which makes the generic argument `'_` invariant + = note: the struct `Invariant<'l>` is invariant over the parameter `'l` + = help: see for more information about variance + +error: lifetime may not live long enough + --> $DIR/flow-sensitive-invariance.rs:20:45 + | +LL | fn use_it<'a, 'b>(choice: bool) -> Result, Invariant<'b>> { + | -- -- lifetime `'b` defined here + | | + | lifetime `'a` defined here +LL | let returned_value = create_invariant(); +LL | if choice { Ok(returned_value) } else { Err(returned_value) } + | ^^^^^^^^^^^^^^^^^^^ function was supposed to return data with lifetime `'b` but it is returning data with lifetime `'a` + | + = help: consider adding the following bound: `'a: 'b` + = note: requirement occurs because of the type `Invariant<'_>`, which makes the generic argument `'_` invariant + = note: the struct `Invariant<'l>` is invariant over the parameter `'l` + = help: see for more information about variance + +help: `'a` and `'b` must be the same: replace one with the other + +error: aborting due to 2 previous errors + diff --git a/tests/ui/nll/polonius/flow-sensitive-invariance.rs b/tests/ui/nll/polonius/flow-sensitive-invariance.rs new file mode 100644 index 0000000000000..c5571f131da3b --- /dev/null +++ b/tests/ui/nll/polonius/flow-sensitive-invariance.rs @@ -0,0 +1,34 @@ +// An example (from @steffahn) of reachability as an approximation of liveness where the polonius +// alpha analysis shows the same imprecision as NLLs, unlike the datalog implementation. + +//@ ignore-compare-mode-polonius (explicit revisions) +//@ revisions: nll polonius legacy +//@ [polonius] compile-flags: -Z polonius=next +//@ [legacy] check-pass +//@ [legacy] compile-flags: -Z polonius=legacy + +use std::cell::Cell; + +struct Invariant<'l>(Cell<&'l ()>); + +fn create_invariant<'l>() -> Invariant<'l> { + Invariant(Cell::new(&())) +} + +fn use_it<'a, 'b>(choice: bool) -> Result, Invariant<'b>> { + let returned_value = create_invariant(); + if choice { Ok(returned_value) } else { Err(returned_value) } + //[nll]~^ ERROR lifetime may not live long enough + //[nll]~| ERROR lifetime may not live long enough + //[polonius]~^^^ ERROR lifetime may not live long enough + //[polonius]~| ERROR lifetime may not live long enough +} + +fn use_it_but_its_the_same_region<'a: 'b, 'b: 'a>( + choice: bool, +) -> Result, Invariant<'b>> { + let returned_value = create_invariant(); + if choice { Ok(returned_value) } else { Err(returned_value) } +} + +fn main() {} From 9badbdf5f9272bf42699edabc14735a1ab42e3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Thu, 26 Jun 2025 15:29:03 +0000 Subject: [PATCH 8/9] add cursor-like example that works This is an example similar to the linked-list cursor examples where the alpha shows the same imprecision as NLLs, but that can work due to the loans not being live after the loop, or the constraint graph being simple enough that the cfg/subset relationships are the same for reachability and liveness. --- .../iterating-updating-mutref.nll.stderr | 15 ++++ .../nll/polonius/iterating-updating-mutref.rs | 87 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 tests/ui/nll/polonius/iterating-updating-mutref.nll.stderr create mode 100644 tests/ui/nll/polonius/iterating-updating-mutref.rs diff --git a/tests/ui/nll/polonius/iterating-updating-mutref.nll.stderr b/tests/ui/nll/polonius/iterating-updating-mutref.nll.stderr new file mode 100644 index 0000000000000..941c736d8d258 --- /dev/null +++ b/tests/ui/nll/polonius/iterating-updating-mutref.nll.stderr @@ -0,0 +1,15 @@ +error[E0499]: cannot borrow `self.buf_read` as mutable more than once at a time + --> $DIR/iterating-updating-mutref.rs:61:23 + | +LL | pub fn next<'a>(&'a mut self) -> &'a str { + | -- lifetime `'a` defined here +LL | loop { +LL | let buf = self.buf_read.fill_buf(); + | ^^^^^^^^^^^^^ `self.buf_read` was mutably borrowed here in the previous iteration of the loop +LL | if let Some(s) = decode(buf) { +LL | return s; + | - returning this value requires that `self.buf_read` is borrowed for `'a` + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0499`. diff --git a/tests/ui/nll/polonius/iterating-updating-mutref.rs b/tests/ui/nll/polonius/iterating-updating-mutref.rs new file mode 100644 index 0000000000000..a315bf66279c6 --- /dev/null +++ b/tests/ui/nll/polonius/iterating-updating-mutref.rs @@ -0,0 +1,87 @@ +// These are some examples of iterating through and updating a mutable ref, similar in spirit to the +// linked-list-like pattern of #46859/#48001 where the polonius alpha analysis shows imprecision, +// unlike the datalog implementation. +// +// They differ in that after the loans prior to the loop are either not live after the loop, or with +// control flow and outlives relationships that are simple enough for the reachability +// approximation. They're thus accepted by the alpha analysis, like NLLs did for the simplest cases +// of flow-sensitivity. + +//@ ignore-compare-mode-polonius (explicit revisions) +//@ revisions: nll polonius legacy +//@ [nll] known-bug: #46859 +//@ [polonius] check-pass +//@ [polonius] compile-flags: -Z polonius=next +//@ [legacy] check-pass +//@ [legacy] compile-flags: -Z polonius=legacy + +// The #46859 OP +struct List { + value: T, + next: Option>>, +} + +fn to_refs(mut list: &mut List) -> Vec<&mut T> { + let mut result = vec![]; + loop { + result.push(&mut list.value); + if let Some(n) = list.next.as_mut() { + list = n; + } else { + return result; + } + } +} + +// A similar construction, where paths in the constraint graph are also clearly terminating, so it's +// fine even for NLLs. +fn to_refs2(mut list: &mut List) -> Vec<&mut T> { + let mut result = vec![]; + loop { + result.push(&mut list.value); + if let Some(n) = list.next.as_mut() { + list = n; + } else { + break; + } + } + + result +} + +// Another MCVE from the same issue, but was rejected by NLLs. +pub struct Decoder { + buf_read: BufRead, +} + +impl Decoder { + // NLLs fail here + pub fn next<'a>(&'a mut self) -> &'a str { + loop { + let buf = self.buf_read.fill_buf(); + if let Some(s) = decode(buf) { + return s; + } + // loop to get more input data + + // At this point `buf` is not used anymore. + // With NLL I would expect the borrow to end here, + // such that `self.buf_read` is not borrowed anymore + // by the time we start the next loop iteration. + } + } +} + +struct BufRead; + +impl BufRead { + fn fill_buf(&mut self) -> &[u8] { + unimplemented!() + } +} + +fn decode(_: &[u8]) -> Option<&str> { + unimplemented!() +} + +fn main() {} From b99fe2b72022f4fe7a6f2e55fd70cc8530566473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Fri, 8 Aug 2025 14:29:50 +0000 Subject: [PATCH 9/9] mark polonius=next's NLL imprecisions as known-bugs - linked-list cursor-like patterns - issue-46589 These are known-bugs for the polonius alpha, where they show the same imprecision as NLLs, but are supported by the old datalog implementation. --- tests/ui/nll/issue-46589.nll.stderr | 2 +- tests/ui/nll/issue-46589.polonius.stderr | 15 +++++++++++++ tests/ui/nll/issue-46589.rs | 12 +++++----- ...ng-updating-cursor-issue-108704.nll.stderr | 2 +- ...dating-cursor-issue-108704.polonius.stderr | 12 ++++++++++ .../iterating-updating-cursor-issue-108704.rs | 7 +++--- ...ing-updating-cursor-issue-57165.nll.stderr | 4 ++-- ...pdating-cursor-issue-57165.polonius.stderr | 22 +++++++++++++++++++ .../iterating-updating-cursor-issue-57165.rs | 7 +++--- ...ing-updating-cursor-issue-63908.nll.stderr | 2 +- ...pdating-cursor-issue-63908.polonius.stderr | 18 +++++++++++++++ .../iterating-updating-cursor-issue-63908.rs | 7 +++--- 12 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 tests/ui/nll/issue-46589.polonius.stderr create mode 100644 tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.polonius.stderr create mode 100644 tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.polonius.stderr create mode 100644 tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.polonius.stderr diff --git a/tests/ui/nll/issue-46589.nll.stderr b/tests/ui/nll/issue-46589.nll.stderr index dc80c1d08deca..f2d08d1f4c834 100644 --- a/tests/ui/nll/issue-46589.nll.stderr +++ b/tests/ui/nll/issue-46589.nll.stderr @@ -1,5 +1,5 @@ error[E0499]: cannot borrow `**other` as mutable more than once at a time - --> $DIR/issue-46589.rs:24:21 + --> $DIR/issue-46589.rs:25:21 | LL | *other = match (*other).get_self() { | -------- first mutable borrow occurs here diff --git a/tests/ui/nll/issue-46589.polonius.stderr b/tests/ui/nll/issue-46589.polonius.stderr new file mode 100644 index 0000000000000..f2d08d1f4c834 --- /dev/null +++ b/tests/ui/nll/issue-46589.polonius.stderr @@ -0,0 +1,15 @@ +error[E0499]: cannot borrow `**other` as mutable more than once at a time + --> $DIR/issue-46589.rs:25:21 + | +LL | *other = match (*other).get_self() { + | -------- first mutable borrow occurs here +LL | Some(s) => s, +LL | None => (*other).new_self() + | ^^^^^^^^ + | | + | second mutable borrow occurs here + | first borrow later used here + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0499`. diff --git a/tests/ui/nll/issue-46589.rs b/tests/ui/nll/issue-46589.rs index 10aff0d7b4ef9..9a23f61b8cb8d 100644 --- a/tests/ui/nll/issue-46589.rs +++ b/tests/ui/nll/issue-46589.rs @@ -1,9 +1,10 @@ //@ ignore-compare-mode-polonius (explicit revisions) -//@ revisions: nll polonius_next polonius -//@ [polonius_next] check-pass -//@ [polonius_next] compile-flags: -Zpolonius=next -//@ [polonius] check-pass -//@ [polonius] compile-flags: -Zpolonius +//@ revisions: nll polonius legacy +//@ [nll] known-bug: #46589 +//@ [polonius] known-bug: #46589 +//@ [polonius] compile-flags: -Zpolonius=next +//@ [legacy] check-pass +//@ [legacy] compile-flags: -Zpolonius=legacy struct Foo; @@ -22,7 +23,6 @@ impl Foo { *other = match (*other).get_self() { Some(s) => s, None => (*other).new_self() - //[nll]~^ ERROR cannot borrow `**other` as mutable more than once at a time [E0499] }; let c = other; diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.nll.stderr b/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.nll.stderr index 7ac4e5de28384..b768f60590cb0 100644 --- a/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.nll.stderr +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.nll.stderr @@ -1,5 +1,5 @@ error[E0499]: cannot borrow `*elements` as mutable more than once at a time - --> $DIR/iterating-updating-cursor-issue-108704.rs:40:26 + --> $DIR/iterating-updating-cursor-issue-108704.rs:41:26 | LL | for (idx, el) in elements.iter_mut().enumerate() { | ^^^^^^^^ diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.polonius.stderr b/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.polonius.stderr new file mode 100644 index 0000000000000..b768f60590cb0 --- /dev/null +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.polonius.stderr @@ -0,0 +1,12 @@ +error[E0499]: cannot borrow `*elements` as mutable more than once at a time + --> $DIR/iterating-updating-cursor-issue-108704.rs:41:26 + | +LL | for (idx, el) in elements.iter_mut().enumerate() { + | ^^^^^^^^ + | | + | `*elements` was mutably borrowed here in the previous iteration of the loop + | first borrow used here, in later iteration of loop + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0499`. diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.rs b/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.rs index 420cb73bed283..673efa8f2259a 100644 --- a/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.rs +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-108704.rs @@ -1,11 +1,12 @@ #![crate_type = "lib"] -// An example from #108704 of the linked-list cursor-like pattern of #46859/#48001. +// An example from #108704 of the linked-list cursor-like pattern of #46859/#48001, where the +// polonius alpha analysis shows the same imprecision as NLLs, unlike the datalog implementation. //@ ignore-compare-mode-polonius (explicit revisions) //@ revisions: nll polonius legacy //@ [nll] known-bug: #108704 -//@ [polonius] check-pass +//@ [polonius] known-bug: #108704 //@ [polonius] compile-flags: -Z polonius=next //@ [legacy] check-pass //@ [legacy] compile-flags: -Z polonius=legacy @@ -32,7 +33,7 @@ fn merge_tree_ok(root: &mut Root, path: Vec) { } } -// NLLs fail here +// NLLs and polonius alpha fail here fn merge_tree_ko(root: &mut Root, path: Vec) { let mut elements = &mut root.children; diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.nll.stderr b/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.nll.stderr index 14e1726e158f4..34b04e707dbd4 100644 --- a/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.nll.stderr +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.nll.stderr @@ -1,5 +1,5 @@ error[E0499]: cannot borrow `p.0` as mutable more than once at a time - --> $DIR/iterating-updating-cursor-issue-57165.rs:29:20 + --> $DIR/iterating-updating-cursor-issue-57165.rs:30:20 | LL | while let Some(now) = p { | ^^^ - first borrow used here, in later iteration of loop @@ -7,7 +7,7 @@ LL | while let Some(now) = p { | `p.0` was mutably borrowed here in the previous iteration of the loop error[E0503]: cannot use `*p` because it was mutably borrowed - --> $DIR/iterating-updating-cursor-issue-57165.rs:29:27 + --> $DIR/iterating-updating-cursor-issue-57165.rs:30:27 | LL | while let Some(now) = p { | --- ^ diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.polonius.stderr b/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.polonius.stderr new file mode 100644 index 0000000000000..34b04e707dbd4 --- /dev/null +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.polonius.stderr @@ -0,0 +1,22 @@ +error[E0499]: cannot borrow `p.0` as mutable more than once at a time + --> $DIR/iterating-updating-cursor-issue-57165.rs:30:20 + | +LL | while let Some(now) = p { + | ^^^ - first borrow used here, in later iteration of loop + | | + | `p.0` was mutably borrowed here in the previous iteration of the loop + +error[E0503]: cannot use `*p` because it was mutably borrowed + --> $DIR/iterating-updating-cursor-issue-57165.rs:30:27 + | +LL | while let Some(now) = p { + | --- ^ + | | | + | | use of borrowed `p.0` + | | borrow later used here + | `p.0` is borrowed here + +error: aborting due to 2 previous errors + +Some errors have detailed explanations: E0499, E0503. +For more information about an error, try `rustc --explain E0499`. diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.rs b/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.rs index 63ec89146d4a7..9670fc77e6faa 100644 --- a/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.rs +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-57165.rs @@ -1,11 +1,12 @@ #![crate_type = "lib"] -// An example from #57165 of the linked-list cursor-like pattern of #46859/#48001. +// An example from #57165 of the linked-list cursor-like pattern of #46859/#48001, where the +// polonius alpha analysis shows the same imprecision as NLLs, unlike the datalog implementation. //@ ignore-compare-mode-polonius (explicit revisions) //@ revisions: nll polonius legacy //@ [nll] known-bug: #57165 -//@ [polonius] check-pass +//@ [polonius] known-bug: #57165 //@ [polonius] compile-flags: -Z polonius=next //@ [legacy] check-pass //@ [legacy] compile-flags: -Z polonius=legacy @@ -22,7 +23,7 @@ fn no_control_flow() { } } -// NLLs fail here +// NLLs and polonius alpha fail here fn conditional() { let mut b = Some(Box::new(X { next: None })); let mut p = &mut b; diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.nll.stderr b/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.nll.stderr index bf38da566c694..b76e4ff639878 100644 --- a/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.nll.stderr +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.nll.stderr @@ -1,5 +1,5 @@ error[E0506]: cannot assign to `*node_ref` because it is borrowed - --> $DIR/iterating-updating-cursor-issue-63908.rs:42:5 + --> $DIR/iterating-updating-cursor-issue-63908.rs:43:5 | LL | fn remove_last_node_iterative(mut node_ref: &mut List) { | - let's call the lifetime of this reference `'1` diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.polonius.stderr b/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.polonius.stderr new file mode 100644 index 0000000000000..b76e4ff639878 --- /dev/null +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.polonius.stderr @@ -0,0 +1,18 @@ +error[E0506]: cannot assign to `*node_ref` because it is borrowed + --> $DIR/iterating-updating-cursor-issue-63908.rs:43:5 + | +LL | fn remove_last_node_iterative(mut node_ref: &mut List) { + | - let's call the lifetime of this reference `'1` +LL | loop { +LL | let next_ref = &mut node_ref.as_mut().unwrap().next; + | -------- `*node_ref` is borrowed here +... +LL | node_ref = next_ref; + | ------------------- assignment requires that `*node_ref` is borrowed for `'1` +... +LL | *node_ref = None; + | ^^^^^^^^^ `*node_ref` is assigned to here but it was already borrowed + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0506`. diff --git a/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.rs b/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.rs index 00e48b65fed15..6682926c5237a 100644 --- a/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.rs +++ b/tests/ui/nll/polonius/iterating-updating-cursor-issue-63908.rs @@ -1,11 +1,12 @@ #![crate_type = "lib"] -// An example from #63908 of the linked-list cursor-like pattern of #46859/#48001. +// An example from #63908 of the linked-list cursor-like pattern of #46859/#48001, where the +// polonius alpha analysis shows the same imprecision as NLLs, unlike the datalog implementation. //@ ignore-compare-mode-polonius (explicit revisions) //@ revisions: nll polonius legacy //@ [nll] known-bug: #63908 -//@ [polonius] check-pass +//@ [polonius] known-bug: #63908 //@ [polonius] compile-flags: -Z polonius=next //@ [legacy] check-pass //@ [legacy] compile-flags: -Z polonius=legacy @@ -27,7 +28,7 @@ fn remove_last_node_recursive(node_ref: &mut List) { } } -// NLLs fail here +// NLLs and polonius alpha fail here fn remove_last_node_iterative(mut node_ref: &mut List) { loop { let next_ref = &mut node_ref.as_mut().unwrap().next;