Unions - #124
Merged
Merged
Conversation
A union is a set of competing readings of the same bytes, in two forms:
a standalone `pub union Name { ... }` item, and an inline anonymous
union in field position, `pub payload: union { ... },`, where the field
name supplies the generated item's name.
A union is a new item kind rather than a new kind of Region. Region
carries no offset - a Vec<Region>'s order *is* the layout, and six
independent accumulators recompute offsets by summing sizes. Giving
unions their own self-contained ItemDefinition, held by a parent as one
ordinary Region, leaves every one of those assumptions true and turns
each site that must branch into a compile error.
Inline unions desugar to a module-scope sibling `{Type}{Field}Union`,
mirroring the generated `{Name}Vftable` structs. A nested path is not an
option: ResolutionContext::add_item resolves a parent directly against
the module map, and declaring_module only learns nested paths from the
grammar walks - so a generated nested item would reach no backend.
Rust lowers to a real `union` with every member in ManuallyDrop, plus
hand-written Debug (and Default under #[defaultable]), which a union
cannot derive; writing them out is what keeps a containing struct's own
derives working. C++ lowers to a native `union`. JSON gains a Union kind
at schema v12, with every member at offset 0.
Rejected inside a union: #[base], vftable blocks, #[address] on a
member, a member named `_`, an empty body, a member larger than #[size],
and nested declarations inside an *inline* union (whose item is
synthesised after the walks that would register them). A generated name
that collides - with a declared item, or with another generated one - is
an error rather than a silent overwrite, since the enclosing type has
already measured what it would replace.
#[size]/#[min_size] asking for more room than any member needs adds a
whole-width `_padding` member: a union has no tail to pad, and without
it both backends would emit a size assertion they cannot satisfy.
Rounding up to the alignment needs no help - Rust and C++ do that for a
union themselves.
Closes #119
The collision check added with unions was in the wrong place: it guarded
the inline-union registration site, but `{Name}Vftable` has the same
hazard and predates it. Declaring
pub type FooVftable { pub sentinel: [u8; 64], }
pub type Foo { vftable { pub fn f(&mut self); }, }
compiles today, and is silently wrong: the registry is a map, so the
generated table loses the insert, Foo::vftable() hands back a pointer to
the user's 64-byte struct, and nothing anywhere says so. The union case
at least failed to compile, via a size-check transmute.
Move the check into a new ResolutionContext::add_generated_item, which
both synthesis sites now use, and rename the error to
GeneratedNameCollision. The batch pre-check in build_inline_union goes
away with it - two fields generating one name now collide on the second
add, like everything else.
Documents the generated-name rules in one place, since they now apply
uniformly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #119.
A union is a set of competing readings of the same bytes. Two forms:
Discriminant-driven member selection stays out of scope, per the issue. A union says what the bytes could be; the consumer decides which reading applies.
Design
A union is a new item kind, not a new kind of
Region.Regioncarries no offset — aVec<Region>'s order is the layout, and six independent accumulators recompute offsets by summing sizes (resolve.rs'sRegions::push, the alignment sweep intype_definition/build.rs, generic folding intype_registry/aliases.rs,json/convert.rs, the LSP'sfield_offset, and the viewer'sFieldSourceView). Teaching all six about overlapping regions is where the bugs would live. A self-containedUnionDefinitionwith its own size and alignment, held by a parent as one perfectly ordinaryRegion, leaves every one of those assumptions true — and being a distinct struct rather thanTypeDefinition { is_union: true }turns each of the ~30 exhaustive match sites into a compile error rather than a silent fallthrough.Inline unions become module-scope siblings named
{Type}{Field}Union, mirroring the generated{Name}Vftablestructs. A genuinely nested path isn't available:ResolutionContext::add_itemresolves an item's parent directly against the module map, anddeclaring_moduleonly learns nested paths through the grammar walks that populateitem_scopes— so a generated nested item would be dropped from every module and reach no backend.Alignment defaults differently from a type's. A type with no implied alignment falls back to the pointer size; a union of two
u8s is genuinely 1-aligned, and widening it would inflate its size.Lowering
union, every member inManuallyDrop<T>unconditionally (so a member's spelling doesn't depend on whether its type happens to be#[copyable]), with hand-writtenDebugand, under#[defaultable],Default. A union can derive neither; emitting them anyway is what keeps a containing struct's own#[derive(Debug, Default)]working.union, withalignas,#pragma pack,static_asserts on size and alignment, and deleted special members when#[pinned].Unionitem kind at schema v12, every member atoffset: 0.Rejections
#[base],vftableblocks,#[address]on a member, a member named_, an empty body, a member larger than#[size], and nested declarations inside an inline union (whose item is synthesised after the walks that would register them — declare it in the enclosing type, or name the union).Generated-name collisions (second commit)
Reviewing the union work turned up that this isn't a union problem —
{Name}Vftablehas the same hazard and predates it. This compiles today:The registry is a map, so the generated table loses the insert.
Foo::vftable()hands back a pointer to the user's 64-byte struct, and nothing anywhere says so. The union case at least failed to compile, via a size-checktransmute.The check now lives in
ResolutionContext::add_generated_item, which both synthesis sites use, so it covers{Name}Vftableand{Type}{Field}Unionalike — a declared item, another generated one, two fields whose names differ only in underscores (b_c/b__c), or two types meeting in the middle (A+b_candAB+cboth giveABCUnion).docs/language.mdgains a Generated names section, since the rule is now uniform.No pyxis-defs project trips it.
#[size]/#[min_size]asking for more room than any member needs adds a whole-width_paddingmember: a union has no tail to pad, and without it both backends would emit a size assertion they cannot satisfy. Rounding up to the alignment needs no such help — Rust and C++ do that for aunionthemselves.Surfaces
Parser, tokenizer, semantic IR, all three backends, pretty-printer, doc-link resolver, tree-sitter grammar (pushed to
ferrobrew/tree-sitter-pyxisas48eb1b7, with the submodule andextension.tomlre-pinned), LSP (completion, hover, outline, references, navigation), the viewer, anddocs/language.md/rust_backend.md/cpp_backend.md/json_backend.md.Verification
python test.pygreen, including the doxygen pass undernix-shell— the emitted Rust crate and C++ corpus both compile,cargo docresolves doc links into union members, andpyxis fmt --checkconfirms the new corpus input is format-stable.tools/check-pyxis-defs.pygreen for bothrustandcppacross all four pyxis-defs projects — no project uses unions yet, so it's a pure regression check that the new item kind didn't disturb existing output.Known limitation, pre-existing
A field cannot reference a nested type by name, bare or qualified —
pub header: Outer::Headerfails for a plaintypetoo, not just a union. The corpus input works around it. Worth its own issue.