From c5bd94bb4750176b46027945e2db76c732700466 Mon Sep 17 00:00:00 2001 From: Philpax Date: Sun, 26 Jul 2026 16:05:16 +0200 Subject: [PATCH 1/2] Add unions 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'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 --- CONTRIBUTING.md | 1 + codegen_tests/input/unions.pyxis | 102 ++ codegen_tests/output/cpp/include/unions.hpp | 153 +++ codegen_tests/output/cpp/src/_all_headers.cpp | 1 + codegen_tests/output/json/output.json | 1067 ++++++++++++++++- codegen_tests/output/rust/lib.rs | 1 + codegen_tests/output/rust/unions.rs | 286 +++++ docs/cpp_backend.md | 34 + docs/json_backend.md | 9 +- docs/language.md | 139 ++- docs/rust_backend.md | 29 + src/backends/cpp/assemble.rs | 1 + src/backends/cpp/deps/mod.rs | 20 + src/backends/cpp/render/mod.rs | 9 + src/backends/cpp/render/nested.rs | 27 +- src/backends/cpp/render/structs.rs | 19 +- src/backends/cpp/render/unions.rs | 81 ++ src/backends/cpp/write.rs | 1 + src/backends/json/convert.rs | 42 +- src/backends/json/mod.rs | 2 +- src/backends/json/schema.rs | 36 +- src/backends/rust/items.rs | 144 ++- src/grammar.rs | 3 +- src/parser/items/mod.rs | 64 +- src/parser/items/types.rs | 62 +- src/parser/items/types/tests.rs | 78 ++ src/parser/items/unions.rs | 90 ++ src/pretty_print/definitions.rs | 52 +- src/pretty_print/tests/formatting.rs | 43 + src/semantic/declaration_registry.rs | 13 +- src/semantic/doc_links/resolver.rs | 41 + src/semantic/error/context.rs | 3 + src/semantic/error/messages.rs | 81 ++ src/semantic/error/mod.rs | 61 + src/semantic/mod.rs | 1 + src/semantic/name_index.rs | 140 +-- src/semantic/queries/helpers.rs | 94 +- src/semantic/queries/resolve.rs | 26 +- src/semantic/queries/root.rs | 16 + src/semantic/queries/source_map.rs | 58 +- src/semantic/tests/mod.rs | 1 + src/semantic/tests/unions.rs | 517 ++++++++ src/semantic/type_definition/build.rs | 127 +- src/semantic/type_definition/mod.rs | 3 + src/semantic/type_registry/mod.rs | 2 +- src/semantic/types/item.rs | 19 +- src/semantic/types/mod.rs | 2 + src/semantic/union_definition/build.rs | 604 ++++++++++ src/semantic/union_definition/mod.rs | 103 ++ src/tokenizer/token.rs | 2 + tooling/lsp/src/handlers/completion.rs | 3 +- tooling/lsp/src/handlers/doc_links.rs | 38 +- .../src/handlers/hover_format/attributes.rs | 37 +- .../lsp/src/handlers/hover_format/types.rs | 41 +- tooling/lsp/src/handlers/navigation/hover.rs | 23 +- tooling/lsp/src/handlers/navigation/mod.rs | 11 + tooling/lsp/src/handlers/outline.rs | 1 + tooling/lsp/src/handlers/references.rs | 66 +- tooling/lsp/tests/snapshots.rs | 4 + tooling/tree-sitter-pyxis | 2 +- tooling/zed-pyxis/extension.toml | 2 +- types/json.ts | 49 +- viewer/src/components/Attributes.tsx | 4 +- viewer/src/components/FieldSourceView.tsx | 27 +- viewer/src/components/FieldTable.tsx | 20 +- viewer/src/components/ItemView.tsx | 101 +- viewer/src/components/KindIcon.tsx | 2 + viewer/src/components/ModuleView.tsx | 8 +- viewer/src/components/SearchBar.tsx | 1 + viewer/src/components/Sidebar.tsx | 42 +- viewer/src/index.css | 6 + viewer/src/utils/colors.ts | 5 + viewer/src/utils/searchUtils.ts | 13 +- 73 files changed, 4650 insertions(+), 366 deletions(-) create mode 100644 codegen_tests/input/unions.pyxis create mode 100644 codegen_tests/output/cpp/include/unions.hpp create mode 100644 codegen_tests/output/rust/unions.rs create mode 100644 src/backends/cpp/render/unions.rs create mode 100644 src/parser/items/unions.rs create mode 100644 src/semantic/tests/unions.rs create mode 100644 src/semantic/union_definition/build.rs create mode 100644 src/semantic/union_definition/mod.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0aa309e9..736b9ef2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,7 @@ When you change the language (new attributes, syntax, types, etc.), you must aud |---------|------|------------------------| | **Compiler frontend** | `src/` | Always. Parser, semantic IR, and all backends (Rust/C++/JSON). | | **Parser test helpers** | `src/parser/attributes.rs` | New `Attribute` variants or test constructors (e.g. `Attribute::pinned()`). The grammar parses `#[ident]` attributes generically, so simple ident attributes need no grammar change. | +| **New item kinds** | `src/parser/items/mod.rs`, `src/semantic/types/item.rs` | Adding a whole item kind (like `union`) means a variant on *both* `ItemDefinitionInner` enums. Both are matched exhaustively in ~30 places across the compiler, LSP, and backends — let the compiler enumerate them rather than grepping. Also `SigKind` (`src/semantic/name_index.rs`) and `ItemKind` (`src/semantic/error/context.rs`). | | **`AttributeName` enum** | `src/semantic/error.rs` | Only if the new attribute participates in conflicting-attribute validation (e.g. `#[packed]` + `#[align]`). Most attributes don't need an entry. | | **Tree-sitter grammar** | `tooling/tree-sitter-pyxis/grammar.js` | Only for new syntax forms or keywords. Ident attributes (`#[foo]`) are already parsed generically as `attribute_ident -> $.identifier`. | | **Highlights query** | `tooling/tree-sitter-pyxis/queries/highlights.scm` | Rarely. Attributes are highlighted generically via `(attribute) @attribute`. Only change if a new node type needs a capture. | diff --git a/codegen_tests/input/unions.pyxis b/codegen_tests/input/unions.pyxis new file mode 100644 index 00000000..2c59da6e --- /dev/null +++ b/codegen_tests/input/unions.pyxis @@ -0,0 +1,102 @@ +/// A value whose bytes have several competing readings. Which one applies is +/// decided by [`TaggedValue::kind`], not by the union itself. +#[copyable] +pub union Payload { + /// Read as a signed integer. + pub as_int: i32, + /// Read as a float. + pub as_float: f32, + /// Read as a pointer to something else. + pub as_ptr: *mut i32, +} + +/// A tagged value pairing a discriminant with a [`Payload`]. When `kind` is 0, +/// the live member is [`Payload::as_int`]. +#[size(0x10)] +pub type TaggedValue { + pub kind: u32, + + #[address(0x8)] + pub payload: Payload, +} + +/// A union declared inline in field position. It lowers to a generated sibling +/// item named `InlineScratchDataUnion`. +pub type InlineScratch { + pub tag: u16, + pub _reserved: unknown<0x6>, + pub data: union { + pub as_u64: u64, + pub as_bytes: [u8; 8], + }, +} + +/// Members can be structs, and the union takes the strictest alignment and the +/// largest size among them. +#[copyable] +pub type Vec2 { + pub x: f32, + pub y: f32, +} + +#[copyable] +pub union Geometry { + pub point: Vec2, + pub scalar: f64, + pub raw: [u8; 8], +} + +/// `#[size]` pads a union out beyond its largest member, and `#[align]` +/// over-aligns it. +#[size(0x10), align(16)] +pub union PaddedSlot { + pub small: u32, + pub medium: u64, +} + +/// `#[size]` alone asks for more room than any member needs, so the union gains +/// a whole-width `_padding` member. Rounding up to `#[align]` needs no such help +/// — see [`PaddedSlot`], which gets none. +#[size(0x10)] +pub union OversizedSlot { + pub small: u32, + pub medium: u64, +} + +/// A pinned union can be neither copied nor moved. +#[pinned] +pub union Anchored { + pub value: u64, + pub halves: [u32; 2], +} + +/// `#[packed]` drops the union's alignment to 1. +#[packed] +pub union PackedPair { + pub word: u16, + pub bytes: [u8; 2], +} + +/// A union nested inside another union, plus a nested item declaration in a +/// union body. +pub union Outer { + /// A type declared inside a union body. Nested declarations work here + /// exactly as they do in a `type` body. + pub type Header { + pub magic: u32, + }, + + pub raw: u32, + pub inner: union { + pub lo: u16, + pub hi: [u8; 2], + }, +} + +/// A defaultable union gets a hand-written `Default` in Rust — a union can't +/// derive one, because nothing knows which member is live. +#[defaultable, copyable] +pub union ZeroInit { + pub count: u32, + pub flags: [u8; 4], +} diff --git a/codegen_tests/output/cpp/include/unions.hpp b/codegen_tests/output/cpp/include/unions.hpp new file mode 100644 index 00000000..783c94ff --- /dev/null +++ b/codegen_tests/output/cpp/include/unions.hpp @@ -0,0 +1,153 @@ +// @generated by pyxis — do not edit +#pragma once + +#include +#include +#include "pyxis_runtime.hpp" + +namespace unions { + struct InlineScratch; + struct TaggedValue; + struct Vec2; + union Anchored; + union Geometry; + union InlineScratchDataUnion; + union Outer; + union OuterInnerUnion; + union OversizedSlot; + union PackedPair; + union PaddedSlot; + union Payload; + union ZeroInit; + + /// A pinned union can be neither copied nor moved. + union alignas(8) Anchored { + ::std::uint64_t value; + ::std::uint32_t halves[2]; + + Anchored(const Anchored&) = delete; + Anchored(Anchored&&) = delete; + Anchored& operator=(const Anchored&) = delete; + Anchored& operator=(Anchored&&) = delete; + }; + static_assert(sizeof(Anchored) == 0x8); + static_assert(alignof(Anchored) == 8); + + /// Members can be structs, and the union takes the strictest alignment and the + /// largest size among them. + struct alignas(8) Vec2 { + float x; + float y; + }; + static_assert(sizeof(Vec2) == 0x8); + static_assert(alignof(Vec2) == 8); + + union alignas(8) Geometry { + Vec2 point; + double scalar; + ::std::uint8_t raw[8]; + }; + static_assert(sizeof(Geometry) == 0x8); + static_assert(alignof(Geometry) == 8); + + union alignas(8) InlineScratchDataUnion { + ::std::uint64_t as_u64; + ::std::uint8_t as_bytes[8]; + }; + static_assert(sizeof(InlineScratchDataUnion) == 0x8); + static_assert(alignof(InlineScratchDataUnion) == 8); + + /// A union declared inline in field position. It lowers to a generated sibling + /// item named `InlineScratchDataUnion`. + struct alignas(8) InlineScratch { + ::std::uint16_t tag; + ::std::uint8_t _reserved[6]; + InlineScratchDataUnion data; + }; + static_assert(sizeof(InlineScratch) == 0x10); + static_assert(alignof(InlineScratch) == 8); + + union alignas(2) OuterInnerUnion { + ::std::uint16_t lo; + ::std::uint8_t hi[2]; + }; + static_assert(sizeof(OuterInnerUnion) == 0x2); + static_assert(alignof(OuterInnerUnion) == 2); + + /// A union nested inside another union, plus a nested item declaration in a + /// union body. + union alignas(4) Outer { + ::std::uint32_t raw; + OuterInnerUnion inner; + + /// A type declared inside a union body. Nested declarations work here + /// exactly as they do in a `type` body. + struct Header { + ::std::uint32_t magic; + }; + }; + static_assert(sizeof(Outer) == 0x4); + static_assert(alignof(Outer) == 4); + + /// `#[size]` alone asks for more room than any member needs, so the union gains + /// a whole-width `_padding` member. Rounding up to `#[align]` needs no such help + /// — see [`PaddedSlot`](@ref unions::PaddedSlot), which gets none. + union alignas(8) OversizedSlot { + ::std::uint32_t small; + ::std::uint64_t medium; + ::std::uint8_t _padding[16]; + }; + static_assert(sizeof(OversizedSlot) == 0x10); + static_assert(alignof(OversizedSlot) == 8); + + /// `#[packed]` drops the union's alignment to 1. + #pragma pack(push, 1) + union alignas(1) PackedPair { + ::std::uint16_t word; + ::std::uint8_t bytes[2]; + }; + #pragma pack(pop) + static_assert(sizeof(PackedPair) == 0x2); + static_assert(alignof(PackedPair) == 1); + + /// `#[size]` pads a union out beyond its largest member, and `#[align]` + /// over-aligns it. + union alignas(16) PaddedSlot { + ::std::uint32_t small; + ::std::uint64_t medium; + }; + static_assert(sizeof(PaddedSlot) == 0x10); + static_assert(alignof(PaddedSlot) == 16); + + /// A value whose bytes have several competing readings. Which one applies is + /// decided by [`TaggedValue::kind`](@ref unions::TaggedValue::kind), not by the union itself. + union alignas(8) Payload { + /// Read as a signed integer. + ::std::int32_t as_int; + /// Read as a float. + float as_float; + /// Read as a pointer to something else. + ::std::int32_t* as_ptr; + }; + static_assert(sizeof(Payload) == 0x8); + static_assert(alignof(Payload) == 8); + + /// A tagged value pairing a discriminant with a [`Payload`](@ref unions::Payload). When `kind` is 0, + /// the live member is [`Payload::as_int`](@ref unions::Payload::as_int). + struct alignas(8) TaggedValue { + ::std::uint32_t kind; + ::std::uint8_t _field_4[4]; + Payload payload; + }; + static_assert(sizeof(TaggedValue) == 0x10); + static_assert(alignof(TaggedValue) == 8); + + /// A defaultable union gets a hand-written `Default` in Rust — a union can't + /// derive one, because nothing knows which member is live. + union alignas(4) ZeroInit { + ::std::uint32_t count; + ::std::uint8_t flags[4]; + }; + static_assert(sizeof(ZeroInit) == 0x4); + static_assert(alignof(ZeroInit) == 4); +} // namespace unions diff --git a/codegen_tests/output/cpp/src/_all_headers.cpp b/codegen_tests/output/cpp/src/_all_headers.cpp index 6201e33b..625003f2 100644 --- a/codegen_tests/output/cpp/src/_all_headers.cpp +++ b/codegen_tests/output/cpp/src/_all_headers.cpp @@ -34,6 +34,7 @@ #include "type_alias_reexport.hpp" #include "type_aliases.hpp" #include "unicode.hpp" +#include "unions.hpp" #include "vftable_indices.hpp" #include "world/atmosphere.hpp" #include "world/deep/marker.hpp" diff --git a/codegen_tests/output/json/output.json b/codegen_tests/output/json/output.json index 1406e4b7..b95a4083 100644 --- a/codegen_tests/output/json/output.json +++ b/codegen_tests/output/json/output.json @@ -1,5 +1,5 @@ { - "schema_version": 11, + "schema_version": 12, "pyxis_version": "0.1.0", "pointer_size": 8, "project_name": "test-project", @@ -9860,6 +9860,967 @@ "line": 8 } }, + "unions::Anchored": { + "path": "unions::Anchored", + "visibility": "public", + "size": 8, + "alignment": 8, + "category": "defined", + "kind": { + "type": "union", + "doc": " A pinned union can be neither copied nor moved.", + "fields": [ + { + "visibility": "public", + "name": "value", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u64" + }, + "offset": 0, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 69 + } + }, + { + "visibility": "public", + "name": "halves", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u32" + }, + "size": 2 + }, + "offset": 0, + "size": 8, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 70 + } + } + ], + "size": 8, + "alignment": 8, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": true + }, + "source": { + "file_index": 36, + "line": 68 + } + }, + "unions::Geometry": { + "path": "unions::Geometry", + "visibility": "public", + "size": 8, + "alignment": 8, + "category": "defined", + "kind": { + "type": "union", + "doc": null, + "fields": [ + { + "visibility": "public", + "name": "point", + "doc": null, + "type_ref": { + "type": "raw", + "path": "unions::Vec2" + }, + "offset": 0, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 44 + } + }, + { + "visibility": "public", + "name": "scalar", + "doc": null, + "type_ref": { + "type": "raw", + "path": "f64" + }, + "offset": 0, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 45 + } + }, + { + "visibility": "public", + "name": "raw", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 8 + }, + "offset": 0, + "size": 8, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 46 + } + } + ], + "size": 8, + "alignment": 8, + "copyable": true, + "cloneable": true, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 43 + } + }, + "unions::InlineScratch": { + "path": "unions::InlineScratch", + "visibility": "public", + "size": 16, + "alignment": 8, + "category": "defined", + "kind": { + "type": "type", + "doc": " A union declared inline in field position. It lowers to a generated sibling\n item named `InlineScratchDataUnion`.", + "fields": [ + { + "visibility": "public", + "name": "tag", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u16" + }, + "offset": 0, + "size": 2, + "alignment": 2, + "is_base": false, + "source": { + "file_index": 36, + "line": 26 + } + }, + { + "visibility": "public", + "name": "_reserved", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 6 + }, + "offset": 2, + "size": 6, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 27 + } + }, + { + "visibility": "public", + "name": "data", + "doc": null, + "type_ref": { + "type": "raw", + "path": "unions::InlineScratchDataUnion" + }, + "offset": 8, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 28 + } + } + ], + "associated_functions": [], + "vftable": null, + "singleton": null, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 25 + } + }, + "unions::InlineScratchDataUnion": { + "path": "unions::InlineScratchDataUnion", + "visibility": "public", + "size": 8, + "alignment": 8, + "category": "defined", + "kind": { + "type": "union", + "doc": null, + "fields": [ + { + "visibility": "public", + "name": "as_u64", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u64" + }, + "offset": 0, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 29 + } + }, + { + "visibility": "public", + "name": "as_bytes", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 8 + }, + "offset": 0, + "size": 8, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 30 + } + } + ], + "size": 8, + "alignment": 8, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 28 + } + }, + "unions::Outer": { + "path": "unions::Outer", + "visibility": "public", + "size": 4, + "alignment": 4, + "category": "defined", + "kind": { + "type": "union", + "doc": " A union nested inside another union, plus a nested item declaration in a\n union body.", + "fields": [ + { + "visibility": "public", + "name": "raw", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 89 + } + }, + { + "visibility": "public", + "name": "inner", + "doc": null, + "type_ref": { + "type": "raw", + "path": "unions::OuterInnerUnion" + }, + "offset": 0, + "size": 2, + "alignment": 2, + "is_base": false, + "source": { + "file_index": 36, + "line": 90 + } + } + ], + "size": 4, + "alignment": 4, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false, + "nested_items": [ + "unions::Outer::Header" + ] + }, + "source": { + "file_index": 36, + "line": 82 + } + }, + "unions::Outer::Header": { + "path": "unions::Outer::Header", + "visibility": "public", + "size": 4, + "alignment": 4, + "category": "defined", + "kind": { + "type": "type", + "doc": " A type declared inside a union body. Nested declarations work here\n exactly as they do in a `type` body.", + "fields": [ + { + "visibility": "public", + "name": "magic", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 86 + } + } + ], + "associated_functions": [], + "vftable": null, + "singleton": null, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 85 + } + }, + "unions::OuterInnerUnion": { + "path": "unions::OuterInnerUnion", + "visibility": "public", + "size": 2, + "alignment": 2, + "category": "defined", + "kind": { + "type": "union", + "doc": null, + "fields": [ + { + "visibility": "public", + "name": "lo", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u16" + }, + "offset": 0, + "size": 2, + "alignment": 2, + "is_base": false, + "source": { + "file_index": 36, + "line": 91 + } + }, + { + "visibility": "public", + "name": "hi", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 2 + }, + "offset": 0, + "size": 2, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 92 + } + } + ], + "size": 2, + "alignment": 2, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 90 + } + }, + "unions::OversizedSlot": { + "path": "unions::OversizedSlot", + "visibility": "public", + "size": 16, + "alignment": 8, + "category": "defined", + "kind": { + "type": "union", + "doc": " `#[size]` alone asks for more room than any member needs, so the union gains\n a whole-width `_padding` member. Rounding up to `#[align]` needs no such help\n — see [`PaddedSlot`], which gets none.", + "doc_links": [ + { + "text": "PaddedSlot", + "target_kind": "item", + "path": "unions::PaddedSlot" + } + ], + "fields": [ + { + "visibility": "public", + "name": "small", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 62 + } + }, + { + "visibility": "public", + "name": "medium", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u64" + }, + "offset": 0, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 63 + } + }, + { + "visibility": "private", + "name": "_padding", + "doc": null, + "doc_links": [ + { + "text": "PaddedSlot", + "target_kind": "item", + "path": "unions::PaddedSlot" + } + ], + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 16 + }, + "offset": 0, + "size": 16, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 57 + } + } + ], + "size": 16, + "alignment": 8, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 61 + } + }, + "unions::PackedPair": { + "path": "unions::PackedPair", + "visibility": "public", + "size": 2, + "alignment": 1, + "category": "defined", + "kind": { + "type": "union", + "doc": " `#[packed]` drops the union's alignment to 1.", + "fields": [ + { + "visibility": "public", + "name": "word", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u16" + }, + "offset": 0, + "size": 2, + "alignment": 2, + "is_base": false, + "source": { + "file_index": 36, + "line": 76 + } + }, + { + "visibility": "public", + "name": "bytes", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 2 + }, + "offset": 0, + "size": 2, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 77 + } + } + ], + "size": 2, + "alignment": 1, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": true, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 75 + } + }, + "unions::PaddedSlot": { + "path": "unions::PaddedSlot", + "visibility": "public", + "size": 16, + "alignment": 16, + "category": "defined", + "kind": { + "type": "union", + "doc": " `#[size]` pads a union out beyond its largest member, and `#[align]`\n over-aligns it.", + "fields": [ + { + "visibility": "public", + "name": "small", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 53 + } + }, + { + "visibility": "public", + "name": "medium", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u64" + }, + "offset": 0, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 54 + } + } + ], + "size": 16, + "alignment": 16, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 52 + } + }, + "unions::Payload": { + "path": "unions::Payload", + "visibility": "public", + "size": 8, + "alignment": 8, + "category": "defined", + "kind": { + "type": "union", + "doc": " A value whose bytes have several competing readings. Which one applies is\n decided by [`TaggedValue::kind`], not by the union itself.", + "doc_links": [ + { + "text": "TaggedValue::kind", + "target_kind": "item", + "path": "unions::TaggedValue", + "anchor": "field-kind" + } + ], + "fields": [ + { + "visibility": "public", + "name": "as_int", + "doc": " Read as a signed integer.", + "type_ref": { + "type": "raw", + "path": "i32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 6 + } + }, + { + "visibility": "public", + "name": "as_float", + "doc": " Read as a float.", + "type_ref": { + "type": "raw", + "path": "f32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 8 + } + }, + { + "visibility": "public", + "name": "as_ptr", + "doc": " Read as a pointer to something else.", + "type_ref": { + "type": "mut_pointer", + "inner": { + "type": "raw", + "path": "i32" + } + }, + "offset": 0, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 10 + } + } + ], + "size": 8, + "alignment": 8, + "copyable": true, + "cloneable": true, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 4 + } + }, + "unions::TaggedValue": { + "path": "unions::TaggedValue", + "visibility": "public", + "size": 16, + "alignment": 8, + "category": "defined", + "kind": { + "type": "type", + "doc": " A tagged value pairing a discriminant with a [`Payload`]. When `kind` is 0,\n the live member is [`Payload::as_int`].", + "doc_links": [ + { + "text": "Payload", + "target_kind": "item", + "path": "unions::Payload" + }, + { + "text": "Payload::as_int", + "target_kind": "item", + "path": "unions::Payload", + "anchor": "field-as_int" + } + ], + "fields": [ + { + "visibility": "public", + "name": "kind", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 17 + } + }, + { + "visibility": "private", + "name": "_field_4", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 4 + }, + "offset": 4, + "size": 4, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 20 + } + }, + { + "visibility": "public", + "name": "payload", + "doc": null, + "type_ref": { + "type": "raw", + "path": "unions::Payload" + }, + "offset": 8, + "size": 8, + "alignment": 8, + "is_base": false, + "source": { + "file_index": 36, + "line": 20 + } + } + ], + "associated_functions": [], + "vftable": null, + "singleton": null, + "copyable": false, + "cloneable": false, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 16 + } + }, + "unions::Vec2": { + "path": "unions::Vec2", + "visibility": "public", + "size": 8, + "alignment": 8, + "category": "defined", + "kind": { + "type": "type", + "doc": " Members can be structs, and the union takes the strictest alignment and the\n largest size among them.", + "fields": [ + { + "visibility": "public", + "name": "x", + "doc": null, + "type_ref": { + "type": "raw", + "path": "f32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 38 + } + }, + { + "visibility": "public", + "name": "y", + "doc": null, + "type_ref": { + "type": "raw", + "path": "f32" + }, + "offset": 4, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 39 + } + } + ], + "associated_functions": [], + "vftable": null, + "singleton": null, + "copyable": true, + "cloneable": true, + "defaultable": false, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 37 + } + }, + "unions::ZeroInit": { + "path": "unions::ZeroInit", + "visibility": "public", + "size": 4, + "alignment": 4, + "category": "defined", + "kind": { + "type": "union", + "doc": " A defaultable union gets a hand-written `Default` in Rust — a union can't\n derive one, because nothing knows which member is live.", + "fields": [ + { + "visibility": "public", + "name": "count", + "doc": null, + "type_ref": { + "type": "raw", + "path": "u32" + }, + "offset": 0, + "size": 4, + "alignment": 4, + "is_base": false, + "source": { + "file_index": 36, + "line": 100 + } + }, + { + "visibility": "public", + "name": "flags", + "doc": null, + "type_ref": { + "type": "array", + "inner": { + "type": "raw", + "path": "u8" + }, + "size": 4 + }, + "offset": 0, + "size": 4, + "alignment": 1, + "is_base": false, + "source": { + "file_index": 36, + "line": 101 + } + } + ], + "size": 4, + "alignment": 4, + "copyable": true, + "cloneable": true, + "defaultable": true, + "packed": false, + "pinned": false + }, + "source": { + "file_index": 36, + "line": 99 + } + }, "vftable_indices::IndexedVftable": { "path": "vftable_indices::IndexedVftable", "visibility": "private", @@ -9886,7 +10847,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 36, + "file_index": 37, "line": 13 } }, @@ -9903,7 +10864,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 36, + "file_index": 37, "line": 23 } } @@ -9930,7 +10891,7 @@ }, "calling_convention": "system", "source": { - "file_index": 36, + "file_index": 37, "line": 13 } }, @@ -9950,7 +10911,7 @@ "return_type": null, "calling_convention": "system", "source": { - "file_index": 36, + "file_index": 37, "line": 11 } }, @@ -9981,7 +10942,7 @@ }, "calling_convention": "system", "source": { - "file_index": 36, + "file_index": 37, "line": 16 } }, @@ -10004,7 +10965,7 @@ }, "calling_convention": "system", "source": { - "file_index": 36, + "file_index": 37, "line": 19 } } @@ -10018,7 +10979,7 @@ "pinned": false }, "source": { - "file_index": 36, + "file_index": 37, "line": 9 } }, @@ -10061,7 +11022,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 36, + "file_index": 37, "line": 13 } }, @@ -10091,7 +11052,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 36, + "file_index": 37, "line": 11 } }, @@ -10131,7 +11092,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 36, + "file_index": 37, "line": 16 } }, @@ -10164,7 +11125,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 36, + "file_index": 37, "line": 19 } } @@ -10179,7 +11140,7 @@ "pinned": false }, "source": { - "file_index": 36, + "file_index": 37, "line": 13 } }, @@ -10227,7 +11188,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 39, + "file_index": 40, "line": 10 } } @@ -10242,7 +11203,7 @@ "pinned": false }, "source": { - "file_index": 39, + "file_index": 40, "line": 9 } }, @@ -10272,7 +11233,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 37, + "file_index": 38, "line": 8 } }, @@ -10289,7 +11250,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 37, + "file_index": 38, "line": 9 } }, @@ -10306,7 +11267,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 37, + "file_index": 38, "line": 10 } } @@ -10321,7 +11282,7 @@ "pinned": false }, "source": { - "file_index": 37, + "file_index": 38, "line": 7 } }, @@ -10348,7 +11309,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 38, + "file_index": 39, "line": 6 } } @@ -10363,7 +11324,7 @@ "pinned": false }, "source": { - "file_index": 38, + "file_index": 39, "line": 5 } }, @@ -10390,7 +11351,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 40, + "file_index": 41, "line": 4 } }, @@ -10407,7 +11368,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 40, + "file_index": 41, "line": 5 } } @@ -10422,7 +11383,7 @@ "pinned": false }, "source": { - "file_index": 40, + "file_index": 41, "line": 3 } }, @@ -10459,7 +11420,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 41, + "file_index": 42, "line": 14 } }, @@ -10479,7 +11440,7 @@ "alignment": 8, "is_base": false, "source": { - "file_index": 41, + "file_index": 42, "line": 15 } } @@ -10494,7 +11455,7 @@ "pinned": false }, "source": { - "file_index": 41, + "file_index": 42, "line": 13 } }, @@ -10521,7 +11482,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 42, + "file_index": 43, "line": 10 } }, @@ -10538,7 +11499,7 @@ "alignment": 4, "is_base": false, "source": { - "file_index": 42, + "file_index": 43, "line": 13 } }, @@ -10559,7 +11520,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 42, + "file_index": 43, "line": 6 } } @@ -10574,7 +11535,7 @@ "pinned": false }, "source": { - "file_index": 42, + "file_index": 43, "line": 9 } }, @@ -10605,7 +11566,7 @@ "alignment": 1, "is_base": false, "source": { - "file_index": 42, + "file_index": 43, "line": 1 } } @@ -10620,7 +11581,7 @@ "pinned": false }, "source": { - "file_index": 42, + "file_index": 43, "line": 4 } } @@ -11514,6 +12475,31 @@ "line": 3 } }, + "unions": { + "doc": null, + "items": [ + "unions::Anchored", + "unions::Geometry", + "unions::InlineScratch", + "unions::InlineScratchDataUnion", + "unions::Outer", + "unions::OuterInnerUnion", + "unions::OversizedSlot", + "unions::PackedPair", + "unions::PaddedSlot", + "unions::Payload", + "unions::TaggedValue", + "unions::Vec2", + "unions::ZeroInit" + ], + "submodules": {}, + "functions": [], + "splices": [], + "source": { + "file_index": 36, + "line": 1 + } + }, "vftable_indices": { "doc": " Vftable with explicit `#[index]` attributes and padding gaps.\n\n `first` is placed at index 0, `second` jumps to index 2 (leaving a padding\n slot at index 1), and `third` continues at index 3. The `#[size(4)]` matches\n the total slot count exactly. This verifies that ascending indices with\n gaps produce the correct vftable layout.", "items": [ @@ -11524,7 +12510,7 @@ "functions": [], "splices": [], "source": { - "file_index": 36, + "file_index": 37, "line": 8 } }, @@ -11543,7 +12529,7 @@ "functions": [], "splices": [], "source": { - "file_index": 37, + "file_index": 38, "line": 1 } }, @@ -11560,7 +12546,7 @@ "functions": [], "splices": [], "source": { - "file_index": 38, + "file_index": 39, "line": 1 } } @@ -11578,7 +12564,7 @@ "functions": [], "splices": [], "source": { - "file_index": 40, + "file_index": 41, "line": 1 } } @@ -11597,7 +12583,7 @@ } ], "source": { - "file_index": 39, + "file_index": 40, "line": 7 } }, @@ -11610,7 +12596,7 @@ "functions": [], "splices": [], "source": { - "file_index": 41, + "file_index": 42, "line": 1 } }, @@ -11624,7 +12610,7 @@ "functions": [], "splices": [], "source": { - "file_index": 42, + "file_index": 43, "line": 1 } } @@ -11666,6 +12652,7 @@ "type_alias_reexport.pyxis", "type_aliases.pyxis", "unicode.pyxis", + "unions.pyxis", "vftable_indices.pyxis", "world/atmosphere.pyxis", "world/deep/marker.pyxis", diff --git a/codegen_tests/output/rust/lib.rs b/codegen_tests/output/rust/lib.rs index 26431c84..435b0cfb 100644 --- a/codegen_tests/output/rust/lib.rs +++ b/codegen_tests/output/rust/lib.rs @@ -131,6 +131,7 @@ pub mod two_base_classes; pub mod type_alias_reexport; pub mod type_aliases; pub mod unicode; +pub mod unions; pub mod vftable_indices; pub mod world; pub mod world_consumer; diff --git a/codegen_tests/output/rust/unions.rs b/codegen_tests/output/rust/unions.rs new file mode 100644 index 00000000..af2bd60b --- /dev/null +++ b/codegen_tests/output/rust/unions.rs @@ -0,0 +1,286 @@ +#![cfg_attr(any(), rustfmt::skip)] +#[repr(C, align(8))] +/// A pinned union can be neither copied nor moved. +pub union Anchored { + pub value: ::core::mem::ManuallyDrop, + pub halves: ::core::mem::ManuallyDrop<[u32; 2]>, +} +fn _Anchored_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x8], Anchored>([0u8; 0x8]); + } + unreachable!() +} +impl ::core::fmt::Debug for Anchored { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("Anchored", " { .. }")) + } +} +#[derive(Copy, Clone)] +#[repr(C, align(8))] +pub union Geometry { + pub point: ::core::mem::ManuallyDrop, + pub scalar: ::core::mem::ManuallyDrop, + pub raw: ::core::mem::ManuallyDrop<[u8; 8]>, +} +fn _Geometry_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x8], Geometry>([0u8; 0x8]); + } + unreachable!() +} +impl ::core::fmt::Debug for Geometry { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("Geometry", " { .. }")) + } +} +#[repr(C, align(8))] +/// A union declared inline in field position. It lowers to a generated sibling +/// item named `InlineScratchDataUnion`. +pub struct InlineScratch { + pub tag: u16, + pub _reserved: [u8; 6], + pub data: crate::unions::InlineScratchDataUnion, +} +fn _InlineScratch_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x10], InlineScratch>([0u8; 0x10]); + } + unreachable!() +} +impl InlineScratch {} +impl std::convert::AsRef for InlineScratch { + fn as_ref(&self) -> &InlineScratch { + self + } +} +impl std::convert::AsMut for InlineScratch { + fn as_mut(&mut self) -> &mut InlineScratch { + self + } +} +#[repr(C, align(8))] +pub union InlineScratchDataUnion { + pub as_u64: ::core::mem::ManuallyDrop, + pub as_bytes: ::core::mem::ManuallyDrop<[u8; 8]>, +} +fn _InlineScratchDataUnion_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x8], InlineScratchDataUnion>([0u8; 0x8]); + } + unreachable!() +} +impl ::core::fmt::Debug for InlineScratchDataUnion { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("InlineScratchDataUnion", " { .. }")) + } +} +#[repr(C, align(4))] +/// A union nested inside another union, plus a nested item declaration in a +/// union body. +pub union Outer { + pub raw: ::core::mem::ManuallyDrop, + pub inner: ::core::mem::ManuallyDrop, +} +fn _Outer_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x4], Outer>([0u8; 0x4]); + } + unreachable!() +} +impl ::core::fmt::Debug for Outer { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("Outer", " { .. }")) + } +} +#[repr(C, align(4))] +/// A type declared inside a union body. Nested declarations work here +/// exactly as they do in a `type` body. +pub struct Outer_Header { + pub magic: u32, +} +fn _Outer_Header_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x4], Outer_Header>([0u8; 0x4]); + } + unreachable!() +} +impl Outer_Header {} +impl std::convert::AsRef for Outer_Header { + fn as_ref(&self) -> &Outer_Header { + self + } +} +impl std::convert::AsMut for Outer_Header { + fn as_mut(&mut self) -> &mut Outer_Header { + self + } +} +#[repr(C, align(2))] +pub union OuterInnerUnion { + pub lo: ::core::mem::ManuallyDrop, + pub hi: ::core::mem::ManuallyDrop<[u8; 2]>, +} +fn _OuterInnerUnion_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x2], OuterInnerUnion>([0u8; 0x2]); + } + unreachable!() +} +impl ::core::fmt::Debug for OuterInnerUnion { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("OuterInnerUnion", " { .. }")) + } +} +#[repr(C, align(8))] +/// `#[size]` alone asks for more room than any member needs, so the union gains +/// a whole-width `_padding` member. Rounding up to `#[align]` needs no such help +/// — see [`PaddedSlot`](crate::unions::PaddedSlot), which gets none. +pub union OversizedSlot { + pub small: ::core::mem::ManuallyDrop, + pub medium: ::core::mem::ManuallyDrop, + _padding: ::core::mem::ManuallyDrop<[u8; 16]>, +} +fn _OversizedSlot_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x10], OversizedSlot>([0u8; 0x10]); + } + unreachable!() +} +impl ::core::fmt::Debug for OversizedSlot { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("OversizedSlot", " { .. }")) + } +} +#[repr(C, packed)] +/// `#[packed]` drops the union's alignment to 1. +pub union PackedPair { + pub word: ::core::mem::ManuallyDrop, + pub bytes: ::core::mem::ManuallyDrop<[u8; 2]>, +} +fn _PackedPair_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x2], PackedPair>([0u8; 0x2]); + } + unreachable!() +} +impl ::core::fmt::Debug for PackedPair { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("PackedPair", " { .. }")) + } +} +#[repr(C, align(16))] +/// `#[size]` pads a union out beyond its largest member, and `#[align]` +/// over-aligns it. +pub union PaddedSlot { + pub small: ::core::mem::ManuallyDrop, + pub medium: ::core::mem::ManuallyDrop, +} +fn _PaddedSlot_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x10], PaddedSlot>([0u8; 0x10]); + } + unreachable!() +} +impl ::core::fmt::Debug for PaddedSlot { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("PaddedSlot", " { .. }")) + } +} +#[derive(Copy, Clone)] +#[repr(C, align(8))] +/// A value whose bytes have several competing readings. Which one applies is +/// decided by [`TaggedValue::kind`](crate::unions::TaggedValue::kind), not by the union itself. +pub union Payload { + /// Read as a signed integer. + pub as_int: ::core::mem::ManuallyDrop, + /// Read as a float. + pub as_float: ::core::mem::ManuallyDrop, + /// Read as a pointer to something else. + pub as_ptr: ::core::mem::ManuallyDrop<*mut i32>, +} +fn _Payload_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x8], Payload>([0u8; 0x8]); + } + unreachable!() +} +impl ::core::fmt::Debug for Payload { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("Payload", " { .. }")) + } +} +#[repr(C, align(8))] +/// A tagged value pairing a discriminant with a [`Payload`](crate::unions::Payload). When `kind` is 0, +/// the live member is [`Payload::as_int`](crate::unions::Payload::as_int). +pub struct TaggedValue { + pub kind: u32, + _field_4: [u8; 4], + pub payload: crate::unions::Payload, +} +fn _TaggedValue_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x10], TaggedValue>([0u8; 0x10]); + } + unreachable!() +} +impl TaggedValue {} +impl std::convert::AsRef for TaggedValue { + fn as_ref(&self) -> &TaggedValue { + self + } +} +impl std::convert::AsMut for TaggedValue { + fn as_mut(&mut self) -> &mut TaggedValue { + self + } +} +#[derive(Copy, Clone)] +#[repr(C, align(8))] +/// Members can be structs, and the union takes the strictest alignment and the +/// largest size among them. +pub struct Vec2 { + pub x: f32, + pub y: f32, +} +fn _Vec2_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x8], Vec2>([0u8; 0x8]); + } + unreachable!() +} +impl Vec2 {} +impl std::convert::AsRef for Vec2 { + fn as_ref(&self) -> &Vec2 { + self + } +} +impl std::convert::AsMut for Vec2 { + fn as_mut(&mut self) -> &mut Vec2 { + self + } +} +#[derive(Copy, Clone)] +#[repr(C, align(4))] +/// A defaultable union gets a hand-written `Default` in Rust — a union can't +/// derive one, because nothing knows which member is live. +pub union ZeroInit { + pub count: ::core::mem::ManuallyDrop, + pub flags: ::core::mem::ManuallyDrop<[u8; 4]>, +} +fn _ZeroInit_size_check() { + unsafe { + ::std::mem::transmute::<[u8; 0x4], ZeroInit>([0u8; 0x4]); + } + unreachable!() +} +impl ::core::fmt::Debug for ZeroInit { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.write_str(concat!("ZeroInit", " { .. }")) + } +} +impl ::core::default::Default for ZeroInit { + fn default() -> Self { + unsafe { ::core::mem::zeroed() } + } +} diff --git a/docs/cpp_backend.md b/docs/cpp_backend.md index e210cf89..ce5509c1 100644 --- a/docs/cpp_backend.md +++ b/docs/cpp_backend.md @@ -81,6 +81,36 @@ some_engine_api(&d->base); // ✅ explicit upcast through the field In Rust the equivalent is `d.as_ref()` (which pyxis generates via `AsRef`/`AsMut` impls). The C++ side picks ergonomics over magic. +## Unions + +A pyxis `union` maps straight onto a C++ `union` — the one place where this +backend needs no encoding tricks, since C++ has the construct natively and +with the same semantics. + +```cpp +union alignas(8) Payload { + ::std::int32_t as_int; + float as_float; + ::std::int32_t* as_ptr; +}; +static_assert(sizeof(Payload) == 0x8); +static_assert(alignof(Payload) == 8); +``` + +The same size/alignment `static_assert`s as a struct, `#pragma pack(push, 1)` +for `#[packed]`, and `alignas(N)` from the resolved alignment. Members that are +themselves unions or structs are ordinary members, and a union's members are +full-definition dependencies exactly as a struct's fields are — so the +dependency graph orders them ahead of the union in the header. + +Forward declarations use `union Name;` rather than `struct Name;`: C++ requires +the class-key to match the definition. + +Nested item declarations inside a union body are rendered in-class, the same way +a struct's are. Inline `pub payload: union { ... }` fields become module-scope +`union {Type}{Field}Union` definitions with an ordinary member referring to +them, so the parent struct is unremarkable. + ## Vftables Each type with a `vftable { ... }` block in pyxis gets: @@ -312,6 +342,10 @@ On native Windows no toolchain file is needed; point CMake at MSVC or copy/move constructors and assignment operators, since pinned types must not be relocated in memory (the target C++ code passes pointers to `this` or its fields around). +- **No union member access helpers** — a union's members are emitted as + plain members. Which one is live is a property of the surrounding + data, and pyxis deliberately does not model that relationship, so + there is nothing for the backend to generate an accessor from. - **No member access control** — pyxis's `pub`/private distinction is rust-only. In cpp every method and field is emitted at struct scope with default visibility (public for `struct`). Backend epilogues diff --git a/docs/json_backend.md b/docs/json_backend.md index ff28adb3..4d96fb75 100644 --- a/docs/json_backend.md +++ b/docs/json_backend.md @@ -30,11 +30,11 @@ The output is a `JsonDocumentation` struct serialized to JSON: ## Schema versioning -The current schema version is **11** (`CURRENT_SCHEMA_VERSION` in `src/backends/json.rs`). The version is bumped on breaking shape changes to the JSON output. The version history is documented in the source file's comments. +The current schema version is **12** (`CURRENT_SCHEMA_VERSION` in `src/backends/json/schema.rs`). The version is bumped on breaking shape changes to the JSON output. The version history is documented in the source file's comments. Older documents (pre-v2) omit the `schema_version` field entirely. Consumers should treat a missing value as version 1. The `pyxis_version` field defaults to `"unknown"` when not available. -Notable version history: schema v10 retired the per-module `backends` map in favor of a flat `splices` array of standalone `prologue`/`epilogue` statements, each carrying its own cfg gate. Schema v11 added `c_string`, `struct`, `array`, and `const_ref` variants to `JsonConstValue` for C-string literals, structured initializers, and constant aliases. +Notable version history: schema v10 retired the per-module `backends` map in favor of a flat `splices` array of standalone `prologue`/`epilogue` statements, each carrying its own cfg gate. Schema v11 added `c_string`, `struct`, `array`, and `const_ref` variants to `JsonConstValue` for C-string literals, structured initializers, and constant aliases. Schema v12 added the `Union` item kind. ## Item model @@ -45,6 +45,7 @@ Each item is a `JsonItem` with a `kind` discriminator: | `Type` | A struct/class definition with fields, vftable, and associated functions. | | `Enum` | An enum with variants. | | `Bitflags` | A bitflags definition with members. | +| `Union` | A union: several readings of the same bytes, only one of which applies at a time. | | `TypeAlias` | A type alias pointing at a target type. | | `Constant` | A compile-time constant value. | | `ExternValue` | A global at a fixed address. | @@ -63,6 +64,10 @@ Every item carries: Type definitions carry their fields as `JsonRegion` entries, each with a name, type, offset, visibility, and optional `#[base]` flag. Vftables carry their function slots as `JsonTypeVftable`. +Union definitions carry their members as `JsonRegion` entries too, but **every one has `offset: 0`** - a union's members are competing readings of the same bytes, not a sequence. A member's `size` is its own, which may be smaller than the union's. Consumers that reconstruct offsets by accumulating field sizes must special-case the `Union` kind. `JsonUnionDefinition` also carries its own `size` and `alignment` (the largest member rounded up, and the strictest member's alignment), the `copyable`/`cloneable`/`defaultable`/`packed`/`pinned` flags, and `nested_items`. + +An inline `pub payload: union { ... }` field appears as two things: an ordinary `JsonRegion` on the parent type pointing at a generated item, and that generated `Union` item at module scope named `{Type}{Field}Union`. + ## Cfg surfacing The JSON backend deliberately does **not** filter by `#[cfg(backend = ...)]`. Every item is emitted with its cfg predicate attached as structured `JsonCfg` data, so downstream tooling can render or filter per their own rules. diff --git a/docs/language.md b/docs/language.md index d7c67f14..9bcf4194 100644 --- a/docs/language.md +++ b/docs/language.md @@ -201,6 +201,127 @@ pub type GameObject { } ``` +## Unions + +A union is a set of competing readings of the same bytes. Every member starts at +offset 0; only one applies at a time, and *which* one is a property of the +surrounding data, not of the union itself. Unions turn up constantly in the code +pyxis describes: variant payloads, reinterpreted scratch space, engine value +types that hold an int or a float or a pointer depending on a nearby tag. + +```pyxis +/// A value whose bytes have several competing readings. +#[copyable] +pub union Payload { + /// Read as a signed integer. + pub as_int: i32, + pub as_float: f32, + pub as_ptr: *mut void, +} +``` + +The union's size is that of its largest member, rounded up to its alignment; its +alignment is the strictest of its members'. Note this differs from a `type`, +which falls back to the pointer size when no alignment is implied — a union of +two `u8`s is genuinely 1-aligned, and widening it would inflate its size. + +A union is used like any other type: + +```pyxis +#[size(0x10)] +pub type TaggedValue { + pub kind: u32, + + #[address(0x8)] + pub payload: Payload, +} +``` + +### Inline unions + +A union that isn't worth naming separately can be written directly in field +position. The field name supplies the name: + +```pyxis +pub type Scratch { + pub tag: u16, + pub _reserved: unknown<6>, + + pub data: union { + pub as_u64: u64, + pub as_bytes: [u8; 8], + }, +} +``` + +This desugars to a generated union item at module scope, named +`{Type}{Field}Union` — here `ScratchDataUnion` — plus an ordinary `data` field +referring to it. The generated item is a sibling of its parent rather than a +nested item, mirroring the generated `{Name}Vftable` structs. + +The `union { … }` form is confined to field position. It cannot appear behind a +pointer, inside an array, or in a function signature; for those, declare a named +union. + +### What a union body can contain + +Inside a *named* union, nested `type`, `enum`, `bitflags`, and `union` +declarations work exactly as they do in a `type` body. A member may itself be an +inline union. + +These constructs are rejected: + +| Rejected | Why | +|------|----| +| `#[base]` on a member | A base class must sit at a known offset, and every union member starts at 0. It would break the inheritance hierarchy, the generated conversions, and vftable inheritance. | +| A `vftable` block | A vftable pointer must occupy offset 0 exclusively, which a union cannot guarantee. | +| `#[address]` on a member | Every union member starts at offset 0 by definition. | +| A member named `_` | `_` marks padding, but every union member already covers the same bytes as its siblings. | +| A nested declaration inside an *inline* union | The generated item is synthesised at a module-scope path, so anything declared under it would be unreachable. Declare it in the enclosing type, or give the union a name. | + +A union with no members is an error — it would have no size. + +The name an inline union generates must be free. `pub union FooBarUnion { … }` +alongside `pub type Foo { pub bar: union { … } }` is an error rather than a +silent overwrite, as are two fields whose names differ only in underscores +(`b_c` and `bc` both generate `BC`). + +### Attributes on unions + +`#[size]`, `#[min_size]`, `#[align]`, `#[packed]`, `#[copyable]`, `#[cloneable]`, +`#[defaultable]`, and `#[pinned]` all mean what they do on a `type`. `#[size(N)]` +fixes the size at `N` and rejects any member larger than `N`; `#[align(N)]` +over-aligns it and rejects an `N` below what the members require. + +Padding a union is not a tail the way it is for a type — there is no tail, only +offset 0 — so when `#[size]` or `#[min_size]` asks for more room than any member +needs, the union gains a `_padding` member covering the whole width. It shows up +in the generated code and in the docs like any other member. + +On an *inline* union, these go on the field, and are forwarded to the generated +union item — except `#[address]`, which continues to mean the field's offset +within its parent: + +```pyxis +pub type Slot { + #[address(0x8)] + #[size(0x10)] + pub payload: union { + pub small: u32, + pub medium: u64, + }, +} +``` + +Unions cannot be generic. + +### Discriminants are out of scope + +A union says what the bytes *could* be; it does not say which reading is live. +Selecting a member based on a nearby tag field is a relationship between fields +rather than a layout fact, and pyxis deliberately does not model it. The +consumer decides which reading applies. + ## Enums An enum describes a tagged integer with named variants: @@ -632,7 +753,7 @@ The size is `N` bytes and alignment is 1. It has no type - you can't take a poin `meta` and `functions` are tokenized as keywords but have no current meaning in the language. Avoid them as identifiers to stay forward-compatible with future features. -The full keyword list is: `pub`, `type`, `enum`, `bitflags`, `impl`, `fn`, `extern`, `use`, `meta`, `functions`, `vftable`, `unknown`, `prologue`, `epilogue`, `mut`, `const`, `self`, `Self`, `_`. +The full keyword list is: `pub`, `type`, `enum`, `bitflags`, `union`, `impl`, `fn`, `extern`, `use`, `meta`, `functions`, `vftable`, `unknown`, `prologue`, `epilogue`, `mut`, `const`, `self`, `Self`, `_`. ## Attributes @@ -642,10 +763,10 @@ Attributes apply to items, fields, and functions. They're written as `#[name]`, | Attribute | Applies to | Effect | |------|------|----| -| `#[size(N)]` | Types | Sets the exact size in bytes. The semantic layer verifies that fields sum to this size. | -| `#[align(N)]` | Types | Sets alignment in bytes. Mutually exclusive with `#[packed]`. | -| `#[min_size(N)]` | Types, opaque types | Sets a minimum size. Useful for opaque forward declarations that need a footprint: `#[min_size(8)] pub type Marker;` gives the type 8 bytes and alignment 1. | -| `#[packed]` | Types | Removes padding between fields. Mutually exclusive with `#[align]`. | +| `#[size(N)]` | Types, unions | Sets the exact size in bytes. The semantic layer verifies that fields sum to this size. | +| `#[align(N)]` | Types, unions | Sets alignment in bytes. Mutually exclusive with `#[packed]`. | +| `#[min_size(N)]` | Types, opaque types, unions | Sets a minimum size. Useful for opaque forward declarations that need a footprint: `#[min_size(8)] pub type Marker;` gives the type 8 bytes and alignment 1. | +| `#[packed]` | Types, unions | Removes padding between fields. Mutually exclusive with `#[align]`. | `#[min_size(N)]` exists because opaque types (`pub type Foo;`) have size 0 by default, and embedding a zero-size type by value is rejected. `#[min_size]` gives the opaque type a real footprint so it can appear as a field without a known layout. If the computed size is smaller than `min_size`, it's rounded up. @@ -691,11 +812,11 @@ The default calling convention is `thiscall` on 32-bit (when the function has `& | Attribute | Applies to | Effect | |------|------|----| -| `#[copyable]` | Types, enums, bitflags | Rust backend: derives `Copy` + `Clone`. Suppressed by `#[pinned]`. | -| `#[cloneable]` | Types, enums, bitflags | Rust backend: derives `Clone` only. Suppressed by `#[pinned]`. | -| `#[defaultable]` | Types, enums, bitflags | Rust backend: derives `Default`. On enums/bitflags, requires a `#[default]` variant. | +| `#[copyable]` | Types, enums, bitflags, unions | Rust backend: derives `Copy` + `Clone`. Suppressed by `#[pinned]`. | +| `#[cloneable]` | Types, enums, bitflags, unions | Rust backend: derives `Clone` only. Suppressed by `#[pinned]`. | +| `#[defaultable]` | Types, enums, bitflags, unions | Rust backend: derives `Default`. On enums/bitflags, requires a `#[default]` variant. On unions, a `Default` impl is written out rather than derived, since a union cannot derive one. | | `#[default]` | Enum/bitflag variants | Marks the default variant for `Default` derivation. | -| `#[pinned]` | Types, enums, bitflags | Rust backend: adds a `PhantomPinned` field and suppresses `Copy`/`Clone`. The type must not be relocated in memory. | +| `#[pinned]` | Types, enums, bitflags, unions | Rust backend: adds a `PhantomPinned` field and suppresses `Copy`/`Clone`. The type must not be relocated in memory. On a union it only suppresses the derives — an extra member would be another reading of the same bytes, not an extra field. | `#[pinned]` exists because some types have addresses that must not change - the target binary passes pointers to `this` or its fields around, and moving the object would invalidate those pointers. `PhantomPinned` makes the type `!Unpin`, forcing consumers to use `Pin<&mut T>` or `Box::pin`. diff --git a/docs/rust_backend.md b/docs/rust_backend.md index 1ba948ab..8b18c2e9 100644 --- a/docs/rust_backend.md +++ b/docs/rust_backend.md @@ -116,6 +116,35 @@ pub struct BuildOptions { For enums and bitflags, the same attributes apply. `#[defaultable]` on an enum or bitflags requires a `#[default]` variant - the `Default` impl returns that variant. +## Unions + +A pyxis `union` lowers to a real Rust `union`, with every member wrapped in `ManuallyDrop`: + +```rust +#[derive(Copy, Clone)] +#[repr(C, align(8))] +pub union Payload { + pub as_int: ::core::mem::ManuallyDrop, + pub as_float: ::core::mem::ManuallyDrop, + pub as_ptr: ::core::mem::ManuallyDrop<*mut i32>, +} +``` + +`ManuallyDrop` is applied unconditionally. Rust requires it for any member whose type isn't `Copy`, and applying it only where needed would make a member's spelling depend on whether its type happened to be `#[copyable]`. It is `repr(transparent)` and derefs to its contents, so it costs nothing at runtime and little at the call site: `unsafe { *value.payload.as_int }`. + +Two impls are written out rather than derived, because a union can derive neither - the compiler has no way to know which member is live: + +| Impl | Behaviour | +|------|-------| +| `Debug` | Prints `Name { .. }`. There is nothing safe to print, since which member applies is a property of the surrounding data. | +| `Default` | Emitted only for `#[defaultable]` unions; returns `unsafe { core::mem::zeroed() }`. The `#[defaultable]` check already rejects members that aren't plain data, so an all-zero reading is valid. | + +Emitting these anyway is what lets a *containing* struct keep its own `#[derive(Debug, Default)]` working. + +`Copy` and `Clone` are derived normally from `#[copyable]`/`#[cloneable]` - `ManuallyDrop: Copy` exactly when `T: Copy`, so the existing trait-constraint checks carry over unchanged. `#[pinned]` suppresses them as it does for structs, but adds no `PhantomPinned` member: an extra member would be another reading of the same bytes, not an extra field. + +Inline unions (`pub payload: union { ... }`) are emitted as ordinary module-scope items named `{Type}{Field}Union`, flattened through the same `flatten_type_name` path as any other item. + ## `#[external_body]` handling When a function has `#[external_body]`, the Rust backend skips code emission entirely. The function signature is not emitted as a Rust method - the user's epilogue `impl` block is the sole source. diff --git a/src/backends/cpp/assemble.rs b/src/backends/cpp/assemble.rs index cf22e571..2dc0821c 100644 --- a/src/backends/cpp/assemble.rs +++ b/src/backends/cpp/assemble.rs @@ -285,6 +285,7 @@ fn intra_module_forward_decls( format!("template <{params}> struct {leaf};") } ItemDefinitionInner::Type(_) => format!("struct {leaf};"), + ItemDefinitionInner::Union(_) => format!("union {leaf};"), ItemDefinitionInner::Enum(ed) => { let underlying = render::render_type(&ed.type_, ctx) .unwrap_or_else(|_| "::std::int32_t".to_string()); diff --git a/src/backends/cpp/deps/mod.rs b/src/backends/cpp/deps/mod.rs index ea53fa0d..196158f3 100644 --- a/src/backends/cpp/deps/mod.rs +++ b/src/backends/cpp/deps/mod.rs @@ -50,6 +50,21 @@ pub(super) fn collect_intra_module_full_deps( ); } } + // A union's members are laid out inside it, so they are full-definition + // dependencies exactly as a struct's fields are. + ItemDefinitionInner::Union(ud) => { + for region in &ud.regions { + walk_intra( + ®ion.type_ref, + EdgeKind::FullDef, + module_path, + item_paths, + registry, + bindings, + out, + ); + } + } ItemDefinitionInner::Enum(ed) => { walk_intra( &ed.type_, @@ -234,6 +249,11 @@ pub fn collect_module_deps( ItemDefinitionInner::Type(td) => { walk_type_def(td, &mut deps, module_path, registry, bindings) } + ItemDefinitionInner::Union(ud) => { + for region in &ud.regions { + walk_region(region, &mut deps, module_path, registry, bindings); + } + } ItemDefinitionInner::Enum(ed) => { walk_enum_def(ed, &mut deps, module_path, registry, bindings) } diff --git a/src/backends/cpp/render/mod.rs b/src/backends/cpp/render/mod.rs index 00493830..ee2f15c0 100644 --- a/src/backends/cpp/render/mod.rs +++ b/src/backends/cpp/render/mod.rs @@ -20,6 +20,7 @@ mod items; mod nested; mod structs; mod types; +mod unions; pub use idents::{cpp_ident, cpp_namespace_ident}; pub use items::{render_free_function_decl, render_free_function_definition}; @@ -137,6 +138,14 @@ pub fn render_item(item: &ItemDefinition, ctx: RenderCtx) -> Result unions::render_union( + &name, + ud, + resolved.size, + resolved.alignment, + ctx, + &item.location, + )?, ItemDefinitionInner::Enum(ed) => { let (mut decl, mut post_cpp) = items::render_enum(&name, ed, resolved.size, ctx, &item.location)?; diff --git a/src/backends/cpp/render/nested.rs b/src/backends/cpp/render/nested.rs index 2ba6e9a1..7e7e374e 100644 --- a/src/backends/cpp/render/nested.rs +++ b/src/backends/cpp/render/nested.rs @@ -7,7 +7,8 @@ use std::fmt::Write; use super::{RenderCtx, items::format_const_value}; use crate::{ backends::Result, - semantic::types::{ConstValue, ItemDefinitionInner, TypeDefinition}, + grammar::ItemPath, + semantic::types::{ConstValue, ItemDefinitionInner}, span::ItemLocation, }; @@ -17,15 +18,15 @@ use crate::{ /// `deferred_consts` for the caller to emit after the class body. pub(super) fn render_declarations( body: &mut String, - td: &TypeDefinition, + nested_item_paths: &[ItemPath], ctx: RenderCtx, deferred_consts: &mut Vec<(String, String, String)>, ) -> Result<()> { - if td.nested_item_paths.is_empty() { + if nested_item_paths.is_empty() { return Ok(()); } let mut prev_was_constant = false; - for nested_path in &td.nested_item_paths { + for nested_path in nested_item_paths { if let Ok(nested_item) = ctx.registry.get(nested_path, &ItemLocation::internal()) { if let Some(nested_resolved) = nested_item.resolved() { let curr_is_constant = @@ -101,6 +102,20 @@ pub(super) fn render_declarations( } writeln!(body, " }};")?; } + ItemDefinitionInner::Union(nested_ud) => { + super::types::render_doc( + body, + &nested_ud.doc, + 1, + ctx, + &nested_item.location, + )?; + writeln!(body, " union {nested_name} {{")?; + for region in &nested_ud.regions { + super::items::render_field_indented(body, region, ctx, false, 2)?; + } + writeln!(body, " }};")?; + } ItemDefinitionInner::Enum(nested_ed) => { super::types::render_doc( body, @@ -221,11 +236,11 @@ pub(super) fn render_declarations( /// picks `out` accordingly. pub(super) fn render_extern_value_definitions( out: &mut String, - td: &TypeDefinition, + nested_item_paths: &[ItemPath], parent_name: &str, ctx: RenderCtx, ) -> Result<()> { - for nested_path in &td.nested_item_paths { + for nested_path in nested_item_paths { let Ok(nested_item) = ctx.registry.get(nested_path, &ItemLocation::internal()) else { continue; }; diff --git a/src/backends/cpp/render/structs.rs b/src/backends/cpp/render/structs.rs index 315c10f1..b5e7c786 100644 --- a/src/backends/cpp/render/structs.rs +++ b/src/backends/cpp/render/structs.rs @@ -77,7 +77,12 @@ pub(super) fn render_struct( render_vftable_declarations(&mut body, td, ctx)?; render_associated_function_declarations(&mut body, td, ctx)?; render_deleted_special_members(&mut body, name, td)?; - super::nested::render_declarations(&mut body, td, ctx, &mut deferred_consts)?; + super::nested::render_declarations( + &mut body, + &td.nested_item_paths, + ctx, + &mut deferred_consts, + )?; if body.trim().is_empty() { writeln!(out, "{header} {{}};")?; @@ -134,7 +139,7 @@ pub(super) fn render_struct( } else { &mut post_cpp }; - super::nested::render_extern_value_definitions(def_out, td, name, ctx)?; + super::nested::render_extern_value_definitions(def_out, &td.nested_item_paths, name, ctx)?; Ok(RenderedItem { decl: out, @@ -241,7 +246,15 @@ fn render_deleted_special_members( name: &str, td: &TypeDefinition, ) -> Result<()> { - if td.pinned { + render_deleted_special_members_if(body, name, td.pinned) +} + +pub(super) fn render_deleted_special_members_if( + body: &mut String, + name: &str, + pinned: bool, +) -> Result<()> { + if pinned { writeln!(body)?; writeln!(body, " {name}(const {name}&) = delete;")?; writeln!(body, " {name}({name}&&) = delete;")?; diff --git a/src/backends/cpp/render/unions.rs b/src/backends/cpp/render/unions.rs new file mode 100644 index 00000000..3f13e685 --- /dev/null +++ b/src/backends/cpp/render/unions.rs @@ -0,0 +1,81 @@ +//! Union rendering. A pyxis union maps straight onto a C++ `union` — the one +//! place where the C++ backend needs no encoding tricks, since C++ has the +//! construct natively. + +use std::fmt::Write; + +use super::{RenderCtx, RenderedItem}; +use crate::{backends::Result, semantic::types::UnionDefinition, span::ItemLocation}; + +pub(super) fn render_union( + name: &str, + ud: &UnionDefinition, + size: usize, + alignment: usize, + ctx: RenderCtx, + location: &ItemLocation, +) -> Result { + let name = &*super::cpp_ident(name); + + let mut out = String::new(); + super::types::render_doc(&mut out, &ud.doc, 0, ctx, location)?; + if ud.packed { + writeln!(out, "#pragma pack(push, 1)")?; + } + + let mut body = String::new(); + for region in &ud.regions { + super::items::render_field(&mut body, region, ctx, false)?; + } + + // Nested items declared inside the union body are rendered in-class, the + // same way a struct's are. Unions have no place for out-of-class constant + // definitions to attach to any differently, so `deferred_consts` is handled + // identically. + let mut deferred_consts: Vec<(String, String, String)> = Vec::new(); + super::structs::render_deleted_special_members_if(&mut body, name, ud.pinned)?; + super::nested::render_declarations( + &mut body, + &ud.nested_item_paths, + ctx, + &mut deferred_consts, + )?; + + if body.trim().is_empty() { + writeln!(out, "union alignas({alignment}) {name} {{}};")?; + } else { + writeln!(out, "union alignas({alignment}) {name} {{")?; + out.push_str(&body); + writeln!(out, "}};")?; + } + if ud.packed { + writeln!(out, "#pragma pack(pop)")?; + } + + if size > 0 { + writeln!(out, "static_assert(sizeof({name}) == 0x{size:X});")?; + } + writeln!(out, "static_assert(alignof({name}) == {alignment});")?; + + let mut post_header = String::new(); + for (const_name, const_type, const_value) in &deferred_consts { + writeln!( + post_header, + "inline const {const_type} {name}::{const_name} = {const_value};" + )?; + } + + let mut post_cpp = String::new(); + super::nested::render_extern_value_definitions( + &mut post_cpp, + &ud.nested_item_paths, + name, + ctx, + )?; + + Ok(RenderedItem { + decl: out, + post_header, + post_cpp, + }) +} diff --git a/src/backends/cpp/write.rs b/src/backends/cpp/write.rs index 3c5af5d2..66a15ddd 100644 --- a/src/backends/cpp/write.rs +++ b/src/backends/cpp/write.rs @@ -285,6 +285,7 @@ pub(super) fn forward_decl_line( { match &resolved.inner { ItemDefinitionInner::Type(_) => return format!("struct {leaf};"), + ItemDefinitionInner::Union(_) => return format!("union {leaf};"), ItemDefinitionInner::Enum(ed) => { let underlying = render::render_type(&ed.type_, ctx) .unwrap_or_else(|_| "::std::int32_t".to_string()); diff --git a/src/backends/json/convert.rs b/src/backends/json/convert.rs index 6dd0ab0f..268b31ea 100644 --- a/src/backends/json/convert.rs +++ b/src/backends/json/convert.rs @@ -10,7 +10,7 @@ use crate::{ ConstValue, EnumDefinition, EnumVariant, ExternValueDefinition as SemanticExternValueDefinition, Function, FunctionBody, ItemCategory, ItemDefinition, ItemDefinitionInner, Region, Type, TypeAliasDefinition, - TypeDefinition, TypeVftable, + TypeDefinition, TypeVftable, UnionDefinition, }, }, source_store::FileStore, @@ -407,6 +407,38 @@ fn convert_type_definition( } } +/// Convert a union. Unlike a type, there is no running offset to accumulate: +/// every member starts at offset 0, which is the whole point of a union. +fn convert_union_definition( + ud: &UnionDefinition, + type_registry: &TypeRegistry, + size: usize, + alignment: usize, + cx: &DocCx, + item_location: &crate::span::ItemLocation, +) -> JsonUnionDefinition { + let fields = ud + .regions + .iter() + .map(|region| convert_region(region, type_registry, 0, cx)) + .collect(); + + let (doc, doc_links) = cx.convert(&ud.doc, item_location); + JsonUnionDefinition { + doc, + doc_links, + fields, + size, + alignment, + copyable: ud.copyable, + cloneable: ud.cloneable, + defaultable: ud.defaultable, + packed: ud.packed, + pinned: ud.pinned, + nested_items: ud.nested_item_paths.iter().map(|p| p.to_string()).collect(), + } +} + fn convert_enum_variant(variant: &EnumVariant, cx: &DocCx) -> JsonEnumVariant { let (doc, doc_links) = cx.convert(&variant.doc, &variant.location); JsonEnumVariant { @@ -574,6 +606,14 @@ fn convert_item( cx, &item.location, )), + ItemDefinitionInner::Union(ud) => JsonItemKind::Union(convert_union_definition( + ud, + type_registry, + resolved.size, + resolved.alignment, + cx, + &item.location, + )), ItemDefinitionInner::Enum(ed) => JsonItemKind::Enum(convert_enum_definition( ed, type_registry, diff --git a/src/backends/json/mod.rs b/src/backends/json/mod.rs index c744f8e5..06d3c52e 100644 --- a/src/backends/json/mod.rs +++ b/src/backends/json/mod.rs @@ -10,5 +10,5 @@ pub use schema::{ JsonExternValueDefinition, JsonFunction, JsonFunctionArgument, JsonFunctionBody, JsonItem, JsonItemCategory, JsonItemKind, JsonModule, JsonReexport, JsonRegion, JsonSourceLocation, JsonSplice, JsonSpliceKind, JsonType, JsonTypeAliasDefinition, JsonTypeDefinition, - JsonTypeVftable, JsonVisibility, export_types, + JsonTypeVftable, JsonUnionDefinition, JsonVisibility, export_types, }; diff --git a/src/backends/json/schema.rs b/src/backends/json/schema.rs index 2d4e2d24..8558148d 100644 --- a/src/backends/json/schema.rs +++ b/src/backends/json/schema.rs @@ -43,7 +43,10 @@ use crate::semantic::types::{CallingConvention, ItemCategory, Visibility}; /// - v11: added `c_string`, `struct`, `array`, and `const_ref` variants to /// `JsonConstValue` for C-string literals, structured initializers, and /// constant aliases. -pub const CURRENT_SCHEMA_VERSION: u32 = 11; +/// - v12: added a `Union` item kind (`JsonUnionDefinition`). Its `fields` are +/// `JsonRegion`s like a type's, but every one has `offset: 0` — a union's +/// members are competing readings of the same bytes, not a sequence. +pub const CURRENT_SCHEMA_VERSION: u32 = 12; /// Top-level JSON documentation structure #[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] @@ -225,6 +228,7 @@ pub enum JsonItemKind { Type(JsonTypeDefinition), Enum(JsonEnumDefinition), Bitflags(JsonBitflagsDefinition), + Union(JsonUnionDefinition), TypeAlias(JsonTypeAliasDefinition), Constant(JsonConstantDefinition), ExternValue(JsonExternValueDefinition), @@ -301,6 +305,36 @@ pub struct JsonTypeDefinition { pub nested_items: Vec, } +/// A union: several readings of the same bytes, only one of which applies at a +/// time. Which one is a property of the surrounding data, not of the union. +#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] +pub struct JsonUnionDefinition { + /// Documentation + pub doc: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub doc_links: Vec, + /// Members. Every one has `offset: 0`; `size` is the member's own size, + /// which may be smaller than the union's. + pub fields: Vec, + /// Total size in bytes: the largest member, rounded up to the alignment + pub size: usize, + /// Alignment in bytes: the strictest of the members' + pub alignment: usize, + /// Whether the union is copyable + pub copyable: bool, + /// Whether the union is cloneable + pub cloneable: bool, + /// Whether the union is defaultable + pub defaultable: bool, + /// Whether the union is packed + pub packed: bool, + /// Whether the union is pinned (non-relocatable) + pub pinned: bool, + /// Item paths of nested items declared inside this union body + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub nested_items: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] pub struct JsonRegion { /// Visibility diff --git a/src/backends/rust/items.rs b/src/backends/rust/items.rs index 3edcc9e8..8d351754 100644 --- a/src/backends/rust/items.rs +++ b/src/backends/rust/items.rs @@ -7,7 +7,8 @@ use crate::{ TypeRegistry, types::{ BitflagsDefinition, EnumDefinition, ItemCategory, ItemDefinition, ItemDefinitionInner, - ItemStateResolved, Region, TypeAliasDefinition, TypeDefinition, Visibility, + ItemStateResolved, Region, TypeAliasDefinition, TypeDefinition, UnionDefinition, + Visibility, }, }, span::ItemLocation, @@ -71,6 +72,17 @@ pub(super) fn build_item( module_paths, doc_cx, ), + IDI::Union(ud) => build_union( + path, + *size, + *alignment, + visibility, + ud, + location, + options, + module_paths, + doc_cx, + ), IDI::Enum(ed) => build_enum( type_registry, path, @@ -435,6 +447,136 @@ fn build_type( }) } +/// Emit a `union` — a real Rust union, not an opaque byte array, so the +/// alternatives are visible in the type. +/// +/// Every member is wrapped in `ManuallyDrop` unconditionally. Rust requires +/// it for any member that isn't `Copy`, and applying it only where needed would +/// make a member's spelling depend on whether its type happened to be +/// `#[copyable]`. `ManuallyDrop` is `repr(transparent)` and derefs to its +/// contents, so it costs nothing at runtime and little at the call site. +/// +/// `Debug` and `Default` are written out rather than derived, because a union +/// cannot derive either — the compiler has no way to know which member is live. +/// Emitting them anyway is what lets a *containing* struct keep its own +/// `#[derive(Debug, Default)]` working. +#[allow(clippy::too_many_arguments)] +fn build_union( + path: &ItemPath, + size: usize, + alignment: usize, + visibility: Visibility, + union_definition: &UnionDefinition, + location: &ItemLocation, + options: &crate::BuildOptions, + module_paths: &BTreeSet, + doc_cx: &DocLinkCx, +) -> Result { + let name = flatten_type_name(path, module_paths); + let name = &name; + let prefix = options.rust_module_prefix.as_ref(); + + let UnionDefinition { + regions, + doc, + copyable, + cloneable, + defaultable, + packed, + pinned, + nested_item_paths: _, + } = union_definition; + + let name_ident = str_to_ident(name); + let visibility_tokens = visibility_to_tokens(visibility); + let doc = doc_cx.node(doc, location); + + let members = regions + .iter() + .map(|r| { + let Region { + visibility, + name: member, + doc, + type_ref, + is_base: _, + location, + } = r; + let member_name = + member + .as_deref() + .ok_or_else(|| BackendError::FieldCodeGenFailed { + type_path: path.clone(), + field_name: "unnamed".to_string(), + kind: crate::backends::error::FieldCodeGenFailedKind::FieldNameNotPresent, + location: *location, + })?; + let member_ident = str_to_ident(member_name); + let visibility = visibility_to_tokens(*visibility); + let syn_type = sa_type_to_syn_type(type_ref, prefix, Some(module_paths))?; + let doc = doc_cx.node(doc, location); + Ok(quote! { + #doc + #visibility #member_ident: ::core::mem::ManuallyDrop<#syn_type> + }) + }) + .collect::>>()?; + + // A pinned union must not be Copy/Clone — that would allow moving out from + // behind a Pin. Note a union cannot carry a PhantomPinned marker the way a + // struct does: an extra member would be another reading of the same bytes, + // not an extra field. + let extra_derives = build_extra_derives(*copyable && !*pinned, *cloneable && !*pinned, false); + let derives = if extra_derives.is_empty() { + quote! {} + } else { + quote! { #[derive(#(#extra_derives),*)] } + }; + + let (packed_repr, alignment_repr) = if *packed { + (quote! { , packed }, quote! {}) + } else { + let alignment: syn::Index = alignment.into(); + (quote! {}, quote! { , align(#alignment) }) + }; + + let size_check_impl = generate_size_check(name, size); + + let debug_impl = quote! { + impl ::core::fmt::Debug for #name_ident { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + // Which member is live is a property of the surrounding data, + // not of the union, so there is nothing safe to print. + f.write_str(concat!(#name, " { .. }")) + } + } + }; + + let default_impl = defaultable.then(|| { + quote! { + impl ::core::default::Default for #name_ident { + fn default() -> Self { + // Every member is plain data (the `#[defaultable]` check + // rejects anything else), so an all-zero reading is valid. + unsafe { ::core::mem::zeroed() } + } + } + } + }); + + Ok(quote! { + #derives + #[repr(C #packed_repr #alignment_repr)] + #doc + #visibility_tokens union #name_ident { + #(#members),* + } + #size_check_impl + #debug_impl + #default_impl + }) +} + #[allow(clippy::too_many_arguments)] fn build_enum( type_registry: &TypeRegistry, diff --git a/src/grammar.rs b/src/grammar.rs index b1f33b55..e137f5fd 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -8,7 +8,7 @@ pub use crate::parser::{ BitflagsDefItem, BitflagsDefinition, BitflagsStatement, Comment, ConstDefinition, EnumDefItem, EnumDefinition, EnumStatement, ExternValueDefinition, ItemDefinition, ItemDefinitionInner, TypeAliasDefinition, TypeDefItem, TypeDefinition, TypeField, - TypeStatement, + TypeStatement, UnionDefinition, }, module::{Module, ModuleItem}, paths::{ItemPath, ItemPathSegment}, @@ -23,6 +23,7 @@ pub mod test_aliases { pub type ID = super::ItemDefinition; pub type TS = super::TypeStatement; pub type TD = super::TypeDefinition; + pub type UD = super::UnionDefinition; pub type ES = super::EnumStatement; pub type ED = super::EnumDefinition; pub type BFS = super::BitflagsStatement; diff --git a/src/parser/items/mod.rs b/src/parser/items/mod.rs index 9e239097..45f3ba4c 100644 --- a/src/parser/items/mod.rs +++ b/src/parser/items/mod.rs @@ -17,11 +17,13 @@ mod bitflags; mod enums; mod misc; mod types; +mod unions; pub use bitflags::{BitflagsDefItem, BitflagsDefinition, BitflagsStatement}; pub use enums::{EnumDefItem, EnumDefinition, EnumStatement}; pub use misc::{ConstDefinition, ExternValueDefinition, TypeAliasDefinition}; pub use types::{TypeDefItem, TypeDefinition, TypeField, TypeStatement}; +pub use unions::UnionDefinition; /// Comment node types #[derive(Debug, Clone, PartialEq, Eq, Hash, HasLocation)] @@ -56,6 +58,7 @@ pub enum ItemDefinitionInner { Type(TypeDefinition), Enum(EnumDefinition), Bitflags(BitflagsDefinition), + Union(UnionDefinition), TypeAlias(TypeAliasDefinition), Constant(ConstDefinition), ExternValue(ExternValueDefinition), @@ -65,6 +68,11 @@ impl From for ItemDefinitionInner { ItemDefinitionInner::Type(item) } } +impl From for ItemDefinitionInner { + fn from(item: UnionDefinition) -> Self { + ItemDefinitionInner::Union(item) + } +} impl From for ItemDefinitionInner { fn from(item: EnumDefinition) -> Self { ItemDefinitionInner::Enum(item) @@ -189,9 +197,9 @@ impl ItemDefinition { pub(crate) fn terminator(&self) -> ItemTerminator { match &self.inner { ItemDefinitionInner::Type(td) if !td.is_opaque => ItemTerminator::SelfTerminating, - ItemDefinitionInner::Enum(_) | ItemDefinitionInner::Bitflags(_) => { - ItemTerminator::SelfTerminating - } + ItemDefinitionInner::Enum(_) + | ItemDefinitionInner::Bitflags(_) + | ItemDefinitionInner::Union(_) => ItemTerminator::SelfTerminating, _ => ItemTerminator::Separated, } } @@ -396,6 +404,36 @@ impl Parser { declaration_location, }) } + TokenKind::Union => { + self.advance(); + let (name, _) = self.expect_ident()?; + // Unions don't support type parameters; `union Name` falls + // through to the `{` expectation below and errors there. + let items = self.parse_union_body()?; + + // Capture the end position + let end_pos = if self.pos > 0 { + self.tokens[self.pos - 1].location.span.end + } else { + self.current().location.span.end + }; + + let location = self.item_location_from_locations(start_pos, end_pos); + Ok(ItemDefinition { + visibility, + name, + type_parameters: vec![], // Unions don't support type parameters + doc_comments, + inner: ItemDefinitionInner::Union(UnionDefinition { + items, + attributes, + inline_trailing_comments, + following_comments, + }), + location, + declaration_location, + }) + } TokenKind::Const => { self.advance(); // consume `const` let (name, _) = self.expect_ident()?; @@ -496,15 +534,25 @@ impl Parser { /// Whether the tokens at `pos` (already advanced past any leading doc /// comments, attributes, and comments) begin a nested item declaration: - /// `type`/`enum`/`bitflags`/`const`, or an `extern : T` value — each - /// optionally `pub`. Note `extern type ...` is deliberately excluded: extern - /// types are module-level only, so `extern` counts as a nested item only - /// when it is *not* immediately followed by `type`. + /// `type`/`enum`/`bitflags`/`union`/`const`, or an `extern : T` value — + /// each optionally `pub`. Note `extern type ...` is deliberately excluded: + /// extern types are module-level only, so `extern` counts as a nested item + /// only when it is *not* immediately followed by `type`. + /// + /// A `union` counts only when it is a *declaration* (`union Name { … }`); + /// the inline anonymous form appears in field position (`pub name: union + /// { … }`) and is handled by `parse_type_statement`. pub(super) fn peek_is_nested_item(&self, pos: usize) -> bool { fn is_item_kw(kind: Option<&TokenKind>) -> bool { matches!( kind, - Some(TokenKind::Type | TokenKind::Enum | TokenKind::Bitflags | TokenKind::Const) + Some( + TokenKind::Type + | TokenKind::Enum + | TokenKind::Bitflags + | TokenKind::Union + | TokenKind::Const + ) ) } let is_extern_value = |pos: usize| { diff --git a/src/parser/items/types.rs b/src/parser/items/types.rs index 96a4d4af..5289d870 100644 --- a/src/parser/items/types.rs +++ b/src/parser/items/types.rs @@ -6,7 +6,7 @@ use crate::{ #[cfg(test)] use crate::span::StripLocations; -use super::{Comment, ItemDefinition}; +use super::{Comment, ItemDefinition, UnionDefinition}; use crate::parser::{ ParseError, attributes::{Attributes, Visibility}, @@ -26,6 +26,18 @@ pub enum TypeField { Vftable(Vec), /// A nested item declaration (enum, type, bitflags, type alias) inside a `type` body. Item(Box), + /// A field whose type is an inline anonymous union: `pub payload: union { … }`. + /// + /// This is a dedicated variant rather than a [`Type`] variant so anonymous + /// unions stay confined to field position — they cannot appear behind a + /// pointer, inside an array, or in a function signature. The field name + /// supplies the union's name; the semantic layer desugars it to a generated + /// union item plus an ordinary field referring to it. + UnionField { + visibility: Visibility, + name: Ident, + body: UnionDefinition, + }, } #[cfg(test)] impl TypeField { @@ -44,6 +56,18 @@ impl TypeField { pub fn item(item: ItemDefinition) -> TypeField { TypeField::Item(Box::new(item)) } + + pub fn union_field( + visibility: Visibility, + name: impl Into, + body: UnionDefinition, + ) -> TypeField { + TypeField::UnionField { + visibility, + name: name.into(), + body, + } + } } impl TypeField { pub fn is_vftable(&self) -> bool { @@ -93,6 +117,19 @@ impl TypeStatement { location: ItemLocation::test(), } } + pub fn union_field( + (visibility, name): (Visibility, &str), + body: UnionDefinition, + ) -> TypeStatement { + TypeStatement { + field: TypeField::union_field(visibility, name, body), + attributes: Default::default(), + doc_comments: vec![], + inline_trailing_comments: Vec::new(), + following_comments: Vec::new(), + location: ItemLocation::test(), + } + } pub fn item(item: ItemDefinition) -> TypeStatement { TypeStatement { field: TypeField::item(item), @@ -320,7 +357,26 @@ impl Parser { let visibility = self.parse_visibility()?; let (name, _) = self.expect_ident()?; self.expect(TokenKind::Colon)?; - let type_ = self.parse_type()?; + + // `pub name: union { … }` — an inline anonymous union rather than a + // type reference. Checked before `parse_type` because `union` is a + // keyword and would not lex as a type identifier anyway. + let field = if matches!(self.peek(), TokenKind::Union) { + self.advance(); + let items = self.parse_union_body()?; + TypeField::UnionField { + visibility, + name, + body: UnionDefinition { + items, + attributes: Attributes::default(), + inline_trailing_comments: Vec::new(), + following_comments: Vec::new(), + }, + } + } else { + TypeField::Field(visibility, name, self.parse_type()?) + }; let end_pos = if self.pos > 0 { self.tokens[self.pos - 1].location.span.end @@ -330,7 +386,7 @@ impl Parser { let location = self.item_location_from_locations(start_pos, end_pos); Ok(TypeStatement { - field: TypeField::Field(visibility, name, type_), + field, attributes, doc_comments, inline_trailing_comments: Vec::new(), // Will be populated by parse_type_def_items diff --git a/src/parser/items/types/tests.rs b/src/parser/items/types/tests.rs index 3acabaaf..9cb5cac2 100644 --- a/src/parser/items/types/tests.rs +++ b/src/parser/items/types/tests.rs @@ -531,3 +531,81 @@ fn module_level_opaque_type_requires_semicolon() { assert!(parse_str_for_tests("pub type Opaque,").is_err()); assert!(parse_str_for_tests("pub type Opaque;").is_ok()); } + +#[test] +fn can_parse_standalone_union() { + let text = r#" + #[copyable] + pub union Payload { + pub as_int: i32, + pub as_float: f32, + } + "#; + + let ast = M::new().with_definitions([ID::new( + (V::Public, "Payload"), + UD::new([ + TS::field((V::Public, "as_int"), T::ident("i32")), + TS::field((V::Public, "as_float"), T::ident("f32")), + ]) + .with_attributes([A::copyable()]), + )]); + + assert_eq!(parse_str_for_tests(text).unwrap().strip_locations(), ast); +} + +#[test] +fn can_parse_inline_union_field() { + let text = r#" + pub type Scratch { + pub tag: u16, + pub data: union { + pub as_u64: u64, + }, + } + "#; + + let ast = M::new().with_definitions([ID::new( + (V::Public, "Scratch"), + TD::new([ + TS::field((V::Public, "tag"), T::ident("u16")), + TS::union_field( + (V::Public, "data"), + UD::new([TS::field((V::Public, "as_u64"), T::ident("u64"))]), + ), + ]), + )]); + + assert_eq!(parse_str_for_tests(text).unwrap().strip_locations(), ast); +} + +#[test] +fn can_parse_nested_union_declaration_and_inline_union_in_union_body() { + let text = r#" + pub union Outer { + pub union Inner { + pub x: u32, + }, + + pub raw: u32, + pub extra: union { + pub lo: u16, + }, + } + "#; + assert!(parse_str_for_tests(text).is_ok()); +} + +#[test] +fn union_does_not_accept_type_parameters() { + // Unions aren't generic; `union Name` fails at the `{` expectation. + assert!(parse_str_for_tests("pub union Bad { pub a: u32, }").is_err()); +} + +#[test] +fn anonymous_union_is_confined_to_field_position() { + // The inline form is not part of the type grammar, so it can't appear + // behind a pointer or inside an array. + assert!(parse_str_for_tests("pub type Bad { pub a: *mut union { pub x: u32, }, }").is_err()); + assert!(parse_str_for_tests("pub type Bad { pub a: [union { pub x: u32, }; 2], }").is_err()); +} diff --git a/src/parser/items/unions.rs b/src/parser/items/unions.rs new file mode 100644 index 00000000..9b03d56a --- /dev/null +++ b/src/parser/items/unions.rs @@ -0,0 +1,90 @@ +use crate::tokenizer::TokenKind; + +#[cfg(test)] +use crate::span::StripLocations; + +use super::{Comment, TypeDefItem, TypeStatement}; +use crate::parser::{ParseError, attributes::Attributes, core::Parser}; + +#[cfg(test)] +use crate::parser::attributes::Attribute; + +/// A `union` body: a set of competing readings of the same bytes, all starting +/// at the same offset. +/// +/// The body reuses [`TypeDefItem`]/[`TypeStatement`] rather than defining a +/// parallel statement type, so comment attribution, attributes, and nested item +/// declarations work exactly as they do in a `type` body. Constructs that make +/// no sense in a union — `vftable` blocks, `#[base]` fields — parse fine here +/// and are rejected in the semantic layer, where the error can carry a span +/// pointing at the offending statement. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct UnionDefinition { + pub items: Vec, + pub attributes: Attributes, + pub inline_trailing_comments: Vec, // Comments on same line as attributes + pub following_comments: Vec, // Comments on lines after attributes +} + +#[cfg(test)] +impl StripLocations for UnionDefinition { + fn strip_locations(&self) -> Self { + UnionDefinition { + items: self + .items + .iter() + .filter_map(|item| match item { + TypeDefItem::Comment(_) => None, // Filter out comments + TypeDefItem::Statement(s) => Some(TypeDefItem::Statement(s.strip_locations())), + }) + .collect(), + attributes: self.attributes.strip_locations(), + inline_trailing_comments: Vec::new(), // Strip trailing comments + following_comments: Vec::new(), + } + } +} + +#[cfg(test)] +impl UnionDefinition { + pub fn new(statements: impl IntoIterator) -> Self { + Self { + items: statements.into_iter().map(TypeDefItem::Statement).collect(), + attributes: Default::default(), + inline_trailing_comments: Vec::new(), + following_comments: Vec::new(), + } + } + pub fn with_attributes(mut self, attributes: impl IntoIterator) -> Self { + self.attributes = Attributes::from_iter(attributes); + self + } + pub fn with_inline_trailing_comments(mut self, inline_trailing_comments: Vec) -> Self { + self.inline_trailing_comments = inline_trailing_comments; + self + } + pub fn with_following_comments(mut self, following_comments: Vec) -> Self { + self.following_comments = following_comments; + self + } +} + +impl UnionDefinition { + pub fn statements(&self) -> impl Iterator { + self.items.iter().filter_map(|item| match item { + TypeDefItem::Statement(stmt) => Some(stmt), + _ => None, + }) + } +} + +impl Parser { + /// Parse a `{ … }` union body. The caller has already consumed the `union` + /// keyword (and, for a named union, the name). + pub(crate) fn parse_union_body(&mut self) -> Result, ParseError> { + self.expect(TokenKind::LBrace)?; + let items = self.parse_type_def_items()?; + self.expect(TokenKind::RBrace)?; + Ok(items) + } +} diff --git a/src/pretty_print/definitions.rs b/src/pretty_print/definitions.rs index dae3bf32..9d5f90dc 100644 --- a/src/pretty_print/definitions.rs +++ b/src/pretty_print/definitions.rs @@ -17,6 +17,7 @@ impl PrettyPrinter { ItemDefinitionInner::Type(td) => { self.print_type_definition(def, td, &type_params, nested) } + ItemDefinitionInner::Union(ud) => self.print_union_definition(def, ud), ItemDefinitionInner::Enum(ed) => self.print_enum_definition(def, ed), ItemDefinitionInner::Bitflags(bf) => self.print_bitflags_definition(def, bf), ItemDefinitionInner::TypeAlias(ta) => { @@ -68,6 +69,11 @@ impl PrettyPrinter { &bf.inline_trailing_comments, &bf.following_comments, ), + ItemDefinitionInner::Union(ud) => ( + &ud.attributes, + &ud.inline_trailing_comments, + &ud.following_comments, + ), ItemDefinitionInner::TypeAlias(ta) => (&ta.attributes, &Vec::new(), &Vec::new()), ItemDefinitionInner::Constant(cd) => (&cd.attributes, &Vec::new(), &Vec::new()), ItemDefinitionInner::ExternValue(ev) => (&ev.attributes, &Vec::new(), &Vec::new()), @@ -122,6 +128,21 @@ impl PrettyPrinter { .unwrap(); } else { writeln!(&mut self.output, "type {}{} {{", def.name, type_params).unwrap(); + self.print_type_body_items(&td.items); + } + } + + /// Print a `union` definition body. A union body is the same AST as a type + /// body, so it groups and orders its items identically. + fn print_union_definition(&mut self, def: &ItemDefinition, ud: &UnionDefinition) { + writeln!(&mut self.output, "union {} {{", def.name).unwrap(); + self.print_type_body_items(&ud.items); + } + + /// Print the contents of a braced type/union body, including the closing + /// brace, with items grouped into constants, nested types, and fields. + fn print_type_body_items(&mut self, items: &[TypeDefItem]) { + { self.indent(); // Partition items into groups: (comments, statement) pairs. @@ -129,7 +150,7 @@ impl PrettyPrinter { // Then split into nested-item groups and other groups. let mut groups: Vec<(Vec<&Comment>, &TypeDefItem)> = Vec::new(); let mut pending_comments: Vec<&Comment> = Vec::new(); - for item in &td.items { + for item in items { match item { TypeDefItem::Comment(c) => { pending_comments.push(c); @@ -428,6 +449,35 @@ impl PrettyPrinter { self.writeln(""); } } + TypeField::UnionField { + visibility, + name, + body, + } => { + self.write_indent(); + if *visibility == Visibility::Public { + write!(&mut self.output, "pub ").unwrap(); + } + writeln!(&mut self.output, "{name}: union {{").unwrap(); + self.print_type_body_items(&body.items); + // `print_type_body_items` closes with `}\n`; an inline union is + // a field, so it needs the field's trailing comma. + if self.output.ends_with("}\n") { + self.output.pop(); + write!(&mut self.output, ",").unwrap(); + } + + for comment in &stmt.inline_trailing_comments { + write!(&mut self.output, " ").unwrap(); + self.print_comment_inline(comment); + } + + writeln!(&mut self.output).unwrap(); + + for comment in &stmt.following_comments { + self.print_comment(comment); + } + } TypeField::Item(inner_def) => { // print_item_definition does its own write_indent() self.print_item_definition(inner_def, true); diff --git a/src/pretty_print/tests/formatting.rs b/src/pretty_print/tests/formatting.rs index fc43b7da..36976937 100644 --- a/src/pretty_print/tests/formatting.rs +++ b/src/pretty_print/tests/formatting.rs @@ -559,3 +559,46 @@ pub type MultipleBlocks { assert_eq!(printed, printed2); } + +#[test] +fn unions_round_trip() { + // Both union forms — a standalone item and an inline anonymous union in + // field position — must survive a round-trip unchanged, including the + // trailing comma the inline form needs as a field. + let text = r#"#[copyable] +pub union Payload { + pub as_int: i32, + pub as_float: f32, +} + +pub type Scratch { + pub tag: u64, + pub data: union { + pub as_u64: u64, + pub as_bytes: [u8; 8], + }, +}"#; + + let module = parse_str_for_tests(text).unwrap(); + assert_eq!(pretty_print(&module), text); +} + +#[test] +fn nested_union_declarations_round_trip() { + // A union body reuses the type-body printer, so nested item declarations + // and nested inline unions must print the same way they do in a type. + let text = r#"pub union Outer { + pub type Header { + pub magic: u32, + }, + + pub raw: u32, + pub inner: union { + pub lo: u16, + pub hi: [u8; 2], + }, +}"#; + + let module = parse_str_for_tests(text).unwrap(); + assert_eq!(pretty_print(&module), text); +} diff --git a/src/semantic/declaration_registry.rs b/src/semantic/declaration_registry.rs index b0318748..59268f28 100644 --- a/src/semantic/declaration_registry.rs +++ b/src/semantic/declaration_registry.rs @@ -198,15 +198,22 @@ impl DeclarationRegistry { } } - /// Recursively register nested items inside a type body. + /// Recursively register nested items inside a type or union body. fn register_nested_in_type( &mut self, inner: &grammar::ItemDefinitionInner, parent_path: &ItemPath, module_path: &ItemPath, ) { - if let grammar::ItemDefinitionInner::Type(td) = inner { - for stmt in td.statements() { + // `union` bodies reuse the `type` body AST, so both are walked the same + // way for nested item declarations. + let statements: Vec<&grammar::TypeStatement> = match inner { + grammar::ItemDefinitionInner::Type(td) => td.statements().collect(), + grammar::ItemDefinitionInner::Union(ud) => ud.statements().collect(), + _ => Vec::new(), + }; + { + for stmt in statements { if let grammar::TypeField::Item(nested) = &stmt.field { let nested_path = parent_path.join(nested.name.as_str().into()); self.items.insert(nested_path.clone(), (**nested).clone()); diff --git a/src/semantic/doc_links/resolver.rs b/src/semantic/doc_links/resolver.rs index 16154f9c..85d3b6e5 100644 --- a/src/semantic/doc_links/resolver.rs +++ b/src/semantic/doc_links/resolver.rs @@ -153,6 +153,15 @@ impl DocLinkResolver { constants, extern_values, }, + // A union's members are linkable exactly like a type's fields; + // it just has nothing else to link to. + Some(ItemDefinitionInner::Union(ud)) => ItemMembers::Type { + methods: vec![], + vftable_methods: vec![], + fields: ud.regions.iter().filter_map(|r| r.name.clone()).collect(), + constants, + extern_values, + }, Some(ItemDefinitionInner::Bitflags(bd)) => ItemMembers::Bitflags { flags: bd.flags.iter().map(|f| f.name.clone()).collect(), constants, @@ -661,6 +670,38 @@ where )?; } } + ItemDefinitionInner::Union(ud) => { + // Same shape as a type: the union's own path joins the scope so bare + // references to its nested items resolve. + let union_scope: Vec = std::iter::once(path.clone()) + .chain(scope.iter().cloned()) + .collect(); + record( + module_path, + &ud.doc, + &union_scope, + enclosing, + &item.location, + )?; + for r in &ud.regions { + record(module_path, &r.doc, &union_scope, enclosing, &r.location)?; + } + for nested_path in &ud.nested_item_paths { + let Ok(nested_item) = type_registry.get(nested_path, &ItemLocation::internal()) + else { + continue; + }; + walk_item_docs( + type_registry, + module_path, + nested_path, + nested_item, + &union_scope, + Some(path), + record, + )?; + } + } ItemDefinitionInner::Enum(ed) => { record(module_path, &ed.doc, scope, enclosing, &item.location)?; for v in &ed.variants { diff --git a/src/semantic/error/context.rs b/src/semantic/error/context.rs index 29f32305..8ece0376 100644 --- a/src/semantic/error/context.rs +++ b/src/semantic/error/context.rs @@ -150,6 +150,7 @@ pub enum ItemKind { Type, Enum, Bitflags, + Union, TypeAlias, Constant, ExternValue, @@ -162,6 +163,7 @@ impl ItemKind { ItemDefinitionInner::Type(_) => ItemKind::Type, ItemDefinitionInner::Enum(_) => ItemKind::Enum, ItemDefinitionInner::Bitflags(_) => ItemKind::Bitflags, + ItemDefinitionInner::Union(_) => ItemKind::Union, ItemDefinitionInner::TypeAlias(_) => ItemKind::TypeAlias, ItemDefinitionInner::Constant(_) => ItemKind::Constant, ItemDefinitionInner::ExternValue(_) => ItemKind::ExternValue, @@ -175,6 +177,7 @@ impl fmt::Display for ItemKind { ItemKind::Type => write!(f, "a type"), ItemKind::Enum => write!(f, "an enum"), ItemKind::Bitflags => write!(f, "a bitflags"), + ItemKind::Union => write!(f, "a union"), ItemKind::TypeAlias => write!(f, "a type alias"), ItemKind::Constant => write!(f, "a constant"), ItemKind::ExternValue => write!(f, "an extern value"), diff --git a/src/semantic/error/messages.rs b/src/semantic/error/messages.rs index 6a6733e7..46de05a5 100644 --- a/src/semantic/error/messages.rs +++ b/src/semantic/error/messages.rs @@ -34,6 +34,15 @@ impl SemanticError { | SemanticError::OverlappingRegions { .. } | SemanticError::ZeroSizeFieldEmbedding { .. } => self.layout_error_message(), + SemanticError::UnionBaseNotAllowed { .. } + | SemanticError::UnionVftableNotAllowed { .. } + | SemanticError::UnionMemberAddress { .. } + | SemanticError::EmptyUnion { .. } + | SemanticError::UnionMemberExceedsSize { .. } + | SemanticError::UnionAnonymousMember { .. } + | SemanticError::InlineUnionNestedItem { .. } + | SemanticError::InlineUnionNameCollision { .. } => self.union_error_message(), + SemanticError::VftableMissingFunctions { .. } | SemanticError::VftableFunctionMismatch { .. } | SemanticError::VftableNonAscendingIndex { .. } @@ -321,6 +330,78 @@ impl SemanticError { } } + /// Constructs that a union body cannot express. + fn union_error_message(&self) -> String { + match self { + SemanticError::UnionBaseNotAllowed { item_path, .. } => { + format!( + "union `{item_path}` declares a `#[base]` member; a base class must sit at a \ + known offset, but every union member starts at offset 0" + ) + } + SemanticError::UnionVftableNotAllowed { item_path, .. } => { + format!( + "union `{item_path}` declares a `vftable` block; a vftable pointer must occupy \ + offset 0 exclusively, which a union cannot guarantee" + ) + } + SemanticError::UnionMemberAddress { + member_name, + item_path, + .. + } => { + format!( + "member `{member_name}` of union `{item_path}` has an `#[address]`; every \ + union member starts at offset 0 by definition" + ) + } + SemanticError::EmptyUnion { item_path, .. } => { + format!("union `{item_path}` has no members, so it has no size") + } + SemanticError::UnionMemberExceedsSize { + member_name, + member_size, + declared_size, + item_path, + .. + } => { + format!( + "member `{member_name}` of union `{item_path}` is {member_size} bytes, which \ + exceeds the union's declared size of {declared_size}" + ) + } + SemanticError::UnionAnonymousMember { item_path, .. } => { + format!( + "`{item_path}` has a union member named `_`; `_` marks padding, but every \ + union member already covers the same bytes as its siblings" + ) + } + SemanticError::InlineUnionNestedItem { + item_name, + item_path, + .. + } => { + format!( + "`{item_name}` is declared inside the inline union field `{item_path}`; inline \ + unions become module-scope siblings, so a nested item under one would never \ + be reachable. Declare it in the enclosing type, or give the union a name." + ) + } + SemanticError::InlineUnionNameCollision { + generated_path, + item_path, + .. + } => { + format!( + "building `{item_path}` generates `{generated_path}`, but that name is already \ + taken. Rename the inline union's field (or, for a vftable, the type itself), \ + or declare the generated item separately." + ) + } + _ => unreachable!(), + } + } + /// Vftable ordering and base-class consistency failures. fn vftable_error_message(&self) -> String { match self { diff --git a/src/semantic/error/mod.rs b/src/semantic/error/mod.rs index af5e2028..58430c55 100644 --- a/src/semantic/error/mod.rs +++ b/src/semantic/error/mod.rs @@ -354,6 +354,59 @@ pub enum SemanticError { field_type: SemanticType, location: ItemLocation, }, + /// A `#[base]` field was declared inside a union. A base at a + /// non-deterministic offset would break the inheritance hierarchy, the + /// generated conversions, and vftable inheritance. + UnionBaseNotAllowed { + item_path: ItemPath, + location: ItemLocation, + }, + /// A `vftable` block was declared inside a union. A vftable pointer must + /// occupy offset 0 exclusively, which a union cannot guarantee. + UnionVftableNotAllowed { + item_path: ItemPath, + location: ItemLocation, + }, + /// `#[address]` was applied to a union member. Every member of a union + /// starts at offset 0 by definition. + UnionMemberAddress { + member_name: String, + item_path: ItemPath, + location: ItemLocation, + }, + /// A union was declared with no members, so it has no size to speak of. + EmptyUnion { + item_path: ItemPath, + location: ItemLocation, + }, + /// A union member is larger than the union's declared `#[size]`. + UnionMemberExceedsSize { + member_name: String, + member_size: usize, + declared_size: usize, + item_path: ItemPath, + location: ItemLocation, + }, + /// A union member was named `_`. In a type that means padding, which a union + /// has no room for — every member already covers the same bytes. + UnionAnonymousMember { + item_path: ItemPath, + location: ItemLocation, + }, + /// An item was declared inside an inline `union { … }` field body. The + /// generated union is a module-scope sibling, so a nested item under it + /// would never be reachable. + InlineUnionNestedItem { + item_name: String, + item_path: ItemPath, + location: ItemLocation, + }, + /// The name generated for an inline union field is already taken. + InlineUnionNameCollision { + generated_path: ItemPath, + item_path: ItemPath, + location: ItemLocation, + }, } impl SemanticError { @@ -412,6 +465,14 @@ impl SemanticError { SemanticError::ConstValueTypeMismatch { location, .. } => Some(location), SemanticError::StrTypeNotConst { location, .. } => Some(location), SemanticError::ZeroSizeFieldEmbedding { location, .. } => Some(location), + SemanticError::UnionBaseNotAllowed { location, .. } => Some(location), + SemanticError::UnionVftableNotAllowed { location, .. } => Some(location), + SemanticError::UnionMemberAddress { location, .. } => Some(location), + SemanticError::EmptyUnion { location, .. } => Some(location), + SemanticError::UnionMemberExceedsSize { location, .. } => Some(location), + SemanticError::UnionAnonymousMember { location, .. } => Some(location), + SemanticError::InlineUnionNestedItem { location, .. } => Some(location), + SemanticError::InlineUnionNameCollision { location, .. } => Some(location), } } diff --git a/src/semantic/mod.rs b/src/semantic/mod.rs index 27ab56b3..1c993dc4 100644 --- a/src/semantic/mod.rs +++ b/src/semantic/mod.rs @@ -21,6 +21,7 @@ pub mod resolution_context; pub(crate) mod type_alias_definition; pub(crate) mod type_definition; pub mod type_registry; +pub(crate) mod union_definition; pub mod validation; #[cfg(test)] diff --git a/src/semantic/name_index.rs b/src/semantic/name_index.rs index 616cdc51..cc1eb972 100644 --- a/src/semantic/name_index.rs +++ b/src/semantic/name_index.rs @@ -26,11 +26,36 @@ pub enum SigKind { Type, Enum, Bitflags, + Union, TypeAlias, Constant, ExternValue, } +/// The kind of a declared item, for the name index's signature map. +fn sig_kind(inner: &grammar::ItemDefinitionInner) -> SigKind { + match inner { + grammar::ItemDefinitionInner::Type(_) => SigKind::Type, + grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, + grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, + grammar::ItemDefinitionInner::Union(_) => SigKind::Union, + grammar::ItemDefinitionInner::TypeAlias(_) => SigKind::TypeAlias, + grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, + grammar::ItemDefinitionInner::ExternValue(_) => SigKind::ExternValue, + } +} + +/// The type-body statements of an item that has one. `union` bodies reuse the +/// `type` body AST, so anywhere that walks a type body for nested items must +/// walk a union body the same way. +fn body_statements(inner: &grammar::ItemDefinitionInner) -> Vec<&grammar::TypeStatement> { + match inner { + grammar::ItemDefinitionInner::Type(td) => td.statements().collect(), + grammar::ItemDefinitionInner::Union(ud) => ud.statements().collect(), + _ => Vec::new(), + } +} + /// A declared item's stable signature: its kind and generic arity. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ItemSig { @@ -136,14 +161,7 @@ impl NameIndex { for def in module.definitions() { let path = module_path.join(def.name.as_str().into()); - let kind = match &def.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, - grammar::ItemDefinitionInner::TypeAlias(_) => SigKind::TypeAlias, - grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, - grammar::ItemDefinitionInner::ExternValue(_) => SigKind::ExternValue, - }; + let kind = sig_kind(&def.inner); self.items.insert( path.clone(), ItemSig { @@ -153,20 +171,13 @@ impl NameIndex { ); self.item_files.insert(path.clone(), file_index); - // Recurse into type bodies to find nested items. + // Recurse into type/union bodies to find nested items. // Only read TypeField::Item data (name, kind, arity) to preserve backdating. - if let grammar::ItemDefinitionInner::Type(td) = &def.inner { - for stmt in td.statements() { + { + for stmt in body_statements(&def.inner) { if let grammar::TypeField::Item(inner) = &stmt.field { let nested_path = path.join(inner.name.as_str().into()); - let nested_kind = match &inner.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, - grammar::ItemDefinitionInner::TypeAlias(_) => SigKind::TypeAlias, - grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, - grammar::ItemDefinitionInner::ExternValue(_) => SigKind::ExternValue, - }; + let nested_kind = sig_kind(&inner.inner); self.items.insert( nested_path.clone(), ItemSig { @@ -190,22 +201,7 @@ impl NameIndex { if let grammar::EnumDefItem::Item(nested2) = item { let nested2_path = nested_path.join(nested2.name.as_str().into()); - let nested2_kind = match &nested2.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => { - SigKind::Bitflags - } - grammar::ItemDefinitionInner::TypeAlias(_) => { - SigKind::TypeAlias - } - grammar::ItemDefinitionInner::Constant(_) => { - SigKind::Constant - } - grammar::ItemDefinitionInner::ExternValue(_) => { - SigKind::ExternValue - } - }; + let nested2_kind = sig_kind(&nested2.inner); self.items.insert( nested2_path.clone(), ItemSig { @@ -230,22 +226,7 @@ impl NameIndex { if let grammar::BitflagsDefItem::Item(nested2) = item { let nested2_path = nested_path.join(nested2.name.as_str().into()); - let nested2_kind = match &nested2.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => { - SigKind::Bitflags - } - grammar::ItemDefinitionInner::TypeAlias(_) => { - SigKind::TypeAlias - } - grammar::ItemDefinitionInner::Constant(_) => { - SigKind::Constant - } - grammar::ItemDefinitionInner::ExternValue(_) => { - SigKind::ExternValue - } - }; + let nested2_kind = sig_kind(&nested2.inner); self.items.insert( nested2_path.clone(), ItemSig { @@ -273,14 +254,7 @@ impl NameIndex { for item in &ed.items { if let grammar::EnumDefItem::Item(inner) = item { let nested_path = path.join(inner.name.as_str().into()); - let nested_kind = match &inner.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, - grammar::ItemDefinitionInner::TypeAlias(_) => SigKind::TypeAlias, - grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, - grammar::ItemDefinitionInner::ExternValue(_) => SigKind::ExternValue, - }; + let nested_kind = sig_kind(&inner.inner); self.items.insert( nested_path.clone(), ItemSig { @@ -305,14 +279,7 @@ impl NameIndex { for item in &bd.items { if let grammar::BitflagsDefItem::Item(inner) = item { let nested_path = path.join(inner.name.as_str().into()); - let nested_kind = match &inner.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, - grammar::ItemDefinitionInner::TypeAlias(_) => SigKind::TypeAlias, - grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, - grammar::ItemDefinitionInner::ExternValue(_) => SigKind::ExternValue, - }; + let nested_kind = sig_kind(&inner.inner); self.items.insert( nested_path.clone(), ItemSig { @@ -370,18 +337,11 @@ impl NameIndex { module_path: &ItemPath, file_index: usize, ) { - if let grammar::ItemDefinitionInner::Type(td) = inner { - for stmt in td.statements() { + { + for stmt in body_statements(inner) { if let grammar::TypeField::Item(nested) = &stmt.field { let nested_path = parent_path.join(nested.name.as_str().into()); - let nested_kind = match &nested.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, - grammar::ItemDefinitionInner::TypeAlias(_) => SigKind::TypeAlias, - grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, - grammar::ItemDefinitionInner::ExternValue(_) => SigKind::ExternValue, - }; + let nested_kind = sig_kind(&nested.inner); self.items.insert( nested_path.clone(), ItemSig { @@ -404,18 +364,7 @@ impl NameIndex { for item in &ed.items { if let grammar::EnumDefItem::Item(nested2) = item { let nested2_path = nested_path.join(nested2.name.as_str().into()); - let nested2_kind = match &nested2.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, - grammar::ItemDefinitionInner::TypeAlias(_) => { - SigKind::TypeAlias - } - grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, - grammar::ItemDefinitionInner::ExternValue(_) => { - SigKind::ExternValue - } - }; + let nested2_kind = sig_kind(&nested2.inner); self.items.insert( nested2_path.clone(), ItemSig { @@ -439,18 +388,7 @@ impl NameIndex { for item in &bd.items { if let grammar::BitflagsDefItem::Item(nested2) = item { let nested2_path = nested_path.join(nested2.name.as_str().into()); - let nested2_kind = match &nested2.inner { - grammar::ItemDefinitionInner::Type(_) => SigKind::Type, - grammar::ItemDefinitionInner::Enum(_) => SigKind::Enum, - grammar::ItemDefinitionInner::Bitflags(_) => SigKind::Bitflags, - grammar::ItemDefinitionInner::TypeAlias(_) => { - SigKind::TypeAlias - } - grammar::ItemDefinitionInner::Constant(_) => SigKind::Constant, - grammar::ItemDefinitionInner::ExternValue(_) => { - SigKind::ExternValue - } - }; + let nested2_kind = sig_kind(&nested2.inner); self.items.insert( nested2_path.clone(), ItemSig { diff --git a/src/semantic/queries/helpers.rs b/src/semantic/queries/helpers.rs index 2222101d..27b38e7c 100644 --- a/src/semantic/queries/helpers.rs +++ b/src/semantic/queries/helpers.rs @@ -18,6 +18,7 @@ use crate::{ self, ItemCategory, ItemDefinition, ItemState, ItemStateResolved, PredefinedItem, TypeDefinition, Visibility, }, + union_definition, }, span::ItemLocation, }; @@ -71,6 +72,17 @@ pub(super) fn build_item( type_param_names, ) } + ItemDefinitionInner::Union(u) => { + let mut ctx = ResolutionContext::new(type_registry, modules); + union_definition::build( + &mut ctx, + item_path, + u, + def_location, + doc_comments, + type_param_names, + ) + } ItemDefinitionInner::Enum(e) => { let ctx_ref = ResolutionContextRef::new(type_registry, modules); enum_definition::build(&ctx_ref, item_path, e, def_location, doc_comments) @@ -125,6 +137,20 @@ pub(super) fn register_predefined(type_registry: &mut TypeRegistry) { } } +/// The `#[cfg(...)]` predicate on an item's own attribute list, whatever kind it +/// is. +pub(super) fn item_cfg(inner: &ItemDefinitionInner) -> Option { + match inner { + ItemDefinitionInner::Type(td) => td.attributes.cfg(), + ItemDefinitionInner::Enum(e) => e.attributes.cfg(), + ItemDefinitionInner::Bitflags(b) => b.attributes.cfg(), + ItemDefinitionInner::Union(u) => u.attributes.cfg(), + ItemDefinitionInner::TypeAlias(ta) => ta.attributes.cfg(), + ItemDefinitionInner::Constant(c) => c.attributes.cfg(), + ItemDefinitionInner::ExternValue(ev) => ev.attributes.cfg(), + } +} + pub(super) fn register_unresolved( type_registry: &mut TypeRegistry, path: &ItemPath, @@ -135,14 +161,7 @@ pub(super) fn register_unresolved( .iter() .map(|tp| tp.name.clone()) .collect(); - let cfg = match &definition.inner { - ItemDefinitionInner::Type(td) => td.attributes.cfg(), - ItemDefinitionInner::Enum(e) => e.attributes.cfg(), - ItemDefinitionInner::Bitflags(b) => b.attributes.cfg(), - ItemDefinitionInner::TypeAlias(ta) => ta.attributes.cfg(), - ItemDefinitionInner::Constant(c) => c.attributes.cfg(), - ItemDefinitionInner::ExternValue(ev) => ev.attributes.cfg(), - }; + let cfg = item_cfg(&definition.inner); type_registry.add(ItemDefinition { visibility: definition.visibility.into(), path: path.clone(), @@ -257,36 +276,47 @@ pub(super) fn value_referenced_types( index: &NameIndex, ) -> Vec { let mut refs = Vec::new(); - match &definition.inner { - ItemDefinitionInner::Type(td) => { - for statement in td.statements() { - match &statement.field { - TypeField::Field(_, _, type_) => { - collect_value_refs(type_, scope, index, &mut refs) - } - // vftable function signatures resolve their by-value arg / - // return types too — a generic like `WeakPtr` in - // a method signature must be resolved to build the vftable. - TypeField::Vftable(functions) => { - for function in functions { - for argument in &function.arguments { - if let Argument::Named { type_, .. } = argument { - collect_value_refs(type_, scope, index, &mut refs); - } - } - if let Some(return_type) = &function.return_type { - collect_value_refs(return_type, scope, index, &mut refs); + + /// A type/union body's by-value references. `union` bodies reuse the `type` + /// body AST, and an inline `union { … }` field nests one inside the other. + fn body_value_refs<'a>( + statements: impl Iterator, + scope: &[ItemPath], + index: &NameIndex, + refs: &mut Vec, + ) { + for statement in statements { + match &statement.field { + TypeField::Field(_, _, type_) => collect_value_refs(type_, scope, index, refs), + // vftable function signatures resolve their by-value arg / + // return types too — a generic like `WeakPtr` in + // a method signature must be resolved to build the vftable. + TypeField::Vftable(functions) => { + for function in functions { + for argument in &function.arguments { + if let Argument::Named { type_, .. } = argument { + collect_value_refs(type_, scope, index, refs); } } + if let Some(return_type) = &function.return_type { + collect_value_refs(return_type, scope, index, refs); + } } - TypeField::Item(inner_def) => { - // Collect value refs from nested item definitions recursively - let nested_refs = value_referenced_types(inner_def, scope, index); - refs.extend(nested_refs); - } + } + TypeField::Item(inner_def) => { + // Collect value refs from nested item definitions recursively + refs.extend(value_referenced_types(inner_def, scope, index)); + } + TypeField::UnionField { body, .. } => { + body_value_refs(body.statements(), scope, index, refs) } } } + } + + match &definition.inner { + ItemDefinitionInner::Type(td) => body_value_refs(td.statements(), scope, index, &mut refs), + ItemDefinitionInner::Union(ud) => body_value_refs(ud.statements(), scope, index, &mut refs), ItemDefinitionInner::Enum(e) => collect_value_refs(&e.type_, scope, index, &mut refs), ItemDefinitionInner::Bitflags(b) => collect_value_refs(&b.type_, scope, index, &mut refs), ItemDefinitionInner::TypeAlias(ta) => { diff --git a/src/semantic/queries/resolve.rs b/src/semantic/queries/resolve.rs index 4d1b693c..7f71ef0b 100644 --- a/src/semantic/queries/resolve.rs +++ b/src/semantic/queries/resolve.rs @@ -6,7 +6,7 @@ use std::{collections::BTreeMap, sync::Arc}; use crate::{ - grammar::{self, ItemDefinitionInner, ItemPath}, + grammar::{self, ItemPath}, semantic::{ Module, SemanticError, TypeRegistry, error::BuildOutcome, @@ -18,8 +18,8 @@ use crate::{ use super::{ super::{db::Db, inputs::SourceSet, ir::ResolvedItem}, helpers::{ - build_item, make_extern_from_sig, make_predefined_definition, make_unresolved_definition, - value_referenced_types, + build_item, item_cfg, make_extern_from_sig, make_predefined_definition, + make_unresolved_definition, value_referenced_types, }, index::{name_index, placeholder_base}, leaf::parse_file, @@ -60,9 +60,16 @@ fn find_grammar_def( // body for a nested item whose name matches target_path.last(). let parent_path = target_path.parent()?; let parent_def = find_grammar_def(module, module_path, &parent_path)?; - if let grammar::ItemDefinitionInner::Type(td) = &parent_def.inner { + // `union` bodies reuse the `type` body AST, so nested items are found the + // same way in both. + let body_statements: Vec<&grammar::TypeStatement> = match &parent_def.inner { + grammar::ItemDefinitionInner::Type(td) => td.statements().collect(), + grammar::ItemDefinitionInner::Union(ud) => ud.statements().collect(), + _ => Vec::new(), + }; + { let leaf = target_path.last()?; - for stmt in td.statements() { + for stmt in body_statements { if let grammar::TypeField::Item(inner) = &stmt.field { if inner.name.as_str() == leaf.as_str() { return Some((**inner).clone()); @@ -269,14 +276,7 @@ pub fn resolve_item<'db>( let (item_def, errors) = match outcome { Ok(BuildOutcome::Resolved(item)) => { - let cfg = match &definition.inner { - ItemDefinitionInner::Type(td) => td.attributes.cfg(), - ItemDefinitionInner::Enum(e) => e.attributes.cfg(), - ItemDefinitionInner::Bitflags(b) => b.attributes.cfg(), - ItemDefinitionInner::TypeAlias(ta) => ta.attributes.cfg(), - ItemDefinitionInner::Constant(c) => c.attributes.cfg(), - ItemDefinitionInner::ExternValue(ev) => ev.attributes.cfg(), - }; + let cfg = item_cfg(&definition.inner); ( ItemDefinition { visibility, diff --git a/src/semantic/queries/root.rs b/src/semantic/queries/root.rs index b4390a0e..3e0b2405 100644 --- a/src/semantic/queries/root.rs +++ b/src/semantic/queries/root.rs @@ -312,6 +312,13 @@ pub fn analyze<'db>( // definition_paths are folded in just below. let mut semantic_errors: Vec = Vec::new(); let item_paths: Vec = decl_registry.item_paths().cloned().collect(); + // Each item is resolved in its own registry overlay, so `resolve_item` can + // only catch a generated name colliding with a *declared* item. Two items + // generating the same name (`type A { b_c: union {…} }` and + // `type AB { c: union {…} }` both produce `ABCUnion`) only meet here, and + // `type_registry.add` is an insert — one would silently replace the other + // after its owner had already measured it. + let mut generated_owners: BTreeMap = BTreeMap::new(); for item_path in &item_paths { let resolved = resolve_item(db, sources, pointer_size, item_path.clone()); semantic_errors.extend(resolved.errors(db).iter().cloned()); @@ -328,6 +335,15 @@ pub fn analyze<'db>( }); } for generated in resolved.generated_items(db).iter() { + if let Some(other) = generated_owners.get(&generated.path) { + semantic_errors.push(SemanticError::InlineUnionNameCollision { + generated_path: generated.path.clone(), + item_path: other.clone(), + location: generated.location, + }); + continue; + } + generated_owners.insert(generated.path.clone(), item_path.clone()); definition_paths.insert(generated.path.clone()); type_registry.add(generated.clone()); } diff --git a/src/semantic/queries/source_map.rs b/src/semantic/queries/source_map.rs index d316b57f..037592dd 100644 --- a/src/semantic/queries/source_map.rs +++ b/src/semantic/queries/source_map.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use crate::{ grammar::{ Argument, Function, ImplItem, ItemDefinition, ItemDefinitionInner, ItemPath, ModuleItem, - Type, TypeField, + Type, TypeField, TypeStatement, }, semantic::name_index::NameIndex, span::{HasLocation, Location, Span}, @@ -117,32 +117,48 @@ fn collect_type_ref_spans( tokens: &[Token], out: &mut Vec<(Span, ItemPath)>, ) { - match &definition.inner { - ItemDefinitionInner::Type(td) => { - for statement in td.statements() { - match &statement.field { - TypeField::Field(_, _, type_) => { - type_ref_spans(type_, scope, index, tokens, out) - } - TypeField::Vftable(functions) => { - for function in functions { - for argument in &function.arguments { - if let Argument::Named { type_, .. } = argument { - type_ref_spans(type_, scope, index, tokens, out); - } - } - if let Some(return_type) = &function.return_type { - type_ref_spans(return_type, scope, index, tokens, out); + /// A type/union body's type-reference spans. `union` bodies reuse the `type` + /// body AST, and an inline `union { … }` field nests one inside the other. + fn body_type_ref_spans<'a>( + statements: impl Iterator, + scope: &[ItemPath], + index: &NameIndex, + tokens: &[Token], + out: &mut Vec<(Span, ItemPath)>, + ) { + for statement in statements { + match &statement.field { + TypeField::Field(_, _, type_) => type_ref_spans(type_, scope, index, tokens, out), + TypeField::Vftable(functions) => { + for function in functions { + for argument in &function.arguments { + if let Argument::Named { type_, .. } = argument { + type_ref_spans(type_, scope, index, tokens, out); } } + if let Some(return_type) = &function.return_type { + type_ref_spans(return_type, scope, index, tokens, out); + } } - TypeField::Item(inner_def) => { - // Collect type reference spans from nested item definitions - collect_type_ref_spans(inner_def, scope, index, tokens, out); - } + } + TypeField::Item(inner_def) => { + // Collect type reference spans from nested item definitions + collect_type_ref_spans(inner_def, scope, index, tokens, out); + } + TypeField::UnionField { body, .. } => { + body_type_ref_spans(body.statements(), scope, index, tokens, out) } } } + } + + match &definition.inner { + ItemDefinitionInner::Type(td) => { + body_type_ref_spans(td.statements(), scope, index, tokens, out) + } + ItemDefinitionInner::Union(ud) => { + body_type_ref_spans(ud.statements(), scope, index, tokens, out) + } ItemDefinitionInner::Enum(e) => type_ref_spans(&e.type_, scope, index, tokens, out), ItemDefinitionInner::Bitflags(b) => type_ref_spans(&b.type_, scope, index, tokens, out), ItemDefinitionInner::TypeAlias(ta) => type_ref_spans(&ta.target, scope, index, tokens, out), diff --git a/src/semantic/tests/mod.rs b/src/semantic/tests/mod.rs index 95c36c19..20e6ebc0 100644 --- a/src/semantic/tests/mod.rs +++ b/src/semantic/tests/mod.rs @@ -40,6 +40,7 @@ mod modules; mod pinned; mod queries; mod type_aliases; +mod unions; mod util; mod vftable; mod visibility; diff --git a/src/semantic/tests/unions.rs b/src/semantic/tests/unions.rs new file mode 100644 index 00000000..bcd0a5f1 --- /dev/null +++ b/src/semantic/tests/unions.rs @@ -0,0 +1,517 @@ +//! Tests for union layout and the constructs a union body rejects. + +use crate::{ + grammar::test_aliases::*, + semantic::{ + error::{AttributeName, SemanticError}, + types::test_aliases::*, + }, + span::ItemLocation, +}; + +use super::util::*; + +/// A union is as large as its largest member, and every member starts at 0. +#[test] +fn union_takes_the_size_of_its_largest_member() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Payload"), + UD::new([ + TS::field((V::Public, "as_int"), T::ident("i32")), + TS::field((V::Public, "as_long"), T::ident("u64")), + TS::field((V::Public, "as_byte"), T::ident("u8")), + ]), + )]), + [SID::defined_resolved( + (SV::Public, "test::Payload"), + SISR::new( + (8, 8), + SUD::new().with_regions([ + SR::field((SV::Public, "as_int"), ST::raw("i32")), + SR::field((SV::Public, "as_long"), ST::raw("u64")), + SR::field((SV::Public, "as_byte"), ST::raw("u8")), + ]), + ), + )], + ); +} + +/// Unlike a type, a union with no explicit alignment does *not* fall back to +/// the pointer size — widening it would inflate its size. +#[test] +fn union_alignment_is_the_strictest_member_not_the_pointer_size() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Small"), + UD::new([ + TS::field((V::Public, "a"), T::ident("u8")), + TS::field((V::Public, "b"), T::ident("u8")), + ]), + )]), + [SID::defined_resolved( + (SV::Public, "test::Small"), + SISR::new( + (1, 1), + SUD::new().with_regions([ + SR::field((SV::Public, "a"), ST::raw("u8")), + SR::field((SV::Public, "b"), ST::raw("u8")), + ]), + ), + )], + ); +} + +/// The natural size is rounded up to the alignment. +#[test] +fn union_size_rounds_up_to_alignment() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Mixed"), + UD::new([ + TS::field((V::Public, "wide"), T::ident("u32")), + TS::field((V::Public, "narrow"), T::ident("u8").array(5)), + ]), + )]), + [SID::defined_resolved( + (SV::Public, "test::Mixed"), + SISR::new( + (8, 4), + SUD::new().with_regions([ + SR::field((SV::Public, "wide"), ST::raw("u32")), + SR::field((SV::Public, "narrow"), ST::raw("u8").array(5)), + ]), + ), + )], + ); +} + +/// `#[size]` pads the union out past its largest member. The padding is another +/// whole-width member, not a tail — a union has no tail — so the backends have +/// something to emit that actually makes `sizeof` match. +#[test] +fn union_size_attribute_pads_out() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Padded"), + UD::new([ + TS::field((V::Public, "small"), T::ident("u32")), + TS::field((V::Public, "medium"), T::ident("u64")), + ]) + .with_attributes([A::size(16)]), + )]), + [SID::defined_resolved( + (SV::Public, "test::Padded"), + SISR::new( + (16, 8), + SUD::new().with_regions([ + SR::field((SV::Public, "small"), ST::raw("u32")), + SR::field((SV::Public, "medium"), ST::raw("u64")), + SR::field((SV::Private, "_padding"), ST::raw("u8").array(16)), + ]), + ), + )], + ); +} + +/// `#[min_size]` raises the floor, and pads the same way `#[size]` does. +#[test] +fn union_min_size_attribute_pads_out() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Floored"), + UD::new([TS::field((V::Public, "small"), T::ident("u32"))]) + .with_attributes([A::min_size(12)]), + )]), + [SID::defined_resolved( + (SV::Public, "test::Floored"), + SISR::new( + (12, 4), + SUD::new().with_regions([ + SR::field((SV::Public, "small"), ST::raw("u32")), + SR::field((SV::Private, "_padding"), ST::raw("u8").array(12)), + ]), + ), + )], + ); +} + +/// A union that already fills its declared size gains no padding member. +#[test] +fn union_exactly_at_declared_size_is_not_padded() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Exact"), + UD::new([TS::field((V::Public, "value"), T::ident("u32"))]) + .with_attributes([A::size(4)]), + )]), + [SID::defined_resolved( + (SV::Public, "test::Exact"), + SISR::new( + (4, 4), + SUD::new().with_regions([SR::field((SV::Public, "value"), ST::raw("u32"))]), + ), + )], + ); +} + +/// `#[align]` over-aligns, and the size rounds up to match. +#[test] +fn union_align_attribute_over_aligns() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "OverAligned"), + UD::new([TS::field((V::Public, "value"), T::ident("u32"))]) + .with_attributes([A::align(16)]), + )]), + [SID::defined_resolved( + (SV::Public, "test::OverAligned"), + SISR::new( + (16, 16), + SUD::new().with_regions([SR::field((SV::Public, "value"), ST::raw("u32"))]), + ), + )], + ); +} + +/// `#[packed]` drops the alignment to 1. +#[test] +fn union_packed_drops_alignment_to_one() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Packed"), + UD::new([ + TS::field((V::Public, "word"), T::ident("u16")), + TS::field((V::Public, "bytes"), T::ident("u8").array(2)), + ]) + .with_attributes([A::packed()]), + )]), + [SID::defined_resolved( + (SV::Public, "test::Packed"), + SISR::new( + (2, 1), + SUD::new().with_packed(true).with_regions([ + SR::field((SV::Public, "word"), ST::raw("u16")), + SR::field((SV::Public, "bytes"), ST::raw("u8").array(2)), + ]), + ), + )], + ); +} + +/// An inline `pub name: union { … }` field becomes a generated module-scope +/// sibling item plus an ordinary field pointing at it. +#[test] +fn inline_union_field_generates_a_sibling_item() { + assert_ast_produces_type_definitions( + M::new().with_definitions([ID::new( + (V::Public, "Scratch"), + TD::new([ + TS::field((V::Public, "tag"), T::ident("u64")), + TS::union_field( + (V::Public, "data"), + UD::new([ + TS::field((V::Public, "as_u64"), T::ident("u64")), + TS::field((V::Public, "as_bytes"), T::ident("u8").array(8)), + ]), + ), + ]) + .with_attributes([A::align(8)]), + )]), + [ + SID::defined_resolved( + (SV::Public, "test::Scratch"), + SISR::new( + (16, 8), + STD::new().with_regions([ + SR::field((SV::Public, "tag"), ST::raw("u64")), + SR::field((SV::Public, "data"), ST::raw("test::ScratchDataUnion")), + ]), + ), + ), + SID::defined_resolved( + (SV::Public, "test::ScratchDataUnion"), + SISR::new( + (8, 8), + SUD::new().with_regions([ + SR::field((SV::Public, "as_u64"), ST::raw("u64")), + SR::field((SV::Public, "as_bytes"), ST::raw("u8").array(8)), + ]), + ), + ), + ], + ); +} + +#[test] +fn base_in_a_union_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([ + TS::field((V::Public, "a"), T::ident("u32")).with_attributes([A::base()]), + TS::field((V::Public, "b"), T::ident("u32")), + ]), + )]), + SemanticError::UnionBaseNotAllowed { + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn vftable_in_a_union_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([ + TS::vftable([F::new((V::Public, "f"), [Ar::mut_self()])]), + TS::field((V::Public, "b"), T::ident("u32")), + ]), + )]), + SemanticError::UnionVftableNotAllowed { + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn address_on_a_union_member_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([ + TS::field((V::Public, "a"), T::ident("u32")), + TS::field((V::Public, "b"), T::ident("u32")).with_attributes([A::address(4)]), + ]), + )]), + SemanticError::UnionMemberAddress { + member_name: "b".to_string(), + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn empty_union_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new((V::Public, "Bad"), UD::new([]))]), + SemanticError::EmptyUnion { + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn member_larger_than_declared_size_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([TS::field((V::Public, "wide"), T::ident("u32"))]) + .with_attributes([A::size(2)]), + )]), + SemanticError::UnionMemberExceedsSize { + member_name: "wide".to_string(), + member_size: 4, + declared_size: 2, + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn align_below_member_requirement_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([TS::field((V::Public, "wide"), T::ident("u64"))]) + .with_attributes([A::align(2)]), + )]), + SemanticError::AlignmentBelowMinimum { + alignment: 2, + required_alignment: 8, + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn anonymous_union_member_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([ + TS::field((V::Public, "_"), T::ident("u32")), + TS::field((V::Public, "b"), T::ident("u32")), + ]), + )]), + SemanticError::UnionAnonymousMember { + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn anonymous_inline_union_field_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + TD::new([TS::union_field( + (V::Public, "_"), + UD::new([TS::field((V::Public, "a"), T::ident("u32"))]), + )]), + )]), + SemanticError::UnionAnonymousMember { + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +/// An inline union's item is synthesised at a module-scope path, so anything +/// declared inside it would be registered nowhere. Reject rather than drop. +#[test] +fn nested_item_in_an_inline_union_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Outer"), + TD::new([TS::union_field( + (V::Public, "payload"), + UD::new([ + TS::field((V::Public, "a"), T::ident("u32")), + TS::item(ID::new( + (V::Public, "Nested"), + TD::new([TS::field((V::Public, "z"), T::ident("u32"))]), + )), + ]), + )]), + )]), + SemanticError::InlineUnionNestedItem { + item_name: "Nested".to_string(), + item_path: IP::from("test::OuterPayloadUnion"), + location: ItemLocation::test(), + }, + ); +} + +/// The generated name must be free: overwriting a user's item would leave the +/// enclosing type asserting a size the replacement no longer has. +#[test] +fn inline_union_colliding_with_a_declared_item_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ + ID::new( + (V::Public, "ScratchDataUnion"), + UD::new([TS::field((V::Public, "a"), T::ident("u64"))]), + ), + ID::new( + (V::Public, "Scratch"), + TD::new([TS::union_field( + (V::Public, "data"), + UD::new([TS::field((V::Public, "x"), T::ident("u16"))]), + )]), + ), + ]), + SemanticError::InlineUnionNameCollision { + generated_path: IP::from("test::ScratchDataUnion"), + item_path: IP::from("test::Scratch"), + location: ItemLocation::test(), + }, + ); +} + +/// Underscores are separators, not characters, when the field name is +/// PascalCased — so `a_b` and `a__b` generate the same name. +#[test] +fn two_inline_unions_generating_the_same_name_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "T"), + TD::new([ + TS::union_field( + (V::Public, "a_b"), + UD::new([TS::field((V::Public, "x"), T::ident("u8"))]), + ), + TS::union_field( + (V::Public, "a__b"), + UD::new([TS::field((V::Public, "y"), T::ident("u64"))]), + ), + ]), + )]), + SemanticError::InlineUnionNameCollision { + generated_path: IP::from("test::TABUnion"), + item_path: IP::from("test::T"), + location: ItemLocation::test(), + }, + ); +} + +/// Two *different* types can also land on one name; they never meet during +/// resolution, so this is caught when their generated items are merged. +#[test] +fn two_types_generating_the_same_union_name_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ + ID::new( + (V::Public, "A"), + TD::new([TS::union_field( + (V::Public, "b_c"), + UD::new([TS::field((V::Public, "x"), T::ident("u8"))]), + )]), + ), + ID::new( + (V::Public, "AB"), + TD::new([TS::union_field( + (V::Public, "c"), + UD::new([TS::field((V::Public, "y"), T::ident("u64"))]), + )]), + ), + ]), + SemanticError::InlineUnionNameCollision { + generated_path: IP::from("test::ABCUnion"), + item_path: IP::from("test::A"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn both_size_and_min_size_on_a_union_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([TS::field((V::Public, "a"), T::ident("u32"))]) + .with_attributes([A::size(8), A::min_size(8)]), + )]), + SemanticError::ConflictingAttributes { + attr1: AttributeName::Size, + attr2: AttributeName::MinSize, + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} + +#[test] +fn packed_and_align_on_a_union_is_rejected() { + assert_ast_produces_exact_error( + M::new().with_definitions([ID::new( + (V::Public, "Bad"), + UD::new([TS::field((V::Public, "a"), T::ident("u32"))]) + .with_attributes([A::packed(), A::align(8)]), + )]), + SemanticError::ConflictingAttributes { + attr1: AttributeName::Packed, + attr2: AttributeName::Align, + item_path: IP::from("test::Bad"), + location: ItemLocation::test(), + }, + ); +} diff --git a/src/semantic/type_definition/build.rs b/src/semantic/type_definition/build.rs index e48d0448..5f633199 100644 --- a/src/semantic/type_definition/build.rs +++ b/src/semantic/type_definition/build.rs @@ -20,26 +20,37 @@ use crate::{ }; use super::{TypeDefinition, vftable}; +use crate::semantic::union_definition::{self, InlineUnionRequest}; /// Type-level attributes parsed from a `TypeDefinition`'s attribute list. -struct TypeAttributes { - target_size: Option, - min_size: Option, - singleton: Option, - copyable: bool, - cloneable: bool, - defaultable: bool, - packed: bool, - pinned: bool, - align: Option, +/// +/// Shared with `union_definition`, which accepts the same layout attributes on +/// the same terms — a union is laid out by different rules, but `#[size]`, +/// `#[align]`, `#[packed]` and the trait attributes mean exactly what they do on +/// a type. +pub(in crate::semantic) struct TypeAttributes { + pub target_size: Option, + pub min_size: Option, + pub singleton: Option, + pub copyable: bool, + pub cloneable: bool, + pub defaultable: bool, + pub packed: bool, + pub pinned: bool, + pub align: Option, } /// The layout-bearing content collected from a type body: the pending field -/// regions, the resolved vftable functions (if any), and the nested item paths. -struct TypeBody { +/// regions, the resolved vftable functions (if any), the nested item paths, and +/// the inline `union { … }` fields awaiting desugaring. +struct TypeBody<'a> { pending_regions: Vec<(Option, Region)>, vftable_functions: Option>, nested_item_paths: Vec, + /// Inline unions can't be built during the walk — registering the generated + /// items needs the resolution context mutably, and the walk holds it + /// immutably. They're collected here and built by `build` immediately after. + inline_unions: Vec>, } pub fn build( @@ -76,8 +87,24 @@ pub fn build( pending_regions, vftable_functions, nested_item_paths, + inline_unions, } = body; + // Desugar the inline `union { … }` fields into generated sibling items. Each + // field's region already points at the item's path, so this only has to make + // that path real before layout asks for its size. + if !inline_unions.is_empty() + && union_definition::build_inline_union( + semantic, + resolvee_path, + &inline_unions, + type_parameters, + )? + .is_none() + { + return Ok(BuildOutcome::Deferred); + } + // Handle min_size: pre-calculate alignment and round up min_size if let Some(min_size_value) = attributes.min_size { attributes.target_size = Some(round_up_min_size( @@ -152,7 +179,9 @@ enum BuildFlow { } /// Parse the type-level attributes (`#[size]`, `#[copyable]`, `#[align]`, ...). -fn parse_type_attributes(attributes: &[grammar::Attribute]) -> Result { +pub(in crate::semantic) fn parse_type_attributes( + attributes: &[grammar::Attribute], +) -> Result { let mut target_size: Option = None; let mut min_size: Option = None; let mut singleton = None; @@ -207,16 +236,17 @@ fn parse_type_attributes(attributes: &[grammar::Attribute]) -> Result( semantic: &ResolutionContext<'_>, module: &Module, resolvee_path: &ItemPath, - definition: &grammar::TypeDefinition, + definition: &'a grammar::TypeDefinition, type_parameters: &[String], -) -> Result> { +) -> Result>> { let mut pending_regions: Vec<(Option, Region)> = vec![]; let mut vftable_functions = None; let mut nested_item_paths: Vec = Vec::new(); + let mut inline_unions: Vec> = Vec::new(); for statement in definition.statements() { let grammar::TypeStatement { field, @@ -373,6 +403,68 @@ fn build_type_body( // Nested items are registered by name_index/declaration_registry (Phase 3), // not during build. We only collect the paths here. } + grammar::TypeField::UnionField { + visibility, + name, + body, + } => { + // `pub payload: union { … }` is one ordinary field whose type is + // a generated sibling item. Recording the region here (rather + // than after the union is built) keeps it in body order, so the + // field lands at the offset the source implies. + // + // The field name is load-bearing twice over — it names the Rust + // field *and* the generated item — so it can't be the anonymous + // `_` that padding uses. + if name.as_str() == "_" { + return Err(SemanticError::UnionAnonymousMember { + item_path: resolvee_path.clone(), + location: statement.location, + }); + } + + let Some(path) = InlineUnionRequest::path_for(resolvee_path, name.as_str()) else { + return Err(SemanticError::ModuleNotFound { + path: resolvee_path.clone(), + location: statement.location, + }); + }; + + let mut address: Option = None; + for attribute in attributes { + if let grammar::Attribute::Function { + name: attr_ident, + items, + .. + } = attribute + && let Some(attr_address) = + attribute::parse_address(attr_ident, items, attribute.location())? + { + address = Some(attr_address); + } + } + + inline_unions.push(InlineUnionRequest { + path: path.clone(), + visibility: (*visibility).into(), + attributes, + body, + doc: doc_comments.to_vec(), + location: statement.location, + }); + + pending_regions.push(( + address, + Region { + visibility: (*visibility).into(), + name: Some(name.0.clone()), + doc: doc_comments.to_vec(), + type_ref: Type::Raw(path), + is_base: false, + location: statement.location, + }, + )); + } } } @@ -380,6 +472,7 @@ fn build_type_body( pending_regions, vftable_functions, nested_item_paths, + inline_unions, })) } @@ -429,7 +522,7 @@ fn round_up_min_size( /// Enforce the `#[defaultable]`, `#[copyable]`, and `#[cloneable]` constraints /// against every region's type. -fn check_trait_constraints( +pub(in crate::semantic) fn check_trait_constraints( semantic: &ResolutionContext<'_>, resolvee_path: &ItemPath, regions: &[Region], diff --git a/src/semantic/type_definition/mod.rs b/src/semantic/type_definition/mod.rs index b7ecd7c1..d32e54a4 100644 --- a/src/semantic/type_definition/mod.rs +++ b/src/semantic/type_definition/mod.rs @@ -16,6 +16,9 @@ mod resolve; mod vftable; pub use build::build; +pub(in crate::semantic) use build::{ + TypeAttributes, check_trait_constraints, parse_type_attributes, +}; pub use region::Region; pub(in crate::semantic) use resolve::get_region_name_and_type_definition; pub use vftable::TypeVftable; diff --git a/src/semantic/type_registry/mod.rs b/src/semantic/type_registry/mod.rs index 8fbbb3a0..58e4bb1d 100644 --- a/src/semantic/type_registry/mod.rs +++ b/src/semantic/type_registry/mod.rs @@ -102,7 +102,7 @@ impl TypeRegistry { /// Look up an item — own additions first, then the base. A re-export alias /// is canonicalized to its target so the resolved definition (which may live /// in this overlay) is found rather than the alias path. - fn lookup(&self, path: &ItemPath) -> Option<&ItemDefinition> { + pub(in crate::semantic) fn lookup(&self, path: &ItemPath) -> Option<&ItemDefinition> { let canonical = self.canonicalize(path); self.types .get(&canonical) diff --git a/src/semantic/types/item.rs b/src/semantic/types/item.rs index 3d7c7d13..9c062e7c 100644 --- a/src/semantic/types/item.rs +++ b/src/semantic/types/item.rs @@ -9,7 +9,7 @@ use crate::span::StripLocations; use super::{ BitflagsDefinition, ConstDefinition, EnumDefinition, ExternValueDefinition, - TypeAliasDefinition, TypeDefinition, Visibility, + TypeAliasDefinition, TypeDefinition, UnionDefinition, Visibility, }; #[derive(PartialEq, Eq, Debug, Clone, Hash)] @@ -18,6 +18,7 @@ pub enum ItemDefinitionInner { Type(TypeDefinition), Enum(EnumDefinition), Bitflags(BitflagsDefinition), + Union(UnionDefinition), TypeAlias(TypeAliasDefinition), Constant(ConstDefinition), ExternValue(ExternValueDefinition), @@ -32,6 +33,11 @@ impl From for ItemDefinitionInner { ItemDefinitionInner::Enum(ed) } } +impl From for ItemDefinitionInner { + fn from(ud: UnionDefinition) -> Self { + ItemDefinitionInner::Union(ud) + } +} impl From for ItemDefinitionInner { fn from(bd: BitflagsDefinition) -> Self { ItemDefinitionInner::Bitflags(bd) @@ -58,6 +64,7 @@ impl ItemDefinitionInner { ItemDefinitionInner::Type(td) => td.defaultable, ItemDefinitionInner::Enum(ed) => ed.default.is_some(), ItemDefinitionInner::Bitflags(bd) => bd.default.is_some(), + ItemDefinitionInner::Union(ud) => ud.defaultable, ItemDefinitionInner::TypeAlias(_) => false, // Type aliases don't have defaultable ItemDefinitionInner::Constant(_) => false, ItemDefinitionInner::ExternValue(_) => false, @@ -68,6 +75,7 @@ impl ItemDefinitionInner { ItemDefinitionInner::Type(td) => td.copyable, ItemDefinitionInner::Enum(ed) => ed.copyable, ItemDefinitionInner::Bitflags(bd) => bd.copyable, + ItemDefinitionInner::Union(ud) => ud.copyable, ItemDefinitionInner::TypeAlias(_) => false, // Type aliases don't have copyable ItemDefinitionInner::Constant(_) => false, ItemDefinitionInner::ExternValue(_) => false, @@ -78,6 +86,7 @@ impl ItemDefinitionInner { ItemDefinitionInner::Type(td) => td.cloneable, ItemDefinitionInner::Enum(ed) => ed.cloneable, ItemDefinitionInner::Bitflags(bd) => bd.cloneable, + ItemDefinitionInner::Union(ud) => ud.cloneable, ItemDefinitionInner::TypeAlias(_) => false, // Type aliases don't have cloneable ItemDefinitionInner::Constant(_) => false, ItemDefinitionInner::ExternValue(_) => false, @@ -88,6 +97,7 @@ impl ItemDefinitionInner { ItemDefinitionInner::Type(td) => td.pinned, ItemDefinitionInner::Enum(ed) => ed.pinned, ItemDefinitionInner::Bitflags(bd) => bd.pinned, + ItemDefinitionInner::Union(ud) => ud.pinned, ItemDefinitionInner::TypeAlias(_) => false, // Type aliases don't have pinned ItemDefinitionInner::Constant(_) => false, ItemDefinitionInner::ExternValue(_) => false, @@ -110,11 +120,18 @@ impl ItemDefinitionInner { ItemDefinitionInner::Type(_) => "a type", ItemDefinitionInner::Enum(_) => "an enum", ItemDefinitionInner::Bitflags(_) => "a bitflags", + ItemDefinitionInner::Union(_) => "a union", ItemDefinitionInner::TypeAlias(_) => "a type alias", ItemDefinitionInner::Constant(_) => "a constant", ItemDefinitionInner::ExternValue(_) => "an extern value", } } + pub fn as_union(&self) -> Option<&UnionDefinition> { + match self { + Self::Union(v) => Some(v), + _ => None, + } + } pub fn as_type_alias(&self) -> Option<&TypeAliasDefinition> { match self { Self::TypeAlias(v) => Some(v), diff --git a/src/semantic/types/mod.rs b/src/semantic/types/mod.rs index b994f4fc..c9932d4a 100644 --- a/src/semantic/types/mod.rs +++ b/src/semantic/types/mod.rs @@ -6,6 +6,7 @@ pub use crate::semantic::{ function::{Argument, CallingConvention, Function, FunctionBody}, type_alias_definition::TypeAliasDefinition, type_definition::{Region, TypeDefinition, TypeVftable}, + union_definition::UnionDefinition, }; mod const_value; @@ -24,6 +25,7 @@ pub use type_::{Type, Visibility}; pub mod test_aliases { pub type SID = super::ItemDefinition; pub type STD = super::TypeDefinition; + pub type SUD = super::UnionDefinition; pub type SED = super::EnumDefinition; pub type SBFD = super::BitflagsDefinition; pub type STAD = super::TypeAliasDefinition; diff --git a/src/semantic/union_definition/build.rs b/src/semantic/union_definition/build.rs new file mode 100644 index 00000000..42ec34cf --- /dev/null +++ b/src/semantic/union_definition/build.rs @@ -0,0 +1,604 @@ +use crate::{ + grammar::{self, ItemPath}, + semantic::{ + attribute, + error::{ + AttributeName, BuildOutcome, Result, SemanticError, UnresolvedTypeContext, + UnresolvedTypeReference, + }, + resolution_context::ResolutionContext, + type_definition::{Region, TypeAttributes, check_trait_constraints, parse_type_attributes}, + type_registry::TypeLookupResult, + types::{ItemCategory, ItemDefinition, ItemState, ItemStateResolved, Type, Visibility}, + }, + span::{HasLocation, ItemLocation}, + util, +}; + +use super::{UnionDefinition, inline_union_name}; + +/// Build a standalone `union Name { … }` item. +pub fn build( + semantic: &mut ResolutionContext<'_>, + resolvee_path: &ItemPath, + definition: &grammar::UnionDefinition, + location: &ItemLocation, + doc_comments: &[String], + type_parameters: &[String], +) -> Result { + let mut generated = Vec::new(); + let outcome = build_state( + semantic, + resolvee_path, + &definition.attributes, + definition, + location, + doc_comments, + type_parameters, + Nesting::Named, + &mut generated, + )?; + + // Only publish the inline unions declared inside this body once the union + // itself is known good, so a deferral leaves the registry untouched. + if matches!(outcome, BuildOutcome::Resolved(_)) { + register_generated(semantic, resolvee_path, generated)?; + } + + Ok(outcome) +} + +/// Whether a union body came from a named `union Name { … }` item or from an +/// inline `pub field: union { … }`. Inline bodies are more restricted: their +/// item is generated at a synthesised path, so nothing may be declared under it. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Nesting { + Named, + Inline, +} + +/// Publish generated items, refusing to overwrite anything already present. +/// +/// [`ResolutionContext::add_item`] is an insert: a collision would silently +/// replace an item that the enclosing type has *already* measured, leaving it +/// asserting a size it no longer has. +fn register_generated( + semantic: &mut ResolutionContext<'_>, + owner: &ItemPath, + generated: Vec, +) -> Result<()> { + for item in generated { + if semantic.type_registry.lookup(&item.path).is_some() { + return Err(SemanticError::InlineUnionNameCollision { + generated_path: item.path.clone(), + item_path: owner.clone(), + location: item.location, + }); + } + semantic.add_item(item)?; + } + Ok(()) +} + +/// A `pub name: union { … }` field awaiting desugaring, collected during a type +/// body walk (which holds the resolution context immutably) and processed by the +/// caller once it can register items. +pub struct InlineUnionRequest<'a> { + pub path: ItemPath, + pub visibility: Visibility, + pub attributes: &'a grammar::Attributes, + pub body: &'a grammar::UnionDefinition, + pub doc: Vec, + pub location: ItemLocation, +} + +impl InlineUnionRequest<'_> { + /// The generated item path for an inline union field on `parent_path`: + /// a module-scope sibling, not a nested item — see [`inline_union_name`]. + pub fn path_for(parent_path: &ItemPath, field_name: &str) -> Option { + let parent_name = parent_path.last()?; + Some( + parent_path + .parent()? + .join(inline_union_name(parent_name.as_str(), field_name).into()), + ) + } +} + +/// Build and register every inline union collected from a type body. +/// +/// Returns `None` if any of them deferred, in which case nothing is registered +/// and the enclosing item should defer too. +pub fn build_inline_union( + semantic: &mut ResolutionContext<'_>, + owner: &ItemPath, + requests: &[InlineUnionRequest<'_>], + type_parameters: &[String], +) -> Result> { + let mut built = Vec::new(); + for request in requests { + let mut generated = Vec::new(); + let outcome = build_state( + semantic, + &request.path, + request.attributes, + request.body, + &request.location, + &request.doc, + type_parameters, + Nesting::Inline, + &mut generated, + )?; + let BuildOutcome::Resolved(state) = outcome else { + return Ok(None); + }; + built.extend(generated); + built.push(ItemDefinition { + visibility: request.visibility, + path: request.path.clone(), + type_parameters: vec![], + state: ItemState::Resolved(state), + category: ItemCategory::Defined, + predefined: None, + cfg: request.attributes.cfg(), + location: request.location, + declaration_location: request.location, + }); + } + + // Two fields in one body can PascalCase to the same generated name + // (`a_b` and `ab` both give `AB`), so check the batch against itself as well + // as against the registry. + let mut claimed: Vec<&ItemPath> = Vec::new(); + for item in &built { + if claimed.contains(&&item.path) { + return Err(SemanticError::InlineUnionNameCollision { + generated_path: item.path.clone(), + item_path: owner.clone(), + location: item.location, + }); + } + claimed.push(&item.path); + } + + register_generated(semantic, owner, built)?; + Ok(Some(())) +} + +/// The layout-bearing content collected from a union body. +struct UnionBody { + regions: Vec, + nested_item_paths: Vec, +} + +/// Compute a union's resolved state. +/// +/// `generated` accumulates the items for any inline unions declared inside this +/// body (at any depth). They are registered by the caller rather than here so +/// that a deferral doesn't leave partially-built items behind. +#[allow(clippy::too_many_arguments)] +fn build_state( + semantic: &ResolutionContext<'_>, + resolvee_path: &ItemPath, + attributes: &grammar::Attributes, + definition: &grammar::UnionDefinition, + location: &ItemLocation, + doc_comments: &[String], + type_parameters: &[String], + nesting: Nesting, + generated: &mut Vec, +) -> Result { + let mut parsed = parse_type_attributes(attributes)?; + + if parsed.target_size.is_some() && parsed.min_size.is_some() { + return Err(SemanticError::ConflictingAttributes { + attr1: AttributeName::Size, + attr2: AttributeName::MinSize, + item_path: resolvee_path.clone(), + location: *location, + }); + } + + let Some(mut body) = build_body( + semantic, + resolvee_path, + definition, + type_parameters, + nesting, + generated, + )? + else { + return Ok(BuildOutcome::Deferred); + }; + + if body.regions.is_empty() { + return Err(SemanticError::EmptyUnion { + item_path: resolvee_path.clone(), + location: *location, + }); + } + + let alignment = resolve_alignment(semantic, resolvee_path, location, &body.regions, &parsed)?; + let Some((size, largest_member)) = resolve_size( + semantic, + resolvee_path, + location, + &body.regions, + &parsed, + alignment, + generated, + )? + else { + return Ok(BuildOutcome::Deferred); + }; + + // `#[size]`/`#[min_size]` can ask for more room than any member needs. A + // type pads at the tail; a union has no tail, so the padding is another + // whole-width reading of the same bytes. Without it the backends would emit + // a union that is `largest_member` bytes wide alongside a size assertion + // demanding `size`, and fail to compile. + // + // Rounding up to the alignment needs no help — both Rust and C++ do that + // for a `union` themselves — so only an explicitly requested excess counts. + if size > round_up(largest_member, alignment) { + body.regions.push(Region { + visibility: Visibility::Private, + name: Some("_padding".to_string()), + doc: vec![], + type_ref: semantic.type_registry.padding_type(size), + is_base: false, + location: *location, + }); + } + + // `#[singleton]` is meaningless on a union; drop it so it can't leak into + // the shared trait-constraint checks with any effect. + parsed.singleton = None; + check_trait_constraints(semantic, resolvee_path, &body.regions, &parsed)?; + + Ok(BuildOutcome::Resolved(ItemStateResolved { + size, + alignment, + inner: UnionDefinition { + regions: body.regions, + doc: doc_comments.to_vec(), + copyable: parsed.copyable, + cloneable: parsed.cloneable, + defaultable: parsed.defaultable, + packed: parsed.packed, + pinned: parsed.pinned, + nested_item_paths: body.nested_item_paths, + } + .into(), + })) +} + +/// Walk a union body, resolving member types and rejecting the constructs a +/// union cannot express. Returns `None` to defer. +fn build_body( + semantic: &ResolutionContext<'_>, + resolvee_path: &ItemPath, + definition: &grammar::UnionDefinition, + type_parameters: &[String], + nesting: Nesting, + generated: &mut Vec, +) -> Result> { + let mut regions: Vec = vec![]; + let mut nested_item_paths: Vec = Vec::new(); + + for statement in definition.statements() { + let grammar::TypeStatement { + field, + attributes, + doc_comments, + .. + } = statement; + + match field { + grammar::TypeField::Field(visibility, field_ident, type_) => { + check_member( + attributes, + resolvee_path, + &field_ident.0, + &statement.location, + )?; + + let module = semantic.get_module_for_path(resolvee_path, &statement.location)?; + let scope: Vec = std::iter::once(resolvee_path.clone()) + .chain(module.scope()) + .collect(); + let type_ = match semantic.type_registry.resolve_grammar_type( + &scope, + type_, + type_parameters, + ) { + TypeLookupResult::Found(t) => t, + TypeLookupResult::NotYetResolved => return Ok(None), + TypeLookupResult::NotFound { type_name } => { + return Err(unresolved(type_name, type_, field_ident, resolvee_path)); + } + TypeLookupResult::PrivateAccess { item_path } => { + return Err(unresolved( + item_path.to_string(), + type_, + field_ident, + resolvee_path, + )); + } + }; + + regions.push(Region { + visibility: (*visibility).into(), + name: Some(field_ident.0.clone()), + doc: doc_comments.to_vec(), + type_ref: type_, + is_base: false, + location: statement.location, + }); + } + grammar::TypeField::UnionField { + visibility, + name, + body, + } => { + check_member(attributes, resolvee_path, &name.0, &statement.location)?; + + let Some(path) = InlineUnionRequest::path_for(resolvee_path, name.as_str()) else { + return Err(SemanticError::ModuleNotFound { + path: resolvee_path.clone(), + location: statement.location, + }); + }; + + // Depth-first: the child is fully built before the parent needs + // its size, and `resolve_size` finds it in `generated`. + let outcome = build_state( + semantic, + &path, + attributes, + body, + &statement.location, + doc_comments, + type_parameters, + Nesting::Inline, + generated, + )?; + let BuildOutcome::Resolved(state) = outcome else { + return Ok(None); + }; + generated.push(ItemDefinition { + visibility: (*visibility).into(), + path: path.clone(), + type_parameters: vec![], + state: ItemState::Resolved(state), + category: ItemCategory::Defined, + predefined: None, + cfg: attributes.cfg(), + location: statement.location, + declaration_location: statement.location, + }); + + regions.push(Region { + visibility: (*visibility).into(), + name: Some(name.0.clone()), + doc: doc_comments.to_vec(), + type_ref: Type::Raw(path), + is_base: false, + location: statement.location, + }); + } + grammar::TypeField::Vftable(_) => { + return Err(SemanticError::UnionVftableNotAllowed { + item_path: resolvee_path.clone(), + location: statement.location, + }); + } + grammar::TypeField::Item(inner_def) => { + // A named union is walked by the grammar passes that populate + // `item_scopes`, so its nested items get real paths. An inline + // union's item is synthesised after those passes have run, so a + // nested item under it would be registered nowhere and reach no + // backend — reject it rather than drop it silently. + if nesting == Nesting::Inline { + return Err(SemanticError::InlineUnionNestedItem { + item_name: inner_def.name.as_str().to_string(), + item_path: resolvee_path.clone(), + location: statement.location, + }); + } + nested_item_paths.push(resolvee_path.join(inner_def.name.as_str().into())); + } + } + } + + Ok(Some(UnionBody { + regions, + nested_item_paths, + })) +} + +/// Reject what a union member can't be: named `_` (padding, which a union has no +/// room for), `#[base]` (a base must sit at a known offset), or `#[address]` +/// (every member starts at offset 0). +fn check_member( + attributes: &grammar::Attributes, + resolvee_path: &ItemPath, + member_name: &str, + location: &ItemLocation, +) -> Result<()> { + if member_name == "_" { + return Err(SemanticError::UnionAnonymousMember { + item_path: resolvee_path.clone(), + location: *location, + }); + } + + for attribute in attributes { + match attribute { + grammar::Attribute::Ident { ident, .. } if ident.as_str() == "base" => { + return Err(SemanticError::UnionBaseNotAllowed { + item_path: resolvee_path.clone(), + location: *location, + }); + } + grammar::Attribute::Function { name, items, .. } + if attribute::parse_address(name, items, attribute.location())?.is_some() => + { + return Err(SemanticError::UnionMemberAddress { + member_name: member_name.to_string(), + item_path: resolvee_path.clone(), + location: *location, + }); + } + _ => {} + } + } + Ok(()) +} + +fn unresolved( + type_name: String, + type_: &grammar::Type, + field_ident: &grammar::Ident, + resolvee_path: &ItemPath, +) -> SemanticError { + let field_name = if field_ident.0 == "_" { + "".to_string() + } else { + field_ident.0.clone() + }; + SemanticError::TypeResolutionStalled { + unresolved_types: vec![resolvee_path.to_string()], + resolved_types: vec![], + unresolved_references: vec![UnresolvedTypeReference { + type_name, + location: *type_.location(), + context: UnresolvedTypeContext::StructField { + field_name, + type_path: resolvee_path.clone(), + }, + }], + } +} + +/// A union's alignment is the strictest of its members', unless `#[align]` asks +/// for more or `#[packed]` asks for none. +/// +/// Note the default differs from a type's: a type with no explicit alignment +/// falls back to the pointer size, but a union of two `u8`s is genuinely +/// 1-aligned, and forcing it wider would inflate its size. +fn resolve_alignment( + semantic: &ResolutionContext<'_>, + resolvee_path: &ItemPath, + location: &ItemLocation, + regions: &[Region], + attributes: &TypeAttributes, +) -> Result { + if attributes.packed { + if attributes.align.is_some() { + return Err(SemanticError::ConflictingAttributes { + attr1: AttributeName::Packed, + attr2: AttributeName::Align, + item_path: resolvee_path.clone(), + location: *location, + }); + } + return Ok(1); + } + + let required_alignment = util::lcm( + regions + .iter() + .flat_map(|r| r.type_ref.alignment(semantic.type_registry)), + ) + .max(1); + + let Some(requested) = attributes.align else { + return Ok(required_alignment); + }; + + if required_alignment > requested { + return Err(SemanticError::AlignmentBelowMinimum { + alignment: requested, + required_alignment, + item_path: resolvee_path.clone(), + location: *location, + }); + } + Ok(requested) +} + +/// A union is as large as its largest member, rounded up to its alignment. +/// `#[size]` fixes the size exactly (and every member must fit); `#[min_size]` +/// raises the floor. Returns the final size alongside the largest member's size, +/// which the caller needs to decide whether to pad — or `None` to defer, if a +/// member's size isn't known yet. +#[allow(clippy::too_many_arguments)] +fn resolve_size( + semantic: &ResolutionContext<'_>, + resolvee_path: &ItemPath, + location: &ItemLocation, + regions: &[Region], + attributes: &TypeAttributes, + alignment: usize, + generated: &[ItemDefinition], +) -> Result> { + let mut largest = 0usize; + for region in regions { + let Some(size) = region_size(semantic, generated, region) else { + return Ok(None); + }; + if let Some(declared) = attributes.target_size + && size > declared + { + return Err(SemanticError::UnionMemberExceedsSize { + member_name: region.name.clone().unwrap_or_else(|| "unnamed".to_string()), + member_size: size, + declared_size: declared, + item_path: resolvee_path.clone(), + location: region.location, + }); + } + largest = largest.max(size); + } + + let size = match (attributes.target_size, attributes.min_size) { + (Some(declared), _) => declared, + (None, Some(min)) => round_up(largest.max(min), alignment), + (None, None) => round_up(largest, alignment), + }; + + if !size.is_multiple_of(alignment) { + return Err(SemanticError::SizeNotAlignmentMultiple { + size, + alignment, + item_path: resolvee_path.clone(), + location: *location, + }); + } + + Ok(Some((size, largest))) +} + +/// A region's size, consulting the not-yet-registered inline unions built during +/// this walk before falling back to the registry. +fn region_size( + semantic: &ResolutionContext<'_>, + generated: &[ItemDefinition], + region: &Region, +) -> Option { + if let Type::Raw(path) = ®ion.type_ref + && let Some(item) = generated.iter().find(|item| &item.path == path) + { + return item.size(); + } + region.size(semantic.type_registry) +} + +fn round_up(value: usize, alignment: usize) -> usize { + if alignment == 0 || value.is_multiple_of(alignment) { + value + } else { + value.div_ceil(alignment) * alignment + } +} diff --git a/src/semantic/union_definition/mod.rs b/src/semantic/union_definition/mod.rs new file mode 100644 index 00000000..13b58e23 --- /dev/null +++ b/src/semantic/union_definition/mod.rs @@ -0,0 +1,103 @@ +use crate::{grammar::ItemPath, semantic::type_definition::Region}; + +#[cfg(test)] +use crate::span::StripLocations; + +mod build; + +pub use build::{InlineUnionRequest, build, build_inline_union}; + +/// A set of competing readings of the same bytes. +/// +/// Every region starts at offset 0; the union's size is that of its largest +/// member (rounded up to its alignment) and its alignment is the strictest of +/// its members'. This is deliberately a separate struct from +/// [`crate::semantic::TypeDefinition`] rather than a flag on it: a +/// `TypeDefinition`'s `Vec` *is* its layout, with offsets implied by +/// accumulating sizes in insertion order. Several places rely on that. Giving +/// unions their own type turns every one of those places into a compile error +/// instead of a silently wrong offset. +#[derive(PartialEq, Eq, Debug, Clone, Default, Hash)] +#[cfg_attr(test, derive(StripLocations))] +pub struct UnionDefinition { + /// The union's members. All of these start at offset 0. + pub regions: Vec, + pub doc: Vec, + pub copyable: bool, + pub cloneable: bool, + pub defaultable: bool, + pub packed: bool, + pub pinned: bool, + /// Item paths of items declared inside this union body (nested items). + pub nested_item_paths: Vec, +} + +#[cfg(test)] +impl UnionDefinition { + pub fn new() -> Self { + Default::default() + } + pub fn with_regions(mut self, regions: impl IntoIterator) -> Self { + self.regions = regions.into_iter().collect(); + self + } + pub fn with_doc(mut self, doc: impl IntoIterator>) -> Self { + self.doc = doc.into_iter().map(|s| s.into()).collect(); + self + } + pub fn with_copyable(mut self, copyable: bool) -> Self { + self.copyable = copyable; + self + } + pub fn with_cloneable(mut self, cloneable: bool) -> Self { + self.cloneable = cloneable; + self + } + pub fn with_defaultable(mut self, defaultable: bool) -> Self { + self.defaultable = defaultable; + self + } + pub fn with_packed(mut self, packed: bool) -> Self { + self.packed = packed; + self + } + pub fn with_pinned(mut self, pinned: bool) -> Self { + self.pinned = pinned; + self + } + pub fn with_nested_item_paths(mut self, paths: impl IntoIterator) -> Self { + self.nested_item_paths = paths.into_iter().collect(); + self + } +} + +impl UnionDefinition { + pub fn doc(&self) -> &[String] { + &self.doc + } +} + +/// The generated item name for an inline union field: `Value` + `payload` → +/// `ValuePayloadUnion`. +/// +/// Inline unions become module-scope siblings of their parent, mirroring the +/// generated `{Name}Vftable` structs. A genuinely nested path (`Value::Payload`) +/// is not an option: `ResolutionContext::add_item` resolves an item's parent +/// directly against the module map, and `declaring_module` only knows nested +/// paths through the grammar walks that populate `item_scopes` — so a generated +/// nested item would be dropped from every module and never reach a backend. +pub fn inline_union_name(parent_name: &str, field_name: &str) -> String { + fn pascal_case(s: &str) -> String { + s.split('_') + .filter(|segment| !segment.is_empty()) + .map(|segment| { + let mut chars = segment.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } + }) + .collect() + } + format!("{parent_name}{}Union", pascal_case(field_name)) +} diff --git a/src/tokenizer/token.rs b/src/tokenizer/token.rs index 9c510825..bf7a421d 100644 --- a/src/tokenizer/token.rs +++ b/src/tokenizer/token.rs @@ -13,6 +13,7 @@ pub enum TokenKind { Type, Enum, Bitflags, + Union, Impl, Fn, Extern, @@ -75,6 +76,7 @@ pub const KEYWORDS: &[(&str, TokenKind)] = &[ ("type", TokenKind::Type), ("enum", TokenKind::Enum), ("bitflags", TokenKind::Bitflags), + ("union", TokenKind::Union), ("impl", TokenKind::Impl), ("fn", TokenKind::Fn), ("extern", TokenKind::Extern), diff --git a/tooling/lsp/src/handlers/completion.rs b/tooling/lsp/src/handlers/completion.rs index 2854dd0b..340dcca3 100644 --- a/tooling/lsp/src/handlers/completion.rs +++ b/tooling/lsp/src/handlers/completion.rs @@ -12,7 +12,7 @@ impl ServerState { // it stays inline. use TokenKind::{ Bitflags, Const, Enum, Epilogue, Extern, Fn, Impl, Mut, Prologue, Pub, SelfType, - SelfValue, Type, Use, Vftable, + SelfValue, Type, Union, Use, Vftable, }; let kw = |k: TokenKind| k.keyword_str().expect("keyword token"); let mut items: Vec = [ @@ -20,6 +20,7 @@ impl ServerState { kw(Type), kw(Enum), kw(Bitflags), + kw(Union), kw(Impl), kw(Fn), kw(Extern), diff --git a/tooling/lsp/src/handlers/doc_links.rs b/tooling/lsp/src/handlers/doc_links.rs index 6569e116..b384a26d 100644 --- a/tooling/lsp/src/handlers/doc_links.rs +++ b/tooling/lsp/src/handlers/doc_links.rs @@ -69,10 +69,16 @@ fn enclosing_in_definition( line_no: usize, ) -> Option { use pyxis::grammar::ItemDefinitionInner as IDI; + // `union` bodies reuse the `type` body AST, so both are walked the same way. + let body_statements: Vec<_> = match &definition.inner { + IDI::Type(td) => td.statements().collect(), + IDI::Union(ud) => ud.statements().collect(), + _ => Vec::new(), + }; match &definition.inner { - IDI::Type(td) => { + IDI::Type(_) | IDI::Union(_) => { // A line inside a nested item's own span belongs to that item. - for statement in td.statements() { + for statement in body_statements { if let TypeField::Item(nested) = &statement.field && location_contains_line(&nested.location, line_no) { @@ -443,8 +449,13 @@ impl ServerState { if &def_path != item { continue; } - if let ItemDefinitionInner::Type(td) = &definition.inner { - for statement in td.statements() { + let body_statements: Vec<_> = match &definition.inner { + ItemDefinitionInner::Type(td) => td.statements().collect(), + ItemDefinitionInner::Union(ud) => ud.statements().collect(), + _ => Vec::new(), + }; + { + for statement in body_statements { match &statement.field { TypeField::Vftable(fns) => { for f in fns { @@ -472,6 +483,25 @@ impl ServerState { // Nested items are handled at the top level; // their internal members are not searched here. } + TypeField::UnionField { + name: field_name, .. + } => { + // The inline union is itself a field; its + // members belong to the generated union item. + if field_name.as_str() == name { + let span = name_token_span( + tokens, + &statement.location.span.start, + name, + ) + .unwrap_or(statement.location.span); + out.push(( + uri.clone(), + span, + format!("**field** `{name}`"), + )); + } + } } } } diff --git a/tooling/lsp/src/handlers/hover_format/attributes.rs b/tooling/lsp/src/handlers/hover_format/attributes.rs index e6b55648..42c29d15 100644 --- a/tooling/lsp/src/handlers/hover_format/attributes.rs +++ b/tooling/lsp/src/handlers/hover_format/attributes.rs @@ -78,6 +78,7 @@ pub(crate) fn attribute_at<'a>( ItemDefinitionInner::Type(td) => &td.attributes, ItemDefinitionInner::Enum(e) => &e.attributes, ItemDefinitionInner::Bitflags(b) => &b.attributes, + ItemDefinitionInner::Union(u) => &u.attributes, ItemDefinitionInner::TypeAlias(ta) => &ta.attributes, ItemDefinitionInner::Constant(c) => &c.attributes, ItemDefinitionInner::ExternValue(ev) => &ev.attributes, @@ -85,21 +86,26 @@ pub(crate) fn attribute_at<'a>( if let Some(hit) = find(inner_attrs) { return Some(hit); } - match &definition.inner { - ItemDefinitionInner::Type(td) => { - for s in td.statements() { - if let Some(hit) = find(&s.attributes) { + // `union` bodies reuse the `type` body AST, so both are + // searched the same way. + let body_statements: Vec<_> = match &definition.inner { + ItemDefinitionInner::Type(td) => td.statements().collect(), + ItemDefinitionInner::Union(u) => u.statements().collect(), + _ => Vec::new(), + }; + for s in body_statements { + if let Some(hit) = find(&s.attributes) { + return Some(hit); + } + if let TypeField::Vftable(fns) = &s.field { + for f in fns { + if let Some(hit) = find(&f.attributes) { return Some(hit); } - if let TypeField::Vftable(fns) = &s.field { - for f in fns { - if let Some(hit) = find(&f.attributes) { - return Some(hit); - } - } - } } } + } + match &definition.inner { ItemDefinitionInner::Enum(e) => { for s in e.statements() { if let Some(hit) = find(&s.attributes) { @@ -114,9 +120,12 @@ pub(crate) fn attribute_at<'a>( } } } - ItemDefinitionInner::TypeAlias(_) => {} - ItemDefinitionInner::Constant(_) => {} - ItemDefinitionInner::ExternValue(_) => {} + // Type/union bodies are searched above. + ItemDefinitionInner::Type(_) + | ItemDefinitionInner::Union(_) + | ItemDefinitionInner::TypeAlias(_) + | ItemDefinitionInner::Constant(_) + | ItemDefinitionInner::ExternValue(_) => {} } } ModuleItem::Impl { impl_block } => { diff --git a/tooling/lsp/src/handlers/hover_format/types.rs b/tooling/lsp/src/handlers/hover_format/types.rs index c7192eb6..cc9dbf7d 100644 --- a/tooling/lsp/src/handlers/hover_format/types.rs +++ b/tooling/lsp/src/handlers/hover_format/types.rs @@ -26,6 +26,7 @@ pub(crate) fn format_type_hover(definition: &ItemDefinition) -> String { ItemDefinitionInner::Type(_) => "type", ItemDefinitionInner::Enum(_) => "enum", ItemDefinitionInner::Bitflags(_) => "bitflags", + ItemDefinitionInner::Union(_) => "union", ItemDefinitionInner::TypeAlias(_) => "type alias", ItemDefinitionInner::Constant(_) => "const", ItemDefinitionInner::ExternValue(_) => "extern", @@ -37,16 +38,42 @@ pub(crate) fn format_type_hover(definition: &ItemDefinition) -> String { md.push_str("\n\n"); } - if let ItemDefinitionInner::Type(td) = &definition.inner { - md.push_str("**Fields:**\n"); - for statement in td.statements() { - if let TypeField::Field(vis, name, type_) = &statement.field { - let vis_str = if matches!(vis, Visibility::Public) { + // A union's members are listed the same way, under a heading that says + // they are alternatives rather than a sequence. + let (heading, statements) = match &definition.inner { + ItemDefinitionInner::Type(td) => { + ("**Fields:**\n", Some(td.statements().collect::>())) + } + ItemDefinitionInner::Union(ud) => ( + "**Members** (all at offset 0, one applies at a time)**:**\n", + Some(ud.statements().collect::>()), + ), + _ => ("", None), + }; + if let Some(statements) = statements { + md.push_str(heading); + for statement in statements { + let vis_str = |vis: &Visibility| { + if matches!(vis, Visibility::Public) { "pub " } else { "" - }; - md.push_str(&format!("- `{}{}: {}`\n", vis_str, name, type_)); + } + }; + match &statement.field { + TypeField::Field(vis, name, type_) => { + md.push_str(&format!("- `{}{}: {}`\n", vis_str(vis), name, type_)); + } + TypeField::UnionField { + visibility, name, .. + } => { + md.push_str(&format!( + "- `{}{}: union {{ … }}`\n", + vis_str(visibility), + name + )); + } + _ => {} } } } diff --git a/tooling/lsp/src/handlers/navigation/hover.rs b/tooling/lsp/src/handlers/navigation/hover.rs index 9a33686c..649f9bf5 100644 --- a/tooling/lsp/src/handlers/navigation/hover.rs +++ b/tooling/lsp/src/handlers/navigation/hover.rs @@ -1,10 +1,7 @@ use super::*; use pyxis::{ - grammar::{ - ExternValueDefinition, FunctionBlock, Ident, ImplItem, Splice, TypeDefinition, - TypeStatement, - }, + grammar::{ExternValueDefinition, FunctionBlock, Ident, ImplItem, Splice, TypeStatement}, semantic::types::ItemDefinitionInner as ResolvedInner, }; @@ -210,7 +207,8 @@ impl ServerState { } } match &definition.inner { - ItemDefinitionInner::Type(td) => self.hover_type_body(ctx, definition, td), + ItemDefinitionInner::Type(td) => self.hover_type_body(ctx, definition, &td.items), + ItemDefinitionInner::Union(ud) => self.hover_type_body(ctx, definition, &ud.items), ItemDefinitionInner::Enum(e) => self.hover_variant( ctx, definition, @@ -276,9 +274,13 @@ impl ServerState { &self, ctx: &HoverCtx, definition: &ItemDefinition, - td: &TypeDefinition, + items: &[TypeDefItem], ) -> Option<(String, Span)> { - for statement in td.statements() { + let statements = items.iter().filter_map(|i| match i { + TypeDefItem::Statement(s) => Some(s), + _ => None, + }); + for statement in statements { if !statement.location.span.contains(&ctx.loc) { continue; } @@ -320,6 +322,13 @@ impl ServerState { // Nested items are hovered by hover_definition's recursion // (via nested_items), which runs before this body walk. } + TypeField::UnionField { body, .. } => { + // The cursor is somewhere inside `name: union { … }` — + // recurse so a member of the inline union hovers as a field. + if let Some(hit) = self.hover_type_body(ctx, definition, &body.items) { + return Some(hit); + } + } } } None diff --git a/tooling/lsp/src/handlers/navigation/mod.rs b/tooling/lsp/src/handlers/navigation/mod.rs index aa545fac..d2ee1f25 100644 --- a/tooling/lsp/src/handlers/navigation/mod.rs +++ b/tooling/lsp/src/handlers/navigation/mod.rs @@ -11,6 +11,17 @@ mod type_hierarchy; /// alongside fields/variants, not in its `statements()`. pub(crate) fn nested_items(definition: &ItemDefinition) -> Vec<&ItemDefinition> { match &definition.inner { + ItemDefinitionInner::Union(ud) => ud + .items + .iter() + .filter_map(|i| match i { + TypeDefItem::Statement(s) => match &s.field { + TypeField::Item(it) => Some(&**it), + _ => None, + }, + _ => None, + }) + .collect(), ItemDefinitionInner::Type(td) => td .items .iter() diff --git a/tooling/lsp/src/handlers/outline.rs b/tooling/lsp/src/handlers/outline.rs index cc2e7684..b65c9a67 100644 --- a/tooling/lsp/src/handlers/outline.rs +++ b/tooling/lsp/src/handlers/outline.rs @@ -471,6 +471,7 @@ pub(crate) fn module_item_to_symbol(item: &ModuleItem, source: &str) -> Option SymbolKind::STRUCT, ItemDefinitionInner::Enum(_) => SymbolKind::ENUM, ItemDefinitionInner::Bitflags(_) => SymbolKind::ENUM, + ItemDefinitionInner::Union(_) => SymbolKind::STRUCT, ItemDefinitionInner::TypeAlias(_) => SymbolKind::TYPE_PARAMETER, ItemDefinitionInner::Constant(_) => SymbolKind::CONSTANT, ItemDefinitionInner::ExternValue(_) => SymbolKind::VARIABLE, diff --git a/tooling/lsp/src/handlers/references.rs b/tooling/lsp/src/handlers/references.rs index 3fbe27ca..3530a029 100644 --- a/tooling/lsp/src/handlers/references.rs +++ b/tooling/lsp/src/handlers/references.rs @@ -189,38 +189,60 @@ pub(crate) fn find_type_ref_in_definition<'a>( ) -> Option> { use pyxis::grammar::ItemDefinitionInner; - match &definition.inner { - ItemDefinitionInner::Type(td) => { - for statement in td.statements() { - match &statement.field { - TypeField::Field(_, _, type_) => { - if let Some(found) = type_hit_at(type_, loc) { - return Some(found); - } + /// A type/union body's type references. `union` bodies reuse the `type` + /// body AST, and an inline `union { … }` field nests one inside the other. + fn body_type_hit<'a>( + statements: impl Iterator, + loc: &Location, + ) -> Option> { + for statement in statements { + match &statement.field { + TypeField::Field(_, _, type_) => { + if let Some(found) = type_hit_at(type_, loc) { + return Some(found); } - TypeField::Vftable(fns) => { - for sig in fns { - for arg in &sig.arguments { - if let Argument::Named { type_, .. } = arg - && let Some(found) = type_hit_at(type_, loc) - { - return Some(found); - } - } - if let Some(ret) = &sig.return_type - && let Some(found) = type_hit_at(ret, loc) + } + TypeField::Vftable(fns) => { + for sig in fns { + for arg in &sig.arguments { + if let Argument::Named { type_, .. } = arg + && let Some(found) = type_hit_at(type_, loc) { return Some(found); } } + if let Some(ret) = &sig.return_type + && let Some(found) = type_hit_at(ret, loc) + { + return Some(found); + } } - TypeField::Item(_) => { - // A nested item's own type references are found by the - // recursion below (via `nested_items`). + } + TypeField::Item(_) => { + // A nested item's own type references are found by the + // recursion below (via `nested_items`). + } + TypeField::UnionField { body, .. } => { + if let Some(found) = body_type_hit(body.statements(), loc) { + return Some(found); } } } } + None + } + + match &definition.inner { + ItemDefinitionInner::Type(td) => { + if let Some(found) = body_type_hit(td.statements(), loc) { + return Some(found); + } + } + ItemDefinitionInner::Union(ud) => { + if let Some(found) = body_type_hit(ud.statements(), loc) { + return Some(found); + } + } // Base type of an enum/bitflags; nested items handled by the recursion below. ItemDefinitionInner::Enum(e) => { if let Some(found) = type_hit_at(&e.type_, loc) { diff --git a/tooling/lsp/tests/snapshots.rs b/tooling/lsp/tests/snapshots.rs index fb6119be..002a9d6c 100644 --- a/tooling/lsp/tests/snapshots.rs +++ b/tooling/lsp/tests/snapshots.rs @@ -179,6 +179,10 @@ fn snapshot_completion() { "kind": 14, "label": "bitflags" }, + { + "kind": 14, + "label": "union" + }, { "kind": 14, "label": "impl" diff --git a/tooling/tree-sitter-pyxis b/tooling/tree-sitter-pyxis index e8dd7722..48eb1b7f 160000 --- a/tooling/tree-sitter-pyxis +++ b/tooling/tree-sitter-pyxis @@ -1 +1 @@ -Subproject commit e8dd772209cef6432f0cbcf6c59806d8b71a361d +Subproject commit 48eb1b7fde018dec8b791eb2c2845d26001f880c diff --git a/tooling/zed-pyxis/extension.toml b/tooling/zed-pyxis/extension.toml index 75141b3b..e9ed4745 100644 --- a/tooling/zed-pyxis/extension.toml +++ b/tooling/zed-pyxis/extension.toml @@ -14,4 +14,4 @@ repository = "https://github.com/ferrobrew/tree-sitter-pyxis" # Must be a full commit SHA, not a branch name: Zed keys its compiled-grammar # cache on this string, so a branch ref is never re-resolved once cached. Bump # it (after pushing the grammar) via `python tooling/sync-grammar.py`. -commit = "e8dd772209cef6432f0cbcf6c59806d8b71a361d" +commit = "48eb1b7fde018dec8b791eb2c2845d26001f880c" diff --git a/types/json.ts b/types/json.ts index 15efd6e0..780deb0b 100644 --- a/types/json.ts +++ b/types/json.ts @@ -356,7 +356,7 @@ source: JsonSourceLocation | null }; export type JsonItemCategory = "defined" | "predefined" | "extern"; -export type JsonItemKind = ({ type: "type" } & JsonTypeDefinition) | ({ type: "enum" } & JsonEnumDefinition) | ({ type: "bitflags" } & JsonBitflagsDefinition) | ({ type: "type_alias" } & JsonTypeAliasDefinition) | ({ type: "constant" } & JsonConstantDefinition) | ({ type: "extern_value" } & JsonExternValueDefinition); +export type JsonItemKind = ({ type: "type" } & JsonTypeDefinition) | ({ type: "enum" } & JsonEnumDefinition) | ({ type: "bitflags" } & JsonBitflagsDefinition) | ({ type: "union" } & JsonUnionDefinition) | ({ type: "type_alias" } & JsonTypeAliasDefinition) | ({ type: "constant" } & JsonConstantDefinition) | ({ type: "extern_value" } & JsonExternValueDefinition); /** * A module containing items and potentially submodules @@ -570,4 +570,51 @@ export type JsonTypeVftable = { */ functions: JsonFunction[] }; +/** + * A union: several readings of the same bytes, only one of which applies at a + * time. Which one is a property of the surrounding data, not of the union. + */ +export type JsonUnionDefinition = { +/** + * Documentation + */ +doc: string | null; doc_links?: JsonDocLink[]; +/** + * Members. Every one has `offset: 0`; `size` is the member's own size, + * which may be smaller than the union's. + */ +fields: JsonRegion[]; +/** + * Total size in bytes: the largest member, rounded up to the alignment + */ +size: number; +/** + * Alignment in bytes: the strictest of the members' + */ +alignment: number; +/** + * Whether the union is copyable + */ +copyable: boolean; +/** + * Whether the union is cloneable + */ +cloneable: boolean; +/** + * Whether the union is defaultable + */ +defaultable: boolean; +/** + * Whether the union is packed + */ +packed: boolean; +/** + * Whether the union is pinned (non-relocatable) + */ +pinned: boolean; +/** + * Item paths of nested items declared inside this union body + */ +nested_items?: string[] }; + export type JsonVisibility = "public" | "private"; \ No newline at end of file diff --git a/viewer/src/components/Attributes.tsx b/viewer/src/components/Attributes.tsx index cdf0af01..bed7acd3 100644 --- a/viewer/src/components/Attributes.tsx +++ b/viewer/src/components/Attributes.tsx @@ -115,7 +115,7 @@ export function ItemAttributes({ item, className = '' }: { item: JsonItem; class } const singleton = - kind.type !== 'type_alias' && kind.type !== 'constant' && kind.type !== 'extern_value' + kind.type === 'type' || kind.type === 'enum' || kind.type === 'bitflags' ? kind.singleton : null; if (singleton != null) { @@ -145,7 +145,7 @@ export function ItemAttributes({ item, className = '' }: { item: JsonItem; class if (kind.cloneable) primary.push(cloneable); if (kind.pinned) primary.push(pinned); } - if (kind.type === 'type') { + if (kind.type === 'type' || kind.type === 'union') { if (kind.defaultable) primary.push(defaultable); if (kind.packed) primary.push(packed); } diff --git a/viewer/src/components/FieldSourceView.tsx b/viewer/src/components/FieldSourceView.tsx index 23b65d61..ad5fd54e 100644 --- a/viewer/src/components/FieldSourceView.tsx +++ b/viewer/src/components/FieldSourceView.tsx @@ -6,26 +6,41 @@ import { formatHexAddress } from '../utils/format'; interface FieldSourceViewProps { fields: JsonRegion[]; modulePath: string; + /** + * How the fields sit in memory. `sequential` (a type body) means each field + * follows the last, so a gap has to be reconstructed as `#[address(...)]`. + * `overlaid` (a union body) means every member starts at offset 0 by + * definition — there is nothing to reconstruct, and comparing offsets + * against a running counter would stamp a bogus `#[address(0x0)]` on every + * member after the first. + */ + layout?: 'sequential' | 'overlaid'; } // Emit an `#[address(...)]` attribute only where a field doesn't sit // immediately after the previous one (i.e. where the source needs to jump), // plus `base` for base-class regions. Computed up front so render stays pure. -function computeFieldAttrs(fields: JsonRegion[]): string[][] { +function computeFieldAttrs(fields: JsonRegion[], layout: 'sequential' | 'overlaid'): string[][] { let expected = 0; return fields.map((field) => { const attrs: string[] = []; - if (field.offset !== expected) attrs.push(`address(${formatHexAddress(field.offset)})`); + if (layout === 'sequential') { + if (field.offset !== expected) attrs.push(`address(${formatHexAddress(field.offset)})`); + expected = field.offset + field.size; + } if (field.is_base) attrs.push('base'); - expected = field.offset + field.size; return attrs; }); } -// Renders the struct body the way it reads in pyxis-defs: `pub name: Type,` +// Renders the struct/union body the way it reads in pyxis-defs: `pub name: Type,` // lines, mirroring how the definitions are actually written. -export function FieldSourceView({ fields, modulePath }: FieldSourceViewProps) { - const fieldAttrs = computeFieldAttrs(fields); +export function FieldSourceView({ + fields, + modulePath, + layout = 'sequential', +}: FieldSourceViewProps) { + const fieldAttrs = computeFieldAttrs(fields, layout); return (
diff --git a/viewer/src/components/FieldTable.tsx b/viewer/src/components/FieldTable.tsx index e778e475..28398e07 100644 --- a/viewer/src/components/FieldTable.tsx +++ b/viewer/src/components/FieldTable.tsx @@ -7,15 +7,23 @@ import { Markdown } from './Markdown'; interface FieldTableProps { fields: JsonRegion[]; modulePath: string; + /** + * Whether the fields are laid out one after another. Union members are not: + * they all start at offset 0, so the offset column carries no information + * and is dropped in favour of a note that says so once. + */ + showOffsets?: boolean; } -export function FieldTable({ fields, modulePath }: FieldTableProps) { +export function FieldTable({ fields, modulePath, showOffsets = true }: FieldTableProps) { return (
- + {showOffsets && ( + + )} @@ -37,9 +45,11 @@ export function FieldTable({ fields, modulePath }: FieldTableProps) { id={field.name ? `field-${field.name}` : undefined} className="border-b border-edge" > - + {showOffsets && ( + + )}
OffsetOffsetName Type Size - 0x{field.offset.toString(16).toUpperCase()} - + 0x{field.offset.toString(16).toUpperCase()} + {field.source ? ( {field.name || ''} diff --git a/viewer/src/components/ItemView.tsx b/viewer/src/components/ItemView.tsx index c5614b19..0d95c398 100644 --- a/viewer/src/components/ItemView.tsx +++ b/viewer/src/components/ItemView.tsx @@ -22,6 +22,7 @@ import type { JsonTypeDefinition, JsonEnumDefinition, JsonBitflagsDefinition, + JsonUnionDefinition, JsonConstValue, JsonDocLink, JsonSplice, @@ -34,11 +35,32 @@ const KIND_KEYWORD: Record = { type: 'type', enum: 'enum', bitflags: 'bitflags', + union: 'union', type_alias: 'type', constant: 'const', extern_value: 'extern', }; +// Map an item kind onto the palette/label vocabulary in `utils/colors`. +function itemTypeOfKind(kind: JsonItem['kind']['type']): ItemType { + switch (kind) { + case 'enum': + return 'enum'; + case 'bitflags': + return 'bitflags'; + case 'union': + return 'union'; + case 'type_alias': + return 'type_alias'; + case 'constant': + return 'constant'; + case 'extern_value': + return 'extern'; + default: + return 'type'; + } +} + // Quiet, typographic doc block. Spacing is owned by the header group, so this // carries no margin of its own. function DocBlock({ doc, docLinks }: { doc: string; docLinks?: JsonDocLink[] }) { @@ -78,12 +100,18 @@ const FIELD_VIEW_MODES: { mode: FieldViewMode; label: string }[] = [ { mode: 'source', label: 'Source' }, ]; +// Union members aren't laid out in sequence, so the nested/absolute-offset +// view has nothing to say about them. +const UNION_VIEW_MODES = FIELD_VIEW_MODES.filter((m) => m.mode !== 'nested'); + function ViewModeToggle({ mode, onModeChange, + modes = FIELD_VIEW_MODES, }: { mode: FieldViewMode; onModeChange: (mode: FieldViewMode) => void; + modes?: { mode: FieldViewMode; label: string }[]; }) { const base = 'px-3 py-1 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent'; @@ -92,7 +120,7 @@ function ViewModeToggle({ return (
- {FIELD_VIEW_MODES.map(({ mode: m, label }) => ( + {modes.map(({ mode: m, label }) => (