Repository navigation
Conversation
A new `rustpython-decimal` crate implements the General Decimal Arithmetic Specification over `malachite-bigint`: construction and the numeric-string grammar, the full arithmetic set, comparison and total ordering, the digit-wise and exponent operations, correctly rounded sqrt/exp/ln/log10/power, and the format-specification mini-language. The algorithms are ported from CPython's `Lib/_pydecimal.py`, which is the executable specification for the parts the standard leaves open; the coefficient is a `BigUint` with a cached digit count instead of a digit string, so what the Python does with slicing is done here with divisions by powers of ten. Conditions follow libmpdec rather than `_pydecimal`: an operation never raises, it accumulates the conditions it hit into a status word that the caller merges into a context. That is what lets a trapped signal report every condition an operation produced rather than only the first. Assisted-by: Claude Code:claude-fable-5-1
`Lib/decimal.py` starts with `from _decimal import *` and falls back to
`Lib/_pydecimal.py` when that fails. There was no `_decimal`, so every program
using decimals ran the pure-Python implementation.
This wires the `rustpython-decimal` engine up as that module: the `Decimal` and
`Context` types with their whole method surface, the signal hierarchy with
libmpdec's condition ordering, the live `flags`/`traps` views, the
context-variable holding the current context, and `localcontext`/`IEEEContext`.
Signatures carry CPython's text signatures, so `inspect.signature` and the
suite's C/Python signature comparison agree.
test_decimal now runs its C classes as well as its Python ones -- 721 tests
including all 143 IBM `decimaltestdata` files -- and passes. A differential
check of 16000 random operations against CPython's `_decimal` agrees on every
result and every status flag.
pyperformance telco: 348 ms/iteration -> 15.4 ms (23x; CPython 3.14 is 4.5 ms).
Decimal('1.23')*Decimal('4.56') 5275 ns -> 267 ns, quantize 6814 ns -> 423 ns,
str() 1658 ns -> 497 ns.
Assisted-by: Claude Code:claude-fable-5-1
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Merging this PR will regress 1 benchmark
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | gc_traversal.py[rustpython] |
683.2 ms | 826.8 ms | -17.37% |
| ⚡ | decimal_factorial.py[rustpython] |
1,228.9 ms | 35 ms | ×35 |
| ⚡ | gc_collect.py[rustpython] |
83.4 ms | 59.7 ms | +39.7% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing moreal:agent/decimal-native-engine (64d35ac) with main (e18facf)
youknowone
left a comment
There was a problem hiding this comment.
Do we really need to reimplement everything ourselves? How about other external libraries? Is there any good decimal libraries?
| table | ||
| }; | ||
|
|
||
| thread_local! { |
…eview
- next_plus/next_minus/next_toward: replace a hand-rolled exact-add
helper that scaled operands without a precision bound (causing
gigabyte-scale allocations for extreme Emin) with a direct call to
the already-bounded arith::add.
- bigops::div_pow10/split_pow10: short-circuit to zero when the shift
exceeds the coefficient's digit count, instead of materializing a
huge power of ten (e.g. int(Decimal('1e-999999999999999999'))).
- Decimal.__round__(ndigits): honor the context's rounding mode
instead of hardcoding ROUND_HALF_EVEN, matching quantize's default.
- setcontext(): clear the copied context's status flags when
installing one of the module's template contexts, matching
_pydecimal's fresh Context.copy() semantics for that path.
- parse_ascii_raw: clamp an out-of-i64-range exponent instead of
rejecting the string outright, so context-based conversion still
produces Infinity/Overflow the way an in-range huge exponent would.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| pub(crate) mod macros; | ||
|
|
||
| mod _asyncio; | ||
| #[path = "_decimal.rs"] |
There was a problem hiding this comment.
| #[path = "_decimal.rs"] |
- fmt::format: bound the fixed-point padding derived from a value's
exponent the same way width/precision are already bounded, so
format(Decimal('1e-999999999999999999'), 'f') raises ValueError
instead of attempting a ~10^18-byte allocation.
- Decimal.as_integer_ratio: an exponent whose reduced power-of-five
denominator doesn't fit a u32 (e.g. Decimal('1e-4294967296')) now
raises MemoryError instead of panicking past an .expect().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lloc errors - fix's clamp branch (Context.clamp=1 with an oversized exponent) now checks MAX_MATERIALIZABLE_DIGITS before padding the coefficient with mul_pow10, matching every other guarded arithmetic path, instead of attempting a ~10^18-digit allocation for a valid context. - next_plus/next_minus now propagate a MALLOC_ERROR raised by the underlying add back to the caller instead of discarding it in their scratch status word, so an allocation failure surfaces as MemoryError (next_toward inherits the fix through both). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ssing
- fmt::format: check the allocation bound before rescaling a nonzero
value to an explicit fixed-point precision, not only afterwards, so
format(Decimal('1e999999999999999999'), '.0f') raises ValueError
instead of materializing a ~10^18-digit coefficient.
- Decimal.from_number: build through the subclass constructor the way
from_float already does, instead of allocating the payload directly
and bypassing a subclass's __new__/__init__.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
ok, I investigated the situation a little bit. CPython decimal expects a specific implementation of decimal - libmpdec. Your patch is not a port of libmpdec, but it implementing generic decimal arithmatic engine so that compatible to libmpdec. Could you tell what our new decimal crate is referencing? Probably we will not easily reach to perf parity building a new one from scratch. Let's directly port libmpdec into Rust. And place it on RustPython/mpdec-rs. This library must be equally fast as mpdec and the calc result must be exactly same as mpdec. The new repository also must have fuzzy test. |
I agree that, since the To be honest, I had set up an iterative process focused on improving The approach I had been taking was to port the existing pure-Python Thank you for pointing this out. 🙏🏻 |
CPython's SignalDict mixes in collections.abc.MutableMapping, so
getcontext().traps.update(...)/.setdefault(...) work on top of
__getitem__/__setitem__ alone. The native class only implemented the
mapping protocol itself, so update and setdefault raised
AttributeError. Add update, setdefault, pop, popitem and clear,
matching MutableMapping's default behavior (the latter three always
raise ValueError("signal keys cannot be deleted"), since deletion is
never allowed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This pull request is closed because it will be worked in other direction.