Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto merge of rust-lang#122792 - Nadrieril:stabilize-min-exh-pats2, r…
Browse files Browse the repository at this point in the history
…=fee1-dead

Stabilize `min_exhaustive_patterns`

## Stabilisation report

I propose we stabilize the [`min_exhaustive_patterns`](rust-lang#119612) language feature.

With this feature, patterns of empty types are considered unreachable when matched by-value. This allows:
```rust
enum Void {}
fn foo() -> Result<u32, Void>;

fn main() {
  let Ok(x) = foo();
  // also
  match foo() {
    Ok(x) => ...,
  }
}
```

This is a subset of the long-unstable [`exhaustive_patterns`](rust-lang#51085) feature. That feature is blocked because omitting empty patterns is tricky when *not* matched by-value. This PR stabilizes the by-value case, which is not tricky.

The not-by-value cases (behind references, pointers, and unions) stay as they are today, e.g.
```rust
enum Void {}
fn foo() -> Result<u32, &Void>;

fn main() {
  let Ok(x) = foo(); // ERROR: missing `Err(_)`
}
```

The consequence on existing code is some extra "unreachable pattern" warnings. This is fully backwards-compatible.

### Comparison with today's rust

This proposal only affects match checking of empty types (i.e. types with no valid values). Non-empty types behave the same with or without this feature. Note that everything below is phrased in terms of `match` but applies equallly to `if let` and other pattern-matching expressions.

To be precise, a visibly empty type is:
- an enum with no variants;
- the never type `!`;
- a struct with a *visible* field of a visibly empty type (and no #[non_exhaustive] annotation);
- a tuple where one of the types is visibly empty;
- en enum with all variants visibly empty (and no `#[non_exhaustive]` annotation);
- a `[T; N]` with `N != 0` and `T` visibly empty;
- all other types are nonempty.

(An extra change was proposed below: that we ignore #[non_exhaustive] for structs since adding fields cannot turn an empty struct into a non-empty one)

For normal types, exhaustiveness checking requires that we list all variants (or use a wildcard). For empty types it's more subtle: in some cases we require a `_` pattern even though there are no valid values that can match it. This is where the difference lies regarding this feature.

#### Today's rust

Under today's rust, a `_` is required for all empty types, except specifically: if the matched expression is of type `!` (the never type) or `EmptyEnum` (where `EmptyEnum` is an enum with no variants), then the `_` is not required.

```rust
let foo: Result<u32, !> = ...;
match foo {
    Ok(x) => ...,
    Err(_) => ..., // required
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => ...,
    Err(_) => ..., // required
}
let foo: &! = ...;
match foo {
    _ => ..., // required
}
fn blah(foo: (u32, !)) {
    match foo {
        _ => ..., // required
    }
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // allowed
    let ptr: *const (u32, !) = ...;
    match *ptr {
        (x, _) => { ... } // required
    }
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // required
    }
}
```

#### After this PR

After this PR, a pattern of an empty type can be omitted if (and only if):
- the match scrutinee expression has type  `!` or `EmptyEnum` (like before);
- *or* the empty type is matched by value (that's the new behavior).

In all other cases, a `_` is required to match on an empty type.

```rust
let foo: Result<u32, !> = ...;
match foo {
    Ok(x) => ..., // `Err` not required
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => ...,
    Err(_) => ..., // required because `!` is under a dereference
}
let foo: &! = ...;
match foo {
    _ => ..., // required because `!` is under a dereference
}
fn blah(foo: (u32, !)) {
    match foo {} // allowed
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // allowed
    let ptr: *const (u32, !) = ...;
    match *ptr {
        (x, _) => { ... } // required because the matched place is under a (pointer) dereference
    }
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // required because the matched place is under a (pointer) dereference
    }
}
```

### Documentation

The reference does not say anything specific about exhaustiveness checking, hence there is nothing to update there. The nomicon does, I opened rust-lang/nomicon#445 to reflect the changes.

### Tests

The relevant tests are in `tests/ui/pattern/usefulness/empty-types.rs`.

### Unresolved Questions

None that I know of.
bors committed Aug 7, 2024
2 parents 6a2cd0d + 52c4b2d commit b66da1f
Showing 99 changed files with 1,060 additions and 935 deletions.
Original file line number Diff line number Diff line change
@@ -585,6 +585,7 @@ pub enum E2<X> {
V4,
}

#[allow(unreachable_patterns)]
fn check_niche_behavior() {
if let E1::V2 { .. } = (E1::V1 { f: true }) {
intrinsics::abort();
Original file line number Diff line number Diff line change
@@ -430,6 +430,7 @@ pub enum E2<X> {
V4,
}

#[allow(unreachable_patterns)]
fn check_niche_behavior () {
if let E1::V2 { .. } = (E1::V1 { f: true }) {
intrinsics::abort();
1 change: 1 addition & 0 deletions compiler/rustc_errors/src/diagnostic_impls.rs
Original file line number Diff line number Diff line change
@@ -66,6 +66,7 @@ macro_rules! into_diag_arg_for_number {
impl IntoDiagArg for $ty {
fn into_diag_arg(self) -> DiagArgValue {
// Convert to a string if it won't fit into `Number`.
#[allow(irrefutable_let_patterns)]
if let Ok(n) = TryInto::<i32>::try_into(self) {
DiagArgValue::Number(n)
} else {
2 changes: 2 additions & 0 deletions compiler/rustc_feature/src/accepted.rs
Original file line number Diff line number Diff line change
@@ -267,6 +267,8 @@ declare_features! (
(accepted, min_const_generics, "1.51.0", Some(74878)),
/// Allows calling `const unsafe fn` inside `unsafe` blocks in `const fn` functions.
(accepted, min_const_unsafe_fn, "1.33.0", Some(55607)),
/// Allows exhaustive pattern matching on uninhabited types when matched by value.
(accepted, min_exhaustive_patterns, "CURRENT_RUSTC_VERSION", Some(119612)),
/// Allows using `Self` and associated types in struct expressions and patterns.
(accepted, more_struct_aliases, "1.16.0", Some(37544)),
/// Allows using the MOVBE target feature.
3 changes: 0 additions & 3 deletions compiler/rustc_feature/src/unstable.rs
Original file line number Diff line number Diff line change
@@ -519,9 +519,6 @@ declare_features! (
(unstable, macro_metavar_expr_concat, "1.81.0", Some(124225)),
/// Allows `#[marker]` on certain traits allowing overlapping implementations.
(unstable, marker_trait_attr, "1.30.0", Some(29864)),
/// Allows exhaustive pattern matching on types that contain uninhabited types in cases that are
/// unambiguously sound.
(unstable, min_exhaustive_patterns, "1.77.0", Some(119612)),
/// A minimal, sound subset of specialization intended to be used by the
/// standard library until the soundness issues with specialization
/// are fixed.
2 changes: 1 addition & 1 deletion compiler/rustc_middle/src/lib.rs
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@
#![allow(rustc::diagnostic_outside_of_impl)]
#![allow(rustc::potential_query_instability)]
#![allow(rustc::untranslatable_diagnostic)]
#![cfg_attr(bootstrap, feature(min_exhaustive_patterns))]
#![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")]
#![doc(rust_logo)]
#![feature(allocator_api)]
@@ -48,7 +49,6 @@
#![feature(iter_from_coroutine)]
#![feature(let_chains)]
#![feature(macro_metavar_expr)]
#![feature(min_exhaustive_patterns)]
#![feature(min_specialization)]
#![feature(negative_impls)]
#![feature(never_type)]
13 changes: 5 additions & 8 deletions compiler/rustc_mir_build/src/build/matches/match_pair.rs
Original file line number Diff line number Diff line change
@@ -208,14 +208,11 @@ impl<'pat, 'tcx> MatchPairTree<'pat, 'tcx> {
subpairs = cx.field_match_pairs(downcast_place, subpatterns);

let irrefutable = adt_def.variants().iter_enumerated().all(|(i, v)| {
i == variant_index || {
(cx.tcx.features().exhaustive_patterns
|| cx.tcx.features().min_exhaustive_patterns)
&& !v
.inhabited_predicate(cx.tcx, adt_def)
.instantiate(cx.tcx, args)
.apply_ignore_module(cx.tcx, cx.param_env)
}
i == variant_index
|| !v
.inhabited_predicate(cx.tcx, adt_def)
.instantiate(cx.tcx, args)
.apply_ignore_module(cx.tcx, cx.param_env)
}) && (adt_def.did().is_local()
|| !adt_def.is_variant_list_non_exhaustive());
if irrefutable {
6 changes: 2 additions & 4 deletions compiler/rustc_mir_build/src/thir/pattern/check_match.rs
Original file line number Diff line number Diff line change
@@ -695,9 +695,7 @@ impl<'p, 'tcx> MatchVisitor<'p, 'tcx> {

// Emit an extra note if the first uncovered witness would be uninhabited
// if we disregard visibility.
let witness_1_is_privately_uninhabited = if (self.tcx.features().exhaustive_patterns
|| self.tcx.features().min_exhaustive_patterns)
&& let Some(witness_1) = witnesses.get(0)
let witness_1_is_privately_uninhabited = if let Some(witness_1) = witnesses.get(0)
&& let ty::Adt(adt, args) = witness_1.ty().kind()
&& adt.is_enum()
&& let Constructor::Variant(variant_index) = witness_1.ctor()
@@ -1059,7 +1057,7 @@ fn report_non_exhaustive_match<'p, 'tcx>(
err.note("`&str` cannot be matched exhaustively, so a wildcard `_` is necessary");
} else if cx.is_foreign_non_exhaustive_enum(ty) {
err.note(format!("`{ty}` is marked as non-exhaustive, so a wildcard `_` is necessary to match exhaustively"));
} else if cx.is_uninhabited(ty.inner()) && cx.tcx.features().min_exhaustive_patterns {
} else if cx.is_uninhabited(ty.inner()) {
// The type is uninhabited yet there is a witness: we must be in the `MaybeInvalid`
// case.
err.note(format!("`{ty}` is uninhabited but is not being matched by value, so a wildcard `_` is required"));
1 change: 0 additions & 1 deletion compiler/rustc_pattern_analysis/src/lib.rs
Original file line number Diff line number Diff line change
@@ -54,7 +54,6 @@ pub trait PatCx: Sized + fmt::Debug {
type PatData: Clone;

fn is_exhaustive_patterns_feature_on(&self) -> bool;
fn is_min_exhaustive_patterns_feature_on(&self) -> bool;

/// The number of fields for this constructor.
fn ctor_arity(&self, ctor: &Constructor<Self>, ty: &Self::Ty) -> usize;
7 changes: 1 addition & 6 deletions compiler/rustc_pattern_analysis/src/rustc.rs
Original file line number Diff line number Diff line change
@@ -237,9 +237,7 @@ impl<'p, 'tcx: 'p> RustcPatCtxt<'p, 'tcx> {
let tys = cx.variant_sub_tys(ty, variant).map(|(field, ty)| {
let is_visible =
adt.is_enum() || field.vis.is_accessible_from(cx.module, cx.tcx);
let is_uninhabited = (cx.tcx.features().exhaustive_patterns
|| cx.tcx.features().min_exhaustive_patterns)
&& cx.is_uninhabited(*ty);
let is_uninhabited = cx.is_uninhabited(*ty);
let skip = is_uninhabited && (!is_visible || is_non_exhaustive);
(ty, PrivateUninhabitedField(skip))
});
@@ -925,9 +923,6 @@ impl<'p, 'tcx: 'p> PatCx for RustcPatCtxt<'p, 'tcx> {
fn is_exhaustive_patterns_feature_on(&self) -> bool {
self.tcx.features().exhaustive_patterns
}
fn is_min_exhaustive_patterns_feature_on(&self) -> bool {
self.tcx.features().min_exhaustive_patterns
}

fn ctor_arity(&self, ctor: &crate::constructor::Constructor<Self>, ty: &Self::Ty) -> usize {
self.ctor_arity(ctor, *ty)
19 changes: 7 additions & 12 deletions compiler/rustc_pattern_analysis/src/usefulness.rs
Original file line number Diff line number Diff line change
@@ -543,13 +543,11 @@
//! recurse into subpatterns. That second part is done through [`PlaceValidity`], most notably
//! [`PlaceValidity::specialize`].
//!
//! Having said all that, in practice we don't fully follow what's been presented in this section.
//! Let's call "toplevel exception" the case where the match scrutinee itself has type `!` or
//! `EmptyEnum`. First, on stable rust, we require `_` patterns for empty types in all cases apart
//! from the toplevel exception. The `exhaustive_patterns` and `min_exaustive_patterns` allow
//! omitting patterns in the cases described above. There's a final detail: in the toplevel
//! exception or with the `exhaustive_patterns` feature, we ignore place validity when checking
//! whether a pattern is required for exhaustiveness. I (Nadrieril) hope to deprecate this behavior.
//! Having said all that, we don't fully follow what's been presented in this section. For
//! backwards-compatibility, we ignore place validity when checking whether a pattern is required
//! for exhaustiveness in two cases: when the `exhaustive_patterns` feature gate is on, or when the
//! match scrutinee itself has type `!` or `EmptyEnum`. I (Nadrieril) hope to deprecate this
//! exception.
//!
//!
//!
@@ -953,13 +951,10 @@ impl<Cx: PatCx> PlaceInfo<Cx> {
self.is_scrutinee && matches!(ctors_for_ty, ConstructorSet::NoConstructors);
// Whether empty patterns are counted as useful or not. We only warn an empty arm unreachable if
// it is guaranteed unreachable by the opsem (i.e. if the place is `known_valid`).
let empty_arms_are_unreachable = self.validity.is_known_valid()
&& (is_toplevel_exception
|| cx.is_exhaustive_patterns_feature_on()
|| cx.is_min_exhaustive_patterns_feature_on());
let empty_arms_are_unreachable = self.validity.is_known_valid();
// Whether empty patterns can be omitted for exhaustiveness. We ignore place validity in the
// toplevel exception and `exhaustive_patterns` cases for backwards compatibility.
let can_omit_empty_arms = empty_arms_are_unreachable
let can_omit_empty_arms = self.validity.is_known_valid()
|| is_toplevel_exception
|| cx.is_exhaustive_patterns_feature_on();

4 changes: 0 additions & 4 deletions compiler/rustc_pattern_analysis/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -152,10 +152,6 @@ impl PatCx for Cx {
false
}

fn is_min_exhaustive_patterns_feature_on(&self) -> bool {
true
}

fn ctor_arity(&self, ctor: &Constructor<Self>, ty: &Self::Ty) -> usize {
ty.sub_tys(ctor).len()
}
2 changes: 1 addition & 1 deletion compiler/rustc_target/src/lib.rs
Original file line number Diff line number Diff line change
@@ -9,12 +9,12 @@
// tidy-alphabetical-start
#![allow(internal_features)]
#![cfg_attr(bootstrap, feature(min_exhaustive_patterns))]
#![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")]
#![doc(rust_logo)]
#![feature(assert_matches)]
#![feature(iter_intersperse)]
#![feature(let_chains)]
#![feature(min_exhaustive_patterns)]
#![feature(rustc_attrs)]
#![feature(rustdoc_internals)]
// tidy-alphabetical-end
1 change: 1 addition & 0 deletions compiler/rustc_transmute/src/layout/nfa.rs
Original file line number Diff line number Diff line change
@@ -87,6 +87,7 @@ where
pub(crate) fn from_tree(tree: Tree<!, R>) -> Result<Self, Uninhabited> {
Ok(match tree {
Tree::Byte(b) => Self::from_byte(b),
#[cfg(bootstrap)]
Tree::Def(..) => unreachable!(),
Tree::Ref(r) => Self::from_ref(r),
Tree::Alt(alts) => {
2 changes: 2 additions & 0 deletions compiler/rustc_type_ir/src/fold.rs
Original file line number Diff line number Diff line change
@@ -91,6 +91,7 @@ pub trait TypeFoldable<I: Interner>: TypeVisitable<I> {
fn fold_with<F: TypeFolder<I>>(self, folder: &mut F) -> Self {
match self.try_fold_with(folder) {
Ok(t) => t,
#[cfg(bootstrap)]
Err(e) => match e {},
}
}
@@ -115,6 +116,7 @@ pub trait TypeSuperFoldable<I: Interner>: TypeFoldable<I> {
fn super_fold_with<F: TypeFolder<I>>(self, folder: &mut F) -> Self {
match self.try_super_fold_with(folder) {
Ok(t) => t,
#[cfg(bootstrap)]
Err(e) => match e {},
}
}
2 changes: 2 additions & 0 deletions library/alloc/src/collections/vec_deque/into_iter.rs
Original file line number Diff line number Diff line change
@@ -121,6 +121,7 @@ impl<T, A: Allocator> Iterator for IntoIter<T, A> {
{
match self.try_fold(init, |b, item| Ok::<B, !>(f(b, item))) {
Ok(b) => b,
#[cfg(bootstrap)]
Err(e) => match e {},
}
}
@@ -242,6 +243,7 @@ impl<T, A: Allocator> DoubleEndedIterator for IntoIter<T, A> {
{
match self.try_rfold(init, |b, item| Ok::<B, !>(f(b, item))) {
Ok(b) => b,
#[cfg(bootstrap)]
Err(e) => match e {},
}
}
2 changes: 1 addition & 1 deletion library/core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -192,6 +192,7 @@
//
// Language features:
// tidy-alphabetical-start
#![cfg_attr(bootstrap, feature(min_exhaustive_patterns))]
#![feature(abi_unadjusted)]
#![feature(adt_const_params)]
#![feature(allow_internal_unsafe)]
@@ -225,7 +226,6 @@
#![feature(link_llvm_intrinsics)]
#![feature(macro_metavar_expr)]
#![feature(marker_trait_attr)]
#![feature(min_exhaustive_patterns)]
#![feature(min_specialization)]
#![feature(multiple_supertrait_upcastable)]
#![feature(must_not_suspend)]
2 changes: 1 addition & 1 deletion library/std/src/lib.rs
Original file line number Diff line number Diff line change
@@ -272,6 +272,7 @@
//
// Language features:
// tidy-alphabetical-start
#![cfg_attr(bootstrap, feature(min_exhaustive_patterns))]
#![feature(alloc_error_handler)]
#![feature(allocator_internals)]
#![feature(allow_internal_unsafe)]
@@ -299,7 +300,6 @@
#![feature(link_cfg)]
#![feature(linkage)]
#![feature(macro_metavar_expr_concat)]
#![feature(min_exhaustive_patterns)]
#![feature(min_specialization)]
#![feature(must_not_suspend)]
#![feature(needs_panic_runtime)]
1 change: 1 addition & 0 deletions src/tools/clippy/clippy_utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -2927,6 +2927,7 @@ pub fn expr_use_ctxt<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) -> ExprU
moved_before_use,
same_ctxt,
},
#[allow(unreachable_patterns)]
Some(ControlFlow::Break(_)) => unreachable!("type of node is ControlFlow<!>"),
None => ExprUseCtxt {
node: Node::Crate(cx.tcx.hir().root_module()),
2 changes: 1 addition & 1 deletion src/tools/clippy/tests/ui/single_match_else.fixed
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ fn main() {

// lint here
use std::convert::Infallible;
if let Ok(a) = Result::<i32, Infallible>::Ok(1) { println!("${:?}", a) } else {
if let Ok(a) = Result::<i32, &Infallible>::Ok(1) { println!("${:?}", a) } else {
println!("else block");
return;
}
2 changes: 1 addition & 1 deletion src/tools/clippy/tests/ui/single_match_else.rs
Original file line number Diff line number Diff line change
@@ -98,7 +98,7 @@ fn main() {

// lint here
use std::convert::Infallible;
match Result::<i32, Infallible>::Ok(1) {
match Result::<i32, &Infallible>::Ok(1) {
Ok(a) => println!("${:?}", a),
Err(_) => {
println!("else block");
4 changes: 2 additions & 2 deletions src/tools/clippy/tests/ui/single_match_else.stderr
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ LL + }
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> tests/ui/single_match_else.rs:101:5
|
LL | / match Result::<i32, Infallible>::Ok(1) {
LL | / match Result::<i32, &Infallible>::Ok(1) {
LL | | Ok(a) => println!("${:?}", a),
LL | | Err(_) => {
LL | | println!("else block");
@@ -75,7 +75,7 @@ LL | | }
|
help: try
|
LL ~ if let Ok(a) = Result::<i32, Infallible>::Ok(1) { println!("${:?}", a) } else {
LL ~ if let Ok(a) = Result::<i32, &Infallible>::Ok(1) { println!("${:?}", a) } else {
LL + println!("else block");
LL + return;
LL + }
1 change: 1 addition & 0 deletions src/tools/miri/src/eval.rs
Original file line number Diff line number Diff line change
@@ -463,6 +463,7 @@ pub fn eval_entry<'tcx>(
let res = match res {
Err(res) => res,
// `Ok` can never happen
#[cfg(bootstrap)]
Ok(never) => match never {},
};

1 change: 1 addition & 0 deletions src/tools/miri/tests/pass/async-fn.rs
Original file line number Diff line number Diff line change
@@ -59,6 +59,7 @@ async fn hello_world() {
}

// This example comes from https://github.com/rust-lang/rust/issues/115145
#[allow(unreachable_patterns)]
async fn uninhabited_variant() {
async fn unreachable(_: Never) {}

1 change: 1 addition & 0 deletions src/tools/miri/tests/pass/enums.rs
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ fn discriminant_overflow() {
}
}

#[allow(unreachable_patterns)]
fn more_discriminant_overflow() {
pub enum Infallible {}

1 change: 1 addition & 0 deletions src/tools/rust-analyzer/crates/hir-ty/src/mir/eval/shim.rs
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ macro_rules! from_bytes {
($ty:tt, $value:expr) => {
($ty::from_le_bytes(match ($value).try_into() {
Ok(it) => it,
#[allow(unreachable_patterns)]
Err(_) => return Err(MirEvalError::InternalError("mismatched size".into())),
}))
};
Original file line number Diff line number Diff line change
@@ -13,17 +13,17 @@ fn opt1(_1: &Result<u32, Void>) -> &u32 {

bb0: {
PlaceMention(_1);
_2 = discriminant((*_1));
switchInt(move _2) -> [0: bb2, 1: bb3, otherwise: bb1];
falseEdge -> [real: bb4, imaginary: bb1];
}

bb1: {
FakeRead(ForMatchedPlace(None), _1);
unreachable;
_2 = discriminant((*_1));
switchInt(move _2) -> [1: bb3, otherwise: bb2];
}

bb2: {
falseEdge -> [real: bb4, imaginary: bb3];
FakeRead(ForMatchedPlace(None), _1);
unreachable;
}

bb3: {
Original file line number Diff line number Diff line change
@@ -11,25 +11,10 @@ fn opt2(_1: &Result<u32, Void>) -> &u32 {

bb0: {
PlaceMention(_1);
_2 = discriminant((*_1));
switchInt(move _2) -> [0: bb2, 1: bb3, otherwise: bb1];
}

bb1: {
FakeRead(ForMatchedPlace(None), _1);
unreachable;
}

bb2: {
StorageLive(_3);
_3 = &(((*_1) as Ok).0: u32);
_0 = &(*_3);
StorageDead(_3);
return;
}

bb3: {
FakeRead(ForMatchedPlace(None), (((*_1) as Err).0: Void));
unreachable;
}
}
Loading

0 comments on commit b66da1f

Please sign in to comment.