#[derive(Debug)] by default

(Result::expect also requires E: Debug)

In that case a project using either .unwrap() or .expect() will have to take the hit to compilation times anyway.

I am of the belief that in principle it should be possible to derive a sane default debug implementation for pretty much every type, once questions like 'how to do that for raw ptrs' are answered. Whether that happens or not is more of a social issue at heart, not a technical one i.e. it's more about "do we want it? Under what circumstances do we want it? When don't we want it?" etc than it is about "can we even do it?".

1 Like

how about a new intrinsic:

  • when exact type is statically known
    • invokes derived or hand-coded Debug impl if present
    • returns symbolic type name otherwise
      • type name is looked up by TypeId in some kind of a global symbol table possibly assembled during linkage
      • binaries compiled without debug info will have no such table and as a fallback numeric value of TypeId will be used
  • method table for every trait object will include enough information to do an equivalent of the above dynamically
1 Like

Isn't this possible with specialization?

How do you see that working, in principle?

On the provided Result::unwrap example:

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

mod spec {
    use super::Result;
    use core::any::type_name;
    use core::fmt::Debug;

    pub(super) trait UnwrapSpec<E> {
        fn unwrap_failed(msg: &str, error: &E) -> !;
    }

    impl<T, E> UnwrapSpec<E> for Result<T, E> {
        #[cold]
        default fn unwrap_failed(msg: &str, _: &E) -> ! {
            panic!("{}: `{}`", msg, type_name::<E>())
        }
    }

    impl<T, E: Debug> UnwrapSpec<E> for Result<T, E> {
        #[cold]
        fn unwrap_failed(msg: &str, error: &E) -> ! {
            panic!("{}: {:?}", msg, error)
        }
    }
}

impl<T, E> Result<T, E> {
    pub fn unwrap(self) -> T {
        use self::spec::UnwrapSpec;
        match self {
            Self::Ok(t) => t,
            Self::Err(ref e) => {
                Self::unwrap_failed("called `Result::unwrap()` on an `Err` value", e)
            }
        }
    }
}

fn main() {
    // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: 10'
    Result::<(), _>::Err(10).unwrap();

    struct NoDbg;
    // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: `playground::main::NoDbg`'
    Result::<(), _>::Err(NoDbg).unwrap();
}

Playground

EDIT: Alternative

1 Like

Inspired by @TimDiekmann (and others), why not introduce the following wrapper type:

#![feature(specialization)]
use core::fmt::{self, Debug, Formatter};

pub struct DebugWrapper<'a, T>(pub &'a T);

impl<'a, T> Debug for DebugWrapper<'a, T> {
    default fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "<{}>", core::any::type_name::<T>())
    }
}

impl<'a, T: Debug> Debug for DebugWrapper<'a, T> {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        self.0.fmt(f)
    }
}

Then, it could be used in generic functions (like Result::expect) to wrap arbitrary types, and could even by used in the code generated by #[derive(Debug)].

Playground link

3 Likes

The use-specialization-to-attempt-Debug was proposed as part of dbg!, but was rejected there.

That might change once specialization becomes stable, but last it came up there wasn't appetite to expose it.

1 Like

This is me thinking out loud, not proposing a solution, but.

It sounds like the fundamental problem lies in the fact that Rust doesn't really recognize PODs and bare tuples.

Like, there's the Debug trait, there's Copy and Clone and Send and Sync, but there's no single way to express "This struct is little more than a tuple with named fields, just figure it out".

Maybe Rust should have a POD trait that's opt-out (so that tuples and hastily-written structs get it too) and gets applied eg to every struct with only public fields. Traits like Debug and Clone would have a default implementation for all types with this trait.

Or not. My point is, Rust needs a stronger story for POD types.

Part of the reason we have Debug/Copy/Clone/Send/Sync as opt-in traits is that they all carry a semver implication that, once opted-into, it is a breaking change to ever remove them. Making any of them opt-out would then make it a subtle breaking change to add a new field that doesn't impl one of those traits for whatever reason. So I'm not sure what we'd want #[derive(POD)] to actually do, other than be trivial sugar for some set of other traits (and iirc a bunch of crates already provide that).

To be clear, I don't have a strong opinion on what should or shouldn't be done here, but it seems clear to me that none of the core language suggestions so far would be net improvements over the status quo. Compiling this "more lazily" somehow to avoid the compile-time penalty does feel promising, but I have no idea how feasible that is in practice.

5 Likes

What do you mean by "recognize"? And what does the existence of PODs and tuples have to do with Debug?

1 Like

I wish we had a way to do derive-like things that weren't just macros, so they could be run later in the pipe with more information. Because it would be awesome if #[derive(POD)] automatically worked if you had trait POD = Copy + Clone + PartialOrd + Hash + PartialEq;, but phase ordering means it cannot.

But is there somewhere later in the pipeline where something like a pre-MIR macro (whatever that would look like) could run? This probably would be a point where plugins are/were permitted, but which would have at least a semi-stable syntax, unlike plugins that are known to be extremely unstable.

It often feels like to me that if we had a good, solid story around "Const Generics" and "Variadic Generics" as well as compile-time "Const Evaluatiion" and "Specialization" that many of these types of things could simply be generic instead of relying upon macros to generate impls. I think all of those things are in the works, so, it might make sense to delay any thoughts of creating new kinds of macros until that space has been fully explored and implemented and there is still some things that can't be done that could then require the idea of new kinds of macros.

3 Likes

I was imagining a very-hypothetical const-only thing where you'd be able to map-reduce all the fields over some weird generic thing.

That wouldn't at all be a replacement for the more complex procedural derive macros, but for all the ones that are just "call this trait method on everything, then combine the results like this to make the output", it'd be great to have that be semantic instead of syntactical.

(I have no idea what that would look like concretely.)

I would think it would be built atop: Const Generics, Variadic Generics, Specialization, and Const Eval. :wink:

1 Like

You're probably overestimating how worked out these features are, though.

Don't get me wrong, I absolutely agree that variadic generics are necessary, and they probably supersede a lot of stuff suggested here, but I don't really think the conceptual work you're thinking about has seen much progress.

This RFC is the most recent and the most detailed so far, but it's fairly narrow. It only covers tuples, not structs and enums, not printf functions, etc.

Other discussions on variadics feel like they're 5-10 years behind what's actually done in modern C++ and D. Lots of proposed solutions rely on recursion and SFINAE, even thought that approach is a huge hack; there is almost no mention of static if, static match or static for.

Again, I do agree that the legwork needs to be done there, but there's a really long way to go.

5 Likes

I disagree on delaying for 2 reasons:

  1. Who knows when the story around all of const generics, variadic generic, const eval, and specialization is actually all there? I wouldn't put my money on 2020 being the year that all that comes together. The Rust roadmap for this year couldn't even tell me what is planned to get done, so we can't rely on that either. Specialization alone is something that's been in limbo for over 5 years, and at the start of 2020 I'm not seeing too much progress on the rest, either. In contrast, a macro can be written now.
  2. A macro can serve as a useful prototype for when the other features eventually do come online. So iterating on what's necessary and desirable (i.e. policy) from a user perspective can start earlier.
3 Likes

I can see adding a lint, that can be disabled at the struct/module/crate level that warns for structs with debug not derived, but, anything beyond that seems to be a waste of time/effort if the ultimate solution should be Const/Variadic/Specialization of Generics and Const eval.

1 Like

Not a waste, we get something in return: a solution to this problem in the interim, even if it is a hacky one, and intended to be deprecated from the start. Wouldn't be the first feature in Rust to go down that road - macro_rules macros were "designed" as a throwaway, for example.