Skip to content
4 changes: 4 additions & 0 deletions compiler/rustc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,10 @@ impl DotDotPos {
pub fn as_opt_usize(&self) -> Option<usize> {
if self.0 == u32::MAX { None } else { Some(self.0 as usize) }
}

pub fn is_some(&self) -> bool {
self.0 != u32::MAX
}
}

impl fmt::Debug for DotDotPos {
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_hir_typeck/src/pat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1835,7 +1835,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {

// Type-check subpatterns.
if subpats.len() == variant.fields.len()
|| subpats.len() < variant.fields.len() && ddpos.as_opt_usize().is_some()
|| subpats.len() < variant.fields.len() && ddpos.is_some()
{
let ty::Adt(_, args) = pat_ty.kind() else {
bug!("unexpected pattern type {:?}", pat_ty);
Expand Down Expand Up @@ -2041,7 +2041,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
) -> Ty<'tcx> {
let tcx = self.tcx;
let mut expected_len = elements.len();
if ddpos.as_opt_usize().is_some() {
if ddpos.is_some() {
// Require known type only when `..` is present.
if let ty::Tuple(tys) = self.structurally_resolve_type(span, expected).kind() {
expected_len = tys.len();
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_middle/src/thir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,9 @@ pub enum PatKind<'tcx> {
/// a single variant.
Leaf {
subpatterns: Vec<FieldPat<'tcx>>,
/// Whether this leaf pattern contains a rest pattern `..`.
/// Used in unsafety checking of union field patterns
has_rest: bool,
},

/// Explicit or implicit `&P` or `&mut P`, for some subpattern `P`.
Expand Down
9 changes: 4 additions & 5 deletions compiler/rustc_middle/src/thir/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,9 @@ pub fn walk_pat<'thir, 'tcx: 'thir, V: Visitor<'thir, 'tcx>>(
if let PatKind::Guard { subpattern, condition } = &pat.kind {
visitor.visit_pat(subpattern);
visitor.visit_expr(&visitor.thir()[*condition]);
return;
};

for_each_immediate_subpat(pat, |p| visitor.visit_pat(p));
} else {
for_each_immediate_subpat(pat, |p| visitor.visit_pat(p));
}
}

/// Invokes `callback` on each immediate subpattern of `pat`, if any.
Expand All @@ -279,7 +278,7 @@ pub(crate) fn for_each_immediate_subpat<'a, 'tcx>(
| PatKind::Deref { subpattern, .. }
| PatKind::DerefPattern { subpattern, .. } => callback(subpattern),

PatKind::Variant { subpatterns, .. } | PatKind::Leaf { subpatterns } => {
PatKind::Variant { subpatterns, .. } | PatKind::Leaf { subpatterns, .. } => {
for field_pat in subpatterns {
callback(&field_pat.pattern);
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir_build/src/builder/matches/match_pair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ impl<'tcx> InterPat<'tcx> {
}
}

PatKind::Leaf { ref subpatterns } => {
PatKind::Leaf { ref subpatterns, .. } => {
let mut subpats = vec![];
for &FieldPat { field, pattern: ref subpat } in subpatterns {
let subplace = place_builder.clone_project(PlaceElem::Field(field, subpat.ty));
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir_build/src/builder/matches/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
visit_subpat(self, subpattern, &ProjectedUserTypesNode::None, f);
}

PatKind::Leaf { ref subpatterns } => {
PatKind::Leaf { ref subpatterns, .. } => {
for subpattern in subpatterns {
let subpattern_user_tys = user_tys.leaf(subpattern.field);
debug!("visit_primary_bindings: subpattern_user_tys={subpattern_user_tys:?}");
Expand Down
49 changes: 39 additions & 10 deletions compiler/rustc_mir_build/src/check_unsafety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,34 +258,55 @@ impl<'a, 'tcx> Visitor<'a, 'tcx> for UnsafetyVisitor<'a, 'tcx> {
// match is conditional on having this value
| PatKind::Constant { .. }
| PatKind::Variant { .. }
| PatKind::Leaf { .. }
| PatKind::Deref { .. }
| PatKind::DerefPattern { .. }
| PatKind::Range { .. }
| PatKind::Slice { .. }

@scottmcm scottmcm Sep 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pondering: [..] is also an irrefutable pattern but isn't updated here (right?)

I don't know if it's possible, but could this whole match change to being about irrefutable pattern instead, or something? If we have to whack-a-mole a whole bunch of things here, that makes me less "oh yeah let's do it" than I was before, since I don't know why people would write this.

(Notably if you're using a pat_param from a macro it'd actually be easier for it to always be unsafe so you don't need to suppress the unneeded-unsafe if they pass something simple.)

Part of why we said that unsafeck is on THIR is that it's more of a lexical check than a flow-sensitive one, so being a bit more unsafe than strictly necessary is generally fine if it's something that the human description of the thing is something that people would say "it's unsafe to do that".

View changes since the review

@Jules-Bertholet Jules-Bertholet Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pondering: [..] is also an irrefutable pattern but isn't updated here (right?)

Yes it is. There's even a test.

could this whole match change to being about irrefutable pattern instead

No, irrefutability isn't sufficient. x is an irrefutable pattern but still needs to be unsafe; & _ probably should be as well. Nor is it even necessary; the unstable guard patterns are refutable, but shouldn't require unsafe.

What we care about is that the pattern does not perform a read/assert validity.

| PatKind::Array { .. }
| PatKind::Guard { .. }
// Never constitutes a witness of uninhabitedness.
| PatKind::Never => {
self.requires_unsafe(pat.span, AccessToUnionField);
return; // we can return here since this already requires unsafe
}
// wildcard doesn't read anything.
PatKind::Wild |
PatKind::Wild
// these just wrap other patterns, which we recurse on below.
PatKind::Or { .. } |
PatKind::Error(_) => {}
| PatKind::Or { .. }
| PatKind::Leaf { .. } // We do extra checks below for patterns lowered from consts
| PatKind::Array { .. }
| PatKind::Guard { .. }
| PatKind::Error(_) => {}
}
};

match &pat.kind {
PatKind::Leaf { subpatterns, .. } => {
PatKind::Leaf { subpatterns, has_rest } => {
if let ty::Adt(adt_def, ..) = pat.ty.kind() {
for pat in subpatterns {
if adt_def.non_enum_variant().fields[pat.field].safety.is_unsafe() {
self.requires_unsafe(pat.pattern.span, UseOfUnsafeField);
let single_variant = adt_def.non_enum_variant();

let scope = self.tcx.parent_module(self.hir_context).to_def_id();

if self.in_union_destructure
&& !has_rest
&& (single_variant.field_list_has_applicable_non_exhaustive()
|| single_variant
.fields
.iter()
.any(|f| !f.vis.is_accessible_from(scope, self.tcx)))
{
// This pattern must have been lowered from a constant.
// Changes to private implementation details of said constant
// must not affect whether we require `unsafe`.
self.requires_unsafe(pat.span, AccessToUnionField);
return;
}
Comment on lines +288 to +301

@Nadrieril Nadrieril Sep 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by the logic here. Are you using has_rest/privacy to detect constant patterns? I'd prefer to add a thir pattern node that remembers patterns that were lowered from a constant (this keeps coming up and will be added by #155216 anyway), and use that to know whether we're inside a constant.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally confused by what has_rest has to do with this PR, given that it's equivalent to a bunch of wildcard patterns anyway.

@Jules-Bertholet Jules-Bertholet Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you using has_rest/privacy to detect constant patterns?

Yes, specifically constant patterns whose equivalent expanded pattern could not have been written directly at the location the pattern is being used. If a struct has non-visible fields or is foreign non_exhaustive, then you need a rest pattern to do a direct pattern match; the absence of such a pattern (constants don't have rest patterns) means the pattern was expanded from a constant in another crate.

I was going for the smallest possible change; if you have a suggestion for a cleaner way to carry though this information, that's fine, will gladly do it your way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to add a thir pattern node that remembers patterns that were lowered from a constant (this keeps coming up and will be added by #155216 anyway), and use that to know whether we're inside a constant.

We already track patterns lowered from constants, I think. It's not a dedicated node anymore, but the constant's DefId gets stored in PatExtra's expanded_const field. #155216 also stores the valtree evaluation in PatExtra, but hopefully that's not needed here?

@Jules-Bertholet Jules-Bertholet Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the constant's DefId gets stored in PatExtra's expanded_const field

I believe that is only true for the outer pattern, not its subpatterns. We could add another field to UnsafetyVisitor to track it, but that's annoying to get right (e.g. #161771 (comment)). Nadri's suggestion of a dedicated HIR pattern node has the same issue.


for subpat in subpatterns {
let field = &single_variant.fields[subpat.field];
if field.safety.is_unsafe() {
self.requires_unsafe(subpat.pattern.span, UseOfUnsafeField);
}
}

if adt_def.is_union() {
let old_in_union_destructure =
std::mem::replace(&mut self.in_union_destructure, true);
Expand Down Expand Up @@ -334,6 +355,14 @@ impl<'a, 'tcx> Visitor<'a, 'tcx> for UnsafetyVisitor<'a, 'tcx> {
visit::walk_pat(self, pat);
self.inside_adt = old_inside_adt;
}
PatKind::Guard { subpattern, condition } => {
self.visit_pat(subpattern);

let old_in_union_destructure =
std::mem::replace(&mut self.in_union_destructure, false);
self.visit_expr(&self.thir()[*condition]);
self.in_union_destructure = old_in_union_destructure;
}
_ => {
visit::walk_pat(self, pat);
}
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_mir_build/src/thir/pattern/const_to_pat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,12 +393,14 @@ impl<'tcx> ConstToPat<'tcx> {
subpatterns: self.lower_field_values_to_fieldpats(
valtree.to_branch().iter().map(|ct| ct.to_value()),
),
has_rest: false,
}
}
ty::Tuple(_) => PatKind::Leaf {
subpatterns: self.lower_field_values_to_fieldpats(
valtree.to_branch().iter().map(|ct| ct.to_value()),
),
has_rest: false,
},
ty::Slice(_) => PatKind::Slice {
prefix: valtree
Expand Down
15 changes: 8 additions & 7 deletions compiler/rustc_mir_build/src/thir/pattern/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ impl<'tcx, 'ptcx> PatCtxt<'tcx, 'ptcx> {
span_bug!(pat.span, "unexpected type for tuple pattern: {:?}", ty);
};
let subpatterns = self.lower_tuple_subpats(pats, tys.len(), ddpos);
PatKind::Leaf { subpatterns }
PatKind::Leaf { subpatterns, has_rest: ddpos.is_some() }
}

hir::PatKind::Binding(explicit_ba, id, ident, sub) => {
Expand Down Expand Up @@ -420,10 +420,10 @@ impl<'tcx, 'ptcx> PatCtxt<'tcx, 'ptcx> {
};
let variant_def = adt_def.variant_of_res(res);
let subpatterns = self.lower_tuple_subpats(pats, variant_def.fields.len(), ddpos);
return self.lower_variant_or_leaf(pat, None, res, subpatterns);
return self.lower_variant_or_leaf(pat, None, res, subpatterns, ddpos.is_some());
}

hir::PatKind::Struct(ref qpath, fields, _) => {
hir::PatKind::Struct(ref qpath, fields, rest) => {
let res = self.typeck_results.qpath_res(qpath, pat.hir_id);
let subpatterns = fields
.iter()
Expand All @@ -437,7 +437,7 @@ impl<'tcx, 'ptcx> PatCtxt<'tcx, 'ptcx> {
})
.collect();

return self.lower_variant_or_leaf(pat, None, res, subpatterns);
return self.lower_variant_or_leaf(pat, None, res, subpatterns, rest.is_some());
}

hir::PatKind::Or(pats) => PatKind::Or { pats: self.lower_patterns(pats) },
Expand Down Expand Up @@ -513,6 +513,7 @@ impl<'tcx, 'ptcx> PatCtxt<'tcx, 'ptcx> {
expr: Option<&'tcx hir::PatExpr<'tcx>>,
res: Res,
subpatterns: Vec<FieldPat<'tcx>>,
has_rest: bool,
) -> Box<Pat<'tcx>> {
// Check whether the caller should have provided an `expr` for this pattern kind.
assert_matches!(
Expand Down Expand Up @@ -563,7 +564,7 @@ impl<'tcx, 'ptcx> PatCtxt<'tcx, 'ptcx> {
subpatterns,
}
} else {
PatKind::Leaf { subpatterns }
PatKind::Leaf { subpatterns, has_rest }
}
}

Expand All @@ -577,7 +578,7 @@ impl<'tcx, 'ptcx> PatCtxt<'tcx, 'ptcx> {
)
| Res::SelfTyParam { .. }
| Res::SelfTyAlias { .. }
| Res::SelfCtor(..) => PatKind::Leaf { subpatterns },
| Res::SelfCtor(..) => PatKind::Leaf { subpatterns, has_rest },
_ => {
let e = match res {
Res::Def(DefKind::ConstParam, def_id) => {
Expand Down Expand Up @@ -645,7 +646,7 @@ impl<'tcx, 'ptcx> PatCtxt<'tcx, 'ptcx> {
_ => {
// The path isn't the name of a constant, so it must actually
// be a unit struct or unit variant (e.g. `Option::None`).
return self.lower_variant_or_leaf(pat, Some(expr), res, vec![]);
return self.lower_variant_or_leaf(pat, Some(expr), res, vec![], false);
}
};

Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_mir_build/src/thir/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,13 +769,14 @@ impl<'a, 'tcx> ThirPrinter<'a, 'tcx> {

print_indented!(self, "}", depth_lvl + 1);
}
PatKind::Leaf { subpatterns } => {
PatKind::Leaf { subpatterns, has_rest } => {
print_indented!(self, "Leaf { ", depth_lvl + 1);
print_indented!(self, "subpatterns: [", depth_lvl + 2);
for field_pat in subpatterns.iter() {
self.print_pat(&field_pat.pattern, depth_lvl + 3);
}
print_indented!(self, "]", depth_lvl + 2);
print_indented!(self, format!("has_rest: {has_rest:?}"), depth_lvl + 2);
print_indented!(self, "}", depth_lvl + 1);
}
PatKind::Deref { pin, subpattern } => {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_pattern_analysis/src/rustc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ impl<'p, 'tcx: 'p> RustcPatCtxt<'p, 'tcx> {
ctor = DerefPattern(cx.reveal_opaque_ty(subpattern.ty));
self.internal_state.has_lowered_deref_pat.set(true);
}
PatKind::Leaf { subpatterns } | PatKind::Variant { subpatterns, .. } => {
PatKind::Leaf { subpatterns, .. } | PatKind::Variant { subpatterns, .. } => {
match ty.kind() {
ty::Tuple(fs) => {
ctor = Struct;
Expand Down
17 changes: 17 additions & 0 deletions tests/ui/pattern/rfc-3637-guard-patterns/union.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//@ run-pass
//! Test that guard patterns in union fields don't impose an `unsafe` requirement.

#![feature(guard_patterns)]
#![expect(incomplete_features)]

union Foo {
field: u8,
}

fn main() {
let foo = Foo { field: 42 };
match foo {
Foo { field: _ if matches!(1, 1) } => (),
_ => panic!(), //~ WARN unreachable
}
}
12 changes: 12 additions & 0 deletions tests/ui/pattern/rfc-3637-guard-patterns/union.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
warning: unreachable pattern
--> $DIR/union.rs:15:9
|
LL | Foo { field: _ if matches!(1, 1) } => (),
| ---------------------------------- matches all the relevant values
LL | _ => panic!(),
| ^ no value can reach this
|
= note: `#[warn(unreachable_patterns)]` (part of `#[warn(unused)]`) on by default

warning: 1 warning emitted

26 changes: 26 additions & 0 deletions tests/ui/union/auxiliary/zst-const.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#[derive(Clone, Copy, PartialEq)]
pub struct HasPrivateField {
not_pub: (),
}

pub const HAS_PRIVATE_FIELD: HasPrivateField = HasPrivateField { not_pub: () };

#[derive(Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct HasNonExhaustiveFieldList {}

pub const HAS_NON_EXHAUSTIVE_FIELD_LIST: HasNonExhaustiveFieldList = HasNonExhaustiveFieldList {};

#[derive(Clone, Copy, PartialEq)]
pub struct HasPrivateTupleField(());

pub const HAS_PRIVATE_TUPLE_FIELD: HasPrivateTupleField = HasPrivateTupleField(());

#[derive(Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct HasNonExhaustiveTupleFieldList();

pub const HAS_NON_EXHAUSTIVE_TUPLE_FIELD_LIST: HasNonExhaustiveTupleFieldList =
HasNonExhaustiveTupleFieldList();

pub const NESTED_CONST: (HasPrivateField,) = (HAS_PRIVATE_FIELD,);
Loading
Loading