Skip to content

Conversation

@arielb1
Copy link
Contributor

@arielb1 arielb1 commented Dec 3, 2025

Implement an error multi provider which allows retrieving multiple types from an Error without O(n^2) performance.

This does make the current Request API slightly slower in some cases because it ends up loading the type id from memory multiple times. I do think there are a few ways around this that require some compiler support - for example, we could somehow store the type id in the vtable. However, the current Request API is very rarely hot so I do not think this is a big problem, and it will not actually become super-slow, it will just perform several reads and branches which are normally fairly fast operations.

cc https://rust-lang.zulipchat.com/#narrow/channel/219381-t-libs/topic/Pushing.20error_generic_member_access.20forward/with/538480946
cc #99301

This still needs to be cleaned up a little, and I'm not that sure this is the best API, but it works and I want to put it for review

r? @the8472

@rustbot
Copy link
Collaborator

rustbot commented Dec 3, 2025

rust-analyzer is developed in its own repository. If possible, consider making this change to rust-lang/rust-analyzer instead.

cc @rust-lang/rust-analyzer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. T-rust-analyzer Relevant to the rust-analyzer team, which will review and decide on the PR/issue. labels Dec 3, 2025
@programmerjake

This comment was marked as resolved.

@arielb1
Copy link
Contributor Author

arielb1 commented Dec 3, 2025

API Sketch

core::error gets the following new sealed traits:

  1. MultiResponse - represents a response containing values from multiple types, and allows fecthing these responses
  2. for<'a> IntoMultiRequest<'a>, representing the types, which does not have functions that are usable by users, but is rather used to represent the "type chain" (if we decide we stabilize this API only once we have variadic generics, we could replace it with a tuple of "values/references").

The MultiResponse trait has the following accessors:

    fn retrieve_ref<R: ?Sized + 'static>(&mut self, fulfil: impl FnOnce(&'a R)) -> &mut Self
    where
        R: ?Sized + 'static;
    fn retrieve_value<V: 'static>(&mut self, fulfil: impl FnOnce(V)) -> &mut Self

and the following new structs, all PhantomData-like:

  1. MultiRequestBuilder<T: IntoMultiRequest>
  2. EmptyMultiRequestBuilder: IntoMultiRequest
  3. ChainRefMultiRequestBuilder<R: 'static + ?Sized, NEXT: IntoMultiRequest> : IntoMultiRequest
  4. ChainValMultiRequestBuilder<V: 'static, NEXT: IntoMultiRequest>: IntoMultiRequest

EmptyMultiRequestBuilder, ChainRefMultiRequestBuilder and ChainValMultiRequestBuilder don't have any methods on them and are only supposed to be used as the type parameter for MultiRequestBuilder.

MultiRequestBuilder has the following functions which allow building it up:

MultiRequestBuilder::<EmptyMultiRequestBuilder>::new() -> Self
MultiRequestBuilder::<T>::with_value::<V: 'static>(&self) -> MultiRequestBuilder<ChainValMultiRequestBuilder<V, T>>
MultiRequestBuilder::<T>::with_ref::<R: 'static + ?Sized>(&self) -> MultiRequestBuilder<ChainRefMultiRequestBuilder<R, T>>

MultiRequestBuilder<T> also has a request function:

MultiRequestBuilder::<T>::request::<'a>(self, err: &'a (impl Error + ?Sized)) -> impl MultiResponse<'a> 

The request function calls the error's provide function to gather up all the responses requested in the request, and packs them into a MultiResponse.

EmptyMultiResponse , ChainRefMultiResponse and ChainValMultiResponse are only supposed to be used behind the impl MultiResponse and are not really user-visible.

An example use looks like:

#![feature(error_generic_member_access)]
use core::fmt;
use core::error::{Error, MultiResponse, Request};

#[derive(Debug)]
struct MyError {
    str_field: &'static str,
    val_field: MyExitCode,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct MyExitCode(u32);

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Example Error")
    }
}

impl Error for MyError {
    fn provide<'a>(&'a self, request: &mut Request<'a>) {
        request
            .provide_ref::<str>(self.str_field)
            .provide_value::<MyExitCode>(self.val_field);
    }
}

fn main() {
    let e = MyError {
        str_field: "hello",
        val_field: MyExitCode(3),
    };

    let mut str_val = None;
    let mut exit_code_val = None;
    let mut string_val = None;
    let mut value = core::error::MultiRequestBuilder::new()
        // request by reference
        .with_ref::<str>()
        // and by value
        .with_value::<MyExitCode>()
        // and some type that isn't in the error
        .with_value::<String>()
        .request(&e)
        // The error has str by reference
        .retrieve_ref::<str>(|val| str_val = Some(val))
        // The error has MyExitCode by value
        .retrieve_value::<MyExitCode>(|val| exit_code_val = Some(val))
        // The error does not have a string field, consume will not be called
        .retrieve_value::<String>(|val| string_val = Some(val));

    assert_eq!(exit_code_val, Some(MyExitCode(3)));
    assert_eq!(str_val, Some("hello"));
    assert_eq!(string_val, None);
}

Alternatives

  1. We could have ChainValMultiResponseBuilder/ChainRefMultiResponseBuilder not be phantoms but rather hold a reference to an Option. This would mean that the builder is not "reusable" but you would not need the "extractor" functions.

@the8472 the8472 added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Dec 3, 2025
@arielb1
Copy link
Contributor Author

arielb1 commented Dec 3, 2025

done the rebase

@programmerjake

This comment was marked as resolved.

@arielb1
Copy link
Contributor Author

arielb1 commented Dec 3, 2025

it still has the issue that git thinks all the code in error/mod.rs is new (breaking git blame) because you don't have a separate commit that only renames error.rs -> error/mod.rs

Will do that

@arielb1
Copy link
Contributor Author

arielb1 commented Dec 3, 2025

The alternative would look like this:

#![feature(error_generic_member_access)]
use core::fmt;
use core::error::{Error, MultiResponse, Request};

#[derive(Debug)]
struct MyError {
    str_field: &'static str,
    val_field: MyExitCode,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct MyExitCode(u32);

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Example Error")
    }
}

impl Error for MyError {
    fn provide<'a>(&'a self, request: &mut Request<'a>) {
        request
            .provide_ref::<str>(self.str_field)
            .provide_value::<MyExitCode>(self.val_field);
    }
}

fn main() {
    let e = MyError {
        str_field: "hello",
        val_field: MyExitCode(3),
    };

    let mut str_val = None;
    let mut exit_code_val = None;
    let mut string_val = None;
    let mut value = core::error::MultiRequestBuilder::new()
        // request by reference
        .with_ref::<str>(&mut str_val)
        // and by value
        .with_value::<MyExitCode>(&mut exit_code_val)
        // and some type that isn't in the error
        .with_value::<String>(&mut string_val)
        .request(&e);
    assert_eq!(exit_code_val, Some(MyExitCode(3)));
    assert_eq!(str_val, Some("hello"));
    assert_eq!(string_val, None);
}

Does feel somewhat better. Should we switch? In some sense like the builder not having mutable references to things, but this does feel OK.

@rust-log-analyzer

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-libs-api-nominated Nominated for discussion during a libs-api team meeting. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. T-rust-analyzer Relevant to the rust-analyzer team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants