Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .cspell.dict/cpython.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,18 @@ dictbytype
DICTFLAG
dictoffset
distpoint
dpower
dynload
echar
elts
Emax
Emin
eooh
eofs
EOOH
esize
Etiny
Etop
evalloop
excepthandler
exceptiontable
Expand Down Expand Up @@ -105,6 +111,7 @@ heaptype
hexdigit
HIGHRES
ialloc
iexp
IFUNC
IMMUTABLETYPE
INCREF
Expand All @@ -114,6 +121,7 @@ inpos
ioffset
isbytecode
ishidden
islogical
ismine
ISPOINTER
isoctal
Expand All @@ -127,6 +135,7 @@ kwonlyargs
kwonlydefaults
lasti
libffi
libmpdec
linearise
lineful
lineiterator
Expand All @@ -146,6 +155,7 @@ mbstreamreader
mbstreamwriter
metavars
miscompiles
mshift
mult
multibytecodec
mystricmp
Expand Down Expand Up @@ -224,6 +234,7 @@ retarget
Rshift
SA_ONSTACK
saveall
scaleb
scls
setdict
setfunc
Expand All @@ -245,6 +256,7 @@ storefast
stringlib
stringized
structseq
strxc
subkwargs
subparams
subscr
Expand All @@ -260,6 +272,7 @@ threadstate
ticketer
tmptype
tok_oldval
torot
tstate
tvars
typeobject
Expand Down Expand Up @@ -295,3 +308,4 @@ xibufferview
xstat
XXPRIME
ENOTCAPABLE
yshift
3 changes: 3 additions & 0 deletions .cspell.dict/rustpython.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ pytype
qsbr
rustix
struc
unallocatable
unscanned
untrapped
zelf
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ rustpython-compiler-core = { path = "crates/compiler-core", version = "0.5.0" }
rustpython-compiler = { path = "crates/compiler", version = "0.5.0" }
rustpython-codegen = { path = "crates/codegen", version = "0.5.0" }
rustpython-common = { path = "crates/common", version = "0.5.0" }
rustpython-decimal = { path = "crates/decimal", version = "0.5.0" }
rustpython-host_env = { path = "crates/host_env", version = "0.5.0" }
rustpython-derive = { path = "crates/derive", version = "0.5.0" }
rustpython-derive-impl = { path = "crates/derive-impl", version = "0.5.0" }
Expand Down
17 changes: 17 additions & 0 deletions crates/decimal/Cargo.toml
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
156 changes: 156 additions & 0 deletions crates/decimal/src/bigops.rs
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! {

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.

Is this safe tls?

/// `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")
}
100 changes: 100 additions & 0 deletions crates/decimal/src/context.rs
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)
}
}
Loading
Loading