rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.34k stars 12.72k forks source link

Compiling ndarray: alignment mismatch between ABI and layout #99836

Closed cuviper closed 2 years ago

cuviper commented 2 years ago

Code

ndarray master as of ddef4d280fb5abc82325287e68ecacfc81882a1e.

Meta

rustc --version --verbose:

rustc 1.64.0-dev
binary: rustc
commit-hash: unknown
commit-date: unknown
host: x86_64-unknown-linux-gnu
release: 1.64.0-dev
LLVM version: 14.0.6

That's commit 2643b16468fda787470340890212591d8bc832b7 built with this config.toml:

profile = "compiler"
changelog-seen = 2
[rust]
debug-assertions = true

Error output

thread 'rustc' panicked at 'assertion failed: `(left == right)`
  left: `Align(8 bytes)`,
 right: `Align(1 bytes)`: alignment mismatch between ABI and layout in Layout {
    ...
}', compiler/rustc_middle/src/ty/layout.rs:240:21

Note that this assertion is under if cfg!(debug_assertions): https://github.com/rust-lang/rust/blob/2643b16468fda787470340890212591d8bc832b7/compiler/rustc_middle/src/ty/layout.rs#L240-L244

Full output

``` thread 'rustc' panicked at 'assertion failed: `(left == right)` left: `Align(8 bytes)`, right: `Align(1 bytes)`: alignment mismatch between ABI and layout in Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Multiple { tag: Initialized { value: Int( I8, false, ), valid_range: 0..=6, }, tag_encoding: Niche { dataful_variant: 1, niche_variants: 0..=0, niche_start: 0, }, tag_field: 0, variants: [ Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 0, }, abi: Aggregate { sized: true, }, largest_niche: None, align: AbiAndPrefAlign { abi: Align(8 bytes), pref: Align(8 bytes), }, size: Size(0 bytes), }, Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 1, }, abi: Scalar( Initialized { value: Int( I8, false, ), valid_range: 1..=6, }, ), largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I8, false, ), valid_range: 1..=6, }, ), align: AbiAndPrefAlign { abi: Align(1 bytes), pref: Align(8 bytes), }, size: Size(1 bytes), }, ], }, abi: Scalar( Initialized { value: Int( I8, false, ), valid_range: 0..=6, }, ), largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I8, false, ), valid_range: 0..=6, }, ), align: AbiAndPrefAlign { abi: Align(8 bytes), pref: Align(8 bytes), }, size: Size(1 bytes), }', compiler/rustc_middle/src/ty/layout.rs:240:21 stack backtrace: 0: rust_begin_unwind 1: core::panicking::panic_fmt 2: core::panicking::assert_failed_inner 3: core::panicking::assert_failed:: 4: rustc_middle::ty::layout::sanity_check_layout::check_layout_abi 5: rustc_middle::ty::context::tls::with_context::, rustc_middle::ty::layout::LayoutError>>::{closure#0}, core::result::Result, rustc_middle::ty::layout::LayoutError>>::{closure#0} 6: rustc_middle::ty::layout::layout_of 7: ::with_deps::<>::with_task_impl, core::result::Result, rustc_middle::ty::layout::LayoutError>>::{closure#0}, core::result::Result, rustc_middle::ty::layout::LayoutError>> 8: >::with_task::, core::result::Result, rustc_middle::ty::layout::LayoutError>> 9: rustc_query_system::query::plumbing::get_query:: 10: ::layout_of 11: ::check 12: ::run_lint 13: rustc_mir_transform::pass_manager::run_passes 14: rustc_mir_transform::run_post_borrowck_cleanup_passes 15: rustc_mir_transform::mir_drops_elaborated_and_const_checked 16: ::with_deps::<>::with_task_impl, &rustc_data_structures::steal::Steal>::{closure#0}, &rustc_data_structures::steal::Steal> 17: >::with_task::, &rustc_data_structures::steal::Steal> 18: rustc_query_system::query::plumbing::try_execute_query::, &rustc_data_structures::steal::Steal>> 19: rustc_query_system::query::plumbing::get_query:: 20: ::mir_drops_elaborated_and_const_checked 21: rustc_mir_transform::optimized_mir 22: ::with_deps::<>::with_task_impl::{closure#0}, &rustc_middle::mir::Body> 23: >::with_task:: 24: rustc_query_system::query::plumbing::try_execute_query::> 25: rustc_query_system::query::plumbing::get_query:: 26: ::optimized_mir 27: ::encode_crate_root 28: rustc_metadata::rmeta::encoder::encode_metadata_impl 29: rustc_data_structures::sync::join:: 30: rustc_metadata::rmeta::encoder::encode_metadata 31: rustc_metadata::fs::encode_and_write_metadata 32: rustc_interface::passes::start_codegen 33: ::enter::<::ongoing_codegen::{closure#0}::{closure#0}, core::result::Result, rustc_errors::ErrorGuaranteed>> 34: >>::compute::<::ongoing_codegen::{closure#0}> 35: ::enter::, rustc_errors::ErrorGuaranteed>> 36: rustc_span::with_source_map::, rustc_interface::interface::create_compiler_and_run, rustc_driver::run_compiler::{closure#1}>::{closure#1}> 37: rustc_interface::interface::create_compiler_and_run::, rustc_driver::run_compiler::{closure#1}> 38: >::set::, rustc_driver::run_compiler::{closure#1}>::{closure#0}, core::result::Result<(), rustc_errors::ErrorGuaranteed>> note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. error: internal compiler error: unexpected panic note: the compiler unexpectedly panicked. this is a bug. note: we would appreciate a bug report: https://github.com/rust-lang/rust/issues/new?labels=C-bug%2C+I-ICE%2C+T-compiler&template=ice.md note: rustc 1.64.0-dev running on x86_64-unknown-linux-gnu note: compiler flags: --crate-type lib -C embed-bitcode=no -C debuginfo=2 -C incremental note: some of the compiler flags provided by cargo are hidden query stack during panic: #0 [layout_of] computing layout of `core::result::Result, error::ShapeError>` #1 [mir_drops_elaborated_and_const_checked] elaborating drops for `slice::::try_from` #2 [optimized_mir] optimizing MIR for `slice::::try_from` end of query stack ```

cuviper commented 2 years ago

cc @RalfJung since you re-enabled these assertions in #98968.

cuviper commented 2 years ago

Here's a smaller reproducer:

fn main() {
    enum Error { A, B }

    type Foo = Result<[usize; 0], Error>;

    dbg!(std::mem::size_of::<Foo>());
    dbg!(std::mem::align_of::<Foo>());
}
Output

``` warning: variants `A` and `B` are never constructed --> align.rs:2:18 | 2 | enum Error { A, B } | ----- ^ ^ | | | variants in this enum | = note: `#[warn(dead_code)]` on by default thread 'rustc' panicked at 'assertion failed: `(left == right)` left: `Align(8 bytes)`, right: `Align(1 bytes)`: alignment mismatch between ABI and layout in Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Multiple { tag: Initialized { value: Int( I8, false, ), valid_range: 0..=2, }, tag_encoding: Niche { dataful_variant: 1, niche_variants: 0..=0, niche_start: 2, }, tag_field: 0, variants: [ Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 0, }, abi: Aggregate { sized: true, }, largest_niche: None, align: AbiAndPrefAlign { abi: Align(8 bytes), pref: Align(8 bytes), }, size: Size(0 bytes), }, Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 1, }, abi: Scalar( Initialized { value: Int( I8, false, ), valid_range: 0..=1, }, ), largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I8, false, ), valid_range: 0..=1, }, ), align: AbiAndPrefAlign { abi: Align(1 bytes), pref: Align(8 bytes), }, size: Size(1 bytes), }, ], }, abi: Scalar( Initialized { value: Int( I8, false, ), valid_range: 0..=2, }, ), largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I8, false, ), valid_range: 0..=2, }, ), align: AbiAndPrefAlign { abi: Align(8 bytes), pref: Align(8 bytes), }, size: Size(1 bytes), }', compiler/rustc_middle/src/ty/layout.rs:240:21 stack backtrace: 0: 0x7ff8704d57b4 - ::fmt::h57183f18665a904e 1: 0x7ff870542308 - core::fmt::write::h1fc3741f5707a7e9 2: 0x7ff8704d88e1 - std::io::Write::write_fmt::h5e582d931bc0e652 3: 0x7ff8704d5609 - std::sys_common::backtrace::print::h915370a8cb158d0f 4: 0x7ff8704c2914 - std::panicking::default_hook::{{closure}}::hd0ff95d9e670b00d 5: 0x7ff8704c26c3 - std::panicking::default_hook::h1a88faa49859dbbf 6: 0x7ff870f9f514 - rustc_driver[ba7942d2ed61c3a8]::DEFAULT_HOOK::{closure#0}::{closure#0} 7: 0x7ff8704c2d4f - std::panicking::rust_panic_with_hook::h216d9410c99eb34c 8: 0x7ff870498a37 - std::panicking::begin_panic_handler::{{closure}}::h7a2b559b1f7ec0ab 9: 0x7ff870498974 - std::sys_common::backtrace::__rust_end_short_backtrace::h94cd04d25d25882a 10: 0x7ff8704c2982 - rust_begin_unwind 11: 0x7ff870493663 - core::panicking::panic_fmt::hb503280b62085f97 12: 0x7ff870532a6e - core::panicking::assert_failed_inner::h2c15a679abd16964 13: 0x7ff870ed41ab - core[6398db64b51061bb]::panicking::assert_failed:: 14: 0x7ff8735c1c53 - rustc_middle[f8389151b4735317]::ty::layout::sanity_check_layout::check_layout_abi 15: 0x7ff8735d049e - rustc_middle[f8389151b4735317]::ty::context::tls::with_context::, rustc_middle[f8389151b4735317]::ty::layout::LayoutError>>::{closure#0}, core[6398db64b51061bb]::result::Result, rustc_middle[f8389151b4735317]::ty::layout::LayoutError>>::{closure#0} 16: 0x7ff8735d1e5b - rustc_middle[f8389151b4735317]::ty::layout::layout_of 17: 0x7ff8727c9568 - rustc_query_system[b1d5cf42652e4b5a]::query::plumbing::get_query:: 18: 0x7ff8725f02a3 - ::layout_of 19: 0x7ff87121315a - ::spanned_layout_of 20: 0x7ff871200dee - >::codegen_rvalue_operand 21: 0x7ff8712076d4 - >::codegen_block 22: 0x7ff8711fc976 - rustc_codegen_ssa[8ac719cfe9297717]::mir::codegen_mir:: 23: 0x7ff87121e776 - rustc_codegen_ssa[8ac719cfe9297717]::base::codegen_instance:: 24: 0x7ff8712145a9 - ::define:: 25: 0x7ff8711cfec4 - rustc_codegen_llvm[4c68097bd99075c]::base::compile_codegen_unit::module_codegen 26: 0x7ff8711eb404 - >::with_task::> 27: 0x7ff8711cfaf7 - rustc_codegen_llvm[4c68097bd99075c]::base::compile_codegen_unit 28: 0x7ff87121da9f - rustc_codegen_ssa[8ac719cfe9297717]::base::codegen_crate:: 29: 0x7ff8711a14fd - ::codegen_crate 30: 0x7ff87101bbdb - ::time::, rustc_interface[72861cfa91fc646]::passes::start_codegen::{closure#0}> 31: 0x7ff8710b3fcf - rustc_interface[72861cfa91fc646]::passes::start_codegen 32: 0x7ff87108b96d - ::enter::<::ongoing_codegen::{closure#0}::{closure#0}, core[6398db64b51061bb]::result::Result, rustc_errors[685f6944170c16d8]::ErrorGuaranteed>> 33: 0x7ff87107da4e - >>::compute::<::ongoing_codegen::{closure#0}> 34: 0x7ff870f3be4c - ::enter::, rustc_errors[685f6944170c16d8]::ErrorGuaranteed>> 35: 0x7ff870f1d687 - rustc_span[e49f3a209ef89a16]::with_source_map::, rustc_interface[72861cfa91fc646]::interface::create_compiler_and_run, rustc_driver[ba7942d2ed61c3a8]::run_compiler::{closure#1}>::{closure#1}> 36: 0x7ff870f3cd15 - rustc_interface[72861cfa91fc646]::interface::create_compiler_and_run::, rustc_driver[ba7942d2ed61c3a8]::run_compiler::{closure#1}> 37: 0x7ff870f16f1f - >::set::, rustc_driver[ba7942d2ed61c3a8]::run_compiler::{closure#1}>::{closure#0}, core[6398db64b51061bb]::result::Result<(), rustc_errors[685f6944170c16d8]::ErrorGuaranteed>> 38: 0x7ff870f90719 - std[a7e9441a3df41b71]::sys_common::backtrace::__rust_begin_short_backtrace::, rustc_driver[ba7942d2ed61c3a8]::run_compiler::{closure#1}>::{closure#0}, core[6398db64b51061bb]::result::Result<(), rustc_errors[685f6944170c16d8]::ErrorGuaranteed>>::{closure#0}, core[6398db64b51061bb]::result::Result<(), rustc_errors[685f6944170c16d8]::ErrorGuaranteed>> 39: 0x7ff870f1e231 - std[a7e9441a3df41b71]::panicking::try::, core[6398db64b51061bb]::panic::unwind_safe::AssertUnwindSafe<::spawn_unchecked_, rustc_driver[ba7942d2ed61c3a8]::run_compiler::{closure#1}>::{closure#0}, core[6398db64b51061bb]::result::Result<(), rustc_errors[685f6944170c16d8]::ErrorGuaranteed>>::{closure#0}, core[6398db64b51061bb]::result::Result<(), rustc_errors[685f6944170c16d8]::ErrorGuaranteed>>::{closure#1}::{closure#0}>> 40: 0x7ff870f3547e - <::spawn_unchecked_, rustc_driver[ba7942d2ed61c3a8]::run_compiler::{closure#1}>::{closure#0}, core[6398db64b51061bb]::result::Result<(), rustc_errors[685f6944170c16d8]::ErrorGuaranteed>>::{closure#0}, core[6398db64b51061bb]::result::Result<(), rustc_errors[685f6944170c16d8]::ErrorGuaranteed>>::{closure#1} as core[6398db64b51061bb]::ops::function::FnOnce<()>>::call_once::{shim:vtable#0} 41: 0x7ff8704efd08 - as core::ops::function::FnOnce>::call_once::h2a92cd1facb82b24 42: 0x7ff8704d3e67 - std::sys::unix::thread::Thread::new::thread_start::h7ad153c748885102 43: 0x7ff870293e2d - start_thread 44: 0x7ff8703191b0 - __clone3 45: 0x0 - error: internal compiler error: unexpected panic note: the compiler unexpectedly panicked. this is a bug. note: we would appreciate a bug report: https://github.com/rust-lang/rust/issues/new?labels=C-bug%2C+I-ICE%2C+T-compiler&template=ice.md note: rustc 1.64.0-dev running on x86_64-unknown-linux-gnu query stack during panic: #0 [layout_of] computing layout of `core::result::Result<[usize; 0], main::Error>` end of query stack warning: 1 warning emitted ```

Note that Foo has size 1 and align 8, which seems really bad...

cuviper commented 2 years ago

This can make safe misaligned references: playground

fn main() {
    #[derive(Debug)]
    #[allow(unused)]
    enum Error { A, B }

    type Foo = Result<[usize; 0], Error>;

    dbg!(std::mem::size_of::<Foo>());
    dbg!(std::mem::align_of::<Foo>());

    let x: [Foo; 2] = [Ok([]), Ok([])];
    let r0: &[usize] = x[0].as_ref().unwrap();
    let r1: &[usize] = x[1].as_ref().unwrap();
    eprintln!("{r0:p} {r1:p}");
}
[src/main.rs:8] std::mem::size_of::<Foo>() = 1
[src/main.rs:9] std::mem::align_of::<Foo>() = 8
0x7ffe2c4ba120 0x7ffe2c4ba121
apiraino commented 2 years ago

WG-prioritization assigning priority (Zulip discussion).

@rustbot label -I-prioritize +P-critical

RalfJung commented 2 years ago

Looks like the assertions found a bug in layout computation? The assertions are right I think, and that layout seems very bogus. Layout was wrong already before my PR, we just didn't catch the problem.

cuviper commented 2 years ago

Yeah, now that I've explored further, I agree the assertions caught a real layout bug. AIUI, the size should always be a multiple of the alignment, since we don't have separate notions of size and stride.

For testing older rustc, I simplified it to this:

fn main() {
    type Foo = Result<[usize; 0], bool>;

    println!(
        "size:{} align:{}",
        std::mem::size_of::<Foo>(),
        std::mem::align_of::<Foo>()
    );
}

Up to Rust 1.23, size and align were 8, and then 1.24 dropped to size 1. That corresponds to the time that niche layout was implemented, #45225 (and dropped from 1.23 in #46925), although I didn't bisect that specifically. cc @eddyb

eddyb commented 2 years ago

Ahh, zero-sized aligned types, the bane of layout algorithms...

Likely needs an audit for everywhere we special-case ZSTs and maybe just make them require align == 1 for now.

In this specific case though, we could keep the niche optimization, just need to increase align of the whole type to the max variants[v].fields[f].align (across all variants v and their fields f).

Then it will look like an Option<([usize; 0], bool)>, i.e. one byte for the either-bool-or-2u8 plus 7 padding bytes.

cuviper commented 2 years ago

Likely needs an audit for everywhere we special-case ZSTs and maybe just make them require align == 1 for now.

I don't understand how you could require that -- [T; 0] must still be aligned like T, right?.

RalfJung commented 2 years ago

Yes, and for that reason [T; 0] is not a ZST for many T. In the WG-UCG we started using the term "1-ZST" to refer to types with size 0 and alignment 1; that's really what we usually mean when we say "ZST".

cuviper commented 2 years ago

Oh I see, the places looking for ZSTs should be looking for "1-ZST", where they want no effect on layout.

eddyb commented 2 years ago

Sorry for the confusion - when I wrote:

Likely needs an audit for everywhere we special-case ZSTs and maybe just make them require align == 1 for now.

I should've been more specific instead of "them", and phrased it more like:

Likely needs an audit for everywhere we special-case ZSTs and change those special cases to also check align == 1 for now.

(or used the 1-ZST naming, but I completely forgot we had that)


(click to open old notes, ended up tracking down the bug and was wrong) Those special cases, FWIW, are almost always **ignoring** ZSTs in a larger aggregate (which is also something I should've been clearer about). For example, the one you're hitting for `Result<[usize; 0], Error>` is most likely deciding¹ that the `Ok` variant is "dataless"/C-like *because* it contains only ZSTs, and the layout ends up being based² off of the only "dataful" variant (`Err`), i.e. somewhere between `Error` and `MaybeUninit`, without considering the "dataless" variants *at all*³. ~~You could say that it's ignoring any ZSTs and then later *assuming* it only ignored 1-ZSTs.~~ ~~This is obviously bad, and so the solutions I previously described were:~~ 1. ~~pessimize some types by only ignoring 1-ZSTs in such situations~~ 2. ~~don't make assumptions about what was ignored, and correctly compute their properties~~ But I wanted to be sure I was accurate, so I checked: * ¹ correct, though it only cares about `Err` being "dataful", and ignores ZST-only variants https://github.com/rust-lang/rust/blob/5dda74a48cd50de10539478c1e0b6699bfdab665/compiler/rustc_middle/src/ty/layout.rs#L1181-L1183 * ² correct-ish (see below), e.g. this part: (`st[i]` is the "dataful" variant, i.e. `Err`) https://github.com/rust-lang/rust/blob/5dda74a48cd50de10539478c1e0b6699bfdab665/compiler/rustc_middle/src/ty/layout.rs#L1233-L1234 * ³ wait, I'm wrong? we totally seem to be combining variant alignments: https://github.com/rust-lang/rust/blob/5dda74a48cd50de10539478c1e0b6699bfdab665/compiler/rustc_middle/src/ty/layout.rs#L1215-L1227

Oh, whoops, misunderstood the bug: alignment is 8 so we do in fact consider [usize; 0] for alignment, but we never adjust the size to be a multiple of alignment.

(This playground is what I used to inspect the layout, pretty useful - also, Error could be replaced by bool in the minimal repro, just noticed)


This is what tagged union enum layout does, after it merges variants' align/size: https://github.com/rust-lang/rust/blob/5dda74a48cd50de10539478c1e0b6699bfdab665/compiler/rustc_middle/src/ty/layout.rs#L1355-L1356 To fix the niche-filling codepath, you need to duplicate the above, just after let size = st[i].size();

However, be careful around abi: the match st[i].abi() { cases for Abi::{Scalar,ScalarPair} will need to also have an if align == st[i].align() && size == st[i].size() guard (as Abi::{Scalar,ScalarPair} have expected align/sizes determined entirely by the Abi value and the target spec).

The fact that align == st[i].align() isn't checked today for Abi::{Scalar,ScalarPair} is technically wrong AFAICT, but I can't think of a way to actually exploit layout.align being larger than what is required for a specific layout.abi. (AFAICT the LLVM backend always makes alignments explicit where possible, and always inserts explicit padding where there are any gaps between fields, the only way any of that could go wrong is if Rust alignment was less than the LLVM alignment for the same type, but only the opposite can be true as LLVM has less information and we also force packed LLVM types at the slightest hint of underaligned fields)

cuviper commented 2 years ago

Here's what I've got so far:

diff --git a/compiler/rustc_middle/src/ty/layout.rs b/compiler/rustc_middle/src/ty/layout.rs
index dde55dd96554..833edd228050 100644
--- a/compiler/rustc_middle/src/ty/layout.rs
+++ b/compiler/rustc_middle/src/ty/layout.rs
@@ -1231,11 +1231,15 @@ fn layout_of_uncached(&self, ty: Ty<'tcx>) -> Result<Layout<'tcx>, LayoutError<'
                                 .collect::<Result<IndexVec<VariantIdx, _>, _>>()?;

                             let offset = st[i].fields().offset(field_index) + niche.offset;
-                            let size = st[i].size();
+
+                            // Align the total size to the largest alignment.
+                            let size = st[i].size().align_to(align.abi);

                             let abi = if st.iter().all(|v| v.abi().is_uninhabited()) {
                                 Abi::Uninhabited
-                            } else {
+                            } else if align == st[i].align() && size == st[i].size() {
+                                // When the total alignment and size match, we can use the
+                                // same ABI as the scalar variant with the reserved niche.
                                 match st[i].abi() {
                                     Abi::Scalar(_) => Abi::Scalar(niche_scalar),
                                     Abi::ScalarPair(first, second) => {
@@ -1249,6 +1253,8 @@ fn layout_of_uncached(&self, ty: Ty<'tcx>) -> Result<Layout<'tcx>, LayoutError<'
                                     }
                                     _ => Abi::Aggregate { sized: true },
                                 }
+                            } else {
+                                Abi::Aggregate { sized: true }
                             };

                             let largest_niche = Niche::from_scalar(dl, offset, niche_scalar);

For the example of Result<[usize; 0], bool>, it ends up using a direct tag instead, still size == align == 8.

The original ndarray build also passes all assertions with this change.

eddyb commented 2 years ago

For the example of Result<[usize; 0], bool>, it ends up using a direct tag instead, still size == align == 8.

Ah, does it hit the check between niche-filling and tag, that IIRC might prefer tag (if the sizes are equal) because it likely results in better codegen/more niches in many cases?

Looks like it: https://github.com/rust-lang/rust/blob/5dda74a48cd50de10539478c1e0b6699bfdab665/compiler/rustc_middle/src/ty/layout.rs#L1555-L1560

In that case, it should get the same layout as s/bool/u8/ (since it ends up not using the niche). With some quick testing, I was able to confirm that [usize; 0] in such a tagged union enum gets placed at the very end (i.e. offset 8 in a size 8 type), which matches the respective handwritten #[repr(C)] struct, so I'm happy to say that all looks good.


We might still want a test where the tagged form doesn't happen.

I was able to emulate the fix described above by making the enum "cross-pollinate" alignments:

enum FixedResult<T, E> {
    Ok(T, [E; 0]),
    Err(E, [T; 0]),
}

Such a definition shouldn't be notably different from Result<T, E> (modulo bugs like this very one). (note that you can't just use Result<(T, [E; 0]), (E, [T; 0])> to emulate the fix, as that will put padding inside variant fields, instead of in between them, which can pessimize tagged layouts)

With that, I was able to come up with an extended example on playground:

// Tagged repr is clever enough to grow tags to fill any padding, e.g.:
// 1.   `T_FF` (one byte of Tag, one byte of padding, two bytes of align=2 Field)
//   -> `TTFF` (Tag has expanded to two bytes, i.e. like `#[repr(u16)]`)
// 2.    `TFF` (one byte of Tag, two bytes of align=1 Field)
//   -> Tag has no room to expand!
//   (this outcome can be forced onto 1. by wrapping Field in `Packed<...>`)
#[repr(packed)]
struct Packed<T>(T);

#[rustc_layout(debug)]
type NicheLosesToTagged = FixedResult<[u64; 0], Packed<std::num::NonZeroU16>>;

#[repr(u16)]
enum U16IsZero { _Zero = 0 }

#[rustc_layout(debug)]
type NicheWinsOverTagged = FixedResult<[u64; 0], Packed<U16IsZero>>;

Relevant parts of the output (cleaned up a bit):

layout_of(FixedResult<[u64; 0], Packed<std::num::NonZeroU16>>) = Layout {
    // ...
    variants: Multiple {
        tag: Initialized {
            value: Int(I8, false),
            valid_range: 0..=1,
        },
        tag_encoding: Direct,
        // ...
    },
    // ...
    align: AbiAndPrefAlign {
        abi: Align(8 bytes),
        pref: Align(8 bytes),
    },
    size: Size(8 bytes),
}
layout_of(FixedResult<[u64; 0], Packed<U16IsZero>>) = Layout {
    // ...
    variants: Multiple {
        tag: Initialized {
            value: Int(I16, false),
            valid_range: 0..=1,
        },
        tag_encoding: Niche {
            dataful_variant: 1,
            niche_variants: 0..=0,
            niche_start: 1,
        },
        // ..
    },
    // ...
    align: AbiAndPrefAlign {
        abi: Align(8 bytes),
        pref: Align(8 bytes),
    },
    size: Size(8 bytes),
}

To be clear, you should be able to get those exact results with Result instead of FixedResult, on your branch.

cuviper commented 2 years ago

That's a nice test, thanks! And indeed, nightly currently makes those both niche with align==8 && size==2, while my branch makes them direct and niche respectively with align == size == 8.