Skip to content
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

Add a test to the CircularBuffer exercise to check if elements in the buffer are dropped when the buffer is dropped #1842

Merged
merged 1 commit into from
Dec 23, 2023

Conversation

LarsKue
Copy link
Contributor

@LarsKue LarsKue commented Dec 22, 2023

A common solution to this exercise is to use MaybeUninit<T> in the buffer. However, MaybeUninit<T> explicitly never calls Ts drop code.

A sound solution should drop elements in the buffer when it is dropped. This test checks for that.

Copy link
Contributor

Hello. Thanks for opening a PR on Exercism 🙂

We ask that all changes to Exercism are discussed on our Community Forum before being opened on GitHub. To enforce this, we automatically close all PRs that are submitted. That doesn't mean your PR is rejected but that we want the initial discussion about it to happen on our forum where a wide range of key contributors across the Exercism ecosystem can weigh in.

You can use this link to copy this into a new topic on the forum. If we decide the PR is appropriate, we'll reopen it and continue with it, so please don't delete your local branch.

If you're interested in learning more about this auto-responder, please read this blog post.


Note: If this PR has been pre-approved, please link back to this PR on the forum thread and a maintainer or staff member will reopen it.

@github-actions github-actions bot closed this Dec 22, 2023
@LarsKue
Copy link
Contributor Author

LarsKue commented Dec 23, 2023

As per request on the forum post, here is a solution that passes the current test suite but not the new test:

#![feature(non_null_convenience)]

use std::mem::MaybeUninit;
use std::ptr::NonNull;

#[derive(Debug)]
pub struct CircularBuffer<T> {
    origin: Box<[MaybeUninit<T>]>,
    reader: NonNull<MaybeUninit<T>>,
    writer: NonNull<MaybeUninit<T>>,
    len: usize,
    cap: usize,
}

#[derive(Debug, PartialEq, Eq)]
pub enum Error {
    EmptyBuffer,
    FullBuffer,
}

impl<T> CircularBuffer<T> {
    pub fn new(capacity: usize) -> Self {
        let mut origin = Self::new_origin(capacity);
        let reader = unsafe { NonNull::new_unchecked(origin.as_mut_ptr()) };
        let writer = reader.clone();

        Self {
            origin,
            reader,
            writer,
            len: 0,
            cap: capacity,
        }
    }

    fn new_origin(capacity: usize) -> Box<[MaybeUninit<T>]> {
        let layout = std::alloc::Layout::array::<T>(capacity).unwrap();
        let ptr = unsafe { std::alloc::alloc(layout) as *mut MaybeUninit<T> };
        unsafe { Box::from_raw(std::slice::from_raw_parts_mut(ptr, capacity)) }
    }

    pub fn write(&mut self, element: T) -> Result<(), Error> {
        if self.is_full() {
            return Err(Error::FullBuffer);
        }

        unsafe {
            // write element
            // safety: we know the writer points into the buffer
            self.writer.write(MaybeUninit::new(element));

            // advance writer
            // safety: use wrapping offset so the writer remains in the buffer
            self.writer = NonNull::new_unchecked(self.writer.as_ptr().wrapping_offset(1));
        }

        self.len += 1;

        Ok(())
    }

    pub fn read(&mut self) -> Result<T, Error> {
        if self.is_empty() {
            return Err(Error::EmptyBuffer);
        }

        let element;
        unsafe {
            // read element
            // safety: we know the reader points into the buffer
            // safety: we know the memory at the reader is initialized because the buffer is not empty
            element = self.reader.as_ptr().read().assume_init();

            // advance reader
            // safety: use wrapping offset so the reader remains in the buffer
            self.reader = NonNull::new_unchecked(self.reader.as_ptr().wrapping_offset(1));
        }

        self.len -= 1;

        Ok(element)
    }

    pub fn clear(&mut self) {
        while let Ok(_) = self.read() { }
    }

    pub fn overwrite(&mut self, element: T) {
        if !self.is_full() {
            self.write(element).unwrap();
            return;
        }

        unsafe {
            // write element
            // safety: we know the writer points into the buffer
            let _ = std::mem::replace(&mut *self.writer.as_ptr(), MaybeUninit::new(element));

            // advance writer
            // safety: use wrapping offset so the writer remains in the buffer
            self.writer = NonNull::new_unchecked(self.writer.as_ptr().wrapping_offset(1));

            // advance reader
            // safety: use wrapping offset so the reader remains in the buffer
            self.reader = NonNull::new_unchecked(self.reader.as_ptr().wrapping_offset(1));
        }
    }

    pub fn is_full(&self) -> bool {
        self.len == self.cap
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn capacity(&self) -> usize {
        self.cap
    }
}

/// Without this the new test is not passed
// impl<T> Drop for CircularBuffer<T> {
//     fn drop(&mut self) {
//         self.clear();
//     }
// }

@senekor senekor reopened this Dec 23, 2023
@senekor
Copy link
Contributor

senekor commented Dec 23, 2023

forum post for reference

@senekor senekor merged commit 39163d0 into exercism:main Dec 23, 2023
11 checks passed
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.

2 participants