Change:
Hi, we just noticed that there has been changes to the inner of , i.e. , that causes a of a discriminant to produce different results across CPU architectures. E.g. When hashing this in the current stable one gets writes that look like this: And depending on the architecture in the current nightly one gets for : However for ex it is still 8 bytes as before. This is a problem as e.g. CRC hashes are no longer compatible between CPU architectures. Not sure if this was intended, but it seems like a breaking change to me? In our case we are sending structures between an embedded and an system where CRCs are calculated based on for said structures to be sent. :
I don't think that the trait was supposed to guarantee stable results over different architectures or crate/compiler versions: the only valid way of using it is for keys in /. There are [crates specifically designed for consistent hashing with a specific algorithm][0], but those are not part of and operate on bytes, and gathering those bytes from a value in memory is a Serde+[Bincode] thing. [0]: [Bincode]:
has been changed from u64 to the actual integer type that represent the discriminant, to support enum, so they hash differently.
Tagging as a regression -- I suspect that this is definitely outside the scope of "intentional coverage" of our stability policy, but we could probably change the Hash impl to always hash the ~same number of bytes. Regardless I don't think Rust guarantees the same value in the discriminant when compiling more than once (much less for different targets), so in that sense it's not really covered either.
Either way we should specifically mention the stability guarantees (or lack thereof) in the documentation.
I’m a bit confused because the issue description mentions two rustc versions. Do you get different results on different architectures with the same compiler version? Does also vary based on the architecture?
No, will always have same size as because is sufficient to fit. However will be of size .
A quick summary of the problem: now calls instead of on all architectures. This breaks code which relies on getting called with the same value on both 64 and 32 bit systems. This can be fixed by manually implementing and for a if it should be stable between architectures. (I think we still don't guarantee that enums with unspecified repr have the same discriminant when compiling for a different target) Not really part of , so I am removing for now, cc
The root of the issue seems to be that is for an enum without a attribute. already is that of the smallest integer type that can fit all discriminant values. Couldn’t be that type as well, rather than ?
I believe being the smallest integer type is an optimisation. When we perform checks the discriminant is still treated as being represented by an (assigning a number non-representable by isize will cause an error). If we make the discriminant same size as , adding a new discriminant may silently break things.
Still, I don't think that code should rely on that at all. We do need a mention of this consideration in the documentation of the trait though.
That changing the source definition of a type without a attribute changes (breaks) some behavior seems less surprising to me than platform-dependent behavior without apparently doing anything platform-specific or directly involving platform-dependent types like and
I see lang was pinged here, but it's not clear to me that this is a lang issue. AFAIK and are libs's domain. (Personally it seems like there's no guarantee that does the same thing on different platforms, so I'd be inclined to solve this by just putting a note in the docs that es across different builds can be different. I do like the point that it seems weird to a discriminant to be a type instead of a type, though.)
If we change the discriminant away from , then what integer type is going to used when typechecking the custom discriminant values? Changing this behaviour might be a breaking change.
The "the trait is implemented for every type" part and the choice of associated type is magical as far as the standard library is concerned, though. (There is no block for it in source code, similar to .) Arguably that’s in the lang team’s domain? We could change the associated type without changing how the part after in the source definition of an enum is type-checked.
What about enums with only 1 variant? would be 0, but we don't have an integer type that can only represent 256. Similar argument applies to enum with tuple/struct variants. For things like , what would be the proper size of its discriminant? 1? But is 4 bytes larger than , while is the same size as . Do we need to have another set of logic to compute the smallest integer type necessary to hold the discriminant, which would not be essentially the same as the size of the enum after layout optimisation? My stance is no, we'd better keep it simple and leave it as .
We talk about "the discriminant" but that can refer to at least four different things that are not necessarily the same: , and its fallback when omitted (start at zero, increment by one). Expressions in that position are type-checked as the integer type used in for the enum if any, otherwise. ( is a type error.) numerical value that you get when converting a field-less enum value to an integer type with the operator. The "source" integer type here is less relevant since we’re converting anyway. return type of type is deliberately opaque. As of Rust 1.44 it doesn’t even provide an ordering. Some aspects like its and behavior are observable, though not guaranteed to be stable across compiler versions. memory representation of an enum may or may not involve a tag (using terminology from https://rust-). for example doesn’t have a tag. When it exists, 2. is defined to be the same as 1. Other than that each of these four things may be more or less related to the others for ease of implementation but they don’t have to. Enums without a attribute have number of memory layout optimization including not having a tag at all for single-variant enums, and making the tag smaller than in some cases even though 1. is type-checked as : () These optimizations could go further and for example make a occupy a single byte by separating the values of 1. and 4., but at the cost of making conversions require a lookup table. (It’s sort of what happens for , with a trivial one-entry "table".) The above also shows that the compiler already has logic for finding the smallest integer type that fits a set of values (although it’s not the only thing that determines the memory representation). could reuse that logic.
We could do it, but I am still not convinced that we should do it. As you mentioned, 1. 2. and 4. are different things already, and changing away from would make 3. no longer same as 1. and 2., while 3. would still not align with 4., therefore making it even more confusing than the behaviour today. After all, we provide no guarantee that the memory layout optimisations will happen (except for ) or the discriminant or the hash would be stable. By making it de facto stable might make people actually depend on that, restricting what we could do in the future.
Trying to reduce de-facto stability of unspecified behavior does not seem to me like a good reason by itself to artificially introduce/preserve target-dependence. Consistency between 1. and 3. for users is a more reasonable argument to me, although is opaque so that consistency is only observable indirectly through unspecified aspects like and . (I wish the default type for 1. was a target-independent one like , or better yet "somehow" not having a single default type in the first place. But pre-1.0 was the time to change that.)
Hmm, so: It seems clear that the trait in general is not meant to produce portable hashes when is used, so the question is really whether (and enums, more generally) are meant to have hashes that are portable across target architectures? I don't think we explicitly discussed that but it seems quite likely we would not have guaranteed it, if we had. It also seems like a reasonable workaround exists. As said, users can write a hasher that routes to (and , of course), and thus explicitly make their hashes independent from the architecture. If we did however want to make hashing enums independent from the target architecture (at least, those that don't explicitly use and ), I guess the proposal is that the for an enum would not be but rather the "smallest integer that works"? This may or may not be easy to do, implementation wise. I guess I don't have a strong opinion about it, but it makes me a bit nervous, since it introduces a new dependency where figuring out the for an enum now requires evaluating the discriminant values -- this is a new opportunity for a cycle. I guess I'd lean against it, myself, all things being equal. ( may have more detailed thoughts.)
This logic is already implemented in the compiler. When the default memory representation of an enum uses a tag, the tag has the representation of the smallest integer type that works.
yes, that logic is implemented, but it is implemented as part of the code, and I am concerned that now we would have to execute that code in order to figure out the , whereas now we don't. At least that's my guess, I've not dug into the code and I might be misremembering things. Perhaps there's already a dependency.
Is this worse in some way than the cycles for structs? The says "computing layout of [...] requires const-evaluating + checking ", which implies to me that this wouldn't actually be a new dependency. That makes me curious about things like But the there already avoids making a mention of , just summoning the value somehow So I guess that potential issue has already been avoided. Hmm, I guess I wasn't thinking about the trait, which describes itself as "unlikely to ever be stabilized". The type itself could, hypothetically, cast to in its implementation if it wanted, as a way to resolve this issue. (Which would even be fine for a discriminant, because it's just . Not that that would necessarily be good -- I'd certainly like to only have to hash one byte.) Right now I think the only stable way to get the value out is with , which truncates/extends to the requested type, so lang might be able to avoid making a guarantee here. (Until someone wants to offer an API like , but that's not here.) This issue is now old enough that it's a de-facto decision that the breakage isn't important enough to just do the change. So I don't know what the correct scope of a change would be here, if any. Given that appears to be platform-dependent, I do think it's not unreasonable for to be platform-dependent as well -- as has already been said, a specific can choose to extend (or truncate) as part of hashing if it wants. Meta: It'd also be a shame to end up with folk wisdom of "you should your enums because it makes the implementation faster".
Untagging as a regression, since this landed in 1.45.
cc
We discussed this in today's meeting, and we feel like the only remaining action here is to document this behavior.
(Any proposals for providing more guarantees about this can be proposed and discussed separately.)

Discussion:
/// `0xFF` byte to the `Hasher` so that the values `("ab", "c")` and `("a", /// "bc")` hash differently. /// <ins> /// ## Portability /// /// Due to differences in endianness and type sizes, data fed by `Hash` to a `Hasher` /// should not be considered portable across platforms. Additionally the data passed by most /// standard library types should not be considered stable between compiler versions. /// /// This means tests shouldn't probe hard-coded hash values or data fed to a `Hasher` and /// instead should check consistency with `Eq`. /// /// Serialization formats intended to be portable between platforms or compiler versions should /// either avoid encoding hashes or only rely on `Hash` and `Hasher` implementations that /// provide additional guarantees. /// </ins> /// [`HashMap`]: ../../std/collections/struct.HashMap.html /// [`HashSet`]: ../../std/collections/struct.HashSet.html /// [`hash`]: Hash::hash