From 9925e83ead30db6469447bdb0c1f6d4968e5cd52 Mon Sep 17 00:00:00 2001 From: knQzx <75641500+knQzx@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:42:56 +0200 Subject: [PATCH 1/2] detect slots layout conflict in multiple inheritance when two or more base classes define non-empty __slots__, cpython raises a TypeError at runtime. this adds a check for that during class metadata computation and flags it as an InvalidInheritance error. fixes #2916 and #2917 --- pyrefly/lib/alt/class/class_metadata.rs | 31 +++++++++++++++ pyrefly/lib/test/slots.rs | 50 +++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/pyrefly/lib/alt/class/class_metadata.rs b/pyrefly/lib/alt/class/class_metadata.rs index 9b0f032cbd..d510bb8183 100644 --- a/pyrefly/lib/alt/class/class_metadata.rs +++ b/pyrefly/lib/alt/class/class_metadata.rs @@ -402,6 +402,37 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } let extends_abc = self.extends_abc(&bases_with_metadata, metaclass); + // Check for __slots__ layout conflict: if two or more base classes + // define non-empty __slots__, CPython raises TypeError at runtime + // ("multiple bases have instance lay-out conflict"). + { + let bases_with_nonempty_slots: Vec<&Class> = bases_with_metadata + .iter() + .filter(|(_, metadata)| { + metadata + .slots_info() + .is_some_and(|si| !si.names.is_empty()) + }) + .map(|(base, _)| base) + .collect(); + if bases_with_nonempty_slots.len() > 1 { + let names: Vec = bases_with_nonempty_slots + .iter() + .map(|b| format!("`{}`", b.name())) + .collect(); + self.error( + errors, + cls.range(), + ErrorInfo::Kind(ErrorKind::InvalidInheritance), + format!( + "Class `{}` has multiple base classes with non-empty `__slots__` ({}), which causes a TypeError at runtime", + cls.name(), + names.join(", "), + ), + ); + } + } + // Compute final base class list. let bases = if is_typed_dict && bases_with_metadata.is_empty() { // This is a "fallback" class that contains attributes that are available on all TypedDict subclasses. diff --git a/pyrefly/lib/test/slots.rs b/pyrefly/lib/test/slots.rs index 60dbc689bb..7c2504bcb2 100644 --- a/pyrefly/lib/test/slots.rs +++ b/pyrefly/lib/test/slots.rs @@ -281,7 +281,6 @@ c.x = 2 # OK: descriptor __set__ handles this, not instance storage // https://github.com/facebook/pyrefly/issues/2917 testcase!( - bug = "Should detect instance layout conflict when multiple bases have __slots__", test_slots_multiple_inheritance_layout_conflict, r#" class Left: @@ -292,13 +291,12 @@ class Right: # Inheriting from two classes that both define non-empty __slots__ # causes a TypeError at runtime. -class Combined(Left, Right): ... +class Combined(Left, Right): ... # E: multiple base classes with non-empty `__slots__` "#, ); // https://github.com/facebook/pyrefly/issues/2916 testcase!( - bug = "Should detect instance layout conflict even with identical slot names", test_slots_layout_conflict_same_names, r#" class First: @@ -308,7 +306,51 @@ class Second: __slots__ = ("x",) # Even though the slot names match, these are different C-level layouts. -class Both(First, Second): ... +class Both(First, Second): ... # E: multiple base classes with non-empty `__slots__` +"#, +); + +testcase!( + test_slots_layout_conflict_empty_slots_ok, + r#" +class A: + __slots__ = ("x",) + +class B: + __slots__ = () + +# One base has non-empty slots, the other has empty slots - this is fine. +class C(A, B): ... +"#, +); + +testcase!( + test_slots_layout_conflict_both_empty_ok, + r#" +class A: + __slots__ = () + +class B: + __slots__ = () + +# Both bases have empty slots - no conflict. +class C(A, B): ... +"#, +); + +testcase!( + test_slots_layout_conflict_three_bases, + r#" +class A: + __slots__ = ("x",) + +class B: + __slots__ = ("y",) + +class C: + __slots__ = ("z",) + +class D(A, B, C): ... # E: multiple base classes with non-empty `__slots__` "#, ); From f3359a3cfbeb077b10f90396db5149f9a3396247 Mon Sep 17 00:00:00 2001 From: knQzx <75641500+knQzx@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:11:15 +0200 Subject: [PATCH 2/2] run cargo fmt --- pyrefly/lib/alt/class/class_metadata.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyrefly/lib/alt/class/class_metadata.rs b/pyrefly/lib/alt/class/class_metadata.rs index d510bb8183..607cf066f7 100644 --- a/pyrefly/lib/alt/class/class_metadata.rs +++ b/pyrefly/lib/alt/class/class_metadata.rs @@ -409,9 +409,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { let bases_with_nonempty_slots: Vec<&Class> = bases_with_metadata .iter() .filter(|(_, metadata)| { - metadata - .slots_info() - .is_some_and(|si| !si.names.is_empty()) + metadata.slots_info().is_some_and(|si| !si.names.is_empty()) }) .map(|(base, _)| base) .collect();