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

Permit ergonomically aborting on allocation failure #1659

Open
joshlf opened this issue Sep 15, 2024 · 0 comments
Open

Permit ergonomically aborting on allocation failure #1659

joshlf opened this issue Sep 15, 2024 · 0 comments

Comments

@joshlf
Copy link
Member

joshlf commented Sep 15, 2024

See also: #528

As of #1478 and #1658, allocation failures in our API are returned as Result::Err rather than being handled by aborting/panicking (e.g. via handle_alloc_error). This is maximally flexible, but introduces an ergonomics burden to users who prefer the old behavior.

Here are two possible designs to make this more ergonomic.

Provide handle_alloc_error on Result<T, AllocError>

Using this extension trait:

trait ResultAllocErrorExt<T> {
    fn unwrap_or_handle_alloc_error(self) -> T;
}

impl<T> ResultAllocErrorExt<T> for Result<T, AllocError> {
    fn unwrap_or_handle_alloc_error(self) -> T {
        match self {
            Ok(t) => t,
            Err(AllocError) => handle_alloc_error(todo!()),
        }
    } 
}

...a caller with a r: Result<T, AllocError> can just call r.unwrap_or_handle_alloc_error().

The downside to this approach is that handle_alloc_error requires a Layout argument. In order to supply this, we'd need to have AllocError carry a Layout, which would make our Results larger and add optimizer pressure.

Parametrize over the error type

We make allocation errors generic with an AllocError trait which can be constructed from a Layout. We also implement this trait for ! - this implementation diverges by calling handle_alloc_error when constructed.

trait AllocError {
    fn from_failed_allocation(layout: Layout) -> Self;
}

struct AllocErr;

impl AllocError for AllocErr {
    fn from_failed_allocation(_layout: Layout) -> Self {
        AllocErr
    }
}

impl AllocError for ! {
    fn from_failed_allocation(layout: Layout) -> Self {
        handle_alloc_error(layout);
    }
}

fn try_reserve<T, E: AllocError>(v: &mut Vec<T>, additional: usize) -> Result<(), E> {
    v.try_reserve(additional).map_err(|_| {
        let layout = unsafe { Layout::from_size_align_unchecked(0, 1) };
        E::from_failed_allocation(layout)
    })
}

A caller can write try_reserve::<_, !>(v, additional).into_ok() to recover the infallible behavior or try_reserve::<_, AllocErr>(v, additional) to keep the fallible behavior.

This has a number of downsides:

  • Since type defaults are not supported on functions, the caller must always specify the error type at the call site
  • We cannot always synthesize a valid Layout for a failed allocation (e.g., this try_reserve function could fail because the user passed an additional that would cause the Vec's size to overflow isize - this cannot be represented using Layout). As a result, we need from_failed_allocation to accept Layouts that might be basically nonsense placeholders like we pass from try_reserve.
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

No branches or pull requests

1 participant