Skip to content

pypy parity: index/slice coercion, sequence concat, in-place operators, overflow guards#560

Open
youknowone wants to merge 6 commits into
mainfrom
rename
Open

pypy parity: index/slice coercion, sequence concat, in-place operators, overflow guards#560
youknowone wants to merge 6 commits into
mainfrom
rename

Conversation

@youknowone

@youknowone youknowone commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Drains the remaining low-risk pypy-parity items from the 3-way (cpython/pypy/pyre) sweep — the integer-coercion, sequence-concatenation, in-place-operator, and numeric-overflow paths. Each behavior was probed against the installed pypy3 (7.3.20, the check.py oracle) and now matches byte-for-byte. Where cpython and pypy disagree (so a check.py baseline test cannot cover it), the behavior was verified by hand against pypy3.

objspace: remap __index__ TypeError; ord names bytes noun

Carries over the two PR #552 review fixes that had not yet landed:

  • getindex_w remaps a TypeError raised by a subscript's __index__ (a non-int return, or a TypeError from inside __index__) to <descr> indices must be integers or slices, not '<type>'; a ValueError still propagates. Applied to the list/tuple/str/bytes get, list/bytearray set, and delitem sites.
  • ord(b"ab") reports bytes of length 2 while bytearray(b"ab") keeps string of length 2.

pypy parity: index coercion and sequence-concat errors

  • builtins::getindex_w — an out-of-word overflow now clamps to i64::MIN for a negative value (was always i64::MAX), so a large-negative slice bound floors to 0 instead of collapsing to the tail: [1,2,3][-(10**30):][1, 2, 3].
  • normalize_slice / sliceobject::adapt_lower_bound — fold a negative index by the length with saturating_add so an i64::MIN bound does not overflow before flooring at 0.
  • str_slice_args (startswith/endswith) — take the start/end bounds through adapt_lower_bound(eval_slice_index(...)), so a non-index bound raises slice indices must be integers or None or have an __index__ method, an __index__ object is honoured, and a positive bound is not upper-clamped.
  • builtin_round — take ndigits through getindex_w (float: clamping; int: space.index); a huge-magnitude negative ndigits short-circuits to 0 rather than building an astronomical power of ten (round(3.14159, 2**63)3.14159).
  • index_to_bigint (hex/oct/bin) — coerce through space.index so a non-int __index__ return raises __index__ returned non-int (type X).
  • formatting number_arg_decimal / number_arg_integer (%d/%i/%o/%x) — remap a TypeError from the numeric decoder to the operand-type error naming the original argument (%d format: a real number is required, not BadIdx).
  • list/tuple/bytes/bytearray __add__ and descroperation::add — return NotImplemented for a non-sequence operand so the operator raises the generic unsupported operand type(s) for +, dropping the cpython-only can only concatenate message; bytearray.__iadd__ on a non-buffer now raises a bytes-like object is required, not 'X'.

fix warnings

Removes three unused-binding warnings in the rtyper (classdesc.rs, rclass.rs, rpbc.rs).

operator/set: in-place operator functions and set slots

  • The operator module gains the in-place functions iadd, isub, imul, imatmul, ifloordiv, imod, itruediv, ipow, ilshift, irshift, iand, ior, ixor (each dispatching the matching in-place binary op) and iconcat (which requires both operands to be subscriptable, else raises 'X' object can't be concatenated). The __iadd__iadd alias table now resolves to real callables.
  • set gains the __isub__, __iand__, __ior__, __ixor__ slots, so s &= t and operator.iand(s, t) update in place; a non-set right operand yields NotImplemented.

int: raise MemoryError from an unallocatably-large left shift

  • 1 << (10**18) drove the big-int << into the infallible global allocator and aborted the process. A new checked_bigint_lshift pre-flights a fallible reservation of the result's 64-bit limb count and raises a catchable MemoryError instead; int and long left shifts route through it. The shift count is carried as u64 so a 32-bit usize target (wasm) does not truncate a 10**18 shift.

float/complex pow: over-range int operand overflows

  • A float/complex power coerces each int operand to a double before the power is computed, so an over-range int base or exponent raises OverflowError up front — even 1.0 ** huge, which never reaches the arithmetic, matching float(huge). In float.__pow__/__rpow__ the check runs before the ternary-modulus rejection, so pow(2.0, huge, 5) raises OverflowError from the coercion rather than TypeError from the modulus.

Verification

  • Probes (custom __index__ objects, huge/negative bounds, non-seq concat, %-format, in-place set/operator ops, 1 << 10**18, over-range float/complex pow) are byte-identical to pypy3 across all cases; the cpython≠pypy edges (3-arg pow ordering, lshift OOM message) were hand-verified against pypy3.
  • Normal slice / concat / hex / round / %-format / startswith / in-place-op / small-shift / ordinary-pow paths are byte-identical to pypy3 (no regression).
  • New synth tests: operator_set_inplace_ops, int_lshift_memoryerror, float_pow_overflow_exp (each restricted to cpython==pypy cases).
  • check.py: dynasm 190/190, cranelift 190/190, wasm 189/189 (3/3 backends).

Deferred (own follow-up slices)

  • int.__lshift__('a', 1) returns NotImplemented where pypy raises TypeError "'int' object expected, got 'str' instead" — needs a systematic unbound-descriptor self-type check across every typed-self builtin; cpython≠pypy so not baseline-testable.
  • x &= y augmented-assignment error symbol — a type mismatch reports the binary-op symbol & rather than &= (e.g. int += str). Needs an Option-returning binop path so the augmented-assign layer can re-emit with the X= symbol.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e2ef7dbc-d446-46e7-b766-e57595b804b0

📥 Commits

Reviewing files that changed from the base of the PR and between 048978b and 35081d2.

📒 Files selected for processing (14)
  • majit/majit-translate/src/annotator/classdesc.rs
  • majit/majit-translate/src/translator/rtyper/rclass.rs
  • majit/majit-translate/src/translator/rtyper/rpbc.rs
  • pyre/bench/synth/float_pow_overflow_exp.py
  • pyre/bench/synth/int_lshift_memoryerror.py
  • pyre/bench/synth/operator_set_inplace_ops.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/module/operator/mod.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.rs
  • pyre/pyre-interpreter/src/objspace/std/formatting.rs
  • pyre/pyre-interpreter/src/sliceobject.rs
  • pyre/pyre-interpreter/src/type_methods.rs
  • pyre/pyre-interpreter/src/typedef.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rename

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f625bb1929

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

} else {
(v as usize).min(char_len as usize)
}
crate::sliceobject::adapt_lower_bound(char_len, args[3])?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return false for out-of-range empty-prefix checks

When start and end are both supplied past the string length, leaving this positive end unbounded makes start <= end, and the later nth() calls map both offsets to bytes.len(), producing an empty window. That makes cases such as 'abc'.startswith('', 5, 10) and endswith('', 5, 10) return True, while PyPy's unicodeobject.py:_unwrap_and_compute_idx_params forces start_index = end_index + 1 when start > length, so the match remains False even for an empty prefix. Clamp end to char_len or otherwise preserve the out-of-range-start sentinel before comparing/building the window.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 15bdca6).

Files in the reviewed diff
majit/majit-translate/src/annotator/classdesc.rs
majit/majit-translate/src/translator/rtyper/rclass.rs
majit/majit-translate/src/translator/rtyper/rpbc.rs
pyre/bench/synth/float_pow_overflow_exp.py
pyre/bench/synth/int_lshift_memoryerror.py
pyre/bench/synth/operator_set_inplace_ops.py
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/module/operator/mod.rs
pyre/pyre-interpreter/src/objspace/descroperation.rs
pyre/pyre-interpreter/src/objspace/std/formatting.rs
pyre/pyre-interpreter/src/sliceobject.rs
pyre/pyre-interpreter/src/type_methods.rs
pyre/pyre-interpreter/src/typedef.rs

1. Regressions to PyPy parity introduced by this patch

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-interpreter/src/typedef.rs:13917pypy/objspace/std/setobject.py:315: intersection_update re-runs collect_iterable(other) once per element of self; a generator is therefore consumed incrementally and can produce an order-dependent wrong intersection. PyPy materializes each non-set operand into a set once before applying intersect_update. This logic was moved unchanged from main.

  • pyre/pyre-interpreter/src/typedef.rs:13943pypy/objspace/std/setobject.py:497: symmetric_difference_update toggles each yielded item directly, so duplicate iterable elements cancel each other ({1}.symmetric_difference_update([2, 2]) leaves {1}). PyPy first converts the operand to a set, then computes the symmetric difference, producing {1, 2}. This logic was moved unchanged from main.

4. Structural adaptations

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/b1c7ed4a8a77f9e41a9c240c7a5fb4f58d231850/pyre-interpreter/src/objspace/descroperation.rs#L92-L93
P2 Badge Preserve zero left shifts before reserving

When the left operand is zero and the shift count is huge but nonnegative, Python returns zero without needing to allocate the shifted magnitude (e.g. 0 << 10**18). This helper now sizes the reservation from the shift count before checking the value, so both the int and long paths that call it can raise MemoryError for a valid zero result; special-case a zero a before computing/reserving limbs.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

getindex_w (baseobjspace.py:1574) remaps a TypeError raised while coercing a
subscript key through __index__ — a non-int __index__ return, or a TypeError
raised inside __index__ — to "<descr> indices must be integers or slices, not
'<type>'" whenever objdescr is set. The inlined subscript coercions
(list/tuple/str/bytes __getitem__, list/bytearray __setitem__, and
subscript_index_w on the __delitem__ path) previously let space.index's
"__index__ returned non-int (type X)" propagate verbatim. A ValueError from
__index__ still propagates; list.insert / list.pop pass no objdescr and keep
surfacing the raw error.

ord() on a multi-byte bytes argument now reports "bytes of length N"
(bytesobject.py:473); bytearray keeps "string of length N"
(bytearrayobject.py:213) — the shared branch reported "string" for both.

Comment-only: pow3 notes the omitted is_cpytype() pow3_bug_compat_cpyext
branch; index_type_error notes the reference pypy3 quotes the %T operand.

Assisted-by: Claude
Route integer coercion through the index protocol and align
sequence-concatenation errors with pypy across the slice, index,
format, round and concat paths.

- builtins::getindex_w: clamp an out-of-word overflow to i64::MIN for a
  negative value (was always i64::MAX), so a large-negative slice bound
  floors to 0 instead of collapsing to the tail.
- normalize_slice / sliceobject::adapt_lower_bound: fold a negative index
  by the length with saturating_add so an i64::MIN bound does not overflow
  before flooring at 0.
- str_slice_args (startswith/endswith): take start/end bounds through
  adapt_lower_bound(eval_slice_index) so a non-index bound raises, an
  __index__ object is honoured, and a positive bound is not upper-clamped.
- builtin_round: take ndigits through getindex_w for a float (clamping)
  and index for an int; a huge-magnitude negative ndigits short-circuits
  to 0 rather than building an astronomical power of ten.
- index_to_bigint (hex/oct/bin): coerce through space.index so a non-int
  __index__ return raises "__index__ returned non-int (type X)".
- formatting number_arg_decimal / number_arg_integer (%d/%i/%o/%x): remap
  a TypeError from the numeric decoder to the operand-type error naming
  the original argument.
- list/tuple/bytes/bytearray __add__ and descroperation::add: return
  NotImplemented for a non-sequence operand so the operator raises the
  generic "unsupported operand type(s) for +", dropping the cpython-only
  "can only concatenate" message; bytearray __iadd__ on a non-buffer now
  raises "a bytes-like object is required, not 'X'".

Assisted-by: Claude
Add the in-place operator-module functions and the mutable set's in-place
operator slots, both previously missing.

- operator: iadd/isub/imul/ifloordiv/imod/itruediv/ipow/ilshift/irshift/
  iand/ior/ixor route through binary_value's in-place path (space.inplace_X),
  and iconcat requires both operands to be subscriptable. The extra_init
  dunder aliases (operator.__iadd__ etc.) now resolve as a result.
- set: __isub__/__iand__/__ior__/__ixor__ mirror setobject.py's
  descr_inplace_sub/_and/_or/_xor — a non-set/-frozenset operand yields
  NotImplemented, otherwise self is mutated through the matching update
  helper and returned. The four *_update method bodies are extracted into
  named functions shared by the app methods and the in-place slots.

Assisted-by: Claude
A left shift whose result exceeds allocatable memory (e.g. `1 << 10**18`)
drove the underlying big-int `<<` into the infallible allocator and aborted
the process. Pre-flight a fallible reservation of the result's 64-bit limb
count in checked_bigint_lshift and raise a catchable MemoryError instead,
matching long_lshift. int_lshift and long_lshift route through the checked
helper; the probe upper-bounds the shift's own allocation so it fails
exactly when the real shift would.

Assisted-by: Claude
A float or complex power coerces each int operand to a double before the
power is computed, so an over-range int base or exponent raises
OverflowError up front -- even `1.0 ** huge`, which never reaches the
arithmetic, matching float(huge). reject_pow_operand_overflow performs
that check and is called on both operands in the two-argument pow /
pow_builtin float and complex paths. In float.__pow__/__rpow__ the check
runs before float_pow_reject_modulus, so `pow(2.0, huge, 5)` raises
OverflowError from the coercion rather than TypeError from the modulus
rejection.

Assisted-by: Claude
@youknowone youknowone changed the title objspace: pypy parity for index coercion, slice clamp, sequence concat pypy parity: index/slice coercion, sequence concat, in-place operators, overflow guards Jul 15, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35081d2f4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +92 to +93
let result_bits = a.bits().saturating_add(shift);
let limbs = (result_bits / 64).saturating_add(2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve zero for huge left shifts

When the left operand is zero and the shift count is very large but still fits in i64 (for example 0 << 10**18), this preflight sizes the reservation from shift alone and returns MemoryError before doing the shift. PyPy/CPython return 0 for zero shifted by any non-negative count, so this changes valid code that used to be allocation-free; short-circuit zero before the reservation in this helper so both the int and long callers keep that behavior.

Useful? React with 👍 / 👎.

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.

1 participant