diff --git a/quaddtype/meson.build b/quaddtype/meson.build index 82fa4bc..220b685 100644 --- a/quaddtype/meson.build +++ b/quaddtype/meson.build @@ -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( diff --git a/quaddtype/numpy_quaddtype/src/lock.c b/quaddtype/numpy_quaddtype/src/lock.c new file mode 100644 index 0000000..929966b --- /dev/null +++ b/quaddtype/numpy_quaddtype/src/lock.c @@ -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 +} \ No newline at end of file diff --git a/quaddtype/numpy_quaddtype/src/lock.h b/quaddtype/numpy_quaddtype/src/lock.h new file mode 100644 index 0000000..6c2a970 --- /dev/null +++ b/quaddtype/numpy_quaddtype/src/lock.h @@ -0,0 +1,18 @@ +#ifndef _QUADDTYPE_LOCK_H +#define _QUADDTYPE_LOCK_H + +#include + +#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 \ No newline at end of file diff --git a/quaddtype/numpy_quaddtype/src/quaddtype_main.c b/quaddtype/numpy_quaddtype/src/quaddtype_main.c index 0cbb652..b268077 100644 --- a/quaddtype/numpy_quaddtype/src/quaddtype_main.c +++ b/quaddtype/numpy_quaddtype/src/quaddtype_main.c @@ -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" @@ -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; diff --git a/quaddtype/numpy_quaddtype/src/scalar.c b/quaddtype/numpy_quaddtype/src/scalar.c index 9b42b71..079c4df 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.c +++ b/quaddtype/numpy_quaddtype/src/scalar.c @@ -15,6 +15,7 @@ #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) @@ -22,18 +23,6 @@ // 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) @@ -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); } @@ -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); @@ -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); } \ No newline at end of file diff --git a/quaddtype/tests/test_quaddtype.py b/quaddtype/tests/test_quaddtype.py index e967b8c..a8d297b 100644 --- a/quaddtype/tests/test_quaddtype.py +++ b/quaddtype/tests/test_quaddtype.py @@ -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.