Repository navigation
decimal: add a native arbitrary-precision decimal engine #8699
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
Closed
Closed
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
27f54ce
decimal: add a native arbitrary-precision decimal engine
moreal db717b0
_decimal: ship the native engine as decimal's accelerator
moreal 9343fcf
decimal: fix rounding, overflow, and unbounded-allocation bugs from r…
moreal b04719c
decimal: fix two interpreter-aborting crashes from a second review pass
moreal b1d9776
decimal: guard clamp coefficient growth, propagate next_plus/minus ma…
moreal a2a969e
decimal: guard fixed-point rescale allocation, fix from_number subcla…
moreal 64d35ac
decimal: restore SignalDict's mutable-mapping surface
moreal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,4 +30,7 @@ pytype | |
| qsbr | ||
| rustix | ||
| struc | ||
| unallocatable | ||
| unscanned | ||
| untrapped | ||
| zelf | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [package] | ||
| name = "rustpython-decimal" | ||
| description = "Arbitrary-precision decimal arithmetic engine following the General Decimal Arithmetic Specification." | ||
| version.workspace = true | ||
| authors.workspace = true | ||
| edition.workspace = true | ||
| rust-version.workspace = true | ||
| repository.workspace = true | ||
| license.workspace = true | ||
|
|
||
| [dependencies] | ||
| malachite-bigint = { workspace = true } | ||
| num-traits = { workspace = true } | ||
| num-integer = { workspace = true } | ||
|
|
||
| [lints] | ||
| workspace = true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| //! Decimal-oriented helpers on top of arbitrary-precision integers. | ||
| //! | ||
| //! `Lib/_pydecimal.py` keeps a coefficient as a string of digits and leans on | ||
| //! `len(str(n))`, slicing and `int(...)` round-trips. Here the coefficient is a | ||
| //! `BigUint`, so the equivalent operations are expressed as divisions by powers | ||
| //! of ten; these helpers provide them together with a cache of the powers. | ||
|
|
||
| use alloc::vec::Vec; | ||
| use core::cell::RefCell; | ||
| use malachite_bigint::BigUint; | ||
| use num_traits::{One, ToPrimitive, Zero}; | ||
|
|
||
| /// `10.pow(e)` for every `e` a `u128` can hold exactly. | ||
| const SMALL_POW10: [u128; 39] = { | ||
| let mut table = [1u128; 39]; | ||
| let mut i = 1; | ||
| while i < 39 { | ||
| table[i] = table[i - 1] * 10; | ||
| i += 1; | ||
| } | ||
| table | ||
| }; | ||
|
|
||
| thread_local! { | ||
| /// `10.pow(1 << k)` for the `k`s reached so far, used to build large powers | ||
| /// of ten by squaring instead of by multiplying ten in a loop. | ||
| static POW10_SQUARES: RefCell<Vec<BigUint>> = const { RefCell::new(Vec::new()) }; | ||
| } | ||
|
|
||
| /// `10.pow(e)`. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if the result cannot be allocated, i.e. for exponents beyond what the | ||
| /// caller's precision limits should already have excluded. | ||
| pub fn pow10(e: u64) -> BigUint { | ||
| if let Some(small) = SMALL_POW10.get(e as usize) { | ||
| return BigUint::from(*small); | ||
| } | ||
| POW10_SQUARES.with(|cache| { | ||
| let mut cache = cache.borrow_mut(); | ||
| if cache.is_empty() { | ||
| cache.push(BigUint::from(10u32)); | ||
| } | ||
| let mut result = BigUint::one(); | ||
| let mut rest = e; | ||
| let mut k = 0usize; | ||
| while rest != 0 { | ||
| if k == cache.len() { | ||
| let next = &cache[k - 1] * &cache[k - 1]; | ||
| cache.push(next); | ||
| } | ||
| if rest & 1 != 0 { | ||
| result *= &cache[k]; | ||
| } | ||
| rest >>= 1; | ||
| k += 1; | ||
| } | ||
| result | ||
| }) | ||
| } | ||
|
|
||
| /// Number of decimal digits in `n`, counting `0` as one digit. | ||
| pub fn digit_count(n: &BigUint) -> i64 { | ||
| if let Some(small) = n.to_u128() { | ||
| return digit_count_u128(small); | ||
| } | ||
| // `n.bits() * log10(2)` under-estimates `floor(log10(n))` by at most one. | ||
| let mut d = ((u128::from(n.bits()) * 1233) >> 12) as u64; | ||
| while *n < pow10(d) { | ||
| d -= 1; | ||
| } | ||
| while *n >= pow10(d + 1) { | ||
| d += 1; | ||
| } | ||
| d as i64 + 1 | ||
| } | ||
|
|
||
| /// Number of decimal digits in `n`, counting `0` as one digit. | ||
| pub fn digit_count_u128(n: u128) -> i64 { | ||
| match SMALL_POW10.binary_search(&n) { | ||
| Ok(i) => i as i64 + 1, | ||
| Err(i) => i as i64, | ||
| } | ||
| .max(1) | ||
| } | ||
|
|
||
| /// `n * 10.pow(e)`. | ||
| pub fn mul_pow10(n: &BigUint, e: u64) -> BigUint { | ||
| if n.is_zero() || e == 0 { | ||
| return n.clone(); | ||
| } | ||
| n * pow10(e) | ||
| } | ||
|
|
||
| /// `n // 10.pow(e)`, i.e. the digits of `n` with the last `e` dropped. | ||
| pub fn div_pow10(n: &BigUint, e: u64) -> BigUint { | ||
| if n.is_zero() || e == 0 { | ||
| return n.clone(); | ||
| } | ||
| n / pow10(e) | ||
| } | ||
|
|
||
| /// `(n // 10.pow(e), n % 10.pow(e))`. | ||
| pub fn split_pow10(n: &BigUint, e: u64) -> (BigUint, BigUint) { | ||
| if e == 0 { | ||
| return (n.clone(), BigUint::zero()); | ||
| } | ||
| if n.is_zero() { | ||
| return (BigUint::zero(), BigUint::zero()); | ||
| } | ||
| let p = pow10(e); | ||
| let q = n / &p; | ||
| let r = n - &q * &p; | ||
| (q, r) | ||
| } | ||
|
|
||
| /// `n % 10.pow(e)`, i.e. the last `e` digits of `n`. | ||
| pub fn rem_pow10(n: &BigUint, e: u64) -> BigUint { | ||
| split_pow10(n, e).1 | ||
| } | ||
|
|
||
| /// The number of times ten divides `n`, or `None` when `n` is zero. | ||
| pub fn trailing_zeros10(n: &BigUint) -> Option<u64> { | ||
| if n.is_zero() { | ||
| return None; | ||
| } | ||
| // A power of ten dividing `n` divides both two and five out of it, so the | ||
| // count is bounded by the number of trailing binary zeros. | ||
| let bound = n.trailing_zeros().unwrap_or(0); | ||
| let mut lo = 0u64; | ||
| let mut hi = bound + 1; | ||
| while lo < hi { | ||
| let mid = lo + (hi - lo).div_ceil(2); | ||
| if rem_pow10(n, mid).is_zero() { | ||
| lo = mid; | ||
| } else { | ||
| hi = mid - 1; | ||
| } | ||
| } | ||
| Some(lo) | ||
| } | ||
|
|
||
| /// The decimal digits of `n`, most significant first, without leading zeros | ||
| /// (`0` renders as a single `0` digit). | ||
| pub fn to_digits(n: &BigUint) -> Vec<u8> { | ||
| n.to_str_radix(10).into_bytes() | ||
| } | ||
|
|
||
| /// Builds an integer from ASCII decimal digits. | ||
| pub fn from_ascii_digits(digits: &[u8]) -> BigUint { | ||
| if digits.is_empty() { | ||
| return BigUint::zero(); | ||
| } | ||
| BigUint::parse_bytes(digits, 10).expect("caller must pass ASCII decimal digits") | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| //! Arithmetic context: the precision, exponent range and rounding that shape | ||
| //! every result. | ||
| //! | ||
| //! Flags and traps live in the binding layer; an engine operation only reports | ||
| //! the conditions it raised through a status word, exactly as libmpdec does. | ||
|
|
||
| use crate::{MAX_EMAX, MAX_PREC, MIN_EMIN}; | ||
|
|
||
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
| pub enum RoundMode { | ||
| Down, | ||
| Up, | ||
| HalfUp, | ||
| HalfDown, | ||
| HalfEven, | ||
| Ceiling, | ||
| Floor, | ||
| ZeroFiveUp, | ||
| } | ||
|
|
||
| impl RoundMode { | ||
| pub const ALL: [Self; 8] = [ | ||
| Self::Down, | ||
| Self::HalfUp, | ||
| Self::HalfEven, | ||
| Self::Ceiling, | ||
| Self::Floor, | ||
| Self::Up, | ||
| Self::HalfDown, | ||
| Self::ZeroFiveUp, | ||
| ]; | ||
|
|
||
| /// The Python-level name, e.g. `ROUND_HALF_EVEN`. | ||
| pub const fn name(self) -> &'static str { | ||
| match self { | ||
| Self::Down => "ROUND_DOWN", | ||
| Self::Up => "ROUND_UP", | ||
| Self::HalfUp => "ROUND_HALF_UP", | ||
| Self::HalfDown => "ROUND_HALF_DOWN", | ||
| Self::HalfEven => "ROUND_HALF_EVEN", | ||
| Self::Ceiling => "ROUND_CEILING", | ||
| Self::Floor => "ROUND_FLOOR", | ||
| Self::ZeroFiveUp => "ROUND_05UP", | ||
| } | ||
| } | ||
|
|
||
| pub fn from_name(name: &str) -> Option<Self> { | ||
| Self::ALL.into_iter().find(|mode| mode.name() == name) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
| pub struct Context { | ||
| pub prec: i64, | ||
| pub emin: i64, | ||
| pub emax: i64, | ||
| pub round: RoundMode, | ||
| pub capitals: bool, | ||
| pub clamp: bool, | ||
| } | ||
|
|
||
| impl Default for Context { | ||
| fn default() -> Self { | ||
| Self { | ||
| prec: 28, | ||
| emin: -999_999, | ||
| emax: 999_999, | ||
| round: RoundMode::HalfEven, | ||
| capitals: true, | ||
| clamp: false, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Context { | ||
| /// The widest context the module can represent, used for the exact | ||
| /// conversions that `Decimal(...)` performs without a context. | ||
| pub const fn max() -> Self { | ||
| Self { | ||
| prec: MAX_PREC, | ||
| emin: MIN_EMIN, | ||
| emax: MAX_EMAX, | ||
| round: RoundMode::HalfEven, | ||
| capitals: true, | ||
| clamp: false, | ||
| } | ||
| } | ||
|
|
||
| /// Smallest exponent a subnormal result may have. | ||
| #[inline] | ||
| pub const fn etiny(&self) -> i64 { | ||
| self.emin - (self.prec - 1) | ||
| } | ||
|
|
||
| /// Largest exponent a result with a full-precision coefficient may have. | ||
| #[inline] | ||
| pub const fn etop(&self) -> i64 { | ||
| self.emax - (self.prec - 1) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this safe tls?