Skip to content

ACP: New Macro - debugging with clean input and clean output. Simultaneously.  #125

Closed as not planned
@amab8901

Description

@amab8901

Proposal

Problem statement

Why should the user have to choose between messy input and messy output? My proposed macro will enable the user to debug their code with clean input and clean output.

Motivation, use-cases

###Short motivation:
If you want to debug, you're currently forced to choose between writing long boilerplate and having messy output that requires more effort to visually navigate. It's important to be able to produce clean and clear output without having to write long boilerplate, especially for beginners and intermediate-level users (I can't speak for advanced users though, since I haven't reached that level myself).

###Long motivation:
If we want to debug code, we have the following options available:

  • dbg!
  • println!
  • print!
  • eprint!
  • eprintln!

As I understand it, print! and eprint! are rarely used because the user can instead choose println! and eprintln! to get the same thing but with clearer structure to more easily distinguish different outputs from each other (although there may be some use cases of print! and eprint!). Thus, print! and eprint! are suboptimal choices for producing clean & clear output with minimal boilerplate.

println! and eprintln! produce clean and clear output, but they require the user to write long boilerplate.

dbg! can be used very concisely and easily, with minimal boilerplate. But the output is messy and takes more effort to visually navigate.

Beginners and intermediate-level users (such as myself) may want to copy code examples from documentation and forums (like github) and paste them into their local environment (or Rust Playground) to play around with it, to better understand what the code actually does. Using println! and eprintln! is cumbersome and messy to write, while cfg! is cumbersome and messy to read its output.

I took the below code example from this page:

fn main() {
   let mut test1 = Test::new("test1");
   let mut test1_pin = unsafe { Pin::new_unchecked(&mut test1) };
   Test::init(test1_pin.as_mut());

   drop(test1_pin);
   println!(r#"test1.b points to "test1": {:?}..."#, test1.b);

   let mut test2 = Test::new("test2");
   mem::swap(&mut test1, &mut test2);
   println!("... and now it points nowhere: {:?}", test1.b);
}
use std::pin::Pin;
use std::marker::PhantomPinned;
use std::mem;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}


impl Test {
    fn new(txt: &str) -> Self {
        Test {
            a: String::from(txt),
            b: std::ptr::null(),
            // This makes our type `!Unpin`
            _marker: PhantomPinned,
        }
    }

    fn init<'a>(self: Pin<&'a mut Self>) {
        let self_ptr: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_ptr;
    }

    fn a<'a>(self: Pin<&'a Self>) -> &'a str {
        &self.get_ref().a
    }

    fn b<'a>(self: Pin<&'a Self>) -> &'a String {
        assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
        unsafe { &*(self.b) }
    }
}

I tried to play around with this code for educational purposes, using two different approaches: println! and dbg!. In both cases, I modified only the main function as follows:

println! approach

Messy input:

fn main() {
   let mut test1 = Test::new("test1");
   println!("test1 = {:#?}", test1);
   println!();
   
   let mut test1_pin = unsafe { Pin::new_unchecked(&mut test1) };
   Test::init(test1_pin.as_mut());
   
   drop(test1_pin);
   println!("test1 = {:#?}", test1);
   println!();

   let mut test2 = Test::new("test2");
   println!("test1 = {:#?}", test1);
   println!();
   println!("test2 = {:#?}", test2);
   println!();
   
   mem::swap(&mut test1, &mut test2);
   println!("test1 = {:#?}", test1);
   println!();
   println!("test2 = {:#?}", test2);
   println!();
}

Clean output:

test1 = Test {
    a: "test1",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}

test1 = Test {
    a: "test1",
    b: 0x00007ffd1bdafb68,
    _marker: PhantomPinned,
}

test1 = Test {
    a: "test1",
    b: 0x00007ffd1bdafb68,
    _marker: PhantomPinned,
}

test2 = Test {
    a: "test2",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}

test1 = Test {
    a: "test2",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}

test2 = Test {
    a: "test1",
    b: 0x00007ffd1bdafb68,
    _marker: PhantomPinned,
}

dbg! approach

Clean input:

fn main() {
   let mut test1 = Test::new("test1");
   dbg!(&test1);
   
   let mut test1_pin = unsafe { Pin::new_unchecked(&mut test1) };
   Test::init(test1_pin.as_mut());
   
   drop(test1_pin);
   dbg!(&test1);

   let mut test2 = Test::new("test2");
   dbg!(&test1, &test2);
   
   mem::swap(&mut test1, &mut test2);
   dbg!(&test1, &test2);
}

Messy output:

[src/main.rs:7] &test1 = Test {
    a: "test1",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}
[src/main.rs:13] &test1 = Test {
    a: "test1",
    b: 0x00007ffca10e7650,
    _marker: PhantomPinned,
}
[src/main.rs:16] &test1 = Test {
    a: "test1",
    b: 0x00007ffca10e7650,
    _marker: PhantomPinned,
}
[src/main.rs:16] &test2 = Test {
    a: "test2",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}
[src/main.rs:19] &test1 = Test {
    a: "test2",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}
[src/main.rs:19] &test2 = Test {
    a: "test1",
    b: 0x00007ffca10e7650,
    _marker: PhantomPinned,
}

Solution sketches

Create new macro by adding the following code (or some variation thereof) into library/std/src/macros.rs:

#[macro_export]
#[cfg_attr(not(test), rustc_diagnostic_item = "dbg_macro")]
#[unstable(feature = "dbg_macro")]
macro_rules! pdbg {
    () => {
        $crate::eprintln!()
    };
    ($val:expr $(,)?) => {
        // Use of `match` here is intentional because it affects the lifetimes
        // of temporaries - https://stackoverflow.com/a/48732525/1063961
        match $val {
            tmp => {
                $crate::eprintln!("{} = {:#?}\n",
                    $crate::stringify!($val), &tmp);
                tmp
            }
        }
    };
    ($($val:expr),+ $(,)?) => {
        ($($crate::pdbg!($val)),+,)
    };
}

If you use my solution, the input and output should both be clean, as follows:

Clean input:

fn main() {
   let mut test1 = Test::new("test1");
   pdbg!(&test1);
   
   let mut test1_pin = unsafe { Pin::new_unchecked(&mut test1) };
   Test::init(test1_pin.as_mut());
   
   drop(test1_pin);
   pdbg!(&test1);

   let mut test2 = Test::new("test2");
   pdbg!(&test1, &test2);
   
   mem::swap(&mut test1, &mut test2);
   pdbg!(&test1, &test2);
}

Clean output:

test1 = Test {
    a: "test1",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}

test1 = Test {
    a: "test1",
    b: 0x00007ffd1bdafb68,
    _marker: PhantomPinned,
}

test1 = Test {
    a: "test1",
    b: 0x00007ffd1bdafb68,
    _marker: PhantomPinned,
}

test2 = Test {
    a: "test2",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}

test1 = Test {
    a: "test2",
    b: 0x0000000000000000,
    _marker: PhantomPinned,
}

test2 = Test {
    a: "test1",
    b: 0x00007ffd1bdafb68,
    _marker: PhantomPinned,
}

Links and related work

This issue is what triggered me into writing this API Change Proposal. I'm probably not the only one who would benefit from this proposed macro.

What happens now?

This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions