Question:
master as of . <!-- If you're using the stable version of the compiler, you should also check if the bug also exists in the beta or nightly versions. --: That's commit built with this : Note that this assertion is under : <details<summary<strongFull output</strong</summary<p</p</details
cc since you re-enabled these assertions in .
Here's a smaller reproducer: <details<summary<strongOutput</strong</summary<p</p</detailsNote that has size 1 and align 8, which seems really bad...
This can make safe misaligned references:
WG-prioritization assigning priority (). label -I-prioritize +P-critical
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.
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 , I simplified it to this: 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, (and dropped from 1.23 in ), although I didn't bisect that specifically. cc
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 (across all variants and their fields ). Then it will look like an , i.e. one byte for the either--or- plus 7 padding bytes.
I don't understand how you could require that -- must still be aligned like , right?.
Yes, and for that reason is not a ZST for many . 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".
Oh I see, the places looking for ZSTs should be looking for "1-ZST", where they want no effect on layout.
Sorry for the confusion - when I wrote: I should've been more specific instead of "them", and phrased it more like: (or used the 1-ZST naming, but I completely forgot we had that) <hr/<details<summary(click to open old notes, ended up tracking down the bug and was wrong)</summaryThose 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 is most likely deciding¹ that the variant is "dataless"/C-like because it contains only ZSTs, and the layout ends up being based² off of the only "dataful" variant (), i.e. somewhere between and , without considering the "dataless" variants at all³. But I wanted to be sure I was accurate, so I checked: ¹ correct, though it only cares about being "dataful", and ignores ZST-only variants ² correct-ish (see below), e.g. this part: ( is the "dataful" variant, i.e. ) ³ wait, I'm wrong? we totally seem to be combining variant alignments: </details<hr/Oh, whoops, misunderstood the bug: alignment is so we do in fact consider for alignment, but we never adjust the size to be a multiple of alignment. ( is what I used to inspect the layout, pretty useful - also, could be replaced by in the minimal repro, just noticed) <hr/This is what tagged union layout does, after it merges variants' /: To fix the niche-filling codepath, you need to duplicate the above, just after  However, be careful around : will need to also have an guard (as have expected align/sizes determined entirely by the value and the target spec). The fact that isn't checked today for is technically wrong AFAICT, but I can't think of a way to actually exploit being larger than what is required for a specific . <sub(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)</sub
Here's what I've got so far: For the example of , it ends up using a direct tag instead, still . The original build also passes all assertions with this change.
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: In that case, it should get the same layout as s/bool/u8/ (since it ends up not using the niche). , I was able to confirm that in such a tagged union gets placed at the very end (i.e. offset 8 in a size 8 type), which matches the respective handwritten struct, so I'm happy to say that all looks good. <hr/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 "cross-pollinate" alignments: Such a definition shouldn't be notably different from (modulo bugs like this very one). (note that you can't just use 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 : Relevant parts of the output (cleaned up a bit): To be clear, you should be able to get those exact results with instead of , on your branch.
That's a nice test, thanks! And indeed, nightly currently makes those both niche with , while my branch makes them direct and niche respectively with .

} _ => Abi::Aggregate { sized: true }, } <ins> } else { Abi::Aggregate { sized: true } </ins> }; let largest_niche = Niche::from_scalar(dl, offset, niche_scalar);


Question:
master as of . <!-- If you're using the stable version of the compiler, you should also check if the bug also exists in the beta or nightly versions. --: That's commit built with this : Note that this assertion is under : <details<summary<strongFull output</strong</summary<p</p</details
cc since you re-enabled these assertions in .
Here's a smaller reproducer: <details<summary<strongOutput</strong</summary<p</p</detailsNote that has size 1 and align 8, which seems really bad...
This can make safe misaligned references:
WG-prioritization assigning priority (). label -I-prioritize +P-critical
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.
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 , I simplified it to this: 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, (and dropped from 1.23 in ), although I didn't bisect that specifically. cc
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 (across all variants and their fields ). Then it will look like an , i.e. one byte for the either--or- plus 7 padding bytes.
I don't understand how you could require that -- must still be aligned like , right?.
Yes, and for that reason is not a ZST for many . 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".
Oh I see, the places looking for ZSTs should be looking for "1-ZST", where they want no effect on layout.
Sorry for the confusion - when I wrote: I should've been more specific instead of "them", and phrased it more like: (or used the 1-ZST naming, but I completely forgot we had that) <hr/<details<summary(click to open old notes, ended up tracking down the bug and was wrong)</summaryThose 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 is most likely deciding¹ that the variant is "dataless"/C-like because it contains only ZSTs, and the layout ends up being based² off of the only "dataful" variant (), i.e. somewhere between and , without considering the "dataless" variants at all³. But I wanted to be sure I was accurate, so I checked: ¹ correct, though it only cares about being "dataful", and ignores ZST-only variants ² correct-ish (see below), e.g. this part: ( is the "dataful" variant, i.e. ) ³ wait, I'm wrong? we totally seem to be combining variant alignments: </details<hr/Oh, whoops, misunderstood the bug: alignment is so we do in fact consider for alignment, but we never adjust the size to be a multiple of alignment. ( is what I used to inspect the layout, pretty useful - also, could be replaced by in the minimal repro, just noticed) <hr/This is what tagged union layout does, after it merges variants' /: To fix the niche-filling codepath, you need to duplicate the above, just after  However, be careful around : will need to also have an guard (as have expected align/sizes determined entirely by the value and the target spec). The fact that isn't checked today for is technically wrong AFAICT, but I can't think of a way to actually exploit being larger than what is required for a specific . <sub(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)</sub
Here's what I've got so far: For the example of , it ends up using a direct tag instead, still . The original build also passes all assertions with this change.
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: In that case, it should get the same layout as s/bool/u8/ (since it ends up not using the niche). , I was able to confirm that in such a tagged union gets placed at the very end (i.e. offset 8 in a size 8 type), which matches the respective handwritten struct, so I'm happy to say that all looks good. <hr/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 "cross-pollinate" alignments: Such a definition shouldn't be notably different from (modulo bugs like this very one). (note that you can't just use 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 : Relevant parts of the output (cleaned up a bit): To be clear, you should be able to get those exact results with instead of , on your branch.
That's a nice test, thanks! And indeed, nightly currently makes those both niche with , while my branch makes them direct and niche respectively with .

<ins> // normalize-stderr-test "pref: Align([1-8] bytes)" -> "pref: $$PREF_ALIGN" #![feature(rustc_attrs)] #![crate_type = "lib"] // Various tests around the behavior of zero-sized arrays and // enum niches, especially that they have coherent size and alignment. // The original problem in #99836 came from ndarray's `TryFrom` for // `SliceInfo<[SliceInfoElem; 0], Din, Dout>`, where that returns // `Result<Self, ShapeError>` ~= `Result<AlignedZST, TypeWithNiche>`. // This is a close enough approximation: #[rustc_layout(debug)] type AlignedResult = Result<[u32; 0], bool>; //~ ERROR: layout_of // The bug gave that size 1 with align 4, but the size should also be 4. // It was also using the bool niche for the enum tag, which is fine, but // after the fix, layout decides to use a direct tagged repr instead. // Here's another case with multiple ZST alignments, where we should // get the maximal alignment and matching size. #[rustc_layout(debug)] enum MultipleAlignments { //~ ERROR: layout_of Align2([u16; 0]), Align4([u32; 0]), Niche(bool), } // 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 = Result<[u32; 0], Packed<std::num::NonZeroU16>>; //~ ERROR: layout_of // Should get tag_encoding: Direct, size == align == 4. #[repr(u16)] enum U16IsZero { _Zero = 0 } #[rustc_layout(debug)] type NicheWinsOverTagged = Result<[u32; 0], Packed<U16IsZero>>; //~ ERROR: layout_of // Should get tag_encoding: Niche, size == align == 4. </ins>


Question:
master as of . <!-- If you're using the stable version of the compiler, you should also check if the bug also exists in the beta or nightly versions. --: That's commit built with this : Note that this assertion is under : <details<summary<strongFull output</strong</summary<p</p</details
cc since you re-enabled these assertions in .
Here's a smaller reproducer: <details<summary<strongOutput</strong</summary<p</p</detailsNote that has size 1 and align 8, which seems really bad...
This can make safe misaligned references:
WG-prioritization assigning priority (). label -I-prioritize +P-critical
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.
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 , I simplified it to this: 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, (and dropped from 1.23 in ), although I didn't bisect that specifically. cc
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 (across all variants and their fields ). Then it will look like an , i.e. one byte for the either--or- plus 7 padding bytes.
I don't understand how you could require that -- must still be aligned like , right?.
Yes, and for that reason is not a ZST for many . 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".
Oh I see, the places looking for ZSTs should be looking for "1-ZST", where they want no effect on layout.
Sorry for the confusion - when I wrote: I should've been more specific instead of "them", and phrased it more like: (or used the 1-ZST naming, but I completely forgot we had that) <hr/<details<summary(click to open old notes, ended up tracking down the bug and was wrong)</summaryThose 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 is most likely deciding¹ that the variant is "dataless"/C-like because it contains only ZSTs, and the layout ends up being based² off of the only "dataful" variant (), i.e. somewhere between and , without considering the "dataless" variants at all³. But I wanted to be sure I was accurate, so I checked: ¹ correct, though it only cares about being "dataful", and ignores ZST-only variants ² correct-ish (see below), e.g. this part: ( is the "dataful" variant, i.e. ) ³ wait, I'm wrong? we totally seem to be combining variant alignments: </details<hr/Oh, whoops, misunderstood the bug: alignment is so we do in fact consider for alignment, but we never adjust the size to be a multiple of alignment. ( is what I used to inspect the layout, pretty useful - also, could be replaced by in the minimal repro, just noticed) <hr/This is what tagged union layout does, after it merges variants' /: To fix the niche-filling codepath, you need to duplicate the above, just after  However, be careful around : will need to also have an guard (as have expected align/sizes determined entirely by the value and the target spec). The fact that isn't checked today for is technically wrong AFAICT, but I can't think of a way to actually exploit being larger than what is required for a specific . <sub(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)</sub
Here's what I've got so far: For the example of , it ends up using a direct tag instead, still . The original build also passes all assertions with this change.
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: In that case, it should get the same layout as s/bool/u8/ (since it ends up not using the niche). , I was able to confirm that in such a tagged union gets placed at the very end (i.e. offset 8 in a size 8 type), which matches the respective handwritten struct, so I'm happy to say that all looks good. <hr/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 "cross-pollinate" alignments: Such a definition shouldn't be notably different from (modulo bugs like this very one). (note that you can't just use 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 : Relevant parts of the output (cleaned up a bit): To be clear, you should be able to get those exact results with instead of , on your branch.
That's a nice test, thanks! And indeed, nightly currently makes those both niche with , while my branch makes them direct and niche respectively with .

<ins> error: layout_of(std::result::Result<[u32; 0], bool>) = Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Multiple { tag: Initialized { value: Int( I8, false, ), valid_range: 0..=1, }, tag_encoding: Direct, tag_field: 0, variants: [ Layout { fields: Arbitrary { offsets: [ Size(4 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 0, }, abi: Aggregate { sized: true, }, largest_niche: None, align: AbiAndPrefAlign { abi: Align(4 bytes), pref: $PREF_ALIGN, }, size: Size(4 bytes), }, Layout { fields: Arbitrary { offsets: [ Size(1 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 1, }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(1 bytes), value: Int( I8, false, ), valid_range: 0..=1, }, ), align: AbiAndPrefAlign { abi: Align(1 bytes), pref: $PREF_ALIGN, }, size: Size(2 bytes), }, ], }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I8, false, ), valid_range: 0..=1, }, ), align: AbiAndPrefAlign { abi: Align(4 bytes), pref: $PREF_ALIGN, }, size: Size(4 bytes), } --> $DIR/zero-sized-array-enum-niche.rs:13:1 | LL | type AlignedResult = Result<[u32; 0], bool>; | ^^^^^^^^^^^^^^^^^^ error: layout_of(MultipleAlignments) = Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Multiple { tag: Initialized { value: Int( I8, false, ), valid_range: 0..=2, }, tag_encoding: Direct, tag_field: 0, variants: [ Layout { fields: Arbitrary { offsets: [ Size(2 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 0, }, abi: Aggregate { sized: true, }, largest_niche: None, align: AbiAndPrefAlign { abi: Align(2 bytes), pref: $PREF_ALIGN, }, size: Size(2 bytes), }, Layout { fields: Arbitrary { offsets: [ Size(4 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 1, }, abi: Aggregate { sized: true, }, largest_niche: None, align: AbiAndPrefAlign { abi: Align(4 bytes), pref: $PREF_ALIGN, }, size: Size(4 bytes), }, Layout { fields: Arbitrary { offsets: [ Size(1 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 2, }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(1 bytes), value: Int( I8, false, ), valid_range: 0..=1, }, ), align: AbiAndPrefAlign { abi: Align(1 bytes), pref: $PREF_ALIGN, }, size: Size(2 bytes), }, ], }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I8, false, ), valid_range: 0..=2, }, ), align: AbiAndPrefAlign { abi: Align(4 bytes), pref: $PREF_ALIGN, }, size: Size(4 bytes), } --> $DIR/zero-sized-array-enum-niche.rs:21:1 | LL | enum MultipleAlignments { | ^^^^^^^^^^^^^^^^^^^^^^^ error: layout_of(std::result::Result<[u32; 0], Packed<std::num::NonZeroU16>>) = Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Multiple { tag: Initialized { value: Int( I8, false, ), valid_range: 0..=1, }, tag_encoding: Direct, tag_field: 0, variants: [ Layout { fields: Arbitrary { offsets: [ Size(4 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 0, }, abi: Aggregate { sized: true, }, largest_niche: None, align: AbiAndPrefAlign { abi: Align(4 bytes), pref: $PREF_ALIGN, }, size: Size(4 bytes), }, Layout { fields: Arbitrary { offsets: [ Size(1 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 1, }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(1 bytes), value: Int( I16, false, ), valid_range: 1..=65535, }, ), align: AbiAndPrefAlign { abi: Align(1 bytes), pref: $PREF_ALIGN, }, size: Size(3 bytes), }, ], }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I8, false, ), valid_range: 0..=1, }, ), align: AbiAndPrefAlign { abi: Align(4 bytes), pref: $PREF_ALIGN, }, size: Size(4 bytes), } --> $DIR/zero-sized-array-enum-niche.rs:37:1 | LL | type NicheLosesToTagged = Result<[u32; 0], Packed<std::num::NonZeroU16>>; | ^^^^^^^^^^^^^^^^^^^^^^^ error: layout_of(std::result::Result<[u32; 0], Packed<U16IsZero>>) = Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, 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, }, 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(4 bytes), pref: $PREF_ALIGN, }, size: Size(0 bytes), }, Layout { fields: Arbitrary { offsets: [ Size(0 bytes), ], memory_index: [ 0, ], }, variants: Single { index: 1, }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I16, false, ), valid_range: 0..=0, }, ), align: AbiAndPrefAlign { abi: Align(1 bytes), pref: $PREF_ALIGN, }, size: Size(2 bytes), }, ], }, abi: Aggregate { sized: true, }, largest_niche: Some( Niche { offset: Size(0 bytes), value: Int( I16, false, ), valid_range: 0..=1, }, ), align: AbiAndPrefAlign { abi: Align(4 bytes), pref: $PREF_ALIGN, }, size: Size(4 bytes), } --> $DIR/zero-sized-array-enum-niche.rs:44:1 | LL | type NicheWinsOverTagged = Result<[u32; 0], Packed<U16IsZero>>; | ^^^^^^^^^^^^^^^^^^^^^^^^ error: aborting due to 4 previous errors </ins>