Short-circuit identity in rich_compare_bool for Eq/Ne (PyObject_RichCompareBool parity)#7734
Conversation
…ompareBool 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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughrich_compare_bool now short-circuits identity for Eq/Ne; mapping and weak proxies delegate to Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 6/8 reviews remaining, refill in 12 minutes and 7 seconds.Comment |
thanks for the quick check ! |
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [x] test: cpython/Lib/test/test_dict.py (TODO: 6) dependencies: dependent tests: (no tests depend on dict) Legend:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/vm/src/builtins/weakref.rs (1)
139-142: Consider extracting a helper for therich_comparetoPyComparisonValueconversion.This pattern appears identically in
mappingproxy.rs,weakproxy.rs, and here:crate::function::PyArithmeticValue::from_object(vm, res) .map(|obj| obj.try_to_bool(vm)) .transpose()A helper in
crate::functionorcrate::protocol::objectcould reduce duplication:pub fn rich_compare_result_to_comparison_value( vm: &VirtualMachine, res: PyObjectRef, ) -> PyResult<PyComparisonValue> { PyArithmeticValue::from_object(vm, res) .map(|obj| obj.try_to_bool(vm)) .transpose() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/builtins/weakref.rs` around lines 139 - 142, The same conversion from an object returned by rich_compare to a PyComparisonValue is duplicated (seen in weakref.rs, weakproxy.rs, mappingproxy.rs) using PyArithmeticValue::from_object(vm, res).map(|obj| obj.try_to_bool(vm)).transpose(); extract a small helper (e.g., rich_compare_result_to_comparison_value(vm, res)) into crate::function or crate::protocol::object that takes (&VirtualMachine, PyObjectRef) and returns PyResult<PyComparisonValue>, then replace the duplicated call sites (including the rich_compare call in weakref.rs) to call that helper; keep the helper name and signature consistent across call sites and handle the same mapping/transpose logic inside it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@crates/vm/src/builtins/weakref.rs`:
- Around line 139-142: The same conversion from an object returned by
rich_compare to a PyComparisonValue is duplicated (seen in weakref.rs,
weakproxy.rs, mappingproxy.rs) using PyArithmeticValue::from_object(vm,
res).map(|obj| obj.try_to_bool(vm)).transpose(); extract a small helper (e.g.,
rich_compare_result_to_comparison_value(vm, res)) into crate::function or
crate::protocol::object that takes (&VirtualMachine, PyObjectRef) and returns
PyResult<PyComparisonValue>, then replace the duplicated call sites (including
the rich_compare call in weakref.rs) to call that helper; keep the helper name
and signature consistent across call sites and handle the same mapping/transpose
logic inside it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 7e83b45e-e50f-48d5-8a2d-e6b02352abeb
📒 Files selected for processing (3)
crates/vm/src/builtins/mappingproxy.rscrates/vm/src/builtins/weakproxy.rscrates/vm/src/builtins/weakref.rs
The identity short-circuit added to rich_compare_bool exposed a latent bug in three proxy types (weakref, weakproxy, mappingproxy): they were delegating their __eq__ to the bool variant on referents, while CPython uses PyObject_RichCompare so the referent's __eq__ runs even when the referents share identity. Fixes test_weak_keyed_cascading_deletes which depends on key __eq__ firing during dict deletion to trigger a side-effect that mutates the key list.
dbb338e to
55c3130
Compare
|
CI was failing on Pushed a follow-up that routes those three through |
Background
CPython distinguishes two comparison entry points (
Objects/object.c):PyObject_RichCompare— returns the raw__eq__/__ne__result; no identity short-circuitPyObject_RichCompareBool— returnsbool; identity implies equality (and inequality is false on identity), short-circuiting before dispatchCollection membership / equality (
x in [x],[nan] == [nan], set/dict comparisons) go through the bool variant and rely on the short-circuit. RustPython'srich_compare_boolskipped the identity check, so a buggy or raising__eq__propagated even when the operand was the same object.Repro
NaN exhibits the canonical asymmetry that proves the contract:
Fix
Add an identity short-circuit at the top of
rich_compare_boolforEq(returnstrue) andNe(returnsfalse). Ordering ops fall through to_cmpbecause Python does not guarantee reflexivity for</<=/>/>=(e.g. user-defined__lt__may legitimately return False forx < x)._cmpitself is untouched, so==/!=operators continue to invoke__eq__/__ne__exactly as before.Tests unmasked
test_dictviews.TestDictViews.test_compare_errorVerification
53 scenarios across 10 categories — byte-identical with CPython 3.14.4 (
diffempty):<,<=,>,>=) — all raise (no shortcut)list,tuple,set,frozenset,dict,dict.keys,deque)values,items)==and!=forlist/tuple/set/frozenset/dict)n == nFalse,[n] == [n]True,n in [n]True)FalseEqclass cases (where__eq__returnsFalse)count,index,remove)add,union,len)get,setdefault)No regressions across 14 modules (~2,402 tests):
test_dictviews,test_dict,test_set,test_list,test_tuple,test_collections,test_typing,test_descr,test_iter,test_compare,test_class,test_userdict,test_userlist,test_ordered_dict.Out-of-scope finding (recorded for follow-up)
range.__contains__andarray.__contains__returnFalseon type mismatch (e.g.RaisingEq() in range(10)returnsFalsewithout calling__eq__). CPython's specializedrange_containsandarray_containsfall back to the default sequence contains for non-fast-path types, which would invoke__eq__. This divergence is independent of therich_compare_boolchange in this PR — those specialized contains slots bypassbool_eqentirely.Summary by CodeRabbit