Skip to content

fix(stl_bind): correct __delitem__ for negative-step slices and re-enable contiguous erase fast path#6088

Draft
henryiii wants to merge 2 commits into
pybind:masterfrom
henryiii:fix/stl-bind-delitem-negative-step
Draft

fix(stl_bind): correct __delitem__ for negative-step slices and re-enable contiguous erase fast path#6088
henryiii wants to merge 2 commits into
pybind:masterfrom
henryiii:fix/stl-bind-delitem-negative-step

Conversation

@henryiii

Copy link
Copy Markdown
Collaborator

🤖 AI text below 🤖

Bug

bind_vector's __delitem__(slice) in stl_bind.h advanced the erase
index by step - 1 for every slice, but that correction is only valid
for positive steps (where erasing an element shifts the later
elements down by one). For negative steps the visited indices are
strictly decreasing, so erasing never shifts them and the - 1 is
wrong:

  • del v[::-2] on [0, 1, 2, 3] produced [1, 2] instead of the
    correct [0, 2] (silent data corruption).
  • del v[::-1] ended up calling v.erase(v.begin() - 1), an
    out-of-bounds access (observed SIGBUS).

Separately, the contiguous fast path was guarded by step == 1 && false
— a debugging artifact from 2016 (25c03ce) — so a contiguous delete
like del v[1:1000] did 999 individual erase calls instead of one
range erase.

Fix

  • Use the signed slice::compute overload so negative steps stay signed
    instead of arriving as wrapped size_t values.
  • Advance by step for negative steps and step - 1 for positive
    steps.
  • Drop the dead && false, restoring the O(n) contiguous range erase for
    step == 1.

Semantics now match CPython del l[slice] exactly for forward,
strided, negative, empty, and full slices.

Tests

Added test_vector_delitem_slice in tests/test_stl_binders.py,
looping over forward / strided / negative-step / empty / full slices for
vectors of several lengths and comparing against the equivalent Python
list del.

Verification

Built a scratch opaque VectorInt module against this branch's headers
and compared del v[...] to Python list semantics across all the slice
cases above (and more) for lengths 0–11 — all match, no out-of-bounds
access.

Part of #6084

…able contiguous erase fast path

The slice __delitem__ binding advanced the erase index by step - 1 for
all steps. That correction is only valid for positive steps, where
erasing shifts later elements down by one. For negative steps the
visited indices are strictly decreasing and erasing never shifts them,
so the extra -1 deleted the wrong elements (e.g. del v[::-2] on
[0,1,2,3] yielded [1,2] instead of [0,2]) and del v[::-1] walked off the
front of the vector (v.begin() - 1, observed SIGBUS).

Switch to the signed slice::compute overload so negative steps stay
signed, advance by step for negative steps and step - 1 for positive
ones, and drop the && false that had disabled the O(n) contiguous fast
path since 2016.

Assisted-by: ClaudeCode:claude-fable-5
Comment thread include/pybind11/stl_bind.h Outdated
ssize_t start = 0, stop = 0, step = 0, slicelength = 0;

if (!slice.compute(v.size(), &start, &stop, &step, &slicelength)) {
if (!slice.compute((ssize_t) v.size(), &start, &stop, &step, &slicelength)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use Cpp style static cast?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI text below 🤖

Done — switched to static_cast<ssize_t>(v.size()) in e5c1c6d.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, it doesn't ask me sometimes. Answer is yes. :)

Comment thread tests/test_stl_binders.py Outdated
assert len(v_int2) == 0


def test_vector_delitem_slice():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not invert the for loop and just pytest parameterize?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI text below 🤖

Done — the test is now parametrized over the slice cases with the size loop inside, in e5c1c6d.

Use static_cast instead of a C-style cast for the slice.compute() size
argument, and convert the __delitem__ slice test to
pytest.mark.parametrize over the slice cases.

Assisted-by: ClaudeCode:claude-fable-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants