Skip to content
Draft
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
63 changes: 56 additions & 7 deletions compiler/rustc_hir_analysis/src/check/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,10 @@ pub(super) fn check_transparent<'tcx>(tcx: TyCtxt<'tcx>, adt: ty::AdtDef<'tcx>)
"`repr(C)` types",
"is a `#[repr(C)]` type, so it is not guaranteed to be zero-sized on all targets.",
),
UnsuitedReason::Array => (
"array types with non-trivial element types",
"is an array with a non-trivial element type, so it is not guaranteed to have a trivial ABI in all situations.",
),
};
Diag::new(
dcx,
Expand Down Expand Up @@ -1785,19 +1789,43 @@ pub(super) fn check_transparent<'tcx>(tcx: TyCtxt<'tcx>, adt: ty::AdtDef<'tcx>)
NonExhaustive,
PrivateField,
ReprC,
Array,
}

fn check_unsuited<'tcx>(
tcx: TyCtxt<'tcx>,
typing_env: ty::TypingEnv<'tcx>,
ty: Ty<'tcx>,
inside_repr_rust_packed_1: bool,
) -> ControlFlow<UnsuitedInfo<'tcx>> {
// We can encounter projections during traversal, so ensure the type is normalized.
let ty =
tcx.try_normalize_erasing_regions(typing_env, Unnormalized::new_wip(ty)).unwrap_or(ty);
match ty.kind() {
ty::Tuple(list) => list.iter().try_for_each(|t| check_unsuited(tcx, typing_env, t)),
ty::Array(ty, _) => check_unsuited(tcx, typing_env, *ty),
ty::Tuple(list) => list
.iter()
.try_for_each(|t| check_unsuited(tcx, typing_env, t, inside_repr_rust_packed_1)),
ty::Array(elem_ty, len) => {
// If we are inside a `#[repr(Rust, packed(1))]` ADT,
// the alignment is guaranteed to be 1 and Rust has full control over the ABI.
// Therefore, we can allow any length-0 array.
Copy link
Copy Markdown
Member

@RalfJung RalfJung May 1, 2026

Choose a reason for hiding this comment

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

Why only 0-length arrays?

View changes since the review

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.

#[repr(Rust, packed(1))] guarantees alignment 1, but not size 0. If the element type contains e.g. repr(C), it might not be portably 0-sized. The if elem_trivial { check_unsuited(...) } check below allows the non-0-length arrays that we can safely allow.

I suppose we could allow types like #[repr(Rust, packed(1))] struct Foo([[u128; 0]; 42]), which this PR currently rejects. But again, that would conflict with your preference for less complexity.

Copy link
Copy Markdown
Member

@RalfJung RalfJung May 2, 2026

Choose a reason for hiding this comment

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

We only ever call check_trivial on types that we already know to be zero-sized. So I think you can remove the length check and everything will remain correct. Or am I missing something?

Copy link
Copy Markdown
Contributor Author

@Jules-Bertholet Jules-Bertholet May 2, 2026

Choose a reason for hiding this comment

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

We only ever call check_trivial on types that we already know to be zero-sized

… on the current target. The element type could be an empty repr(C) struct. We have to reject this:

#[repr(C)]
struct MaybeZst;

#[repr(packed)]
struct MaybeZstWrap([MaybeZst; 42]);

#[repr(transparent)]
struct ThisShouldntCompile(u32, MaybeZstWrap);

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 see. That warrants a comment in the code and a test case.

// This special case is needed to support the `ghost` crate.
if inside_repr_rust_packed_1
&& let ty::ConstKind::Value(v) = len.kind()
&& v.try_to_target_usize(tcx) == Some(0)
{
return ControlFlow::Continue(());
}

let elem_layout = tcx.layout_of(typing_env.as_query_input(*elem_ty));
Copy link
Copy Markdown
Member

@RalfJung RalfJung May 1, 2026

Choose a reason for hiding this comment

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

Suggested change
let elem_layout = tcx.layout_of(typing_env.as_query_input(*elem_ty));
// We are concerned about arrays for two reasons:
// - Arrays are part of the C ABI so we don't want to make assumptions about their ABI
// - The alignment of zero-sized arrays is target-dependent.
// So we only accept arrays whose element types are themselves 1-ZST that are "trivial" for repr(transparent).
// This unnecessarily considers a repr(Rust) newtype around a `[u8; 1]` to be "unsuited", but that's not a big loss (and it can be worked around by adding `packed`).
let elem_layout = tcx.layout_of(typing_env.as_query_input(*elem_ty));

View changes since the review

let elem_trivial = elem_layout.is_ok_and(|layout| layout.is_1zst());

if elem_trivial {
check_unsuited(tcx, typing_env, *elem_ty, inside_repr_rust_packed_1)
} else {
ControlFlow::Break(UnsuitedInfo { ty, reason: UnsuitedReason::Array })
}
}
ty::Adt(def, args) => {
if !def.did().is_local() && !find_attr!(tcx, def.did(), RustcPubTransparent(_)) {
let non_exhaustive = def.is_variant_list_non_exhaustive()
Expand All @@ -1814,12 +1842,22 @@ pub(super) fn check_transparent<'tcx>(tcx: TyCtxt<'tcx>, adt: ty::AdtDef<'tcx>)
});
}
}
if def.repr().c() {

let repr = def.repr();

if repr.c() {
return ControlFlow::Break(UnsuitedInfo { ty, reason: UnsuitedReason::ReprC });
}
def.all_fields()
.map(|field| field.ty(tcx, args))
.try_for_each(|t| check_unsuited(tcx, typing_env, t))

def.all_fields().map(|field| field.ty(tcx, args)).try_for_each(|t| {
check_unsuited(
tcx,
typing_env,
t,
inside_repr_rust_packed_1
|| def.repr().pack.is_some_and(|a| a.bytes() == 1),
Copy link
Copy Markdown
Member

@RalfJung RalfJung May 1, 2026

Choose a reason for hiding this comment

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

This will also be true for repr(Swift, packed) if we get that in the future... I guess there's no good way to check for repr(Rust, packed)?

View changes since the review

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.

There isn't, sadly

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.

One verbose way to achieve this would be an exhaustive field match on ReprOptions similar to #155418.

Comment on lines +1857 to +1858
Copy link
Copy Markdown
Member

@RalfJung RalfJung May 1, 2026

Choose a reason for hiding this comment

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

This ... || ... expression seems worth let-binding before the map.

View changes since the review

)
})
}
_ => ControlFlow::Continue(()),
}
Expand All @@ -1828,11 +1866,22 @@ pub(super) fn check_transparent<'tcx>(tcx: TyCtxt<'tcx>, adt: ty::AdtDef<'tcx>)
let mut prev_unsuited_1zst = false;
for field in field_infos {
if field.trivial
&& let Some(unsuited) = check_unsuited(tcx, typing_env, field.ty).break_value()
&& let Some(unsuited) = check_unsuited(tcx, typing_env, field.ty, false).break_value()
Copy link
Copy Markdown
Member

@RalfJung RalfJung May 1, 2026

Choose a reason for hiding this comment

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

Suggested change
&& let Some(unsuited) = check_unsuited(tcx, typing_env, field.ty, false).break_value()
&& let Some(unsuited) = check_unsuited(tcx, typing_env, field.ty, /* inside_repr_rust_packed_1 */ false).break_value()

View changes since the review

{
// If there are any non-trivial fields, then there can be no non-exhaustive 1-zsts.
// Otherwise, it's only an issue if there's >1 non-exhaustive 1-zst.
if non_trivial_count > 0 || prev_unsuited_1zst {
if matches!(unsuited.reason, UnsuitedReason::Array) {
#[derive(Diagnostic)]
#[diag("😱 Crater REGRESSION 😱")]
struct CraterFail {
#[primary_span]
span: Span,
}

tcx.dcx().emit_err(CraterFail { span: field.span });
}

tcx.emit_node_span_lint(
REPR_TRANSPARENT_NON_ZST_FIELDS,
tcx.local_def_id_to_hir_id(adt.did().expect_local()),
Expand Down
4 changes: 2 additions & 2 deletions src/tools/miri/tests/pass/function_calls/abi_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ fn test_abi_newtype<T: Copy + Default>() {
struct Wrapper2a<T>((), T);
#[repr(transparent)]
#[derive(Copy, Clone)]
struct Wrapper3<T>(Zst, T, [u8; 0]);
struct Wrapper3<T>(Zst, T, [(); 42]);

let t = T::default();
test_abi_compat(t, Wrapper(t));
test_abi_compat(t, Wrapper2(t, ()));
test_abi_compat(t, Wrapper2a((), t));
test_abi_compat(t, Wrapper3(Zst, t, []));
test_abi_compat(t, Wrapper3(Zst, t, [(); 42]));
test_abi_compat(t, mem::MaybeUninit::new(t)); // MaybeUninit is `repr(transparent)`
}

Expand Down
2 changes: 1 addition & 1 deletion tests/codegen-llvm/repr/transparent.rs
Copy link
Copy Markdown
Member

@RalfJung RalfJung May 1, 2026

Choose a reason for hiding this comment

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

Please add tests at least for

  • newtype around [u16; 0]
  • newtype around [u8; 0] (with a comment explaining that rejecting this is unnecessary but it's not worth the extra logic to accept it)
  • the type generated by ghost

View changes since the review

Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub extern "C" fn test_WithZst(_: WithZst) -> WithZst {
}

#[repr(transparent)]
pub struct WithZeroSizedArray(*const f32, [i8; 0]);
pub struct WithZeroSizedArray(*const f32, [(); 42]);

// CHECK: define{{.*}}ptr @test_WithZeroSizedArray(ptr noundef %_1)
#[no_mangle]
Expand Down
15 changes: 9 additions & 6 deletions tests/ui/abi/compatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,13 @@ macro_rules! test_abi_compatible {
use super::*;
// Declaring a `type` doesn't even check well-formedness, so we also declare a function.
fn check_wf(_x: $t1, _y: $t2) {}
// Test argument and return value, `Rust` and `C` ABIs.
// Test argument and return value in various ABIs.
#[rustc_abi(assert_eq)]
type TestRust = (fn($t1) -> $t1, fn($t2) -> $t2);
#[rustc_abi(assert_eq)]
type TestC = (extern "C" fn($t1) -> $t1, extern "C" fn($t2) -> $t2);
#[rustc_abi(assert_eq)]
type TestSystem = (extern "system" fn($t1) -> $t1, extern "system" fn($t2) -> $t2);
Copy link
Copy Markdown
Member

@RalfJung RalfJung May 1, 2026

Choose a reason for hiding this comment

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

This has nothing to do with repr(transparent), does it?

View changes since the review

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.

With the whole business of the Windows ABI passing some empty structs by pointer, I figured that an extra check in this area can't hurt. I don't think this PR would break anything there, but that's what tests are for

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.

Maybe make this a separate PR then so we don't mix up the discussion.

We run this test on lots of targets, and for almost all of them "system" is the same as "C". So I think this is mostly a waste of time. However you make a good point -- it would make sense to also test some of the target-specific ABIs here. We can just add those under cfg(...) then I think?

}
};
}
Expand Down Expand Up @@ -200,7 +202,8 @@ test_abi_compatible!(isize_int, isize, i64);

// Compatibility of 1-ZST.
test_abi_compatible!(zst_unit, Zst, ());
test_abi_compatible!(zst_array, Zst, [u8; 0]);
test_abi_compatible!(zst_array, Zst, [(); 0]);
test_abi_compatible!(zst_array_2, Zst, [(); 42]);
test_abi_compatible!(nonzero_int, NonZero<i32>, i32);

// `#[repr(C)]` enums should not change ABI based on individual variant inhabitedness.
Expand All @@ -220,7 +223,7 @@ struct TransparentWrapper1<T: ?Sized>(T);
#[repr(transparent)]
struct TransparentWrapper2<T: ?Sized>((), Zst, T);
#[repr(transparent)]
struct TransparentWrapper3<T>(T, [u8; 0], PhantomData<u64>);
struct TransparentWrapper3<T>(T, [Zst; 42], PhantomData<u64>);
#[repr(transparent)]
union TransparentWrapperUnion<T> {
nothing: (),
Expand Down Expand Up @@ -299,14 +302,14 @@ macro_rules! test_nonnull {
test_abi_compatible!(result_ok_unit, Result<(), $t>, $t);
test_abi_compatible!(result_err_zst, Result<$t, Zst>, $t);
test_abi_compatible!(result_ok_zst, Result<Zst, $t>, $t);
test_abi_compatible!(result_err_arr, Result<$t, [i8; 0]>, $t);
test_abi_compatible!(result_ok_arr, Result<[i8; 0], $t>, $t);
test_abi_compatible!(result_err_arr, Result<$t, [(); 42]>, $t);
test_abi_compatible!(result_ok_arr, Result<[(); 42], $t>, $t);
test_abi_compatible!(result_err_void, Result<$t, Void>, $t);
test_abi_compatible!(result_ok_void, Result<Void, $t>, $t);
test_abi_compatible!(either_err_zst, Either<$t, Zst>, $t);
test_abi_compatible!(either_ok_zst, Either<Zst, $t>, $t);
test_abi_compatible!(either2_err_zst, Either2<$t, Zst>, $t);
test_abi_compatible!(either2_err_arr, Either2<$t, [i8; 0]>, $t);
test_abi_compatible!(either2_err_arr, Either2<$t, [(); 42]>, $t);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/layout/homogeneous-aggr-transparent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct Wrapper1<T>(T);
#[repr(transparent)]
struct Wrapper2<T>((), Zst, T);
#[repr(transparent)]
struct Wrapper3<T>(T, [u8; 0], PhantomData<u64>);
struct Wrapper3<T>(T, [Zst; 42], PhantomData<u64>);
#[repr(transparent)]
union WrapperUnion<T: Copy> {
nothing: (),
Expand Down
Loading