diff --git a/.changeset/pod_collections_and_refactor.md b/.changeset/pod_collections_and_refactor.md new file mode 100644 index 00000000..44da4dd1 --- /dev/null +++ b/.changeset/pod_collections_and_refactor.md @@ -0,0 +1,5 @@ +--- +pina_pod_primitives: minor +--- + +Add `PodOption`, `PodString`, and `PodVec` fixed-capacity collection types for zero-copy Solana account layouts. Split the monolithic `lib.rs` into a multi-file module structure for maintainability. Add kani proof harnesses for collection types. diff --git a/Cargo.toml b/Cargo.toml index dd2538b9..6ad79166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,6 +185,7 @@ level = "warn" check-cfg = [ 'cfg(target_os, values("solana"))', 'cfg(target_feature, values("static-syscalls"))', + 'cfg(kani)', ] [workspace.lints.clippy] diff --git a/changelog.md b/changelog.md index 61500a91..9418cff4 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,48 @@ All notable changes to this project will be documented in this file. +## Unreleased + +### Features + +#### Add PodOption, PodString, and PodVec collection types (pina_pod_primitives) + +Fixed-capacity, alignment-1 collection types for zero-copy Solana account layouts: + +- `PodOption` — fixed-size optional with 1-byte discriminant +- `PodString` — fixed-capacity string with length prefix +- `PodVec` — fixed-capacity vector with length prefix + +All implement `bytemuck::Pod` + `bytemuck::Zeroable`. Overflow returns `PodCollectionError`. + +New mdt providers: + +- `podCollectionTypesTable` — collection types reference table +- `podCollectionDescription` — collection type semantics + +Updated documentation: + +- `pina_pod_primitives/readme.md` — added collection types section +- `docs/src/core-concepts.md` — added Pod collection types section +- `docs/src/crates-and-features.md` — added collection types and description +- `readme.md` — added Pod collection types section +- Updated `podTypesTable` and `podArithmeticDescription` providers to clarify integer-only scope + +### Refactoring + +#### Split `pina_pod_primitives/lib.rs` into multi-file module structure + +The monolithic `lib.rs` (2322 lines) was split into focused modules: + +- `pod_bool.rs` — PodBool type +- `pod_numeric.rs` — PodU16..PodI128 via macros +- `macros.rs` — define_pod_unsigned!/define_pod_signed! etc. +- `error.rs` — PodCollectionError +- `option.rs` — PodOption + kani proofs +- `string.rs` — PodString + kani proofs +- `vec.rs` — PodVec + kani proofs +- `tests/` — unit tests per type + ## 0.8.0 (2026-03-30) ### Breaking Changes diff --git a/crates/pina_pod_primitives/readme.md b/crates/pina_pod_primitives/readme.md index b1196b68..345e1b99 100644 --- a/crates/pina_pod_primitives/readme.md +++ b/crates/pina_pod_primitives/readme.md @@ -2,17 +2,17 @@
-Alignment-safe primitive POD wrappers used by Pina and generated Codama Rust clients. +Alignment-safe primitive POD wrappers and fixed-capacity collection types used by Pina and generated Codama Rust clients. [![Crates.io][crate-image]][crate-link] [![Docs.rs][docs-image]][docs-link] [![CI][ci-status-image]][ci-status-link] [![License][unlicense-image]][unlicense-link] [![codecov][codecov-image]][codecov-link] -This crate provides `PodBool`, `PodU16`, `PodI16`, `PodU32`, `PodI32`, `PodU64`, `PodI64`, `PodU128`, and `PodI128` for use in `#[repr(C)]` zero-copy layouts. +This crate provides `PodBool`, `PodU16`, `PodI16`, `PodU32`, `PodI32`, `PodU64`, `PodI64`, `PodU128`, and `PodI128` for use in `#[repr(C)]` zero-copy layouts, plus fixed-capacity collection types `PodOption`, `PodString`, and `PodVec`. ## Arithmetic -Arithmetic operators (`+`, `-`, `*`) use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. +Arithmetic operators (`+`, `-`, `*`) on Pod **integer** types use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. @@ -26,7 +26,7 @@ Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. cargo add pina_pod_primitives ``` -## Types +## Integer types
@@ -48,6 +48,30 @@ All types are `#[repr(transparent)]` over byte arrays (or `u8` for `PodBool`) an +## Collection types + +
+ + + +| Type | Purpose | Layout | +| -------------------------- | ---------------------- | ----------------------------------------- | +| `PodOption` | Fixed-size `Option` | 1-byte discriminant + `T` | +| `PodString` | Fixed-capacity string | `PFX`-byte length prefix + `N` data bytes | +| `PodVec` | Fixed-capacity vec | `PFX`-byte length prefix + `N` elements | + +All collection types are `#[repr(C)]`, alignment-1, and implement `bytemuck::Pod` + `bytemuck::Zeroable`. Length prefixes (`PFX`) default to 1 byte for strings (max 255) and 2 bytes for vectors (max 65 535 elements). + + + + + +Collection types store data inline with a length prefix, enabling zero-copy access inside `#[repr(C)]` account structs. Overflow is detected at insertion time — `try_set` / `try_push` return `Err(PodCollectionError::Overflow)` when capacity is exceeded. + +`PodString` provides UTF-8 validation via `try_as_str()`, while `PodVec` offers slice-based access via `as_slice()` / `as_mut_slice()`. `PodOption` mirrors the `Option` API with `get()`, `set()`, and `clear()`. + + + [crate-image]: https://img.shields.io/crates/v/pina_pod_primitives.svg?style=flat-square [crate-link]: https://crates.io/crates/pina_pod_primitives [docs-image]: https://docs.rs/pina_pod_primitives/badge.svg diff --git a/crates/pina_pod_primitives/src/error.rs b/crates/pina_pod_primitives/src/error.rs new file mode 100644 index 00000000..6ebaeb1f --- /dev/null +++ b/crates/pina_pod_primitives/src/error.rs @@ -0,0 +1,35 @@ +//! Error type for Pod collection operations. + +use core::fmt; + +/// Error type for collection operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PodCollectionError { + /// Value exceeds capacity. + Overflow, + /// Invalid UTF-8 in string data. + InvalidUtf8, + /// Index out of bounds. + OutOfBounds, +} + +impl fmt::Display for PodCollectionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Overflow => write!(f, "value exceeds capacity"), + Self::InvalidUtf8 => write!(f, "invalid UTF-8"), + Self::OutOfBounds => write!(f, "index out of bounds"), + } + } +} + +/// Returns the maximum `N` value representable by a `PFX`-byte length prefix. +pub(crate) const fn max_n_for_pfx(pfx: usize) -> usize { + match pfx { + 1 => u8::MAX as usize, + 2 => u16::MAX as usize, + 4 => u32::MAX as usize, + 8 => usize::MAX, + _ => 0, + } +} diff --git a/crates/pina_pod_primitives/src/lib.rs b/crates/pina_pod_primitives/src/lib.rs index 84cf9060..aeebf25e 100644 --- a/crates/pina_pod_primitives/src/lib.rs +++ b/crates/pina_pod_primitives/src/lib.rs @@ -1,1486 +1,57 @@ #![no_std] -//! Alignment-safe primitive wrappers that can be used in `Pod` structs. +//! Alignment-safe primitive wrappers and fixed-capacity collection types +//! for use in `Pod` structs. //! -//! Pod types (`PodU64`, `PodU32`, etc.) wrap native integers in `[u8; N]` -//! arrays, guaranteeing alignment 1. This allows direct pointer casts from -//! account data without alignment concerns — critical for `#[repr(C)]` +//! Pod integer types (`PodU64`, `PodU32`, etc.) wrap native integers in +//! `[u8; N]` arrays, guaranteeing alignment 1. This allows direct pointer casts +//! from account data without alignment concerns — critical for `#[repr(C)]` //! zero-copy structs on Solana. //! //! # Arithmetic //! -//! Arithmetic operators (`+`, `-`, `*`) use **wrapping** semantics in release -//! builds for CU efficiency and **panic on overflow** in debug builds. Use -//! `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow -//! must be detected in all build profiles. +//! Arithmetic operators (`+`, `-`, `*`) on Pod **integer** types use **wrapping** +//! semantics in release builds for CU efficiency and **panic on overflow** in +//! debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, +//! `checked_div` where overflow must be detected in all build profiles. //! //! # Constants //! //! Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. - -use core::fmt; -use core::mem::align_of; -use core::mem::size_of; - -use bytemuck::Pod; -use bytemuck::Zeroable; - -/// The standard `bool` is not a `Pod`, define a replacement that is. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodBool(pub u8); - -impl PodBool { - pub const fn from_bool(b: bool) -> Self { - Self(if b { 1 } else { 0 }) - } - - /// Returns `true` if the underlying byte is a canonical boolean value - /// (`0` or `1`). - /// - /// Non-canonical values (2–255) are accepted by `bytemuck` deserialization - /// and convert to `true`, but two non-canonical `PodBool` values - /// representing the same logical boolean may fail `PartialEq` comparison. - /// Use this method to validate account data at deserialization boundaries. - pub const fn is_canonical(&self) -> bool { - self.0 == 0 || self.0 == 1 - } -} - -impl From for PodBool { - fn from(b: bool) -> Self { - Self::from_bool(b) - } -} - -impl From<&bool> for PodBool { - fn from(b: &bool) -> Self { - Self(u8::from(*b)) - } -} - -impl From<&PodBool> for bool { - fn from(b: &PodBool) -> Self { - b.0 != 0 - } -} - -impl From for bool { - fn from(b: PodBool) -> Self { - b.0 != 0 - } -} - -impl core::ops::Not for PodBool { - type Output = Self; - - #[inline] - fn not(self) -> Self { - Self::from_bool(!bool::from(self)) - } -} - -impl fmt::Display for PodBool { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - bool::from(*self).fmt(f) - } -} - -/// Implements bidirectional conversion between a `Pod*` wrapper type and its -/// corresponding standard integer. -/// -/// For a given pair `($P, $I)`, this generates: -/// - `$P::from_primitive($I) -> $P` (const) -/// - `From<$I> for $P` -/// - `From<$P> for $I` -#[macro_export] -macro_rules! impl_int_conversion { - ($P:ty, $I:ty) => { - impl $P { - pub const fn from_primitive(n: $I) -> Self { - Self(n.to_le_bytes()) - } - - /// Returns the contained native value, converting from - /// little-endian bytes. - #[inline] - pub const fn get(&self) -> $I { - <$I>::from_le_bytes(self.0) - } - } - - impl From<$I> for $P { - fn from(n: $I) -> Self { - Self::from_primitive(n) - } - } - - impl From<$P> for $I { - fn from(pod: $P) -> Self { - pod.get() - } - } - }; -} - -/// Implements constants, ordering, display, checked/saturating arithmetic, and -/// helper methods for a Pod integer type. -macro_rules! impl_pod_common { - ($name:ident, $native:ty, $size:expr) => { - impl $name { - /// The largest value representable by the underlying integer type. - pub const MAX: Self = Self(<$native>::MAX.to_le_bytes()); - /// The smallest value representable by the underlying integer type. - pub const MIN: Self = Self(<$native>::MIN.to_le_bytes()); - /// The zero value. - pub const ZERO: Self = Self([0u8; $size]); - - /// Returns `true` if the value is zero. - #[inline] - #[must_use] - pub fn is_zero(&self) -> bool { - self.0 == [0u8; $size] - } - - /// Checked addition. Returns `None` on overflow. - #[inline] - #[must_use] - pub fn checked_add(self, rhs: impl Into<$name>) -> Option { - self.get().checked_add(rhs.into().get()).map(Self::from) - } - - /// Checked subtraction. Returns `None` on underflow. - #[inline] - #[must_use] - pub fn checked_sub(self, rhs: impl Into<$name>) -> Option { - self.get().checked_sub(rhs.into().get()).map(Self::from) - } - - /// Checked multiplication. Returns `None` on overflow. - #[inline] - #[must_use] - pub fn checked_mul(self, rhs: impl Into<$name>) -> Option { - self.get().checked_mul(rhs.into().get()).map(Self::from) - } - - /// Checked division. Returns `None` if `rhs` is zero. - #[inline] - #[must_use] - pub fn checked_div(self, rhs: impl Into<$name>) -> Option { - self.get().checked_div(rhs.into().get()).map(Self::from) - } - - /// Saturating addition. Clamps at the numeric bounds instead of - /// overflowing. - #[inline] - #[must_use] - pub fn saturating_add(self, rhs: impl Into<$name>) -> Self { - Self::from(self.get().saturating_add(rhs.into().get())) - } - - /// Saturating subtraction. Clamps at the numeric bound instead of - /// underflowing. - #[inline] - #[must_use] - pub fn saturating_sub(self, rhs: impl Into<$name>) -> Self { - Self::from(self.get().saturating_sub(rhs.into().get())) - } - - /// Saturating multiplication. Clamps at the numeric bounds instead - /// of overflowing. - #[inline] - #[must_use] - pub fn saturating_mul(self, rhs: impl Into<$name>) -> Self { - Self::from(self.get().saturating_mul(rhs.into().get())) - } - } - - impl PartialOrd for $name { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - - impl Ord for $name { - #[inline] - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.get().cmp(&other.get()) - } - } - - impl PartialEq<$native> for $name { - #[inline] - fn eq(&self, other: &$native) -> bool { - self.get() == *other - } - } - - impl PartialOrd<$native> for $name { - #[inline] - fn partial_cmp(&self, other: &$native) -> Option { - self.get().partial_cmp(other) - } - } - - impl fmt::Display for $name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.get().fmt(f) - } - } - }; -} - -/// Implements arithmetic operators for a Pod type. -/// -/// In debug builds, operators panic on overflow via `checked_*`. In release -/// builds, they use `wrapping_*` for CU efficiency on Solana. -macro_rules! impl_pod_arithmetic { - ($name:ident, $native:ty) => { - // --- Pod + native --- - - impl core::ops::Add<$native> for $name { - type Output = Self; - - #[inline] - fn add(self, rhs: $native) -> Self { - #[cfg(debug_assertions)] - { - Self::from( - self.get() - .checked_add(rhs) - .unwrap_or_else(|| panic!("attempt to add with overflow")), - ) - } - #[cfg(not(debug_assertions))] - { - Self::from(self.get().wrapping_add(rhs)) - } - } - } - - impl core::ops::Sub<$native> for $name { - type Output = Self; - - #[inline] - fn sub(self, rhs: $native) -> Self { - #[cfg(debug_assertions)] - { - Self::from( - self.get() - .checked_sub(rhs) - .unwrap_or_else(|| panic!("attempt to subtract with overflow")), - ) - } - #[cfg(not(debug_assertions))] - { - Self::from(self.get().wrapping_sub(rhs)) - } - } - } - - impl core::ops::Mul<$native> for $name { - type Output = Self; - - #[inline] - fn mul(self, rhs: $native) -> Self { - #[cfg(debug_assertions)] - { - Self::from( - self.get() - .checked_mul(rhs) - .unwrap_or_else(|| panic!("attempt to multiply with overflow")), - ) - } - #[cfg(not(debug_assertions))] - { - Self::from(self.get().wrapping_mul(rhs)) - } - } - } - - impl core::ops::Div<$native> for $name { - type Output = Self; - - #[inline] - fn div(self, rhs: $native) -> Self { - Self::from(self.get() / rhs) - } - } - - impl core::ops::Rem<$native> for $name { - type Output = Self; - - #[inline] - fn rem(self, rhs: $native) -> Self { - Self::from(self.get() % rhs) - } - } - - // --- Pod + Pod --- - - impl core::ops::Add for $name { - type Output = Self; - - #[inline] - fn add(self, rhs: Self) -> Self { - self + rhs.get() - } - } - - impl core::ops::Sub for $name { - type Output = Self; - - #[inline] - fn sub(self, rhs: Self) -> Self { - self - rhs.get() - } - } - - impl core::ops::Mul for $name { - type Output = Self; - - #[inline] - fn mul(self, rhs: Self) -> Self { - self * rhs.get() - } - } - - impl core::ops::Div for $name { - type Output = Self; - - #[inline] - fn div(self, rhs: Self) -> Self { - self / rhs.get() - } - } - - impl core::ops::Rem for $name { - type Output = Self; - - #[inline] - fn rem(self, rhs: Self) -> Self { - self % rhs.get() - } - } - - // --- Assign with native --- - - impl core::ops::AddAssign<$native> for $name { - #[inline] - fn add_assign(&mut self, rhs: $native) { - *self = *self + rhs; - } - } - - impl core::ops::SubAssign<$native> for $name { - #[inline] - fn sub_assign(&mut self, rhs: $native) { - *self = *self - rhs; - } - } - - impl core::ops::MulAssign<$native> for $name { - #[inline] - fn mul_assign(&mut self, rhs: $native) { - *self = *self * rhs; - } - } - - impl core::ops::DivAssign<$native> for $name { - #[inline] - fn div_assign(&mut self, rhs: $native) { - *self = *self / rhs; - } - } - - impl core::ops::RemAssign<$native> for $name { - #[inline] - fn rem_assign(&mut self, rhs: $native) { - *self = *self % rhs; - } - } - - // --- Assign with Pod --- - - impl core::ops::AddAssign for $name { - #[inline] - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; - } - } - - impl core::ops::SubAssign for $name { - #[inline] - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; - } - } - - impl core::ops::MulAssign for $name { - #[inline] - fn mul_assign(&mut self, rhs: Self) { - *self = *self * rhs; - } - } - - impl core::ops::DivAssign for $name { - #[inline] - fn div_assign(&mut self, rhs: Self) { - *self = *self / rhs; - } - } - - impl core::ops::RemAssign for $name { - #[inline] - fn rem_assign(&mut self, rhs: Self) { - *self = *self % rhs; - } - } - - // --- Bitwise --- - - impl core::ops::BitAnd<$native> for $name { - type Output = Self; - - #[inline] - fn bitand(self, rhs: $native) -> Self { - Self::from(self.get() & rhs) - } - } - - impl core::ops::BitOr<$native> for $name { - type Output = Self; - - #[inline] - fn bitor(self, rhs: $native) -> Self { - Self::from(self.get() | rhs) - } - } - - impl core::ops::BitXor<$native> for $name { - type Output = Self; - - #[inline] - fn bitxor(self, rhs: $native) -> Self { - Self::from(self.get() ^ rhs) - } - } - - impl core::ops::BitAnd for $name { - type Output = Self; - - #[inline] - fn bitand(self, rhs: Self) -> Self { - self & rhs.get() - } - } - - impl core::ops::BitOr for $name { - type Output = Self; - - #[inline] - fn bitor(self, rhs: Self) -> Self { - self | rhs.get() - } - } - - impl core::ops::BitXor for $name { - type Output = Self; - - #[inline] - fn bitxor(self, rhs: Self) -> Self { - self ^ rhs.get() - } - } - - impl core::ops::Shl for $name { - type Output = Self; - - #[inline] - fn shl(self, rhs: u32) -> Self { - Self::from(self.get() << rhs) - } - } - - impl core::ops::Shr for $name { - type Output = Self; - - #[inline] - fn shr(self, rhs: u32) -> Self { - Self::from(self.get() >> rhs) - } - } - - impl core::ops::Not for $name { - type Output = Self; - - #[inline] - fn not(self) -> Self { - Self::from(!self.get()) - } - } - - // --- Bitwise assign with native --- - - impl core::ops::BitAndAssign<$native> for $name { - #[inline] - fn bitand_assign(&mut self, rhs: $native) { - *self = *self & rhs; - } - } - - impl core::ops::BitOrAssign<$native> for $name { - #[inline] - fn bitor_assign(&mut self, rhs: $native) { - *self = *self | rhs; - } - } - - impl core::ops::BitXorAssign<$native> for $name { - #[inline] - fn bitxor_assign(&mut self, rhs: $native) { - *self = *self ^ rhs; - } - } - - // --- Bitwise assign with Pod --- - - impl core::ops::BitAndAssign for $name { - #[inline] - fn bitand_assign(&mut self, rhs: Self) { - *self = *self & rhs; - } - } - - impl core::ops::BitOrAssign for $name { - #[inline] - fn bitor_assign(&mut self, rhs: Self) { - *self = *self | rhs; - } - } - - impl core::ops::BitXorAssign for $name { - #[inline] - fn bitxor_assign(&mut self, rhs: Self) { - *self = *self ^ rhs; - } - } - - impl core::ops::ShlAssign for $name { - #[inline] - fn shl_assign(&mut self, rhs: u32) { - *self = *self << rhs; - } - } - - impl core::ops::ShrAssign for $name { - #[inline] - fn shr_assign(&mut self, rhs: u32) { - *self = *self >> rhs; - } - } - }; -} - -/// Implements `Neg` for signed Pod types. -macro_rules! impl_pod_neg { - ($name:ident, $native:ty) => { - impl core::ops::Neg for $name { - type Output = Self; - - #[inline] - fn neg(self) -> Self { - #[cfg(debug_assertions)] - { - Self::from( - self.get() - .checked_neg() - .unwrap_or_else(|| panic!("attempt to negate with overflow")), - ) - } - #[cfg(not(debug_assertions))] - { - Self::from(self.get().wrapping_neg()) - } - } - } - }; -} - -/// Defines an unsigned Pod integer type with full operator support. -macro_rules! define_pod_unsigned { - ($name:ident, $native:ty, $size:expr, $doc:expr) => { - #[doc = $doc] - #[derive(Clone, Copy, Default, PartialEq, Eq, Pod, Zeroable)] - #[repr(transparent)] - pub struct $name(pub [u8; $size]); - - impl_int_conversion!($name, $native); - impl_pod_common!($name, $native, $size); - impl_pod_arithmetic!($name, $native); - - impl fmt::Debug for $name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}({})", stringify!($name), self.get()) - } - } - }; -} - -/// Defines a signed Pod integer type with full operator support. -macro_rules! define_pod_signed { - ($name:ident, $native:ty, $size:expr, $doc:expr) => { - #[doc = $doc] - #[derive(Clone, Copy, Default, PartialEq, Eq, Pod, Zeroable)] - #[repr(transparent)] - pub struct $name(pub [u8; $size]); - - impl_int_conversion!($name, $native); - impl_pod_common!($name, $native, $size); - impl_pod_arithmetic!($name, $native); - impl_pod_neg!($name, $native); - - impl fmt::Debug for $name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}({})", stringify!($name), self.get()) - } - } - }; -} - -define_pod_unsigned!( - PodU16, - u16, - 2, - "An alignment-1 wrapper around `u16` stored as `[u8; 2]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -define_pod_signed!( - PodI16, - i16, - 2, - "An alignment-1 wrapper around `i16` stored as `[u8; 2]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -define_pod_unsigned!( - PodU32, - u32, - 4, - "An alignment-1 wrapper around `u32` stored as `[u8; 4]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -define_pod_signed!( - PodI32, - i32, - 4, - "An alignment-1 wrapper around `i32` stored as `[u8; 4]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -define_pod_unsigned!( - PodU64, - u64, - 8, - "An alignment-1 wrapper around `u64` stored as `[u8; 8]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -define_pod_signed!( - PodI64, - i64, - 8, - "An alignment-1 wrapper around `i64` stored as `[u8; 8]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -define_pod_unsigned!( - PodU128, - u128, - 16, - "An alignment-1 wrapper around `u128` stored as `[u8; 16]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -define_pod_signed!( - PodI128, - i128, - 16, - "An alignment-1 wrapper around `i128` stored as `[u8; 16]`.\n\nEnables safe zero-copy access \ - inside `#[repr(C)]` account structs." -); - -// Compile-time invariant: all Pod types must have alignment 1 and correct -// size. These assertions guard against future changes that could break -// zero-copy access. -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 2); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 2); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 4); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 4); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 8); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 8); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 16); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 16); -const _: () = assert!(align_of::() == 1); -const _: () = assert!(size_of::() == 1); - -#[cfg(test)] -extern crate std; +//! +//! # Collection types +//! +//! `PodOption`, `PodString`, and `PodVec` are +//! fixed-capacity, alignment-1 types that store data inline with a length +//! prefix. They implement `bytemuck::Pod` + `bytemuck::Zeroable` and can be +//! embedded directly in `#[repr(C)]` account structs. Overflow is detected at +//! insertion time via `try_set` / `try_push`, which return +//! `Err(PodCollectionError::Overflow)` when capacity is exceeded. + +// Allow unsafe code for the collection types that need MaybeUninit. +// Safety is guaranteed by: +// - All types are #[repr(C)] with alignment 1 +// - MaybeUninit allows any bit pattern (satisfying Pod requirements) +// - Length prefixes prevent reading uninitialized data as initialized +#![allow(unsafe_code)] + +mod error; +mod macros; +mod option; +mod pod_bool; +mod pod_numeric; +mod string; +mod vec; #[cfg(test)] -mod tests { - use bytemuck::try_from_bytes; - - use super::*; - - // ======================================================================= - // PodBool tests - // ======================================================================= - - #[test] - fn pod_bool_roundtrip() { - for i in 0..=u8::MAX { - let value = *try_from_bytes::(&[i]).unwrap(); - assert_eq!(i != 0, bool::from(value)); - } - } - - /// Demonstrates that non-canonical PodBool values (2–255) convert to - /// `true` but fail `PartialEq` against `PodBool(1)`. Programs should - /// use `is_canonical()` to detect this at deserialization boundaries. - #[test] - fn pod_bool_non_canonical_equality_mismatch() { - let canonical_true = PodBool::from_bool(true); - let non_canonical_true = *try_from_bytes::(&[2]).unwrap(); - - // Both convert to `true`... - assert!(bool::from(canonical_true)); - assert!(bool::from(non_canonical_true)); - - // ...but fail PartialEq because the raw bytes differ. - assert_ne!(canonical_true, non_canonical_true); - - // `is_canonical` detects the non-standard encoding. - assert!(canonical_true.is_canonical()); - assert!(!non_canonical_true.is_canonical()); - } - - #[test] - fn pod_bool_is_canonical_boundary_values() { - assert!(PodBool(0).is_canonical()); - assert!(PodBool(1).is_canonical()); - assert!(!PodBool(2).is_canonical()); - assert!(!PodBool(127).is_canonical()); - assert!(!PodBool(255).is_canonical()); - } - - #[test] - fn pod_bool_from_bool_produces_canonical() { - assert!(PodBool::from_bool(false).is_canonical()); - assert!(PodBool::from_bool(true).is_canonical()); - assert!(PodBool::from(false).is_canonical()); - assert!(PodBool::from(true).is_canonical()); - } - - #[test] - fn pod_bool_from_ref() { - let t = true; - let f = false; - assert_eq!(PodBool::from(&t), PodBool(1)); - assert_eq!(PodBool::from(&f), PodBool(0)); - } - - #[test] - fn pod_bool_from_ref_roundtrip() { - let pod = PodBool(1); - assert!(bool::from(&pod)); - let pod = PodBool(0); - assert!(!bool::from(&pod)); - } - - #[test] - fn pod_bool_default_is_false() { - let default = PodBool::default(); - assert_eq!(default.0, 0); - assert!(!bool::from(default)); - assert!(default.is_canonical()); - } - - #[test] - fn pod_bool_not() { - assert_eq!(!PodBool::from_bool(true), PodBool::from_bool(false)); - assert_eq!(!PodBool::from_bool(false), PodBool::from_bool(true)); - // Non-canonical values treated as true - assert_eq!(!PodBool(42), PodBool::from_bool(false)); - } - - #[test] - fn pod_bool_display() { - assert_eq!(std::format!("{}", PodBool::from_bool(true)), "true"); - assert_eq!(std::format!("{}", PodBool::from_bool(false)), "false"); - } - - // ======================================================================= - // Conversion roundtrip tests - // ======================================================================= - - #[test] - fn pod_u16_roundtrip() { - assert_eq!(1u16, u16::from(PodU16::from_primitive(1))); - } - - #[test] - fn pod_i16_roundtrip() { - assert_eq!(-1i16, i16::from(PodI16::from_primitive(-1))); - } - - #[test] - fn pod_u32_roundtrip() { - assert_eq!(7u32, u32::from(PodU32::from_primitive(7))); - } - - #[test] - fn pod_i32_roundtrip() { - assert_eq!(-7i32, i32::from(PodI32::from_primitive(-7))); - } - - #[test] - fn pod_u64_roundtrip() { - assert_eq!(9u64, u64::from(PodU64::from_primitive(9))); - } - - #[test] - fn pod_i64_roundtrip() { - assert_eq!(-9i64, i64::from(PodI64::from_primitive(-9))); - } - - #[test] - fn pod_u128_roundtrip() { - assert_eq!(11u128, u128::from(PodU128::from_primitive(11))); - } - - #[test] - fn pod_i128_roundtrip() { - assert_eq!(-11i128, i128::from(PodI128::from_primitive(-11))); - } - - // ======================================================================= - // Boundary value tests - // ======================================================================= - - #[test] - fn pod_u16_boundary_values() { - assert_eq!(0u16, u16::from(PodU16::from_primitive(0))); - assert_eq!(u16::MAX, u16::from(PodU16::from_primitive(u16::MAX))); - } - - #[test] - fn pod_i16_boundary_values() { - assert_eq!(i16::MIN, i16::from(PodI16::from_primitive(i16::MIN))); - assert_eq!(i16::MAX, i16::from(PodI16::from_primitive(i16::MAX))); - assert_eq!(0i16, i16::from(PodI16::from_primitive(0))); - } - - #[test] - fn pod_u32_boundary_values() { - assert_eq!(0u32, u32::from(PodU32::from_primitive(0))); - assert_eq!(u32::MAX, u32::from(PodU32::from_primitive(u32::MAX))); - } - - #[test] - fn pod_i32_boundary_values() { - assert_eq!(i32::MIN, i32::from(PodI32::from_primitive(i32::MIN))); - assert_eq!(i32::MAX, i32::from(PodI32::from_primitive(i32::MAX))); - } - - #[test] - fn pod_u64_boundary_values() { - assert_eq!(0u64, u64::from(PodU64::from_primitive(0))); - assert_eq!(u64::MAX, u64::from(PodU64::from_primitive(u64::MAX))); - } - - #[test] - fn pod_i64_boundary_values() { - assert_eq!(i64::MIN, i64::from(PodI64::from_primitive(i64::MIN))); - assert_eq!(i64::MAX, i64::from(PodI64::from_primitive(i64::MAX))); - } - - #[test] - fn pod_u128_boundary_values() { - assert_eq!(0u128, u128::from(PodU128::from_primitive(0))); - assert_eq!(u128::MAX, u128::from(PodU128::from_primitive(u128::MAX))); - } - - #[test] - fn pod_i128_boundary_values() { - assert_eq!(i128::MIN, i128::from(PodI128::from_primitive(i128::MIN))); - assert_eq!(i128::MAX, i128::from(PodI128::from_primitive(i128::MAX))); - } - - /// Verify that all Pod types store bytes in little-endian order, which - /// is the native byte order on Solana's BPF/SBF target. - #[test] - fn pod_types_use_little_endian_byte_order() { - let u16_val = PodU16::from_primitive(0x0102); - assert_eq!(u16_val.0, [0x02, 0x01]); - - let u32_val = PodU32::from_primitive(0x01020304); - assert_eq!(u32_val.0, [0x04, 0x03, 0x02, 0x01]); - - let u64_val = PodU64::from_primitive(0x0102030405060708); - assert_eq!(u64_val.0, [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]); - } - - /// Verify that bytemuck deserialization of Pod types works correctly - /// from raw byte slices, simulating zero-copy account data access. - #[test] - fn pod_types_bytemuck_from_bytes() { - let bytes_u16 = [0x39, 0x05]; // 0x0539 = 1337 - let val = try_from_bytes::(&bytes_u16).unwrap(); - assert_eq!(u16::from(*val), 1337); - - let bytes_u32 = [0xEF, 0xBE, 0xAD, 0xDE]; // 0xDEADBEEF - let val = try_from_bytes::(&bytes_u32).unwrap(); - assert_eq!(u32::from(*val), 0xDEAD_BEEF); - - let bytes_i16 = [0xFF, 0xFF]; // -1 in two's complement LE - let val = try_from_bytes::(&bytes_i16).unwrap(); - assert_eq!(i16::from(*val), -1); - } - - #[test] - fn pod_default_is_zero() { - assert_eq!(u16::from(PodU16::default()), 0); - assert_eq!(i16::from(PodI16::default()), 0); - assert_eq!(u32::from(PodU32::default()), 0); - assert_eq!(i32::from(PodI32::default()), 0); - assert_eq!(u64::from(PodU64::default()), 0); - assert_eq!(i64::from(PodI64::default()), 0); - assert_eq!(u128::from(PodU128::default()), 0); - assert_eq!(i128::from(PodI128::default()), 0); - } - - // ======================================================================= - // Constants tests - // ======================================================================= - - #[test] - fn pod_constants_zero() { - assert!(PodU16::ZERO.is_zero()); - assert!(PodU32::ZERO.is_zero()); - assert!(PodU64::ZERO.is_zero()); - assert!(PodU128::ZERO.is_zero()); - assert!(PodI16::ZERO.is_zero()); - assert!(PodI32::ZERO.is_zero()); - assert!(PodI64::ZERO.is_zero()); - assert!(PodI128::ZERO.is_zero()); - } - - #[test] - fn pod_constants_min_max() { - assert_eq!(PodU16::MIN.get(), u16::MIN); - assert_eq!(PodU16::MAX.get(), u16::MAX); - assert_eq!(PodU32::MIN.get(), u32::MIN); - assert_eq!(PodU32::MAX.get(), u32::MAX); - assert_eq!(PodU64::MIN.get(), u64::MIN); - assert_eq!(PodU64::MAX.get(), u64::MAX); - assert_eq!(PodU128::MIN.get(), u128::MIN); - assert_eq!(PodU128::MAX.get(), u128::MAX); - assert_eq!(PodI16::MIN.get(), i16::MIN); - assert_eq!(PodI16::MAX.get(), i16::MAX); - assert_eq!(PodI32::MIN.get(), i32::MIN); - assert_eq!(PodI32::MAX.get(), i32::MAX); - assert_eq!(PodI64::MIN.get(), i64::MIN); - assert_eq!(PodI64::MAX.get(), i64::MAX); - assert_eq!(PodI128::MIN.get(), i128::MIN); - assert_eq!(PodI128::MAX.get(), i128::MAX); - } - - #[test] - fn pod_is_zero_false_for_nonzero() { - assert!(!PodU64::from_primitive(1).is_zero()); - assert!(!PodI64::from_primitive(-1).is_zero()); - assert!(!PodU128::MAX.is_zero()); - } - - // ======================================================================= - // Arithmetic tests (Add, Sub, Mul, Div, Rem) - // ======================================================================= - - #[test] - fn pod_add_native() { - assert_eq!((PodU64::from(10u64) + 5u64).get(), 15); - assert_eq!((PodI32::from(10i32) + 5i32).get(), 15); - assert_eq!((PodI32::from(-10i32) + 5i32).get(), -5); - } - - #[test] - fn pod_add_pod() { - let a = PodU64::from(10u64); - let b = PodU64::from(20u64); - assert_eq!((a + b).get(), 30); - } - - #[test] - fn pod_sub_native() { - assert_eq!((PodU64::from(10u64) - 5u64).get(), 5); - assert_eq!((PodI32::from(-10i32) - 5i32).get(), -15); - } - - #[test] - fn pod_sub_pod() { - let a = PodU64::from(20u64); - let b = PodU64::from(5u64); - assert_eq!((a - b).get(), 15); - } - - #[test] - fn pod_mul_native() { - assert_eq!((PodU64::from(6u64) * 7u64).get(), 42); - assert_eq!((PodI32::from(-3i32) * 4i32).get(), -12); - } - - #[test] - fn pod_mul_pod() { - let a = PodU32::from(6u32); - let b = PodU32::from(7u32); - assert_eq!((a * b).get(), 42); - } - - #[test] - fn pod_div_native() { - assert_eq!((PodU64::from(42u64) / 7u64).get(), 6); - assert_eq!((PodI32::from(-12i32) / 4i32).get(), -3); - } - - #[test] - fn pod_div_pod() { - let a = PodU64::from(42u64); - let b = PodU64::from(7u64); - assert_eq!((a / b).get(), 6); - } - - #[test] - fn pod_rem_native() { - assert_eq!((PodU64::from(10u64) % 3u64).get(), 1); - assert_eq!((PodI32::from(-10i32) % 3i32).get(), -1); - } - - #[test] - fn pod_rem_pod() { - let a = PodU64::from(10u64); - let b = PodU64::from(3u64); - assert_eq!((a % b).get(), 1); - } - - // ======================================================================= - // Assign operators - // ======================================================================= - - #[test] - fn pod_add_assign_native() { - let mut v = PodU64::from(10u64); - v += 5u64; - assert_eq!(v.get(), 15); - } - - #[test] - fn pod_add_assign_pod() { - let mut v = PodU64::from(10u64); - v += PodU64::from(5u64); - assert_eq!(v.get(), 15); - } - - #[test] - fn pod_sub_assign_native() { - let mut v = PodU64::from(10u64); - v -= 3u64; - assert_eq!(v.get(), 7); - } - - #[test] - fn pod_sub_assign_pod() { - let mut v = PodU64::from(10u64); - v -= PodU64::from(3u64); - assert_eq!(v.get(), 7); - } - - #[test] - fn pod_mul_assign_native() { - let mut v = PodU32::from(5u32); - v *= 4u32; - assert_eq!(v.get(), 20); - } - - #[test] - fn pod_mul_assign_pod() { - let mut v = PodU32::from(5u32); - v *= PodU32::from(4u32); - assert_eq!(v.get(), 20); - } - - #[test] - fn pod_div_assign_native() { - let mut v = PodU64::from(20u64); - v /= 5u64; - assert_eq!(v.get(), 4); - } - - #[test] - fn pod_div_assign_pod() { - let mut v = PodU64::from(20u64); - v /= PodU64::from(5u64); - assert_eq!(v.get(), 4); - } - - #[test] - fn pod_rem_assign_native() { - let mut v = PodU64::from(10u64); - v %= 3u64; - assert_eq!(v.get(), 1); - } - - #[test] - fn pod_rem_assign_pod() { - let mut v = PodU64::from(10u64); - v %= PodU64::from(3u64); - assert_eq!(v.get(), 1); - } - - // ======================================================================= - // Bitwise tests - // ======================================================================= - - #[test] - fn pod_bitand_native() { - assert_eq!((PodU32::from(0xFF00u32) & 0x0FF0u32).get(), 0x0F00); - } - - #[test] - fn pod_bitand_pod() { - let a = PodU32::from(0xFF00u32); - let b = PodU32::from(0x0FF0u32); - assert_eq!((a & b).get(), 0x0F00); - } - - #[test] - fn pod_bitor_native() { - assert_eq!((PodU32::from(0xFF00u32) | 0x00FFu32).get(), 0xFFFF); - } - - #[test] - fn pod_bitor_pod() { - let a = PodU32::from(0xFF00u32); - let b = PodU32::from(0x00FFu32); - assert_eq!((a | b).get(), 0xFFFF); - } - - #[test] - fn pod_bitxor_native() { - assert_eq!((PodU32::from(0xFFFFu32) ^ 0xFF00u32).get(), 0x00FF); - } - - #[test] - fn pod_bitxor_pod() { - let a = PodU32::from(0xFFFFu32); - let b = PodU32::from(0xFF00u32); - assert_eq!((a ^ b).get(), 0x00FF); - } - - #[test] - fn pod_shl() { - assert_eq!((PodU32::from(1u32) << 4).get(), 16); - } - - #[test] - fn pod_shr() { - assert_eq!((PodU32::from(16u32) >> 4).get(), 1); - } - - #[test] - fn pod_not() { - assert_eq!((!PodU16::from(0u16)).get(), u16::MAX); - assert_eq!((!PodI16::from(0i16)).get(), -1i16); - } - - // --- Bitwise assign --- - - #[test] - fn pod_bitand_assign_native() { - let mut v = PodU32::from(0xFF00u32); - v &= 0x0FF0u32; - assert_eq!(v.get(), 0x0F00); - } - - #[test] - fn pod_bitand_assign_pod() { - let mut v = PodU32::from(0xFF00u32); - v &= PodU32::from(0x0FF0u32); - assert_eq!(v.get(), 0x0F00); - } - - #[test] - fn pod_bitor_assign_native() { - let mut v = PodU32::from(0xFF00u32); - v |= 0x00FFu32; - assert_eq!(v.get(), 0xFFFF); - } - - #[test] - fn pod_bitor_assign_pod() { - let mut v = PodU32::from(0xFF00u32); - v |= PodU32::from(0x00FFu32); - assert_eq!(v.get(), 0xFFFF); - } - - #[test] - fn pod_bitxor_assign_native() { - let mut v = PodU32::from(0xFFFFu32); - v ^= 0xFF00u32; - assert_eq!(v.get(), 0x00FF); - } - - #[test] - fn pod_bitxor_assign_pod() { - let mut v = PodU32::from(0xFFFFu32); - v ^= PodU32::from(0xFF00u32); - assert_eq!(v.get(), 0x00FF); - } - - #[test] - fn pod_shl_assign() { - let mut v = PodU32::from(1u32); - v <<= 4; - assert_eq!(v.get(), 16); - } - - #[test] - fn pod_shr_assign() { - let mut v = PodU32::from(16u32); - v >>= 4; - assert_eq!(v.get(), 1); - } - - // ======================================================================= - // Neg for signed types - // ======================================================================= - - #[test] - fn pod_neg_i16() { - assert_eq!((-PodI16::from(5i16)).get(), -5); - assert_eq!((-PodI16::from(-5i16)).get(), 5); - assert_eq!((-PodI16::from(0i16)).get(), 0); - } - - #[test] - fn pod_neg_i32() { - assert_eq!((-PodI32::from(42i32)).get(), -42); - } - - #[test] - fn pod_neg_i64() { - assert_eq!((-PodI64::from(100i64)).get(), -100); - } - - #[test] - fn pod_neg_i128() { - assert_eq!((-PodI128::from(999i128)).get(), -999); - } - - // ======================================================================= - // Checked arithmetic - // ======================================================================= - - #[test] - fn pod_checked_add_ok() { - assert_eq!( - PodU64::from(10u64).checked_add(5u64), - Some(PodU64::from(15u64)) - ); - } - - #[test] - fn pod_checked_add_overflow() { - assert_eq!(PodU64::MAX.checked_add(1u64), None); - } - - #[test] - fn pod_checked_add_pod() { - assert_eq!( - PodU32::from(10u32).checked_add(PodU32::from(5u32)), - Some(PodU32::from(15u32)) - ); - } - - #[test] - fn pod_checked_sub_ok() { - assert_eq!( - PodU64::from(10u64).checked_sub(5u64), - Some(PodU64::from(5u64)) - ); - } - - #[test] - fn pod_checked_sub_underflow() { - assert_eq!(PodU64::from(5u64).checked_sub(10u64), None); - } - - #[test] - fn pod_checked_mul_ok() { - assert_eq!( - PodU64::from(6u64).checked_mul(7u64), - Some(PodU64::from(42u64)) - ); - } - - #[test] - fn pod_checked_mul_overflow() { - assert_eq!(PodU64::MAX.checked_mul(2u64), None); - } - - #[test] - fn pod_checked_div_ok() { - assert_eq!( - PodU64::from(42u64).checked_div(7u64), - Some(PodU64::from(6u64)) - ); - } - - #[test] - fn pod_checked_div_by_zero() { - assert_eq!(PodU64::from(42u64).checked_div(0u64), None); - } - - #[test] - fn pod_checked_signed_overflow() { - assert_eq!(PodI64::MIN.checked_sub(1i64), None); - assert_eq!(PodI64::MAX.checked_add(1i64), None); - } - - // ======================================================================= - // Saturating arithmetic - // ======================================================================= - - #[test] - fn pod_saturating_add() { - assert_eq!(PodU64::MAX.saturating_add(100u64), PodU64::MAX); - assert_eq!( - PodU64::from(10u64).saturating_add(5u64), - PodU64::from(15u64) - ); - } - - #[test] - fn pod_saturating_sub() { - assert_eq!(PodU64::from(5u64).saturating_sub(10u64), PodU64::ZERO); - assert_eq!(PodU64::from(10u64).saturating_sub(5u64), PodU64::from(5u64)); - } - - #[test] - fn pod_saturating_mul() { - assert_eq!(PodU64::MAX.saturating_mul(2u64), PodU64::MAX); - assert_eq!(PodU64::from(6u64).saturating_mul(7u64), PodU64::from(42u64)); - } - - #[test] - fn pod_saturating_signed() { - assert_eq!(PodI64::MAX.saturating_add(100i64), PodI64::MAX); - assert_eq!(PodI64::MIN.saturating_sub(100i64), PodI64::MIN); - assert_eq!(PodI64::MAX.saturating_mul(2i64), PodI64::MAX); - assert_eq!(PodI64::MIN.saturating_mul(2i64), PodI64::MIN); - } - - // ======================================================================= - // Ordering tests - // ======================================================================= - - #[test] - fn pod_ordering() { - assert!(PodU64::from(10u64) > PodU64::from(5u64)); - assert!(PodU64::from(5u64) < PodU64::from(10u64)); - assert!(PodU64::from(5u64) == PodU64::from(5u64)); - - assert!(PodI64::from(-10i64) < PodI64::from(5i64)); - assert!(PodI64::from(5i64) > PodI64::from(-10i64)); - } - - #[test] - fn pod_partial_eq_native() { - assert!(PodU64::from(42u64) == 42u64); - assert!(PodI32::from(-5i32) == -5i32); - assert!(PodU64::from(42u64) != 43u64); - } - - #[test] - fn pod_partial_ord_native() { - assert!(PodU64::from(10u64) > 5u64); - assert!(PodU64::from(5u64) < 10u64); - assert!(PodI32::from(-10i32) < 0i32); - } - - // ======================================================================= - // Display / Debug tests - // ======================================================================= - - #[test] - fn pod_display() { - assert_eq!(std::format!("{}", PodU64::from(42u64)), "42"); - assert_eq!(std::format!("{}", PodI32::from(-7i32)), "-7"); - assert_eq!(std::format!("{}", PodU128::from(0u128)), "0"); - } - - #[test] - fn pod_debug() { - assert_eq!(std::format!("{:?}", PodU64::from(42u64)), "PodU64(42)"); - assert_eq!(std::format!("{:?}", PodI32::from(-7i32)), "PodI32(-7)"); - } - - // ======================================================================= - // Get method tests - // ======================================================================= - - #[test] - fn pod_get_method() { - assert_eq!(PodU16::from(1337u16).get(), 1337); - assert_eq!(PodI16::from(-42i16).get(), -42); - assert_eq!(PodU32::from(0xDEAD_BEEFu32).get(), 0xDEAD_BEEF); - assert_eq!(PodI32::from(i32::MIN).get(), i32::MIN); - assert_eq!(PodU64::from(u64::MAX).get(), u64::MAX); - assert_eq!(PodI64::from(i64::MAX).get(), i64::MAX); - assert_eq!(PodU128::from(u128::MAX).get(), u128::MAX); - assert_eq!(PodI128::from(i128::MIN).get(), i128::MIN); - } - - // ======================================================================= - // Ergonomic usage pattern: counter increment (the motivating use case) - // ======================================================================= - - #[test] - fn ergonomic_counter_increment() { - // Simulates struct field usage: my_account.count += 1; - let mut count = PodU64::from(0u64); - count += 1u64; - assert_eq!(count.get(), 1); - count += 1u64; - assert_eq!(count.get(), 2); - } - - #[test] - fn ergonomic_balance_arithmetic() { - let mut balance = PodU64::from(1000u64); - let fee = PodU64::from(25u64); - balance -= fee; - assert_eq!(balance.get(), 975); - } -} +mod tests; + +pub use error::PodCollectionError; +pub use option::PodOption; +pub use pod_bool::PodBool; +// Numeric types are defined via macros in the `numeric` module and re-exported +// here for the public API. The macros themselves are `#[macro_export]` so they +// are available at the crate root. +pub use pod_numeric::{PodI16, PodI32, PodI64, PodI128, PodU16, PodU32, PodU64, PodU128}; +pub use string::PodString; +pub use vec::PodVec; diff --git a/crates/pina_pod_primitives/src/macros.rs b/crates/pina_pod_primitives/src/macros.rs new file mode 100644 index 00000000..f08a88d4 --- /dev/null +++ b/crates/pina_pod_primitives/src/macros.rs @@ -0,0 +1,572 @@ +//! Internal macros for Pod integer type code generation. +//! +//! These macros are `#[macro_export]` so they are available at the crate root +//! for use by other modules (e.g. `numeric.rs`). + +/// Implements bidirectional conversion between a `Pod*` wrapper type and its +/// corresponding standard integer. +/// +/// For a given pair `($P, $I)`, this generates: +/// - `$P::from_primitive($I) -> $P` (const) +/// - `From<$I> for $P` +/// - `From<$P> for $I` +#[macro_export] +macro_rules! impl_int_conversion { + ($P:ty, $I:ty) => { + impl $P { + pub const fn from_primitive(n: $I) -> Self { + Self(n.to_le_bytes()) + } + + /// Returns the contained native value, converting from + /// little-endian bytes. + #[inline] + pub const fn get(&self) -> $I { + <$I>::from_le_bytes(self.0) + } + } + + impl From<$I> for $P { + fn from(n: $I) -> Self { + Self::from_primitive(n) + } + } + + impl From<$P> for $I { + fn from(pod: $P) -> Self { + pod.get() + } + } + }; +} + +/// Implements constants, ordering, display, checked/saturating arithmetic, and +/// helper methods for a Pod integer type. +#[macro_export] +macro_rules! impl_pod_common { + ($name:ident, $native:ty, $size:expr) => { + impl $name { + /// The largest value representable by the underlying integer type. + pub const MAX: Self = Self(<$native>::MAX.to_le_bytes()); + /// The smallest value representable by the underlying integer type. + pub const MIN: Self = Self(<$native>::MIN.to_le_bytes()); + /// The zero value. + pub const ZERO: Self = Self([0u8; $size]); + + /// Returns `true` if the value is zero. + #[inline] + #[must_use] + pub fn is_zero(&self) -> bool { + self.0 == [0u8; $size] + } + + /// Checked addition. Returns `None` on overflow. + #[inline] + #[must_use] + pub fn checked_add(self, rhs: impl Into<$name>) -> Option { + self.get().checked_add(rhs.into().get()).map(Self::from) + } + + /// Checked subtraction. Returns `None` on underflow. + #[inline] + #[must_use] + pub fn checked_sub(self, rhs: impl Into<$name>) -> Option { + self.get().checked_sub(rhs.into().get()).map(Self::from) + } + + /// Checked multiplication. Returns `None` on overflow. + #[inline] + #[must_use] + pub fn checked_mul(self, rhs: impl Into<$name>) -> Option { + self.get().checked_mul(rhs.into().get()).map(Self::from) + } + + /// Checked division. Returns `None` if `rhs` is zero. + #[inline] + #[must_use] + pub fn checked_div(self, rhs: impl Into<$name>) -> Option { + self.get().checked_div(rhs.into().get()).map(Self::from) + } + + /// Saturating addition. Clamps at the numeric bounds instead of + /// overflowing. + #[inline] + #[must_use] + pub fn saturating_add(self, rhs: impl Into<$name>) -> Self { + Self::from(self.get().saturating_add(rhs.into().get())) + } + + /// Saturating subtraction. Clamps at the numeric bound instead of + /// underflowing. + #[inline] + #[must_use] + pub fn saturating_sub(self, rhs: impl Into<$name>) -> Self { + Self::from(self.get().saturating_sub(rhs.into().get())) + } + + /// Saturating multiplication. Clamps at the numeric bounds instead + /// of overflowing. + #[inline] + #[must_use] + pub fn saturating_mul(self, rhs: impl Into<$name>) -> Self { + Self::from(self.get().saturating_mul(rhs.into().get())) + } + } + + impl PartialOrd for $name { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for $name { + #[inline] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.get().cmp(&other.get()) + } + } + + impl PartialEq<$native> for $name { + #[inline] + fn eq(&self, other: &$native) -> bool { + self.get() == *other + } + } + + impl PartialOrd<$native> for $name { + #[inline] + fn partial_cmp(&self, other: &$native) -> Option { + self.get().partial_cmp(other) + } + } + + impl core::fmt::Display for $name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.get().fmt(f) + } + } + }; +} + +/// Implements arithmetic operators for a Pod type. +/// +/// In debug builds, operators panic on overflow via `checked_*`. In release +/// builds, they use `wrapping_*` for CU efficiency on Solana. +#[macro_export] +macro_rules! impl_pod_arithmetic { + ($name:ident, $native:ty) => { + // --- Pod + native --- + + impl core::ops::Add<$native> for $name { + type Output = Self; + + #[inline] + fn add(self, rhs: $native) -> Self { + #[cfg(debug_assertions)] + { + Self::from( + self.get() + .checked_add(rhs) + .unwrap_or_else(|| panic!("attempt to add with overflow")), + ) + } + #[cfg(not(debug_assertions))] + { + Self::from(self.get().wrapping_add(rhs)) + } + } + } + + impl core::ops::Sub<$native> for $name { + type Output = Self; + + #[inline] + fn sub(self, rhs: $native) -> Self { + #[cfg(debug_assertions)] + { + Self::from( + self.get() + .checked_sub(rhs) + .unwrap_or_else(|| panic!("attempt to subtract with overflow")), + ) + } + #[cfg(not(debug_assertions))] + { + Self::from(self.get().wrapping_sub(rhs)) + } + } + } + + impl core::ops::Mul<$native> for $name { + type Output = Self; + + #[inline] + fn mul(self, rhs: $native) -> Self { + #[cfg(debug_assertions)] + { + Self::from( + self.get() + .checked_mul(rhs) + .unwrap_or_else(|| panic!("attempt to multiply with overflow")), + ) + } + #[cfg(not(debug_assertions))] + { + Self::from(self.get().wrapping_mul(rhs)) + } + } + } + + impl core::ops::Div<$native> for $name { + type Output = Self; + + #[inline] + fn div(self, rhs: $native) -> Self { + Self::from(self.get() / rhs) + } + } + + impl core::ops::Rem<$native> for $name { + type Output = Self; + + #[inline] + fn rem(self, rhs: $native) -> Self { + Self::from(self.get() % rhs) + } + } + + // --- Pod + Pod --- + + impl core::ops::Add for $name { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self { + self + rhs.get() + } + } + + impl core::ops::Sub for $name { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self { + self - rhs.get() + } + } + + impl core::ops::Mul for $name { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self { + self * rhs.get() + } + } + + impl core::ops::Div for $name { + type Output = Self; + + #[inline] + fn div(self, rhs: Self) -> Self { + self / rhs.get() + } + } + + impl core::ops::Rem for $name { + type Output = Self; + + #[inline] + fn rem(self, rhs: Self) -> Self { + self % rhs.get() + } + } + + // --- Assign with native --- + + impl core::ops::AddAssign<$native> for $name { + #[inline] + fn add_assign(&mut self, rhs: $native) { + *self = *self + rhs; + } + } + + impl core::ops::SubAssign<$native> for $name { + #[inline] + fn sub_assign(&mut self, rhs: $native) { + *self = *self - rhs; + } + } + + impl core::ops::MulAssign<$native> for $name { + #[inline] + fn mul_assign(&mut self, rhs: $native) { + *self = *self * rhs; + } + } + + impl core::ops::DivAssign<$native> for $name { + #[inline] + fn div_assign(&mut self, rhs: $native) { + *self = *self / rhs; + } + } + + impl core::ops::RemAssign<$native> for $name { + #[inline] + fn rem_assign(&mut self, rhs: $native) { + *self = *self % rhs; + } + } + + // --- Assign with Pod --- + + impl core::ops::AddAssign for $name { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } + } + + impl core::ops::SubAssign for $name { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + + impl core::ops::MulAssign for $name { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } + } + + impl core::ops::DivAssign for $name { + #[inline] + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } + } + + impl core::ops::RemAssign for $name { + #[inline] + fn rem_assign(&mut self, rhs: Self) { + *self = *self % rhs; + } + } + + // --- Bitwise --- + + impl core::ops::BitAnd<$native> for $name { + type Output = Self; + + #[inline] + fn bitand(self, rhs: $native) -> Self { + Self::from(self.get() & rhs) + } + } + + impl core::ops::BitOr<$native> for $name { + type Output = Self; + + #[inline] + fn bitor(self, rhs: $native) -> Self { + Self::from(self.get() | rhs) + } + } + + impl core::ops::BitXor<$native> for $name { + type Output = Self; + + #[inline] + fn bitxor(self, rhs: $native) -> Self { + Self::from(self.get() ^ rhs) + } + } + + impl core::ops::BitAnd for $name { + type Output = Self; + + #[inline] + fn bitand(self, rhs: Self) -> Self { + self & rhs.get() + } + } + + impl core::ops::BitOr for $name { + type Output = Self; + + #[inline] + fn bitor(self, rhs: Self) -> Self { + self | rhs.get() + } + } + + impl core::ops::BitXor for $name { + type Output = Self; + + #[inline] + fn bitxor(self, rhs: Self) -> Self { + self ^ rhs.get() + } + } + + impl core::ops::Shl for $name { + type Output = Self; + + #[inline] + fn shl(self, rhs: u32) -> Self { + Self::from(self.get() << rhs) + } + } + + impl core::ops::Shr for $name { + type Output = Self; + + #[inline] + fn shr(self, rhs: u32) -> Self { + Self::from(self.get() >> rhs) + } + } + + impl core::ops::Not for $name { + type Output = Self; + + #[inline] + fn not(self) -> Self { + Self::from(!self.get()) + } + } + + // --- Bitwise assign with native --- + + impl core::ops::BitAndAssign<$native> for $name { + #[inline] + fn bitand_assign(&mut self, rhs: $native) { + *self = *self & rhs; + } + } + + impl core::ops::BitOrAssign<$native> for $name { + #[inline] + fn bitor_assign(&mut self, rhs: $native) { + *self = *self | rhs; + } + } + + impl core::ops::BitXorAssign<$native> for $name { + #[inline] + fn bitxor_assign(&mut self, rhs: $native) { + *self = *self ^ rhs; + } + } + + // --- Bitwise assign with Pod --- + + impl core::ops::BitAndAssign for $name { + #[inline] + fn bitand_assign(&mut self, rhs: Self) { + *self = *self & rhs; + } + } + + impl core::ops::BitOrAssign for $name { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } + } + + impl core::ops::BitXorAssign for $name { + #[inline] + fn bitxor_assign(&mut self, rhs: Self) { + *self = *self ^ rhs; + } + } + + impl core::ops::ShlAssign for $name { + #[inline] + fn shl_assign(&mut self, rhs: u32) { + *self = *self << rhs; + } + } + + impl core::ops::ShrAssign for $name { + #[inline] + fn shr_assign(&mut self, rhs: u32) { + *self = *self >> rhs; + } + } + }; +} + +/// Implements `Neg` for signed Pod types. +#[macro_export] +macro_rules! impl_pod_neg { + ($name:ident, $native:ty) => { + impl core::ops::Neg for $name { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + #[cfg(debug_assertions)] + { + Self::from( + self.get() + .checked_neg() + .unwrap_or_else(|| panic!("attempt to negate with overflow")), + ) + } + #[cfg(not(debug_assertions))] + { + Self::from(self.get().wrapping_neg()) + } + } + } + }; +} + +/// Defines an unsigned Pod integer type with full operator support. +#[macro_export] +macro_rules! define_pod_unsigned { + ($name:ident, $native:ty, $size:expr, $doc:expr) => { + #[doc = $doc] + #[derive(Clone, Copy, Default, PartialEq, Eq, Pod, Zeroable)] + #[repr(transparent)] + pub struct $name(pub [u8; $size]); + + $crate::impl_int_conversion!($name, $native); + $crate::impl_pod_common!($name, $native, $size); + $crate::impl_pod_arithmetic!($name, $native); + + impl core::fmt::Debug for $name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}({})", stringify!($name), self.get()) + } + } + }; +} + +/// Defines a signed Pod integer type with full operator support. +#[macro_export] +macro_rules! define_pod_signed { + ($name:ident, $native:ty, $size:expr, $doc:expr) => { + #[doc = $doc] + #[derive(Clone, Copy, Default, PartialEq, Eq, Pod, Zeroable)] + #[repr(transparent)] + pub struct $name(pub [u8; $size]); + + $crate::impl_int_conversion!($name, $native); + $crate::impl_pod_common!($name, $native, $size); + $crate::impl_pod_arithmetic!($name, $native); + $crate::impl_pod_neg!($name, $native); + + impl core::fmt::Debug for $name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}({})", stringify!($name), self.get()) + } + } + }; +} diff --git a/crates/pina_pod_primitives/src/option.rs b/crates/pina_pod_primitives/src/option.rs new file mode 100644 index 00000000..97ece8f3 --- /dev/null +++ b/crates/pina_pod_primitives/src/option.rs @@ -0,0 +1,193 @@ +//! Fixed-size optional value with 1-byte discriminant. + +use core::fmt; +use core::mem::MaybeUninit; +use core::mem::align_of; +use core::mem::size_of; + +use bytemuck::Pod; +use bytemuck::Zeroable; + +/// A fixed-size optional value with `1` byte discriminant (`0=None`, `1=Some`). +/// +/// # Layout +/// - Byte 0: discriminant (`0` or `1`) +/// - Bytes `1..1+size_of::()`: value (uninitialized if `None`) +#[repr(C)] +#[derive(Copy, Clone)] +pub struct PodOption { + tag: u8, + value: MaybeUninit, +} + +impl PodOption { + /// Creates a `None` value. + pub const fn none() -> Self { + Self { + tag: 0, + value: MaybeUninit::uninit(), + } + } + + /// Creates a `Some` value. + pub const fn some(value: T) -> Self { + Self { + tag: 1, + value: MaybeUninit::new(value), + } + } + + /// Returns `true` if the option is `None`. + pub const fn is_none(&self) -> bool { + self.tag == 0 + } + + /// Returns `true` if the option is `Some`. + pub const fn is_some(&self) -> bool { + self.tag == 1 + } + + /// Returns the value if `Some`, otherwise `None`. + pub fn get(&self) -> Option { + if self.tag == 1 { + // SAFETY: tag == 1 means value was initialized + Some(unsafe { self.value.assume_init() }) + } else { + None + } + } + + /// Returns a reference to the value if `Some`. + pub fn as_ref(&self) -> Option<&T> { + if self.tag == 1 { + // SAFETY: tag == 1 means value was initialized + Some(unsafe { &*self.value.as_ptr() }) + } else { + None + } + } + + /// Returns a mutable reference to the value if `Some`. + pub fn as_mut(&mut self) -> Option<&mut T> { + if self.tag == 1 { + // SAFETY: tag == 1 means value was initialized + Some(unsafe { &mut *self.value.as_mut_ptr() }) + } else { + None + } + } + + /// Sets the value to `Some`. + pub fn set(&mut self, value: T) { + self.value = MaybeUninit::new(value); + self.tag = 1; + } + + /// Sets the value to `None`. + pub fn clear(&mut self) { + self.tag = 0; + } + + /// Returns the raw tag byte. + pub const fn raw_tag(&self) -> u8 { + self.tag + } + + /// # Safety + /// Caller must ensure this is `Some`, otherwise returns uninitialized data. + pub unsafe fn assume_init(&self) -> &T { + unsafe { &*self.value.as_ptr() } + } +} + +impl Default for PodOption { + fn default() -> Self { + Self::none() + } +} + +impl PartialEq for PodOption { + fn eq(&self, other: &Self) -> bool { + match (self.tag, other.tag) { + (0, 0) => true, + (1, 1) => unsafe { self.value.assume_init() == other.value.assume_init() }, + _ => false, + } + } +} + +impl Eq for PodOption {} + +impl fmt::Debug for PodOption +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.get() { + Some(v) => f.debug_tuple("PodOption::Some").field(&v).finish(), + None => write!(f, "PodOption::None"), + } + } +} + +// SAFETY: PodOption is #[repr(C)] with tag: u8 + MaybeUninit where T: Pod. +// T: Pod guarantees T is align-1 and valid for any bit pattern, so the +// MaybeUninit doesn't violate Pod requirements. +unsafe impl Zeroable for PodOption {} +unsafe impl Pod for PodOption {} + +// Compile-time layout assertions for PodOption +const _: () = assert!(align_of::>() == 1); +const _: () = assert!(size_of::>() == 2); // 1 tag + 1 value + +// --------------------------------------------------------------------------- +// Kani model-checking proof harnesses +// --------------------------------------------------------------------------- + +#[cfg(kani)] +mod kani_proofs { + use super::*; + + #[kani::proof] + fn none_is_none() { + let opt: PodOption = PodOption::none(); + assert!(opt.is_none()); + assert!(!opt.is_some()); + assert_eq!(opt.get(), None); + } + + #[kani::proof] + fn some_roundtrip() { + let val: u8 = kani::any(); + let opt = PodOption::some(val); + assert!(opt.is_some()); + assert_eq!(opt.get(), Some(val)); + } + + #[kani::proof] + fn set_then_get() { + let val: u8 = kani::any(); + let mut opt = PodOption::::none(); + opt.set(val); + assert_eq!(opt.get(), Some(val)); + } + + #[kani::proof] + fn clear_after_some() { + let val: u8 = kani::any(); + let mut opt = PodOption::some(val); + opt.clear(); + assert!(opt.is_none()); + assert_eq!(opt.get(), None); + } + + #[kani::proof] + fn tag_byte_matches_state() { + let val: u8 = kani::any(); + let some = PodOption::some(val); + assert_eq!(some.raw_tag(), 1); + + let none: PodOption = PodOption::none(); + assert_eq!(none.raw_tag(), 0); + } +} diff --git a/crates/pina_pod_primitives/src/pod_bool.rs b/crates/pina_pod_primitives/src/pod_bool.rs new file mode 100644 index 00000000..c6e205a4 --- /dev/null +++ b/crates/pina_pod_primitives/src/pod_bool.rs @@ -0,0 +1,69 @@ +//! The standard `bool` is not a `Pod`, define a replacement that is. + +use core::fmt; + +use bytemuck::Pod; +use bytemuck::Zeroable; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PodBool(pub u8); + +impl PodBool { + pub const fn from_bool(b: bool) -> Self { + Self(if b { 1 } else { 0 }) + } + + /// Returns `true` if the underlying byte is a canonical boolean value + /// (`0` or `1`). + /// + /// Non-canonical values (2–255) are accepted by `bytemuck` deserialization + /// and convert to `true`, but two non-canonical `PodBool` values + /// representing the same logical boolean may fail `PartialEq` comparison. + /// Use this method to validate account data at deserialization boundaries. + pub const fn is_canonical(&self) -> bool { + self.0 == 0 || self.0 == 1 + } +} + +impl From for PodBool { + fn from(b: bool) -> Self { + Self::from_bool(b) + } +} + +impl From<&bool> for PodBool { + fn from(b: &bool) -> Self { + Self(u8::from(*b)) + } +} + +impl From<&PodBool> for bool { + fn from(b: &PodBool) -> Self { + b.0 != 0 + } +} + +impl From for bool { + fn from(b: PodBool) -> Self { + b.0 != 0 + } +} + +impl core::ops::Not for PodBool { + type Output = Self; + + #[inline] + fn not(self) -> Self { + Self::from_bool(!bool::from(self)) + } +} + +impl fmt::Display for PodBool { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + bool::from(*self).fmt(f) + } +} + +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 1); diff --git a/crates/pina_pod_primitives/src/pod_numeric.rs b/crates/pina_pod_primitives/src/pod_numeric.rs new file mode 100644 index 00000000..092826d5 --- /dev/null +++ b/crates/pina_pod_primitives/src/pod_numeric.rs @@ -0,0 +1,90 @@ +//! Pod integer type definitions (`PodU16`, `PodI16`, …, `PodU128`, `PodI128`). + +use bytemuck::Pod; +use bytemuck::Zeroable; + +use crate::define_pod_signed; +use crate::define_pod_unsigned; + +define_pod_unsigned!( + PodU16, + u16, + 2, + "An alignment-1 wrapper around `u16` stored as `[u8; 2]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +define_pod_signed!( + PodI16, + i16, + 2, + "An alignment-1 wrapper around `i16` stored as `[u8; 2]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +define_pod_unsigned!( + PodU32, + u32, + 4, + "An alignment-1 wrapper around `u32` stored as `[u8; 4]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +define_pod_signed!( + PodI32, + i32, + 4, + "An alignment-1 wrapper around `i32` stored as `[u8; 4]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +define_pod_unsigned!( + PodU64, + u64, + 8, + "An alignment-1 wrapper around `u64` stored as `[u8; 8]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +define_pod_signed!( + PodI64, + i64, + 8, + "An alignment-1 wrapper around `i64` stored as `[u8; 8]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +define_pod_unsigned!( + PodU128, + u128, + 16, + "An alignment-1 wrapper around `u128` stored as `[u8; 16]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +define_pod_signed!( + PodI128, + i128, + 16, + "An alignment-1 wrapper around `i128` stored as `[u8; 16]`.\n\nEnables safe zero-copy access \ + inside `#[repr(C)]` account structs." +); + +// Compile-time invariant: all numeric Pod types must have alignment 1 and +// correct size. +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 2); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 2); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 4); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 4); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 8); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 8); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 16); +const _: () = assert!(align_of::() == 1); +const _: () = assert!(size_of::() == 16); diff --git a/crates/pina_pod_primitives/src/string.rs b/crates/pina_pod_primitives/src/string.rs new file mode 100644 index 00000000..ea2bb22d --- /dev/null +++ b/crates/pina_pod_primitives/src/string.rs @@ -0,0 +1,368 @@ +//! Fixed-capacity string with length prefix. + +use core::fmt; +use core::mem::MaybeUninit; +use core::mem::align_of; +use core::mem::size_of; + +use bytemuck::Pod; +use bytemuck::Zeroable; + +use crate::error::PodCollectionError; +use crate::error::max_n_for_pfx; + +/// A fixed-capacity string stored inline with a length prefix. +/// +/// Default prefix size is `1` byte (u8), supporting strings up to 255 bytes. +/// Use `PodString` for up to 65,535 bytes, etc. +/// +/// # Layout +/// - Bytes 0..PFX: length prefix (little-endian) +/// - Bytes PFX..PFX+N: UTF-8 data (may be partially uninitialized) +#[repr(C)] +#[derive(Copy, Clone)] +pub struct PodString { + len: [u8; PFX], + data: [MaybeUninit; N], +} + +// Compile-time validation of PFX +impl PodString { + /// Use this const to trigger the compile-time assertions. + pub const VALID: () = Self::_CAP_CHECK; + const _CAP_CHECK: () = { + assert!( + PFX == 1 || PFX == 2 || PFX == 4 || PFX == 8, + "PodString: PFX must be 1, 2, 4, or 8" + ); + assert!( + N <= max_n_for_pfx(PFX), + "PodString: N exceeds the maximum value representable by the PFX-byte length \ + prefix" + ); + }; +} + +impl PodString { + #[inline] + fn decode_len(&self) -> usize { + match PFX { + 1 => self.len[0] as usize, + 2 => u16::from_le_bytes([self.len[0], self.len[1]]) as usize, + 4 => u32::from_le_bytes([self.len[0], self.len[1], self.len[2], self.len[3]]) as usize, + 8 => { + u64::from_le_bytes([ + self.len[0], + self.len[1], + self.len[2], + self.len[3], + self.len[4], + self.len[5], + self.len[6], + self.len[7], + ]) as usize + } + _ => unreachable!(), + } + } + + #[inline] + fn encode_len(&mut self, n: usize) { + match PFX { + 1 => self.len[0] = n as u8, + 2 => { + let bytes = (n as u16).to_le_bytes(); + self.len.copy_from_slice(&bytes); + } + 4 => { + let bytes = (n as u32).to_le_bytes(); + self.len.copy_from_slice(&bytes); + } + 8 => { + let bytes = (n as u64).to_le_bytes(); + self.len.copy_from_slice(&bytes); + } + _ => unreachable!(), + } + } + + /// Returns the logical length of the string (clamped to capacity). + #[inline] + pub fn len(&self) -> usize { + self.decode_len().min(N) + } + + /// Returns `true` if the string is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the maximum capacity. + pub const fn capacity(&self) -> usize { + N + } + + /// Returns the string as a `&str`. + /// + /// # Safety + /// This assumes the stored bytes are valid UTF-8. For untrusted account + /// data, use `try_as_str()` instead. + #[inline] + pub unsafe fn as_str_unchecked(&self) -> &str { + unsafe { + let len = self.len(); + let bytes = core::slice::from_raw_parts(self.data.as_ptr().cast::(), len); + core::str::from_utf8_unchecked(bytes) + } + } + + /// Returns the string as a `&str`, validating UTF-8. + pub fn try_as_str(&self) -> Result<&str, PodCollectionError> { + let len = self.len(); + let bytes = unsafe { core::slice::from_raw_parts(self.data.as_ptr().cast::(), len) }; + core::str::from_utf8(bytes).map_err(|_| PodCollectionError::InvalidUtf8) + } + + /// Returns the raw bytes (may include trailing garbage — use `len()` for valid slice). + pub fn as_bytes(&self) -> &[u8] { + let len = self.len(); + unsafe { core::slice::from_raw_parts(self.data.as_ptr().cast::(), len) } + } + + /// Sets the string to a new value, returning error if too long. + pub fn try_set(&mut self, value: &str) -> Result<(), PodCollectionError> { + let vlen = value.len(); + if vlen > N { + return Err(PodCollectionError::Overflow); + } + unsafe { + core::ptr::copy_nonoverlapping( + value.as_ptr(), + self.data.as_mut_ptr().cast::(), + vlen, + ); + } + self.encode_len(vlen); + Ok(()) + } + + /// Sets the string to a new value. + /// + /// Returns `false` if the value was truncated due to exceeding capacity. + #[must_use = "returns false if value exceeds capacity"] + pub fn set(&mut self, value: &str) -> bool { + self.try_set(value).is_ok() + } + + /// Appends a string slice, returning error if capacity exceeded. + pub fn try_push_str(&mut self, value: &str) -> Result<(), PodCollectionError> { + let cur = self.len(); + let vlen = value.len(); + let new_len = cur + vlen; + if new_len > N { + return Err(PodCollectionError::Overflow); + } + unsafe { + core::ptr::copy_nonoverlapping( + value.as_ptr(), + self.data.as_mut_ptr().cast::().add(cur), + vlen, + ); + } + self.encode_len(new_len); + Ok(()) + } + + /// Appends a string slice. + /// + /// Returns `false` if appending would exceed capacity. + #[must_use = "returns false if append would exceed capacity"] + pub fn push_str(&mut self, value: &str) -> bool { + self.try_push_str(value).is_ok() + } + + /// Clears the string (sets length to 0). + pub fn clear(&mut self) { + self.len = [0u8; PFX]; + } +} + +impl Default for PodString { + fn default() -> Self { + Self { + len: [0u8; PFX], + data: [MaybeUninit::uninit(); N], + } + } +} + +impl core::ops::Deref for PodString { + type Target = str; + + fn deref(&self) -> &str { + unsafe { self.as_str_unchecked() } + } +} + +impl AsRef for PodString { + fn as_ref(&self) -> &str { + unsafe { self.as_str_unchecked() } + } +} + +impl AsRef<[u8]> for PodString { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl PartialEq for PodString { + fn eq(&self, other: &Self) -> bool { + self.as_bytes() == other.as_bytes() + } +} + +impl Eq for PodString {} + +impl PartialEq for PodString { + fn eq(&self, other: &str) -> bool { + self.as_bytes() == other.as_bytes() + } +} + +impl fmt::Debug for PodString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.try_as_str() { + Ok(s) => fmt::Debug::fmt(s, f), + Err(_) => { + f.debug_struct("PodString") + .field("len", &self.len()) + .finish() + } + } + } +} + +impl fmt::Display for PodString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.try_as_str() { + Ok(s) => f.write_str(s), + Err(_) => write!(f, ""), + } + } +} + +// SAFETY: PodString is #[repr(C)] with len: [u8; PFX] + data: [MaybeUninit; N]. +// Both have align 1 and any bit pattern is valid. +unsafe impl Zeroable for PodString {} +unsafe impl Pod for PodString {} + +// Compile-time layout assertions +const _: () = assert!(align_of::>() == 1); +const _: () = assert!(size_of::>() == 1); +const _: () = assert!(size_of::>() == 33); +const _: () = assert!(size_of::>() == 256); +const _: () = assert!(size_of::>() == 2); +const _: () = assert!(size_of::>() == 102); + +// --------------------------------------------------------------------------- +// Kani model-checking proof harnesses +// --------------------------------------------------------------------------- + +#[cfg(kani)] +mod kani_proofs { + use super::*; + + #[kani::proof] + fn encode_decode_roundtrip_pfx1() { + let n: usize = kani::any(); + kani::assume(n <= u8::MAX as usize); + let mut s = PodString::<255, 1>::default(); + s.encode_len(n); + assert!(s.decode_len() == n); + } + + #[kani::proof] + fn encode_decode_roundtrip_pfx2() { + let n: usize = kani::any(); + kani::assume(n <= u16::MAX as usize); + let mut s = PodString::<255, 2>::default(); + s.encode_len(n); + assert!(s.decode_len() == n); + } + + #[kani::proof] + fn len_clamp_pfx1() { + let raw: [u8; 1] = kani::any(); + let s = PodString::<8, 1> { + len: raw, + data: [MaybeUninit::uninit(); 8], + }; + assert!(s.len() <= 8); + } + + #[kani::proof] + fn len_clamp_pfx2() { + let raw: [u8; 2] = kani::any(); + let s = PodString::<8, 2> { + len: raw, + data: [MaybeUninit::uninit(); 8], + }; + assert!(s.len() <= 8); + } + + #[kani::proof] + #[kani::unwind(10)] + fn set_then_as_bytes_len() { + let vlen: usize = kani::any(); + kani::assume(vlen <= 8); + let content = [0x41u8; 8]; + let mut s = PodString::<8>::default(); + let ok = s.set(unsafe { core::str::from_utf8_unchecked(&content[..vlen]) }); + assert!(ok); + assert!(s.len() == vlen); + assert!(s.as_bytes().len() == vlen); + } + + #[kani::proof] + fn set_rejects_over_capacity() { + let vlen: usize = kani::any(); + kani::assume(vlen > 4); + kani::assume(vlen <= 8); + let content = [0x41u8; 8]; + let mut s = PodString::<4>::default(); + assert!(!s.set(unsafe { core::str::from_utf8_unchecked(&content[..vlen]) })); + } + + #[kani::proof] + #[kani::unwind(10)] + fn push_str_len_accounting() { + let a_len: usize = kani::any(); + let b_len: usize = kani::any(); + kani::assume(a_len <= 4); + kani::assume(b_len <= 4); + kani::assume(a_len + b_len <= 8); + + let buf = [0x41u8; 8]; + let mut s = PodString::<8>::default(); + assert!(s.set(unsafe { core::str::from_utf8_unchecked(&buf[..a_len]) })); + assert!(s.push_str(unsafe { core::str::from_utf8_unchecked(&buf[..b_len]) })); + assert!(s.len() == a_len + b_len); + } + + #[kani::proof] + fn push_str_rejects_overflow() { + let a_len: usize = kani::any(); + let b_len: usize = kani::any(); + kani::assume(a_len <= 4); + kani::assume(b_len <= 8); + kani::assume(a_len + b_len > 4); + + let buf = [0x41u8; 8]; + let mut s = PodString::<4>::default(); + assert!(s.set(unsafe { core::str::from_utf8_unchecked(&buf[..a_len]) })); + assert!(!s.push_str(unsafe { core::str::from_utf8_unchecked(&buf[..b_len]) })); + assert!(s.len() == a_len); + } +} diff --git a/crates/pina_pod_primitives/src/tests/mod.rs b/crates/pina_pod_primitives/src/tests/mod.rs new file mode 100644 index 00000000..46292082 --- /dev/null +++ b/crates/pina_pod_primitives/src/tests/mod.rs @@ -0,0 +1,15 @@ +//! Integration tests for Pod types. + +extern crate std; +use std::vec; +use std::vec::Vec; + +use bytemuck::try_from_bytes; + +use crate::*; + +mod option; +mod pod_bool; +mod pod_numeric; +mod pod_vec; +mod string; diff --git a/crates/pina_pod_primitives/src/tests/option.rs b/crates/pina_pod_primitives/src/tests/option.rs new file mode 100644 index 00000000..e0ace27d --- /dev/null +++ b/crates/pina_pod_primitives/src/tests/option.rs @@ -0,0 +1,50 @@ +use core::mem::size_of; + +use super::*; + +#[test] +fn pod_option_none() { + let opt = PodOption::::none(); + assert!(opt.is_none()); + assert!(!opt.is_some()); + assert_eq!(opt.get(), None); +} + +#[test] +fn pod_option_some() { + let opt = PodOption::some(PodU64::from(42u64)); + assert!(!opt.is_none()); + assert!(opt.is_some()); + assert_eq!(opt.get(), Some(PodU64::from(42u64))); +} + +#[test] +fn pod_option_set_and_clear() { + let mut opt = PodOption::::none(); + opt.set(PodU64::from(100u64)); + assert!(opt.is_some()); + assert_eq!(opt.get(), Some(PodU64::from(100u64))); + opt.clear(); + assert!(opt.is_none()); +} + +#[test] +fn pod_option_default_is_none() { + let opt = PodOption::::default(); + assert!(opt.is_none()); +} + +#[test] +fn pod_option_bytemuck_roundtrip() { + let opt = PodOption::some(PodU64::from(0xDEAD_BEEF_u64)); + let bytes: &[u8] = unsafe { + core::slice::from_raw_parts( + &opt as *const _ as *const u8, + size_of::>(), + ) + }; + assert_eq!(bytes[0], 1); // Some tag + assert_eq!(bytes[1..9], 0xDEAD_BEEF_u64.to_le_bytes()); + let restored = unsafe { &*(bytes.as_ptr() as *const PodOption) }; + assert_eq!(restored.get(), Some(PodU64::from(0xDEAD_BEEF_u64))); +} diff --git a/crates/pina_pod_primitives/src/tests/pod_bool.rs b/crates/pina_pod_primitives/src/tests/pod_bool.rs new file mode 100644 index 00000000..d04b347d --- /dev/null +++ b/crates/pina_pod_primitives/src/tests/pod_bool.rs @@ -0,0 +1,77 @@ +use super::*; + +#[test] +fn pod_bool_roundtrip() { + for i in 0..=u8::MAX { + let value = *try_from_bytes::(&[i]).unwrap(); + assert_eq!(i != 0, bool::from(value)); + } +} + +#[test] +fn pod_bool_non_canonical_equality_mismatch() { + let canonical_true = PodBool::from_bool(true); + let non_canonical_true = *try_from_bytes::(&[2]).unwrap(); + + assert!(bool::from(canonical_true)); + assert!(bool::from(non_canonical_true)); + + assert_ne!(canonical_true, non_canonical_true); + + assert!(canonical_true.is_canonical()); + assert!(!non_canonical_true.is_canonical()); +} + +#[test] +fn pod_bool_is_canonical_boundary_values() { + assert!(PodBool(0).is_canonical()); + assert!(PodBool(1).is_canonical()); + assert!(!PodBool(2).is_canonical()); + assert!(!PodBool(127).is_canonical()); + assert!(!PodBool(255).is_canonical()); +} + +#[test] +fn pod_bool_from_bool_produces_canonical() { + assert!(PodBool::from_bool(false).is_canonical()); + assert!(PodBool::from_bool(true).is_canonical()); + assert!(PodBool::from(false).is_canonical()); + assert!(PodBool::from(true).is_canonical()); +} + +#[test] +fn pod_bool_from_ref() { + let t = true; + let f = false; + assert_eq!(PodBool::from(&t), PodBool(1)); + assert_eq!(PodBool::from(&f), PodBool(0)); +} + +#[test] +fn pod_bool_from_ref_roundtrip() { + let pod = PodBool(1); + assert!(bool::from(&pod)); + let pod = PodBool(0); + assert!(!bool::from(&pod)); +} + +#[test] +fn pod_bool_default_is_false() { + let default = PodBool::default(); + assert_eq!(default.0, 0); + assert!(!bool::from(default)); + assert!(default.is_canonical()); +} + +#[test] +fn pod_bool_not() { + assert_eq!(!PodBool::from_bool(true), PodBool::from_bool(false)); + assert_eq!(!PodBool::from_bool(false), PodBool::from_bool(true)); + assert_eq!(!PodBool(42), PodBool::from_bool(false)); +} + +#[test] +fn pod_bool_display() { + assert_eq!(std::format!("{}", PodBool::from_bool(true)), "true"); + assert_eq!(std::format!("{}", PodBool::from_bool(false)), "false"); +} diff --git a/crates/pina_pod_primitives/src/tests/pod_numeric.rs b/crates/pina_pod_primitives/src/tests/pod_numeric.rs new file mode 100644 index 00000000..02980b84 --- /dev/null +++ b/crates/pina_pod_primitives/src/tests/pod_numeric.rs @@ -0,0 +1,595 @@ +use super::*; + +#[test] +fn pod_u16_roundtrip() { + assert_eq!(1u16, u16::from(PodU16::from_primitive(1))); +} + +#[test] +fn pod_i16_roundtrip() { + assert_eq!(-1i16, i16::from(PodI16::from_primitive(-1))); +} + +#[test] +fn pod_u32_roundtrip() { + assert_eq!(7u32, u32::from(PodU32::from_primitive(7))); +} + +#[test] +fn pod_i32_roundtrip() { + assert_eq!(-7i32, i32::from(PodI32::from_primitive(-7))); +} + +#[test] +fn pod_u64_roundtrip() { + assert_eq!(9u64, u64::from(PodU64::from_primitive(9))); +} + +#[test] +fn pod_i64_roundtrip() { + assert_eq!(-9i64, i64::from(PodI64::from_primitive(-9))); +} + +#[test] +fn pod_u128_roundtrip() { + assert_eq!(11u128, u128::from(PodU128::from_primitive(11))); +} + +#[test] +fn pod_i128_roundtrip() { + assert_eq!(-11i128, i128::from(PodI128::from_primitive(-11))); +} + +#[test] +fn pod_u16_boundary_values() { + assert_eq!(0u16, u16::from(PodU16::from_primitive(0))); + assert_eq!(u16::MAX, u16::from(PodU16::from_primitive(u16::MAX))); +} + +#[test] +fn pod_i16_boundary_values() { + assert_eq!(i16::MIN, i16::from(PodI16::from_primitive(i16::MIN))); + assert_eq!(i16::MAX, i16::from(PodI16::from_primitive(i16::MAX))); + assert_eq!(0i16, i16::from(PodI16::from_primitive(0))); +} + +#[test] +fn pod_u32_boundary_values() { + assert_eq!(0u32, u32::from(PodU32::from_primitive(0))); + assert_eq!(u32::MAX, u32::from(PodU32::from_primitive(u32::MAX))); +} + +#[test] +fn pod_i32_boundary_values() { + assert_eq!(i32::MIN, i32::from(PodI32::from_primitive(i32::MIN))); + assert_eq!(i32::MAX, i32::from(PodI32::from_primitive(i32::MAX))); +} + +#[test] +fn pod_u64_boundary_values() { + assert_eq!(0u64, u64::from(PodU64::from_primitive(0))); + assert_eq!(u64::MAX, u64::from(PodU64::from_primitive(u64::MAX))); +} + +#[test] +fn pod_i64_boundary_values() { + assert_eq!(i64::MIN, i64::from(PodI64::from_primitive(i64::MIN))); + assert_eq!(i64::MAX, i64::from(PodI64::from_primitive(i64::MAX))); +} + +#[test] +fn pod_u128_boundary_values() { + assert_eq!(0u128, u128::from(PodU128::from_primitive(0))); + assert_eq!(u128::MAX, u128::from(PodU128::from_primitive(u128::MAX))); +} + +#[test] +fn pod_i128_boundary_values() { + assert_eq!(i128::MIN, i128::from(PodI128::from_primitive(i128::MIN))); + assert_eq!(i128::MAX, i128::from(PodI128::from_primitive(i128::MAX))); +} + +#[test] +fn pod_types_use_little_endian_byte_order() { + let u16_val = PodU16::from_primitive(0x0102); + assert_eq!(u16_val.0, [0x02, 0x01]); + + let u32_val = PodU32::from_primitive(0x01020304); + assert_eq!(u32_val.0, [0x04, 0x03, 0x02, 0x01]); + + let u64_val = PodU64::from_primitive(0x0102030405060708); + assert_eq!(u64_val.0, [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]); +} + +#[test] +fn pod_types_bytemuck_from_bytes() { + let bytes_u16 = [0x39, 0x05]; + let val = try_from_bytes::(&bytes_u16).unwrap(); + assert_eq!(u16::from(*val), 1337); + + let bytes_u32 = [0xEF, 0xBE, 0xAD, 0xDE]; + let val = try_from_bytes::(&bytes_u32).unwrap(); + assert_eq!(u32::from(*val), 0xDEAD_BEEF); + + let bytes_i16 = [0xFF, 0xFF]; + let val = try_from_bytes::(&bytes_i16).unwrap(); + assert_eq!(i16::from(*val), -1); +} + +#[test] +fn pod_default_is_zero() { + assert_eq!(u16::from(PodU16::default()), 0); + assert_eq!(i16::from(PodI16::default()), 0); + assert_eq!(u32::from(PodU32::default()), 0); + assert_eq!(i32::from(PodI32::default()), 0); + assert_eq!(u64::from(PodU64::default()), 0); + assert_eq!(i64::from(PodI64::default()), 0); + assert_eq!(u128::from(PodU128::default()), 0); + assert_eq!(i128::from(PodI128::default()), 0); +} + +#[test] +fn pod_constants_zero() { + assert!(PodU16::ZERO.is_zero()); + assert!(PodU32::ZERO.is_zero()); + assert!(PodU64::ZERO.is_zero()); + assert!(PodU128::ZERO.is_zero()); + assert!(PodI16::ZERO.is_zero()); + assert!(PodI32::ZERO.is_zero()); + assert!(PodI64::ZERO.is_zero()); + assert!(PodI128::ZERO.is_zero()); +} + +#[test] +fn pod_constants_min_max() { + assert_eq!(PodU16::MIN.get(), u16::MIN); + assert_eq!(PodU16::MAX.get(), u16::MAX); + assert_eq!(PodU32::MIN.get(), u32::MIN); + assert_eq!(PodU32::MAX.get(), u32::MAX); + assert_eq!(PodU64::MIN.get(), u64::MIN); + assert_eq!(PodU64::MAX.get(), u64::MAX); + assert_eq!(PodU128::MIN.get(), u128::MIN); + assert_eq!(PodU128::MAX.get(), u128::MAX); + assert_eq!(PodI16::MIN.get(), i16::MIN); + assert_eq!(PodI16::MAX.get(), i16::MAX); + assert_eq!(PodI32::MIN.get(), i32::MIN); + assert_eq!(PodI32::MAX.get(), i32::MAX); + assert_eq!(PodI64::MIN.get(), i64::MIN); + assert_eq!(PodI64::MAX.get(), i64::MAX); + assert_eq!(PodI128::MIN.get(), i128::MIN); + assert_eq!(PodI128::MAX.get(), i128::MAX); +} + +#[test] +fn pod_is_zero_false_for_nonzero() { + assert!(!PodU64::from_primitive(1).is_zero()); + assert!(!PodI64::from_primitive(-1).is_zero()); + assert!(!PodU128::MAX.is_zero()); +} + +#[test] +fn pod_add_native() { + assert_eq!((PodU64::from(10u64) + 5u64).get(), 15); + assert_eq!((PodI32::from(10i32) + 5i32).get(), 15); + assert_eq!((PodI32::from(-10i32) + 5i32).get(), -5); +} + +#[test] +fn pod_add_pod() { + let a = PodU64::from(10u64); + let b = PodU64::from(20u64); + assert_eq!((a + b).get(), 30); +} + +#[test] +fn pod_sub_native() { + assert_eq!((PodU64::from(10u64) - 5u64).get(), 5); + assert_eq!((PodI32::from(-10i32) - 5i32).get(), -15); +} + +#[test] +fn pod_sub_pod() { + let a = PodU64::from(20u64); + let b = PodU64::from(5u64); + assert_eq!((a - b).get(), 15); +} + +#[test] +fn pod_mul_native() { + assert_eq!((PodU64::from(6u64) * 7u64).get(), 42); + assert_eq!((PodI32::from(-3i32) * 4i32).get(), -12); +} + +#[test] +fn pod_mul_pod() { + let a = PodU32::from(6u32); + let b = PodU32::from(7u32); + assert_eq!((a * b).get(), 42); +} + +#[test] +fn pod_div_native() { + assert_eq!((PodU64::from(42u64) / 7u64).get(), 6); + assert_eq!((PodI32::from(-12i32) / 4i32).get(), -3); +} + +#[test] +fn pod_div_pod() { + let a = PodU64::from(42u64); + let b = PodU64::from(7u64); + assert_eq!((a / b).get(), 6); +} + +#[test] +fn pod_rem_native() { + assert_eq!((PodU64::from(10u64) % 3u64).get(), 1); + assert_eq!((PodI32::from(-10i32) % 3i32).get(), -1); +} + +#[test] +fn pod_rem_pod() { + let a = PodU64::from(10u64); + let b = PodU64::from(3u64); + assert_eq!((a % b).get(), 1); +} + +#[test] +fn pod_add_assign_native() { + let mut v = PodU64::from(10u64); + v += 5u64; + assert_eq!(v.get(), 15); +} + +#[test] +fn pod_add_assign_pod() { + let mut v = PodU64::from(10u64); + v += PodU64::from(5u64); + assert_eq!(v.get(), 15); +} + +#[test] +fn pod_sub_assign_native() { + let mut v = PodU64::from(10u64); + v -= 3u64; + assert_eq!(v.get(), 7); +} + +#[test] +fn pod_sub_assign_pod() { + let mut v = PodU64::from(10u64); + v -= PodU64::from(3u64); + assert_eq!(v.get(), 7); +} + +#[test] +fn pod_mul_assign_native() { + let mut v = PodU32::from(5u32); + v *= 4u32; + assert_eq!(v.get(), 20); +} + +#[test] +fn pod_mul_assign_pod() { + let mut v = PodU32::from(5u32); + v *= PodU32::from(4u32); + assert_eq!(v.get(), 20); +} + +#[test] +fn pod_div_assign_native() { + let mut v = PodU64::from(20u64); + v /= 5u64; + assert_eq!(v.get(), 4); +} + +#[test] +fn pod_div_assign_pod() { + let mut v = PodU64::from(20u64); + v /= PodU64::from(5u64); + assert_eq!(v.get(), 4); +} + +#[test] +fn pod_rem_assign_native() { + let mut v = PodU64::from(10u64); + v %= 3u64; + assert_eq!(v.get(), 1); +} + +#[test] +fn pod_rem_assign_pod() { + let mut v = PodU64::from(10u64); + v %= PodU64::from(3u64); + assert_eq!(v.get(), 1); +} + +#[test] +fn pod_bitand_native() { + assert_eq!((PodU32::from(0xFF00u32) & 0x0FF0u32).get(), 0x0F00); +} + +#[test] +fn pod_bitand_pod() { + let a = PodU32::from(0xFF00u32); + let b = PodU32::from(0x0FF0u32); + assert_eq!((a & b).get(), 0x0F00); +} + +#[test] +fn pod_bitor_native() { + assert_eq!((PodU32::from(0xFF00u32) | 0x00FFu32).get(), 0xFFFF); +} + +#[test] +fn pod_bitor_pod() { + let a = PodU32::from(0xFF00u32); + let b = PodU32::from(0x00FFu32); + assert_eq!((a | b).get(), 0xFFFF); +} + +#[test] +fn pod_bitxor_native() { + assert_eq!((PodU32::from(0xFFFFu32) ^ 0xFF00u32).get(), 0x00FF); +} + +#[test] +fn pod_bitxor_pod() { + let a = PodU32::from(0xFFFFu32); + let b = PodU32::from(0xFF00u32); + assert_eq!((a ^ b).get(), 0x00FF); +} + +#[test] +fn pod_shl() { + assert_eq!((PodU32::from(1u32) << 4).get(), 16); +} + +#[test] +fn pod_shr() { + assert_eq!((PodU32::from(16u32) >> 4).get(), 1); +} + +#[test] +fn pod_not() { + assert_eq!((!PodU16::from(0u16)).get(), u16::MAX); + assert_eq!((!PodI16::from(0i16)).get(), -1i16); +} + +#[test] +fn pod_bitand_assign_native() { + let mut v = PodU32::from(0xFF00u32); + v &= 0x0FF0u32; + assert_eq!(v.get(), 0x0F00); +} + +#[test] +fn pod_bitand_assign_pod() { + let mut v = PodU32::from(0xFF00u32); + v &= PodU32::from(0x0FF0u32); + assert_eq!(v.get(), 0x0F00); +} + +#[test] +fn pod_bitor_assign_native() { + let mut v = PodU32::from(0xFF00u32); + v |= 0x00FFu32; + assert_eq!(v.get(), 0xFFFF); +} + +#[test] +fn pod_bitor_assign_pod() { + let mut v = PodU32::from(0xFF00u32); + v |= PodU32::from(0x00FFu32); + assert_eq!(v.get(), 0xFFFF); +} + +#[test] +fn pod_bitxor_assign_native() { + let mut v = PodU32::from(0xFFFFu32); + v ^= 0xFF00u32; + assert_eq!(v.get(), 0x00FF); +} + +#[test] +fn pod_bitxor_assign_pod() { + let mut v = PodU32::from(0xFFFFu32); + v ^= PodU32::from(0xFF00u32); + assert_eq!(v.get(), 0x00FF); +} + +#[test] +fn pod_shl_assign() { + let mut v = PodU32::from(1u32); + v <<= 4; + assert_eq!(v.get(), 16); +} + +#[test] +fn pod_shr_assign() { + let mut v = PodU32::from(16u32); + v >>= 4; + assert_eq!(v.get(), 1); +} + +#[test] +fn pod_neg_i16() { + assert_eq!((-PodI16::from(5i16)).get(), -5); + assert_eq!((-PodI16::from(-5i16)).get(), 5); + assert_eq!((-PodI16::from(0i16)).get(), 0); +} + +#[test] +fn pod_neg_i32() { + assert_eq!((-PodI32::from(42i32)).get(), -42); +} + +#[test] +fn pod_neg_i64() { + assert_eq!((-PodI64::from(100i64)).get(), -100); +} + +#[test] +fn pod_neg_i128() { + assert_eq!((-PodI128::from(999i128)).get(), -999); +} + +#[test] +fn pod_checked_add_ok() { + assert_eq!( + PodU64::from(10u64).checked_add(5u64), + Some(PodU64::from(15u64)) + ); +} + +#[test] +fn pod_checked_add_overflow() { + assert_eq!(PodU64::MAX.checked_add(1u64), None); +} + +#[test] +fn pod_checked_add_pod() { + assert_eq!( + PodU32::from(10u32).checked_add(PodU32::from(5u32)), + Some(PodU32::from(15u32)) + ); +} + +#[test] +fn pod_checked_sub_ok() { + assert_eq!( + PodU64::from(10u64).checked_sub(5u64), + Some(PodU64::from(5u64)) + ); +} + +#[test] +fn pod_checked_sub_underflow() { + assert_eq!(PodU64::from(5u64).checked_sub(10u64), None); +} + +#[test] +fn pod_checked_mul_ok() { + assert_eq!( + PodU64::from(6u64).checked_mul(7u64), + Some(PodU64::from(42u64)) + ); +} + +#[test] +fn pod_checked_mul_overflow() { + assert_eq!(PodU64::MAX.checked_mul(2u64), None); +} + +#[test] +fn pod_checked_div_ok() { + assert_eq!( + PodU64::from(42u64).checked_div(7u64), + Some(PodU64::from(6u64)) + ); +} + +#[test] +fn pod_checked_div_by_zero() { + assert_eq!(PodU64::from(42u64).checked_div(0u64), None); +} + +#[test] +fn pod_checked_signed_overflow() { + assert_eq!(PodI64::MIN.checked_sub(1i64), None); + assert_eq!(PodI64::MAX.checked_add(1i64), None); +} + +#[test] +fn pod_saturating_add() { + assert_eq!(PodU64::MAX.saturating_add(100u64), PodU64::MAX); + assert_eq!( + PodU64::from(10u64).saturating_add(5u64), + PodU64::from(15u64) + ); +} + +#[test] +fn pod_saturating_sub() { + assert_eq!(PodU64::from(5u64).saturating_sub(10u64), PodU64::ZERO); + assert_eq!(PodU64::from(10u64).saturating_sub(5u64), PodU64::from(5u64)); +} + +#[test] +fn pod_saturating_mul() { + assert_eq!(PodU64::MAX.saturating_mul(2u64), PodU64::MAX); + assert_eq!(PodU64::from(6u64).saturating_mul(7u64), PodU64::from(42u64)); +} + +#[test] +fn pod_saturating_signed() { + assert_eq!(PodI64::MAX.saturating_add(100i64), PodI64::MAX); + assert_eq!(PodI64::MIN.saturating_sub(100i64), PodI64::MIN); + assert_eq!(PodI64::MAX.saturating_mul(2i64), PodI64::MAX); + assert_eq!(PodI64::MIN.saturating_mul(2i64), PodI64::MIN); +} + +#[test] +fn pod_ordering() { + assert!(PodU64::from(10u64) > PodU64::from(5u64)); + assert!(PodU64::from(5u64) < PodU64::from(10u64)); + assert!(PodU64::from(5u64) == PodU64::from(5u64)); + + assert!(PodI64::from(-10i64) < PodI64::from(5i64)); + assert!(PodI64::from(5i64) > PodI64::from(-10i64)); +} + +#[test] +fn pod_partial_eq_native() { + assert!(PodU64::from(42u64) == 42u64); + assert!(PodI32::from(-5i32) == -5i32); + assert!(PodU64::from(42u64) != 43u64); +} + +#[test] +fn pod_partial_ord_native() { + assert!(PodU64::from(10u64) > 5u64); + assert!(PodU64::from(5u64) < 10u64); + assert!(PodI32::from(-10i32) < 0i32); +} + +#[test] +fn pod_display() { + assert_eq!(std::format!("{}", PodU64::from(42u64)), "42"); + assert_eq!(std::format!("{}", PodI32::from(-7i32)), "-7"); + assert_eq!(std::format!("{}", PodU128::from(0u128)), "0"); +} + +#[test] +fn pod_debug() { + assert_eq!(std::format!("{:?}", PodU64::from(42u64)), "PodU64(42)"); + assert_eq!(std::format!("{:?}", PodI32::from(-7i32)), "PodI32(-7)"); +} + +#[test] +fn pod_get_method() { + assert_eq!(PodU16::from(1337u16).get(), 1337); + assert_eq!(PodI16::from(-42i16).get(), -42); + assert_eq!(PodU32::from(0xDEAD_BEEFu32).get(), 0xDEAD_BEEF); + assert_eq!(PodI32::from(i32::MIN).get(), i32::MIN); + assert_eq!(PodU64::from(u64::MAX).get(), u64::MAX); + assert_eq!(PodI64::from(i64::MAX).get(), i64::MAX); + assert_eq!(PodU128::from(u128::MAX).get(), u128::MAX); + assert_eq!(PodI128::from(i128::MIN).get(), i128::MIN); +} + +#[test] +fn ergonomic_counter_increment() { + let mut count = PodU64::from(0u64); + count += 1u64; + assert_eq!(count.get(), 1); + count += 1u64; + assert_eq!(count.get(), 2); +} + +#[test] +fn ergonomic_balance_arithmetic() { + let mut balance = PodU64::from(1000u64); + let fee = PodU64::from(25u64); + balance -= fee; + assert_eq!(balance.get(), 975); +} diff --git a/crates/pina_pod_primitives/src/tests/pod_vec.rs b/crates/pina_pod_primitives/src/tests/pod_vec.rs new file mode 100644 index 00000000..9241e186 --- /dev/null +++ b/crates/pina_pod_primitives/src/tests/pod_vec.rs @@ -0,0 +1,76 @@ +use core::mem::size_of; + +use super::*; + +#[test] +fn pod_vec_empty() { + let v = PodVec::::default(); + assert!(v.is_empty()); + assert_eq!(v.len(), 0); + assert_eq!(v.capacity(), 10); + assert_eq!(v.as_slice(), &[] as &[PodU64]); +} + +#[test] +fn pod_vec_push_and_get() { + let mut v = PodVec::::default(); + assert!(v.try_push(PodU64::from(1u64)).is_ok()); + assert!(v.try_push(PodU64::from(2u64)).is_ok()); + assert_eq!(v.len(), 2); + assert_eq!(v.get(0), Some(&PodU64::from(1u64))); + assert_eq!(v.get(1), Some(&PodU64::from(2u64))); + assert_eq!(v.get(2), None); +} + +#[test] +fn pod_vec_as_slice() { + let mut v = PodVec::::default(); + v.push(PodU64::from(100u64)); + v.push(PodU64::from(200u64)); + let slice: Vec = v.as_slice().iter().map(|x| x.get()).collect(); + assert_eq!(slice, vec![100, 200]); +} + +#[test] +fn pod_vec_overflow_rejected() { + let mut v = PodVec::::default(); + assert!(v.try_push(PodU64::from(1u64)).is_ok()); + assert!(v.try_push(PodU64::from(2u64)).is_ok()); + assert!(v.try_push(PodU64::from(3u64)).is_err()); // at capacity + assert_eq!(v.len(), 2); +} + +#[test] +fn pod_vec_pop() { + let mut v = PodVec::::default(); + v.push(PodU64::from(42u64)); + v.push(PodU64::from(99u64)); + assert_eq!(v.pop(), Some(PodU64::from(99u64))); + assert_eq!(v.pop(), Some(PodU64::from(42u64))); + assert_eq!(v.pop(), None); +} + +#[test] +fn pod_vec_clear() { + let mut v = PodVec::::default(); + v.push(PodU64::from(1u64)); + v.push(PodU64::from(2u64)); + v.clear(); + assert!(v.is_empty()); + assert_eq!(v.len(), 0); +} + +#[test] +fn pod_vec_bytemuck_roundtrip() { + let mut v = PodVec::::default(); + v.push(PodU64::from(100u64)); + v.push(PodU64::from(200u64)); + let bytes: &[u8] = unsafe { + core::slice::from_raw_parts(&v as *const _ as *const u8, size_of::>()) + }; + assert_eq!(bytes[0..2], [2, 0]); // len = 2 in LE + let restored = unsafe { &*(bytes.as_ptr() as *const PodVec) }; + assert_eq!(restored.len(), 2); + assert_eq!(restored.get(0), Some(&PodU64::from(100u64))); + assert_eq!(restored.get(1), Some(&PodU64::from(200u64))); +} diff --git a/crates/pina_pod_primitives/src/tests/string.rs b/crates/pina_pod_primitives/src/tests/string.rs new file mode 100644 index 00000000..a862ef6c --- /dev/null +++ b/crates/pina_pod_primitives/src/tests/string.rs @@ -0,0 +1,58 @@ +use core::mem::size_of; + +use super::*; + +#[test] +fn pod_string_empty() { + let s = PodString::<32>::default(); + assert!(s.is_empty()); + assert_eq!(s.len(), 0); + assert_eq!(s.capacity(), 32); + assert_eq!(s.as_bytes(), b""); +} + +#[test] +fn pod_string_set_and_get() { + let mut s = PodString::<32>::default(); + assert!(s.try_set("hello").is_ok()); + assert_eq!(s.len(), 5); + assert_eq!(s.as_bytes(), b"hello"); + assert_eq!(s.try_as_str().unwrap(), "hello"); +} + +#[test] +fn pod_string_push_str() { + let mut s = PodString::<32>::default(); + s.set("hello"); + assert!(s.try_push_str(" world").is_ok()); + assert_eq!(s.try_as_str().unwrap(), "hello world"); +} + +#[test] +fn pod_string_overflow_rejected() { + let mut s = PodString::<4>::default(); + assert!(s.try_set("hello").is_err()); // 5 bytes > 4 capacity + assert!(s.is_empty()); // unchanged +} + +#[test] +fn pod_string_clear() { + let mut s = PodString::<32>::default(); + s.set("test"); + assert!(!s.is_empty()); + s.clear(); + assert!(s.is_empty()); +} + +#[test] +fn pod_string_bytemuck_roundtrip() { + let mut s = PodString::<32>::default(); + s.set("test"); + let bytes: &[u8] = unsafe { + core::slice::from_raw_parts(&s as *const _ as *const u8, size_of::>()) + }; + assert_eq!(bytes[0], 4); // len = 4 + assert_eq!(&bytes[1..5], b"test"); + let restored = unsafe { &*(bytes.as_ptr() as *const PodString<32>) }; + assert_eq!(restored.try_as_str().unwrap(), "test"); +} diff --git a/crates/pina_pod_primitives/src/vec.rs b/crates/pina_pod_primitives/src/vec.rs new file mode 100644 index 00000000..dad6375c --- /dev/null +++ b/crates/pina_pod_primitives/src/vec.rs @@ -0,0 +1,279 @@ +//! Fixed-capacity vector with length prefix. + +use core::fmt; +use core::mem::MaybeUninit; +use core::mem::align_of; +use core::mem::size_of; + +use bytemuck::Pod; +use bytemuck::Zeroable; + +use crate::PodU64; +use crate::error::PodCollectionError; +use crate::error::max_n_for_pfx; + +/// A fixed-capacity vector stored inline with a length prefix. +/// +/// Default prefix size is `2` bytes (u16), supporting up to 65,535 elements. +/// Use `PodVec` for up to 255 elements, etc. +/// +/// # Layout +/// - Bytes 0..PFX: element count prefix (little-endian) +/// - Bytes `PFX..PFX+(N*size_of::())`: element data (may be partially uninitialized) +#[repr(C)] +#[derive(Copy, Clone)] +pub struct PodVec { + len: [u8; PFX], + data: [MaybeUninit; N], +} + +// Compile-time validation of PFX +impl PodVec { + /// Use this const to trigger the compile-time assertions. + pub const VALID: () = Self::_CAP_CHECK; + const _CAP_CHECK: () = { + assert!( + PFX == 1 || PFX == 2 || PFX == 4 || PFX == 8, + "PodVec: PFX must be 1, 2, 4, or 8" + ); + assert!( + N <= max_n_for_pfx(PFX), + "PodVec: N exceeds the maximum value representable by the PFX-byte length \ + prefix" + ); + }; +} + +impl PodVec { + #[inline] + fn decode_len(&self) -> usize { + match PFX { + 1 => self.len[0] as usize, + 2 => u16::from_le_bytes([self.len[0], self.len[1]]) as usize, + 4 => u32::from_le_bytes([self.len[0], self.len[1], self.len[2], self.len[3]]) as usize, + 8 => { + u64::from_le_bytes([ + self.len[0], + self.len[1], + self.len[2], + self.len[3], + self.len[4], + self.len[5], + self.len[6], + self.len[7], + ]) as usize + } + _ => unreachable!(), + } + } + + #[inline] + fn encode_len(&mut self, n: usize) { + match PFX { + 1 => self.len[0] = n as u8, + 2 => { + let bytes = (n as u16).to_le_bytes(); + self.len.copy_from_slice(&bytes); + } + 4 => { + let bytes = (n as u32).to_le_bytes(); + self.len.copy_from_slice(&bytes); + } + 8 => { + let bytes = (n as u64).to_le_bytes(); + self.len.copy_from_slice(&bytes); + } + _ => unreachable!(), + } + } + + /// Returns the number of elements (clamped to capacity). + #[inline] + pub fn len(&self) -> usize { + self.decode_len().min(N) + } + + /// Returns `true` if the vector is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the maximum capacity. + pub const fn capacity(&self) -> usize { + N + } + + /// Returns a slice of the initialized elements. + pub fn as_slice(&self) -> &[T] { + let len = self.len(); + unsafe { core::slice::from_raw_parts(self.data.as_ptr().cast::(), len) } + } + + /// Returns a mutable slice of the initialized elements. + pub fn as_mut_slice(&mut self) -> &mut [T] { + let len = self.len(); + unsafe { core::slice::from_raw_parts_mut(self.data.as_mut_ptr().cast::(), len) } + } + + /// Returns the element at the given index. + pub fn get(&self, index: usize) -> Option<&T> { + if index < self.len() { + Some(unsafe { &*self.data.as_ptr().add(index).cast::() }) + } else { + None + } + } + + /// Returns a mutable reference to the element at the given index. + pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { + if index < self.len() { + Some(unsafe { &mut *self.data.as_mut_ptr().add(index).cast::() }) + } else { + None + } + } + + /// Pushes an element, returning error if at capacity. + pub fn try_push(&mut self, value: T) -> Result<(), PodCollectionError> { + let len = self.len(); + if len >= N { + return Err(PodCollectionError::Overflow); + } + unsafe { + self.data.as_mut_ptr().add(len).cast::().write(value); + } + self.encode_len(len + 1); + Ok(()) + } + + /// Pushes an element. + /// + /// Returns `false` if at capacity. + #[must_use = "returns false if at capacity"] + pub fn push(&mut self, value: T) -> bool { + self.try_push(value).is_ok() + } + + /// Pops the last element. + pub fn pop(&mut self) -> Option { + let len = self.len(); + if len == 0 { + return None; + } + let value = unsafe { self.data.as_ptr().add(len - 1).cast::().read() }; + self.encode_len(len - 1); + Some(value) + } + + /// Clears the vector (sets length to 0). + pub fn clear(&mut self) { + self.len = [0u8; PFX]; + } +} + +impl Default for PodVec { + fn default() -> Self { + Self { + len: [0u8; PFX], + data: [MaybeUninit::uninit(); N], + } + } +} + +impl core::ops::Deref for PodVec { + type Target = [T]; + + fn deref(&self) -> &[T] { + self.as_slice() + } +} + +impl core::ops::DerefMut for PodVec { + fn deref_mut(&mut self) -> &mut [T] { + self.as_mut_slice() + } +} + +impl AsRef<[T]> for PodVec { + fn as_ref(&self) -> &[T] { + self.as_slice() + } +} + +impl PartialEq for PodVec { + fn eq(&self, other: &Self) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl Eq for PodVec {} + +impl fmt::Debug for PodVec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.as_slice().iter()).finish() + } +} + +// SAFETY: PodVec is #[repr(C)] with len: [u8; PFX] + data: [MaybeUninit; N] where T: Pod. +// Both have align 1 and any bit pattern is valid. +unsafe impl Zeroable for PodVec {} +unsafe impl Pod for PodVec {} + +// Compile-time layout assertions +const _: () = assert!(align_of::>() == 1); +const _: () = assert!(size_of::>() == 2 + 10); +const _: () = assert!(size_of::>() == 2 + 80); + +// --------------------------------------------------------------------------- +// Kani model-checking proof harnesses +// --------------------------------------------------------------------------- + +#[cfg(kani)] +mod kani_proofs { + use super::*; + + #[kani::proof] + fn push_pop_roundtrip() { + let val: u8 = kani::any(); + let mut v = PodVec::::default(); + assert!(v.push(val)); + assert_eq!(v.len(), 1); + assert_eq!(v.pop(), Some(val)); + assert_eq!(v.len(), 0); + } + + #[kani::proof] + fn overflow_rejected() { + let mut v = PodVec::::default(); + v.push(1); + v.push(2); + assert!(!v.push(3)); // at capacity + assert_eq!(v.len(), 2); + } + + #[kani::proof] + fn empty_pop_returns_none() { + let mut v = PodVec::::default(); + assert_eq!(v.pop(), None); + } + + #[kani::proof] + fn clear_resets_len() { + let val: u8 = kani::any(); + let mut v = PodVec::::default(); + v.push(val); + v.clear(); + assert!(v.is_empty()); + assert_eq!(v.len(), 0); + } + + #[kani::proof] + fn get_in_bounds() { + let val: u8 = kani::any(); + let mut v = PodVec::::default(); + v.push(val); + assert_eq!(v.get(0), Some(&val)); + assert_eq!(v.get(1), None); + } +} diff --git a/docs/src/core-concepts.md b/docs/src/core-concepts.md index 83389762..c931e756 100644 --- a/docs/src/core-concepts.md +++ b/docs/src/core-concepts.md @@ -130,7 +130,7 @@ All types are `#[repr(transparent)]` over byte arrays (or `u8` for `PodBool`) an -Arithmetic operators (`+`, `-`, `*`) use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. +Arithmetic operators (`+`, `-`, `*`) on Pod **integer** types use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. @@ -143,6 +143,28 @@ my_account.count += 1u64; let fee = balance.checked_mul(3u64).unwrap_or(PodU64::MAX); ``` +## Pod collection types + + + +| Type | Purpose | Layout | +| -------------------------- | ---------------------- | ----------------------------------------- | +| `PodOption` | Fixed-size `Option` | 1-byte discriminant + `T` | +| `PodString` | Fixed-capacity string | `PFX`-byte length prefix + `N` data bytes | +| `PodVec` | Fixed-capacity vec | `PFX`-byte length prefix + `N` elements | + +All collection types are `#[repr(C)]`, alignment-1, and implement `bytemuck::Pod` + `bytemuck::Zeroable`. Length prefixes (`PFX`) default to 1 byte for strings (max 255) and 2 bytes for vectors (max 65 535 elements). + + + + + +Collection types store data inline with a length prefix, enabling zero-copy access inside `#[repr(C)]` account structs. Overflow is detected at insertion time — `try_set` / `try_push` return `Err(PodCollectionError::Overflow)` when capacity is exceeded. + +`PodString` provides UTF-8 validation via `try_as_str()`, while `PodVec` offers slice-based access via `as_slice()` / `as_mut_slice()`. `PodOption` mirrors the `Option` API with `get()`, `set()`, and `clear()`. + + + ## Instruction introspection diff --git a/docs/src/crates-and-features.md b/docs/src/crates-and-features.md index cdba1eba..cdf5bbb6 100644 --- a/docs/src/crates-and-features.md +++ b/docs/src/crates-and-features.md @@ -2,15 +2,15 @@ -| Crate | Path | Description | -| ---------------------- | ----------------------------- | ----------------------------------------------------------------- | -| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | -| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | -| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | -| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | -| `pina_pod_primitives` | `crates/pina_pod_primitives` | Alignment-safe `no_std` POD primitive wrappers. | -| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | -| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. | +| Crate | Path | Description | +| ---------------------- | ----------------------------- | ---------------------------------------------------------------------------- | +| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | +| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | +| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | +| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | +| `pina_pod_primitives` | `crates/pina_pod_primitives` | `no_std` POD primitives — integer/bool wrappers, fixed-capacity collections. | +| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | +| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. | @@ -78,16 +78,36 @@ Library surface: ## `crates/pina_pod_primitives` -`no_std` crate containing alignment-safe POD primitive wrappers (`PodBool`, `PodU*`, `PodI*`) and conversion macro helpers shared by `pina` and generated clients. +`no_std` crate containing alignment-safe POD primitive wrappers (`PodBool`, `PodU*`, `PodI*`) and fixed-capacity collection types (`PodOption`, `PodString`, `PodVec`) shared by `pina` and generated clients. -Arithmetic operators (`+`, `-`, `*`) use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. +Arithmetic operators (`+`, `-`, `*`) on Pod **integer** types use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. + + +| Type | Purpose | Layout | +| -------------------------- | ---------------------- | ----------------------------------------- | +| `PodOption` | Fixed-size `Option` | 1-byte discriminant + `T` | +| `PodString` | Fixed-capacity string | `PFX`-byte length prefix + `N` data bytes | +| `PodVec` | Fixed-capacity vec | `PFX`-byte length prefix + `N` elements | + +All collection types are `#[repr(C)]`, alignment-1, and implement `bytemuck::Pod` + `bytemuck::Zeroable`. Length prefixes (`PFX`) default to 1 byte for strings (max 255) and 2 bytes for vectors (max 65 535 elements). + + + + + +Collection types store data inline with a length prefix, enabling zero-copy access inside `#[repr(C)]` account structs. Overflow is detected at insertion time — `try_set` / `try_push` return `Err(PodCollectionError::Overflow)` when capacity is exceeded. + +`PodString` provides UTF-8 validation via `try_as_str()`, while `PodVec` offers slice-based access via `as_slice()` / `as_mut_slice()`. `PodOption` mirrors the `Option` API with `get()`, `set()`, and `clear()`. + + + ## `crates/pina_profile` diff --git a/readme.md b/readme.md index 801ca1d8..ed564bdc 100644 --- a/readme.md +++ b/readme.md @@ -32,15 +32,15 @@ A performant Solana smart contract framework built on top of [pinocchio](https:/ -| Crate | Path | Description | -| ---------------------- | ----------------------------- | ----------------------------------------------------------------- | -| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | -| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | -| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | -| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | -| `pina_pod_primitives` | `crates/pina_pod_primitives` | Alignment-safe `no_std` POD primitive wrappers. | -| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | -| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. | +| Crate | Path | Description | +| ---------------------- | ----------------------------- | ---------------------------------------------------------------------------- | +| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | +| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | +| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | +| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | +| `pina_pod_primitives` | `crates/pina_pod_primitives` | `no_std` POD primitives — integer/bool wrappers, fixed-capacity collections. | +| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | +| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. | @@ -569,12 +569,38 @@ let clamped = amount.saturating_add(PodU64::MAX); -Arithmetic operators (`+`, `-`, `*`) use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. +Arithmetic operators (`+`, `-`, `*`) on Pod **integer** types use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. +### Pod collection types + +
+ +Fixed-capacity collections that store data inline with a length prefix, enabling zero-copy access inside `#[repr(C)]` account structs. + + + +| Type | Purpose | Layout | +| -------------------------- | ---------------------- | ----------------------------------------- | +| `PodOption` | Fixed-size `Option` | 1-byte discriminant + `T` | +| `PodString` | Fixed-capacity string | `PFX`-byte length prefix + `N` data bytes | +| `PodVec` | Fixed-capacity vec | `PFX`-byte length prefix + `N` elements | + +All collection types are `#[repr(C)]`, alignment-1, and implement `bytemuck::Pod` + `bytemuck::Zeroable`. Length prefixes (`PFX`) default to 1 byte for strings (max 255) and 2 bytes for vectors (max 65 535 elements). + + + + + +Collection types store data inline with a length prefix, enabling zero-copy access inside `#[repr(C)]` account structs. Overflow is detected at insertion time — `try_set` / `try_push` return `Err(PodCollectionError::Overflow)` when capacity is exceeded. + +`PodString` provides UTF-8 validation via `try_as_str()`, while `PodVec` offers slice-based access via `as_slice()` / `as_mut_slice()`. `PodOption` mirrors the `Option` API with `get()`, `set()`, and `clear()`. + + + ### CPI helpers
@@ -691,15 +717,15 @@ The profiler decodes each SBF instruction opcode and assigns costs: regular inst -| Crate | Path | Description | -| ---------------------- | ----------------------------- | ----------------------------------------------------------------- | -| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | -| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | -| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | -| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | -| `pina_pod_primitives` | `crates/pina_pod_primitives` | Alignment-safe `no_std` POD primitive wrappers. | -| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | -| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. | +| Crate | Path | Description | +| ---------------------- | ----------------------------- | ---------------------------------------------------------------------------- | +| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | +| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | +| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | +| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | +| `pina_pod_primitives` | `crates/pina_pod_primitives` | `no_std` POD primitives — integer/bool wrappers, fixed-capacity collections. | +| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | +| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. | diff --git a/templates/pina-overview.t.md b/templates/pina-overview.t.md index a39212c3..9296041b 100644 --- a/templates/pina-overview.t.md +++ b/templates/pina-overview.t.md @@ -46,9 +46,29 @@ All types are `#[repr(transparent)]` over byte arrays (or `u8` for `PodBool`) an + + +| Type | Purpose | Layout | +| -------------------------- | ---------------------- | ----------------------------------------- | +| `PodOption` | Fixed-size `Option` | 1-byte discriminant + `T` | +| `PodString` | Fixed-capacity string | `PFX`-byte length prefix + `N` data bytes | +| `PodVec` | Fixed-capacity vec | `PFX`-byte length prefix + `N` elements | + +All collection types are `#[repr(C)]`, alignment-1, and implement `bytemuck::Pod` + `bytemuck::Zeroable`. Length prefixes (`PFX`) default to 1 byte for strings (max 255) and 2 bytes for vectors (max 65 535 elements). + + + + + +Collection types store data inline with a length prefix, enabling zero-copy access inside `#[repr(C)]` account structs. Overflow is detected at insertion time — `try_set` / `try_push` return `Err(PodCollectionError::Overflow)` when capacity is exceeded. + +`PodString` provides UTF-8 validation via `try_as_str()`, while `PodVec` offers slice-based access via `as_slice()` / `as_mut_slice()`. `PodOption` mirrors the `Option` API with `get()`, `set()`, and `clear()`. + + + -Arithmetic operators (`+`, `-`, `*`) use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. +Arithmetic operators (`+`, `-`, `*`) on Pod **integer** types use **wrapping** semantics in release builds for CU efficiency and **panic on overflow** in debug builds. Use `checked_add`, `checked_sub`, `checked_mul`, `checked_div` where overflow must be detected in all build profiles. Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. @@ -56,15 +76,15 @@ Each Pod integer type provides `ZERO`, `MIN`, and `MAX` constants. -| Crate | Path | Description | -| ---------------------- | ----------------------------- | ----------------------------------------------------------------- | -| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | -| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | -| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | -| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | -| `pina_pod_primitives` | `crates/pina_pod_primitives` | Alignment-safe `no_std` POD primitive wrappers. | -| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | -| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. | +| Crate | Path | Description | +| ---------------------- | ----------------------------- | ---------------------------------------------------------------------------- | +| `pina` | `crates/pina` | Core framework — traits, account loaders, CPI helpers, Pod types. | +| `pina_macros` | `crates/pina_macros` | Proc macros — `#[account]`, `#[instruction]`, `#[event]`, etc. | +| `pina_cli` | `crates/pina_cli` | CLI/library for IDL generation, Codama integration, scaffolding. | +| `pina_codama_renderer` | `crates/pina_codama_renderer` | Repository-local Codama Rust renderer for Pina-style clients. | +| `pina_pod_primitives` | `crates/pina_pod_primitives` | `no_std` POD primitives — integer/bool wrappers, fixed-capacity collections. | +| `pina_profile` | `crates/pina_profile` | Static CU profiler for compiled SBF programs. | +| `pina_sdk_ids` | `crates/pina_sdk_ids` | Typed constants for well-known Solana program/sysvar IDs. |