From 6b52ec34e1fb2c3653246d0eff5afe4e0e6504e4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 10:40:04 +0000 Subject: [PATCH 01/34] WIP librt.random --- mypy/typeshed/stubs/librt/librt/random.pyi | 3 + mypyc/build.py | 1 + mypyc/lib-rt/random/librt_random.c | 141 +++++++++++++++++++++ mypyc/lib-rt/setup.py | 6 + mypyc/test-data/run-librt-random.test | 59 +++++++++ mypyc/test/test_run.py | 1 + 6 files changed, 211 insertions(+) create mode 100644 mypy/typeshed/stubs/librt/librt/random.pyi create mode 100644 mypyc/lib-rt/random/librt_random.c create mode 100644 mypyc/test-data/run-librt-random.test diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi new file mode 100644 index 0000000000000..5acdfe4e54b75 --- /dev/null +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -0,0 +1,3 @@ +class Random: + def __init__(self) -> None: ... + def randint(self, a: int, b: int) -> int: ... diff --git a/mypyc/build.py b/mypyc/build.py index d55334d8ac800..7004075fe42c9 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -121,6 +121,7 @@ class ModDesc(NamedTuple): ["vecs"], ), ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []), + ModDesc("librt.random", ["random/librt_random.c"], [], []), ] try: diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c new file mode 100644 index 0000000000000..54165cc89fb6c --- /dev/null +++ b/mypyc/lib-rt/random/librt_random.c @@ -0,0 +1,141 @@ +#define PY_SSIZE_T_CLEAN +#include +#include +#include + +// +// Random +// + +typedef struct { + PyObject_HEAD + unsigned int seed; +} RandomObject; + +static PyTypeObject RandomType; + +static PyObject* +Random_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + if (type != &RandomType) { + PyErr_SetString(PyExc_TypeError, "Random cannot be subclassed"); + return NULL; + } + + RandomObject *self = (RandomObject *)type->tp_alloc(type, 0); + if (self != NULL) { + self->seed = (unsigned int)time(NULL); + } + return (PyObject *)self; +} + +static int +Random_init(RandomObject *self, PyObject *args, PyObject *kwds) +{ + if (!PyArg_ParseTuple(args, "")) { + return -1; + } + + if (kwds != NULL && PyDict_Size(kwds) > 0) { + PyErr_SetString(PyExc_TypeError, + "Random() takes no keyword arguments"); + return -1; + } + + return 0; +} + +// Simple linear congruential generator +static inline unsigned int +next_random(RandomObject *self) { + self->seed = self->seed * 1103515245 + 12345; + return self->seed; +} + +static PyObject* +Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { + if (nargs != 2) { + PyErr_Format(PyExc_TypeError, + "randint() takes exactly 2 arguments (%zd given)", nargs); + return NULL; + } + + long long a = PyLong_AsLongLong(args[0]); + if (a == -1 && PyErr_Occurred()) + return NULL; + + long long b = PyLong_AsLongLong(args[1]); + if (b == -1 && PyErr_Occurred()) + return NULL; + + if (a > b) { + PyErr_SetString(PyExc_ValueError, + "empty range for randint()"); + return NULL; + } + + unsigned long long range = (unsigned long long)(b - a) + 1; + unsigned int r = next_random(self); + long long result = a + (long long)(r % range); + return PyLong_FromLongLong(result); +} + +static PyMethodDef Random_methods[] = { + {"randint", (PyCFunction) Random_randint, METH_FASTCALL, + PyDoc_STR("Return random integer in range [a, b], including both end points.") + }, + {NULL} /* Sentinel */ +}; + +static PyTypeObject RandomType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "Random", + .tp_doc = PyDoc_STR("Fast random number generator"), + .tp_basicsize = sizeof(RandomObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = Random_new, + .tp_init = (initproc) Random_init, + .tp_methods = Random_methods, +}; + +// Module definition + +static PyMethodDef librt_random_module_methods[] = { + {NULL, NULL, 0, NULL} +}; + +static int +librt_random_module_exec(PyObject *m) +{ + if (PyType_Ready(&RandomType) < 0) { + return -1; + } + if (PyModule_AddObjectRef(m, "Random", (PyObject *) &RandomType) < 0) { + return -1; + } + return 0; +} + +static PyModuleDef_Slot librt_random_module_slots[] = { + {Py_mod_exec, librt_random_module_exec}, +#ifdef Py_MOD_GIL_NOT_USED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + +static PyModuleDef librt_random_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "random", + .m_doc = "Fast random number generation", + .m_size = 0, + .m_methods = librt_random_module_methods, + .m_slots = librt_random_module_slots, +}; + +PyMODINIT_FUNC +PyInit_random(void) +{ + return PyModuleDef_Init(&librt_random_module); +} diff --git a/mypyc/lib-rt/setup.py b/mypyc/lib-rt/setup.py index 49b6c10201317..6ad0070eef45f 100644 --- a/mypyc/lib-rt/setup.py +++ b/mypyc/lib-rt/setup.py @@ -151,5 +151,11 @@ def run(self) -> None: Extension( "librt.time", ["time/librt_time.c"], include_dirs=["."], extra_compile_args=cflags ), + Extension( + "librt.random", + ["random/librt_random.c"], + include_dirs=["."], + extra_compile_args=cflags, + ), ] ) diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test new file mode 100644 index 0000000000000..2ed4025a087c5 --- /dev/null +++ b/mypyc/test-data/run-librt-random.test @@ -0,0 +1,59 @@ +[case testRandomBasic_librt] +from librt.random import Random + +def test_random_construct() -> None: + r = Random() + assert isinstance(r, Random) + +def test_randint_basic() -> None: + r = Random() + for i in range(100): + val = r.randint(0, 10) + assert 0 <= val <= 10 + +def test_randint_single_value() -> None: + r = Random() + for i in range(10): + assert r.randint(5, 5) == 5 + +def test_randint_negative_range() -> None: + r = Random() + for i in range(100): + val = r.randint(-10, -1) + assert -10 <= val <= -1 + +def test_randint_mixed_range() -> None: + r = Random() + for i in range(100): + val = r.randint(-5, 5) + assert -5 <= val <= 5 + +def test_randint_large_range() -> None: + r = Random() + for i in range(100): + val = r.randint(0, 1000000) + assert 0 <= val <= 1000000 + +def test_randint_produces_different_values() -> None: + r = Random() + values = set() + for i in range(100): + values.add(r.randint(0, 1000000)) + # With range 0-1000000 and 100 samples, we should get at least 2 distinct values + assert len(values) > 1 + +[case testRandomErrors_librt] +from librt.random import Random +from testutil import assertRaises + +def test_randint_empty_range() -> None: + r = Random() + with assertRaises(ValueError, "empty range"): + r.randint(10, 5) + +def test_randint_wrong_arg_count() -> None: + r = Random() + with assertRaises(TypeError): + r.randint(1) # type: ignore[call-arg] + with assertRaises(TypeError): + r.randint(1, 2, 3) # type: ignore[call-arg] diff --git a/mypyc/test/test_run.py b/mypyc/test/test_run.py index 8fb861f5c2aae..e7be5fcf8425a 100644 --- a/mypyc/test/test_run.py +++ b/mypyc/test/test_run.py @@ -81,6 +81,7 @@ "run-librt-strings.test", "run-base64.test", "run-librt-time.test", + "run-librt-random.test", "run-match.test", "run-vecs-i64-interp.test", "run-vecs-misc-interp.test", From 861b10f467742882a9c230b888737f2bba33e63a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 10:44:33 +0000 Subject: [PATCH 02/34] Add random() --- mypy/typeshed/stubs/librt/librt/random.pyi | 1 + mypyc/lib-rt/random/librt_random.c | 11 +++++++++++ mypyc/test-data/run-librt-random.test | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index 5acdfe4e54b75..df40b0c50167d 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -1,3 +1,4 @@ class Random: def __init__(self) -> None: ... def randint(self, a: int, b: int) -> int: ... + def random(self) -> float: ... diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 54165cc89fb6c..1791457db1105 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -80,10 +80,21 @@ Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { return PyLong_FromLongLong(result); } +static PyObject* +Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { + unsigned int r = next_random(self); + // Scale to [0.0, 1.0) + double result = r / 4294967296.0; // 2^32 + return PyFloat_FromDouble(result); +} + static PyMethodDef Random_methods[] = { {"randint", (PyCFunction) Random_randint, METH_FASTCALL, PyDoc_STR("Return random integer in range [a, b], including both end points.") }, + {"random", (PyCFunction) Random_random, METH_NOARGS, + PyDoc_STR("Return random float in [0.0, 1.0).") + }, {NULL} /* Sentinel */ }; diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 2ed4025a087c5..7d571a35b18f2 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -42,6 +42,24 @@ def test_randint_produces_different_values() -> None: # With range 0-1000000 and 100 samples, we should get at least 2 distinct values assert len(values) > 1 +def test_random_basic() -> None: + r = Random() + for i in range(100): + val = r.random() + assert 0.0 <= val < 1.0 + +def test_random_returns_float() -> None: + r = Random() + val = r.random() + assert isinstance(val, float) + +def test_random_produces_different_values() -> None: + r = Random() + values = set() + for i in range(100): + values.add(r.random()) + assert len(values) > 1 + [case testRandomErrors_librt] from librt.random import Random from testutil import assertRaises From 4051186315602221088aa2f961a69ea6658e6f05 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 10:57:27 +0000 Subject: [PATCH 03/34] Add ChaCha8 RNG --- mypyc/lib-rt/random/librt_random.c | 147 +++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 16 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 1791457db1105..7c74f79b8620b 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -1,15 +1,134 @@ #define PY_SSIZE_T_CLEAN #include -#include -#include +#include +#include + +#include "mypyc_util.h" + +// +// ChaCha8 PRNG with forward secrecy +// + +#define CHACHA8_RESEED_INTERVAL 16 + +typedef struct { + uint32_t seed[8]; // 256-bit key + uint32_t buf[16]; // output buffer: one ChaCha8 block + uint32_t counter; // block counter + uint8_t used; // index into buf + uint8_t n; // usable values in buf (8 or 16) + uint8_t blocks_left; // blocks until next reseed +} chacha8_rng; + +static inline uint32_t +rotl32(uint32_t x, int n) { + return (x << n) | (x >> (32 - n)); +} + +#define QUARTERROUND(a, b, c, d) \ + do { \ + a += b; d ^= a; d = rotl32(d, 16); \ + c += d; b ^= c; b = rotl32(b, 12); \ + a += b; d ^= a; d = rotl32(d, 8); \ + c += d; b ^= c; b = rotl32(b, 7); \ + } while (0) + +static void +chacha8_block(const uint32_t seed[8], uint32_t counter, uint32_t out[16]) +{ + // "expand 32-byte k" + uint32_t s[16] = { + 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, + seed[0], seed[1], seed[2], seed[3], + seed[4], seed[5], seed[6], seed[7], + counter, 0, 0, 0 // counter (low 32), counter (high 32), nonce + }; + + memcpy(out, s, sizeof(uint32_t) * 16); + + // 4 double-rounds = 8 rounds + for (int i = 0; i < 4; i++) { + // Column rounds + QUARTERROUND(out[0], out[4], out[ 8], out[12]); + QUARTERROUND(out[1], out[5], out[ 9], out[13]); + QUARTERROUND(out[2], out[6], out[10], out[14]); + QUARTERROUND(out[3], out[7], out[11], out[15]); + // Diagonal rounds + QUARTERROUND(out[0], out[5], out[10], out[15]); + QUARTERROUND(out[1], out[6], out[11], out[12]); + QUARTERROUND(out[2], out[7], out[ 8], out[13]); + QUARTERROUND(out[3], out[4], out[ 9], out[14]); + } + + // Add original state back (non-invertible) + for (int i = 0; i < 16; i++) + out[i] += s[i]; +} + +// Fill entropy from OS via os.urandom(), which handles short reads, +// EINTR, and platform differences internally. +// Returns 0 on success, -1 on failure (with Python exception set). +static int +fill_os_entropy(void *buf, size_t len) +{ + PyObject *os_mod = PyImport_ImportModule("os"); + if (os_mod == NULL) + return -1; + PyObject *bytes = PyObject_CallMethod(os_mod, "urandom", "n", (Py_ssize_t)len); + Py_DECREF(os_mod); + if (bytes == NULL) + return -1; + memcpy(buf, PyBytes_AS_STRING(bytes), len); + Py_DECREF(bytes); + return 0; +} + +static void +chacha8_refill(chacha8_rng *rng) +{ + chacha8_block(rng->seed, rng->counter, rng->buf); + rng->counter++; + rng->used = 0; + rng->blocks_left--; + + if (unlikely(rng->blocks_left == 0)) { + // Forward secrecy reseed: steal last 8 words as new key + memcpy(rng->seed, rng->buf + 8, sizeof(uint32_t) * 8); + rng->n = 8; // only 8 words usable this block + rng->counter = 0; + rng->blocks_left = CHACHA8_RESEED_INTERVAL; + } else { + rng->n = 16; + } +} + +static inline uint32_t +chacha8_next(chacha8_rng *rng) +{ + if (unlikely(rng->used >= rng->n)) + chacha8_refill(rng); + return rng->buf[rng->used++]; +} + +static int +chacha8_init(chacha8_rng *rng) +{ + if (fill_os_entropy(rng->seed, sizeof(rng->seed)) < 0) + return -1; + rng->counter = 0; + rng->used = 16; // force immediate refill on first call + rng->n = 16; + rng->blocks_left = CHACHA8_RESEED_INTERVAL; + return 0; +} // -// Random +// Random Python type // typedef struct { PyObject_HEAD - unsigned int seed; + chacha8_rng rng; } RandomObject; static PyTypeObject RandomType; @@ -24,7 +143,10 @@ Random_new(PyTypeObject *type, PyObject *args, PyObject *kwds) RandomObject *self = (RandomObject *)type->tp_alloc(type, 0); if (self != NULL) { - self->seed = (unsigned int)time(NULL); + if (chacha8_init(&self->rng) < 0) { + Py_DECREF(self); + return NULL; + } } return (PyObject *)self; } @@ -45,13 +167,6 @@ Random_init(RandomObject *self, PyObject *args, PyObject *kwds) return 0; } -// Simple linear congruential generator -static inline unsigned int -next_random(RandomObject *self) { - self->seed = self->seed * 1103515245 + 12345; - return self->seed; -} - static PyObject* Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { if (nargs != 2) { @@ -75,14 +190,14 @@ Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { } unsigned long long range = (unsigned long long)(b - a) + 1; - unsigned int r = next_random(self); + uint32_t r = chacha8_next(&self->rng); long long result = a + (long long)(r % range); return PyLong_FromLongLong(result); } static PyObject* Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { - unsigned int r = next_random(self); + uint32_t r = chacha8_next(&self->rng); // Scale to [0.0, 1.0) double result = r / 4294967296.0; // 2^32 return PyFloat_FromDouble(result); @@ -101,7 +216,7 @@ static PyMethodDef Random_methods[] = { static PyTypeObject RandomType = { .ob_base = PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "Random", - .tp_doc = PyDoc_STR("Fast random number generator"), + .tp_doc = PyDoc_STR("Fast random number generator using ChaCha8"), .tp_basicsize = sizeof(RandomObject), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, @@ -139,7 +254,7 @@ static PyModuleDef_Slot librt_random_module_slots[] = { static PyModuleDef librt_random_module = { .m_base = PyModuleDef_HEAD_INIT, .m_name = "random", - .m_doc = "Fast random number generation", + .m_doc = "Fast random number generation using ChaCha8", .m_size = 0, .m_methods = librt_random_module_methods, .m_slots = librt_random_module_slots, From 1fb499786178f4fed208767841a84edbc6694a9e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 11:09:03 +0000 Subject: [PATCH 04/34] Add module-level functions --- mypyc/lib-rt/random/librt_random.c | 154 ++++++++++++++++++++++++++ mypyc/test-data/run-librt-random.test | 38 ++++++- 2 files changed, 191 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 7c74f79b8620b..a45ce399d54f8 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -3,6 +3,12 @@ #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + #include "mypyc_util.h" // @@ -122,6 +128,145 @@ chacha8_init(chacha8_rng *rng) return 0; } +// +// Thread-local global RNG for module-level random()/randint() +// +// thread_local pointer for fast access (direct %fs/%gs-relative load), +// platform TLS key with destructor for cleanup on thread exit. +// + +#ifdef _WIN32 +static __declspec(thread) chacha8_rng *tls_rng = NULL; +#else +static __thread chacha8_rng *tls_rng = NULL; +#endif + +#ifdef _WIN32 +static DWORD tls_key = FLS_OUT_OF_INDEXES; + +static void NTAPI +tls_rng_destructor(void *ptr) +{ + if (ptr != NULL) { + memset(ptr, 0, sizeof(chacha8_rng)); + PyMem_RawFree(ptr); + } +} +#else +static pthread_key_t tls_key; + +static void +tls_rng_destructor(void *ptr) +{ + if (ptr != NULL) { + memset(ptr, 0, sizeof(chacha8_rng)); + PyMem_RawFree(ptr); + } +} +#endif + +static int tls_key_created = 0; + +static int +ensure_tls_key(void) +{ + if (likely(tls_key_created)) + return 0; +#ifdef _WIN32 + tls_key = FlsAlloc(tls_rng_destructor); + if (tls_key == FLS_OUT_OF_INDEXES) { + PyErr_SetString(PyExc_OSError, "FlsAlloc failed"); + return -1; + } +#else + if (pthread_key_create(&tls_key, tls_rng_destructor) != 0) { + PyErr_SetString(PyExc_OSError, "pthread_key_create failed"); + return -1; + } +#endif + tls_key_created = 1; + return 0; +} + +// Get the thread-local RNG, initializing on first use. +// Returns NULL with Python exception set on failure. +static chacha8_rng * +get_thread_rng(void) +{ + chacha8_rng *rng = tls_rng; + if (likely(rng != NULL)) + return rng; + + // First use on this thread — allocate and seed + rng = PyMem_RawMalloc(sizeof(chacha8_rng)); + if (rng == NULL) { + PyErr_NoMemory(); + return NULL; + } + if (chacha8_init(rng) < 0) { + PyMem_RawFree(rng); + return NULL; + } + + // Register with platform TLS for destructor +#ifdef _WIN32 + FlsSetValue(tls_key, rng); +#else + pthread_setspecific(tls_key, rng); +#endif + + tls_rng = rng; + return rng; +} + +// +// Module-level random() and randint() +// + +static PyObject* +module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + uint32_t r = chacha8_next(rng); + double result = r / 4294967296.0; // 2^32 + return PyFloat_FromDouble(result); +} + +static PyObject* +module_randint(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 2) { + PyErr_Format(PyExc_TypeError, + "randint() takes exactly 2 arguments (%zd given)", nargs); + return NULL; + } + + long long a = PyLong_AsLongLong(args[0]); + if (a == -1 && PyErr_Occurred()) + return NULL; + + long long b = PyLong_AsLongLong(args[1]); + if (b == -1 && PyErr_Occurred()) + return NULL; + + if (a > b) { + PyErr_SetString(PyExc_ValueError, + "empty range for randint()"); + return NULL; + } + + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + + unsigned long long range = (unsigned long long)(b - a) + 1; + uint32_t r = chacha8_next(rng); + long long result = a + (long long)(r % range); + return PyLong_FromLongLong(result); +} + // // Random Python type // @@ -228,12 +373,21 @@ static PyTypeObject RandomType = { // Module definition static PyMethodDef librt_random_module_methods[] = { + {"random", (PyCFunction) module_random, METH_NOARGS, + PyDoc_STR("Return random float in [0.0, 1.0) using thread-local RNG.") + }, + {"randint", (PyCFunction) module_randint, METH_FASTCALL, + PyDoc_STR("Return random integer in range [a, b] using thread-local RNG.") + }, {NULL, NULL, 0, NULL} }; static int librt_random_module_exec(PyObject *m) { + if (ensure_tls_key() < 0) { + return -1; + } if (PyType_Ready(&RandomType) < 0) { return -1; } diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 7d571a35b18f2..04ba7ccec7ba0 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -60,8 +60,40 @@ def test_random_produces_different_values() -> None: values.add(r.random()) assert len(values) > 1 +[case testRandomModuleLevel_librt] +from librt.random import random, randint + +def test_module_random_basic() -> None: + for i in range(100): + val = random() + assert 0.0 <= val < 1.0 + +def test_module_random_returns_float() -> None: + assert isinstance(random(), float) + +def test_module_random_produces_different_values() -> None: + values = set() + for i in range(100): + values.add(random()) + assert len(values) > 1 + +def test_module_randint_basic() -> None: + for i in range(100): + val = randint(0, 10) + assert 0 <= val <= 10 + +def test_module_randint_single_value() -> None: + for i in range(10): + assert randint(5, 5) == 5 + +def test_module_randint_produces_different_values() -> None: + values = set() + for i in range(100): + values.add(randint(0, 1000000)) + assert len(values) > 1 + [case testRandomErrors_librt] -from librt.random import Random +from librt.random import Random, randint from testutil import assertRaises def test_randint_empty_range() -> None: @@ -75,3 +107,7 @@ def test_randint_wrong_arg_count() -> None: r.randint(1) # type: ignore[call-arg] with assertRaises(TypeError): r.randint(1, 2, 3) # type: ignore[call-arg] + +def test_module_randint_empty_range() -> None: + with assertRaises(ValueError, "empty range"): + randint(10, 5) From 4518d409b1d1bd1398d9cc955dce13f13bd628a9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 11:13:17 +0000 Subject: [PATCH 05/34] Add randrange --- mypy/typeshed/stubs/librt/librt/random.pyi | 13 ++++ mypyc/lib-rt/random/librt_random.c | 85 ++++++++++++++++++++-- mypyc/test-data/run-librt-random.test | 74 ++++++++++++++++++- 3 files changed, 162 insertions(+), 10 deletions(-) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index df40b0c50167d..6a2ed85edf1a5 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -1,4 +1,17 @@ +from typing import overload + +def random() -> float: ... +def randint(a: int, b: int) -> int: ... +@overload +def randrange(stop: int, /) -> int: ... +@overload +def randrange(start: int, stop: int, /) -> int: ... + class Random: def __init__(self) -> None: ... def randint(self, a: int, b: int) -> int: ... + @overload + def randrange(self, stop: int, /) -> int: ... + @overload + def randrange(self, start: int, stop: int, /) -> int: ... def random(self) -> float: ... diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index a45ce399d54f8..91fa4aef3188b 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -234,6 +234,16 @@ module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) return PyFloat_FromDouble(result); } +// Generate random integer in [a, b] using the given RNG. +static inline PyObject* +randint_impl(chacha8_rng *rng, long long a, long long b) +{ + unsigned long long range = (unsigned long long)(b - a) + 1; + uint32_t r = chacha8_next(rng); + long long result = a + (long long)(r % range); + return PyLong_FromLongLong(result); +} + static PyObject* module_randint(PyObject *module, PyObject *const *args, Py_ssize_t nargs) { @@ -261,10 +271,58 @@ module_randint(PyObject *module, PyObject *const *args, Py_ssize_t nargs) if (rng == NULL) return NULL; - unsigned long long range = (unsigned long long)(b - a) + 1; - uint32_t r = chacha8_next(rng); - long long result = a + (long long)(r % range); - return PyLong_FromLongLong(result); + return randint_impl(rng, a, b); +} + +// Parse 1 or 2 int args for randrange([start,] stop). +// Sets *a to start (default 0), *b to stop-1. +// Returns 0 on success, -1 on error (with exception set). +static int +parse_randrange_args(PyObject *const *args, Py_ssize_t nargs, + long long *a, long long *b) +{ + if (nargs == 1) { + *a = 0; + long long stop = PyLong_AsLongLong(args[0]); + if (stop == -1 && PyErr_Occurred()) + return -1; + if (stop <= 0) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return -1; + } + *b = stop - 1; + } else if (nargs == 2) { + *a = PyLong_AsLongLong(args[0]); + if (*a == -1 && PyErr_Occurred()) + return -1; + long long stop = PyLong_AsLongLong(args[1]); + if (stop == -1 && PyErr_Occurred()) + return -1; + if (*a >= stop) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return -1; + } + *b = stop - 1; + } else { + PyErr_Format(PyExc_TypeError, + "randrange() takes 1 or 2 arguments (%zd given)", nargs); + return -1; + } + return 0; +} + +static PyObject* +module_randrange(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + long long a, b; + if (parse_randrange_args(args, nargs, &a, &b) < 0) + return NULL; + + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + + return randint_impl(rng, a, b); } // @@ -334,10 +392,15 @@ Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { return NULL; } - unsigned long long range = (unsigned long long)(b - a) + 1; - uint32_t r = chacha8_next(&self->rng); - long long result = a + (long long)(r % range); - return PyLong_FromLongLong(result); + return randint_impl(&self->rng, a, b); +} + +static PyObject* +Random_randrange(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { + long long a, b; + if (parse_randrange_args(args, nargs, &a, &b) < 0) + return NULL; + return randint_impl(&self->rng, a, b); } static PyObject* @@ -352,6 +415,9 @@ static PyMethodDef Random_methods[] = { {"randint", (PyCFunction) Random_randint, METH_FASTCALL, PyDoc_STR("Return random integer in range [a, b], including both end points.") }, + {"randrange", (PyCFunction) Random_randrange, METH_FASTCALL, + PyDoc_STR("Return random integer in range [start, stop).") + }, {"random", (PyCFunction) Random_random, METH_NOARGS, PyDoc_STR("Return random float in [0.0, 1.0).") }, @@ -379,6 +445,9 @@ static PyMethodDef librt_random_module_methods[] = { {"randint", (PyCFunction) module_randint, METH_FASTCALL, PyDoc_STR("Return random integer in range [a, b] using thread-local RNG.") }, + {"randrange", (PyCFunction) module_randrange, METH_FASTCALL, + PyDoc_STR("Return random integer in range [start, stop) using thread-local RNG.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 04ba7ccec7ba0..a5d20d78d5a25 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -60,8 +60,38 @@ def test_random_produces_different_values() -> None: values.add(r.random()) assert len(values) > 1 +def test_randrange_one_arg() -> None: + r = Random() + for i in range(100): + val = r.randrange(10) + assert 0 <= val < 10 + +def test_randrange_two_args() -> None: + r = Random() + for i in range(100): + val = r.randrange(5, 15) + assert 5 <= val < 15 + +def test_randrange_negative() -> None: + r = Random() + for i in range(100): + val = r.randrange(-10, 0) + assert -10 <= val < 0 + +def test_randrange_single_value() -> None: + r = Random() + for i in range(10): + assert r.randrange(7, 8) == 7 + +def test_randrange_produces_different_values() -> None: + r = Random() + values = set() + for i in range(100): + values.add(r.randrange(1000000)) + assert len(values) > 1 + [case testRandomModuleLevel_librt] -from librt.random import random, randint +from librt.random import random, randint, randrange def test_module_random_basic() -> None: for i in range(100): @@ -92,8 +122,24 @@ def test_module_randint_produces_different_values() -> None: values.add(randint(0, 1000000)) assert len(values) > 1 +def test_module_randrange_one_arg() -> None: + for i in range(100): + val = randrange(10) + assert 0 <= val < 10 + +def test_module_randrange_two_args() -> None: + for i in range(100): + val = randrange(5, 15) + assert 5 <= val < 15 + +def test_module_randrange_produces_different_values() -> None: + values = set() + for i in range(100): + values.add(randrange(1000000)) + assert len(values) > 1 + [case testRandomErrors_librt] -from librt.random import Random, randint +from librt.random import Random, randint, randrange from testutil import assertRaises def test_randint_empty_range() -> None: @@ -111,3 +157,27 @@ def test_randint_wrong_arg_count() -> None: def test_module_randint_empty_range() -> None: with assertRaises(ValueError, "empty range"): randint(10, 5) + +def test_randrange_empty_range() -> None: + r = Random() + with assertRaises(ValueError, "empty range"): + r.randrange(0) + with assertRaises(ValueError, "empty range"): + r.randrange(-5) + with assertRaises(ValueError, "empty range"): + r.randrange(10, 10) + with assertRaises(ValueError, "empty range"): + r.randrange(10, 5) + +def test_randrange_wrong_arg_count() -> None: + r = Random() + with assertRaises(TypeError): + r.randrange() # type: ignore[call-overload] + with assertRaises(TypeError): + r.randrange(1, 2, 3) # type: ignore[call-overload] + +def test_module_randrange_empty_range() -> None: + with assertRaises(ValueError, "empty range"): + randrange(0) + with assertRaises(ValueError, "empty range"): + randrange(10, 5) From 474c62545273102d287526cf89f8fbd5e0fb2af8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 11:17:38 +0000 Subject: [PATCH 06/34] Support explicit seeds --- mypy/typeshed/stubs/librt/librt/random.pyi | 4 +- mypyc/lib-rt/random/librt_random.c | 91 +++++++++++++++++++--- mypyc/test-data/run-librt-random.test | 51 +++++++++++- 3 files changed, 133 insertions(+), 13 deletions(-) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index 6a2ed85edf1a5..1003e4d3441c6 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -6,12 +6,14 @@ def randint(a: int, b: int) -> int: ... def randrange(stop: int, /) -> int: ... @overload def randrange(start: int, stop: int, /) -> int: ... +def seed(n: int, /) -> None: ... class Random: - def __init__(self) -> None: ... + def __init__(self, seed: int | None = None) -> None: ... def randint(self, a: int, b: int) -> int: ... @overload def randrange(self, stop: int, /) -> int: ... @overload def randrange(self, start: int, stop: int, /) -> int: ... def random(self) -> float: ... + def seed(self, n: int, /) -> None: ... diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 91fa4aef3188b..aaa5693ba1bfa 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -116,18 +116,40 @@ chacha8_next(chacha8_rng *rng) return rng->buf[rng->used++]; } -static int -chacha8_init(chacha8_rng *rng) +static void +chacha8_reset(chacha8_rng *rng) { - if (fill_os_entropy(rng->seed, sizeof(rng->seed)) < 0) - return -1; rng->counter = 0; rng->used = 16; // force immediate refill on first call rng->n = 16; rng->blocks_left = CHACHA8_RESEED_INTERVAL; +} + +static int +chacha8_init(chacha8_rng *rng) +{ + if (fill_os_entropy(rng->seed, sizeof(rng->seed)) < 0) + return -1; + chacha8_reset(rng); return 0; } +// Seed from an integer by hashing it through ChaCha8 to fill the 256-bit key. +static void +chacha8_seed_int(chacha8_rng *rng, long long seed_val) +{ + // Use the integer to construct a simple initial key, then run one + // ChaCha8 block to diffuse it across all 256 bits. + memset(rng->seed, 0, sizeof(rng->seed)); + rng->seed[0] = (uint32_t)(seed_val & 0xFFFFFFFF); + rng->seed[1] = (uint32_t)((uint64_t)seed_val >> 32); + + uint32_t out[16]; + chacha8_block(rng->seed, 0, out); + memcpy(rng->seed, out, sizeof(rng->seed)); + chacha8_reset(rng); +} + // // Thread-local global RNG for module-level random()/randint() // @@ -325,6 +347,26 @@ module_randrange(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return randint_impl(rng, a, b); } +static PyObject* +module_seed(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 1) { + PyErr_Format(PyExc_TypeError, + "seed() takes exactly 1 argument (%zd given)", nargs); + return NULL; + } + long long seed_val = PyLong_AsLongLong(args[0]); + if (seed_val == -1 && PyErr_Occurred()) + return NULL; + + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + + chacha8_seed_int(rng, seed_val); + Py_RETURN_NONE; +} + // // Random Python type // @@ -345,19 +387,16 @@ Random_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } RandomObject *self = (RandomObject *)type->tp_alloc(type, 0); - if (self != NULL) { - if (chacha8_init(&self->rng) < 0) { - Py_DECREF(self); - return NULL; - } - } + // Seeding is done in tp_init return (PyObject *)self; } static int Random_init(RandomObject *self, PyObject *args, PyObject *kwds) { - if (!PyArg_ParseTuple(args, "")) { + PyObject *seed_obj = NULL; + + if (!PyArg_ParseTuple(args, "|O", &seed_obj)) { return -1; } @@ -367,6 +406,16 @@ Random_init(RandomObject *self, PyObject *args, PyObject *kwds) return -1; } + if (seed_obj == NULL || seed_obj == Py_None) { + if (chacha8_init(&self->rng) < 0) + return -1; + } else { + long long seed_val = PyLong_AsLongLong(seed_obj); + if (seed_val == -1 && PyErr_Occurred()) + return -1; + chacha8_seed_int(&self->rng, seed_val); + } + return 0; } @@ -411,6 +460,20 @@ Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { return PyFloat_FromDouble(result); } +static PyObject* +Random_seed(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { + if (nargs != 1) { + PyErr_Format(PyExc_TypeError, + "seed() takes exactly 1 argument (%zd given)", nargs); + return NULL; + } + long long seed_val = PyLong_AsLongLong(args[0]); + if (seed_val == -1 && PyErr_Occurred()) + return NULL; + chacha8_seed_int(&self->rng, seed_val); + Py_RETURN_NONE; +} + static PyMethodDef Random_methods[] = { {"randint", (PyCFunction) Random_randint, METH_FASTCALL, PyDoc_STR("Return random integer in range [a, b], including both end points.") @@ -421,6 +484,9 @@ static PyMethodDef Random_methods[] = { {"random", (PyCFunction) Random_random, METH_NOARGS, PyDoc_STR("Return random float in [0.0, 1.0).") }, + {"seed", (PyCFunction) Random_seed, METH_FASTCALL, + PyDoc_STR("Seed the random number generator with an integer.") + }, {NULL} /* Sentinel */ }; @@ -448,6 +514,9 @@ static PyMethodDef librt_random_module_methods[] = { {"randrange", (PyCFunction) module_randrange, METH_FASTCALL, PyDoc_STR("Return random integer in range [start, stop) using thread-local RNG.") }, + {"seed", (PyCFunction) module_seed, METH_FASTCALL, + PyDoc_STR("Seed the thread-local RNG with an integer.") + }, {NULL, NULL, 0, NULL} }; diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index a5d20d78d5a25..b811188978123 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -90,8 +90,43 @@ def test_randrange_produces_different_values() -> None: values.add(r.randrange(1000000)) assert len(values) > 1 +def test_constructor_seed() -> None: + r1 = Random(42) + r2 = Random(42) + vals1 = [r1.randint(0, 1000000) for _ in range(20)] + vals2 = [r2.randint(0, 1000000) for _ in range(20)] + assert vals1 == vals2 + +def test_constructor_seed_different() -> None: + r1 = Random(42) + r2 = Random(43) + vals1 = [r1.randint(0, 1000000) for _ in range(20)] + vals2 = [r2.randint(0, 1000000) for _ in range(20)] + assert vals1 != vals2 + +def test_constructor_none_seed() -> None: + r = Random(None) + val = r.random() + assert 0.0 <= val < 1.0 + +def test_seed_method() -> None: + r = Random(0) + r.seed(42) + vals1 = [r.randint(0, 1000000) for _ in range(20)] + r.seed(42) + vals2 = [r.randint(0, 1000000) for _ in range(20)] + assert vals1 == vals2 + +def test_seed_method_resets_state() -> None: + r = Random(42) + expected = [r.randint(0, 1000000) for _ in range(20)] + # Consume some values, then reseed + r.seed(42) + actual = [r.randint(0, 1000000) for _ in range(20)] + assert expected == actual + [case testRandomModuleLevel_librt] -from librt.random import random, randint, randrange +from librt.random import random, randint, randrange, seed def test_module_random_basic() -> None: for i in range(100): @@ -138,6 +173,20 @@ def test_module_randrange_produces_different_values() -> None: values.add(randrange(1000000)) assert len(values) > 1 +def test_module_seed_reproducible() -> None: + seed(42) + vals1 = [randint(0, 1000000) for _ in range(20)] + seed(42) + vals2 = [randint(0, 1000000) for _ in range(20)] + assert vals1 == vals2 + +def test_module_seed_different() -> None: + seed(42) + vals1 = [randint(0, 1000000) for _ in range(20)] + seed(43) + vals2 = [randint(0, 1000000) for _ in range(20)] + assert vals1 != vals2 + [case testRandomErrors_librt] from librt.random import Random, randint, randrange from testutil import assertRaises From 34dd760770c9616eb8d142e64d55ea4681879d3f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 11:24:56 +0000 Subject: [PATCH 07/34] Use i64 as integer type --- mypy/typeshed/stubs/librt/librt/random.pyi | 20 ++++---- mypyc/build.py | 2 +- mypyc/lib-rt/random/librt_random.c | 55 +++++++++++----------- mypyc/lib-rt/setup.py | 9 +++- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index 1003e4d3441c6..0268223c5590f 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -1,19 +1,21 @@ from typing import overload +from mypy_extensions import i64 + def random() -> float: ... -def randint(a: int, b: int) -> int: ... +def randint(a: i64, b: i64) -> i64: ... @overload -def randrange(stop: int, /) -> int: ... +def randrange(stop: i64, /) -> i64: ... @overload -def randrange(start: int, stop: int, /) -> int: ... -def seed(n: int, /) -> None: ... +def randrange(start: i64, stop: i64, /) -> i64: ... +def seed(n: i64, /) -> None: ... class Random: - def __init__(self, seed: int | None = None) -> None: ... - def randint(self, a: int, b: int) -> int: ... + def __init__(self, seed: i64 | None = None) -> None: ... + def randint(self, a: i64, b: i64) -> i64: ... @overload - def randrange(self, stop: int, /) -> int: ... + def randrange(self, stop: i64, /) -> i64: ... @overload - def randrange(self, start: int, stop: int, /) -> int: ... + def randrange(self, start: i64, stop: i64, /) -> i64: ... def random(self) -> float: ... - def seed(self, n: int, /) -> None: ... + def seed(self, n: i64, /) -> None: ... diff --git a/mypyc/build.py b/mypyc/build.py index 7004075fe42c9..e2d8fe96fb43a 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -121,7 +121,7 @@ class ModDesc(NamedTuple): ["vecs"], ), ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []), - ModDesc("librt.random", ["random/librt_random.c"], [], []), + ModDesc("librt.random", ["random/librt_random.c"], [], ["random"]), ] try: diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index aaa5693ba1bfa..c0fd170e30aa9 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -10,6 +10,7 @@ #endif #include "mypyc_util.h" +#include "CPy.h" // // ChaCha8 PRNG with forward secrecy @@ -136,7 +137,7 @@ chacha8_init(chacha8_rng *rng) // Seed from an integer by hashing it through ChaCha8 to fill the 256-bit key. static void -chacha8_seed_int(chacha8_rng *rng, long long seed_val) +chacha8_seed_int(chacha8_rng *rng, int64_t seed_val) { // Use the integer to construct a simple initial key, then run one // ChaCha8 block to diffuse it across all 256 bits. @@ -258,11 +259,11 @@ module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) // Generate random integer in [a, b] using the given RNG. static inline PyObject* -randint_impl(chacha8_rng *rng, long long a, long long b) +randint_impl(chacha8_rng *rng, int64_t a, int64_t b) { - unsigned long long range = (unsigned long long)(b - a) + 1; + uint64_t range = (uint64_t)(b - a) + 1; uint32_t r = chacha8_next(rng); - long long result = a + (long long)(r % range); + int64_t result = a + (int64_t)(r % range); return PyLong_FromLongLong(result); } @@ -275,12 +276,12 @@ module_randint(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return NULL; } - long long a = PyLong_AsLongLong(args[0]); - if (a == -1 && PyErr_Occurred()) + int64_t a = CPyLong_AsInt64(args[0]); + if (unlikely(a == CPY_LL_INT_ERROR && PyErr_Occurred())) return NULL; - long long b = PyLong_AsLongLong(args[1]); - if (b == -1 && PyErr_Occurred()) + int64_t b = CPyLong_AsInt64(args[1]); + if (unlikely(b == CPY_LL_INT_ERROR && PyErr_Occurred())) return NULL; if (a > b) { @@ -301,12 +302,12 @@ module_randint(PyObject *module, PyObject *const *args, Py_ssize_t nargs) // Returns 0 on success, -1 on error (with exception set). static int parse_randrange_args(PyObject *const *args, Py_ssize_t nargs, - long long *a, long long *b) + int64_t *a, int64_t *b) { if (nargs == 1) { *a = 0; - long long stop = PyLong_AsLongLong(args[0]); - if (stop == -1 && PyErr_Occurred()) + int64_t stop = CPyLong_AsInt64(args[0]); + if (unlikely(stop == CPY_LL_INT_ERROR && PyErr_Occurred())) return -1; if (stop <= 0) { PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); @@ -314,11 +315,11 @@ parse_randrange_args(PyObject *const *args, Py_ssize_t nargs, } *b = stop - 1; } else if (nargs == 2) { - *a = PyLong_AsLongLong(args[0]); - if (*a == -1 && PyErr_Occurred()) + *a = CPyLong_AsInt64(args[0]); + if (unlikely(*a == CPY_LL_INT_ERROR && PyErr_Occurred())) return -1; - long long stop = PyLong_AsLongLong(args[1]); - if (stop == -1 && PyErr_Occurred()) + int64_t stop = CPyLong_AsInt64(args[1]); + if (unlikely(stop == CPY_LL_INT_ERROR && PyErr_Occurred())) return -1; if (*a >= stop) { PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); @@ -336,7 +337,7 @@ parse_randrange_args(PyObject *const *args, Py_ssize_t nargs, static PyObject* module_randrange(PyObject *module, PyObject *const *args, Py_ssize_t nargs) { - long long a, b; + int64_t a, b; if (parse_randrange_args(args, nargs, &a, &b) < 0) return NULL; @@ -355,8 +356,8 @@ module_seed(PyObject *module, PyObject *const *args, Py_ssize_t nargs) "seed() takes exactly 1 argument (%zd given)", nargs); return NULL; } - long long seed_val = PyLong_AsLongLong(args[0]); - if (seed_val == -1 && PyErr_Occurred()) + int64_t seed_val = CPyLong_AsInt64(args[0]); + if (unlikely(seed_val == CPY_LL_INT_ERROR && PyErr_Occurred())) return NULL; chacha8_rng *rng = get_thread_rng(); @@ -410,8 +411,8 @@ Random_init(RandomObject *self, PyObject *args, PyObject *kwds) if (chacha8_init(&self->rng) < 0) return -1; } else { - long long seed_val = PyLong_AsLongLong(seed_obj); - if (seed_val == -1 && PyErr_Occurred()) + int64_t seed_val = CPyLong_AsInt64(seed_obj); + if (unlikely(seed_val == CPY_LL_INT_ERROR && PyErr_Occurred())) return -1; chacha8_seed_int(&self->rng, seed_val); } @@ -427,12 +428,12 @@ Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { return NULL; } - long long a = PyLong_AsLongLong(args[0]); - if (a == -1 && PyErr_Occurred()) + int64_t a = CPyLong_AsInt64(args[0]); + if (unlikely(a == CPY_LL_INT_ERROR && PyErr_Occurred())) return NULL; - long long b = PyLong_AsLongLong(args[1]); - if (b == -1 && PyErr_Occurred()) + int64_t b = CPyLong_AsInt64(args[1]); + if (unlikely(b == CPY_LL_INT_ERROR && PyErr_Occurred())) return NULL; if (a > b) { @@ -446,7 +447,7 @@ Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { static PyObject* Random_randrange(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { - long long a, b; + int64_t a, b; if (parse_randrange_args(args, nargs, &a, &b) < 0) return NULL; return randint_impl(&self->rng, a, b); @@ -467,8 +468,8 @@ Random_seed(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { "seed() takes exactly 1 argument (%zd given)", nargs); return NULL; } - long long seed_val = PyLong_AsLongLong(args[0]); - if (seed_val == -1 && PyErr_Occurred()) + int64_t seed_val = CPyLong_AsInt64(args[0]); + if (unlikely(seed_val == CPY_LL_INT_ERROR && PyErr_Occurred())) return NULL; chacha8_seed_int(&self->rng, seed_val); Py_RETURN_NONE; diff --git a/mypyc/lib-rt/setup.py b/mypyc/lib-rt/setup.py index 6ad0070eef45f..371b322ca18b2 100644 --- a/mypyc/lib-rt/setup.py +++ b/mypyc/lib-rt/setup.py @@ -153,7 +153,14 @@ def run(self) -> None: ), Extension( "librt.random", - ["random/librt_random.c"], + [ + "random/librt_random.c", + "init.c", + "int_ops.c", + "exc_ops.c", + "pythonsupport.c", + "getargsfast.c", + ], include_dirs=["."], extra_compile_args=cflags, ), From fec55835682446c5b73586170f927b3ddb93bb3c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 13:07:42 +0000 Subject: [PATCH 08/34] Add randbits62() --- mypy/typeshed/stubs/librt/librt/random.pyi | 2 ++ mypyc/lib-rt/random/librt_random.c | 29 +++++++++++++++++++ mypyc/test-data/run-librt-random.test | 33 +++++++++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index 0268223c5590f..dc732825764e1 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -8,6 +8,7 @@ def randint(a: i64, b: i64) -> i64: ... def randrange(stop: i64, /) -> i64: ... @overload def randrange(start: i64, stop: i64, /) -> i64: ... +def randbits62() -> i64: ... def seed(n: i64, /) -> None: ... class Random: @@ -18,4 +19,5 @@ class Random: @overload def randrange(self, start: i64, stop: i64, /) -> i64: ... def random(self) -> float: ... + def randbits62(self) -> i64: ... def seed(self, n: i64, /) -> None: ... diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index c0fd170e30aa9..b9eabf40f87d0 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -257,6 +257,24 @@ module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) return PyFloat_FromDouble(result); } +// Return a random non-negative 62-bit integer (fits in a tagged integer). +static inline int64_t +randbits62_impl(chacha8_rng *rng) +{ + uint32_t lo = chacha8_next(rng); + uint32_t hi = chacha8_next(rng); + return (int64_t)(((uint64_t)hi << 30) | (lo >> 2)); +} + +static PyObject* +module_randbits62(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + return PyLong_FromLongLong(randbits62_impl(rng)); +} + // Generate random integer in [a, b] using the given RNG. static inline PyObject* randint_impl(chacha8_rng *rng, int64_t a, int64_t b) @@ -461,6 +479,11 @@ Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { return PyFloat_FromDouble(result); } +static PyObject* +Random_randbits62(RandomObject *self, PyObject *Py_UNUSED(ignored)) { + return PyLong_FromLongLong(randbits62_impl(&self->rng)); +} + static PyObject* Random_seed(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { if (nargs != 1) { @@ -485,6 +508,9 @@ static PyMethodDef Random_methods[] = { {"random", (PyCFunction) Random_random, METH_NOARGS, PyDoc_STR("Return random float in [0.0, 1.0).") }, + {"randbits62", (PyCFunction) Random_randbits62, METH_NOARGS, + PyDoc_STR("Return random non-negative 62-bit integer.") + }, {"seed", (PyCFunction) Random_seed, METH_FASTCALL, PyDoc_STR("Seed the random number generator with an integer.") }, @@ -515,6 +541,9 @@ static PyMethodDef librt_random_module_methods[] = { {"randrange", (PyCFunction) module_randrange, METH_FASTCALL, PyDoc_STR("Return random integer in range [start, stop) using thread-local RNG.") }, + {"randbits62", (PyCFunction) module_randbits62, METH_NOARGS, + PyDoc_STR("Return random non-negative 62-bit integer using thread-local RNG.") + }, {"seed", (PyCFunction) module_seed, METH_FASTCALL, PyDoc_STR("Seed the thread-local RNG with an integer.") }, diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index b811188978123..400312607e22a 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -125,8 +125,28 @@ def test_seed_method_resets_state() -> None: actual = [r.randint(0, 1000000) for _ in range(20)] assert expected == actual +def test_randbits62_basic() -> None: + r = Random() + for i in range(100): + val = r.randbits62() + assert 0 <= val < (1 << 62) + +def test_randbits62_produces_different_values() -> None: + r = Random() + values = set() + for i in range(100): + values.add(r.randbits62()) + assert len(values) > 1 + +def test_randbits62_reproducible() -> None: + r1 = Random(42) + r2 = Random(42) + vals1 = [r1.randbits62() for _ in range(20)] + vals2 = [r2.randbits62() for _ in range(20)] + assert vals1 == vals2 + [case testRandomModuleLevel_librt] -from librt.random import random, randint, randrange, seed +from librt.random import random, randint, randrange, randbits62, seed def test_module_random_basic() -> None: for i in range(100): @@ -173,6 +193,17 @@ def test_module_randrange_produces_different_values() -> None: values.add(randrange(1000000)) assert len(values) > 1 +def test_module_randbits62_basic() -> None: + for i in range(100): + val = randbits62() + assert 0 <= val < (1 << 62) + +def test_module_randbits62_produces_different_values() -> None: + values = set() + for i in range(100): + values.add(randbits62()) + assert len(values) > 1 + def test_module_seed_reproducible() -> None: seed(42) vals1 = [randint(0, 1000000) for _ in range(20)] From db1e2d4209d5bbeafe6b3d82757b2408503a450a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 13:24:13 +0000 Subject: [PATCH 09/34] Add few Random primitives --- mypyc/codegen/emitmodule.py | 5 ++ mypyc/ir/deps.py | 1 + mypyc/ir/rtypes.py | 7 ++- mypyc/lib-rt/random/librt_random.c | 57 +++++++++++++++++++++++ mypyc/primitives/registry.py | 1 + mypyc/test-data/irbuild-librt-random.test | 25 ++++++++++ mypyc/test-data/run-librt-random.test | 6 +-- mypyc/test/test_irbuild.py | 1 + 8 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 mypyc/test-data/irbuild-librt-random.test diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 2025426188412..3f10df7fa8c98 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -59,6 +59,7 @@ from mypyc.errors import Errors from mypyc.ir.deps import ( LIBRT_BASE64, + LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_TIME, LIBRT_VECS, @@ -1224,6 +1225,10 @@ def emit_module_exec_func( emitter.emit_line("if (import_librt_vecs() < 0) {") emitter.emit_line("return -1;") emitter.emit_line("}") + if LIBRT_RANDOM in module.dependencies: + emitter.emit_line("if (import_librt_random() < 0) {") + emitter.emit_line("return -1;") + emitter.emit_line("}") emitter.emit_line("PyObject* modname = NULL;") if self.multi_phase_init: emitter.emit_line(f"{module_static} = module;") diff --git a/mypyc/ir/deps.py b/mypyc/ir/deps.py index 20b1f102ee383..751845d3a324c 100644 --- a/mypyc/ir/deps.py +++ b/mypyc/ir/deps.py @@ -109,6 +109,7 @@ def get_header(self) -> str: LIBRT_BASE64: Final = Capsule("librt.base64") LIBRT_VECS: Final = Capsule("librt.vecs") LIBRT_TIME: Final = Capsule("librt.time") +LIBRT_RANDOM: Final = Capsule("librt.random") BYTES_EXTRA_OPS: Final = SourceDep("bytes_extra_ops.c") BYTES_WRITER_EXTRA_OPS: Final = SourceDep("byteswriter_extra_ops.c") diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 60e9b49582bc3..024417f52a987 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -41,7 +41,7 @@ class to enable the new behavior. In rare cases, adding a new from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeGuard, TypeVar, Union, final from mypyc.common import HAVE_IMMORTAL, IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name -from mypyc.ir.deps import LIBRT_STRINGS, LIBRT_VECS, Dependency +from mypyc.ir.deps import LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_VECS, Dependency from mypyc.namegen import NameGenerator if TYPE_CHECKING: @@ -544,10 +544,15 @@ def __hash__(self) -> int: ("librt.strings.BytesWriter", (LIBRT_STRINGS,)), ("librt.strings.StringWriter", (LIBRT_STRINGS,)), ] +} | { + "librt.random.Random": RPrimitive( + "librt.random.Random", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_RANDOM,) + ), } bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"] string_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.StringWriter"] +random_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.random.Random"] def is_native_rprimitive(rtype: RType) -> bool: diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index b9eabf40f87d0..3a151a490b4ff 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -11,6 +11,7 @@ #include "mypyc_util.h" #include "CPy.h" +#include "librt_random.h" // // ChaCha8 PRNG with forward secrecy @@ -438,6 +439,34 @@ Random_init(RandomObject *self, PyObject *args, PyObject *kwds) return 0; } +// Internal constructors for capsule API (bypass tp_new/tp_init) + +static PyObject * +Random_internal(void) { + RandomObject *self = (RandomObject *)RandomType.tp_alloc(&RandomType, 0); + if (self == NULL) + return NULL; + if (chacha8_init(&self->rng) < 0) { + Py_DECREF(self); + return NULL; + } + return (PyObject *)self; +} + +static PyObject * +Random_from_seed_internal(int64_t seed_val) { + RandomObject *self = (RandomObject *)RandomType.tp_alloc(&RandomType, 0); + if (self == NULL) + return NULL; + chacha8_seed_int(&self->rng, seed_val); + return (PyObject *)self; +} + +static PyTypeObject * +Random_type_internal(void) { + return &RandomType; +} + static PyObject* Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { if (nargs != 2) { @@ -550,6 +579,20 @@ static PyMethodDef librt_random_module_methods[] = { {NULL, NULL, 0, NULL} }; +#ifdef MYPYC_EXPERIMENTAL + +static int +random_abi_version(void) { + return LIBRT_RANDOM_ABI_VERSION; +} + +static int +random_api_version(void) { + return LIBRT_RANDOM_API_VERSION; +} + +#endif + static int librt_random_module_exec(PyObject *m) { @@ -562,6 +605,20 @@ librt_random_module_exec(PyObject *m) if (PyModule_AddObjectRef(m, "Random", (PyObject *) &RandomType) < 0) { return -1; } +#ifdef MYPYC_EXPERIMENTAL + // Export mypyc internal C API via capsule + static void *librt_random_api[LIBRT_RANDOM_API_LEN] = { + (void *)random_abi_version, + (void *)random_api_version, + (void *)Random_internal, + (void *)Random_from_seed_internal, + (void *)Random_type_internal, + }; + PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); + if (PyModule_Add(m, "_C_API", c_api_object) < 0) { + return -1; + } +#endif return 0; } diff --git a/mypyc/primitives/registry.py b/mypyc/primitives/registry.py index c04b4ff65a757..e22a044d9bb27 100644 --- a/mypyc/primitives/registry.py +++ b/mypyc/primitives/registry.py @@ -403,6 +403,7 @@ def load_global_op(name: str, type: RType, src: str) -> LoadAddressDescription: import mypyc.primitives.dict_ops import mypyc.primitives.float_ops import mypyc.primitives.int_ops +import mypyc.primitives.librt_random_ops import mypyc.primitives.librt_strings_ops import mypyc.primitives.librt_time_ops import mypyc.primitives.librt_vecs_ops diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test new file mode 100644 index 0000000000000..3718fff271670 --- /dev/null +++ b/mypyc/test-data/irbuild-librt-random.test @@ -0,0 +1,25 @@ +[case testLibrtRandomConstructor_experimental_64bit] +from librt.random import Random + +def make_random() -> Random: + return Random() +[out] +def make_random(): + r0 :: librt.random.Random +L0: + r0 = LibRTRandom_Random_internal() + return r0 + +[case testLibrtRandomConstructorWithSeed_experimental_64bit] +from librt.random import Random +from mypy_extensions import i64 + +def make_random_seeded(n: i64) -> Random: + return Random(n) +[out] +def make_random_seeded(n): + n :: i64 + r0 :: librt.random.Random +L0: + r0 = LibRTRandom_Random_from_seed_internal(n) + return r0 diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 400312607e22a..5e60ffcf20e3b 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -1,4 +1,4 @@ -[case testRandomBasic_librt] +[case testRandomBasic_librt_experimental] from librt.random import Random def test_random_construct() -> None: @@ -145,7 +145,7 @@ def test_randbits62_reproducible() -> None: vals2 = [r2.randbits62() for _ in range(20)] assert vals1 == vals2 -[case testRandomModuleLevel_librt] +[case testRandomModuleLevel_librt_experimental] from librt.random import random, randint, randrange, randbits62, seed def test_module_random_basic() -> None: @@ -218,7 +218,7 @@ def test_module_seed_different() -> None: vals2 = [randint(0, 1000000) for _ in range(20)] assert vals1 != vals2 -[case testRandomErrors_librt] +[case testRandomErrors_librt_experimental] from librt.random import Random, randint, randrange from testutil import assertRaises diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index f1f0ec777c3da..7e3993e267e74 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -59,6 +59,7 @@ "irbuild-math.test", "irbuild-weakref.test", "irbuild-librt-strings.test", + "irbuild-librt-random.test", "irbuild-base64.test", "irbuild-time.test", "irbuild-match.test", From dc3b90420ce242c19446c4ce04d7006431ade833 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 13:28:57 +0000 Subject: [PATCH 10/34] Add randbits62() primitive --- mypyc/lib-rt/random/librt_random.c | 6 ++++++ mypyc/test-data/irbuild-librt-random.test | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 3a151a490b4ff..053e7839c512e 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -467,6 +467,11 @@ Random_type_internal(void) { return &RandomType; } +static int64_t +Random_randbits62_internal(PyObject *self) { + return randbits62_impl(&((RandomObject *)self)->rng); +} + static PyObject* Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { if (nargs != 2) { @@ -613,6 +618,7 @@ librt_random_module_exec(PyObject *m) (void *)Random_internal, (void *)Random_from_seed_internal, (void *)Random_type_internal, + (void *)Random_randbits62_internal, }; PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index 3718fff271670..f980600cbf33d 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -23,3 +23,17 @@ def make_random_seeded(n): L0: r0 = LibRTRandom_Random_from_seed_internal(n) return r0 + +[case testLibrtRandomRandbits62_experimental_64bit] +from librt.random import Random +from mypy_extensions import i64 + +def randbits(r: Random) -> i64: + return r.randbits62() +[out] +def randbits(r): + r :: librt.random.Random + r0 :: i64 +L0: + r0 = LibRTRandom_Random_randbits62_internal(r) + return r0 From 22da84abe87d55db31a3264762909a0b32c3ebd7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 13:34:49 +0000 Subject: [PATCH 11/34] Unrelated: fix librt optimized build --- mypyc/build.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypyc/build.py b/mypyc/build.py index e2d8fe96fb43a..6e31f47edda13 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -632,6 +632,9 @@ def get_cflags( # Disables C Preprocessor (cpp) warnings # See https://github.com/mypyc/mypyc/issues/956 "-Wno-cpp", + "-Wno-array-bounds", + "-Wno-stringop-overread", + "-Wno-stringop-overflow", ] if log_trace: cflags.append("-DMYPYC_LOG_TRACE") From f7df2e885c6e14e0289e3b8a06b8a1c290527414 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 13:40:34 +0000 Subject: [PATCH 12/34] Add primitive for random() --- mypyc/lib-rt/random/librt_random.c | 7 +++++++ mypyc/test-data/irbuild-librt-random.test | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 053e7839c512e..9cef290aad887 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -472,6 +472,12 @@ Random_randbits62_internal(PyObject *self) { return randbits62_impl(&((RandomObject *)self)->rng); } +static double +Random_random_internal(PyObject *self) { + uint32_t r = chacha8_next(&((RandomObject *)self)->rng); + return r / 4294967296.0; +} + static PyObject* Random_randint(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { if (nargs != 2) { @@ -619,6 +625,7 @@ librt_random_module_exec(PyObject *m) (void *)Random_from_seed_internal, (void *)Random_type_internal, (void *)Random_randbits62_internal, + (void *)Random_random_internal, }; PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index f980600cbf33d..df634e262b5d2 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -37,3 +37,16 @@ def randbits(r): L0: r0 = LibRTRandom_Random_randbits62_internal(r) return r0 + +[case testLibrtRandomRandom_experimental_64bit] +from librt.random import Random + +def rand(r: Random) -> float: + return r.random() +[out] +def rand(r): + r :: librt.random.Random + r0 :: float +L0: + r0 = LibRTRandom_Random_random_internal(r) + return r0 From 00c2e8564ace19338d841a39f2fe38c533d62f41 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 14:04:32 +0000 Subject: [PATCH 13/34] Addd randbits31() --- mypy/typeshed/stubs/librt/librt/random.pyi | 4 ++- mypyc/lib-rt/random/librt_random.c | 33 ++++++++++++++++++++++ mypyc/test-data/irbuild-librt-random.test | 14 +++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index dc732825764e1..3fc1b3652ac0c 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -1,6 +1,6 @@ from typing import overload -from mypy_extensions import i64 +from mypy_extensions import i32, i64 def random() -> float: ... def randint(a: i64, b: i64) -> i64: ... @@ -8,6 +8,7 @@ def randint(a: i64, b: i64) -> i64: ... def randrange(stop: i64, /) -> i64: ... @overload def randrange(start: i64, stop: i64, /) -> i64: ... +def randbits31() -> i32: ... def randbits62() -> i64: ... def seed(n: i64, /) -> None: ... @@ -19,5 +20,6 @@ class Random: @overload def randrange(self, start: i64, stop: i64, /) -> i64: ... def random(self) -> float: ... + def randbits31(self) -> i32: ... def randbits62(self) -> i64: ... def seed(self, n: i64, /) -> None: ... diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 9cef290aad887..d4442ef019946 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -258,6 +258,13 @@ module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) return PyFloat_FromDouble(result); } +// Return a random non-negative 31-bit integer [0, 2^31). +static inline int32_t +randbits31_impl(chacha8_rng *rng) +{ + return (int32_t)(chacha8_next(rng) >> 1); +} + // Return a random non-negative 62-bit integer (fits in a tagged integer). static inline int64_t randbits62_impl(chacha8_rng *rng) @@ -267,6 +274,15 @@ randbits62_impl(chacha8_rng *rng) return (int64_t)(((uint64_t)hi << 30) | (lo >> 2)); } +static PyObject* +module_randbits31(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + return PyLong_FromLong(randbits31_impl(rng)); +} + static PyObject* module_randbits62(PyObject *module, PyObject *Py_UNUSED(ignored)) { @@ -472,6 +488,11 @@ Random_randbits62_internal(PyObject *self) { return randbits62_impl(&((RandomObject *)self)->rng); } +static int32_t +Random_randbits31_internal(PyObject *self) { + return randbits31_impl(&((RandomObject *)self)->rng); +} + static double Random_random_internal(PyObject *self) { uint32_t r = chacha8_next(&((RandomObject *)self)->rng); @@ -519,6 +540,11 @@ Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { return PyFloat_FromDouble(result); } +static PyObject* +Random_randbits31(RandomObject *self, PyObject *Py_UNUSED(ignored)) { + return PyLong_FromLong(randbits31_impl(&self->rng)); +} + static PyObject* Random_randbits62(RandomObject *self, PyObject *Py_UNUSED(ignored)) { return PyLong_FromLongLong(randbits62_impl(&self->rng)); @@ -548,6 +574,9 @@ static PyMethodDef Random_methods[] = { {"random", (PyCFunction) Random_random, METH_NOARGS, PyDoc_STR("Return random float in [0.0, 1.0).") }, + {"randbits31", (PyCFunction) Random_randbits31, METH_NOARGS, + PyDoc_STR("Return random non-negative 31-bit integer.") + }, {"randbits62", (PyCFunction) Random_randbits62, METH_NOARGS, PyDoc_STR("Return random non-negative 62-bit integer.") }, @@ -581,6 +610,9 @@ static PyMethodDef librt_random_module_methods[] = { {"randrange", (PyCFunction) module_randrange, METH_FASTCALL, PyDoc_STR("Return random integer in range [start, stop) using thread-local RNG.") }, + {"randbits31", (PyCFunction) module_randbits31, METH_NOARGS, + PyDoc_STR("Return random non-negative 31-bit integer using thread-local RNG.") + }, {"randbits62", (PyCFunction) module_randbits62, METH_NOARGS, PyDoc_STR("Return random non-negative 62-bit integer using thread-local RNG.") }, @@ -626,6 +658,7 @@ librt_random_module_exec(PyObject *m) (void *)Random_type_internal, (void *)Random_randbits62_internal, (void *)Random_random_internal, + (void *)Random_randbits31_internal, }; PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index df634e262b5d2..695a02d41b0be 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -24,6 +24,20 @@ L0: r0 = LibRTRandom_Random_from_seed_internal(n) return r0 +[case testLibrtRandomRandbits31_experimental_64bit] +from librt.random import Random +from mypy_extensions import i32 + +def randbits31(r: Random) -> i32: + return r.randbits31() +[out] +def randbits31(r): + r :: librt.random.Random + r0 :: i32 +L0: + r0 = LibRTRandom_Random_randbits31_internal(r) + return r0 + [case testLibrtRandomRandbits62_experimental_64bit] from librt.random import Random from mypy_extensions import i64 From a0095a8f6a8b98e9e2014bafe882e56c90872f43 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 14:12:21 +0000 Subject: [PATCH 14/34] Primitives for randint and randrange --- mypyc/lib-rt/random/librt_random.c | 35 ++++++++++++++++++++++ mypyc/test-data/irbuild-librt-random.test | 36 +++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index d4442ef019946..b10eabcb39bd4 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -493,6 +493,38 @@ Random_randbits31_internal(PyObject *self) { return randbits31_impl(&((RandomObject *)self)->rng); } +static int64_t +Random_randrange1_internal(PyObject *self, int64_t stop) { + if (unlikely(stop <= 0)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + uint32_t r = chacha8_next(&((RandomObject *)self)->rng); + return (int64_t)(r % (uint64_t)stop); +} + +static int64_t +Random_randrange2_internal(PyObject *self, int64_t start, int64_t stop) { + if (unlikely(start >= stop)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + uint64_t range = (uint64_t)(stop - start); + uint32_t r = chacha8_next(&((RandomObject *)self)->rng); + return start + (int64_t)(r % range); +} + +static int64_t +Random_randint_internal(PyObject *self, int64_t a, int64_t b) { + if (unlikely(a > b)) { + PyErr_SetString(PyExc_ValueError, "empty range for randint()"); + return CPY_LL_INT_ERROR; + } + uint64_t range = (uint64_t)(b - a) + 1; + uint32_t r = chacha8_next(&((RandomObject *)self)->rng); + return a + (int64_t)(r % range); +} + static double Random_random_internal(PyObject *self) { uint32_t r = chacha8_next(&((RandomObject *)self)->rng); @@ -659,6 +691,9 @@ librt_random_module_exec(PyObject *m) (void *)Random_randbits62_internal, (void *)Random_random_internal, (void *)Random_randbits31_internal, + (void *)Random_randint_internal, + (void *)Random_randrange1_internal, + (void *)Random_randrange2_internal, }; PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index 695a02d41b0be..b76c0cbbe3748 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -24,6 +24,42 @@ L0: r0 = LibRTRandom_Random_from_seed_internal(n) return r0 +[case testLibrtRandomRandrange_experimental_64bit] +from librt.random import Random +from mypy_extensions import i64 + +def randrange1(r: Random, stop: i64) -> i64: + return r.randrange(stop) +def randrange2(r: Random, start: i64, stop: i64) -> i64: + return r.randrange(start, stop) +[out] +def randrange1(r, stop): + r :: librt.random.Random + stop, r0 :: i64 +L0: + r0 = LibRTRandom_Random_randrange1_internal(r, stop) + return r0 +def randrange2(r, start, stop): + r :: librt.random.Random + start, stop, r0 :: i64 +L0: + r0 = LibRTRandom_Random_randrange2_internal(r, start, stop) + return r0 + +[case testLibrtRandomRandint_experimental_64bit] +from librt.random import Random +from mypy_extensions import i64 + +def randint(r: Random, a: i64, b: i64) -> i64: + return r.randint(a, b) +[out] +def randint(r, a, b): + r :: librt.random.Random + a, b, r0 :: i64 +L0: + r0 = LibRTRandom_Random_randint_internal(r, a, b) + return r0 + [case testLibrtRandomRandbits31_experimental_64bit] from librt.random import Random from mypy_extensions import i32 From c8364470cd80b519574941ab1012e4394a235eca Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 14:27:54 +0000 Subject: [PATCH 15/34] Add more module level function primitives --- mypyc/lib-rt/random/librt_random.c | 56 +++++++++++++++++++++++ mypyc/test-data/irbuild-librt-random.test | 45 ++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index b10eabcb39bd4..7b693add192b1 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -654,6 +654,58 @@ static PyMethodDef librt_random_module_methods[] = { {NULL, NULL, 0, NULL} }; +// Module-level internal functions for mypyc primitives (use thread-local RNG) + +static double +module_random_internal(void) { + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + abort(); + uint32_t r = chacha8_next(rng); + return r / 4294967296.0; +} + +static int64_t +module_randint_internal(int64_t a, int64_t b) { + if (unlikely(a > b)) { + PyErr_SetString(PyExc_ValueError, "empty range for randint()"); + return CPY_LL_INT_ERROR; + } + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + abort(); + uint64_t range = (uint64_t)(b - a) + 1; + uint32_t r = chacha8_next(rng); + return a + (int64_t)(r % range); +} + +static int64_t +module_randrange1_internal(int64_t stop) { + if (unlikely(stop <= 0)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + abort(); + uint32_t r = chacha8_next(rng); + return (int64_t)(r % (uint64_t)stop); +} + +static int64_t +module_randrange2_internal(int64_t start, int64_t stop) { + if (unlikely(start >= stop)) { + PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); + return CPY_LL_INT_ERROR; + } + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + abort(); + uint64_t range = (uint64_t)(stop - start); + uint32_t r = chacha8_next(rng); + return start + (int64_t)(r % range); +} + #ifdef MYPYC_EXPERIMENTAL static int @@ -694,6 +746,10 @@ librt_random_module_exec(PyObject *m) (void *)Random_randint_internal, (void *)Random_randrange1_internal, (void *)Random_randrange2_internal, + (void *)module_random_internal, + (void *)module_randint_internal, + (void *)module_randrange1_internal, + (void *)module_randrange2_internal, }; PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index b76c0cbbe3748..38ff4312fccfd 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -100,3 +100,48 @@ def rand(r): L0: r0 = LibRTRandom_Random_random_internal(r) return r0 + +[case testLibrtRandomModuleRandom_experimental_64bit] +from librt.random import random + +def module_random() -> float: + return random() +[out] +def module_random(): + r0 :: float +L0: + r0 = LibRTRandom_module_random_internal() + return r0 + +[case testLibrtRandomModuleRandint_experimental_64bit] +from librt.random import randint +from mypy_extensions import i64 + +def module_randint(a: i64, b: i64) -> i64: + return randint(a, b) +[out] +def module_randint(a, b): + a, b, r0 :: i64 +L0: + r0 = LibRTRandom_module_randint_internal(a, b) + return r0 + +[case testLibrtRandomModuleRandrange_experimental_64bit] +from librt.random import randrange +from mypy_extensions import i64 + +def module_randrange1(stop: i64) -> i64: + return randrange(stop) +def module_randrange2(start: i64, stop: i64) -> i64: + return randrange(start, stop) +[out] +def module_randrange1(stop): + stop, r0 :: i64 +L0: + r0 = LibRTRandom_module_randrange1_internal(stop) + return r0 +def module_randrange2(start, stop): + start, stop, r0 :: i64 +L0: + r0 = LibRTRandom_module_randrange2_internal(start, stop) + return r0 From 9d82d785941095639e1e7820fbf1080dd2f076b8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 14:34:05 +0000 Subject: [PATCH 16/34] Add more primitives --- mypyc/lib-rt/random/librt_random.c | 18 ++++++++++++++++ mypyc/test-data/irbuild-librt-random.test | 26 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 7b693add192b1..3a80792038e2b 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -706,6 +706,22 @@ module_randrange2_internal(int64_t start, int64_t stop) { return start + (int64_t)(r % range); } +static int32_t +module_randbits31_internal(void) { + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + abort(); + return randbits31_impl(rng); +} + +static int64_t +module_randbits62_internal(void) { + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + abort(); + return randbits62_impl(rng); +} + #ifdef MYPYC_EXPERIMENTAL static int @@ -750,6 +766,8 @@ librt_random_module_exec(PyObject *m) (void *)module_randint_internal, (void *)module_randrange1_internal, (void *)module_randrange2_internal, + (void *)module_randbits31_internal, + (void *)module_randbits62_internal, }; PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index 38ff4312fccfd..138b225b7d26e 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -145,3 +145,29 @@ def module_randrange2(start, stop): L0: r0 = LibRTRandom_module_randrange2_internal(start, stop) return r0 + +[case testLibrtRandomModuleRandbits31_experimental_64bit] +from librt.random import randbits31 +from mypy_extensions import i32 + +def module_randbits31() -> i32: + return randbits31() +[out] +def module_randbits31(): + r0 :: i32 +L0: + r0 = LibRTRandom_module_randbits31_internal() + return r0 + +[case testLibrtRandomModuleRandbits62_experimental_64bit] +from librt.random import randbits62 +from mypy_extensions import i64 + +def module_randbits62() -> i64: + return randbits62() +[out] +def module_randbits62(): + r0 :: i64 +L0: + r0 = LibRTRandom_module_randbits62_internal() + return r0 From 65109ce8e90a9b1835325df6d7852db32e570347 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 14:40:43 +0000 Subject: [PATCH 17/34] Handle larger ranges better and improve error handling --- mypyc/lib-rt/random/librt_random.c | 46 +++++++++++++++++------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 3a80792038e2b..3a4a16a8951b9 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -118,6 +118,19 @@ chacha8_next(chacha8_rng *rng) return rng->buf[rng->used++]; } +// Return a uniformly distributed random value in [0, range). +// Uses 32 bits for small ranges, 64 bits for large ranges to avoid modulo bias. +static inline uint64_t +chacha8_next_ranged(chacha8_rng *rng, uint64_t range) +{ + if (likely(range <= UINT32_MAX)) { + uint32_t r = chacha8_next(rng); + return r % range; + } + uint64_t r = ((uint64_t)chacha8_next(rng) << 32) | chacha8_next(rng); + return r % range; +} + static void chacha8_reset(chacha8_rng *rng) { @@ -297,8 +310,7 @@ static inline PyObject* randint_impl(chacha8_rng *rng, int64_t a, int64_t b) { uint64_t range = (uint64_t)(b - a) + 1; - uint32_t r = chacha8_next(rng); - int64_t result = a + (int64_t)(r % range); + int64_t result = a + (int64_t)chacha8_next_ranged(rng, range); return PyLong_FromLongLong(result); } @@ -499,8 +511,7 @@ Random_randrange1_internal(PyObject *self, int64_t stop) { PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); return CPY_LL_INT_ERROR; } - uint32_t r = chacha8_next(&((RandomObject *)self)->rng); - return (int64_t)(r % (uint64_t)stop); + return (int64_t)chacha8_next_ranged(&((RandomObject *)self)->rng, (uint64_t)stop); } static int64_t @@ -510,8 +521,7 @@ Random_randrange2_internal(PyObject *self, int64_t start, int64_t stop) { return CPY_LL_INT_ERROR; } uint64_t range = (uint64_t)(stop - start); - uint32_t r = chacha8_next(&((RandomObject *)self)->rng); - return start + (int64_t)(r % range); + return start + (int64_t)chacha8_next_ranged(&((RandomObject *)self)->rng, range); } static int64_t @@ -521,8 +531,7 @@ Random_randint_internal(PyObject *self, int64_t a, int64_t b) { return CPY_LL_INT_ERROR; } uint64_t range = (uint64_t)(b - a) + 1; - uint32_t r = chacha8_next(&((RandomObject *)self)->rng); - return a + (int64_t)(r % range); + return a + (int64_t)chacha8_next_ranged(&((RandomObject *)self)->rng, range); } static double @@ -660,7 +669,7 @@ static double module_random_internal(void) { chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - abort(); + CPyError_OutOfMemory(); uint32_t r = chacha8_next(rng); return r / 4294967296.0; } @@ -673,10 +682,9 @@ module_randint_internal(int64_t a, int64_t b) { } chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - abort(); + CPyError_OutOfMemory(); uint64_t range = (uint64_t)(b - a) + 1; - uint32_t r = chacha8_next(rng); - return a + (int64_t)(r % range); + return a + (int64_t)chacha8_next_ranged(rng, range); } static int64_t @@ -687,9 +695,8 @@ module_randrange1_internal(int64_t stop) { } chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - abort(); - uint32_t r = chacha8_next(rng); - return (int64_t)(r % (uint64_t)stop); + CPyError_OutOfMemory(); + return (int64_t)chacha8_next_ranged(rng, (uint64_t)stop); } static int64_t @@ -700,17 +707,16 @@ module_randrange2_internal(int64_t start, int64_t stop) { } chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - abort(); + CPyError_OutOfMemory(); uint64_t range = (uint64_t)(stop - start); - uint32_t r = chacha8_next(rng); - return start + (int64_t)(r % range); + return start + (int64_t)chacha8_next_ranged(rng, range); } static int32_t module_randbits31_internal(void) { chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - abort(); + CPyError_OutOfMemory(); return randbits31_impl(rng); } @@ -718,7 +724,7 @@ static int64_t module_randbits62_internal(void) { chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - abort(); + CPyError_OutOfMemory(); return randbits62_impl(rng); } From af12b54d29f8ffbaa85936bc7dddf2b382fa1a0f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 21 Mar 2026 15:37:03 +0000 Subject: [PATCH 18/34] Improve quality of random numbers --- mypyc/lib-rt/random/librt_random.c | 70 ++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 3a4a16a8951b9..b5aa45049a9bd 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -68,7 +68,7 @@ chacha8_block(const uint32_t seed[8], uint32_t counter, uint32_t out[16]) QUARTERROUND(out[3], out[4], out[ 9], out[14]); } - // Add original state back (non-invertible) + // Add original state back (standard ChaCha finalization) for (int i = 0; i < 16; i++) out[i] += s[i]; } @@ -118,17 +118,50 @@ chacha8_next(chacha8_rng *rng) return rng->buf[rng->used++]; } -// Return a uniformly distributed random value in [0, range). -// Uses 32 bits for small ranges, 64 bits for large ranges to avoid modulo bias. +// Return 64 bits of randomness (two consecutive 32-bit words, single bounds check). +static inline uint64_t +chacha8_next64(chacha8_rng *rng) +{ + // Need 2 words available; if fewer than 2, refill first. + if (unlikely(rng->used + 1 >= rng->n)) + // Use two separate calls to handle block boundary correctly. + return ((uint64_t)chacha8_next(rng) << 32) | chacha8_next(rng); + uint32_t hi = rng->buf[rng->used++]; + uint32_t lo = rng->buf[rng->used++]; + return ((uint64_t)hi << 32) | lo; +} + +// Return a uniformly distributed random value in [0, range) using +// Lemire's nearly divisionless method for perfect uniformity. +// Uses 32-bit multiply for small ranges, 64-bit for large ranges. static inline uint64_t chacha8_next_ranged(chacha8_rng *rng, uint64_t range) { if (likely(range <= UINT32_MAX)) { - uint32_t r = chacha8_next(rng); - return r % range; + // 32-bit Lemire: multiply r * range to get 64-bit product, + // upper 32 bits are the result in [0, range). + uint64_t m = (uint64_t)chacha8_next(rng) * range; + uint32_t lo = (uint32_t)m; + if (unlikely(lo < range)) { + uint32_t thresh = (uint32_t)(-(uint32_t)range) % (uint32_t)range; + while (lo < thresh) { + m = (uint64_t)chacha8_next(rng) * range; + lo = (uint32_t)m; + } + } + return m >> 32; + } + // 64-bit Lemire: use __uint128_t for the 128-bit product. + __uint128_t m = (__uint128_t)chacha8_next64(rng) * range; + uint64_t lo = (uint64_t)m; + if (unlikely(lo < range)) { + uint64_t thresh = (-range) % range; + while (lo < thresh) { + m = (__uint128_t)chacha8_next64(rng) * range; + lo = (uint64_t)m; + } } - uint64_t r = ((uint64_t)chacha8_next(rng) << 32) | chacha8_next(rng); - return r % range; + return (uint64_t)(m >> 64); } static void @@ -256,6 +289,14 @@ get_thread_rng(void) return rng; } +// Return a random double in [0.0, 1.0) with 53 bits of mantissa precision. +static inline double +random_double_impl(chacha8_rng *rng) +{ + uint64_t r = chacha8_next64(rng); + return (double)(r >> 11) * (1.0 / 9007199254740992.0); // 1/2^53 +} + // // Module-level random() and randint() // @@ -266,9 +307,7 @@ module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) chacha8_rng *rng = get_thread_rng(); if (rng == NULL) return NULL; - uint32_t r = chacha8_next(rng); - double result = r / 4294967296.0; // 2^32 - return PyFloat_FromDouble(result); + return PyFloat_FromDouble(random_double_impl(rng)); } // Return a random non-negative 31-bit integer [0, 2^31). @@ -536,8 +575,7 @@ Random_randint_internal(PyObject *self, int64_t a, int64_t b) { static double Random_random_internal(PyObject *self) { - uint32_t r = chacha8_next(&((RandomObject *)self)->rng); - return r / 4294967296.0; + return random_double_impl(&((RandomObject *)self)->rng); } static PyObject* @@ -575,10 +613,7 @@ Random_randrange(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { static PyObject* Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { - uint32_t r = chacha8_next(&self->rng); - // Scale to [0.0, 1.0) - double result = r / 4294967296.0; // 2^32 - return PyFloat_FromDouble(result); + return PyFloat_FromDouble(random_double_impl(&self->rng)); } static PyObject* @@ -670,8 +705,7 @@ module_random_internal(void) { chacha8_rng *rng = get_thread_rng(); if (rng == NULL) CPyError_OutOfMemory(); - uint32_t r = chacha8_next(rng); - return r / 4294967296.0; + return random_double_impl(rng); } static int64_t From 9703fdb38d8d55d40749d7202c7e1abcfa4955cd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Apr 2026 11:12:56 +0100 Subject: [PATCH 19/34] Add missing file --- mypyc/lib-rt/random/librt_random.h | 80 ++++++++++++++ mypyc/primitives/librt_random_ops.py | 158 +++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 mypyc/lib-rt/random/librt_random.h create mode 100644 mypyc/primitives/librt_random_ops.py diff --git a/mypyc/lib-rt/random/librt_random.h b/mypyc/lib-rt/random/librt_random.h new file mode 100644 index 0000000000000..4aea9422fb96b --- /dev/null +++ b/mypyc/lib-rt/random/librt_random.h @@ -0,0 +1,80 @@ +#ifndef LIBRT_RANDOM_H +#define LIBRT_RANDOM_H + +#ifndef MYPYC_EXPERIMENTAL + +static int +import_librt_random(void) +{ + // All librt.random features are experimental for now, so don't set up the API here + return 0; +} + +#else // MYPYC_EXPERIMENTAL + +#include + +#define LIBRT_RANDOM_ABI_VERSION 1 +#define LIBRT_RANDOM_API_VERSION 9 +#define LIBRT_RANDOM_API_LEN 17 + +static void *LibRTRandom_API[LIBRT_RANDOM_API_LEN]; + +#define LibRTRandom_ABIVersion (*(int (*)(void)) LibRTRandom_API[0]) +#define LibRTRandom_APIVersion (*(int (*)(void)) LibRTRandom_API[1]) +#define LibRTRandom_Random_internal (*(PyObject* (*)(void)) LibRTRandom_API[2]) +#define LibRTRandom_Random_from_seed_internal (*(PyObject* (*)(int64_t)) LibRTRandom_API[3]) +#define LibRTRandom_Random_type_internal (*(PyTypeObject* (*)(void)) LibRTRandom_API[4]) +#define LibRTRandom_Random_randbits62_internal (*(int64_t (*)(PyObject*)) LibRTRandom_API[5]) +#define LibRTRandom_Random_random_internal (*(double (*)(PyObject*)) LibRTRandom_API[6]) +#define LibRTRandom_Random_randbits31_internal (*(int32_t (*)(PyObject*)) LibRTRandom_API[7]) +#define LibRTRandom_Random_randint_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[8]) +#define LibRTRandom_Random_randrange1_internal (*(int64_t (*)(PyObject*, int64_t)) LibRTRandom_API[9]) +#define LibRTRandom_Random_randrange2_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[10]) +#define LibRTRandom_module_random_internal (*(double (*)(void)) LibRTRandom_API[11]) +#define LibRTRandom_module_randint_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[12]) +#define LibRTRandom_module_randrange1_internal (*(int64_t (*)(int64_t)) LibRTRandom_API[13]) +#define LibRTRandom_module_randrange2_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[14]) +#define LibRTRandom_module_randbits31_internal (*(int32_t (*)(void)) LibRTRandom_API[15]) +#define LibRTRandom_module_randbits62_internal (*(int64_t (*)(void)) LibRTRandom_API[16]) + +static int +import_librt_random(void) +{ + PyObject *mod = PyImport_ImportModule("librt.random"); + if (mod == NULL) + return -1; + Py_DECREF(mod); // we import just for the side effect of making the below work. + void *capsule = PyCapsule_Import("librt.random._C_API", 0); + if (capsule == NULL) + return -1; + memcpy(LibRTRandom_API, capsule, sizeof(LibRTRandom_API)); + if (LibRTRandom_ABIVersion() != LIBRT_RANDOM_ABI_VERSION) { + char err[128]; + snprintf(err, sizeof(err), "ABI version conflict for librt.random, expected %d, found %d", + LIBRT_RANDOM_ABI_VERSION, + LibRTRandom_ABIVersion() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + if (LibRTRandom_APIVersion() < LIBRT_RANDOM_API_VERSION) { + char err[128]; + snprintf(err, sizeof(err), + "API version conflict for librt.random, expected %d or newer, found %d (hint: upgrade librt)", + LIBRT_RANDOM_API_VERSION, + LibRTRandom_APIVersion() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + return 0; +} + +static inline bool CPyRandom_Check(PyObject *obj) { + return Py_TYPE(obj) == LibRTRandom_Random_type_internal(); +} + +#endif // MYPYC_EXPERIMENTAL + +#endif // LIBRT_RANDOM_H diff --git a/mypyc/primitives/librt_random_ops.py b/mypyc/primitives/librt_random_ops.py new file mode 100644 index 0000000000000..a0ba63ccd2023 --- /dev/null +++ b/mypyc/primitives/librt_random_ops.py @@ -0,0 +1,158 @@ +from mypyc.ir.deps import LIBRT_RANDOM +from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER +from mypyc.ir.rtypes import float_rprimitive, int32_rprimitive, int64_rprimitive, random_rprimitive +from mypyc.primitives.registry import function_op, method_op + +# Random() -- construct with OS entropy +function_op( + name="librt.random.Random", + arg_types=[], + return_type=random_rprimitive, + c_function_name="LibRTRandom_Random_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Random(seed) -- construct with integer seed +function_op( + name="librt.random.Random", + arg_types=[int64_rprimitive], + return_type=random_rprimitive, + c_function_name="LibRTRandom_Random_from_seed_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Random.randint(a, b) -- return random integer in [a, b] +method_op( + name="randint", + arg_types=[random_rprimitive, int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_Random_randint_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Random.randrange(stop) -- return random integer in [0, stop) +method_op( + name="randrange", + arg_types=[random_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_Random_randrange1_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Random.randrange(start, stop) -- return random integer in [start, stop) +method_op( + name="randrange", + arg_types=[random_rprimitive, int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_Random_randrange2_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Random.randbits31() -- return random 31-bit integer as i32 +method_op( + name="randbits31", + arg_types=[random_rprimitive], + return_type=int32_rprimitive, + c_function_name="LibRTRandom_Random_randbits31_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Random.randbits62() -- return random 62-bit integer +method_op( + name="randbits62", + arg_types=[random_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_Random_randbits62_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Random.random() -- return random float in [0.0, 1.0) +method_op( + name="random", + arg_types=[random_rprimitive], + return_type=float_rprimitive, + c_function_name="LibRTRandom_Random_random_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Module-level random() -- return random float using thread-local RNG +function_op( + name="librt.random.random", + arg_types=[], + return_type=float_rprimitive, + c_function_name="LibRTRandom_module_random_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randrange(stop) -- return random integer using thread-local RNG +function_op( + name="librt.random.randrange", + arg_types=[int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_module_randrange1_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randrange(start, stop) -- return random integer using thread-local RNG +function_op( + name="librt.random.randrange", + arg_types=[int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_module_randrange2_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randbits31() -- return random 31-bit integer using thread-local RNG +function_op( + name="librt.random.randbits31", + arg_types=[], + return_type=int32_rprimitive, + c_function_name="LibRTRandom_module_randbits31_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randbits62() -- return random 62-bit integer using thread-local RNG +function_op( + name="librt.random.randbits62", + arg_types=[], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_module_randbits62_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_RANDOM], +) + +# Module-level randint(a, b) -- return random integer using thread-local RNG +function_op( + name="librt.random.randint", + arg_types=[int64_rprimitive, int64_rprimitive], + return_type=int64_rprimitive, + c_function_name="LibRTRandom_module_randint_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_RANDOM], +) From 52d140612e0b0aa8c83efa67e6d001a858119c81 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 13:34:54 +0100 Subject: [PATCH 20/34] Update api definition to match current approach --- mypyc/build.py | 7 ++- mypyc/lib-rt/random/librt_random.h | 70 -------------------------- mypyc/lib-rt/random/librt_random_api.c | 48 ++++++++++++++++++ mypyc/lib-rt/random/librt_random_api.h | 40 +++++++++++++++ 4 files changed, 94 insertions(+), 71 deletions(-) create mode 100644 mypyc/lib-rt/random/librt_random_api.c create mode 100644 mypyc/lib-rt/random/librt_random_api.h diff --git a/mypyc/build.py b/mypyc/build.py index 6e31f47edda13..6abe0504c963b 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -121,7 +121,12 @@ class ModDesc(NamedTuple): ["vecs"], ), ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []), - ModDesc("librt.random", ["random/librt_random.c"], [], ["random"]), + ModDesc( + "librt.random", + ["random/librt_random.c"], + ["random/librt_random.h", "random/librt_random_api.h", "random/librt_random_api.c"], + ["random"], + ), ] try: diff --git a/mypyc/lib-rt/random/librt_random.h b/mypyc/lib-rt/random/librt_random.h index 4aea9422fb96b..abb8df06edbc8 100644 --- a/mypyc/lib-rt/random/librt_random.h +++ b/mypyc/lib-rt/random/librt_random.h @@ -1,80 +1,10 @@ #ifndef LIBRT_RANDOM_H #define LIBRT_RANDOM_H -#ifndef MYPYC_EXPERIMENTAL - -static int -import_librt_random(void) -{ - // All librt.random features are experimental for now, so don't set up the API here - return 0; -} - -#else // MYPYC_EXPERIMENTAL - #include #define LIBRT_RANDOM_ABI_VERSION 1 #define LIBRT_RANDOM_API_VERSION 9 #define LIBRT_RANDOM_API_LEN 17 -static void *LibRTRandom_API[LIBRT_RANDOM_API_LEN]; - -#define LibRTRandom_ABIVersion (*(int (*)(void)) LibRTRandom_API[0]) -#define LibRTRandom_APIVersion (*(int (*)(void)) LibRTRandom_API[1]) -#define LibRTRandom_Random_internal (*(PyObject* (*)(void)) LibRTRandom_API[2]) -#define LibRTRandom_Random_from_seed_internal (*(PyObject* (*)(int64_t)) LibRTRandom_API[3]) -#define LibRTRandom_Random_type_internal (*(PyTypeObject* (*)(void)) LibRTRandom_API[4]) -#define LibRTRandom_Random_randbits62_internal (*(int64_t (*)(PyObject*)) LibRTRandom_API[5]) -#define LibRTRandom_Random_random_internal (*(double (*)(PyObject*)) LibRTRandom_API[6]) -#define LibRTRandom_Random_randbits31_internal (*(int32_t (*)(PyObject*)) LibRTRandom_API[7]) -#define LibRTRandom_Random_randint_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[8]) -#define LibRTRandom_Random_randrange1_internal (*(int64_t (*)(PyObject*, int64_t)) LibRTRandom_API[9]) -#define LibRTRandom_Random_randrange2_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[10]) -#define LibRTRandom_module_random_internal (*(double (*)(void)) LibRTRandom_API[11]) -#define LibRTRandom_module_randint_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[12]) -#define LibRTRandom_module_randrange1_internal (*(int64_t (*)(int64_t)) LibRTRandom_API[13]) -#define LibRTRandom_module_randrange2_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[14]) -#define LibRTRandom_module_randbits31_internal (*(int32_t (*)(void)) LibRTRandom_API[15]) -#define LibRTRandom_module_randbits62_internal (*(int64_t (*)(void)) LibRTRandom_API[16]) - -static int -import_librt_random(void) -{ - PyObject *mod = PyImport_ImportModule("librt.random"); - if (mod == NULL) - return -1; - Py_DECREF(mod); // we import just for the side effect of making the below work. - void *capsule = PyCapsule_Import("librt.random._C_API", 0); - if (capsule == NULL) - return -1; - memcpy(LibRTRandom_API, capsule, sizeof(LibRTRandom_API)); - if (LibRTRandom_ABIVersion() != LIBRT_RANDOM_ABI_VERSION) { - char err[128]; - snprintf(err, sizeof(err), "ABI version conflict for librt.random, expected %d, found %d", - LIBRT_RANDOM_ABI_VERSION, - LibRTRandom_ABIVersion() - ); - PyErr_SetString(PyExc_ValueError, err); - return -1; - } - if (LibRTRandom_APIVersion() < LIBRT_RANDOM_API_VERSION) { - char err[128]; - snprintf(err, sizeof(err), - "API version conflict for librt.random, expected %d or newer, found %d (hint: upgrade librt)", - LIBRT_RANDOM_API_VERSION, - LibRTRandom_APIVersion() - ); - PyErr_SetString(PyExc_ValueError, err); - return -1; - } - return 0; -} - -static inline bool CPyRandom_Check(PyObject *obj) { - return Py_TYPE(obj) == LibRTRandom_Random_type_internal(); -} - -#endif // MYPYC_EXPERIMENTAL - #endif // LIBRT_RANDOM_H diff --git a/mypyc/lib-rt/random/librt_random_api.c b/mypyc/lib-rt/random/librt_random_api.c new file mode 100644 index 0000000000000..d418ef1ad6ae7 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random_api.c @@ -0,0 +1,48 @@ +#include "librt_random_api.h" + +void *LibRTRandom_API[LIBRT_RANDOM_API_LEN] = {0}; + +int +import_librt_random(void) +{ +#ifndef MYPYC_EXPERIMENTAL + // All librt.random features are experimental for now, so don't set up the API here. + return 0; +#else + PyObject *mod = PyImport_ImportModule("librt.random"); + if (mod == NULL) + return -1; + Py_DECREF(mod); // we import just for the side effect of making the below work. + void **capsule = (void **)PyCapsule_Import("librt.random._C_API", 0); + if (capsule == NULL) + return -1; + + // Only after version validation succeeds can we safely copy the full table. + int (*abi_version)(void) = (int (*)(void))capsule[0]; + int (*api_version)(void) = (int (*)(void))capsule[1]; + if (abi_version() != LIBRT_RANDOM_ABI_VERSION) { + char err[128]; + snprintf(err, sizeof(err), "ABI version conflict for librt.random, expected %d, found %d", + LIBRT_RANDOM_ABI_VERSION, + abi_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + if (api_version() < LIBRT_RANDOM_API_VERSION) { + char err[128]; + snprintf(err, sizeof(err), + "API version conflict for librt.random, expected %d or newer, found %d (hint: upgrade librt)", + LIBRT_RANDOM_API_VERSION, + api_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + // Provider API version is >= our expected version, which (by the API + // compatibility contract) means it has at least LIBRT_RANDOM_API_LEN + // entries, so this copy is safe. + memcpy(LibRTRandom_API, capsule, sizeof(LibRTRandom_API)); + return 0; +#endif +} diff --git a/mypyc/lib-rt/random/librt_random_api.h b/mypyc/lib-rt/random/librt_random_api.h new file mode 100644 index 0000000000000..1a90275daddc2 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random_api.h @@ -0,0 +1,40 @@ +#ifndef LIBRT_RANDOM_API_H +#define LIBRT_RANDOM_API_H + +#include +#include +#include +#include "librt_random.h" + +int +import_librt_random(void); + +#ifdef MYPYC_EXPERIMENTAL + +extern void *LibRTRandom_API[LIBRT_RANDOM_API_LEN]; + +#define LibRTRandom_ABIVersion (*(int (*)(void)) LibRTRandom_API[0]) +#define LibRTRandom_APIVersion (*(int (*)(void)) LibRTRandom_API[1]) +#define LibRTRandom_Random_internal (*(PyObject* (*)(void)) LibRTRandom_API[2]) +#define LibRTRandom_Random_from_seed_internal (*(PyObject* (*)(int64_t)) LibRTRandom_API[3]) +#define LibRTRandom_Random_type_internal (*(PyTypeObject* (*)(void)) LibRTRandom_API[4]) +#define LibRTRandom_Random_randbits62_internal (*(int64_t (*)(PyObject*)) LibRTRandom_API[5]) +#define LibRTRandom_Random_random_internal (*(double (*)(PyObject*)) LibRTRandom_API[6]) +#define LibRTRandom_Random_randbits31_internal (*(int32_t (*)(PyObject*)) LibRTRandom_API[7]) +#define LibRTRandom_Random_randint_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[8]) +#define LibRTRandom_Random_randrange1_internal (*(int64_t (*)(PyObject*, int64_t)) LibRTRandom_API[9]) +#define LibRTRandom_Random_randrange2_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[10]) +#define LibRTRandom_module_random_internal (*(double (*)(void)) LibRTRandom_API[11]) +#define LibRTRandom_module_randint_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[12]) +#define LibRTRandom_module_randrange1_internal (*(int64_t (*)(int64_t)) LibRTRandom_API[13]) +#define LibRTRandom_module_randrange2_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[14]) +#define LibRTRandom_module_randbits31_internal (*(int32_t (*)(void)) LibRTRandom_API[15]) +#define LibRTRandom_module_randbits62_internal (*(int64_t (*)(void)) LibRTRandom_API[16]) + +static inline bool CPyRandom_Check(PyObject *obj) { + return Py_TYPE(obj) == LibRTRandom_Random_type_internal(); +} + +#endif // MYPYC_EXPERIMENTAL + +#endif // LIBRT_RANDOM_API_H From cbe4c4b2b53832ede4bc12b76b773a7c84fc99c3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 13:45:15 +0100 Subject: [PATCH 21/34] Drop randbits31 and ranbits62 as low value --- mypy/typeshed/stubs/librt/librt/random.pyi | 6 +- mypyc/lib-rt/random/librt_random.c | 86 ---------------------- mypyc/lib-rt/random/librt_random.h | 2 +- mypyc/lib-rt/random/librt_random_api.h | 20 ++--- mypyc/primitives/librt_random_ops.py | 46 +----------- mypyc/test-data/irbuild-librt-random.test | 54 -------------- mypyc/test-data/run-librt-random.test | 33 +-------- 7 files changed, 12 insertions(+), 235 deletions(-) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index 3fc1b3652ac0c..0268223c5590f 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -1,6 +1,6 @@ from typing import overload -from mypy_extensions import i32, i64 +from mypy_extensions import i64 def random() -> float: ... def randint(a: i64, b: i64) -> i64: ... @@ -8,8 +8,6 @@ def randint(a: i64, b: i64) -> i64: ... def randrange(stop: i64, /) -> i64: ... @overload def randrange(start: i64, stop: i64, /) -> i64: ... -def randbits31() -> i32: ... -def randbits62() -> i64: ... def seed(n: i64, /) -> None: ... class Random: @@ -20,6 +18,4 @@ class Random: @overload def randrange(self, start: i64, stop: i64, /) -> i64: ... def random(self) -> float: ... - def randbits31(self) -> i32: ... - def randbits62(self) -> i64: ... def seed(self, n: i64, /) -> None: ... diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index b5aa45049a9bd..f54d2a88a08b6 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -310,40 +310,6 @@ module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) return PyFloat_FromDouble(random_double_impl(rng)); } -// Return a random non-negative 31-bit integer [0, 2^31). -static inline int32_t -randbits31_impl(chacha8_rng *rng) -{ - return (int32_t)(chacha8_next(rng) >> 1); -} - -// Return a random non-negative 62-bit integer (fits in a tagged integer). -static inline int64_t -randbits62_impl(chacha8_rng *rng) -{ - uint32_t lo = chacha8_next(rng); - uint32_t hi = chacha8_next(rng); - return (int64_t)(((uint64_t)hi << 30) | (lo >> 2)); -} - -static PyObject* -module_randbits31(PyObject *module, PyObject *Py_UNUSED(ignored)) -{ - chacha8_rng *rng = get_thread_rng(); - if (rng == NULL) - return NULL; - return PyLong_FromLong(randbits31_impl(rng)); -} - -static PyObject* -module_randbits62(PyObject *module, PyObject *Py_UNUSED(ignored)) -{ - chacha8_rng *rng = get_thread_rng(); - if (rng == NULL) - return NULL; - return PyLong_FromLongLong(randbits62_impl(rng)); -} - // Generate random integer in [a, b] using the given RNG. static inline PyObject* randint_impl(chacha8_rng *rng, int64_t a, int64_t b) @@ -534,16 +500,6 @@ Random_type_internal(void) { return &RandomType; } -static int64_t -Random_randbits62_internal(PyObject *self) { - return randbits62_impl(&((RandomObject *)self)->rng); -} - -static int32_t -Random_randbits31_internal(PyObject *self) { - return randbits31_impl(&((RandomObject *)self)->rng); -} - static int64_t Random_randrange1_internal(PyObject *self, int64_t stop) { if (unlikely(stop <= 0)) { @@ -616,16 +572,6 @@ Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { return PyFloat_FromDouble(random_double_impl(&self->rng)); } -static PyObject* -Random_randbits31(RandomObject *self, PyObject *Py_UNUSED(ignored)) { - return PyLong_FromLong(randbits31_impl(&self->rng)); -} - -static PyObject* -Random_randbits62(RandomObject *self, PyObject *Py_UNUSED(ignored)) { - return PyLong_FromLongLong(randbits62_impl(&self->rng)); -} - static PyObject* Random_seed(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { if (nargs != 1) { @@ -650,12 +596,6 @@ static PyMethodDef Random_methods[] = { {"random", (PyCFunction) Random_random, METH_NOARGS, PyDoc_STR("Return random float in [0.0, 1.0).") }, - {"randbits31", (PyCFunction) Random_randbits31, METH_NOARGS, - PyDoc_STR("Return random non-negative 31-bit integer.") - }, - {"randbits62", (PyCFunction) Random_randbits62, METH_NOARGS, - PyDoc_STR("Return random non-negative 62-bit integer.") - }, {"seed", (PyCFunction) Random_seed, METH_FASTCALL, PyDoc_STR("Seed the random number generator with an integer.") }, @@ -686,12 +626,6 @@ static PyMethodDef librt_random_module_methods[] = { {"randrange", (PyCFunction) module_randrange, METH_FASTCALL, PyDoc_STR("Return random integer in range [start, stop) using thread-local RNG.") }, - {"randbits31", (PyCFunction) module_randbits31, METH_NOARGS, - PyDoc_STR("Return random non-negative 31-bit integer using thread-local RNG.") - }, - {"randbits62", (PyCFunction) module_randbits62, METH_NOARGS, - PyDoc_STR("Return random non-negative 62-bit integer using thread-local RNG.") - }, {"seed", (PyCFunction) module_seed, METH_FASTCALL, PyDoc_STR("Seed the thread-local RNG with an integer.") }, @@ -746,22 +680,6 @@ module_randrange2_internal(int64_t start, int64_t stop) { return start + (int64_t)chacha8_next_ranged(rng, range); } -static int32_t -module_randbits31_internal(void) { - chacha8_rng *rng = get_thread_rng(); - if (rng == NULL) - CPyError_OutOfMemory(); - return randbits31_impl(rng); -} - -static int64_t -module_randbits62_internal(void) { - chacha8_rng *rng = get_thread_rng(); - if (rng == NULL) - CPyError_OutOfMemory(); - return randbits62_impl(rng); -} - #ifdef MYPYC_EXPERIMENTAL static int @@ -796,9 +714,7 @@ librt_random_module_exec(PyObject *m) (void *)Random_internal, (void *)Random_from_seed_internal, (void *)Random_type_internal, - (void *)Random_randbits62_internal, (void *)Random_random_internal, - (void *)Random_randbits31_internal, (void *)Random_randint_internal, (void *)Random_randrange1_internal, (void *)Random_randrange2_internal, @@ -806,8 +722,6 @@ librt_random_module_exec(PyObject *m) (void *)module_randint_internal, (void *)module_randrange1_internal, (void *)module_randrange2_internal, - (void *)module_randbits31_internal, - (void *)module_randbits62_internal, }; PyObject *c_api_object = PyCapsule_New((void *)librt_random_api, "librt.random._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/lib-rt/random/librt_random.h b/mypyc/lib-rt/random/librt_random.h index abb8df06edbc8..2eabfbd021bc9 100644 --- a/mypyc/lib-rt/random/librt_random.h +++ b/mypyc/lib-rt/random/librt_random.h @@ -5,6 +5,6 @@ #define LIBRT_RANDOM_ABI_VERSION 1 #define LIBRT_RANDOM_API_VERSION 9 -#define LIBRT_RANDOM_API_LEN 17 +#define LIBRT_RANDOM_API_LEN 13 #endif // LIBRT_RANDOM_H diff --git a/mypyc/lib-rt/random/librt_random_api.h b/mypyc/lib-rt/random/librt_random_api.h index 1a90275daddc2..f85f4bd2efcc0 100644 --- a/mypyc/lib-rt/random/librt_random_api.h +++ b/mypyc/lib-rt/random/librt_random_api.h @@ -18,18 +18,14 @@ extern void *LibRTRandom_API[LIBRT_RANDOM_API_LEN]; #define LibRTRandom_Random_internal (*(PyObject* (*)(void)) LibRTRandom_API[2]) #define LibRTRandom_Random_from_seed_internal (*(PyObject* (*)(int64_t)) LibRTRandom_API[3]) #define LibRTRandom_Random_type_internal (*(PyTypeObject* (*)(void)) LibRTRandom_API[4]) -#define LibRTRandom_Random_randbits62_internal (*(int64_t (*)(PyObject*)) LibRTRandom_API[5]) -#define LibRTRandom_Random_random_internal (*(double (*)(PyObject*)) LibRTRandom_API[6]) -#define LibRTRandom_Random_randbits31_internal (*(int32_t (*)(PyObject*)) LibRTRandom_API[7]) -#define LibRTRandom_Random_randint_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[8]) -#define LibRTRandom_Random_randrange1_internal (*(int64_t (*)(PyObject*, int64_t)) LibRTRandom_API[9]) -#define LibRTRandom_Random_randrange2_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[10]) -#define LibRTRandom_module_random_internal (*(double (*)(void)) LibRTRandom_API[11]) -#define LibRTRandom_module_randint_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[12]) -#define LibRTRandom_module_randrange1_internal (*(int64_t (*)(int64_t)) LibRTRandom_API[13]) -#define LibRTRandom_module_randrange2_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[14]) -#define LibRTRandom_module_randbits31_internal (*(int32_t (*)(void)) LibRTRandom_API[15]) -#define LibRTRandom_module_randbits62_internal (*(int64_t (*)(void)) LibRTRandom_API[16]) +#define LibRTRandom_Random_random_internal (*(double (*)(PyObject*)) LibRTRandom_API[5]) +#define LibRTRandom_Random_randint_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[6]) +#define LibRTRandom_Random_randrange1_internal (*(int64_t (*)(PyObject*, int64_t)) LibRTRandom_API[7]) +#define LibRTRandom_Random_randrange2_internal (*(int64_t (*)(PyObject*, int64_t, int64_t)) LibRTRandom_API[8]) +#define LibRTRandom_module_random_internal (*(double (*)(void)) LibRTRandom_API[9]) +#define LibRTRandom_module_randint_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[10]) +#define LibRTRandom_module_randrange1_internal (*(int64_t (*)(int64_t)) LibRTRandom_API[11]) +#define LibRTRandom_module_randrange2_internal (*(int64_t (*)(int64_t, int64_t)) LibRTRandom_API[12]) static inline bool CPyRandom_Check(PyObject *obj) { return Py_TYPE(obj) == LibRTRandom_Random_type_internal(); diff --git a/mypyc/primitives/librt_random_ops.py b/mypyc/primitives/librt_random_ops.py index a0ba63ccd2023..a445a7c284de6 100644 --- a/mypyc/primitives/librt_random_ops.py +++ b/mypyc/primitives/librt_random_ops.py @@ -1,6 +1,6 @@ from mypyc.ir.deps import LIBRT_RANDOM from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER -from mypyc.ir.rtypes import float_rprimitive, int32_rprimitive, int64_rprimitive, random_rprimitive +from mypyc.ir.rtypes import float_rprimitive, int64_rprimitive, random_rprimitive from mypyc.primitives.registry import function_op, method_op # Random() -- construct with OS entropy @@ -58,28 +58,6 @@ dependencies=[LIBRT_RANDOM], ) -# Random.randbits31() -- return random 31-bit integer as i32 -method_op( - name="randbits31", - arg_types=[random_rprimitive], - return_type=int32_rprimitive, - c_function_name="LibRTRandom_Random_randbits31_internal", - error_kind=ERR_NEVER, - experimental=True, - dependencies=[LIBRT_RANDOM], -) - -# Random.randbits62() -- return random 62-bit integer -method_op( - name="randbits62", - arg_types=[random_rprimitive], - return_type=int64_rprimitive, - c_function_name="LibRTRandom_Random_randbits62_internal", - error_kind=ERR_NEVER, - experimental=True, - dependencies=[LIBRT_RANDOM], -) - # Random.random() -- return random float in [0.0, 1.0) method_op( name="random", @@ -124,28 +102,6 @@ dependencies=[LIBRT_RANDOM], ) -# Module-level randbits31() -- return random 31-bit integer using thread-local RNG -function_op( - name="librt.random.randbits31", - arg_types=[], - return_type=int32_rprimitive, - c_function_name="LibRTRandom_module_randbits31_internal", - error_kind=ERR_NEVER, - experimental=True, - dependencies=[LIBRT_RANDOM], -) - -# Module-level randbits62() -- return random 62-bit integer using thread-local RNG -function_op( - name="librt.random.randbits62", - arg_types=[], - return_type=int64_rprimitive, - c_function_name="LibRTRandom_module_randbits62_internal", - error_kind=ERR_NEVER, - experimental=True, - dependencies=[LIBRT_RANDOM], -) - # Module-level randint(a, b) -- return random integer using thread-local RNG function_op( name="librt.random.randint", diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index 138b225b7d26e..9bb175170f494 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -60,34 +60,6 @@ L0: r0 = LibRTRandom_Random_randint_internal(r, a, b) return r0 -[case testLibrtRandomRandbits31_experimental_64bit] -from librt.random import Random -from mypy_extensions import i32 - -def randbits31(r: Random) -> i32: - return r.randbits31() -[out] -def randbits31(r): - r :: librt.random.Random - r0 :: i32 -L0: - r0 = LibRTRandom_Random_randbits31_internal(r) - return r0 - -[case testLibrtRandomRandbits62_experimental_64bit] -from librt.random import Random -from mypy_extensions import i64 - -def randbits(r: Random) -> i64: - return r.randbits62() -[out] -def randbits(r): - r :: librt.random.Random - r0 :: i64 -L0: - r0 = LibRTRandom_Random_randbits62_internal(r) - return r0 - [case testLibrtRandomRandom_experimental_64bit] from librt.random import Random @@ -145,29 +117,3 @@ def module_randrange2(start, stop): L0: r0 = LibRTRandom_module_randrange2_internal(start, stop) return r0 - -[case testLibrtRandomModuleRandbits31_experimental_64bit] -from librt.random import randbits31 -from mypy_extensions import i32 - -def module_randbits31() -> i32: - return randbits31() -[out] -def module_randbits31(): - r0 :: i32 -L0: - r0 = LibRTRandom_module_randbits31_internal() - return r0 - -[case testLibrtRandomModuleRandbits62_experimental_64bit] -from librt.random import randbits62 -from mypy_extensions import i64 - -def module_randbits62() -> i64: - return randbits62() -[out] -def module_randbits62(): - r0 :: i64 -L0: - r0 = LibRTRandom_module_randbits62_internal() - return r0 diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 5e60ffcf20e3b..101029822914b 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -125,28 +125,8 @@ def test_seed_method_resets_state() -> None: actual = [r.randint(0, 1000000) for _ in range(20)] assert expected == actual -def test_randbits62_basic() -> None: - r = Random() - for i in range(100): - val = r.randbits62() - assert 0 <= val < (1 << 62) - -def test_randbits62_produces_different_values() -> None: - r = Random() - values = set() - for i in range(100): - values.add(r.randbits62()) - assert len(values) > 1 - -def test_randbits62_reproducible() -> None: - r1 = Random(42) - r2 = Random(42) - vals1 = [r1.randbits62() for _ in range(20)] - vals2 = [r2.randbits62() for _ in range(20)] - assert vals1 == vals2 - [case testRandomModuleLevel_librt_experimental] -from librt.random import random, randint, randrange, randbits62, seed +from librt.random import random, randint, randrange, seed def test_module_random_basic() -> None: for i in range(100): @@ -193,17 +173,6 @@ def test_module_randrange_produces_different_values() -> None: values.add(randrange(1000000)) assert len(values) > 1 -def test_module_randbits62_basic() -> None: - for i in range(100): - val = randbits62() - assert 0 <= val < (1 << 62) - -def test_module_randbits62_produces_different_values() -> None: - values = set() - for i in range(100): - values.add(randbits62()) - assert len(values) > 1 - def test_module_seed_reproducible() -> None: seed(42) vals1 = [randint(0, 1000000) for _ in range(20)] From 4841850b786dcf2a20ba484cf410add567cccdd6 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 14:01:57 +0100 Subject: [PATCH 22/34] Add missing #include --- mypyc/lib-rt/random/librt_random.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index f54d2a88a08b6..aa74c9ab68908 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -1,3 +1,5 @@ +#include "pythoncapi_compat.h" + #define PY_SSIZE_T_CLEAN #include #include From d6a3e5b9bc4a83dbde660f9bec131874852ee13f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 14:05:45 +0100 Subject: [PATCH 23/34] Fixes to large integer ranges --- mypyc/lib-rt/random/librt_random.c | 30 +++++++---- mypyc/test-data/run-librt-random.test | 74 +++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index aa74c9ab68908..c5ee23436d13a 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -166,6 +166,15 @@ chacha8_next_ranged(chacha8_rng *rng, uint64_t range) return (uint64_t)(m >> 64); } +// Return a random i64 starting at 'start', with 'range' possible values. +// A zero range represents the full 2**64 i64 domain. +static inline int64_t +random_i64_from_range(chacha8_rng *rng, int64_t start, uint64_t range) +{ + uint64_t offset = range == 0 ? chacha8_next64(rng) : chacha8_next_ranged(rng, range); + return (int64_t)((uint64_t)start + offset); +} + static void chacha8_reset(chacha8_rng *rng) { @@ -316,9 +325,8 @@ module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) static inline PyObject* randint_impl(chacha8_rng *rng, int64_t a, int64_t b) { - uint64_t range = (uint64_t)(b - a) + 1; - int64_t result = a + (int64_t)chacha8_next_ranged(rng, range); - return PyLong_FromLongLong(result); + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return PyLong_FromLongLong(random_i64_from_range(rng, a, range)); } static PyObject* @@ -517,8 +525,8 @@ Random_randrange2_internal(PyObject *self, int64_t start, int64_t stop) { PyErr_SetString(PyExc_ValueError, "empty range for randrange()"); return CPY_LL_INT_ERROR; } - uint64_t range = (uint64_t)(stop - start); - return start + (int64_t)chacha8_next_ranged(&((RandomObject *)self)->rng, range); + uint64_t range = (uint64_t)stop - (uint64_t)start; + return random_i64_from_range(&((RandomObject *)self)->rng, start, range); } static int64_t @@ -527,8 +535,8 @@ Random_randint_internal(PyObject *self, int64_t a, int64_t b) { PyErr_SetString(PyExc_ValueError, "empty range for randint()"); return CPY_LL_INT_ERROR; } - uint64_t range = (uint64_t)(b - a) + 1; - return a + (int64_t)chacha8_next_ranged(&((RandomObject *)self)->rng, range); + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return random_i64_from_range(&((RandomObject *)self)->rng, a, range); } static double @@ -653,8 +661,8 @@ module_randint_internal(int64_t a, int64_t b) { chacha8_rng *rng = get_thread_rng(); if (rng == NULL) CPyError_OutOfMemory(); - uint64_t range = (uint64_t)(b - a) + 1; - return a + (int64_t)chacha8_next_ranged(rng, range); + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return random_i64_from_range(rng, a, range); } static int64_t @@ -678,8 +686,8 @@ module_randrange2_internal(int64_t start, int64_t stop) { chacha8_rng *rng = get_thread_rng(); if (rng == NULL) CPyError_OutOfMemory(); - uint64_t range = (uint64_t)(stop - start); - return start + (int64_t)chacha8_next_ranged(rng, range); + uint64_t range = (uint64_t)stop - (uint64_t)start; + return random_i64_from_range(rng, start, range); } #ifdef MYPYC_EXPERIMENTAL diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 101029822914b..243dbde2d1098 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -187,6 +187,80 @@ def test_module_seed_different() -> None: vals2 = [randint(0, 1000000) for _ in range(20)] assert vals1 != vals2 +[case testRandomWideI64Ranges_librt_experimental] +from typing import Any + +from librt.random import Random, randint, randrange +from mypy_extensions import i64 + +def method_randint(r: Random, a: i64, b: i64) -> i64: + return r.randint(a, b) + +def method_randrange(r: Random, a: i64, b: i64) -> i64: + return r.randrange(a, b) + +def module_randint(a: i64, b: i64) -> i64: + return randint(a, b) + +def module_randrange(a: i64, b: i64) -> i64: + return randrange(a, b) + +def test_full_i64_randint_native() -> None: + lo: i64 = -9223372036854775808 + hi: i64 = 9223372036854775807 + r = Random(42) + saw_non_min = False + for i in range(20): + val = method_randint(r, lo, hi) + assert lo <= val <= hi + if val != lo: + saw_non_min = True + assert saw_non_min + +def test_full_i64_randint_module_native() -> None: + lo: i64 = -9223372036854775808 + hi: i64 = 9223372036854775807 + saw_non_min = False + for i in range(20): + val = module_randint(lo, hi) + assert lo <= val <= hi + if val != lo: + saw_non_min = True + assert saw_non_min + +def test_wide_i64_randrange_native() -> None: + lo: i64 = -9223372036854775808 + hi: i64 = 9223372036854775807 + r = Random(43) + for i in range(20): + val = method_randrange(r, lo, hi) + assert lo <= val < hi + val = module_randrange(lo, hi) + assert lo <= val < hi + +def test_full_i64_randint_python_api() -> None: + r: Any = Random(42) + lo = -9223372036854775808 + hi = 9223372036854775807 + saw_non_min = False + for i in range(20): + val = r.randint(lo, hi) + assert lo <= val <= hi + if val != lo: + saw_non_min = True + assert saw_non_min + +def test_wide_i64_randrange_python_api() -> None: + r: Any = Random(43) + randrange_any: Any = randrange + lo = -9223372036854775808 + hi = 9223372036854775807 + for i in range(20): + val = r.randrange(lo, hi) + assert lo <= val < hi + val = randrange_any(lo, hi) + assert lo <= val < hi + [case testRandomErrors_librt_experimental] from librt.random import Random, randint, randrange from testutil import assertRaises From 11b937f00e71c464e78bc787e54bc68714189ced Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 14:09:42 +0100 Subject: [PATCH 24/34] Make Random final --- mypy/typeshed/stubs/librt/librt/random.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi index 0268223c5590f..d1330aa56faf1 100644 --- a/mypy/typeshed/stubs/librt/librt/random.pyi +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -1,4 +1,4 @@ -from typing import overload +from typing import final, overload from mypy_extensions import i64 @@ -10,6 +10,7 @@ def randrange(stop: i64, /) -> i64: ... def randrange(start: i64, stop: i64, /) -> i64: ... def seed(n: i64, /) -> None: ... +@final class Random: def __init__(self, seed: i64 | None = None) -> None: ... def randint(self, a: i64, b: i64) -> i64: ... From 125bdee400c823f5e0cfd6bcdbe2a179dbbc0a1c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 14:15:08 +0100 Subject: [PATCH 25/34] Improve error handling --- mypyc/lib-rt/random/librt_random.c | 8 ++++---- mypyc/primitives/librt_random_ops.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index c5ee23436d13a..2c95ebd3760ea 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -648,7 +648,7 @@ static double module_random_internal(void) { chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - CPyError_OutOfMemory(); + return CPY_FLOAT_ERROR; return random_double_impl(rng); } @@ -660,7 +660,7 @@ module_randint_internal(int64_t a, int64_t b) { } chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - CPyError_OutOfMemory(); + return CPY_LL_INT_ERROR; uint64_t range = (uint64_t)b - (uint64_t)a + 1; return random_i64_from_range(rng, a, range); } @@ -673,7 +673,7 @@ module_randrange1_internal(int64_t stop) { } chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - CPyError_OutOfMemory(); + return CPY_LL_INT_ERROR; return (int64_t)chacha8_next_ranged(rng, (uint64_t)stop); } @@ -685,7 +685,7 @@ module_randrange2_internal(int64_t start, int64_t stop) { } chacha8_rng *rng = get_thread_rng(); if (rng == NULL) - CPyError_OutOfMemory(); + return CPY_LL_INT_ERROR; uint64_t range = (uint64_t)stop - (uint64_t)start; return random_i64_from_range(rng, start, range); } diff --git a/mypyc/primitives/librt_random_ops.py b/mypyc/primitives/librt_random_ops.py index a445a7c284de6..1345e7aad53a3 100644 --- a/mypyc/primitives/librt_random_ops.py +++ b/mypyc/primitives/librt_random_ops.py @@ -1,5 +1,5 @@ from mypyc.ir.deps import LIBRT_RANDOM -from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER +from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER from mypyc.ir.rtypes import float_rprimitive, int64_rprimitive, random_rprimitive from mypyc.primitives.registry import function_op, method_op @@ -75,7 +75,7 @@ arg_types=[], return_type=float_rprimitive, c_function_name="LibRTRandom_module_random_internal", - error_kind=ERR_NEVER, + error_kind=ERR_MAGIC_OVERLAPPING, experimental=True, dependencies=[LIBRT_RANDOM], ) From 0fde34c9290cd6498a36fa5bbec7b7c80f91770e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 14:43:07 +0100 Subject: [PATCH 26/34] Avoid the use of 128 bit integers for better portability --- mypyc/lib-rt/random/librt_random.c | 31 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 2c95ebd3760ea..d2adc364c23ff 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -133,12 +133,13 @@ chacha8_next64(chacha8_rng *rng) return ((uint64_t)hi << 32) | lo; } -// Return a uniformly distributed random value in [0, range) using -// Lemire's nearly divisionless method for perfect uniformity. -// Uses 32-bit multiply for small ranges, 64-bit for large ranges. +// Return a uniformly distributed random value in [0, range). +// Use Lemire's nearly divisionless method for small ranges, and a portable +// rejection sampler for larger ranges to avoid non-standard 128-bit arithmetic. static inline uint64_t chacha8_next_ranged(chacha8_rng *rng, uint64_t range) { + assert(range != 0); if (likely(range <= UINT32_MAX)) { // 32-bit Lemire: multiply r * range to get 64-bit product, // upper 32 bits are the result in [0, range). @@ -153,17 +154,19 @@ chacha8_next_ranged(chacha8_rng *rng, uint64_t range) } return m >> 32; } - // 64-bit Lemire: use __uint128_t for the 128-bit product. - __uint128_t m = (__uint128_t)chacha8_next64(rng) * range; - uint64_t lo = (uint64_t)m; - if (unlikely(lo < range)) { - uint64_t thresh = (-range) % range; - while (lo < thresh) { - m = (__uint128_t)chacha8_next64(rng) * range; - lo = (uint64_t)m; - } - } - return (uint64_t)(m >> 64); + // If range is a power of two, masking produces an unbiased result. + if ((range & (range - 1)) == 0) { + return chacha8_next64(rng) & (range - 1); + } + uint64_t r; + // In unsigned arithmetic, -range is 2**64 - range, so this computes + // 2**64 % range. Rejecting values below this threshold leaves exactly + // floor(2**64 / range) full buckets of size range, avoiding modulo bias. + uint64_t thresh = -range % range; + do { + r = chacha8_next64(rng); + } while (unlikely(r < thresh)); + return r % range; } // Return a random i64 starting at 'start', with 'range' possible values. From 2e2925efe53adcef7cc1fe9e9d3a8c18c353eb2f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 14:49:26 +0100 Subject: [PATCH 27/34] Minor fixes --- mypyc/lib-rt/random/librt_random_api.c | 2 ++ mypyc/primitives/librt_random_ops.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random_api.c b/mypyc/lib-rt/random/librt_random_api.c index d418ef1ad6ae7..db5974d027100 100644 --- a/mypyc/lib-rt/random/librt_random_api.c +++ b/mypyc/lib-rt/random/librt_random_api.c @@ -1,3 +1,5 @@ +#include + #include "librt_random_api.h" void *LibRTRandom_API[LIBRT_RANDOM_API_LEN] = {0}; diff --git a/mypyc/primitives/librt_random_ops.py b/mypyc/primitives/librt_random_ops.py index 1345e7aad53a3..fd9662a5e26da 100644 --- a/mypyc/primitives/librt_random_ops.py +++ b/mypyc/primitives/librt_random_ops.py @@ -1,5 +1,5 @@ from mypyc.ir.deps import LIBRT_RANDOM -from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER +from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER from mypyc.ir.rtypes import float_rprimitive, int64_rprimitive, random_rprimitive from mypyc.primitives.registry import function_op, method_op @@ -75,7 +75,7 @@ arg_types=[], return_type=float_rprimitive, c_function_name="LibRTRandom_module_random_internal", - error_kind=ERR_MAGIC_OVERLAPPING, + error_kind=ERR_MAGIC, experimental=True, dependencies=[LIBRT_RANDOM], ) From 062fa124ff59319d5de75c8de79065628cd54ee2 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 7 May 2026 14:56:11 +0100 Subject: [PATCH 28/34] Lint --- mypyc/ir/rtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 024417f52a987..db29f9e304d8d 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -547,7 +547,7 @@ def __hash__(self) -> int: } | { "librt.random.Random": RPrimitive( "librt.random.Random", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_RANDOM,) - ), + ) } bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"] From 99fb11497e3888ec6dfa18ddd459361158efa106 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 8 May 2026 10:24:06 +0100 Subject: [PATCH 29/34] Refactor tests --- mypyc/test-data/run-librt-random.test | 31 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 243dbde2d1098..fe3ab0ecc44cd 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -1,5 +1,13 @@ -[case testRandomBasic_librt_experimental] -from librt.random import Random +[case testRandom_librt_experimental] +from typing import Any + +from librt.random import Random, random, randint, randrange, seed +from mypy_extensions import i64 +from testutil import assertRaises + +# +# Random object basics +# def test_random_construct() -> None: r = Random() @@ -125,8 +133,9 @@ def test_seed_method_resets_state() -> None: actual = [r.randint(0, 1000000) for _ in range(20)] assert expected == actual -[case testRandomModuleLevel_librt_experimental] -from librt.random import random, randint, randrange, seed +# +# Module-level functions +# def test_module_random_basic() -> None: for i in range(100): @@ -187,11 +196,9 @@ def test_module_seed_different() -> None: vals2 = [randint(0, 1000000) for _ in range(20)] assert vals1 != vals2 -[case testRandomWideI64Ranges_librt_experimental] -from typing import Any - -from librt.random import Random, randint, randrange -from mypy_extensions import i64 +# +# Wide i64 ranges +# def method_randint(r: Random, a: i64, b: i64) -> i64: return r.randint(a, b) @@ -261,9 +268,9 @@ def test_wide_i64_randrange_python_api() -> None: val = randrange_any(lo, hi) assert lo <= val < hi -[case testRandomErrors_librt_experimental] -from librt.random import Random, randint, randrange -from testutil import assertRaises +# +# Error handling +# def test_randint_empty_range() -> None: r = Random() From 5a58dfc515859e8ddb2a8d2bb0e26c2b2b9957a6 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 8 May 2026 10:41:01 +0100 Subject: [PATCH 30/34] Test calls using Any --- mypyc/test-data/run-librt-random.test | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index fe3ab0ecc44cd..0c9107343e676 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -196,6 +196,37 @@ def test_module_seed_different() -> None: vals2 = [randint(0, 1000000) for _ in range(20)] assert vals1 != vals2 +# +# Wrapper function calling convention (via Any) +# + +def test_method_random_via_wrapper() -> None: + r: Any = Random(42) + val = r.random() + assert isinstance(val, float) + assert 0.0 <= val < 1.0 + +def test_method_seed_via_wrapper() -> None: + r: Any = Random(0) + r.seed(42) + val = r.random() + assert 0.0 <= val < 1.0 + +def test_module_random_via_wrapper() -> None: + random_any: Any = random + val = random_any() + assert isinstance(val, float) + assert 0.0 <= val < 1.0 + +def test_module_randint_via_wrapper() -> None: + randint_any: Any = randint + val = randint_any(0, 10) + assert 0 <= val <= 10 + +def test_module_seed_via_wrapper() -> None: + seed_any: Any = seed + seed_any(42) + # # Wide i64 ranges # From 17c2b17015c4043b75349b44642f8fe1fd7e55ae Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 8 May 2026 11:14:04 +0100 Subject: [PATCH 31/34] Make get_thread_rng inline function --- mypyc/lib-rt/random/librt_random.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index d2adc364c23ff..1b3ad36ab7755 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -274,7 +274,7 @@ ensure_tls_key(void) // Get the thread-local RNG, initializing on first use. // Returns NULL with Python exception set on failure. -static chacha8_rng * +static inline chacha8_rng * get_thread_rng(void) { chacha8_rng *rng = tls_rng; From 4137427c692522234c1f3ed9f3613a2175136a04 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 8 May 2026 11:32:42 +0100 Subject: [PATCH 32/34] Don't put it behind experimental feature flag --- mypyc/lib-rt/random/librt_random.c | 6 ------ mypyc/lib-rt/random/librt_random_api.c | 5 ----- mypyc/lib-rt/random/librt_random_api.h | 4 ---- mypyc/primitives/librt_random_ops.py | 10 ---------- mypyc/test-data/irbuild-librt-random.test | 16 ++++++++-------- mypyc/test-data/run-librt-random.test | 2 +- 6 files changed, 9 insertions(+), 34 deletions(-) diff --git a/mypyc/lib-rt/random/librt_random.c b/mypyc/lib-rt/random/librt_random.c index 1b3ad36ab7755..7dc590eaa5946 100644 --- a/mypyc/lib-rt/random/librt_random.c +++ b/mypyc/lib-rt/random/librt_random.c @@ -693,8 +693,6 @@ module_randrange2_internal(int64_t start, int64_t stop) { return random_i64_from_range(rng, start, range); } -#ifdef MYPYC_EXPERIMENTAL - static int random_abi_version(void) { return LIBRT_RANDOM_ABI_VERSION; @@ -705,8 +703,6 @@ random_api_version(void) { return LIBRT_RANDOM_API_VERSION; } -#endif - static int librt_random_module_exec(PyObject *m) { @@ -719,7 +715,6 @@ librt_random_module_exec(PyObject *m) if (PyModule_AddObjectRef(m, "Random", (PyObject *) &RandomType) < 0) { return -1; } -#ifdef MYPYC_EXPERIMENTAL // Export mypyc internal C API via capsule static void *librt_random_api[LIBRT_RANDOM_API_LEN] = { (void *)random_abi_version, @@ -740,7 +735,6 @@ librt_random_module_exec(PyObject *m) if (PyModule_Add(m, "_C_API", c_api_object) < 0) { return -1; } -#endif return 0; } diff --git a/mypyc/lib-rt/random/librt_random_api.c b/mypyc/lib-rt/random/librt_random_api.c index db5974d027100..157fa82b82eb3 100644 --- a/mypyc/lib-rt/random/librt_random_api.c +++ b/mypyc/lib-rt/random/librt_random_api.c @@ -7,10 +7,6 @@ void *LibRTRandom_API[LIBRT_RANDOM_API_LEN] = {0}; int import_librt_random(void) { -#ifndef MYPYC_EXPERIMENTAL - // All librt.random features are experimental for now, so don't set up the API here. - return 0; -#else PyObject *mod = PyImport_ImportModule("librt.random"); if (mod == NULL) return -1; @@ -46,5 +42,4 @@ import_librt_random(void) // entries, so this copy is safe. memcpy(LibRTRandom_API, capsule, sizeof(LibRTRandom_API)); return 0; -#endif } diff --git a/mypyc/lib-rt/random/librt_random_api.h b/mypyc/lib-rt/random/librt_random_api.h index f85f4bd2efcc0..2794de0dd7e58 100644 --- a/mypyc/lib-rt/random/librt_random_api.h +++ b/mypyc/lib-rt/random/librt_random_api.h @@ -9,8 +9,6 @@ int import_librt_random(void); -#ifdef MYPYC_EXPERIMENTAL - extern void *LibRTRandom_API[LIBRT_RANDOM_API_LEN]; #define LibRTRandom_ABIVersion (*(int (*)(void)) LibRTRandom_API[0]) @@ -31,6 +29,4 @@ static inline bool CPyRandom_Check(PyObject *obj) { return Py_TYPE(obj) == LibRTRandom_Random_type_internal(); } -#endif // MYPYC_EXPERIMENTAL - #endif // LIBRT_RANDOM_API_H diff --git a/mypyc/primitives/librt_random_ops.py b/mypyc/primitives/librt_random_ops.py index fd9662a5e26da..6aaee84ecd0d6 100644 --- a/mypyc/primitives/librt_random_ops.py +++ b/mypyc/primitives/librt_random_ops.py @@ -10,7 +10,6 @@ return_type=random_rprimitive, c_function_name="LibRTRandom_Random_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -21,7 +20,6 @@ return_type=random_rprimitive, c_function_name="LibRTRandom_Random_from_seed_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -32,7 +30,6 @@ return_type=int64_rprimitive, c_function_name="LibRTRandom_Random_randint_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -43,7 +40,6 @@ return_type=int64_rprimitive, c_function_name="LibRTRandom_Random_randrange1_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -54,7 +50,6 @@ return_type=int64_rprimitive, c_function_name="LibRTRandom_Random_randrange2_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -65,7 +60,6 @@ return_type=float_rprimitive, c_function_name="LibRTRandom_Random_random_internal", error_kind=ERR_NEVER, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -76,7 +70,6 @@ return_type=float_rprimitive, c_function_name="LibRTRandom_module_random_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -87,7 +80,6 @@ return_type=int64_rprimitive, c_function_name="LibRTRandom_module_randrange1_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -98,7 +90,6 @@ return_type=int64_rprimitive, c_function_name="LibRTRandom_module_randrange2_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) @@ -109,6 +100,5 @@ return_type=int64_rprimitive, c_function_name="LibRTRandom_module_randint_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_RANDOM], ) diff --git a/mypyc/test-data/irbuild-librt-random.test b/mypyc/test-data/irbuild-librt-random.test index 9bb175170f494..9215c13c88d6e 100644 --- a/mypyc/test-data/irbuild-librt-random.test +++ b/mypyc/test-data/irbuild-librt-random.test @@ -1,4 +1,4 @@ -[case testLibrtRandomConstructor_experimental_64bit] +[case testLibrtRandomConstructor_64bit] from librt.random import Random def make_random() -> Random: @@ -10,7 +10,7 @@ L0: r0 = LibRTRandom_Random_internal() return r0 -[case testLibrtRandomConstructorWithSeed_experimental_64bit] +[case testLibrtRandomConstructorWithSeed_64bit] from librt.random import Random from mypy_extensions import i64 @@ -24,7 +24,7 @@ L0: r0 = LibRTRandom_Random_from_seed_internal(n) return r0 -[case testLibrtRandomRandrange_experimental_64bit] +[case testLibrtRandomRandrange_64bit] from librt.random import Random from mypy_extensions import i64 @@ -46,7 +46,7 @@ L0: r0 = LibRTRandom_Random_randrange2_internal(r, start, stop) return r0 -[case testLibrtRandomRandint_experimental_64bit] +[case testLibrtRandomRandint_64bit] from librt.random import Random from mypy_extensions import i64 @@ -60,7 +60,7 @@ L0: r0 = LibRTRandom_Random_randint_internal(r, a, b) return r0 -[case testLibrtRandomRandom_experimental_64bit] +[case testLibrtRandomRandom_64bit] from librt.random import Random def rand(r: Random) -> float: @@ -73,7 +73,7 @@ L0: r0 = LibRTRandom_Random_random_internal(r) return r0 -[case testLibrtRandomModuleRandom_experimental_64bit] +[case testLibrtRandomModuleRandom_64bit] from librt.random import random def module_random() -> float: @@ -85,7 +85,7 @@ L0: r0 = LibRTRandom_module_random_internal() return r0 -[case testLibrtRandomModuleRandint_experimental_64bit] +[case testLibrtRandomModuleRandint_64bit] from librt.random import randint from mypy_extensions import i64 @@ -98,7 +98,7 @@ L0: r0 = LibRTRandom_module_randint_internal(a, b) return r0 -[case testLibrtRandomModuleRandrange_experimental_64bit] +[case testLibrtRandomModuleRandrange_64bit] from librt.random import randrange from mypy_extensions import i64 diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test index 0c9107343e676..0b34222678018 100644 --- a/mypyc/test-data/run-librt-random.test +++ b/mypyc/test-data/run-librt-random.test @@ -1,4 +1,4 @@ -[case testRandom_librt_experimental] +[case testRandom_librt] from typing import Any from librt.random import Random, random, randint, randrange, seed From bd432dbea30859682ae67c5f8c9b640e1d9cb0f9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 8 May 2026 13:36:42 +0100 Subject: [PATCH 33/34] Address code review --- mypyc/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/build.py b/mypyc/build.py index 6abe0504c963b..9b4e1d6ca557a 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -124,7 +124,7 @@ class ModDesc(NamedTuple): ModDesc( "librt.random", ["random/librt_random.c"], - ["random/librt_random.h", "random/librt_random_api.h", "random/librt_random_api.c"], + ["random/librt_random.h"], ["random"], ), ] From 0124255bcb4c2bbbf3486df5e5b9310d1f542155 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 12:39:04 +0000 Subject: [PATCH 34/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/build.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mypyc/build.py b/mypyc/build.py index 9b4e1d6ca557a..08eeb13c91752 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -121,12 +121,7 @@ class ModDesc(NamedTuple): ["vecs"], ), ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []), - ModDesc( - "librt.random", - ["random/librt_random.c"], - ["random/librt_random.h"], - ["random"], - ), + ModDesc("librt.random", ["random/librt_random.c"], ["random/librt_random.h"], ["random"]), ] try: