Skip to content

deref_patterns: let string and byte string literal patterns peel references and smart pointers before matching #140658

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 101 additions & 39 deletions compiler/rustc_hir_typeck/src/pat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,16 +177,20 @@ enum PeelKind {
/// Only peel reference types. This is used for explicit `deref!(_)` patterns, which dereference
/// any number of `&`/`&mut` references, plus a single smart pointer.
ExplicitDerefPat,
/// Implicitly peel any number of references, and if `deref_patterns` is enabled, smart pointer
/// ADTs. In order to peel only as much as necessary for the pattern to match, the `until_adt`
/// field contains the ADT def that the pattern is a constructor for, if applicable, so that we
/// don't peel it. See [`ResolvedPat`] for more information.
Implicit { until_adt: Option<DefId> },
/// Implicitly peel references, and if `deref_patterns` is enabled, smart pointer ADTs.
Implicit {
/// The ADT the pattern is a constructor for, if applicable, so that we don't peel it. See
/// [`ResolvedPat`] for more information.
until_adt: Option<DefId>,
/// The number of references at the head of the pattern's type, so we can leave that many
/// untouched. This is `1` for string literals, and `0` for most patterns.
pat_ref_layers: usize,
},
}

impl AdjustMode {
const fn peel_until_adt(opt_adt_def: Option<DefId>) -> AdjustMode {
AdjustMode::Peel { kind: PeelKind::Implicit { until_adt: opt_adt_def } }
AdjustMode::Peel { kind: PeelKind::Implicit { until_adt: opt_adt_def, pat_ref_layers: 0 } }
}
const fn peel_all() -> AdjustMode {
AdjustMode::peel_until_adt(None)
Expand Down Expand Up @@ -488,9 +492,10 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
match pat.kind {
// Peel off a `&` or `&mut` from the scrutinee type. See the examples in
// `tests/ui/rfcs/rfc-2005-default-binding-mode`.
_ if let AdjustMode::Peel { .. } = adjust_mode
_ if let AdjustMode::Peel { kind: peel_kind } = adjust_mode
&& pat.default_binding_modes
&& let ty::Ref(_, inner_ty, inner_mutability) = *expected.kind() =>
&& let ty::Ref(_, inner_ty, inner_mutability) = *expected.kind()
&& self.should_peel_ref(peel_kind, expected) =>
{
debug!("inspecting {:?}", expected);

Expand Down Expand Up @@ -531,24 +536,9 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
// If `deref_patterns` is enabled, peel a smart pointer from the scrutinee type. See the
// examples in `tests/ui/pattern/deref_patterns/`.
_ if self.tcx.features().deref_patterns()
&& let AdjustMode::Peel { kind: PeelKind::Implicit { until_adt } } = adjust_mode
&& let AdjustMode::Peel { kind: peel_kind } = adjust_mode
&& pat.default_binding_modes
// For simplicity, only apply overloaded derefs if `expected` is a known ADT.
// FIXME(deref_patterns): we'll get better diagnostics for users trying to
// implicitly deref generics if we allow them here, but primitives, tuples, and
// inference vars definitely should be stopped. Figure out what makes most sense.
&& let ty::Adt(scrutinee_adt, _) = *expected.kind()
// Don't peel if the pattern type already matches the scrutinee. E.g., stop here if
// matching on a `Cow<'a, T>` scrutinee with a `Cow::Owned(_)` pattern.
&& until_adt != Some(scrutinee_adt.did())
// At this point, the pattern isn't able to match `expected` without peeling. Check
// that it implements `Deref` before assuming it's a smart pointer, to get a normal
// type error instead of a missing impl error if not. This only checks for `Deref`,
// not `DerefPure`: we require that too, but we want a trait error if it's missing.
&& let Some(deref_trait) = self.tcx.lang_items().deref_trait()
&& self
.type_implements_trait(deref_trait, [expected], self.param_env)
.may_apply() =>
&& self.should_peel_smart_pointer(peel_kind, expected) =>
{
debug!("scrutinee ty {expected:?} is a smart pointer, inserting overloaded deref");
// The scrutinee is a smart pointer; implicitly dereference it. This adds a
Expand Down Expand Up @@ -680,21 +670,32 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {

// String and byte-string literals result in types `&str` and `&[u8]` respectively.
// All other literals result in non-reference types.
// As a result, we allow `if let 0 = &&0 {}` but not `if let "foo" = &&"foo" {}`.
//
// Call `resolve_vars_if_possible` here for inline const blocks.
PatKind::Expr(lt) => match self.resolve_vars_if_possible(self.check_pat_expr_unadjusted(lt)).kind() {
ty::Ref(..) => AdjustMode::Pass,
_ => {
// Path patterns have already been handled, and inline const blocks currently
// aren't possible to write, so any handling for them would be untested.
if cfg!(debug_assertions)
&& self.tcx.features().deref_patterns()
&& !matches!(lt.kind, PatExprKind::Lit { .. })
{
span_bug!(lt.span, "FIXME(deref_patterns): adjust mode unimplemented for {:?}", lt.kind);
// As a result, we allow `if let 0 = &&0 {}` but not `if let "foo" = &&"foo" {}` unless
// `deref_patterns` is enabled.
PatKind::Expr(lt) => {
// Path patterns have already been handled, and inline const blocks currently
// aren't possible to write, so any handling for them would be untested.
if cfg!(debug_assertions)
&& self.tcx.features().deref_patterns()
&& !matches!(lt.kind, PatExprKind::Lit { .. })
{
span_bug!(lt.span, "FIXME(deref_patterns): adjust mode unimplemented for {:?}", lt.kind);
}
// Call `resolve_vars_if_possible` here for inline const blocks.
let lit_ty = self.resolve_vars_if_possible(self.check_pat_expr_unadjusted(lt));
// If `deref_patterns` is enabled, allow `if let "foo" = &&"foo" {}`.
if self.tcx.features().deref_patterns() {
let mut peeled_ty = lit_ty;
let mut pat_ref_layers = 0;
while let ty::Ref(_, inner_ty, mutbl) = *peeled_ty.kind() {
// We rely on references at the head of constants being immutable.
debug_assert!(mutbl.is_not());
pat_ref_layers += 1;
peeled_ty = inner_ty;
}
AdjustMode::peel_all()
AdjustMode::Peel { kind: PeelKind::Implicit { until_adt: None, pat_ref_layers } }
} else {
if lit_ty.is_ref() { AdjustMode::Pass } else { AdjustMode::peel_all() }
}
},

Expand All @@ -720,6 +721,67 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
}
}

/// Assuming `expected` is a reference type, determine whether to peel it before matching.
fn should_peel_ref(&self, peel_kind: PeelKind, mut expected: Ty<'tcx>) -> bool {
debug_assert!(expected.is_ref());
let pat_ref_layers = match peel_kind {
PeelKind::ExplicitDerefPat => 0,
PeelKind::Implicit { pat_ref_layers, .. } => pat_ref_layers,
};

// Most patterns don't have reference types, so we'll want to peel all references from the
// scrutinee before matching. To optimize for the common case, return early.
if pat_ref_layers == 0 {
return true;
}
debug_assert!(
self.tcx.features().deref_patterns(),
"Peeling for patterns with reference types is gated by `deref_patterns`."
);

// If the pattern has as many or more layers of reference as the expected type, we can match
// without peeling more, unless we find a smart pointer or `&mut` that we also need to peel.
// We don't treat `&` and `&mut` as interchangeable, but by peeling `&mut`s before matching,
// we can still, e.g., match on a `&mut str` with a string literal pattern. This is because
// string literal patterns may be used where `str` is expected.
let mut expected_ref_layers = 0;
while let ty::Ref(_, inner_ty, mutbl) = *expected.kind() {
if mutbl.is_mut() {
// Mutable references can't be in the final value of constants, thus they can't be
// at the head of their types, thus we should always peel `&mut`.
return true;
}
expected_ref_layers += 1;
expected = inner_ty;
}
pat_ref_layers < expected_ref_layers || self.should_peel_smart_pointer(peel_kind, expected)
}

/// Determine whether `expected` is a smart pointer type that should be peeled before matching.
fn should_peel_smart_pointer(&self, peel_kind: PeelKind, expected: Ty<'tcx>) -> bool {
// Explicit `deref!(_)` patterns match against smart pointers; don't peel in that case.
if let PeelKind::Implicit { until_adt, .. } = peel_kind
// For simplicity, only apply overloaded derefs if `expected` is a known ADT.
// FIXME(deref_patterns): we'll get better diagnostics for users trying to
// implicitly deref generics if we allow them here, but primitives, tuples, and
// inference vars definitely should be stopped. Figure out what makes most sense.
&& let ty::Adt(scrutinee_adt, _) = *expected.kind()
// Don't peel if the pattern type already matches the scrutinee. E.g., stop here if
// matching on a `Cow<'a, T>` scrutinee with a `Cow::Owned(_)` pattern.
&& until_adt != Some(scrutinee_adt.did())
// At this point, the pattern isn't able to match `expected` without peeling. Check
// that it implements `Deref` before assuming it's a smart pointer, to get a normal
// type error instead of a missing impl error if not. This only checks for `Deref`,
// not `DerefPure`: we require that too, but we want a trait error if it's missing.
&& let Some(deref_trait) = self.tcx.lang_items().deref_trait()
&& self.type_implements_trait(deref_trait, [expected], self.param_env).may_apply()
{
true
} else {
false
}
}

fn check_pat_expr_unadjusted(&self, lt: &'tcx hir::PatExpr<'tcx>) -> Ty<'tcx> {
let ty = match &lt.kind {
rustc_hir::PatExprKind::Lit { lit, negated } => {
Expand Down
31 changes: 24 additions & 7 deletions src/doc/unstable-book/src/language-features/deref-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,26 @@ let deref!(x) = Box::new(NoCopy) else { unreachable!() };
drop::<NoCopy>(x);
```

Additionally, when `deref_patterns` is enabled, string literal patterns may be written where `str`
is expected. Likewise, byte string literal patterns may be written where `[u8]` or `[u8; _]` is
expected. This lets them be used in `deref!(_)` patterns:
Additionally, `deref_patterns` implements changes to string and byte string literal patterns,
allowing then to be used in deref patterns:

```rust
# #![feature(deref_patterns)]
# #![allow(incomplete_features)]
match ("test".to_string(), b"test".to_vec()) {
(deref!("test"), deref!(b"test")) => {}
match ("test".to_string(), Box::from("test"), b"test".to_vec()) {
("test", "test", b"test") => {}
_ => panic!(),
}

// This works through multiple layers of reference and smart pointer:
match (&Box::new(&"test".to_string()), &&&"test") {
("test", "test") => {}
_ => panic!(),
}

// `deref!("...")` syntax may also be used:
match "test".to_string() {
deref!("test") => {}
_ => panic!(),
}

Expand All @@ -82,10 +93,16 @@ match *"test" {
"test" => {}
_ => panic!(),
}
match *b"test" {
b"test" => {}
_ => panic!(),
}
match *(b"test" as &[u8]) {
b"test" => {}
_ => panic!(),
}
```

Implicit deref pattern syntax is not yet supported for string or byte string literals.

[`box_patterns`]: ./box-patterns.md
[`string_deref_patterns`]: ./string-deref-patterns.md
[smart pointers in the standard library]: https://doc.rust-lang.org/std/ops/trait.DerefPure.html#implementors
19 changes: 19 additions & 0 deletions tests/ui/pattern/deref-patterns/byte-string-type-errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,23 @@ fn main() {
if let b"test" = *b"this array is too long" {}
//~^ ERROR mismatched types
//~| NOTE expected an array with a size of 22, found one with a size of 4

// Test matching on `&mut T`: we peel the `&mut` before applying the usual special cases.
// No special cases apply to `()`, so the "found" type is the type of the literal.
if let b"test" = &mut () {}
//~^ ERROR mismatched types
//~| NOTE expected `()`, found `&[u8; 4]`

// If the pointee is an array or slice, the usual special cases will apply to the "found" type:
if let b"test" = &mut [] as &mut [i8] {}
//~^ ERROR mismatched type
//~| NOTE expected `[i8]`, found `[u8]`

if let b"test" = &mut [()] {}
//~^ ERROR mismatched types
//~| NOTE expected `[(); 1]`, found `[u8; 4]`

if let b"test" = &mut *b"this array is too long" {}
//~^ ERROR mismatched type
//~| NOTE expected an array with a size of 22, found one with a size of 4
}
40 changes: 39 additions & 1 deletion tests/ui/pattern/deref-patterns/byte-string-type-errors.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,44 @@ LL | if let b"test" = *b"this array is too long" {}
| |
| expected an array with a size of 22, found one with a size of 4

error: aborting due to 5 previous errors
error[E0308]: mismatched types
--> $DIR/byte-string-type-errors.rs:39:12
|
LL | if let b"test" = &mut () {}
| ^^^^^^^ ------- this expression has type `&mut ()`
| |
| expected `()`, found `&[u8; 4]`

error[E0308]: mismatched types
--> $DIR/byte-string-type-errors.rs:44:12
|
LL | if let b"test" = &mut [] as &mut [i8] {}
| ^^^^^^^ -------------------- this expression has type `&mut [i8]`
| |
| expected `[i8]`, found `[u8]`
|
= note: expected slice `[i8]`
found slice `[u8]`

error[E0308]: mismatched types
--> $DIR/byte-string-type-errors.rs:48:12
|
LL | if let b"test" = &mut [()] {}
| ^^^^^^^ --------- this expression has type `&mut [(); 1]`
| |
| expected `[(); 1]`, found `[u8; 4]`
|
= note: expected array `[(); 1]`
found array `[u8; 4]`

error[E0308]: mismatched types
--> $DIR/byte-string-type-errors.rs:52:12
|
LL | if let b"test" = &mut *b"this array is too long" {}
| ^^^^^^^ ------------------------------- this expression has type `&mut [u8; 22]`
| |
| expected an array with a size of 22, found one with a size of 4

error: aborting due to 9 previous errors

For more information about this error, try `rustc --explain E0308`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//@ revisions: stable deref_patterns
//@[deref_patterns] check-pass
//! `deref_patterns` allows string and byte string literal patterns to implicitly peel references
//! and smart pointers from the scrutinee before matching. Since strings and byte strings themselves
//! have reference types, we need to make sure we don't peel too much. By leaving the type of the
//! match scrutinee partially uninferred, these tests make sure we only peel as much as needed in
//! order to match. In particular, when peeling isn't needed, the results should be the same was
//! we'd get without `deref_patterns` enabled.
#![cfg_attr(deref_patterns, feature(deref_patterns))]
#![cfg_attr(deref_patterns, expect(incomplete_features))]

fn uninferred<T>() -> T { unimplemented!() }

// Assert type equality without allowing coercions.
trait Is<T> {}
impl<T> Is<T> for T {}
fn has_type<T>(_: impl Is<T>) {}

fn main() {
// We don't need to peel anything to unify the type of `x` with `&str`, so `x: &str`.
let x = uninferred();
if let "..." = x {}
has_type::<&str>(x);

// We don't need to peel anything to unify the type of `&x` with `&[u8; 3]`, so `x: [u8; 3]`.
let x = uninferred();
if let b"..." = &x {}
has_type::<[u8; 3]>(x);

// Peeling a single `&` lets us unify the type of `&x` with `&[u8; 3]`, giving `x: [u8; 3]`.
let x = uninferred();
if let b"..." = &&x {}
//[stable]~^ ERROR: mismatched types
has_type::<[u8; 3]>(x);

// We have to peel both the `&` and the box before unifying the type of `x` with `&str`.
let x = uninferred();
if let "..." = &Box::new(x) {}
//[stable]~^ ERROR mismatched types
has_type::<&str>(x);

// After peeling the box, we can unify the type of `&x` with `&[u8; 3]`, giving `x: [u8; 3]`.
let x = uninferred();
if let b"..." = Box::new(&x) {}
//[stable]~^ ERROR mismatched types
has_type::<[u8; 3]>(x);

// `&` and `&mut` aren't interchangeable: `&mut`s need to be peeled before unifying, like boxes:
let mut x = uninferred();
if let "..." = &mut x {}
//[stable]~^ ERROR mismatched types
has_type::<&str>(x);
}
Loading
Loading