diff --git a/.github/workflows/big_endian.yml b/.github/workflows/big_endian.yml new file mode 100644 index 0000000..6057f5d --- /dev/null +++ b/.github/workflows/big_endian.yml @@ -0,0 +1,142 @@ +name: Big-Endian Architecture Tests + +on: + pull_request: + branches: + - main + paths: + - "quaddtype/**" + - ".github/workflows/**" + workflow_dispatch: + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + big_endian_tests: + runs-on: ubuntu-22.04 + continue-on-error: true + strategy: + fail-fast: false + matrix: + BUILD_PROP: + - [ + "s390x (IBM Z Big Endian)", + "s390x-linux-gnu", + "s390x/ubuntu:22.04", + "s390x", + ] + - [ + "s390x - baseline(Z13)", + "s390x-linux-gnu", + "s390x/ubuntu:22.04", + "s390x", + ] + env: + ARCH_NAME: ${{ matrix.BUILD_PROP[0] }} + TOOLCHAIN_NAME: ${{ matrix.BUILD_PROP[1] }} + DOCKER_CONTAINER: ${{ matrix.BUILD_PROP[2] }} + ARCH: ${{ matrix.BUILD_PROP[3] }} + TERM: xterm-256color + + name: "${{ matrix.BUILD_PROP[0] }}" + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-tags: true + persist-credentials: false + + - name: Initialize binfmt_misc for qemu-user-static + run: | + # Enable QEMU user-mode emulation for cross-architecture execution + docker run --rm --privileged tonistiigi/binfmt:qemu-v9.2.2-52 --install all + + - name: Install cross-compilation toolchain + run: | + sudo apt update + sudo apt install -y ninja-build gcc-${TOOLCHAIN_NAME} g++-${TOOLCHAIN_NAME} gfortran-${TOOLCHAIN_NAME} + + - name: Cache docker container + uses: actions/cache@v4 + id: container-cache + with: + path: ~/docker_${{ matrix.BUILD_PROP[1] }} + key: container-quaddtype-${{ runner.os }}-${{ matrix.BUILD_PROP[1] }}-${{ matrix.BUILD_PROP[2] }}-${{ hashFiles('quaddtype/pyproject.toml') }} + + - name: Create cross-compilation container + if: steps.container-cache.outputs.cache-hit != 'true' + run: | + docker run --platform=linux/${ARCH} --name quaddtype_container --interactive \ + -v /:/host -v $(pwd):/workspace ${DOCKER_CONTAINER} /bin/bash -c " + # Update package manager and install essential tools + apt update && + apt install -y cmake git python3 python-is-python3 python3-dev python3-pip build-essential && + + # Create necessary symlinks for cross-compilation + mkdir -p /lib64 && ln -sf /host/lib64/ld-* /lib64/ || true && + ln -sf /host/lib/x86_64-linux-gnu /lib/x86_64-linux-gnu || true && + + # Link cross-compilation toolchain from host + rm -rf /usr/${TOOLCHAIN_NAME} && ln -sf /host/usr/${TOOLCHAIN_NAME} /usr/${TOOLCHAIN_NAME} && + rm -rf /usr/lib/gcc/${TOOLCHAIN_NAME} && ln -sf /host/usr/lib/gcc-cross/${TOOLCHAIN_NAME} /usr/lib/gcc/${TOOLCHAIN_NAME} && + + # Set up compiler symlinks + rm -f /usr/bin/gcc && ln -sf /host/usr/bin/${TOOLCHAIN_NAME}-gcc /usr/bin/gcc && + rm -f /usr/bin/g++ && ln -sf /host/usr/bin/${TOOLCHAIN_NAME}-g++ /usr/bin/g++ && + rm -f /usr/bin/gfortran && ln -sf /host/usr/bin/${TOOLCHAIN_NAME}-gfortran /usr/bin/gfortran && + + # Set up binutils + rm -f /usr/bin/ar && ln -sf /host/usr/bin/${TOOLCHAIN_NAME}-ar /usr/bin/ar && + rm -f /usr/bin/as && ln -sf /host/usr/bin/${TOOLCHAIN_NAME}-as /usr/bin/as && + rm -f /usr/bin/ld && ln -sf /host/usr/bin/${TOOLCHAIN_NAME}-ld /usr/bin/ld && + rm -f /usr/bin/ld.bfd && ln -sf /host/usr/bin/${TOOLCHAIN_NAME}-ld.bfd /usr/bin/ld.bfd && + + # Link build tools + rm -f /usr/bin/ninja && ln -sf /host/usr/bin/ninja /usr/bin/ninja && + rm -f /usr/local/bin/ninja && mkdir -p /usr/local/bin && ln -sf /host/usr/bin/ninja /usr/local/bin/ninja && + + # Configure git for workspace access + git config --global --add safe.directory /workspace && + + # Install Python build dependencies + python -m pip install --upgrade pip && + python -m pip install meson>=1.3.2 meson-python wheel numpy && + python -m pip install pytest pytest-run-parallel pytest-timeout && + + # Install system dependencies for quaddtype (SLEEF dependencies) + apt install -y libssl-dev libfftw3-dev pkg-config + " + docker commit quaddtype_container quaddtype_container + mkdir -p "~/docker_${TOOLCHAIN_NAME}" + docker save -o "~/docker_${TOOLCHAIN_NAME}/quaddtype_container.tar" quaddtype_container + + - name: Load container from cache + if: steps.container-cache.outputs.cache-hit == 'true' + run: docker load -i "~/docker_${TOOLCHAIN_NAME}/quaddtype_container.tar" + + - name: Build quaddtype with cross-compilation and testing + run: | + docker run --rm --platform=linux/${ARCH} -e "TERM=xterm-256color" \ + -v $(pwd):/workspace -v /:/host quaddtype_container \ + /bin/script -e -q -c "/bin/bash --noprofile --norc -eo pipefail -c ' + cd /workspace/quaddtype && + echo \"Building quaddtype for ${ARCH_NAME}...\" && + + # Set OpenMP linking for cross-compilation + export LDFLAGS=\"-fopenmp\" && + + # Install quaddtype with test dependencies + python -m pip install .[test] -v --no-build-isolation --force-reinstall + + cd .. + python -m pytest -vvv --color=yes --timeout=600 --tb=short quaddtype/tests/ + '" diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 0000000..c35ae1f --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,48 @@ +name: Validate static types in quaddtype +permissions: read-all + +on: + pull_request: + paths: + - .github/workflows/typecheck.yml + - quaddtype/numpy_quaddtype/* + - quaddtype/meson.build + - quaddtype/pyproject.toml + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + typecheck_quaddtype: + runs-on: ubuntu-latest + timeout-minutes: 2 + + steps: + - uses: actions/checkout@v5.0.0 + + - uses: astral-sh/setup-uv@v6.7.0 + with: + activate-environment: true + python-version: "3.10" + + - name: install + working-directory: quaddtype + run: uv pip install mypy pyright . + + - name: pyright + working-directory: quaddtype + run: pyright + + - name: pyright --verifytypes + working-directory: quaddtype + run: pyright --ignoreexternal --verifytypes numpy_quaddtype + + - name: mypy + working-directory: quaddtype + run: mypy --no-incremental --cache-dir=/dev/null . + + - name: stubtest + working-directory: quaddtype + run: stubtest --mypy-config-file pyproject.toml numpy_quaddtype diff --git a/quaddtype/README.md b/quaddtype/README.md index 86eae75..80fa86a 100644 --- a/quaddtype/README.md +++ b/quaddtype/README.md @@ -1,4 +1,4 @@ -# Numpy-QuadDType +# NumPy-QuadDType A cross-platform Quad (128-bit) float Data-Type for NumPy. diff --git a/quaddtype/meson.build b/quaddtype/meson.build index 04b506b..75e9e38 100644 --- a/quaddtype/meson.build +++ b/quaddtype/meson.build @@ -35,7 +35,7 @@ if openmp_dep.found() dependencies += openmp_dep endif -# compiler flags for QBLAS compatibility +# compiler flags for QBLAS compatibility if not is_windows # QBLAS requires extended numeric literals for Q suffix support # if compiler supports (usually gcc) @@ -61,14 +61,14 @@ foreach optional_attr: optional_variable_attributes code = ''' #pragma GCC diagnostic error "-Wattributes" #pragma clang diagnostic error "-Wattributes" - + int @0@ foo; - + int main() { return 0; } '''.format(attr) - + if c.compiles(code, name: optional_attr[0]) cdata.set10(optional_attr[1], true) message('Thread-local storage support found: @0@'.format(attr)) @@ -122,6 +122,9 @@ srcs = [ py.install_sources( [ 'numpy_quaddtype/__init__.py', + 'numpy_quaddtype/__init__.pyi', + 'numpy_quaddtype/_quaddtype_main.pyi', + 'numpy_quaddtype/py.typed', ], subdir: 'numpy_quaddtype', pure: false @@ -134,4 +137,4 @@ py.extension_module('_quaddtype_main', install: true, subdir: 'numpy_quaddtype', include_directories: [includes, build_includes], -) \ No newline at end of file +) diff --git a/quaddtype/numpy_quaddtype/__init__.pyi b/quaddtype/numpy_quaddtype/__init__.pyi new file mode 100644 index 0000000..ae9aa25 --- /dev/null +++ b/quaddtype/numpy_quaddtype/__init__.pyi @@ -0,0 +1,56 @@ +from typing import Final + +from ._quaddtype_main import ( + QuadPrecDType, + QuadPrecision, + _IntoQuad, # type-check only # pyright: ignore[reportPrivateUsage] + get_num_threads, + get_quadblas_version, + is_longdouble_128, + set_num_threads, +) + +__all__ = [ + "QuadPrecision", + "QuadPrecDType", + "SleefQuadPrecision", + "LongDoubleQuadPrecision", + "SleefQuadPrecDType", + "LongDoubleQuadPrecDType", + "is_longdouble_128", + "pi", + "e", + "log2e", + "log10e", + "ln2", + "ln10", + "max_value", + "epsilon", + "smallest_normal", + "smallest_subnormal", + "bits", + "precision", + "resolution", + "set_num_threads", + "get_num_threads", + "get_quadblas_version", +] + +def SleefQuadPrecision(value: _IntoQuad) -> QuadPrecision: ... +def LongDoubleQuadPrecision(value: _IntoQuad) -> QuadPrecision: ... +def SleefQuadPrecDType() -> QuadPrecDType: ... +def LongDoubleQuadPrecDType() -> QuadPrecDType: ... + +pi: Final[QuadPrecision] = ... +e: Final[QuadPrecision] = ... +log2e: Final[QuadPrecision] = ... +log10e: Final[QuadPrecision] = ... +ln2: Final[QuadPrecision] = ... +ln10: Final[QuadPrecision] = ... +max_value: Final[QuadPrecision] = ... +epsilon: Final[QuadPrecision] = ... +smallest_normal: Final[QuadPrecision] = ... +smallest_subnormal: Final[QuadPrecision] = ... +resolution: Final[QuadPrecision] = ... +bits: Final = 128 +precision: Final = 33 diff --git a/quaddtype/numpy_quaddtype/_quaddtype_main.pyi b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi new file mode 100644 index 0000000..21e2850 --- /dev/null +++ b/quaddtype/numpy_quaddtype/_quaddtype_main.pyi @@ -0,0 +1,135 @@ +from typing import Any, Literal, TypeAlias, final, overload + +import numpy as np +from typing_extensions import Never, Self, override + +_Backend: TypeAlias = Literal["sleef", "longdouble"] +_IntoQuad: TypeAlias = QuadPrecision | float | str +_CastsQuad: TypeAlias = _IntoQuad | np.floating[Any] | np.integer[Any] | np.bool_ + +@final +class QuadPrecDType(np.dtype[QuadPrecision]): # type: ignore[misc, type-var] # pyright: ignore[reportGeneralTypeIssues, reportInvalidTypeArguments] + def __new__(cls, /, backend: _Backend = "sleef") -> Self: ... + + # `numpy.dtype` overrides + names: None # pyright: ignore[reportIncompatibleVariableOverride] + @property + @override + def alignment(self) -> Literal[16]: ... + @property + @override + def itemsize(self) -> Literal[16]: ... + @property + @override + def name(self) -> Literal["QuadPrecDType128"]: ... + @property + @override + def byteorder(self) -> Literal["|"]: ... + @property + @override + def char(self) -> Literal["\x00"]: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + @property + @override + def kind(self) -> Literal["\x00"]: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + @property + @override + def num(self) -> Literal[-1]: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + @property + @override + def shape(self) -> tuple[()]: ... + @property + @override + def ndim(self) -> Literal[0]: ... + @property + @override + def fields(self) -> None: ... + @property + @override + def base(self) -> Self: ... + @property + @override + def subdtype(self) -> None: ... + @property + @override + def hasobject(self) -> Literal[False]: ... + @property + @override + def isbuiltin(self) -> Literal[0]: ... + @property + @override + def isnative(self) -> Literal[True]: ... + @property + @override + def isalignedstruct(self) -> Literal[False]: ... + @override + def __getitem__(self, key: Never, /) -> Self: ... # type: ignore[override] + +@final +class QuadPrecision: # NOTE: It doesn't inherit from `np.generic` which is type-unsafe + def __new__(cls, /, value: _IntoQuad, backend: _Backend = "sleef") -> Self: ... + + # Rich comparison operators + # NOTE: Unlike other numpy scalars, these return `builtins.bool`, not `np.bool`. + @override + def __eq__(self, other: object, /) -> bool: ... + @override + def __ne__(self, other: object, /) -> bool: ... + def __lt__(self, other: _IntoQuad, /) -> bool: ... + def __le__(self, other: _IntoQuad, /) -> bool: ... + def __gt__(self, other: _IntoQuad, /) -> bool: ... + def __ge__(self, other: _IntoQuad, /) -> bool: ... + + # Binary operators + def __add__(self, other: _CastsQuad, /) -> Self: ... + def __radd__(self, other: _CastsQuad, /) -> Self: ... # type: ignore[misc] + def __sub__(self, other: _CastsQuad, /) -> Self: ... + def __rsub__(self, other: _CastsQuad, /) -> Self: ... + def __mul__(self, other: _CastsQuad, /) -> Self: ... + def __rmul__(self, other: _CastsQuad, /) -> Self: ... # type: ignore[misc] + def __pow__(self, other: _CastsQuad, mod: None = None, /) -> Self: ... + def __rpow__(self, other: _CastsQuad, mod: None = None, /) -> Self: ... + def __truediv__(self, other: _CastsQuad, /) -> Self: ... + def __rtruediv__(self, other: _CastsQuad, /) -> Self: ... + + # Unary operators + def __neg__(self, /) -> Self: ... + def __pos__(self, /) -> Self: ... + def __abs__(self, /) -> Self: ... + + # Conversion methods + def __bool__(self, /) -> bool: ... + def __int__(self, /) -> int: ... + def __float__(self, /) -> float: ... + + # String representation + @override + def __repr__(self, /) -> str: ... + @override + def __str__(self, /) -> str: ... + +# +def is_longdouble_128() -> bool: ... + +@overload +def get_sleef_constant(constant_name: Literal["bits", "precision"], /) -> int: ... +@overload +def get_sleef_constant( + constant_name: Literal[ + "pi", + "e", + "log2e", + "log10e", + "ln2", + "ln10", + "max_value", + "epsilon", + "smallest_normal", + "smallest_subnormal", + "resolution", + ], + /, +) -> QuadPrecision: ... + +def set_num_threads(num_threads: int, /) -> None: ... +def get_num_threads() -> int: ... +def get_quadblas_version() -> str: ... diff --git a/quaddtype/numpy_quaddtype/py.typed b/quaddtype/numpy_quaddtype/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/quaddtype/numpy_quaddtype/src/casts.cpp b/quaddtype/numpy_quaddtype/src/casts.cpp index bfd4296..3e25eaa 100644 --- a/quaddtype/numpy_quaddtype/src/casts.cpp +++ b/quaddtype/numpy_quaddtype/src/casts.cpp @@ -904,4 +904,4 @@ free_casts(void) } } spec_count = 0; -} \ No newline at end of file +} diff --git a/quaddtype/numpy_quaddtype/src/dragon4.c b/quaddtype/numpy_quaddtype/src/dragon4.c index 8e96c0b..dac8fa0 100644 --- a/quaddtype/numpy_quaddtype/src/dragon4.c +++ b/quaddtype/numpy_quaddtype/src/dragon4.c @@ -1885,8 +1885,13 @@ Dragon4_PrintFloat_Sleef_quad(Sleef_quad *value, Dragon4_Options *opt) union { Sleef_quad q; struct { +#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + npy_uint64 hi; + npy_uint64 lo; +#else npy_uint64 lo; npy_uint64 hi; +#endif } i; } u; u.q = *value; @@ -2002,7 +2007,7 @@ Dragon4_Positional(PyObject *obj, DigitMode digit_mode, CutoffMode cutoff_mode, { npy_double v; - if (PyArray_IsScalar(obj, QuadPrecDType)) { + if (PyObject_TypeCheck(obj, &QuadPrecision_Type)) { QuadPrecisionObject *quad_obj = (QuadPrecisionObject *)obj; if (quad_obj->backend == BACKEND_SLEEF) { return Dragon4_Positional_QuadDType(&quad_obj->value.sleef_value, digit_mode, @@ -2025,7 +2030,7 @@ Dragon4_Scientific(PyObject *obj, DigitMode digit_mode, int precision, int min_d { npy_double val; - if (PyArray_IsScalar(obj, QuadPrecDType)) { + if (PyObject_TypeCheck(obj, &QuadPrecision_Type)) { QuadPrecisionObject *quad_obj = (QuadPrecisionObject *)obj; if (quad_obj->backend == BACKEND_SLEEF) { return Dragon4_Scientific_QuadDType(&quad_obj->value.sleef_value, digit_mode, precision, @@ -2039,4 +2044,4 @@ Dragon4_Scientific(PyObject *obj, DigitMode digit_mode, int precision, int min_d } return NULL; -} \ 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 734de21..b8c2bc4 100644 --- a/quaddtype/numpy_quaddtype/src/quaddtype_main.c +++ b/quaddtype/numpy_quaddtype/src/quaddtype_main.c @@ -33,7 +33,6 @@ py_is_longdouble_128(PyObject *self, PyObject *args) #ifdef SLEEF_QUAD_C static const Sleef_quad SMALLEST_SUBNORMAL_VALUE = SLEEF_QUAD_DENORM_MIN; #else -// Use the exact same struct layout as the original buggy code static const union { struct { #if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) @@ -94,6 +93,7 @@ get_sleef_constant(PyObject *self, PyObject *args) result->value.sleef_value = SLEEF_QUAD_MIN; } else if (strcmp(constant_name, "smallest_subnormal") == 0) { + // or just use sleef_q(+0x0000000000000LL, 0x0000000000000001ULL, -16383); result->value.sleef_value = SMALLEST_SUBNORMAL_VALUE; } else if (strcmp(constant_name, "bits") == 0) { diff --git a/quaddtype/numpy_quaddtype/src/scalar.h b/quaddtype/numpy_quaddtype/src/scalar.h index 4fac1ad..7499b1a 100644 --- a/quaddtype/numpy_quaddtype/src/scalar.h +++ b/quaddtype/numpy_quaddtype/src/scalar.h @@ -31,11 +31,8 @@ QuadPrecision_from_object(PyObject *value, QuadBackendType backend); int init_quadprecision_scalar(void); -#define PyArray_IsScalar(obj, QuadPrecDType) PyObject_TypeCheck(obj, &QuadPrecision_Type) -#define PyArrayScalar_VAL(obj, QuadPrecDType) (((QuadPrecisionObject *)obj)->value) - #ifdef __cplusplus } #endif -#endif \ No newline at end of file +#endif diff --git a/quaddtype/pyproject.toml b/quaddtype/pyproject.toml index 466b523..dc9f777 100644 --- a/quaddtype/pyproject.toml +++ b/quaddtype/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ "meson>=1.3.2", - "meson-python", + "meson-python>=0.18.0", "wheel", "numpy" ] @@ -12,8 +12,20 @@ name = "numpy_quaddtype" description = "Quad (128-bit) float dtype for numpy" version = "0.2.0" readme = 'README.md' -license = { file = "LICENSE" } +license = "BSD-3-Clause" +license-files = ["LICENSE"] authors = [{name = "Swayam Singh", email = "singhswayam008@gmail.com"}] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Free Threading", + "Typing :: Typed", +] requires-python = ">=3.10.0" dependencies = [ "numpy" @@ -23,4 +35,23 @@ dependencies = [ test = [ "pytest", "pytest-run-parallel" -] \ No newline at end of file +] + +[project.urls] +Repository = "https://github.com/numpy/numpy-user-dtypes" +Documentation = "https://github.com/numpy/numpy-user-dtypes/tree/main/quaddtype" +Issues = "https://github.com/numpy/numpy-user-dtypes/issues" + +[tool.pyright] +include = ["numpy_quaddtype/*.pyi"] +typeCheckingMode = "strict" +enableTypeIgnoreComments = false +reportImplicitOverride = true +reportUnnecessaryTypeIgnoreComment = true + +[tool.mypy] +strict = true +strict_equality_for_none = true +exclude = ["build", "numpy_quaddtype/src", "subprojects", "tests"] +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +warn_unreachable = false