diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index f9aa440edee..3d695046af7 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -35,6 +35,8 @@ ceval cfield cfws CFWS +chans +cidx CLASSDEREF classdict cmpop @@ -170,6 +172,7 @@ nseen NSIGNALS numer nvars +obmalloc opname opnames orelse @@ -206,6 +209,7 @@ PYTHONTRACEMALLOC PYTHONUTF8 pythonw PYTHREAD_NAME +recvd releasebuffer repr resinfo @@ -241,6 +245,7 @@ sval swappedbytes swaptimize sysdict +tbexc tbstderr templatelib testconsole @@ -256,6 +261,7 @@ Typeparam typeparams typeslots unaryop +unboundop uncollectable Unhandle unparse @@ -278,5 +284,6 @@ withitem withs worklist XSETREF +xibufferview xstat XXPRIME diff --git a/Lib/concurrent/interpreters/__init__.py b/Lib/concurrent/interpreters/__init__.py new file mode 100644 index 00000000000..ea4147ee9a2 --- /dev/null +++ b/Lib/concurrent/interpreters/__init__.py @@ -0,0 +1,247 @@ +"""Subinterpreters High Level Module.""" + +import threading +import weakref +import _interpreters + +# aliases: +from _interpreters import ( + InterpreterError, InterpreterNotFoundError, NotShareableError, + is_shareable, +) +from ._queues import ( + create as create_queue, + Queue, QueueEmpty, QueueFull, +) + + +__all__ = [ + 'get_current', 'get_main', 'create', 'list_all', 'is_shareable', + 'Interpreter', + 'InterpreterError', 'InterpreterNotFoundError', 'ExecutionFailed', + 'NotShareableError', + 'create_queue', 'Queue', 'QueueEmpty', 'QueueFull', +] + + +_EXEC_FAILURE_STR = """ +{superstr} + +Uncaught in the interpreter: + +{formatted} +""".strip() + +class ExecutionFailed(InterpreterError): + """An unhandled exception happened during execution. + + This is raised from Interpreter.exec() and Interpreter.call(). + """ + + def __init__(self, excinfo): + msg = excinfo.formatted + if not msg: + if excinfo.type and excinfo.msg: + msg = f'{excinfo.type.__name__}: {excinfo.msg}' + else: + msg = excinfo.type.__name__ or excinfo.msg + super().__init__(msg) + self.excinfo = excinfo + + def __str__(self): + try: + formatted = self.excinfo.errdisplay + except Exception: + return super().__str__() + else: + return _EXEC_FAILURE_STR.format( + superstr=super().__str__(), + formatted=formatted, + ) + + +def create(): + """Return a new (idle) Python interpreter.""" + id = _interpreters.create(reqrefs=True) + return Interpreter(id, _ownsref=True) + + +def list_all(): + """Return all existing interpreters.""" + return [Interpreter(id, _whence=whence) + for id, whence in _interpreters.list_all(require_ready=True)] + + +def get_current(): + """Return the currently running interpreter.""" + id, whence = _interpreters.get_current() + return Interpreter(id, _whence=whence) + + +def get_main(): + """Return the main interpreter.""" + id, whence = _interpreters.get_main() + assert whence == _interpreters.WHENCE_RUNTIME, repr(whence) + return Interpreter(id, _whence=whence) + + +_known = weakref.WeakValueDictionary() + +class Interpreter: + """A single Python interpreter. + + Attributes: + + "id" - the unique process-global ID number for the interpreter + "whence" - indicates where the interpreter was created + + If the interpreter wasn't created by this module + then any method that modifies the interpreter will fail, + i.e. .close(), .prepare_main(), .exec(), and .call() + """ + + _WHENCE_TO_STR = { + _interpreters.WHENCE_UNKNOWN: 'unknown', + _interpreters.WHENCE_RUNTIME: 'runtime init', + _interpreters.WHENCE_LEGACY_CAPI: 'legacy C-API', + _interpreters.WHENCE_CAPI: 'C-API', + _interpreters.WHENCE_XI: 'cross-interpreter C-API', + _interpreters.WHENCE_STDLIB: '_interpreters module', + } + + def __new__(cls, id, /, _whence=None, _ownsref=None): + # There is only one instance for any given ID. + if not isinstance(id, int): + raise TypeError(f'id must be an int, got {id!r}') + id = int(id) + if _whence is None: + if _ownsref: + _whence = _interpreters.WHENCE_STDLIB + else: + _whence = _interpreters.whence(id) + assert _whence in cls._WHENCE_TO_STR, repr(_whence) + if _ownsref is None: + _ownsref = (_whence == _interpreters.WHENCE_STDLIB) + try: + self = _known[id] + assert hasattr(self, '_ownsref') + except KeyError: + self = super().__new__(cls) + _known[id] = self + self._id = id + self._whence = _whence + self._ownsref = _ownsref + if _ownsref: + # This may raise InterpreterNotFoundError: + _interpreters.incref(id) + return self + + def __repr__(self): + return f'{type(self).__name__}({self.id})' + + def __hash__(self): + return hash(self._id) + + def __del__(self): + self._decref() + + # for pickling: + def __reduce__(self): + return (type(self), (self._id,)) + + # gh-135729: Globals might be destroyed by the time this is called, so we + # need to keep references ourself + def _decref(self, *, + InterpreterNotFoundError=InterpreterNotFoundError, + _interp_decref=_interpreters.decref, + ): + if not self._ownsref: + return + self._ownsref = False + try: + _interp_decref(self._id) + except InterpreterNotFoundError: + pass + + @property + def id(self): + return self._id + + @property + def whence(self): + return self._WHENCE_TO_STR[self._whence] + + def is_running(self): + """Return whether or not the identified interpreter is running.""" + return _interpreters.is_running(self._id) + + # Everything past here is available only to interpreters created by + # interpreters.create(). + + def close(self): + """Finalize and destroy the interpreter. + + Attempting to destroy the current interpreter results + in an InterpreterError. + """ + return _interpreters.destroy(self._id, restrict=True) + + def prepare_main(self, ns=None, /, **kwargs): + """Bind the given values into the interpreter's __main__. + + The values must be shareable. + """ + ns = dict(ns, **kwargs) if ns is not None else kwargs + _interpreters.set___main___attrs(self._id, ns, restrict=True) + + def exec(self, code, /): + """Run the given source code in the interpreter. + + This is essentially the same as calling the builtin "exec" + with this interpreter, using the __dict__ of its __main__ + module as both globals and locals. + + There is no return value. + + If the code raises an unhandled exception then an ExecutionFailed + exception is raised, which summarizes the unhandled exception. + The actual exception is discarded because objects cannot be + shared between interpreters. + + This blocks the current Python thread until done. During + that time, the previous interpreter is allowed to run + in other threads. + """ + excinfo = _interpreters.exec(self._id, code, restrict=True) + if excinfo is not None: + raise ExecutionFailed(excinfo) + + def _call(self, callable, args, kwargs): + res, excinfo = _interpreters.call(self._id, callable, args, kwargs, restrict=True) + if excinfo is not None: + raise ExecutionFailed(excinfo) + return res + + def call(self, callable, /, *args, **kwargs): + """Call the object in the interpreter with given args/kwargs. + + Nearly all callables, args, kwargs, and return values are + supported. All "shareable" objects are supported, as are + "stateless" functions (meaning non-closures that do not use + any globals). This method will fall back to pickle. + + If the callable raises an exception then the error display + (including full traceback) is sent back between the interpreters + and an ExecutionFailed exception is raised, much like what + happens with Interpreter.exec(). + """ + return self._call(callable, args, kwargs) + + def call_in_thread(self, callable, /, *args, **kwargs): + """Return a new thread that calls the object in the interpreter. + + The return value and any raised exception are discarded. + """ + t = threading.Thread(target=self._call, args=(callable, args, kwargs)) + t.start() + return t diff --git a/Lib/concurrent/interpreters/_crossinterp.py b/Lib/concurrent/interpreters/_crossinterp.py new file mode 100644 index 00000000000..a5f46b20fbb --- /dev/null +++ b/Lib/concurrent/interpreters/_crossinterp.py @@ -0,0 +1,107 @@ +"""Common code between queues and channels.""" + + +class ItemInterpreterDestroyed(Exception): + """Raised when trying to get an item whose interpreter was destroyed.""" + + +class classonly: + """A non-data descriptor that makes a value only visible on the class. + + This is like the "classmethod" builtin, but does not show up on + instances of the class. It may be used as a decorator. + """ + + def __init__(self, value): + self.value = value + self.getter = classmethod(value).__get__ + self.name = None + + def __set_name__(self, cls, name): + if self.name is not None: + raise TypeError('already used') + self.name = name + + def __get__(self, obj, cls): + if obj is not None: + raise AttributeError(self.name) + # called on the class + return self.getter(None, cls) + + +class UnboundItem: + """Represents a cross-interpreter item no longer bound to an interpreter. + + An item is unbound when the interpreter that added it to the + cross-interpreter container is destroyed. + """ + + __slots__ = () + + @classonly + def singleton(cls, kind, module, name='UNBOUND'): + doc = cls.__doc__ + if doc: + doc = doc.replace( + 'cross-interpreter container', kind, + ).replace( + 'cross-interpreter', kind, + ) + subclass = type( + f'Unbound{kind.capitalize()}Item', + (cls,), + { + "_MODULE": module, + "_NAME": name, + "__doc__": doc, + }, + ) + return object.__new__(subclass) + + _MODULE = __name__ + _NAME = 'UNBOUND' + + def __new__(cls): + raise Exception(f'use {cls._MODULE}.{cls._NAME}') + + def __repr__(self): + return f'{self._MODULE}.{self._NAME}' +# return f'interpreters._queues.UNBOUND' + + +UNBOUND = object.__new__(UnboundItem) +UNBOUND_ERROR = object() +UNBOUND_REMOVE = object() + +_UNBOUND_CONSTANT_TO_FLAG = { + UNBOUND_REMOVE: 1, + UNBOUND_ERROR: 2, + UNBOUND: 3, +} +_UNBOUND_FLAG_TO_CONSTANT = {v: k + for k, v in _UNBOUND_CONSTANT_TO_FLAG.items()} + + +def serialize_unbound(unbound): + op = unbound + try: + flag = _UNBOUND_CONSTANT_TO_FLAG[op] + except KeyError: + raise NotImplementedError(f'unsupported unbound replacement op {op!r}') + return flag, + + +def resolve_unbound(flag, exctype_destroyed): + try: + op = _UNBOUND_FLAG_TO_CONSTANT[flag] + except KeyError: + raise NotImplementedError(f'unsupported unbound replacement op {flag!r}') + if op is UNBOUND_REMOVE: + # "remove" not possible here + raise NotImplementedError + elif op is UNBOUND_ERROR: + raise exctype_destroyed("item's original interpreter destroyed") + elif op is UNBOUND: + return UNBOUND + else: + raise NotImplementedError(repr(op)) diff --git a/Lib/concurrent/interpreters/_queues.py b/Lib/concurrent/interpreters/_queues.py new file mode 100644 index 00000000000..56c947f7d2a --- /dev/null +++ b/Lib/concurrent/interpreters/_queues.py @@ -0,0 +1,290 @@ +"""Cross-interpreter Queues High Level Module.""" + +import pickle +import queue +import time +import weakref +import _interpqueues as _queues +from . import _crossinterp + +# aliases: +from _interpqueues import ( + QueueError, QueueNotFoundError, +) +from ._crossinterp import ( + UNBOUND_ERROR, UNBOUND_REMOVE, +) + +__all__ = [ + 'UNBOUND', 'UNBOUND_ERROR', 'UNBOUND_REMOVE', + 'create', 'list_all', + 'Queue', + 'QueueError', 'QueueNotFoundError', 'QueueEmpty', 'QueueFull', + 'ItemInterpreterDestroyed', +] + + +class QueueEmpty(QueueError, queue.Empty): + """Raised from get_nowait() when the queue is empty. + + It is also raised from get() if it times out. + """ + + +class QueueFull(QueueError, queue.Full): + """Raised from put_nowait() when the queue is full. + + It is also raised from put() if it times out. + """ + + +class ItemInterpreterDestroyed(QueueError, + _crossinterp.ItemInterpreterDestroyed): + """Raised from get() and get_nowait().""" + + +_SHARED_ONLY = 0 +_PICKLED = 1 + + +UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__) + + +def _serialize_unbound(unbound): + if unbound is UNBOUND: + unbound = _crossinterp.UNBOUND + return _crossinterp.serialize_unbound(unbound) + + +def _resolve_unbound(flag): + resolved = _crossinterp.resolve_unbound(flag, ItemInterpreterDestroyed) + if resolved is _crossinterp.UNBOUND: + resolved = UNBOUND + return resolved + + +def create(maxsize=0, *, unbounditems=UNBOUND): + """Return a new cross-interpreter queue. + + The queue may be used to pass data safely between interpreters. + + "unbounditems" sets the default for Queue.put(); see that method for + supported values. The default value is UNBOUND, which replaces + the unbound item. + """ + unbound = _serialize_unbound(unbounditems) + unboundop, = unbound + qid = _queues.create(maxsize, unboundop, -1) + self = Queue(qid) + self._set_unbound(unboundop, unbounditems) + return self + + +def list_all(): + """Return a list of all open queues.""" + queues = [] + for qid, unboundop, _ in _queues.list_all(): + self = Queue(qid) + if not hasattr(self, '_unbound'): + self._set_unbound(unboundop) + else: + assert self._unbound[0] == unboundop + queues.append(self) + return queues + + +_known_queues = weakref.WeakValueDictionary() + +class Queue: + """A cross-interpreter queue.""" + + def __new__(cls, id, /): + # There is only one instance for any given ID. + if isinstance(id, int): + id = int(id) + else: + raise TypeError(f'id must be an int, got {id!r}') + try: + self = _known_queues[id] + except KeyError: + self = super().__new__(cls) + self._id = id + _known_queues[id] = self + _queues.bind(id) + return self + + def __del__(self): + try: + _queues.release(self._id) + except QueueNotFoundError: + pass + try: + del _known_queues[self._id] + except KeyError: + pass + + def __repr__(self): + return f'{type(self).__name__}({self.id})' + + def __hash__(self): + return hash(self._id) + + # for pickling: + def __reduce__(self): + return (type(self), (self._id,)) + + def _set_unbound(self, op, items=None): + assert not hasattr(self, '_unbound') + if items is None: + items = _resolve_unbound(op) + unbound = (op, items) + self._unbound = unbound + return unbound + + @property + def id(self): + return self._id + + @property + def unbounditems(self): + try: + _, items = self._unbound + except AttributeError: + op, _ = _queues.get_queue_defaults(self._id) + _, items = self._set_unbound(op) + return items + + @property + def maxsize(self): + try: + return self._maxsize + except AttributeError: + self._maxsize = _queues.get_maxsize(self._id) + return self._maxsize + + def empty(self): + return self.qsize() == 0 + + def full(self): + return _queues.is_full(self._id) + + def qsize(self): + return _queues.get_count(self._id) + + def put(self, obj, block=True, timeout=None, *, + unbounditems=None, + _delay=10 / 1000, # 10 milliseconds + ): + """Add the object to the queue. + + If "block" is true, this blocks while the queue is full. + + For most objects, the object received through Queue.get() will + be a new one, equivalent to the original and not sharing any + actual underlying data. The notable exceptions include + cross-interpreter types (like Queue) and memoryview, where the + underlying data is actually shared. Furthermore, some types + can be sent through a queue more efficiently than others. This + group includes various immutable types like int, str, bytes, and + tuple (if the items are likewise efficiently shareable). + See interpreters.is_shareable(). + + "unbounditems" controls the behavior of Queue.get() for the given + object if the current interpreter (calling put()) is later + destroyed. + + If "unbounditems" is None (the default) then it uses the + queue's default, set with create_queue(), + which is usually UNBOUND. + + If "unbounditems" is UNBOUND_ERROR then get() will raise an + ItemInterpreterDestroyed exception if the original interpreter + has been destroyed. This does not otherwise affect the queue; + the next call to put() will work like normal, returning the next + item in the queue. + + If "unbounditems" is UNBOUND_REMOVE then the item will be removed + from the queue as soon as the original interpreter is destroyed. + Be aware that this will introduce an imbalance between put() + and get() calls. + + If "unbounditems" is UNBOUND then it is returned by get() in place + of the unbound item. + """ + if not block: + return self.put_nowait(obj, unbounditems=unbounditems) + if unbounditems is None: + unboundop = -1 + else: + unboundop, = _serialize_unbound(unbounditems) + if timeout is not None: + timeout = int(timeout) + if timeout < 0: + raise ValueError(f'timeout value must be non-negative') + end = time.time() + timeout + while True: + try: + _queues.put(self._id, obj, unboundop) + except QueueFull as exc: + if timeout is not None and time.time() >= end: + raise # re-raise + time.sleep(_delay) + else: + break + + def put_nowait(self, obj, *, unbounditems=None): + if unbounditems is None: + unboundop = -1 + else: + unboundop, = _serialize_unbound(unbounditems) + _queues.put(self._id, obj, unboundop) + + def get(self, block=True, timeout=None, *, + _delay=10 / 1000, # 10 milliseconds + ): + """Return the next object from the queue. + + If "block" is true, this blocks while the queue is empty. + + If the next item's original interpreter has been destroyed + then the "next object" is determined by the value of the + "unbounditems" argument to put(). + """ + if not block: + return self.get_nowait() + if timeout is not None: + timeout = int(timeout) + if timeout < 0: + raise ValueError(f'timeout value must be non-negative') + end = time.time() + timeout + while True: + try: + obj, unboundop = _queues.get(self._id) + except QueueEmpty as exc: + if timeout is not None and time.time() >= end: + raise # re-raise + time.sleep(_delay) + else: + break + if unboundop is not None: + assert obj is None, repr(obj) + return _resolve_unbound(unboundop) + return obj + + def get_nowait(self): + """Return the next object from the channel. + + If the queue is empty then raise QueueEmpty. Otherwise this + is the same as get(). + """ + try: + obj, unboundop = _queues.get(self._id) + except QueueEmpty as exc: + raise # re-raise + if unboundop is not None: + assert obj is None, repr(obj) + return _resolve_unbound(unboundop) + return obj + + +_queues._register_heap_types(Queue, QueueEmpty, QueueFull) diff --git a/Lib/test/string_tests.py b/Lib/test/string_tests.py index 0c159e02fb9..66e6d78940b 100644 --- a/Lib/test/string_tests.py +++ b/Lib/test/string_tests.py @@ -1301,7 +1301,6 @@ def test___contains__(self): self.checkequal(False, 'asd', '__contains__', 'asdf') self.checkequal(False, '', '__contains__', 'asdf') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_subscript(self): self.checkequal('a', 'abc', '__getitem__', 0) self.checkequal('c', 'abc', '__getitem__', -1) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index f2f99d366d9..ec65819d2fa 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -691,7 +691,6 @@ class A: with self.assertRaisesRegex(AttributeError, error_msg): del A.x - @unittest.expectedFailure # TODO: RUSTPYTHON def testObjectAttributeAccessErrorMessages(self): class A: pass diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 16df318ae8e..fc894fda65a 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -234,7 +234,6 @@ def test_basic_script(self): importlib.machinery.SourceFileLoader, expected_cwd=script_dir) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_script_abspath(self): # pass the script using the relative path, expect the absolute path # in __file__ diff --git a/Lib/test/test_descrtut.py b/Lib/test/test_descrtut.py index 15c3a6ebeed..5374c47507b 100644 --- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -136,7 +136,7 @@ def merge(self, other): >>> a.default = -1 >>> a[1] -1 - >>> a.x1 = 1 # TODO: RUSTPYTHON; # doctest: +EXPECTED_FAILURE + >>> a.x1 = 1 Traceback (most recent call last): File "", line 1, in ? AttributeError: 'defaultdict2' object has no attribute 'x1' and no __dict__ for setting new attributes diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 6791675800d..56575664be1 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2554,6 +2554,7 @@ def test_disallowed_reimport(self): self.assertIsNot(excsnap, None) @requires_subinterpreters + @unittest.skip("TODO: RUSTPYTHON; test requires _testsinglephase module") def test_pyinit_function_raises_exception(self): # gh-144601: PyInit functions that raised exceptions would cause a # crash when imported from a subinterpreter. diff --git a/crates/stdlib/src/_queue.rs b/crates/stdlib/src/_queue.rs index 96e77a34a0b..d1ee1531392 100644 --- a/crates/stdlib/src/_queue.rs +++ b/crates/stdlib/src/_queue.rs @@ -47,7 +47,7 @@ mod _queue { #[repr(transparent)] pub(crate) struct PyEmptyError(PyException); - #[pyclass(flags(HAS_WEAKREF))] + #[pyclass(flags(HAS_WEAKREF, BASETYPE))] impl PyEmptyError {} /// ## See Also diff --git a/crates/vm/src/builtins/bytearray.rs b/crates/vm/src/builtins/bytearray.rs index f063a1d08c9..f42dc7a852f 100644 --- a/crates/vm/src/builtins/bytearray.rs +++ b/crates/vm/src/builtins/bytearray.rs @@ -23,9 +23,7 @@ use crate::{ }, }, convert::{ToPyObject, ToPyResult}, - function::{ - ArgBytesLike, ArgIterable, ArgSize, OptionalArg, OptionalOption, PyComparisonValue, - }, + function::{ArgBytesLike, ArgSize, OptionalArg, OptionalOption, PyComparisonValue}, protocol::{ BufferDescriptor, BufferFlags, BufferMethods, BufferResizeGuard, PyBuffer, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods, @@ -355,7 +353,7 @@ impl PyByteArray { } #[pymethod] - fn join(&self, iter: ArgIterable, vm: &VirtualMachine) -> PyResult { + fn join(&self, iter: PyObjectRef, vm: &VirtualMachine) -> PyResult { // Driving the iterable runs Python, which can reach this bytearray, // so the separator is taken by value rather than left borrowed. let separator = self.inner().clone(); diff --git a/crates/vm/src/builtins/bytes.rs b/crates/vm/src/builtins/bytes.rs index e611e1929f0..1dc6264976b 100644 --- a/crates/vm/src/builtins/bytes.rs +++ b/crates/vm/src/builtins/bytes.rs @@ -17,10 +17,7 @@ use crate::{ class::PyClassImpl, common::{hash::PyHash, lock::PyMutex}, convert::{ToPyObject, ToPyResult}, - function::{ - ArgBytesLike, ArgIndex, ArgIterable, FuncArgs, OptionalArg, OptionalOption, - PyComparisonValue, - }, + function::{ArgBytesLike, ArgIndex, FuncArgs, OptionalArg, OptionalOption, PyComparisonValue}, protocol::{ BufferDescriptor, BufferFlags, BufferMethods, PyBuffer, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods, @@ -359,7 +356,7 @@ impl PyBytes { } #[pymethod] - fn join(&self, iter: ArgIterable, vm: &VirtualMachine) -> PyResult { + fn join(&self, iter: PyObjectRef, vm: &VirtualMachine) -> PyResult { Ok(self.inner.join(iter, vm)?.into()) } @@ -730,8 +727,8 @@ impl Comparable for PyBytes { return Err(vm.new_type_error(format!( "'{}' not supported between instances of '{}' and '{}'", op.operator_token(), - zelf.class().name(), - other.class().name() + zelf.class().slot_name(), + other.class().slot_name() ))); } else { zelf.inner.cmp(other, op, vm) diff --git a/crates/vm/src/builtins/complex.rs b/crates/vm/src/builtins/complex.rs index 7dcbedf7e17..069cd4e4490 100644 --- a/crates/vm/src/builtins/complex.rs +++ b/crates/vm/src/builtins/complex.rs @@ -227,8 +227,8 @@ impl Constructor for PyComplex { return Ok(Self::from(Complex64 { re, im })); } else { return Err(vm.new_type_error(format!( - "complex() first argument must be a string or a number, not '{}'", - val.class().name() + "complex() argument must be a string or a number, not {}", + val.class().slot_name() ))); } } diff --git a/crates/vm/src/builtins/float.rs b/crates/vm/src/builtins/float.rs index 6a646440ec2..30c6e3c1f38 100644 --- a/crates/vm/src/builtins/float.rs +++ b/crates/vm/src/builtins/float.rs @@ -223,8 +223,8 @@ pub fn float_from_string(val: PyObjectRef, vm: &VirtualMachine) -> PyResult &*buffer_lock } else { return Err(vm.new_type_error(format!( - "float() argument must be a string or a number, not '{}'", - val.class().name() + "float() argument must be a string or a real number, not '{}'", + val.class().slot_name() ))); }; crate::literal::float::parse_bytes(b).ok_or_else(|| { diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 94c231b0b83..b8975cf1102 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -940,7 +940,7 @@ impl PyFunction { } #[pygetset] - fn __defaults__(&self) -> Option { + pub(crate) fn __defaults__(&self) -> Option { self.defaults_and_kwdefaults.lock().0.clone() } #[pygetset(setter)] @@ -953,7 +953,7 @@ impl PyFunction { } #[pygetset] - fn __kwdefaults__(&self) -> Option { + pub(crate) fn __kwdefaults__(&self) -> Option { self.defaults_and_kwdefaults.lock().1.clone() } #[pygetset(setter)] diff --git a/crates/vm/src/builtins/memory.rs b/crates/vm/src/builtins/memory.rs index 86396825b0c..03ed7e75a75 100644 --- a/crates/vm/src/builtins/memory.rs +++ b/crates/vm/src/builtins/memory.rs @@ -118,6 +118,13 @@ impl PyMemoryView { Self::from_object_with_flags(obj, BufferFlags::FULL_RO, vm) } + /// One share of the underlying buffer export. Used for cross-interpreter + /// `send_buffer` so the destination memoryview sees the same memory. + #[must_use] + pub fn clone_buffer(&self) -> PyBuffer { + self.buffer.clone() + } + // PyMemoryView_FromObjectAndFlags pub fn from_object_with_flags( obj: &PyObject, @@ -128,9 +135,14 @@ impl PyMemoryView { other.try_not_released(vm)?; other.try_not_restricted(vm)?; Ok(other.new_view()) - } else { + } else if obj.check_buffer() { let buffer = PyBuffer::from_object(vm, obj, flags)?; Self::from_buffer(buffer, vm) + } else { + Err(vm.new_type_error(format!( + "memoryview: a bytes-like object is required, not '{}'", + obj.class().name() + ))) } } @@ -1344,8 +1356,8 @@ impl Comparable for PyMemoryView { _ => Err(vm.new_type_error(format!( "'{}' not supported between instances of '{}' and '{}'", op.operator_token(), - zelf.class().name(), - other.class().name() + zelf.class().slot_name(), + other.class().slot_name() ))), } } diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index 05d2edd2082..f69a54e77fe 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -410,7 +410,7 @@ impl PyBaseObject { if !format_spec.is_empty() { return Err(vm.new_type_error(format!( "unsupported format string passed to {}.__format__", - obj.class().name() + obj.class().slot_name() ))); } obj.str(vm) diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index dc5a1daf4b9..b912d4e26dc 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::{ AsObject, Context, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, - TryFromBorrowedObject, VirtualMachine, + TryFromBorrowedObject, TryFromObject, VirtualMachine, anystr::{self, AnyStr, AnyStrContainer, AnyStrWrapper, StringRange, adjust_indices}, atomic_func, bytes_inner::{swapcase_ascii, title_ascii}, @@ -670,7 +670,7 @@ impl PyStr { } else { Err(vm.new_type_error(format!( "'in ' requires string as left operand, not {}", - needle.class().name() + needle.class().slot_name() ))) } } @@ -680,7 +680,7 @@ impl PyStr { } fn _getitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { - let item = match SequenceIndex::try_from_borrowed_object(vm, needle, "str")? { + let item = match SequenceIndex::try_from_str_subscript(vm, needle)? { SequenceIndex::Int(i) => self.getitem_by_index(vm, i)?.to_pyobject(vm), SequenceIndex::Slice(slice) => self.getitem_by_slice(vm, slice)?.to_pyobject(vm), }; @@ -1169,14 +1169,20 @@ impl PyStr { } #[pymethod] - fn join( - zelf: PyRef, - iterable: ArgIterable, - vm: &VirtualMachine, - ) -> PyResult { + fn join(zelf: PyRef, iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult { // `PyUnicode_Join()` reaches its elements through `PySequence_Fast()`, - // which fills a list from the iterator and so asks it how long it is. - let iter = iterable.iter_sized(vm)?; + // which fills a list from the iterator and so asks it how long it is, + // and which has its own wording for what it cannot iterate. + let iterable = ArgIterable::::try_from_object(vm, iterable) + .map_err(|_| vm.new_type_error("can only join an iterable"))?; + let iter = iterable.iter_sized(vm)?.enumerate().map(|(i, obj)| { + obj?.downcast::().map_err(|obj| { + vm.new_type_error(format!( + "sequence item {i}: expected str instance, {} found", + obj.class().slot_name() + )) + }) + }); let joined = match iter.exactly_one() { Ok(first) => { let first = first?; @@ -1406,7 +1412,10 @@ impl PyStr { #[pymethod] pub fn translate(&self, table: PyObjectRef, vm: &VirtualMachine) -> PyResult { vm.get_method_or_type_error(table.clone(), identifier!(vm, __getitem__), || { - format!("'{}' object is not subscriptable", table.class().name()) + format!( + "'{}' object is not subscriptable", + table.class().slot_name() + ) })?; let mut translated = Wtf8Buf::new(); diff --git a/crates/vm/src/bytes_inner.rs b/crates/vm/src/bytes_inner.rs index d87fee4c2a2..fbdebb0bd02 100644 --- a/crates/vm/src/bytes_inner.rs +++ b/crates/vm/src/bytes_inner.rs @@ -628,9 +628,22 @@ impl PyBytesInner { .py_count(needle.as_slice(), range, |h, n| h.find_iter(n).count())) } - pub fn join(&self, iterable: ArgIterable, vm: &VirtualMachine) -> PyResult> { + // stringlib_bytes_join + pub fn join(&self, iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult> { // `PySequence_Fast()`, as in `PyUnicode_Join()`. - let iter = iterable.iter_sized(vm)?; + let iterable = ArgIterable::::try_from_object(vm, iterable) + .map_err(|_| vm.new_type_error("can only join an iterable"))?; + let iter = iterable.iter_sized(vm)?.enumerate().map(|(i, obj)| { + let obj = obj?; + // Whatever the buffer lookup ran into, the item is only ever + // reported as the wrong kind of thing. + Self::try_from_object(vm, obj.clone()).map_err(|_| { + vm.new_type_error(format!( + "sequence item {i}: expected a bytes-like object, {} found", + obj.class().slot_name() + )) + }) + }); self.elements.py_join(iter) } diff --git a/crates/vm/src/cformat.rs b/crates/vm/src/cformat.rs index 00147103851..0c1a0a41410 100644 --- a/crates/vm/src/cformat.rs +++ b/crates/vm/src/cformat.rs @@ -95,7 +95,7 @@ fn spec_format_bytes( Err(vm.new_type_error(format!( "%{} format: a real number is required, not {}", spec.format_type.to_char(), - obj.class().name() + obj.class().slot_name() ))) } }) @@ -209,7 +209,7 @@ fn spec_format_string( Err(vm.new_type_error(format!( "%{} format: a real number is required, not {}", spec.format_type.to_char(), - obj.class().name() + obj.class().slot_name() ))) } }) diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index d102b9a6d8e..4cc4912ec7b 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -8087,7 +8087,7 @@ impl ExecutingFrame<'_> { vm.new_type_error(format!( "{} argument after * must be an iterable, not {}", func_str, - args_obj.class().name() + args_obj.class().slot_name() )) } else { e diff --git a/crates/vm/src/function/getargs.rs b/crates/vm/src/function/getargs.rs new file mode 100644 index 00000000000..cc335727b7f --- /dev/null +++ b/crates/vm/src/function/getargs.rs @@ -0,0 +1,123 @@ +//! `PyArg_ParseTupleAndKeywords` argument matching. + +use super::FuncArgs; +use crate::{PyObject, PyObjectRef, PyResult, VirtualMachine, common::wtf8::Wtf8}; + +/// The keyword list of a `PyArg_ParseTupleAndKeywords` call together with the +/// `|`, `$` and `:` markers of its format string. Names are matched to the +/// supplied arguments; converting them is left to the caller. +pub(crate) struct ArgSpec<'a> { + /// What the `:name` suffix names the function in error messages. + pub(crate) fname: &'a str, + /// Every parameter name, in `kwlist` order. None may be positional-only. + pub(crate) keywords: &'a [&'a str], + /// Where `|` sits: how many leading parameters are required. Equal to the + /// parameter count when the format string has no `|`. + pub(crate) required: usize, + /// Where `$` sits: how many leading parameters may be given positionally. + /// Equal to the parameter count when the format string has no `$`. + pub(crate) max_positional: usize, +} + +impl ArgSpec<'_> { + pub(crate) fn parse( + &self, + args: &FuncArgs, + vm: &VirtualMachine, + ) -> PyResult>> { + self.parse_with(args, |_, _, _| Ok(()), vm) + } + + /// `check` runs for each supplied argument in `kwlist` order, so a + /// conversion failure is reported ahead of the checks that follow it. + pub(crate) fn parse_with( + &self, + args: &FuncArgs, + check: impl Fn(usize, &PyObject, &VirtualMachine) -> PyResult<()>, + vm: &VirtualMachine, + ) -> PyResult>> { + let fname = self.fname; + let len = self.keywords.len(); + let nargs = args.args.len(); + let mut nkwargs = args.kwargs.len(); + if nargs + nkwargs > len { + // Saying "keyword" when nothing was passed positionally keeps the + // message right for keyword-only signatures. + return Err(vm.new_type_error(format!( + "{fname}() takes at most {len} {}argument{} ({} given)", + if nargs == 0 { "keyword " } else { "" }, + if len == 1 { "" } else { "s" }, + nargs + nkwargs, + ))); + } + + let mut slots = vec![None; len]; + let mut exhausted = false; + for (i, slot) in slots.iter_mut().enumerate() { + if i == self.max_positional && self.max_positional < nargs { + return Err(vm.new_type_error(if self.max_positional == 0 { + format!("{fname}() takes no positional arguments") + } else { + format!( + "{fname}() takes {} {} positional argument{} ({nargs} given)", + if self.required < len { + "at most" + } else { + "exactly" + }, + self.max_positional, + if self.max_positional == 1 { "" } else { "s" }, + ) + })); + } + let current = if i < nargs { + Some(args.args[i].clone()) + } else if nkwargs > 0 { + args.kwargs + .get(self.keywords[i]) + .inspect(|_| nkwargs -= 1) + .cloned() + } else { + None + }; + if let Some(obj) = current { + check(i, &obj, vm)?; + *slot = Some(obj); + continue; + } + if i < self.required { + return Err(vm.new_type_error(format!( + "{fname}() missing required argument '{}' (pos {})", + self.keywords[i], + i + 1 + ))); + } + // All the required arguments are in and no keyword is left over, + // so nothing below can fail. + if nkwargs == 0 { + exhausted = true; + break; + } + } + + if !exhausted && nkwargs > 0 { + for (i, name) in self.keywords.iter().enumerate().take(nargs) { + if args.kwargs.contains_key(name) { + return Err(vm.new_type_error(format!( + "argument for {fname}() given by name ('{name}') and position ({})", + i + 1 + ))); + } + } + for key in args.kwargs.keys() { + if !self.keywords.iter().any(|name| &**key == Wtf8::new(name)) { + return Err(vm.new_type_error(format!( + "{fname}() got an unexpected keyword argument '{key}'" + ))); + } + } + return Err(vm.new_type_error(format!("invalid keyword argument for {fname}()"))); + } + Ok(slots) + } +} diff --git a/crates/vm/src/function/mod.rs b/crates/vm/src/function/mod.rs index 2b37c9fc8de..eb77c42e4c6 100644 --- a/crates/vm/src/function/mod.rs +++ b/crates/vm/src/function/mod.rs @@ -4,6 +4,7 @@ mod buffer; mod builtin; mod either; mod fspath; +mod getargs; mod getset; pub(crate) mod method; mod number; @@ -21,6 +22,7 @@ pub use buffer::{ pub use builtin::{IntoPyNativeFn, PyNativeFn, static_func, static_raw_func}; pub use either::Either; pub use fspath::FsPath; +pub(crate) use getargs::ArgSpec; pub use getset::PySetterValue; pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc}; pub use method::{HeapMethodDef, PyMethodDef, PyMethodFlags}; diff --git a/crates/vm/src/function/protocol.rs b/crates/vm/src/function/protocol.rs index 5d5e4527f57..5883a2a4691 100644 --- a/crates/vm/src/function/protocol.rs +++ b/crates/vm/src/function/protocol.rs @@ -57,9 +57,10 @@ impl From for PyObjectRef { impl TryFromObject for ArgCallable { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { let Some(callable) = obj.to_callable() else { - return Err( - vm.new_type_error(format!("'{}' object is not callable", obj.class().name())) - ); + return Err(vm.new_type_error(format!( + "'{}' object is not callable", + obj.class().slot_name() + ))); }; let call = callable.call; Ok(Self { obj, call }) @@ -126,7 +127,7 @@ where let cls = obj.class(); let iter_fn = cls.slots.iter.load(); if iter_fn.is_none() && !cls.has_attr(identifier!(vm, __getitem__)) { - return Err(vm.new_type_error(format!("'{}' object is not iterable", cls.name()))); + return Err(vm.new_type_error(format!("'{}' object is not iterable", cls.slot_name()))); } Ok(Self { iterable: obj, diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index a15e30c34a3..9a8c27197c0 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -111,8 +111,9 @@ pub use self::object::{ }; pub use self::vm::runtime; pub use self::vm::{ - Context, Interpreter, InterpreterBuilder, InterpreterInfo, InterpreterWhence, - MAIN_INTERPRETER_ID, Settings, VirtualMachine, + Context, InterpFeatureFlags, Interpreter, InterpreterBuilder, InterpreterConfig, + InterpreterGil, InterpreterInfo, InterpreterWhence, MAIN_INTERPRETER_ID, Settings, + VirtualMachine, }; pub use rustpython_common as common; diff --git a/crates/vm/src/protocol/callable.rs b/crates/vm/src/protocol/callable.rs index 5a1605d4b5e..e8647ebe1d8 100644 --- a/crates/vm/src/protocol/callable.rs +++ b/crates/vm/src/protocol/callable.rs @@ -28,9 +28,10 @@ impl PyObject { /// PyObject_Call pub fn call_with_args(&self, args: FuncArgs, vm: &VirtualMachine) -> PyResult { let Some(callable) = self.to_callable() else { - return Err( - vm.new_type_error(format!("'{}' object is not callable", self.class().name())) - ); + return Err(vm.new_type_error(format!( + "'{}' object is not callable", + self.class().slot_name() + ))); }; vm_trace!("Invoke: {:?} {:?}", callable, args); callable.invoke(args, vm) @@ -47,9 +48,10 @@ impl PyObject { vm: &VirtualMachine, ) -> PyResult { let Some(callable) = self.to_callable() else { - return Err( - vm.new_type_error(format!("'{}' object is not callable", self.class().name())) - ); + return Err(vm.new_type_error(format!( + "'{}' object is not callable", + self.class().slot_name() + ))); }; callable.invoke_vectorcall(args, nargs, kwnames, vm) } diff --git a/crates/vm/src/protocol/iter.rs b/crates/vm/src/protocol/iter.rs index 3df72fd80eb..d2001e96699 100644 --- a/crates/vm/src/protocol/iter.rs +++ b/crates/vm/src/protocol/iter.rs @@ -50,7 +50,7 @@ where .ok_or_else(|| { vm.new_type_error(format!( "'{}' object is not an iterator", - self.0.borrow().class().name() + self.0.borrow().class().slot_name() )) })?; iternext(self.0.borrow(), vm) @@ -144,7 +144,7 @@ impl TryFromObject for PyIter { } else { Err(vm.new_type_error(format!( "iter() returned non-iterator of type '{}'", - iter.class().name() + iter.class().slot_name() ))) } } else if let Ok(seq_iter) = PySequenceIterator::new(iter_target.clone(), vm) { @@ -152,7 +152,7 @@ impl TryFromObject for PyIter { } else { Err(vm.new_type_error(format!( "'{}' object is not iterable", - iter_target.class().name() + iter_target.class().slot_name() ))) } } diff --git a/crates/vm/src/protocol/mapping.rs b/crates/vm/src/protocol/mapping.rs index 1900e072db3..01db553ccbf 100644 --- a/crates/vm/src/protocol/mapping.rs +++ b/crates/vm/src/protocol/mapping.rs @@ -83,7 +83,10 @@ impl PyObject { if mapping.check() { Ok(mapping) } else { - Err(vm.new_type_error(format!("{} is not a mapping object", self.class()))) + Err(vm.new_type_error(format!( + "{} is not a mapping object", + self.class().slot_name() + ))) } } } @@ -123,12 +126,24 @@ impl PyMapping<'_> { self.slots().length.load().map(|f| f(self, vm)) } + // Py_ssize_t PyMapping_Size(PyObject *o) pub fn length(self, vm: &VirtualMachine) -> PyResult { self.length_opt(vm).ok_or_else(|| { - vm.new_type_error(format!( - "object of type '{}' has no len() or not a mapping", - self.obj.class() - )) + let name = self.obj.class().slot_name(); + // Something that measures itself as a sequence is no mapping at all. + let msg = if self + .obj + .sequence_unchecked() + .slots() + .length + .load() + .is_some() + { + format!("{name} is not a mapping") + } else { + format!("object of type '{name}' has no len()") + }; + vm.new_type_error(msg) })? } @@ -146,10 +161,9 @@ impl PyMapping<'_> { } fn _subscript(self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { - let f = - self.slots().subscript.load().ok_or_else(|| { - vm.new_type_error(format!("{} is not a mapping", self.obj.class())) - })?; + let f = self.slots().subscript.load().ok_or_else(|| { + vm.new_type_error(format!("{} is not a mapping", self.obj.class().slot_name())) + })?; f(self, needle, vm) } @@ -162,7 +176,7 @@ impl PyMapping<'_> { let f = self.slots().ass_subscript.load().ok_or_else(|| { vm.new_type_error(format!( "'{}' object does not support item assignment", - self.obj.class() + self.obj.class().slot_name() )) })?; f(self, needle, value, vm) @@ -205,9 +219,9 @@ impl PyMapping<'_> { let iter = meth_output.get_iter(vm).map_err(|_| { vm.new_type_error(format!( "{}.{}() returned a non-iterable (type {})", - self.obj.class(), + self.obj.class().slot_name(), method_name.as_str(), - meth_output.class() + meth_output.class().slot_name() )) })?; diff --git a/crates/vm/src/protocol/number.rs b/crates/vm/src/protocol/number.rs index 6f566431da7..05fc7952dd9 100644 --- a/crates/vm/src/protocol/number.rs +++ b/crates/vm/src/protocol/number.rs @@ -62,7 +62,7 @@ impl PyObject { self.try_index_opt(vm).transpose()?.ok_or_else(|| { vm.new_type_error(format!( "'{}' object cannot be interpreted as an integer", - self.class() + self.class().slot_name() )) }) } @@ -93,7 +93,7 @@ impl PyObject { } else { Err(vm.new_type_error(format!( "int() argument must be a string, a bytes-like object or a real number, not '{}'", - self.class() + self.class().slot_name() ))) } } @@ -112,7 +112,10 @@ impl PyObject { #[inline] pub fn try_float(&self, vm: &VirtualMachine) -> PyResult> { self.try_float_opt(vm).ok_or_else(|| { - vm.new_type_error(format!("must be real number, not {}", self.class())) + vm.new_type_error(format!( + "must be real number, not {}", + self.class().slot_name() + )) })? } } @@ -673,18 +676,18 @@ impl PyNumber<'_> { let ret_class = ret.class().to_owned(); if let Some(ret) = ret.downcast_ref::() { let msg = format!( - "__int__ returned non-int (type {ret_class}). \ + "__int__ returned non-int (type {}). \ The ability to return an instance of a strict subclass of int is deprecated, \ -and may be removed in a future version of Python." +and may be removed in a future version of Python.", + ret_class.slot_name() ); _warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm)?; Ok(ret.to_owned()) } else { Err(vm.new_type_error(format!( - "{}.__int__ returned non-int(type {})", - self.class(), - ret_class + "__int__ returned non-int (type {})", + ret_class.slot_name() ))) } }) @@ -702,18 +705,18 @@ and may be removed in a future version of Python." let ret_class = ret.class().to_owned(); if let Some(ret) = ret.downcast_ref::() { let msg = format!( - "__index__ returned non-int (type {ret_class}). \ + "__index__ returned non-int (type {}). \ The ability to return an instance of a strict subclass of int is deprecated, \ -and may be removed in a future version of Python." +and may be removed in a future version of Python.", + ret_class.slot_name() ); _warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm)?; Ok(ret.to_owned()) } else { Err(vm.new_type_error(format!( - "{}.__index__ returned non-int(type {})", - self.class(), - ret_class + "__index__ returned non-int (type {})", + ret_class.slot_name() ))) } }) @@ -731,18 +734,20 @@ and may be removed in a future version of Python." let ret_class = ret.class().to_owned(); if let Some(ret) = ret.downcast_ref::() { let msg = format!( - "__float__ returned non-float (type {ret_class}). \ + "{}.__float__ returned non-float (type {}). \ The ability to return an instance of a strict subclass of float is deprecated, \ -and may be removed in a future version of Python." +and may be removed in a future version of Python.", + self.class().slot_name(), + ret_class.slot_name() ); _warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm)?; Ok(ret.to_owned()) } else { Err(vm.new_type_error(format!( - "{}.__float__ returned non-float(type {})", - self.class(), - ret_class + "{}.__float__ returned non-float (type {})", + self.class().slot_name(), + ret_class.slot_name() ))) } }) diff --git a/crates/vm/src/protocol/object.rs b/crates/vm/src/protocol/object.rs index 1e312a2a3de..bcb00920cac 100644 --- a/crates/vm/src/protocol/object.rs +++ b/crates/vm/src/protocol/object.rs @@ -3,8 +3,8 @@ use crate::{ AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, TryFromObject, VirtualMachine, builtins::{ - PyBytes, PyDict, PyDictRef, PyGenericAlias, PyInt, PyList, PyStr, PyTuple, PyTupleRef, - PyType, PyTypeRef, PyUtf8Str, int::check_int_to_str_digits, pystr::AsPyStr, + PyBaseObject, PyBytes, PyDict, PyDictRef, PyGenericAlias, PyInt, PyList, PyStr, PyTuple, + PyTupleRef, PyType, PyTypeRef, PyUtf8Str, int::check_int_to_str_digits, pystr::AsPyStr, }, common::{hash::PyHash, str::to_ascii}, convert::ToPyObject, @@ -93,7 +93,7 @@ impl PyObject { let Some(_aiter_method) = aiter_method else { return Err(vm.new_type_error(format!( "'{}' object is not an async iterable", - self.class().name() + self.class().slot_name() ))); }; @@ -110,7 +110,7 @@ impl PyObject { if !iterator.class().has_attr(identifier!(vm, __anext__)) { return Err(vm.new_type_error(format!( "'{}' object is not an async iterator", - iterator.class().name() + iterator.class().slot_name() ))); } @@ -151,7 +151,7 @@ impl PyObject { let has_getattr = cls.slots.getattro.load().is_some(); vm.new_type_error(format!( "'{}' object has {} attributes ({} {})", - cls.name(), + cls.slot_name(), if has_getattr { "only read-only" } else { "no" }, if attr_value.is_assign() { "assign to" @@ -185,37 +185,60 @@ impl PyObject { vm: &VirtualMachine, ) -> PyResult<()> { vm_trace!("object.__setattr__({:?}, {}, {:?})", self, attr_name, value); - if let Some(attr) = vm + let descr = vm .ctx .interned_str(attr_name) - .and_then(|attr_name| self.get_class_attr(attr_name)) + .and_then(|attr_name| self.get_class_attr(attr_name)); + if let Some(attr) = &descr + && let Some(descriptor) = attr.class().slots.descr_set.load() { - let descr_set = attr.class().slots.descr_set.load(); - if let Some(descriptor) = descr_set { - return descriptor(&attr, self.to_owned(), value, vm); - } + return descriptor(attr, self.to_owned(), value, vm); } - if let Some(instance_dict) = self.instance_dict() { - if let PySetterValue::Assign(value) = value { - instance_dict - .get_or_insert(vm) - .set_item(attr_name, value, vm)?; - } else if let Some(dict) = instance_dict.get() { - dict.del_item(attr_name, vm).map_err(|e| { - if e.fast_isinstance(vm.ctx.exceptions.key_error) { - vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned()) - } else { - e - } - })?; - } else { - return Err(vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned())); + let Some(instance_dict) = self.instance_dict() else { + let name = self.class().slot_name(); + if descr.is_some() { + return Err(vm.new_attribute_error(format!( + "'{name}' object attribute '{attr_name}' is read-only" + ))); } - Ok(()) + // Only a type that left __setattr__ alone can be told about the + // missing __dict__, since overriding it is what hides the slot. + let generic_setattro = self.class().slots.setattro.load().is_some_and(|f| { + crate::types::fn_addr(f) + == crate::types::fn_addr( + PyBaseObject::slot_setattro as crate::types::SetattroFunc, + ) + }); + let msg = if generic_setattro { + format!( + "'{name}' object has no attribute '{attr_name}' and no \ + __dict__ for setting new attributes" + ) + } else { + format!("'{name}' object has no attribute '{attr_name}'") + }; + let exc = vm.new_attribute_error(msg); + vm.set_attribute_error_context(&exc, self.to_owned(), attr_name.to_owned()); + return Err(exc); + }; + + if let PySetterValue::Assign(value) = value { + instance_dict + .get_or_insert(vm) + .set_item(attr_name, value, vm)?; + } else if let Some(dict) = instance_dict.get() { + dict.del_item(attr_name, vm).map_err(|e| { + if e.fast_isinstance(vm.ctx.exceptions.key_error) { + vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned()) + } else { + e + } + })?; } else { - Err(vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned())) + return Err(vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned())); } + Ok(()) } pub fn generic_getattr(&self, name: &Py, vm: &VirtualMachine) -> PyResult { @@ -342,8 +365,8 @@ impl PyObject { _ => Err(vm.new_type_error(format!( "'{}' not supported between instances of '{}' and '{}'", op.operator_token(), - self.class().name(), - other.class().name() + self.class().slot_name(), + other.class().slot_name() ))), } } @@ -431,23 +454,20 @@ impl PyObject { s.downcast::().map_err(|obj| { vm.new_type_error(format!( "__str__ returned non-string (type {})", - obj.class().name() + obj.class().slot_name() )) }) } - // Equivalent to CPython's check_class. Returns Ok(()) if cls is a valid class, - // Err with TypeError if not. Uses abstract_get_bases internally. - fn check_class(&self, vm: &VirtualMachine, msg: F) -> PyResult<()> - where - F: Fn() -> String, - { + // check_class. Returns Ok(()) if cls is a valid class, Err with TypeError if + // not. Uses abstract_get_bases internally. + fn check_class(&self, vm: &VirtualMachine, msg: &'static str) -> PyResult<()> { if self.abstract_get_bases(vm)?.is_some() { // Has __bases__, it's a valid class Ok(()) } else { // No __bases__ or __bases__ is not a tuple - Err(vm.new_type_error(msg())) + Err(vm.new_type_error(msg)) } } @@ -529,18 +549,14 @@ impl PyObject { } // Check if derived is a class - self.check_class(vm, || { - format!("issubclass() arg 1 must be a class, not {}", self.class()) - })?; + self.check_class(vm, "issubclass() arg 1 must be a class")?; // Check if cls is a class, tuple, or union (matches CPython's order and message) if !cls.class().is(vm.ctx.types.union_type) { - cls.check_class(vm, || { - format!( - "issubclass() arg 2 must be a class, a tuple of classes, or a union, not {}", - cls.class() - ) - })?; + cls.check_class( + vm, + "issubclass() arg 2 must be a class, a tuple of classes, or a union", + )?; } self.abstract_issubclass(cls, vm) @@ -620,12 +636,10 @@ impl PyObject { Ok(retval) } else { // Not a type object, check if it's a valid class - cls.check_class(vm, || { - format!( - "isinstance() arg 2 must be a type, a tuple of types, or a union, not {}", - cls.class() - ) - })?; + cls.check_class( + vm, + "isinstance() arg 2 must be a type, a tuple of types, or a union", + )?; if let Some(i_cls) = vm.get_attribute_opt(self.to_owned(), identifier!(vm, __class__))? @@ -698,7 +712,7 @@ impl PyObject { return vm.with_recursion("while hashing", || hash(self, vm)); } - Err(vm.new_type_error(format!("unhashable type: '{}'", self.class().name()))) + Err(vm.new_type_error(format!("unhashable type: '{}'", self.class().slot_name()))) } // type protocol @@ -722,7 +736,7 @@ impl PyObject { self.length_opt(vm).ok_or_else(|| { vm.new_type_error(format!( "object of type '{}' has no len()", - self.class().name() + self.class().slot_name() )) })? } @@ -754,10 +768,13 @@ impl PyObject { } return Err(vm.new_type_error(format!( "type '{}' is not subscriptable", - self.downcast_ref::().unwrap().name() + self.downcast_ref::().unwrap().slot_name() ))); } - Err(vm.new_type_error(format!("'{}' object is not subscriptable", self.class()))) + Err(vm.new_type_error(format!( + "'{}' object is not subscriptable", + self.class().slot_name() + ))) } } @@ -784,8 +801,8 @@ impl PyObject { } Err(vm.new_type_error(format!( - "'{}' does not support item assignment", - self.class() + "'{}' object does not support item assignment", + self.class().slot_name() ))) } @@ -805,7 +822,15 @@ impl PyObject { return f(seq, i, None, vm); } - Err(vm.new_type_error(format!("'{}' does not support item deletion", self.class()))) + // A type carrying a sequence table turns the deletion down in + // PySequence_DelItem's words instead; every heap type carries one. + let name = self.class().slot_name(); + let msg = if seq.slots().has_any() || self.class().heaptype_ext.is_some() { + format!("'{name}' object doesn't support item deletion") + } else { + format!("'{name}' object does not support item deletion") + }; + Err(vm.new_type_error(msg)) } /// Equivalent to CPython's _PyObject_LookupSpecial diff --git a/crates/vm/src/protocol/sequence.rs b/crates/vm/src/protocol/sequence.rs index 774e579ee62..0c0bb503718 100644 --- a/crates/vm/src/protocol/sequence.rs +++ b/crates/vm/src/protocol/sequence.rs @@ -40,6 +40,19 @@ impl PySequenceSlots { self.item.load().is_some() } + /// Whether any slot is filled, which is what a non-null `tp_as_sequence` + /// amounts to for a statically declared type. + pub fn has_any(&self) -> bool { + self.length.load().is_some() + || self.concat.load().is_some() + || self.repeat.load().is_some() + || self.item.load().is_some() + || self.ass_item.load().is_some() + || self.contains.load().is_some() + || self.inplace_concat.load().is_some() + || self.inplace_repeat.load().is_some() + } + /// Copy from static PySequenceMethods pub fn copy_from(&self, methods: &PySequenceMethods) { if let Some(f) = methods.length { @@ -120,7 +133,7 @@ impl PyObject { if seq.check() { Ok(seq) } else { - Err(vm.new_type_error(format!("'{}' is not a sequence", self.class()))) + Err(vm.new_type_error(format!("{} is not a sequence", self.class().slot_name()))) } } } @@ -152,12 +165,17 @@ impl PySequence<'_> { self.slots().length.load().map(|f| f(self, vm)) } + // Py_ssize_t PySequence_Size(PyObject *s) pub fn length(self, vm: &VirtualMachine) -> PyResult { self.length_opt(vm).ok_or_else(|| { - vm.new_type_error(format!( - "'{}' is not a sequence or has no len()", - self.obj.class() - )) + let name = self.obj.class().slot_name(); + // Something that measures itself as a mapping is no sequence at all. + let msg = if self.obj.mapping_unchecked().slots().length.load().is_some() { + format!("{name} is not a sequence") + } else { + format!("object of type '{name}' has no len()") + }; + vm.new_type_error(msg) })? } @@ -176,7 +194,7 @@ impl PySequence<'_> { Err(vm.new_type_error(format!( "'{}' object can't be concatenated", - self.obj.class() + self.obj.class().slot_name() ))) } @@ -193,7 +211,10 @@ impl PySequence<'_> { } } - Err(vm.new_type_error(format!("'{}' object can't be repeated", self.obj.class()))) + Err(vm.new_type_error(format!( + "'{}' object can't be repeated", + self.obj.class().slot_name() + ))) } pub fn inplace_concat(self, other: &PyObject, vm: &VirtualMachine) -> PyResult { @@ -214,7 +235,7 @@ impl PySequence<'_> { Err(vm.new_type_error(format!( "'{}' object can't be concatenated", - self.obj.class() + self.obj.class().slot_name() ))) } @@ -234,7 +255,10 @@ impl PySequence<'_> { } } - Err(vm.new_type_error(format!("'{}' object can't be repeated", self.obj.class()))) + Err(vm.new_type_error(format!( + "'{}' object can't be repeated", + self.obj.class().slot_name() + ))) } pub fn get_item(self, i: isize, vm: &VirtualMachine) -> PyResult { @@ -242,10 +266,21 @@ impl PySequence<'_> { return f(self, i, vm); } - Err(vm.new_type_error(format!( - "'{}' is not a sequence or does not support indexing", - self.obj.class() - ))) + let name = self.obj.class().slot_name(); + // Something that subscripts itself as a mapping is no sequence at all. + let msg = if self + .obj + .mapping_unchecked() + .slots() + .subscript + .load() + .is_some() + { + format!("{name} is not a sequence") + } else { + format!("'{name}' object does not support indexing") + }; + Err(vm.new_type_error(msg)) } fn _ass_item(self, i: isize, value: Option, vm: &VirtualMachine) -> PyResult<()> { @@ -253,15 +288,22 @@ impl PySequence<'_> { return f(self, i, value, vm); } - Err(vm.new_type_error(format!( - "'{}' is not a sequence or doesn't support item {}", - self.obj.class(), - if value.is_some() { - "assignment" - } else { - "deletion" - } - ))) + let name = self.obj.class().slot_name(); + let msg = if self + .obj + .mapping_unchecked() + .slots() + .ass_subscript + .load() + .is_some() + { + format!("{name} is not a sequence") + } else if value.is_some() { + format!("'{name}' object does not support item assignment") + } else { + format!("'{name}' object doesn't support item deletion") + }; + Err(vm.new_type_error(msg)) } pub fn set_item(self, i: isize, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { @@ -281,7 +323,10 @@ impl PySequence<'_> { }; mapping.subscript(&slice.into_pyobject(vm), vm) } else { - Err(vm.new_type_error(format!("'{}' object is unsliceable", self.obj.class()))) + Err(vm.new_type_error(format!( + "'{}' object is unsliceable", + self.obj.class().slot_name() + ))) } } @@ -303,7 +348,7 @@ impl PySequence<'_> { } else { Err(vm.new_type_error(format!( "'{}' object doesn't support slice {}", - self.obj.class(), + self.obj.class().slot_name(), if value.is_some() { "assignment" } else { @@ -413,7 +458,7 @@ impl PySequence<'_> { if e.fast_isinstance(vm.ctx.exceptions.type_error) { vm.new_type_error(format!( "argument of type '{}' is not a container or iterable", - self.obj.class().name() + self.obj.class().slot_name() )) } else { e diff --git a/crates/vm/src/sliceable.rs b/crates/vm/src/sliceable.rs index ef78614efd5..857d8eb3289 100644 --- a/crates/vm/src/sliceable.rs +++ b/crates/vm/src/sliceable.rs @@ -258,30 +258,49 @@ pub enum SequenceIndex { } impl SequenceIndex { + /// The index or slice `obj` stands for, or `None` when it is neither. + fn try_from_object_opt(vm: &VirtualMachine, obj: &PyObject) -> Option> { + const OVERFLOW: &str = "cannot fit 'int' into an index-sized integer"; + if let Some(i) = obj.downcast_ref::() { + // TODO: number protocol + Some( + i.try_to_primitive(vm) + .map_err(|_| vm.new_index_error(OVERFLOW)) + .map(Self::Int), + ) + } else if let Some(slice) = obj.downcast_ref::() { + Some(slice.to_saturated(vm).map(Self::Slice)) + } else { + // TODO: __index__ for indices is no more supported? + obj.try_index_opt(vm).map(|i| { + i?.try_to_primitive(vm) + .map_err(|_| vm.new_index_error(OVERFLOW)) + .map(Self::Int) + }) + } + } + pub fn try_from_borrowed_object( vm: &VirtualMachine, obj: &PyObject, type_name: &str, ) -> PyResult { - if let Some(i) = obj.downcast_ref::() { - // TODO: number protocol - i.try_to_primitive(vm) - .map_err(|_| vm.new_index_error("cannot fit 'int' into an index-sized integer")) - .map(Self::Int) - } else if let Some(slice) = obj.downcast_ref::() { - slice.to_saturated(vm).map(Self::Slice) - } else if let Some(i) = obj.try_index_opt(vm) { - // TODO: __index__ for indices is no more supported? - i?.try_to_primitive(vm) - .map_err(|_| vm.new_index_error("cannot fit 'int' into an index-sized integer")) - .map(Self::Int) - } else { + Self::try_from_object_opt(vm, obj).unwrap_or_else(|| { Err(vm.new_type_error(format!( - "{} indices must be integers or slices or classes that override __index__ operator, not '{}'", - type_name, - obj.class() + "{type_name} indices must be integers or slices, not {}", + obj.class().slot_name() ))) - } + }) + } + + /// `unicode_subscript`, which turns down what it cannot use in its own words. + pub fn try_from_str_subscript(vm: &VirtualMachine, obj: &PyObject) -> PyResult { + Self::try_from_object_opt(vm, obj).unwrap_or_else(|| { + Err(vm.new_type_error(format!( + "string indices must be integers, not '{}'", + obj.class().slot_name() + ))) + }) } } diff --git a/crates/vm/src/stdlib/_interpchannels.rs b/crates/vm/src/stdlib/_interpchannels.rs new file mode 100644 index 00000000000..5d0a5af2463 --- /dev/null +++ b/crates/vm/src/stdlib/_interpchannels.rs @@ -0,0 +1,1298 @@ +//! Low-level cross-interpreter channels (`_interpchannels`). +//! +//! Mirrors CPython `Modules/_interpchannelsmodule.c`. + +#[cfg_attr(not(feature = "threading"), allow(unused_imports))] +pub(crate) use _interpchannels::clear_interpreter; +pub(crate) use _interpchannels::module_def; +pub(crate) use _interpchannels::{channel_id_from_parts, channel_id_parts}; + +#[pymodule] +pub(crate) mod _interpchannels { + use crate::{ + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + builtins::{PyBaseExceptionRef, PyInt, PyMemoryView, PyModule, PyType}, + convert::ToPyObject, + function::{ArgSpec, Either, FuncArgs, PyComparisonValue}, + protocol::{PyNumber, PyNumberMethods}, + types::{AsNumber, Constructor, Hashable, PyComparisonOp, PyStructSequence, Representable}, + vm::crossinterp::{self, Fallback, SharedValue, UNBOUND_REPLACE}, + }; + use alloc::collections::BTreeMap; + use alloc::{collections::VecDeque, sync::Arc}; + use core::time::Duration; + use num_traits::ToPrimitive; + use parking_lot::{Condvar, Mutex}; + use std::{sync::OnceLock, time::Instant}; + + const CHANNEL_SEND: i32 = 1; + const CHANNEL_BOTH: i32 = 0; + const CHANNEL_RECV: i32 = -1; + + #[pyattr] + #[pyexception(name = "ChannelError", base = crate::exceptions::types::PyRuntimeError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyChannelError(crate::exceptions::types::PyRuntimeError); + + #[pyexception] + impl PyChannelError {} + + #[pyattr] + #[pyexception(name = "ChannelNotFoundError", base = PyChannelError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyChannelNotFoundError(PyChannelError); + + #[pyexception] + impl PyChannelNotFoundError {} + + #[pyattr] + #[pyexception(name = "ChannelClosedError", base = PyChannelError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyChannelClosedError(PyChannelError); + + #[pyexception] + impl PyChannelClosedError {} + + #[pyattr] + #[pyexception(name = "ChannelEmptyError", base = PyChannelError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyChannelEmptyError(PyChannelError); + + #[pyexception] + impl PyChannelEmptyError {} + + #[pyattr] + #[pyexception(name = "ChannelNotEmptyError", base = PyChannelError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyChannelNotEmptyError(PyChannelError); + + #[pyexception] + impl PyChannelNotEmptyError {} + + /// The `ERR_CHANNEL_*` codes `handle_channel_error` maps to exceptions. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) enum ChanErr { + NotFound, + Closed, + ClosedWaiting, + InterpClosed, + Empty, + NotEmpty, + } + + impl ChanErr { + fn into_py(self, cid: i64, vm: &VirtualMachine) -> PyBaseExceptionRef { + let (class, msg) = match self { + Self::NotFound => ( + PyChannelNotFoundError::class(&vm.ctx), + format!("channel {cid} not found"), + ), + Self::Closed => ( + PyChannelClosedError::class(&vm.ctx), + format!("channel {cid} is closed"), + ), + Self::ClosedWaiting => ( + PyChannelClosedError::class(&vm.ctx), + format!("channel {cid} has closed"), + ), + Self::InterpClosed => ( + PyChannelClosedError::class(&vm.ctx), + format!("channel {cid} is already closed"), + ), + Self::Empty => ( + PyChannelEmptyError::class(&vm.ctx), + format!("channel {cid} is empty"), + ), + Self::NotEmpty => ( + PyChannelNotEmptyError::class(&vm.ctx), + format!("channel {cid} may not be closed if not empty (try force=True)"), + ), + }; + vm.new_exception_msg(class.to_owned(), msg.into()) + } + } + + type ChanResult = Result; + + /// A blocked `send()`; the receiver hands back whether the item was taken. + struct Waiting { + state: Mutex, + cond: Condvar, + } + + #[derive(Default)] + struct WaitState { + released: bool, + received: bool, + } + + impl Waiting { + fn new() -> Arc { + Arc::new(Self { + state: Mutex::new(WaitState::default()), + cond: Condvar::new(), + }) + } + + fn release(&self, received: bool) { + let mut state = self.state.lock(); + if !state.released { + state.released = true; + state.received = received; + } + self.cond.notify_all(); + } + } + + /// Release waiters with the interpreter detached: the waiting thread holds + /// this mutex across its wait, so an attached taker could never be stopped. + fn release_waiters(waiters: Vec<(Arc, bool)>, vm: &VirtualMachine) { + if waiters.is_empty() { + return; + } + vm.allow_threads(|| { + for (w, received) in waiters { + w.release(received); + } + }); + } + + fn release_waiters_detached(waiters: Vec<(Arc, bool)>) { + for (w, received) in waiters { + w.release(received); + } + } + + struct ChannelItem { + /// The interpreter that added the item to the queue. + interpid: i64, + /// `None` once the owning interpreter is gone (the item is "unbound"). + data: Option, + unboundop: i32, + waiting: Option>, + } + + /// The interpreters bound to each end, in association order. + #[derive(Default)] + struct ChannelEnds { + send: Vec<(i64, bool)>, + recv: Vec<(i64, bool)>, + numsendopen: isize, + numrecvopen: isize, + } + + impl ChannelEnds { + fn list(&mut self, send: bool) -> &mut Vec<(i64, bool)> { + if send { &mut self.send } else { &mut self.recv } + } + + fn find(&self, interpid: i64, send: bool) -> Option { + let list = if send { &self.send } else { &self.recv }; + list.iter().position(|&(id, _)| id == interpid) + } + + fn add(&mut self, interpid: i64, send: bool) { + self.list(send).push((interpid, true)); + if send { + self.numsendopen += 1; + } else { + self.numrecvopen += 1; + } + } + + fn associate(&mut self, interpid: i64, send: bool) -> ChanResult<()> { + match self.find(interpid, send) { + Some(i) => { + if self.list(send)[i].1 { + Ok(()) + } else { + Err(ChanErr::Closed) + } + } + None => { + self.add(interpid, send); + Ok(()) + } + } + } + + fn is_open(&self) -> bool { + if self.numsendopen != 0 || self.numrecvopen != 0 { + return true; + } + // The channel has never had any interpreters associated with it. + self.send.is_empty() && self.recv.is_empty() + } + + fn release_end(&mut self, index: usize, send: bool) { + self.list(send)[index].1 = false; + if send { + self.numsendopen -= 1; + } else { + self.numrecvopen -= 1; + } + } + + /// `which >= 0` releases the send end, `which <= 0` the recv end. + fn release_interpreter(&mut self, interpid: i64, which: i32) { + for (send, apply) in [(true, which >= 0), (false, which <= 0)] { + if !apply { + continue; + } + let index = match self.find(interpid, send) { + Some(i) => i, + None => { + // Never associated, so add it first. + self.add(interpid, send); + self.list(send).len() - 1 + } + }; + self.release_end(index, send); + } + } + + fn release_all(&mut self) { + for send in [true, false] { + for i in 0..self.list(send).len() { + self.release_end(i, send); + } + } + } + + fn clear_interpreter(&mut self, interpid: i64) { + for send in [true, false] { + if let Some(i) = self.find(interpid, send) { + self.release_end(i, send); + } + } + } + } + + struct ChannelState { + queue: VecDeque, + ends: ChannelEnds, + unboundop: i32, + fallback: i32, + open: bool, + /// Send is closed and the queue is draining. + closing: bool, + } + + struct Channel { + state: Mutex, + } + + /// `_channelref`: the channel plus the number of live `ChannelID` objects. + struct ChannelRef { + chan: Option>, + objcount: isize, + } + + struct Channels { + next_id: i64, + refs: BTreeMap, + } + + fn channels() -> &'static Mutex { + static CHANNELS: OnceLock> = OnceLock::new(); + CHANNELS.get_or_init(|| { + Mutex::new(Channels { + next_id: 0, + refs: BTreeMap::new(), + }) + }) + } + + /// `_channels_lookup`: the channel, or why it is unusable. + fn channels_lookup(cid: i64) -> ChanResult> { + let table = channels().lock(); + let entry = table.refs.get(&cid).ok_or(ChanErr::NotFound)?; + let chan = entry.chan.as_ref().ok_or(ChanErr::Closed)?; + if !chan.state.lock().open { + return Err(ChanErr::Closed); + } + Ok(chan.clone()) + } + + fn channel_create(unboundop: i32, fallback: i32) -> i64 { + let mut table = channels().lock(); + let cid = table.next_id; + table.next_id += 1; + table.refs.insert( + cid, + ChannelRef { + chan: Some(Arc::new(Channel { + state: Mutex::new(ChannelState { + queue: VecDeque::new(), + ends: ChannelEnds::default(), + unboundop, + fallback, + open: true, + closing: false, + }), + })), + objcount: 0, + }, + ); + cid + } + + /// `channel_destroy`: forget the channel entirely. + fn channel_destroy(cid: i64) -> ChanResult, bool)>> { + let mut table = channels().lock(); + let entry = table.refs.remove(&cid).ok_or(ChanErr::NotFound)?; + Ok(match entry.chan { + Some(chan) => drain_queue(&mut chan.state.lock()), + None => Vec::new(), + }) + } + + fn drain_queue(state: &mut ChannelState) -> Vec<(Arc, bool)> { + state + .queue + .drain(..) + .filter_map(|item| item.waiting.map(|w| (w, false))) + .collect() + } + + /// `_channels_release_cid_object`: drop a `ChannelID` reference. + fn release_cid_object(cid: i64) -> Vec<(Arc, bool)> { + let mut table = channels().lock(); + let Some(entry) = table.refs.get_mut(&cid) else { + // Already destroyed. + return Vec::new(); + }; + entry.objcount -= 1; + if entry.objcount != 0 { + return Vec::new(); + } + let entry = table.refs.remove(&cid).expect("just looked it up"); + match entry.chan { + Some(chan) => drain_queue(&mut chan.state.lock()), + None => Vec::new(), + } + } + + /// `_channels_add_id_object`. + fn add_cid_object(cid: i64) -> ChanResult<()> { + let mut table = channels().lock(); + let entry = table.refs.get_mut(&cid).ok_or(ChanErr::NotFound)?; + entry.objcount += 1; + Ok(()) + } + + /// `_channel_finish_closing`: a "closing" channel that just went empty is + /// gone for good. + fn finish_closing(cid: i64) { + let mut table = channels().lock(); + let Some(entry) = table.refs.get_mut(&cid) else { + return; + }; + let Some(chan) = entry.chan.as_ref() else { + return; + }; + let done = { + let mut state = chan.state.lock(); + if !state.closing || !state.queue.is_empty() { + false + } else { + state.closing = false; + state.open = false; + true + } + }; + if done { + entry.chan = None; + } + } + + /// `_channels_close`. + fn channel_close(cid: i64, end: i32, force: bool) -> ChanResult, bool)>> { + let mut table = channels().lock(); + let entry = table.refs.get_mut(&cid).ok_or(ChanErr::NotFound)?; + let chan = entry.chan.as_ref().ok_or(ChanErr::Closed)?.clone(); + let mut state = chan.state.lock(); + if !force && end == CHANNEL_SEND && state.closing { + return Err(ChanErr::Closed); + } + if !state.open { + return Err(ChanErr::Closed); + } + if !force && !state.queue.is_empty() { + if end != CHANNEL_SEND { + return Err(ChanErr::NotEmpty); + } + if state.closing { + return Err(ChanErr::Closed); + } + // Mark the channel as closing; it is cleaned up once drained. + state.closing = true; + return Ok(Vec::new()); + } + let waiters = drain_queue(&mut state); + state.open = false; + state.ends.release_all(); + drop(state); + entry.chan = None; + Ok(waiters) + } + + /// `channel_release`: close one or both ends for the current interpreter. + fn channel_release(cid: i64, interpid: i64, send: bool, recv: bool) -> ChanResult<()> { + let chan = channels_lookup(cid)?; + let mut state = chan.state.lock(); + if !state.open { + return Err(ChanErr::Closed); + } + let which = i32::from(send) - i32::from(recv); + state.ends.release_interpreter(interpid, which); + state.open = state.ends.is_open(); + Ok(()) + } + + #[cfg_attr(not(feature = "threading"), allow(dead_code))] + pub(crate) fn clear_interpreter(interpid: i64) { + let chans: Vec> = channels() + .lock() + .refs + .values() + .filter_map(|r| r.chan.clone()) + .collect(); + let mut waiters = Vec::new(); + for chan in chans { + let mut state = chan.state.lock(); + let mut i = 0; + while i < state.queue.len() { + let item = &mut state.queue[i]; + if item.interpid != interpid || item.data.is_none() { + i += 1; + continue; + } + if item.unboundop == crossinterp::UNBOUND_REMOVE { + let item = state.queue.remove(i).expect("index checked"); + if let Some(w) = item.waiting { + waiters.push((w, false)); + } + continue; + } + // UNBOUND_ERROR / UNBOUND_REPLACE keep the slot but throw the + // data away; the item is now "unbound". + item.data = None; + i += 1; + } + state.ends.clear_interpreter(interpid); + state.open = state.ends.is_open(); + } + release_waiters_detached(waiters); + } + + /// `resolve_unboundop`. + fn resolve_unboundop(arg: i32, default: i32, vm: &VirtualMachine) -> PyResult { + if arg < 0 { + return Ok(default); + } + match arg { + crossinterp::UNBOUND_REMOVE + | crossinterp::UNBOUND_ERROR + | crossinterp::UNBOUND_REPLACE => Ok(arg), + _ => Err(vm.new_value_error(format!("unsupported unboundop {arg}"))), + } + } + + /// `resolve_fallback`. + fn resolve_fallback(arg: i32, default: i32, vm: &VirtualMachine) -> PyResult { + let value = if arg < 0 { default } else { arg }; + Fallback::from_i32(value) + .ok_or_else(|| vm.new_value_error(format!("unsupported fallback {arg}"))) + } + + /// `channel_id_converter`. + fn parse_cid(obj: &PyObject, vm: &VirtualMachine) -> PyResult<(i64, i32)> { + if let Some(cid) = obj.downcast_ref::() { + return Ok((cid.cid, cid.end)); + } + if !obj.number().is_index() { + return Err(vm.new_type_error(format!( + "channel ID must be an int, got {}", + obj.class().name() + ))); + } + let n = obj.try_index(vm)?; + let id = n + .as_bigint() + .to_i64() + .ok_or_else(|| vm.new_overflow_error("int too big to convert"))?; + if id < 0 { + let repr = obj.repr(vm)?; + return Err( + vm.new_value_error(format!("channel ID must be a non-negative int, got {repr}")) + ); + } + Ok((id, CHANNEL_BOTH)) + } + + /// The single-`cid` signature shared by several module functions. + fn cid_arg(args: &FuncArgs, func: &'static str, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: func, + keywords: &["cid"], + required: 1, + max_positional: 1, + } + .parse(args, vm)?; + Ok(parse_cid(parsed[0].as_deref().unwrap(), vm)?.0) + } + + /// The `(cid, *, send=False, recv=False, force=False)` signature shared by + /// `channel_close` and `channel_release`. + fn end_args( + args: &FuncArgs, + func: &'static str, + vm: &VirtualMachine, + ) -> PyResult>> { + ArgSpec { + fname: func, + keywords: &["cid", "send", "recv", "force"], + required: 1, + max_positional: 1, + } + .parse_with( + args, + |i, obj, vm| match i { + 0 => parse_cid(obj, vm).map(drop), + _ => Ok(()), + }, + vm, + ) + } + + /// The `p` converter: a predicate that only reads truthiness. + fn flag(slot: Option<&PyObjectRef>, vm: &VirtualMachine) -> PyResult { + match slot { + Some(o) => o.clone().is_true(vm), + None => Ok(false), + } + } + + /// The `i` converter. + fn int_arg(slot: Option<&PyObject>, vm: &VirtualMachine) -> PyResult> { + slot.map(|o| { + let ival = o.try_index(vm)?.as_bigint().to_i64().ok_or_else(|| { + vm.new_overflow_error("Python int too large to convert to C long") + })?; + i32::try_from(ival).map_err(|_| { + let msg = if ival > i32::MAX as i64 { + "signed integer is greater than maximum" + } else { + "signed integer is less than minimum" + }; + vm.new_overflow_error(msg) + }) + }) + .transpose() + } + + #[pyattr] + #[pyclass(name = "ChannelID", module = "_interpchannels")] + #[derive(Debug, PyPayload)] + pub(crate) struct ChannelID { + cid: i64, + end: i32, + resolve: bool, + } + + impl Drop for ChannelID { + fn drop(&mut self) { + release_waiters_detached(release_cid_object(self.cid)); + } + } + + impl Representable for ChannelID { + #[inline] + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let name = zelf.class().name().to_string(); + Ok(match zelf.end { + CHANNEL_SEND => format!("{name}({}, send=True)", zelf.cid), + CHANNEL_RECV => format!("{name}({}, recv=True)", zelf.cid), + _ => format!("{name}({})", zelf.cid), + }) + } + } + + impl Hashable for ChannelID { + #[inline] + fn hash(zelf: &Py, vm: &VirtualMachine) -> PyResult { + vm.ctx.new_int(zelf.cid).as_object().hash(vm) + } + } + + impl AsNumber for ChannelID { + fn as_number() -> &'static PyNumberMethods { + static METHODS: PyNumberMethods = PyNumberMethods { + int: Some(|obj, vm| { + let zelf = obj.downcast_ref::().unwrap(); + Ok(vm.ctx.new_int(zelf.cid).into()) + }), + index: Some(|obj, vm| { + let zelf = obj.downcast_ref::().unwrap(); + Ok(vm.ctx.new_int(zelf.cid).into()) + }), + ..PyNumberMethods::NOT_IMPLEMENTED + }; + &METHODS + } + } + + #[pyclass( + with(Representable, Hashable, AsNumber), + flags(BASETYPE, IMMUTABLETYPE, DISALLOW_INSTANTIATION) + )] + impl ChannelID { + /// `channelid_richcompare`. + #[pyslot] + fn slot_richcompare( + zelf: &PyObject, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult> { + if !matches!(op, PyComparisonOp::Eq | PyComparisonOp::Ne) { + return Ok(Either::B(PyComparisonValue::NotImplemented)); + } + let Some(zelf) = zelf.downcast_ref::() else { + return Ok(Either::B(PyComparisonValue::NotImplemented)); + }; + let equal = if let Some(o) = other.downcast_ref::() { + zelf.end == o.end && zelf.cid == o.cid + } else if let Some(n) = other.downcast_ref::() { + // Fast path + n.try_to_primitive::(vm) + .is_ok_and(|v| v >= 0 && v == zelf.cid) + } else if PyNumber::check(other) { + let id_obj: PyObjectRef = vm.ctx.new_int(zelf.cid).into(); + return id_obj.rich_compare(other.to_owned(), op, vm).map(Either::A); + } else { + return Ok(Either::B(PyComparisonValue::NotImplemented)); + }; + Ok(Either::B(PyComparisonValue::Implemented( + (op == PyComparisonOp::Eq) == equal, + ))) + } + + #[pygetset] + fn end(&self) -> String { + match self.end { + CHANNEL_SEND => "send".to_owned(), + CHANNEL_RECV => "recv".to_owned(), + _ => "both".to_owned(), + } + } + + #[pygetset(name = "send")] + fn send_end(&self, vm: &VirtualMachine) -> PyResult { + channel_id_from_parts(self.cid, CHANNEL_SEND, true, self.resolve, vm) + } + + #[pygetset(name = "recv")] + fn recv_end(&self, vm: &VirtualMachine) -> PyResult { + channel_id_from_parts(self.cid, CHANNEL_RECV, true, self.resolve, vm) + } + + #[pymethod] + fn __str__(&self) -> String { + self.cid.to_string() + } + } + + /// `newchannelid`. + pub(crate) fn channel_id_from_parts( + cid: i64, + end: i32, + force: bool, + resolve: bool, + vm: &VirtualMachine, + ) -> PyResult { + match add_cid_object(cid) { + Ok(()) => {} + Err(ChanErr::NotFound) if force => {} + Err(e) => return Err(e.into_py(cid, vm)), + } + Ok(ChannelID { cid, end, resolve }.to_pyobject(vm)) + } + + pub(crate) fn channel_id_parts(obj: &PyObject) -> Option<(i64, i32)> { + obj.downcast_ref::().map(|c| (c.cid, c.end)) + } + + /// `_channelid_new`. + fn channel_id_new(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "ChannelID.__new__", + keywords: &["id", "send", "recv", "force", "_resolve"], + required: 1, + max_positional: 1, + } + .parse_with( + &args, + |i, obj, vm| match i { + 0 => parse_cid(obj, vm).map(drop), + _ => Ok(()), + }, + vm, + )?; + let (cid, mut end) = parse_cid(parsed[0].as_deref().unwrap(), vm)?; + let tri = |slot: Option<&PyObjectRef>| -> PyResult> { + slot.map(|o| o.clone().is_true(vm)).transpose() + }; + let send = tri(parsed[1].as_ref())?; + let recv = tri(parsed[2].as_ref())?; + let force = flag(parsed[3].as_ref(), vm)?; + let resolve = flag(parsed[4].as_ref(), vm)?; + match (send, recv) { + (Some(false), Some(false)) => { + return Err(vm.new_value_error("'send' and 'recv' cannot both be False")); + } + (Some(true), Some(true)) => end = CHANNEL_BOTH, + (Some(true), _) => end = CHANNEL_SEND, + (_, Some(true)) => end = CHANNEL_RECV, + _ => {} + } + channel_id_from_parts(cid, end, force, resolve, vm) + } + + #[pystruct_sequence_data] + struct ChannelInfoData { + open: bool, + closing: bool, + closed: bool, + count: i64, + num_interp_send: isize, + num_interp_send_released: isize, + num_interp_recv: isize, + num_interp_recv_released: isize, + #[pystruct_sequence(skip)] + num_interp_both: isize, + #[pystruct_sequence(skip)] + num_interp_both_released: isize, + #[pystruct_sequence(skip)] + num_interp_both_send_released: isize, + #[pystruct_sequence(skip)] + num_interp_both_recv_released: isize, + #[pystruct_sequence(skip)] + send_associated: bool, + #[pystruct_sequence(skip)] + send_released: bool, + #[pystruct_sequence(skip)] + recv_associated: bool, + #[pystruct_sequence(skip)] + recv_released: bool, + } + + #[pyattr] + #[pystruct_sequence( + name = "ChannelInfo", + module = "_interpchannels", + data = "ChannelInfoData" + )] + struct PyChannelInfo; + + #[pyclass(with(PyStructSequence))] + impl PyChannelInfo {} + + #[pyfunction] + fn create(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "create", + keywords: &["unboundop", "fallback"], + required: 0, + max_positional: 2, + } + .parse(&args, vm)?; + let unboundarg = int_arg(parsed[0].as_deref(), vm)?.unwrap_or(-1); + let fallbackarg = int_arg(parsed[1].as_deref(), vm)?.unwrap_or(-1); + let unboundop = resolve_unboundop(unboundarg, UNBOUND_REPLACE, vm)?; + let fallback = resolve_fallback(fallbackarg, Fallback::Full.as_i32(), vm)?; + let cid = channel_create(unboundop, fallback.as_i32()); + channel_id_from_parts(cid, CHANNEL_BOTH, false, false, vm) + } + + #[pyfunction] + fn destroy(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let cid = cid_arg(&args, "channel_destroy", vm)?; + let waiters = channel_destroy(cid).map_err(|e| e.into_py(cid, vm))?; + release_waiters(waiters, vm); + Ok(()) + } + + #[pyfunction] + fn list_all(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if !args.args.is_empty() || !args.kwargs.is_empty() { + return Err(vm.new_type_error(format!( + "_interpchannels.list_all() takes no arguments ({} given)", + args.args.len() + args.kwargs.len() + ))); + } + let chans: Vec<(i64, i32, i32)> = channels() + .lock() + .refs + .iter() + .filter_map(|(&cid, r)| { + let state = r.chan.as_ref()?.state.lock(); + Some((cid, state.unboundop, state.fallback)) + }) + .collect(); + let mut items = Vec::with_capacity(chans.len()); + for (cid, unboundop, fallback) in chans { + items.push( + vm.ctx + .new_tuple(vec![ + channel_id_from_parts(cid, CHANNEL_BOTH, false, false, vm)?, + vm.ctx.new_int(unboundop).into(), + vm.ctx.new_int(fallback).into(), + ]) + .into(), + ); + } + Ok(vm.ctx.new_list(items).into()) + } + + #[pyfunction] + fn list_interpreters(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "channel_list_interpreters", + keywords: &["cid", "send"], + required: 2, + max_positional: 1, + } + .parse_with( + &args, + |i, obj, vm| match i { + 0 => parse_cid(obj, vm).map(drop), + _ => Ok(()), + }, + vm, + )?; + let cid = parse_cid(parsed[0].as_deref().unwrap(), vm)?.0; + let send = flag(parsed[1].as_ref(), vm)?; + let chan = channels_lookup(cid).map_err(|e| e.into_py(cid, vm))?; + let state = chan.state.lock(); + if send && state.closing { + return Err(ChanErr::Closed.into_py(cid, vm)); + } + let mut out = Vec::new(); + for info in crate::vm::runtime::list_interpreters() { + if state.ends.find(info.id, send).is_some_and(|i| { + if send { + state.ends.send[i].1 + } else { + state.ends.recv[i].1 + } + }) { + out.push(vm.ctx.new_int(info.id).into()); + } + } + Ok(vm.ctx.new_list(out).into()) + } + + /// `channel_send` up to its `closing` check, which precedes converting the + /// object to cross-interpreter data. + fn send_begin(cid: i64, vm: &VirtualMachine) -> PyResult> { + let chan = channels_lookup(cid).map_err(|e| e.into_py(cid, vm))?; + if chan.state.lock().closing { + return Err(ChanErr::Closed.into_py(cid, vm)); + } + Ok(chan) + } + + #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + crate::stdlib::_interpreters::init_xi_types(vm); + __module_exec(vm, module); + Ok(()) + } + + /// `_channel_add` plus, when `waiting` is set, `channel_send_wait`. + fn do_send( + chan: &Channel, + cid: i64, + value: SharedValue, + unboundop: i32, + blocking: bool, + timeout: Option, + vm: &VirtualMachine, + ) -> PyResult<()> { + let waiting = blocking.then(Waiting::new); + { + let mut state = chan.state.lock(); + if !state.open { + return Err(ChanErr::Closed.into_py(cid, vm)); + } + state + .ends + .associate(vm.state.interpreter_id, true) + .map_err(|_| ChanErr::InterpClosed.into_py(cid, vm))?; + state.queue.push_back(ChannelItem { + interpid: vm.state.interpreter_id, + data: Some(value), + unboundop, + waiting: waiting.clone(), + }); + } + let Some(waiting) = waiting else { + return Ok(()); + }; + + let deadline = timeout.map(|t| Instant::now() + t); + let timed_out = vm.allow_threads(|| { + let mut state = waiting.state.lock(); + loop { + if state.released { + return false; + } + let Some(deadline) = deadline else { + waiting.cond.wait(&mut state); + continue; + }; + let now = Instant::now(); + if now >= deadline || waiting.cond.wait_until(&mut state, deadline).timed_out() { + return !state.released; + } + } + }); + + if timed_out { + // The send is failing now, so make sure the item won't be received. + let mut state = chan.state.lock(); + if let Some(pos) = state.queue.iter().position(|it| { + it.waiting + .as_ref() + .is_some_and(|w| Arc::ptr_eq(w, &waiting)) + }) { + state.queue.remove(pos); + } + drop(state); + finish_closing(cid); + if !waiting.state.lock().received { + return Err(vm + .new_os_subtype_error( + vm.ctx.exceptions.timeout_error.to_owned(), + None, + "timed out", + ) + .upcast()); + } + return Ok(()); + } + if !waiting.state.lock().received { + return Err(ChanErr::ClosedWaiting.into_py(cid, vm)); + } + Ok(()) + } + + fn send_args( + args: &FuncArgs, + func: &'static str, + vm: &VirtualMachine, + ) -> PyResult<(i64, PyObjectRef, i32, Fallback, bool, Option)> { + let parsed = ArgSpec { + fname: func, + keywords: &["cid", "obj", "unboundop", "fallback", "blocking", "timeout"], + required: 2, + max_positional: 4, + } + .parse_with( + args, + |i, obj, vm| match i { + 0 => parse_cid(obj, vm).map(drop), + 2 | 3 => int_arg(Some(obj), vm).map(drop), + _ => Ok(()), + }, + vm, + )?; + let cid = parse_cid(parsed[0].as_deref().unwrap(), vm)?.0; + let obj = parsed[1].clone().unwrap(); + let unboundarg = int_arg(parsed[2].as_deref(), vm)?.unwrap_or(-1); + let fallbackarg = int_arg(parsed[3].as_deref(), vm)?.unwrap_or(-1); + let blocking = match &parsed[4] { + Some(o) => o.clone().is_true(vm)?, + None => true, + }; + let timeout = parse_timeout(parsed[5].as_deref(), blocking, vm)?; + // The channel is only consulted when one of the arguments needs its default. + let (default_unboundop, default_fallback) = if unboundarg < 0 || fallbackarg < 0 { + let chan = channels_lookup(cid).map_err(|e| e.into_py(cid, vm))?; + let state = chan.state.lock(); + (state.unboundop, state.fallback) + } else { + (-1, -1) + }; + let unboundop = resolve_unboundop(unboundarg, default_unboundop, vm)?; + let fallback = resolve_fallback(fallbackarg, default_fallback, vm)?; + Ok((cid, obj, unboundop, fallback, blocking, timeout)) + } + + #[pyfunction] + fn send(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let (cid, obj, unboundop, fallback, blocking, timeout) = + send_args(&args, "channel_send", vm)?; + let chan = send_begin(cid, vm)?; + let value = SharedValue::from_object(&obj, fallback, vm)?; + do_send(&chan, cid, value, unboundop, blocking, timeout, vm) + } + + #[pyfunction] + fn send_buffer(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let (cid, obj, unboundop, fallback, blocking, timeout) = + send_args(&args, "channel_send_buffer", vm)?; + // The buffer is wrapped in a memoryview, and that is what gets shared. + let view = PyMemoryView::from_object(&obj, vm)?.into_pyobject(vm); + let chan = send_begin(cid, vm)?; + let value = SharedValue::from_object(&view, fallback, vm)?; + do_send(&chan, cid, value, unboundop, blocking, timeout, vm) + } + + /// `PyThread_ParseTimeoutArg`. + fn parse_timeout( + timeout: Option<&PyObject>, + blocking: bool, + vm: &VirtualMachine, + ) -> PyResult> { + let Some(t) = timeout.filter(|t| !vm.is_none(t)) else { + return Ok(None); + }; + if !blocking { + return Err(vm.new_value_error("can't specify a timeout for a non-blocking call")); + } + let secs = t.try_float(vm)?.to_f64(); + if secs < 0.0 { + return Err(vm.new_value_error("timeout value must be a non-negative number")); + } + Ok(Some(Duration::from_secs_f64(secs))) + } + + #[pyfunction] + fn recv(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "channel_recv", + keywords: &["cid", "default"], + required: 1, + max_positional: 2, + } + .parse(&args, vm)?; + let cid = parse_cid(parsed[0].as_deref().unwrap(), vm)?.0; + let default = parsed[1].as_deref(); + let popped = channel_next(cid, vm.state.interpreter_id); + finish_closing(cid); + let item = match popped { + Ok(item) => item, + Err(ChanErr::Empty) => match default { + Some(d) => return Ok(vm.ctx.new_tuple(vec![d.to_owned(), vm.ctx.none()]).into()), + None => return Err(ChanErr::Empty.into_py(cid, vm)), + }, + Err(e) => return Err(e.into_py(cid, vm)), + }; + let (data, unboundop, waiting) = item; + let Some(data) = data else { + // The item was unbound. + return Ok(vm + .ctx + .new_tuple(vec![vm.ctx.none(), vm.ctx.new_int(unboundop).into()]) + .into()); + }; + let obj = data.into_object(vm)?; + if let Some(w) = waiting { + release_waiters(vec![(w, true)], vm); + } + Ok(vm.ctx.new_tuple(vec![obj, vm.ctx.none()]).into()) + } + + /// `_channel_next`. + #[allow(clippy::type_complexity)] + fn channel_next( + cid: i64, + interpid: i64, + ) -> ChanResult<(Option, i32, Option>)> { + let chan = channels_lookup(cid)?; + let mut state = chan.state.lock(); + if !state.open { + return Err(ChanErr::Closed); + } + state + .ends + .associate(interpid, false) + .map_err(|_| ChanErr::InterpClosed)?; + match state.queue.pop_front() { + Some(item) => Ok((item.data, item.unboundop, item.waiting)), + None => { + if state.closing { + state.open = false; + } + Err(ChanErr::Empty) + } + } + } + + #[pyfunction] + fn close(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let parsed = end_args(&args, "channel_close", vm)?; + let cid = parse_cid(parsed[0].as_deref().unwrap(), vm)?.0; + let send = flag(parsed[1].as_ref(), vm)?; + let recv = flag(parsed[2].as_ref(), vm)?; + let force = flag(parsed[3].as_ref(), vm)?; + let end = i32::from(send) - i32::from(recv); + let waiters = channel_close(cid, end, force).map_err(|e| e.into_py(cid, vm))?; + release_waiters(waiters, vm); + Ok(()) + } + + #[pyfunction] + fn release(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let parsed = end_args(&args, "channel_release", vm)?; + let cid = parse_cid(parsed[0].as_deref().unwrap(), vm)?.0; + let mut send = flag(parsed[1].as_ref(), vm)?; + let mut recv = flag(parsed[2].as_ref(), vm)?; + if !send && !recv { + send = true; + recv = true; + } + channel_release(cid, vm.state.interpreter_id, send, recv).map_err(|e| e.into_py(cid, vm)) + } + + #[pyfunction] + fn get_count(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let cid = cid_arg(&args, "get_count", vm)?; + let chan = channels_lookup(cid).map_err(|e| e.into_py(cid, vm))?; + let count = chan.state.lock().queue.len(); + Ok(count) + } + + #[pyfunction] + fn get_info(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let cid = cid_arg(&args, "_get_info", vm)?; + let interpid = vm.state.interpreter_id; + let table = channels().lock(); + let entry = table + .refs + .get(&cid) + .ok_or_else(|| ChanErr::NotFound.into_py(cid, vm))?; + let mut info = ChannelInfoData { + open: false, + closing: false, + closed: true, + count: 0, + num_interp_send: 0, + num_interp_send_released: 0, + num_interp_recv: 0, + num_interp_recv_released: 0, + num_interp_both: 0, + num_interp_both_released: 0, + num_interp_both_send_released: 0, + num_interp_both_recv_released: 0, + send_associated: false, + send_released: false, + recv_associated: false, + recv_released: false, + }; + let Some(chan) = entry.chan.as_ref() else { + return Ok(info); + }; + let state = chan.state.lock(); + if !state.open { + return Ok(info); + } + info.closed = false; + info.closing = state.closing; + info.open = !state.closing; + info.count = state.queue.len() as i64; + + for &(id, open) in &state.ends.send { + if id == interpid { + info.send_associated = open; + info.send_released = !open; + } + if open { + info.num_interp_send += 1; + } else { + info.num_interp_send_released += 1; + } + } + for &(id, recv_open) in &state.ends.recv { + if id == interpid { + info.recv_associated = recv_open; + info.recv_released = !recv_open; + } + match state.ends.find(id, true).map(|i| state.ends.send[i].1) { + None => { + if recv_open { + info.num_interp_recv += 1; + } else { + info.num_interp_recv_released += 1; + } + } + Some(send_open) => match (recv_open, send_open) { + (true, true) => { + info.num_interp_both += 1; + info.num_interp_send -= 1; + } + (true, false) => { + info.num_interp_both_recv_released += 1; + info.num_interp_send_released -= 1; + } + (false, true) => { + info.num_interp_both_send_released += 1; + info.num_interp_send -= 1; + } + (false, false) => { + info.num_interp_both_released += 1; + info.num_interp_send_released -= 1; + } + }, + } + } + Ok(info) + } + + #[pyfunction] + fn get_channel_defaults(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let cid = cid_arg(&args, "get_channel_defaults", vm)?; + let chan = channels_lookup(cid).map_err(|e| e.into_py(cid, vm))?; + let (unboundop, fallback) = { + let state = chan.state.lock(); + (state.unboundop, state.fallback) + }; + Ok(vm + .ctx + .new_tuple(vec![ + vm.ctx.new_int(unboundop).into(), + vm.ctx.new_int(fallback).into(), + ]) + .into()) + } + + #[pyfunction] + fn _channel_id(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + channel_id_new(args, vm) + } + + #[pyfunction] + fn _register_end_types(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let parsed = ArgSpec { + fname: "_register_end_types", + keywords: &["send", "recv"], + required: 2, + max_positional: 2, + } + .parse(&args, vm)?; + for (slot, name) in parsed.iter().zip(["send", "recv"]) { + if !slot.as_ref().unwrap().downcastable::() { + return Err(vm.new_type_error(format!("expected a type for '{name}'"))); + } + } + Ok(()) + } +} diff --git a/crates/vm/src/stdlib/_interpqueues.rs b/crates/vm/src/stdlib/_interpqueues.rs new file mode 100644 index 00000000000..e4075f88811 --- /dev/null +++ b/crates/vm/src/stdlib/_interpqueues.rs @@ -0,0 +1,678 @@ +//! Low-level cross-interpreter queues (`_interpqueues`). + +#[cfg_attr(not(feature = "threading"), allow(unused_imports))] +pub(crate) use _interpqueues::clear_interpreter; +pub(crate) use _interpqueues::{ + is_external_queue, module_def, queue_from_xid, queue_id_from_object, queue_xid_decref, + queue_xid_incref, +}; + +#[pymodule] +pub(crate) mod _interpqueues { + use crate::{ + AsObject, Py, PyObject, PyPayload, PyResult, VirtualMachine, + builtins::{PyBaseExceptionRef, PyModule, PyType, PyTypeRef}, + function::{ArgSpec, FuncArgs}, + types::Constructor, + vm::crossinterp::{self, Fallback, SharedValue, UNBOUND_REMOVE, UNBOUND_REPLACE}, + }; + use alloc::{collections::BTreeMap, collections::VecDeque, sync::Arc}; + use core::cell::Cell; + use num_traits::{Signed, ToPrimitive}; + use parking_lot::Mutex; + use std::sync::OnceLock; + + #[pyattr] + #[pyexception(name = "QueueError", module = "concurrent.interpreters", base = crate::exceptions::types::PyRuntimeError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyQueueError(crate::exceptions::types::PyRuntimeError); + + #[pyexception] + impl PyQueueError {} + + #[pyattr] + #[pyexception(name = "QueueNotFoundError", module = "concurrent.interpreters", base = PyQueueError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyQueueNotFoundError(PyQueueError); + + #[pyexception] + impl PyQueueNotFoundError {} + + /// The failures the queue store itself can report, all of which map to + /// exceptions owned by this module. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum QueueErr { + NoNextId, + NotFound, + NeverBound, + } + + impl QueueErr { + fn into_py(self, qid: i64, vm: &VirtualMachine) -> PyBaseExceptionRef { + let (class, msg) = match self { + Self::NoNextId => ( + PyQueueError::class(&vm.ctx).to_owned(), + "ran out of queue IDs".to_owned(), + ), + Self::NotFound => ( + PyQueueNotFoundError::class(&vm.ctx).to_owned(), + format!("queue {qid} not found"), + ), + Self::NeverBound => ( + PyQueueError::class(&vm.ctx).to_owned(), + format!("queue {qid} never bound"), + ), + }; + vm.new_exception_msg(class, msg.into()) + } + } + + /// `queue.Empty`, which belongs to the `_queues` module and is therefore + /// only reachable once that module has been imported. + fn queue_empty_error(qid: i64, vm: &VirtualMachine) -> PyResult { + let class = ensure_external_types(vm)?.empty; + Ok(vm.new_exception_msg(class, format!("queue {qid} is empty").into())) + } + + /// `queue.Full`, which belongs to the `_queues` module and is therefore + /// only reachable once that module has been imported. + fn queue_full_error(qid: i64, vm: &VirtualMachine) -> PyResult { + let class = ensure_external_types(vm)?.full; + Ok(vm.new_exception_msg(class, format!("queue {qid} is full").into())) + } + + type QueueResult = Result; + + #[derive(Clone)] + struct ExternalTypes { + queue: PyTypeRef, + empty: PyTypeRef, + full: PyTypeRef, + } + + #[cfg(feature = "threading")] + fn external_types() -> &'static Mutex> { + static TYPES: OnceLock>> = OnceLock::new(); + TYPES.get_or_init(|| Mutex::new(BTreeMap::new())) + } + + #[cfg(not(feature = "threading"))] + std::thread_local! { + static EXTERNAL_TYPES: core::cell::RefCell> = + const { core::cell::RefCell::new(BTreeMap::new()) }; + } + + fn get_external_types(interpid: i64) -> Option { + #[cfg(feature = "threading")] + { + external_types().lock().get(&interpid).cloned() + } + #[cfg(not(feature = "threading"))] + { + EXTERNAL_TYPES.with(|types| types.borrow().get(&interpid).cloned()) + } + } + + fn set_external_types(interpid: i64, types: ExternalTypes) { + #[cfg(feature = "threading")] + { + external_types().lock().insert(interpid, types); + } + #[cfg(not(feature = "threading"))] + { + EXTERNAL_TYPES.with(|registered| { + registered.borrow_mut().insert(interpid, types); + }); + } + } + + #[cfg_attr(not(feature = "threading"), allow(dead_code))] + fn remove_external_types(interpid: i64) { + #[cfg(feature = "threading")] + { + external_types().lock().remove(&interpid); + } + #[cfg(not(feature = "threading"))] + { + EXTERNAL_TYPES.with(|types| { + types.borrow_mut().remove(&interpid); + }); + } + } + + fn ensure_highlevel_module_loaded(vm: &VirtualMachine) -> PyResult<()> { + vm.import("concurrent.interpreters._queues", 0).map(drop) + } + + fn ensure_external_types(vm: &VirtualMachine) -> PyResult { + let interpid = vm.state.interpreter_id; + if let Some(types) = get_external_types(interpid) { + return Ok(types); + } + ensure_highlevel_module_loaded(vm)?; + get_external_types(interpid) + .ok_or_else(|| vm.new_runtime_error("queue types were not registered")) + } + + #[cfg_attr(not(feature = "threading"), allow(dead_code))] + struct QueueItem { + interpid: i64, + data: Option, + unboundop: i32, + } + + struct Queue { + alive: bool, + maxsize: isize, + items: VecDeque, + unboundop: i32, + fallback: Fallback, + } + + impl Queue { + fn is_full(&self) -> bool { + self.maxsize > 0 && self.items.len() >= self.maxsize as usize + } + } + + struct QueueRef { + queue: Arc>, + refcount: isize, + } + + struct Queues { + refs: BTreeMap, + next_id: i64, + } + + fn queues() -> &'static Mutex { + static QUEUES: OnceLock> = OnceLock::new(); + QUEUES.get_or_init(|| { + Mutex::new(Queues { + refs: BTreeMap::new(), + next_id: 1, + }) + }) + } + + fn queue_lookup(qid: i64) -> QueueResult>> { + queues() + .lock() + .refs + .get(&qid) + .map(|r| r.queue.clone()) + .ok_or(QueueErr::NotFound) + } + + fn queue_create(maxsize: isize, unboundop: i32, fallback: Fallback) -> QueueResult { + let mut queues = queues().lock(); + let qid = queues.next_id; + if qid < 0 { + return Err(QueueErr::NoNextId); + } + queues.next_id = qid.checked_add(1).unwrap_or(-1); + queues.refs.insert( + qid, + QueueRef { + queue: Arc::new(Mutex::new(Queue { + alive: true, + maxsize, + items: VecDeque::new(), + unboundop, + fallback, + })), + refcount: 0, + }, + ); + Ok(qid) + } + + fn kill_queue(queue: &Arc>) { + queue.lock().alive = false; + } + + fn queue_destroy(qid: i64) -> QueueResult<()> { + let queue = queues() + .lock() + .refs + .remove(&qid) + .ok_or(QueueErr::NotFound)?; + kill_queue(&queue.queue); + drop(queue); + Ok(()) + } + + fn queue_defaults(qid: i64) -> QueueResult<(i32, Fallback)> { + let queue = queue_lookup(qid)?; + let state = queue.lock(); + if !state.alive { + return Err(QueueErr::NotFound); + } + Ok((state.unboundop, state.fallback)) + } + + fn queue_bind(qid: i64) -> QueueResult<()> { + let mut queues = queues().lock(); + let queue = queues.refs.get_mut(&qid).ok_or(QueueErr::NotFound)?; + queue.refcount += 1; + Ok(()) + } + + fn queue_release(qid: i64) -> QueueResult<()> { + let removed = { + let mut queues = queues().lock(); + let queue = queues.refs.get_mut(&qid).ok_or(QueueErr::NotFound)?; + if queue.refcount == 0 { + return Err(QueueErr::NeverBound); + } + queue.refcount -= 1; + if queue.refcount == 0 { + queues.refs.remove(&qid) + } else { + None + } + }; + if let Some(queue) = removed { + kill_queue(&queue.queue); + drop(queue); + } + Ok(()) + } + + fn resolve_unboundop(arg: i32, default: i32, vm: &VirtualMachine) -> PyResult { + if arg < 0 { + return Ok(default); + } + match arg { + crossinterp::UNBOUND_REMOVE + | crossinterp::UNBOUND_ERROR + | crossinterp::UNBOUND_REPLACE => Ok(arg), + _ => Err(vm.new_value_error(format!("unsupported unboundop {arg}"))), + } + } + + fn resolve_fallback(arg: i32, default: i32, vm: &VirtualMachine) -> PyResult { + let value = if arg < 0 { default } else { arg }; + Fallback::from_i32(value) + .ok_or_else(|| vm.new_value_error(format!("unsupported fallback {arg}"))) + } + + fn int_arg(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + let value = + obj.try_index(vm)?.as_bigint().to_i64().ok_or_else(|| { + vm.new_overflow_error("Python int too large to convert to C long") + })?; + i32::try_from(value).map_err(|_| { + let msg = if value > i32::MAX as i64 { + "signed integer is greater than maximum" + } else { + "signed integer is less than minimum" + }; + vm.new_overflow_error(msg) + }) + } + + fn ssize_arg(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + obj.try_index(vm)? + .as_bigint() + .to_isize() + .ok_or_else(|| vm.new_overflow_error("Python int too large to convert to C ssize_t")) + } + + fn parse_qid(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + if !obj.number().is_index() { + return Err(vm.new_type_error(format!( + "queue ID must be an int, got {}", + obj.class().name() + ))); + } + let value = obj.try_index(vm)?; + let bigint = value.as_bigint(); + if bigint.is_negative() { + let repr = obj.repr(vm)?; + return Err( + vm.new_value_error(format!("queue ID must be a non-negative int, got {repr}")) + ); + } + if let Some(qid) = bigint.to_i64() { + return Ok(qid); + } + let repr = obj.repr(vm)?; + Err(vm.new_overflow_error(format!("max queue ID is {}, got {repr}", i64::MAX))) + } + + fn qid_arg(args: &FuncArgs, func: &'static str, vm: &VirtualMachine) -> PyResult { + let qid = Cell::new(None); + ArgSpec { + fname: func, + keywords: &["qid"], + required: 1, + max_positional: 1, + } + .parse_with( + args, + |_, obj, vm| { + qid.set(Some(parse_qid(obj, vm)?)); + Ok(()) + }, + vm, + )?; + Ok(qid.get().unwrap()) + } + + #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + crate::stdlib::_interpreters::init_xi_types(vm); + __module_exec(vm, module); + Ok(()) + } + + #[pyfunction] + fn create(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let maxsize = Cell::new(None); + let unboundarg = Cell::new(None); + let fallbackarg = Cell::new(None); + ArgSpec { + fname: "create", + keywords: &["maxsize", "unboundop", "fallback"], + required: 1, + max_positional: 3, + } + .parse_with( + &args, + |i, obj, vm| { + match i { + 0 => maxsize.set(Some(ssize_arg(obj, vm)?)), + 1 => unboundarg.set(Some(int_arg(obj, vm)?)), + 2 => fallbackarg.set(Some(int_arg(obj, vm)?)), + _ => unreachable!(), + } + Ok(()) + }, + vm, + )?; + let unboundop = resolve_unboundop(unboundarg.get().unwrap_or(-1), UNBOUND_REPLACE, vm)?; + let fallback = + resolve_fallback(fallbackarg.get().unwrap_or(-1), Fallback::Full.as_i32(), vm)?; + queue_create(maxsize.get().unwrap(), unboundop, fallback).map_err(|err| err.into_py(-1, vm)) + } + + #[pyfunction] + fn destroy(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let qid = qid_arg(&args, "destroy", vm)?; + queue_destroy(qid).map_err(|err| err.into_py(qid, vm)) + } + + #[pyfunction] + fn list_all(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if !args.kwargs.is_empty() { + return Err(vm.new_type_error("_interpqueues.list_all() takes no keyword arguments")); + } + if !args.args.is_empty() { + return Err(vm.new_type_error(format!( + "_interpqueues.list_all() takes no arguments ({} given)", + args.args.len() + ))); + } + let entries: Vec<_> = queues() + .lock() + .refs + .iter() + .rev() + .map(|(&qid, queue)| (qid, queue.queue.clone())) + .collect(); + let mut result = Vec::with_capacity(entries.len()); + for (qid, queue) in entries { + let state = queue.lock(); + if state.alive { + result.push( + vm.ctx + .new_tuple(vec![ + vm.ctx.new_int(qid).into(), + vm.ctx.new_int(state.unboundop).into(), + vm.ctx.new_int(state.fallback.as_i32()).into(), + ]) + .into(), + ); + } + } + Ok(vm.ctx.new_list(result).into()) + } + + #[pyfunction] + fn put(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let qid = Cell::new(None); + let unboundarg = Cell::new(None); + let fallbackarg = Cell::new(None); + let parsed = ArgSpec { + fname: "put", + keywords: &["qid", "obj", "unboundop", "fallback"], + required: 2, + max_positional: 4, + } + .parse_with( + &args, + |i, obj, vm| { + match i { + 0 => qid.set(Some(parse_qid(obj, vm)?)), + 2 => unboundarg.set(Some(int_arg(obj, vm)?)), + 3 => fallbackarg.set(Some(int_arg(obj, vm)?)), + _ => {} + } + Ok(()) + }, + vm, + )?; + let qid = qid.get().unwrap(); + let obj = parsed[1].as_deref().unwrap(); + let unboundarg = unboundarg.get().unwrap_or(-1); + let fallbackarg = fallbackarg.get().unwrap_or(-1); + // The queue is only consulted when one of the arguments needs its default. + let (default_unboundop, default_fallback) = if unboundarg < 0 || fallbackarg < 0 { + let (unboundop, fallback) = queue_defaults(qid).map_err(|err| err.into_py(qid, vm))?; + (unboundop, fallback.as_i32()) + } else { + (-1, -1) + }; + let unboundop = resolve_unboundop(unboundarg, default_unboundop, vm)?; + let fallback = resolve_fallback(fallbackarg, default_fallback, vm)?; + let value = SharedValue::from_object(obj, fallback, vm)?; + let queue = queue_lookup(qid).map_err(|err| err.into_py(qid, vm))?; + let mut state = queue.lock(); + if !state.alive { + return Err(QueueErr::NotFound.into_py(qid, vm)); + } + if state.is_full() { + drop(state); + return Err(queue_full_error(qid, vm)?); + } + state.items.push_back(QueueItem { + interpid: vm.state.interpreter_id, + data: Some(value), + unboundop, + }); + Ok(()) + } + + #[pyfunction] + fn get(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let qid = qid_arg(&args, "get", vm)?; + let item = { + let queue = queue_lookup(qid).map_err(|err| err.into_py(qid, vm))?; + let mut state = queue.lock(); + if !state.alive { + return Err(QueueErr::NotFound.into_py(qid, vm)); + } + match state.items.pop_front() { + Some(item) => item, + None => { + drop(state); + return Err(queue_empty_error(qid, vm)?); + } + } + }; + let (obj, unboundop) = match item.data { + Some(data) => (data.into_object(vm)?, vm.ctx.none()), + None => (vm.ctx.none(), vm.ctx.new_int(item.unboundop).into()), + }; + Ok(vm.ctx.new_tuple(vec![obj, unboundop]).into()) + } + + #[pyfunction] + fn bind(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let qid = qid_arg(&args, "bind", vm)?; + queue_bind(qid).map_err(|err| err.into_py(qid, vm)) + } + + #[pyfunction] + fn release(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let qid = qid_arg(&args, "release", vm)?; + queue_release(qid).map_err(|err| err.into_py(qid, vm)) + } + + #[pyfunction] + fn get_maxsize(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let qid = qid_arg(&args, "get_maxsize", vm)?; + let queue = queue_lookup(qid).map_err(|err| err.into_py(qid, vm))?; + let state = queue.lock(); + if !state.alive { + return Err(QueueErr::NotFound.into_py(qid, vm)); + } + Ok(state.maxsize) + } + + #[pyfunction] + fn get_queue_defaults(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let qid = qid_arg(&args, "get_queue_defaults", vm)?; + let (unboundop, fallback) = queue_defaults(qid).map_err(|err| err.into_py(qid, vm))?; + Ok(vm + .ctx + .new_tuple(vec![ + vm.ctx.new_int(unboundop).into(), + vm.ctx.new_int(fallback.as_i32()).into(), + ]) + .into()) + } + + #[pyfunction] + fn is_full(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let qid = qid_arg(&args, "is_full", vm)?; + let queue = queue_lookup(qid).map_err(|err| err.into_py(qid, vm))?; + let state = queue.lock(); + if !state.alive { + return Err(QueueErr::NotFound.into_py(qid, vm)); + } + Ok(state.is_full()) + } + + #[pyfunction] + fn get_count(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let qid = qid_arg(&args, "get_count", vm)?; + let queue = queue_lookup(qid).map_err(|err| err.into_py(qid, vm))?; + let state = queue.lock(); + if !state.alive { + return Err(QueueErr::NotFound.into_py(qid, vm)); + } + Ok(state.items.len()) + } + + #[pyfunction] + fn _register_heap_types(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let parsed = ArgSpec { + fname: "_register_heap_types", + keywords: &["queuetype", "emptyerror", "fullerror"], + required: 3, + max_positional: 3, + } + .parse(&args, vm)?; + let queue = parsed[0] + .clone() + .unwrap() + .downcast::() + .map_err(|_| vm.new_type_error("expected a type for 'queuetype'"))?; + let empty = parsed[1] + .clone() + .unwrap() + .downcast::() + .ok() + .filter(|ty| ty.fast_issubclass(vm.ctx.exceptions.base_exception_type)) + .ok_or_else(|| vm.new_type_error("expected an exception type for 'emptyerror'"))?; + let full = parsed[2] + .clone() + .unwrap() + .downcast::() + .ok() + .filter(|ty| ty.fast_issubclass(vm.ctx.exceptions.base_exception_type)) + .ok_or_else(|| vm.new_type_error("expected an exception type for 'fullerror'"))?; + set_external_types( + vm.state.interpreter_id, + ExternalTypes { queue, empty, full }, + ); + Ok(()) + } + + pub(crate) fn is_external_queue(obj: &PyObject, vm: &VirtualMachine) -> bool { + get_external_types(vm.state.interpreter_id) + .is_some_and(|types| obj.class().is(&types.queue)) + } + + pub(crate) fn queue_id_from_object( + obj: &PyObject, + vm: &VirtualMachine, + ) -> Option> { + if !is_external_queue(obj, vm) { + return None; + } + Some(obj.get_attr("_id", vm).and_then(|id| parse_qid(&id, vm))) + } + + pub(crate) fn queue_from_xid(qid: i64, vm: &VirtualMachine) -> PyResult { + let queue_type = ensure_external_types(vm)?.queue; + queue_type.as_object().call((vm.ctx.new_int(qid),), vm) + } + + pub(crate) fn queue_xid_incref(qid: i64) -> bool { + queue_bind(qid).is_ok() + } + + pub(crate) fn queue_xid_decref(qid: i64) { + match queue_release(qid) { + // Already destroyed. + Err(QueueErr::NotFound) => {} + // The reference being released was taken by `queue_xid_incref`, + // so the queue cannot be unbound here. + res => debug_assert!(res.is_ok()), + } + } + + #[cfg_attr(not(feature = "threading"), allow(dead_code))] + pub(crate) fn clear_interpreter(interpid: i64) { + remove_external_types(interpid); + let queues: Vec<_> = queues() + .lock() + .refs + .values() + .map(|queue| queue.queue.clone()) + .collect(); + let mut dropped = Vec::new(); + for queue in queues { + let mut state = queue.lock(); + let mut index = 0; + while index < state.items.len() { + if state.items[index].interpid != interpid { + index += 1; + continue; + } + if state.items[index].unboundop == UNBOUND_REMOVE { + dropped.push(state.items.remove(index).and_then(|item| item.data)); + } else { + dropped.push(state.items[index].data.take()); + index += 1; + } + } + } + drop(dropped); + } +} diff --git a/crates/vm/src/stdlib/_interpreters.rs b/crates/vm/src/stdlib/_interpreters.rs new file mode 100644 index 00000000000..0df14836440 --- /dev/null +++ b/crates/vm/src/stdlib/_interpreters.rs @@ -0,0 +1,1029 @@ +//! Low-level multiple-interpreter primitives (`_interpreters`). +//! +//! Mirrors CPython `Modules/_interpretersmodule.c`. + +pub(crate) use _interpreters::{init_xi_types, module_def}; +#[cfg_attr(not(feature = "threading"), allow(unused_imports))] +pub(crate) use _interpreters::{ + interpreter_error, interpreter_not_found, not_shareable_error, xibufferview_from_buffer, +}; + +#[pymodule] +pub(crate) mod _interpreters { + use crate::{ + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::{PyBaseExceptionRef, PyCode, PyDict, PyException, PyFunction, PyModule, PyStr}, + class::PyClassImpl, + function::{ArgSpec, FuncArgs}, + protocol::{BufferMethods, PyBuffer}, + types::{AsBuffer, Constructor}, + vm::{ + InterpreterConfig, InterpreterGil, InterpreterWhence, + crossinterp::{self, ExcInfo, Fallback, SharedValue}, + runtime, + }, + }; + use core::sync::atomic::Ordering; + use num_traits::ToPrimitive; + + #[pyattr] + pub(crate) const WHENCE_UNKNOWN: i32 = InterpreterWhence::Unknown as i32; + #[pyattr] + pub(crate) const WHENCE_RUNTIME: i32 = InterpreterWhence::Runtime as i32; + #[pyattr] + pub(crate) const WHENCE_LEGACY_CAPI: i32 = InterpreterWhence::LegacyCapi as i32; + #[pyattr] + pub(crate) const WHENCE_CAPI: i32 = InterpreterWhence::Capi as i32; + #[pyattr] + pub(crate) const WHENCE_XI: i32 = InterpreterWhence::Xi as i32; + #[pyattr] + pub(crate) const WHENCE_STDLIB: i32 = InterpreterWhence::Stdlib as i32; + + #[pyattr] + #[pyexception(name = "InterpreterError", module = "concurrent.interpreters", base = PyException)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyInterpreterError(PyException); + + #[pyexception] + impl PyInterpreterError {} + + #[pyattr] + #[pyexception(name = "InterpreterNotFoundError", module = "concurrent.interpreters", base = PyInterpreterError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyInterpreterNotFoundError(PyInterpreterError); + + #[pyexception] + impl PyInterpreterNotFoundError {} + + #[pyattr] + #[pyexception(name = "NotShareableError", module = "concurrent.interpreters", base = crate::exceptions::types::PyTypeError)] + #[derive(Debug)] + #[repr(transparent)] + pub(crate) struct PyNotShareableError(crate::exceptions::types::PyTypeError); + + #[pyexception] + impl PyNotShareableError {} + + #[pyattr] + #[pyclass(name = "CrossInterpreterBufferView", module = "_interpreters")] + #[derive(Debug, PyPayload)] + pub(crate) struct XiBufferView { + view: PyBuffer, + } + + static XI_BUFFER_VIEW_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| buffer.obj_as::().view.obj_bytes(), + obj_bytes_mut: |buffer| buffer.obj_as::().view.obj_bytes_mut(), + release: |_buffer| {}, + retain: |_buffer| {}, + }; + + #[pyclass(with(AsBuffer), flags(BASETYPE, IMMUTABLETYPE, DISALLOW_INSTANTIATION))] + impl XiBufferView {} + + impl AsBuffer for XiBufferView { + // xibufferview_getbuf: the view is handed out as-is, but with this + // object as the exporter so the sending interpreter's object stays out + // of the receiving one. + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(PyBuffer::new( + zelf.to_owned().into(), + zelf.view.desc.clone(), + &XI_BUFFER_VIEW_METHODS, + )) + } + } + + /// `xibufferview_from_buffer`. + pub(crate) fn xibufferview_from_buffer(view: PyBuffer, vm: &VirtualMachine) -> PyObjectRef { + XiBufferView { view }.into_pyobject(vm) + } + + /// The cross-interpreter exception types belong to the runtime rather than + /// to this module, so they are created before either of the modules that + /// raise them finishes importing. + pub(crate) fn init_xi_types(vm: &VirtualMachine) { + let module = vm.new_pyobj("concurrent.interpreters"); + for class in [ + PyInterpreterError::make_static_type(), + PyInterpreterNotFoundError::make_static_type(), + PyNotShareableError::make_static_type(), + ] { + class.set_attr(crate::identifier!(vm, __module__), module.clone()); + } + } + + #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + init_xi_types(vm); + __module_exec(vm, module); + // register_memoryview_xid + crossinterp::register_memoryview_xid(); + Ok(()) + } + + pub(crate) fn interpreter_error( + vm: &VirtualMachine, + msg: impl Into, + ) -> PyBaseExceptionRef { + vm.new_exception_msg( + PyInterpreterError::class(&vm.ctx).to_owned(), + msg.into().into(), + ) + } + + pub(crate) fn interpreter_not_found(vm: &VirtualMachine, id: i64) -> PyBaseExceptionRef { + vm.new_exception_msg( + PyInterpreterNotFoundError::class(&vm.ctx).to_owned(), + format!("unrecognized interpreter ID {id}").into(), + ) + } + + pub(crate) fn not_shareable_error( + vm: &VirtualMachine, + msg: impl Into, + ) -> PyBaseExceptionRef { + vm.new_exception_msg( + PyNotShareableError::class(&vm.ctx).to_owned(), + msg.into().into(), + ) + } + + /// `_PyArg_BadArgument`. + fn bad_argument( + func: &str, + pos: &str, + expected: &str, + obj: &PyObject, + vm: &VirtualMachine, + ) -> PyBaseExceptionRef { + vm.new_type_error(format!( + "{func}() {pos} must be {expected}, not {}", + if vm.is_none(obj) { + "None".into() + } else { + obj.class().name() + } + )) + } + + /// `_PyInterpreterState_ObjectToID`. + fn parse_id(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + if !obj.number().is_index() { + return Err(vm.new_type_error(format!( + "interpreter ID must be an int, got {}", + obj.class().name() + ))); + } + let n = obj.try_index(vm)?; + let id = n + .as_bigint() + .to_i64() + .ok_or_else(|| vm.new_overflow_error("int too big to convert"))?; + if id < 0 { + let repr = obj.repr(vm)?; + return Err(vm.new_value_error(format!( + "interpreter ID must be a non-negative int, got {repr}" + ))); + } + Ok(id) + } + + /// `resolve_interp`. `id` is `None` for the current interpreter. + fn resolve_interp( + id: Option, + restricted: bool, + reqready: bool, + op: &str, + vm: &VirtualMachine, + ) -> PyResult { + // The wording depends on whether an ID was passed, not on which + // interpreter it names. + let target = match id { + Some(id) => format!("interpreter {id}"), + None => "current interpreter".to_owned(), + }; + let id = id.unwrap_or(vm.state.interpreter_id); + let state = runtime::lookup_interpreter(id).ok_or_else(|| interpreter_not_found(vm, id))?; + if reqready && !state.ready.load(Ordering::Acquire) { + return Err(interpreter_error( + vm, + format!("cannot {op} {target} (not ready)"), + )); + } + if restricted && state.whence != InterpreterWhence::Stdlib { + return Err(interpreter_error( + vm, + format!("cannot {op} unrecognized {target}"), + )); + } + Ok(id) + } + + fn require_dict<'a>( + obj: &'a PyObject, + func: &str, + pos: &str, + vm: &VirtualMachine, + ) -> PyResult<&'a Py> { + obj.downcast_ref::() + .ok_or_else(|| bad_argument(func, pos, "dict", obj, vm)) + } + + /// The `p` converter: a predicate that only reads truthiness. + fn flag(slot: Option<&PyObjectRef>, vm: &VirtualMachine) -> PyResult { + match slot { + Some(o) => o.clone().is_true(vm), + None => Ok(false), + } + } + + /// The `O!` converter for `PyDict_Type`. + fn check_dict(obj: &PyObject, func: &str, pos: &str, vm: &VirtualMachine) -> PyResult<()> { + require_dict(obj, func, pos, vm).map(drop) + } + + /// `_config_dict_get_bool`: only exact `True` / `False` are accepted. + fn config_bool(value: &PyObject, name: &str, vm: &VirtualMachine) -> PyResult { + if value.is(&vm.ctx.true_value) { + Ok(true) + } else if value.is(&vm.ctx.false_value) { + Ok(false) + } else { + Err(vm.new_type_error(format!("invalid config type: {name}"))) + } + } + + fn config_gil(value: &PyObject, vm: &VirtualMachine) -> PyResult { + let s = value + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("invalid config type: gil"))?; + let s = s.to_str().unwrap_or(""); + InterpreterGil::from_name(s).ok_or_else(|| { + vm.new_value_error(format!("unsupported interpreter config .gil value '{s}'")) + }) + } + + /// `interp_config_from_dict`. + fn config_from_dict( + cfg: &mut InterpreterConfig, + dict: &Py, + missing_allowed: bool, + vm: &VirtualMachine, + ) -> PyResult<()> { + let mut seen = 0usize; + macro_rules! copy_bool { + ($field:ident) => { + match dict.get_item_opt(stringify!($field), vm)? { + Some(v) => { + cfg.$field = config_bool(&v, stringify!($field), vm)?; + seen += 1; + } + None if missing_allowed => {} + None => { + return Err(vm.new_value_error(format!( + "missing config key: {}", + stringify!($field) + ))); + } + } + }; + } + copy_bool!(use_main_obmalloc); + copy_bool!(allow_fork); + copy_bool!(allow_exec); + copy_bool!(allow_threads); + copy_bool!(allow_daemon_threads); + copy_bool!(check_multi_interp_extensions); + match dict.get_item_opt("gil", vm)? { + Some(v) => { + cfg.gil = config_gil(&v, vm)?; + seen += 1; + } + None if missing_allowed => {} + None => return Err(vm.new_value_error("missing config key: gil")), + } + + let unused = dict.__len__() - seen; + if unused > 0 { + let extra = vm.ctx.new_dict(); + for (key, value) in dict { + let known = key.downcast_ref::().is_some_and(|s| { + matches!( + s.to_str().unwrap_or(""), + "use_main_obmalloc" + | "allow_fork" + | "allow_exec" + | "allow_threads" + | "allow_daemon_threads" + | "check_multi_interp_extensions" + | "gil" + ) + }); + if !known { + extra.set_item(&*key, value, vm)?; + } + } + let repr = extra.as_object().repr(vm)?; + let plural = if unused == 1 { "item" } else { "items" }; + return Err( + vm.new_value_error(format!("config dict has {unused} extra {plural} ({repr})")) + ); + } + Ok(()) + } + + /// `init_named_config`. + fn named_config(name: Option<&str>, vm: &VirtualMachine) -> PyResult { + let name = name.unwrap_or("isolated"); + InterpreterConfig::named(name) + .ok_or_else(|| vm.new_value_error(format!("unsupported config name '{name}'"))) + } + + /// `config_from_object`. + fn parse_config(obj: Option<&PyObject>, vm: &VirtualMachine) -> PyResult { + let Some(obj) = obj.filter(|o| !vm.is_none(o)) else { + return named_config(None, vm); + }; + if let Some(s) = obj.downcast_ref::() { + return named_config(s.to_str(), vm); + } + let dict = obj.get_attr("__dict__", vm).map_err(|_| { + let repr = obj + .repr(vm) + .map_or_else(|_| obj.class().name().to_string(), |s| s.to_string()); + vm.new_type_error(format!("bad config {repr}")) + })?; + let dict = dict + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("dict expected"))?; + let mut cfg = InterpreterConfig::ISOLATED; + config_from_dict(&mut cfg, dict, false, vm)?; + Ok(cfg) + } + + /// `_PyInterpreterConfig_AsDict` wrapped in a `types.SimpleNamespace`. + fn config_namespace(cfg: InterpreterConfig, vm: &VirtualMachine) -> PyObjectRef { + crate::py_namespace!(vm, { + "use_main_obmalloc" => vm.ctx.new_bool(cfg.use_main_obmalloc), + "allow_fork" => vm.ctx.new_bool(cfg.allow_fork), + "allow_exec" => vm.ctx.new_bool(cfg.allow_exec), + "allow_threads" => vm.ctx.new_bool(cfg.allow_threads), + "allow_daemon_threads" => vm.ctx.new_bool(cfg.allow_daemon_threads), + "check_multi_interp_extensions" => vm.ctx.new_bool(cfg.check_multi_interp_extensions), + "gil" => vm.ctx.new_str(cfg.gil.as_str()), + }) + .into() + } + + fn summary(id: i64, whence: InterpreterWhence, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx + .new_tuple(vec![ + vm.ctx.new_int(id).into(), + vm.ctx.new_int(whence.as_i32()).into(), + ]) + .into() + } + + #[pyfunction] + fn new_config(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + const FUNCNAME: &str = "_interpreters.new_config"; + if args.args.len() > 1 { + return Err(vm.new_type_error(format!( + "{FUNCNAME}() takes at most 1 argument ({} given)", + args.args.len() + ))); + } + // The `s` converter: a str, and nothing else. + let name = match args.args.first() { + Some(o) => Some( + o.downcast_ref::() + .and_then(|s| s.to_str()) + .ok_or_else(|| bad_argument(FUNCNAME, "argument 1", "str", o, vm))? + .to_owned(), + ), + None => None, + }; + let mut cfg = named_config(name.as_deref(), vm)?; + if !args.kwargs.is_empty() { + let overrides = vm.ctx.new_dict(); + for (key, value) in &args.kwargs { + overrides.set_item(&**key, value.clone(), vm)?; + } + config_from_dict(&mut cfg, &overrides, true, vm)?; + } + Ok(config_namespace(cfg, vm)) + } + + #[pyfunction] + fn create(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "create", + keywords: &["config", "reqrefs"], + required: 0, + max_positional: 1, + } + .parse(&args, vm)?; + let reqrefs = flag(parsed[1].as_ref(), vm)?; + let config = parse_config(parsed[0].as_deref(), vm)?; + new_interpreter(config, reqrefs, vm) + } + + /// `_PyXI_NewInterpreter`, registered as a runtime-owned interpreter. + #[cfg(feature = "threading")] + fn new_interpreter(config: InterpreterConfig, reqrefs: bool, vm: &VirtualMachine) -> PyResult { + let interp = + crate::Interpreter::create_subinterpreter_from_vm(vm, config).map_err(|msg| { + let cause = vm.new_runtime_error(msg.to_owned()); + let exc = interpreter_error(vm, "interpreter creation failed"); + exc.set___context__(Some(cause)); + exc + })?; + if reqrefs { + // Decref to 0 will destroy the interpreter. + interp + .global_state + .require_idref + .store(true, Ordering::Release); + } + let id = runtime::store_owned_interpreter(interp); + Ok(vm.ctx.new_int(id).into()) + } + + #[cfg(not(feature = "threading"))] + fn new_interpreter( + _config: InterpreterConfig, + _reqrefs: bool, + vm: &VirtualMachine, + ) -> PyResult { + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + + #[pyfunction] + fn destroy(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let parsed = ArgSpec { + fname: "destroy", + keywords: &["id", "restrict"], + required: 1, + max_positional: 1, + } + .parse(&args, vm)?; + let restricted = flag(parsed[1].as_ref(), vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + resolve_interp(Some(id), restricted, false, "destroy", vm)?; + if id == vm.state.interpreter_id { + return Err(interpreter_error( + vm, + "cannot destroy the current interpreter", + )); + } + if crossinterp::is_running(id) { + return Err(interpreter_error(vm, "interpreter running")); + } + #[cfg(feature = "threading")] + { + if runtime::is_owned_interpreter(id) { + runtime::destroy_owned_interpreter(id) + .ok_or_else(|| interpreter_not_found(vm, id))?; + return Ok(()); + } + } + Err(interpreter_not_found(vm, id)) + } + + #[pyfunction] + fn list_all(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "_interpreters.list_all", + keywords: &["require_ready"], + required: 0, + max_positional: 0, + } + .parse(&args, vm)?; + let reqready = flag(parsed[0].as_ref(), vm)?; + let mut items = Vec::new(); + for info in runtime::list_interpreters() { + if reqready + && !runtime::lookup_interpreter(info.id) + .is_some_and(|s| s.ready.load(Ordering::Acquire)) + { + continue; + } + items.push(summary(info.id, info.whence, vm)); + } + Ok(vm.ctx.new_list(items).into()) + } + + #[pyfunction] + fn get_current(vm: &VirtualMachine) -> PyObjectRef { + summary(vm.state.interpreter_id, vm.state.whence, vm) + } + + #[pyfunction] + fn get_main(vm: &VirtualMachine) -> PyObjectRef { + let id = runtime::main_interpreter_id().unwrap_or(0); + summary(id, InterpreterWhence::Runtime, vm) + } + + #[pyfunction] + fn is_running(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "is_running", + keywords: &["id", "restrict"], + required: 1, + max_positional: 1, + } + .parse(&args, vm)?; + let restricted = flag(parsed[1].as_ref(), vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + resolve_interp(Some(id), restricted, true, "check if running for", vm)?; + Ok(vm.ctx.new_bool(crossinterp::is_running(id)).into()) + } + + #[pyfunction] + fn whence(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "whence", + keywords: &["id"], + required: 1, + max_positional: 1, + } + .parse(&args, vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + let state = runtime::lookup_interpreter(id).ok_or_else(|| interpreter_not_found(vm, id))?; + Ok(vm.ctx.new_int(state.whence.as_i32()).into()) + } + + #[pyfunction] + fn get_config(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "get_config", + keywords: &["id", "restrict"], + required: 1, + max_positional: 1, + } + .parse(&args, vm)?; + let restricted = flag(parsed[1].as_ref(), vm)?; + let id_obj = parsed[0].as_deref().unwrap(); + let id = if vm.is_none(id_obj) { + None + } else { + Some(parse_id(id_obj, vm)?) + }; + let id = resolve_interp(id, restricted, false, "get the config of", vm)?; + let state = runtime::lookup_interpreter(id).ok_or_else(|| interpreter_not_found(vm, id))?; + Ok(config_namespace(state.config(), vm)) + } + + #[pyfunction] + fn is_shareable(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "is_shareable", + keywords: &["obj"], + required: 1, + max_positional: 1, + } + .parse(&args, vm)?; + Ok(crossinterp::is_shareable(parsed[0].as_deref().unwrap(), vm)) + } + + /// `_run_in_interpreter` for the script forms: `Ok(None)` on success, + /// `Ok(Some(excinfo))` when the script raised. + #[cfg(feature = "threading")] + fn run_code_in( + id: i64, + code: PyRef, + shared: Option<&Py>, + vm: &VirtualMachine, + ) -> PyResult { + let script = SharedValue::from_code(&code, vm)?; + let shared_vals = shared + .map(|dict| { + let mut out = Vec::new(); + for (key, value) in dict { + let name = crossinterp::utf8_key(&key, vm)?.to_owned(); + let val = SharedValue::from_object(&value, Fallback::XidataOnly, vm)?; + out.push((name, val)); + } + PyResult::Ok(out) + }) + .transpose()?; + match crossinterp::with_interpreter(id, vm, |target| { + let ns = target.main_namespace()?; + if let Some(vals) = &shared_vals { + for (name, val) in vals { + ns.set_item(name.as_str(), val.clone().into_object(target)?, target)?; + } + } + let code = script + .clone() + .into_object(target)? + .downcast::() + .map_err(|_| target.new_type_error("expected code"))?; + let scope = crate::scope::Scope::with_builtins(None, ns, target); + match target.run_code_obj(code, scope) { + Ok(_) => Ok(None), + Err(exc) => Ok(Some(ExcInfo::capture(&exc, target))), + } + }) { + Ok(None) => Ok(vm.ctx.none()), + Ok(Some(info)) => Ok(info.into_namespace(vm)), + Err(e) => Err(e), + } + } + + #[cfg(not(feature = "threading"))] + fn run_code_in( + _id: i64, + _code: PyRef, + _shared: Option<&Py>, + vm: &VirtualMachine, + ) -> PyResult { + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + + /// The shared `(id, , shared=None, *, restrict=False)` prologue. + /// `second_check` is the converter of the format string's second slot. + fn script_args( + args: &FuncArgs, + func: &'static str, + second_name: &'static str, + second_check: fn(&PyObject, &VirtualMachine) -> PyResult<()>, + op: &str, + vm: &VirtualMachine, + ) -> PyResult<(i64, PyObjectRef, Option>)> { + let parsed = ArgSpec { + fname: func, + keywords: &["id", second_name, "shared", "restrict"], + required: 2, + max_positional: 3, + } + .parse_with( + args, + |i, obj, vm| match i { + 1 => second_check(obj, vm), + 2 => check_dict(obj, func, "argument 3", vm), + _ => Ok(()), + }, + vm, + )?; + let restrict = flag(parsed[3].as_ref(), vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + resolve_interp(Some(id), restrict, true, op, vm)?; + let shared = parsed[2].clone().map(|o| o.downcast::().unwrap()); + Ok((id, parsed[1].clone().unwrap(), shared)) + } + + #[pyfunction] + fn exec(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + const FUNCNAME: &str = "_interpreters.exec"; + // The code need not be "pure": globals resolve against __main__. + let (id, code_obj, shared) = + script_args(&args, FUNCNAME, "code", |_, _| Ok(()), "exec code for", vm)?; + let code = crossinterp::script_code(&code_obj, vm)?; + run_code_in(id, code, shared.as_deref(), vm) + } + + #[pyfunction] + fn run_string(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + const FUNCNAME: &str = "_interpreters.run_string"; + let (id, script, shared) = script_args( + &args, + FUNCNAME, + "script", + |obj, vm| { + // The `U` converter. + obj.downcastable::() + .then_some(()) + .ok_or_else(|| bad_argument(FUNCNAME, "argument 2", "str", obj, vm)) + }, + "run a string in", + vm, + )?; + let code = crossinterp::script_code(&script, vm)?; + run_code_in(id, code, shared.as_deref(), vm) + } + + #[pyfunction] + fn run_func(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + const FUNCNAME: &str = "_interpreters.run_func"; + // Globals are not checked either; they resolve against __main__. + let (id, func, shared) = script_args( + &args, + FUNCNAME, + "func", + |_, _| Ok(()), + "run a function in", + vm, + )?; + if !func.downcastable::() && !func.downcastable::() { + return Err(bad_argument( + FUNCNAME, + "argument 2", + "a function", + &func, + vm, + )); + } + let code = crossinterp::script_code(&func, vm)?; + run_code_in(id, code, shared.as_deref(), vm) + } + + #[pyfunction] + fn call(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + const FUNCNAME: &str = "_interpreters.call"; + let parsed = ArgSpec { + fname: FUNCNAME, + keywords: &[ + "id", + "callable", + "args", + "kwargs", + "preserve_exc", + "restrict", + ], + required: 2, + max_positional: 4, + } + .parse_with( + &args, + |i, obj, vm| match i { + 2 => obj + .downcastable::() + .then_some(()) + .ok_or_else(|| bad_argument(FUNCNAME, "argument 3", "tuple", obj, vm)), + 3 => check_dict(obj, FUNCNAME, "argument 4", vm), + _ => Ok(()), + }, + vm, + )?; + // preserve_exc is accepted and ignored: an unpickled exception is + // always a new object. + let restrict = flag(parsed[5].as_ref(), vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + resolve_interp(Some(id), restrict, true, "make a call in", vm)?; + + let callable = parsed[1].as_deref().unwrap(); + if !callable.is_callable() { + let repr = callable.repr(vm)?; + return Err(vm.new_type_error(format!("expected a callable, got {repr}"))); + } + let func = SharedValue::from_callable(callable, vm)?; + let packed_args = match parsed[2].as_deref() { + Some(o) + if !o + .downcast_ref::() + .unwrap() + .is_empty() => + { + Some(SharedValue::from_object(o, Fallback::Full, vm)?) + } + _ => None, + }; + let packed_kwargs = match parsed[3].as_deref() { + Some(o) if !o.downcast_ref::().unwrap().is_empty() => { + Some(SharedValue::from_object(o, Fallback::Full, vm)?) + } + _ => None, + }; + + call_in(id, func, packed_args, packed_kwargs, vm) + } + + /// What running the callable in the target interpreter produced. + #[cfg(feature = "threading")] + enum CallOutcome { + Returned(SharedValue), + Raised(ExcInfo), + /// An argument could not be unpacked or the result has no + /// cross-interpreter data; `msg` is what `wrap_notshareable` labelled it. + NotShareable { + info: ExcInfo, + msg: Option, + }, + } + + #[cfg(feature = "threading")] + fn call_in( + id: i64, + func: SharedValue, + packed_args: Option, + packed_kwargs: Option, + vm: &VirtualMachine, + ) -> PyResult { + { + let outcome = crossinterp::with_interpreter(id, vm, |target| { + // _interp_call_unpack, whose failures are labelled by the part + // that could not be rebuilt. + let unpack = || -> Result<_, (PyBaseExceptionRef, &'static str)> { + let func = func.clone().into_object(target).map_err(|e| (e, "func"))?; + let call_args = match &packed_args { + Some(v) => v + .clone() + .into_object(target) + .map_err(|e| (e, "args"))? + .downcast::() + .map_err(|_| (target.new_type_error("expected tuple"), "args"))? + .as_slice() + .to_vec(), + None => Vec::new(), + }; + let mut func_args = FuncArgs::from(call_args); + if let Some(v) = &packed_kwargs { + let dict = v + .clone() + .into_object(target) + .map_err(|e| (e, "kwargs"))? + .downcast::() + .map_err(|_| (target.new_type_error("expected dict"), "kwargs"))?; + for (key, value) in &*dict { + let name = crossinterp::utf8_key(&key, target) + .map_err(|e| (e, "kwargs"))? + .to_owned(); + func_args.kwargs.insert(name.into(), value); + } + } + Ok((func, func_args)) + }; + let (func, func_args) = match unpack() { + Ok(unpacked) => unpacked, + Err((exc, label)) => { + return Ok(CallOutcome::NotShareable { + info: ExcInfo::capture(&exc, target), + msg: Some(format!("{label} not shareable")), + }); + } + }; + Ok(match func.call(func_args, target) { + Ok(res) => match SharedValue::from_object(&res, Fallback::Full, target) { + Ok(v) => CallOutcome::Returned(v), + Err(exc) => CallOutcome::NotShareable { + info: ExcInfo::capture(&exc, target), + msg: None, + }, + }, + Err(exc) => CallOutcome::Raised(ExcInfo::capture(&exc, target)), + }) + })?; + let (res, excinfo) = match outcome { + CallOutcome::Returned(v) => (v.into_object(vm)?, vm.ctx.none()), + CallOutcome::Raised(info) => (vm.ctx.none(), info.into_namespace(vm)), + CallOutcome::NotShareable { info, msg } => { + return Err(info.into_not_shareable(msg, vm)); + } + }; + Ok(vm.ctx.new_tuple(vec![res, excinfo]).into()) + } + } + + #[cfg(not(feature = "threading"))] + fn call_in( + _id: i64, + _func: SharedValue, + _packed_args: Option, + _packed_kwargs: Option, + vm: &VirtualMachine, + ) -> PyResult { + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + + #[pyfunction(name = "set___main___attrs")] + fn set_main_attrs(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + const FUNCNAME: &str = "_interpreters.set___main___attrs"; + let parsed = ArgSpec { + fname: FUNCNAME, + keywords: &["id", "updates", "restrict"], + required: 2, + max_positional: 2, + } + .parse_with( + &args, + |i, obj, vm| match i { + 1 => check_dict(obj, FUNCNAME, "argument 2", vm), + _ => Ok(()), + }, + vm, + )?; + let restrict = flag(parsed[2].as_ref(), vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + resolve_interp(Some(id), restrict, true, "update __main__ for", vm)?; + let updates = parsed[1] + .as_deref() + .unwrap() + .downcast_ref::() + .unwrap(); + if updates.is_empty() { + return Err(vm.new_value_error("arg 2 must be a non-empty dict")); + } + let mut vals = Vec::new(); + for (key, value) in updates { + let name = crossinterp::utf8_key(&key, vm)?.to_owned(); + let val = SharedValue::from_object(&value, Fallback::XidataOnly, vm)?; + vals.push((name, val)); + } + bind_main_attrs(id, vals, vm) + } + + #[cfg(feature = "threading")] + fn bind_main_attrs( + id: i64, + vals: Vec<(String, SharedValue)>, + vm: &VirtualMachine, + ) -> PyResult<()> { + crossinterp::with_interpreter(id, vm, |target| { + let ns = target.main_namespace()?; + for (name, val) in &vals { + ns.set_item(name.as_str(), val.clone().into_object(target)?, target)?; + } + Ok(()) + }) + } + + #[cfg(not(feature = "threading"))] + fn bind_main_attrs( + _id: i64, + _vals: Vec<(String, SharedValue)>, + vm: &VirtualMachine, + ) -> PyResult<()> { + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + + #[pyfunction] + fn incref(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let parsed = ArgSpec { + fname: "incref", + keywords: &["id", "implieslink", "restrict"], + required: 1, + max_positional: 1, + } + .parse(&args, vm)?; + let implieslink = flag(parsed[1].as_ref(), vm)?; + let restricted = flag(parsed[2].as_ref(), vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + resolve_interp(Some(id), restricted, true, "incref", vm)?; + let state = runtime::lookup_interpreter(id).ok_or_else(|| interpreter_not_found(vm, id))?; + if implieslink { + // Decref to 0 will destroy the interpreter. + state.require_idref.store(true, Ordering::Release); + } + state.id_refcount.fetch_add(1, Ordering::AcqRel); + Ok(()) + } + + #[pyfunction] + fn decref(args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + let parsed = ArgSpec { + fname: "decref", + keywords: &["id", "restrict"], + required: 1, + max_positional: 1, + } + .parse(&args, vm)?; + let restricted = flag(parsed[1].as_ref(), vm)?; + let id = parse_id(parsed[0].as_deref().unwrap(), vm)?; + resolve_interp(Some(id), restricted, true, "decref", vm)?; + let state = runtime::lookup_interpreter(id).ok_or_else(|| interpreter_not_found(vm, id))?; + let prev = state.id_refcount.fetch_sub(1, Ordering::AcqRel); + if prev == 1 && state.require_idref.load(Ordering::Acquire) { + #[cfg(feature = "threading")] + { + if runtime::is_owned_interpreter(id) && id != vm.state.interpreter_id { + let _ = runtime::destroy_owned_interpreter(id); + } + } + } + Ok(()) + } + + #[pyfunction] + fn capture_exception(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let parsed = ArgSpec { + fname: "capture_exception", + keywords: &["exc"], + required: 0, + max_positional: 1, + } + .parse(&args, vm)?; + let exc = match parsed + .into_iter() + .next() + .unwrap() + .filter(|e| !vm.is_none(e)) + { + Some(e) => e + .downcast::() + .map_err(|e| { + let repr = e + .repr(vm) + .map_or_else(|_| e.class().name().to_string(), |s| s.to_string()); + vm.new_type_error(format!("expected exception, got {repr}")) + })?, + // `PyErr_GetRaisedException`: an exception is in flight only while + // no Python code is running, so there is never one to capture here. + None => return Ok(vm.ctx.none()), + }; + Ok(ExcInfo::capture(&exc, vm).into_namespace(vm)) + } +} diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index 45b92ff97e7..35b24563759 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -531,6 +531,11 @@ pub(crate) mod _thread { vm, )?; + if !vm.state.allow_threads() { + return Err(vm.new_runtime_error( + "thread is not supported for isolated subinterpreters".to_owned(), + )); + } if vm .state .finalizing @@ -701,9 +706,8 @@ pub(crate) mod _thread { } #[pyfunction] - fn daemon_threads_allowed() -> bool { - // RustPython always allows daemon threads - true + fn daemon_threads_allowed(vm: &VirtualMachine) -> bool { + vm.state.allow_daemon_threads() } // Registry for non-daemon threads that need to be joined at shutdown @@ -1817,6 +1821,11 @@ pub(crate) mod _thread { vm, )?; + if !vm.state.allow_threads() { + return Err(vm.new_runtime_error( + "thread is not supported for isolated subinterpreters".to_owned(), + )); + } if vm .state .finalizing diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 75d84abfb70..a26d1a33a7f 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -973,8 +973,8 @@ mod builtins { ) -> PyResult { if !PyIter::check(&iterator) { return Err(vm.new_type_error(format!( - "{} object is not an iterator", - iterator.class().name() + "'{}' object is not an iterator", + iterator.class().slot_name() ))); } PyIter::new(iterator) @@ -1134,8 +1134,8 @@ mod builtins { .get_special_method(&number, identifier!(vm, __round__))? .ok_or_else(|| { vm.new_type_error(format!( - "type {} doesn't define __round__", - number.class().name() + "type {} doesn't define __round__ method", + number.class().slot_name() )) })?; match ndigits.flatten() { diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index 97fb4b372cf..f504bad44f6 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -59,6 +59,9 @@ pub(crate) mod msvcrt; ))] mod pwd; +pub(crate) mod _interpchannels; +pub(crate) mod _interpqueues; +pub(crate) mod _interpreters; #[cfg(feature = "host_env")] pub(crate) mod _signal; #[cfg(feature = "threading")] @@ -134,6 +137,9 @@ pub fn builtin_module_defs(ctx: &Context) -> Vec<&'static PyModuleDef> { _sysconfig::module_def(ctx), #[cfg(feature = "threading")] _thread::module_def(ctx), + _interpchannels::module_def(ctx), + _interpqueues::module_def(ctx), + _interpreters::module_def(ctx), time::module_def(ctx), _typing::module_def(ctx), _warnings::module_def(ctx), diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index ebe539621c1..2dfe98fb9d7 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -509,6 +509,11 @@ pub(crate) mod module { argv: Either, vm: &VirtualMachine, ) -> PyResult<()> { + if !vm.state.allow_exec() { + return Err( + vm.new_runtime_error("exec not supported for isolated subinterpreters".to_owned()) + ); + } let make_widestring = |s: &str| widestring::WideCString::from_os_str(s).map_err(|err| err.to_pyexception(vm)); @@ -539,6 +544,11 @@ pub(crate) mod module { env: ArgMapping, vm: &VirtualMachine, ) -> PyResult<()> { + if !vm.state.allow_exec() { + return Err( + vm.new_runtime_error("exec not supported for isolated subinterpreters".to_owned()) + ); + } let make_widestring = |s: &str| widestring::WideCString::from_os_str(s).map_err(|err| err.to_pyexception(vm)); diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 5953f9c874d..e175d99ced4 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -858,6 +858,11 @@ pub mod module { "can't fork at interpreter shutdown".into(), )); } + if !vm.state.allow_fork() { + return Err( + vm.new_runtime_error("fork not supported for isolated subinterpreters".to_owned()) + ); + } // RustPython does not yet have C-level audit hooks; call sys.audit() // to preserve Python-visible behavior and failure semantics. @@ -1077,6 +1082,11 @@ pub mod module { argv: Either, vm: &VirtualMachine, ) -> PyResult<()> { + if !vm.state.allow_exec() { + return Err( + vm.new_runtime_error("exec not supported for isolated subinterpreters".to_owned()) + ); + } let path = path.into_cstring(vm)?; let argv = vm.extract_elements_with(argv.as_ref(), |obj| { @@ -1101,6 +1111,11 @@ pub mod module { env: ArgMapping, vm: &VirtualMachine, ) -> PyResult<()> { + if !vm.state.allow_exec() { + return Err( + vm.new_runtime_error("exec not supported for isolated subinterpreters".to_owned()) + ); + } let path = path.into_cstring(vm)?; let argv = vm.extract_elements_with(argv.as_ref(), |obj| { diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index d44b0789af3..7fb5d5a263f 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -583,7 +583,7 @@ fn iter_wrapper(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult { let iter_attr = cls.get_attr(identifier!(vm, __iter__)); match iter_attr { Some(attr) if vm.is_none(&attr) => { - Err(vm.new_type_error(format!("'{}' object is not iterable", cls.name()))) + Err(vm.new_type_error(format!("'{}' object is not iterable", cls.slot_name()))) } _ => vm.call_special_method(&zelf, identifier!(vm, __iter__), ()), } diff --git a/crates/vm/src/vm/crossinterp.rs b/crates/vm/src/vm/crossinterp.rs new file mode 100644 index 00000000000..31a4e36399d --- /dev/null +++ b/crates/vm/src/vm/crossinterp.rs @@ -0,0 +1,807 @@ +//! Cross-interpreter object sharing and exception snapshots (XI data). +//! +//! Interpreters do not share `PyObject` graphs. Shareable values are converted +//! to an interpreter-neutral payload and rebuilt in the destination. + +use crate::{ + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromBorrowedObject, + VirtualMachine, + builtins::{ + PyBaseExceptionRef, PyBytes, PyCode, PyDict, PyFloat, PyFunction, PyInt, PyMemoryView, + PyStr, PyStrInterned, PyTuple, + }, + bytecode::{ + BorrowedConstant, CodeFlags, Constant, Instruction, + oparg::{OpArg, OpArgState}, + }, + protocol::PyBuffer, +}; +use core::sync::atomic::{AtomicBool, Ordering}; +use num_traits::ToPrimitive; +use rustpython_common::wtf8::Wtf8Buf; + +static MEMORYVIEW_REGISTERED: AtomicBool = AtomicBool::new(false); + +/// `register_memoryview_xid`: the builtin memoryview gets its getdata function +/// only once `_interpreters` has been imported. +pub(crate) fn register_memoryview_xid() { + MEMORYVIEW_REGISTERED.store(true, Ordering::Relaxed); +} + +fn memoryview_registered() -> bool { + MEMORYVIEW_REGISTERED.load(Ordering::Relaxed) +} + +/// Unbound-item ops (`UNBOUND_REMOVE` / `UNBOUND_ERROR` / `UNBOUND_REPLACE`). +pub const UNBOUND_REMOVE: i32 = 1; +pub const UNBOUND_ERROR: i32 = 2; +pub const UNBOUND_REPLACE: i32 = 3; + +/// `xidata_fallback_t`: what to do with an object that has no XI data support. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Fallback { + /// `_PyXIDATA_XIDATA_ONLY`: raise `NotShareableError`. + XidataOnly = 0, + /// `_PyXIDATA_FULL_FALLBACK`: fall back to stateless functions, then pickle. + Full = 1, +} + +impl Fallback { + #[must_use] + pub const fn from_i32(v: i32) -> Option { + Some(match v { + 0 => Self::XidataOnly, + 1 => Self::Full, + _ => return None, + }) + } + + #[must_use] + pub const fn as_i32(self) -> i32 { + self as i32 + } +} + +/// A queue reference held by cross-interpreter data. Cloning takes another +/// reference and dropping releases one. +pub struct SharedQueueId { + qid: i64, +} + +impl SharedQueueId { + fn new(qid: i64) -> Option { + if crate::stdlib::_interpqueues::queue_xid_incref(qid) { + Some(Self { qid }) + } else { + None + } + } +} + +impl Clone for SharedQueueId { + fn clone(&self) -> Self { + let held = crate::stdlib::_interpqueues::queue_xid_incref(self.qid); + debug_assert!(held); + Self { qid: self.qid } + } +} + +impl Drop for SharedQueueId { + fn drop(&mut self) { + crate::stdlib::_interpqueues::queue_xid_decref(self.qid); + } +} + +/// Interpreter-neutral payload that can be materialized in any interpreter +/// sharing the process-wide [`crate::Context`]. +#[derive(Clone)] +pub enum SharedValue { + None, + Bool(bool), + Int(isize), + Float(f64), + Bytes(Vec), + Str(Wtf8Buf), + Tuple(Vec), + Channel { + cid: i64, + end: i32, + }, + Queue(SharedQueueId), + #[cfg(feature = "threading")] + Buffer(PyBuffer), + #[cfg(not(feature = "threading"))] + Buffer(Vec), + /// Marshalled code object. + Code(Vec), + /// Marshalled code of a stateless function; rebuilt against `__main__`. + Function(Vec), + /// `pickle.dumps` output, used by [`Fallback::Full`]. + Pickled(Vec), +} + +impl SharedValue { + pub fn from_object(obj: &PyObject, fallback: Fallback, vm: &VirtualMachine) -> PyResult { + match Self::basic_from_object(obj, fallback, vm) { + Ok(v) => Ok(v), + Err(exc) => { + if fallback == Fallback::XidataOnly { + return Err(exc); + } + if obj.downcastable::() + && let Ok(v) = Self::from_function(obj, vm) + { + return Ok(v); + } + // We could try marshal here but we don't for now. + match pickle_dumps(obj, vm) { + Ok(data) => Ok(Self::Pickled(data)), + // Raise the original exception. + Err(_) => Err(exc), + } + } + } + } + + /// `_get_xidata`: the registered "getdata" functions, i.e. no pickle + /// fallback. Both a missing registration and a failing conversion are + /// reported under `obj`, the latter keeping its own failure as the cause. + fn basic_from_object( + obj: &PyObject, + fallback: Fallback, + vm: &VirtualMachine, + ) -> PyResult { + match Self::getdata(obj, fallback, vm) { + Some(res) => res.map_err(|cause| not_shareable(vm, obj, Some(cause))), + None => Err(not_shareable(vm, obj, None)), + } + } + + /// `lookup_getdata` followed by the call itself. `None` when the type has + /// no registered getdata function. + fn getdata(obj: &PyObject, fallback: Fallback, vm: &VirtualMachine) -> Option> { + if vm.is_none(obj) { + return Some(Ok(Self::None)); + } + let cls = obj.class(); + if cls.is(vm.ctx.types.bool_type) { + return Some(obj.to_owned().is_true(vm).map(Self::Bool)); + } + if cls.is(vm.ctx.types.int_type) { + let n = obj.downcast_ref::().unwrap(); + // `_long_shared`: the size of shareable ints is bounded by + // sys.maxsize. + return Some( + n.as_bigint() + .to_isize() + .map(Self::Int) + .ok_or_else(|| vm.new_overflow_error("try sending as bytes")), + ); + } + if cls.is(vm.ctx.types.float_type) { + let f = obj.downcast_ref::().unwrap(); + return Some(Ok(Self::Float(f.to_f64()))); + } + if cls.is(vm.ctx.types.bytes_type) { + let b = obj.downcast_ref::().unwrap(); + return Some(Ok(Self::Bytes(b.as_bytes().to_vec()))); + } + if cls.is(vm.ctx.types.str_type) { + let s = obj.downcast_ref::().unwrap(); + return Some(Ok(Self::Str(s.as_wtf8().to_owned()))); + } + if cls.is(vm.ctx.types.tuple_type) { + let t = obj.downcast_ref::().unwrap(); + return Some( + t.iter() + .map(|item| { + vm.with_recursion("while sharing a tuple", || { + Self::from_object(item, fallback, vm) + }) + }) + .collect::>>() + .map(Self::Tuple), + ); + } + // `_pybuffer_shared`, registered for the builtin memoryview by + // `_interpreters` and never unregistered. + if cls.is(vm.ctx.types.memoryview_type) && memoryview_registered() { + return Some(Self::from_buffer_object(obj, vm)); + } + // Registered by the `_interpchannels` module for ChannelID. + if let Some(ch) = crate::stdlib::_interpchannels::channel_id_parts(obj) { + return Some(Ok(Self::Channel { + cid: ch.0, + end: ch.1, + })); + } + if let Some(qid) = crate::stdlib::_interpqueues::queue_id_from_object(obj, vm) { + return match qid { + Ok(qid) => SharedQueueId::new(qid).map(|qid| Ok(Self::Queue(qid))), + Err(exc) => Some(Err(exc)), + }; + } + None + } + + /// `_PyFunction_GetXIData`: only stateless functions are shareable. + fn from_function(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + let func = obj.downcast_ref::().ok_or_else(|| { + not_shareable_error( + vm, + format!("expected a function, got {}", render_repr(obj, vm)), + ) + })?; + verify_stateless_function(func, vm).map_err(|cause| { + not_shareable_error_from(vm, "only stateless functions are shareable", cause) + })?; + let code = (*func.code).to_owned(); + Ok(Self::Function(marshal_dumps(code.as_object(), vm)?)) + } + + /// `_interp_call_pack`'s handling of the callable: a stateless function + /// travels as its code, anything else is pickled. + pub fn from_callable(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + match Self::from_function(obj, vm) { + Ok(v) => Ok(v), + Err(exc) => match pickle_dumps(obj, vm) { + Ok(data) => Ok(Self::Pickled(data)), + // Raise the exception from the stateless check. + Err(_) => Err(exc), + }, + } + } + + /// `_PyCode_GetXIData`. + pub fn from_code(code: &Py, vm: &VirtualMachine) -> PyResult { + Ok(Self::Code(marshal_dumps(code.as_object(), vm)?)) + } + + pub fn from_buffer_object(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + let view = PyMemoryView::from_object(obj, vm)?; + #[cfg(feature = "threading")] + { + Ok(Self::Buffer(view.clone_buffer())) + } + #[cfg(not(feature = "threading"))] + { + let buf = view.clone_buffer(); + let bytes = buf.as_contiguous().map(|b| b.to_vec()).unwrap_or_default(); + Ok(Self::Buffer(bytes)) + } + } + + pub fn into_object(self, vm: &VirtualMachine) -> PyResult { + match self { + Self::None => Ok(vm.ctx.none()), + Self::Bool(b) => Ok(vm.ctx.new_bool(b).into()), + Self::Int(n) => Ok(vm.ctx.new_int(n).into()), + Self::Float(f) => Ok(vm.ctx.new_float(f).into()), + Self::Bytes(b) => Ok(vm.ctx.new_bytes(b).into()), + Self::Str(s) => Ok(vm.ctx.new_str(s).into()), + Self::Tuple(items) => { + let mut els = Vec::with_capacity(items.len()); + for item in items { + els.push(item.into_object(vm)?); + } + Ok(vm.ctx.new_tuple(els).into()) + } + Self::Channel { cid, end } => { + crate::stdlib::_interpchannels::channel_id_from_parts(cid, end, false, false, vm) + } + Self::Queue(queue) => crate::stdlib::_interpqueues::queue_from_xid(queue.qid, vm), + Self::Buffer(buffer) => { + #[cfg(not(feature = "threading"))] + let buffer = PyBuffer::from_byte_vector(buffer, vm); + // _memoryview_from_xid + let view = crate::stdlib::_interpreters::xibufferview_from_buffer(buffer, vm); + let mv = PyMemoryView::from_object(&view, vm)?; + Ok(mv.into_pyobject(vm)) + } + Self::Code(data) => marshal_loads(&data, vm), + Self::Function(data) => { + let code = marshal_loads(&data, vm)? + .downcast::() + .map_err(|_| vm.new_type_error("expected code"))?; + // Stateless functions have no globals, so `__main__` is used, + // just like for builtins such as exec(). + let globals = vm.main_namespace()?; + Ok(PyFunction::new(code, globals, vm)?.into_pyobject(vm)) + } + Self::Pickled(data) => pickle_loads(&data, vm), + } + } +} + +fn marshal_dumps(obj: &PyObject, vm: &VirtualMachine) -> PyResult> { + let dumps = vm.import("marshal", 0)?.get_attr("dumps", vm)?; + let bytes = dumps.call((obj.to_owned(),), vm)?; + let bytes = bytes + .downcast::() + .map_err(|_| vm.new_type_error("marshal.dumps() did not return bytes"))?; + Ok(bytes.as_bytes().to_vec()) +} + +fn marshal_loads(data: &[u8], vm: &VirtualMachine) -> PyResult { + let loads = vm.import("marshal", 0)?.get_attr("loads", vm)?; + loads.call((vm.ctx.new_bytes(data.to_vec()),), vm) +} + +fn pickle_dumps(obj: &PyObject, vm: &VirtualMachine) -> PyResult> { + let dumps = vm.import("pickle", 0)?.get_attr("dumps", vm)?; + let bytes = dumps.call((obj.to_owned(),), vm)?; + let bytes = bytes + .downcast::() + .map_err(|_| vm.new_type_error("pickle.dumps() did not return bytes"))?; + Ok(bytes.as_bytes().to_vec()) +} + +fn pickle_loads(data: &[u8], vm: &VirtualMachine) -> PyResult { + let loads = vm.import("pickle", 0)?.get_attr("loads", vm)?; + loads + .call((vm.ctx.new_bytes(data.to_vec()),), vm) + .map_err(|cause| not_shareable_error_from(vm, "object could not be unpickled", cause)) +} + +fn not_shareable_error(vm: &VirtualMachine, msg: impl Into) -> PyBaseExceptionRef { + crate::stdlib::_interpreters::not_shareable_error(vm, msg) +} + +/// `set_notshareableerror`: the failure that led here becomes the cause. +fn not_shareable_error_from( + vm: &VirtualMachine, + msg: impl Into, + cause: PyBaseExceptionRef, +) -> PyBaseExceptionRef { + let exc = not_shareable_error(vm, msg); + exc.set___cause__(Some(cause)); + exc +} + +/// `_set_xid_lookup_failure` without a message of its own. +fn not_shareable( + vm: &VirtualMachine, + obj: &PyObject, + cause: Option, +) -> PyBaseExceptionRef { + let msg = format!( + "{} does not support cross-interpreter data", + render_repr(obj, vm) + ); + match cause { + Some(cause) => not_shareable_error_from(vm, msg, cause), + None => not_shareable_error(vm, msg), + } +} + +fn render_repr(obj: &PyObject, vm: &VirtualMachine) -> String { + obj.repr(vm) + .map_or_else(|_| obj.class().name().to_string(), |s| s.to_string()) +} + +/// Exact-type shareable check used by `_interpreters.is_shareable`. +/// +/// This mirrors `_PyObject_CheckXIData`: only the type's registration is +/// consulted, so a tuple is "shareable" even when its items are not. +pub fn is_shareable(obj: &PyObject, vm: &VirtualMachine) -> bool { + if vm.is_none(obj) { + return true; + } + let cls = obj.class(); + cls.is(vm.ctx.types.bool_type) + || cls.is(vm.ctx.types.int_type) + || cls.is(vm.ctx.types.float_type) + || cls.is(vm.ctx.types.bytes_type) + || cls.is(vm.ctx.types.str_type) + || cls.is(vm.ctx.types.tuple_type) + || (cls.is(vm.ctx.types.memoryview_type) && memoryview_registered()) + || crate::stdlib::_interpchannels::channel_id_parts(obj).is_some() + || crate::stdlib::_interpqueues::is_external_queue(obj, vm) +} + +/// Encode a namespace key as UTF-8, rejecting non-strings and lone surrogates. +pub fn utf8_key<'a>(key: &'a PyObject, vm: &'a VirtualMachine) -> PyResult<&'a str> { + let s = key + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("bad argument type for built-in operation"))?; + s.as_wtf8().as_str().map_err(|_| { + vm.new_unicode_encode_error_real( + vm.ctx.new_str("utf-8"), + s.to_owned(), + 0, + s.char_len(), + vm.ctx.new_str("surrogates not allowed"), + ) + }) +} + +/// `_convert_exc_to_TracebackException` followed by `_format_TracebackException`. +fn format_traceback_exception(exc: &PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { + let create = vm + .import("traceback", 0)? + .get_attr("TracebackException", vm)? + .get_attr("from_exception", vm)?; + let kwargs: crate::function::KwArgs = [ + ( + "save_exc_type".to_owned(), + vm.ctx.false_value.clone().into(), + ), + ("lookup_lines".to_owned(), vm.ctx.false_value.clone().into()), + ] + .into_iter() + .collect(); + let tbexc = create.call( + crate::function::FuncArgs::new(vec![exc.as_object().to_owned()], kwargs), + vm, + )?; + let lines = vm.call_method(&tbexc, "format", ())?; + let empty: PyObjectRef = vm.ctx.empty_str.to_owned().into(); + let mut formatted = vm + .call_method(&empty, "join", (lines,))? + .str(vm)? + .to_string(); + // Remove a trailing newline if needed. + if formatted.ends_with('\n') { + formatted.pop(); + } + Ok(formatted) +} + +/// `_PyXI_excinfo_format`, with `_excinfo_normalize_type` folded in: the +/// `builtins` and `__main__` modules are left out of the name. +fn format_exc_snapshot(module: &str, qualname: &str, msg: Option<&str>) -> String { + let name = if module == "builtins" || module == "__main__" { + qualname.to_owned() + } else { + format!("{module}.{qualname}") + }; + match msg { + Some(msg) => format!("{name}: {msg}"), + None => name, + } +} + +/// Snapshot of an exception that can be rebuilt in another interpreter. +pub struct ExcInfo { + pub type_name: String, + pub type_qualname: String, + pub type_module: String, + pub msg: Option, + pub formatted: String, + pub errdisplay: String, +} + +impl ExcInfo { + pub fn capture(exc: &PyBaseExceptionRef, vm: &VirtualMachine) -> Self { + let cls = exc.class(); + let type_name = cls.name().to_owned(); + let type_qualname = cls + .as_object() + .get_attr("__qualname__", vm) + .ok() + .and_then(|o| o.str(vm).ok()) + .map_or_else(|| type_name.clone(), |s| s.to_string()); + let type_module = cls + .as_object() + .get_attr("__module__", vm) + .ok() + .and_then(|o| o.str(vm).ok()) + .map_or_else(|| "builtins".to_owned(), |s| s.to_string()); + let msg = exc.as_object().str(vm).ok().map(|s| s.to_string()); + let formatted = format_exc_snapshot(&type_module, &type_qualname, msg.as_deref()); + let errdisplay = format_traceback_exception(exc, vm).unwrap_or_else(|_| { + let mut buf = String::new(); + let _ = vm.write_exception(&mut buf, exc); + buf + }); + Self { + type_name, + type_qualname, + type_module, + msg, + formatted, + errdisplay, + } + } + + /// `_PyXI_ApplyError` for `_PyXI_ERR_NOT_SHAREABLE`: the snapshot is + /// re-raised as a plain `Exception` and becomes the cause. + pub fn into_not_shareable( + self, + msg: Option, + vm: &VirtualMachine, + ) -> PyBaseExceptionRef { + let cause = vm.new_exception_msg( + vm.ctx.exceptions.exception_type.to_owned(), + self.errdisplay.into(), + ); + let msg = + msg.unwrap_or_else(|| "object does not support cross-interpreter data".to_owned()); + not_shareable_error_from(vm, msg, cause) + } + + pub fn into_namespace(self, vm: &VirtualMachine) -> PyObjectRef { + let type_ns = crate::py_namespace!(vm, { + "__name__" => vm.ctx.new_str(self.type_name), + "__qualname__" => vm.ctx.new_str(self.type_qualname), + "__module__" => vm.ctx.new_str(self.type_module), + }); + let msg_obj = match self.msg { + Some(m) => vm.ctx.new_str(m).into(), + None => vm.ctx.none(), + }; + crate::py_namespace!(vm, { + "type" => type_ns, + "msg" => msg_obj, + "formatted" => vm.ctx.new_str(self.formatted), + "errdisplay" => vm.ctx.new_str(self.errdisplay), + }) + .into() + } +} + +/// `_PyCode_VerifyStateless`. `namespaces` holds the globals and the builtins +/// the code's `LOAD_GLOBAL` names are resolved against; without them every such +/// name stays unknown, which `_PyCode_CheckNoExternalState` lets through. +fn verify_stateless( + code: &PyCode, + namespaces: Option<(&Py, &Py)>, + vm: &VirtualMachine, +) -> PyResult<()> { + if !code.freevars.is_empty() { + return Err(vm.new_value_error("closures not supported")); + } + let Some((globals, builtins)) = namespaces else { + return Ok(()); + }; + // Having builtins to resolve against bumps numbuiltin by one, so a name + // that is neither a global nor a builtin is rejected along with the ones + // the globals do hold. + for name in global_names(code) { + if globals.contains_key(name, vm) || !builtins.contains_key(name, vm) { + return Err(vm.new_value_error("globals not supported")); + } + } + Ok(()) +} + +/// The `co_names` entries `identify_unbound_names` reaches through `LOAD_GLOBAL`. +fn global_names(code: &PyCode) -> impl Iterator + '_ { + walk_instructions(code).filter_map(|(_, instr, arg)| match instr { + Instruction::LoadGlobal { namei } => Some(code.names[(namei.get(arg) >> 1) as usize]), + _ => None, + }) +} + +/// The instruction stream with its inline caches skipped and its specialized +/// and instrumented opcodes mapped back, yielding `(offset, instruction, arg)`. +fn walk_instructions(code: &PyCode) -> impl Iterator + '_ { + let units = &code.instructions; + let mut arg_state = OpArgState::default(); + let mut offset = 0; + core::iter::from_fn(move || { + if offset >= units.len() { + return None; + } + let at = offset; + let op = units.read_op(at); + let arg = arg_state.extend(units.read_arg(at)); + if !matches!(op, Instruction::ExtendedArg) { + arg_state.reset(); + } + offset = at + 1 + op.cache_entries(); + Some((at, op.deoptimize(), arg)) + }) +} + +/// `_PyFunction_VerifyStateless`. +fn verify_stateless_function(func: &PyFunction, vm: &VirtualMachine) -> PyResult<()> { + // `__globals__` is a dict by construction, so only the builtins are checked. + let builtins = func.builtins.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!( + "unsupported builtins {}", + render_repr(&func.builtins, vm) + )) + })?; + if func.__defaults__().is_some_and(|d| !d.is_empty()) { + return Err(vm.new_value_error("defaults not supported")); + } + if func.__kwdefaults__().is_some_and(|d| !d.is_empty()) { + return Err(vm.new_value_error("keyword defaults not supported")); + } + if func.closure.as_ref().is_some_and(|c| !c.is_empty()) { + return Err(vm.new_value_error("closures not supported")); + } + verify_stateless(&func.code, Some((&func.globals, builtins)), vm) +} + +/// `verify_script`: a script takes no arguments and returns only None. +pub fn verify_script(code: &PyCode, vm: &VirtualMachine) -> PyResult<()> { + verify_stateless(code, None, vm)?; + if code.arg_count > 0 + || code.posonlyarg_count > 0 + || code.kwonlyarg_count > 0 + || code.flags.contains(CodeFlags::VARARGS) + || code.flags.contains(CodeFlags::VARKEYWORDS) + { + return Err(vm.new_value_error("code with args not supported")); + } + if !code_returns_only_none(code) { + return Err(vm.new_value_error("code that returns a value is not a script")); + } + Ok(()) +} + +/// `_PyCode_CheckPureFunction`. +fn is_pure_function(code: &PyCode) -> bool { + !code.flags.intersects( + CodeFlags::GENERATOR + | CodeFlags::COROUTINE + | CodeFlags::ITERABLE_COROUTINE + | CodeFlags::ASYNC_GENERATOR, + ) +} + +/// `_PyCode_ReturnsOnlyNone`. Here "value" means a non-None value, since a bare +/// return is identical to returning None explicitly, as is a missing return +/// statement at the end of the function. +fn code_returns_only_none(code: &PyCode) -> bool { + if !is_pure_function(code) { + return false; + } + let units = &code.instructions; + let Some(last) = units.len().checked_sub(1) else { + return true; + }; + // The last instruction either returns or raises. We can take advantage + // of that for a quick exit. + let final_op = units.read_op(last).deoptimize(); + + // Look up None in co_consts. + let Some(none_index) = code + .constants + .iter() + .position(|c| matches!(c.borrow_constant(), BorrowedConstant::None)) + else { + // None wasn't there, which means there was no implicit return, + // "return", or "return None". + + // That means there must be an explicit return (non-None), or it only + // raises. + if matches!(final_op, Instruction::ReturnValue) { + // It was an explicit return (non-None). + return false; + } + // It must end with a raise then. We still have to walk the bytecode + // to see if there's any explicit return (non-None). + return !walk_instructions(code).any(|(_, op, _)| matches!(op, Instruction::ReturnValue)); + }; + // Walk the bytecode, looking for RETURN_VALUE. + for (at, op, _) in walk_instructions(code) { + if !matches!(op, Instruction::ReturnValue) { + continue; + } + // Ignore it if it returns None. + if let Some(prev) = at.checked_sub(1) + && matches!(units.read_op(prev).deoptimize(), Instruction::LoadConst { .. }) + // We don't worry about EXTENDED_ARG for now. + && usize::from(u8::from(units.read_arg(prev))) == none_index + { + continue; + } + return false; + } + true +} + +/// Extract a script code object from source text / function / code object. +pub fn script_code(obj: &PyObject, vm: &VirtualMachine) -> PyResult> { + let code = if let Ok(code) = obj.to_owned().downcast::() { + code + } else if let Some(func) = obj.downcast_ref::() { + (*func.code).to_owned() + } else { + #[allow(unused_variables)] + let src = source_as_string(obj, vm)?; + // Compile in the caller so SyntaxError is raised here. + #[cfg(feature = "rustpython-compiler")] + { + vm.compile(&src, crate::compiler::Mode::Exec, "