Skip to content

Add support for ListView #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Add support for ListView #92

wants to merge 3 commits into from

Conversation

grod220
Copy link
Member

@grod220 grod220 commented Jun 6, 2025

Currently, PodSliceMut has served as the data structure for dynamically sized list. As we expand functionality to this, it is starting to feel misnamed (as Slices generally aren't things that are resized).

Changes made

  • Add new ListView struct that serves as the future home for mutations involving ordered lists
  • Copy over methods 1:1 from PodSliceMut: unpack, init, push
  • Add new remove methods: remove_at, remove_first_where, len + new tests
  • Mark PodSliceMut as deprecated

pod/src/list.rs Outdated
Comment on lines 37 to 127
fn unpack_internal<'a>(data: &'a mut [u8], init: bool) -> Result<Self, ProgramError>
where
'a: 'data,
{
if data.len() < LENGTH_SIZE {
return Err(PodSliceError::BufferTooSmall.into());
}
let (length, data) = data.split_at_mut(LENGTH_SIZE);
let length = pod_from_bytes_mut::<PodU32>(length)?;
if init {
*length = 0.into();
}
let max_length = max_len_for_type::<T>(data.len(), u32::from(*length) as usize)?;
let data = pod_slice_from_bytes_mut(data)?;
Ok(Self {
length,
data,
max_length,
})
}

/// Unpack the mutable buffer into a mutable slice
pub fn unpack<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ false)
}

/// Unpack the mutable buffer into a mutable slice, and initialize the
/// slice to 0-length
pub fn init<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ true)
}

/// Add another item to the slice
pub fn push(&mut self, t: T) -> Result<(), ProgramError> {
let length = u32::from(*self.length);
if length as usize == self.max_length {
Err(PodSliceError::BufferTooSmall.into())
} else {
self.data[length as usize] = t;
*self.length = length.saturating_add(1).into();
Ok(())
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are 1:1 copied from PodSliceMut

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually hoping we could turn this into free functions and that way we don't have to maintain two at once, but instead we can just call into them from each API. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like ListView is a superset of PodSliceMut functionality. Looks like it can just use the new thing under the hood with the same effect. Have a look and let me know what you think.

@grod220 grod220 force-pushed the podslice-remove-methods branch 3 times, most recently from 4f972f1 to 817eb9f Compare June 6, 2025 12:33
@grod220 grod220 requested review from febo, joncinque and buffalojoec June 6, 2025 12:36
Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly small points, looks good to me!

As another naming idea, what we're really providing is a View on existing data, so it's like a Vec that won't reallocate, which also similar to an arena allocator.

So we could go with ListView or NoAllocVec or ArenaList. I kind of like the last one, since it might describe the situation best.

pod/src/list.rs Outdated
/// ## Memory Layout
///
/// The structure assumes the underlying byte buffer is formatted as follows:
/// 1. **Length**: A `u32` value (`PodU32`) at the beginning of the buffer,
Copy link
Contributor

@febo febo Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having 4 bytes as a "header" shifts the offset for the array of Ts to only accept types with alignments 1, 2, or 4 in the case that the byte array has alignment 8. It would be more flexible to have 8 bytes (PodU64) so the offset is then compatible with 1, 2, 4 and 8. Another alternative is to have the length as a generic type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should probably be a generic, so that we can offer a way for existing implementations dependent on PodSlice/PodSliceMut to reliably read their data, which is going to be serialized with a u32 length prefix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added generic for Length 👍

Copy link
Contributor

@febo febo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very nice – left a few comments. The main one is about the alignment. It would be nice if we could make it more flexible to support types 8-byte aligned.

@febo
Copy link
Contributor

febo commented Jun 7, 2025

So we could go with ListView or NoAllocVec or ArenaList. I kind of like the last one, since it might describe the situation best.

I have a slight preference for ListView – I am not sure if users will make the connection between arena allocator and ArenaList. 😊

@buffalojoec
Copy link
Contributor

So we could go with ListView or NoAllocVec or ArenaList. I kind of like the last one, since it might describe the situation best.

I have a slight preference for ListView – I am not sure if users will make the connection between arena allocator and ArenaList. 😊

Agreed, I personally have never heard the term "arena" in this context before, and I assume most developers will whiff on the underlying meaning at first. However, "view" implies readonly, and that's not the case here.

Here are a few ideas:

  • PodList (I think it's actually ok)
  • StackList
  • BoxList
  • Dynamic + any of the above
  • PodSpan

pod/src/list.rs Outdated
Comment on lines 37 to 127
fn unpack_internal<'a>(data: &'a mut [u8], init: bool) -> Result<Self, ProgramError>
where
'a: 'data,
{
if data.len() < LENGTH_SIZE {
return Err(PodSliceError::BufferTooSmall.into());
}
let (length, data) = data.split_at_mut(LENGTH_SIZE);
let length = pod_from_bytes_mut::<PodU32>(length)?;
if init {
*length = 0.into();
}
let max_length = max_len_for_type::<T>(data.len(), u32::from(*length) as usize)?;
let data = pod_slice_from_bytes_mut(data)?;
Ok(Self {
length,
data,
max_length,
})
}

/// Unpack the mutable buffer into a mutable slice
pub fn unpack<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ false)
}

/// Unpack the mutable buffer into a mutable slice, and initialize the
/// slice to 0-length
pub fn init<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ true)
}

/// Add another item to the slice
pub fn push(&mut self, t: T) -> Result<(), ProgramError> {
let length = u32::from(*self.length);
if length as usize == self.max_length {
Err(PodSliceError::BufferTooSmall.into())
} else {
self.data[length as usize] = t;
*self.length = length.saturating_add(1).into();
Ok(())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually hoping we could turn this into free functions and that way we don't have to maintain two at once, but instead we can just call into them from each API. What do you think?

Comment on lines +127 to +194
#[cfg(test)]
mod tests {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building on my other comment about the reusable free functions, we could probably either test just the free functions (in one place) or make the tests generic over each collection API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If PodSliceMut uses ListView under the hood, is seems like it should get same coverage guarantees.

pod/src/slice.rs Outdated
Comment on lines 52 to 54
#[deprecated(
since = "0.6.0",
note = "This struct will be removed in the next major release (1.0.0). Please use `PodList` instead."
)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if deprecation is really necessary here. I don't really see any harm in leaving the slice-based ones alone, even if the mutable one isn't super aptly-named. I could be swayed here, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that if we're deprecating then I suppose we don't need the free functions, but it may not be a bad idea regardless, especially if we ever build more list-based APIs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, given the functionality overlap, I'm in favor of sunsetting it so we don't need to maintain both.

@febo
Copy link
Contributor

febo commented Jun 7, 2025

So we could go with ListView or NoAllocVec or ArenaList. I kind of like the last one, since it might describe the situation best.

I have a slight preference for ListView – I am not sure if users will make the connection between arena allocator and ArenaList. 😊

Agreed, I personally have never heard the term "arena" in this context before, and I assume most developers will whiff on the underlying meaning at first. However, "view" implies readonly, and that's not the case here.

Here are a few ideas:

  • PodList (I think it's actually ok)
  • StackList
  • BoxList
  • Dynamic + any of the above
  • PodSpan

Agree with your point on the "View" implying read-only, but while the type has a "dynamic" size its capacity is fixed. In that sense it is a view. 😊

The only concern that I have with using Pod* is that the type itself is not Pod, so it might look a bit odd.

Copy link
Member Author

@grod220 grod220 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay on this! Wasn't quite sure we were going to need this until recent confirmations in the Unstake program.

pod/src/list.rs Outdated
/// ## Memory Layout
///
/// The structure assumes the underlying byte buffer is formatted as follows:
/// 1. **Length**: A `u32` value (`PodU32`) at the beginning of the buffer,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added generic for Length 👍

pod/src/list.rs Outdated
Comment on lines 37 to 127
fn unpack_internal<'a>(data: &'a mut [u8], init: bool) -> Result<Self, ProgramError>
where
'a: 'data,
{
if data.len() < LENGTH_SIZE {
return Err(PodSliceError::BufferTooSmall.into());
}
let (length, data) = data.split_at_mut(LENGTH_SIZE);
let length = pod_from_bytes_mut::<PodU32>(length)?;
if init {
*length = 0.into();
}
let max_length = max_len_for_type::<T>(data.len(), u32::from(*length) as usize)?;
let data = pod_slice_from_bytes_mut(data)?;
Ok(Self {
length,
data,
max_length,
})
}

/// Unpack the mutable buffer into a mutable slice
pub fn unpack<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ false)
}

/// Unpack the mutable buffer into a mutable slice, and initialize the
/// slice to 0-length
pub fn init<'a>(data: &'a mut [u8]) -> Result<Self, ProgramError>
where
'a: 'data,
{
Self::unpack_internal(data, /* init */ true)
}

/// Add another item to the slice
pub fn push(&mut self, t: T) -> Result<(), ProgramError> {
let length = u32::from(*self.length);
if length as usize == self.max_length {
Err(PodSliceError::BufferTooSmall.into())
} else {
self.data[length as usize] = t;
*self.length = length.saturating_add(1).into();
Ok(())
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like ListView is a superset of PodSliceMut functionality. Looks like it can just use the new thing under the hood with the same effect. Have a look and let me know what you think.

Comment on lines +127 to +194
#[cfg(test)]
mod tests {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If PodSliceMut uses ListView under the hood, is seems like it should get same coverage guarantees.

pod/src/slice.rs Outdated
Comment on lines 52 to 54
#[deprecated(
since = "0.6.0",
note = "This struct will be removed in the next major release (1.0.0). Please use `PodList` instead."
)]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, given the functionality overlap, I'm in favor of sunsetting it so we don't need to maintain both.

@grod220 grod220 force-pushed the podslice-remove-methods branch from 817eb9f to 56057f1 Compare July 9, 2025 11:53
@grod220 grod220 changed the title Add support for PodList Add support for ListView Jul 9, 2025
@grod220 grod220 requested review from buffalojoec, joncinque and febo July 9, 2025 12:21
Copy link
Contributor

@buffalojoec buffalojoec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!! This is looking fantastic, and the diagrams under unpack_internal are really an elegant touch for helping follow the slice processing.

}
}

/// A mutable, variable-length collection of `Pod` types backed by a byte buffer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last bit "backed by a byte buffer" isn't super clear. How about something like this?

"An API for interpreting a raw buffer (&[u8]) as a mutable, variable-length collection of Pod elements."

Comment on lines +39 to +40
/// such as Solana programs, allowing for dynamic-length data structures within a
/// fixed-size account.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// such as Solana programs, allowing for dynamic-length data structures within a
/// fixed-size account.
/// such as Solana programs, allowing for efficient reads and manipulation of
/// dynamic-length data structures.

Comment on lines +130 to +135
/// Trait for types that can be used as length fields in Pod data structures
pub trait PodLength: bytemuck::Pod + Copy {
fn as_usize(&self) -> usize;

fn from_usize(val: usize) -> Result<Self, crate::error::PodSliceError>;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bytemuck already requires Copy for Pod: https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html

Also, I don't think this belongs in here. I think PodLength should have its own module.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these methods just Into<usize> and TryFrom<usize> after all? Why not just add those to the trait constraints?

pub trait PodLength: bytemuck::Pod + Into<usize> + TryFrom<usize> {}

u16::from(*self) as usize
}

fn from_usize(val: usize) -> Result<Self, crate::error::PodSliceError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn from_usize(val: usize) -> Result<Self, crate::error::PodSliceError> {
fn try_from_usize(val: usize) -> Result<Self, crate::error::PodSliceError> {

length: &'data mut PodU32,
data: &'data mut [T],
max_length: usize,
inner: ListView<'data, T, PodU32>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This deprecation strategy works for me!

Comment on lines +20 to +29
let remainder = length_size
.checked_rem(data_align)
.ok_or(ProgramError::ArithmeticOverflow)?;
if remainder == 0 {
Ok(0)
} else {
data_align
.checked_sub(remainder)
.ok_or(ProgramError::ArithmeticOverflow)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block could be simplified:

Suggested change
let remainder = length_size
.checked_rem(data_align)
.ok_or(ProgramError::ArithmeticOverflow)?;
if remainder == 0 {
Ok(0)
} else {
data_align
.checked_sub(remainder)
.ok_or(ProgramError::ArithmeticOverflow)
}
length_size
.checked_rem(data_align)
.and_then(|rem| data_align.checked_sub(rem))
.ok_or(ProgramError::ArithmeticOverflow)

}

/// Add another item to the slice
pub fn push(&mut self, t: T) -> Result<(), ProgramError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is an outward-facing API method:

Suggested change
pub fn push(&mut self, t: T) -> Result<(), ProgramError> {
pub fn push(&mut self, item: T) -> Result<(), ProgramError> {

/// Add another item to the slice
pub fn push(&mut self, t: T) -> Result<(), ProgramError> {
let length = self.length.as_usize();
if length == self.max_length {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we can't really end up here because of how unpack works, but maybe >= is safer?


/// Find the first element that satisfies `predicate` and remove it,
/// returning the element.
pub fn remove_first_where<P>(&mut self, predicate: P) -> Result<T, ProgramError>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can be swayed on this, but I would actually say we don't offer this method. It's not bad to implement on the caller side.


/// Remove and return the element at `index`, shifting all later
/// elements one position to the left.
pub fn remove_at(&mut self, index: usize) -> Result<T, ProgramError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just call this remove, since it behaves exactly like Vec::remove!

Suggested change
pub fn remove_at(&mut self, index: usize) -> Result<T, ProgramError> {
pub fn remove(&mut self, index: usize) -> Result<T, ProgramError> {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants