Skip to content

bpo-41324 Add a minimal decimal capsule API #21519

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

Merged
merged 21 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8754016
Add a minimal decimal capsule API.
skrah Jul 17, 2020
72f95d8
Add PyDec_GetConst() and PyDec_GetDigits().
skrah Jul 18, 2020
83366fb
Group the functions into simple and advanced API.
skrah Jul 18, 2020
3636589
Add PyDec_IsSpecial(), PyDec_IsNaN() and PyDec_IsInfinite().
skrah Jul 18, 2020
3208099
Mention ABI stability of mpd_t.
skrah Jul 18, 2020
fa6e201
Remove test_decimal_api_get_digits() duplicate.
skrah Jul 18, 2020
3a0db41
Move the TypeError into PyDec_AsUint128Triple() for a consistent API.
skrah Jul 18, 2020
2fcc6ff
Fix error message.
skrah Jul 18, 2020
9ac33c3
Add documentation.
skrah Jul 18, 2020
d8e23da
Appease the Sphinx linter.
skrah Jul 18, 2020
948a62e
Rename decimal.h to pydecimal.h.
skrah Jul 20, 2020
cff9084
Move decimal test functions out of the datetime section.
skrah Jul 20, 2020
fff8826
Fix typo.
skrah Jul 20, 2020
193bc41
Merge branch 'master' of https://github.com/python/cpython into minim…
skrah Jul 20, 2020
79c46d9
Make maxcontext a static const.
skrah Jul 20, 2020
9bd11b6
Merge branch 'master' of https://github.com/python/cpython into minim…
skrah Jul 22, 2020
81992c5
Remove libmpdec_triple.h.
skrah Jul 22, 2020
b62baf6
Merge branch 'master' of https://github.com/python/cpython into minim…
skrah Aug 8, 2020
dd860dd
Merge branch 'master' of https://github.com/python/cpython into minim…
skrah Aug 10, 2020
3bf0e2d
Fix array type (not that it matters with all that capsule casting).
skrah Aug 10, 2020
ec73b29
Add blurb.
skrah Aug 10, 2020
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
1 change: 1 addition & 0 deletions Doc/c-api/concrete.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,4 @@ Other Objects
coro.rst
contextvars.rst
datetime.rst
decimal.rst
231 changes: 231 additions & 0 deletions Doc/c-api/decimal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
.. sectionauthor:: Stefan Krah

.. highlight:: c


Decimal capsule API
===================

Capsule API functions can be used in the same manner as regular library
functions, provided that the API has been initialized.


Initialize
----------

Typically, a C extension module that uses the decimal API will do these
steps in its init function:

.. code-block::

#include "pydecimal.h"

static int decimal_initialized = 0;
if (!decimal_initialized) {
if (import_decimal() < 0) {
return NULL;
}

decimal_initialized = 1;
}


Type checking, predicates, accessors
------------------------------------

.. c:function:: int PyDec_TypeCheck(const PyObject *dec)

Return 1 if ``dec`` is a Decimal, 0 otherwise. This function does not set
any exceptions.


.. c:function:: int PyDec_IsSpecial(const PyObject *dec)

Return 1 if ``dec`` is ``NaN``, ``sNaN`` or ``Infinity``, 0 otherwise.

Set TypeError and return -1 if ``dec`` is not a Decimal. It is guaranteed that
this is the only failure mode, so if ``dec`` has already been type-checked, no
errors can occur and the function can be treated as a simple predicate.


.. c:function:: int PyDec_IsNaN(const PyObject *dec)

Return 1 if ``dec`` is ``NaN`` or ``sNaN``, 0 otherwise.

Set TypeError and return -1 if ``dec`` is not a Decimal. It is guaranteed that
this is the only failure mode, so if ``dec`` has already been type-checked, no
errors can occur and the function can be treated as a simple predicate.


.. c:function:: int PyDec_IsInfinite(const PyObject *dec)

Return 1 if ``dec`` is ``Infinity``, 0 otherwise.

Set TypeError and return -1 if ``dec`` is not a Decimal. It is guaranteed that
this is the only failure mode, so if ``dec`` has already been type-checked, no
errors can occur and the function can be treated as a simple predicate.


.. c:function:: int64_t PyDec_GetDigits(const PyObject *dec)

Return the number of digits in the coefficient. For ``Infinity``, the
number of digits is always zero. Typically, the same applies to ``NaN``
and ``sNaN``, but both of these can have a payload that is equivalent to
a coefficient. Therefore, ``NaNs`` can have a nonzero return value.

Set TypeError and return -1 if ``dec`` is not a Decimal. It is guaranteed that
this is the only failure mode, so if ``dec`` has already been type-checked, no
errors can occur and the function can be treated as a simple accessor.


Exact conversions between decimals and primitive C types
--------------------------------------------------------

This API supports conversions for decimals with a coefficient up to 38 digits.

Data structures
~~~~~~~~~~~~~~~

The conversion functions use the following status codes and data structures:

.. code-block::

/* status cases for getting a triple */
enum mpd_triple_class {
MPD_TRIPLE_NORMAL,
MPD_TRIPLE_INF,
MPD_TRIPLE_QNAN,
MPD_TRIPLE_SNAN,
MPD_TRIPLE_ERROR,
};

typedef struct {
enum mpd_triple_class tag;
uint8_t sign;
uint64_t hi;
uint64_t lo;
int64_t exp;
} mpd_uint128_triple_t;

The status cases are explained below. ``sign`` is 0 for positive and 1 for negative.
``((uint128_t)hi << 64) + lo`` is the coefficient, ``exp`` is the exponent.

The data structure is called "triple" because the decimal triple (sign, coeff, exp)
is an established term and (``hi``, ``lo``) represents a single ``uint128_t`` coefficient.


Functions
~~~~~~~~~

.. c:function:: mpd_uint128_triple_t PyDec_AsUint128Triple(const PyObject *dec)

Convert a decimal to a triple. As above, it is guaranteed that the only
Python failure mode is a TypeError, checks can be omitted if the type is
known.

For simplicity, the usage of the function and all special cases are
explained in code form and comments:

.. code-block::

triple = PyDec_AsUint128Triple(dec);
switch (triple.tag) {
case MPD_TRIPLE_QNAN:
/*
* Success: handle a quiet NaN.
* 1) triple.sign is 0 or 1.
* 2) triple.exp is always 0.
* 3) If triple.hi or triple.lo are nonzero, the NaN has a payload.
*/
break;

case MPD_TRIPLE_SNAN:
/*
* Success: handle a signaling NaN.
* 1) triple.sign is 0 or 1.
* 2) triple.exp is always 0.
* 3) If triple.hi or triple.lo are nonzero, the sNaN has a payload.
*/
break;

case MPD_TRIPLE_INF:
/*
* Success: handle Infinity.
* 1) triple.sign is 0 or 1.
* 2) triple.exp is always 0.
* 3) triple.hi and triple.lo are always zero.
*/
break;

case MPD_TRIPLE_NORMAL:
/* Success: handle a finite value. */
break;

case MPD_TRIPLE_ERROR:
/* TypeError check: can be omitted if the type of dec is known. */
if (PyErr_Occurred()) {
return NULL;
}

/* Too large for conversion. PyDec_AsUint128Triple() does not set an
exception so applications can choose themselves. Typically this
would be a ValueError. */
PyErr_SetString(PyExc_ValueError,
"value out of bounds for a uint128 triple");
return NULL;
}

.. c:function:: PyObject *PyDec_FromUint128Triple(const mpd_uint128_triple_t *triple)

Create a decimal from a triple. The following rules must be observed for
initializing the triple:

1) ``triple.sign`` must always be 0 (for positive) or 1 (for negative).

2) ``MPD_TRIPLE_QNAN``: ``triple.exp`` must be 0. If ``triple.hi`` or ``triple.lo``
are nonzero, create a ``NaN`` with a payload.

3) ``MPD_TRIPLE_SNAN``: ``triple.exp`` must be 0. If ``triple.hi`` or ``triple.lo``
are nonzero, create an ``sNaN`` with a payload.

4) ``MPD_TRIPLE_INF``: ``triple.exp``, ``triple.hi`` and ``triple.lo`` must be zero.

5) ``MPD_TRIPLE_NORMAL``: ``MPD_MIN_ETINY + 38 < triple.exp < MPD_MAX_EMAX - 38``.
``triple.hi`` and ``triple.lo`` can be chosen freely.

6) ``MPD_TRIPLE_ERROR``: It is always an error to set this tag.


If one of the above conditions is not met, the function returns ``NaN`` if
the ``InvalidOperation`` trap is not set in the thread local context. Otherwise,
it sets the ``InvalidOperation`` exception and returns NULL.

Additionally, though extremely unlikely give the small allocation sizes,
the function can set ``MemoryError`` and return ``NULL``.


Advanced API
------------

This API enables the use of ``libmpdec`` functions. Since Python is compiled with
hidden symbols, the API requires an external libmpdec and the ``mpdecimal.h``
header.


Functions
~~~~~~~~~

.. c:function:: PyObject *PyDec_Alloc(void)

Return a new decimal that can be used in the ``result`` position of ``libmpdec``
functions.

.. c:function:: mpd_t *PyDec_Get(PyObject *v)

Get a pointer to the internal ``mpd_t`` of the decimal. Decimals are immutable,
so this function must only be used on a new Decimal that has been created by
PyDec_Alloc().

.. c:function:: const mpd_t *PyDec_GetConst(const PyObject *v)

Get a pointer to the constant internal ``mpd_t`` of the decimal.
Loading