Skip to content

Commit

Permalink
Auto merge of #85769 - jhpratt:stabilize-const-transmute-union, r=Ral…
Browse files Browse the repository at this point in the history
…fJung

Stabilize `const_fn_transmute`, `const_fn_union`

This PR stabilizes the `const_fn_transmute` and `const_fn_union` features. It _does not_ stabilize any methods (obviously aside from `transmute`) that are blocked on only these features.

Closes #53605. Closes #51909.
  • Loading branch information
bors committed Jul 28, 2021
2 parents 2faabf5 + 37af399 commit 8b50cc9
Show file tree
Hide file tree
Showing 47 changed files with 147 additions and 535 deletions.
2 changes: 1 addition & 1 deletion compiler/rustc_ast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)]
#![feature(box_syntax)]
#![feature(box_patterns)]
#![feature(const_fn_transmute)]
#![cfg_attr(bootstrap, feature(const_fn_transmute))]
#![feature(crate_visibility_modifier)]
#![feature(iter_zip)]
#![feature(label_break_value)]
Expand Down
4 changes: 4 additions & 0 deletions compiler/rustc_feature/src/accepted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ declare_features! (
/// Allows bindings in the subpattern of a binding pattern.
/// For example, you can write `x @ Some(y)`.
(accepted, bindings_after_at, "1.54.0", Some(65490), None),
/// Allows calling `transmute` in const fn
(accepted, const_fn_transmute, "1.56.0", Some(53605), None),
/// Allows accessing fields of unions inside `const` functions.
(accepted, const_fn_union, "1.56.0", Some(51909), None),

// -------------------------------------------------------------------------
// feature-group-end: accepted features
Expand Down
6 changes: 0 additions & 6 deletions compiler/rustc_feature/src/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,9 +413,6 @@ declare_features! (
/// Allows inferring `'static` outlives requirements (RFC 2093).
(active, infer_static_outlives_requirements, "1.26.0", Some(54185), None),

/// Allows accessing fields of unions inside `const` functions.
(active, const_fn_union, "1.27.0", Some(51909), None),

/// Allows dereferencing raw pointers during const eval.
(active, const_raw_ptr_deref, "1.27.0", Some(51911), None),

Expand Down Expand Up @@ -565,9 +562,6 @@ declare_features! (
/// Lazily evaluate constants. This allows constants to depend on type parameters.
(incomplete, lazy_normalization_consts, "1.46.0", Some(72219), None),

/// Allows calling `transmute` in const fn
(active, const_fn_transmute, "1.46.0", Some(53605), None),

/// Allows `if let` guard in match arms.
(incomplete, if_let_guard, "1.47.0", Some(51114), None),

Expand Down
16 changes: 1 addition & 15 deletions compiler/rustc_mir/src/transform/check_consts/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,12 +748,7 @@ impl Visitor<'tcx> for Checker<'mir, 'tcx> {
| ProjectionElem::Downcast(..)
| ProjectionElem::Subslice { .. }
| ProjectionElem::Field(..)
| ProjectionElem::Index(_) => {
let base_ty = Place::ty_from(place_local, proj_base, self.body, self.tcx).ty;
if base_ty.is_union() {
self.check_op(ops::UnionAccess);
}
}
| ProjectionElem::Index(_) => {}
}
}

Expand Down Expand Up @@ -876,15 +871,6 @@ impl Visitor<'tcx> for Checker<'mir, 'tcx> {

let is_intrinsic = tcx.fn_sig(callee).abi() == RustIntrinsic;

// HACK: This is to "unstabilize" the `transmute` intrinsic
// within const fns. `transmute` is allowed in all other const contexts.
// This won't really scale to more intrinsics or functions. Let's allow const
// transmutes in const fn before we add more hacks to this.
if is_intrinsic && tcx.item_name(callee) == sym::transmute {
self.check_op(ops::Transmute);
return;
}

if !tcx.is_const_fn_raw(callee) {
let mut permitted = false;

Expand Down
45 changes: 0 additions & 45 deletions compiler/rustc_mir/src/transform/check_consts/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,51 +501,6 @@ impl NonConstOp for ThreadLocalAccess {
}
}

#[derive(Debug)]
pub struct Transmute;
impl NonConstOp for Transmute {
fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
if ccx.const_kind() != hir::ConstContext::ConstFn {
Status::Allowed
} else {
Status::Unstable(sym::const_fn_transmute)
}
}

fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
let mut err = feature_err(
&ccx.tcx.sess.parse_sess,
sym::const_fn_transmute,
span,
&format!("`transmute` is not allowed in {}s", ccx.const_kind()),
);
err.note("`transmute` is only allowed in constants and statics for now");
err
}
}

#[derive(Debug)]
pub struct UnionAccess;
impl NonConstOp for UnionAccess {
fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
// Union accesses are stable in all contexts except `const fn`.
if ccx.const_kind() != hir::ConstContext::ConstFn {
Status::Allowed
} else {
Status::Unstable(sym::const_fn_union)
}
}

fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
feature_err(
&ccx.tcx.sess.parse_sess,
sym::const_fn_union,
span,
"unions in const fn are unstable",
)
}
}

// Types that cannot appear in the signature or locals of a `const fn`.
pub mod ty {
use super::*;
Expand Down
5 changes: 3 additions & 2 deletions library/core/src/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,9 @@ extern "rust-intrinsic" {
/// cause [undefined behavior][ub] with this function. `transmute` should be
/// the absolute last resort.
///
/// Transmuting pointers to integers in a `const` context is [undefined behavior][ub].
/// Any attempt to use the resulting value for integer operations will abort const-evaluation.
///
/// The [nomicon](../../nomicon/transmutes.html) has additional
/// documentation.
///
Expand Down Expand Up @@ -1128,8 +1131,6 @@ extern "rust-intrinsic" {
/// }
/// ```
#[stable(feature = "rust1", since = "1.0.0")]
// NOTE: While this makes the intrinsic const stable, we have some custom code in const fn
// checks that prevent its use within `const fn`.
#[rustc_const_stable(feature = "const_transmute", since = "1.46.0")]
#[rustc_diagnostic_item = "transmute"]
pub fn transmute<T, U>(e: T) -> U;
Expand Down
4 changes: 2 additions & 2 deletions library/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
#![feature(const_refs_to_cell)]
#![feature(const_panic)]
#![feature(const_pin)]
#![feature(const_fn_union)]
#![cfg_attr(bootstrap, feature(const_fn_union))]
#![feature(const_impl_trait)]
#![feature(const_fn_floating_point_arithmetic)]
#![feature(const_fn_fn_ptr_basics)]
Expand Down Expand Up @@ -159,7 +159,7 @@
#![feature(rtm_target_feature)]
#![feature(f16c_target_feature)]
#![feature(hexagon_target_feature)]
#![feature(const_fn_transmute)]
#![cfg_attr(bootstrap, feature(const_fn_transmute))]
#![feature(abi_unadjusted)]
#![feature(adx_target_feature)]
#![feature(associated_type_bounds)]
Expand Down
4 changes: 2 additions & 2 deletions library/core/src/num/int_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2096,7 +2096,7 @@ macro_rules! int_impl {
#[rustc_const_stable(feature = "const_int_conversion", since = "1.44.0")]
// SAFETY: const sound because integers are plain old datatypes so we can always
// transmute them to arrays of bytes
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
#[inline]
pub const fn to_ne_bytes(self) -> [u8; mem::size_of::<Self>()] {
// SAFETY: integers are plain old datatypes so we can always transmute them to
Expand Down Expand Up @@ -2202,7 +2202,7 @@ macro_rules! int_impl {
#[rustc_const_stable(feature = "const_int_conversion", since = "1.44.0")]
// SAFETY: const sound because integers are plain old datatypes so we can always
// transmute to them
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
#[inline]
pub const fn from_ne_bytes(bytes: [u8; mem::size_of::<Self>()]) -> Self {
// SAFETY: integers are plain old datatypes so we can always transmute to them
Expand Down
4 changes: 2 additions & 2 deletions library/core/src/num/uint_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1926,7 +1926,7 @@ macro_rules! uint_impl {
#[rustc_const_stable(feature = "const_int_conversion", since = "1.44.0")]
// SAFETY: const sound because integers are plain old datatypes so we can always
// transmute them to arrays of bytes
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
#[inline]
pub const fn to_ne_bytes(self) -> [u8; mem::size_of::<Self>()] {
// SAFETY: integers are plain old datatypes so we can always transmute them to
Expand Down Expand Up @@ -2032,7 +2032,7 @@ macro_rules! uint_impl {
#[rustc_const_stable(feature = "const_int_conversion", since = "1.44.0")]
// SAFETY: const sound because integers are plain old datatypes so we can always
// transmute to them
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
#[inline]
pub const fn from_ne_bytes(bytes: [u8; mem::size_of::<Self>()]) -> Self {
// SAFETY: integers are plain old datatypes so we can always transmute to them
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/slice/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl<T> [T] {
#[rustc_const_stable(feature = "const_slice_len", since = "1.39.0")]
#[inline]
// SAFETY: const sound because we transmute out the length field as a usize (which it must be)
#[rustc_allow_const_fn_unstable(const_fn_union)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_union))]
pub const fn len(&self) -> usize {
// FIXME: Replace with `crate::ptr::metadata(self)` when that is const-stable.
// As of this writing this causes a "Const-stable functions can only call other
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/str/converts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ pub fn from_utf8_mut(v: &mut [u8]) -> Result<&mut str, Utf8Error> {
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_stable(feature = "const_str_from_utf8_unchecked", since = "1.55.0")]
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
pub const unsafe fn from_utf8_unchecked(v: &[u8]) -> &str {
// SAFETY: the caller must guarantee that the bytes `v` are valid UTF-8.
// Also relies on `&str` and `&[u8]` having the same layout.
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/str/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ impl str {
#[rustc_const_stable(feature = "str_as_bytes", since = "1.39.0")]
#[inline(always)]
#[allow(unused_attributes)]
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
pub const fn as_bytes(&self) -> &[u8] {
// SAFETY: const sound because we transmute two types with the same layout
unsafe { mem::transmute(self) }
Expand Down
2 changes: 1 addition & 1 deletion library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@
#![feature(const_cstr_unchecked)]
#![feature(const_fn_floating_point_arithmetic)]
#![feature(const_fn_fn_ptr_basics)]
#![feature(const_fn_transmute)]
#![cfg_attr(bootstrap, feature(const_fn_transmute))]
#![feature(const_io_structs)]
#![feature(const_ip)]
#![feature(const_ipv4)]
Expand Down
4 changes: 2 additions & 2 deletions library/std/src/net/ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ impl Ipv6Addr {
///
/// let addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff);
/// ```
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
#[rustc_const_stable(feature = "const_ipv6", since = "1.32.0")]
#[stable(feature = "rust1", since = "1.0.0")]
#[inline]
Expand Down Expand Up @@ -1149,7 +1149,7 @@ impl Ipv6Addr {
/// assert_eq!(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff).segments(),
/// [0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff]);
/// ```
#[rustc_allow_const_fn_unstable(const_fn_transmute)]
#[cfg_attr(bootstrap, rustc_allow_const_fn_unstable(const_fn_transmute))]
#[rustc_const_stable(feature = "const_ipv6", since = "1.50.0")]
#[stable(feature = "rust1", since = "1.0.0")]
#[inline]
Expand Down
4 changes: 1 addition & 3 deletions src/test/mir-opt/issues/issue-75439.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// EMIT_MIR issue_75439.foo.MatchBranchSimplification.diff

#![feature(const_fn_transmute)]

use std::mem::transmute;

pub fn foo(bytes: [u8; 16]) -> Option<[u8; 4]> {
Expand All @@ -16,5 +14,5 @@ pub fn foo(bytes: [u8; 16]) -> Option<[u8; 4]> {
}

fn main() {
let _ = foo([0; 16]);
let _ = foo([0; 16]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
+ // MIR for `foo` after MatchBranchSimplification

fn foo(_1: [u8; 16]) -> Option<[u8; 4]> {
debug bytes => _1; // in scope 0 at $DIR/issue-75439.rs:7:12: 7:17
let mut _0: std::option::Option<[u8; 4]>; // return place in scope 0 at $DIR/issue-75439.rs:7:32: 7:47
let _2: [u32; 4]; // in scope 0 at $DIR/issue-75439.rs:9:9: 9:15
let mut _3: [u8; 16]; // in scope 0 at $DIR/issue-75439.rs:9:47: 9:52
let mut _5: [u8; 4]; // in scope 0 at $DIR/issue-75439.rs:12:14: 12:38
let mut _6: u32; // in scope 0 at $DIR/issue-75439.rs:12:33: 12:35
debug bytes => _1; // in scope 0 at $DIR/issue-75439.rs:5:12: 5:17
let mut _0: std::option::Option<[u8; 4]>; // return place in scope 0 at $DIR/issue-75439.rs:5:32: 5:47
let _2: [u32; 4]; // in scope 0 at $DIR/issue-75439.rs:7:9: 7:15
let mut _3: [u8; 16]; // in scope 0 at $DIR/issue-75439.rs:7:47: 7:52
let mut _5: [u8; 4]; // in scope 0 at $DIR/issue-75439.rs:10:14: 10:38
let mut _6: u32; // in scope 0 at $DIR/issue-75439.rs:10:33: 10:35
scope 1 {
debug dwords => _2; // in scope 1 at $DIR/issue-75439.rs:9:9: 9:15
let _4: u32; // in scope 1 at $DIR/issue-75439.rs:11:27: 11:29
debug dwords => _2; // in scope 1 at $DIR/issue-75439.rs:7:9: 7:15
let _4: u32; // in scope 1 at $DIR/issue-75439.rs:9:27: 9:29
scope 3 {
debug ip => _4; // in scope 3 at $DIR/issue-75439.rs:11:27: 11:29
debug ip => _4; // in scope 3 at $DIR/issue-75439.rs:9:27: 9:29
scope 4 {
}
}
Expand All @@ -21,67 +21,67 @@
}

bb0: {
StorageLive(_2); // scope 0 at $DIR/issue-75439.rs:9:9: 9:15
StorageLive(_3); // scope 2 at $DIR/issue-75439.rs:9:47: 9:52
_3 = _1; // scope 2 at $DIR/issue-75439.rs:9:47: 9:52
_2 = transmute::<[u8; 16], [u32; 4]>(move _3) -> bb1; // scope 2 at $DIR/issue-75439.rs:9:37: 9:53
StorageLive(_2); // scope 0 at $DIR/issue-75439.rs:7:9: 7:15
StorageLive(_3); // scope 2 at $DIR/issue-75439.rs:7:47: 7:52
_3 = _1; // scope 2 at $DIR/issue-75439.rs:7:47: 7:52
_2 = transmute::<[u8; 16], [u32; 4]>(move _3) -> bb1; // scope 2 at $DIR/issue-75439.rs:7:37: 7:53
// mir::Constant
// + span: $DIR/issue-75439.rs:9:37: 9:46
// + span: $DIR/issue-75439.rs:7:37: 7:46
// + literal: Const { ty: unsafe extern "rust-intrinsic" fn([u8; 16]) -> [u32; 4] {std::intrinsics::transmute::<[u8; 16], [u32; 4]>}, val: Value(Scalar(<ZST>)) }
}

bb1: {
StorageDead(_3); // scope 2 at $DIR/issue-75439.rs:9:52: 9:53
switchInt(_2[0 of 4]) -> [0_u32: bb2, otherwise: bb4]; // scope 1 at $DIR/issue-75439.rs:11:13: 11:14
StorageDead(_3); // scope 2 at $DIR/issue-75439.rs:7:52: 7:53
switchInt(_2[0 of 4]) -> [0_u32: bb2, otherwise: bb4]; // scope 1 at $DIR/issue-75439.rs:9:13: 9:14
}

bb2: {
switchInt(_2[1 of 4]) -> [0_u32: bb3, otherwise: bb4]; // scope 1 at $DIR/issue-75439.rs:11:16: 11:17
switchInt(_2[1 of 4]) -> [0_u32: bb3, otherwise: bb4]; // scope 1 at $DIR/issue-75439.rs:9:16: 9:17
}

bb3: {
switchInt(_2[2 of 4]) -> [0_u32: bb6, 4294901760_u32: bb7, otherwise: bb4]; // scope 1 at $DIR/issue-75439.rs:11:19: 11:20
switchInt(_2[2 of 4]) -> [0_u32: bb6, 4294901760_u32: bb7, otherwise: bb4]; // scope 1 at $DIR/issue-75439.rs:9:19: 9:20
}

bb4: {
discriminant(_0) = 0; // scope 1 at $DIR/issue-75439.rs:14:9: 14:13
goto -> bb9; // scope 1 at $DIR/issue-75439.rs:11:5: 15:6
discriminant(_0) = 0; // scope 1 at $DIR/issue-75439.rs:12:9: 12:13
goto -> bb9; // scope 1 at $DIR/issue-75439.rs:9:5: 13:6
}

bb5: {
StorageLive(_5); // scope 3 at $DIR/issue-75439.rs:12:14: 12:38
StorageLive(_6); // scope 4 at $DIR/issue-75439.rs:12:33: 12:35
_6 = _4; // scope 4 at $DIR/issue-75439.rs:12:33: 12:35
_5 = transmute::<u32, [u8; 4]>(move _6) -> bb8; // scope 4 at $DIR/issue-75439.rs:12:23: 12:36
StorageLive(_5); // scope 3 at $DIR/issue-75439.rs:10:14: 10:38
StorageLive(_6); // scope 4 at $DIR/issue-75439.rs:10:33: 10:35
_6 = _4; // scope 4 at $DIR/issue-75439.rs:10:33: 10:35
_5 = transmute::<u32, [u8; 4]>(move _6) -> bb8; // scope 4 at $DIR/issue-75439.rs:10:23: 10:36
// mir::Constant
// + span: $DIR/issue-75439.rs:12:23: 12:32
// + span: $DIR/issue-75439.rs:10:23: 10:32
// + literal: Const { ty: unsafe extern "rust-intrinsic" fn(u32) -> [u8; 4] {std::intrinsics::transmute::<u32, [u8; 4]>}, val: Value(Scalar(<ZST>)) }
}

bb6: {
StorageLive(_4); // scope 1 at $DIR/issue-75439.rs:11:27: 11:29
_4 = _2[3 of 4]; // scope 1 at $DIR/issue-75439.rs:11:27: 11:29
goto -> bb5; // scope 1 at $DIR/issue-75439.rs:11:5: 15:6
StorageLive(_4); // scope 1 at $DIR/issue-75439.rs:9:27: 9:29
_4 = _2[3 of 4]; // scope 1 at $DIR/issue-75439.rs:9:27: 9:29
goto -> bb5; // scope 1 at $DIR/issue-75439.rs:9:5: 13:6
}

bb7: {
StorageLive(_4); // scope 1 at $DIR/issue-75439.rs:11:27: 11:29
_4 = _2[3 of 4]; // scope 1 at $DIR/issue-75439.rs:11:27: 11:29
goto -> bb5; // scope 1 at $DIR/issue-75439.rs:11:5: 15:6
StorageLive(_4); // scope 1 at $DIR/issue-75439.rs:9:27: 9:29
_4 = _2[3 of 4]; // scope 1 at $DIR/issue-75439.rs:9:27: 9:29
goto -> bb5; // scope 1 at $DIR/issue-75439.rs:9:5: 13:6
}

bb8: {
StorageDead(_6); // scope 4 at $DIR/issue-75439.rs:12:35: 12:36
((_0 as Some).0: [u8; 4]) = move _5; // scope 3 at $DIR/issue-75439.rs:12:9: 12:39
discriminant(_0) = 1; // scope 3 at $DIR/issue-75439.rs:12:9: 12:39
StorageDead(_5); // scope 3 at $DIR/issue-75439.rs:12:38: 12:39
StorageDead(_4); // scope 1 at $DIR/issue-75439.rs:13:5: 13:6
goto -> bb9; // scope 1 at $DIR/issue-75439.rs:11:5: 15:6
StorageDead(_6); // scope 4 at $DIR/issue-75439.rs:10:35: 10:36
((_0 as Some).0: [u8; 4]) = move _5; // scope 3 at $DIR/issue-75439.rs:10:9: 10:39
discriminant(_0) = 1; // scope 3 at $DIR/issue-75439.rs:10:9: 10:39
StorageDead(_5); // scope 3 at $DIR/issue-75439.rs:10:38: 10:39
StorageDead(_4); // scope 1 at $DIR/issue-75439.rs:11:5: 11:6
goto -> bb9; // scope 1 at $DIR/issue-75439.rs:9:5: 13:6
}

bb9: {
StorageDead(_2); // scope 0 at $DIR/issue-75439.rs:16:1: 16:2
return; // scope 0 at $DIR/issue-75439.rs:16:2: 16:2
StorageDead(_2); // scope 0 at $DIR/issue-75439.rs:14:1: 14:2
return; // scope 0 at $DIR/issue-75439.rs:14:2: 14:2
}
}

1 change: 0 additions & 1 deletion src/test/ui/consts/const-eval/const_transmute.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// run-pass

#![feature(const_fn_union)]
#![allow(dead_code)]

#[repr(C)]
Expand Down

0 comments on commit 8b50cc9

Please sign in to comment.