Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
102 changes: 102 additions & 0 deletions codegen_tests/input/unions.pyxis
Original file line number Diff line number Diff line change
@@ -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],
}
153 changes: 153 additions & 0 deletions codegen_tests/output/cpp/include/unions.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// @generated by pyxis — do not edit
#pragma once

#include <cstdint>
#include <cstddef>
#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
1 change: 1 addition & 0 deletions codegen_tests/output/cpp/src/_all_headers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading