Skip to content

Implement cell comparison and repr - #8458

Open
name-of-okja wants to merge 1 commit into
RustPython:mainfrom
name-of-okja:fix/cell-repr-and-comparison
Open

Implement cell comparison and repr#8458
name-of-okja wants to merge 1 commit into
RustPython:mainfrom
name-of-okja:fix/cell-repr-and-comparison

Conversation

@name-of-okja

@name-of-okja name-of-okja commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

PyCell filled neither the richcompare nor the repr slot, so object's
address-based defaults showed through. Cells were compared as boxes rather than
by what they hold:

>>> cell(1) == cell(1)
False                       # CPython: True
>>> cell(-36) == cell(-36.0)
False                       # CPython: True
>>> cell(2) < cell(3)
TypeError: '<' not supported between instances of 'cell' and 'cell'
>>> repr(cell(1))
'<cell object at 0x7f...>'  # CPython: '<cell at 0x7f...: int object at 0x1b...>'

The box is an implementation detail, so CPython compares the contents instead
and renders both addresses in the repr. This ports the two missing slots.

Comparison follows cell_richcompare, which delegates to
cell_compare_impl:

https://github.com/python/cpython/blob/v3.14.0/Objects/cellobject.c#L85-L115

Contents are compared when both cells are full; otherwise the emptiness flags
are compared, which is how CPython gets "empty cells come before anything
else".

Two details of that delegation are load-bearing:

  • The slot is filled directly rather than through Comparable, whose cmp
    can only answer with a bool. CPython returns whatever PyObject_RichCompare
    produced, so a contained __eq__ yielding a non-bool has to pass through
    untouched — and coercing it would additionally call __bool__, raising
    exceptions CPython never raises. The empty-cell branch still answers with a
    bool, so the slot returns Either.

  • PyObject_RichCompare is used rather than PyObject_RichCompareBool,
    matching CPython. The latter short-circuits Eq on identity, which is
    observable:

    >>> c = cell(float('nan'))
    >>> c == c
    False                     # contents are actually compared

Repr follows cell_repr, including the empty case and the %.80s bound on
the contained type name — at most 80 bytes, dropping a character the cut would
leave incomplete:

https://github.com/python/cpython/blob/v3.14.0/Objects/cellobject.c#L117-L128

The type also becomes unhashable. This is not cosmetic: content-based
equality combined with the inherited identity hash would break the invariant
that objects comparing equal hash equally, so {cell(1): v}[cell(1)] would
raise KeyError. Hashing the contents is not an option either, because
cell_contents is writable and a key's hash has to stay put:

https://github.com/python/cpython/blob/v3.14.0/Objects/cellobject.c#L158-L170

CPython arrives at the same place implicitly — cell leaves tp_hash empty
while filling tp_richcompare, and the two slots are inherited as a pair:

https://github.com/python/cpython/blob/v3.14.0/Objects/typeobject.c#L8261-L8276

list reaches it explicitly with PyObject_HashNotImplemented, which is the
shape unhashable = true already has in RustPython.

Test Plan

  • Removed @unittest.expectedFailure from the two tests that now pass:
    test_funcattrs.CellTest.test_comparison and test_reprlib.test_cell.
  • Differentially compared against CPython 3.14.0 over every documented cell
    behaviour — repr (filled, empty, constructed directly), all six comparison
    operators, empty-vs-filled ordering in both directions, non-cell operands,
    the nan identity case, hashing, and mutation through cell_contents.
    Output is identical once addresses are normalised.
  • Separately compared the two cases raised in review: contained __eq__/__lt__
    returning a non-bool or an object whose __bool__ raises, and type names of
    79/80/81 ASCII bytes plus 100 each of 2-, 3- and 4-byte characters. The 3-byte
    case is the one that pins the bound to bytes rather than characters — CPython
    renders 26 characters (78 bytes) there, since a 27th would reach 81.
  • test_funcattrs and test_reprlib pass with no unexpected successes.
  • 27 modules that exercise closures, cells and comparison pass (3,872 tests):
    serialization (test_pickle, test_copy, test_marshal), frame
    reconstruction (test_inspect, test_pdb, test_annotationlib), cycle
    collection (test_gc, test_weakref), plus test_richcmp, test_sys,
    test_code, test_scope, test_types, test_descr, test_class,
    test_super, test_builtin, test_functools, test_typing,
    test_dataclasses, test_generators, test_doctest and others.
  • cargo test -p rustpython-vm -p rustpython-common, cargo fmt --check and
    cargo clippy --all-targets are clean.

One gap worth naming: no test in the CPython suite covers cell being
unhashable, so that part rests on the reasoning above rather than on coverage.
Nothing in crates/ or Lib/ hashes a cell or depends on the old repr.

Copilot AI lite review requested due to automatic review settings August 7, 2026 10:10
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] lib: cpython/Lib/reprlib.py
[x] test: cpython/Lib/test/test_reprlib.py

dependencies:

  • reprlib

dependent tests: (245 tests)

  • reprlib: test_reprlib
    • bdb: test_bdb test_pdb
    • collections: test_annotationlib test_array test_asyncio test_bisect test_builtin test_c_locale_coercion test_call test_collections test_configparser test_contains test_context test_copy test_csv test_ctypes test_defaultdict test_deque test_descr test_dict test_dictviews test_embed test_enum test_exception_group test_file test_fileinput test_fileio test_frame test_funcattrs test_functools test_genericalias test_hash test_httpservers test_inspect test_io test_ipaddress test_iter test_iterlen test_json test_logging test_math test_monitoring test_ordered_dict test_pathlib test_patma test_pickle test_plistlib test_pprint test_pydoc test_random test_richcmp test_set test_shelve test_sqlite3 test_statistics test_string test_struct test_sys test_traceback test_tuple test_types test_typing test_unittest test_urllib test_userdict test_userlist test_userstring test_weakref test_weakset test_with
      • ast: test_ast test_compile test_compiler_codegen test_dis test_fstring test_future_stmt test_peepholer test_peg_generator test_site test_ssl test_type_comments test_ucn test_unparse
      • concurrent.futures._base: test_concurrent_futures
      • dbm.dumb: test_dbm_dumb
      • dbm.sqlite3: test_dbm_sqlite3
      • difflib: test_difflib test_profile test_sys_settrace
      • dis: test__opcode test_code test_compiler_assemble test_dtrace test_opcache test_positional_only_arg test_type_cache
      • email.feedparser: test_email
      • http.client: test_docxmlrpc test_hashlib test_unicodedata test_urllib2 test_wsgiref test_xmlrpc
      • importlib.metadata: test_importlib test_zoneinfo
      • inspect: test_abc test_argparse test_asyncgen test_buffer test_clinic test_coroutines test_decimal test_generators test_grammar test_ntpath test_operator test_posixpath test_signal test_turtle test_type_annotations test_yield_from test_zipimport test_zipimport_support
      • logging: test_asyncio test_pkgutil test_support test_urllib2net
      • multiprocessing: test_asyncio test_compileall test_concurrent_futures test_fcntl test_memoryview test_multiprocessing_main_handling test_re test_socket
      • pkgutil: test_pyrepl test_runpy
      • platform: test__locale test__osx_support test_asyncio test_baseexception test_cmath test_ctypes test_mimetypes test_os test_platform test_posix test_regrtest test_shutil test_strptime test_sysconfig test_time test_winreg
      • pprint: test_htmlparser test_sys_setprofile
      • queue: test_android test_asyncio test_concurrent_futures test_dummy_thread test_sched
      • selectors: test_asyncio test_selectors test_subprocess
      • shlex: test_shlex test_venv test_webbrowser
      • shutil: test_bz2 test_ctypes test_filecmp test_glob test_importlib test_largefile test_launcher test_modulefinder test_peg_generator test_py_compile test_sax test_string_literals test_tarfile test_tempfile test_unicode_file
      • ssl: test_ftplib test_httplib test_imaplib test_poplib test_urllib2_localnet
      • string: test_email test_fnmatch test_grp test_importlib test_mmap test_pwd test_pyrepl test_secrets test_string test_tokenize test_zipfile
      • threading: test_asyncio test_bytes test_concurrent_futures test_contextlib test_ctypes test_external_inspection test_fork1 test_gc test_importlib test_ioctl test_itertools test_linecache test_pathlib test_poll test_pyrepl test_queue test_robotparser test_smtplib test_socketserver test_super test_syslog test_termios test_threadedtempfile test_threading test_threading_local test_zstd
      • tokenize: test_tabnanny
      • traceback: test_asyncio test_code_module test_contextlib_async test_dictcomps test_exceptions test_http_cookiejar test_importlib test_listcomps test_pyexpat test_setcomps test_unittest
      • tracemalloc: test_tracemalloc
      • urllib.parse: test_http_cookies test_urllibnet test_urlparse
      • wave: test_wave
      • xml.etree.ElementTree: test_doctest
    • dataclasses: test__colorize test_ctypes
      • pstats: test_pstats

[x] test: cpython/Lib/test/test_funcattrs.py (TODO: 1)

dependencies:

dependent tests: (no tests depend on funcattrs)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

PyCell is explicitly unhashable. It compares contained values, orders empty cells before populated cells, and represents its identity with contents or an empty marker.

Changes

PyCell behavior

Layer / File(s) Summary
Hashability, comparison, and representation protocols
crates/vm/src/builtins/function.rs
PyCell is marked unhashable. Comparisons delegate to contained values, order empty cells before populated cells, and return NotImplemented for non-cell objects. Representations include cell and contained-object identity or empty.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: implementing cell comparison and representation behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/vm/src/builtins/function.rs`:
- Around line 1585-1588: Update the PyCell formatting branch in the function
containing the Some(value) match to truncate value.class().slot_name() to at
most 80 Unicode characters before passing it to format!, while preserving the
existing repr structure and address formatting; add or update a regression test
if the surrounding test suite covers cell representations.
- Line 1574: Update the Some/Some branch of Comparable’s rich-comparison
handling to return the PyObjectRef produced by rich_compare directly instead of
coercing it with is_true(vm). Preserve special handling that converts only
Boolean or NotImplemented results, while leaving empty-cell ordering unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e491cd7-5a34-4657-a35c-6c2e76fcec4d

📥 Commits

Reviewing files that changed from the base of the PR and between 1c7759c and bba2b7f.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_funcattrs.py is excluded by !Lib/**
  • Lib/test/test_reprlib.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/vm/src/builtins/function.rs

Comment thread crates/vm/src/builtins/function.rs Outdated
Comment thread crates/vm/src/builtins/function.rs Outdated

Copilot AI 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.

Pull request overview

Adds CPython-parity behavior for cell objects by implementing content-based comparison and a CPython-like repr, and updates stdlib tests that were previously marked as expected failures for RustPython.

Changes:

  • Make cell unhashable to preserve the equality/hash invariant after switching to content-based equality.
  • Add a cell repr that includes the cell address and (when non-empty) the contained object’s type and address.
  • Un-xfail CPython tests that now pass (test_funcattrs.CellTest.test_comparison, test_reprlib.test_cell).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
Lib/test/test_reprlib.py Removes expectedFailure marker for cell repr test now that cell.__repr__ is implemented.
Lib/test/test_funcattrs.py Removes expectedFailure marker for cell comparison test now that cell rich-compare is implemented.
crates/vm/src/builtins/function.rs Implements cell comparison + repr and marks cell as unhashable.
Suppressed comments (1)

crates/vm/src/builtins/function.rs:1576

  • In the full-cell case, a.rich_compare(b, op, vm)?.is_true(vm) coerces the rich-compare result to bool, which is not what CPython does for cells (it returns the raw PyObject_RichCompare result). This also means a non-bool __eq__ result will be truth-tested (invoking __bool__/__len__) and can raise where CPython would not. Implement tp_richcompare directly and return Either::A(...) for the full-vs-full branch, while keeping the empty-ordering behavior as a boolean.
impl Comparable for PyCell {
    fn cmp(
        zelf: &Py<Self>,
        other: &PyObject,
        op: PyComparisonOp,
        vm: &VirtualMachine,
    ) -> PyResult<PyComparisonValue> {
        let other = class_or_notimplemented!(Self, other);
        // compare cells by contents; empty cells come before anything else
        match (zelf.get(), other.get()) {
            (Some(a), Some(b)) => a.rich_compare(b, op, vm)?.is_true(vm).map(Into::into),
            (a, b) => Ok(op.eval_ord(b.is_none().cmp(&a.is_none())).into()),
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/vm/src/builtins/function.rs Outdated
}

#[pyclass(with(Constructor))]
#[pyclass(with(Constructor, Comparable, Representable))]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct on both counts, and done in 962ad63 — dropped Comparable and gave PyCell its own #[pyslot] slot_richcompare, returning Either::A for full-vs-full and Either::B only for the empty-cell ordering, where a bool is the right answer.

The __bool__ side effect you flagged is the sharpest part of this. With an __eq__ that returns an object whose __bool__ raises, CPython hands back the object, while the coerced version raised a RuntimeError that CPython never produces. Non-bool results were flattened too: 'I am not a bool' became True.

PyBaseObject was a useful precedent for filling the slot by hand rather than through the trait.

The cell type filled neither the richcompare nor the repr slot, so
object's address-based defaults showed through: cell(1) == cell(1) was
False, ordering raised TypeError, and repr rendered
<cell object at 0x...> rather than <cell at 0x...: int object at 0x...>.

Compare cells by contents, with empty cells ordering before everything
else, and render CPython's repr for both the filled and empty cases.

The comparison fills the richcompare slot directly rather than going
through Comparable, whose cmp() can only answer with a bool. CPython
returns whatever PyObject_RichCompare produced, so a contained __eq__
that yields a non-bool must pass through untouched; coercing it would
also call __bool__ and surface exceptions CPython never raises. The
empty-cell branch still answers with a bool, so both arms are needed and
the slot returns Either.

The repr truncates the contained type name the way "%.80s" does: at most
80 bytes, dropping a character the cut would leave incomplete.

Mark the type unhashable. Content-based equality combined with the
inherited identity hash would break the hash/eq contract, and hashing the
contents is not possible either because cell_contents is writable.
CPython gets this implicitly, because defining tp_richcompare suppresses
tp_hash inheritance.

Reference: CPython Objects/cellobject.c, cell_richcompare and cell_repr.

Assisted-by: Claude Code:claude-opus-5
Copilot AI review requested due to automatic review settings August 7, 2026 10:49
@name-of-okja
name-of-okja force-pushed the fix/cell-repr-and-comparison branch from bba2b7f to 962ad63 Compare August 7, 2026 10:49

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/vm/src/builtins/function.rs:1600

  • The PR description explicitly says the %.80s truncation of the contained value’s type name is not reproduced, but the implementation here does truncate to 80 bytes. This is a behavioral discrepancy (and adds extra complexity) relative to what the PR claims.

Either update the PR description to match the intended CPython-faithful behavior, or (if the goal is to avoid truncation) drop the truncation logic and render the full type_name.

                // CPython renders the type name with "%.80s", which reads at
                // most 80 bytes and drops a character left incomplete by the cut.
                let mut end = type_name.len().min(80);
                while !type_name.is_char_boundary(end) {
                    end -= 1;

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