Skip to content

objspace: pypy-parity for operator errors, __index__ coercion, 3-arg pow - #552

Merged
youknowone merged 5 commits into
mainfrom
rename
Jul 14, 2026
Merged

objspace: pypy-parity for operator errors, __index__ coercion, 3-arg pow#552
youknowone merged 5 commits into
mainfrom
rename

Conversation

@youknowone

Copy link
Copy Markdown
Owner

Five interpreter object-space slices bringing error/coercion behavior in line
with PyPy (rpython//pypy/ upstream). Each is independently verified against
pypy3 and, where cpython==pypy, guarded by a synthetic regression test.

Slices

Operator TypeError messages: name real class, pypy text (b15f509af26)
Binary (+ - * / // % << >> & | ^) and comparison error messages take the
operand name from object_functionstr_type_name (the __class__ name) instead
of (*ll_type(x)).name — a user class now reads 'Foo', not 'object'. The
** operator / 2-arg pow message reads for ** or pow():, and unary
+ - ~/abs on an unsupported operand read unsupported operand type for unary pos|neg|~|abs: (_make_unaryop_impl). Synth operator_error_typename
guards the binop/comparison/** cases (cpython==pypy); unary/abs match pypy but
diverge from cpython, so are verified out of band.

set.remove KeyError carries key; ord() names arg type (0d8e2973b0d)
set.remove(x) on a missing element now raises key_error_with_key(x) (the
KeyError carries x, matching dict[missing]) instead of a fixed string.
ord() on a non-string names the argument's type via
object_functionstr_type_name (e.g. but int found). Synth
set_remove_ord_errors.

list: coerce subscript/insert/pop index through __index__ (c2e91f3e481)
getitem_list/setitem_list/delitem_slot and list.insert/list.pop run a
non-int, non-slice index through __index__ (getindex_w). Hot get/set inline
the coercion (is_int fast path + __index__ slow path) to keep a concrete Int
repr in the rtyper; cold del/insert/pop route through
subscript_index_w/getindex_w_index. Fixes a RecursionError where a non-int
del list[k] fell through to the generic __delitem__ slot and recursed.
Overflow raises cannot fit '<type>' into an index-sized integer; a non-index
key raises <descr> indices must be integers or slices, not '<type>'; out-of-range
raises list index out of range. Synth list_subscript_index,
list_insert_pop_index.

seq: coerce tuple/str/bytes subscript index through __index__ (3b08028f82f)
getitem_tuple/getitem_str/getitem_bytes_like coerce a non-int, non-slice
key through __index__, mirroring getitem_list. getitem_str routes its
non-index TypeError through index_type_error, adding the missing or slices
clause; a bytes index-out-of-range reads byte index out of range. Synth
tuple_str_bytes_subscript_index.

pow: 3-arg power tries only forward __pow__, not __rpow__ (00eccce001d)
3-arg pow(base, exp, mod) consults only the forward __pow__ on the base
(descroperation.py:459) and raises the three-operand unsupported operand type(s) for pow(): T, T, T on NotImplemented — matching PyPy, not cpython. The
integer modular-power fast path moves from pow3 into int.__pow__, so a base
whose type overrides __pow__ is honoured (pow(MyInt(2), 3, 5) calls the
override). int.__pow__ returns NotImplemented for a non-integer modulus and
computes the modular power directly rather than re-entering pow3, fixing an
infinite recursion that crashed pow(2, 10, 100.0) / pow(2, 10, 100j) with
RecursionError. A float base rejects a non-None modulus with TypeError
pow() 3rd argument not allowed unless all arguments are integers, a complex
base with ValueError complex modulo. Removes the now-unused
try_dispatch_ternary_special/should_try_reverse_first. Synth
pow3_arg_types.

Verification

  • pyre==pypy matrices for each slice; pow 11/11 + subscript value/oob/big cases.
  • python ./pyre/check.py: cranelift 187/187, wasm 186/186 green; the sole
    dynasm miss is the pre-existing nested_loop perf-boundary flake (output
    byte-identical to pypy, passes in isolation, uses none of this code).
  • codex parity review: 0 Section-1 regressions.

🤖 Generated with Claude Code

Operator error paths derived the operand type name from
(*ll_type(x)).name, which is 'object' for user-defined classes, and used
cpython-style unary/abs wording that pyre does not target.

- Binary (+, -, *, /, //, %, <<, >>, &, |, ^) and comparison error
  messages take the operand name from object_functionstr_type_name (the
  Python __class__ name), so a user class reads 'Foo' not 'object'.
- The ** operator (and 2-arg pow) message reads
  'unsupported operand type(s) for ** or pow():'.
- Unary +/-/~ and abs on an unsupported operand read 'unsupported operand
  type for unary pos|neg|~|abs:' (_make_unaryop_impl) instead of cpython's
  'bad operand type'.

Add synth/operator_error_typename regression test (binop/comparison/**
are cpython==pypy; unary/abs match pypy but diverge from cpython, so are
verified out of band).

Assisted-by: Claude
set.remove(x) on a missing element raised KeyError with the fixed string
"set.remove(x): x not in set"; it now raises key_error_with_key(x) so the
KeyError carries x itself (str and args match dict[missing]).

ord() on a non-string argument reported "but other type found"; it now
names the argument's type via object_functionstr_type_name (e.g. "but int
found").

Add synth/set_remove_ord_errors regression test.

Assisted-by: Claude
getitem_list/setitem_list/delitem_slot and list.insert/list.pop run a
non-int, non-slice index through __index__ (getindex_w) instead of
rejecting or ignoring it:

- getitem_list and setitem_list inline the coercion (is_int fast path plus
  the __index__ slow path) so the hot integer subscript keeps a concrete
  Int repr in the rtyper; delitem and insert/pop route through the
  subscript_index_w / getindex_w_index helpers on their cold paths.
- delitem no longer lets a non-int key fall through to the generic
  __delitem__ slot (bound to delitem_slot), which recursed into itself and
  raised RecursionError; it coerces or raises here.
- an index too large for a machine word raises "cannot fit '<type>' into an
  index-sized integer" (IndexError for subscript, OverflowError for
  insert/pop); a non-index, non-slice key raises "<descr> indices must be
  integers or slices, not '<type>'" naming the key's real class.
- setitem/delitem out-of-range raise IndexError "list index out of range".

subscript_index_w generalizes the former bytearray_index over the descr.

Assisted-by: Claude
getitem_tuple, getitem_str, and getitem_bytes_like coerce a non-int,
non-slice key through __index__ (getindex_w), mirroring getitem_list: an
inline is_int fast path keeps the hot integer subscript's Int repr concrete
in the rtyper, an overflowing index raises IndexError "cannot fit '<type>'
into an index-sized integer", and a non-index key raises "<descr> indices
must be integers or slices, not '<type>'".

getitem_str routes its non-index TypeError through index_type_error, adding
the "or slices" clause it was missing ("string indices must be integers or
slices, not '<type>'"). A bytes index-out-of-range now reads "byte index out
of range".

Assisted-by: Claude
Three-argument pow(base, exp, mod) now consults only the forward __pow__ on
the base (descroperation.py:459) and raises the three-operand "unsupported
operand type(s) for pow(): T, T, T" TypeError when it returns NotImplemented;
the reflected __rpow__ is no longer threaded through the modulus, so pow3
mirrors PyPy rather than CPython here.

The integer modular-power fast path moves out of pow3 into int.__pow__, so a
base whose type overrides __pow__ is honoured for three-arg power
(pow(MyInt(2), 3, 5) calls the override) instead of being shadowed by the
fast path. int.__pow__ returns NotImplemented for a non-integer modulus and
computes the modular power directly rather than re-entering pow3, fixing an
infinite recursion that crashed pow(2, 10, 100.0) and pow(2, 10, 100j) with
RecursionError.

A float base (or float __rpow__) rejects a non-None modulus with TypeError
"pow() 3rd argument not allowed unless all arguments are integers"
(floatobject.py:588), and a complex base with ValueError "complex modulo"
(complexobject.py:525), instead of silently ignoring the modulus.

The now-unused try_dispatch_ternary_special and its should_try_reverse_first
helper are removed, and binary/ternary operand-type-error formatting shares an
operand_type_name helper. The proxy 3-arg test is updated to expect the
forward-only behavior. synth pow3_arg_types covers the cpython==pypy cases.

Assisted-by: Claude
@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: 34 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: 9954ac00-9d29-4908-95f5-38a1ba9619a9

📥 Commits

Reviewing files that changed from the base of the PR and between 8df6bf3 and 00eccce.

📒 Files selected for processing (12)
  • pyre/bench/synth/list_insert_pop_index.py
  • pyre/bench/synth/list_subscript_index.py
  • pyre/bench/synth/operator_error_typename.py
  • pyre/bench/synth/pow3_arg_types.py
  • pyre/bench/synth/set_remove_ord_errors.py
  • pyre/bench/synth/tuple_str_bytes_subscript_index.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.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

https://github.com/youknowone/pyre/blob/00eccce001dcb54b7603b49f2e4be2b7e7143192/pyre-interpreter/src/baseobjspace.rs#L1291
P2 Badge Preserve getindex_w TypeError remapping

When an object defines __index__ but that method returns a non-int, this propagates space_index's TypeError (for example __index__ returned non-int...) instead of applying the getindex_w(index, IndexError, "list") behavior cited above. In PyPy, pypy/interpreter/baseobjspace.py:1567-1579 catches TypeError from space.index() whenever objdescr is set and re-raises the sequence-specific list indices must be integers or slices... error; the same inlined pattern is copied to tuple/str/bytes and list assignment, so bad custom index objects now diverge on all of those subscript paths.

ℹ️ 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".

@github-actions

Copy link
Copy Markdown

🤖 Codex parity review

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

Files in the reviewed diff
pyre/bench/synth/list_insert_pop_index.py
pyre/bench/synth/list_subscript_index.py
pyre/bench/synth/operator_error_typename.py
pyre/bench/synth/pow3_arg_types.py
pyre/bench/synth/set_remove_ord_errors.py
pyre/bench/synth/tuple_str_bytes_subscript_index.py
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs
pyre/pyre-interpreter/src/objspace/descroperation.rs
pyre/pyre-interpreter/src/type_methods.rs
pyre/pyre-interpreter/src/typedef.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/objspace/descroperation.rs:3578 ↔ pypy/objspace/descroperation.py:857: unary positive now reports unsupported operand type for unary pos, but PyPy’s generated unary-operation message uses the operator symbol: unsupported operand type for unary +.

  • pyre/pyre-interpreter/src/objspace/descroperation.rs:3618 ↔ pypy/objspace/descroperation.py:857: unary negation now reports unsupported operand type for unary neg, but PyPy uses unsupported operand type for unary -.

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

  • pyre/pyre-interpreter/src/builtins.rs:7261 ↔ pypy/objspace/std/bytesobject.py:473: ord(b"ab") is reported as “string of length 2” because the shared bytes/bytearray branch always says string; PyPy’s bytes implementation says bytes of length 2. The bytearray wording is correctly string, so this needs a bytes-specific branch.

4. Structural adaptations

  • pyre/pyre-interpreter/src/baseobjspace.rs:1263 ↔ pypy/interpreter/baseobjspace.py:1577: sequence-index TypeErrors use CPython-compatible quoted type names (not 'float'), while PyPy’s %T template emits the unquoted form (not float). This shared helper affects the patched list, tuple, str, bytes, bytearray, assignment, and deletion paths.

  • pyre/pyre-interpreter/src/objspace/descroperation.rs:2762 ↔ pypy/objspace/descroperation.py:454: three-argument pow intentionally omits PyPy’s cpyext-type compatibility branch (pow3_bug_compat_cpyext), which depends on PyPy’s cpyext type model and has no direct Rust equivalent.

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