From 9bb14faad3e127557debe529ac4d841ee2c31cfb Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Fri, 3 Apr 2026 19:08:56 +0100 Subject: [PATCH 01/14] Use an aggregate equality comparison for constant array/slice patterns when possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When every element in an array or slice pattern is a constant and there is no `..` subpattern, the match builder now emits a single call to `PartialEq::eq` instead of comparing each element one by one. This drastically reduces the number of MIR basic blocks for large constant-array matches – e.g. a 64-element `[u8; 64]` match previously generated 64 separate comparison blocks and now generates just one `PartialEq::eq` call that LLVM can lower to a `memcmp()` The optimisation is gated on having at least two constant elements. Single-element arrays still use a plain scalar comparison. Example: ```rust const FOO: [u8; 64] = *b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; pub fn foo(x: &[u8; 64]) -> bool { // Before: 64 basic blocks, one per byte. // After: a single `PartialEq::eq()` call. matches!(x, &FOO) } ``` --- .../src/builder/matches/buckets.rs | 5 + .../src/builder/matches/match_pair.rs | 123 ++++++++++++++---- .../src/builder/matches/mod.rs | 8 ++ .../src/builder/matches/test.rs | 55 +++++++- 4 files changed, 161 insertions(+), 30 deletions(-) diff --git a/compiler/rustc_mir_build/src/builder/matches/buckets.rs b/compiler/rustc_mir_build/src/builder/matches/buckets.rs index 0d2e9bf87585d..77f2a938f2b56 100644 --- a/compiler/rustc_mir_build/src/builder/matches/buckets.rs +++ b/compiler/rustc_mir_build/src/builder/matches/buckets.rs @@ -323,6 +323,10 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { value: case_val, kind: PatConstKind::Float | PatConstKind::Other, }, + ) + | ( + TestKind::AggregateEq { value: test_val, .. }, + TestableCase::Constant { value: case_val, kind: PatConstKind::Aggregate }, ) => { if test_val == case_val { fully_matched = true; @@ -353,6 +357,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { | TestKind::Range { .. } | TestKind::StringEq { .. } | TestKind::ScalarEq { .. } + | TestKind::AggregateEq { .. } | TestKind::Deref { .. }, _, ) => { diff --git a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs index b4ce8149f5e4d..5fffa7fa5ffc5 100644 --- a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs +++ b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs @@ -4,7 +4,7 @@ use rustc_abi::FieldIdx; use rustc_middle::mir::{Pinnedness, Place, PlaceElem, ProjectionElem}; use rustc_middle::span_bug; use rustc_middle::thir::{Ascription, DerefPatBorrowMode, FieldPat, Pat, PatKind}; -use rustc_middle::ty::{self, Ty, TypeVisitableExt}; +use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; use rustc_span::Span; use crate::builder::Builder; @@ -13,6 +13,33 @@ use crate::builder::matches::{ FlatPat, MatchPairTree, PatConstKind, PatternExtraData, SliceLenOp, TestableCase, }; +/// Checks whether every pattern in `elements` is a `PatKind::Constant` and, +/// if so, reconstructs a single aggregate `ty::Value` that represents the whole +/// array or slice. Returns `None` when any element is not a constant or the +/// sequence is too short to benefit from an aggregate comparison. +fn try_reconstruct_aggregate_constant<'tcx>( + tcx: TyCtxt<'tcx>, + aggregate_ty: Ty<'tcx>, + elements: &[Pat<'tcx>], +) -> Option> { + // A single element (or empty array) is not worth an aggregate comparison. + if elements.len() <= 1 { + return None; + } + let branches = elements + .iter() + .map(|pat| { + if let PatKind::Constant { value } = pat.kind { + Some(ty::Const::new_value(tcx, value.valtree, value.ty)) + } else { + None + } + }) + .collect::>>()?; + let valtree = ty::ValTree::from_branches(tcx, branches); + Some(ty::Value { ty: aggregate_ty, valtree }) +} + /// For an array or slice pattern's subpatterns (prefix/slice/suffix), returns a list /// of those subpatterns, each paired with a suitably-projected [`PlaceBuilder`]. fn prefix_slice_suffix<'a, 'tcx>( @@ -344,10 +371,29 @@ impl<'tcx> InterPat<'tcx> { _ => None, }; if let Some(array_len) = array_len { - for (subplace, subpat) in - prefix_slice_suffix(&place_builder, Some(array_len), prefix, slice, suffix) + // When all elements are constants and there is no `..` + // subpattern, compare the whole array at once via + // `PartialEq::eq` rather than element by element. + if slice.is_none() + && suffix.is_empty() + && let Some(aggregate_value) = + try_reconstruct_aggregate_constant(cx.tcx, pattern.ty, prefix) { - subpats.push(InterPat::lower_thir_pat(cx, subplace, subpat)); + Some(TestableCase::Constant { + value: aggregate_value, + kind: PatConstKind::Aggregate, + }) + } else { + for (subplace, subpat) in prefix_slice_suffix( + &place_builder, + Some(array_len), + prefix, + slice, + suffix, + ) { + subpats.push(InterPat::lower_thir_pat(cx, subplace, subpat)); + } + None } } else { // If the array length couldn't be determined, ignore the @@ -359,33 +405,60 @@ impl<'tcx> InterPat<'tcx> { pattern.ty ), ); + None } - - None } PatKind::Slice { ref prefix, ref slice, ref suffix } => { - for (subplace, subpat) in - prefix_slice_suffix(&place_builder, None, prefix, slice, suffix) + // When there is no `..`, all elements are constants, and + // there are at least two of them, collapse the individual + // element subpairs into a single aggregate comparison that + // is performed after the length check. + if slice.is_none() + && suffix.is_empty() + && let Some(aggregate_value) = + try_reconstruct_aggregate_constant(cx.tcx, pattern.ty, prefix) { - subpats.push(InterPat::lower_thir_pat(cx, subplace, subpat)); - } - - if prefix.is_empty() && slice.is_some() && suffix.is_empty() { - // A slice pattern shaped like `[..]` is irrefutable. - // It can match a slice of any length, so no length test is needed. - None - } else { - // Any other shape of slice pattern requires a length test. - // Slice patterns with a `..` subpattern require a minimum - // length; those without `..` require an exact length. + subpats.push(InterPat { + place, + testable_case: Some(TestableCase::Constant { + value: aggregate_value, + kind: PatConstKind::Aggregate, + }), + subpats: Vec::new(), + or_subpats: None, + ascriptions: Vec::new(), + binding: None, + pattern_span: pattern.span, + is_never: false, + }); Some(TestableCase::Slice { - len: u64::try_from(prefix.len() + suffix.len()).unwrap(), - op: if slice.is_some() { - SliceLenOp::GreaterOrEqual - } else { - SliceLenOp::Equal - }, + len: u64::try_from(prefix.len()).unwrap(), + op: SliceLenOp::Equal, }) + } else { + for (subplace, subpat) in + prefix_slice_suffix(&place_builder, None, prefix, slice, suffix) + { + subpats.push(InterPat::lower_thir_pat(cx, subplace, subpat)); + } + + if prefix.is_empty() && slice.is_some() && suffix.is_empty() { + // A slice pattern shaped like `[..]` is irrefutable. + // It can match a slice of any length, so no length test is needed. + None + } else { + // Any other shape of slice pattern requires a length test. + // Slice patterns with a `..` subpattern require a minimum + // length; those without `..` require an exact length. + Some(TestableCase::Slice { + len: u64::try_from(prefix.len() + suffix.len()).unwrap(), + op: if slice.is_some() { + SliceLenOp::GreaterOrEqual + } else { + SliceLenOp::Equal + }, + }) + } } } diff --git a/compiler/rustc_mir_build/src/builder/matches/mod.rs b/compiler/rustc_mir_build/src/builder/matches/mod.rs index 109f4de2698a4..7fcf53d4132b8 100644 --- a/compiler/rustc_mir_build/src/builder/matches/mod.rs +++ b/compiler/rustc_mir_build/src/builder/matches/mod.rs @@ -1248,6 +1248,10 @@ enum PatConstKind { Float, /// Constant string values, tested via string equality. String, + /// Constant array or slice values where every element is a constant. + /// Tested by calling `PartialEq::eq` on the whole aggregate at once, + /// rather than comparing element by element. + Aggregate, /// Any other constant-pattern is usually tested via some kind of equality /// check. Types that might be encountered here include: /// - raw pointers derived from integer values @@ -1333,6 +1337,10 @@ enum TestKind<'tcx> { /// Tests the place against a constant using scalar equality. ScalarEq { value: ty::Value<'tcx> }, + /// Tests the place against a constant array or slice using `PartialEq::eq`, + /// comparing the whole aggregate at once rather than element by element. + AggregateEq { value: ty::Value<'tcx> }, + /// Test whether the value falls within an inclusive or exclusive range. Range(Arc>), diff --git a/compiler/rustc_mir_build/src/builder/matches/test.rs b/compiler/rustc_mir_build/src/builder/matches/test.rs index 1c234bb8d70dc..d5f19066c24ff 100644 --- a/compiler/rustc_mir_build/src/builder/matches/test.rs +++ b/compiler/rustc_mir_build/src/builder/matches/test.rs @@ -40,6 +40,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { TestableCase::Constant { value, kind: PatConstKind::String } => { TestKind::StringEq { value } } + TestableCase::Constant { value, kind: PatConstKind::Aggregate } => { + TestKind::AggregateEq { value } + } TestableCase::Constant { value, kind: PatConstKind::Float | PatConstKind::Other } => { TestKind::ScalarEq { value } } @@ -168,16 +171,54 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { // Compare two strings using `::eq`. // (Interestingly this means that exhaustiveness analysis relies, for soundness, // on the `PartialEq` impl for `str` to be correct!) - self.string_compare( + self.non_scalar_compare( block, success_block, fail_block, source_info, + tcx.types.str_, expected_value_operand, Operand::Copy(actual_value_ref_place), ); } + TestKind::AggregateEq { value } => { + let tcx = self.tcx; + let success_block = target_block(TestBranch::Success); + let fail_block = target_block(TestBranch::Failure); + + let aggregate_ty = value.ty; + let ref_ty = Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, aggregate_ty); + + // The constant has type `[T; N]` (or `[T]`), but calling + // `PartialEq::eq` requires `&[T; N]` (or `&[T]`) operands. + // Valtree representations are the same with or without the + // reference wrapper, so we can reinterpret by replacing the type. + let expected_value = ty::Value { ty: ref_ty, valtree: value.valtree }; + let expected_operand = + self.literal_operand(test.span, Const::from_ty_value(tcx, expected_value)); + + // Create a reference to the scrutinee place. + let actual_ref_place = self.temp(ref_ty, test.span); + self.cfg.push_assign( + block, + self.source_info(test.span), + actual_ref_place, + Rvalue::Ref(tcx.lifetimes.re_erased, BorrowKind::Shared, place), + ); + + // Compare using `::eq` where `T` is the array or slice type. + self.non_scalar_compare( + block, + success_block, + fail_block, + source_info, + aggregate_ty, + expected_operand, + Operand::Copy(actual_ref_place), + ); + } + TestKind::ScalarEq { value } => { let tcx = self.tcx; let success_block = target_block(TestBranch::Success); @@ -409,19 +450,23 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { ); } - /// Compare two values of type `&str` using `::eq`. - fn string_compare( + /// Compare two reference values using `::eq`. + /// + /// `compared_ty` is the *inner* type (e.g. `str`, `[u8; 64]`); + /// `expect` and `val` must already be references to that type. + fn non_scalar_compare( &mut self, block: BasicBlock, success_block: BasicBlock, fail_block: BasicBlock, source_info: SourceInfo, + compared_ty: Ty<'tcx>, expect: Operand<'tcx>, val: Operand<'tcx>, ) { - let str_ty = self.tcx.types.str_; let eq_def_id = self.tcx.require_lang_item(LangItem::PartialEq, source_info.span); - let method = trait_method(self.tcx, eq_def_id, sym::eq, &[str_ty.into(), str_ty.into()]); + let method = + trait_method(self.tcx, eq_def_id, sym::eq, &[compared_ty.into(), compared_ty.into()]); let bool_ty = self.tcx.types.bool; let eq_result = self.temp(bool_ty, source_info.span); From 085b0a4ad4975609bf67200556cd18a0d7da6c93 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Fri, 3 Apr 2026 19:20:33 +0100 Subject: [PATCH 02/14] Add a MIR test ensuring we use an aggregate comparison when matching on a large fixed-length array --- ...eq.array_match.built.after.panic-abort.mir | 51 +++++++++++++++++++ ...q.array_match.built.after.panic-unwind.mir | 51 +++++++++++++++++++ .../building/match/aggregate_array_eq.rs | 15 ++++++ 3 files changed, 117 insertions(+) create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.rs diff --git a/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir new file mode 100644 index 0000000000000..85778f3bee8cb --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir @@ -0,0 +1,51 @@ +// MIR for `array_match` after built + +fn array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[u8; 4]; + let mut _3: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _2 = &_1; + _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; + } + + bb1: { + _0 = const false; + goto -> bb7; + } + + bb2: { + falseEdge -> [real: bb6, imaginary: bb1]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(move _3) -> [0: bb1, otherwise: bb2]; + } + + bb5: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb6: { + _0 = const true; + goto -> bb7; + } + + bb7: { + return; + } + + bb8 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..85778f3bee8cb --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir @@ -0,0 +1,51 @@ +// MIR for `array_match` after built + +fn array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[u8; 4]; + let mut _3: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _2 = &_1; + _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; + } + + bb1: { + _0 = const false; + goto -> bb7; + } + + bb2: { + falseEdge -> [real: bb6, imaginary: bb1]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(move _3) -> [0: bb1, otherwise: bb2]; + } + + bb5: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb6: { + _0 = const true; + goto -> bb7; + } + + bb7: { + return; + } + + bb8 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.rs b/tests/mir-opt/building/match/aggregate_array_eq.rs new file mode 100644 index 0000000000000..1c77b432ddb7d --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.rs @@ -0,0 +1,15 @@ +// EMIT_MIR_FOR_EACH_PANIC_STRATEGY +//@ compile-flags: -Zmir-opt-level=0 + +// Verify that matching against a constant array pattern produces a single +// `PartialEq::eq` call rather than element-by-element comparisons. + +#![crate_type = "lib"] + +// EMIT_MIR aggregate_array_eq.array_match.built.after.mir +pub fn array_match(x: [u8; 4]) -> bool { + // CHECK-LABEL: fn array_match( + // CHECK: <[u8; 4] as PartialEq>::eq + // CHECK-NOT: switchInt(copy _1[ + matches!(x, [1, 2, 3, 4]) +} From 9665678b6792c7f6d6a14c422e135f6aca979522 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 12 Apr 2026 22:29:45 +0100 Subject: [PATCH 03/14] Add another test, a run-pass one --- tests/ui/match/aggregate-array-eq.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/ui/match/aggregate-array-eq.rs diff --git a/tests/ui/match/aggregate-array-eq.rs b/tests/ui/match/aggregate-array-eq.rs new file mode 100644 index 0000000000000..6622eef77e103 --- /dev/null +++ b/tests/ui/match/aggregate-array-eq.rs @@ -0,0 +1,16 @@ +//! Verify that matching against a constant array pattern produces correct +//! results at runtime, complementing the MIR test in +//! `tests/mir-opt/building/match/aggregate_array_eq.rs` which checks that +//! a single aggregate `PartialEq::eq` call is emitted. +//@ run-pass + +fn array_match(x: [u8; 4]) -> bool { + matches!(x, [1, 2, 3, 4]) +} + +fn main() { + assert!(array_match([1, 2, 3, 4])); + assert!(!array_match([1, 2, 3, 5])); + assert!(!array_match([0, 0, 0, 0])); + assert!(!array_match([4, 3, 2, 1])); +} From 6171eddd9b1e7db6953efd33b3f7b73b3ea72ea3 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 12 Apr 2026 22:34:14 +0100 Subject: [PATCH 04/14] Extend both tests with the example from https://github.com/rust-lang/rust/issues/103073 --- .../building/match/aggregate_array_eq.rs | 22 +++ ...y_from_matched.built.after.panic-abort.mir | 157 ++++++++++++++++++ ..._from_matched.built.after.panic-unwind.mir | 157 ++++++++++++++++++ tests/ui/match/aggregate-array-eq.rs | 26 +++ 4 files changed, 362 insertions(+) create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir diff --git a/tests/mir-opt/building/match/aggregate_array_eq.rs b/tests/mir-opt/building/match/aggregate_array_eq.rs index 1c77b432ddb7d..860c0bcd17113 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.rs +++ b/tests/mir-opt/building/match/aggregate_array_eq.rs @@ -13,3 +13,25 @@ pub fn array_match(x: [u8; 4]) -> bool { // CHECK-NOT: switchInt(copy _1[ matches!(x, [1, 2, 3, 4]) } + +pub enum MyEnum { + A, + B, + C, + D, +} + +// Regression test for https://github.com/rust-lang/rust/issues/103073. +// EMIT_MIR aggregate_array_eq.try_from_matched.built.after.mir +pub fn try_from_matched(value: [u8; 4]) -> Result { + // CHECK-LABEL: fn try_from_matched( + // CHECK: <[u8; 4] as PartialEq>::eq + // CHECK-NOT: switchInt(copy (*_2)[ + match &value { + b"ABCD" => Ok(MyEnum::A), + b"EFGH" => Ok(MyEnum::B), + b"IJKL" => Ok(MyEnum::C), + b"MNOP" => Ok(MyEnum::D), + _ => Err(()), + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir new file mode 100644 index 0000000000000..008691c44deef --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir @@ -0,0 +1,157 @@ +// MIR for `try_from_matched` after built + +fn try_from_matched(_1: [u8; 4]) -> Result { + debug value => _1; + let mut _0: std::result::Result; + let mut _2: &[u8; 4]; + let mut _3: &[u8; 4]; + let mut _4: bool; + let mut _5: &[u8; 4]; + let mut _6: bool; + let mut _7: &[u8; 4]; + let mut _8: bool; + let mut _9: &[u8; 4]; + let mut _10: bool; + let mut _11: MyEnum; + let mut _12: MyEnum; + let mut _13: MyEnum; + let mut _14: MyEnum; + let mut _15: (); + + bb0: { + StorageLive(_2); + _2 = &_1; + PlaceMention(_2); + _9 = &(*_2); + _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; + } + + bb1: { + StorageLive(_15); + _15 = (); + _0 = Result::::Err(move _15); + StorageDead(_15); + goto -> bb25; + } + + bb2: { + falseEdge -> [real: bb24, imaginary: bb4]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + _7 = &(*_2); + _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; + } + + bb5: { + goto -> bb1; + } + + bb6: { + falseEdge -> [real: bb23, imaginary: bb8]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + _5 = &(*_2); + _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; + } + + bb9: { + goto -> bb5; + } + + bb10: { + falseEdge -> [real: bb22, imaginary: bb12]; + } + + bb11: { + goto -> bb9; + } + + bb12: { + _3 = &(*_2); + _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; + } + + bb13: { + goto -> bb9; + } + + bb14: { + falseEdge -> [real: bb21, imaginary: bb1]; + } + + bb15: { + goto -> bb13; + } + + bb16: { + switchInt(move _4) -> [0: bb13, otherwise: bb14]; + } + + bb17: { + switchInt(move _6) -> [0: bb12, otherwise: bb10]; + } + + bb18: { + switchInt(move _8) -> [0: bb8, otherwise: bb6]; + } + + bb19: { + switchInt(move _10) -> [0: bb4, otherwise: bb2]; + } + + bb20: { + FakeRead(ForMatchedPlace(None), _2); + unreachable; + } + + bb21: { + StorageLive(_14); + _14 = MyEnum::D; + _0 = Result::::Ok(move _14); + StorageDead(_14); + goto -> bb25; + } + + bb22: { + StorageLive(_13); + _13 = MyEnum::C; + _0 = Result::::Ok(move _13); + StorageDead(_13); + goto -> bb25; + } + + bb23: { + StorageLive(_12); + _12 = MyEnum::B; + _0 = Result::::Ok(move _12); + StorageDead(_12); + goto -> bb25; + } + + bb24: { + StorageLive(_11); + _11 = MyEnum::A; + _0 = Result::::Ok(move _11); + StorageDead(_11); + goto -> bb25; + } + + bb25: { + StorageDead(_2); + return; + } + + bb26 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..008691c44deef --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir @@ -0,0 +1,157 @@ +// MIR for `try_from_matched` after built + +fn try_from_matched(_1: [u8; 4]) -> Result { + debug value => _1; + let mut _0: std::result::Result; + let mut _2: &[u8; 4]; + let mut _3: &[u8; 4]; + let mut _4: bool; + let mut _5: &[u8; 4]; + let mut _6: bool; + let mut _7: &[u8; 4]; + let mut _8: bool; + let mut _9: &[u8; 4]; + let mut _10: bool; + let mut _11: MyEnum; + let mut _12: MyEnum; + let mut _13: MyEnum; + let mut _14: MyEnum; + let mut _15: (); + + bb0: { + StorageLive(_2); + _2 = &_1; + PlaceMention(_2); + _9 = &(*_2); + _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; + } + + bb1: { + StorageLive(_15); + _15 = (); + _0 = Result::::Err(move _15); + StorageDead(_15); + goto -> bb25; + } + + bb2: { + falseEdge -> [real: bb24, imaginary: bb4]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + _7 = &(*_2); + _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; + } + + bb5: { + goto -> bb1; + } + + bb6: { + falseEdge -> [real: bb23, imaginary: bb8]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + _5 = &(*_2); + _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; + } + + bb9: { + goto -> bb5; + } + + bb10: { + falseEdge -> [real: bb22, imaginary: bb12]; + } + + bb11: { + goto -> bb9; + } + + bb12: { + _3 = &(*_2); + _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; + } + + bb13: { + goto -> bb9; + } + + bb14: { + falseEdge -> [real: bb21, imaginary: bb1]; + } + + bb15: { + goto -> bb13; + } + + bb16: { + switchInt(move _4) -> [0: bb13, otherwise: bb14]; + } + + bb17: { + switchInt(move _6) -> [0: bb12, otherwise: bb10]; + } + + bb18: { + switchInt(move _8) -> [0: bb8, otherwise: bb6]; + } + + bb19: { + switchInt(move _10) -> [0: bb4, otherwise: bb2]; + } + + bb20: { + FakeRead(ForMatchedPlace(None), _2); + unreachable; + } + + bb21: { + StorageLive(_14); + _14 = MyEnum::D; + _0 = Result::::Ok(move _14); + StorageDead(_14); + goto -> bb25; + } + + bb22: { + StorageLive(_13); + _13 = MyEnum::C; + _0 = Result::::Ok(move _13); + StorageDead(_13); + goto -> bb25; + } + + bb23: { + StorageLive(_12); + _12 = MyEnum::B; + _0 = Result::::Ok(move _12); + StorageDead(_12); + goto -> bb25; + } + + bb24: { + StorageLive(_11); + _11 = MyEnum::A; + _0 = Result::::Ok(move _11); + StorageDead(_11); + goto -> bb25; + } + + bb25: { + StorageDead(_2); + return; + } + + bb26 (cleanup): { + resume; + } +} diff --git a/tests/ui/match/aggregate-array-eq.rs b/tests/ui/match/aggregate-array-eq.rs index 6622eef77e103..7341d441fd80a 100644 --- a/tests/ui/match/aggregate-array-eq.rs +++ b/tests/ui/match/aggregate-array-eq.rs @@ -8,9 +8,35 @@ fn array_match(x: [u8; 4]) -> bool { matches!(x, [1, 2, 3, 4]) } +#[derive(Debug, PartialEq)] +enum MyEnum { + A, + B, + C, + D, +} + +// Regression test for https://github.com/rust-lang/rust/issues/103073. +fn try_from_matched(value: [u8; 4]) -> Result { + match &value { + b"ABCD" => Ok(MyEnum::A), + b"EFGH" => Ok(MyEnum::B), + b"IJKL" => Ok(MyEnum::C), + b"MNOP" => Ok(MyEnum::D), + _ => Err(()), + } +} + fn main() { assert!(array_match([1, 2, 3, 4])); assert!(!array_match([1, 2, 3, 5])); assert!(!array_match([0, 0, 0, 0])); assert!(!array_match([4, 3, 2, 1])); + + assert_eq!(try_from_matched(*b"ABCD"), Ok(MyEnum::A)); + assert_eq!(try_from_matched(*b"EFGH"), Ok(MyEnum::B)); + assert_eq!(try_from_matched(*b"IJKL"), Ok(MyEnum::C)); + assert_eq!(try_from_matched(*b"MNOP"), Ok(MyEnum::D)); + assert_eq!(try_from_matched(*b"ZZZZ"), Err(())); + assert_eq!(try_from_matched(*b"ABCE"), Err(())); } From 4df802d21512cc3bc78fae2193e7ad1c51db465c Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Mon, 13 Apr 2026 00:23:49 +0100 Subject: [PATCH 05/14] Make sure the new aggregate equality comparison is excluded from const contexts This is unless the `const_cmp` feature is enabled, in which case `PartialEq` becomes available in said contexts. --- .../src/builder/matches/match_pair.rs | 19 ++++++++++++++++++- compiler/rustc_span/src/symbol.rs | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs index 5fffa7fa5ffc5..6606096840d3f 100644 --- a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs +++ b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs @@ -5,7 +5,7 @@ use rustc_middle::mir::{Pinnedness, Place, PlaceElem, ProjectionElem}; use rustc_middle::span_bug; use rustc_middle::thir::{Ascription, DerefPatBorrowMode, FieldPat, Pat, PatKind}; use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; -use rustc_span::Span; +use rustc_span::{Span, sym}; use crate::builder::Builder; use crate::builder::expr::as_place::{PlaceBase, PlaceBuilder}; @@ -40,6 +40,21 @@ fn try_reconstruct_aggregate_constant<'tcx>( Some(ty::Value { ty: aggregate_ty, valtree }) } +impl<'a, 'tcx> Builder<'a, 'tcx> { + /// Check if we can use aggregate `PartialEq::eq` comparisons for constant array/slice patterns. + /// This is not possible in const contexts unless `#![feature(const_cmp, const_trait_impl)]` are enabled, + /// because`PartialEq` is not const-stable. + fn can_use_aggregate_eq(&self) -> bool { + let const_partial_eq_enabled = { + let features = self.tcx.features(); + features.enabled(sym::const_trait_impl) && features.enabled(sym::const_cmp) + }; + let in_const_context = self.tcx.is_const_fn(self.def_id.to_def_id()) + || !self.tcx.hir_body_owner_kind(self.def_id).is_fn_or_closure(); + !in_const_context || const_partial_eq_enabled + } +} + /// For an array or slice pattern's subpatterns (prefix/slice/suffix), returns a list /// of those subpatterns, each paired with a suitably-projected [`PlaceBuilder`]. fn prefix_slice_suffix<'a, 'tcx>( @@ -376,6 +391,7 @@ impl<'tcx> InterPat<'tcx> { // `PartialEq::eq` rather than element by element. if slice.is_none() && suffix.is_empty() + && cx.can_use_aggregate_eq() && let Some(aggregate_value) = try_reconstruct_aggregate_constant(cx.tcx, pattern.ty, prefix) { @@ -415,6 +431,7 @@ impl<'tcx> InterPat<'tcx> { // is performed after the length check. if slice.is_none() && suffix.is_empty() + && cx.can_use_aggregate_eq() && let Some(aggregate_value) = try_reconstruct_aggregate_constant(cx.tcx, pattern.ty, prefix) { diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index a293e106bd914..0fbac594f8dbf 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -661,6 +661,7 @@ symbols! { const_block_items, const_c_variadic, const_closures, + const_cmp, const_compare_raw_pointers, const_constructor, const_continue, From 4c496f9593cbe0529d3395b07f6de4f9390210df Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Mon, 13 Apr 2026 00:24:56 +0100 Subject: [PATCH 06/14] Update the tests to cover const contexts as well --- ...st_array_match.built.after.panic-abort.mir | 64 ++++++ ...t_array_match.built.after.panic-unwind.mir | 64 ++++++ ...y_from_matched.built.after.panic-abort.mir | 197 ++++++++++++++++++ ..._from_matched.built.after.panic-unwind.mir | 197 ++++++++++++++++++ .../building/match/aggregate_array_eq.rs | 26 +++ ...st_array_match.built.after.panic-abort.mir | 51 +++++ ...t_array_match.built.after.panic-unwind.mir | 51 +++++ ...y_from_matched.built.after.panic-abort.mir | 157 ++++++++++++++ ..._from_matched.built.after.panic-unwind.mir | 157 ++++++++++++++ .../match/aggregate_array_eq_const_cmp.rs | 39 ++++ tests/ui/match/aggregate-array-eq.rs | 45 ++++ 11 files changed, 1048 insertions(+) create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-unwind.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-unwind.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs diff --git a/tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-abort.mir new file mode 100644 index 0000000000000..c785ea537e9c9 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-abort.mir @@ -0,0 +1,64 @@ +// MIR for `const_array_match` after built + +fn const_array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + switchInt(copy _1[0 of 4]) -> [1: bb2, otherwise: bb1]; + } + + bb1: { + _0 = const false; + goto -> bb12; + } + + bb2: { + switchInt(copy _1[1 of 4]) -> [2: bb4, otherwise: bb3]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(copy _1[2 of 4]) -> [3: bb6, otherwise: bb5]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(copy _1[3 of 4]) -> [4: bb8, otherwise: bb7]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + falseEdge -> [real: bb11, imaginary: bb1]; + } + + bb9: { + goto -> bb7; + } + + bb10: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb11: { + _0 = const true; + goto -> bb12; + } + + bb12: { + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..c785ea537e9c9 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.const_array_match.built.after.panic-unwind.mir @@ -0,0 +1,64 @@ +// MIR for `const_array_match` after built + +fn const_array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + switchInt(copy _1[0 of 4]) -> [1: bb2, otherwise: bb1]; + } + + bb1: { + _0 = const false; + goto -> bb12; + } + + bb2: { + switchInt(copy _1[1 of 4]) -> [2: bb4, otherwise: bb3]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(copy _1[2 of 4]) -> [3: bb6, otherwise: bb5]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(copy _1[3 of 4]) -> [4: bb8, otherwise: bb7]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + falseEdge -> [real: bb11, imaginary: bb1]; + } + + bb9: { + goto -> bb7; + } + + bb10: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb11: { + _0 = const true; + goto -> bb12; + } + + bb12: { + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-abort.mir new file mode 100644 index 0000000000000..4b105ec6d5d01 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-abort.mir @@ -0,0 +1,197 @@ +// MIR for `const_try_from_matched` after built + +fn const_try_from_matched(_1: [u8; 4]) -> Result { + debug value => _1; + let mut _0: std::result::Result; + let mut _2: &[u8; 4]; + let mut _3: MyEnum; + let mut _4: MyEnum; + let mut _5: MyEnum; + let mut _6: MyEnum; + let mut _7: (); + + bb0: { + StorageLive(_2); + _2 = &_1; + PlaceMention(_2); + switchInt(copy (*_2)[0 of 4]) -> [65: bb2, 69: bb10, 73: bb18, 77: bb26, otherwise: bb1]; + } + + bb1: { + StorageLive(_7); + _7 = (); + _0 = Result::::Err(move _7); + StorageDead(_7); + goto -> bb39; + } + + bb2: { + switchInt(copy (*_2)[1 of 4]) -> [66: bb4, otherwise: bb3]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(copy (*_2)[2 of 4]) -> [67: bb6, otherwise: bb5]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(copy (*_2)[3 of 4]) -> [68: bb8, otherwise: bb7]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + falseEdge -> [real: bb38, imaginary: bb10]; + } + + bb9: { + goto -> bb7; + } + + bb10: { + switchInt(copy (*_2)[1 of 4]) -> [70: bb12, otherwise: bb11]; + } + + bb11: { + goto -> bb1; + } + + bb12: { + switchInt(copy (*_2)[2 of 4]) -> [71: bb14, otherwise: bb13]; + } + + bb13: { + goto -> bb11; + } + + bb14: { + switchInt(copy (*_2)[3 of 4]) -> [72: bb16, otherwise: bb15]; + } + + bb15: { + goto -> bb13; + } + + bb16: { + falseEdge -> [real: bb37, imaginary: bb18]; + } + + bb17: { + goto -> bb15; + } + + bb18: { + switchInt(copy (*_2)[1 of 4]) -> [74: bb20, otherwise: bb19]; + } + + bb19: { + goto -> bb1; + } + + bb20: { + switchInt(copy (*_2)[2 of 4]) -> [75: bb22, otherwise: bb21]; + } + + bb21: { + goto -> bb19; + } + + bb22: { + switchInt(copy (*_2)[3 of 4]) -> [76: bb24, otherwise: bb23]; + } + + bb23: { + goto -> bb21; + } + + bb24: { + falseEdge -> [real: bb36, imaginary: bb26]; + } + + bb25: { + goto -> bb23; + } + + bb26: { + switchInt(copy (*_2)[1 of 4]) -> [78: bb28, otherwise: bb27]; + } + + bb27: { + goto -> bb1; + } + + bb28: { + switchInt(copy (*_2)[2 of 4]) -> [79: bb30, otherwise: bb29]; + } + + bb29: { + goto -> bb27; + } + + bb30: { + switchInt(copy (*_2)[3 of 4]) -> [80: bb32, otherwise: bb31]; + } + + bb31: { + goto -> bb29; + } + + bb32: { + falseEdge -> [real: bb35, imaginary: bb1]; + } + + bb33: { + goto -> bb31; + } + + bb34: { + FakeRead(ForMatchedPlace(None), _2); + unreachable; + } + + bb35: { + StorageLive(_6); + _6 = MyEnum::D; + _0 = Result::::Ok(move _6); + StorageDead(_6); + goto -> bb39; + } + + bb36: { + StorageLive(_5); + _5 = MyEnum::C; + _0 = Result::::Ok(move _5); + StorageDead(_5); + goto -> bb39; + } + + bb37: { + StorageLive(_4); + _4 = MyEnum::B; + _0 = Result::::Ok(move _4); + StorageDead(_4); + goto -> bb39; + } + + bb38: { + StorageLive(_3); + _3 = MyEnum::A; + _0 = Result::::Ok(move _3); + StorageDead(_3); + goto -> bb39; + } + + bb39: { + StorageDead(_2); + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..4b105ec6d5d01 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.const_try_from_matched.built.after.panic-unwind.mir @@ -0,0 +1,197 @@ +// MIR for `const_try_from_matched` after built + +fn const_try_from_matched(_1: [u8; 4]) -> Result { + debug value => _1; + let mut _0: std::result::Result; + let mut _2: &[u8; 4]; + let mut _3: MyEnum; + let mut _4: MyEnum; + let mut _5: MyEnum; + let mut _6: MyEnum; + let mut _7: (); + + bb0: { + StorageLive(_2); + _2 = &_1; + PlaceMention(_2); + switchInt(copy (*_2)[0 of 4]) -> [65: bb2, 69: bb10, 73: bb18, 77: bb26, otherwise: bb1]; + } + + bb1: { + StorageLive(_7); + _7 = (); + _0 = Result::::Err(move _7); + StorageDead(_7); + goto -> bb39; + } + + bb2: { + switchInt(copy (*_2)[1 of 4]) -> [66: bb4, otherwise: bb3]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(copy (*_2)[2 of 4]) -> [67: bb6, otherwise: bb5]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(copy (*_2)[3 of 4]) -> [68: bb8, otherwise: bb7]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + falseEdge -> [real: bb38, imaginary: bb10]; + } + + bb9: { + goto -> bb7; + } + + bb10: { + switchInt(copy (*_2)[1 of 4]) -> [70: bb12, otherwise: bb11]; + } + + bb11: { + goto -> bb1; + } + + bb12: { + switchInt(copy (*_2)[2 of 4]) -> [71: bb14, otherwise: bb13]; + } + + bb13: { + goto -> bb11; + } + + bb14: { + switchInt(copy (*_2)[3 of 4]) -> [72: bb16, otherwise: bb15]; + } + + bb15: { + goto -> bb13; + } + + bb16: { + falseEdge -> [real: bb37, imaginary: bb18]; + } + + bb17: { + goto -> bb15; + } + + bb18: { + switchInt(copy (*_2)[1 of 4]) -> [74: bb20, otherwise: bb19]; + } + + bb19: { + goto -> bb1; + } + + bb20: { + switchInt(copy (*_2)[2 of 4]) -> [75: bb22, otherwise: bb21]; + } + + bb21: { + goto -> bb19; + } + + bb22: { + switchInt(copy (*_2)[3 of 4]) -> [76: bb24, otherwise: bb23]; + } + + bb23: { + goto -> bb21; + } + + bb24: { + falseEdge -> [real: bb36, imaginary: bb26]; + } + + bb25: { + goto -> bb23; + } + + bb26: { + switchInt(copy (*_2)[1 of 4]) -> [78: bb28, otherwise: bb27]; + } + + bb27: { + goto -> bb1; + } + + bb28: { + switchInt(copy (*_2)[2 of 4]) -> [79: bb30, otherwise: bb29]; + } + + bb29: { + goto -> bb27; + } + + bb30: { + switchInt(copy (*_2)[3 of 4]) -> [80: bb32, otherwise: bb31]; + } + + bb31: { + goto -> bb29; + } + + bb32: { + falseEdge -> [real: bb35, imaginary: bb1]; + } + + bb33: { + goto -> bb31; + } + + bb34: { + FakeRead(ForMatchedPlace(None), _2); + unreachable; + } + + bb35: { + StorageLive(_6); + _6 = MyEnum::D; + _0 = Result::::Ok(move _6); + StorageDead(_6); + goto -> bb39; + } + + bb36: { + StorageLive(_5); + _5 = MyEnum::C; + _0 = Result::::Ok(move _5); + StorageDead(_5); + goto -> bb39; + } + + bb37: { + StorageLive(_4); + _4 = MyEnum::B; + _0 = Result::::Ok(move _4); + StorageDead(_4); + goto -> bb39; + } + + bb38: { + StorageLive(_3); + _3 = MyEnum::A; + _0 = Result::::Ok(move _3); + StorageDead(_3); + goto -> bb39; + } + + bb39: { + StorageDead(_2); + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.rs b/tests/mir-opt/building/match/aggregate_array_eq.rs index 860c0bcd17113..d72eee8d7f853 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.rs +++ b/tests/mir-opt/building/match/aggregate_array_eq.rs @@ -3,6 +3,8 @@ // Verify that matching against a constant array pattern produces a single // `PartialEq::eq` call rather than element-by-element comparisons. +// In const contexts, the aggregate comparison must NOT be used because +// `PartialEq` is not const-stable (unless `#![feature(const_cmp)]`). #![crate_type = "lib"] @@ -35,3 +37,27 @@ pub fn try_from_matched(value: [u8; 4]) -> Result { _ => Err(()), } } + +// In a const fn, the aggregate comparison must not be used because +// `PartialEq::eq` cannot be called during const evaluation. +// EMIT_MIR aggregate_array_eq.const_array_match.built.after.mir +pub const fn const_array_match(x: [u8; 4]) -> bool { + // CHECK-LABEL: fn const_array_match( + // CHECK-NOT: PartialEq + // CHECK: switchInt + matches!(x, [1, 2, 3, 4]) +} + +// EMIT_MIR aggregate_array_eq.const_try_from_matched.built.after.mir +pub const fn const_try_from_matched(value: [u8; 4]) -> Result { + // CHECK-LABEL: fn const_try_from_matched( + // CHECK-NOT: PartialEq + // CHECK: switchInt + match &value { + b"ABCD" => Ok(MyEnum::A), + b"EFGH" => Ok(MyEnum::B), + b"IJKL" => Ok(MyEnum::C), + b"MNOP" => Ok(MyEnum::D), + _ => Err(()), + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir new file mode 100644 index 0000000000000..a0ea09cad59c4 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir @@ -0,0 +1,51 @@ +// MIR for `const_array_match` after built + +fn const_array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[u8; 4]; + let mut _3: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _2 = &_1; + _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; + } + + bb1: { + _0 = const false; + goto -> bb7; + } + + bb2: { + falseEdge -> [real: bb6, imaginary: bb1]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(move _3) -> [0: bb1, otherwise: bb2]; + } + + bb5: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb6: { + _0 = const true; + goto -> bb7; + } + + bb7: { + return; + } + + bb8 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..a0ea09cad59c4 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir @@ -0,0 +1,51 @@ +// MIR for `const_array_match` after built + +fn const_array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[u8; 4]; + let mut _3: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _2 = &_1; + _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; + } + + bb1: { + _0 = const false; + goto -> bb7; + } + + bb2: { + falseEdge -> [real: bb6, imaginary: bb1]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(move _3) -> [0: bb1, otherwise: bb2]; + } + + bb5: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb6: { + _0 = const true; + goto -> bb7; + } + + bb7: { + return; + } + + bb8 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir new file mode 100644 index 0000000000000..0a3783666912d --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir @@ -0,0 +1,157 @@ +// MIR for `const_try_from_matched` after built + +fn const_try_from_matched(_1: [u8; 4]) -> Result { + debug value => _1; + let mut _0: std::result::Result; + let mut _2: &[u8; 4]; + let mut _3: &[u8; 4]; + let mut _4: bool; + let mut _5: &[u8; 4]; + let mut _6: bool; + let mut _7: &[u8; 4]; + let mut _8: bool; + let mut _9: &[u8; 4]; + let mut _10: bool; + let mut _11: MyEnum; + let mut _12: MyEnum; + let mut _13: MyEnum; + let mut _14: MyEnum; + let mut _15: (); + + bb0: { + StorageLive(_2); + _2 = &_1; + PlaceMention(_2); + _9 = &(*_2); + _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; + } + + bb1: { + StorageLive(_15); + _15 = (); + _0 = Result::::Err(move _15); + StorageDead(_15); + goto -> bb25; + } + + bb2: { + falseEdge -> [real: bb24, imaginary: bb4]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + _7 = &(*_2); + _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; + } + + bb5: { + goto -> bb1; + } + + bb6: { + falseEdge -> [real: bb23, imaginary: bb8]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + _5 = &(*_2); + _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; + } + + bb9: { + goto -> bb5; + } + + bb10: { + falseEdge -> [real: bb22, imaginary: bb12]; + } + + bb11: { + goto -> bb9; + } + + bb12: { + _3 = &(*_2); + _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; + } + + bb13: { + goto -> bb9; + } + + bb14: { + falseEdge -> [real: bb21, imaginary: bb1]; + } + + bb15: { + goto -> bb13; + } + + bb16: { + switchInt(move _4) -> [0: bb13, otherwise: bb14]; + } + + bb17: { + switchInt(move _6) -> [0: bb12, otherwise: bb10]; + } + + bb18: { + switchInt(move _8) -> [0: bb8, otherwise: bb6]; + } + + bb19: { + switchInt(move _10) -> [0: bb4, otherwise: bb2]; + } + + bb20: { + FakeRead(ForMatchedPlace(None), _2); + unreachable; + } + + bb21: { + StorageLive(_14); + _14 = MyEnum::D; + _0 = Result::::Ok(move _14); + StorageDead(_14); + goto -> bb25; + } + + bb22: { + StorageLive(_13); + _13 = MyEnum::C; + _0 = Result::::Ok(move _13); + StorageDead(_13); + goto -> bb25; + } + + bb23: { + StorageLive(_12); + _12 = MyEnum::B; + _0 = Result::::Ok(move _12); + StorageDead(_12); + goto -> bb25; + } + + bb24: { + StorageLive(_11); + _11 = MyEnum::A; + _0 = Result::::Ok(move _11); + StorageDead(_11); + goto -> bb25; + } + + bb25: { + StorageDead(_2); + return; + } + + bb26 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..0a3783666912d --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir @@ -0,0 +1,157 @@ +// MIR for `const_try_from_matched` after built + +fn const_try_from_matched(_1: [u8; 4]) -> Result { + debug value => _1; + let mut _0: std::result::Result; + let mut _2: &[u8; 4]; + let mut _3: &[u8; 4]; + let mut _4: bool; + let mut _5: &[u8; 4]; + let mut _6: bool; + let mut _7: &[u8; 4]; + let mut _8: bool; + let mut _9: &[u8; 4]; + let mut _10: bool; + let mut _11: MyEnum; + let mut _12: MyEnum; + let mut _13: MyEnum; + let mut _14: MyEnum; + let mut _15: (); + + bb0: { + StorageLive(_2); + _2 = &_1; + PlaceMention(_2); + _9 = &(*_2); + _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; + } + + bb1: { + StorageLive(_15); + _15 = (); + _0 = Result::::Err(move _15); + StorageDead(_15); + goto -> bb25; + } + + bb2: { + falseEdge -> [real: bb24, imaginary: bb4]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + _7 = &(*_2); + _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; + } + + bb5: { + goto -> bb1; + } + + bb6: { + falseEdge -> [real: bb23, imaginary: bb8]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + _5 = &(*_2); + _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; + } + + bb9: { + goto -> bb5; + } + + bb10: { + falseEdge -> [real: bb22, imaginary: bb12]; + } + + bb11: { + goto -> bb9; + } + + bb12: { + _3 = &(*_2); + _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; + } + + bb13: { + goto -> bb9; + } + + bb14: { + falseEdge -> [real: bb21, imaginary: bb1]; + } + + bb15: { + goto -> bb13; + } + + bb16: { + switchInt(move _4) -> [0: bb13, otherwise: bb14]; + } + + bb17: { + switchInt(move _6) -> [0: bb12, otherwise: bb10]; + } + + bb18: { + switchInt(move _8) -> [0: bb8, otherwise: bb6]; + } + + bb19: { + switchInt(move _10) -> [0: bb4, otherwise: bb2]; + } + + bb20: { + FakeRead(ForMatchedPlace(None), _2); + unreachable; + } + + bb21: { + StorageLive(_14); + _14 = MyEnum::D; + _0 = Result::::Ok(move _14); + StorageDead(_14); + goto -> bb25; + } + + bb22: { + StorageLive(_13); + _13 = MyEnum::C; + _0 = Result::::Ok(move _13); + StorageDead(_13); + goto -> bb25; + } + + bb23: { + StorageLive(_12); + _12 = MyEnum::B; + _0 = Result::::Ok(move _12); + StorageDead(_12); + goto -> bb25; + } + + bb24: { + StorageLive(_11); + _11 = MyEnum::A; + _0 = Result::::Ok(move _11); + StorageDead(_11); + goto -> bb25; + } + + bb25: { + StorageDead(_2); + return; + } + + bb26 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs new file mode 100644 index 0000000000000..d70ab9b6bbcc1 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs @@ -0,0 +1,39 @@ +// EMIT_MIR_FOR_EACH_PANIC_STRATEGY +//@ compile-flags: -Zmir-opt-level=0 + +// Verify that with `#![feature(const_cmp)]` and `#![feature(const_trait_impl)]`, +// const functions also use aggregate `PartialEq::eq` comparisons for constant +// array patterns, matching the behaviour of non-const functions. + +#![crate_type = "lib"] +#![feature(const_cmp)] +#![feature(const_trait_impl)] + +pub enum MyEnum { + A, + B, + C, + D, +} + +// EMIT_MIR aggregate_array_eq_const_cmp.const_array_match.built.after.mir +pub const fn const_array_match(x: [u8; 4]) -> bool { + // CHECK-LABEL: fn const_array_match( + // CHECK: <[u8; 4] as PartialEq>::eq + // CHECK-NOT: switchInt(copy _1[ + matches!(x, [1, 2, 3, 4]) +} + +// EMIT_MIR aggregate_array_eq_const_cmp.const_try_from_matched.built.after.mir +pub const fn const_try_from_matched(value: [u8; 4]) -> Result { + // CHECK-LABEL: fn const_try_from_matched( + // CHECK: <[u8; 4] as PartialEq>::eq + // CHECK-NOT: switchInt(copy (*_2)[ + match &value { + b"ABCD" => Ok(MyEnum::A), + b"EFGH" => Ok(MyEnum::B), + b"IJKL" => Ok(MyEnum::C), + b"MNOP" => Ok(MyEnum::D), + _ => Err(()), + } +} diff --git a/tests/ui/match/aggregate-array-eq.rs b/tests/ui/match/aggregate-array-eq.rs index 7341d441fd80a..f5234e08b4f52 100644 --- a/tests/ui/match/aggregate-array-eq.rs +++ b/tests/ui/match/aggregate-array-eq.rs @@ -2,6 +2,9 @@ //! results at runtime, complementing the MIR test in //! `tests/mir-opt/building/match/aggregate_array_eq.rs` which checks that //! a single aggregate `PartialEq::eq` call is emitted. +//! +//! Also verify that const-context variants (which fall back to +//! element-by-element comparison) produce the same results. //@ run-pass fn array_match(x: [u8; 4]) -> bool { @@ -27,6 +30,22 @@ fn try_from_matched(value: [u8; 4]) -> Result { } } +// Const fn variants use element-by-element comparison because +// `PartialEq::eq` is not available in const contexts. +const fn const_array_match(x: [u8; 4]) -> bool { + matches!(x, [1, 2, 3, 4]) +} + +const fn const_try_from_matched(value: [u8; 4]) -> Result { + match &value { + b"ABCD" => Ok(MyEnum::A), + b"EFGH" => Ok(MyEnum::B), + b"IJKL" => Ok(MyEnum::C), + b"MNOP" => Ok(MyEnum::D), + _ => Err(()), + } +} + fn main() { assert!(array_match([1, 2, 3, 4])); assert!(!array_match([1, 2, 3, 5])); @@ -39,4 +58,30 @@ fn main() { assert_eq!(try_from_matched(*b"MNOP"), Ok(MyEnum::D)); assert_eq!(try_from_matched(*b"ZZZZ"), Err(())); assert_eq!(try_from_matched(*b"ABCE"), Err(())); + + // Const fn variants called at runtime. + assert!(const_array_match([1, 2, 3, 4])); + assert!(!const_array_match([1, 2, 3, 5])); + assert!(!const_array_match([0, 0, 0, 0])); + assert!(!const_array_match([4, 3, 2, 1])); + + assert_eq!(const_try_from_matched(*b"ABCD"), Ok(MyEnum::A)); + assert_eq!(const_try_from_matched(*b"EFGH"), Ok(MyEnum::B)); + assert_eq!(const_try_from_matched(*b"IJKL"), Ok(MyEnum::C)); + assert_eq!(const_try_from_matched(*b"MNOP"), Ok(MyEnum::D)); + assert_eq!(const_try_from_matched(*b"ZZZZ"), Err(())); + assert_eq!(const_try_from_matched(*b"ABCE"), Err(())); + + // Const fn variants evaluated at compile time. + const MATCH_TRUE: bool = const_array_match([1, 2, 3, 4]); + const MATCH_FALSE: bool = const_array_match([1, 2, 3, 5]); + assert!(MATCH_TRUE); + assert!(!MATCH_FALSE); + + const FROM_ABCD: Result = const_try_from_matched(*b"ABCD"); + const FROM_MNOP: Result = const_try_from_matched(*b"MNOP"); + const FROM_ZZZZ: Result = const_try_from_matched(*b"ZZZZ"); + assert_eq!(FROM_ABCD, Ok(MyEnum::A)); + assert_eq!(FROM_MNOP, Ok(MyEnum::D)); + assert_eq!(FROM_ZZZZ, Err(())); } From ca8b70b3d4a57ce1edf5fe05acb1849009093eb0 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 26 Apr 2026 20:33:16 +0100 Subject: [PATCH 07/14] Raise the aggregate equality comparison threshold so simple arrays don't get captured by this logic --- .../rustc_mir_build/src/builder/matches/match_pair.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs index 6606096840d3f..f84ec7076a58b 100644 --- a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs +++ b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs @@ -13,6 +13,11 @@ use crate::builder::matches::{ FlatPat, MatchPairTree, PatConstKind, PatternExtraData, SliceLenOp, TestableCase, }; +/// Below this length, an array or slice pattern is compared element by element +/// rather than as a single aggregate, since the per-element comparisons are +/// unlikely to be more expensive than a `PartialEq::eq` call. +const AGGREGATE_EQ_MIN_LEN: usize = 4; + /// Checks whether every pattern in `elements` is a `PatKind::Constant` and, /// if so, reconstructs a single aggregate `ty::Value` that represents the whole /// array or slice. Returns `None` when any element is not a constant or the @@ -22,8 +27,8 @@ fn try_reconstruct_aggregate_constant<'tcx>( aggregate_ty: Ty<'tcx>, elements: &[Pat<'tcx>], ) -> Option> { - // A single element (or empty array) is not worth an aggregate comparison. - if elements.len() <= 1 { + // Short arrays are not worth an aggregate comparison. + if elements.len() < AGGREGATE_EQ_MIN_LEN { return None; } let branches = elements From 0da5fed8c23e97af75a271201ea1d8c0bd8214cd Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 26 Apr 2026 20:41:12 +0100 Subject: [PATCH 08/14] Merge the AggregateEq and StringEq test arms into a single case --- .../src/builder/matches/test.rs | 76 ++++++------------- 1 file changed, 22 insertions(+), 54 deletions(-) diff --git a/compiler/rustc_mir_build/src/builder/matches/test.rs b/compiler/rustc_mir_build/src/builder/matches/test.rs index d5f19066c24ff..418c2163be9e4 100644 --- a/compiler/rustc_mir_build/src/builder/matches/test.rs +++ b/compiler/rustc_mir_build/src/builder/matches/test.rs @@ -140,27 +140,32 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { self.cfg.terminate(block, self.source_info(match_start_span), terminator); } - TestKind::StringEq { value } => { + TestKind::StringEq { value } | TestKind::AggregateEq { value } => { let tcx = self.tcx; let success_block = target_block(TestBranch::Success); let fail_block = target_block(TestBranch::Failure); - let ref_str_ty = Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, tcx.types.str_); - assert!(ref_str_ty.is_imm_ref_str(), "{ref_str_ty:?}"); + let inner_ty = value.ty; + if matches!(test.kind, TestKind::StringEq { .. }) { + assert!( + inner_ty.is_str(), + "unexpected value type for StringEq test: {value:?}" + ); + } + let ref_ty = Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, inner_ty); - // The string constant we're testing against has type `str`, but - // calling `::eq` requires `&str` operands. - // - // Because `str` and `&str` have the same valtree representation, - // we can "cast" to the desired type by just replacing the type. - assert!(value.ty.is_str(), "unexpected value type for StringEq test: {value:?}"); - let expected_value = ty::Value { ty: ref_str_ty, valtree: value.valtree }; + // The constant we're testing against has type `str`, `[T; N]`, or `[T]`, + // but calling `::eq` requires a reference operand + // (`&str`, `&[T; N]`, or `&[T]`). Valtree representations are the same + // with or without the reference wrapper, so we can "cast" to the + // desired type by just replacing the type. + let expected_value = ty::Value { ty: ref_ty, valtree: value.valtree }; let expected_value_operand = self.literal_operand(test.span, Const::from_ty_value(tcx, expected_value)); - // Similarly, the scrutinized place has type `str`, but we need `&str`. - // Get a reference by doing `let actual_value_ref_place: &str = &place`. - let actual_value_ref_place = self.temp(ref_str_ty, test.span); + // Similarly, the scrutinised place has the inner type, but we need a + // reference. Get one by doing `let actual_value_ref_place = &place`. + let actual_value_ref_place = self.temp(ref_ty, test.span); self.cfg.push_assign( block, self.source_info(test.span), @@ -168,57 +173,20 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { Rvalue::Ref(tcx.lifetimes.re_erased, BorrowKind::Shared, place), ); - // Compare two strings using `::eq`. - // (Interestingly this means that exhaustiveness analysis relies, for soundness, - // on the `PartialEq` impl for `str` to be correct!) + // Compare the two values using `::eq`. + // (Interestingly this means that, for `str`, exhaustiveness analysis + // relies for soundness on the `PartialEq` impl for `str` to be correct!) self.non_scalar_compare( block, success_block, fail_block, source_info, - tcx.types.str_, + inner_ty, expected_value_operand, Operand::Copy(actual_value_ref_place), ); } - TestKind::AggregateEq { value } => { - let tcx = self.tcx; - let success_block = target_block(TestBranch::Success); - let fail_block = target_block(TestBranch::Failure); - - let aggregate_ty = value.ty; - let ref_ty = Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, aggregate_ty); - - // The constant has type `[T; N]` (or `[T]`), but calling - // `PartialEq::eq` requires `&[T; N]` (or `&[T]`) operands. - // Valtree representations are the same with or without the - // reference wrapper, so we can reinterpret by replacing the type. - let expected_value = ty::Value { ty: ref_ty, valtree: value.valtree }; - let expected_operand = - self.literal_operand(test.span, Const::from_ty_value(tcx, expected_value)); - - // Create a reference to the scrutinee place. - let actual_ref_place = self.temp(ref_ty, test.span); - self.cfg.push_assign( - block, - self.source_info(test.span), - actual_ref_place, - Rvalue::Ref(tcx.lifetimes.re_erased, BorrowKind::Shared, place), - ); - - // Compare using `::eq` where `T` is the array or slice type. - self.non_scalar_compare( - block, - success_block, - fail_block, - source_info, - aggregate_ty, - expected_operand, - Operand::Copy(actual_ref_place), - ); - } - TestKind::ScalarEq { value } => { let tcx = self.tcx; let success_block = target_block(TestBranch::Success); From 776ace0fd7c7cb080ba2d968bcdf3e3bc0f80764 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 26 Apr 2026 20:45:39 +0100 Subject: [PATCH 09/14] Add a missing space --- compiler/rustc_mir_build/src/builder/matches/match_pair.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs index f84ec7076a58b..a6519de97930a 100644 --- a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs +++ b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs @@ -48,7 +48,7 @@ fn try_reconstruct_aggregate_constant<'tcx>( impl<'a, 'tcx> Builder<'a, 'tcx> { /// Check if we can use aggregate `PartialEq::eq` comparisons for constant array/slice patterns. /// This is not possible in const contexts unless `#![feature(const_cmp, const_trait_impl)]` are enabled, - /// because`PartialEq` is not const-stable. + /// because `PartialEq` is not const-stable. fn can_use_aggregate_eq(&self) -> bool { let const_partial_eq_enabled = { let features = self.tcx.features(); From 9f195dc3dc03a4276f5b7de584934718227d711d Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 26 Apr 2026 22:23:08 +0100 Subject: [PATCH 10/14] Remove the special treatment of the `const_cmp` and `const_trait_impl` features --- .../src/builder/matches/match_pair.rs | 11 +- compiler/rustc_span/src/symbol.rs | 1 - .../building/match/aggregate_array_eq.rs | 2 +- ...st_array_match.built.after.panic-abort.mir | 51 ------ ...t_array_match.built.after.panic-unwind.mir | 51 ------ ...y_from_matched.built.after.panic-abort.mir | 157 ------------------ ..._from_matched.built.after.panic-unwind.mir | 157 ------------------ .../match/aggregate_array_eq_const_cmp.rs | 39 ----- 8 files changed, 4 insertions(+), 465 deletions(-) delete mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir delete mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir delete mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir delete mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir delete mode 100644 tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs diff --git a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs index a6519de97930a..abb6cb3f88b71 100644 --- a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs +++ b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs @@ -5,7 +5,7 @@ use rustc_middle::mir::{Pinnedness, Place, PlaceElem, ProjectionElem}; use rustc_middle::span_bug; use rustc_middle::thir::{Ascription, DerefPatBorrowMode, FieldPat, Pat, PatKind}; use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; -use rustc_span::{Span, sym}; +use rustc_span::Span; use crate::builder::Builder; use crate::builder::expr::as_place::{PlaceBase, PlaceBuilder}; @@ -47,16 +47,11 @@ fn try_reconstruct_aggregate_constant<'tcx>( impl<'a, 'tcx> Builder<'a, 'tcx> { /// Check if we can use aggregate `PartialEq::eq` comparisons for constant array/slice patterns. - /// This is not possible in const contexts unless `#![feature(const_cmp, const_trait_impl)]` are enabled, - /// because `PartialEq` is not const-stable. + /// This is not possible in const contexts, because `PartialEq` is not const-stable yet. fn can_use_aggregate_eq(&self) -> bool { - let const_partial_eq_enabled = { - let features = self.tcx.features(); - features.enabled(sym::const_trait_impl) && features.enabled(sym::const_cmp) - }; let in_const_context = self.tcx.is_const_fn(self.def_id.to_def_id()) || !self.tcx.hir_body_owner_kind(self.def_id).is_fn_or_closure(); - !in_const_context || const_partial_eq_enabled + !in_const_context } } diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 0fbac594f8dbf..a293e106bd914 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -661,7 +661,6 @@ symbols! { const_block_items, const_c_variadic, const_closures, - const_cmp, const_compare_raw_pointers, const_constructor, const_continue, diff --git a/tests/mir-opt/building/match/aggregate_array_eq.rs b/tests/mir-opt/building/match/aggregate_array_eq.rs index d72eee8d7f853..94c857458f9ee 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.rs +++ b/tests/mir-opt/building/match/aggregate_array_eq.rs @@ -4,7 +4,7 @@ // Verify that matching against a constant array pattern produces a single // `PartialEq::eq` call rather than element-by-element comparisons. // In const contexts, the aggregate comparison must NOT be used because -// `PartialEq` is not const-stable (unless `#![feature(const_cmp)]`). +// `PartialEq` is not const-stable. #![crate_type = "lib"] diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir deleted file mode 100644 index a0ea09cad59c4..0000000000000 --- a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-abort.mir +++ /dev/null @@ -1,51 +0,0 @@ -// MIR for `const_array_match` after built - -fn const_array_match(_1: [u8; 4]) -> bool { - debug x => _1; - let mut _0: bool; - let mut _2: &[u8; 4]; - let mut _3: bool; - scope 1 { - } - - bb0: { - PlaceMention(_1); - _2 = &_1; - _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; - } - - bb1: { - _0 = const false; - goto -> bb7; - } - - bb2: { - falseEdge -> [real: bb6, imaginary: bb1]; - } - - bb3: { - goto -> bb1; - } - - bb4: { - switchInt(move _3) -> [0: bb1, otherwise: bb2]; - } - - bb5: { - FakeRead(ForMatchedPlace(None), _1); - unreachable; - } - - bb6: { - _0 = const true; - goto -> bb7; - } - - bb7: { - return; - } - - bb8 (cleanup): { - resume; - } -} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir deleted file mode 100644 index a0ea09cad59c4..0000000000000 --- a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_array_match.built.after.panic-unwind.mir +++ /dev/null @@ -1,51 +0,0 @@ -// MIR for `const_array_match` after built - -fn const_array_match(_1: [u8; 4]) -> bool { - debug x => _1; - let mut _0: bool; - let mut _2: &[u8; 4]; - let mut _3: bool; - scope 1 { - } - - bb0: { - PlaceMention(_1); - _2 = &_1; - _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; - } - - bb1: { - _0 = const false; - goto -> bb7; - } - - bb2: { - falseEdge -> [real: bb6, imaginary: bb1]; - } - - bb3: { - goto -> bb1; - } - - bb4: { - switchInt(move _3) -> [0: bb1, otherwise: bb2]; - } - - bb5: { - FakeRead(ForMatchedPlace(None), _1); - unreachable; - } - - bb6: { - _0 = const true; - goto -> bb7; - } - - bb7: { - return; - } - - bb8 (cleanup): { - resume; - } -} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir deleted file mode 100644 index 0a3783666912d..0000000000000 --- a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-abort.mir +++ /dev/null @@ -1,157 +0,0 @@ -// MIR for `const_try_from_matched` after built - -fn const_try_from_matched(_1: [u8; 4]) -> Result { - debug value => _1; - let mut _0: std::result::Result; - let mut _2: &[u8; 4]; - let mut _3: &[u8; 4]; - let mut _4: bool; - let mut _5: &[u8; 4]; - let mut _6: bool; - let mut _7: &[u8; 4]; - let mut _8: bool; - let mut _9: &[u8; 4]; - let mut _10: bool; - let mut _11: MyEnum; - let mut _12: MyEnum; - let mut _13: MyEnum; - let mut _14: MyEnum; - let mut _15: (); - - bb0: { - StorageLive(_2); - _2 = &_1; - PlaceMention(_2); - _9 = &(*_2); - _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; - } - - bb1: { - StorageLive(_15); - _15 = (); - _0 = Result::::Err(move _15); - StorageDead(_15); - goto -> bb25; - } - - bb2: { - falseEdge -> [real: bb24, imaginary: bb4]; - } - - bb3: { - goto -> bb1; - } - - bb4: { - _7 = &(*_2); - _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; - } - - bb5: { - goto -> bb1; - } - - bb6: { - falseEdge -> [real: bb23, imaginary: bb8]; - } - - bb7: { - goto -> bb5; - } - - bb8: { - _5 = &(*_2); - _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; - } - - bb9: { - goto -> bb5; - } - - bb10: { - falseEdge -> [real: bb22, imaginary: bb12]; - } - - bb11: { - goto -> bb9; - } - - bb12: { - _3 = &(*_2); - _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; - } - - bb13: { - goto -> bb9; - } - - bb14: { - falseEdge -> [real: bb21, imaginary: bb1]; - } - - bb15: { - goto -> bb13; - } - - bb16: { - switchInt(move _4) -> [0: bb13, otherwise: bb14]; - } - - bb17: { - switchInt(move _6) -> [0: bb12, otherwise: bb10]; - } - - bb18: { - switchInt(move _8) -> [0: bb8, otherwise: bb6]; - } - - bb19: { - switchInt(move _10) -> [0: bb4, otherwise: bb2]; - } - - bb20: { - FakeRead(ForMatchedPlace(None), _2); - unreachable; - } - - bb21: { - StorageLive(_14); - _14 = MyEnum::D; - _0 = Result::::Ok(move _14); - StorageDead(_14); - goto -> bb25; - } - - bb22: { - StorageLive(_13); - _13 = MyEnum::C; - _0 = Result::::Ok(move _13); - StorageDead(_13); - goto -> bb25; - } - - bb23: { - StorageLive(_12); - _12 = MyEnum::B; - _0 = Result::::Ok(move _12); - StorageDead(_12); - goto -> bb25; - } - - bb24: { - StorageLive(_11); - _11 = MyEnum::A; - _0 = Result::::Ok(move _11); - StorageDead(_11); - goto -> bb25; - } - - bb25: { - StorageDead(_2); - return; - } - - bb26 (cleanup): { - resume; - } -} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir deleted file mode 100644 index 0a3783666912d..0000000000000 --- a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.const_try_from_matched.built.after.panic-unwind.mir +++ /dev/null @@ -1,157 +0,0 @@ -// MIR for `const_try_from_matched` after built - -fn const_try_from_matched(_1: [u8; 4]) -> Result { - debug value => _1; - let mut _0: std::result::Result; - let mut _2: &[u8; 4]; - let mut _3: &[u8; 4]; - let mut _4: bool; - let mut _5: &[u8; 4]; - let mut _6: bool; - let mut _7: &[u8; 4]; - let mut _8: bool; - let mut _9: &[u8; 4]; - let mut _10: bool; - let mut _11: MyEnum; - let mut _12: MyEnum; - let mut _13: MyEnum; - let mut _14: MyEnum; - let mut _15: (); - - bb0: { - StorageLive(_2); - _2 = &_1; - PlaceMention(_2); - _9 = &(*_2); - _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; - } - - bb1: { - StorageLive(_15); - _15 = (); - _0 = Result::::Err(move _15); - StorageDead(_15); - goto -> bb25; - } - - bb2: { - falseEdge -> [real: bb24, imaginary: bb4]; - } - - bb3: { - goto -> bb1; - } - - bb4: { - _7 = &(*_2); - _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; - } - - bb5: { - goto -> bb1; - } - - bb6: { - falseEdge -> [real: bb23, imaginary: bb8]; - } - - bb7: { - goto -> bb5; - } - - bb8: { - _5 = &(*_2); - _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; - } - - bb9: { - goto -> bb5; - } - - bb10: { - falseEdge -> [real: bb22, imaginary: bb12]; - } - - bb11: { - goto -> bb9; - } - - bb12: { - _3 = &(*_2); - _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; - } - - bb13: { - goto -> bb9; - } - - bb14: { - falseEdge -> [real: bb21, imaginary: bb1]; - } - - bb15: { - goto -> bb13; - } - - bb16: { - switchInt(move _4) -> [0: bb13, otherwise: bb14]; - } - - bb17: { - switchInt(move _6) -> [0: bb12, otherwise: bb10]; - } - - bb18: { - switchInt(move _8) -> [0: bb8, otherwise: bb6]; - } - - bb19: { - switchInt(move _10) -> [0: bb4, otherwise: bb2]; - } - - bb20: { - FakeRead(ForMatchedPlace(None), _2); - unreachable; - } - - bb21: { - StorageLive(_14); - _14 = MyEnum::D; - _0 = Result::::Ok(move _14); - StorageDead(_14); - goto -> bb25; - } - - bb22: { - StorageLive(_13); - _13 = MyEnum::C; - _0 = Result::::Ok(move _13); - StorageDead(_13); - goto -> bb25; - } - - bb23: { - StorageLive(_12); - _12 = MyEnum::B; - _0 = Result::::Ok(move _12); - StorageDead(_12); - goto -> bb25; - } - - bb24: { - StorageLive(_11); - _11 = MyEnum::A; - _0 = Result::::Ok(move _11); - StorageDead(_11); - goto -> bb25; - } - - bb25: { - StorageDead(_2); - return; - } - - bb26 (cleanup): { - resume; - } -} diff --git a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs b/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs deleted file mode 100644 index d70ab9b6bbcc1..0000000000000 --- a/tests/mir-opt/building/match/aggregate_array_eq_const_cmp.rs +++ /dev/null @@ -1,39 +0,0 @@ -// EMIT_MIR_FOR_EACH_PANIC_STRATEGY -//@ compile-flags: -Zmir-opt-level=0 - -// Verify that with `#![feature(const_cmp)]` and `#![feature(const_trait_impl)]`, -// const functions also use aggregate `PartialEq::eq` comparisons for constant -// array patterns, matching the behaviour of non-const functions. - -#![crate_type = "lib"] -#![feature(const_cmp)] -#![feature(const_trait_impl)] - -pub enum MyEnum { - A, - B, - C, - D, -} - -// EMIT_MIR aggregate_array_eq_const_cmp.const_array_match.built.after.mir -pub const fn const_array_match(x: [u8; 4]) -> bool { - // CHECK-LABEL: fn const_array_match( - // CHECK: <[u8; 4] as PartialEq>::eq - // CHECK-NOT: switchInt(copy _1[ - matches!(x, [1, 2, 3, 4]) -} - -// EMIT_MIR aggregate_array_eq_const_cmp.const_try_from_matched.built.after.mir -pub const fn const_try_from_matched(value: [u8; 4]) -> Result { - // CHECK-LABEL: fn const_try_from_matched( - // CHECK: <[u8; 4] as PartialEq>::eq - // CHECK-NOT: switchInt(copy (*_2)[ - match &value { - b"ABCD" => Ok(MyEnum::A), - b"EFGH" => Ok(MyEnum::B), - b"IJKL" => Ok(MyEnum::C), - b"MNOP" => Ok(MyEnum::D), - _ => Err(()), - } -} From 10cbf96b9a46dcdb859daf982c0b7d6d806ab95d Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sat, 11 Jul 2026 20:17:43 +0100 Subject: [PATCH 11/14] Pass the original constant value through THIR instead of reconstructing it Following the review feedback, `const_to_pat()` now records the original constant value on the array and slice pattern nodes it expands, and match lowering reads that value back instead of attempting to reconstruct an aggregate constant from the individual element subpatterns. This has two consequences. First, hand-written array and slice patterns are no longer collapsed into aggregate comparisons; the user's intent to match element by element is respected before the MIR boundary. Only patterns that were expanded from an actual constant (a named constant or a byte-string literal) use the aggregate `PartialEq::eq` comparison, and for those the semantics of matching against a constant and comparing with `PartialEq::eq` coincide. Second, nested constant arrays now benefit from the aggregate comparison as well, since the recorded value covers the whole constant, whereas the reconstruction required every immediate element of the pattern to be a leaf constant. --- compiler/rustc_middle/src/thir.rs | 8 ++ .../src/builder/matches/match_pair.rs | 76 ++++++++----------- .../src/builder/matches/mod.rs | 4 +- .../src/thir/pattern/const_to_pat.rs | 10 ++- compiler/rustc_mir_build/src/thir/print.rs | 7 +- ...en_array_match.built.after.panic-abort.mir | 64 ++++++++++++++++ ...n_array_match.built.after.panic-unwind.mir | 64 ++++++++++++++++ .../building/match/aggregate_array_eq.rs | 27 ++++++- ...eq.slice_match.built.after.panic-abort.mir | 67 ++++++++++++++++ ...q.slice_match.built.after.panic-unwind.mir | 67 ++++++++++++++++ tests/ui/match/aggregate-array-eq.rs | 40 ++++++++-- tests/ui/thir-print/str-patterns.stdout | 1 + .../thir-print/thir-tree-array-index.stdout | 2 + .../ui/thir-print/thir-tree-match-for.stdout | 1 + 14 files changed, 381 insertions(+), 57 deletions(-) create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-unwind.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir diff --git a/compiler/rustc_middle/src/thir.rs b/compiler/rustc_middle/src/thir.rs index 8e7d3d0d9c656..220ab16a44d48 100644 --- a/compiler/rustc_middle/src/thir.rs +++ b/compiler/rustc_middle/src/thir.rs @@ -660,6 +660,14 @@ pub struct PatExtra<'tcx> { /// the pattern node back to the `DefId` of its original constant. pub expanded_const: Option, + /// If present, the original constant value that this array or slice + /// pattern node was expanded from by `const_to_pat`. + /// + /// Match lowering uses this to compare the scrutinee against the original + /// constant as a whole via `PartialEq::eq`, rather than element by + /// element. + pub expanded_const_value: Option>, + /// User-written types that must be preserved into MIR so that they can be /// checked. pub ascriptions: Vec>, diff --git a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs index abb6cb3f88b71..93e4ba874cea2 100644 --- a/compiler/rustc_mir_build/src/builder/matches/match_pair.rs +++ b/compiler/rustc_mir_build/src/builder/matches/match_pair.rs @@ -4,7 +4,7 @@ use rustc_abi::FieldIdx; use rustc_middle::mir::{Pinnedness, Place, PlaceElem, ProjectionElem}; use rustc_middle::span_bug; use rustc_middle::thir::{Ascription, DerefPatBorrowMode, FieldPat, Pat, PatKind}; -use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; +use rustc_middle::ty::{self, Ty, TypeVisitableExt}; use rustc_span::Span; use crate::builder::Builder; @@ -18,33 +18,6 @@ use crate::builder::matches::{ /// unlikely to be more expensive than a `PartialEq::eq` call. const AGGREGATE_EQ_MIN_LEN: usize = 4; -/// Checks whether every pattern in `elements` is a `PatKind::Constant` and, -/// if so, reconstructs a single aggregate `ty::Value` that represents the whole -/// array or slice. Returns `None` when any element is not a constant or the -/// sequence is too short to benefit from an aggregate comparison. -fn try_reconstruct_aggregate_constant<'tcx>( - tcx: TyCtxt<'tcx>, - aggregate_ty: Ty<'tcx>, - elements: &[Pat<'tcx>], -) -> Option> { - // Short arrays are not worth an aggregate comparison. - if elements.len() < AGGREGATE_EQ_MIN_LEN { - return None; - } - let branches = elements - .iter() - .map(|pat| { - if let PatKind::Constant { value } = pat.kind { - Some(ty::Const::new_value(tcx, value.valtree, value.ty)) - } else { - None - } - }) - .collect::>>()?; - let valtree = ty::ValTree::from_branches(tcx, branches); - Some(ty::Value { ty: aggregate_ty, valtree }) -} - impl<'a, 'tcx> Builder<'a, 'tcx> { /// Check if we can use aggregate `PartialEq::eq` comparisons for constant array/slice patterns. /// This is not possible in const contexts, because `PartialEq` is not const-stable yet. @@ -53,6 +26,25 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { || !self.tcx.hir_body_owner_kind(self.def_id).is_fn_or_closure(); !in_const_context } + + /// If the given array or slice pattern node was expanded from a constant + /// by `const_to_pat` and an aggregate comparison is both possible and + /// worthwhile, returns the original constant value, so that the scrutinee + /// can be compared against it as a whole via `PartialEq::eq`. + /// + /// Note that this deliberately does not apply to hand-written array or + /// slice patterns, which only ever match element by element. + fn aggregate_const_value( + &self, + pattern: &Pat<'tcx>, + element_count: usize, + ) -> Option> { + let value = pattern.extra.as_deref()?.expanded_const_value?; + if element_count < AGGREGATE_EQ_MIN_LEN || !self.can_use_aggregate_eq() { + return None; + } + Some(value) + } } /// For an array or slice pattern's subpatterns (prefix/slice/suffix), returns a list @@ -386,15 +378,11 @@ impl<'tcx> InterPat<'tcx> { _ => None, }; if let Some(array_len) = array_len { - // When all elements are constants and there is no `..` - // subpattern, compare the whole array at once via + // If this pattern was expanded from a constant, compare + // the whole array against that constant at once via // `PartialEq::eq` rather than element by element. - if slice.is_none() - && suffix.is_empty() - && cx.can_use_aggregate_eq() - && let Some(aggregate_value) = - try_reconstruct_aggregate_constant(cx.tcx, pattern.ty, prefix) - { + if let Some(aggregate_value) = cx.aggregate_const_value(pattern, prefix.len()) { + debug_assert!(slice.is_none() && suffix.is_empty()); Some(TestableCase::Constant { value: aggregate_value, kind: PatConstKind::Aggregate, @@ -425,16 +413,12 @@ impl<'tcx> InterPat<'tcx> { } } PatKind::Slice { ref prefix, ref slice, ref suffix } => { - // When there is no `..`, all elements are constants, and - // there are at least two of them, collapse the individual - // element subpairs into a single aggregate comparison that - // is performed after the length check. - if slice.is_none() - && suffix.is_empty() - && cx.can_use_aggregate_eq() - && let Some(aggregate_value) = - try_reconstruct_aggregate_constant(cx.tcx, pattern.ty, prefix) - { + // If this pattern was expanded from a constant, compare the + // whole slice against that constant at once via + // `PartialEq::eq` after the length check, rather than + // element by element. + if let Some(aggregate_value) = cx.aggregate_const_value(pattern, prefix.len()) { + debug_assert!(slice.is_none() && suffix.is_empty()); subpats.push(InterPat { place, testable_case: Some(TestableCase::Constant { diff --git a/compiler/rustc_mir_build/src/builder/matches/mod.rs b/compiler/rustc_mir_build/src/builder/matches/mod.rs index 7fcf53d4132b8..54b561bef93b0 100644 --- a/compiler/rustc_mir_build/src/builder/matches/mod.rs +++ b/compiler/rustc_mir_build/src/builder/matches/mod.rs @@ -1248,8 +1248,8 @@ enum PatConstKind { Float, /// Constant string values, tested via string equality. String, - /// Constant array or slice values where every element is a constant. - /// Tested by calling `PartialEq::eq` on the whole aggregate at once, + /// Constant array or slice values that array/slice patterns were expanded + /// from. Tested by calling `PartialEq::eq` on the whole aggregate at once, /// rather than comparing element by element. Aggregate, /// Any other constant-pattern is usually tested via some kind of equality diff --git a/compiler/rustc_mir_build/src/thir/pattern/const_to_pat.rs b/compiler/rustc_mir_build/src/thir/pattern/const_to_pat.rs index 143df22452987..3e1a25ef5f14e 100644 --- a/compiler/rustc_mir_build/src/thir/pattern/const_to_pat.rs +++ b/compiler/rustc_mir_build/src/thir/pattern/const_to_pat.rs @@ -473,7 +473,15 @@ impl<'tcx> ConstToPat<'tcx> { } }; - Box::new(Pat { span, ty, kind, extra: None }) + let mut pat = Box::new(Pat { span, ty, kind, extra: None }); + if matches!(ty.kind(), ty::Array(..) | ty::Slice(_)) { + // Record the original constant value on array and slice nodes, so + // that match lowering can compare the scrutinee against the whole + // constant at once via `PartialEq::eq`, rather than element by + // element. + pat.extra.get_or_insert_default().expanded_const_value = Some(value); + } + pat } } diff --git a/compiler/rustc_mir_build/src/thir/print.rs b/compiler/rustc_mir_build/src/thir/print.rs index ddb56a04c308d..6b51f862840b3 100644 --- a/compiler/rustc_mir_build/src/thir/print.rs +++ b/compiler/rustc_mir_build/src/thir/print.rs @@ -703,10 +703,15 @@ impl<'a, 'tcx> ThirPrinter<'a, 'tcx> { return; }; - let PatExtra { expanded_const, ascriptions } = extra; + let PatExtra { expanded_const, expanded_const_value, ascriptions } = extra; print_indented!(self, "extra: PatExtra {", depth_lvl); print_indented!(self, format_args!("expanded_const: {expanded_const:?}"), depth_lvl + 1); + print_indented!( + self, + format_args!("expanded_const_value: {expanded_const_value:?}"), + depth_lvl + 1 + ); self.print_list( "ascriptions", ascriptions, diff --git a/tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-abort.mir new file mode 100644 index 0000000000000..7f2e610b412a2 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-abort.mir @@ -0,0 +1,64 @@ +// MIR for `handwritten_array_match` after built + +fn handwritten_array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + switchInt(copy _1[0 of 4]) -> [1: bb2, otherwise: bb1]; + } + + bb1: { + _0 = const false; + goto -> bb12; + } + + bb2: { + switchInt(copy _1[1 of 4]) -> [2: bb4, otherwise: bb3]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(copy _1[2 of 4]) -> [3: bb6, otherwise: bb5]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(copy _1[3 of 4]) -> [4: bb8, otherwise: bb7]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + falseEdge -> [real: bb11, imaginary: bb1]; + } + + bb9: { + goto -> bb7; + } + + bb10: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb11: { + _0 = const true; + goto -> bb12; + } + + bb12: { + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..7f2e610b412a2 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.handwritten_array_match.built.after.panic-unwind.mir @@ -0,0 +1,64 @@ +// MIR for `handwritten_array_match` after built + +fn handwritten_array_match(_1: [u8; 4]) -> bool { + debug x => _1; + let mut _0: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + switchInt(copy _1[0 of 4]) -> [1: bb2, otherwise: bb1]; + } + + bb1: { + _0 = const false; + goto -> bb12; + } + + bb2: { + switchInt(copy _1[1 of 4]) -> [2: bb4, otherwise: bb3]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(copy _1[2 of 4]) -> [3: bb6, otherwise: bb5]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(copy _1[3 of 4]) -> [4: bb8, otherwise: bb7]; + } + + bb7: { + goto -> bb5; + } + + bb8: { + falseEdge -> [real: bb11, imaginary: bb1]; + } + + bb9: { + goto -> bb7; + } + + bb10: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb11: { + _0 = const true; + goto -> bb12; + } + + bb12: { + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.rs b/tests/mir-opt/building/match/aggregate_array_eq.rs index 94c857458f9ee..8d1bb05b51cca 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.rs +++ b/tests/mir-opt/building/match/aggregate_array_eq.rs @@ -1,8 +1,14 @@ // EMIT_MIR_FOR_EACH_PANIC_STRATEGY //@ compile-flags: -Zmir-opt-level=0 -// Verify that matching against a constant array pattern produces a single +// Verify that matching against an array/slice pattern that was expanded from +// a constant (a named constant or a byte-string literal) produces a single // `PartialEq::eq` call rather than element-by-element comparisons. +// +// Hand-written array patterns must keep the element-by-element comparisons: +// they only borrow the scrutinee for as long as `PartialEq::eq` would, but +// user intent is respected before the MIR boundary. +// // In const contexts, the aggregate comparison must NOT be used because // `PartialEq` is not const-stable. @@ -13,9 +19,25 @@ pub fn array_match(x: [u8; 4]) -> bool { // CHECK-LABEL: fn array_match( // CHECK: <[u8; 4] as PartialEq>::eq // CHECK-NOT: switchInt(copy _1[ + const EXPECTED: [u8; 4] = [1, 2, 3, 4]; + matches!(x, EXPECTED) +} + +// EMIT_MIR aggregate_array_eq.handwritten_array_match.built.after.mir +pub fn handwritten_array_match(x: [u8; 4]) -> bool { + // CHECK-LABEL: fn handwritten_array_match( + // CHECK-NOT: PartialEq + // CHECK: switchInt matches!(x, [1, 2, 3, 4]) } +// EMIT_MIR aggregate_array_eq.slice_match.built.after.mir +pub fn slice_match(x: &[u8]) -> bool { + // CHECK-LABEL: fn slice_match( + // CHECK: <[u8] as PartialEq>::eq + matches!(x, b"ABCD") +} + pub enum MyEnum { A, B, @@ -45,7 +67,8 @@ pub const fn const_array_match(x: [u8; 4]) -> bool { // CHECK-LABEL: fn const_array_match( // CHECK-NOT: PartialEq // CHECK: switchInt - matches!(x, [1, 2, 3, 4]) + const EXPECTED: [u8; 4] = [1, 2, 3, 4]; + matches!(x, EXPECTED) } // EMIT_MIR aggregate_array_eq.const_try_from_matched.built.after.mir diff --git a/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir new file mode 100644 index 0000000000000..cc0bb7775b7d2 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir @@ -0,0 +1,67 @@ +// MIR for `slice_match` after built + +fn slice_match(_1: &[u8]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[u8]; + let mut _3: bool; + let mut _4: usize; + let mut _5: usize; + let mut _6: usize; + let mut _7: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _5 = PtrMetadata(copy _1); + _4 = move _5; + _6 = const 4_usize; + _7 = Eq(move _4, move _6); + switchInt(move _7) -> [0: bb1, otherwise: bb2]; + } + + bb1: { + _0 = const false; + goto -> bb9; + } + + bb2: { + _2 = &(*_1); + _3 = <[u8] as PartialEq>::eq(copy _2, const b"ABCD") -> [return: bb6, unwind: bb10]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + falseEdge -> [real: bb8, imaginary: bb1]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(move _3) -> [0: bb3, otherwise: bb4]; + } + + bb7: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb8: { + _0 = const true; + goto -> bb9; + } + + bb9: { + return; + } + + bb10 (cleanup): { + resume; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..cc0bb7775b7d2 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir @@ -0,0 +1,67 @@ +// MIR for `slice_match` after built + +fn slice_match(_1: &[u8]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[u8]; + let mut _3: bool; + let mut _4: usize; + let mut _5: usize; + let mut _6: usize; + let mut _7: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _5 = PtrMetadata(copy _1); + _4 = move _5; + _6 = const 4_usize; + _7 = Eq(move _4, move _6); + switchInt(move _7) -> [0: bb1, otherwise: bb2]; + } + + bb1: { + _0 = const false; + goto -> bb9; + } + + bb2: { + _2 = &(*_1); + _3 = <[u8] as PartialEq>::eq(copy _2, const b"ABCD") -> [return: bb6, unwind: bb10]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + falseEdge -> [real: bb8, imaginary: bb1]; + } + + bb5: { + goto -> bb3; + } + + bb6: { + switchInt(move _3) -> [0: bb3, otherwise: bb4]; + } + + bb7: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb8: { + _0 = const true; + goto -> bb9; + } + + bb9: { + return; + } + + bb10 (cleanup): { + resume; + } +} diff --git a/tests/ui/match/aggregate-array-eq.rs b/tests/ui/match/aggregate-array-eq.rs index f5234e08b4f52..95a01d401ef6b 100644 --- a/tests/ui/match/aggregate-array-eq.rs +++ b/tests/ui/match/aggregate-array-eq.rs @@ -1,16 +1,33 @@ -//! Verify that matching against a constant array pattern produces correct -//! results at runtime, complementing the MIR test in +//! Verify that matching against array/slice patterns expanded from constants +//! produces correct results at runtime, complementing the MIR test in //! `tests/mir-opt/building/match/aggregate_array_eq.rs` which checks that //! a single aggregate `PartialEq::eq` call is emitted. //! -//! Also verify that const-context variants (which fall back to -//! element-by-element comparison) produce the same results. +//! Also verify that the variants which fall back to element-by-element +//! comparison (hand-written patterns and const contexts) produce the same +//! results. //@ run-pass +const EXPECTED: [u8; 4] = [1, 2, 3, 4]; + fn array_match(x: [u8; 4]) -> bool { + matches!(x, EXPECTED) +} + +fn handwritten_array_match(x: [u8; 4]) -> bool { matches!(x, [1, 2, 3, 4]) } +fn slice_match(x: &[u8]) -> bool { + matches!(x, b"ABCD") +} + +const NESTED: [[u8; 4]; 4] = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]; + +fn nested_array_match(x: [[u8; 4]; 4]) -> bool { + matches!(x, NESTED) +} + #[derive(Debug, PartialEq)] enum MyEnum { A, @@ -33,7 +50,7 @@ fn try_from_matched(value: [u8; 4]) -> Result { // Const fn variants use element-by-element comparison because // `PartialEq::eq` is not available in const contexts. const fn const_array_match(x: [u8; 4]) -> bool { - matches!(x, [1, 2, 3, 4]) + matches!(x, EXPECTED) } const fn const_try_from_matched(value: [u8; 4]) -> Result { @@ -52,6 +69,19 @@ fn main() { assert!(!array_match([0, 0, 0, 0])); assert!(!array_match([4, 3, 2, 1])); + assert!(handwritten_array_match([1, 2, 3, 4])); + assert!(!handwritten_array_match([1, 2, 3, 5])); + + assert!(slice_match(b"ABCD")); + assert!(!slice_match(b"ABCE")); + assert!(!slice_match(b"ABC")); + assert!(!slice_match(b"ABCDE")); + assert!(!slice_match(b"")); + + assert!(nested_array_match(NESTED)); + assert!(!nested_array_match([[0; 4]; 4])); + assert!(!nested_array_match([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 0]])); + assert_eq!(try_from_matched(*b"ABCD"), Ok(MyEnum::A)); assert_eq!(try_from_matched(*b"EFGH"), Ok(MyEnum::B)); assert_eq!(try_from_matched(*b"IJKL"), Ok(MyEnum::C)); diff --git a/tests/ui/thir-print/str-patterns.stdout b/tests/ui/thir-print/str-patterns.stdout index da1f86b8fc591..93f92e2ca0be9 100644 --- a/tests/ui/thir-print/str-patterns.stdout +++ b/tests/ui/thir-print/str-patterns.stdout @@ -48,6 +48,7 @@ Thir { expanded_const: Some( DefId(0:4 ~ str_patterns[fc71]::CONSTANT), ), + expanded_const_value: None, ascriptions: [], }, ), diff --git a/tests/ui/thir-print/thir-tree-array-index.stdout b/tests/ui/thir-print/thir-tree-array-index.stdout index 4e40bcbbf4e07..b167169ca87fa 100644 --- a/tests/ui/thir-print/thir-tree-array-index.stdout +++ b/tests/ui/thir-print/thir-tree-array-index.stdout @@ -129,6 +129,7 @@ body: span: $DIR/thir-tree-array-index.rs:7:7: 7:9 (#0) extra: PatExtra { expanded_const: None + expanded_const_value: None ascriptions: [ Ascription { annotation: CanonicalUserTypeAnnotation { user_ty: Canonical { value: UserType { kind: Ty([usize; 5_usize]), bounds: [] }, max_universe: U0, var_kinds: [] }, span: $DIR/thir-tree-array-index.rs:7:11: 7:21 (#0), inferred_ty: [usize; 5_usize] }, variance: + } ] @@ -279,6 +280,7 @@ body: span: $DIR/thir-tree-array-index.rs:8:7: 8:9 (#0) extra: PatExtra { expanded_const: None + expanded_const_value: None ascriptions: [ Ascription { annotation: CanonicalUserTypeAnnotation { user_ty: Canonical { value: UserType { kind: Ty([usize; 5_usize]), bounds: [] }, max_universe: U0, var_kinds: [] }, span: $DIR/thir-tree-array-index.rs:8:11: 8:21 (#0), inferred_ty: [usize; 5_usize] }, variance: + } ] diff --git a/tests/ui/thir-print/thir-tree-match-for.stdout b/tests/ui/thir-print/thir-tree-match-for.stdout index ac71f6a1d1f07..9f0b5918255e0 100644 --- a/tests/ui/thir-print/thir-tree-match-for.stdout +++ b/tests/ui/thir-print/thir-tree-match-for.stdout @@ -171,6 +171,7 @@ body: span: $DIR/thir-tree-match-for.rs:10:5: 10:9 (#0) extra: PatExtra { expanded_const: None + expanded_const_value: None ascriptions: [] } kind: PatKind { From f89a54e4b6c1228f793dd5ef4cb5ed3c9cdfcb7d Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sat, 11 Jul 2026 20:46:12 +0100 Subject: [PATCH 12/14] Assert that the aggregate equality comparison cannot unwind Give the `PartialEq::eq` call emitted for constant array/slice patterns `UnwindAction::Unreachable` instead of an unwind edge. The built-in `PartialEq` implementations for arrays and slices can be trusted not to panic, and the unwind edge would not be harmless: since the aggregate comparison replaces a series of `SwitchInt` tests that could never unwind, the extra edge would make borrow-checking stricter about the drop order in unwinding code, turning previously accepted programs into errors. The string equality tests keep their unwind edge, as they have always had one. --- .../src/builder/matches/test.rs | 23 +++++++++++-- ...eq.array_match.built.after.panic-abort.mir | 6 +--- ...q.array_match.built.after.panic-unwind.mir | 6 +--- .../building/match/aggregate_array_eq.rs | 5 ++- ...eq.slice_match.built.after.panic-abort.mir | 6 +--- ...q.slice_match.built.after.panic-unwind.mir | 6 +--- ...y_from_matched.built.after.panic-abort.mir | 12 +++---- ..._from_matched.built.after.panic-unwind.mir | 12 +++---- .../aggregate-array-eq-guard-drop-order.rs | 32 +++++++++++++++++++ 9 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 tests/ui/match/aggregate-array-eq-guard-drop-order.rs diff --git a/compiler/rustc_mir_build/src/builder/matches/test.rs b/compiler/rustc_mir_build/src/builder/matches/test.rs index 418c2163be9e4..e3d2c8703997d 100644 --- a/compiler/rustc_mir_build/src/builder/matches/test.rs +++ b/compiler/rustc_mir_build/src/builder/matches/test.rs @@ -176,6 +176,14 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { // Compare the two values using `::eq`. // (Interestingly this means that, for `str`, exhaustiveness analysis // relies for soundness on the `PartialEq` impl for `str` to be correct!) + // + // The aggregate comparisons call the built-in `PartialEq` impls for + // arrays and slices, which can be trusted not to panic, so they are + // asserted not to unwind. An unwind edge here would be a breaking + // change: string equality tests have always had one, but the + // aggregate tests replace a series of `SwitchInt`s that never could + // unwind, and the extra edge would make borrow-checking stricter. + let can_unwind = matches!(test.kind, TestKind::StringEq { .. }); self.non_scalar_compare( block, success_block, @@ -184,6 +192,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { inner_ty, expected_value_operand, Operand::Copy(actual_value_ref_place), + can_unwind, ); } @@ -422,6 +431,13 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { /// /// `compared_ty` is the *inner* type (e.g. `str`, `[u8; 64]`); /// `expect` and `val` must already be references to that type. + /// + /// When `can_unwind` is false, the call is given `UnwindAction::Unreachable` + /// and no unwind edge, asserting that the `PartialEq::eq` implementation + /// cannot panic. This matters beyond codegen: an unwinding call would make + /// borrow-checking of the surrounding match stricter, because the unwind + /// path can create drop-order conflicts that the ordinary path does not + /// have. fn non_scalar_compare( &mut self, block: BasicBlock, @@ -431,6 +447,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { compared_ty: Ty<'tcx>, expect: Operand<'tcx>, val: Operand<'tcx>, + can_unwind: bool, ) { let eq_def_id = self.tcx.require_lang_item(LangItem::PartialEq, source_info.span); let method = @@ -461,12 +478,14 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { .into(), destination: eq_result, target: Some(eq_block), - unwind: UnwindAction::Continue, + unwind: if can_unwind { UnwindAction::Continue } else { UnwindAction::Unreachable }, call_source: CallSource::MatchCmp, fn_span: source_info.span, }, ); - self.diverge_from(block); + if can_unwind { + self.diverge_from(block); + } // check the result self.cfg.terminate( diff --git a/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir index 85778f3bee8cb..97ca0c11c39a5 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir +++ b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-abort.mir @@ -11,7 +11,7 @@ fn array_match(_1: [u8; 4]) -> bool { bb0: { PlaceMention(_1); _2 = &_1; - _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; + _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind unreachable]; } bb1: { @@ -44,8 +44,4 @@ fn array_match(_1: [u8; 4]) -> bool { bb7: { return; } - - bb8 (cleanup): { - resume; - } } diff --git a/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir index 85778f3bee8cb..97ca0c11c39a5 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir +++ b/tests/mir-opt/building/match/aggregate_array_eq.array_match.built.after.panic-unwind.mir @@ -11,7 +11,7 @@ fn array_match(_1: [u8; 4]) -> bool { bb0: { PlaceMention(_1); _2 = &_1; - _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind: bb8]; + _3 = <[u8; 4] as PartialEq>::eq(copy _2, const &*b"\x01\x02\x03\x04") -> [return: bb4, unwind unreachable]; } bb1: { @@ -44,8 +44,4 @@ fn array_match(_1: [u8; 4]) -> bool { bb7: { return; } - - bb8 (cleanup): { - resume; - } } diff --git a/tests/mir-opt/building/match/aggregate_array_eq.rs b/tests/mir-opt/building/match/aggregate_array_eq.rs index 8d1bb05b51cca..0f3b766f8b101 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.rs +++ b/tests/mir-opt/building/match/aggregate_array_eq.rs @@ -3,7 +3,8 @@ // Verify that matching against an array/slice pattern that was expanded from // a constant (a named constant or a byte-string literal) produces a single -// `PartialEq::eq` call rather than element-by-element comparisons. +// `PartialEq::eq` call rather than element-by-element comparisons. The call +// must be marked as non-unwinding. // // Hand-written array patterns must keep the element-by-element comparisons: // they only borrow the scrutinee for as long as `PartialEq::eq` would, but @@ -18,6 +19,7 @@ pub fn array_match(x: [u8; 4]) -> bool { // CHECK-LABEL: fn array_match( // CHECK: <[u8; 4] as PartialEq>::eq + // CHECK-SAME: unwind unreachable // CHECK-NOT: switchInt(copy _1[ const EXPECTED: [u8; 4] = [1, 2, 3, 4]; matches!(x, EXPECTED) @@ -35,6 +37,7 @@ pub fn handwritten_array_match(x: [u8; 4]) -> bool { pub fn slice_match(x: &[u8]) -> bool { // CHECK-LABEL: fn slice_match( // CHECK: <[u8] as PartialEq>::eq + // CHECK-SAME: unwind unreachable matches!(x, b"ABCD") } diff --git a/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir index cc0bb7775b7d2..fbae5ccbc2e87 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir +++ b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-abort.mir @@ -28,7 +28,7 @@ fn slice_match(_1: &[u8]) -> bool { bb2: { _2 = &(*_1); - _3 = <[u8] as PartialEq>::eq(copy _2, const b"ABCD") -> [return: bb6, unwind: bb10]; + _3 = <[u8] as PartialEq>::eq(copy _2, const b"ABCD") -> [return: bb6, unwind unreachable]; } bb3: { @@ -60,8 +60,4 @@ fn slice_match(_1: &[u8]) -> bool { bb9: { return; } - - bb10 (cleanup): { - resume; - } } diff --git a/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir index cc0bb7775b7d2..fbae5ccbc2e87 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir +++ b/tests/mir-opt/building/match/aggregate_array_eq.slice_match.built.after.panic-unwind.mir @@ -28,7 +28,7 @@ fn slice_match(_1: &[u8]) -> bool { bb2: { _2 = &(*_1); - _3 = <[u8] as PartialEq>::eq(copy _2, const b"ABCD") -> [return: bb6, unwind: bb10]; + _3 = <[u8] as PartialEq>::eq(copy _2, const b"ABCD") -> [return: bb6, unwind unreachable]; } bb3: { @@ -60,8 +60,4 @@ fn slice_match(_1: &[u8]) -> bool { bb9: { return; } - - bb10 (cleanup): { - resume; - } } diff --git a/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir index 008691c44deef..660dab2044a65 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir +++ b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-abort.mir @@ -23,7 +23,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { _2 = &_1; PlaceMention(_2); _9 = &(*_2); - _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; + _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind unreachable]; } bb1: { @@ -44,7 +44,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { bb4: { _7 = &(*_2); - _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; + _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind unreachable]; } bb5: { @@ -61,7 +61,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { bb8: { _5 = &(*_2); - _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; + _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind unreachable]; } bb9: { @@ -78,7 +78,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { bb12: { _3 = &(*_2); - _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; + _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind unreachable]; } bb13: { @@ -150,8 +150,4 @@ fn try_from_matched(_1: [u8; 4]) -> Result { StorageDead(_2); return; } - - bb26 (cleanup): { - resume; - } } diff --git a/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir index 008691c44deef..660dab2044a65 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir +++ b/tests/mir-opt/building/match/aggregate_array_eq.try_from_matched.built.after.panic-unwind.mir @@ -23,7 +23,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { _2 = &_1; PlaceMention(_2); _9 = &(*_2); - _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind: bb26]; + _10 = <[u8; 4] as PartialEq>::eq(copy _9, const &*b"ABCD") -> [return: bb19, unwind unreachable]; } bb1: { @@ -44,7 +44,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { bb4: { _7 = &(*_2); - _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind: bb26]; + _8 = <[u8; 4] as PartialEq>::eq(copy _7, const &*b"EFGH") -> [return: bb18, unwind unreachable]; } bb5: { @@ -61,7 +61,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { bb8: { _5 = &(*_2); - _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind: bb26]; + _6 = <[u8; 4] as PartialEq>::eq(copy _5, const &*b"IJKL") -> [return: bb17, unwind unreachable]; } bb9: { @@ -78,7 +78,7 @@ fn try_from_matched(_1: [u8; 4]) -> Result { bb12: { _3 = &(*_2); - _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind: bb26]; + _4 = <[u8; 4] as PartialEq>::eq(copy _3, const &*b"MNOP") -> [return: bb16, unwind unreachable]; } bb13: { @@ -150,8 +150,4 @@ fn try_from_matched(_1: [u8; 4]) -> Result { StorageDead(_2); return; } - - bb26 (cleanup): { - resume; - } } diff --git a/tests/ui/match/aggregate-array-eq-guard-drop-order.rs b/tests/ui/match/aggregate-array-eq-guard-drop-order.rs new file mode 100644 index 0000000000000..df7b46360c46e --- /dev/null +++ b/tests/ui/match/aggregate-array-eq-guard-drop-order.rs @@ -0,0 +1,32 @@ +//! The aggregate `PartialEq::eq` comparison emitted for constant array/slice +//! patterns must be marked as non-unwinding. If it could unwind, this program +//! would fail borrow-checking in edition 2021: the unwind path from the guard +//! would require the scrutinee temporary, which borrows `referent`, to be +//! dropped in a different order relative to `referent` than on the ordinary +//! path. An explicit `slice == b"ABCD"` guard, which is an unwinding call, +//! still errors here. +//@ check-pass +//@ edition: 2021 + +struct Referent; +impl Drop for Referent { + fn drop(&mut self) {} +} + +struct DropMeFirst<'a>(&'a Referent); +impl Drop for DropMeFirst<'_> { + fn drop(&mut self) {} +} + +fn foo(slice: &[u8]) -> u32 { + let referent = Referent; + match DropMeFirst(&referent) { + _dropped_first if matches!(slice, b"ABCD") => 0, + _dropped_first => 1, + } +} + +fn main() { + assert_eq!(foo(b"ABCD"), 0); + assert_eq!(foo(b"ZZZZ"), 1); +} From c02f65809f08f3a386f0dfe0ac2c26ca0dfc4e7e Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 2 Aug 2026 12:37:58 +0100 Subject: [PATCH 13/14] Add a MIR test for a constant array with a custom element type --- ...nt_array_match.built.after.panic-abort.mir | 47 +++++++++++++++++++ ...t_array_match.built.after.panic-unwind.mir | 47 +++++++++++++++++++ .../building/match/aggregate_array_eq.rs | 16 +++++++ 3 files changed, 110 insertions(+) create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-abort.mir create mode 100644 tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-unwind.mir diff --git a/tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-abort.mir b/tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-abort.mir new file mode 100644 index 0000000000000..4a4d9fe633ce9 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-abort.mir @@ -0,0 +1,47 @@ +// MIR for `custom_element_array_match` after built + +fn custom_element_array_match(_1: [Element; 4]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[Element; 4]; + let mut _3: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _2 = &_1; + _3 = <[Element; 4] as PartialEq>::eq(copy _2, const &[Element(1), Element(2), Element(3), Element(4)]) -> [return: bb4, unwind unreachable]; + } + + bb1: { + _0 = const false; + goto -> bb7; + } + + bb2: { + falseEdge -> [real: bb6, imaginary: bb1]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(move _3) -> [0: bb1, otherwise: bb2]; + } + + bb5: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb6: { + _0 = const true; + goto -> bb7; + } + + bb7: { + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-unwind.mir b/tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-unwind.mir new file mode 100644 index 0000000000000..4a4d9fe633ce9 --- /dev/null +++ b/tests/mir-opt/building/match/aggregate_array_eq.custom_element_array_match.built.after.panic-unwind.mir @@ -0,0 +1,47 @@ +// MIR for `custom_element_array_match` after built + +fn custom_element_array_match(_1: [Element; 4]) -> bool { + debug x => _1; + let mut _0: bool; + let mut _2: &[Element; 4]; + let mut _3: bool; + scope 1 { + } + + bb0: { + PlaceMention(_1); + _2 = &_1; + _3 = <[Element; 4] as PartialEq>::eq(copy _2, const &[Element(1), Element(2), Element(3), Element(4)]) -> [return: bb4, unwind unreachable]; + } + + bb1: { + _0 = const false; + goto -> bb7; + } + + bb2: { + falseEdge -> [real: bb6, imaginary: bb1]; + } + + bb3: { + goto -> bb1; + } + + bb4: { + switchInt(move _3) -> [0: bb1, otherwise: bb2]; + } + + bb5: { + FakeRead(ForMatchedPlace(None), _1); + unreachable; + } + + bb6: { + _0 = const true; + goto -> bb7; + } + + bb7: { + return; + } +} diff --git a/tests/mir-opt/building/match/aggregate_array_eq.rs b/tests/mir-opt/building/match/aggregate_array_eq.rs index 0f3b766f8b101..f67456868cea0 100644 --- a/tests/mir-opt/building/match/aggregate_array_eq.rs +++ b/tests/mir-opt/building/match/aggregate_array_eq.rs @@ -33,6 +33,22 @@ pub fn handwritten_array_match(x: [u8; 4]) -> bool { matches!(x, [1, 2, 3, 4]) } +#[derive(PartialEq, Eq)] +pub struct Element(u8); + +// The element type does not have to be a primitive: the aggregate comparison +// calls `<[Element; 4] as PartialEq>::eq`, which in turn calls the derived +// `PartialEq` implementation for `Element`. +// EMIT_MIR aggregate_array_eq.custom_element_array_match.built.after.mir +pub fn custom_element_array_match(x: [Element; 4]) -> bool { + // CHECK-LABEL: fn custom_element_array_match( + // CHECK: <[Element; 4] as PartialEq>::eq + // CHECK-SAME: unwind unreachable + // CHECK-NOT: switchInt(copy _1[ + const EXPECTED: [Element; 4] = [Element(1), Element(2), Element(3), Element(4)]; + matches!(x, EXPECTED) +} + // EMIT_MIR aggregate_array_eq.slice_match.built.after.mir pub fn slice_match(x: &[u8]) -> bool { // CHECK-LABEL: fn slice_match( From 2ea4f271f66986ec8fe931117ea4073762509748 Mon Sep 17 00:00:00 2001 From: Jacob Adam Date: Sun, 2 Aug 2026 22:47:38 +0100 Subject: [PATCH 14/14] Expand on why the aggregate comparison is assumed not to unwind --- compiler/rustc_mir_build/src/builder/matches/test.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_mir_build/src/builder/matches/test.rs b/compiler/rustc_mir_build/src/builder/matches/test.rs index e3d2c8703997d..21668df260b5f 100644 --- a/compiler/rustc_mir_build/src/builder/matches/test.rs +++ b/compiler/rustc_mir_build/src/builder/matches/test.rs @@ -177,12 +177,12 @@ impl<'a, 'tcx> Builder<'a, 'tcx> { // (Interestingly this means that, for `str`, exhaustiveness analysis // relies for soundness on the `PartialEq` impl for `str` to be correct!) // - // The aggregate comparisons call the built-in `PartialEq` impls for - // arrays and slices, which can be trusted not to panic, so they are - // asserted not to unwind. An unwind edge here would be a breaking - // change: string equality tests have always had one, but the - // aggregate tests replace a series of `SwitchInt`s that never could - // unwind, and the extra edge would make borrow-checking stricter. + // The aggregate comparisons, unlike the long-standing string ones, are + // asserted not to unwind, since an unwind edge would make + // borrow-checking stricter than for the `SwitchInt`s they replace. + // That is sound because a constant is only allowed in a pattern if its + // type is structural match, so the array/slice impl and every element + // impl it delegates to are derived or primitive, and cannot panic. let can_unwind = matches!(test.kind, TestKind::StringEq { .. }); self.non_scalar_compare( block,