Skip to content

units: Prevent casting pub enums as ints#4127

Merged
apoelstra merged 1 commit intorust-bitcoin:masterfrom
tcharding:02-26-no-cast
Mar 7, 2025
Merged

units: Prevent casting pub enums as ints#4127
apoelstra merged 1 commit intorust-bitcoin:masterfrom
tcharding:02-26-no-cast

Conversation

@tcharding
Copy link
Copy Markdown
Member

A public enum with simple variants gets an automatic integer variant that can be cast by library consumers. This puts a unnecessary maintenance burden upon us because we cannot then add variants in the middle of others.

Add a hidden variant to the single public non-error enum in units.

@tcharding
Copy link
Copy Markdown
Member Author

I didn't go searching for the name you suggested already @Kixunil, happy to change the one here to it if you post it.

@github-actions github-actions bot added the C-units PRs modifying the units crate label Feb 26, 2025
@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@github-actions github-actions bot added the API break This PR requires a version bump for the next release label Feb 26, 2025
@github-actions
Copy link
Copy Markdown

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

@jirijakes
Copy link
Copy Markdown
Contributor

The annoying and perhaps confusing part is that LSP (rust-analyzer) still offers it for completions and other assistance.

fn x(d: Denomination) {
    match d {
      |
    }
}

Fill match arms:

fn x(d: Denomination) {
    match d {
        Denomination::Bitcoin => todo!(),
        Denomination::CentiBitcoin => todo!(),
        Denomination::MilliBitcoin => todo!(),
        Denomination::MicroBitcoin => todo!(),
        Denomination::Bit => todo!(),
        Denomination::Satoshi => todo!(),
        Denomination::PreventCastingVariantAsInt(infallible) => todo!(),
    }
}

Completion:

image

@tcharding
Copy link
Copy Markdown
Member Author

Damn, yes that is ugly.

/// satoshi (1 BTC = 100,000,000 satoshi).
Satoshi,
#[doc(hidden)]
PreventCastingVariantAsInt(Infallible),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

By convention (and perhaps to silence lints) such hidden enum variants typically start with underscore

Denomination::MicroBitcoin => -2,
Denomination::Bit => -2,
Denomination::Satoshi => 0,
Denomination::PreventCastingVariantAsInt(_) => i8::MIN,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No, this has to return match {} to convey the impossibility.

Denomination::MicroBitcoin => "uBTC",
Denomination::Bit => "bits",
Denomination::Satoshi => "satoshi",
Denomination::PreventCastingVariantAsInt(_) => "no-use",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here.

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Feb 26, 2025

@jirijakes if you're experiencing it in a downstream crate that's a bug in RA worth reporting. Anyway, I do think this is a hack and ideally the language would have a first class support for this. But it's also possible that underscore will fix it.

Either way, it's better to have an extra variant which obviously shouldn't be used (Maybe we should have "DoNotUse" in the name) than having a silent breakage like the last time.

@jirijakes
Copy link
Copy Markdown
Contributor

if you're experiencing it in a downstream crate

No, it it was within rust-bitcoin.

And really, in downstream, the hidden variant is suppressed. No underscore even needed. Interesting. Is it because of doc hidden?

@Kixunil
Copy link
Copy Markdown
Collaborator

Kixunil commented Feb 26, 2025

Great!

Is it because of doc hidden?

Yes, that should be the reason.

@tcharding tcharding force-pushed the 02-26-no-cast branch 2 times, most recently from c0a1150 to 5645b6b Compare March 4, 2025 02:39
@tcharding tcharding requested a review from Kixunil March 4, 2025 02:39
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2025

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

1 similar comment
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2025

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

Kixunil
Kixunil previously requested changes Mar 4, 2025
Satoshi,
/// Stops users from casting this enum to an integer.
#[doc(hidden)]
_Never(Infallible),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The previous name was better, just the underscore was something worth adding.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I thought that because it may show up in rust-analyzer that a really long name was annoying and _Never would be less so?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why would it be annoying? But anyway, it only shows up in our crate, not downstream. If you want something terse that conveys the intent name it _DoNotUse (and explicitly tell in comment that this may get removed if one day Rust supports disabling casts natively).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Used as suggested.

Denomination::MicroBitcoin => -2,
Denomination::Bit => -2,
Denomination::Satoshi => 0,
Denomination::_Never(_) => unreachable!("cannot create this variant"),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This can be statically proven and it should be.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I couldn't get the match {} thing to build, I should have said that when pushing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's impossible, we've used that idiom multiple times in the crate. What's the error message?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe the syntax you want is

...
Denomination::_Never(x) => match x  {}
...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agh, thanks!

A public enum with simple variants gets an automatic integer variant
that can be cast by library consumers. This puts a unnecessary
maintenance burden upon us because we cannot then add variants in the
middle of others.

Add a hidden variant to the single public non-error enum in `units`.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 6, 2025

🚨 API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

Copy link
Copy Markdown
Collaborator

@Kixunil Kixunil left a comment

Choose a reason for hiding this comment

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

ACK 97453ef

Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK 97453ef; successfully ran local tests

@apoelstra apoelstra merged commit 1a18ff5 into rust-bitcoin:master Mar 7, 2025
24 checks passed
@tcharding tcharding deleted the 02-26-no-cast branch March 9, 2025 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API break This PR requires a version bump for the next release C-units PRs modifying the units crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants