Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions compiler/rustc_abi/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ impl<Cx: HasDataLayout> LayoutCalculator<Cx> {

let niche_filling_layout = calculate_niche_filling_layout();

let discr_type = repr.discr_type();
let discr_type = repr.discr_type(dl);
let discr_int = Integer::from_attr(dl, discr_type);
// Because we can only represent one range of valid values, we'll look for the
// largest range of invalid values and pick everything else as the range of valid
Expand Down Expand Up @@ -875,7 +875,7 @@ impl<Cx: HasDataLayout> LayoutCalculator<Cx> {
return Err(LayoutCalculatorError::SizeOverflow);
}

let typeck_ity = Integer::from_attr(dl, repr.discr_type());
let typeck_ity = Integer::from_attr(dl, repr.discr_type(dl));
if typeck_ity < min_ity {
// It is a bug if Layout decided on a greater discriminant size than typeck for
// some reason at this point (based on values discriminant can take on). Mostly
Expand Down
23 changes: 20 additions & 3 deletions compiler/rustc_abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,22 @@ impl ReprOptions {

/// Returns the discriminant type, given these `repr` options.
/// This must only be called on enums!
pub fn discr_type(&self) -> IntegerType {
self.int.unwrap_or(IntegerType::Pointer(true))
///
/// This is the "typeck type" of the discriminant, which is effectively the maximum size:
/// discriminant values will be wrapped to fit (with a lint). Layout can later decide to use a
/// smaller type (which it will do depending on the actual discriminant values, also enforcing
/// `c_enum_min_size` along the way) and that will work just fine, it just induces casts when
/// getting/setting the discriminant.
pub fn discr_type(&self, cx: &impl HasDataLayout) -> IntegerType {
self.int.unwrap_or(
if self.c()
&& let Some(max_size) = cx.data_layout().c_enum_max_size
{
IntegerType::Fixed(max_size, true)
} else {
IntegerType::Pointer(true)
},
)
}

/// Returns `true` if this `#[repr()]` should inhabit "smart enum
Expand Down Expand Up @@ -274,6 +288,8 @@ pub struct TargetDataLayout {
/// Note: This isn't in LLVM's data layout string, it is `short_enum`
/// so the only valid spec for LLVM is c_int::BITS or 8
pub c_enum_min_size: Integer,
/// Maximum size of #[repr(C)] enums (defaults to pointer size).
pub c_enum_max_size: Option<Integer>,
Copy link
Member

Choose a reason for hiding this comment

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

kinda feels like we should just have a Range here, but on the other hand ranges usually have an unwieldy API so I'm not sure that's actually a good idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also how would the range look like in JSON?

Also, the top end of the range is optional so it can be pointer-sized (Integer only has fixed-sized variants). Though maybe that's not actually correct and the pointer size has no influence on the enum size in C.

Copy link
Member

@workingjubilee workingjubilee Sep 13, 2025

Choose a reason for hiding this comment

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

That does indeed seem to be an incorrect assumption. ( Apologies as I am probably going to reiterate things you have already read a bit, but there are a few comments I have to give along the way. )

C11

C11 says only that the enum's representation should fit an int:

The expression that defines the value of an enumeration constant shall be an integer constant expression that has a value representable as an int.

This would seem to disallow using something that only fits in a long, but then the following is specified:

Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined but shall be capable of representing the values of all the members of the enumeration.

This is what suggests that you can just do whatever in terms of sizing the C enum. Some use this to shrink enums to char, thus our usage of c_enum_min_size to begin with, but the previous clause seems to ban using long int and thus seems to mandate a maximum.

Now, as we know, most C compilers except MSVC disregard this and allow long-fitted values in practice. This is partially because C++ allows larger enums, which also have specified underlying integer types (like Rust does).

C23

So, they updated to allow the C++ form, and then updated the logic to make clearer that the real requirement is that there be a unifying underlying type:

All enumerations have an underlying type. The underlying type can be explicitly specified using an enum type specifier and is its fixed underlying type. If it is not explicitly specified, the underlying type is the enumeration’s compatible type, which is either char or a standard or extended signed or unsigned integer type.

An "extended... integer type" can include a "vendor type", as with __int128 on some platforms (so, i128). By saying "standard or extended... integer type", they have opened the door to a compiler even using essentially any size of integer type. They then reiterate that there must be a unifying type for the enum (emphasis mine):

For all the integer constant expressions which make up the values of the enumeration constants, there shall be a type capable of representing all the values that is a standard or extended signed or unsigned integer type, or char.

Note however that the reality that the enumeration members, as syntactically interacted with by programmers, are really constants, remains in effect. That means that if there is no fixed underlying type named by the programmer, then they have their own types when named as constants, which may differ from the type of the enum. This becomes visible when you do a cast, as given by 6.7.7.3.20 (EXAMPLE 4):

enum no_underlying {
    a0
};

int main (void) {
    int a = _Generic(a0,
        int: 2,
        unsigned char: 1,
        default: 0
    );
    int b = _Generic((enum no_underlying)a0,
        int: 2,
        unsigned char: 1,
        default: 0
    );
    return a + b;
}

The value returned is implementation-defined, based on whether the underlying type picked by the compiler for the enum is an int or an unsigned char. If an underlying type has been specified using the equivalent to our repr(u8):

enum underlying: unsigned char {
    a0
};

Then we have a guaranteed value, because we have a guaranteed type for every one of the enum's constants.

Copy link
Member

@workingjubilee workingjubilee Sep 13, 2025

Choose a reason for hiding this comment

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

...Mind, the enumeration constant detail is pure nonsense from Rust's POV, so it mostly serves to explain why C compilers have such a rough time figuring out how to handle this, since they didn't have the good sense to not extend the language with implementation-defined "sure, why not?"s. On some level they have to simultaneously pretend their enums are both ints and then some other type.

Here, it does mean that "pointer-size" for a C enum would seem to be meaningless.

Copy link
Member

@workingjubilee workingjubilee Sep 13, 2025

Choose a reason for hiding this comment

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

Hmm. Thinking about it, I guess they can hypothetically pick intptr_t as the type they're using, if that's a type that is held as distinct from any other integer type instead of a typedef? Hmm. I kinda think that's not going to be the case, though, and the limits will be arbitrary instead.

}

impl Default for TargetDataLayout {
Expand Down Expand Up @@ -307,6 +323,7 @@ impl Default for TargetDataLayout {
address_space_info: vec![],
instruction_address_space: AddressSpace::ZERO,
c_enum_min_size: Integer::I32,
c_enum_max_size: None,
}
}
}
Expand All @@ -327,7 +344,7 @@ impl TargetDataLayout {
/// [llvm data layout string](https://llvm.org/docs/LangRef.html#data-layout)
///
/// This function doesn't fill `c_enum_min_size` and it will always be `I32` since it can not be
/// determined from llvm string.
/// determined from llvm string. Likewise, it does not fill in `c_enum_max_size`.
pub fn parse_from_llvm_datalayout_string<'a>(
input: &'a str,
default_address_space: AddressSpace,
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_hir_analysis/src/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ pub(super) fn lower_variant_ctor(tcx: TyCtxt<'_>, def_id: LocalDefId) {

pub(super) fn lower_enum_variant_types(tcx: TyCtxt<'_>, def_id: DefId) {
let def = tcx.adt_def(def_id);
let repr_type = def.repr().discr_type();
let repr_type = def.repr().discr_type(&tcx);
let initial = repr_type.initial_discriminant(tcx);
let mut prev_discr = None::<Discr<'_>>;

Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_hir_analysis/src/collect/type_of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fn anon_const_type_of<'tcx>(icx: &ItemCtxt<'tcx>, def_id: LocalDefId) -> Ty<'tcx
}) if anon_hir_id == hir_id => const_arg_anon_type_of(icx, arg_hir_id, span),

Node::Variant(Variant { disr_expr: Some(e), .. }) if e.hir_id == hir_id => {
tcx.adt_def(tcx.hir_get_parent_item(hir_id)).repr().discr_type().to_ty(tcx)
tcx.adt_def(tcx.hir_get_parent_item(hir_id)).repr().discr_type(&tcx).to_ty(tcx)
}
// Sort of affects the type system, but only for the purpose of diagnostics
// so no need for ConstArg.
Expand Down
6 changes: 3 additions & 3 deletions compiler/rustc_middle/src/ty/adt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ impl<'tcx> AdtDef<'tcx> {
) -> Result<Discr<'tcx>, ErrorGuaranteed> {
assert!(self.is_enum());

let repr_type = self.repr().discr_type();
let repr_type = self.repr().discr_type(&tcx);
match tcx.const_eval_poly(expr_did) {
Ok(val) => {
let typing_env = ty::TypingEnv::post_analysis(tcx, expr_did);
Expand Down Expand Up @@ -561,7 +561,7 @@ impl<'tcx> AdtDef<'tcx> {
tcx: TyCtxt<'tcx>,
) -> impl Iterator<Item = (VariantIdx, Discr<'tcx>)> {
assert!(self.is_enum());
let repr_type = self.repr().discr_type();
let repr_type = self.repr().discr_type(&tcx);
let initial = repr_type.initial_discriminant(tcx);
let mut prev_discr = None::<Discr<'tcx>>;
self.variants().iter_enumerated().map(move |(i, v)| {
Expand Down Expand Up @@ -600,7 +600,7 @@ impl<'tcx> AdtDef<'tcx> {
{
val
} else {
self.repr().discr_type().initial_discriminant(tcx)
self.repr().discr_type(&tcx).initial_discriminant(tcx)
};
explicit_value.checked_add(tcx, offset as u128).0
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_middle/src/ty/sty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1589,7 +1589,7 @@ impl<'tcx> Ty<'tcx> {
/// Returns the type of the discriminant of this type.
pub fn discriminant_ty(self, tcx: TyCtxt<'tcx>) -> Ty<'tcx> {
match self.kind() {
ty::Adt(adt, _) if adt.is_enum() => adt.repr().discr_type().to_ty(tcx),
ty::Adt(adt, _) if adt.is_enum() => adt.repr().discr_type(&tcx).to_ty(tcx),
ty::Coroutine(_, args) => args.as_coroutine().discr_ty(tcx),

ty::Param(_) | ty::Alias(..) | ty::Infer(ty::TyVar(_)) => {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir_build/src/builder/expr/as_rvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
let (source, ty) = if let ty::Adt(adt_def, ..) = source_expr.ty.kind()
&& adt_def.is_enum()
{
let discr_ty = adt_def.repr().discr_type().to_ty(this.tcx);
let discr_ty = adt_def.repr().discr_type(&this.tcx).to_ty(this.tcx);
let temp = unpack!(block = this.as_temp(block, scope, source, Mutability::Not));
let discr = this.temp(discr_ty, source_expr.span);
this.cfg.push_assign(
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir_build/src/builder/matches/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
otherwise_block,
);
debug!("num_enum_variants: {}", adt_def.variants().len());
let discr_ty = adt_def.repr().discr_type().to_ty(self.tcx);
let discr_ty = adt_def.repr().discr_type(&self.tcx).to_ty(self.tcx);
let discr = self.temp(discr_ty, test.span);
self.cfg.push_assign(
block,
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir_build/src/thir/cx/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ impl<'tcx> ThirBuildCx<'tcx> {
let (discr_did, discr_offset) = adt_def.discriminant_def_for_variant(idx);

use rustc_middle::ty::util::IntTypeExt;
let ty = adt_def.repr().discr_type();
let ty = adt_def.repr().discr_type(&tcx);
let discr_ty = ty.to_ty(tcx);

let size = tcx
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir_transform/src/elaborate_drop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,7 @@ where
// Additionally, we do not want to switch on the
// discriminant after it is free-ed, because that
// way lies only trouble.
let discr_ty = adt.repr().discr_type().to_ty(self.tcx());
let discr_ty = adt.repr().discr_type(&self.tcx()).to_ty(self.tcx());
let discr = Place::from(self.new_temp(discr_ty));
let discr_rv = Rvalue::Discriminant(self.place);
let switch_block = BasicBlockData::new_stmts(
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_mir_transform/src/large_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl<'tcx> crate::MirPass<'tcx> for EnumSizeOpt {
let const_assign = StatementKind::Assign(Box::new((place, rval)));

let discr_place =
Place::from(patch.new_temp(adt_def.repr().discr_type().to_ty(tcx), span));
Place::from(patch.new_temp(adt_def.repr().discr_type(&tcx).to_ty(tcx), span));
let store_discr =
StatementKind::Assign(Box::new((discr_place, Rvalue::Discriminant(*rhs))));

Expand Down
5 changes: 5 additions & 0 deletions compiler/rustc_target/src/spec/base/msvc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ pub(crate) fn opts() -> TargetOptions {
emit_debug_gdb_scripts: false,
archive_format: "coff".into(),

// MSVC does not seem to ever automatically increase enums beyond their default size (see
// <https://github.com/rust-lang/rust/issues/124403>, <https://godbolt.org/z/1Pdb3hP9E>).
c_enum_min_bits: Some(32),
c_enum_max_bits: Some(32),
Copy link
Member Author

Choose a reason for hiding this comment

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

This also affects the UEFI targets. Is that intended?

Copy link
Member

Choose a reason for hiding this comment

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

In general, they use the MSVC ABI for things, yes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Does the psABI document the enum size rules? I would be surprised if it did.^^


// Currently this is the only supported method of debuginfo on MSVC
// where `*.pdb` files show up next to the final artifact.
split_debuginfo: SplitDebuginfo::Packed,
Expand Down
10 changes: 10 additions & 0 deletions compiler/rustc_target/src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2386,6 +2386,13 @@ impl Target {
self.c_enum_min_bits.unwrap_or(self.c_int_width as _),
))
.map_err(|err| TargetDataLayoutErrors::InvalidBitsSize { err })?;
dl.c_enum_max_size = match self.c_enum_max_bits {
None => None,
Some(max_bits) => Some(
Integer::from_size(Size::from_bits(max_bits))
.map_err(|err| TargetDataLayoutErrors::InvalidBitsSize { err })?,
),
};

Ok(dl)
}
Expand Down Expand Up @@ -2797,6 +2804,8 @@ pub struct TargetOptions {

/// Minimum number of bits in #[repr(C)] enum. Defaults to the size of c_int
pub c_enum_min_bits: Option<u64>,
/// Maximum number of bits in #[repr(C)] enum. Defaults to the pointer size.
pub c_enum_max_bits: Option<u64>,

/// Whether or not the DWARF `.debug_aranges` section should be generated.
pub generate_arange_section: bool,
Expand Down Expand Up @@ -3044,6 +3053,7 @@ impl Default for TargetOptions {
supported_split_debuginfo: Cow::Borrowed(&[SplitDebuginfo::Off]),
supported_sanitizers: SanitizerSet::empty(),
c_enum_min_bits: None,
c_enum_max_bits: None,
generate_arange_section: true,
supports_stack_protector: true,
entry_name: "main".into(),
Expand Down
2 changes: 1 addition & 1 deletion src/tools/clippy/clippy_lints/src/enum_clike.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl<'tcx> LateLintPass<'tcx> for UnportableVariant {
if let ty::Adt(adt, _) = ty.kind()
&& adt.is_enum()
{
ty = adt.repr().discr_type().to_ty(cx.tcx);
ty = adt.repr().discr_type(&cx.tcx).to_ty(cx.tcx);
}
match ty.kind() {
ty::Int(IntTy::Isize) => {
Expand Down
3 changes: 2 additions & 1 deletion src/tools/rust-analyzer/crates/hir-ty/src/chalk_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ impl chalk_solve::RustIrDatabase<Interner> for ChalkContext<'_> {
&& let hir_def::AdtId::EnumId(e) = id.0
{
let enum_data = self.db.enum_signature(e);
let ty = enum_data.repr.unwrap_or_default().discr_type();
let dl = self.db.target_data_layout(self.krate).expect("FIXME");
let ty = enum_data.repr.unwrap_or_default().discr_type(&*dl);
return chalk_ir::TyKind::Scalar(match ty {
hir_def::layout::IntegerType::Pointer(is_signed) => match is_signed {
true => chalk_ir::Scalar::Int(chalk_ir::IntTy::Isize),
Expand Down
58 changes: 56 additions & 2 deletions tests/auxiliary/minicore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,18 +162,59 @@ macro_rules! stringify {
};
}

#[lang = "neg"]
pub trait Neg {
type Output;

fn neg(self) -> Self::Output;
}

impl Neg for i32 {
type Output = i32;

fn neg(self) -> i32 {
loop {} // Dummy impl, not actually used
}
}

impl Neg for isize {
type Output = isize;

fn neg(self) -> isize {
loop {} // Dummy impl, not actually used
}
}

#[lang = "add"]
pub trait Add<Rhs = Self> {
type Output;

fn add(self, _: Rhs) -> Self::Output;
}

impl Add<isize> for isize {
impl Add for isize {
type Output = isize;

fn add(self, other: isize) -> isize {
7 // avoid needing to add all of the overflow handling and panic language items
loop {} // Dummy impl, not actually used
}
}

#[lang = "eq"]
pub trait PartialEq<Rhs = Self> {
fn eq(&self, other: &Rhs) -> bool;

fn ne(&self, other: &Rhs) -> bool {
match self.eq(other) {
true => false,
false => true,
}
}
}

impl PartialEq for usize {
fn eq(&self, other: &Self) -> bool {
loop {} // Dummy impl, not actually used
}
}

Expand Down Expand Up @@ -231,6 +272,19 @@ pub mod mem {
#[rustc_nounwind]
#[rustc_intrinsic]
pub unsafe fn transmute<Src, Dst>(src: Src) -> Dst;

#[rustc_nounwind]
#[rustc_intrinsic]
pub const fn size_of<T>() -> usize;
#[rustc_nounwind]
#[rustc_intrinsic]
pub const fn align_of<T>() -> usize;
}

pub mod hint {
#[rustc_nounwind]
#[rustc_intrinsic]
pub const unsafe fn unreachable() -> !;
}

#[lang = "c_void"]
Expand Down
28 changes: 28 additions & 0 deletions tests/ui/enum-discriminant/repr-c-size.linux32.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
warning: literal out of range for `isize`
--> $DIR/repr-c-size.rs:24:9
|
LL | A = 9223372036854775807, // i64::MAX
| ^^^^^^^^^^^^^^^^^^^
|
= note: the literal `9223372036854775807` does not fit into the type `isize` whose range is `-2147483648..=2147483647`
note: the lint level is defined here
--> $DIR/repr-c-size.rs:22:8
|
LL | #[warn(overflowing_literals)]
| ^^^^^^^^^^^^^^^^^^^^

warning: literal out of range for `isize`
--> $DIR/repr-c-size.rs:43:9
|
LL | A = 4294967294, // u32::MAX - 1
| ^^^^^^^^^^
|
= note: the literal `4294967294` does not fit into the type `isize` whose range is `-2147483648..=2147483647`
note: the lint level is defined here
--> $DIR/repr-c-size.rs:41:8
|
LL | #[warn(overflowing_literals)]
| ^^^^^^^^^^^^^^^^^^^^

warning: 2 warnings emitted

30 changes: 30 additions & 0 deletions tests/ui/enum-discriminant/repr-c-size.msvc32.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
warning: literal out of range for `i32`
--> $DIR/repr-c-size.rs:24:9
|
LL | A = 9223372036854775807, // i64::MAX
| ^^^^^^^^^^^^^^^^^^^
|
= note: the literal `9223372036854775807` does not fit into the type `i32` whose range is `-2147483648..=2147483647`
= help: consider using the type `i64` instead
note: the lint level is defined here
--> $DIR/repr-c-size.rs:22:8
|
LL | #[warn(overflowing_literals)]
| ^^^^^^^^^^^^^^^^^^^^

warning: literal out of range for `i32`
--> $DIR/repr-c-size.rs:43:9
|
LL | A = 4294967294, // u32::MAX - 1
| ^^^^^^^^^^
|
= note: the literal `4294967294` does not fit into the type `i32` whose range is `-2147483648..=2147483647`
= help: consider using the type `u32` instead
note: the lint level is defined here
--> $DIR/repr-c-size.rs:41:8
|
LL | #[warn(overflowing_literals)]
| ^^^^^^^^^^^^^^^^^^^^

warning: 2 warnings emitted

Loading
Loading