diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da6d3d5..ce32f36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,15 +13,8 @@ jobs: fail-fast: false matrix: platform: ["ubuntu-latest", "macos-latest"] - python-version: ["3.7", "3.8"] - gap-version: ["4.10.2", "4.11.0"] - exclude: - - python-version: "3.7" - gap-version: "4.11.0" - - python-version: "3.8" - gap-version: "4.10.2" - - platform: "macos-latest" - python-version: "3.7" + python-version: ["3.9", "3.10", "3.11", "3.12"] + gap-version: ["4.12.2", "4.13.0"] runs-on: "${{ matrix.platform }}" steps: - uses: actions/checkout@v4 @@ -36,7 +29,7 @@ jobs: with: auto-update-conda: true python-version: "${{ matrix.python-version }}" - use-only-tar-bz2: true + use-only-tar-bz2: false - name: "Conda info" run: "conda info" - name: "Conda install dependencies" diff --git a/gappy/core.pyx b/gappy/core.pyx index 8a80964..4619c91 100644 --- a/gappy/core.pyx +++ b/gappy/core.pyx @@ -163,7 +163,7 @@ cdef void dereference_obj(Obj obj): owned_objects_refcount[wrapped] = refcount - 1 -cdef void gasman_callback() with gil: +cdef void gasman_callback() noexcept with gil: """ Callback before each GAP garbage collection """ @@ -454,7 +454,7 @@ cdef str extract_errout(): return msg_py -cdef void error_handler() with gil: +cdef void error_handler() noexcept with gil: """ The gappy error handler. @@ -463,7 +463,7 @@ cdef void error_handler() with gil: TODO: We should probably prevent re-entering this function if we are already handling an error; if there is an error in our stream - handling code below it could result in a stack overflow. + h/andling code below it could result in a stack overflow. """ cdef PyObject* exc_type = NULL cdef PyObject* exc_val = NULL @@ -1387,8 +1387,9 @@ cdef class Gap: >>> gap.collect() """ self.initialize() - rc = GAP_CollectBags(1) - if rc != 1: + try: + GAP_CollectBags(1) + except: raise RuntimeError('Garbage collection failed.') diff --git a/gappy/cpython/pycore_long.h b/gappy/cpython/pycore_long.h new file mode 100644 index 0000000..99561f1 --- /dev/null +++ b/gappy/cpython/pycore_long.h @@ -0,0 +1,98 @@ +#include "Python.h" +#include + +#if PY_VERSION_HEX >= 0x030C00A5 +#define ob_digit(o) (((PyLongObject*)o)->long_value.ob_digit) +#else +#define ob_digit(o) (((PyLongObject*)o)->ob_digit) +#endif + +#if PY_VERSION_HEX >= 0x030C00A7 +// taken from cpython:Include/internal/pycore_long.h @ 3.12 + +/* Long value tag bits: + * 0-1: Sign bits value = (1-sign), ie. negative=2, positive=0, zero=1. + * 2: Reserved for immortality bit + * 3+ Unsigned digit count + */ +#define SIGN_MASK 3 +#define SIGN_ZERO 1 +#define SIGN_NEGATIVE 2 +#define NON_SIZE_BITS 3 + +static inline bool +_PyLong_IsZero(const PyLongObject *op) +{ + return (op->long_value.lv_tag & SIGN_MASK) == SIGN_ZERO; +} + +static inline bool +_PyLong_IsNegative(const PyLongObject *op) +{ + return (op->long_value.lv_tag & SIGN_MASK) == SIGN_NEGATIVE; +} + +static inline bool +_PyLong_IsPositive(const PyLongObject *op) +{ + return (op->long_value.lv_tag & SIGN_MASK) == 0; +} + +static inline Py_ssize_t +_PyLong_DigitCount(const PyLongObject *op) +{ + assert(PyLong_Check(op)); + return op->long_value.lv_tag >> NON_SIZE_BITS; +} + +#define TAG_FROM_SIGN_AND_SIZE(sign, size) ((1 - (sign)) | ((size) << NON_SIZE_BITS)) + +static inline void +_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) +{ + assert(size >= 0); + assert(-1 <= sign && sign <= 1); + assert(sign != 0 || size == 0); + op->long_value.lv_tag = TAG_FROM_SIGN_AND_SIZE(sign, (size_t)size); +} + +#else +// fallback for < 3.12 + +static inline bool +_PyLong_IsZero(const PyLongObject *op) +{ + return Py_SIZE(op) == 0; +} + +static inline bool +_PyLong_IsNegative(const PyLongObject *op) +{ + return Py_SIZE(op) < 0; +} + +static inline bool +_PyLong_IsPositive(const PyLongObject *op) +{ + return Py_SIZE(op) > 0; +} + +static inline Py_ssize_t +_PyLong_DigitCount(const PyLongObject *op) +{ + Py_ssize_t size = Py_SIZE(op); + return size < 0 ? -size : size; +} + +static inline void +_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) +{ +#if (PY_MAJOR_VERSION == 3) && (PY_MINOR_VERSION < 9) +// The function Py_SET_SIZE is defined starting with python 3.9. + Py_SIZE(op) = size; +#else + Py_SET_SIZE(op, sign < 0 ? -size : size); +#endif +} + +#endif diff --git a/gappy/cpython/pycore_long.pxd b/gappy/cpython/pycore_long.pxd new file mode 100644 index 0000000..41de637 --- /dev/null +++ b/gappy/cpython/pycore_long.pxd @@ -0,0 +1,9 @@ +from cpython.longintrepr cimport py_long, digit + +cdef extern from "pycore_long.h": + digit* ob_digit(py_long o) + bint _PyLong_IsZero(py_long o) + bint _PyLong_IsNegative(py_long o) + bint _PyLong_IsPositive(py_long o) + Py_ssize_t _PyLong_DigitCount(py_long o) + void _PyLong_SetSignAndDigitCount(py_long o, int sign, Py_ssize_t size) diff --git a/gappy/gap_includes.pxd b/gappy/gap_includes.pxd index bd8f93e..0a2ba0e 100644 --- a/gappy/gap_includes.pxd +++ b/gappy/gap_includes.pxd @@ -125,27 +125,6 @@ cdef extern from "gap/libgap-api.h" nogil: Obj GAP_NewPrecord(Int) -cdef extern from "gap/gasman.h" nogil: - """ - #define GAP_CollectBags(full) CollectBags(0, full) - """ - void GAP_MarkBag "MarkBag" (Obj bag) - UInt GAP_CollectBags(UInt full) - - -cdef extern from "gap/io.h" nogil: - UInt OpenOutputStream(Obj stream) - UInt CloseOutput() - - -# TODO: Replace this with a GAP_MakeStringWithLen from the public API; -# see https://github.com/gap-system/gap/issues/4211 -cdef extern from "gap/stringobj.h" nogil: - """ - static inline Obj GAP_MakeStringWithLen(char *s, size_t len) { - Obj ret; - C_NEW_STRING(ret, len, s); - return ret; - } - """ - Obj GAP_MakeStringWithLen(char *, size_t) + void GAP_MarkBag (Obj bag) + void GAP_CollectBags(UInt) + Obj GAP_MakeStringWithLen(const char *, UInt) diff --git a/gappy/gapobj.pyx b/gappy/gapobj.pyx index e520eb7..f7ad939 100644 --- a/gappy/gapobj.pyx +++ b/gappy/gapobj.pyx @@ -35,6 +35,7 @@ from .exceptions import GAPError from .operations import OperationInspector from .utils import _SPECIAL_ATTRS, _converter_for_type +from .cpython.pycore_long cimport (ob_digit) ############################################################################ ### helper functions to construct lists and records ######################## @@ -101,18 +102,18 @@ cdef void capture_stdout(Obj func, Obj obj, Obj out): args[0] = out args[1] = GAP_True stream = GAP_CallFuncArray(output_text_string, 2, args) - stream_ok = OpenOutputStream(stream) + # stream_ok = OpenOutputStream(stream) sig_off() - if not stream_ok: - raise GAPError("failed to open output capture stream for " - "representing GAP object") + #if not stream_ok: + # raise GAPError("failed to open output capture stream for " + # "representing GAP object") args[0] = obj - sig_on() - GAP_CallFuncArray(func, 1, args) - CloseOutput() - sig_off() + #sig_on() + #GAP_CallFuncArray(func, 1, args) + #CloseOutput() + #sig_off() finally: sig_GAP_Leave() @@ -205,13 +206,13 @@ cdef Obj make_gap_integer(x) except NULL: if -1 <= size <= 1: # Shortcut for smaller ints (up to 30 bits) - s = ((x).ob_digit[0]) + s = ((ob_digit(x))[0]) limbs = &s else: # See https://github.com/gap-system/gap/issues/4209 mpz_init(z) mpz_import(z, size * sign, -1, sizeof(digit), 0, - (sizeof(digit) * 8) - PyLong_SHIFT, (x).ob_digit) + (sizeof(digit) * 8) - PyLong_SHIFT, ob_digit(x)) do_clear = 1 if sign < 0: mpz_neg(z, z) @@ -271,16 +272,14 @@ cdef Obj make_gap_string(s) except NULL: ``Obj`` A GAP C ``Obj`` representing a GAP string. """ - - cdef bytes b - + cdef Obj res + cdef UInt slen try: GAP_Enter() - if not isinstance(s, bytes): - b = s.encode('utf-8') - else: - b = s - return GAP_MakeStringWithLen(b, len(b)) + b = s.encode() + slen = len(b) + res = GAP_MakeStringWithLen(b, slen) + return res finally: GAP_Leave() @@ -1807,7 +1806,7 @@ cdef class GapInteger(GapObj): # e.g. if 2**30 we require 31 bits and with PyLong_SHIFT = 30 # this returns 2 x = _PyLong_New((nbits + PyLong_SHIFT - 1) // PyLong_SHIFT) - mpz_export(x.ob_digit, NULL, -1, sizeof(digit), 0, + mpz_export(ob_digit(x), NULL, -1, sizeof(digit), 0, (sizeof(digit) * 8) - PyLong_SHIFT, z) sign = mpz_sgn(z) x *= sign diff --git a/pyproject.toml b/pyproject.toml index a291bf5..0fbbf72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [build-system] requires = [ - "Cython<3.0.0", + "Cython", "cysignals", - "setuptools>=42", + "setuptools>=61.2", "setuptools_scm[toml]>=3.4", "wheel" ] diff --git a/setup.cfg b/setup.cfg index 0c33e14..dcce272 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,14 +16,17 @@ classifiers = License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Operating System :: POSIX :: Linux Programming Language :: Cython - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Scientific/Engineering :: Mathematics [options] # We set packages to find: to automatically find all sub-packages packages = find: -python_requires = >=3.7 +python_requires = >=3.8 setup_requires = setuptools_scm install_requires = cysignals