Followups to #4157: remove Amount::from_sat_unchecked#4256
Followups to #4157: remove Amount::from_sat_unchecked#4256apoelstra merged 8 commits intorust-bitcoin:masterfrom
Conversation
The `from_int_btc_const` constructors are specifically designed for easily creating amount types in const context but currently they return an error which is annoying to handle in const context. If we make the `whole_bitcoin` parameter a 16 bit integer this gives us a nicer const constructor with the downside that it can only create values upto a maximum of - unsigned: 65_536 - signed: 32_767 That is plenty high enough for most use cases. Then use the new `from_int_btc_const` in associated consts. Note that because `from_sat` checks max (and min) values we must define max and min from sats directly.
|
🚨 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. |
Pull Request Test Coverage Report for Build 13931957355Details
💛 - Coveralls |
|
API breaks are as expected: renaming |
|
Oh, oops, we need to deprecate Edit done |
e71aa08 to
61c54aa
Compare
|
🚨 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. |
| None => panic!("cannot overflow in i64"), | ||
| let sats = btc * 100_000_000; | ||
|
|
||
| match SignedAmount::from_sat(sats) { |
There was a problem hiding this comment.
I tend to agree that just a comment saying why this won't panic is more readable. Otherwise I might read this and think why is there an err(_) arm.
There was a problem hiding this comment.
I guess performance doesn't matter here since it's const context? There are some Acks for this PR so I know it's not going to be changed now, I'm just mostly curious for my own understanding.
| /// | ||
| /// Caller to guarantee that `satoshi` is within valid range. See [`Self::MAX`]. | ||
| pub const fn from_sat_unchecked(satoshi: u64) -> Amount { Self(satoshi) } | ||
| /// Accepts an `u32` which is guaranteed to be in range for the type, but which can only |
There was a problem hiding this comment.
| /// Accepts an `u32` which is guaranteed to be in range for the type, but which can only | |
| /// Accepts a `u32` which is guaranteed to be in range for the type, but which can only |
There was a problem hiding this comment.
Can you please post what commit you're commenting on? Github shows 3 or 4 lines of context and nothing else so it is very hard to figure out what change you're talking about without further hints.
There was a problem hiding this comment.
It's 61c54aa
But it's unrelated, so not needed for this PR. Could be added to the typos list.
There was a problem hiding this comment.
It's just a minor nit on grammar: beaa2db#diff-0ced1794ac364cd494a4351ae6baed7b4d14d3dc673b945f3eae8f83e4e93844R62
Should be a instead of an since followed by something that is or sounds like a vowel.
There was a problem hiding this comment.
should still be a I think but feel free to disregard my nit picking of course.
| /// in const context. | ||
| #[allow(clippy::missing_panics_doc)] | ||
| pub const fn from_int_btc_const(whole_bitcoin: u16) -> Amount { | ||
| pub const fn from_btc_u16(whole_bitcoin: u16) -> Amount { |
There was a problem hiding this comment.
Should this be u32? The idea being that u32 max is greater than max number of bitcoins 21 million? u16 is 65,535 which isn't enough to represent every bitcoin.
There was a problem hiding this comment.
Ok, I guess you mean to have this for cases where only a small number of bitcoin are represented and to use from_btc for other cases? It seems confusing but if that's the intent then it's not a mistake.
There was a problem hiding this comment.
Ah I see, this allows for a restricted bounds since u32 max would be 429496729500000000 sats which is greater than the maximum number sats. Sorry, didn't think that through before.
| #[allow(clippy::missing_panics_doc)] | ||
| pub fn to_signed(self) -> SignedAmount { | ||
| SignedAmount::from_sat_unchecked(self.to_sat() as i64) // Cast ok, signed amount and amount share positive range. | ||
| SignedAmount::from_sat(self.to_sat() as i64) // Cast ok, signed amount and amount share positive range. |
There was a problem hiding this comment.
The comment and the expect message are saying the same thing. Maybe just move the comment into the expect message?
There was a problem hiding this comment.
No, they're not. The comment is about the cast. The expect is about the call to from_sat.
Kixunil
left a comment
There was a problem hiding this comment.
Cool! The PR improved more things than I expected. Just a few suggestions to make it even better.
units/src/amount/signed.rs
Outdated
| /// in const context. | ||
| #[allow(clippy::missing_panics_doc)] | ||
| #[deprecated(since = "TBD", note = "renamed to `from_btc_i16`")] | ||
| pub const fn from_int_btc_const(whole_bitcoin: i16) -> SignedAmount { |
There was a problem hiding this comment.
This also changed type so wouldn't it be better to backport the deprecation?
There was a problem hiding this comment.
Ah, you mean, backport only the deprecation but not the type change?
There was a problem hiding this comment.
To be clearer:
- Deprecate the
from_int_btcin 0.32.x and backport the newfrom_btc_i16to 0.32.x - Then just delete
from_int_btcin master since its type changed (and the old type signature is untenable because of the MAX_MONEY changes)
There was a problem hiding this comment.
Yes, that's what I meant.
There was a problem hiding this comment.
Changed to just delete the old method. Separately we should backport a deprecation of it.
units/src/amount/unsigned.rs
Outdated
| return Err(ParseAmountError(ParseAmountErrorInner::OutOfRange( | ||
| OutOfRangeError::negative(), | ||
| ))), | ||
| }; |
There was a problem hiding this comment.
Yeah, if we returned SignedAmount this entire thing would simplify to if is_neg { return ... }
| /// | ||
| /// Caller to guarantee that `satoshi` is within valid range. See [`Self::MAX`]. | ||
| pub const fn from_sat_unchecked(satoshi: u64) -> Amount { Self(satoshi) } | ||
| /// Accepts an `u32` which is guaranteed to be in range for the type, but which can only |
There was a problem hiding this comment.
It's 61c54aa
But it's unrelated, so not needed for this PR. Could be added to the typos list.
| pub fn to_signed(self) -> SignedAmount { | ||
| SignedAmount::from_sat_unchecked(self.to_sat() as i64) // Cast ok, signed amount and amount share positive range. | ||
| SignedAmount::from_sat(self.to_sat() as i64) // Cast ok, signed amount and amount share positive range. | ||
| .expect("range of Amount is within range of SignedAmount") |
There was a problem hiding this comment.
Honestly, this is the kind of thing where I'd prefer unchecked (by turning it private instead of deleting). The compiler doesn't understand our invariant (even niche-filling optimization that I came up with cannot convey it perfectly, just some other upper bound), so this won't get optimized out in non-trivial code and it's the kind of thing where one wouldn't expect a performance hit.
But it's fine to leave it as-is for now. Even wait until someone complains.
There was a problem hiding this comment.
If you think the compiler won't understand this, we could do a match and an unsafe core::hint::unreachable_unchecked. I'd rather do that than introduce a from_sat_unchecked for a single call site.
Regardless, I think let's leave it for now.
There was a problem hiding this comment.
OK, that's a reasonable approach.
We have `from_int_btc_const` on both `Amount` and `SignedAmount` because the "real" `from_int_btc` is generic over the integer type it accepts, which means that it cannot be a constfn. But we do want a constfn. However, just because `from_int_btc_const` exists for the sake of constfn doesn't mean that that's what it *is*. So rename these methods to reflect what they *are*.
We have a ton of calls to `from_sat_unchecked` for small constants which were clearly in range, e.g. in fee.rs. Add a new constfn for these cases. Don't bother making a generic Into<u32>/Into<u16> variant because there isn't an obvious name for it. There are 7 instances where we're using this method with values that are out of range, which we leave as from_sat_unchecked for now.
There are only 7 instances of this so just call .unwrap() on each one.
There is no need for this. It's a 2-line diff to change it.
This private function is used for string-parsing an amount. It returns a sign boolean and a u64, but its surrounding logic can be simplified if it just returns a i64 (which is OK now since the range of `Amount` fits into the range of i64). Along the way we eliminate some instances of from_sat_unchecked. Annoyingly we still need the bool to distinguish -0 from +0; when parsing Amount we reject -0 (and we have tests for this). This causes a couple error constructors to no longer be used outside of unit tests. May be worth looking at whether we need them, or whether we should be using them in parse_signed_to_satoshi.
…undary It's conceptually a bit tortured to have an `Amount` type defined in a private module, with an _unchecked method allowing you to set values out of range, which needs to be used outside of the module to *define* the range and the constructors that check it. Move the constants and constructors inside the privacy module, where they can be written directly. This is easier to understand and eliminates a couple _unchecked calls.
Turns out we don't even need this. It is only used in one place now and we can just stick an .expect there.
61c54aa to
2958521
Compare
|
🚨 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. |
| ))); | ||
| } | ||
| Ok(Self::from_sat_unchecked(sats)) | ||
| Self::try_from(amount).map_err(|e| ParseAmountError(ParseAmountErrorInner::OutOfRange(e))) |
There was a problem hiding this comment.
Hmm, the error should be unreachable here since we return early if it's negative. It's not trivial to prove. The only somewhat sensible idea I have is something like this:
/// Like SignedAmount but can represent -0
/// Returned from `parse_signed_to_satoshi`
enum FatAmount {
Positive(Amount),
Negative(Amount),
}
// in parse_signed_to_satoshi
if is_negative { FatAmount::Negative(ret) } else { FatAmount::Positive(ret) }
// in Amount::from_str_in
match amount {
FatAmount::Positive(amount) => Ok(amount),
FatAmount::Negative(_) => Err(...),
}
// in SignedAmount::from_str_in
match amount {
FatAmount::Positive(amount) => Ok(amount.into()),
FatAmount::Negative(amount) => Ok(-SignedAmount::from(amount)),
}Though it's a bit heavy, so if you don't think it's worth it it's fine.
There was a problem hiding this comment.
I think it's too heavy. As in the other case, we can use unreachable_unchecked if we want to try to optimize this. But I'd rather not do that now since it's just an optimization and considerably increases the review difficulty.
For now I can replace the map_err with an expect though, since you're right that the error path is actually unreachable now.
There was a problem hiding this comment.
Well, it's also a proof that it won't panic or do something weird (for some cases of "weird"). And as my recent findings suggest, doing too many expects is not really that great. :)
But still, I don't mind either map_err or expect in this PR. This can be improved later.
There was a problem hiding this comment.
I'm gonna leave it as is to avoid losing your ACK.
|
ACK 2958521 |
| fn parse_signed_to_satoshi( | ||
| mut s: &str, | ||
| denom: Denomination, | ||
| ) -> Result<(bool, u64), InnerParseError> { |
There was a problem hiding this comment.
It's strange to see u64 replaced by SignedAmount in the diff. I'd have expected to see i64 replaced by SignedAmount.
There was a problem hiding this comment.
It's explained in the commit message. I suggest you read them.
There was a problem hiding this comment.
I read through them yesterday although I guess this one is new today. Today I was paging through the diff to see what change although I should pay closer attention to what commit messages changed as well.
There was a problem hiding this comment.
Discussions also often provide insight.
| if is_negative { | ||
| ret = -ret; | ||
| } | ||
| Ok((is_negative, ret)) |
There was a problem hiding this comment.
I'm confused why is_negative returned. Doesn't SighnedAmount contain the information about if it's negative using two's compliment?
There was a problem hiding this comment.
Yes, but not if it's negative in decimal notation.
There was a problem hiding this comment.
That's literally a comment that says it's need for negative zero which I failed to read. Funny since the exclusion of negative zero was also added by me.
|
Gentle bump bro - 12 hours come on now :) |
|
As long as no other PR comes in that would break this and trigger re-review of this large change I'm fine with waiting. :) (added P-hight to express that) |
Sorry. I queued up two earlier versions of this, so I wound up testing 24 commits for this, plus 6 for #4248, and overnight I leave "away from keyboard" mode on so when it finished tests it statred testing other things instead of merging. |
|
No stress man, I was just winding you up. |
ca6c607 Adhere to sanity rules for amount types (Tobin C. Harding) 6c614d9 units: Fix panic message (Tobin C. Harding) Pull request description: This is a follow up to #4256 - onwards and upwards! - Patch 1: Fix the incorrect BTC value in panic message - Patch 2: Strictly adhere to the sanity rules (#4090) Close: #4140 ACKs for top commit: apoelstra: ACK ca6c607; successfully ran local tests Tree-SHA512: 6d7fd60830e1a0f6d6262ab02ec6e297b095d0fe8fb7737563979652e4a3b4a9477a79982201c42b08e2555fd23dc5c430549966b534bdf45f40621ae81da83a
This does some followups to #4157.
It pulls in #4254, which changes the signature of
Amount::from_int_btc_constand also replaces achecked_mulwhich can't ever fail with a*.It then renames
from_int_btc_consttofrom_btc_u16/from_btc_i16as appropriate, since these names reflect what the function does. It also adds an analogous method for makingAmounts from sats rather than BTC.Then it systematically removes every instance of
from_sat_unchecked. There wind up being 7 instances in tests that I just added unwraps() to, plus one instance where we hadlet sat = Amount::from_sat_uncheckedwhich I also added an unwrap() to.In the real code, there was one instance (
Amount::to_signed) where I needed to add anexpect. Every other instance I was able to remove, by refactoring logic to use the error case as appropriate.