Skip to content

Commit fe4c8b3

Browse files
Short-circuit identity in rich_compare_bool for Eq/Ne (PyObject_RichCompareBool parity)
CPython distinguishes two comparison entry points (Objects/object.c): - PyObject_RichCompare returns the raw __eq__ / __ne__ result; no identity short-circuit - PyObject_RichCompareBool returns bool; identity implies equality (and inequality is false on identity), short-circuiting before dispatch Collection membership / equality (x in [x], [nan] == [nan], set/dict comparisons) go through the bool variant and rely on the short-circuit. RustPython's rich_compare_bool skipped the identity check, so a buggy or raising __eq__ propagated even when the operand was the same object. Add an identity short-circuit at the top of rich_compare_bool for Eq (returns true) and Ne (returns false). Ordering ops fall through to _cmp because Python does not guarantee reflexivity for </<=/>/>=. _cmp itself is untouched, so == / != operators continue to invoke __eq__ / __ne__ exactly as before. Unmasks test_dictviews.TestDictViews.test_compare_error. Verified byte-identical with CPython 3.14.4 across 53 scenarios in 10 categories (collection membership / equality / ordering ops / NaN / hash collision / dict views / list-set-dict ops). 14-module regression sweep ~2,402 tests passes with no regressions.
1 parent 6b67067 commit fe4c8b3

2 files changed

Lines changed: 14 additions & 1 deletion

File tree

Lib/test/test_dictviews.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,6 @@ def test_copy(self):
292292
self.assertRaises(TypeError, copy.copy, d.values())
293293
self.assertRaises(TypeError, copy.copy, d.items())
294294

295-
@unittest.expectedFailure # TODO: RUSTPYTHON
296295
def test_compare_error(self):
297296
class Exc(Exception):
298297
pass

crates/vm/src/protocol/object.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,20 @@ impl PyObject {
348348
op_id: PyComparisonOp,
349349
vm: &VirtualMachine,
350350
) -> PyResult<bool> {
351+
// CPython parity: PyObject_RichCompareBool guarantees identity implies
352+
// equality (and inequality is false on identity), short-circuiting
353+
// before dispatch. Collection membership / equality (e.g. `x in [x]`,
354+
// `[nan] == [nan]`) depend on this even when `__eq__` would raise
355+
// or return False. Only Eq/Ne are decidable from identity; ordering
356+
// ops fall through to `_cmp` because Python does not guarantee
357+
// reflexivity for `<`/`<=`/`>`/`>=`.
358+
if self.is(other) {
359+
match op_id {
360+
PyComparisonOp::Eq => return Ok(true),
361+
PyComparisonOp::Ne => return Ok(false),
362+
_ => {}
363+
}
364+
}
351365
match self._cmp(other, op_id, vm)? {
352366
Either::A(obj) => obj.try_to_bool(vm),
353367
Either::B(other) => Ok(other),

0 commit comments

Comments
 (0)