Commit Graph
125 Commits
Author SHA1 Message Date
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 7108ff0f69 marshal: round-trip code constants through the runtime bag (#8516)
* Keep abnormal marshal loop test aligned with CPython

Assisted-by: OpenAI Codex:gpt-5.5

* marshal: retain runtime code constants while loading

Decode code fields through the runtime MarshalBag so co_consts values that do not fit the compiler constant enum remain available to VM code wrappers while the compiler table receives shape placeholders.

Assisted-by: OpenAI Codex:GPT-5.4

* marshal: write and read co_consts through the runtime bag

serialize_code gains a serialize_code_with variant that writes each
co_consts entry through a caller-supplied writer; serialize_code keeps
the BorrowedConstant writer as its default.  The VM writer passes its own
write_object_depth, so a code constant that Literal holds but
BorrowedConstant cannot describe reaches the stream instead of panicking
in borrow_obj_constant, and a constant shared with the enclosing object
takes an entry in the writer's reference table.

PyMarshalBag implements constant_ref_from_value, bytes_from_value,
str_from_value and tuple_elements_from_value, which
deserialize_code_value_inner requires to decode code fields through the
runtime bag.

Assisted-by: Claude
2026-08-14 03:30:51 +09:00
Jeong, YunWon 84a0ba7864 Fix marshal recursive reference loading (#8501)
* Fix marshal recursive reference loading

Create reference-tracked containers before reading their children so recursive list, dict, set, and tuple graphs can be unmarshaled. Preserve interned string markers through the runtime bag and add an initialization-only tuple construction path.

Assisted-by: Codex:gpt-5

* Preserve marshal container insertion errors

Keep Python exceptions raised while constructing unmarshaled sets, frozensets, and dictionaries instead of collapsing them into ValueError. This makes abnormal recursive hash-container streams report TypeError like CPython and removes the remaining test_marshal expected failure.

Assisted-by: Codex:gpt-5

* Enable full abnormal marshal reference loop test

* Test direct marshal tuple reference loop

Assisted-by: Codex:gpt-5
2026-08-12 16:28:09 +09:00
Joshua Megnauth 33eb99af44 Fix some clippy lints for 1.99 (#8412) 2026-07-30 22:11:31 +09:00
Jeong, YunWon 16917e0f2a Use dict keys-version stamps and entry-index hints in LOAD_ATTR/STORE_ATTR specializations (#8350)
* Use dict keys-version stamps and entry-index hints in attr specializations

Add a keys-version stamp to Dict: a globally unique u32 assigned lazily
and reset on any key-set change (new key, deletion, clear). Value-only
updates keep the stamp.

- LoadAttrMethodWithValues / LoadAttrNondescriptorWithValues: cache the
  instance dict's stamp in the pointer cache and skip the shadow probe
  while the stamp matches.
- LoadAttrWithHint: cache the entry index at specialization time; a hit
  is an identity check on the entry key instead of a hash probe, with
  self-refresh on miss.
- StoreAttrWithHint / StoreAttrInstanceValue: replace values through the
  cached entry index via set_item_with_hint.

Fixes youknowone#42

Assisted-by: Claude

* Share keys-version stamps between dicts with identical layouts

Derive the keys-version stamp from a shape — the ordered interned-key
sequence of a hole-free dict — instead of always allocating a unique
stamp. Dicts with identical layouts (instances of the same class built
by the same __init__) now carry equal stamps, so a LOAD_ATTR cache
entry populated by one instance skips the shadow probe for every
instance sharing the layout.

Shapes are held in a fixed-size lock-free table keyed by interned key
addresses; dicts with holes, non-interned keys, or more than 32 keys
fall back to dict-unique stamps.

Assisted-by: Claude

* Address clippy and review feedback for keys-version stamps

- Use BuildHasher::hash_one for shape hashing (clippy manual_hash_one)
- Fix set_item_with_hint doc: a refreshed hint is returned on any hint
  miss, not only when the hinted slot was vacant
- Route both holey-dict instances through a single LOAD_ATTR cache site
  in the snippet test

Assisted-by: Claude

* Restore try_read_cached_descriptor doc comment to its function

The doc block and #[inline] were left attached to store_attr_dict_hinted
when it was inserted above try_read_cached_descriptor.

Assisted-by: Claude

* Specialize LoadAttrWithHint even when the entry index exceeds u16

hint_for_key returns None both for an absent key and for a present key
whose entry index does not fit in u16, so very large instance dicts
stopped specializing entirely. Use get_item_opt_refresh_hint to decide
presence, degrading an unrepresentable hint to 0: the handler then
keeps taking its full-probe fallback path.

Assisted-by: Claude
2026-07-23 16:18:09 +09:00
Jeong, YunWon 210ccc889c Thread-safe type operations: type lock, QSBR type-cache reclamation, GC stop-the-world, and interpreter optimizations (#7416)
* type lock

* Drop old PyObjectRef outside type lock to prevent deadlock

Dropping values inside with_type_lock can trigger weakref callbacks,
which may access attributes (LOAD_ATTR specialization) and re-acquire
the non-reentrant type mutex, causing deadlock.

Return old values from lock closures so they drop after lock release.

* Align type lock behavior with CPython

* Add PUBLISHED flag bit to RefCount state word

Assisted-by: Claude

* Add QSBR module for deferred memory reclamation

Assisted-by: Claude

* Defer memory free of cache-published objects via QSBR

Assisted-by: Claude

* Wire QSBR checkpoints into thread lifecycle and eval breaker

Assisted-by: Claude

* Publish type-cache values to QSBR and bypass cache without a VM

Mark cached type-method values as QSBR-published before storing their
pointer in TYPE_CACHE, so racing readers never try-incref freed memory.
Debug-assert that the current thread is ATTACHED whenever a lock-free
cache read happens.

Delete find_name_in_mro_without_vm and its unsound direct SeqLock read
outside any VM/thread registration; find_name_in_mro now falls back to
the locked, uncached MRO walk when no VM is current. This also removes
assign_version_tag()'s last caller, so its unlocked fallback is deleted
along with it (version_for_specialization keeps the locked path).

VirtualMachine::initialize() runs Python bytecode (e.g. importing
codecs/encodings) before any enter_vm scope exists, which left the
bootstrap thread not ATTACHED for cache reads during startup. Add
thread::VmBootstrapGuard, an RAII counterpart to enter_vm usable across
statements that need &mut VirtualMachine, and wrap initialize() with it.

Assisted-by: Claude

* Use try-incref reads and QSBR-backed swaps in specialization cache

Assisted-by: Claude

* Reset QSBR thread registry after fork in the child

Add Qsbr::reset_after_fork, which clears the registered thread slots
before draining the retire queue. Call it in py_os_after_fork_child,
right after type_cache_after_fork() and before
_thread::after_fork_child() re-registers the surviving thread.

Dead parent threads' Arc<QsbrSlot> handles live on in the child's
copied memory with no destructor ever running, so without this the
registry keeps them 'online' forever and future grace periods never
complete.

Assisted-by: Claude

* Add threaded stress test for type cache mutation races

Assisted-by: Claude

* Restrict qsbr internals to pub(crate) and fix clippy lints

Downgrade pub items in the private qsbr::threading module (QsbrSlot,
Qsbr, QSBR, and its methods) to pub(crate), since they were never
reachable outside the crate. Downgrade ThreadSlot::qsbr accordingly to
avoid a private-interfaces warning. Import Arc/Weak from alloc instead
of std to match clippy::std_instead_of_alloc. Rewrite the process()
comment: goals are not strictly ordered by push order since advance()
and the queue push aren't atomic together, so concurrent free_delayed
calls can interleave; the drained prefix stays sound because each item
individually passed poll, with reordering only affecting reclamation
latency.

Assisted-by: Claude

* Gate per-instruction QSBR check behind a global pending flag

eval_breaker_tripped() and check_signals() called
thread::qsbr_break_requested() on every bytecode instruction, which
does a thread-local lookup, RefCell borrow, and atomic load even when
no QSBR retirement is pending. This caused a measured 10-18% slowdown
on tight interpreter loops.

Add Qsbr::pending, a global AtomicBool set under the queue lock while
free_delayed() holds a retired allocation, and cleared under the same
lock by process()/drain_all() once the queue empties. The hot path now
checks Qsbr::break_pending() (a single relaxed static load) before
touching thread-local state, and only pays the TLS cost while a
retirement is actually in flight.

Assisted-by: Claude

* Merge signal and QSBR eval-breaker flags into one atomic word

Replace the separate ANY_TRIGGERED AtomicBool and QSBR pending
AtomicBool checks in the per-instruction eval-breaker path with a
single AtomicU8 (EVAL_BREAKER) holding a bit per source. The global
QSBR instance mirrors its pending state into the QSBR bit under the
same queue-lock critical sections that already toggle pending, while
local QSBR instances used by unit tests are left untouched.

eval_breaker_tripped() and check_signals() now read/act on the merged
word (eval_breaker_pending(), qsbr_bit_set()) instead of issuing two
separate atomic loads per instruction. set_triggered() switches from
store to fetch_or so a signal handler never clobbers the QSBR bit.

Removed the now-unreferenced is_triggered() helper; break_pending()
is kept for unit tests only and marked allow(dead_code) outside test
builds.

Assisted-by: Claude

* Skip freelist reuse for published objects in default_dealloc

Freelist-eligible payloads (tuple, int, list, dict, float, ...) were
routed to T::freelist_push before ever reaching PyInner::dealloc, so a
published object (e.g. a tuple cached as a type attribute) bypassed the
is_published -> free_delayed QSBR hook entirely. That let PyRef::new_ref
reuse the slot and rewrite the refcount word with a non-atomic
core::ptr::write racing a reader's atomic try-incref, and let
FreeList::drop call alloc::dealloc directly while a reader could still
be mid-read.

Gate the freelist branch in default_dealloc on
RefCount::is_published(), checked before any teardown. A published
object now always falls through to PyInner::dealloc, whose existing
hook defers the memory free via QSBR.

Document the non-unix (e.g. Windows) reclamation-latency limitation in
the QSBR design doc's Lifecycle integration section (previously only
noted in a code comment in vm/thread.rs).

Extend the type-cache stress test so the mutator also churns a
freelist-eligible published tuple class attribute (C.shape), read by
readers each inner loop and deleted on the same cadence as C.m, to
exercise the fixed bypass.

Assisted-by: Claude

* Use itoa for PyInt::to_str_radix_10 i64 fast path

Format small integers with itoa::Buffer instead of i64::to_string()
in PyInt::to_str_radix_10(), which backs both str(int)/repr(int) for
exact int and PyObject::str()'s exact-int fast path. Adds the itoa
crate as a workspace dependency.

Assisted-by: Claude

* Make object attribute dunders wrapper descriptors

Remove the __getattribute__, __setattr__ and __delattr__ #[pymethod]s
from PyBaseObject so add_operators installs PyWrapper slot wrappers for
them from the #[pyslot] functions instead of method descriptors.

lookup_slot_in_mro now classifies these entries as NativeSlot, so heap
types without Python-level overrides keep the native getattro/setattro
slot functions, which lets LOAD_ATTR/STORE_ATTR specialization apply to
instances of Python-defined classes.

Since __setattr__ and __delattr__ share the setattro slot, resolve both
names together in update_one_slot: overriding only one of them no longer
lets the other name's native resolution overwrite the dispatching
wrapper.

Assisted-by: Claude

* Resolve TpSetattro pair on attribute deletion, not just addition

update_one_slot's TpSetattro branch only ran the combined
__setattr__/__delattr__ resolution (added in the previous commit) when
ADD was true. On deletion (e.g. del C.__setattr__), it fell back to
inherit_from_mro, which copies the base class's slot and discards the
class's own remaining Python-level override of the other name.

lookup_slot_in_mro reads the current attribute dicts, so the same
resolution is correct for both addition and deletion; the ADD-only
guard is removed and the logic now always runs.

Assisted-by: Claude

* Generate Opcode::cache_entries/deopt as table lookups

generate_rs_opcode_metadata.py now emits a 256-entry const array for
each function instead of a chained match over opcode names.
Opcode::deopt() indexes an [Option<Opcode>; 256] table built from the
existing specialization-to-base mapping, and Opcode::cache_entries()
indexes a [u8; 256] table that bakes in the same deopt/to_base
composition deoptimize() previously performed before its own match.
Regenerated opcode_metadata.rs from the updated script; PseudoOpcode's
bodies are unchanged since it has no cache/deopt entries to table.

Added an exhaustive test in instruction.rs that checks every valid u8
opcode against a frozen copy of the old chained-match bodies.

Assisted-by: Claude

* Make Opcode/Instruction numeric conversions O(1)

Opcode::as_numeric, Opcode::as_instruction and Instruction::as_opcode
were each a per-variant match over ~230 arms in the define_opcodes!
macro. Opcode now carries the same explicit $op_id discriminants and
#[repr($typ)] as Instruction, which makes the conversions sound as:

- Opcode::as_numeric: a plain `self as $typ` identity cast.
- Opcode::as_instruction / Instruction::as_opcode: a mem::transmute,
  relying on both enums sharing one-$typ-wide layout (Instruction's
  payload fields are all the zero-sized Arg<T> marker, checked by a
  new per-instantiation size_of assertion).

try_from_numeric is left as a match: $op_id values have gaps
(specialized/instrumented opcode ranges), so a range check can't
replace it.

Adds #[inline] to the rewritten conversions plus deoptimize, and to
the small generated wrapper/table-lookup functions from the previous
opcode-metadata change (as_u8/as_u16, cache_entries, deopt, to_base)
via generate_rs_opcode_metadata.py, then regenerates
opcode_metadata.rs.

Extends the instruction.rs equivalence-test pattern with two
exhaustive tests (u8 and u16 instantiations) checking the new
identity-cast/transmute conversions against the untouched
try_from_numeric/TryFrom paths.

Assisted-by: Claude

* Pop CallIsinstance arguments directly instead of collecting into Vecs

The specialized handler built two heap-allocated Vecs per call
(pop_multiple().collect() plus a with_capacity(2) buffer) before
invoking is_instance. Pop the class and instance straight off the
stack for the effective two-argument case, removing the per-call
allocations.

Assisted-by: Claude

* Inherit tp_new instead of always installing new_wrapper

update_one_slot's TpNew branch stored new_wrapper for every ADD, so all
heap types had slots.new == new_wrapper and the CallAllocAndEnterInit
specialization never fired. Now __new__ is resolved through the MRO
dicts: an own or Python-level definition installs new_wrapper, while a
builtin __new__ entry (or none) inherits slots.new from the solid base.

The now-reachable CallAllocAndEnterInit path gains the missing guards:
the specializer requires co_argcount == oparg + 1 (via
can_specialize_call) and rejects generator-like __init__, and the
handler re-checks the argcount against the class-level init cache.
Without the argcount check, __init__ defaults broke re._parser
(UnboundLocalError in SubPattern.__init__). Remove the now-unused
is_simple_for_call_specialization.

Assisted-by: Claude

* Skip locals dict allocation for the init-cleanup shim frame

Mark the shim code object NEWLOCALS and create the shim frame with no
locals mapping so FrameLocals::lazy() is used. The shim only executes
ExitInitCheck/ReturnValue and never touches locals, so this removes one
dict allocation per specialized instantiation.

Assisted-by: Claude

* Guard CALL_ALLOC_AND_ENTER_INIT with cached function version

The init specialization cache stored only the type version, so swapping
__init__.__code__ (which zeroes func_version) did not deopt the warmed
call site and stale code assumptions (argcount shape, no varargs/kwonly,
not a generator) were applied to the new code object.

Store get_version_for_current_state() in the specialization cache when
caching __init__ and re-check func_version() in the handler before using
the cached function, deopting to the generic call path on mismatch, the
same way the getitem specialization cache does.

Assisted-by: Claude

* Apply rustfmt and ruff-format fixes to files from earlier commits

Assisted-by: Claude

* Add frame object freelist

Frame.iframe becomes FrameUnsafeCell<Option<InterpreterFrame>> so a dead
frame can be left as an empty husk. Traverse::clear extracts all owned
child references (including localsplus values via LocalsPlus::clear_into)
and empties the cell. Dead frame allocations are cached in a thread-local
FreeList<Frame> (up to 200 entries) and reused by PyRef::new_ref.

clear_generator returns early when the frame was already cleared by cycle
collection; generator drops can run after tp_clear of their frame.

gc: skip tp_clear for objects saved to gc.garbage by DEBUG_SAVEALL, since
they remain reachable from Python.

Assisted-by: Claude

* Drop frame children directly in tp_clear instead of extracting

Pushing the frame's ~10 child references into the clear buffer grew a
Vec on every frame deallocation. Take the interpreter frame out of the
cell and drop it in place; LocalsPlus::clear_into is removed.

Assisted-by: Claude

* Untrack objects before tp_clear and guard cross-thread f_locals

The cycle collector called tp_clear on unreachable objects while they
were still linked into the GC generation lists, so another thread could
obtain a strong reference to an already-cleared object (e.g. a frame
husk with iframe == None) via gc.get_objects() and access the cleared
payload. Untrack the dead set before the clear phase, mirroring the
untrack-then-clear ordering of the refcount dealloc path.

Objects that gained an external reference before untracking are found
by comparing each object's strong count against the references coming
from within the dead set; such late-resurrected objects and everything
reachable from them are re-tracked and skipped, letting a later
collection retry once the external references are released.

Also reject f_locals access for a frame currently executing on another
thread. Reading fastlocals of such a frame races with the executing
thread overwriting the slots (torn reads of dropped values). Access
from the executing thread itself (locals(), trace callbacks) is still
allowed via the current-frame chain check.

Assisted-by: Claude

* Skip localsplus heap copy for uniquely referenced dying frames

release_datastack_frame now checks the frame's strong count after
untracking it from the GC generation lists. When the caller holds the
only reference, localsplus values are dropped in place and the data
stack storage is released without the heap copy; escaped frames
(traceback, sys._getframe, trace callbacks) keep the copy path.
The three inline materialize+pop call sites in function.rs are routed
through release_datastack_frame.

Assisted-by: Claude

* Stage exact-args call arguments in stack slot buffers

CallPyExactArgs, CallBoundMethodExactArgs and CallAllocAndEnterInit no
longer collect popped arguments into a per-call Vec (plus a second Vec
for self-prepending). Arguments are popped into a fixed-size
Option<PyObjectRef> slot buffer (CallArgBuffer, 8 inline slots, heap
fallback for larger arities) and moved into fastlocals via the new
invoke_exact_args_slots / a take() iterator.

- prepare_exact_args_frame now accepts an ExactSizeIterator of args
- specialization_run_init_cleanup_shim takes the slot buffer and fills
  slot 0 with new_obj, dropping one redundant clone
- LoadAttrGetattributeOverridden, LoadAttrProperty and
  BinaryOpSubscrGetitem pass inline slot arrays instead of vec![]

Assisted-by: Claude

* Reduce allocations in generic call paths

- FuncArgs::prepend_arg: use reserve instead of reserve_exact so
  exact-capacity vectors do not realloc on every prepend.
- IntoFuncArgs::into_method_args: build the final args vec once with
  capacity len + 1 instead of prepending into a full vector.
- PyType::call: skip cloning FuncArgs when no init slot can run after
  slot_new (no init slot, not `type`, and slot_new is not new_wrapper).

Assisted-by: Claude

* Skip redundant exc_info restore on frame exit

with_frame_impl saves the current exc_info slot value and restores it
when the frame exits. When the slot still holds the same value, the
restore rewrites the slot and recomputes the thread-exception mirror
for no effect. Add restore_exception, which compares the slot against
the saved value by pointer identity and skips the store when they
match. The saved value is a strong reference held for the whole frame
scope, so the pointed-to object cannot be freed and its address reused
while the frame runs, making the pointer comparison free of ABA.

Assisted-by: Claude

* Apply rustfmt to gc_state.rs

Assisted-by: Claude

* Avoid empty FuncArgs clone in PyType::call

Cloning args for the init call clones the kwargs map even when args is
empty, which shows up in no-argument instantiation profiles. Prepare
the init-call args before slot_new: a default FuncArgs when args is
empty (indistinguishable from a clone of empty args), the existing
clone otherwise. The empty check also keeps the init-slot load of the
clone-elision path off the no-argument call path.

Assisted-by: Claude

* Push freelist husks only after tp_clear and child drops

default_dealloc pushed the object onto the thread-local freelist before
running clear_fn. Payloads that drop children inside clear (Frame) can
run __del__ there, and a reentrant allocation could pop the husk and
overwrite its payload while clear_fn still held a &mut borrow of it.
Tuples had the same window through the extracted-edges drop, which ran
after the push.

Reorder default_dealloc to clear, drop the extracted children, and only
then attempt the freelist push, so the husk becomes reusable only when
no borrows into the payload remain.

The tuple freelist bucketed husks by element count, which required
reading the payload during push; after this reordering the elements are
already cleared. PyInner<PyTuple> is a fixed-size allocation (the
elements box is dropped and replaced on reuse), so replace the per-size
buckets with the shared single-list FreeList. Remove the now-unused
pyinner_layout helper.

Assisted-by: Claude

* Resolve __set__ and __delete__ together for the descr_set slot

The descr_set slot serves both __set__ and __delete__, but the
TpDescrSet accessor resolved only the single modified name and fell
back to MRO inheritance on delete. Deleting one of the pair could
disable the surviving operation: after `del D.__set__`, an instance
delete stopped calling the remaining __delete__.

Resolve both names together like the TpSetattro accessor does: any
Python-level definition selects the dispatching wrapper, matching
native functions are stored directly, a single native function with
the other name absent is stored directly, and the slot is inherited
from the MRO only when neither name resolves.

Assisted-by: Claude

* Update type base on __bases__ assignment

set_bases replaced bases and mro but never updated the stored base, so
__base__ kept reporting the old value after reassignment and slot
resolution (tp_new inheritance, set_new/set_alloc during init_slots)
read the stale solid base.

Change the base field to PyAtomicRef<Option<PyType>> so it can be
swapped under the type lock while remaining lock-free for readers, and
recompute it in set_bases with the same best_base validation type
creation uses (BASETYPE flag, instance layout conflict among bases).
The swapped-out base is parked in the frame's temporary refs; other
references released inside the critical section are dropped after the
lock is released.

Also in set_bases:
- remove this class from the old bases' subclass lists (pruning dead
  entries), so repeated assignment no longer accumulates duplicates
- roll back bases, base, and all updated mros when the recursive mro
  update fails, instead of leaving the type half-reparented
- fix the misformatted empty-tuple error message

Remove the expectedFailure marker from test_unsubclassable_types,
which now passes.

Assisted-by: Claude

* Fix rollback order, dead weakrefs, and lock-held drops in set_bases

- Restore recorded mros in reverse on rollback so a class visited
  multiple times through diamond inheritance ends with its original
  mro instead of an intermediate one.
- Skip dead weakrefs in the subclass list during the recursive mro
  update instead of panicking on upgrade.
- Retire the replaced mros on the success path instead of dropping
  them while the type lock is held.

Assisted-by: Claude

* Reify number sub-slot wrapper once in update_one_slot

The update_sub_slot! macro expanded the Python-method wrapper closure at
both the own and inherited store sites. Each expansion is a distinct fn
item, so a base and a non-overriding subclass stored wrappers with
distinct addresses in unmerged debug builds. binary_op1 compares slot fn
addresses to detect whether a subclass overrides the operator, so the
inherited slot was misread as an override and C() // E() dispatched to
__rfloordiv__ instead of __floordiv__.

Bind the wrapper store in a single closure and call it from both branches
so the fn item is reified once and the slot value stays identical across
a base and its subclasses. Document the conservative-on-mismatch nature
of the new_wrapper address guard in call_wrapped.

Assisted-by: Claude

* Restrict type_cache_after_fork visibility and guard type-cache reads

type_cache_after_fork is only called from within the crate, so mark it
pub(crate) to resolve the unreachable-pub warning.

Add debug_assert_current_thread_attached at the has_name_in_mro lock-free
type-cache read site, matching lookup_ref_and_version_interned, and gate
the function on debug_assertions so it is not compiled unused in release
builds where the call sites are elided.

Assisted-by: Claude

* Guard BinaryOpSubscrGetitem with the cached type version

The specialized BINARY_OP_SUBSCR_GETITEM handler used the cached
__getitem__ after checking only the function version, unlike the sibling
specialized handlers which revalidate the type version tag first.

Store the type version in the inline cache at specialization time and
revalidate owner.class().tp_version_tag against it before using the cached
function, deopting to the generic subscript path on mismatch.

Assisted-by: Claude

* Call __abstractmethods__ __len__ once in object.__new__

The abstract-method check invoked the user-visible __len__ twice: once via
length_opt for the count and again while materializing the method names.
Derive the count from the materialized list instead, so __len__ runs once.

Assisted-by: Claude

* Use i64 fast paths for specialized int add/sub/mul

Rewrite execute_binary_op_int to box results through new_int via i64
checked arithmetic instead of raw BigInt ops with new_bigint. Add an
int_mul helper and a shared int_fast_op that computes the i64 result and
falls back to the BigInt operation on to_i64 or checked-op failure.

Wire int_mul into both the specialized BinaryOpMultiplyInt handler and
the generic execute_bin_op Multiply/InplaceMultiply path, gated on exact
int operands so subclasses keep dispatching through _mul/_imul.

Assisted-by: Claude

* Use i64 fast paths for exact int floordiv and remainder

Add floordiv_i64/mod_i64 computing i64 floor-division and divisor-signed
remainder, guarding zero divisor and i64::MIN overflow by returning None.
Wire them through int_floordiv/int_mod (shared int_div_fast_op boxes via
new_int) into the generic execute_bin_op FloorDivide/Remainder and their
Inplace variants, gated on exact int operands so subclasses and zero
divisors fall through to the existing _floordiv/_mod/_ifloordiv/_imod
slow path.

Assisted-by: Claude

* Skip trashcan and untrack for non-GC-tracked objects in dealloc

default_dealloc read is_gc_tracked() only to decide untracking, and
entered the trashcan recursion guard unconditionally. Non-GC objects
(int, float, str, ...) own no child references that recurse during
deallocation, so they need neither the trashcan nor untracking. Read
is_gc_tracked() once and gate both trashcan begin/end and untrack on it,
removing three thread-local accesses per non-GC object deallocation.

Assisted-by: Claude

* Merge trashcan depth and defer queue into one thread-local struct

trashcan::end accessed two separate thread-locals (DEALLOC_DEPTH and
DEALLOC_QUEUE) at the outermost deallocation. Combine both into a single
`Trashcan` thread-local holding Cell-based depth and queue fields, so
begin and end each reach their state through one thread-local access.
The queue is set back before each deferred dealloc call so reentrant
begin/end during draining never holds an outstanding borrow.

Assisted-by: Claude

* Check object layout compatibility on __bases__ and __class__ assignment

Add a shared compatible_for_assignment helper that walks each type to the
base that fixes its instance layout and rejects the assignment when the old
and new layouts differ (basicsize, itemsize, member count, __dict__,
__weakref__, and __slots__ names). Wire it into set_bases, which previously
performed no layout check, and replace the inline check in __class__
assignment, which compared the two types directly and over-rejected a
subclass that adds no layout.

Enable test_descr.test_builtin_bases, which the new set_bases check passes.

Assisted-by: Claude

* Stop the world around GC pointer-reading phases

The cycle collector reads each tracked object's interpreter state during
reference subtraction, the reachability walk and the strong-reference
snapshot, including the localsplus of frames other threads are executing.
Those slots are written without synchronization, so the reads are a data
race under threading.

collect_inner now stops the world before taking the generation read locks
and restarts it after the strong-reference snapshot, before the
finalizer/weakref/tp_clear phases. A debug assertion checks that no frame
on any thread's call stack was classified unreachable.

Automatic collections from maybe_collect are deferred to the next bytecode
safepoint via a new eval-breaker GC bit instead of running synchronously:
a synchronous collection can hold an internal lock (e.g. the lazy frame
locals cell) that another thread is blocked on with no way to reach a
safepoint, deadlocking the stop.

Hardens the stop-the-world machinery for these frequent, concurrent GC
stops: attach_thread now honors a pending stop after re-attaching so a
thread doing rapid allow_threads calls cannot run past the requester;
stop_the_world completion is level-triggered on all-threads-suspended
rather than an edge-triggered countdown; and the thread-start started/ready
handshakes detach while waiting so the waiter is parkable.

Updates the Frame and PyRwLock/PyMutex traversal SAFETY comments to state
the actual invariant.

Assisted-by: Claude

* Add threaded GC vs executing-frame stress snippet

Workers churn frame state (recursion, generators, frame cycles) while a
collector loops gc.collect() and an introspector walks live frame objects.
Exercises the stop-the-world barrier around GC traversal.

Assisted-by: Claude

* Serialize fork and GC stop-the-world requesters

fork() (posix before/after-fork) and the cycle collector both drive the
single StopTheWorldState. With no mutual exclusion their requester word and
suspension countdown could interleave and be clobbered, so the completion
check never converged and a requester waited on itself forever (reproducible
parent-side hang when forking with GC enabled).

Add one exclusion held for the whole stop->start span of either requester:
stop_the_world acquires it before any stop bookkeeping and
start_the_world/reset_after_fork release it. The acquire is park-friendly
(poll try-lock and honor a pending suspend between tries) so a requester
blocked behind an active stop can still be force-parked instead of deadlocking
the active requester. The acquirer holds no other lock while spinning.

Also correct the PyRwLock traverse SAFETY note: a failed try_read may mean a
force-parked thread holds the write lock; skipping is safe because
under-traversal only over-approximates liveness. Document the residual
gc.collect()-under-a-non-generation-lock exposure at the collect_inner barrier.

Assisted-by: Claude

* Add fork under concurrent GC stop-the-world snippet

Worker threads allocate cyclic garbage with GC enabled while the main thread
forks repeatedly and each child collects. Exercises the fork/GC
stop-the-world exclusion; the allocation rate is throttled so a collection
stays cheap in unoptimized builds.

Assisted-by: Claude

* Detach while acquiring the import lock

The global import lock is held across bytecode by the importlib bootstrap,
so its holder can be parked at a safepoint mid-hold. Acquiring it while
attached let another thread block attached on the lock, so a stop-the-world
requester could wait forever for that attached thread to suspend while the
holder stayed parked.

Wrap IMP_LOCK acquisition in allow_threads in both _imp.acquire_lock and the
pre-fork acquire_imp_lock_for_fork so the wait honors stop-the-world requests.
Correct the acquire_exclusion comment: a spinning requester may hold IMP_LOCK
(fork) or the collecting mutex (GC); safety relies on those never being
acquired attached-blocking by another thread.

Assisted-by: Claude

* Add concurrent-import vs GC deadlock snippet

Two threads re-import modules (contending the import lock) while a third
storms the cycle collector and a fourth allocates cyclic garbage. Regressed
as a hang before the import lock acquisition was made park-friendly.

Assisted-by: Claude

* Create call frames untracked and track generators explicitly

Add PyPayload::NEW_REF_UNTRACKED (default false, true for Frame) so
PyRef::new_ref skips auto-tracking ordinary call frames in the GC.
Generator/coroutine/async-generator frames are tracked explicitly in
invoke_with_locals before their generator back-reference is installed.
run_frame debug-asserts that a datastack frame is untracked on entry.

Assisted-by: Claude

* Track escaped call frames lazily at datastack release

Rewrite release_datastack_frame around the invariant that a datastack
frame is never GC-tracked while it runs: strong_count() == 1 means the
frame never escaped, so drop localsplus in place without touching the
GC; strong_count() > 1 means it escaped, so materialize localsplus onto
the heap and then track the frame. This removes the untrack and the
untrack-recheck dance from the common non-escaping path.

Assisted-by: Claude

* Guard the tracked-frame localsplus invariant

Document at Frame::traverse that references to a frame are always
recorded as graph edges, so the collector reads a frame's localsplus
only when the frame is itself a tracked candidate. Add debug assertions
at both frame track sites (escaped datastack frames and generator
frames) that a frame is heap-backed before it is tracked, so a collector
never reads data-stack-resident, still-mutating storage.

Assisted-by: Claude

* Reword frame/dealloc invariant comments and drop needless borrow

- frame.rs: replace the task codename in the tracked-frame localsplus
  invariant comment with a description of the collector-vs-executing-frame
  behavior.
- object.rs: remove needless borrow on current_cls in the __class__
  assignment compatibility check.
- core.rs: rewrite the default_dealloc trashcan/untrack-skip comment to
  state the actual invariant, which now covers untracked non-escaped
  frames releasing at interpreter depth with bounded recursion.

Assisted-by: Claude

* Move type SetAttr slot rewrite inside the type lock and tighten cache reads

Run update_slot inside the same with_type_lock transaction as
modified_inner and the attributes dict mutation, so the version
invalidation, dict change, and slot-table rewrite are published together.

Load init_version and getitem_version with Acquire in the specialization
cache readers to pair with the Release stores in the writers.

Assisted-by: Claude

* Capture the type version before reading mutable slots in specializers

specialize_load_attr, specialize_store_attr, specialize_to_bool, and the
CallAllocAndEnterInit path of specialize_call read the version tag with
version_for_specialization before inspecting getattro, setattro, the
bool/len slots, and tp_new/tp_alloc, so a concurrent install of the
corresponding dunder invalidates the version the specialization is cached
against.

In the BinaryOpSubscr __getitem__ path, check the HEAPTYPE and
eval-frame gates before lookup_ref_and_version_interned, which takes the
global type lock and may allocate a version tag.

Assisted-by: Claude

* Use _get_method_dict for the test_type check in the test runner

Replace the direct __func__.__dict__ access with the _get_method_dict
helper so plain functions without __func__ do not raise AttributeError.

Assisted-by: Claude

* Fix lint hooks: cspell dictionary entries, spelling fix, formatting

Add qsbr to the rustpython dictionary and reborrows/reparenting to
the top-level word list. Rename oldto/newto locals to old_to/new_to.
Fix Stabilise -> Stabilize typo in a frame.rs comment.
Apply cargo fmt to object.rs, type.rs, and frame.rs, and ruff
format/check fixes to two extra_tests snippet files.

Assisted-by: Claude

* Rebuild all slots for type and descendants on __bases__ reassignment

Add PyType::update_all_slots, which invalidates version tags and iterates
the full SLOT_DEFS name table calling update_slot::<true> for each distinct
name. Unlike init_slots, which is additive and driven only by dunder names
present in the current MRO, this resets a slot whose method left the MRO
instead of leaving a stale dispatcher pointer.

Call it from set_bases in place of the previous modified_inner + init_slots
pair, so reassigning __bases__ rebuilds slots for the type and every
descendant.

Add extra_tests/snippets/type_bases_slot_rebuild.py covering removed-method
resets on zelf and deep descendants, the wrong-target switch, __getattr__,
the added-method mirror, and a swap-away-and-back round trip.

Assisted-by: Claude

* Derive the thread-local frame stack from the frame chain

Remove the per-VM `frames` Vec. current_frame, sys._getframe,
sys._getframemodulename, frame.f_back, sys.monitoring re-instrumentation,
gc.get_referrers, faulthandler stack dumps and the post-fork slot rebuild
now walk the signal-safe CURRENT_FRAME/`previous` chain instead. Add
`Py::from_payload_ptr` to recover a frame object from a chain pointer.

The cross-thread `ThreadSlot::frames` registry (sys._current_frames,
cross-thread f_back, GC stop-the-world assertion) is unchanged.

Assisted-by: Claude

* Inherit sub-slot fallback into the field being resolved

update_one_slot's number/sequence/mapping fallback inherited via the
accessor's default field. Left and right binary ops (add/right_add)
share one accessor but occupy distinct fields, so resolving an absent
right op or deleting it reset the left op's field, dropping a
still-defined __add__ dispatcher. Inherit the exact field under
resolution instead.

Assisted-by: Claude

* Gate type_cache_after_fork to its fork caller and use Self in downcast

type_cache_after_fork is only called from the unix fork path, so gate it
with all(feature = "host_env", unix) to match the caller and avoid a
dead-code error on non-unix targets. Replace the explicit PyType with Self
in the modified_inner downcast.

Assisted-by: Claude

* Unmark test_attr and test_method_call_error in test_monitoring

Both TestLoadSuperAttr tests now pass; remove their expectedFailure markers.

Assisted-by: Claude

* Run tp_new specialization __init__ without a trampoline frame

CallAllocAndEnterInit ran __init__ inside a synthetic init-cleanup shim
frame whose code carried the __init__ name. Since the thread frame stack
is derived from the frame chain, that shim frame was visible to
sys._getframe, f_back walks, traceback construction and inspect.stack /
inspect.trace. Its code object has empty co_positions, so a stack walk
that reached it raised StopIteration inside inspect._get_code_position,
cascading into unrelated failures (test_inspect trace/stack/frame,
asyncio source traceback).

Call __init__ directly via run_frame, enforce the __init__() should
return None contract inline, and drop the shim: the init-cleanup code
object, its builder, with_frame_untraced, monitoring_disabled_for_code,
and the extra-frame datastack/recursion budget. Removing the second
frame per construction also cuts the specialization's per-call cost.

Assisted-by: Claude

* Replace per-call thread-frame mutex with an atomic top-of-stack (unix)

On unix threading builds, publish each thread's top Python frame in a
single relaxed AtomicPtr store from set_current_frame instead of pushing
onto a parking_lot::Mutex<Vec<FramePtr>> per call. Cross-thread readers
(sys._current_frames, cross-thread f_back, faulthandler.dump_traceback,
the GC unreachable debug-assert) run under stop-the-world and walk the
published top frame down the Frame::previous chain; the owning thread is
then parked at a safepoint, so the pointer and the frames it reaches are
quiescent and alive. The faulthandler watchdog is a plain OS thread that
cannot stop-the-world, so it walks the chain lock-free and best-effort.

Non-unix threading builds have no stop-the-world and keep the existing
mutex-guarded frame stack unchanged.

Assisted-by: Claude

* Skip exc_info save/restore for callees that never touch the slot

with_frame saves and restores the shared exc_info slot around every
Python call to contain frames that leave it unbalanced. Cache a
has_exc_handling bit on PyCode at creation, set when the bytecode
contains any opcode that calls vm.set_exception (PushExcInfo, PopExcept,
CheckEgMatch, EndAsyncFor, InstrumentedEndAsyncFor). A callee whose code
has none of these cannot mutate the slot, so the save and restore are
skipped for it. Generators go through resume_gen_frame and are
unaffected.

Assisted-by: Claude

* Return a write-through FrameLocalsProxy from frame.f_locals

Optimized (function) frames now expose `f_locals` as a `FrameLocalsProxy`
implementing PEP 667 semantics instead of a cached snapshot dict:

- reads go live through the fast-local slots; each access mints a fresh
  proxy; keys that do not name a fast local are stored in a per-frame
  `f_extra_locals` side dict and folded into `locals()`.
- writes to a fast-local key store into the slot (or its cell) in place;
  deleting a fast local raises ValueError; extra keys delete normally.
- full mapping protocol: keys/values/items (lists), get/pop/setdefault,
  update (dict or FrameLocalsProxy only), __or__/__ior__/__ror__ (dict
  result), copy (plain dict), __reduce__ blocks pickling/copy, repr with
  recursion guard, mapping-pattern and Mapping ABC support.

Class/module/exec frames keep returning their namespace mapping directly.
Cross-thread access to a frame running on another thread still raises
RuntimeError.

A closed generator now keeps its frame locals when a durable frame
reference escaped (f_locals proxy, sys._getframe, f_back), matching
take_ownership; the escape is tracked with a per-frame flag. The
snapshot-then-fold locals_to_fast/locals_dirty write-back is retired
since proxy writes reach the slots directly.

Assisted-by: Claude

* Retain the caller frame so f_back resolves after it returns

When a frame escapes its execution (referenced through a traceback,
`sys._getframe`, `f_locals`, ...), capture a strong reference to its
caller at release time. `f_back` consults it once the caller has left
the live frame chain, so the Python-visible frame chain survives return.
The retained reference is a GC-traversed edge and is cleared by
`frame.clear()`, so ancestor chains stay collectable.

Assisted-by: Claude

* Fix debug-build native stack overflow on deep recursion

Make check_c_stack_overflow one-sided (trip whenever the stack pointer
is below the soft limit) so a single native frame larger than the margin
cannot step past the danger band undetected.

Raise the debug STACK_MARGIN_BYTES from 4096 to 16384 words so the margin
exceeds a single debug interpreter frame, leaving headroom to raise
RecursionError. Release margin unchanged.

Clamp the soft-limit margin to half the stack so small explicit thread
stacks do not get a soft limit above their stack top.

Assisted-by: Claude

* Allocate exception instance __dict__ lazily

Exception construction went through into_ref_with_type, which eagerly
allocated an empty instance dict for every HAS_DICT type. Add
into_ref_with_type_lazy_dict, which builds the instance with an
unallocated dict slot, and route the four exception construction sites
(PyBaseException, PyOSError, OSErrorBuilder, PyBaseExceptionGroup)
through it. The dict now materializes on first attribute write or
__dict__ access via the existing get_or_insert path.

add_note and PyImportError::slot_init now obtain the dict through
object_get_dict so they materialize it instead of assuming it exists.

A freshly constructed exception no longer reports an empty dict in
gc.get_referents, matching the reference interpreter.

Assisted-by: Claude

* Allocate exception __dict__ lazily in vm.new_exception

Route vm.new_exception() through into_ref_with_type_lazy_dict so
internally raised exceptions (new_type_error, new_value_error, etc.)
start without an instance dict, matching the slot_new path. The dict
is materialized on the first attribute write or __dict__ access.

Assisted-by: Claude

* Validate FrameLocalsProxy.update() arguments

Reject keyword arguments and require exactly one positional argument,
raising TypeError with the "takes no keyword arguments" and "takes
exactly one argument (N given)" messages.

Assisted-by: Claude

* Address review nits in frame and faulthandler

- Drop stale vm.frames reference from the release_datastack_frame
  uniqueness argument.
- Assert has_exc_handling when unwinding an Except-typed stack slot,
  documenting the invariant that guards the shared exc_info write.
- Truncate the specialized __init__ return-type name to 200 chars,
  matching the unspecialized wrapper.
- Reword two comments to describe behavior without prose references
  to CPython.

Assisted-by: Claude

* Use scopeguard for start_the_world in faulthandler dump_all_threads

Wrap the unix stop-the-world registry walk in a scope with
scopeguard::defer! so start_the_world runs on panic, matching the
f_back and get_all_current_frames sites. Add scopeguard to the stdlib
dependencies. Also correct the restore_exception doc comment to name
with_frame after the rename.

Assisted-by: Claude

* Gitignore docs/superpowers

Assisted-by: Claude

* Rename type_bases_slot_rebuild.py to builtin_type_bases.py

Match the builtin_* naming convention of extra_tests/snippets.

Assisted-by: Claude

* Apply rustfmt, ruff, and cspell lint fixes

Reformat with rustfmt, fix import spacing in builtin_type_bases.py with
ruff, and add "pointee" to the cspell word list.

Assisted-by: Claude

* Gate unix-only QSBR methods to their call sites

`online`, `drain_all`, and `reset_after_fork` are called only from unix
code (thread attach/detach, post-fork reset), and `offline` from unix
code plus a unit test. Gate them with matching cfg so `-D dead_code`
does not fire on non-unix targets.

Assisted-by: Claude

* Visit the function closure tuple as a GC edge

PyFunction::traverse visited the cells inside the closure tuple instead
of the tuple object itself, so the tuple's reference from the function
was never subtracted during cycle collection. A closure tuple that
reached back to its function (or, through a frame retained by f_back,
to a Thread) was stranded as a false GC root and never collected,
leaking the whole cycle. Visit the tuple itself, matching clear().

Assisted-by: Claude

* Apply formatting hook fix to builtin_type_bases.py

Assisted-by: Claude

* Unmark asyncgen finalization-by-gc tests in test_base_events

test_asyncgen_finalization_by_gc and
test_asyncgen_finalization_by_gc_in_other_thread now pass; GC finalizes
the async generators.

Assisted-by: Claude

* Unmark test_sni_callback_refcycle in test_ssl

The servername-callback reference cycle is now collected by GC.

Assisted-by: Claude

* Widen GC stop-the-world gates from unix-only to all threading builds

Change `cfg(all(unix, feature = "threading"))` to `cfg(feature = "threading")`
on the stop-the-world machinery so it also compiles and runs on non-unix
threading builds:

- StopTheWorldState, its stats, stw_trace, and the stop_the_world field
- ThreadSlot state/stop_requested/thread fields and their initializers
- wait_while_suspended/attach_thread/detach_thread/suspend_if_needed/do_suspend,
  allow_threads, stop_requested_for_current_thread, and the enter_vm /
  VmBootstrapGuard / attach_current_thread / release_current_thread / cleanup
  attach-state wiring
- eval_breaker_tripped, check_signals, run_scheduled_gc, signal GC_BIT /
  schedule_gc / take_gc_scheduled, and the frame.rs safepoint call
- CollectStopTheWorld and its use in collect_inner
- QSBR::online/offline, now called from attach/detach on all threading builds
- debug_assert_current_thread_attached and its type-cache call sites

maybe_collect defers auto-collection to the bytecode safepoint on every
threading build instead of only unix; non-threading builds keep the inline
collect. stw_trace writes to std stderr on non-unix.

top_frame publishing (CURRENT_TOP_FRAME_SLOT, set_current_frame), the
frame-walk debug assert in collect_inner, and the fork reinit helpers remain
unix-only; non-unix keeps ThreadSlot::frames for introspection.

Assisted-by: Claude

* Detach current thread around blocking _winapi/_overlapped waits

Wrap the blocking Windows wait calls in `vm.allow_threads` so the calling
thread transitions ATTACHED -> DETACHED for the duration of the wait:

- _winapi: WaitForSingleObject, WaitForMultipleObjects,
  BatchedWaitForMultipleObjects, ConnectNamedPipe, ReadFile,
  Overlapped.GetOverlappedResult
- _overlapped: Overlapped.getresult

These previously blocked while ATTACHED, so a stop-the-world requester
could never suspend the thread and spun in its wait loop indefinitely.

* Format WaitForMultipleObjects allow_threads closure

* Treat concurrent sni_callback removal as no-op in invoke_sni_callback

* Assert capi refcount on a fresh mortal list instead of the int type object

The refcount test asserted exact incref/decref deltas on PyInt's shared
type object. That object's reference count is perturbed by the other capi
tests running in parallel, and is immortal under some interpreter
configurations, so the deltas were not reliably +1/-1. Assert them on a
freshly created, uniquely owned list whose reference count is private to
the test and mortal.

Assisted-by: Claude

* Traverse and GC-track PyOSError instances

The `#[pyexception]` struct macro now forwards a `traverse` option to the
generated `#[pyclass]`, and `ExceptionItemMeta` accepts the `traverse`
key. `PyOSError` is marked `traverse = "manual"`, so `HAS_TRAVERSE` is
true and OSError-family instances are tracked at creation and traversed
by the collector.

`PyOSError::traverse` now visits the underlying `PyBaseException`
(traceback, cause, context, args) instead of `PyException::try_traverse`,
which was a no-op because `PyException` has `HAS_TRAVERSE = false`.

* Unmark test_blockingioerror in test_io

The BlockingIOError reference cycle is now collected by GC.

Assisted-by: Claude
2026-07-08 15:21:41 +09:00
Jeong, YunWon a1ba6a662c Align compiler and AST behavior with CPython (#8138)
* Align compiler and AST behavior with CPython

Update the compiler pipeline, AST module support, and related VM compile/eval paths toward CPython 3.14 behavior.

Use upstream Ruff parser crates directly and keep RustPython-specific syntax preflight handling in the compile path.

Refresh opcode metadata, snapshots, and targeted tests for the aligned bytecode and AST behavior.

* fix

* Fix PR 8138 CI failures

* Fix match mapping wildcard scan complexity

* fixes

* Use Ruff runtime AST fields

* Minimize residual patch diff

* refactor error handling

* Remove _ast conversion context wrappers

* Clean up _ast conversion glue and compiler warning escalation

Rename the Node conversion parameter `ctx` to `vm` throughout the _ast
module.

Remove the unused compile_program, compile_program_single,
compile_block_expression, and compile_expression forwarders; compile_top
stays as the entry point.

Stash an escalated compiler SyntaxWarning so a non-SyntaxWarning category
propagates unchanged instead of being rewritten to SyntaxError, matching
PyErr_ExceptionMatches(SyntaxWarning) in compiler_warn. Drop the now-unused
CompileWarningError::into_codegen_error.

Replace the PositionalArguments two-variant enum with a struct holding a
shared range and a PositionalArgumentsKind enum.

Assisted-by: Claude

* Represent compiler flags as a CompilerFlags bitflags

Replace the loose PY_CF_*/CO_* i32 constants in compile_mode with a
CompilerFlags bitflags covering the PyCF_* flags and __future__
CO_FUTURE_* bits. The Python-visible ast.PyCF_* attribute values are
derived from it via bits(); parser input modes stay plain consts.

builtins::compile and the compile_string flag handling test flags
through CompilerFlags::contains instead of raw bit math.

Assisted-by: Claude

* Fix PR 8138 test regressions

* Fix PR 8138 CI failures

* Avoid shell interpolation in Linux deps action

* Fix review findings in compiler/AST changes

- frame.rs: compute the MATCH_CLASS class name lazily so a successful
  match no longer takes the type name lock or allocates; the
  __match_args__-not-a-tuple branch reuses it instead of recomputing.
- _ast/constant.rs: add the missing leading space to the with_recursion
  label ("maximum recursion depth exceeded during compilation").
- sys.set_int_max_str_digits: restore the "maxdigits must be 0 or larger
  than {n}" error text.
- codegen: set CO_MAXBLOCKS to 20 in both ir.rs and compile.rs.
- _ast/exception.rs: extract the duplicated excepthandler node guard
  into ensure_excepthandler_node.
- codegen: correct the validate_keywords doc comment.
- Drop redundant "CPython" qualifiers from comments added in this branch.

Assisted-by: Claude
2026-06-26 18:22:08 +09:00
Shahar Naveh c6e254fbdc General code nits (#8135)
* General nitpicks

* Move Compiler::constant_truthiness to a ConstantData method

* More code cleanup

* bytecode::CodeFlags -> CodeFlags

* More code clenaups
2026-06-22 16:46:17 +09:00
Shahar Naveh 11da0b42c3 clippy enum_glob_use (#8121) 2026-06-17 13:24:36 +09:00
Jeong, YunWon 5add3d6a69 Align codegen passes and opcode metadata with CPython (#7987)
* Share marshal ref table between code object and its internals

read_marshal_bytes, _str, _str_vec, _name_tuple, and _const_tuple now
take a shared ref table and resolve TYPE_REF / register FLAG_REF
entries. deserialize_code is split into a public wrapper and an inner
function that receives the ref table; deserialize_value_depth opens a
fresh inner ref space when it hits Type::Code, mirroring CPython's
behaviour of putting the code object itself at ref slot 0. Nested code
objects inside const tuples reuse the surrounding code's ref space via
the new read_const_value helper.

* Align PYC magic number, FORMAT_VERSION, and header check with CPython 3.14

PYC_MAGIC_NUMBER changes from 2994 to 3627, matching CPython 3.14's
pyc_magic_number_token (0x0a0d0e2b). marshal FORMAT_VERSION drops from
5 to 4 (the encoder/marshal.version value; the decoder already accepts
both). check_pyc_magic_number_bytes now compares all four magic bytes
instead of the first two.

* Accept CPython-tagged .pyc as read-only bytecode source

SourceFileLoader.get_code now also looks for .pyc files using
_RP_FALLBACK_CACHE_TAGS (currently ('cpython-314',)) in addition to
sys.implementation.cache_tag. The matched .pyc is only used for
reading; recompilation still writes to the RustPython-tagged path, so
CPython's .pyc is never overwritten. Source-stat / hash / timestamp
validation logic is unchanged.

* Apply rustfmt to marshal helpers

* Marshal PySlice from format version 4 instead of 5

CPython's marshal supports TYPE_SLICE from format version 4 onwards
and that is the default version. Rejecting slice dumps below version
5 made marshal.dumps(slice(...)) fail with the default version and
broke test.test_marshal.SliceTestCase.test_slice.

* Revert "Accept CPython-tagged .pyc as read-only bytecode source"

Lib/importlib/_bootstrap_external.py is CPython's own code copied
verbatim; local patches here defeat compatibility tracking. The
cpython-XX cache_tag fallback needs to live on the RustPython side
(Rust code or sys.implementation.cache_tag policy), not as edits to
the imported standard library.

This reverts commit 1fc426d0fb5fcdb50d35cad13bbb43e8f6ce1c7f.

* Set marshal FORMAT_VERSION to 5 to match CPython 3.14.5

Py_MARSHAL_VERSION is 5 in CPython 3.14.5 (Include/marshal.h:16) and
TYPE_SLICE serialization rejects version < 5 (Python/marshal.c:720).
Restore the same threshold and constant so marshal.version and the
slice-marshal gate match CPython.

* Thread marshal recursion depth through nested code objects

Code objects embedded in const-tuples reset the depth budget on each
recursion, so a hostile or pathological marshal stream of code-in-tuple-
in-code can blow the stack despite MAX_MARSHAL_STACK_DEPTH. Pass the
current depth through deserialize_code_inner and read_marshal_const_tuple
and decrement at each code-object/tuple boundary.

Also route dict keys through deserialize_value_after_header so TYPE_CODE
keys decode instead of failing with BadType.

* align compiler to CPython

* Align codegen with CPython compile.c

Rename CFG helpers and accessors to the names used in CPython's
compile.c (basicblock_next_instr, basicblock_last_instr,
basicblock_append_instructions, bb_has_fallthrough, is_jump,
make_cfg_traversal_stack, mark_warm/mark_cold, etc.). Drop the unused
boolop-folding gate, mark_cpython_cfg_label_block helper, and
ComprehensionLoopControl::iter_range field.

Track an is_coroutine flag on SymbolTable, set in async def, await, and
async comprehensions, and propagate it through non-generator
comprehensions per symtable_handle_comprehension().

Mark SetupCleanup/SetupFinally/SetupWith as has_arg pseudo-ops, mark
ForIter as a terminator, and add has_arg/has_const on AnyInstruction.
Fix Instruction::stack_effect_jump to delegate to the opcode's
stack_effect_jump rather than stack_effect.

* Align codegen IR with CPython CFG structures

* Match CPython CFG annotation offset arithmetic

* Propagate CPython CFG label translation errors

* Align CPython exception target labeling flow

* Propagate CPython CFG traversal stack allocation errors

* Match CPython optimize_load_fast allocation flow

* Propagate CPython basicblock allocation errors

* Propagate CPython redundant NOP cleanup errors

* Propagate CPython unused const cleanup errors

* Propagate CPython const folding errors

* Propagate CPython swaptimize allocation errors

* Match CPython list-to-tuple fold allocation

* Skip const folding on CPython allocation failures

* Skip subscript folding on CPython allocation failures

* Propagate CPython assembler allocation errors

* Propagate CPython localsplus allocation errors

* Propagate CPython localsplus setup allocation errors

* Propagate CPython jump label map allocation errors

* Propagate CPython instruction sequence allocation errors

* Propagate CPython instruction label allocation errors

* Guard CPython codegen block allocation

* Guard CPython label shadow allocation

* Propagate CPython label shadow allocation errors

* Align CPython c-array allocation updates

* Align CPython ref stack growth

* Match CPython CFG builder debug check

* Avoid Rust-only CFG append clone

* Reuse CPython cleared block slots

* Use CPython block append in copy_basicblock

* Match CPython cfg builder creation order

* Clear label map after CPython apply pass

* Match CPython cfg builder allocation check

* Propagate CPython c-array size errors

* Drop Rust-only label uniqueness check

* Drop Rust-only label shadow debug checks

* Propagate CFG block index overflow

* Propagate CFG label oparg overflow

* Model CPython basicblock instruction storage

* Drop Rust-only recorded CFG precheck

* Model CPython instruction sequence storage

* Propagate instruction sequence offset overflow

* Model CPython instruction sequence labels

* Match CPython jump offset arithmetic

* Match CPython exception table arithmetic

* Match CPython label index arithmetic

* Match CPython instruction offset casts

* Match CPython jump offset indexing

* Match CPython oparg locals casts

* Match CPython localsplus offset arithmetic

* Match CPython cell prefix indexing

* Match CPython C array growth arithmetic

* Match CPython label map allocation arithmetic

* Match CPython label map size tracking

* Use CPython label map size in sequence passes

* Assert CPython label map clearing invariants

* Match CPython label oparg assignment

* Match CPython compiler direct arithmetic

* Match CPython load fast local casts

* Match CPython load fast depth assert

* Match CPython resume depth flagging

* Match CPython stack depth arithmetic

* Drop Rust-only stack overflow error

* Return CPython stack depth directly

* Match CPython C array growth errors

* Match CPython instruction insert asserts

* Match CPython unreachable pseudo jump

* Match CPython CFG size guard

* Match CPython superinstruction assert

* Match CPython redundant jump assert

* Match CPython stackdepth errors

* Match CPython jump offset flow

* Match CPython assembler buffer defaults

* Match CPython bytecode emit growth

* Match CPython assembler entry growth

* Match CPython assembler growth overflow check

* Match CPython remove_unreachable structure

* Match CPython static swap flow

* Inline CPython code unit preprocessing

* Match CPython C array growth checks

* Match CPython label map size guard

* Match CPython load-fast flow

* Simplify CPython CFG condition flow

* Align exception fallthrough propagation

* Match CPython pseudo target table

* Match CPython annotations CFG assert

* Match CPython inverted op assert

* Match CPython many-locals guard

* Reject deopt opcodes in CFG stack effects

* Match CPython invalid stack effect error

* Test CPython deopt stack effect guard

* Match CPython load-fast extended-arg assert

* Match CPython instruction allocation asserts

* Match CPython basicblock last-instr asserts

* Match CPython opcode range asserts

* Assert CPython fallthrough line propagation invariant

* Assert CPython CFG target offset sign

* Assert CPython exception fallthrough invariant

* Assert CPython exception stack bounds

* Assert CPython traversal stack allocation

* Match CPython label-map allocation in shadow

* Mirror CPython label-map sentinel fill

* Match CPython CFG builder allocation asserts

* Match CPython exception stack structure

* Match CPython ref stack structure

* Match CPython CFG traversal stack structure

* Mirror CPython CFG traversal stack pointer

* Use CPython fixed exception handler stack

* Mirror CPython ref stack capacity field

* Match CPython swap optimizer scratch stack

* Align static swap helpers with CPython blocks

* Align swaptimize signature with CPython

* Match CPython redundant pair pass result

* Match CPython inline pass result

* Match CPython redundant NOP pass results

* Fix bytecode metadata after upstream rebase

* Match CPython opcode stack metadata

* Add CPython identifiers to cspell dictionary

Add CNOTAB, LNOTAB, ialloc, ioffset, iused, nblocks, ncellsused,
ncellvars, nextop, noffsets, nvars, swaptimize, untargeted to
.cspell.dict/cpython.txt for the new CFG/assembler code in
crates/codegen/src/ir.rs.

* Fix CI failures from bytecode-parity work

- clippy: drop redundant `test_` prefix on three test functions and
  remove an unnecessary `u32` cast in basicblock_clear_reuses_cpython_spare_slots_in_offset_order
- insta: regenerate nested_double_async_with snapshot to match the new
  CFG output that drops unreferenced labels after the redundant-NOP pass
- regrtest: drop `@expectedFailure` markers from test_func_args,
  test_meth_args (test_compile), test_disassemble_with,
  test_disassemble_try_finally (test_dis), and test_except_star
  (test_monitoring) which now pass

* Resync generated opcode metadata

Empty conf.toml since WithExceptStart and Setup{Cleanup,Finally,With}
stack effects already match CPython, so the TODO override entries are
stale and only cause CI hook diffs.

Regenerate opcode_metadata.rs and drop the matching SetupCleanup/
SetupFinally/SetupWith assertions on PseudoOpcode::has_arg(); their
`HAS_ARG` flag comes from pseudo definitions in bytecodes.c that the
upstream analyzer does not propagate through PseudoInstruction.properties,
so the generated has_arg() excludes them. has_target() still covers
these block-push pseudos via is_block_push().

* Drop is_block_push has_arg invariant

The CPython invariant `assert(OPCODE_HAS_ARG(op) || !IS_BLOCK_PUSH(op))`
relies on SETUP_{FINALLY,CLEANUP,WITH} carrying `HAS_ARG_FLAG` in
CPython's metadata. The autogen tool reads pseudo-opcode properties from
target instructions and does not propagate the pseudo's own
HAS_ARG flag, so PseudoOpcode::has_arg() omits these three opcodes.
Drop the debug_assert that fired inside py_freeze proc-macro expansion.

* Auto-generate has_eval_break and route AnyInstruction has_arg/has_const via macro

Add fn_has_eval_break to generate_rs_opcode_metadata.py using CPython's
Properties.eval_breaker, removing the hand-written matches! body for
Opcode::has_eval_break and PseudoOpcode::has_eval_break.

Forward has_arg/has_const from Instruction and PseudoInstruction to
their opcode, so AnyInstruction can use either_real_pseudo! like the
other has_* accessors instead of an open-coded match.
2026-05-28 09:19:11 +09:00
Shahar Naveh 26dfeae512 Autogen opcodes metadata (#7983) 2026-05-27 16:44:58 +09:00
Shahar Naveh 9a0291188c Clippy rules (test related) (#7968) 2026-05-25 22:03:18 +09:00
Jeong, YunWon 1d95df70bd Align marshal and .pyc with CPython 3.14 (#7958)
* Share marshal ref table between code object and its internals

read_marshal_bytes, _str, _str_vec, _name_tuple, and _const_tuple now
take a shared ref table and resolve TYPE_REF / register FLAG_REF
entries. deserialize_code is split into a public wrapper and an inner
function that receives the ref table; deserialize_value_depth opens a
fresh inner ref space when it hits Type::Code, mirroring CPython's
behaviour of putting the code object itself at ref slot 0. Nested code
objects inside const tuples reuse the surrounding code's ref space via
the new read_const_value helper.

* Align PYC magic number, FORMAT_VERSION, and header check with CPython 3.14

PYC_MAGIC_NUMBER changes from 2994 to 3627, matching CPython 3.14's
pyc_magic_number_token (0x0a0d0e2b). marshal FORMAT_VERSION drops from
5 to 4 (the encoder/marshal.version value; the decoder already accepts
both). check_pyc_magic_number_bytes now compares all four magic bytes
instead of the first two.

* Add CPython 3.14 .pyc decoding regression tests

Two fixture-based tests pin the marshal decoder against actual CPython
3.14 marshal.dumps() output: a trivial module that exercises FLAG_REF
plus TYPE_REF for qualname, and a module with a nested function that
exercises ref sharing between a const tuple and its surrounding code
object.

* Accept CPython-tagged .pyc as read-only bytecode source

SourceFileLoader.get_code now also looks for .pyc files using
_RP_FALLBACK_CACHE_TAGS (currently ('cpython-314',)) in addition to
sys.implementation.cache_tag. The matched .pyc is only used for
reading; recompilation still writes to the RustPython-tagged path, so
CPython's .pyc is never overwritten. Source-stat / hash / timestamp
validation logic is unchanged.

* Apply rustfmt to marshal helpers

* Marshal PySlice from format version 4 instead of 5

CPython's marshal supports TYPE_SLICE from format version 4 onwards
and that is the default version. Rejecting slice dumps below version
5 made marshal.dumps(slice(...)) fail with the default version and
broke test.test_marshal.SliceTestCase.test_slice.

* Revert "Accept CPython-tagged .pyc as read-only bytecode source"

Lib/importlib/_bootstrap_external.py is CPython's own code copied
verbatim; local patches here defeat compatibility tracking. The
cpython-XX cache_tag fallback needs to live on the RustPython side
(Rust code or sys.implementation.cache_tag policy), not as edits to
the imported standard library.

This reverts commit 1fc426d0fb5fcdb50d35cad13bbb43e8f6ce1c7f.

* Format sys.implementation.cache_tag as cpython-{MAJOR}{MINOR}

Use the CPython compatibility version (e.g. cpython-314) instead of
the rustpython-{MAJOR_IMPL}_{MINOR_IMPL} interpreter version string.

* Set marshal FORMAT_VERSION to 5 to match CPython 3.14.5

Py_MARSHAL_VERSION is 5 in CPython 3.14.5 (Include/marshal.h:16) and
TYPE_SLICE serialization rejects version < 5 (Python/marshal.c:720).
Restore the same threshold and constant so marshal.version and the
slice-marshal gate match CPython.

* Thread marshal recursion depth through nested code objects

Code objects embedded in const-tuples reset the depth budget on each
recursion, so a hostile or pathological marshal stream of code-in-tuple-
in-code can blow the stack despite MAX_MARSHAL_STACK_DEPTH. Pass the
current depth through deserialize_code_inner and read_marshal_const_tuple
and decrement at each code-object/tuple boundary.

Also route dict keys through deserialize_value_after_header so TYPE_CODE
keys decode instead of failing with BadType.
2026-05-24 19:30:42 +09:00
Jeong, YunWon 6440e64355 Align codegen metadata with CPython (#7952) 2026-05-23 20:16:03 +09:00
Shahar Naveh acf12468c4 Clippy nursery lints (#7875) 2026-05-15 10:38:36 +00:00
fanninpm 738d36eb04 Move dependencies to workspace for compiler-core crate (#7874)
* Add dependency from `compiler-core` crate to workspace

* Declare dependency as workspace = true
2026-05-15 16:52:31 +09:00
Shahar Naveh 7b2379dbef Add more pedantic clippy rules (#7830) 2026-05-12 20:57:37 +09:00
Shahar Naveh 542ddd40d9 Autogen instructions & opcodes (#7797) 2026-05-10 22:13:38 +09:00
Shahar Naveh 76128d183f Remove unused rust impl for formatting dis output (#7660)
* Remove unused rust impl for formatting dis output

* remove `examples/dis.rs`

* Added tests

* Update lock

* Try to set snapshot dir

* Remove verbose flag

* Regenerate snapshots after #7711

* Revert "Bump insta from 1.46.3 to 1.47.2 (#7706)"

This reverts commit e6d9ea6bfe.

* Debug info

* Show diff as well

* Show debug faster

* CI: true env

* Recert CI

* Add `CI: true` in ci emv

* Reapply "Bump insta from 1.46.3 to 1.47.2 (#7706)"

This reverts commit 693ca8cbe4d7885a81162a9be31e8bb567db885a.

* simplify macro

* trim on function side

* Force insta workspace root

* fix merge
2026-05-05 08:09:35 +00:00
Shahar Naveh 00fef131bd Opcode name (#7433) 2026-05-04 20:49:47 +09:00
Shahar Naveh 323a45ef07 Enable some pedantic clippy lints (#7764) 2026-05-04 10:25:37 +09:00
Shahar Naveh 5c1a2d7876 AnyInstruction helpers + doc imrovments (#7757) 2026-05-03 00:13:31 +09:00
Shahar Naveh 5ea40b496f Opcode descs not hardcoded (#7682)
* Add `::iterator()` for oparg enums

* adjust `get_special_method_names`

* Adjust intristic function 2 oparg

* Adjust intristic function 1 oparg

* Adjust binary operator oparg
2026-04-25 21:57:02 +09:00
Shahar Naveh f3aee2ce5a oparg_enum! to support custom display value (#7654) 2026-04-23 15:32:01 +09:00
Jeong, YunWon 9ee32a1b38 Bytecode parity - boolop, comprehension, CFG passes (#7631)
* Bytecode parity - boolop, comprehension, CFG passes

- Flatten nested same-op BoolOp and add IfExpr to jump_if
- Simplify is_name_imported to module-level only
- Enable inlined comprehensions in module/class scope
- Add TweakInlinedComprehensionScopes with fast_hidden tracking
- Track fb_range in FBlockInfo for with-statement line info
- Add constant subscript folding and unary Not folding
- Add emit_return_const_no_location for implicit returns
- Use JumpNoInterrupt for ternary/except jumps
- Add STACK_USE_GUIDELINE threshold for collection building
- Reorder CFG pipeline: inline small blocks earlier, resolve
  line numbers before cold block extraction
- Add redirect_empty_unconditional_jump_targets,
  reorder_conditional_chain_and_jump_back_blocks,
  materialize_empty_conditional_exit_targets,
  duplicate_shared_jump_back_targets passes
- Add borrow deoptimization for multi-handler, named-except,
  protected conditional tail, and protected import joins
- Run folding/optimization passes twice around peephole

* Bytecode parity - slice augassign, async comp inline

- Augmented assignment with two-part slices uses BINARY_SLICE/STORE_SLICE
- Inline async comprehensions (remove await/async guards)
- Inlined comprehension cleanup jump uses JumpNoInterrupt
- Class firstlineno uses first decorator line when decorated
- Recurse into nested functions for static attribute collection
- Fold unary positive complex constants (+0.0j)
- Add deoptimize_borrow_for_folded_nonliteral_exprs pass
- Add inline_single_predecessor_artificial_expr_exit_blocks pass
- Skip shared artificial expr-stmt exit blocks in small-block inlining
- Mark folded boolop tail as folded_from_nonliteral_expr

* apply reviews
2026-04-21 00:10:00 +09:00
Jeong, YunWon a04189fa34 Align bytecode codegen structure with CPython 3.14 (#7588)
* Align bytecode codegen structure with CPython 3.14

* Bytecode parity - constant folding, annotation ordering, superinstruction alignment

- Add BoolOp constant folding with short-circuit semantics in compile_expression
- Add constant truthiness evaluation for assert statement optimization
- Disable const collection/boolop folding in starred unpack and assignment contexts
- Move annotation block generation after body with AnnotationsPlaceholder splicing
- Reorder insert_superinstructions to run before push_cold_blocks (matching flowgraph.c)
- Lower LOAD_CLOSURE after superinstructions to avoid false LOAD_FAST_LOAD_FAST
- Add ToBool before PopJumpIf in comparisons and chained compare cleanup blocks
- Unify annotation dict building to always use incremental BuildMap + StoreSubscr
- Add TrueDivide constant folding for integer operands
- Fold constant sets to Frozenset (not Tuple) in try_fold_constant_collection
- Add PyVmBag for frozenset constant materialization in code objects
- Add remove_redundant_const_pop_top_pairs pass and peephole const+branch folding
- Emit Nop for skipped constant expressions and constant-true asserts
- Preserve comprehension local ordering by source-order bound name collection
- Simplify annotation scanning in symboltable (remove simple-name gate)

* Fix CI regressions in marshal and fast-local ops

* impl more

* Align bytecode codegen with CPython structure

* Bytecode parity - comprehension/except scope ordering, load_fast_borrow fixes

- Reorder comprehension symbol-table walk so the outermost iterator
  registers its sub_tables in the enclosing scope before the comp
  scope, and rescan elt/ifs in CPython's order. Codegen peeks past the
  outermost iterator's nested scopes to find the comprehension table.
- For plain try/except, emit handler sub_tables before the else block
  so codegen's linear sub_table cursor stays aligned.
- Rename `collect_simple_annotations` to `collect_annotations` and
  evaluate non-simple annotations during __annotate__ compilation to
  preserve source-order side effects while keeping the simple-name
  index stable.
- Dedupe equivalent code constants in `arg_constant` and add a
  structural equality check on `CodeObject`.
- Disable LOAD_FAST_BORROW for the tail end block when a try has a
  bare `except:` clause, and have `new_block` inherit the flag from
  the current block.
- Remove `cfg!(debug_assertions)` guard around the
  `optimize_load_fast_borrow` start-depth check so mismatches are
  handled (return instead of assert) in release builds.
- Collapse nop-only blocks that precede a return epilogue and hoist
  the prior line number into the next real instruction so the
  line table matches.
- Unmark now-passing `test_consts_in_conditionals`,
  `test_load_fast_unknown_simple`,
  `test_load_fast_known_because_already_loaded`, and PEP 646 f3/f4
  annotation checks.

* Bytecode parity - try/except line tracking, assert 0 shape

- In `compile_try_except`, drop the leading Nop and set the end
  block's source range from the last orelse/body statement so line
  events after the try fall on the right line.
- Recognise constant-false asserts as the direct-raise shape (no
  ToBool/PopJumpIfFalse) and flip the test assertion accordingly.
- Extend `remove_redundant_nops_in_blocks` to also look through a
  trailing nop before a return-epilogue pair (LoadConst/ReturnValue
  or LoadSmallInt/ReturnValue) so the epilogue keeps the correct
  line number.
- Rename `preds` to `predecessor_blocks` in the LOAD_FAST_BORROW
  disable pass and add a test-only `debug_late_cfg_trace` helper.
- Regenerate the `nested_double_async_with` snapshot: the tail
  reference to `stop_exc` now emits LOAD_FAST instead of
  LOAD_FAST_BORROW.

* Bytecode parity - iter folding, break/continue line, cold inlining

- Fold a constant list iterable into a constant tuple in for-loop
  iterable position, matching the CPython optimizer, and strip a
  redundant LIST_TO_TUPLE immediately before GET_ITER in the IR
  peephole pass.
- Emit a Nop at the break/continue source range before unwinding
  so line events land on the break/continue statement instead of
  the following instruction.
- Drop `propagate_disable_load_fast_borrow`; the forward propagation
  was over-zealous and the per-block inheritance in `new_block` plus
  the bare-except marker are enough.
- Relax `inline_small_or_no_lineno_blocks` so small exit blocks at
  the tail of a cold block are always inlined, not just return
  epilogues.
- Add codegen tests covering the LIST_TO_TUPLE/GET_ITER peephole and
  the late-CFG trace helper for a for-loop list-literal iterable.
2026-04-18 09:19:11 +09:00
Shahar Naveh 11493eb56b Add const methods for oparg enums (#7617)
* Add const methods for oparg enums

* Shorten doc link
2026-04-17 20:06:30 +09:00
Shahar Naveh 930a07b7df Constify OpArgState methods (#7616) 2026-04-17 20:05:37 +09:00
Shahar Naveh 8d1000d485 Simplify Intruction.deopt() (#7615)
* Simplify `Instruction.deopt()`

* Adjust `scripts/generate_opcode_metadata.py`
2026-04-17 20:04:22 +09:00
Shahar Naveh e52e698d36 Simplify cache_entries match statement (#7613)
* Simplify cache_entries method

* Use `Opcode`
2026-04-16 08:52:11 +09:00
Shahar Naveh 63fa928074 Macro for defining opcode & instruction enums (#7573)
* Macro for defining opcode & instruction enums

* Convert `Instruction` as well

* revert `#[repr(...)]` changes
2026-04-16 03:18:14 +09:00
Jeong, YunWon 030da76d4e Add InstructionMetadata::stack_effect_jump for branch stack effects (#7585)
* Add InstructionMetadata::stack_effect_jump for branch stack effects

CPython's compile.c provides stack_effect(opcode, oparg, jump) where the
jump parameter selects between fallthrough and branch effects. The existing
stack_effect() only returns the fallthrough effect.

Add stack_effect_jump() that returns the branch effect. Most instructions
have identical fallthrough/branch effects; ForIter and Send are the
exceptions (ForIter: fallthrough=+1, branch=-1; Send: fallthrough=0,
branch=-1).

* apply review
2026-04-13 17:25:26 +09:00
Jeong, YunWon 7abf8cc247 Bytecode parity - for-loop cleanup, return duplication, late jump threading fix (#7580)
- Use POP_TOP instead of POP_ITER for for-loop break/return cleanup
- Expand duplicate_end_returns to clone final return for jump predecessors
- Restrict late jump threading pass to unconditional jumps only
- Skip exception blocks in inline/reorder passes
- Simplify threaded_jump_instr NoInterrupt handling
2026-04-12 22:43:35 +09:00
Jeong, YunWon 501ff88aed Improve codegen bytecode parity (#7541)
- Add CFG block splitting, jump threading, backward jump normalization
- Add genexpr StopIteration wrapper
- Add ConstantData::Slice and constant slice folding
- Add duplicate_exits_without_lineno and Block: Clone
- Add builtin(genexpr) optimization for tuple/list/set/all/any
- Add compile_try_except_no_finally for try-except without finally
- Add module_name_declared_global_in_nested_scope
- Add constant tuple folding in try_fold_constant_expr
- Add fstring literal-only optimization and empty literal elision
- Fix duplicate_exits_without_lineno: splice new blocks into linked list
2026-03-31 15:45:18 +09:00
Shahar Naveh dbf64f873f Remove oparg builders (#7537) 2026-03-30 18:53:13 +09:00
Jeong, YunWon 6f98ae3f79 Bytecode parity (#7535)
* Add CFG block splitting, jump threading, backward jump normalization, genexpr StopIteration wrapper

- split_blocks_at_jumps: split blocks at branch points so each has one exit
- jump_threading: thread jumps through single-jump blocks (flowgraph.c jump_thread)
- Backward conditional jump normalization: invert and create NOT_TAKEN+JUMP block
- Follow empty blocks in jump-to-return optimization (next_nonempty_block)
- Add PEP 479 StopIteration handler to compile_comprehension for generators

* Add ConstantData::Slice and constant slice folding

- Add Slice variant to ConstantData and BorrowedConstant
- Fold constant slices (x[:3], x[1:4]) into LOAD_CONST(slice(...))
- Marshal serialization/deserialization for Slice type
- Box::leak in borrow_obj_constant for PySlice roundtrip

* Add ConstantData::Frozenset variant (type only, folding deferred)

Add Frozenset to ConstantData, BorrowedConstant, and marshal support.
Actual frozenset folding (BUILD_SET + CONTAINS_OP → LOAD_CONST frozenset)
requires VirtualMachine for element hashing and is deferred.

* Add duplicate_exits_without_lineno (disabled) and Block: Clone

Prepare infrastructure for exit block duplication optimization.
Currently disabled pending stackdepth integration.
2026-03-30 12:52:04 +09:00
Jeong, YunWon cbdc7bb587 Compiler parity: docstring dedent, StopIteration wrapper, constant folding (#7530) 2026-03-29 22:36:20 +09:00
Shahar Naveh a4e7b89b8d Oparg resume depth (#7515)
* Base resume context

* Fixes for api change

* Align codegen

* Align `frame.rs` to the api changes

* fix jit

* Use new oparg

* Fix doc

* let `ir` to decide exception depth
2026-03-27 21:47:52 +09:00
Jeong, YunWon 1bf829a456 Bytecode parity (#7504)
* Match CPython LOAD_SPECIAL stack semantics for with/async-with

LOAD_SPECIAL now pushes (callable, self_or_null) matching CPython's
CALL convention, instead of a single bound method:
- Function descriptors: push (func, self)
- Plain attributes: push (bound, NULL)

Updated all with-statement paths:
- Entry: add SWAP 3 after SWAP 2, remove PUSH_NULL before CALL 0
- Normal exit: remove PUSH_NULL before CALL 3
- Exception handler (WITH_EXCEPT_START): read exit_func at TOS-4
  and self_or_null at TOS-3
- Suppress block: 3 POP_TOPs after POP_EXCEPT (was 2)
- FBlock exit (preserve_tos): SWAP 3 + SWAP 2 rotation
- UnwindAction::With: remove PUSH_NULL

Stack effects updated: LoadSpecial (2,1), WithExceptStart (7,6)

* Normalize LOAD_FAST_CHECK and JUMP_BACKWARD_NO_INTERRUPT

Add LOAD_FAST_CHECK → LOAD_FAST and JUMP_BACKWARD_NO_INTERRUPT →
JUMP_BACKWARD to opname normalization in dis_dump.py. These are
optimization variants with identical semantics.

* Add EXTENDED_ARG to SKIP_OPS, normalize LOAD_FAST_CHECK and JUMP_BACKWARD_NO_INTERRUPT

* Remove duplicate return-None when block already has return

Skip duplicate_end_returns for blocks that already end with
LOAD_CONST + RETURN_VALUE. Run DCE + unreachable elimination
after duplication to remove the now-unreachable original return
block.

* Improve __static_attributes__ collection accuracy

- Support tuple/list unpacking targets: (self.x, self.y) = val
- Skip @staticmethod and @classmethod decorated methods
- Use scan_target_for_attrs helper for recursive target scanning

* Use method mode for function-local import attribute calls

Function-local imports (scope is Local+IMPORTED) should use method
mode LOAD_ATTR like regular names, not plain mode. Only module/class
scope imports use plain LOAD_ATTR + PUSH_NULL.

* Optimize constant iterable before GET_ITER to LOAD_CONST tuple

Convert BUILD_LIST/SET 0 + LOAD_CONST + LIST_EXTEND/SET_UPDATE + GET_ITER
to just LOAD_CONST (tuple) + GET_ITER, matching CPython's optimization
for constant list/set literals in for-loop iterables.

Also fix is_name_imported to use method mode for function-local imports,
and improve __static_attributes__ accuracy (skip @classmethod/@staticmethod,
handle tuple/list unpacking targets).

* Fix cell variable ordering: parameters first, then alphabetical

CPython orders cell variables with parameter cells first (in
parameter definition order), then non-parameter cells sorted
alphabetically. Previously all cells were sorted alphabetically.

Also add for-loop iterable optimization: constant BUILD_LIST/SET
before GET_ITER is folded to just LOAD_CONST tuple.

* Emit COPY_FREE_VARS before MAKE_CELL matching CPython order

CPython emits COPY_FREE_VARS first, then MAKE_CELL instructions.
Previously RustPython emitted them in reverse order.

* Fix RESUME AfterYield encoding to match CPython 3.14 (value 5)

CPython 3.14 uses RESUME arg=5 for after-yield, not 1.
Also reorder COPY_FREE_VARS before MAKE_CELL and fix cell
variable ordering (parameters first, then alphabetical).

* Address code review feedback from #7481

- Set is_generator flag for generator expressions in scan_comprehension
- Fix posonlyargs priority in collect_static_attributes first param
- Add match statement support to scan_store_attrs
- Fix stale decorator stack comment
- Reorder NOP removal after fold_unary_negative for better collection folding

* Fold constant list/set/tuple literals in compiler

When all elements of a list/set/tuple literal are constants and
there are 3+ elements, fold them into a single constant:
- list: BUILD_LIST 0 + LOAD_CONST (tuple) + LIST_EXTEND 1
- set:  BUILD_SET 0  + LOAD_CONST (tuple) + SET_UPDATE 1
- tuple: LOAD_CONST (tuple)

This matches CPython's compiler optimization and fixes the most
common bytecode difference (92/200 sampled files).

Also add bytecode comparison scripts (dis_dump.py, compare_bytecode.py)
for systematic parity tracking.

* Use BUILD_MAP 0 + MAP_ADD for large dicts (>= 16 pairs)

Match CPython's compiler behavior: dicts with 16+ key-value pairs
use BUILD_MAP 0 followed by MAP_ADD for each pair, instead of
pushing all keys/values on the stack and calling BUILD_MAP N.

* Fix clippy warnings and cargo fmt

* fix surrogate
2026-03-25 22:25:21 +09:00
dependabot[bot] 8e67a15f41 Bump lz4_flex from 0.12.1 to 0.13.0 (#7497)
Bumps [lz4_flex](https://github.com/pseitz/lz4_flex) from 0.12.1 to 0.13.0.
- [Release notes](https://github.com/pseitz/lz4_flex/releases)
- [Changelog](https://github.com/PSeitz/lz4_flex/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pseitz/lz4_flex/compare/0.12.1...0.13.0)

---
updated-dependencies:
- dependency-name: lz4_flex
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 17:51:11 +09:00
Jeong, YunWon 98dba36af5 marshal (#7467)
* CPython-compatible marshal format

Unify marshal to a single CPython-compatible format. No separate
"cpython_marshal" reader — one format for frozen modules, .pyc
files, and the Python-level marshal module.

- ComparisonOperator: `(cmp_index << 5) | mask` matching COMPARE_OP
- MakeFunctionFlag: bit-position matching SET_FUNCTION_ATTRIBUTE
- Exception table varint: big-endian (matching Python/assemble.c)
- Linetable varint: little-endian (unchanged)
- Integer: TYPE_INT (i32) / TYPE_LONG (base-2^15 digits)
- Code objects: CPython field order (argcount, posonlyargcount, ...,
  co_localsplusnames, co_localspluskinds, ..., co_exceptiontable)

- FLAG_REF / TYPE_REF for object deduplication (version >= 3)
- allow_code keyword argument on dumps/loads/dump/load
- Subclass rejection (int/float/complex/tuple/list/dict/set/frozenset)
- Slice serialization (version >= 5)
- Buffer protocol fallback for memoryview/array
- Recursion depth limit (2000) for both reads and writes
- Streaming load (reads one object, seeks file position)
- TYPE_INT64, TYPE_FLOAT (text), TYPE_COMPLEX (text) for compat

serialize_code writes co_localsplusnames/co_localspluskinds from
split varnames/cellvars/freevars. deserialize_code splits them back.
Cell variable DEREF indices are translated between flat (wire) and
cell-relative (internal) representations in both directions.

Replace bitwise trick with match for new ComparisonOperator values.

21 -> 3 expected failures. Remaining: test_bad_reader (IO layer),
test_deterministic_sets (PYTHONHASHSEED), testIntern (string interning).

* Address code review: preserve CO_FAST_HIDDEN, fix varint overflow

- Use original localspluskinds from marshal data instead of
  rebuilding, preserving CO_FAST_HIDDEN and other flags
- Fix write_varint_be to handle values >= 2^30 (add 6th chunk)
- Remove unused build_localspluskinds_from_split

* Add depth guard to deserialize_value_typed

Prevents usize underflow when dict key deserialization path calls
deserialize_value_typed with depth=0 on composite types.
2026-03-23 13:10:51 +09:00
Jeong, YunWon bef2e41295 Bytecode parity (#7475)
* Emit TO_BOOL before conditional jumps, fix class/module prologue

- Emit TO_BOOL before POP_JUMP_IF_TRUE/FALSE in the general case
  of compile_jump_if (Compare expressions excluded since they
  already produce a bool)
- Module-level __doc__: use STORE_NAME instead of STORE_GLOBAL
- Class body __module__: use LOAD_NAME instead of LOAD_GLOBAL
- Class body: store __firstlineno__ before __doc__

* Emit MAKE_CELL and COPY_FREE_VARS before RESUME

Emit MAKE_CELL for each cell variable and COPY_FREE_VARS N for
free variables at the start of each code object, before RESUME.
These instructions are no-ops in the VM but align the bytecode
with CPython 3.14's output.

* Emit __static_attributes__ at end of class bodies

Store a tuple of attribute names (currently always empty) as
__static_attributes__ in the class namespace, matching CPython
3.14's class body epilogue. Attribute name collection from
self.xxx accesses is a follow-up task.

* Remove expectedFailure from DictProxyTests iter tests

test_iter_keys, test_iter_values, test_iter_items now pass
because class bodies emit __static_attributes__ and
__firstlineno__, matching the expected dict key set.

* Use 1-based stack indexing for LIST_EXTEND, SET_UPDATE, etc.

Switch LIST_APPEND, LIST_EXTEND, SET_ADD, SET_UPDATE, MAP_ADD
from 0-based to 1-based stack depth argument, matching CPython's
PEEK(oparg) convention. Adjust the VM to subtract 1 before
calling nth_value.

* Use plain LOAD_ATTR + PUSH_NULL for calls on imported names

When the call target is an attribute of an imported name (e.g.,
logging.getLogger()), use plain LOAD_ATTR (method_flag=0) with
a separate PUSH_NULL instead of method-mode LOAD_ATTR. This
matches CPython 3.14's behavior which avoids the method call
optimization for module attribute access.

* Duplicate return-None epilogue for fall-through blocks

When the last block in a code object is exactly LOAD_CONST None +
RETURN_VALUE (the implicit return), duplicate these instructions
into blocks that would otherwise fall through to it. This matches
CPython 3.14's behavior of giving each code path its own explicit
return instruction.

* Run cargo fmt on ir.rs

* Remove expectedFailure from test_intrinsic_1 in test_dis

* Emit TO_BOOL before conditional jumps for all expressions including Compare

* Add __classdict__ cell for classes with function definitions

Set needs_classdict=true for class scopes that contain function
definitions (def/async def), matching CPython 3.14's behavior for
PEP 649 deferred annotation support. Also restore the Compare
expression check in compile_jump_if to skip TO_BOOL for comparison
operations.

* Emit __classdictcell__ store in class body epilogue

Store the __classdict__ cell reference as __classdictcell__ in
the class namespace when the class has __classdict__ as a cell
variable. Uses LOAD_DEREF (RustPython separates cell vars from
fast locals unlike CPython's unified array).

* Always run DCE to remove dead code after terminal instructions

Run basic dead code elimination (truncating instructions after
RETURN_VALUE/RAISE/JUMP within blocks) at all optimization
levels, not just optimize > 0. CPython always removes this dead
code during assembly.

* Restrict LOAD_ATTR plain mode to module/class scope imports

Only use plain LOAD_ATTR + PUSH_NULL for imports at module or
class scope. Function-local imports use method call mode LOAD_ATTR,
matching CPython 3.14's behavior.

* Eliminate unreachable blocks after jump normalization

Split DCE into two phases: (1) within-block truncation after
terminal instructions (always runs), (2) whole-block elimination
for blocks only reachable via fall-through from terminal blocks
(runs after normalize_jumps when dead jump instructions exist).

* Fold BUILD_TUPLE 0 into LOAD_CONST empty tuple

Convert BUILD_TUPLE with size 0 to LOAD_CONST () during constant
folding, matching CPython's optimization for empty tuple literals.

* Handle __classcell__ and __classdictcell__ in type.__new__

- Remove __classcell__ from class dict after setting the cell value
- Add __classdictcell__ handling: set cell to class namespace dict,
  then remove from class dict
- Register __classdictcell__ identifier
- Use LoadClosure instead of LoadDeref for __classdictcell__ emission
- Reorder MakeFunctionFlag bits to match CPython
- Run ruff format on scripts

* Revert __classdict__ cell and __classdictcell__ changes

The __classdict__ cell addition (for classes with function defs)
and __classdictcell__ store caused cell initialization failures
in importlib. These require deeper VM changes to properly support
the cell variable lifecycle. Reverted for stability.

* Fix unreachable block elimination with fixpoint reachability

Use fixpoint iteration to properly determine block reachability:
only mark jump targets of already-reachable blocks, preventing
orphaned blocks from falsely marking their targets as reachable.
Also add a final DCE pass after assembly NOP removal to catch
dead code created by normalize_jumps.

* Check enclosing scopes for IMPORTED flag in LOAD_ATTR mode

When deciding whether to use plain LOAD_ATTR for attribute calls,
check if the name is imported in any enclosing scope (not just
the current scope). This handles the common pattern where a module
is imported at module level but used inside functions.

* Add __classdict__ cell for classes with function definitions

Set needs_classdict=true when a class scope contains function
definitions (def/async def), matching CPython 3.14 which always
creates a __classdict__ cell for PEP 649 support in such classes.

* Store __classdictcell__ in class body epilogue

Store the __classdict__ cell reference as __classdictcell__ in
the class namespace using LoadClosure (which loads the cell
object itself, not the value inside). This matches CPython 3.14's
class body epilogue.

* Fix clippy collapsible_if warnings and cargo fmt

* Revert __classdict__ and __classdictcell__ changes (cause import failures)

* Revert type.__new__ __classcell__ removal and __classdictcell__ handling

Revert the class cell cleanup changes from e6975f973 that cause
import failures when frozen module bytecode is stale. The original
behavior (not removing __classcell__ from class dict) is restored.

* Re-add __classdict__ cell and __classdictcell__ store

Restore the __classdict__ cell for classes with function
definitions and __classdictcell__ store in class body epilogue.
Previous failure was caused by stale .pyc cache files containing
bytecode from an intermediate MakeFunctionFlag reorder attempt,
not by these changes themselves.

* Reorder MakeFunctionFlag to match CPython's SET_FUNCTION_ATTRIBUTE

Reorder discriminants: Defaults=0, KwOnlyDefaults=1, Annotations=2,
Closure=3, Annotate=4, TypeParams=5. This aligns the oparg values
with CPython 3.14's convention.

Note: after this change, stale .pyc cache files must be deleted
(find . -name '*.pyc' -delete) to avoid bytecode mismatch errors.

* Use CPython-compatible power-of-two encoding for SET_FUNCTION_ATTRIBUTE

Override From/TryFrom for MakeFunctionFlag to use power-of-two
values (1,2,4,8,16,32) matching CPython's SET_FUNCTION_ATTRIBUTE
oparg encoding, instead of sequential discriminants (0,1,2,3,4,5).

* Remove expectedFailure from test_elim_jump_after_return1 and test_no_jump_over_return_out_of_finally_block

* Remove __classcell__ and __classdictcell__ from class dict in type.__new__

* Remove expectedFailure from test___classcell___expected_behaviour, cargo fmt

* Handle MakeCell and CopyFreeVars as no-ops in JIT

These prologue instructions are handled at frame creation time
by the VM. The JIT operates on already-initialized frames, so
these can be safely skipped during compilation.

* Remove expectedFailure from test_load_fast_known_simple

* Restore expectedFailure for test_load_fast_known_simple

The test expects LOAD_FAST_BORROW_LOAD_FAST_BORROW superinstruction
which RustPython does not emit yet.
2026-03-23 11:31:30 +09:00
Jeong, YunWon fb9cafa8c9 Enable PEP 709 inlined comprehensions (#7412)
* Enable PEP 709 inlined comprehensions for function-like scopes

Activate the existing compile_inlined_comprehension() implementation
by fixing 6 bugs that prevented it from working:

- LoadFastAndClear: push NULL (not None) when slot is empty so
  StoreFast can restore empty state after comprehension
- StoreFast: accept NULL from stack for the restore path
- sub_tables.remove(0) replaced with next_sub_table cursor to
  match the pattern used elsewhere in the compiler
- in_inlined_comp flag moved from non-inlined to inlined path
- is_inlined_comprehension_context() now checks comp_inlined flag
  and restricts inlining to function-like scopes
- comp_inlined set only when parent scope uses fastlocals

Symbol table analysis handles conflict detection:
- Nested scopes in comprehension → skip inlining
- Bound name conflicts with parent symbol → skip inlining
- Cross-comprehension reference conflicts → skip inlining
- Splice comprehension sub_tables into parent for nested scope tracking

* Add localspluskinds, unify DEREF to localsplus index

- Add CO_FAST_LOCAL/CELL/FREE/HIDDEN constants and
  localspluskinds field to CodeObject for per-slot metadata
- Change DEREF instruction opargs from cell-relative indices
  (NameIdx) to localsplus absolute indices (oparg::VarNum)
- Add fixup_deref_opargs pass in ir.rs to convert cell-relative
  indices to localsplus indices after finalization
- Replace get_cell_name with get_localsplus_name in
  InstrDisplayContext trait
- Update VM cell_ref/get_cell_contents/set_cell_contents to use
  localsplus indices directly (no nlocals offset)
- Update function.rs cell2arg, super.rs __class__ lookup with
  explicit nlocals offsets

* Fix clippy warnings, formatting, restore _opcode_metadata.py

Fix cast_possible_truncation, nonminimal_bool, collapsible_if,
manual_contains clippy lints. Restore _opcode_metadata.py to
upstream/main version (3.14 aligned).

Pre-copy closure cells in Frame::new for coroutine locals().
Handle raw values in merged cell slots during inlined comps.
Exclude async comprehensions from inlining path.

* Exclude async/await comprehensions from PEP 709 inlining in symboltable

Async comprehensions and comprehensions with await in the element
expression need their own coroutine scope and cannot be inlined.
The symboltable builder was not checking these conditions, causing
incorrect symbol scope resolution when an async comprehension was
nested inside an inlined comprehension (e.g. [[x async for x in g]
for j in items]).
2026-03-21 22:48:35 +09:00
Shahar Naveh 33b024b70e Resume opcode to hold ResumeType (#7465) 2026-03-19 14:24:07 +00:00
Shahar Naveh 3069d06851 Align _opcode_metadata.py to 3.14.3 (#7456)
* Align `_opcode_metadata.py` to 3.14.3

* Unmark passing test

* Ensure python 3.14 runs on CI

* Update banner

* Fix `test__opcode.py`

* Adjust generate script

* Fix docs
2026-03-18 22:09:00 +09:00
Jeong, YunWon 8a05c5ca94 Fix test_code: compiler and code object improvements
- Add CO_NESTED flag (0x10) for nested function scopes
- Emit LOAD_SMALL_INT for integers 0..=255 instead of LOAD_CONST
- Eliminate dead constant expression statements (no side effects)
- Ensure None in co_consts for functions with no other constants
- Add code.__replace__() for copy.replace() support
- Mark test_co_lnotab and test_invalid_bytecode as expectedFailure
2026-03-17 10:54:12 +09:00
Jeong, YunWon 33ef1b9057 Implement code object __eq__/__hash__
- Add Comparable and Hashable traits for PyCode
- Compare by name, args, flags, bytecode, consts, names, vars, linetable
- Hash by tuple of key attributes matching CPython's code_hash
- Remove unused custom_ops slice in Instruction::try_from
- Add co_lnotab intentional non-implementation comment
2026-03-16 15:25:29 +09:00
Shahar Naveh 92135de258 Newtype var_nums oparg (#7431) 2026-03-15 17:01:03 +09:00
Jeong, YunWon 13a0623eb1 Adapt bitflagset to use enum (#7419) 2026-03-14 09:44:02 +09:00
Shahar Naveh 6c8f19e0d2 Newtype oparg align methods (#7403)
* Align methods for newtype opargs
2026-03-12 23:41:21 +09:00