Refactor and redesign API

This commit is contained in:
Michael Pfaff 2024-03-21 00:34:50 -04:00
parent cab16130f7
commit db01cce71c
6 changed files with 260 additions and 191 deletions

View File

@ -5,8 +5,6 @@ edition = "2021"
[features]
default = []
backtrace = []
extra-backtrace = ["backtrace"]
clone-with-caveats = []
arc-backtrace = []
termination = ["dep:ansee"]

View File

@ -1,12 +1,13 @@
#[cfg(feature = "backtrace")]
use std::backtrace::BacktraceStatus;
use std::borrow::Cow;
use std::fmt;
use std::panic::Location;
use std::sync::Arc;
use crate::report::report_write;
use crate::ReportOpts;
use crate::{
report::{fmt_report, DetailIndent},
How, ReportOpts,
};
/// Provides context furthering the explanation of *how* you got to an error.
#[derive(Debug)]
@ -14,12 +15,12 @@ use crate::ReportOpts;
any(feature = "arc-backtrace", feature = "clone-with-caveats"),
derive(Clone)
)]
pub struct Context {
pub struct DetailTree {
pub(crate) detail: Detail,
pub(crate) extra: Vec<Detail>,
pub(crate) extra: Vec<DetailTree>,
}
impl Context {
impl DetailTree {
pub(crate) fn new(detail: Detail) -> Self {
Self {
detail,
@ -31,7 +32,7 @@ impl Context {
&self.detail
}
pub fn extra(&self) -> &[Detail] {
pub fn extra(&self) -> &[DetailTree] {
&self.extra
}
@ -39,32 +40,65 @@ impl Context {
&mut self.detail
}
pub fn extra_mut(&mut self) -> &mut [Detail] {
pub fn extra_mut(&mut self) -> &mut [DetailTree] {
&mut self.extra
}
pub fn push_extra(&mut self, detail: impl IntoContext) {
let detail = detail.into_context();
if detail.extra.is_empty() {
self.extra.push(detail.detail);
} else {
self.extra.push(Detail::Context(detail.into()));
}
pub fn push_extra(&mut self, detail: impl IntoDetails) {
let detail = detail.into_details();
self.extra.push(detail);
}
pub fn pop_extra(&mut self) -> Option<Detail> {
pub fn pop_extra(&mut self) -> Option<DetailTree> {
self.extra.pop()
}
fn is_multi_line(&self) -> bool {
if self.extra().is_empty() {
match self.detail {
Detail::Str(s) => s.contains('\n'),
Detail::String(ref s) => s.contains('\n'),
Detail::Location(_) => false,
Detail::Backtrace(_) => true,
Detail::Error(_) => false,
Detail::HowError(ref e) => e.0.context.len() > 1,
}
} else {
true
}
}
}
impl fmt::Display for Context {
impl fmt::Display for DetailTree {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.detail.fmt(f)?;
let opts = ReportOpts::default();
for detail in &self.extra {
let mut iter = self.extra.iter().peekable();
while let Some(detail) = iter.next() {
f.write_str("\n")?;
report_write!(f, &opts.indent().next(), "- {detail}")?;
if iter.peek().is_none() {
fmt_report(
f,
&opts.indent().next(),
DetailIndent::<false>,
&format_args!("└╼ {detail}"),
)?;
} else if detail.is_multi_line() {
fmt_report(
f,
&opts.indent().next(),
DetailIndent::<true>,
&format_args!("├╼ {detail}"),
)?;
} else {
fmt_report(
f,
&opts.indent().next(),
DetailIndent::<false>,
&format_args!("├╼ {detail}"),
)?;
}
}
Ok(())
}
@ -80,14 +114,12 @@ pub enum Detail {
Str(&'static str),
String(String),
Location(Location<'static>),
#[cfg(feature = "backtrace")]
Backtrace(PrivateBacktrace),
Error(PrivateError),
Context(Box<Context>),
HowError(How),
}
impl Detail {
#[cfg(feature = "backtrace")]
#[track_caller]
pub fn backtrace() -> Self {
use std::backtrace::BacktraceStatus::*;
@ -104,7 +136,6 @@ impl Detail {
#[derive(Debug, Clone)]
pub struct PrivateError(pub(crate) Arc<dyn std::error::Error + Send + Sync>);
#[cfg(feature = "backtrace")]
#[derive(Debug)]
#[cfg_attr(
any(feature = "arc-backtrace", feature = "clone-with-caveats"),
@ -113,7 +144,6 @@ pub struct PrivateError(pub(crate) Arc<dyn std::error::Error + Send + Sync>);
pub struct PrivateBacktrace(pub(crate) Backtrace);
// will be replaced with std::backtrace::Backtrace if and when it is Clone
#[cfg(feature = "backtrace")]
#[derive(Debug)]
#[cfg_attr(feature = "arc-backtrace", derive(Clone))]
pub(crate) enum Backtrace {
@ -125,11 +155,7 @@ pub(crate) enum Backtrace {
Other(std::backtrace::Backtrace),
}
#[cfg(all(
feature = "backtrace",
feature = "clone-with-caveats",
not(feature = "arc-backtrace")
))]
#[cfg(all(feature = "clone-with-caveats", not(feature = "arc-backtrace")))]
impl Clone for Backtrace {
fn clone(&self) -> Self {
match self {
@ -145,15 +171,12 @@ impl fmt::Display for Detail {
Self::Str(s) => f.write_str(s),
Self::String(s) => f.write_str(s),
Self::Location(l) => write!(f, "at {l}"),
#[cfg(feature = "backtrace")]
Self::Backtrace(PrivateBacktrace(Backtrace::Unsupported)) => f.write_str(
"I'd like to show you a backtrace,\n but it's not supported on your platform",
),
#[cfg(feature = "backtrace")]
Self::Backtrace(PrivateBacktrace(Backtrace::Disabled)) => {
f.write_str("If you'd like a backtrace,\n try again with RUST_BACKTRACE=1")
}
#[cfg(feature = "backtrace")]
Self::Backtrace(PrivateBacktrace(Backtrace::Other(bt))) => {
f.write_str(if bt.status() == BacktraceStatus::Captured {
"Here is the backtrace:\n"
@ -163,119 +186,119 @@ impl fmt::Display for Detail {
write!(f, "{}", bt)
}
Self::Error(PrivateError(e)) => e.fmt(f),
Self::Context(c) => c.fmt(f),
Self::HowError(e) => e.fmt(f),
}
}
}
pub trait IntoContext: Sized {
fn into_context(self) -> Context;
pub trait IntoDetails: Sized {
fn into_details(self) -> DetailTree;
/// Annotates the context with the given detail.
#[inline(always)]
fn with(self, other: impl IntoContext) -> Context {
self.into_context().with(other)
fn with(self, detail: impl IntoDetails) -> DetailTree {
self.into_details().with(detail)
}
/// Annotates the context with the caller location.
#[inline(always)]
#[track_caller]
fn with_caller(self) -> Context {
fn with_caller(self) -> DetailTree {
self.with(Location::caller())
}
/// Annotates the context with the current backtrace.
#[inline(always)]
#[track_caller]
fn with_backtrace(self) -> Context {
#[cfg(feature = "backtrace")]
return self.with(Detail::backtrace());
#[cfg(not(feature = "backtrace"))]
self.into_context()
fn with_backtrace(self) -> DetailTree {
self.with(Detail::backtrace())
}
}
impl IntoContext for Context {
impl IntoDetails for DetailTree {
#[inline(always)]
fn into_context(self) -> Context {
fn into_details(self) -> DetailTree {
self
}
/// Chains another piece of context that is a child from a hierarchical perspective.
#[track_caller]
#[inline]
fn with(mut self, other: impl IntoContext) -> Self {
fn with(mut self, other: impl IntoDetails) -> Self {
self.push_extra(other);
self
}
}
impl IntoContext for String {
impl IntoDetails for String {
#[inline(always)]
fn into_context(self) -> Context {
Detail::String(self).into_context()
fn into_details(self) -> DetailTree {
Detail::String(self).into_details()
}
}
impl IntoContext for &'static str {
impl IntoDetails for &'static str {
#[inline(always)]
fn into_context(self) -> Context {
Detail::Str(self).into_context()
fn into_details(self) -> DetailTree {
Detail::Str(self).into_details()
}
}
impl IntoContext for Cow<'static, str> {
impl IntoDetails for Cow<'static, str> {
#[inline]
fn into_context(self) -> Context {
fn into_details(self) -> DetailTree {
match self {
Cow::Borrowed(s) => s.into_context(),
Cow::Owned(s) => s.into_context(),
Cow::Borrowed(s) => s.into_details(),
Cow::Owned(s) => s.into_details(),
}
}
}
impl<'a> IntoContext for &'a Location<'static> {
impl<'a> IntoDetails for &'a Location<'static> {
#[inline]
fn into_context(self) -> Context {
Location::into_context(*self)
fn into_details(self) -> DetailTree {
Location::into_details(*self)
}
}
impl IntoContext for Location<'static> {
impl IntoDetails for Location<'static> {
#[inline]
fn into_context(self) -> Context {
Detail::Location(self).into_context()
fn into_details(self) -> DetailTree {
Detail::Location(self).into_details()
}
}
impl<E> IntoContext for Arc<E>
impl<E> IntoDetails for Arc<E>
where
E: std::error::Error + Send + Sync + 'static,
{
#[inline]
fn into_context(self) -> Context {
Detail::Error(PrivateError(self)).into_context()
fn into_details(self) -> DetailTree {
Detail::Error(PrivateError(self)).into_details()
}
}
impl IntoContext for Arc<dyn std::error::Error + Send + Sync> {
impl IntoDetails for Arc<dyn std::error::Error + Send + Sync> {
#[inline]
fn into_context(self) -> Context {
Detail::Error(PrivateError(self)).into_context()
fn into_details(self) -> DetailTree {
Detail::Error(PrivateError(self)).into_details()
}
}
impl IntoContext for Detail {
impl IntoDetails for Detail {
#[inline(always)]
fn into_context(self) -> Context {
Context::new(self)
fn into_details(self) -> DetailTree {
DetailTree::new(self)
}
}
impl<C, F> IntoContext for F
impl<C, F> IntoDetails for F
where
C: IntoContext,
C: IntoDetails,
F: FnOnce() -> C,
{
#[inline(always)]
fn into_context(self) -> Context {
self().into_context()
fn into_details(self) -> DetailTree {
self().into_details()
}
}

View File

@ -1,41 +1,31 @@
use std::panic::Location;
use std::sync::Arc;
use crate::{Detail, How, IntoContext};
use crate::{How, IntoDetails};
pub trait Explain: Sized {
type Output;
type Output: Explain;
#[track_caller]
#[must_use]
fn context(self, context: impl IntoContext) -> Self::Output;
fn without_explanation(self) -> Self::Output;
#[inline]
#[track_caller]
#[must_use]
fn frame(self, context: impl IntoContext) -> Self::Output {
let mut context = context.into_context();
context.extra.reserve(if cfg!(feature = "extra-backtrace") {
2
} else {
1
});
context.extra.push(Detail::Location(*Location::caller()));
#[cfg(feature = "extra-backtrace")]
context.extra.push(Detail::backtrace());
self.context(context)
}
fn context(self, context: impl IntoDetails) -> Self::Output;
}
impl Explain for How {
type Output = Self;
#[inline(always)]
fn without_explanation(self) -> Self::Output {
self
}
#[inline(always)]
#[track_caller]
fn context(mut self, context: impl IntoContext) -> Self {
self.push_context(context.into_context());
fn context(mut self, context: impl IntoDetails) -> Self {
self.push_context(context.into_details());
self
}
}
@ -59,20 +49,44 @@ where
{
type Output = Result<T, How>;
#[inline(always)]
fn without_explanation(self) -> Self::Output {
#[cold]
#[track_caller]
fn err_inner<E>(e: E) -> How
where
E: std::error::Error + 'static,
{
// TODO: when specialization is stable, or at least *safe*, specialize on `E = How` and
// `E: Send + Sync + 'static` and remove the `+ 'static` bound from E on this Explain
// impl.
match typeid_cast::cast(e) {
Ok(e) => e,
//Err(e) => How::new(Context(ContextInner::Elem(Detail::Error(Arc::new(e))))),
Err(e) => How::new(e.to_string()),
}
}
match self {
Ok(t) => Ok(t),
Err(e) => Err(err_inner(e)),
}
}
#[inline(always)]
#[track_caller]
fn context(self, context: impl IntoContext) -> Self::Output {
fn context(self, context: impl IntoDetails) -> Self::Output {
#[cold]
#[track_caller]
fn into_and_context<E, C>(e: E, c: C) -> How
where
E: std::error::Error + 'static,
C: IntoContext,
C: IntoDetails,
{
// TODO: when specialization is stable, or at least *safe*, specialize on `E = How` and
// `E: Send + Sync + 'static` and remove the `+ 'static` bound from E on this Explain
// impl.
match typeid_cast::cast(e) {
Ok(e) => e,
// TODO: specialize on Send + Sync at runtime or compile time (possibly via
// specialization)
//Err(e) => How::new(Context(ContextInner::Elem(Detail::Error(Arc::new(e))))),
Err(e) => How::new(e.to_string()),
}
@ -91,29 +105,46 @@ where
{
type Output = How;
#[inline]
fn without_explanation(self) -> Self::Output {
How::new(self)
}
#[inline]
#[track_caller]
fn context(self, context: impl IntoContext) -> Self::Output {
How::new(self).context(context)
fn context(self, context: impl IntoDetails) -> Self::Output {
self.without_explanation().context(context)
}
}
impl Explain for Arc<dyn std::error::Error + Send + Sync> {
type Output = How;
fn without_explanation(self) -> Self::Output {
How::new(self)
}
#[inline]
#[track_caller]
fn context(self, context: impl IntoContext) -> Self::Output {
How::new(self).context(context)
fn context(self, context: impl IntoDetails) -> Self::Output {
self.without_explanation().context(context)
}
}
impl<T> Explain for Option<T> {
type Output = Result<T, How>;
#[inline(always)]
fn without_explanation(self) -> Self::Output {
match self {
Some(t) => Ok(t),
None => Err(How::new("called `Option::unwrap()` on a `None` value")),
}
}
#[inline(always)]
#[track_caller]
fn context(self, context: impl IntoContext) -> Self::Output {
fn context(self, context: impl IntoDetails) -> Self::Output {
match self {
Some(t) => Ok(t),
None => Err(How::new(context)),

View File

@ -2,7 +2,7 @@ use core::panic::Location;
use crate::*;
use crate::report::report_write;
use self::report::{fmt_report, DetailIndent};
/// The error type.
///
@ -13,29 +13,30 @@ use crate::report::report_write;
any(feature = "arc-backtrace", feature = "clone-with-caveats"),
derive(Clone)
)]
pub struct How(Box<HowInner>);
pub struct How(pub(crate) Box<HowInner>);
struct HowInner {
pub(crate) struct HowInner {
location: &'static Location<'static>,
// TODO: consider storing this vec inline (sharing the allocation with rest of the struct.
// Probably move after `backtrace`)
context: Vec<Context>,
pub(crate) context: Vec<DetailTree>,
}
impl How {
#[must_use]
#[inline(never)]
#[track_caller]
pub fn new(context: impl IntoContext) -> Self {
pub fn new(details: impl IntoDetails) -> Self {
let location = Location::caller();
#[allow(unused_mut)]
let mut how = Self(Box::new(HowInner {
location,
context: Vec::with_capacity(4),
}))
.frame(context);
#[cfg(all(feature = "backtrace", not(feature = "extra-backtrace")))]
how.top_mut().extra.push(Detail::backtrace());
.context(details);
if cfg!(not(feature = "extra-backtrace")) {
how.top_mut().push_extra(Detail::backtrace());
}
how
}
@ -45,7 +46,7 @@ impl How {
}
#[inline]
pub fn top(&self) -> &Context {
pub fn top(&self) -> &DetailTree {
// SAFETY: we only ever push values into context, and the constructor ensures that there
// is at least 1 value in context.
let o = self.0.context.iter().next();
@ -60,7 +61,7 @@ impl How {
}
#[inline]
pub fn bottom(&self) -> &Context {
pub fn bottom(&self) -> &DetailTree {
// SAFETY: we only ever push values into context, and the constructor ensures that there
// is at least 1 value in context.
let o = self.0.context.iter().next_back();
@ -75,7 +76,7 @@ impl How {
}
#[inline]
pub fn top_mut(&mut self) -> &mut Context {
pub fn top_mut(&mut self) -> &mut DetailTree {
// SAFETY: we only ever push values into context, and the constructor ensures that there
// is at least 1 value in context.
let o = self.0.context.iter_mut().next();
@ -90,7 +91,7 @@ impl How {
}
#[inline]
pub fn bottom_mut(&mut self) -> &mut Context {
pub fn bottom_mut(&mut self) -> &mut DetailTree {
// SAFETY: we only ever push values into context, and the constructor ensures that there
// is at least 1 value in context.
let o = self.0.context.iter_mut().next_back();
@ -105,7 +106,7 @@ impl How {
}
#[inline]
pub fn into_context(self) -> impl Iterator<Item = Context> {
pub fn into_context_iter(self) -> impl Iterator<Item = DetailTree> {
self.0.context.into_iter()
}
@ -117,7 +118,7 @@ impl How {
}
#[inline]
pub(crate) fn push_context(&mut self, context: Context) {
pub(crate) fn push_context(&mut self, context: DetailTree) {
self.0.context.push(context);
}
}
@ -149,7 +150,7 @@ impl std::fmt::Display for How {
f.write_str("\n")?;
}
report_write!(f, &opts.indent().next(), "{ctx}")?;
fmt_report(f, &opts.indent().next(), DetailIndent::<false>, ctx)?;
}
Ok(())
}
@ -165,3 +166,10 @@ impl std::fmt::Debug for How {
}
}
}
impl IntoDetails for How {
#[inline]
fn into_details(self) -> crate::DetailTree {
Detail::HowError(self).into_details()
}
}

View File

@ -3,7 +3,7 @@
#![feature(doc_auto_cfg)]
mod context;
pub use context::{Context, Detail, IntoContext};
pub use context::{Detail, DetailTree, IntoDetails};
mod report;
pub use report::{Report, ReportOpts};

View File

@ -1,4 +1,4 @@
use std::fmt::Write;
use std::fmt::{self, Display, Write};
#[derive(Debug, Clone, Copy, Default)]
pub struct ReportOpts {
@ -31,21 +31,28 @@ impl ReportOpts {
}
}
macro_rules! report_write {
($f:expr, $opts:expr, $msg:literal$(, $($tt:tt)+)?) => {
<::std::fmt::Arguments<'_> as $crate::Report>::fmt(&::std::format_args!(
$msg$(, $($tt)+)?), $f, $opts
)
};
pub trait Indent {
fn write(&self, n: usize, f: &mut impl Write) -> fmt::Result;
}
pub(crate) use report_write;
#[inline]
fn write_indent(n: usize, f: &mut impl Write) -> std::fmt::Result {
for _ in 0..n {
f.write_str(" ")?;
impl<'a, T: Indent> Indent for &'a T {
#[inline]
fn write(&self, n: usize, f: &mut impl Write) -> fmt::Result {
T::write(*self, n, f)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DetailIndent<const LINED: bool>;
impl<const LINED: bool> Indent for DetailIndent<LINED> {
#[inline]
fn write(&self, n: usize, f: &mut impl Write) -> fmt::Result {
for _ in 0..n {
f.write_str(if LINED { "" } else { " " })?;
}
Ok(())
}
Ok(())
}
/// `Report` should format the output in a manner suitable for debugging, similar to
@ -56,62 +63,64 @@ pub trait Report {
fn fmt(&self, f: &mut impl Write, opts: &ReportOpts) -> std::fmt::Result;
}
impl<T> Report for T
where
T: std::fmt::Display,
{
#[inline(never)]
fn fmt(&self, f: &mut impl Write, opts: &ReportOpts) -> std::fmt::Result {
use std::fmt::Error;
struct IndentedWrite<'a, W: Write> {
f: &'a mut W,
n: usize,
pub fn fmt_report(
f: &mut impl Write,
opts: &ReportOpts,
indent: impl Indent,
t: &impl Display,
) -> std::fmt::Result {
if opts.indentation > 0 {
if opts.should_indent() {
indent.write(opts.indentation, f)?;
}
impl<'a, W> Write for IndentedWrite<'a, W>
where
W: Write,
{
#[inline]
fn write_str(&mut self, s: &str) -> Result<(), Error> {
// TODO: any room for optimization?
// iterates over the lines where each str ends with the line terminator.
// after giving this a bit of thought I think it is best to indent after any
// trailing newline.
let mut ss = s.split_inclusive('\n');
if let Some(mut s) = ss.next() {
self.f.write_str(s)?;
for s_next in ss {
write_indent(self.n, &mut self.f)?;
self.f.write_str(s_next)?;
s = s_next;
}
if matches!(s.chars().rev().next(), Some('\n')) {
write_indent(self.n, &mut self.f)?;
}
}
Ok(())
}
#[inline]
fn write_char(&mut self, c: char) -> Result<(), Error> {
self.f.write_char(c)?;
if c == '\n' {
write_indent(self.n, &mut self.f)?;
}
Ok(())
}
}
if opts.indentation > 0 {
if opts.should_indent() {
write_indent(opts.indentation, f)?;
}
IndentedWrite {
f,
n: opts.indentation,
}
.write_fmt(format_args!("{self}"))
} else {
f.write_fmt(format_args!("{self}"))
IndentedWrite {
f,
n: opts.indentation,
indent,
}
.write_fmt(format_args!("{t}"))
} else {
f.write_fmt(format_args!("{t}"))
}
}
pub struct IndentedWrite<'a, I: Indent, W: Write> {
f: &'a mut W,
indent: I,
n: usize,
}
impl<'a, I, W> Write for IndentedWrite<'a, I, W>
where
I: Indent,
W: Write,
{
#[inline]
fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
// TODO: any room for optimization?
// iterates over the lines where each str ends with the line terminator.
// after giving this a bit of thought I think it is best to indent after any
// trailing newline.
let mut ss = s.split_inclusive('\n');
if let Some(mut s) = ss.next() {
self.f.write_str(s)?;
for s_next in ss {
self.indent.write(self.n, &mut self.f)?;
self.f.write_str(s_next)?;
s = s_next;
}
if matches!(s.chars().rev().next(), Some('\n')) {
self.indent.write(self.n, &mut self.f)?;
}
}
Ok(())
}
#[inline]
fn write_char(&mut self, c: char) -> Result<(), fmt::Error> {
self.f.write_char(c)?;
if c == '\n' {
self.indent.write(self.n, &mut self.f)?;
}
Ok(())
}
}