Commit Graph
358 Commits
Author SHA1 Message Date
Jeong, YunWon 819b4b86fd Fix malformed Unicode error constructors (#8497)
* Fix malformed Unicode error constructors

Replace message-only Unicode decode and encode errors with fully initialized exceptions, preserve source objects and failure ranges, and remove the obsolete constructors.

Assisted-by: Codex:gpt-5

* Fix Windows Unicode CI regressions

Assisted-by: Codex:gpt-5
2026-08-17 00:33:56 +09:00
Jeong, YunWon a78d0eb577 Fix crashes found hunting the last open fuzzing record (#8524)
* specialize: check the member descriptor's type before caching its slot offset

The LOAD_ATTR/STORE_ATTR specializations cached the slot offset of any member
descriptor found on the owner's type and then guarded the specialized
instruction on the type version alone, while descr_get()/descr_set() check on
every access that the instance belongs to the type the descriptor was defined
for. A descriptor taken from a wider class and bound to a narrower one read
past the instance's slot array once the cache warmed up:

    class Big:
        __slots__ = ("a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7")
    class Narrow:
        __slots__ = ("z",)
    Narrow.x = Big.__dict__["a7"]
    o = Narrow()
    for _ in range(1000):
        try: o.x
        except TypeError: pass
    # index out of bounds: the len is 1 but the index is 7 (object/core.rs)

A class with no slots at all reached the ext_ref().unwrap() on the same line.

Assisted-by: Claude

* socket: reserve recv()'s buffer fallibly

recv() and recvfrom() handed the caller's bufsize straight to
Vec::with_capacity, so an unreachable size aborted the process through
handle_alloc_error before any syscall was made:

    socket.socket().recv(2**62)
    # memory allocation of 4611686018427387904 bytes failed -> SIGABRT

try_reserve_exact reports MemoryError instead, which is what CPython raises.

Assisted-by: Claude

* types: count the __call__ and __get__ slot dispatches as recursion

Both wrappers re-enter Python without pushing a frame, so nothing counted the
nesting when the special method named the object it was looked up on:

    class C: pass
    c = C(); C.__call__ = c
    c()                                    # native stack overflow, SIGSEGV

    class D: pass
    d = D(); D.__get__ = d; D.x = d
    d.x                                    # the same, through descr_get

with_recursion around the two dispatches raises RecursionError instead, the
way Py_EnterRecursiveCall bounds a tp_call dispatch. It costs about 5% on a
__call__ dispatch and 3% on a __get__ dispatch through these wrappers.

Assisted-by: Claude

* typevar: show a ParamSpecArgs origin by its repr

ParamSpecArgs and ParamSpecKwargs fell back to a Rust `{:?}` of __origin__ when
it had no __name__. That walks the object graph natively through Debug for
PyInner, where no recursion guard sits, so a single repr() of a deeply nested
chain overflowed the native stack:

    a = object()
    for _ in range(30000):
        a = typing.ParamSpecArgs(a)
    repr(a)                                # SIGSEGV

The origin is formatted with its repr now, which is guarded, and a ParamSpec
origin is recognized by its type rather than by carrying a __name__.

Assisted-by: Claude

* Do not hold a lock across a call back into Python

Three places kept a lock while running code that can reach the same object, so
a callback that touched it wedged the process:

    _asyncio.future_add_to_awaited_by(fut, waiter)   # waiter.__hash__ adds again
    select.select(elements, [], [], 0)               # fileno() clears `elements`
    select.poll().poll(1000)                         # SIGALRM handler registers

The future's awaited-by field is read and written under its lock but the set is
built outside it, the list extraction re-reads the list on each step the way
map_iterable_object() does, and poll() waits on a copy of its descriptors. All
three ran forever before and now finish the way they do on CPython.

Assisted-by: Claude

* Validate memoryview.cast() arguments and export negative strides correctly

cast() accepted any struct format and any shape element. A zero-size
format ('0s') and a 0 in the shape both reached a division by zero;
cast() now takes only a native single character format, optionally
'@'-prefixed, and shape elements that are ints greater than zero.

A view with a negative stride starts at its last item, so the bytes it
exported began there and its own offsets walked off the front of them.
Such a view now exports the whole underlying buffer with `start` folded
into the descriptor's offsets, and zip_eq() hands over a whole run only
when both sides are contiguous in the last dimension.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Charge native recursion to the stack, not to the frame limit

with_recursion() checked the limit sys.setrecursionlimit() sets and
incremented the same counter that pushing a frame does, so a guard on a
native dispatch spent what Python code had left to call with, and did so
where sys._getframe() cannot see it: test.support.get_recursion_available()
reported frames that were no longer there. Py_EnterRecursiveCall bounds
the native stack instead, which is a separate budget, and the C stack
check with_recursion already performs is that bound.

The snippets pinning the guarded paths nest deep enough to reach the
stack rather than the frame limit.

Assisted-by: Claude

* Report a size that cannot be allocated instead of aborting on it

A size taken from Python went straight into an infallible allocation in
several places, so the process aborted through handle_alloc_error before
any exception could be raised:

- str/bytes/bytearray center(), ljust(), rjust() and zfill() reserved
  the padded result for the caller's width
- expandtabs() built its runs of spaces from a tabsize of any width;
  the argument is a C int, and a wider one does not fit
- Buffered{Reader,Writer,Random} allocated buffer_size, and read(),
  read1() and FileIO.read() their read size
- bytes(n) and bytearray(n) allocated n
- pbkdf2_hmac() allocated the derived key length, which is a C int

Each of these now reports MemoryError, or OverflowError where the
argument does not fit the type it is declared with.

new_zeroed_bytes() leaves the zeroing to the allocator, so a large
request costs the pages that are written to rather than all of them.

Assisted-by: Claude

* marshal: answer allow_code where a code object is written or read

allow_code was answered by walking the whole result a second time, with
no depth counter and no record of what it had already seen, so a value
that referred back to itself or nested deeply enough ran off the native
stack. w_object() and r_object() answer it where the code object is,
inside the walk that already bounds its depth and resolves references.

A container length is read the way r_long() reads one: it is signed, so
a length with the top bit set is out of range rather than four billion
items to reserve room for.

load() no longer holds a borrow of the buffer read() returned across the
seek() it makes afterwards.

Assisted-by: Claude

* Do not lock an object while running code that can reach it

Several places held a lock or a borrow of an object across a call back
into Python, so a callback that touched the same object waited on a lock
its own caller was holding:

- memoryview slice assignment read a source overlapping the destination,
  and __setitem__ converted the value while holding the write borrow
- BytesIO.readinto() read into a buffer viewing the same BytesIO
- array.__setitem__ converted the value under the array's write lock,
  and mmap.write() read a source viewing the same map
- bytearray.join() and bytearray.__mod__ drove Python with the
  bytearray borrowed
- array and bytearray answered "is this resizable" after taking the
  write lock, though an export is exactly a borrow someone else holds

A TextIOWrapper cookie now has to name a position inside what was
decoded in characters as well as in bytes; only the byte offset was
checked, and the character count is what read() and tell() index with.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Stop asserting a pbkdf2 message that depends on the width of a C long

The snippet asserted "key length is too great.", which pbkdf2_hmac()
only reaches once the length has been converted; where a C long is
narrower than the length asked for, the conversion fails first and says
so instead. Both are OverflowError, which is what the case is about.

test_support.test_get_recursion_depth passes now that a native recursion
guard no longer spends frames get_recursion_depth() cannot see.

Assisted-by: Claude

* Publish and read the same pointer for a thread's top frame

set_current_frame() casts the `Py<FrameObject>` it publishes straight to
`*mut FrameObject`, so ThreadSlot::top_frame holds the object's base.
sys._current_frames() read it back through Py::from_payload_ptr(), which
subtracts the payload offset from what it is given. The reference it took
therefore incremented, and later decremented, a word 48 bytes ahead of
the frame -- inside the object allocated before it, whose OnceLock state
word sits exactly there for two frames adjacent in the size class. The
neighbour then read an initialized-looking cold pointer that had never
been written and locked whatever the uninitialized word addressed, so
the thread that owned it crashed rather than the one that read.

The slot now holds `*mut Py<FrameObject>`, which is what both sides mean.

A thread parked in a call has no FrameObject for its topmost frame, so
top_frame is null there and the reader takes the materialize path
instead: test_sys.test_current_frames never reaches the branch. The
snippet takes _current_frames() against threads that are running.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Decide stop-the-world parking under the thread registry lock

do_suspend() published SUSPENDED first and only then re-read `requested`,
restoring itself to ATTACHED if the stop had ended in the meantime. That
made a thread the second writer able to leave SUSPENDED, so a stop whose
completion check had already observed the thread parked could be undone
behind the requester's back:

  worker    CAS ATTACHED -> SUSPENDED
  requester all_non_requester_suspended() -> true, world_stopped = true
  requester start_the_world(): requested = false, then walks the registry
  worker    reads requested == false, stores ATTACHED

With the store landing inside that walk the debug assertion in
start_the_world fires; with the walk already past the slot, a following
stop force-parks the thread DETACHED -> SUSPENDED, counts it as stopped,
and the store then puts it back to ATTACHED with the world declared
stopped and the thread running bytecode.

`requested` is set in init_thread_countdown() and cleared in
start_the_world() with the registry held, and start_the_world() keeps
holding it while releasing every SUSPENDED thread. Taking the registry
around the check and the transition therefore makes the two orders the
only ones possible: park before that release pass and be woken by it, or
find the request already withdrawn and stay ATTACHED. The requester is
left as the only writer that takes a thread out of SUSPENDED, and the
self-restore is gone.

suspend_if_needed() takes the VirtualMachine to reach the registry.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Keep an atexit callback alive while it is being compared

atexit.unregister() releases the callback list around each __eq__ call and
identified the entry it had compared by the address of its Box. __eq__ can
call atexit._clear(), which drops that Box, and atexit.register(), whose
new Box lands on the freed allocation; the identity search then matched
the freshly registered callback and removed it.

  atexit.register(a); atexit.register(b); atexit.register(c)
  # __eq__ runs _clear() then register(d), returns True
  atexit.unregister(probe)

left no callbacks registered where CPython leaves d.

Entries are Arc-shared now, so unregister() holds the one it is comparing
and matches it with Arc::ptr_eq: an address cannot be reused while the
comparison that named it is still running.

Assisted-by: Claude

* Hold atexit entries in PyRc rather than Arc

PyObjectRef is Send and Sync only under the threading feature, so an Arc
over a callback entry trips clippy::arc_with_non_send_sync in builds
without it, such as the wasm package. PyRc is Arc there and Rc otherwise.

Assisted-by: Claude

* Do not hold a buffer's storage while waiting for a peer

FileIO.readinto() and socket.recv_into()/recvfrom_into() took the target
buffer's write borrow and kept it for the whole call, including the wait
for data a pipe, socket or terminal may never deliver. What CPython holds
across that wait is the export, which only forbids resizing; the borrow is
a lock every other thread touching the same object waits on, so

    threading.Thread(target=lambda: sock.recv_into(buf)).start()
    len(buf)

did not answer until the peer sent. A thread parked on that lock is
ATTACHED and never reaches a safepoint, so gc.collect() in a third thread
waited for the peer as well: one incidental read of the buffer stopped the
world from being stopped at all.

The wait now runs against storage of its own and the bytes are copied over
once they arrive, with the export held throughout so the target still
cannot be resized meanwhile. A seekable file answers from itself rather
than from a peer, so FileIO.readinto() writes into the target directly
there and the buffered read path is unchanged.

Assisted-by: Claude

* Do not hold a buffer's storage while waiting to hand it over

socket.send()/sendall()/sendto()/sendmsg() and FileIO.write() kept the
source buffer's read borrow for the whole call, including the wait for a
peer that may never make room. That borrow is a lock every other thread
writing to the same object waits on, so

    threading.Thread(target=lambda: sock.sendall(buf)).start()
    buf[0] = 1

did not return until the peer read; and a thread parked there is ATTACHED
and never reaches a safepoint, so gc.collect() in a third thread waited
for the peer too -- the same wedge readinto() had on the receiving side.

ArgBytesLike::borrow_buf_unlocked() answers with bytes that survive the
borrow being dropped. An immutable object hands out a plain reference and
locks nothing, so those are sent where they lie and bytes and memoryviews
over them cost nothing; only bytes reached through a lock are copied out
first. The export is held throughout either way, so the source still
cannot be resized while it is being sent.

The regression snippet covers both directions now and is renamed for it.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Check select()'s descriptor limit while the sequence is walked

seq2set() collected the whole sequence and compared the result's length
against FD_SETSIZE afterwards. Selectable::try_from_object() calls
fileno(), which runs Python and can append to the list being walked, and
the walk re-reads the list on every step, so the collection had no end to
reach and the comparison was never made. seq2set in Modules/selectmodule.c
checks the count per element instead.

stdlib_select.py gains a fileno() that appends to its own list, and
releases its sockets with close() rather than by dropping the name.

Assisted-by: Claude

* Bound native recursion where the C stack cannot be measured

check_c_stack_overflow() answers no unconditionally under miri and on
musl, where the stack pointer is not read. Since 9c9905aff that check is
all with_recursion() does, so every guard placed on native recursion --
__call__ and __get__ dispatch among them -- was a no-op on those targets
and the nesting ran until the stack ran out.

with_recursion() now counts its own depth on those targets and refuses
past NATIVE_RECURSION_LIMIT_UNMEASURED. The count is separate from the
frame limit sys.setrecursionlimit() sets, and compiles away where the
stack pointer can be read.

Assisted-by: Claude

* Report the failure to allocate pbkdf2's key and Take.readinto's scratch

Both buffers are sized from an argument -- pbkdf2_hmac's dklen accepts up
to i32::MAX, and readinto's from the length of the destination -- and were
built with vec![0u8; n], which aborts the process on allocation failure.
new_zeroed_bytes() raises MemoryError instead.

Assisted-by: Claude

* Read the frozen-code tuple length as a signed length

The '(' branches in read_marshal_str_vec() and read_marshal_const_tuple()
took the length with read_u32() as usize, so a value with the top bit set
read as four billion items rather than as out of range. read_len() is what
every other length in this file goes through, and it reinterprets as i32.

Both readers serve deserialize_code(), which reads only the frozen modules
baked in at build time, so this changes no reachable behavior;
marshal.loads() already went through read_len().

Assisted-by: Claude

* Make the snippets from the fuzzer sweep assert what they check

builtin_str.py expanded a tab to 2**31-1 columns, allocating 2 GiB to
observe that the width is accepted; a string with no tab observes the same
acceptance without laying anything out.

stdlib_array.py caught the refusal of frombytes() on its own exported
buffer and passed silently when nothing was raised. stdlib_hashlib.py
used a bare `assert False` as its failure branch. Both now say so through
the same shapes the other snippets use.

stdlib_socket.py also accepts OverflowError from recv() with a size that
does not fit the platform's C int.

stdlib_threading_current_frames.py indexed the frame chain for "f123"
without first asserting it is there.

Assisted-by: Claude

* Ask for a marshal container's room instead of assuming it

A flagged tuple or list is published in the reference table before its
children are read, and the placeholder was built with vec![none; len].
The length is the input's to choose and read_len() lets it reach
i32::MAX, so marshal.loads(b"\xa8\xff\xff\xff\x7f") -- five bytes -- asks
for 17 GB of element slots and aborts the process where the allocator
cannot serve it. r_object() allocates the container up front too, but
PyTuple_New() reports what it cannot get.

The elements are now reserved with try_reserve_exact() and a refusal is
raised as MemoryError through the decoder's pending-error channel.
PyTuple::new_marshal_placeholder() held nothing but that allocation and
is gone; the caller builds the elements and uses new_ref().

Assisted-by: Claude

* Compare the blocking-buffer snippet against something

measure() asserted len(buf) == len(buf), which holds whatever the length
is; it now takes the length the caller expects. The pipe case compared
the drained total against the whole source, while an unbuffered write()
reports only what it transferred and a signal can cut that short; it now
compares against what write() returned.

Assisted-by: Claude

* Refuse array.frombytes() a source whose items are not bytes

frombytes() reads its argument as bytes, but accepted any contiguous
buffer, so array("i").frombytes(memoryview(array("d", [1.0]))) appended a
double's bytes read as ints instead of raising. array_array_frombytes_impl
requires an itemsize of 1; ArgBytesLike now reports the itemsize so the
same check can be made here.

The BufferError that a resize meets while the array is exported also names
the array rather than repeating bytearray's wording.

stdlib_array.py's frombytes-of-itself case used typecode "i", where the
new check answers before the resize guard is reached; it uses "b" so the
guard is what refuses, and asserts the wider case separately.

Assisted-by: Claude

* Tell apart the ways marshal data can be bad

A type byte no reader knows, a back reference that names nothing, and
TYPE_NULL all came out as a bare "bad marshal data" ValueError. r_object()
answers the first two with "bad marshal data (unknown type code)" and
"bad marshal data (invalid reference)", and read_object() answers the
third with a TypeError, "NULL object in marshal data for object", since
TYPE_NULL stands for no object rather than for a value.

MarshalError gains the three cases and deserialize_value() maps them.
The container-specific wording r_object() uses for a NULL read inside a
tuple or list is not reproduced; the exception type is.

Assisted-by: Claude

* Hash the collector's tables by address rather than by SipHash

A collection keys three sets and two maps by object address, and it visits
every tracked object and every edge between them, so the hashing is a
per-edge cost. Those tables used the default RandomState, whose SipHash
buys resistance against a caller choosing colliding keys -- and nothing
chooses these keys: they are addresses this process handed out into tables
that live and die inside one collection. A profile of gc.collect() over a
423k-object heap spent 45% of its samples in SipHash.

They now hash with a splitmix64 finalizer. The shifts matter: a table
picks its bucket from the low bits and an address arrives with those bits
zeroed by alignment, so a plain multiply leaves every object in a handful
of buckets and is slower than SipHash was.

The reachability walk also copied each object's referent vector out of the
map it was cached in, a second pass over every edge; it reads them in
place, and reference subtraction hands its vector to the map instead of
cloning it.

Measured over 423k live objects: 0.93s to 0.15s. Over 843k dead ones:
3.00s to 0.79s. extra_tests/snippets/stdlib_threading_gc_import.py, whose
collector thread calls gc.collect() in a loop, ran anywhere from 2.7s to
28s and now runs in 3.1-3.5s: a collection that takes longer leaves more
garbage for the next one to walk, so the cost fed back on itself.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* Keep the collection's candidates and their counts in one table

A collection built a set of candidates and, beside it, a map from the same
addresses to their reference counts. Both were probed for every edge in
the heap -- membership from the set, the count from the map -- so each
edge paid to hash the same address twice, and each candidate paid to be
inserted twice. The map alone answers both questions.

The candidates also keep a walkable order now, which the reference
subtraction pass needs since it writes the counts while reading the
candidates, and which the unreachable set is built from instead of a
set difference.

Over the 423k-object heap measured in the previous commit: 0.15s to
0.13s live, and 0.79s to 0.49s dead.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* gc: collect referents into one buffer instead of a vector per object

Step 3 allocated a `Vec` for every tracked object to hold its referents
and kept them all in a map until step 4 read them back. The referents now
go into a single growing buffer, with the map holding each object's range
into it.

Adds `PyObject::gc_extend_referent_ptrs`, which appends to a caller's
buffer; `gc_get_referent_ptrs` calls it with a fresh one.

Assisted-by: Claude

* memoryview and struct: match the checks and errors of the reference

memoryview:
- `cast()` accepted a source and destination that are both item types,
  which reinterprets the items rather than re-dividing the bytes; one side
  now has to be a byte format.
- A cast to `shape=()` returned without checking that the buffer holds
  exactly the one item that shape describes.
- `hash()` hashes the bytes, so it now raises ValueError for a view whose
  items are not bytes, rather than returning a hash that disagrees with the
  value the view compares equal to.
- `tobytes()` takes the `order` argument, with 'F' walking a
  multidimensional view down its columns; `BufferDescriptor` gained
  `for_each_segment_fortran` for that walk.

struct:
- A value the format has no room for reported "argument out of range"
  instead of naming the format and its range. The format character is now
  passed to the packing functions to report it.
- `Struct.__new__` no longer reads the format; `__init__` does, so
  `__init__` can be called again and a subclass can pass the format up.
  Methods raise RuntimeError until it has run, and `Struct` is a base type.

Removes the expectedFailure from test_Struct_reinitialization and
test_struct_subclass_instantiation.

Assisted-by: Claude
Assisted-by: Codex:GPT-5

* io: decide the readinto path by file type, not by seekability

FileIO.readinto wrote straight into the caller's buffer, holding its write
borrow, when the fd was seekable; otherwise it read aside into scratch and
copied. Seekability stood in for "this read answers without waiting on a
peer", which a pipe on Windows breaks: lseek on one succeeds, so the pipe
took the borrow-holding path and every other thread touching that bytearray
waited for the peer.

host_io::reads_without_waiting answers it directly -- seekability elsewhere,
GetFileType() == FILE_TYPE_DISK on Windows.

The regression snippet times each operation separately, so a failure names
the one that waited; it asserts the transfer is still in flight before
checking the export; and the socket case fills the connection until it
refuses rather than assuming a size that outruns it, which SO_SNDBUF on an
already-connected pair does not settle.

Assisted-by: Claude
2026-08-17 00:33:27 +09:00
Jeong, YunWon c603eb7c9a Implement PEP 688 and rework the buffer protocol around managed exports (#8523)
* Implement PEP 688 __buffer__ and __release_buffer__

A Python class could not export a buffer: the slot machinery had no
bf_getbuffer or bf_releasebuffer, and every consumer acquired buffers as
PyBUF_FULL_RO through a module of PyBUF_* constants.

Add both slots. PyBuffer::release now runs a Python __release_buffer__
before the exporter's own release, once per acquisition, which PyBuffer
tracks with an `acquired` flag that clones do not inherit. An export made
by a Python __buffer__ is held by a _buffer_wrapper payload that counts its
exports and drops the returned memoryview with the last one, and the view
handed to __release_buffer__ is a _buffer_window that owns no export, so
releasing it inside the hook is inert instead of re-entering it.

Replace the PyBUF_* constants with a BufferFlags bitflags type whose
composite requests are supersets of the simpler ones, so `contains` answers
the REQ_* questions, and pass the request to PyBuffer::from_object. Each
consumer now asks for what its counterpart asks for: y* arguments for
SIMPLE, w* for WRITABLE, BytesIO.write for CONTIG_RO, bytes(), bytearray()
and memoryview() for FULL_RO. memoryview checks the request in
memory_getbuf, and array.array and mmap.mmap expose __release_buffer__.

Test buffer support with PyObject::check_buffer (PyObject_CheckBuffer)
instead of attempting an acquisition, so an exception raised by __buffer__
is no longer reported as the object not being bytes-like, and a __buffer__
with side effects runs once. PyBytesInner becomes a y* conversion as a
result: bytes and bytearray methods no longer accept iterables of ints, and
find, index, count and __contains__ take the arguments
parse_args_finds_byte and bytes_contains describe.

A view exports its start offset in the descriptor rather than in its
window, which fixes a panic when collecting from a negative-stride view.

BytesIO.write rechecks closed after acquiring its buffer, which __buffer__
can close in between.

Assisted-by: Claude Code:claude-opus-5

* Rework the buffer protocol around managed exports and view offsets

Give `PyBuffer` the `_PyManagedBufferObject` shape: one `bf_getbuffer`
acquisition is shared by every handle taken from it, cloning takes another
share instead of re-acquiring, and the exporter's release runs once when the
last share goes away. Remove `retain`, the unsafe `drop_without_release`, the
three `impl Drop`s and the `ManuallyDrop` that stood in for this. Add
`abort_acquisition` so a failed request does not run `bf_releasebuffer`.

Move the view start into `BufferDescriptor::offset`, the `Py_buffer.buf`
analogue, and drop the separate `start` fields on `PyMemoryView` and
`PyBufferWrapper`. Slicing goes through `SaturatedSlice::adjust_indices_start`,
which reproduces `PySlice_AdjustIndices` and keeps the adjusted start.

Fix `zip_eq` to take its contiguous fast path only when both last dimensions
are contiguous, and make `for_each_segment` and `zip_eq` handle zero-length and
zero-dimensional views.

Add `BufferDescriptor::projected` so a request without `PyBUF_ND`,
`PyBUF_STRIDES` or `PyBUF_FORMAT` receives a correspondingly reduced
descriptor, and reject a request without `PyBUF_INDIRECT` against an exporter
that has suboffsets.

Copy the source first in `memoryview` slice assignment when both sides reach
the same root exporter.

Hold the export across the resize in `bytearray.extend`, take `y*` in
`marshal.loads`, stop probing the buffer protocol in `FsPath`, rewrite `ord`
over the concrete string types, fold `array`'s buffer slot into one
`slot_as_buffer`, take `w*`/`y*` in `_overlapped`, and thread the new `offset`
field through the `_ctypes` descriptors.

Assisted-by: Claude

* Re-check a memoryview after a conversion and tighten cast and release

`pack_single` and `unpack_single` addressed the buffer with a position taken
before `__index__` ran, so releasing the view from that conversion read or wrote
outside the exporter's storage, panicking when it had also shrunk. Check the
released flag again once the conversion is done, as `CHECK_RELEASED_AGAIN` does.

Reject a cast to a format that is not a single native format character with an
optional `@` in front of it. An empty format reached a division by its item size
of zero. get_native_fmtchar

Report a second `__release_buffer__` on the same view as a `ValueError` rather
than accepting it, and check that the view belongs to the object first; the
silent case is a view that exports nothing. wrap_releasebuffer

Compare against another memoryview by reading its view where it lies instead of
acquiring a buffer from it, so the restricted view handed to
`__release_buffer__` compares equal rather than unequal in one direction only.
memory_richcompare

Name the type in the unraisable an exception from `__release_buffer__` reports,
as `releasebuffer_call_python` does, instead of reporting the exporter object.

Assisted-by: Claude

* Re-check the destination of a memoryview slice assignment

Acquiring the source runs `__buffer__`, which can release the destination view,
so check the released flag again once the source is in hand and before the
structures are compared, as `copy_single` does.

Build the sliced destination as a view that counts as no export, the way a
`Py_buffer dest = *view` copy does. Holding one kept the exporter unresizable
for the length of the assignment, so a source that released the view and then
resized the exporter met a `BufferError` instead of the assignment reporting the
released view.

Assisted-by: Claude

* Answer the memoryview contiguity getsets with the layout

`f_contiguous` reported the row-major answer for one dimension and False
for any more, so a view laid out both ways, such as one of shape (1, 8),
reported False while `check_buffer_request` accepted a `F_CONTIGUOUS`
request on it. `contiguous` answered row-major order alone.

Move the Fortran-order test from `PyMemoryView` to `BufferDescriptor`,
next to the row-major one it differs from only in iteration order, and
answer all three getsets with it.

Assisted-by: Claude
2026-08-16 21:32:29 +09:00
Jeong, YunWon 11de045ef8 Per-interpreter runtime state and a process-wide GC stop-the-world (#8517)
* vm: add per-interpreter runtime state and interpreter registry

- vm/runtime.rs: process-global interpreter registry (monotonic ids, weak
  entries), InterpreterWhence/InterpreterInfo, process main id recording via
  main_interpreter_id(), a threading-gated owner map (store_owned_interpreter/
  take_owned_interpreter/is_owned_interpreter/owned_interpreter_count), and the
  SUPPORTS_ISOLATED_INTERPRETERS constant.
- PyGlobalState gains interpreter_id/whence/is_main and is_main_interpreter();
  PyConfig/Settings derive Clone so a subinterpreter can clone parent config.
- Interpreter: id()/whence()/is_main()/is_process_main(),
  create_subinterpreter() and create_owned_subinterpreter(); unregister on Drop.
- thread.rs: per-interpreter thread slots (INTERP_THREAD_SLOTS), slot swap when
  switching interpreters on one OS thread, cleanup keyed by interpreter id.
- Install signal handlers and init the main-thread ident only on the main
  interpreter; _thread._is_main_interpreter reflects the current interpreter.
- sys.implementation.supports_isolated_interpreters reads the constant.
- Guard the registry static for non-threading builds where rc::Weak is !Send.

Assisted-by: Claude Code:claude-opus-4-8

* gc: stop every interpreter while collecting

The generation lists are process-global, so a collection reads and frees
objects owned by every interpreter. CollectStopTheWorld stopped only the
collecting interpreter, leaving other interpreters' threads free to mutate
the same object graph during the reference-subtraction, reachability and
snapshot phases.

Stop all live interpreters instead, in runtime id order, and restart them in
reverse. The global `collecting` mutex serializes collectors process-wide, so
no second collector takes these exclusions in another order; fork acquires a
single interpreter's exclusion, so the orders cannot cycle.

- runtime: add live_interpreter_states(), ordered by interpreter id.
- StopTheWorldState methods take &PyGlobalState instead of &VirtualMachine,
  so an interpreter's world can be stopped without a VM for it; update the
  call sites in frame, _thread, posix, faulthandler and capi.
- Document in gc_state() that the collector is process-wide: gc.disable(),
  thresholds, gc.garbage and gc.get_objects() observe process-wide state, and
  a per-interpreter collector additionally needs untrack_object (called from
  default_dealloc with no VM in scope) routed to the owning interpreter.

Tests: stop_the_world_parks_threads_of_another_interpreter asserts a thread
entered in one interpreter parks another interpreter's threads (it fails
without this change), plus a collect-while-another-interpreter-churns test.

Assisted-by: Claude Code:claude-opus-4-8

* vm: scope the interpreter registry per thread without threading

The registry held `rc::Weak` in a process-global `OnceLock` and covered the
resulting `!Send`/`!Sync` with an `unsafe impl` justifying it as "non-threading
builds are single-threaded". That is not this codebase's model: `static_cell!`
is thread-local without the `threading` feature precisely so each OS thread can
own its own `Context::genesis()` and `GcState`, so two threads could reach the
same `Rc` counts through the registry.

Use `static_cell!` for the registry as well, matching `gc_state()`, and drop the
`unsafe impl`. Ids are consequently unique per registry rather than per process
in non-threading builds, which is documented on `alloc_interpreter_id`.

Move the recorded main interpreter id into the registry so it follows the same
scoping instead of living in a separate global `OnceLock`.

Also make `CollectStopTheWorld::new` accumulate into a live guard: it built a
bare `Vec` and only moved it into the restarting `Drop` type after stopping
every interpreter, so an unwind partway through the loop left the already
stopped interpreters parked forever with their exclusion held.

Assisted-by: Claude Code:claude-opus-4-8

* vm: keep the interpreter registry usable across bootstrap, drop and fork

Three gaps where the registry did not describe the interpreters that actually
exist, each of which hides an interpreter from the collector's stop-the-world.

Register before `initialize()`. Registration ran as the last step of
`initialize_vm`, so the whole bootstrap — which executes Python bytecode and
allocates GC-tracked objects — was invisible to `live_interpreter_states()`.
It still cannot run any earlier than this: the init hooks take
`PyRc::get_mut` on the state, which fails as soon as the registry holds a weak
reference to it.

Stop unregistering in `Interpreter::drop`. The handle does not decide the
interpreter's lifetime — every `ThreadedVirtualMachine` from `new_thread()`
holds its own `PyRc<PyGlobalState>` — so an interpreter with running workers
disappeared from the registry while its threads kept mutating the object graph.
The entries are weak, so lifetime is already correct without the removal; dead
entries are now reaped when registering instead. Interpreters are consequently
released rather than unregistered at a fixed point, so the two tests asserting
disappearance now wait for it: a collection in progress legitimately holds a
reference to every live interpreter.

Repair other interpreters after fork. `py_os_after_fork_child` only fixed the
forking interpreter, leaving every other one with slots for threads that did
not survive (still ATTACHED if they were running bytecode) plus locks and
stop-the-world flags held by them. Since a collection stops all interpreters,
the child's first collection would wait for threads that no longer exist. Reset
their locks, stop-the-world state and thread tables, drop this thread's cached
slots for them, and reinit the registry's own locks first, since enumerating
interpreters now takes them.

Tests: test_gc, test_threading and test_fork1 pass, as do the vm tests in both
the threading and default configurations.

Assisted-by: Claude Code:claude-opus-4-8

* vm: attach and detach thread slots when switching interpreters

`enter_vm` decided whether to attach from `was_outermost` (an empty VM_STACK),
which held while a thread could only ever be in one interpreter. With a slot per
(thread, interpreter) pair, entering interpreter B from a thread already inside
interpreter A's section switched CURRENT_THREAD_SLOT to B's slot but attached
nothing: the thread then ran B's bytecode with B's slot DETACHED while A's slot
stayed ATTACHED. A collector stopping B force-parks the DETACHED slot and
concludes B is stopped, and then walks the object graph this thread is still
mutating.

Pair the attach/detach with the slot switch instead (≈ `_PyThreadState_Swap`):
`begin_interpreter_section` detaches the enclosing interpreter's slot, makes the
target slot current and attaches it, and `end_interpreter_section` undoes that
and re-attaches the enclosing interpreter. Both live in `set_current_vm`, which
every path making a VM current already goes through, so `enter_vm` and
`VmBootstrapGuard` no longer track outermost-ness themselves.

`nested_enter_of_subinterpreter_is_stoppable` covers this: it runs a
subinterpreter nested inside the parent's section and asserts the sub's threads
park when the sub's world is stopped. It fails with the previous
attach-at-outermost-only behavior.

Assisted-by: Claude Code:claude-opus-4-8

* vm: list only the current interpreter's subclasses

Interpreters share the context, so `class Foo(int)` in one of them pushes
onto the same `int.subclasses` every other one reads, and
`int.__subclasses__()` returned types no other interpreter can reach.

Record the creating interpreter on `HeapTypeExt` and filter
`__subclasses__` by it, the way `lookup_tp_subclasses` reads
`tp_subclasses` out of per-interpreter state for static builtin types.
Types built before any interpreter exists — the ones the shared context
creates, including the exception hierarchy — carry no id and stay visible
to every interpreter, which is what `_PyStaticType_InitBuiltin` produces
by registering the builtin subclass links once per interpreter.

The other walks over `subclasses` (version-tag invalidation, abc flag
propagation, mro updates, slot propagation) are left as they are: each
starts from a type being mutated, so from a heap type, whose subclasses
all live in the interpreter that created it.

`subinterpreter_subclasses_are_scoped_to_their_interpreter` covers this
and fails without the filter.

Assisted-by: Claude Code:claude-opus-5

* gc: give each interpreter its own collector state

The generation lists stay process-wide, because an object is untracked from
`default_dealloc`, where no interpreter is in scope to route to. What changes
is that a collection no longer acts on every interpreter's objects, and the gc
module no longer reports one interpreter's state to another.

`track_object` stamps the running interpreter into a new `gc_owner` word on the
object header, and a collection takes as candidates only the objects carrying
its own tag plus the ones carrying none. `gc_owner` fits in the padding the
header's alignment already forces, so objects do not grow; an assertion on the
header size keeps it that way.

Objects allocated with no interpreter running — everything the shared context
builds — carry no owner and stay candidates for every interpreter, which is
where they were before. Objects that outlive the interpreter that tracked them
are adopted the same way by the next full collection, rather than being left to
a collector that will never come.

`enabled`, the thresholds, the debug flags, the statistics, `gc.garbage` and
`gc.callbacks` move onto `PyGlobalState` — the last two off the shared
`Context` — so `gc.disable()`, `gc.set_threshold()`, `gc.get_stats()`,
`gc.get_objects()` and `gc.garbage` describe the interpreter that asks. The
occupancy counts behind `gc.get_count()` and `gc.get_freeze_count()` stay
process-wide: they measure how full the shared lists are. Their decrements are
saturating now, since a collection zeroes the generations it emptied while
another interpreter's objects are still sitting in them.

Stop-the-world still stops every interpreter. Unowned objects are candidates
and any interpreter can incref one, so the refcounts a collection reads are
only stable while all of them are parked.

This also fixes a deadlock it exposed: `CollectStopTheWorld` dropped its
references to the stopped interpreters while the collection still held the
generation read locks, so releasing the last reference to one — which frees its
objects, and so untracks them — waited for a write lock behind that read lock.
The references are now held until the guard itself drops.

`collections_only_reach_the_collecting_interpreter` and
`get_objects_only_reports_the_calling_interpreter` cover this and both fail
without the owner check.

Assisted-by: Claude Code:claude-opus-5

* _queue, _thread: detach before taking locks held across waits

`_queue.Semaphore` holds its mutex across the `allow_threads` condvar wait,
and `join_internal` holds a thread handle's completion mutex the same way.
Stop-the-world can stop a thread while it holds either one. The remaining
acquisitions ran attached, so a thread blocking on such a mutex had no
safepoint left to reach: the stop never completed, and the holder was never
resumed to release it.

Route those acquisitions through helpers that detach first. The fork-child
reinit paths keep their direct locks.

Assisted-by: Claude

* _io, _winapi: detach on the remaining stopped-holdable lock takes

`TextIOWrapper.__repr__` took `data` directly while every other method takes it
through `lock_opt`, which detaches. `Overlapped` holds `inner` across the
`allow_threads` in `GetOverlappedResult`, and all four of its takes were direct.

A thread stopped by stop-the-world can be holding either mutex, so taking one
while attached left the blocked thread with no safepoint to reach.

Assisted-by: Claude

* gc: size the interpreter owner tag to the header padding

`gc_owner` was a u32. With 4-byte pointers its alignment pushed it out of the
padding that follows the gc bits and generation, growing every object by a
word and tripping the `SIZEOF_PYOBJECT_HEAD` assertion on 32-bit targets.

Introduce `GcOwner = u16`, which the `repr(C)` layout places at offset 10 on
32-bit and 18 on 64-bit, leaving the header at 6 words on both. Tags now run
out after 65535 interpreters; `alloc_owner` already falls back to
`GC_NO_OWNER`, so an interpreter past that collects as it did before tagging.

Also widen three test deadlines that measure liveness, not speed.

Assisted-by: Claude

* vm: reuse cleared frame blocks and shorten interpreter hot paths

- `datastack` remembers the most recently popped frame block. `push_frame`
  reports an exact LIFO reuse, and `setup_datastack_frame` then skips
  zero-filling localsplus.
- Small-int loads push the context's cached int as a borrowed stack reference
  instead of taking a new one.
- Calls to jitted functions go straight to `execute_call_vectorcall`.
- Binary-op specialization reads ints through the new `PyInt::try_to_i64_fast`
  instead of the generic primitive conversion, and `try_to_bool` moves its
  non-bool path into a `#[cold]` helper.
- Dict caches read an entry through a keys-version stamp
  (`get_index_if_keys_version`) rather than an entry-index hint.
- Frame publishing caches a pointer to `ThreadSlot::top_iframe` in a
  thread-local `Cell` instead of borrowing `CURRENT_THREAD_SLOT`.

* gc, vm: address review notes on owner tags and interpreter docs

Look up retired owner tags with a sorted binary search instead of a linear
scan, which every scanned object paid for once per dropped interpreter.

Drop the claim that clearing a tag frees it for reuse; `alloc_owner` only
ever hands out new tags. Also correct the tag space it mentions, which is
16-bit since the tag was sized to the header padding.

Document `is_main` as "top-level interpreter" rather than "the process
main": every top-level interpreter sets it, and only the first registered
one becomes the main `main_interpreter_id` reports.

Assert membership rather than an exact owned-interpreter count delta; the
owned table is process-global and other tests store into it in parallel.

Assisted-by: Claude

* vm: gate interpreter registration on stop-the-world admission

A collection snapshots the registry, stops the interpreters it found, and
then reads tracked objects with their threads parked. An interpreter that
registered after the snapshot was taken was absent from it, so nothing
stopped it and its bootstrap ran Python — allocating and mutating the
shared generation lists — underneath that scan.

Registration and the stop now share a process-global gate: the collection
holds it from before the snapshot until the restart, and registration takes
it around the registry insert. The insert runs detached, since a thread
that waited for the gate, or re-attached while holding it, would leave the
stop it waits for no safepoint to complete at.

Assisted-by: Claude

* gc: keep the tracking counters off the barrier path

Every tracked allocation and every free went through sequentially consistent
counter updates, and each free through a `fetch_update` CAS loop. The counters
drive only the gen0 threshold and `gc.get_count()`, and the generation locks —
not the counters — order the list changes they describe, so they are relaxed
now and the decrement is a load plus a conditional `fetch_sub`.

`is_enabled` and `threshold`, both read once per allocation, are relaxed for
the same reason: an allocation racing `gc.disable()` may use either value.

Drop `alloc_count`, which nothing has ever read.

Assisted-by: Claude

* dict: settle lookups and iteration steps under one read guard

A lookup probed under a read guard, dropped it with the matched entry in
hand, and then took the lock again to re-find the entry. `lookup_extract`
reads the entry while the probe still holds the guard when key identity
settles the match, which is the case that cannot run Python.

Dict and set iterators took one lock to compare the size and another to read
the entry, then cloned both the key and the value even though a keys or
values view keeps only one of them. `next_entry_checked` does the size check
and the read under one guard, and clones through a per-view projection.

A store that missed its inline-cache hint re-probed the dict afterwards only
to recover the entry index the store had just computed; `unchecked_push`
reports that index instead.

Assisted-by: Claude

* vm: build a call's argument vector once

The specialized CALL handlers collected the positional arguments into one
vector and then copied them into a second one to put `self` in front, so
every specialized builtin, method-descriptor, class and non-Python call paid
two allocations, a copy and two frees. The arguments are already laid out on
the value stack in vectorcall order, so `take_call_args` fills one vector by
index — the shape `execute_call_vectorcall` already used.

`vectorcall_native_function` and the keyword path of `vectorcall_function`
then cloned that vector again to build `FuncArgs`; both now move it in
through `from_vectorcall_owned`, as the other vectorcall slots do.

Assisted-by: Claude

* vm: reach an instance dict without cloning it

The specialized attribute instructions cloned the instance dict — a rwlock
round-trip plus a refcount round-trip — for two things that only look at it:
`LoadAttrMethodLazyDict` asking whether the dict exists, and the keys-version
stamp check that is the whole of `shadowing_instance_attr`'s fast path. Both
now read it borrowed, through `has_instance_dict` / `with_instance_dict`.

`generic_getattr_opt` probed the dict with the name's `&Wtf8`, which hashes
the name on every lookup and can never match a key by pointer. Passing the
`Py<PyStr>` uses the string's cached hash and the interned-key identity
check, and drops an allocation when a stored key is not an exact `str`.

Assisted-by: Claude

* vm: shorten the per-instruction safepoint and the call preamble

The dispatch loop asks once per instruction whether stop-the-world wants this
thread, and that read went through `CURRENT_THREAD_SLOT` — a `RefCell` borrow,
so two stores to thread-local memory around an `Option` test. Cache the
`stop_requested` pointer in a plain `Cell` at the same three places the frame
pointers are cached, and the safepoint becomes one relaxed load.

`lasti` is advanced from the index the loop just read, rather than reloaded
to increment it, and `Resume` reads `quickened` before swapping it, so a call
to an already-quickened code object costs a load instead of an atomic
read-modify-write.

An exact-args vectorcall to a Python function built a heap `FrameObject`
where the equivalent `invoke` path uses a data stack frame; it now does the
same when tracing is off.

Assisted-by: Claude

* vm: give KwArgs a zero-sized hasher

`KwArgs::default()` is built for every call, keyword-less ones included, and
it seeded a `RandomState` each time — a thread-local read and 16 bytes in
every `FuncArgs`. Keyword names come from the program text, so the map now
uses `BuildHasherDefault`, whose `Default` is a zero-init.

Assisted-by: Claude

* vm: promote borrowed stack refs before a yield

`yield <small int>` tripped the "borrowed refs on stack at yield point"
assertion: `LoadSmallInt` pushes a borrowed ref, and a yield saves the stack
with the frame, which is exactly what that assertion forbids. Promote the
stack first, so a suspended frame owns everything it holds.

Assisted-by: Claude

* vm: run the exact-args vectorcall on a heap frame again

`vectorcall_function` took the data stack path for an exact-args call whenever
tracing was off. A data stack frame that materializes — one traceback entry is
enough — copies its localsplus into the `FrameObject`, so every value it holds
gains a reference for as long as the frame runs, and a tail call holds the
callee's function object until the caller's next call or return. Both are
reachable without this hunk, through the `CallPyExactArgs` specialization, but
routing the vectorcall entry point through the data stack made them apply from
a call site's first execution: test_enumerate, test_memoryio, test_sys,
test_tempfile and test_traceback all read a refcount or a finalization that
moved.

Assisted-by: Claude

* vm: release a tail call's function at the callee's return

The trampoline moved the callee's function object into the caller's
`SuspendedFrame` and dropped it only once the caller had run again, so a
function entered through a tail call kept one reference past its return:
`sys.getrefcount` on it read one high for the caller's next stretch of
bytecode, and anything the function reached stayed alive that much longer.

The callee's frame is released before the `ReturnValue` or `Unwind` action is
formed, and a materialized frame object holds its own references, so the owner
is dropped where the suspended caller is popped instead.

Assisted-by: Claude

* vm: stop copying a running frame's locals when it materializes

`materialize_slow` cloned the source frame's fast locals into the new
`FrameObject`, so every value in a data stack frame gained a reference lasting
as long as that frame object: one traceback entry through a frame kept all of
its locals alive, and `sys.getrefcount` read one high for each of them.

Nothing read that copy while the frame ran. `find_live_source_iframe` already
routes reads to the live frame, and `exit_iframe` fills the slots from it at
return, so the copy now starts empty as `materialize_slow_chain`'s does, and
`snapshot_to_heap` goes with its last caller.

Two readers had to be brought to the live frame the same way.
`has_active_hidden_locals` read the frame object's own slots, which let
`locals()` in a class body write an inlined comprehension's hidden variable
into the class namespace; it now shares `live_fastlocals` with the other two.
Another thread's frame has no live frame to reach from here, so the six
cross-thread sites fill the copy under stop-the-world through
`materialize_with_locals`.

Assisted-by: Claude

* vm: key data stack frame reuse on the exact frame size

push_frame and pop_frame compared 16-byte-aligned sizes, so two frames
whose sizes differ by less than the alignment matched as a reuse. The
caller reads that as "every slot was cleared by the previous frame", but
the larger frame's tail slots were never touched by it.

Assisted-by: Claude

* vm: store the frame object payload address in the cross-thread slot

set_current_frame and reinit_frame_slot_after_fork wrote the address of
the Py<FrameObject>, while the cross-thread readers hand that pointer to
Py::from_payload_ptr, which subtracts the object header from it. Both
writers now store the payload address.

Assisted-by: Claude

* vm: copy a foreign thread's frames instead of linking them

sys._current_frames(), _thread._current_exceptions() and the cross-thread
f_back path handed back frame objects still linked to the running frames.
The owning thread wrote its locals into those same buffers at exit_iframe
while the reader was cloning out of them. materialize_detached and
materialize_detached_chain copy the values under stop-the-world and leave
nothing for the owner to write into; materialize_with_locals is gone.

FrameColdData gains attached_tid, the thread still running the frame a
frame object was materialized from, which exit_iframe clears once it has
written the values in. check_locals_access now rejects a read from any
other thread while it is set; the FrameOwner test could not, because
materialize_slow forces FrameObject ownership.

Also removes with_iframe, which had no callers.

Assisted-by: Claude

* vm: resolve the two rustdoc links this branch added

MAIN_INTERPRETER_ID is not in scope in interpreter.rs, and new_thread only
exists in a threading build, so neither link resolved.

Assisted-by: Claude
2026-08-16 21:21:31 +09:00
Jiseok CHOI dbd3a0a6b3 sqlite3: add SQLITE_DBCONFIG constants, setconfig() and getconfig() (#8535)
* sqlite3: add SQLITE_DBCONFIG constants, setconfig() and getconfig()

Implement Connection.setconfig() and Connection.getconfig() using
sqlite3_db_config(), and export all SQLITE_DBCONFIG_* integer
constants to the module.

Fixes test_connection_config in test_dbapi.py.

Assisted-by: GitHub Copilot:claude-sonnet-4-6

* sqlite3: fix setconfig to propagate actual SQLite error

When sqlite3_db_config() returns a non-OK result, propagate the actual
SQLite error instead of replacing it with a generic 'Unable to set
config' message, to match CPython's set_error_from_db() behavior.

Assisted-by: GitHub Copilot:claude-sonnet-4-6
2026-08-16 20:13:10 +09:00
Jiseok CHOI e19b622174 sqlite3: pass None for NULL authorizer args instead of crashing (#8534)
* sqlite3: pass None for NULL authorizer args instead of crashing

CPython's authorizer callback receives NULL for arg1/arg2/db_name/access
when not applicable (e.g. SQLITE_READ on a table gives NULL for the
database name in some versions). Previously RustPython passed these
pointers to ptr_to_str which would crash or produce an error.

Now ptr_to_str_or_none is used: NULL pointers become Python None, which
matches CPython behavior and allows test_table_access and
test_column_access to pass.

Assisted-by: GitHub Copilot:claude-sonnet-4-6

* fix(sqlite3): avoid reentrant deadlock in Statement::new

sqlite3_prepare_v2 can synchronously invoke the authorizer callback,
which may call back into Connection methods (e.g. set_authorizer)
that require the same db lock. Holding db_lock across the prepare()
call caused a self-deadlock when a callback re-entered the connection.

Release the lock after sql_limit check and copy the raw handle before
calling prepare(), so FFI calls that can trigger Python re-entrancy
happen outside the lock scope.

Fixes hang in test_authorizer_concurrent_mutation_in_call
2026-08-15 21:05:33 +09:00
Jiseok CHOI a142de9abc sqlite3: add Connection exception attributes and fix autocommit ValueError (#8533)
Add DB-API 2.0 optional extension: expose exception classes as attributes
on Connection objects (Warning, Error, InterfaceError, DatabaseError,
DataError, OperationalError, IntegrityError, InternalError,
ProgrammingError, NotSupportedError).

Also fix autocommit validation to raise ValueError (not TypeError) when
an unsupported type is passed.

Assisted-by: GitHub Copilot:claude-sonnet-4-6
2026-08-15 21:05:12 +09:00
Jiseok CHOI 7cda53b946 sqlite3: validate narg before creating functions/aggregates (#8532)
CPython 3.14 added check_num_params() which raises ProgrammingError when
narg/n_arg/num_params is out of range (-1..=SQLITE_LIMIT_FUNCTION_ARG).
RustPython passed invalid values directly to SQLite, resulting in an
OperationalError instead.

Add check_num_params() helper and call it in create_function(),
create_aggregate(), and create_window_function().

Assisted-by: GitHub Copilot:claude-sonnet-4-6
2026-08-15 21:04:45 +09:00
Jeong, YunWon 259ce8ce66 Fix the itertools.tee leak and race, the contextvars and generator races, _asyncio's InvalidStateError lookup, and _imp's frozen data (#8518)
* _imp: read get_frozen_object's data as a whole marshal value

The data argument went through deserialize_code(), which reads a code body
without the type byte in front of it, so nothing marshal.dumps() produces was
accepted. Read it with marshal.loads() and require a code object back.

Assisted-by: Claude

* _imp: bind find_frozen's arguments with FromArgs

The arity check was done by hand on a FuncArgs. Take the arguments through a
FromArgs struct instead, which makes withdata keyword-only, and fill in the
data it asks for: the frozen encoding is not marshal, so the code is
re-serialized into what get_frozen_object() reads back.

Assisted-by: Claude

* itertools: give tee's shared buffer a Python type

The buffer was a PyRc<PyItertoolsTeeData>, which is not a Python object, so the
collector could not walk into it and any cycle running through a tee was
uncollectable. Make it the _tee_dataobject type with a traverse, held by
PyRef, and split the rest along the same lines: tee() is a function, _tee is
the iterator type it builds, and _tee takes a single iterable rather than
returning a tuple from __new__. _tee is weak-referenceable, tee() rejects a
negative n with ValueError and reserves its tuple fallibly.

test_itertools.test_tee passes now; its expectedFailure marker is removed.

Assisted-by: Claude

* contextvars: hold the shared context state under locks

The variable map was a RefCell, the enter flag, the context index, the token's
used flag and the variable hash were Cells, and each carried an unsafe impl
Sync. A Context or ContextVar shared between threads overlapped their borrows
and panicked. The map and the per-variable cache are now PyMutex, the flags and
the index are atomics, entering a context is a compare_exchange, and the three
unsafe impl Sync are gone.

The cache also stopped being read through AtomicCell::as_ptr, which raced a
concurrent store on a value holding a PyObjectRef.

Values displaced from the map or the cache are dropped after the lock is
released: __del__ can come straight back into the same context, and the locks
are not reentrant.

Assisted-by: Claude

* generator: read the frame state under the running claim

send(), send_none(), throw() and close() read `closed` and `frame.lasti()`
before `running` was compare_exchanged, so the frame they went on to resume
could be one another thread had already advanced. A resume that decided from
`lasti() == 0` that the generator had not started pushes no value onto the
value stack, and the code after the yield pops one, which underflows the stack.

The compare_exchange now hands back a guard, taken before those reads and
released after maybe_close(), so the generator is retired while it is still
claimed.

Assisted-by: Claude

* itertools: claim a tee's position and its buffer as one step

`_tee::next` read `index` and moved it on afterwards, and
`_tee_dataobject::get_item` released `running` before the value it fetched
from the source was cached.

Two callers on one `_tee` then read the same index, hand out the same value
twice and advance past a value that was never cached, which leaves `index`
past `values.len()` and indexes the buffer out of bounds. Two callers at the
same index on separate tees each fetch a value from the source, and one of the
two is dropped without reaching a caller.

Both claims now cover the read and the update.

Assisted-by: Claude

* _asyncio: require InvalidStateError to be a type

new_invalid_state_error() called whatever `asyncio.exceptions.InvalidStateError`
names and unwrapped the downcast of the result, so a future asked for a result
it does not have panicked once that attribute was rebound to something that is
not an exception:

    asyncio.InvalidStateError = lambda *args: 42
    _asyncio.Future(loop=object()).result()

The type is looked up the way get_cancelled_error_type() looks its own up, and
raised with new_exception_msg; a lookup that does not produce an exception type
falls back to RuntimeError, as the other arms already did.

Assisted-by: Claude
2026-08-14 16:05:30 +09:00
Jeong, YunWon d5d19e0611 Fix 33 reproduced fuzzing and static-review defects (#8514)
* mmap: guard move() against dest/src past the mapping size

An offset larger than the mapping made `size - dest` underflow, producing
an out-of-range slice index panic. Check the high side first, as write() does.

Assisted-by: Claude

* exceptions: keep ImportError name/path/name_from out of an empty instance dict

ImportError.__reduce__ unwrapped get_arg(0), which is None when the exception
was constructed with no positional argument, aborting on pickle.dumps(ImportError()).
Fall back to the full args tuple there, and expose name/path/name_from as class
attributes defaulting to None so a bare ImportError() reduces to (cls, ()).

Assisted-by: Claude

* asyncio: stop unwrapping the _current_tasks downcast in current_task()

_asyncio._current_tasks is a reassignable module attribute; the three sibling
functions already degrade gracefully when it is not a dict, current_task() did not.

Assisted-by: Claude

* asyncio: raise TypeError when FutureIter.throw()'s exception class returns a non-exception

exc_type.call() goes through a Python-controlled __new__, so its result can be
any object; the downcast was unwrapped. Report it as a TypeError instead.

Assisted-by: Claude

* sequence: use a fallible reservation for sequence repetition

The guard only rejects a repeat whose per-element size crosses MAX_MEMORY_SIZE,
so (1,) * (10**12) still reached Vec::with_capacity and aborted in the allocator.
Reserve fallibly and surface a MemoryError.

Assisted-by: Claude

* collections: reject an oversized deque repetition with MemoryError

deque * sys.maxsize reached the allocator and aborted with a capacity overflow.
Apply the MAX_MEMORY_SIZE guard the other sequences already carry.

Assisted-by: Claude

* itertools: raise OverflowError for an out-of-range r argument

combinations/combinations_with_replacement/permutations narrowed r with
to_usize().unwrap(), so r=2**64 panicked instead of raising.

Assisted-by: Claude

* math: stream the generic sumprod path instead of collecting both iterables

The big-int path collected each argument into a Vec before multiplying, so an
unbounded iterable exhausted memory. Advance both iterators in lockstep and
accumulate, reporting a length mismatch when only one is exhausted.

Assisted-by: Claude

* _imp: raise TypeError for a second positional argument to find_frozen

withdata is keyword-only and unimplemented; passing it positionally hit an
unimplemented!() and aborted.

Assisted-by: Claude

* _typing: check _idfunc arity before indexing args

_typing._idfunc() with no argument indexed args[0] out of bounds.

Assisted-by: Claude

* exceptions: require a sequence for the ExceptionGroup excs argument

The argument was collected before being validated, so an unbounded iterable
such as itertools.count() exhausted memory. Reject a non-sequence up front.

Assisted-by: Claude

* mmap: treat an inverted find/rfind range as empty

find(b"x", 5, 2) built a slice whose start exceeded its end and panicked.

Assisted-by: Claude

* collections: give deque and defaultdict a GC traverse

Neither type opted into traversal, so a reference cycle through a deque or a
defaultdict was never collected.

Assisted-by: Claude

* itertools: opt the iterator types into GC traverse

cycle and its siblings hold Python references but declared no traverse, so a
cycle built through one of them leaked.

Assisted-by: Claude

* _ctypes: mask an out-of-range int instead of panicking

c_char_p(2**64) and pointer item assignment called .expect() on the narrowing
conversion. Wrap the value to the target width, which is what the C
implementation stores.

Assisted-by: Claude

* hashlib: import _hashlib when a hash module is loaded

The hash object type is a static type owned by _hashlib; calling _md5.md5()
without _hashlib imported hit an uninitialized static type and panicked.

Assisted-by: Claude

* _csv: fall back to the built-in dialect defaults when none is registered

The dialect table is empty until csv.py registers 'excel', so _csv.reader([])
unwrapped a missing entry and panicked.

Assisted-by: Claude

* builtins: reject surrogates in compile()/eval() source instead of panicking

expect_str() panics on a str containing surrogates; convert with try_as_utf8
so eval(chr(0xd800)) raises.

Assisted-by: Claude

* _suggestions: require a list for _generate_suggestions candidates

The argument was collected before validation, so an unbounded iterable
exhausted memory.

Assisted-by: Claude

* lzma: size the filter chain before consuming it

filters= was collected into a Vec before the length check, so an unbounded
iterable exhausted memory. Take the length through the sequence protocol first.

Assisted-by: Claude

* classmethod: opt into GC traverse

staticmethod already declares traverse; classmethod did not, so a cycle through
the wrapped callable leaked.

Assisted-by: Claude

* posix: reject unbounded iterables in posix_spawn and setgroups

argv, setsigdef, setsigmask and setgroups bound an ArgIterable and collected it
before validating, so an infinite generator exhausted memory. Require a
list/tuple for argv, validate signals while streaming, and take setgroups
through the sequence protocol.

Assisted-by: Claude

* _ctypes: size Array slice assignment and _argtypes_ before collecting

Both eagerly collected their argument, so an unbounded iterable exhausted
memory before the length check ran.

Assisted-by: Claude

* sys: propagate the breakpointhook warning failure

warn() was unwrapped, so an unimportable $PYTHONBREAKPOINT under
-W error panicked instead of raising.

Assisted-by: Claude

* structseq: require the sequence argument when constructing a struct sequence

A no-argument construction produced an empty backing tuple, and reading any
named field then indexed out of bounds.

Assisted-by: Claude

* Guard the hash slot dispatch against unbounded recursion

`PyObject::hash` invoked the type's hash slot directly, so an element-wise
`__hash__` following a deeply nested object graph recursed one native frame
per level and overflowed the stack. Wrap the dispatch in `with_recursion`,
matching the repr and rich-compare dispatches in the same file, so
`hash(x)` on a nested tuple/GenericAlias/slice raises `RecursionError`.

Assisted-by: Claude

* genericalias: guard the __parameters__ walk against unbounded recursion

`make_parameters_from_slice` recursed into every list/tuple argument with
nothing counting the frames, so `list[L]` for a self-referential or deeply
nested `L` overflowed the native stack at subscript time. Wrap the descent
in `with_recursion`, which makes the walk fallible; `PyGenericAlias::new`,
`from_args` and `make_parameters` now return `PyResult` and every caller
propagates.

Assisted-by: Claude

* Fix the type confusion in PyAtomicRef's Debug impl

`PyAtomicRef<T>` stores a pointer to a `Py<T>`, as `Deref`, `load_raw`,
`swap` and `Drop` all read it, but `Debug` cast it to a bare `T` and
formatted the object header as payload bytes. For `PyFunction`, whose
`code: PyAtomicRef<PyCode>` has a pointer-chasing `Debug`, that
dereferenced header words and segfaulted.

Cast to `PyObject` instead, which is what `Drop` already does and which
also covers the `PyAtomicRef<PyObject>` and `PyAtomicRef<Option<T>>`
instantiations that have no `Py<T>`.

`_asyncio._enter_task` reached this through `{:?}` in its "Cannot enter
into task" message; format the two tasks with their Python repr, which is
what the message is meant to show.

Assisted-by: Claude

* _sre: disallow instantiating Match

`Match` inherited `object.__new__`, so `M.__new__(M)` produced an instance
whose `regs`/`string`/`pattern` were never filled in by a match run; the
mapping subscript path read them and dereferenced garbage. The type has no
public constructor, so mark it `DISALLOW_INSTANTIATION`, which makes
`M.__new__(M)` raise `TypeError: cannot create 're.Match' instances`.

Assisted-by: Claude

* utils: return the empty repr instead of asserting a non-empty collection

`collection_repr` took the first element with an `.expect()` justified by the
caller's preceding non-empty check. Another thread clearing the collection
between that check and the iteration made the iterator yield nothing and
panicked the worker. Fall back to the caller-supplied empty form, which is
the text those callers already produce for an empty collection.

Assisted-by: Claude

* itertools: advance cycle's index atomically

`cycle.__next__` did `fetch_add(1)` and then reset the index to 0 in a
separate store, so two threads replaying the saved items could both read an
index past the end of `saved` and panic on the slice access. Do the advance
and the wrap in one `fetch_update`.

Assisted-by: Claude

* _ctypes: reject a float argument to a foreign call without argtypes

`conv_param`, the conversion used when `argtypes` is not set, converted its
argument with `try_int`, which goes through `__int__` and so accepts a
float. `libc.strlen(1.5)` therefore passed 1 where the callee expects a
`char *` and the callee dereferenced it. Match `ConvParam`, which does a
`PyLong_Check` and converts nothing: take the branch only for an `int` (or
a subclass, so `True` still converts), and let a float fall through to
"Don't know how to convert parameter".

The branch below it converted a float to a C double, but `try_int` claimed
every float before it could run, so it was dead; `CArgValue::Double`
existed only for that branch and both go. Typed doubles are unaffected —
they travel as `CArgValue::Typed` with code 'd'.

Assisted-by: Claude

* Report the iterator itself from PyIter's traverse

`Traverse for PyIter<O>` delegated to the inherent `PyObject::traverse` of
the object it wraps, so it reported that iterator's referents instead of
the iterator. The iterator's own reference to those referents was then
never subtracted during the collector's reference-subtraction pass, the
referents kept a non-zero gc_refs, and every object reachable from them
was classified as a root.

Any cycle running through a type with a `PyIter` field therefore survived
collection: `map`, `filter`, `zip`, `enumerate`, `reversed` and the
`itertools` iterators all leaked, while the same cycle through a `list`,
`tuple` or `list_iterator` collected. Report the wrapped object, as the
`PyObjectRef`, `PyRef<T>` and `PyStackRef` impls do.

`itertools.tee` still leaks: its shared buffer is a `PyRc<PyItertoolsTeeData>`
rather than a Python object, so the collector cannot see through it.

Assisted-by: Claude

* Remove the obsolete expectedFailure on test_code_module.test_unicode_error

Compiling a source string containing a lone surrogate now raises
UnicodeEncodeError, so the test passes.

Assisted-by: Claude

* itertools: reserve the combination indices fallibly

`combinations` and `combinations_with_replacement` built their index vector
with an infallible allocation, so an `r` that passes the ssize_t check but
does not fit in memory aborted the process instead of raising MemoryError.

Assisted-by: Claude

* Apply the struct sequence constructor's dict argument

`structseq(sequence, dict)` discarded its second argument, so the hidden
fields past `n_sequence_fields` — `tm_zone`, `st_atime` and friends — were
always None when constructed directly or restored from a `(sequence, dict)`
pickle, and a non-dict second argument was accepted silently.

Take the dict, require it to be a dict, and fill the hidden slots the
sequence did not cover from it. A key that names a field the sequence
already supplied, or no field at all, is now a
"got duplicate or unexpected field name(s)" TypeError instead of being
dropped. Both arguments are bindable by name, as `sequence` and `dict`.

`os.stat_result` and `os.statvfs_result` did not accept a second argument
at all; they and `time.struct_time` now share the parsing.

Assisted-by: Claude

* _imp: report the argument count in find_frozen's arity error

Assisted-by: Claude

* Add regression tests for the reproduced crashers

One case per catalog entry, each asserting the behavior the fix produces:
recursion guards, the memory-unsafety sites, the overflow and unbounded
allocation guards, the eager-collection rejections, the unwrap sites, and
the cycles the collector now breaks. Every expected value was checked
against CPython 3.14.

Assisted-by: Claude

* Tolerate a changed-size RuntimeError in the set repr stress test

The mutator and reader threads race on purpose; a "changed size during
iteration" RuntimeError is a valid outcome of that race and should not fail
the test. The panic it guards against is not.

Assisted-by: Claude

* Move the crash regression tests into per-module snippets

crash_regressions.py collected every reproduced crasher in one file. Split it
into the snippet for the module each case exercises, and add stdlib_gc.py,
stdlib_asyncio.py, stdlib_lzma.py, stdlib_threading_set_repr.py and
stdlib_threading_itertools_cycle.py for the cases with no existing home.

The suite runs every snippet under the host CPython too, so the checks only
RustPython raises are guarded by sys.implementation.name: the hash and
__parameters__ recursion depth, and the deque repeat overflow. The float
ctypes argument accepts either TypeError or ctypes.ArgumentError.

Assisted-by: Claude
2026-08-14 00:57:10 +09:00
Jeong, YunWon 161ba746fc ssl: pass test_ssl with the rustls backend (#8502)
* Fix rustls test_ssl compatibility

Assisted-by: OpenAI Codex:GPT-5

* Keep urllib3 compatible SSL version prefix

Assisted-by: OpenAI Codex:GPT-5
2026-08-12 17:55:23 +09:00
Jeaeun Kim 6b747b5a76 ssl: support explicit session reuse (#8466)
* ssl: support explicit session reuse

Assisted-by: Codex-5.6-sol

* ssl: support explicit TLS 1.3 session reuse

Assisted-by: Codex-5.6-sol
2026-08-10 00:43:44 +09:00
lms0806 25e2df1731 Implement TclObject support in tkinter varname_converter (#8465) 2026-08-08 22:30:43 +09:00
Joshua Megnauth 3c70a1ba58 Replace deprecated chrono with jiff (#8436)
See: chronotope/chrono#1768
2026-08-04 16:50:22 +09:00
Jeong, YunWon cbd77af58c socket: accept a filesystem-encoded hostname in sethostname (#8437)
`socket.sethostname` took `PyUtf8StrRef`, so it rejected `bytes` outright and
refused a `str` carrying a surrogate escape. `socketmodule.c
socket_sethostname` accepts a bytes object directly, falls back to
`PyUnicode_FSConverter` for anything else, and hands the syscall the resulting
buffer and its length — the name is never required to be UTF-8.

Take `FsPath`, the converter `if_nametoindex` in this same module already uses,
and pass its bytes down. `host_env::socket::sethostname` correspondingly takes
`&[u8]` and builds the `OsStr` from them; `nix::unistd::sethostname` accepts
`AsRef<OsStr>` and passes pointer and length to the syscall, so nothing on the
path needs a NUL terminator or valid UTF-8.

`Lib/test/test_socket.py test_sethostname` covers this: it calls
`socket.sethostname(b'bar')` and asserts the hostname changed. The test is
skipped unless run as root, which is why the gap went unnoticed.

Assisted-by: Claude
2026-08-03 21:46:27 +09:00
Jeaeun Kim 4ad64a99a2 ssl: store SSL socket owners as weak references (#8423)
Assisted-by: Codex:gpt-5.6-sol
2026-08-03 01:38:44 +09:00
Jeaeun Kim 8c241d86bc fix(socket): use object repr in deallocation warnings (#8422)
Assisted-by: Codex:gpt-5.6-sol
2026-08-02 00:25:43 +09:00
이광효 beff2b02ac csv: port CPython reader state machine (#8381)
* csv: port CPython reader state machine

Replace the csv-core reader and per-item quote scanner with one Rust
implementation of CPython's nine-state reader parser. Keep parser state
across iterator items and distinguish virtual item boundaries from true
iterator exhaustion.

Centralize field completion so quote provenance, empty-field None conversion,
float conversion, strict parsing, field limits, blank rows, and escaped or
quoted newlines share one path. Apply the existing reentrant-iterator
generation check to every iterator item consumed by a record.

Also accept empty ASCII line terminators like CPython, report a missing
csv-core writer sentinel as _csv.Error instead of panicking, and name the
FSM entry point process_parser_input.

Remove the expected-failure markers from the eleven reader tests that now
pass.

Assisted-by: Codex:gpt-5.6-sol

* csv: test CRLF boundaries with custom lineterminator
2026-08-01 11:35:37 +09:00
Chanho Lee 4d890b33db Fix sqlite autocommit cursor transactions (#8419)
Assisted-by: Codex:gpt-5.6-sol
2026-08-01 11:28:59 +09:00
Jeaeun Kim cb6ffff36b fix(ssl): emit missing deprecation warnings (#8418)
Assisted-by: Codex:gpt-5.6-sol
2026-08-01 11:28:26 +09:00
Jeaeun Kim 8771b0a382 fix(ssl): make Certificate immutable and non-instantiable (#8417)
Assisted-by: Codex:gpt-5.6-sol
2026-08-01 11:28:00 +09:00
Jeong, YunWon cdf006cfa1 Split InterpreterFrame from FrameObject for stack-allocated execution (#8354)
* Add LightFrame for unobserved Python-to-Python calls

Allocate a lightweight LightFrame header on the DataStack instead of a
full Frame PyObject for specialized exact-args call paths. The frame is
materialized lazily only when observed (sys._getframe, traceback, tracing).

Key changes:
- LightFrame struct with borrowed pointers and no refcount overhead
- FrameSource enum (Heavy/Light) replacing the object field on ExecutingFrame
- CURRENT_LIGHT_FRAME TLS for the light frame chain
- Interleaved heavy/light frame walking in frame_at_offset_vm and
  current_thread_frame_vm
- invoke_light_slots on PyFunction for the fast call path

Call overhead reduced from ~92ns to ~58ns (~37% reduction).
fib(28) improved from ~152ms to ~111ms (~27% reduction).

Assisted-by: Claude

* Streamline with_frame: inline recursion check, skip tracing dispatch

- Inline check_recursive_call instead of wrapping in with_recursion closure
- Amortize C stack overflow check to every 64th call depth
- Skip dispatch_traced_frame when use_tracing is false (hot path)
- Add recursion_depth_increment/decrement helpers

Assisted-by: Claude

* Address review findings for LightFrame correctness

- Fix materialize_light_frame previous pointer: use payload pointer
  (&**frame) instead of Py<Frame> pointer, matching the chain convention
- Fix predecessor ref management: use retained_back instead of
  mem::forget to keep materialized predecessors alive
- Fix materialized ref reclaim: use FrameRef::from_raw to drop the
  leaked keeper ref without double-incrementing
- Use PyAtomic<u32> for LightFrame.lasti instead of AtomicU32, removing
  the unsound cast on non-threading targets
- Add amortized C stack overflow check to light frame path
- Add scopeguard to light frame execution for panic safety
- Remove unnecessary Send/Sync impls from LightFrame
- Make check_c_stack_overflow pub(crate) for light frame path access

Assisted-by: Claude

* Fix CI failures: cspell, stale materialized state, f_back chain gaps

- Fix cspell: 'amortised' → 'amortized'
- Sync lasti, prev_line, and fastlocals when re-observing a materialized
  light frame, fixing stale f_locals/f_lineno in test_inspect and others
- In with_frame, materialize any active light frame as the heavy frame's
  predecessor and store it in retained_back, so f_back and inspect.stack
  see the correct interleaved order (H_new → L_mat → H_old)
- Guard invoke_light_slots on NEWLOCALS | OPTIMIZED code flags

Assisted-by: Claude

* Unify frame chain: replace dual-chain with single FrameChainPtr

Replace the dual-chain frame management (CURRENT_FRAME AtomicPtr<Frame>
+ CURRENT_LIGHT_FRAME Cell<*const LightFrame>) with a single unified
chain using FrameChainPtr, a tagged pointer where bit 0 distinguishes
heavy (*const Frame) from light (*const LightFrame) entries.

Changes:
- Add FrameChainPtr type with tagged pointer encoding
- Change LightFrame: replace previous_light + saved_current_frame with
  single previous: FrameChainPtr field
- Change InterpreterFrame.previous from AtomicPtr<Frame> to PyAtomic<usize>
- Change CURRENT_FRAME TLS from AtomicPtr<Frame> to AtomicUsize
- Remove CURRENT_LIGHT_FRAME TLS entirely
- Simplify all chain walkers (frame_at_offset, frame_at_offset_vm,
  find_owned_chain_frame, for_each_current_frame, current_thread_frame)
- Remove light-frame materialization block from with_frame
- Update f_back to dispatch on FrameChainPtr tags
- Update faulthandler and gc_state to walk unified chain
- Adaptive C stack check: every call in debug, every 16th in release

Assisted-by: Claude

* Fix light frame materialization: current_frame, f_back, and GC tracking

- current_frame() now uses current_thread_frame_vm() to materialize
  light frames, fixing super()/compile()/exec() when called from light
  frame context
- materialize_light_frame stores retained_back for heavy predecessors
  too, so f_back resolves after the caller returns
- Escaped materialized frames (traceback, sys._getframe) are GC-tracked
  at cleanup so reference cycles are collectible
- Restore pre-existing @expectedFailure markers for test_frame proxy tests

Assisted-by: Claude

* Limit light frame materialization to super() only

current_frame() returns heavy-only again to avoid unnecessary
materialization that extends object lifetimes. super() init uses
current_thread_frame_vm() directly since it must inspect the
light frame's code and freevars to find __class__.

Assisted-by: Claude

* Fix remaining CI failures: C stack check, GC tracking threshold, expectedFailure

- Check C stack overflow every call in light frame path (not every
  16th) to prevent stack overflow on CI workers with smaller stacks
- Fix GC tracking threshold: escaped materialized frame detection
  uses strong_count > 1 (was > 2, missed frames referenced only by
  traceback in the result)
- current_frame() materializes light frames so warnings, compile(),
  and other callers see the correct frame
- Mark test_futures2 PyFutureTests.test_task_exc_handler_correct_context
  as expectedFailure (pre-existing on main: PyTask __del__ timing)

Assisted-by: Claude

* Avoid materialization in hot paths: add light-frame-aware accessors

current_frame() stays heavy-only to avoid costly materialization on
every call. Instead, add current_globals(), current_code(), and
current_builtins() that read directly from light frame raw pointers
without creating frame PyObjects.

Callers migrated:
- PyFunction:🆕 current_builtins() for fallback builtins lookup
- compile()/eval(): current_code() for future feature flags
- type.__new__: current_globals() for __module__ detection
- typevar/typing: current_globals() for caller module name
- _io: current_code() for source path check
- super(): current_thread_frame_vm() for __class__ cell access
- warnings.warn: current_thread_frame_vm() for stack level walk
- import: current_globals() for module resolution

Assisted-by: Claude

* Fix current_locals() to include light frames

current_locals() must materialize light frames so locals() builtin
returns the correct scope. This fixes test_zipimport doctest and
other tests that rely on locals() in light frame context.

Assisted-by: Claude

* Fix clippy, profile tracing, resume_gen_frame, and faulthandler issues

- Fix clippy: collapsible_if in materialize_light_frame, map_unwrap_or in import
- Fix profile breakage: use current_thread_frame_vm in trace_event_inner to
  materialize light frames, preventing frame identity mismatch when profiling
  is enabled inside a light frame (e.g. profile.Profile.runctx)
- Disable light frames inside trace/profile callbacks (tracing_is_suppressed)
- Fix resume_gen_frame: skip light frames in previous chain to avoid dangling
  pointers after generator suspend
- Fix faulthandler: emit '<no Python frame>' when chain has only light frames

Assisted-by: Claude

* Fix materialized frame refcount: transfer localsplus ownership on exit

Remove @expectedFailure from test_futures2: the underlying issue was that
materialized light frames cloned localsplus values (refcount +1 each),
keeping objects alive until the next GC cycle collection.

Fix: after run_light_frame returns, transfer ownership of localsplus
values from the light frame to the materialized frame, replacing the
clones. This eliminates the extra refcount, allowing objects to be
GC'd immediately when their last reference is dropped.

Also stabilize materialized frame's localsplus on the heap in
sync_materialized_on_exit before GC tracking — the data-stack backing
will be popped imminently.

Assisted-by: Claude

* Fix rustfmt, increase stack margin, address review comments

- Fix rustfmt formatting issues (lint CI failure)
- Increase STACK_MARGIN_BYTES from 2048 to 3072 words to prevent C stack
  overflow in deep recursion tests on macOS CI (test_functools SIGSEGV)
- Skip dispatch_traced_frame when use_tracing is false (review comment)
- Fix post-fork top_frame initialization: walk past light frames to find
  nearest heavy frame (review comment)

Assisted-by: Claude

* Remove redundant test override in test_futures2

The override only called super() without any marker, making it a
no-op that prek flags as a redundant test patch.

Assisted-by: Claude

* Fix test_pdb and test_functools CI failures

- Revert with_frame dispatch_traced_frame skip: tracing can be enabled
  mid-execution (pdb.set_trace), so Return events must always be checked.
  This fixes 3 test_pdb doctest failures across all platforms.
- Check C stack overflow on every with_frame call instead of every 64th:
  the previous sampling approach missed overflows between checks when
  native stack frames are large (invoke_light_slots + lru_cache recursion).
- Add #[inline(never)] to invoke_light_slots to prevent the optimizer
  from merging its stack frame into callers.
- Add early C stack check before DataStack allocation in invoke_light_slots.

Assisted-by: Claude

* Rename Frame to FrameObject, FrameRef to FrameObjectRef

Prepare for _PyInterpreterFrame/PyFrameObject separation by
renaming the PyObject-backed frame type to FrameObject.
Python-visible name ("frame") is unchanged.

Assisted-by: Claude

* Remove Deref<Target = InterpreterFrame> from FrameObject

Replace implicit field access through Deref with explicit
iframe()/iframe_ref()/iframe_mut() calls. This decouples
FrameObject from InterpreterFrame field layout, enabling
independent modification of InterpreterFrame in future commits.

Assisted-by: Claude

* Move InterpreterFrame identity fields to raw pointers

InterpreterFrame's code/globals/builtins/func_obj fields are now
borrowed raw pointers instead of owned PyRef/PyObjectRef. Ownership
of these references is anchored in FrameObject's new owned_code,
owned_globals, owned_builtins, owned_func_obj fields.

This aligns InterpreterFrame's layout with LightFrame's existing
raw-pointer pattern, enabling future DataStack-based allocation
without PyObject heap allocation.

- Add code()/globals()/builtins()/func_obj() accessor methods
- Add FrameObject::new_ref() for combined create+allocate+patch
- Update ExecutingFrame to use &Py<PyCode> / &Py<PyDict> / &PyObject
- Update monitoring.rs functions to take &Py<PyCode> instead of &PyRef<PyCode>
- Add Send+Sync impls for InterpreterFrame (raw pointers are !Send by default)

Assisted-by: Claude

* Remove LightFrame, FrameChainPtr, and dual-frame-type infrastructure

Delete LightFrame struct, FrameChainPtr tagged pointer, FrameSource enum,
and all supporting functions: materialize_light_frame, sync_light_to_materialized,
transfer_localsplus_to_materialized, sync_materialized_on_exit,
run_light_frame, invoke_light_slots body, LocalsPlus::from_datastack_raw.

All call paths now use a single frame type (FrameObject wrapping
InterpreterFrame). The frame chain is a simple *const FrameObject linked
list through InterpreterFrame.previous.

invoke_light_slots delegates to invoke_exact_args_slots.
ExecutingFrame.frame_source replaced with ExecutingFrame.frame.
Frame chain walking simplified throughout (no more light/heavy dispatch).

Net: -724 lines of dual-frame-type complexity.

Assisted-by: Claude

* Clean up invoke_light_slots and _vm function variants

- Replace invoke_light_slots calls with invoke_exact_args_slots
- Delete the invoke_light_slots delegate method
- Replace current_thread_frame_vm/frame_at_offset_vm with their
  non-_vm equivalents (no more light frame materialization needed)
- Remove stale light-frame comment in callable.rs

Assisted-by: Claude

* Fix CI lint issues

- Move github context expressions from run blocks to env blocks
  (zizmor/template-injection)
- Add workflow-level permissions: {} to lib-deps-check.yaml
- Suppress zizmor excessive-permissions for lib-deps-check.yaml
  (pull_request_target is required for PR comments)
- Apply rustfmt formatting

Assisted-by: Claude

* Stack-allocate InterpreterFrame for non-generator function calls

- Extract InterpreterFrame::new() from FrameObject::new()
- Change frame chain from *const FrameObject to *const InterpreterFrame
- Add InterpreterFrame.materialized field for lazy FrameObject creation
- Add vm.with_iframe()/run_frame_fast() for stack-allocated frame execution
- Move ExecutingFrame.frame to iframe: *const InterpreterFrame
- Implement lazy materialize() for on-demand FrameObject creation
- Update invoke_with_locals/invoke_prepared_exact_args to skip heap allocation
- Update frame chain walking in faulthandler, gc_state, builtins/frame
- super() reads InterpreterFrame directly without materialization

Assisted-by: Claude

* Remove unnecessary refcount and atomic ops from fast call path

- Use raw pointer for func_obj instead of cloning PyObjectRef (saves 2 atomic RMW)
- Skip owner AcqRel swap for stack-allocated frames (always Thread-owned)

Assisted-by: Claude

* Change trace field to Option<PyObjectRef>, remove vm from InterpreterFrame::new

- trace: PyMutex<PyObjectRef> -> PyMutex<Option<PyObjectRef>>
- Eliminates vm.ctx.none() refcount inc/dec per frame init
- Remove vm parameter from InterpreterFrame::new() (no longer needed)
- f_trace getter returns None when trace is unset
- f_trace setter stores None instead of the None singleton

Assisted-by: Claude

* Store current frame pointer on VirtualMachine for fast lookup

- Add current_frame_ptr Cell<usize> field to VirtualMachine
- Hot path (with_iframe) reads/writes vm.current_frame_ptr instead of TLS
- TLS CURRENT_FRAME still maintained for signal-safe traceback walking
- Use set_current_frame_nosave() to skip cross-thread top_frame update
- super(), frame_at_offset, current_thread_frame_materialize use vm Cell

Assisted-by: Claude

* Apply rustfmt

Assisted-by: Claude

* Fix clippy use_self warnings, add inline hints for hot path

Assisted-by: Claude

* Remove code refcount clone from fast call path

Defer code.to_owned() to the generator/coroutine path only. On the fast
path, use &Py<PyCode> directly — the code object is alive via the
PyFunction on the caller's stack.

Assisted-by: Claude

* Remove VM current_frame_ptr Cell (fix SIGSEGV in threading builds)

The Cell<usize> on VirtualMachine could desynchronize from the TLS
CURRENT_FRAME in nested VM scenarios (enter_vm called with different
VMs on the same thread). Revert to TLS-only frame chain management.

Keep set_current_frame_nosave() for the stack-frame fast path: it
skips cross-thread top_frame publication but still does the TLS swap.

Fix clippy not_unsafe_ptr_arg_deref in set_current_frame.
Fix unnecessary Result wrapper in make_generator_or_coro.

Assisted-by: Claude

* Fix CI failures: GC frame assertion, faulthandler Radium import, dead code warning

- Fix SIGSEGV in threading builds: GC debug assertion walked frame
  chain via top_frame, which is not updated by set_current_frame_nosave
  used by stack-allocated iframes. Walk CURRENT_FRAME TLS chain instead.
- Add InterpreterFrame::get_lasti() accessor, remove direct Radium
  import from faulthandler.rs (fixes unused import warning).
- Add cfg_attr allow(dead_code) for from_payload_ptr without threading
  (fixes WASM build warning).

Assisted-by: Claude

* Fix materialized frame: copy localsplus, clear stale previous pointers

- materialize_slow: copy fastlocals snapshot to heap instead of empty
  localsplus (fixes index-out-of-bounds panic in locals()/f_locals)
- materialize_slow: set previous to null (stack iframes become dangling)
- with_frame/resume_gen_frame: clear iframe.previous on pop (prevents
  dangling pointers to freed stack iframes)
- f_back: walk TLS CURRENT_FRAME chain instead of top_frame for
  cross-thread lookup (top_frame is stale for stack-allocated iframes)
- check_locals_access: match materialized frames by comparing the
  Py<FrameObject> address stored in the stack iframe's materialized field

Assisted-by: Claude

* Fix Windows clippy: gate unix-only imports and variables

FrameObject, Py imports and top_iframe variable are only used in
unix+threading cfg blocks. Gate them properly for non-unix builds.

Assisted-by: Claude

* Fall back to FrameObject when tracing; fix cross-thread frame access

- invoke_with_locals: fall back to heap-allocated FrameObject path
  when use_tracing is active, so trace/profile callbacks fire correctly.
  This fixes test_trace, test_bdb, test_monitoring regressions.
- set_current_frame: publish top_iframe alongside top_frame in ThreadSlot
  so cross-thread readers (sys._current_frames) can materialize
  stack-allocated frames from other threads under stop-the-world.
- get_all_current_frames: use TLS CURRENT_FRAME for current thread,
  fall back to top_iframe materialization for other threads.
- Use set_current_frame (not nosave) in with_iframe for proper
  cross-thread visibility.

Assisted-by: Claude

* Apply rustfmt

Assisted-by: Claude

* Fix Windows: gate unix-only top_iframe_ptr variable

Assisted-by: Claude

* Fix frame chain: f_back, retained_back, owner, faulthandler

- Set materialized frame owner to FrameObject (not Thread) so
  frame.clear() works on traceback frames
- Fix f_back for materialized frames: walk TLS chain to find source
  iframe and materialize its previous frame
- Fix faulthandler dump_traceback to use dump_live_frames (reads
  live lasti from stack iframes instead of stale materialized copies)
- Add retained_back propagation in with_frame cleanup: when a frame
  escapes, materialize caller and mark it escaped for chain propagation
- Fix release_datastack_frame: don't overwrite retained_back if
  already set by with_frame cleanup

Assisted-by: Claude

* Apply rustfmt

Assisted-by: Claude

* Refine with_frame retained_back: use frame_obj instead of materialize

Avoid materializing caller frames in with_frame cleanup to prevent
creating localsplus snapshots that keep extra refcounts. Only use
existing FrameObjects (frame_obj) for retained_back. Also keep
previous pointer alive in release_datastack_frame since the caller
is still executing at that point.

Assisted-by: Claude

* Fix f_lineno: use Cell<u32> for prev_line, update on every instruction

prev_line was only updated when tracing was active, causing f_lineno
to return stale values when observed mid-CALL (e.g. from warnings.warn
or sys._getframe). Changed prev_line from u32 to Cell<u32> for
interior mutability so it can be read safely while ExecutingFrame
holds a shared reference, and update it on every instruction so
f_lineno always returns the correct line.

Assisted-by: Claude

* Fix f_lineno for materialized frames: read live prev_line from TLS chain

Materialized FrameObjects have a snapshot of prev_line from materialize
time. When f_lineno is called on a materialized frame that is still
executing (its source iframe is on the TLS chain), walk the TLS chain
to find the source iframe and read its live prev_line. This fixes
lineno tracking in warnings, gettext, and other modules that read
frame line numbers during function calls.

Also fixed the pointer comparison: use from_payload_ptr to convert
FrameObject payload address to Py<FrameObject> address for matching
against the materialized field.

Assisted-by: Claude

* Sync live frame state for materialized frames

- Add find_live_source_iframe() to walk TLS chain and find the live
  source InterpreterFrame for a materialized FrameObject
- f_lasti: read live lasti from source iframe when available
- f_lineno: use read_volatile for live prev_line from source iframe
- sync_visible_locals_to_mapping: read live localsplus from source iframe
- framelocalsproxy_getval: read live localsplus from source iframe
- with_iframe cleanup: sync localsplus, prev_line, lasti to materialized
  FrameObject when the frame returns
- sync_fastlocals_from: new LocalsPlus method to update fastlocals

Fixes test_inspect (stale f_locals, stale lineno in positions),
test_listcomps (stale iteration variable in locals()).

Assisted-by: Claude

* Fix retained_back: use materialize_chain and strong_count check

- Add materialize_chain() that creates a lightweight FrameObject with
  empty localsplus for f_back chain building only. This avoids cloning
  local variables which would create extra refcounts (fixes test_memoryview).
- with_frame cleanup: use strong_count > 1 instead of escaped flag to
  determine if retained_back is needed, ensuring chain propagation.
- with_iframe cleanup: use materialize_chain for retained_back to avoid
  extra refcounts from localsplus snapshots.

Fixes test_memoryview (refcount from retained_back chain) and
test_traceback.test_extract_stack (f_back chain depth).

Assisted-by: Claude

* Fix cross-thread f_back chain for sys._current_frames/exceptions

- get_all_current_frames: materialize entire frame chain and link
  retained_back during stop-the-world for other threads
- f_back: use stop-the-world to safely materialize cross-thread frame
  chains on unix when prev iframe is on another thread
- f_back: check frame_obj() before STW for fast path

Fixes test_sys.test_current_frames and test_sys.test_current_exceptions.

Assisted-by: Claude

* Fix monitoring LINE events and faulthandler thread dump

- Skip all instrumented opcodes in bytecode loop prev_line update to
  avoid defeating InstrumentedLine de-duplication
- Add prev_line update in execute_instrumented for non-RESUME/non-LINE
  instrumented opcodes
- update_events_mask: walk TLS iframe chain to re-instrument all frames
  including stack-allocated ones (fixes missing LINE events for inline
  frames already past RESUME)
- faulthandler: use top_iframe fallback for stack-allocated frames in
  dump_all_threads and watchdog_thread
- faulthandler: use dump_live_frames for current thread dump

Fixes test_monitoring LINE/CALL tests, test_faulthandler thread dumps.

Assisted-by: Claude

* Fix clippy: remove duplicate Radium import, use previous() helper

Assisted-by: Claude

* Fix materialized frame issues: GC tracking, live locals, f_trace propagation

- Track materialized FrameObjects in GC so cycle collection works
  (fixes __del__ not called at exit for exception traceback cycles)
- materialize_chain returns owned PyRef without temporary_refs to
  avoid defeating GC cycle detection from non-GC-tracked storage
- framelocalsproxy_setval writes to live source iframe when available
  (fixes f_locals proxy writes not affecting executing frame)
- f_lineno uses lasti-based line for non-executing frames instead of
  prev_line (fixes PEP 626 wrong line after exception unwind)
- f_trace/f_trace_lines/f_trace_opcodes setters propagate to live
  source iframe (fixes pdb set_trace not working through materialized
  frames)
- frame.clear() checks find_live_source_iframe to reject clearing a
  frame that is backed by a live stack-allocated iframe
- Fix clippy warnings and unused imports in faulthandler
- Remove unsafe top_iframe access from faulthandler watchdog thread

Assisted-by: Claude

* Fix faulthandler crashes, GC tracking timing, and test_generators

- Use top_iframe instead of top_frame for frame chain walking in
  faulthandler (dump_all_threads and watchdog), avoiding stale or
  GC-cleared FrameObject pointer dereference
- Defer GC tracking of materialized FrameObjects to with_iframe
  cleanup (after set_current_frame restores old chain), preventing
  premature collection while temporary_refs still holds the only ref
- Add FrameObject::try_iframe() for safe access to potentially
  GC-cleared frames
- Remove expectedFailure for test_exhausted_generator_frame_cycle
  (now passes with GC tracking fix)
- Add 'noalias' to cspell dictionary

Assisted-by: Claude

* Fix Windows frame dump and remove dead code

- Publish top_iframe on all platforms (not just unix) so Windows
  faulthandler and sys._current_frames can see stack-allocated frames
- Use top_iframe instead of frames Mutex in Windows faulthandler
  dump and watchdog paths
- Use top_iframe in non-unix get_all_current_frames for sys._current_frames
- Remove unused dump_traceback_thread_chain (replaced by top_iframe walk)
- Mark test_pdb_await_support as expected failure

Assisted-by: Claude

* Fix ThreadSlot init on all platforms, skip test_pdb_await_support

- Add top_iframe to ThreadSlot initializer in init_thread_slot_if_needed
  (fixes wasm32, Windows compile errors)
- Skip test_pdb_await_support on RustPython (async pdb exception
  callback receives None exc argument)

Assisted-by: Claude

* Remove dead faulthandler code (dump_frame_from_ref, dump_traceback_thread_frames, try_iframe)

These functions are no longer called after switching all frame chain
walking to use top_iframe directly.

Assisted-by: Claude

* Fix Windows sys._current_frames: materialize full frame chain

The non-unix get_all_current_frames path was only materializing the
top iframe without linking retained_back, so f_back chain walking
could not find deeper frames like f123() in test_sys tests.
Replicate the full chain materialization from the unix path.

Assisted-by: Claude

* Use STW-based cross-thread f_back on all platforms

Replace the non-unix frames-mutex fallback with the same
stop-the-world iframe chain materialization used on unix.
Fixes test_current_exceptions on Windows where stack-allocated
iframes were not found in the FrameObject-only frames list.

Assisted-by: Claude

* Fix set_f_lineno to write to live iframe, use STW for cross-thread access

- set_f_lineno: write lasti/pending_stack_pops to the live source
  iframe instead of the materialized copy so debugger jumps take
  effect on stack-allocated frames
- f_back cross-thread: enter STW before dereferencing the prev
  pointer to prevent use-after-free if the owning thread returns
- get_all_current_frames (non-unix): add STW protection when
  walking cross-thread iframe chains, matching the unix path

Assisted-by: Claude

* Guard current_location() against lasti==0, always capture retained_back

- current_location(): return first_line_number instead of
  panicking when lasti is 0 (before first instruction executes)
- with_frame retained_back: materialize the caller iframe when
  needed so f_back always resolves for escaped FrameObjects

Assisted-by: Claude

* Fix format, revert eager retained_back materialization

Remove double blank line in frame.rs (lint failure).
Revert with_frame retained_back to only use already-materialized
caller FrameObjects — eagerly materializing the caller added
refcounts on local variables, preventing timely deallocation and
causing test_io, test_memoryview, and test_futures failures.

Assisted-by: Claude

* Remove rustpython-unicode-isolation-issue.md from gitignore

Assisted-by: Claude

* Remove test_pdb_await_support skip: async pdb now works correctly

Assisted-by: Claude

* Fix STOP_ITERATION monitoring: wrap value in StopIteration instance

fire_stop_iteration was passing the raw iterator return value to
callbacks, but the STOP_ITERATION event callback signature expects
a StopIteration exception instance. Wrap non-StopIteration values
in a new StopIteration(value), matching PyMonitoring_FireStopIterationEvent.

This fixes test_pdb_await_support where bdb's exception_callback
received None instead of a StopIteration instance.

Assisted-by: Claude
2026-08-01 00:59:41 +09:00
박효종 6a7d0fd144 Allow lone-surrogate keyword keys in f(**d) (#8409)
* Allow lone-surrogate keyword keys in f(**d)

`f(**{'\udc81': 2})` raised `TypeError: keywords must be strings` even
though the key is a valid `str`, because `KwArgs` stored keys as Rust
`String` (strict UTF-8) and `collect_ex_args` narrowed each key to
`PyUtf8Str` (valid UTF-8 required). CPython only checks that the key is a
`str` (`Py_TPFLAGS_UNICODE_SUBCLASS`), never encoding validity.

Change `KwArgs<T>`'s key type from `String` to `Wtf8Buf` (the WTF-8
storage `PyStr` already uses) and relax the keyword-key downcast from
`PyUtf8Str` to `PyStr` in `collect_ex_args`, `from_vectorcall`/
`from_vectorcall_owned` (which previously panicked on surrogate keys),
the `functools.partial` keyword merge, and the C-API `dict_to_kwargs`.

`Wtf8Buf` borrows only as `Wtf8`, so inherent `get`/`contains_key`/
`swap_remove`/`shift_remove(&str)` on `KwArgs` restore the `&str` lookup
interface `String: Borrow<str>` used to provide (via the zero-cost
`Wtf8::new` cast), and a generic `FromIterator<(K: Into<Wtf8Buf>, T)>`
keeps construction sites unchanged. WTF-8 awareness stays localized to
`function/argument.rs`.

Fixes #8228

Assisted-by: Claude Code:claude-opus-4-8

* Avoid cloning kwargs keys in _ast, borrow via as_ref instead

`new_str` only needs a `&Wtf8`, so pass `key.as_ref()` rather than
cloning the key into an owned `Wtf8Buf`. The key is reused afterwards
(error message, `intern_str`), so a borrow is the right fit.

Assisted-by: Claude Code:claude-opus-4-8
2026-07-30 22:13:22 +09:00
Joshua Megnauth 33eb99af44 Fix some clippy lints for 1.99 (#8412) 2026-07-30 22:11:31 +09:00
조진명 071d54d442 csv: support multi-character lineterminator in writer (#8328)
* csv: support multi-character lineterminator in writer

Store the dialect line terminator as an owned String instead of the
single-byte csv_core::Terminator, dropping PyDialect's Copy derive. The
manual writer paths emit the full terminator; the csv-core-backed
QUOTE_ALL/QUOTE_NONNUMERIC paths emit a sentinel byte (preserving
csv-core's quote/empty-record bookkeeping) and append the real
terminator. field_needs_quotes/escape now quote a field containing any
terminator byte. The reader ignores lineterminator and always uses CRLF,
matching CPython and avoiding mid-UTF-8 record splits.

* csv: unmark now-passing test_write_lineterminator

* csv: address review feedback

- Remove the redundant per-branch sentinel terminator setup in to_writer;
  the unconditional terminator call after the match is the single source.
- Reword the empty-lineterminator errors to "must not be empty" (the
  constraint is non-empty, not single-character) on both entry points.
- Promote the sentinel invariant check in writerow from debug_assert_eq!
  to assert_eq! so it also guards release builds.
- Add a snippet case for a field containing a line-break byte to ensure
  the csv-core path drops only the trailing terminator.

* csv: reject non-ASCII lineterminator

The writer decides what to quote and escape by comparing raw bytes, so a
non-ASCII terminator quoted a field that merely shared a UTF-8 lead byte,
and QUOTE_NONE escaped individual bytes of the terminator and then failed to
decode the record back to a string.

Reject non-ASCII terminators, including lone surrogates, as csv.Error when
the dialect is parsed, and leave code-point-wise handling to a follow-up
(#8310). The non-string error message now matches CPython as well.
2026-07-30 00:15:33 +09:00
Kwak Byoung Min a49e8f9b5f Add seekable() to mmap.mmap (#8405)
* Allow mmap-backed readers to identify seek support

mmap already supports seek() and tell(); expose seekable() to match CPython.

Confidence: high
Scope-risk: narrow
Tested: test_mmap, stdlib_mmap snippet, clippy, prek, extra_tests
Not-tested: full workspace test blocked by local rustpython-capi SIGSEGV
Assisted-by: Codex:gpt-5.6-sol

* Keep mmap regression test lint-clean

CI ruff check removes the redundant blank line and treats the auto-fix as a failure.

Constraint: CI runs prek with ruff check
Confidence: high
Scope-risk: narrow
Tested: prek run --all-files; cargo run -- extra_tests/snippets/stdlib_mmap.py
Assisted-by: Codex:gpt-5.6-sol

* Keep mmap seekability method conventional

The method is invoked at runtime through the Python wrapper, so const qualification provides no benefit.

Constraint: Python methods execute at runtime
Confidence: high
Scope-risk: narrow
Tested: prek run --all-files; cargo run -- extra_tests/snippets/stdlib_mmap.py
Assisted-by: Codex:gpt-5.6-sol
2026-07-29 20:28:32 +09:00
Jeong, YunWon 8c8caec52d Fix SymbolUsge::Iter, os.rename, sre_engine (#8390)
* Fix SymbolUsge::Iter

* fix os.rename

* fix sre

* Re-export rename from crate::posix on Windows

posix_windows.rs is mounted as the `posix` module on Windows, so
`crate::posix_windows` does not resolve.

Assisted-by: Claude

* Apply rustfmt to sre_engine tests

Assisted-by: Claude

* Update test expectations for the sre and tokenize fixes

- test_re: drop expectedFailure from test_word_boundaries,
  test_possessive_quantifiers and test_bug_gh101955
- test_inspect, test_pydoc: drop expectedFailure from the signature
  tests that now pass
- test_pdb: drop expectedFailure from the two file-modification tests
  and +EXPECTED_FAILURE from the test_post_mortem_chained and
  test_pdb_asynctask doctests
- test_unittest: mark test_autospec_on_bound_builtin_function as
  expectedFailure; inspect.signature() now succeeds on time.ctime
  because of its auto-generated __text_signature__

Assisted-by: Claude
2026-07-28 10:20:04 +09:00
Kwak Byoung Min a0f4f6a001 Reject empty UU input before decoding (#8389)
Constraint: Match CPython's a2b_uu empty-input behavior.
Confidence: high
Scope-risk: narrow
Directive: Preserve decoding behavior for non-empty UU buffers.
Tested: prek run --all-files; test_binascii; cargo fmt --check; cargo clippy -p rustpython-stdlib -- -D warnings; cargo test -p rustpython-stdlib; manual full-suite verification
Assisted-by: Codex:gpt-5.6-sol
2026-07-27 21:43:15 +09:00
Chanho Lee 15e7b2027e Fix sqlite autocommit lifecycle (#8387)
* fix: open disabled sqlite autocommit transactions

Assisted-by: Codex:gpt-5.6-sol

* fix: reopen disabled sqlite transactions after commit

Assisted-by: Codex:gpt-5.6-sol

* fix: reopen disabled sqlite transactions after rollback

Assisted-by: Codex:gpt-5.6-sol

* fix: roll back disabled sqlite connections on close

Assisted-by: Codex:gpt-5.6-sol
2026-07-27 21:42:19 +09:00
Joshua Megnauth 71f2ffc289 Use memchr consistently for NUL checks (#8372)
`memchr` is used throughout RustPython for searching through bytes
expediently. However, for NUL checks, it's scantily used. Instead, our
NUL checks either use `contains` or `memchr`.

I switched all of the `contains(b'\0')` I could find to using memchr
instead. I marked the failure paths as cold to hint to LLVM that
interior NULs are truly exceptional. This should help branch prediction
a bit which is nice for string/bytes functions since they are likely
called a lot.
2026-07-27 21:35:01 +09:00
Sumi JeongandClaude Sonnet 5 82a34ba16d Implement termios.tcgetwinsize/tcsetwinsize (#8347)
* Implement termios.tcgetwinsize/tcsetwinsize

Adds ioctl(TIOCGWINSZ/TIOCSWINSZ) wrappers in host_env and the
corresponding Python-facing functions in the termios stdlib module,
removing the associated expectedFailure markers in test_termios.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT

* Use ret < 0 convention for ioctl error checks in termios winsize

POSIX ioctl only guarantees -1 on failure, not exactly 0 on success;
matches the existing check_libc_neg convention used elsewhere in
host_env (e.g. fcntl.rs, posix.rs::get_terminal_size).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT

* Reject non-sequence size in tcsetwinsize

extract_elements_with also accepted dicts (treating keys as elements).
Switch to try_sequence(), matching CPython's PySequence_Check, so only
real sequences (tuple/list/etc.) are accepted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT

* Use `core::mem::zeroed` instead of `std::mem::zeroed`

`std::mem` just re-exports `core::mem`, but clippy prefers importing
from core when there's no OS dependency. This was breaking the wasm CI build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT

* Use try_index for tcsetwinsize row/col conversion

CPython's PyLong_AsLong calls __index__ on non-int objects before
converting, so downcast_ref::<PyInt> was stricter than CPython.
try_index matches that behavior and is more concise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT

* Mark test_fork/test_spawn_doesnt_hang as expected failures

Both were hidden by a skipIf on tty.tcgetwinsize, which now exists.
Root cause: pty.fork() calls os.login_tty(), which isn't implemented,
so the forked child crashes before the test body runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT

* Use unittest.skip instead of expectedFailure for pty.fork() tests

expectedFailure still runs the test body, so pty.fork()'s real fork()
still happens and the child crashes on missing os.login_tty inside the
parallel test runner's worker process, corrupting its JSON reporting
channel ("worker bug"). skip prevents the method from running at all,
avoiding that. Verified locally with -j 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbzcYLnX5tMfTM9BP7FDTT

* Clarify skip reason for pty.fork() tests

* Simplify tcgetwinsize/tcsetwinsize fd argument with Fildes destructuring

* Explain why pty.fork() tests use skip instead of expectedFailure

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 13:30:48 +09:00
Yubin Kim 45b9c2e1a3 Replace malformed unicode error helper calls with the _real variants (#8359)
* Replace malformed unicode error helper calls with the _real variants

new_unicode_decode_error / new_unicode_encode_error build the exception
from a bare message without running the initializer, so the result has
none of the five attributes a unicode error must carry and str() renders
as an empty string. Convert the non-Windows call sites in csv, socket,
getlogin and the fs-path decoders to the _real constructors, passing the
source bytes/str and the failing offset from the captured Utf8Error.

The Windows-gated sites (nt, mbcs/oem codecs) and the sites whose source
object is not reachable (uname, array) are left for follow-ups.

Assisted-by: Claude Code:claude-fable-5

* Report the full invalid UTF-8 span in the converted decode errors

The conversions used valid_up_to() + 1 for the decode-error end offset,
which under-reports multi-byte invalid sequences. Take the span from the
Utf8Error instead: valid_up_to() + error_len(), or the input length for a
truncated sequence (error_len() == None), matching CPython.

Assisted-by: Claude Code:claude-opus-4-8
2026-07-26 13:14:50 +09:00
Jiseok CHOI 375051ddf8 sqlite3: raise ProgrammingError when named param receives sequence (#8363)
CPython raises ProgrammingError with "Binding N is a named parameter"
when a query uses named placeholders (, , ) but
the caller passes a sequence instead of a mapping.

RustPython's bind_parameters_sequence() did not check whether each
binding slot is a named parameter, so no error was raised.

Fix: call sqlite3_bind_parameter_name() for each slot in
bind_parameters_sequence(). If the first byte of the returned name is
not '?' (i.e. it is a named placeholder), raise ProgrammingError before
attempting to bind.

Assisted-by: GitHub Copilot:claude-sonnet-4-6
2026-07-26 13:02:25 +09:00
sijun-yang 77115b853b Fix b2a_qp CRLF boundary check (#8358)
Assisted-by: Codex:gpt-5.6-sol
2026-07-25 20:00:30 +09:00
Jiseok CHOI 64352495d7 sqlite3: allow Row construction with cursor having no description (#8364)
Row(cursor, data) raised ValueError when cursor.description was None.
CPython allows this case and returns an empty key list.

- Use empty tuple when description is None instead of raising
- Include the key name in the IndexError when a string key is not found

Assisted-by: GitHub Copilot:claude-sonnet-4-6
2026-07-25 19:55:36 +09:00
Jiseok CHOI 2082675d96 sqlite3: fix closed connection error message to match CPython (#8362)
When a Connection is explicitly closed via con.close(), subsequent
operations (cursor(), commit(), rollback(), create_function(), etc.)
should raise ProgrammingError with 'Cannot operate on a closed database.'
to match CPython behaviour.

Previously, _db_lock() always returned 'Base Connection.__init__ not
called.' when self.db was None, without distinguishing between a
connection that was never initialised (subclass before __init__) and
one that was initialised and then explicitly closed.

Fix: inspect the initialized atomic flag — if True but db is None,
the connection was closed; if False, it was never initialised.

Assisted-by: GitHub Copilot:claude-sonnet-4-6
2026-07-25 19:54:59 +09:00
Joshua Megnauth 05564d97fa Unify nul errors (#8339)
Part of #8245 to reduce the amount of work needed to review. I
simplified the embedded nul errors by forwarding to the implementations
in `vm::exceptions`.
2026-07-23 22:25:27 +09:00
Kwak Byoung Min d50071eaaa bz2: return streaming output from BZ2Compressor.compress() (#8330)
* bz2: return streaming output from BZ2Compressor.compress()

  Return output produced during compression instead of waiting for flush().

  Constraint: Preserve encoder state without flushing the BZ2 stream
  Confidence: high
  Scope-risk: narrow
  Directive: Do not flush merely to retrieve partial output
  Tested: test_bz2; stdlib_bz2.py; prek run --all-files
  Assisted-by: Codex:gpt-5.6-sol

* bz2: remove redundant streaming-output snippet
2026-07-21 00:30:10 +09:00
이광효 f4ef94c464 csv: handle empty fields with skipinitialspace (#8304)
* csv: handle empty fields with skipinitialspace

skipinitialspace preprocessed records by trimming both ends of every split
field. Fields containing only spaces could therefore produce an invalid
slice, while trailing spaces were removed even though CPython preserves them.

Trim only field prefixes in the csv-core path and make the remaining
all-space trimming helper return an empty slice safely.

QUOTE_NOTNULL and QUOTE_STRINGS add another distinction: an unquoted empty
field becomes None, but a quoted empty field remains an empty string. Both
forms have identical decoded bytes, so extend the existing QUOTE_NONE custom
reader into one shared path that records whether each field started quoted.
Use that metadata only for the null conversion while retaining QUOTE_NONE
escape behavior.

Unmark Test_Csv.test_read_skipinitialspace now that its standard,
QUOTE_NOTNULL, and QUOTE_STRINGS cases pass.

This keeps the existing one-item reader lifecycle and does not add the larger
strict or multiline parser state machine.

Assisted-by: Codex:gpt-5-sol

* csv: make skipinitialspace quote-aware

The csv-core preprocessing path split each raw iterator item on every
delimiter before trimming field prefixes. Delimiters inside quoted fields were
therefore treated as separators, so spaces after them could be removed before
csv-core parsed the record.

Extract the quote, escape, delimiter, and field-start transitions from
read_quote_record() into a small per-item scanner. Reuse its events in the
skipinitialspace preprocessor and discard only InitialSpace events while
copying all other source bytes unchanged.

This keeps the custom quote-mode reader and preprocessing logic aligned
without introducing persistent multiline or strict parser state. Remove the
delimiter cache that was only needed by the old split-and-join path and add a
regression for spaces following a delimiter inside a quoted field.

Assisted-by: Codex:gpt-5-sol
2026-07-20 10:57:38 +00:00
이광효 570258f6cb csv: reject reentrant reader advancement (#8324)
* csv: reject reentrant reader advancement

Snapshot the reader generation before calling the input iterator. If a nested
call completes a record and advances the same reader, reject the outer call
with csv.Error instead of continuing with stale parser state.

Increment the generation on each successful record path, matching CPython's
handling of the existing reentrant-reader regression. Enable that test by
removing its expectedFailure marker.

Assisted-by: Codex:gpt-5.6-sol

* csv: invalidate outer reads after reentry

Advance the reader generation immediately after the reentrancy check, before
validating or parsing the returned input item. This invalidates the outer call
even when a nested read later fails validation or parsing.

Remove the per-return generation updates now that the common path advances it
exactly once.

Assisted-by: Codex:gpt-5.6-sol

* csv: generation type conversion(usize to u64)

On 32-bit targets (like WebAssembly, which RustPython supports), usize is 32-bit.
Csv with over 32-bit rows can raise panic on these platform.
type conversion from usize to u64.
2026-07-20 17:05:51 +09:00
Jeong, YunWon c7d265e8dd ctypes: fix Windows pip truststore (#8314)
* ctypes: preserve result when errcheck returns args

* ctypes: cover Windows pip truststore

* capi: implement PyEval_SaveThread

* capi: merge saved thread state into PyThreadState
2026-07-20 15:21:42 +09:00
조진명 270587e670 csv: quote embedded CR/LF under QUOTE_MINIMAL (#8315)
* csv: quote embedded CR/LF under QUOTE_MINIMAL with custom lineterminator

QUOTE_MINIMAL rows were serialized by csv-core, which only quotes bytes
registered for the configured terminator, so fields containing '\r' or
'\n' were left unquoted when the lineterminator was not CR/LF (e.g. '!'
or '\0'). Route QUOTE_MINIMAL through a hand-written path that uses
field_needs_quotes, which always treats '\r'/'\n' as needing quotes.
A single empty field is quoted and an empty row emits only the
terminator, matching CPython. QUOTE_ALL / QUOTE_NONNUMERIC still use
csv-core.

Assisted-by: Claude Code:claude-opus-4-8

* csv: unmark now-passing writer tests

test_write_iterable and test_write_empty_fields pass with the
QUOTE_MINIMAL writer fix (empty rows no longer emit an empty field).

Assisted-by: Claude Code:claude-opus-4-8
2026-07-19 13:58:32 +09:00
Jiseok CHOI a218e4ce99 sqlite3: fix isolation_level TypeError message to match CPython (#8313)
Assisted-by: GitHub Copilot:claude-sonnet-4-6
2026-07-19 13:56:02 +09:00
Leesoo Ahn c7005c5d79 stdlib: remove unused warning on sock_wait() (#8303)
The function is used in 'feature = ssl'. However, the current codebase
defines it without a conditional compilation expression and always be
compiled.

This commit removes the unused warning by adding a conditional
compilation expression, '#[cfg(feature = "ssl")]'.
2026-07-19 13:51:53 +09:00
OkJaandClaude Opus 4.8 f74e7256a0 Implement pyexpat ordered_attributes (#8305)
When `ordered_attributes` is set, report attributes to the
StartElementHandler as a flat `[name, value, ...]` list matching
CPython's my_StartElementHandler, instead of always passing a dict.

`xml.etree.ElementTree` and `xml.dom.minidom` (via expatbuilder) both
enable this flag and rely on the flat-list contract, so parsing any
element with attributes previously failed: the dict made `_start` do
integer indexing, raising KeyError which was silently swallowed by the
handler invoker, leaving the tree empty ("missing toplevel element").

Reference (CPython 3.14):
https://github.com/python/cpython/blob/3.14/Modules/pyexpat.c#L439-L443

Remove `@unittest.expectedFailure` from tests that now pass
(23 in test_xml_etree, 15 in test_minidom, 1 in test_regrtest).

Assisted-by: Claude:claude-opus-4-8

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:46:57 +09:00
조진명 452f2b0e5b csv: apply dialect quoting and lineterminator in writer (#8254)
* csv: map QUOTE_MINIMAL to Necessary quote style

QuoteStyle::Minimal was mapped to csv_core's Always, which quoted every
field even under QUOTE_MINIMAL. Map it to Necessary so only fields that
require quoting are quoted.

Unmarks Test_Csv.test_write_quoting.

Assisted-by: Claude Code:claude-opus-4-8

* csv: apply dialect quoting and lineterminator in writer

csv.writer ignored the quoting and lineterminator from a dialect (whether
passed by name or object), always using the defaults. Resolve both from the
dialect via get_quoting()/get_lineterminator(), mirroring get_delimiter(), so
an explicit keyword argument still overrides the dialect.

Unmarks TestDialectUnix.test_simple_writer.

Assisted-by: Claude Code:claude-opus-4-8
2026-07-16 17:42:02 +09:00
Sumi JeongandJeong, YunWon 9d9fc27082 _queue: check for pending signals during blocking get() (#8252)
* _queue: check for pending signals during blocking get()

Semaphore::acquire() waited on the condvar in a single uninterrupted call,
so a Python-level signal handler never ran until the wait itself returned.
check_signals() is only invoked from the bytecode dispatch loop, and this
native wait never passed through it -- CPython chunks its equivalent
blocking wait for the same reason.

Cap each wait at a short interval and call check_signals() between chunks,
only giving up once the caller's real deadline has actually elapsed. The
mutex guard must be dropped before check_signals() runs: a signal handler
that calls back into the same queue on the same thread would otherwise try
to relock a mutex it's still holding and deadlock against itself.

Add a regression snippet that arms a SIGALRM well before a helper thread
unblocks the queue, and asserts the handler ran near the alarm instead of
only once get() returned.

Refs #8250
Assisted-by: Claude

* _queue: correct SIGNAL_CHECK_INTERVAL doc comment

The old comment claimed this mirrors CPython's PyThread_acquire_lock_timed,
but CPython doesn't chunk its wait either -- it makes one sem_timedwait call
and relies on the OS reporting EINTR. parking_lot's Condvar has no equivalent
signal to surface, so polling is a workaround for that gap, not a mirror of
CPython's approach.

Assisted-by: Claude

* Apply suggestions from code review

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>

* _queue: fix comment indentation (cargo fmt)

Assisted-by: Claude

---------

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2026-07-16 17:39:48 +09:00
박효종 76ca1ff106 Add 'w' (Py_UCS4) typecode support to array (#8266)
* Support array 'w' (Py_UCS4) typecode

Python 3.13 added the 'w' typecode holding fixed 4-byte unicode code
points, unlike 'u' which is platform-dependent wchar_t. Add a Ucs4Char
element type and route it through the existing unicode-array paths:
constructor from str, fromunicode()/tounicode(), __reduce_ex__, repr,
byteswap, and the machine-format code (Utf32). Also register 'w' as a
struct format char in vm::buffer.

Remove the now-passing expectedFailure markers for 'w' tests in
test_array.py.

Assisted-by: Claude

* Reject non-native byte order for the 'w' format char

FormatType::parse accepted 'w' after '<', '>', '!', or '=', but the
non-native branch of FormatType::info has no Ucs4Char arm and hits
unreachable!(), so struct.calcsize('>w') panicked the interpreter.
Add Ucs4Char to the existing native-only filter in FormatCode::parse
(alongside SSizeT/SizeT/VoidP) so those formats fail with the regular
"bad char in struct format" error instead. Native 'w' and
memoryview(array('w', ...)) are unaffected.

Assisted-by: Claude

* Unmark 'w'-typecode tests in test_re and test_csv

test_re's test_empty_array and test_csv's test_char_write construct
array('w', ...) and were marked expectedFailure while the typecode was
unsupported. Now that 'w' works they pass, and regrtest counts the
unexpected successes as failures in CI. Remove the two markers, which
restores both tests to their CPython v3.14.6 form; test_re, test_csv,
and test_array all pass.

Assisted-by: Claude
2026-07-15 23:18:03 +09:00
Jeaeun Kim 8376a03366 Match CPython unsigned socket conversion errors (#8257)
Assisted-by: Codex:gpt-5.4
2026-07-15 23:16:42 +09:00
Kwak Byoung Min fe7993572c Forward pyexpat lexical events (#8264)
* Restore pyexpat lexical callback compatibility

Forward processing instructions, comments, and CDATA boundaries already emitted by xml-rs so registered Python handlers observe CPython-compatible events.

Constraint: Reuse existing xml-rs events without changing the dependency
Confidence: high
Scope-risk: narrow
Tested: prek; test_pyexpat; test_sax; 402 extra tests; workspace tests; clippy
Assisted-by: Codex:gpt-5.6-sol

* Recognize XML compatibility restored by lexical events

Remove stale expected-failure markers for CPython XML tests now satisfied by pyexpat lexical-event forwarding, and simplify parser event error propagation.

Constraint: CPython test edits are limited to removing stale expectedFailure markers
Confidence: high
Scope-risk: narrow
Directive: Keep parser event dispatch aligned with pyexpat handler behavior
Tested: user-verified full suite; prek run --all-files; cargo run --release -- -m test test_pyexpat test_xml_etree test_xml_etree_c test_minidom test_sax
Assisted-by: Codex:gpt-5.6-sol
2026-07-13 18:40:18 +09:00