The name freed up by the rename now means what C means by it: the members overlay one storage, the size is the largest of them, the alignment the strictest, and nothing anywhere records which one was written. It serves two things that wanted it. Binding a C header means holding the union the library holds and reading whichever member the library's own tag says is live -- a tag Flan cannot see, because the rule relating them is prose in a manual. Overlaying an f32 on a u32 to look at its bits is the other, and it is the same read. So that read is defined rather than refused. This is the one place in the checker where bytes win over safety on purpose, and the alternative was not a safer language, it was no feature: type punning *is* reading the member that was not written. The promise is the one C's implementations make and C's standard does not -- the layout is the target's, the bytes are the bytes, a read is a reinterpretation of them -- and what is not promised is anything about bytes nobody wrote, where a member wider than the one last stored reads a tail that is indeterminate exactly as a struct's padding is. ZII narrows that to almost nothing: a union starts all-bytes-zero unless uninit says otherwise. uninit on one is allowed, unlike on a defdata. The refusal there was never about garbage; it is that a tag steers, and a tag no case names falls past every comparison in a match into a block LLVM may treat as unreachable. An untagged union steers nothing. Which is also why three things are refused, each for a reason that does not expire with a milestone. No move-only member: nothing knows which member is live, so nothing can tear one down, and unlike the struct and defdata refusals this is not waiting on recursive teardown -- there is no fact for teardown to read. No bool at any depth: an i1 loaded from a byte that is neither 0 nor 1 is a value the optimiser may assume cannot exist, and a union is the only type that can produce one. No defdata at any depth, for the reason uninit gives, arriving the other way round. An Option member is fine and the walk says why: its match is a tag test and a branch, not a chain with an unreachable tail. Two members in one literal, a match on a union, a union map key and a member written into a global initialiser are each refused by name. A union is a field list whose every offset is zero, so it travels as a Tast.structure and the checker, the emitter and the x86 backend each grow one table rather than one shape. A value is a zeroed temporary and a store -- Set over Pfield, which every backend already has -- so there is no new IR node and no layout rule spelled out a second time per backend. The LLVM type is the blob clang gives a union, the DWARF is DW_TAG_union_type with every member at zero, and the printer names the type and does not walk it: it cannot know which member is live, and one of them may be a pointer. cimport can now check what it could not. A C record holding a union member was not recorded at all, so the defstruct beside it went unchecked rather than checked wrongly; a named union member resolves to a defunion now and the whole record is compared field by field. The defunion itself is compared against the header's union as a set and not in order -- every member is at offset zero, so a permuted one is the same type and reporting it would be a finding that is not one -- while a member the header has and Flan lacks is reported, because that is what changes the size. A defunion against a C struct, or a defstruct against a C union, is reported in both directions. An anonymous union member is still skipped, and the comment now says that the gap is on the Flan side: there is nothing to declare.
90 lines
3.3 KiB
Plaintext
90 lines
3.3 KiB
Plaintext
;; The untagged union: one storage, several ways of reading it.
|
|
;;
|
|
;; Every line here is a fact about *bytes*, which is the whole reason the
|
|
;; program exists rather than a checker row. A union's layout is the only
|
|
;; thing about it that can be wrong silently: reading the member that was not
|
|
;; written is defined behaviour in Flan, so nothing at run time would notice a
|
|
;; member placed at the wrong offset or a type sized to the wrong member --
|
|
;; the numbers would simply be different ones. So the numbers are written
|
|
;; down, and both backends have to produce them.
|
|
;;
|
|
;; 0x3F800000 is 1.0f and 0x4000000000000000 is 2.0. They are here as decimal
|
|
;; literals because that is what a reader who doubts the output has to be able
|
|
;; to check by hand against IEEE 754, and a hex literal would only move the
|
|
;; question.
|
|
|
|
(defstruct P [x f32 y f32])
|
|
|
|
(defunion Bits [i i32 f f32 bs [4 u8]])
|
|
(defunion Wide [n i64 d f64 p P bs [8 u8]])
|
|
|
|
;; A union inside a struct, which is the FFI shape: the C library keeps the
|
|
;; tag beside the union and the rule relating them is prose in its manual, so
|
|
;; `kind' here is an ordinary field this program reads itself.
|
|
(defstruct Slot [kind i32 v Bits])
|
|
|
|
;; Zeroed and uninit, side by side. A data type refuses uninit because its tag
|
|
;; steers a match; this one has no tag to steer anything, so both are legal
|
|
;; and the zeroed one is all-bytes-zero.
|
|
(defvar zeroed Bits)
|
|
(defvar scratch Bits uninit)
|
|
|
|
(defn as-float [b Bits] f32 (.f b))
|
|
|
|
(defn of-float [x f32] Bits (Bits {.f x}))
|
|
|
|
(defn main [] i32
|
|
;; Punning, both directions, through the same storage.
|
|
(let [b (Bits {.i 1065353216})]
|
|
(println (.f b))
|
|
(println (.i b))
|
|
;; The bytes little-endian: 0x3F800000 is 00 00 80 3F.
|
|
(println (at (.bs b) 0))
|
|
(println (at (.bs b) 3))
|
|
;; A member written through a place, which is the other half of the same
|
|
;; claim -- the read above could have been folded from the literal, and a
|
|
;; store into the union could not.
|
|
(set (.f b) 2.0)
|
|
(println (.i b)))
|
|
|
|
;; The union crosses a call boundary in both directions by value.
|
|
(println (as-float (of-float 0.5)))
|
|
|
|
;; A wider union: the size is the widest member and not the first one. An
|
|
;; array of two is where that shows up -- writing element 1 would land
|
|
;; inside element 0 if the type were sized to its i64 member alone and the
|
|
;; f64 or the [8 u8] were wider, and every element's value would change
|
|
;; under the other's write.
|
|
(let [w (Wide {.d 2.0})]
|
|
(println (.n w))
|
|
(println (.x (.p w))))
|
|
|
|
(let [ws (array 2 Wide)]
|
|
(set (.n (at ws 0)) 11)
|
|
(set (.n (at ws 1)) 22)
|
|
(println (.n (at ws 0)))
|
|
(println (.n (at ws 1))))
|
|
|
|
;; ZII: a union with no member given is all-bytes-zero, and so is a global
|
|
;; declared with no value.
|
|
(let [empty (Bits {})]
|
|
(println (.i empty)))
|
|
(println (.i zeroed))
|
|
;; And a global is written and read like any other place.
|
|
(set (.i scratch) 7)
|
|
(println (.i scratch))
|
|
|
|
;; A union inside a struct, with the tag the program keeps itself.
|
|
(let [s (Slot {.kind 1 .v (Bits {.f 1.5})})]
|
|
(println (.kind s))
|
|
(println (.f (.v s)))
|
|
(set (.kind s) 2)
|
|
(set (.i (.v s)) 9)
|
|
(println (.kind s))
|
|
(println (.i (.v s))))
|
|
|
|
;; Printed by name and not walked: the printer cannot know which member is
|
|
;; live, and a member may be a pointer.
|
|
(println zeroed)
|
|
0)
|