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 Provider API for context information on errors (and possibly other things) #133

Merged
merged 9 commits into from
Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/engine/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/engine/lib/provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "provider"
version = "0.0.0"
edition = "2021"
description = "`Provider` trait and accompanying API"
publish = false
65 changes: 65 additions & 0 deletions packages/engine/lib/provider/src/internal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Internal API

use super::{TypeId, TypeTag};

/// Sealed trait representing a type-erased tagged object.
///
/// # Safety
///
/// This trait must be exclusively implemented by [`TagValue`] as [`Tagged::is`] relies on `tag_id`
/// and the [`TypeId`] must not be overwritten.
///
/// [`Tagged::is`]: #method.is
pub(super) unsafe trait Tagged<'p>: 'p {
TimDiekmann marked this conversation as resolved.
Show resolved Hide resolved
/// The [`TypeId`] of the [`TypeTag`] this value was tagged with.
fn tag_id(&self) -> TypeId;
}

/// A concrete tagged value for a given tag `I`.
///
/// This is the only type which implements the [`Tagged`] trait, and encodes additional information
/// about the specific [`TypeTag`] into the type. This allows for multiple different tags to support
/// overlapping value ranges, for example, both the [`Ref<str>`] and [`Value<&'static str>`] tags
/// can be used to tag a value of type [`&'static str`].
///
/// [`Ref<str>`]: crate::tags::Ref
/// [`Value<&'static str>`]: crate::tags::Value
/// [`&'static str`]: str
#[repr(transparent)]
pub(super) struct TagValue<'p, I: TypeTag<'p>>(pub(super) I::Type);

unsafe impl<'p, I> Tagged<'p> for TagValue<'p, I>
where
I: TypeTag<'p>,
{
fn tag_id(&self) -> TypeId {
TypeId::of::<I>()
}
}

impl<'p> dyn Tagged<'p> {
/// Returns `true` if the dynamic type is tagged with `I`.
#[inline]
pub(super) fn is<I>(&self) -> bool
where
I: TypeTag<'p>,
{
self.tag_id() == TypeId::of::<I>()
}

/// Returns some reference to the dynamic value if it is tagged with `I`, or [`None`] if it
/// isn't.
#[inline]
pub(super) fn downcast_mut<I>(&mut self) -> Option<&mut TagValue<'p, I>>
where
I: TypeTag<'p>,
{
if self.is::<I>() {
let tag_value = (self as *mut Self).cast::<TagValue<'p, I>>();
// SAFETY: Just checked whether we're pointing to a `TagValue<'p, I>`
unsafe { Some(&mut *tag_value) }
} else {
None
}
}
}
253 changes: 253 additions & 0 deletions packages/engine/lib/provider/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
//! Contains the [`Provider`] trait and accompanying API, which enable trait objects to provide data
//! based on typed requests, an alternate form of runtime reflection.
//!
//! [`Provider`] and the associated APIs support generic, type-driven access to data, and a
//! mechanism for implementers to provide such data. The key parts of the interface are the
//! [`Provider`] trait for objects which can provide data, and the [`request_by_type_tag`] function
//! for data from an object which implements [`Provider`]. Note that end users should not call
//! requesting [`request_by_type_tag`] directly, it is a helper function for intermediate
//! implementers to use to implement a user-facing interface.
//!
//! Typically, a data provider is a trait object of a trait which extends [`Provider`]. A user will
//! request data from the trait object by specifying the type or a type tag (a type tag is a type
//! used only as a type parameter to identify the type which the user wants to receive).
//!
//! ## Data flow
//!
//! * A user requests an object, which is delegated to [`request_by_type_tag`]
//! * [`request_by_type_tag`] creates a [`Requisition`] object and passes it to
//! [`Provider::provide`]
//! * The object provider's implementation of `Provider::provide` tries providing values of
//! different types using `Requisition::provide_*`. If the type tag matches the type requested by
//! the user, it will be stored in the [`Requisition`] object.
//! * [`request_by_type_tag`] unpacks the [`Requisition`] object and returns any stored value to the
//! user.
//!
//! # Examples
// Taken from https://github.com/rust-lang/rfcs/pull/3192
//!
//! To provide data for example on an error type, the [`Provider`] API enables:
//!
//! ```rust
//! # #![feature(backtrace)]
//! use std::backtrace::Backtrace;
//!
//! use provider::{tags, Provider, Requisition, TypeTag};
//!
//! struct MyError {
//! backtrace: Backtrace,
//! suggestion: String,
//! }
//!
//! impl Provider for MyError {
//! fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) {
//! req.provide_ref(&self.backtrace)
//! .provide_ref(self.suggestion.as_str());
//! }
//! }
//!
//! trait MyErrorTrait: Provider {}
//!
//! impl MyErrorTrait for MyError {}
//!
//! impl dyn MyErrorTrait {
//! fn request_ref<T: ?Sized + 'static>(&self) -> Option<&T> {
//! provider::request_by_type_tag::<'_, tags::Ref<T>, _>(self)
//! }
//! }
//! ```
//!
//! In another module or crate, this can be requested for any `dyn MyErrorTrait`, not just
//! `MyError`:
//! ```rust
//! # #![feature(backtrace)]
//! # use std::backtrace::Backtrace;
//! # use provider::{Provider, Requisition, TypeTag, tags};
//! # struct MyError { backtrace: Backtrace, suggestion: String }
//! # impl Provider for MyError {
//! # fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) {
//! # req.provide_ref(&self.backtrace)
//! # .provide_ref(self.suggestion.as_str());
//! # }
//! # }
//! # trait MyErrorTrait: Provider {}
//! # impl MyErrorTrait for MyError {}
//! # impl dyn MyErrorTrait {
//! # fn request_ref<T: ?Sized + 'static>(&self) -> Option<&T> {
//! # provider::request_by_type_tag::<'_, tags::Ref<T>, _>(self)
//! # }
//! # }
//! fn report_error(e: &(dyn MyErrorTrait + 'static)) {
//! // Generic error handling
//! // ...
//!
//! // print backtrace
//! if let Some(backtrace) = e.request_ref::<Backtrace>() {
//! println!("{backtrace:?}")
//! }
//! # assert!(e.request_ref::<Backtrace>().is_some());
//!
//! // print suggestion text
//! if let Some(suggestions) = e.request_ref::<str>() {
//! println!("Suggestion: {suggestions}")
//! }
//! # assert_eq!(e.request_ref::<str>().unwrap(), "Do it correctly next time!");
//! }
//!
//! fn main() {
//! let error = MyError {
//! backtrace: Backtrace::capture(),
//! suggestion: "Do it correctly next time!".to_string(),
//! };
//!
//! report_error(&error);
//! }
//! ```

// Heavily inspired by https://github.com/rust-lang/project-error-handling/issues/3:
// The project-error-handling tries to improves the error trait. In order to move the trait into
// `core`, an alternative solution to backtrace provisioning had to be found. This is, where the
// provider API comes from.
//
// TODO: replace library with https://github.com/rust-lang/project-error-handling/issues/3.

#![warn(clippy::pedantic, clippy::nursery)]
Copy link
Member

Choose a reason for hiding this comment

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

+1 for these as warnings. I don't think we should ever block on them, but it's good to get the visibility (provided folks are able to make sensible decisions about what to address/when, and can avoid the distraction of their likely omnipresence).

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually we block on any warning currently which is strictly not backwards compatible:

cargo clippy --manifest-path ${{ matrix.directory }}/Cargo.toml --all --all-features -- -D warnings

I have written this crate taking into account those warnings. Adding those to our current engine would be just noise. We even disabled some of the most annoying clippy lints, but as we proceed, we want to enable them again:

#![allow(
clippy::borrowed_box,
clippy::wrong_self_convention,
clippy::diverging_sub_expression,
clippy::expect_fun_call,
clippy::large_enum_variant,
clippy::type_complexity,
clippy::module_inception,
clippy::new_ret_no_self,
clippy::unnecessary_cast,
clippy::enum_variant_names,
clippy::should_implement_trait,
clippy::mutex_atomic
)]


pub mod tags;

mod internal;
mod requisition;

use core::any::TypeId;

use self::internal::{TagValue, Tagged};
use crate::requisition::{ConcreteRequisition, RequisitionImpl};

/// Trait implemented by a type which can dynamically provide tagged values.
pub trait Provider {
/// Object providers should implement this method to provide *all* values they are able to
/// provide using `req`.
fn provide<'p>(&'p self, req: Requisition<'p, '_>);
}

/// Request a specific value by a given tag from the `Provider`.
pub fn request_by_type_tag<'p, I, P: Provider + ?Sized>(provider: &'p P) -> Option<I::Type>
where
I: TypeTag<'p>,
{
let mut req: ConcreteRequisition<'p, I> = RequisitionImpl {
tagged: TagValue(None),
};
provider.provide(Requisition(&mut req));
req.tagged.0
}

/// This trait is implemented by specific `TypeTag` types in order to allow describing a type which
/// can be requested for a given lifetime `'p`.
///
/// A few example implementations for type-driven `TypeTag`s can be found in the [`tags`] module,
/// although crates may also implement their own tags for more complex types with internal
/// lifetimes.
pub trait TypeTag<'p>: Sized + 'static {
/// The type of values which may be tagged by this `TypeTag` for the given lifetime.
type Type: 'p;
}

/// A helper object for providing objects by type.
///
/// An object provider provides values by calling this type's provide methods. Note, that
/// `Requisition` is a wrapper around a mutable reference to a [`TypeTag`]ged value.
pub struct Requisition<'p, 'r>(&'r mut RequisitionImpl<dyn Tagged<'p> + 'p>);

#[cfg(test)]
pub(crate) mod tests {
use crate::{tags, Provider, Requisition, TypeTag};

struct CustomTagA;
impl<'p> TypeTag<'p> for CustomTagA {
type Type = usize;
}

struct CustomTagB;
impl<'p> TypeTag<'p> for CustomTagB {
type Type = usize;
}

pub(crate) struct MyError {
value: usize,
reference: usize,
custom_tag_a: usize,
custom_tag_b: usize,
option: Option<usize>,
result_ok: Result<u32, i32>,
result_err: Result<i32, u32>,
}

impl Provider for MyError {
fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) {
req.provide_value(|| self.value)
.provide_ref(&self.reference)
.provide_with::<CustomTagA, _>(|| self.custom_tag_a)
.provide::<CustomTagB>(self.custom_tag_b)
.provide::<tags::OptionTag<tags::Value<usize>>>(self.option)
.provide::<tags::ResultTag<tags::Value<u32>, tags::Value<i32>>>(self.result_ok)
.provide::<tags::ResultTag<tags::Value<i32>, tags::Value<u32>>>(self.result_err);
}
}

pub(crate) const ERR: MyError = MyError {
value: 1,
reference: 2,
custom_tag_a: 3,
custom_tag_b: 4,
option: Some(5),
result_ok: Ok(6),
result_err: Err(7),
};

#[test]
fn provide_value() {
assert_eq!(
crate::request_by_type_tag::<tags::Value<usize>, _>(&ERR),
Some(1)
);
}

#[test]
fn provide_ref() {
assert_eq!(
crate::request_by_type_tag::<tags::Ref<usize>, _>(&ERR),
Some(&2)
);
}

#[test]
fn provide_with() {
assert_eq!(crate::request_by_type_tag::<CustomTagA, _>(&ERR), Some(3));
}

#[test]
fn provide() {
assert_eq!(crate::request_by_type_tag::<CustomTagB, _>(&ERR), Some(4));
}

#[test]
fn tags() {
assert_eq!(
crate::request_by_type_tag::<tags::OptionTag<tags::Value<usize>>, _>(&ERR),
Some(Some(5))
);
assert_eq!(
crate::request_by_type_tag::<tags::ResultTag<tags::Value<u32>, tags::Value<i32>>, _>(
&ERR
),
Some(Ok(6))
);
assert_eq!(
crate::request_by_type_tag::<tags::ResultTag<tags::Value<i32>, tags::Value<u32>>, _>(
&ERR
),
Some(Err(7))
);
}
}
59 changes: 59 additions & 0 deletions packages/engine/lib/provider/src/requisition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! Internal API

use crate::{tags, Requisition, TagValue, TypeTag};

impl<'p> Requisition<'p, '_> {
/// Provide a value with the given [`TypeTag`].
pub fn provide<I>(&mut self, value: I::Type) -> &mut Self
where
I: TypeTag<'p>,
{
if let Some(res @ TagValue(Option::None)) =
self.0.tagged.downcast_mut::<tags::OptionTag<I>>()
{
res.0 = Some(value);
}
self
}

/// Provide a value or other type with only static lifetimes.
pub fn provide_value<T, F>(&mut self, f: F) -> &mut Self
where
T: 'static,
F: FnOnce() -> T,
{
self.provide_with::<tags::Value<T>, F>(f)
}

/// Provide a reference, note that `T` must be bounded by `'static`, but may be unsized.
pub fn provide_ref<T: ?Sized + 'static>(&mut self, value: &'p T) -> &mut Self {
self.provide::<tags::Ref<T>>(value)
}

/// Provide a value with the given [`TypeTag`], using a closure to prevent unnecessary work.
pub fn provide_with<I, F>(&mut self, f: F) -> &mut Self
where
I: TypeTag<'p>,
F: FnOnce() -> I::Type,
{
if let Some(res @ TagValue(Option::None)) =
self.0.tagged.downcast_mut::<tags::OptionTag<I>>()
{
res.0 = Some(f());
}
self
}
}

/// A concrete request for a tagged value. Can be coerced to [`Requisition`] to be passed to
/// provider methods.
pub(super) type ConcreteRequisition<'p, I> = RequisitionImpl<TagValue<'p, tags::OptionTag<I>>>;

/// Implementation detail shared between [`Requisition`] and [`ConcreteRequisition`].
///
/// Generally this value is used through the [`Requisition`] type as an `&mut Requisition<'p>` out
/// parameter, or constructed with the `ConcreteRequisition<'p, I>` type alias.
#[repr(transparent)]
pub(super) struct RequisitionImpl<T: ?Sized> {
pub(super) tagged: T,
}
Loading