As I already mentioned, undef semantics are underspecified and internally inconsistent. A reasonably accurate model is to say that there's not just one undef value, there's an undef value for every set of bitstrings of the given length. So for instance, there's an i32 undef value that is "all-0 or all-1". The literal undef corresponds to the special case of the set of all bitstrings. If a particular bit is known to be 0, then we have the set of bitstrings with that property. And so on. A lot of confusion arises from undef sometimes being used to refer to the specific "set of all values" and sometimes to any "set of at least 2 values".
There is no advantage to doing it per bit in Rust, so we do it per byte because that is simpler. There is no Rust program where this could possibly make a difference.
Thanks for the clarification on undef, confusion did indeed arise Is this documented somewhere, or is it just tacit knowledge of LLVM experts?
As for the bit-vs-byte granularity, tracking uninitializedness by bit won't make any difference to a pure Rust program (yet), but it will make a difference if we attempt to read or write C bitfields from Rust. Are we willing to sacrifice the ability to interact with C bitfields to make the Abstract Machine (and, presumably, Miri) simpler? Or would tracking undefinedness by bit have other negative consequences for the compiler or standard library that haven't been discussed yet?
The docs kind of hint at this when they explain select undef, %X, %Y. They basically say that this corresponds to the set {%X, %Y}. But people think formal language is scary so they describe stuff in English and then it becomes ambiguous...
I learned this by talking to various people and reading papers such as this one, but those papers are not official documentation, they "just" reflect researchers' best understanding of the semantics. I think I heard about this even before reading that paper but I don't know where or from whom...
This also a great paper to read though I could not find a section there that goes into this amount of detail on undef.
If we ever introduce an operation in Rust where bit vs byte tracking of initialization makes a difference, then we will consider that question. Currently, Rust has no operation to read a C bitflag, you have to do byte-sized loads and stores instead, so it is moot to discuss this point.
Hmm, I thought that freeze is the operation that could allow us to read a C bitfield in Rust. Let's say that we have the following C code:
// in Clang on aarch64 this struct has size 4 and alignment of 4, and the fields are
// encoded as bits 0..1, 1..14, 14..21 when the bytes of the struct are interpreted
// as a little-endian 32-bit integer
struct Order {
int side: 1;
int price: 13;
int qty: 7;
}
void set_price(struct Order* order, int price) {
order->price = price;
}
then I would expect this Rust code to work:
#[repr(C)]
struct Order {
bits: u32,
}
extern "C" {
unsafe fn set_price(order: *mut Order, price: i32);
}
let mut order = MaybeUninit::<Order>::uninit();
// this is safe: `set_price()` requires that `order: struct Order* order` points
// to an object that's 4 bytes long and aligned to 4 bytes, and that it can write there,
// and all these conditions are satisfied
unsafe { set_price(order.as_mut_ptr(), 1234) };
// `set_price()` initializes bits 1..14 of `order`, leaving the rest uninitialized
// the `freeze()` operations keeps them unchanged, and sets the others to arbitrary values.
// the `.assume_init()` is safe, because any bit pattern is a valid `u32`
let order_bits = unsafe { order.freeze().assume_init() }.bits;
// we can now extract the `price` bitfield from `order_bits`
assert_eq!((order_bits >> 1) & ((1 << 13) - 1), 1234);
However, if we track undefinedness by byte, then this would not work, because no byte in order is fully initialized, so order.freeze() would produce an arbitrary 32-bit value and would not preserve the bits 1..14.
Multi-language programs have their own special considerations. Typically you connect these programs on the asm level. The asm generated from the C code will write at least one byte to memory, so the Rust story for that code can consider that byte to be initialized. Cross-lang LTO makes things more complicated. But either way you are not talking about a Rust program any more so the Rust AM is the wrong thing to look at.
Please keep the thread on-topic, this tangent doesn't have anything to do with freeze.
I am sorry that you find this off-topic. I am writing an RFC for the freeze operation, and I included accessing C bitfields among the motivating use cases and had a whole section explaining the rationale for freezing by bit vs by byte. I'll rewrite this to make it clear that a Rust programs still track initializedness by byte, but that the freeze operation in Rust corresponds to the freeze instruction in LLVM that tracks undefinedness/poison by bit, which is important if the Rust code needs to interact with C or another language on the LLVM level, where the Rust AM does not apply.
The only case where C bitfields might need freeze in Rust is when doing cross-language LTO. And yeah for that use-case you have to reason at the LLVM IR level, which also means there's few stable guarantees as both rustc and clang don't promise much about the LLVM IR they generate. So, while I can see the desire for an official way to interop with C bitfields, please understand that this argument is very different in nature and much more tenuous than things you can motivate entirely within Rust.