Skip to content

Commit

Permalink
Rollup merge of #121917 - GuillaumeGomez:pattern-complexity_limit.rs,…
Browse files Browse the repository at this point in the history
… r=Nadrieril

Add new `pattern_complexity` attribute to add possibility to limit and check recursion in pattern matching

Needed for rust-lang/rust-analyzer#9528.

This PR adds a new attribute only available when running rust testsuite called `pattern_complexity` which allows to set the maximum recursion for the pattern matching. It is quite useful to ensure the complexity doesn't grow, like in `tests/ui/pattern/usefulness/issue-118437-exponential-time-on-diagonal-match.rs`.

r? `@Nadrieril`
  • Loading branch information
GuillaumeGomez authored Mar 3, 2024
2 parents 1090205 + f04c5c5 commit 7d8f74f
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 6 deletions.
4 changes: 4 additions & 0 deletions compiler/rustc_feature/src/builtin_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,10 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
omit_gdb_pretty_printer_section, Normal, template!(Word), WarnFollowing,
"the `#[omit_gdb_pretty_printer_section]` attribute is just used for the Rust test suite",
),
rustc_attr!(
TEST, pattern_complexity, CrateLevel, template!(NameValueStr: "N"),
ErrorFollowing, @only_local: true,
),
];

pub fn deprecated_attributes() -> Vec<&'static BuiltinAttribute> {
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_feature/src/unstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ declare_features! (
(internal, negative_bounds, "1.71.0", None),
/// Allows using `#[omit_gdb_pretty_printer_section]`.
(internal, omit_gdb_pretty_printer_section, "1.5.0", None),
/// Set the maximum pattern complexity allowed (not limited by default).
(internal, pattern_complexity, "CURRENT_RUSTC_VERSION", None),
/// Allows using `#[prelude_import]` on glob `use` items.
(internal, prelude_import, "1.2.0", None),
/// Used to identify crates that contain the profiler runtime.
Expand Down
11 changes: 9 additions & 2 deletions compiler/rustc_middle/src/middle/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,21 @@ pub fn get_recursion_limit(krate_attrs: &[Attribute], sess: &Session) -> Limit {
}

fn get_limit(krate_attrs: &[Attribute], sess: &Session, name: Symbol, default: usize) -> Limit {
match get_limit_size(krate_attrs, sess, name) {
Some(size) => Limit::new(size),
None => Limit::new(default),
}
}

pub fn get_limit_size(krate_attrs: &[Attribute], sess: &Session, name: Symbol) -> Option<usize> {
for attr in krate_attrs {
if !attr.has_name(name) {
continue;
}

if let Some(s) = attr.value_str() {
match s.as_str().parse() {
Ok(n) => return Limit::new(n),
Ok(n) => return Some(n),
Err(e) => {
let value_span = attr
.meta()
Expand All @@ -69,5 +76,5 @@ fn get_limit(krate_attrs: &[Attribute], sess: &Session, name: Symbol, default: u
}
}
}
return Limit::new(default);
None
}
8 changes: 6 additions & 2 deletions compiler/rustc_mir_build/src/thir/pattern/check_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use rustc_hir as hir;
use rustc_hir::def::*;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::HirId;
use rustc_middle::middle::limits::get_limit_size;
use rustc_middle::thir::visit::Visitor;
use rustc_middle::thir::*;
use rustc_middle::ty::print::with_no_trimmed_paths;
Expand All @@ -26,7 +27,7 @@ use rustc_session::lint::builtin::{
};
use rustc_session::Session;
use rustc_span::hygiene::DesugaringKind;
use rustc_span::Span;
use rustc_span::{sym, Span};

pub(crate) fn check_match(tcx: TyCtxt<'_>, def_id: LocalDefId) -> Result<(), ErrorGuaranteed> {
let typeck_results = tcx.typeck(def_id);
Expand Down Expand Up @@ -403,8 +404,11 @@ impl<'p, 'tcx> MatchVisitor<'p, 'tcx> {
arms: &[MatchArm<'p, 'tcx>],
scrut_ty: Ty<'tcx>,
) -> Result<UsefulnessReport<'p, 'tcx>, ErrorGuaranteed> {
let pattern_complexity_limit =
get_limit_size(cx.tcx.hir().krate_attrs(), cx.tcx.sess, sym::pattern_complexity);
let report =
rustc_pattern_analysis::analyze_match(&cx, &arms, scrut_ty).map_err(|err| {
rustc_pattern_analysis::analyze_match(&cx, &arms, scrut_ty, pattern_complexity_limit)
.map_err(|err| {
self.error = Err(err);
err
})?;
Expand Down
7 changes: 6 additions & 1 deletion compiler/rustc_pattern_analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ pub trait TypeCx: Sized + fmt::Debug {
_overlaps_with: &[&DeconstructedPat<Self>],
) {
}

/// The maximum pattern complexity limit was reached.
fn complexity_exceeded(&self) -> Result<(), Self::Error>;
}

/// The arm of a match expression.
Expand All @@ -167,10 +170,12 @@ pub fn analyze_match<'p, 'tcx>(
tycx: &RustcMatchCheckCtxt<'p, 'tcx>,
arms: &[rustc::MatchArm<'p, 'tcx>],
scrut_ty: Ty<'tcx>,
pattern_complexity_limit: Option<usize>,
) -> Result<rustc::UsefulnessReport<'p, 'tcx>, ErrorGuaranteed> {
let scrut_ty = tycx.reveal_opaque_ty(scrut_ty);
let scrut_validity = ValidityConstraint::from_bool(tycx.known_valid_scrutinee);
let report = compute_match_usefulness(tycx, arms, scrut_ty, scrut_validity)?;
let report =
compute_match_usefulness(tycx, arms, scrut_ty, scrut_validity, pattern_complexity_limit)?;

// Run the non_exhaustive_omitted_patterns lint. Only run on refutable patterns to avoid hitting
// `if let`s. Only run if the match is exhaustive otherwise the error is redundant.
Expand Down
5 changes: 5 additions & 0 deletions compiler/rustc_pattern_analysis/src/rustc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,11 @@ impl<'p, 'tcx: 'p> TypeCx for RustcMatchCheckCtxt<'p, 'tcx> {
errors::OverlappingRangeEndpoints { overlap: overlaps, range: pat_span },
);
}

fn complexity_exceeded(&self) -> Result<(), Self::Error> {
let span = self.whole_match_span.unwrap_or(self.scrut_span);
Err(self.tcx.dcx().span_err(span, "reached pattern complexity limit"))
}
}

/// Recursively expand this pattern into its subpatterns. Only useful for or-patterns.
Expand Down
24 changes: 23 additions & 1 deletion compiler/rustc_pattern_analysis/src/usefulness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,21 @@ struct UsefulnessCtxt<'a, Cx: TypeCx> {
/// Collect the patterns found useful during usefulness checking. This is used to lint
/// unreachable (sub)patterns.
useful_subpatterns: FxHashSet<PatId>,
complexity_limit: Option<usize>,
complexity_level: usize,
}

impl<'a, Cx: TypeCx> UsefulnessCtxt<'a, Cx> {
fn increase_complexity_level(&mut self, complexity_add: usize) -> Result<(), Cx::Error> {
self.complexity_level += complexity_add;
if self
.complexity_limit
.is_some_and(|complexity_limit| complexity_limit < self.complexity_level)
{
return self.tycx.complexity_exceeded();
}
Ok(())
}
}

/// Context that provides information local to a place under investigation.
Expand Down Expand Up @@ -1552,6 +1567,7 @@ fn compute_exhaustiveness_and_usefulness<'a, 'p, Cx: TypeCx>(
}

let Some(place) = matrix.head_place() else {
mcx.increase_complexity_level(matrix.rows().len())?;
// The base case: there are no columns in the matrix. We are morally pattern-matching on ().
// A row is useful iff it has no (unguarded) rows above it.
let mut useful = true; // Whether the next row is useful.
Expand Down Expand Up @@ -1690,8 +1706,14 @@ pub fn compute_match_usefulness<'p, Cx: TypeCx>(
arms: &[MatchArm<'p, Cx>],
scrut_ty: Cx::Ty,
scrut_validity: ValidityConstraint,
complexity_limit: Option<usize>,
) -> Result<UsefulnessReport<'p, Cx>, Cx::Error> {
let mut cx = UsefulnessCtxt { tycx, useful_subpatterns: FxHashSet::default() };
let mut cx = UsefulnessCtxt {
tycx,
useful_subpatterns: FxHashSet::default(),
complexity_limit,
complexity_level: 0,
};
let mut matrix = Matrix::new(arms, scrut_ty, scrut_validity);
let non_exhaustiveness_witnesses = compute_exhaustiveness_and_usefulness(&mut cx, &mut matrix)?;

Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,7 @@ symbols! {
pat,
pat_param,
path,
pattern_complexity,
pattern_parentheses,
phantom_data,
pic,
Expand Down
6 changes: 6 additions & 0 deletions tests/ui/feature-gates/feature-gate-pattern-complexity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// check that `pattern_complexity` is feature-gated

#![pattern_complexity = "42"]
//~^ ERROR: the `#[pattern_complexity]` attribute is just used for rustc unit tests

fn main() {}
12 changes: 12 additions & 0 deletions tests/ui/feature-gates/feature-gate-pattern-complexity.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
error[E0658]: the `#[pattern_complexity]` attribute is just used for rustc unit tests and will never be stable
--> $DIR/feature-gate-pattern-complexity.rs:3:1
|
LL | #![pattern_complexity = "42"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: add `#![feature(rustc_attrs)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0658`.
106 changes: 106 additions & 0 deletions tests/ui/pattern/complexity_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#![feature(rustc_attrs)]
#![pattern_complexity = "10000"]

#[derive(Default)]
struct BaseCommand {
field01: bool,
field02: bool,
field03: bool,
field04: bool,
field05: bool,
field06: bool,
field07: bool,
field08: bool,
field09: bool,
field10: bool,
field11: bool,
field12: bool,
field13: bool,
field14: bool,
field15: bool,
field16: bool,
field17: bool,
field18: bool,
field19: bool,
field20: bool,
field21: bool,
field22: bool,
field23: bool,
field24: bool,
field25: bool,
field26: bool,
field27: bool,
field28: bool,
field29: bool,
field30: bool,
}

fn request_key(command: BaseCommand) {
match command { //~ ERROR: reached pattern complexity limit
BaseCommand { field01: true, .. } => {}
BaseCommand { field02: true, .. } => {}
BaseCommand { field03: true, .. } => {}
BaseCommand { field04: true, .. } => {}
BaseCommand { field05: true, .. } => {}
BaseCommand { field06: true, .. } => {}
BaseCommand { field07: true, .. } => {}
BaseCommand { field08: true, .. } => {}
BaseCommand { field09: true, .. } => {}
BaseCommand { field10: true, .. } => {}
BaseCommand { field11: true, .. } => {}
BaseCommand { field12: true, .. } => {}
BaseCommand { field13: true, .. } => {}
BaseCommand { field14: true, .. } => {}
BaseCommand { field15: true, .. } => {}
BaseCommand { field16: true, .. } => {}
BaseCommand { field17: true, .. } => {}
BaseCommand { field18: true, .. } => {}
BaseCommand { field19: true, .. } => {}
BaseCommand { field20: true, .. } => {}
BaseCommand { field21: true, .. } => {}
BaseCommand { field22: true, .. } => {}
BaseCommand { field23: true, .. } => {}
BaseCommand { field24: true, .. } => {}
BaseCommand { field25: true, .. } => {}
BaseCommand { field26: true, .. } => {}
BaseCommand { field27: true, .. } => {}
BaseCommand { field28: true, .. } => {}
BaseCommand { field29: true, .. } => {}
BaseCommand { field30: true, .. } => {}

BaseCommand { field01: false, .. } => {}
BaseCommand { field02: false, .. } => {}
BaseCommand { field03: false, .. } => {}
BaseCommand { field04: false, .. } => {}
BaseCommand { field05: false, .. } => {}
BaseCommand { field06: false, .. } => {}
BaseCommand { field07: false, .. } => {}
BaseCommand { field08: false, .. } => {}
BaseCommand { field09: false, .. } => {}
BaseCommand { field10: false, .. } => {}
BaseCommand { field11: false, .. } => {}
BaseCommand { field12: false, .. } => {}
BaseCommand { field13: false, .. } => {}
BaseCommand { field14: false, .. } => {}
BaseCommand { field15: false, .. } => {}
BaseCommand { field16: false, .. } => {}
BaseCommand { field17: false, .. } => {}
BaseCommand { field18: false, .. } => {}
BaseCommand { field19: false, .. } => {}
BaseCommand { field20: false, .. } => {}
BaseCommand { field21: false, .. } => {}
BaseCommand { field22: false, .. } => {}
BaseCommand { field23: false, .. } => {}
BaseCommand { field24: false, .. } => {}
BaseCommand { field25: false, .. } => {}
BaseCommand { field26: false, .. } => {}
BaseCommand { field27: false, .. } => {}
BaseCommand { field28: false, .. } => {}
BaseCommand { field29: false, .. } => {}
BaseCommand { field30: false, .. } => {}
}
}

fn main() {
request_key(BaseCommand::default());
}
14 changes: 14 additions & 0 deletions tests/ui/pattern/complexity_limit.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
error: reached pattern complexity limit
--> $DIR/complexity_limit.rs:39:5
|
LL | / match command {
LL | | BaseCommand { field01: true, .. } => {}
LL | | BaseCommand { field02: true, .. } => {}
LL | | BaseCommand { field03: true, .. } => {}
... |
LL | | BaseCommand { field30: false, .. } => {}
LL | | }
| |_____^

error: aborting due to 1 previous error

Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#![feature(rustc_attrs)]
#![pattern_complexity = "61"]

//@ check-pass
struct BaseCommand {
field01: bool,
Expand Down

0 comments on commit 7d8f74f

Please sign in to comment.