diff --git a/mypy/typeshed/stubs/librt/librt/random.pyi b/mypy/typeshed/stubs/librt/librt/random.pyi new file mode 100644 index 0000000000000..d1330aa56faf1 --- /dev/null +++ b/mypy/typeshed/stubs/librt/librt/random.pyi @@ -0,0 +1,22 @@ +from typing import final, overload + +from mypy_extensions import i64 + +def random() -> float: ... +def randint(a: i64, b: i64) -> i64: ... +@overload +def randrange(stop: i64, /) -> i64: ... +@overload +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: ... + @overload + def randrange(self, stop: i64, /) -> i64: ... + @overload + def randrange(self, start: i64, stop: i64, /) -> i64: ... + def random(self) -> float: ... + def seed(self, n: i64, /) -> None: ... diff --git a/mypyc/build.py b/mypyc/build.py index d55334d8ac800..08eeb13c91752 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"], ["random/librt_random.h"], ["random"]), ] try: @@ -631,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") 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..db29f9e304d8d 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 new file mode 100644 index 0000000000000..7dc590eaa5946 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random.c @@ -0,0 +1,762 @@ +#include "pythoncapi_compat.h" + +#define PY_SSIZE_T_CLEAN +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "mypyc_util.h" +#include "CPy.h" +#include "librt_random.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 (standard ChaCha finalization) + 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++]; +} + +// 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). +// 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). + 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; + } + // 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. +// 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) +{ + 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, 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. + 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() +// +// 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 inline 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; +} + +// 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() +// + +static PyObject* +module_random(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + chacha8_rng *rng = get_thread_rng(); + if (rng == NULL) + return NULL; + return PyFloat_FromDouble(random_double_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) +{ + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return PyLong_FromLongLong(random_i64_from_range(rng, a, range)); +} + +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; + } + + int64_t a = CPyLong_AsInt64(args[0]); + if (unlikely(a == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + int64_t b = CPyLong_AsInt64(args[1]); + if (unlikely(b == CPY_LL_INT_ERROR && 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; + + 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, + int64_t *a, int64_t *b) +{ + if (nargs == 1) { + *a = 0; + 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()"); + return -1; + } + *b = stop - 1; + } else if (nargs == 2) { + *a = CPyLong_AsInt64(args[0]); + if (unlikely(*a == CPY_LL_INT_ERROR && PyErr_Occurred())) + return -1; + 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()"); + 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) +{ + int64_t 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); +} + +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; + } + 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(); + if (rng == NULL) + return NULL; + + chacha8_seed_int(rng, seed_val); + Py_RETURN_NONE; +} + +// +// Random Python type +// + +typedef struct { + PyObject_HEAD + chacha8_rng rng; +} 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); + // Seeding is done in tp_init + return (PyObject *)self; +} + +static int +Random_init(RandomObject *self, PyObject *args, PyObject *kwds) +{ + PyObject *seed_obj = NULL; + + if (!PyArg_ParseTuple(args, "|O", &seed_obj)) { + return -1; + } + + if (kwds != NULL && PyDict_Size(kwds) > 0) { + PyErr_SetString(PyExc_TypeError, + "Random() takes no keyword arguments"); + return -1; + } + + if (seed_obj == NULL || seed_obj == Py_None) { + if (chacha8_init(&self->rng) < 0) + return -1; + } else { + 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); + } + + 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 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; + } + return (int64_t)chacha8_next_ranged(&((RandomObject *)self)->rng, (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 - (uint64_t)start; + return random_i64_from_range(&((RandomObject *)self)->rng, start, 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 - (uint64_t)a + 1; + return random_i64_from_range(&((RandomObject *)self)->rng, a, range); +} + +static double +Random_random_internal(PyObject *self) { + return random_double_impl(&((RandomObject *)self)->rng); +} + +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; + } + + int64_t a = CPyLong_AsInt64(args[0]); + if (unlikely(a == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + int64_t b = CPyLong_AsInt64(args[1]); + if (unlikely(b == CPY_LL_INT_ERROR && PyErr_Occurred())) + return NULL; + + if (a > b) { + PyErr_SetString(PyExc_ValueError, + "empty range for randint()"); + return NULL; + } + + return randint_impl(&self->rng, a, b); +} + +static PyObject* +Random_randrange(RandomObject *self, PyObject *const *args, Py_ssize_t nargs) { + int64_t a, b; + if (parse_randrange_args(args, nargs, &a, &b) < 0) + return NULL; + return randint_impl(&self->rng, a, b); +} + +static PyObject* +Random_random(RandomObject *self, PyObject *Py_UNUSED(ignored)) { + return PyFloat_FromDouble(random_double_impl(&self->rng)); +} + +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; + } + 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; +} + +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).") + }, + {"seed", (PyCFunction) Random_seed, METH_FASTCALL, + PyDoc_STR("Seed the random number generator with an integer.") + }, + {NULL} /* Sentinel */ +}; + +static PyTypeObject RandomType = { + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "Random", + .tp_doc = PyDoc_STR("Fast random number generator using ChaCha8"), + .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[] = { + {"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.") + }, + {"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} +}; + +// 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) + return CPY_FLOAT_ERROR; + return random_double_impl(rng); +} + +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) + return CPY_LL_INT_ERROR; + uint64_t range = (uint64_t)b - (uint64_t)a + 1; + return random_i64_from_range(rng, a, 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) + return CPY_LL_INT_ERROR; + return (int64_t)chacha8_next_ranged(rng, (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) + return CPY_LL_INT_ERROR; + uint64_t range = (uint64_t)stop - (uint64_t)start; + return random_i64_from_range(rng, start, range); +} + +static int +random_abi_version(void) { + return LIBRT_RANDOM_ABI_VERSION; +} + +static int +random_api_version(void) { + return LIBRT_RANDOM_API_VERSION; +} + +static int +librt_random_module_exec(PyObject *m) +{ + if (ensure_tls_key() < 0) { + return -1; + } + if (PyType_Ready(&RandomType) < 0) { + return -1; + } + if (PyModule_AddObjectRef(m, "Random", (PyObject *) &RandomType) < 0) { + return -1; + } + // 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, + (void *)Random_random_internal, + (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) { + 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 using ChaCha8", + .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/random/librt_random.h b/mypyc/lib-rt/random/librt_random.h new file mode 100644 index 0000000000000..2eabfbd021bc9 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random.h @@ -0,0 +1,10 @@ +#ifndef LIBRT_RANDOM_H +#define LIBRT_RANDOM_H + +#include + +#define LIBRT_RANDOM_ABI_VERSION 1 +#define LIBRT_RANDOM_API_VERSION 9 +#define LIBRT_RANDOM_API_LEN 13 + +#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..157fa82b82eb3 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random_api.c @@ -0,0 +1,45 @@ +#include + +#include "librt_random_api.h" + +void *LibRTRandom_API[LIBRT_RANDOM_API_LEN] = {0}; + +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 = (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; +} 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..2794de0dd7e58 --- /dev/null +++ b/mypyc/lib-rt/random/librt_random_api.h @@ -0,0 +1,32 @@ +#ifndef LIBRT_RANDOM_API_H +#define LIBRT_RANDOM_API_H + +#include +#include +#include +#include "librt_random.h" + +int +import_librt_random(void); + +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_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(); +} + +#endif // LIBRT_RANDOM_API_H diff --git a/mypyc/lib-rt/setup.py b/mypyc/lib-rt/setup.py index 49b6c10201317..371b322ca18b2 100644 --- a/mypyc/lib-rt/setup.py +++ b/mypyc/lib-rt/setup.py @@ -151,5 +151,18 @@ def run(self) -> None: Extension( "librt.time", ["time/librt_time.c"], include_dirs=["."], extra_compile_args=cflags ), + Extension( + "librt.random", + [ + "random/librt_random.c", + "init.c", + "int_ops.c", + "exc_ops.c", + "pythonsupport.c", + "getargsfast.c", + ], + include_dirs=["."], + extra_compile_args=cflags, + ), ] ) diff --git a/mypyc/primitives/librt_random_ops.py b/mypyc/primitives/librt_random_ops.py new file mode 100644 index 0000000000000..6aaee84ecd0d6 --- /dev/null +++ b/mypyc/primitives/librt_random_ops.py @@ -0,0 +1,104 @@ +from mypyc.ir.deps import LIBRT_RANDOM +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 + +# 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, + 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, + 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, + 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, + 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, + 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, + 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_MAGIC, + 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, + 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, + 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, + dependencies=[LIBRT_RANDOM], +) 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..9215c13c88d6e --- /dev/null +++ b/mypyc/test-data/irbuild-librt-random.test @@ -0,0 +1,119 @@ +[case testLibrtRandomConstructor_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_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 + +[case testLibrtRandomRandrange_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_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 testLibrtRandomRandom_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 + +[case testLibrtRandomModuleRandom_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_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_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 diff --git a/mypyc/test-data/run-librt-random.test b/mypyc/test-data/run-librt-random.test new file mode 100644 index 0000000000000..0b34222678018 --- /dev/null +++ b/mypyc/test-data/run-librt-random.test @@ -0,0 +1,344 @@ +[case testRandom_librt] +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() + 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 + +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 + +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 + +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 + +# +# Module-level functions +# + +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 + +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 + +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 + +# +# 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 +# + +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 + +# +# Error handling +# + +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] + +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) 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", 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",