Rollup merge of #136299 - lqd:polonius-next-episode-9, r=jackh726
Ignore NLL boring locals in polonius diagnostics Another easy one ``@jackh726`` (the diff is inflated by blessed test expectations don't worry :) NLLs don't compute liveness for boring locals, and therefore cannot find them in causes explaining borrows. In polonius, we don't have this liveness optimization (we may be able to do something partially similar in the future, e.g. for function parameters and the like), so we do encounter these in diagnostics even though we don't want to. This PR: - restructures the polonius context into per-phase data, in spirit as you requested in an earlier review - stores the locals NLLs would consider boring into the errors/diagnostics data - ignores these if a boring local is found when trying to explain borrows This PR fixes around 80 cases of diagnostics differences between `-Zpolonius=next` and NLLs. I've also added explicit revisions to a few polonius tests (both for the in-tree implementation as well as the datalog implementation -- even if we'll eventually remove them). I didn't do this for all the "dead" expectations that were removed from #136112 for that same reason, it's fine. I'll soon/eventually add explicit revisions where they're needed: there's only a handful of tests left to fix. r? ``@jackh726``
This commit is contained in:
commit
e38f1152be
30 changed files with 377 additions and 153 deletions
|
@ -622,8 +622,25 @@ impl<'tcx> MirBorrowckCtxt<'_, '_, 'tcx> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NLL doesn't consider boring locals for liveness, and wouldn't encounter a
|
||||||
|
// `Cause::LiveVar` for such a local. Polonius can't avoid computing liveness for boring
|
||||||
|
// locals yet, and will encounter them when trying to explain why a borrow contains a given
|
||||||
|
// point.
|
||||||
|
//
|
||||||
|
// We want to focus on relevant live locals in diagnostics, so when polonius is enabled, we
|
||||||
|
// ensure that we don't emit live boring locals as explanations.
|
||||||
|
let is_local_boring = |local| {
|
||||||
|
if let Some(polonius_diagnostics) = self.polonius_diagnostics {
|
||||||
|
polonius_diagnostics.boring_nll_locals.contains(&local)
|
||||||
|
} else {
|
||||||
|
assert!(!tcx.sess.opts.unstable_opts.polonius.is_next_enabled());
|
||||||
|
|
||||||
|
// Boring locals are never the cause of a borrow explanation in NLLs.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
match find_use::find(body, regioncx, tcx, region_sub, use_location) {
|
match find_use::find(body, regioncx, tcx, region_sub, use_location) {
|
||||||
Some(Cause::LiveVar(local, location)) => {
|
Some(Cause::LiveVar(local, location)) if !is_local_boring(local) => {
|
||||||
let span = body.source_info(location).span;
|
let span = body.source_info(location).span;
|
||||||
let spans = self
|
let spans = self
|
||||||
.move_spans(Place::from(local).as_ref(), location)
|
.move_spans(Place::from(local).as_ref(), location)
|
||||||
|
@ -666,7 +683,9 @@ impl<'tcx> MirBorrowckCtxt<'_, '_, 'tcx> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
Some(Cause::LiveVar(..)) | None => {
|
||||||
|
// Here, under NLL: no cause was found. Under polonius: no cause was found, or a
|
||||||
|
// boring local was found, which we ignore like NLLs do to match its diagnostics.
|
||||||
if let Some(region) = self.to_error_region_vid(borrow_region_vid) {
|
if let Some(region) = self.to_error_region_vid(borrow_region_vid) {
|
||||||
let (category, from_closure, span, region_name, path) =
|
let (category, from_closure, span, region_name, path) =
|
||||||
self.free_region_constraint_info(borrow_region_vid, region);
|
self.free_region_constraint_info(borrow_region_vid, region);
|
||||||
|
|
|
@ -60,6 +60,7 @@ use crate::diagnostics::{
|
||||||
use crate::path_utils::*;
|
use crate::path_utils::*;
|
||||||
use crate::place_ext::PlaceExt;
|
use crate::place_ext::PlaceExt;
|
||||||
use crate::places_conflict::{PlaceConflictBias, places_conflict};
|
use crate::places_conflict::{PlaceConflictBias, places_conflict};
|
||||||
|
use crate::polonius::PoloniusDiagnosticsContext;
|
||||||
use crate::polonius::legacy::{PoloniusLocationTable, PoloniusOutput};
|
use crate::polonius::legacy::{PoloniusLocationTable, PoloniusOutput};
|
||||||
use crate::prefixes::PrefixSet;
|
use crate::prefixes::PrefixSet;
|
||||||
use crate::region_infer::RegionInferenceContext;
|
use crate::region_infer::RegionInferenceContext;
|
||||||
|
@ -198,7 +199,7 @@ fn do_mir_borrowck<'tcx>(
|
||||||
polonius_output,
|
polonius_output,
|
||||||
opt_closure_req,
|
opt_closure_req,
|
||||||
nll_errors,
|
nll_errors,
|
||||||
localized_outlives_constraints,
|
polonius_diagnostics,
|
||||||
} = nll::compute_regions(
|
} = nll::compute_regions(
|
||||||
&infcx,
|
&infcx,
|
||||||
free_regions,
|
free_regions,
|
||||||
|
@ -270,6 +271,7 @@ fn do_mir_borrowck<'tcx>(
|
||||||
polonius_output: None,
|
polonius_output: None,
|
||||||
move_errors: Vec::new(),
|
move_errors: Vec::new(),
|
||||||
diags_buffer,
|
diags_buffer,
|
||||||
|
polonius_diagnostics: polonius_diagnostics.as_ref(),
|
||||||
};
|
};
|
||||||
struct MoveVisitor<'a, 'b, 'infcx, 'tcx> {
|
struct MoveVisitor<'a, 'b, 'infcx, 'tcx> {
|
||||||
ctxt: &'a mut MirBorrowckCtxt<'b, 'infcx, 'tcx>,
|
ctxt: &'a mut MirBorrowckCtxt<'b, 'infcx, 'tcx>,
|
||||||
|
@ -308,6 +310,7 @@ fn do_mir_borrowck<'tcx>(
|
||||||
polonius_output,
|
polonius_output,
|
||||||
move_errors: Vec::new(),
|
move_errors: Vec::new(),
|
||||||
diags_buffer,
|
diags_buffer,
|
||||||
|
polonius_diagnostics: polonius_diagnostics.as_ref(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compute and report region errors, if any.
|
// Compute and report region errors, if any.
|
||||||
|
@ -329,7 +332,7 @@ fn do_mir_borrowck<'tcx>(
|
||||||
body,
|
body,
|
||||||
®ioncx,
|
®ioncx,
|
||||||
&borrow_set,
|
&borrow_set,
|
||||||
localized_outlives_constraints,
|
polonius_diagnostics.as_ref(),
|
||||||
&opt_closure_req,
|
&opt_closure_req,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -579,6 +582,9 @@ struct MirBorrowckCtxt<'a, 'infcx, 'tcx> {
|
||||||
|
|
||||||
diags_buffer: &'a mut BorrowckDiagnosticsBuffer<'infcx, 'tcx>,
|
diags_buffer: &'a mut BorrowckDiagnosticsBuffer<'infcx, 'tcx>,
|
||||||
move_errors: Vec<MoveError<'tcx>>,
|
move_errors: Vec<MoveError<'tcx>>,
|
||||||
|
|
||||||
|
/// When using `-Zpolonius=next`: the data used to compute errors and diagnostics.
|
||||||
|
polonius_diagnostics: Option<&'a PoloniusDiagnosticsContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that:
|
// Check that:
|
||||||
|
|
|
@ -27,7 +27,7 @@ use tracing::{debug, instrument};
|
||||||
use crate::borrow_set::BorrowSet;
|
use crate::borrow_set::BorrowSet;
|
||||||
use crate::consumers::ConsumerOptions;
|
use crate::consumers::ConsumerOptions;
|
||||||
use crate::diagnostics::{BorrowckDiagnosticsBuffer, RegionErrors};
|
use crate::diagnostics::{BorrowckDiagnosticsBuffer, RegionErrors};
|
||||||
use crate::polonius::LocalizedOutlivesConstraintSet;
|
use crate::polonius::PoloniusDiagnosticsContext;
|
||||||
use crate::polonius::legacy::{
|
use crate::polonius::legacy::{
|
||||||
PoloniusFacts, PoloniusFactsExt, PoloniusLocationTable, PoloniusOutput,
|
PoloniusFacts, PoloniusFactsExt, PoloniusLocationTable, PoloniusOutput,
|
||||||
};
|
};
|
||||||
|
@ -46,8 +46,9 @@ pub(crate) struct NllOutput<'tcx> {
|
||||||
pub opt_closure_req: Option<ClosureRegionRequirements<'tcx>>,
|
pub opt_closure_req: Option<ClosureRegionRequirements<'tcx>>,
|
||||||
pub nll_errors: RegionErrors<'tcx>,
|
pub nll_errors: RegionErrors<'tcx>,
|
||||||
|
|
||||||
/// When using `-Zpolonius=next`: the localized typeck and liveness constraints.
|
/// When using `-Zpolonius=next`: the data used to compute errors and diagnostics, e.g.
|
||||||
pub localized_outlives_constraints: Option<LocalizedOutlivesConstraintSet>,
|
/// localized typeck and liveness constraints.
|
||||||
|
pub polonius_diagnostics: Option<PoloniusDiagnosticsContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rewrites the regions in the MIR to use NLL variables, also scraping out the set of universal
|
/// Rewrites the regions in the MIR to use NLL variables, also scraping out the set of universal
|
||||||
|
@ -144,7 +145,7 @@ pub(crate) fn compute_regions<'a, 'tcx>(
|
||||||
|
|
||||||
// If requested for `-Zpolonius=next`, convert NLL constraints to localized outlives constraints
|
// If requested for `-Zpolonius=next`, convert NLL constraints to localized outlives constraints
|
||||||
// and use them to compute loan liveness.
|
// and use them to compute loan liveness.
|
||||||
let localized_outlives_constraints = polonius_context.as_ref().map(|polonius_context| {
|
let polonius_diagnostics = polonius_context.map(|polonius_context| {
|
||||||
polonius_context.compute_loan_liveness(infcx.tcx, &mut regioncx, body, borrow_set)
|
polonius_context.compute_loan_liveness(infcx.tcx, &mut regioncx, body, borrow_set)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,7 +189,7 @@ pub(crate) fn compute_regions<'a, 'tcx>(
|
||||||
polonius_output,
|
polonius_output,
|
||||||
opt_closure_req: closure_region_requirements,
|
opt_closure_req: closure_region_requirements,
|
||||||
nll_errors,
|
nll_errors,
|
||||||
localized_outlives_constraints,
|
polonius_diagnostics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,9 @@ use rustc_session::config::MirIncludeSpans;
|
||||||
|
|
||||||
use crate::borrow_set::BorrowSet;
|
use crate::borrow_set::BorrowSet;
|
||||||
use crate::constraints::OutlivesConstraint;
|
use crate::constraints::OutlivesConstraint;
|
||||||
use crate::polonius::{LocalizedOutlivesConstraint, LocalizedOutlivesConstraintSet};
|
use crate::polonius::{
|
||||||
|
LocalizedOutlivesConstraint, LocalizedOutlivesConstraintSet, PoloniusDiagnosticsContext,
|
||||||
|
};
|
||||||
use crate::region_infer::values::LivenessValues;
|
use crate::region_infer::values::LivenessValues;
|
||||||
use crate::type_check::Locations;
|
use crate::type_check::Locations;
|
||||||
use crate::{BorrowckInferCtxt, RegionInferenceContext};
|
use crate::{BorrowckInferCtxt, RegionInferenceContext};
|
||||||
|
@ -23,7 +25,7 @@ pub(crate) fn dump_polonius_mir<'tcx>(
|
||||||
body: &Body<'tcx>,
|
body: &Body<'tcx>,
|
||||||
regioncx: &RegionInferenceContext<'tcx>,
|
regioncx: &RegionInferenceContext<'tcx>,
|
||||||
borrow_set: &BorrowSet<'tcx>,
|
borrow_set: &BorrowSet<'tcx>,
|
||||||
localized_outlives_constraints: Option<LocalizedOutlivesConstraintSet>,
|
polonius_diagnostics: Option<&PoloniusDiagnosticsContext>,
|
||||||
closure_region_requirements: &Option<ClosureRegionRequirements<'tcx>>,
|
closure_region_requirements: &Option<ClosureRegionRequirements<'tcx>>,
|
||||||
) {
|
) {
|
||||||
let tcx = infcx.tcx;
|
let tcx = infcx.tcx;
|
||||||
|
@ -35,8 +37,8 @@ pub(crate) fn dump_polonius_mir<'tcx>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let localized_outlives_constraints = localized_outlives_constraints
|
let polonius_diagnostics =
|
||||||
.expect("missing localized constraints with `-Zpolonius=next`");
|
polonius_diagnostics.expect("missing diagnostics context with `-Zpolonius=next`");
|
||||||
|
|
||||||
let _: io::Result<()> = try {
|
let _: io::Result<()> = try {
|
||||||
let mut file = create_dump_file(tcx, "html", false, "polonius", &0, body)?;
|
let mut file = create_dump_file(tcx, "html", false, "polonius", &0, body)?;
|
||||||
|
@ -45,7 +47,7 @@ pub(crate) fn dump_polonius_mir<'tcx>(
|
||||||
body,
|
body,
|
||||||
regioncx,
|
regioncx,
|
||||||
borrow_set,
|
borrow_set,
|
||||||
localized_outlives_constraints,
|
&polonius_diagnostics.localized_outlives_constraints,
|
||||||
closure_region_requirements,
|
closure_region_requirements,
|
||||||
&mut file,
|
&mut file,
|
||||||
)?;
|
)?;
|
||||||
|
@ -63,7 +65,7 @@ fn emit_polonius_dump<'tcx>(
|
||||||
body: &Body<'tcx>,
|
body: &Body<'tcx>,
|
||||||
regioncx: &RegionInferenceContext<'tcx>,
|
regioncx: &RegionInferenceContext<'tcx>,
|
||||||
borrow_set: &BorrowSet<'tcx>,
|
borrow_set: &BorrowSet<'tcx>,
|
||||||
localized_outlives_constraints: LocalizedOutlivesConstraintSet,
|
localized_outlives_constraints: &LocalizedOutlivesConstraintSet,
|
||||||
closure_region_requirements: &Option<ClosureRegionRequirements<'tcx>>,
|
closure_region_requirements: &Option<ClosureRegionRequirements<'tcx>>,
|
||||||
out: &mut dyn io::Write,
|
out: &mut dyn io::Write,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use rustc_index::bit_set::SparseBitMatrix;
|
use rustc_index::bit_set::SparseBitMatrix;
|
||||||
use rustc_index::interval::SparseIntervalMatrix;
|
|
||||||
use rustc_middle::mir::{Body, Location};
|
use rustc_middle::mir::{Body, Location};
|
||||||
use rustc_middle::ty::relate::{self, Relate, RelateResult, TypeRelation};
|
use rustc_middle::ty::relate::{self, Relate, RelateResult, TypeRelation};
|
||||||
use rustc_middle::ty::{self, RegionVid, Ty, TyCtxt, TypeVisitable};
|
use rustc_middle::ty::{self, RegionVid, Ty, TyCtxt, TypeVisitable};
|
||||||
|
@ -9,12 +8,12 @@ use rustc_mir_dataflow::points::PointIndex;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ConstraintDirection, LocalizedOutlivesConstraint, LocalizedOutlivesConstraintSet,
|
ConstraintDirection, LocalizedOutlivesConstraint, LocalizedOutlivesConstraintSet,
|
||||||
PoloniusContext,
|
PoloniusLivenessContext,
|
||||||
};
|
};
|
||||||
use crate::region_infer::values::LivenessValues;
|
use crate::region_infer::values::LivenessValues;
|
||||||
use crate::universal_regions::UniversalRegions;
|
use crate::universal_regions::UniversalRegions;
|
||||||
|
|
||||||
impl PoloniusContext {
|
impl PoloniusLivenessContext {
|
||||||
/// Record the variance of each region contained within the given value.
|
/// Record the variance of each region contained within the given value.
|
||||||
pub(crate) fn record_live_region_variance<'tcx>(
|
pub(crate) fn record_live_region_variance<'tcx>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -30,25 +29,6 @@ impl PoloniusContext {
|
||||||
};
|
};
|
||||||
extractor.relate(value, value).expect("Can't have a type error relating to itself");
|
extractor.relate(value, value).expect("Can't have a type error relating to itself");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unlike NLLs, in polonius we traverse the cfg to look for regions live across an edge, so we
|
|
||||||
/// need to transpose the "points where each region is live" matrix to a "live regions per point"
|
|
||||||
/// matrix.
|
|
||||||
// FIXME: avoid this conversion by always storing liveness data in this shape in the rest of
|
|
||||||
// borrowck.
|
|
||||||
pub(crate) fn record_live_regions_per_point(
|
|
||||||
&mut self,
|
|
||||||
num_regions: usize,
|
|
||||||
points_per_live_region: &SparseIntervalMatrix<RegionVid, PointIndex>,
|
|
||||||
) {
|
|
||||||
let mut live_regions_per_point = SparseBitMatrix::new(num_regions);
|
|
||||||
for region in points_per_live_region.rows() {
|
|
||||||
for point in points_per_live_region.row(region).unwrap().iter() {
|
|
||||||
live_regions_per_point.insert(point, region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.live_regions = Some(live_regions_per_point);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Propagate loans throughout the CFG: for each statement in the MIR, create localized outlives
|
/// Propagate loans throughout the CFG: for each statement in the MIR, create localized outlives
|
||||||
|
|
|
@ -32,6 +32,16 @@
|
||||||
//! - <https://smallcultfollowing.com/babysteps/blog/2023/09/22/polonius-part-1/>
|
//! - <https://smallcultfollowing.com/babysteps/blog/2023/09/22/polonius-part-1/>
|
||||||
//! - <https://smallcultfollowing.com/babysteps/blog/2023/09/29/polonius-part-2/>
|
//! - <https://smallcultfollowing.com/babysteps/blog/2023/09/29/polonius-part-2/>
|
||||||
//!
|
//!
|
||||||
|
//!
|
||||||
|
//! Data flows like this:
|
||||||
|
//! 1) during MIR typeck, record liveness data needed later: live region variances, as well as the
|
||||||
|
//! usual NLL liveness data (just computed on more locals). That's the [PoloniusLivenessContext].
|
||||||
|
//! 2) once that is done, variance data is transferred, and the NLL region liveness is converted to
|
||||||
|
//! the polonius shape. That's the main [PoloniusContext].
|
||||||
|
//! 3) during region inference, that data and the NLL outlives constraints are used to create the
|
||||||
|
//! localized outlives constraints, as described above. That's the [PoloniusDiagnosticsContext].
|
||||||
|
//! 4) transfer this back to the main borrowck procedure: it handles computing errors and
|
||||||
|
//! diagnostics, debugging and MIR dumping concerns.
|
||||||
|
|
||||||
mod constraints;
|
mod constraints;
|
||||||
mod dump;
|
mod dump;
|
||||||
|
@ -42,8 +52,10 @@ mod typeck_constraints;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use rustc_data_structures::fx::FxHashSet;
|
||||||
use rustc_index::bit_set::SparseBitMatrix;
|
use rustc_index::bit_set::SparseBitMatrix;
|
||||||
use rustc_middle::mir::Body;
|
use rustc_index::interval::SparseIntervalMatrix;
|
||||||
|
use rustc_middle::mir::{Body, Local};
|
||||||
use rustc_middle::ty::{RegionVid, TyCtxt};
|
use rustc_middle::ty::{RegionVid, TyCtxt};
|
||||||
use rustc_mir_dataflow::points::PointIndex;
|
use rustc_mir_dataflow::points::PointIndex;
|
||||||
|
|
||||||
|
@ -57,15 +69,40 @@ use crate::{BorrowSet, RegionInferenceContext};
|
||||||
|
|
||||||
pub(crate) type LiveLoans = SparseBitMatrix<PointIndex, BorrowIndex>;
|
pub(crate) type LiveLoans = SparseBitMatrix<PointIndex, BorrowIndex>;
|
||||||
|
|
||||||
/// This struct holds the data needed to create the Polonius localized constraints.
|
/// This struct holds the liveness data created during MIR typeck, and which will be used later in
|
||||||
pub(crate) struct PoloniusContext {
|
/// the process, to compute the polonius localized constraints.
|
||||||
/// The set of regions that are live at a given point in the CFG, used to create localized
|
#[derive(Default)]
|
||||||
/// outlives constraints between regions that are live at connected points in the CFG.
|
pub(crate) struct PoloniusLivenessContext {
|
||||||
live_regions: Option<SparseBitMatrix<PointIndex, RegionVid>>,
|
|
||||||
|
|
||||||
/// The expected edge direction per live region: the kind of directed edge we'll create as
|
/// The expected edge direction per live region: the kind of directed edge we'll create as
|
||||||
/// liveness constraints depends on the variance of types with respect to each contained region.
|
/// liveness constraints depends on the variance of types with respect to each contained region.
|
||||||
live_region_variances: BTreeMap<RegionVid, ConstraintDirection>,
|
live_region_variances: BTreeMap<RegionVid, ConstraintDirection>,
|
||||||
|
|
||||||
|
/// The regions that outlive free regions are used to distinguish relevant live locals from
|
||||||
|
/// boring locals. A boring local is one whose type contains only such regions. Polonius
|
||||||
|
/// currently has more boring locals than NLLs so we record the latter to use in errors and
|
||||||
|
/// diagnostics, to focus on the locals we consider relevant and match NLL diagnostics.
|
||||||
|
pub(crate) boring_nll_locals: FxHashSet<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct holds the data needed to create the Polonius localized constraints. Its data is
|
||||||
|
/// transferred and converted from the [PoloniusLivenessContext] at the end of MIR typeck.
|
||||||
|
pub(crate) struct PoloniusContext {
|
||||||
|
/// The liveness data we recorded during MIR typeck.
|
||||||
|
liveness_context: PoloniusLivenessContext,
|
||||||
|
|
||||||
|
/// The set of regions that are live at a given point in the CFG, used to create localized
|
||||||
|
/// outlives constraints between regions that are live at connected points in the CFG.
|
||||||
|
live_regions: SparseBitMatrix<PointIndex, RegionVid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct holds the data needed by the borrowck error computation and diagnostics. Its data is
|
||||||
|
/// computed from the [PoloniusContext] when computing NLL regions.
|
||||||
|
pub(crate) struct PoloniusDiagnosticsContext {
|
||||||
|
/// The localized outlives constraints that were computed in the main analysis.
|
||||||
|
localized_outlives_constraints: LocalizedOutlivesConstraintSet,
|
||||||
|
|
||||||
|
/// The liveness data computed during MIR typeck: [PoloniusLivenessContext::boring_nll_locals].
|
||||||
|
pub(crate) boring_nll_locals: FxHashSet<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The direction a constraint can flow into. Used to create liveness constraints according to
|
/// The direction a constraint can flow into. Used to create liveness constraints according to
|
||||||
|
@ -83,8 +120,24 @@ enum ConstraintDirection {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PoloniusContext {
|
impl PoloniusContext {
|
||||||
pub(crate) fn new() -> PoloniusContext {
|
/// Unlike NLLs, in polonius we traverse the cfg to look for regions live across an edge, so we
|
||||||
Self { live_region_variances: BTreeMap::new(), live_regions: None }
|
/// need to transpose the "points where each region is live" matrix to a "live regions per point"
|
||||||
|
/// matrix.
|
||||||
|
// FIXME: avoid this conversion by always storing liveness data in this shape in the rest of
|
||||||
|
// borrowck.
|
||||||
|
pub(crate) fn create_from_liveness(
|
||||||
|
liveness_context: PoloniusLivenessContext,
|
||||||
|
num_regions: usize,
|
||||||
|
points_per_live_region: &SparseIntervalMatrix<RegionVid, PointIndex>,
|
||||||
|
) -> PoloniusContext {
|
||||||
|
let mut live_regions_per_point = SparseBitMatrix::new(num_regions);
|
||||||
|
for region in points_per_live_region.rows() {
|
||||||
|
for point in points_per_live_region.row(region).unwrap().iter() {
|
||||||
|
live_regions_per_point.insert(point, region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PoloniusContext { live_regions: live_regions_per_point, liveness_context }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes live loans using the set of loans model for `-Zpolonius=next`.
|
/// Computes live loans using the set of loans model for `-Zpolonius=next`.
|
||||||
|
@ -95,13 +148,18 @@ impl PoloniusContext {
|
||||||
///
|
///
|
||||||
/// Then, this graph is traversed, and combined with kills, reachability is recorded as loan
|
/// 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.
|
/// 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>(
|
pub(crate) fn compute_loan_liveness<'tcx>(
|
||||||
&self,
|
self,
|
||||||
tcx: TyCtxt<'tcx>,
|
tcx: TyCtxt<'tcx>,
|
||||||
regioncx: &mut RegionInferenceContext<'tcx>,
|
regioncx: &mut RegionInferenceContext<'tcx>,
|
||||||
body: &Body<'tcx>,
|
body: &Body<'tcx>,
|
||||||
borrow_set: &BorrowSet<'tcx>,
|
borrow_set: &BorrowSet<'tcx>,
|
||||||
) -> LocalizedOutlivesConstraintSet {
|
) -> PoloniusDiagnosticsContext {
|
||||||
|
let PoloniusLivenessContext { live_region_variances, boring_nll_locals } =
|
||||||
|
self.liveness_context;
|
||||||
|
|
||||||
let mut localized_outlives_constraints = LocalizedOutlivesConstraintSet::default();
|
let mut localized_outlives_constraints = LocalizedOutlivesConstraintSet::default();
|
||||||
convert_typeck_constraints(
|
convert_typeck_constraints(
|
||||||
tcx,
|
tcx,
|
||||||
|
@ -112,14 +170,11 @@ impl PoloniusContext {
|
||||||
&mut localized_outlives_constraints,
|
&mut localized_outlives_constraints,
|
||||||
);
|
);
|
||||||
|
|
||||||
let live_regions = self.live_regions.as_ref().expect(
|
|
||||||
"live regions per-point data should have been created at the end of MIR typeck",
|
|
||||||
);
|
|
||||||
create_liveness_constraints(
|
create_liveness_constraints(
|
||||||
body,
|
body,
|
||||||
regioncx.liveness_constraints(),
|
regioncx.liveness_constraints(),
|
||||||
live_regions,
|
&self.live_regions,
|
||||||
&self.live_region_variances,
|
&live_region_variances,
|
||||||
regioncx.universal_regions(),
|
regioncx.universal_regions(),
|
||||||
&mut localized_outlives_constraints,
|
&mut localized_outlives_constraints,
|
||||||
);
|
);
|
||||||
|
@ -136,6 +191,6 @@ impl PoloniusContext {
|
||||||
);
|
);
|
||||||
regioncx.record_live_loans(live_loans);
|
regioncx.record_live_loans(live_loans);
|
||||||
|
|
||||||
localized_outlives_constraints
|
PoloniusDiagnosticsContext { localized_outlives_constraints, boring_nll_locals }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use tracing::debug;
|
||||||
|
|
||||||
use super::TypeChecker;
|
use super::TypeChecker;
|
||||||
use crate::constraints::OutlivesConstraintSet;
|
use crate::constraints::OutlivesConstraintSet;
|
||||||
use crate::polonius::PoloniusContext;
|
use crate::polonius::PoloniusLivenessContext;
|
||||||
use crate::region_infer::values::LivenessValues;
|
use crate::region_infer::values::LivenessValues;
|
||||||
use crate::universal_regions::UniversalRegions;
|
use crate::universal_regions::UniversalRegions;
|
||||||
|
|
||||||
|
@ -38,19 +38,24 @@ pub(super) fn generate<'a, 'tcx>(
|
||||||
) {
|
) {
|
||||||
debug!("liveness::generate");
|
debug!("liveness::generate");
|
||||||
|
|
||||||
|
let mut free_regions = regions_that_outlive_free_regions(
|
||||||
|
typeck.infcx.num_region_vars(),
|
||||||
|
&typeck.universal_regions,
|
||||||
|
&typeck.constraints.outlives_constraints,
|
||||||
|
);
|
||||||
|
|
||||||
// NLLs can avoid computing some liveness data here because its constraints are
|
// NLLs can avoid computing some liveness data here because its constraints are
|
||||||
// location-insensitive, but that doesn't work in polonius: locals whose type contains a region
|
// location-insensitive, but that doesn't work in polonius: locals whose type contains a region
|
||||||
// that outlives a free region are not necessarily live everywhere in a flow-sensitive setting,
|
// that outlives a free region are not necessarily live everywhere in a flow-sensitive setting,
|
||||||
// unlike NLLs.
|
// unlike NLLs.
|
||||||
let free_regions = if !typeck.tcx().sess.opts.unstable_opts.polonius.is_next_enabled() {
|
// We do record these regions in the polonius context, since they're used to differentiate
|
||||||
regions_that_outlive_free_regions(
|
// relevant and boring locals, which is a key distinction used later in diagnostics.
|
||||||
typeck.infcx.num_region_vars(),
|
if typeck.tcx().sess.opts.unstable_opts.polonius.is_next_enabled() {
|
||||||
&typeck.universal_regions,
|
let (_, boring_locals) = compute_relevant_live_locals(typeck.tcx(), &free_regions, body);
|
||||||
&typeck.constraints.outlives_constraints,
|
typeck.polonius_liveness.as_mut().unwrap().boring_nll_locals =
|
||||||
)
|
boring_locals.into_iter().collect();
|
||||||
} else {
|
free_regions = typeck.universal_regions.universal_regions_iter().collect();
|
||||||
typeck.universal_regions.universal_regions_iter().collect()
|
}
|
||||||
};
|
|
||||||
let (relevant_live_locals, boring_locals) =
|
let (relevant_live_locals, boring_locals) =
|
||||||
compute_relevant_live_locals(typeck.tcx(), &free_regions, body);
|
compute_relevant_live_locals(typeck.tcx(), &free_regions, body);
|
||||||
|
|
||||||
|
@ -70,7 +75,7 @@ pub(super) fn generate<'a, 'tcx>(
|
||||||
typeck.tcx(),
|
typeck.tcx(),
|
||||||
&mut typeck.constraints.liveness_constraints,
|
&mut typeck.constraints.liveness_constraints,
|
||||||
&typeck.universal_regions,
|
&typeck.universal_regions,
|
||||||
&mut typeck.polonius_context,
|
&mut typeck.polonius_liveness,
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -147,11 +152,11 @@ fn record_regular_live_regions<'tcx>(
|
||||||
tcx: TyCtxt<'tcx>,
|
tcx: TyCtxt<'tcx>,
|
||||||
liveness_constraints: &mut LivenessValues,
|
liveness_constraints: &mut LivenessValues,
|
||||||
universal_regions: &UniversalRegions<'tcx>,
|
universal_regions: &UniversalRegions<'tcx>,
|
||||||
polonius_context: &mut Option<PoloniusContext>,
|
polonius_liveness: &mut Option<PoloniusLivenessContext>,
|
||||||
body: &Body<'tcx>,
|
body: &Body<'tcx>,
|
||||||
) {
|
) {
|
||||||
let mut visitor =
|
let mut visitor =
|
||||||
LiveVariablesVisitor { tcx, liveness_constraints, universal_regions, polonius_context };
|
LiveVariablesVisitor { tcx, liveness_constraints, universal_regions, polonius_liveness };
|
||||||
for (bb, data) in body.basic_blocks.iter_enumerated() {
|
for (bb, data) in body.basic_blocks.iter_enumerated() {
|
||||||
visitor.visit_basic_block_data(bb, data);
|
visitor.visit_basic_block_data(bb, data);
|
||||||
}
|
}
|
||||||
|
@ -162,7 +167,7 @@ struct LiveVariablesVisitor<'a, 'tcx> {
|
||||||
tcx: TyCtxt<'tcx>,
|
tcx: TyCtxt<'tcx>,
|
||||||
liveness_constraints: &'a mut LivenessValues,
|
liveness_constraints: &'a mut LivenessValues,
|
||||||
universal_regions: &'a UniversalRegions<'tcx>,
|
universal_regions: &'a UniversalRegions<'tcx>,
|
||||||
polonius_context: &'a mut Option<PoloniusContext>,
|
polonius_liveness: &'a mut Option<PoloniusLivenessContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'tcx> Visitor<'tcx> for LiveVariablesVisitor<'a, 'tcx> {
|
impl<'a, 'tcx> Visitor<'tcx> for LiveVariablesVisitor<'a, 'tcx> {
|
||||||
|
@ -214,8 +219,8 @@ impl<'a, 'tcx> LiveVariablesVisitor<'a, 'tcx> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// When using `-Zpolonius=next`, we record the variance of each live region.
|
// When using `-Zpolonius=next`, we record the variance of each live region.
|
||||||
if let Some(polonius_context) = self.polonius_context {
|
if let Some(polonius_liveness) = self.polonius_liveness {
|
||||||
polonius_context.record_live_region_variance(self.tcx, self.universal_regions, value);
|
polonius_liveness.record_live_region_variance(self.tcx, self.universal_regions, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -580,8 +580,8 @@ impl<'tcx> LivenessContext<'_, '_, '_, 'tcx> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// When using `-Zpolonius=next`, we record the variance of each live region.
|
// When using `-Zpolonius=next`, we record the variance of each live region.
|
||||||
if let Some(polonius_context) = typeck.polonius_context {
|
if let Some(polonius_liveness) = typeck.polonius_liveness.as_mut() {
|
||||||
polonius_context.record_live_region_variance(
|
polonius_liveness.record_live_region_variance(
|
||||||
typeck.infcx.tcx,
|
typeck.infcx.tcx,
|
||||||
typeck.universal_regions,
|
typeck.universal_regions,
|
||||||
value,
|
value,
|
||||||
|
|
|
@ -48,8 +48,8 @@ use crate::borrow_set::BorrowSet;
|
||||||
use crate::constraints::{OutlivesConstraint, OutlivesConstraintSet};
|
use crate::constraints::{OutlivesConstraint, OutlivesConstraintSet};
|
||||||
use crate::diagnostics::UniverseInfo;
|
use crate::diagnostics::UniverseInfo;
|
||||||
use crate::member_constraints::MemberConstraintSet;
|
use crate::member_constraints::MemberConstraintSet;
|
||||||
use crate::polonius::PoloniusContext;
|
|
||||||
use crate::polonius::legacy::{PoloniusFacts, PoloniusLocationTable};
|
use crate::polonius::legacy::{PoloniusFacts, PoloniusLocationTable};
|
||||||
|
use crate::polonius::{PoloniusContext, PoloniusLivenessContext};
|
||||||
use crate::region_infer::TypeTest;
|
use crate::region_infer::TypeTest;
|
||||||
use crate::region_infer::values::{LivenessValues, PlaceholderIndex, PlaceholderIndices};
|
use crate::region_infer::values::{LivenessValues, PlaceholderIndex, PlaceholderIndices};
|
||||||
use crate::renumber::RegionCtxt;
|
use crate::renumber::RegionCtxt;
|
||||||
|
@ -148,8 +148,8 @@ pub(crate) fn type_check<'a, 'tcx>(
|
||||||
|
|
||||||
debug!(?normalized_inputs_and_output);
|
debug!(?normalized_inputs_and_output);
|
||||||
|
|
||||||
let mut polonius_context = if infcx.tcx.sess.opts.unstable_opts.polonius.is_next_enabled() {
|
let polonius_liveness = if infcx.tcx.sess.opts.unstable_opts.polonius.is_next_enabled() {
|
||||||
Some(PoloniusContext::new())
|
Some(PoloniusLivenessContext::default())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -168,7 +168,7 @@ pub(crate) fn type_check<'a, 'tcx>(
|
||||||
polonius_facts,
|
polonius_facts,
|
||||||
borrow_set,
|
borrow_set,
|
||||||
constraints: &mut constraints,
|
constraints: &mut constraints,
|
||||||
polonius_context: &mut polonius_context,
|
polonius_liveness,
|
||||||
};
|
};
|
||||||
|
|
||||||
typeck.check_user_type_annotations();
|
typeck.check_user_type_annotations();
|
||||||
|
@ -185,11 +185,14 @@ pub(crate) fn type_check<'a, 'tcx>(
|
||||||
let opaque_type_values =
|
let opaque_type_values =
|
||||||
opaque_types::take_opaques_and_register_member_constraints(&mut typeck);
|
opaque_types::take_opaques_and_register_member_constraints(&mut typeck);
|
||||||
|
|
||||||
if let Some(polonius_context) = typeck.polonius_context.as_mut() {
|
// We're done with typeck, we can finalize the polonius liveness context for region inference.
|
||||||
let num_regions = infcx.num_region_vars();
|
let polonius_context = typeck.polonius_liveness.take().map(|liveness_context| {
|
||||||
let points_per_live_region = typeck.constraints.liveness_constraints.points();
|
PoloniusContext::create_from_liveness(
|
||||||
polonius_context.record_live_regions_per_point(num_regions, points_per_live_region);
|
liveness_context,
|
||||||
}
|
infcx.num_region_vars(),
|
||||||
|
typeck.constraints.liveness_constraints.points(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
MirTypeckResults {
|
MirTypeckResults {
|
||||||
constraints,
|
constraints,
|
||||||
|
@ -583,8 +586,8 @@ struct TypeChecker<'a, 'tcx> {
|
||||||
polonius_facts: &'a mut Option<PoloniusFacts>,
|
polonius_facts: &'a mut Option<PoloniusFacts>,
|
||||||
borrow_set: &'a BorrowSet<'tcx>,
|
borrow_set: &'a BorrowSet<'tcx>,
|
||||||
constraints: &'a mut MirTypeckRegionConstraints<'tcx>,
|
constraints: &'a mut MirTypeckRegionConstraints<'tcx>,
|
||||||
/// When using `-Zpolonius=next`, the helper data used to create polonius constraints.
|
/// When using `-Zpolonius=next`, the liveness helper data used to create polonius constraints.
|
||||||
polonius_context: &'a mut Option<PoloniusContext>,
|
polonius_liveness: Option<PoloniusLivenessContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Holder struct for passing results from MIR typeck to the rest of the non-lexical regions
|
/// Holder struct for passing results from MIR typeck to the rest of the non-lexical regions
|
||||||
|
|
|
@ -3146,7 +3146,6 @@ ui/nll/issue-97997.rs
|
||||||
ui/nll/issue-98170.rs
|
ui/nll/issue-98170.rs
|
||||||
ui/nll/issue-98589-closures-relate-named-regions.rs
|
ui/nll/issue-98589-closures-relate-named-regions.rs
|
||||||
ui/nll/issue-98693.rs
|
ui/nll/issue-98693.rs
|
||||||
ui/nll/polonius/issue-46589.rs
|
|
||||||
ui/nll/relate_tys/issue-48071.rs
|
ui/nll/relate_tys/issue-48071.rs
|
||||||
ui/nll/ty-outlives/issue-53789-1.rs
|
ui/nll/ty-outlives/issue-53789-1.rs
|
||||||
ui/nll/ty-outlives/issue-53789-2.rs
|
ui/nll/ty-outlives/issue-53789-2.rs
|
||||||
|
|
18
tests/ui/nll/get_default.legacy.stderr
Normal file
18
tests/ui/nll/get_default.legacy.stderr
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
||||||
|
--> $DIR/get_default.rs:35:17
|
||||||
|
|
|
||||||
|
LL | fn err(map: &mut Map) -> &String {
|
||||||
|
| - let's call the lifetime of this reference `'1`
|
||||||
|
LL | loop {
|
||||||
|
LL | match map.get() {
|
||||||
|
| --- immutable borrow occurs here
|
||||||
|
LL | Some(v) => {
|
||||||
|
LL | map.set(String::new()); // We always expect an error here.
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
||||||
|
LL |
|
||||||
|
LL | return v;
|
||||||
|
| - returning this value requires that `*map` is borrowed for `'1`
|
||||||
|
|
||||||
|
error: aborting due to 1 previous error
|
||||||
|
|
||||||
|
For more information about this error, try `rustc --explain E0502`.
|
|
@ -1,5 +1,5 @@
|
||||||
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
||||||
--> $DIR/get_default.rs:21:17
|
--> $DIR/get_default.rs:24:17
|
||||||
|
|
|
|
||||||
LL | fn ok(map: &mut Map) -> &String {
|
LL | fn ok(map: &mut Map) -> &String {
|
||||||
| - let's call the lifetime of this reference `'1`
|
| - let's call the lifetime of this reference `'1`
|
||||||
|
@ -14,7 +14,7 @@ LL | map.set(String::new()); // Ideally, this would not error.
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
||||||
|
|
||||||
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
||||||
--> $DIR/get_default.rs:32:17
|
--> $DIR/get_default.rs:35:17
|
||||||
|
|
|
|
||||||
LL | fn err(map: &mut Map) -> &String {
|
LL | fn err(map: &mut Map) -> &String {
|
||||||
| - let's call the lifetime of this reference `'1`
|
| - let's call the lifetime of this reference `'1`
|
||||||
|
@ -22,14 +22,14 @@ LL | loop {
|
||||||
LL | match map.get() {
|
LL | match map.get() {
|
||||||
| --- immutable borrow occurs here
|
| --- immutable borrow occurs here
|
||||||
LL | Some(v) => {
|
LL | Some(v) => {
|
||||||
LL | map.set(String::new()); // Both AST and MIR error here
|
LL | map.set(String::new()); // We always expect an error here.
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
||||||
LL |
|
LL |
|
||||||
LL | return v;
|
LL | return v;
|
||||||
| - returning this value requires that `*map` is borrowed for `'1`
|
| - returning this value requires that `*map` is borrowed for `'1`
|
||||||
|
|
||||||
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
||||||
--> $DIR/get_default.rs:37:17
|
--> $DIR/get_default.rs:40:17
|
||||||
|
|
|
|
||||||
LL | fn err(map: &mut Map) -> &String {
|
LL | fn err(map: &mut Map) -> &String {
|
||||||
| - let's call the lifetime of this reference `'1`
|
| - let's call the lifetime of this reference `'1`
|
||||||
|
@ -40,7 +40,7 @@ LL | match map.get() {
|
||||||
LL | return v;
|
LL | return v;
|
||||||
| - returning this value requires that `*map` is borrowed for `'1`
|
| - returning this value requires that `*map` is borrowed for `'1`
|
||||||
...
|
...
|
||||||
LL | map.set(String::new()); // Ideally, just AST would error here
|
LL | map.set(String::new()); // Ideally, this would not error.
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
||||||
|
|
||||||
error: aborting due to 3 previous errors
|
error: aborting due to 3 previous errors
|
18
tests/ui/nll/get_default.polonius.stderr
Normal file
18
tests/ui/nll/get_default.polonius.stderr
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
|
||||||
|
--> $DIR/get_default.rs:35:17
|
||||||
|
|
|
||||||
|
LL | fn err(map: &mut Map) -> &String {
|
||||||
|
| - let's call the lifetime of this reference `'1`
|
||||||
|
LL | loop {
|
||||||
|
LL | match map.get() {
|
||||||
|
| --- immutable borrow occurs here
|
||||||
|
LL | Some(v) => {
|
||||||
|
LL | map.set(String::new()); // We always expect an error here.
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
||||||
|
LL |
|
||||||
|
LL | return v;
|
||||||
|
| - returning this value requires that `*map` is borrowed for `'1`
|
||||||
|
|
||||||
|
error: aborting due to 1 previous error
|
||||||
|
|
||||||
|
For more information about this error, try `rustc --explain E0502`.
|
|
@ -1,7 +1,10 @@
|
||||||
// Basic test for free regions in the NLL code. This test ought to
|
// Basic test for free regions in the NLL code. This test ought to
|
||||||
// report an error due to a reborrowing constraint. Right now, we get
|
// report an error due to a reborrowing constraint.
|
||||||
// a variety of errors from the older, AST-based machinery (notably
|
|
||||||
// borrowck), and then we get the NLL error at the end.
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: nll polonius legacy
|
||||||
|
//@ [polonius] compile-flags: -Z polonius=next
|
||||||
|
//@ [legacy] compile-flags: -Z polonius=legacy
|
||||||
|
|
||||||
struct Map {
|
struct Map {
|
||||||
}
|
}
|
||||||
|
@ -19,7 +22,7 @@ fn ok(map: &mut Map) -> &String {
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
map.set(String::new()); // Ideally, this would not error.
|
map.set(String::new()); // Ideally, this would not error.
|
||||||
//~^ ERROR borrowed as immutable
|
//[nll]~^ ERROR borrowed as immutable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,13 +32,13 @@ fn err(map: &mut Map) -> &String {
|
||||||
loop {
|
loop {
|
||||||
match map.get() {
|
match map.get() {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
map.set(String::new()); // Both AST and MIR error here
|
map.set(String::new()); // We always expect an error here.
|
||||||
//~^ ERROR borrowed as immutable
|
//~^ ERROR borrowed as immutable
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
map.set(String::new()); // Ideally, just AST would error here
|
map.set(String::new()); // Ideally, this would not error.
|
||||||
//~^ ERROR borrowed as immutable
|
//[nll]~^ ERROR borrowed as immutable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
error[E0499]: cannot borrow `**other` as mutable more than once at a time
|
error[E0499]: cannot borrow `**other` as mutable more than once at a time
|
||||||
--> $DIR/issue-46589.rs:23:21
|
--> $DIR/issue-46589.rs:24:21
|
||||||
|
|
|
|
||||||
LL | *other = match (*other).get_self() {
|
LL | *other = match (*other).get_self() {
|
||||||
| -------- first mutable borrow occurs here
|
| -------- first mutable borrow occurs here
|
|
@ -1,8 +1,9 @@
|
||||||
// This tests passes in Polonius mode, so is skipped in the automated compare-mode.
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
// We will manually check it passes in Polonius tests, as we can't have a test here
|
//@ revisions: nll polonius_next polonius
|
||||||
// which conditionally passes depending on a test revision/compile-flags.
|
//@ [polonius_next] check-pass
|
||||||
|
//@ [polonius_next] compile-flags: -Zpolonius=next
|
||||||
//@ ignore-compare-mode-polonius
|
//@ [polonius] check-pass
|
||||||
|
//@ [polonius] compile-flags: -Zpolonius
|
||||||
|
|
||||||
struct Foo;
|
struct Foo;
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ impl Foo {
|
||||||
*other = match (*other).get_self() {
|
*other = match (*other).get_self() {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => (*other).new_self()
|
None => (*other).new_self()
|
||||||
//~^ ERROR cannot borrow `**other` as mutable more than once at a time [E0499]
|
//[nll]~^ ERROR cannot borrow `**other` as mutable more than once at a time [E0499]
|
||||||
};
|
};
|
||||||
|
|
||||||
let c = other;
|
let c = other;
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
// facts only on simple assignments, but not projections, incorrectly causing errors to be emitted
|
// facts only on simple assignments, but not projections, incorrectly causing errors to be emitted
|
||||||
// for code accepted by NLL. They are all variations from example code in the NLL RFC.
|
// for code accepted by NLL. They are all variations from example code in the NLL RFC.
|
||||||
|
|
||||||
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: polonius_next polonius
|
||||||
//@ check-pass
|
//@ check-pass
|
||||||
//@ compile-flags: -Z polonius
|
//@ [polonius_next] compile-flags: -Z polonius=next
|
||||||
|
//@ [polonius] compile-flags: -Z polonius
|
||||||
|
|
||||||
struct List<T> {
|
struct List<T> {
|
||||||
value: T,
|
value: T,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
error[E0499]: cannot borrow `list.0.value` as mutable more than once at a time
|
error[E0499]: cannot borrow `list.0.value` as mutable more than once at a time
|
||||||
--> $DIR/assignment-to-differing-field.rs:20:21
|
--> $DIR/assignment-to-differing-field.rs:23:21
|
||||||
|
|
|
|
||||||
LL | fn assignment_to_field_projection<'a, T>(
|
LL | fn assignment_to_field_projection<'a, T>(
|
||||||
| -- lifetime `'a` defined here
|
| -- lifetime `'a` defined here
|
||||||
|
@ -11,7 +11,7 @@ LL | return result;
|
||||||
| ------ returning this value requires that `list.0.value` is borrowed for `'a`
|
| ------ returning this value requires that `list.0.value` is borrowed for `'a`
|
||||||
|
|
||||||
error[E0499]: cannot borrow `list.0.next` as mutable more than once at a time
|
error[E0499]: cannot borrow `list.0.next` as mutable more than once at a time
|
||||||
--> $DIR/assignment-to-differing-field.rs:23:26
|
--> $DIR/assignment-to-differing-field.rs:26:26
|
||||||
|
|
|
|
||||||
LL | fn assignment_to_field_projection<'a, T>(
|
LL | fn assignment_to_field_projection<'a, T>(
|
||||||
| -- lifetime `'a` defined here
|
| -- lifetime `'a` defined here
|
||||||
|
@ -23,7 +23,7 @@ LL | list.1 = n;
|
||||||
| ---------- assignment requires that `list.0.next` is borrowed for `'a`
|
| ---------- assignment requires that `list.0.next` is borrowed for `'a`
|
||||||
|
|
||||||
error[E0499]: cannot borrow `list.0.0.0.0.0.value` as mutable more than once at a time
|
error[E0499]: cannot borrow `list.0.0.0.0.0.value` as mutable more than once at a time
|
||||||
--> $DIR/assignment-to-differing-field.rs:37:21
|
--> $DIR/assignment-to-differing-field.rs:40:21
|
||||||
|
|
|
|
||||||
LL | fn assignment_through_projection_chain<'a, T>(
|
LL | fn assignment_through_projection_chain<'a, T>(
|
||||||
| -- lifetime `'a` defined here
|
| -- lifetime `'a` defined here
|
||||||
|
@ -35,7 +35,7 @@ LL | return result;
|
||||||
| ------ returning this value requires that `list.0.0.0.0.0.value` is borrowed for `'a`
|
| ------ returning this value requires that `list.0.0.0.0.0.value` is borrowed for `'a`
|
||||||
|
|
||||||
error[E0499]: cannot borrow `list.0.0.0.0.0.next` as mutable more than once at a time
|
error[E0499]: cannot borrow `list.0.0.0.0.0.next` as mutable more than once at a time
|
||||||
--> $DIR/assignment-to-differing-field.rs:40:26
|
--> $DIR/assignment-to-differing-field.rs:43:26
|
||||||
|
|
|
|
||||||
LL | fn assignment_through_projection_chain<'a, T>(
|
LL | fn assignment_through_projection_chain<'a, T>(
|
||||||
| -- lifetime `'a` defined here
|
| -- lifetime `'a` defined here
|
|
@ -0,0 +1,51 @@
|
||||||
|
error[E0499]: cannot borrow `list.0.value` as mutable more than once at a time
|
||||||
|
--> $DIR/assignment-to-differing-field.rs:23:21
|
||||||
|
|
|
||||||
|
LL | fn assignment_to_field_projection<'a, T>(
|
||||||
|
| -- lifetime `'a` defined here
|
||||||
|
...
|
||||||
|
LL | result.push(&mut (list.0).value);
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^ `list.0.value` was mutably borrowed here in the previous iteration of the loop
|
||||||
|
...
|
||||||
|
LL | return result;
|
||||||
|
| ------ returning this value requires that `list.0.value` is borrowed for `'a`
|
||||||
|
|
||||||
|
error[E0499]: cannot borrow `list.0.next` as mutable more than once at a time
|
||||||
|
--> $DIR/assignment-to-differing-field.rs:26:26
|
||||||
|
|
|
||||||
|
LL | fn assignment_to_field_projection<'a, T>(
|
||||||
|
| -- lifetime `'a` defined here
|
||||||
|
...
|
||||||
|
LL | if let Some(n) = (list.0).next.as_mut() {
|
||||||
|
| ^^^^^^^^^^^^^ `list.0.next` was mutably borrowed here in the previous iteration of the loop
|
||||||
|
LL |
|
||||||
|
LL | list.1 = n;
|
||||||
|
| ---------- assignment requires that `list.0.next` is borrowed for `'a`
|
||||||
|
|
||||||
|
error[E0499]: cannot borrow `list.0.0.0.0.0.value` as mutable more than once at a time
|
||||||
|
--> $DIR/assignment-to-differing-field.rs:40:21
|
||||||
|
|
|
||||||
|
LL | fn assignment_through_projection_chain<'a, T>(
|
||||||
|
| -- lifetime `'a` defined here
|
||||||
|
...
|
||||||
|
LL | result.push(&mut ((((list.0).0).0).0).0.value);
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `list.0.0.0.0.0.value` was mutably borrowed here in the previous iteration of the loop
|
||||||
|
...
|
||||||
|
LL | return result;
|
||||||
|
| ------ returning this value requires that `list.0.0.0.0.0.value` is borrowed for `'a`
|
||||||
|
|
||||||
|
error[E0499]: cannot borrow `list.0.0.0.0.0.next` as mutable more than once at a time
|
||||||
|
--> $DIR/assignment-to-differing-field.rs:43:26
|
||||||
|
|
|
||||||
|
LL | fn assignment_through_projection_chain<'a, T>(
|
||||||
|
| -- lifetime `'a` defined here
|
||||||
|
...
|
||||||
|
LL | if let Some(n) = ((((list.0).0).0).0).0.next.as_mut() {
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `list.0.0.0.0.0.next` was mutably borrowed here in the previous iteration of the loop
|
||||||
|
LL |
|
||||||
|
LL | *((((list.0).0).0).0).1 = n;
|
||||||
|
| --------------------------- assignment requires that `list.0.0.0.0.0.next` is borrowed for `'a`
|
||||||
|
|
||||||
|
error: aborting due to 4 previous errors
|
||||||
|
|
||||||
|
For more information about this error, try `rustc --explain E0499`.
|
|
@ -4,7 +4,10 @@
|
||||||
// that we do not kill too many borrows. Assignments to the `.1`
|
// that we do not kill too many borrows. Assignments to the `.1`
|
||||||
// field projections should leave the borrows on `.0` intact.
|
// field projections should leave the borrows on `.0` intact.
|
||||||
|
|
||||||
//@ compile-flags: -Z polonius
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: polonius legacy
|
||||||
|
//@ [polonius] compile-flags: -Z polonius=next
|
||||||
|
//@ [legacy] compile-flags: -Z polonius=legacy
|
||||||
|
|
||||||
struct List<T> {
|
struct List<T> {
|
||||||
value: T,
|
value: T,
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
// by NLL but was incorrectly rejected by Polonius because of these
|
// by NLL but was incorrectly rejected by Polonius because of these
|
||||||
// missing `killed` facts.
|
// missing `killed` facts.
|
||||||
|
|
||||||
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: polonius_next polonius
|
||||||
//@ check-pass
|
//@ check-pass
|
||||||
//@ compile-flags: -Z polonius
|
//@ [polonius_next] compile-flags: -Z polonius=next
|
||||||
|
//@ [polonius] compile-flags: -Z polonius
|
||||||
|
|
||||||
struct Thing;
|
struct Thing;
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
// This test is a copy of `ui/nll/issue-46589.rs` which fails in NLL but succeeds in Polonius.
|
|
||||||
// As we can't have a test here which conditionally passes depending on a test
|
|
||||||
// revision/compile-flags. We ensure here that it passes in Polonius mode.
|
|
||||||
|
|
||||||
//@ check-pass
|
|
||||||
//@ compile-flags: -Z polonius
|
|
||||||
|
|
||||||
struct Foo;
|
|
||||||
|
|
||||||
impl Foo {
|
|
||||||
fn get_self(&mut self) -> Option<&mut Self> {
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_self(&mut self) -> &mut Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_bug(&mut self) {
|
|
||||||
let other = &mut (&mut *self);
|
|
||||||
|
|
||||||
*other = match (*other).get_self() {
|
|
||||||
Some(s) => s,
|
|
||||||
None => (*other).new_self()
|
|
||||||
};
|
|
||||||
|
|
||||||
let c = other;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {}
|
|
|
@ -5,9 +5,11 @@
|
||||||
// than `liveness::trace`, on some specific CFGs shapes: a variable was dead during tracing but its
|
// than `liveness::trace`, on some specific CFGs shapes: a variable was dead during tracing but its
|
||||||
// regions were marked live later, and live loans were not recomputed at this point.
|
// regions were marked live later, and live loans were not recomputed at this point.
|
||||||
|
|
||||||
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: polonius_next polonius
|
||||||
//@ check-pass
|
//@ check-pass
|
||||||
//@ revisions: nll polonius
|
//@ [polonius_next] compile-flags: -Z polonius=next
|
||||||
//@ [polonius] compile-flags: -Zpolonius=next
|
//@ [polonius] compile-flags: -Z polonius
|
||||||
|
|
||||||
// minimized from wavefc-cli-3.0.0
|
// minimized from wavefc-cli-3.0.0
|
||||||
fn repro1() {
|
fn repro1() {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
error[E0515]: cannot return reference to local variable `x`
|
error[E0515]: cannot return reference to local variable `x`
|
||||||
--> $DIR/polonius-smoke-test.rs:6:5
|
--> $DIR/polonius-smoke-test.rs:10:5
|
||||||
|
|
|
|
||||||
LL | &x
|
LL | &x
|
||||||
| ^^ returns a reference to data owned by the current function
|
| ^^ returns a reference to data owned by the current function
|
||||||
|
|
||||||
error[E0503]: cannot use `x` because it was mutably borrowed
|
error[E0503]: cannot use `x` because it was mutably borrowed
|
||||||
--> $DIR/polonius-smoke-test.rs:12:13
|
--> $DIR/polonius-smoke-test.rs:16:13
|
||||||
|
|
|
|
||||||
LL | let y = &mut x;
|
LL | let y = &mut x;
|
||||||
| ------ `x` is borrowed here
|
| ------ `x` is borrowed here
|
||||||
|
@ -15,7 +15,7 @@ LL | let w = y;
|
||||||
| - borrow later used here
|
| - borrow later used here
|
||||||
|
|
||||||
error[E0505]: cannot move out of `x` because it is borrowed
|
error[E0505]: cannot move out of `x` because it is borrowed
|
||||||
--> $DIR/polonius-smoke-test.rs:18:13
|
--> $DIR/polonius-smoke-test.rs:22:13
|
||||||
|
|
|
|
||||||
LL | pub fn use_while_mut_fr(x: &mut i32) -> &mut i32 {
|
LL | pub fn use_while_mut_fr(x: &mut i32) -> &mut i32 {
|
||||||
| - - let's call the lifetime of this reference `'1`
|
| - - let's call the lifetime of this reference `'1`
|
||||||
|
@ -35,7 +35,7 @@ LL + let y = &mut x.clone();
|
||||||
|
|
|
|
||||||
|
|
||||||
error[E0505]: cannot move out of `s` because it is borrowed
|
error[E0505]: cannot move out of `s` because it is borrowed
|
||||||
--> $DIR/polonius-smoke-test.rs:42:5
|
--> $DIR/polonius-smoke-test.rs:46:5
|
||||||
|
|
|
|
||||||
LL | let s = &mut 1;
|
LL | let s = &mut 1;
|
||||||
| - binding `s` declared here
|
| - binding `s` declared here
|
59
tests/ui/nll/polonius/polonius-smoke-test.polonius.stderr
Normal file
59
tests/ui/nll/polonius/polonius-smoke-test.polonius.stderr
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
error[E0515]: cannot return reference to local variable `x`
|
||||||
|
--> $DIR/polonius-smoke-test.rs:10:5
|
||||||
|
|
|
||||||
|
LL | &x
|
||||||
|
| ^^ returns a reference to data owned by the current function
|
||||||
|
|
||||||
|
error[E0503]: cannot use `x` because it was mutably borrowed
|
||||||
|
--> $DIR/polonius-smoke-test.rs:16:13
|
||||||
|
|
|
||||||
|
LL | let y = &mut x;
|
||||||
|
| ------ `x` is borrowed here
|
||||||
|
LL | let z = x;
|
||||||
|
| ^ use of borrowed `x`
|
||||||
|
LL | let w = y;
|
||||||
|
| - borrow later used here
|
||||||
|
|
||||||
|
error[E0505]: cannot move out of `x` because it is borrowed
|
||||||
|
--> $DIR/polonius-smoke-test.rs:22:13
|
||||||
|
|
|
||||||
|
LL | pub fn use_while_mut_fr(x: &mut i32) -> &mut i32 {
|
||||||
|
| - - let's call the lifetime of this reference `'1`
|
||||||
|
| |
|
||||||
|
| binding `x` declared here
|
||||||
|
LL | let y = &mut *x;
|
||||||
|
| ------- borrow of `*x` occurs here
|
||||||
|
LL | let z = x;
|
||||||
|
| ^ move out of `x` occurs here
|
||||||
|
LL | y
|
||||||
|
| - returning this value requires that `*x` is borrowed for `'1`
|
||||||
|
|
|
||||||
|
help: consider cloning the value if the performance cost is acceptable
|
||||||
|
|
|
||||||
|
LL - let y = &mut *x;
|
||||||
|
LL + let y = &mut x.clone();
|
||||||
|
|
|
||||||
|
|
||||||
|
error[E0505]: cannot move out of `s` because it is borrowed
|
||||||
|
--> $DIR/polonius-smoke-test.rs:46:5
|
||||||
|
|
|
||||||
|
LL | let s = &mut 1;
|
||||||
|
| - binding `s` declared here
|
||||||
|
LL | let r = &mut *s;
|
||||||
|
| ------- borrow of `*s` occurs here
|
||||||
|
LL | let tmp = foo(&r);
|
||||||
|
LL | s;
|
||||||
|
| ^ move out of `s` occurs here
|
||||||
|
LL | tmp;
|
||||||
|
| --- borrow later used here
|
||||||
|
|
|
||||||
|
help: consider cloning the value if the performance cost is acceptable
|
||||||
|
|
|
||||||
|
LL - let r = &mut *s;
|
||||||
|
LL + let r = &mut s.clone();
|
||||||
|
|
|
||||||
|
|
||||||
|
error: aborting due to 4 previous errors
|
||||||
|
|
||||||
|
Some errors have detailed explanations: E0503, E0505, E0515.
|
||||||
|
For more information about an error, try `rustc --explain E0503`.
|
|
@ -1,5 +1,9 @@
|
||||||
// Check that Polonius borrow check works for simple cases.
|
// Check that Polonius works for simple cases.
|
||||||
//@ compile-flags: -Z polonius
|
|
||||||
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: polonius legacy
|
||||||
|
//@ [polonius] compile-flags: -Z polonius=next
|
||||||
|
//@ [legacy] compile-flags: -Z polonius=legacy
|
||||||
|
|
||||||
pub fn return_ref_to_local() -> &'static i32 {
|
pub fn return_ref_to_local() -> &'static i32 {
|
||||||
let x = 0;
|
let x = 0;
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
// is correctly accepted by NLL but was incorrectly rejected by
|
// is correctly accepted by NLL but was incorrectly rejected by
|
||||||
// Polonius because of these missing `killed` facts.
|
// Polonius because of these missing `killed` facts.
|
||||||
|
|
||||||
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: polonius_next polonius
|
||||||
//@ check-pass
|
//@ check-pass
|
||||||
//@ compile-flags: -Z polonius
|
//@ [polonius_next] compile-flags: -Z polonius=next
|
||||||
|
//@ [polonius] compile-flags: -Z polonius
|
||||||
|
|
||||||
use std::{io, mem};
|
use std::{io, mem};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
error: lifetime may not live long enough
|
error: lifetime may not live long enough
|
||||||
--> $DIR/subset-relations.rs:10:5
|
--> $DIR/subset-relations.rs:13:5
|
||||||
|
|
|
|
||||||
LL | fn missing_subset<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
|
LL | fn missing_subset<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
|
||||||
| -- -- lifetime `'b` defined here
|
| -- -- lifetime `'b` defined here
|
14
tests/ui/nll/polonius/subset-relations.polonius.stderr
Normal file
14
tests/ui/nll/polonius/subset-relations.polonius.stderr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
error: lifetime may not live long enough
|
||||||
|
--> $DIR/subset-relations.rs:13:5
|
||||||
|
|
|
||||||
|
LL | fn missing_subset<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
|
||||||
|
| -- -- lifetime `'b` defined here
|
||||||
|
| |
|
||||||
|
| lifetime `'a` defined here
|
||||||
|
LL | y
|
||||||
|
| ^ 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`
|
||||||
|
|
||||||
|
error: aborting due to 1 previous error
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
// two free regions outlive each other, without any evidence that this
|
// two free regions outlive each other, without any evidence that this
|
||||||
// relation holds.
|
// relation holds.
|
||||||
|
|
||||||
//@ compile-flags: -Z polonius
|
//@ ignore-compare-mode-polonius (explicit revisions)
|
||||||
|
//@ revisions: polonius legacy
|
||||||
|
//@ [polonius] compile-flags: -Z polonius=next
|
||||||
|
//@ [legacy] compile-flags: -Z polonius=legacy
|
||||||
|
|
||||||
// returning `y` requires that `'b: 'a`, but it's not known to be true
|
// returning `y` requires that `'b: 'a`, but it's not known to be true
|
||||||
fn missing_subset<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
|
fn missing_subset<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
|
||||||
|
@ -22,7 +25,7 @@ fn implied_bounds_subset<'a, 'b>(x: &'a &'b mut u32) -> &'a u32 {
|
||||||
|
|
||||||
// `'b: 'a` is declared, and `'a: 'c` is known via implied bounds:
|
// `'b: 'a` is declared, and `'a: 'c` is known via implied bounds:
|
||||||
// `'b: 'c` is therefore known to hold transitively
|
// `'b: 'c` is therefore known to hold transitively
|
||||||
fn transitively_valid_subset<'a, 'b: 'a, 'c>(x: &'c &'a u32, y: &'b u32) -> &'c u32 {
|
fn transitively_valid_subset<'a, 'b: 'a, 'c>(x: &'c &'a u32, y: &'b u32) -> &'c u32 {
|
||||||
y
|
y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue