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

hrp: add constructor from an arbitrary T: Display #196

Merged
merged 1 commit into from
Aug 15, 2024

Conversation

apoelstra
Copy link
Member

Fixes #195


let mut byte_formatter = ByteFormatter { arr: [0; MAX_HRP_LEN], index: 0, error: None };

write!(byte_formatter, "{}", data).expect("custom Formatter cannot fail");
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this macro call have to allocate a String because data is not know until runtime?

My understanding is that write! calls format_args! which creates an Arguments type which has an as_str method but only if there are no runtime args.

In other words isn't this the same performance as:

    pub fn parse_display<T: core::fmt::Display>(data: T) -> Result<Self, Error> {
        let s = data.to_string();
        Self::parse(&s)
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

This code works without any allocator. It's not allocating a string. It would also be insane for the fmt::Write API to work this way, where every single call to write_str involved a gratuitous allocation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Or indeed, for every call to write! anywhere to involve gratuitous allocations.

If you think this could be true we'd need to change our fmt::Display implementations throughout every one of our crates to avoid this macro.

Copy link
Member Author

Choose a reason for hiding this comment

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

It took me a little while to trace through the code but the core of write! is the write function which iterates over the "pieces" of an Arguments and does not allocate.

Copy link
Member

Choose a reason for hiding this comment

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

Cool, TIL. Thanks for the link (I was unconvinced before it, you saved me trawling through the source :)

Comment on lines 140 to 143
for b in s.bytes() {
// Continue after finding an error so that we report the first invalid
// character, not the last.
if !b.is_ascii() {
Copy link
Member

@tcharding tcharding Aug 6, 2024

Choose a reason for hiding this comment

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

There are a few problems here, I uncovered them by writing a test to check the code comment (which is incorrect, we return the last error), I think we want break instead of continue.

Also we need to iterate over chars and check they are ascii instead of using bytes - we do this in parse above.

Copy link
Member

Choose a reason for hiding this comment

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

Instead of commenting each line I just wrote some tests that I think should pass

    #[test]
    fn parse_non_ascii() {
        assert_eq!(Hrp::parse("❤").unwrap_err(), Error::NonAsciiChar('❤'));
    }

    #[test]
    fn parse_display_non_ascii() {
        assert_eq!(Hrp::parse("❤").unwrap_err(), Error::NonAsciiChar('❤'));
    }

    #[test]
    fn parse_display_returns_first_error() {
        assert_eq!(Hrp::parse_display("❤ ").unwrap_err(), Error::NonAsciiChar('❤'));
    }

    // This test shows that the error does not contain heart.
    #[test]
    fn parse_display_iterates_chars() {
        // FIXME: Once test above passes the error char will need changing.
        assert_eq!(Hrp::parse_display(" ❤").unwrap_err(), Error::NonAsciiChar('❤'));
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks! I will take these tests and make them pass.

@apoelstra
Copy link
Member Author

Should be good now.

Copy link
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 2fdc674

Comment on lines +177 to +178
// Unconditionally update self.index so that in the case of a too-long
// string, our error return will reflect the full length.
Copy link
Member

Choose a reason for hiding this comment

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

nit: This comment is stale now I believe.

Copy link
Member

@tcharding tcharding Aug 6, 2024

Choose a reason for hiding this comment

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

I think we could just check the length at the start and early return.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see why this is stale. The line is called every time the write_str function is called, for the reason that the comment describes.

We could do a length check at the start and early return but I don't see what that would accomplish other than increasing the cyclotomic complexity of the function.

Copy link
Member

Choose a reason for hiding this comment

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

ooo, I see. Don't mind me.

@apoelstra
Copy link
Member Author

I think we may want to backport this to 0.11, since when we release 0.12, rust-bitcoin 0.32 won't be able to use it (thanks to rust-bitcoin/rust-bitcoin#3043).

Same with #197, so let's wait until that's done as well.


#[test]
fn parse_display_non_ascii() {
assert_eq!(Hrp::parse("❤").unwrap_err(), Error::NonAsciiChar('❤'));
Copy link
Member

Choose a reason for hiding this comment

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

Should this call parse_display here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, yes! Fixed.

Copy link
Member

Choose a reason for hiding this comment

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

A bug in the unit tests, what are the odds ;)

Copy link
Member

Choose a reason for hiding this comment

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

Oh actually, if this is based on the unit test code I posted above I intentionally included one that parsed using Hrp::parse.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, you intentionally included one, but you unintentionally included another one :).

Copy link
Member

Choose a reason for hiding this comment

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

Sweet, cheers. Its hard to get good help :)

Copy link
Member

@clarkmoody clarkmoody left a comment

Choose a reason for hiding this comment

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

ACK 11d6cb4

@apoelstra apoelstra merged commit e35cdda into rust-bitcoin:master Aug 15, 2024
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Hrp should have a constructor from a T: Display
3 participants