From 962ad63bc2c3931949b73d8a5c12cea7053e0339 Mon Sep 17 00:00:00 2001 From: OkJa Date: Sun, 2 Aug 2026 13:20:25 +0900 Subject: [PATCH] Implement cell comparison and repr 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 rather than . 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 --- Lib/test/test_funcattrs.py | 1 - Lib/test/test_reprlib.py | 1 - crates/vm/src/builtins/function.rs | 48 ++++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py index ff696c5c153..bb9c88efec6 100644 --- a/Lib/test/test_funcattrs.py +++ b/Lib/test/test_funcattrs.py @@ -432,7 +432,6 @@ def f(): class CellTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_comparison(self): # These tests are here simply to exercise the comparison code; # their presence should not be interpreted as providing any diff --git a/Lib/test/test_reprlib.py b/Lib/test/test_reprlib.py index db3d87bd17a..22a55b57c07 100644 --- a/Lib/test/test_reprlib.py +++ b/Lib/test/test_reprlib.py @@ -237,7 +237,6 @@ def test_nesting(self): eq(r([[[[[[{}]]]]]]), "[[[[[[{}]]]]]]") eq(r([[[[[[[{}]]]]]]]), "[[[[[[[...]]]]]]]") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_cell(self): def get_cell(): x = 42 diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index d1fa5222393..90315bcb194 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -15,7 +15,7 @@ use crate::{ class::PyClassImpl, common::wtf8::{Wtf8Buf, wtf8_concat}, frame::{FrameObject, FrameObjectRef}, - function::{FuncArgs, OptionalArg, PyComparisonValue, PySetterValue}, + function::{Either, FuncArgs, OptionalArg, PyComparisonValue, PySetterValue}, scope::Scope, types::{ Callable, Comparable, Constructor, GetAttr, GetDescriptor, Hashable, PyComparisonOp, @@ -1507,7 +1507,7 @@ impl Representable for PyBoundMethod { } } -#[pyclass(module = false, name = "cell", traverse)] +#[pyclass(module = false, name = "cell", unhashable = true, traverse)] #[derive(Debug, Default)] pub(crate) struct PyCell { contents: PyMutex>, @@ -1530,8 +1530,26 @@ impl Constructor for PyCell { } } -#[pyclass(with(Constructor))] +#[pyclass(with(Constructor, Representable))] impl PyCell { + #[pyslot] + fn slot_richcompare( + zelf: &PyObject, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult> { + let (Some(zelf), Some(other)) = (zelf.downcast_ref::(), other.downcast_ref::()) + else { + return Ok(Either::B(PyComparisonValue::NotImplemented)); + }; + // 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).map(Either::A), + (a, b) => Ok(Either::B(op.eval_ord(b.is_none().cmp(&a.is_none())).into())), + } + } + pub(crate) const fn new(contents: Option) -> Self { Self { contents: PyMutex::new(contents), @@ -1561,6 +1579,30 @@ impl PyCell { } } +impl Representable for PyCell { + #[inline] + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let id = zelf.get_id(); + Ok(match zelf.get() { + Some(value) => { + let type_name = value.class().slot_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; + } + format!( + "", + &type_name[..end], + value.get_id() + ) + } + None => format!(""), + }) + } +} + /// Vectorcall implementation for PyFunction (PEP 590). /// Takes owned args to avoid cloning when filling fastlocals. pub(crate) fn vectorcall_function(