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::provide 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) -> Option<R>
    where
        R: ?Sized + 'static;
    fn retrieve_value<V: 'static>(&mut self) -> Option<V>

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 request= 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);

    assert_eq!(request.retrieve_value::<MyExitCode>(, Some(MyExitCode(3)));
    assert_eq!(request.retrieve_ref::<str>(), Some("hello"));
    assert_eq!(request.retrieve_value::<String>(), 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.

@arielb1
Copy link
Contributor Author

arielb1 commented Dec 9, 2025

Updated API to avoid the unneeded fulfil functions. Thanks @shepmaster ! (The original reason I did it was because I had problems with private-in-public lints, which I now managed to avoid )

@arielb1 arielb1 changed the title [semi-wip] implement error multi provider implement error multi provider Dec 9, 2025
@arielb1
Copy link
Contributor Author

arielb1 commented Dec 9, 2025

Moved the multiprovider to the provide module

@rustbot
Copy link
Collaborator

rustbot commented Dec 11, 2025

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

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