Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions quaddtype/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ srcs = [
'numpy_quaddtype/src/umath/matmul.h',
'numpy_quaddtype/src/umath/matmul.cpp',
'numpy_quaddtype/src/constants.hpp',
'numpy_quaddtype/src/lock.h',
'numpy_quaddtype/src/lock.c',
]

py.install_sources(
Expand Down
17 changes: 17 additions & 0 deletions quaddtype/numpy_quaddtype/src/lock.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include "lock.h"

#if PY_VERSION_HEX < 0x30d00b3
PyThread_type_lock sleef_lock = NULL;
#else
PyMutex sleef_lock = {0};
#endif

void init_sleef_locks(void)
{
#if PY_VERSION_HEX < 0x30d00b3
sleef_lock = PyThread_allocate_lock();
if (!sleef_lock) {
PyErr_NoMemory();
}
#endif
}
18 changes: 18 additions & 0 deletions quaddtype/numpy_quaddtype/src/lock.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#ifndef _QUADDTYPE_LOCK_H
#define _QUADDTYPE_LOCK_H

#include <Python.h>

#if PY_VERSION_HEX < 0x30d00b3
extern PyThread_type_lock sleef_lock;
#define LOCK_SLEEF PyThread_acquire_lock(sleef_lock, WAIT_LOCK)
#define UNLOCK_SLEEF PyThread_release_lock(sleef_lock)
#else
extern PyMutex sleef_lock;
#define LOCK_SLEEF PyMutex_Lock(&sleef_lock)
#define UNLOCK_SLEEF PyMutex_Unlock(&sleef_lock)
#endif

void init_sleef_locks(void);

#endif // _QUADDTYPE_LOCK_H
3 changes: 3 additions & 0 deletions quaddtype/numpy_quaddtype/src/quaddtype_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "numpy/dtype_api.h"
#include "numpy/ufuncobject.h"

#include "lock.h"
#include "scalar.h"
#include "dtype.h"
#include "umath/umath.h"
Expand Down Expand Up @@ -96,6 +97,8 @@ PyInit__quaddtype_main(void)
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif

init_sleef_locks();

if (init_quadprecision_scalar() < 0)
goto error;

Expand Down
45 changes: 23 additions & 22 deletions quaddtype/numpy_quaddtype/src/scalar.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,14 @@
#include "scalar_ops.h"
#include "dragon4.h"
#include "dtype.h"
#include "lock.h"

// For IEEE 754 binary128 (quad precision), we need 36 decimal digits
// to guarantee round-trip conversion (string -> parse -> equals original value)
// Formula: ceil(1 + MANT_DIG * log10(2)) = ceil(1 + 113 * 0.30103) = 36
// src: https://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format
#define SLEEF_QUAD_DECIMAL_DIG 36

#if PY_VERSION_HEX < 0x30d00b3
static PyThread_type_lock sleef_lock;
#define LOCK_SLEEF PyThread_acquire_lock(sleef_lock, WAIT_LOCK)
#define UNLOCK_SLEEF PyThread_release_lock(sleef_lock)
#else
static PyMutex sleef_lock = {0};
#define LOCK_SLEEF PyMutex_Lock(&sleef_lock)
#define UNLOCK_SLEEF PyMutex_Unlock(&sleef_lock)
#endif




QuadPrecisionObject *
QuadPrecision_raw_new(QuadBackendType backend)
Expand Down Expand Up @@ -219,6 +208,25 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
return NULL;
}
}
else if (PyBytes_Check(value)) {
const char *s = PyBytes_AsString(value);
if (s == NULL) {
Py_DECREF(self);
return NULL;
}
char *endptr = NULL;
if (backend == BACKEND_SLEEF) {
self->value.sleef_value = Sleef_strtoq(s, &endptr);
}
else {
self->value.longdouble_value = strtold(s, &endptr);
}
if (*endptr != '\0' || endptr == s) {
PyErr_SetString(PyExc_ValueError, "Unable to parse bytes to QuadPrecision");
Py_DECREF(self);
return NULL;
}
}
else if (PyLong_Check(value)) {
return quad_from_py_int(value, backend, self);
}
Expand All @@ -242,21 +250,21 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend)
const char *type_cstr = PyUnicode_AsUTF8(type_str);
if (type_cstr != NULL) {
PyErr_Format(PyExc_TypeError,
"QuadPrecision value must be a quad, float, int, string, array or sequence, but got %s "
"QuadPrecision value must be a quad, float, int, string, bytes, array or sequence, but got %s "
"instead",
type_cstr);
}
else {
PyErr_SetString(
PyExc_TypeError,
"QuadPrecision value must be a quad, float, int, string, array or sequence, but got an "
"QuadPrecision value must be a quad, float, int, string, bytes, array or sequence, but got an "
"unknown type instead");
}
Py_DECREF(type_str);
}
else {
PyErr_SetString(PyExc_TypeError,
"QuadPrecision value must be a quad, float, int, string, array or sequence, but got an "
"QuadPrecision value must be a quad, float, int, string, bytes, array or sequence, but got an "
"unknown type instead");
}
Py_DECREF(self);
Expand Down Expand Up @@ -636,13 +644,6 @@ PyTypeObject QuadPrecision_Type = {
int
init_quadprecision_scalar(void)
{
#if PY_VERSION_HEX < 0x30d00b3
sleef_lock = PyThread_allocate_lock();
if (sleef_lock == NULL) {
PyErr_NoMemory();
return -1;
}
#endif
QuadPrecision_Type.tp_base = &PyFloatingArrType_Type;
return PyType_Ready(&QuadPrecision_Type);
}
128 changes: 128 additions & 0 deletions quaddtype/tests/test_quaddtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,134 @@ def test_string_roundtrip():
)


class TestBytesSupport:
"""Test suite for QuadPrecision bytes input support."""

@pytest.mark.parametrize("original", [
QuadPrecision("0.417022004702574000667425480060047"), # Random value
QuadPrecision("1.23456789012345678901234567890123456789"), # Many digits
pytest.param(numpy_quaddtype.pi, id="pi"), # Mathematical constant
pytest.param(numpy_quaddtype.e, id="e"),
QuadPrecision("1e-100"), # Very small
QuadPrecision("1e100"), # Very large
QuadPrecision("-3.14159265358979323846264338327950288419"), # Negative pi
QuadPrecision("0.0"), # Zero
QuadPrecision("-0.0"), # Negative zero
QuadPrecision("1.0"), # One
QuadPrecision("-1.0"), # Negative one
])
def test_bytes_roundtrip(self, original):
"""Test that bytes representations of quad precision values roundtrip correctly."""
string_repr = str(original)
bytes_repr = string_repr.encode("ascii")
reconstructed = QuadPrecision(bytes_repr)

# Values should be exactly equal (bit-for-bit identical)
assert reconstructed == original, (
f"Bytes round-trip failed for {repr(original)}:\n"
f" Original: {repr(original)}\n"
f" Bytes: {bytes_repr}\n"
f" Reconstructed: {repr(reconstructed)}"
)

@pytest.mark.parametrize("bytes_val,expected_str", [
# Simple numeric values
(b"1.0", "1.0"),
(b"-1.0", "-1.0"),
(b"0.0", "0.0"),
(b"3.14159", "3.14159"),
# Scientific notation
(b"1e10", "1e10"),
(b"1e-10", "1e-10"),
(b"2.5e100", "2.5e100"),
(b"-3.7e-50", "-3.7e-50"),
])
def test_bytes_creation_basic(self, bytes_val, expected_str):
"""Test basic creation of QuadPrecision from bytes objects."""
assert QuadPrecision(bytes_val) == QuadPrecision(expected_str)

@pytest.mark.parametrize("bytes_val,check_func", [
# Very large and very small numbers
(b"1e308", lambda x: x == QuadPrecision("1e308")),
(b"1e-308", lambda x: x == QuadPrecision("1e-308")),
# Special values
(b"inf", lambda x: np.isinf(x)),
(b"-inf", lambda x: np.isinf(x) and x < 0),
(b"nan", lambda x: np.isnan(x)),
])
def test_bytes_creation_edge_cases(self, bytes_val, check_func):
"""Test edge cases for QuadPrecision creation from bytes."""
val = QuadPrecision(bytes_val)
assert check_func(val)

@pytest.mark.parametrize("invalid_bytes", [
b"", # Empty bytes
b"not_a_number", # Invalid format
b"1.23abc", # Trailing garbage
b"abc1.23", # Leading garbage
])
def test_bytes_invalid_input(self, invalid_bytes):
"""Test that invalid bytes input raises appropriate errors."""
with pytest.raises(ValueError, match="Unable to parse bytes to QuadPrecision"):
QuadPrecision(invalid_bytes)

@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
@pytest.mark.parametrize("bytes_val", [
b"1.0",
b"-1.0",
b"3.141592653589793238462643383279502884197",
b"1e100",
b"1e-100",
b"0.0",
])
def test_bytes_backend_consistency(self, backend, bytes_val):
"""Test that bytes parsing works consistently across backends."""
quad_val = QuadPrecision(bytes_val, backend=backend)
str_val = QuadPrecision(bytes_val.decode("ascii"), backend=backend)

# Bytes and string should produce identical results
assert quad_val == str_val, (
f"Backend {backend}: bytes and string parsing differ for {bytes_val}\n"
f" From bytes: {repr(quad_val)}\n"
f" From string: {repr(str_val)}"
)

@pytest.mark.parametrize("bytes_val,expected_str", [
# Leading whitespace is OK (consumed by parser)
(b" 1.0", "1.0"),
(b" 3.14", "3.14"),
])
def test_bytes_whitespace_valid(self, bytes_val, expected_str):
"""Test handling of valid whitespace in bytes input."""
assert QuadPrecision(bytes_val) == QuadPrecision(expected_str)

@pytest.mark.parametrize("invalid_bytes", [
b"1.0 ", # Trailing whitespace
b"1.0 ", # Multiple trailing spaces
b"1 .0", # Internal whitespace
b"1. 0", # Internal whitespace
])
def test_bytes_whitespace_invalid(self, invalid_bytes):
"""Test that invalid whitespace in bytes input raises errors."""
with pytest.raises(ValueError, match="Unable to parse bytes to QuadPrecision"):
QuadPrecision(invalid_bytes)

@pytest.mark.parametrize("test_str", [
"1.0",
"-3.14159265358979323846264338327950288419",
"1e100",
"2.71828182845904523536028747135266249775",
])
def test_bytes_encoding_compatibility(self, test_str):
"""Test that bytes created from different encodings work correctly."""
from_string = QuadPrecision(test_str)
from_bytes = QuadPrecision(test_str.encode("ascii"))
from_bytes_utf8 = QuadPrecision(test_str.encode("utf-8"))

assert from_string == from_bytes
assert from_string == from_bytes_utf8


def test_string_subclass_parsing():
"""Test that QuadPrecision handles string subclasses correctly.

Expand Down