Most active commenters
  • NoTeslaThrow(8)
  • packetlost(6)
  • mmastrac(4)
  • (4)
  • fc417fc802(4)
  • returningfory2(3)
  • hinkley(3)
  • LegionMammal978(3)
  • imtringued(3)

146 points returningfory2 | 75 comments | | HN request time: 2.593s | source | bottom
1. packetlost ◴[] No.43645287[source]
I don't think it's actually "flattening" the enums discriminant spaces (though maybe it is). My guess is this is just 32-bit alignment requirement (ie. bytes 1:4 are padding) + sub-byte discriminant sizes. The surprising thing here is that the ordering of Outer's variants doesn't match the ordering as defined, instead having variant D's discriminant be 0 (0 << size_of::<Inner's discriminant>). size_of::<Inner> is actually 33 bits and size_of::<Outer> is 34 bits and then you just apply alignment requirements to each field. Actual size_of calls will account for alignment and padding, but the compiler knows the truth.

What's cool about this in general is nested match statements can be flattened into a jumplist (idk if rustc does this, but it's possible in theory).

replies(2): >>43646587 #>>43649004 #
2. mmastrac ◴[] No.43645485[source]
This is a great way to see why invalid UTF-8 strings and unicode chars cause undefined behaviour in Rust. `char` is a special integer type, known to have a valid range which is a sub-range of its storage type. Outside of dataless enums, this is the only datatype with this behaviour (EDIT: I neglected NonZero<...>/NonZeroXXX and some other zero-niche types).

If you manage to construct an invalid char from an invalid string or any other way, you can defeat the niche optimization code and accidentally create yourself an unsound transmute, which is game over for soundness.

replies(5): >>43645776 #>>43645961 #>>43646463 #>>43646643 #>>43651356 #
3. pitaj ◴[] No.43645579[source]
This actually is niche optimization. The outer enum is using the niches available in the tag of the inner enum for its own discriminant.

The author seems to have a limited understanding of niche optimization.

replies(1): >>43646157 #
4. lordnacho ◴[] No.43645689[source]
Does the niche optimization require the compiler to know things about the type? So that only certain specializations will work?

Also, how do I get some code to do the memory layout vizualizer, perhaps one that is a bit more advanced and knows what pointers are?

replies(1): >>43645823 #
5. NoTeslaThrow ◴[] No.43645776[source]
> This is a great way to see why invalid UTF-8 strings and unicode chars cause undefined behaviour in Rust.

What does "undefined behavior" mean without a spec? Wouldn't the behavior rustc produces today be de-facto defined behavior? It seems like the contention is violating some transmute constraint, but does this not result in reproducible runtime behavior? In what context are you framing "soundness"?

EDIT: I'm honestly befuddled why anyone would downvote this. I certainly don't think this is detracting from the conversation at all—how can you understand the semantics of the above comment without understanding what the intended meaning of "undefined behavior" or "soundness" is?

replies(5): >>43645920 #>>43645923 #>>43646838 #>>43647769 #>>43648876 #
6. pornel ◴[] No.43645823[source]
Internally the compiler tracks what "niches" exist in types, like minimum and maximum valid values, and takes the unused values for enum tags.

One thing it can't use is padding in structs, because references to individual fields must remain valid, and they don't guarantee that padding will be preserved.

replies(2): >>43645982 #>>43646104 #
7. mmastrac ◴[] No.43645920{3}[source]
> What does "undefined behavior" mean without a spec?

While not as formalized as C/C++, Rust's "spec" exists in the reference, nomicon, RFCs and documentation. I believe that there is a desire for a spec, but enough resources exist that the community can continue without one with no major negative side-effects (unless you want to re-implement the compiler from scratch, I suppose).

The compiler may exploit "lack of UB" for optimizations, e.g., using a known-invalid value as a niche, optimizing away safety checks, etc.

> Wouldn't the behavior rustc produces today be de-facto defined behavior?

Absolutely not. Bugs are fixed and the behaviour changes. Not often, but it happens.

This post probably answers a lot of your reply as well: https://jacko.io/safety_and_soundness.html

replies(1): >>43645935 #
8. newpavlov ◴[] No.43645923{3}[source]
"Undefined behavior" means that the compiler can apply optimizations which assume that it does not happen, resulting in an incorrect code. A simpler example is `Option<NonZeroU8>`, the compiler assumes that `NonZeroU8` can never contain 0, thus it can use 0 as value for `None`. Now, if you take a reference to the inner `NonZeroU8` stored in `Some` and write 0 to it, you changed `Some` to `None`, while other optimizations may rely on the assumption that references to the content of `Some` can not flip the enum variant to `None`.

You don't need a full language spec to declare something UB. And, arguably, from the compiler correctness perspective, there is no fundamental difference between walls of prose in the C/C++ "spec" and the "informal spec" currently used by Rust. (Well, there is the CompCert exception, but it's quite far from the mainstream compilers in many regards)

replies(1): >>43645946 #
9. NoTeslaThrow ◴[] No.43645935{4}[source]
EDIT:

> While not as formalized as C/C++, Rust's "spec" exists in the reference, nomicon, RFCs and documentation. I believe that there is a desire for a spec, but enough resources exist that the community can continue without one with no major negative side-effects (unless you want to re-implement the compiler from scratch, I suppose).

Thank you, I was unaware that this is a thing.

> This post probably answers a lot of your reply as well: https://jacko.io/safety_and_soundness.html

This appears to also rely on "undefined behavior" as a meaningful term.

replies(1): >>43646003 #
10. NoTeslaThrow ◴[] No.43645946{4}[source]
> resulting in an incorrect code.

Incorrect with respect to an assumption furnished where? Your sibling comment mentions RFCs—is this behavior tied to some kind of documented expectation?

> A simpler example is `Option<NonZeroU8>`, the compiler assumes that `NonZeroU8` can never contain 0, thus it can use 0 as value for `None`. Now, if you take a reference to the inner `NonZeroU8` stored in `Some` and write 0 to it, you changed `Some` to `None`, while other optimizations may rely on the assumption that references to the content of `Some` can not flip the enum variant to `None`.

That seems to be the intended behavior, unless I'm reading incorrectly. Why else would you write a 0 to it? Also, does this not require using the `unsafe` keyword? So is tricking the compiler into producing the behavior you described not the expected and intended behavior?

replies(3): >>43646036 #>>43646048 #>>43649155 #
11. timerol ◴[] No.43645961[source]
> Outside of dataless enums, this is the only datatype with this behaviour.

Note that there are non-zero integer types that can also be used in this way, like NonZeroU8 https://doc.rust-lang.org/std/num/type.NonZeroU8.html. The NULL pointer is also used as a niche, and you can create your own as well, as documented in https://www.0xatticus.com/posts/understanding_rust_niche/

replies(2): >>43646020 #>>43646076 #
12. ◴[] No.43645982{3}[source]
13. mmastrac ◴[] No.43646003{5}[source]
> This appears to also rely on "undefined behavior" as a meaningful term.

I assure you it is a meaningful term:

https://llvm.org/docs/UndefinedBehavior.html

replies(1): >>43646034 #
14. mmastrac ◴[] No.43646020{3}[source]
Ack, yeah. I forgot about those despite having used them. That's a good point and I stand corrected. Edited post above.
replies(1): >>43646436 #
15. NoTeslaThrow ◴[] No.43646034{6}[source]
Ok, but in the context of the language at hand? Presumably the IR has distinct semantics from the language that generates the IR. Does UB just strictly resolve to LLVM UB? That's very reasonable!
replies(2): >>43646173 #>>43646377 #
16. newpavlov ◴[] No.43646036{5}[source]
>Incorrect with respect to an assumption furnished where?

In the definition of the `NonZeroU8` type. Or in a more practical terms, in LLVM, when we generate LLVM IR we communicate this property to LLVM and it in turn uses it to apply optimizations to our code.

>Also, does this not require using the `unsafe` keyword?

Yes, it requires `unsafe` and the point is that writing 0 to `NonZeroU8` is UB since it breaks the locality principle critical for correctness of optimizations. Applying just one incorrect (because of the broken assumption) optimization together with numerous other (correct) optimizations can easily lead to very surprising results, which are practically impossible to predict and debug. This is why it's considered such anathema to have UB in code, since having UB in one place may completely break code somewhere far away.

17. fc417fc802 ◴[] No.43646048{5}[source]
It's not intended in that the compiler may have optimized based on the assumption that you have now gone and violated via an unsafe block. Just as in C that would produce undefined behavior in that there's no telling what consequences the optimization might have. The lack of a formal specification isn't relevant.
18. tialaramex ◴[] No.43646076{3}[source]
Well, in practice you can't make your own non-enum types in stable Rust with this property, to unblock this Rust needs pattern types, (I wanted a path to do this a different way, but I was persuaded it's not a good idea). As that link explains the stdlib is doing this via a perma-unstable compiler use only attribute.

But yes, there are the NonZero integers, and you can make your own NonBlah integer using the "XOR trick" for a relatively tiny performance overhead, as well as you can make enums which is how the current CompactString works.

The link you gave mentions that Rust does this for other types, but in particular OwnedFd is often useful on Unix systems. Option<OwnedFd> has the same implementation as a C file descriptor, but the same ergonomics as a fancy high level data structure, that's the sort of optimisation we're here for.

Alas the Windows equivalent can't do this because different parts of Microsoft use all zeroes and -1 to mean different things, so both are potentially valid.

19. ◴[] No.43646104{3}[source]
20. returningfory2 ◴[] No.43646157[source]
In the strictly technical sense I agree, however in the Rust community when people say "the niche optimization" they usually are referring only to the simplest case. For example in the book Rust For Rustaceans (written by a pretty serious Rust expert and aimed at intermediate-level Rust programmers) the nice optimization is described as "the Rust compiler using invalid bit patterns to represent enum variants that hold no data". Note the "hold no data" part - it doesn't incorporate the case in the blog.

In any case, the definition of what is exactly niche optimization is besides the point. The point of the post is: the literature gives you the impression that there's one limited form of enum size optimization in the Rust compiler, but in fact there are other optimizations too. And knowing this is useful!

replies(1): >>43662021 #
21. fc417fc802 ◴[] No.43646173{7}[source]
No. UB is a term of art here.

Consider a hypothetical non-LLVM full reimplementation of the compiler. If it optimizes and there are invalid assumptions then there is likely UB. LLVM isn't involved in that case though.

replies(1): >>43646570 #
22. vollbrecht ◴[] No.43646377{7}[source]
You can find a general overview for the language at hand in "The rust reference"[1]. For a more formal document, you can have a look in to the ferroscene language specification list of undefined behaviour[2] section. From there you can jump to different section, and see legality rules, and undefined behavior sections for each.

The ferroscene language spec was recently donated to the rust foundation.

[1] https://doc.rust-lang.org/reference/behavior-considered-unde... [2] https://spec.ferrocene.dev/undefined-behavior.html

23. deathanatos ◴[] No.43646436{4}[source]
I guess it depends on whether the sentence is only qualified as "integer" types, but bool is sort of the same way, no? A bool must be either 0 or 1 (false or true), or it's UB.

(And I think for much the same reason, the niche optimization. Option<bool> is 1 B.)

(And for the non-Rustaceans, the only way to get a bool to be not false or true, i.e., not 0 or 1, would be unsafe {} code. Or put differently, not having a bool be "2" is an invariant unsafe code must not violate. (IIRC, at all times, even in unsafe code.))

24. hinkley ◴[] No.43646463[source]
I seem to recall someone posting a ridiculously fast utf-8 validator here based on SIMD instructions. Nothing is free but some things can be dirt cheap.
replies(1): >>43649353 #
25. subarctic ◴[] No.43646490[source]
When was this implemented? I remember when people used to have to manually flatten nested enums in order to save space
replies(1): >>43646704 #
26. NoTeslaThrow ◴[] No.43646570{8}[source]
> If it optimizes and there are invalid assumptions then there is likely UB.

It's the distinguishing from bugs that concerns me.

replies(3): >>43646620 #>>43646899 #>>43649091 #
27. hinkley ◴[] No.43646587[source]
In a lot of languages space optimizing Optional Types without using a reserved enum value or pointer tags would lead to memory model problems with atomically writing two values at once which might be more easily solved in a borrow semantics world. I hope there is someone out there mining research papers for the implementation strategies that were abandoned as unworkable due to bookkeeping issues, which Rust has already paid.

But in the case of Options they tend to be both write-once and short-lived, so that removes a lot of necessity. Options are going to stay mostly on the stack and in async callbacks, unless they go into caches.

But for other data structures where multiples fields need a tag, I suspect Rust could use some bitfields for representing them. You’d need a fairly big win to make it worth implementing however.

replies(2): >>43646937 #>>43647055 #
28. fc417fc802 ◴[] No.43646620{9}[source]
I don't follow. Isn't UB a subset of bugs or alternatively a follow on consequence that causes observable behavior to further deviate?
replies(1): >>43647058 #
29. tlb ◴[] No.43646643[source]
If the compiler is using a niche, it should really check every assignment that it's not accidentally the niche. That's still faster than also writing the tag.
replies(1): >>43647456 #
30. returningfory2 ◴[] No.43646704[source]
Many years ago I think, here: https://github.com/rust-lang/rust/issues/46213. (Found this via https://lobste.rs/s/w3jjb2/surprising_enum_size_optimization...)
31. kazinator ◴[] No.43646741[source]
This seems like it could be summarized as tag merging. When the member of an enum is another enum, then the two can be effectively merged, rather than tested, so that there is one discriminant tag. The tag values have to be displaced/renumbered so that their ranges do not overlap.
32. ◴[] No.43646838{3}[source]
33. vlovich123 ◴[] No.43646899{9}[source]
It is a bug - you’ve violated the contract between the language and the compiler.

Just like segfault or logic bug, it’s a class of bugs. Why is special though is that in most bugs you just hit an invalid state. In UB you can end up executing code that never existed or not executing code that does exist. Or any number of other things can happen because the compiler applies an optimization assuming a runtime state you promised it would never occur but did.

It’s slightly different from being a strict subset because UB is actually exploited to perform optimizations - UB is not allowed so the compiler is able to emit more efficient code is taught to exploit that and the language allows for it (eg the niche optimization the blog describes)

34. packetlost ◴[] No.43646937{3}[source]
I'm honestly not exactly sure what you're talking about, but the fundamental limit for atomic writes is usually the register-size for a CPU which is usually 64 or 32 bits. Considering enum variants are often larger than a single register in size, I don't see how atomic writes would ever be sane expectation.
replies(2): >>43647197 #>>43649639 #
35. Georgelemental ◴[] No.43647055{3}[source]
> In a lot of languages space optimizing Optional Types without using a reserved enum value or pointer tags would lead to memory model problems with atomically writing two values at once which might be more easily solved in a borrow semantics world.

Yes, Rust suppresses the niche optimization for values wrapped in an `UnsafeCell` (which is how you signal to the compiler that “atomically writing two values at once” might happen). https://github.com/rust-lang/rust/pull/68491

36. NoTeslaThrow ◴[] No.43647058{10}[source]
> Isn't UB a subset of bugs

No, not at all. UB can still produce correct and expected results for the entire input domain.

replies(3): >>43647136 #>>43648979 #>>43649194 #
37. fc417fc802 ◴[] No.43647136{11}[source]
If I have a bug that only triggers between 9 and 10 am EST on Mondays that is still a bug, no? Now extend that to "rand(1.0) < 0.01". Now extend that to a check using __TIME__ that goes off at compile time instead of runtime (some binaries are buggy, some aren't). Now extend that to UB.
38. hinkley ◴[] No.43647197{4}[source]
Updating an aligned pointer is atomic. If you haven’t tagged it by moving bits to a neighboring word.
replies(1): >>43647322 #
39. packetlost ◴[] No.43647322{5}[source]
I see. Yeah, you would either have to add the tagging to the upper bits of the pointer itself or concede that updates to a tagged type is not atomic. I feel like the latter is fine in most every scenario I've encountered in Rust (thanks to borrow checker rules) but in other languages the same cannot be said.
40. jenadine ◴[] No.43647456{3}[source]
It doesn't need to check the assignment because that type cannot be the niche by construction.
replies(1): >>43651422 #
41. duckerude ◴[] No.43647769{3}[source]
It means that anything strange that happens next isn't a language bug.

Whether something is a bug or not is sometimes hard to pin down because there's no formal spec. Most of the time it's pretty clear though. Most software doesn't have a formal spec and manages to categorize bugs anyway.

replies(1): >>43649494 #
42. Joker_vD ◴[] No.43647843[source]
Yeah, there are all sorts of nifty little tricks about compressing the enums, Appel dedicates a huge chunk of section "4.1. Data representation" on it in his "Compiling with Continuations" book about the internals of the SML/NJ compiler for the Standard ML, and ultimately concludes that the returns are quickly getting really diminishing with those, and since datatypes can actually be abstract in ML (in pretty much the same way they can be in Rust), the applicability of those tricks is generally restricted to unexported, intramodular datatypes.
replies(1): >>43650901 #
43. ben0x539 ◴[] No.43648876{3}[source]
> I'm honestly befuddled why anyone would downvote this.

I think there's two parts to this. First, there's a bit of a history of people making disingenious jabs at Rust for not having an "ISO C++" style spec. Typically people would try to suggest that Rust can't be ready for production or shouldn't receive support in other ecosystems without being certified by some manner of international committee. Second, Rust by now has an extensive tradition of people discussing memory safety invariants, what soundness means, formal models of what is a valid memory access, desirable optimizations, etc, etc, so your question what undefined behavior means could be taken to be, like, polemically reductive or dismissive.

In context I don't think it's what you're doing, but I would also not be surprised if a lot of people reading Rust-related HN discussions are just super tired of anything that even slightly looks like an effort to re-litigate undefined behavior from first principles, because it tends to derail more specific discussions.

replies(2): >>43649500 #>>43652296 #
44. ◴[] No.43648979{11}[source]
45. LegionMammal978 ◴[] No.43649004[source]
> I don't think it's actually "flattening" the enums discriminant spaces (though maybe it is).

It is. One easy way to see this is with an Option<Option<bool>> [0]: if both options are Some, it takes the value 0 or 1 depending on the boolean; if the inner Option is None, it takes the value 2; and if the outer Option is None, it takes the value 3. And as you keep adding more Options, they take values 4, 5, 6, etc. to represent None, while still only taking up 1 byte.

[0] https://play.rust-lang.org/?version=stable&mode=debug&editio...

replies(1): >>43650162 #
46. lmm ◴[] No.43649091{9}[source]
Anything a compiler does with code which is UB is not a bug in the compiler. That's pretty much the definition of UB.
47. lmm ◴[] No.43649155{5}[source]
> is tricking the compiler into producing the behavior you described not the expected and intended behavior?

It might be what that programmer intended and expected, but they should not expect it. E.g. the current compiler might check for 0, and a future more optimized compiler might optimize out that check (because it knows the Option is not None) and then e.g. perform an out-of-bounds array access (if you were using that NonZeroU8 as an index into some kind of 1-based array).

48. pharrington ◴[] No.43649194{11}[source]
"can" is extremely different than "will"!
49. wolf550e ◴[] No.43649353{3}[source]
simdutf [1] from the same people who did simdjson [2]

1 - https://simdutf.github.io/simdutf/

2 - https://simdjson.org/

50. NoTeslaThrow ◴[] No.43649494{4}[source]
> It means that anything strange that happens next isn't a language bug.

This is even more vague. The language is getting blamed regardless. This makes no sense.

replies(1): >>43651143 #
51. NoTeslaThrow ◴[] No.43649500{4}[source]
Tbh, I just really hate the term "undefined behavior". It really feels like laziness in terms of what the possible damage might entail.
replies(3): >>43650357 #>>43651484 #>>43661990 #
52. dataflow ◴[] No.43649639{4}[source]
> the fundamental limit for atomic writes is usually the register-size for a CPU which is usually 64 or 32 bits

CPUs nowadays support double the largest general-purpose register width. Unofficially, some CPUs can also do twice that: https://rigtorp.se/isatomic/

53. packetlost ◴[] No.43650162{3}[source]
I think that's just niche optimization. If you change from bool to a u8 it doesn't use the invalid bit pattern as a discriminant even though it could: https://play.rust-lang.org/?version=stable&mode=debug&editio...
replies(2): >>43650492 #>>43650496 #
54. arlort ◴[] No.43650357{5}[source]
It is a term of art in compilers/language design though, isn't it?

If you break an invariant the compiler is relying on for optimization then you can't say for sure what the effect after all optimisation passes or in future versions of the compiler will be. It's just "undefined"

55. ComputerGuru ◴[] No.43650446[source]
I have actually requested an even more intelligent/aggressive size optimization for nested enums, where values are automatically reassigned to not coincide/conflict to enable tag-less differentiation. Consensus was that it’s a big ask though doable, but maybe possible to implement in a more restrictive version.

https://rust-lang.zulipchat.com/#narrow/channel/131828-t-com...

replies(1): >>43650788 #
56. LegionMammal978 ◴[] No.43650492{4}[source]
What invalid bit pattern? A u8 can be anything from 0 to 255, so the Option necessarily has to put its discriminant into another byte. If you replace it with a NonZeroU8, then the compiler will duly use the forbidden 0 value for the first Option level, and a separate byte for all further levels.

(Granted, in the None variant, the byte used for the u8 is not usable, but if we're already using a separate discriminant byte, 256 variants should be plenty.)

replies(1): >>43654032 #
57. int_19h ◴[] No.43650496{4}[source]
But there's no invalid u8 bit pattern.
replies(1): >>43654039 #
58. infogulch ◴[] No.43650788[source]
Good idea! I like the solution of an explicit annotation setting the first variant's discriminant (the rest autoicrement?), which enables you to manually coordinate your enums to not overlap. Then an optimization for nested enums where if all cases are other enums with no overlapping discriminants they can all share one field. This seems achievable.

The "but do it automatically" part seems rather problematic. Requiring global analysis is a big ask indeed.

Say, if you change a variant's discriminant value, does that count as a semver breaking change? Probably. Would it be an ABI breaking change? Probably.

59. kristianp ◴[] No.43650901[source]
I like how Appel's book is still being referenced despite being first published in 1992!
60. dwattttt ◴[] No.43651143{5}[source]
No: the language defined that e.g. a NonZeroU8 can't contain 0, and the only way it could is via illegal means. You don't need a formal proof to describe that.

To try to characterise what any compiler, hypothetical or not, does if you nonetheless produce one (again, via means that aren't valid) isn't meaningful.

61. imtringued ◴[] No.43651356[source]
Fine, I'll take the 4 byte hit for security and safety critical software then.

Edit: In retrospect, the optimization doesn't actually cause any security or safety problems, because unsafe code can break any invariant, including an enum with a separated tag and value. The particular memory layout of the enum is irrelevant.

replies(1): >>43651820 #
62. imtringued ◴[] No.43651422{4}[source]
The problem is that you need to validate every potentially written niche after an unsafe block.

There is no generic way to re-validate structs in a bounded address space. You'd need something akin to a garbage collector that traces references at fixed offsets including type knowledge. This is not completely infeasible since Rust has a lot of information at compile time to avoid checks, but the extreme cases where people are writing to complicated graph like structures inside unsafe {} can realistically only be dealt with through tracing all safe references that lie inside the bounded address space.

In practice it will also be a struggle to sandbox C code into a small enough CHERI style address space so that you don't have to check literally your entire computer's memory after an FFI call.

It's not the enums that are the problem. unsafe can break anything if you are determined enough.

replies(1): >>43654054 #
63. imtringued ◴[] No.43651484{5}[source]
Yeah I personally think the problem isn't undefined behavior itself, but the C development culture where undefined behavior is sprinkled all over the language to the point where it has become unavoidable plus the inevitable assignment of blame onto C developers, because everyone knows there is enough time in the day for fuzzing your entire code base.
64. tialaramex ◴[] No.43651820{3}[source]
Exactly. The correct C for working with what may or may not be a valid file descriptor produces the same machine code as the Rust with Option<OwnedFd>. But, if you mess this up and forget to handle an invalid descriptor the C compiler has no idea and your bug goes unnoticed whereas the Rust won't compile because None isn't Some(OwnedFd).
65. zozbot234 ◴[] No.43652296{4}[source]
> Second, Rust by now has an extensive tradition of people discussing memory safety invariants, what soundness means, formal models of what is a valid memory access

Rust is still lacking a definitive formal model of "soundness" in unsafe code. I'm not sure why you're suggesting that this is not a valid criticism or remark, it's just a fact.

replies(1): >>43659145 #
66. packetlost ◴[] No.43654032{5}[source]
The same way as it does in the bool case? The u8 bits are invalid if either of the Options are None, but in particular if the Outer option is Some the Inner is None the bit that would otherwise be used for the bool (in the first example) is used to discriminate Outer, but doesn't do so in the case of the u8.
replies(1): >>43657240 #
67. packetlost ◴[] No.43654039{5}[source]
But there is for Option<u8> which is the entirety of the u8s bits if the Option is None.
replies(1): >>43668343 #
68. im3w1l ◴[] No.43654054{5}[source]
Isn't it the other way around? An unsafe block must respect niches.
69. LegionMammal978 ◴[] No.43657240{6}[source]
I don't really get what distinction you're trying to draw between niche optimization and the bitwise stuff you keep talking about. As far as I am aware, the Rust compiler never works by counting bits in any context. The LLVM backend sometimes does, but it's not responsible for enum layout.

The mental model is, an enum payload will have some number of integer/pointer values with niches in their representation. Niches don't work by counting bits, they work as numeric ranges. E.g., a char is just a u32 with values 0 through 0x10ffff, a bool is a u8 with values 0 and 1, a reference is just a pointer with any value except 0, etc., and the niche is precisely the negation of this range.

Sometimes the niche corresponds to bits used in a valid value (e.g., the 0 value of a NonZeroU8), and sometimes it corresponds to other bits (e.g., values 2 through 255 of a bool): the compiler only cares about the ranges, not the bits. If there is no large-enough niche, then the discriminant is placed in a separate byte.

An outer enum can't use sometimes-valid payloads in an inner enum to represent its discriminant, if that's what you're trying to say. Multiple discriminants can be 'flattened' into a single continuous range of niche values, but they can't be 'flattened' into inner enums' payloads. That would cause a weird inversion where you need to read the inner discriminant just to know whether the outer discriminant is valid.

(The compiler does have a few tricks up its sleeve to make the most of niches. E.g., in a Result<(u8, bool), u8>, an Err(42) becomes [42, 2], but in a Result<(bool, u8), u8>, an Err(42) becomes [2, 42] (https://play.rust-lang.org/?version=stable&mode=debug&editio...). The 42 is repositioned to keep the niche intact.)

70. ben0x539 ◴[] No.43659145{5}[source]
Showing up out of nowhere pretending like they haven't even thought about what it means isn't helpful though.
71. pcwalton ◴[] No.43660666[source]
That's a great article. Niche optimization might seem minor, but it actually results in substantial memory usage savings in real-world Rust applications, because idiomatic Rust uses enums everywhere.
72. Dylan16807 ◴[] No.43661990{5}[source]
In a situation like this, causing UB is basically saying you deliberately corrupted your memory.

How are you supposed to be specific about what the possible damage might entail for corrupted memory? If you have a function with an "if" or a "while" or a "switch" in it, and you break the variable being evaluated, you might cause the program to skip over the choices and run whatever happens to be next in memory. What's the non-lazy listing of possible outcomes at that point?

73. Dylan16807 ◴[] No.43662021{3}[source]
> In any case, the definition of what is exactly niche optimization is besides the point. The point of the post is: the literature gives you the impression that there's one limited form of enum size optimization in the Rust compiler, but in fact there are other optimizations too. And knowing this is useful!

It's useful if you got that specific impression.

But I didn't. So I was disappointed and unenlightened because I was promised a non-niche optimization and I didn't get one.

74. int_19h ◴[] No.43668343{6}[source]
I'm still confused. You have a grand total of 257 possible combinations here:

- Some(Some(x)) for x in [0, 255]

- Some(None)

- None

No matter how you slice it, you can't fit them all into 8 bits, hence why it needs an extra byte.