From a0d75cb25af392d8cf0d920e5996c496495cfd15 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 24 Jul 2026 13:19:23 +0900 Subject: [PATCH] fix --- conformance/third_party/conformance.exp | 18 ++++++------- conformance/third_party/conformance.result | 5 +--- conformance/third_party/results.json | 8 +++--- pyrefly/lib/alt/call.rs | 28 ++++++++++++++++--- pyrefly/lib/alt/class/class_field.rs | 31 ++++++++++++++++++++++ pyrefly/lib/alt/function.rs | 26 ++++++++++++++++++ pyrefly/lib/solver/subset.rs | 6 +++++ pyrefly/lib/solver/type_order.rs | 5 ++++ pyrefly/lib/test/callable.rs | 5 ++-- 9 files changed, 108 insertions(+), 24 deletions(-) diff --git a/conformance/third_party/conformance.exp b/conformance/third_party/conformance.exp index 872442f7f4..489f35d2de 100644 --- a/conformance/third_party/conformance.exp +++ b/conformance/third_party/conformance.exp @@ -3754,8 +3754,8 @@ { "code": -2, "column": 12, - "concise_description": "revealed type: (x: list[Unknown], y: list[Unknown]) -> Class8[Unknown]", - "description": "revealed type: (x: list[Unknown], y: list[Unknown]) -> Class8[Unknown]", + "concise_description": "revealed type: [T](x: list[T], y: list[T]) -> Class8[T]", + "description": "revealed type: [T](x: list[T], y: list[T]) -> Class8[T]", "line": 184, "name": "reveal-type", "severity": "info", @@ -3764,14 +3764,14 @@ }, { "code": -2, - "column": 12, - "concise_description": "assert_type(Class8[Unknown], Class8[str]) failed", - "description": "assert_type(Class8[Unknown], Class8[str]) failed", - "line": 185, - "name": "assert-type", + "column": 9, + "concise_description": "Argument `list[str]` is not assignable to parameter `y` with type `list[int]`", + "description": "Argument `list[str]` is not assignable to parameter `y` with type `list[int]`", + "line": 186, + "name": "bad-argument-type", "severity": "error", - "stop_column": 41, - "stop_line": 185 + "stop_column": 13, + "stop_line": 186 }, { "code": -2, diff --git a/conformance/third_party/conformance.result b/conformance/third_party/conformance.result index 91efdbb9f9..206eeddcc9 100644 --- a/conformance/third_party/conformance.result +++ b/conformance/third_party/conformance.result @@ -27,10 +27,7 @@ "constructors_call_metaclass.py": [], "constructors_call_new.py": [], "constructors_call_type.py": [], - "constructors_callable.py": [ - "Line 186: Expected 1 errors", - "Line 185: Unexpected errors ['assert_type(Class8[Unknown], Class8[str]) failed']" - ], + "constructors_callable.py": [], "constructors_consistency.py": [], "dataclasses_descriptors.py": [ "Line 32: Unexpected errors ['Cannot set field `y` to data descriptor `Desc1` with inconsistent types\\n Return type `Desc1 | int` of `Desc1.__get__` is not assignable to value type `int` of `Desc1.__set__`']", diff --git a/conformance/third_party/results.json b/conformance/third_party/results.json index b986c48fb0..e6429efa87 100644 --- a/conformance/third_party/results.json +++ b/conformance/third_party/results.json @@ -1,9 +1,9 @@ { "total": 140, - "pass": 134, - "fail": 6, + "pass": 135, + "fail": 5, "pass_rate": 0.96, - "differences": 13, + "differences": 11, "passing": [ "aliases_explicit.py", "aliases_implicit.py", @@ -25,6 +25,7 @@ "constructors_call_metaclass.py", "constructors_call_new.py", "constructors_call_type.py", + "constructors_callable.py", "constructors_consistency.py", "dataclasses_final.py", "dataclasses_frozen.py", @@ -143,7 +144,6 @@ "failing": { "annotations_forward_refs.py": 2, "callables_annotation.py": 1, - "constructors_callable.py": 2, "dataclasses_descriptors.py": 2, "exceptions_context_managers.py": 2, "generics_scoping.py": 4 diff --git a/pyrefly/lib/alt/call.rs b/pyrefly/lib/alt/call.rs index 388c2c70e0..f8cda60d32 100644 --- a/pyrefly/lib/alt/call.rs +++ b/pyrefly/lib/alt/call.rs @@ -1965,6 +1965,22 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } pub fn constructor_to_callable(&self, cls: &ClassType) -> Type { + let new_attr_ty = self.get_dunder_new(cls, false); + self.constructor_to_callable_impl(cls, new_attr_ty, false) + } + + /// Convert a bare class definition while keeping its type parameters generic. + pub fn constructor_to_callable_for_class_def(&self, cls: &ClassType) -> Option { + let new_attr_ty = self.get_dunder_new_for_class_def(cls)?; + Some(self.constructor_to_callable_impl(cls, Some(new_attr_ty), true)) + } + + fn constructor_to_callable_impl( + &self, + cls: &ClassType, + new_attr_ty: Option, + preserve_class_tparams: bool, + ) -> Type { let class_type = self.heap.mk_class_type(cls.clone()); if let Some(metaclass_call_attr_ty) = self.get_metaclass_dunder_call(cls) { // Use the metaclass __call__ directly (ignoring __new__ and __init__) when either: @@ -1989,10 +2005,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { )) }; // Check the __new__ method and whether it comes from object or has been overridden - let (new_attr_ty, overrides_new) = if let Some(t) = self - .get_dunder_new(cls, false) - .and_then(|t| self.bind_dunder_new(&t, cls.clone())) - { + let bind_new = |t: Type| { + if preserve_class_tparams { + self.bind_dunder_new_for_class_def(&t, cls.clone()) + } else { + self.bind_dunder_new(&t, cls.clone()) + } + }; + let (new_attr_ty, overrides_new) = if let Some(t) = new_attr_ty.and_then(bind_new) { if t.callable_return_type(self.heap) .is_some_and(|ret| !self.is_compatible_constructor_return(&ret, cls.class_object())) { diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index e9f464201f..da1db4c2c9 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -4846,6 +4846,37 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } + /// Get `__new__` through class access when its non-receiver parameters use class type params. + pub fn get_dunder_new_for_class_def(&self, cls: &ClassType) -> Option { + let new_member = + self.get_class_member_with_defining_class(cls.class_object(), &dunder::NEW)?; + if new_member.is_defined_on("builtins", "object") { + None + } else { + let new_ty = self + .as_class_attribute( + &dunder::NEW, + &new_member.value, + &ClassBase::ClassDef(cls.clone()), + ) + .as_instance_method()?; + let class_tparams = self.get_class_tparams(cls.class_object()); + let mut uses_class_tparam = false; + new_ty.visit_toplevel_callable(|callable| { + if let Some(callable) = callable.strip_first_param() { + let mut quantifieds = SmallSet::new(); + callable + .params + .visit(&mut |ty| ty.collect_quantifieds(&mut quantifieds)); + uses_class_tparam |= class_tparams + .iter() + .any(|tparam| quantifieds.contains(tparam)); + } + }); + uses_class_tparam.then_some(new_ty) + } + } + fn get_dunder_init_helper(&self, instance: &Instance, get_object_init: bool) -> Option { let init_method = self.get_class_member_with_defining_class(instance.class, &dunder::INIT)?; diff --git a/pyrefly/lib/alt/function.rs b/pyrefly/lib/alt/function.rs index 06f2cd63be..696bf77b65 100644 --- a/pyrefly/lib/alt/function.rs +++ b/pyrefly/lib/alt/function.rs @@ -2397,6 +2397,32 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { ) } + /// Bind `__new__` while keeping class type parameters used by the constructor callable. + pub fn bind_dunder_new_for_class_def(&self, t: &Type, cls: ClassType) -> Option { + let mut bound = self.bind_function( + t, + &self.heap.mk_type_of(self.heap.mk_class_type(cls)), + false, + &mut |a, b| self.is_subset_eq(a, b), + )?; + self.expand_mut(&mut bound); + match bound { + Type::Forall(forall) => { + let Forall { tparams, body } = *forall; + let mut used = SmallSet::new(); + body.visit(&mut |ty| ty.collect_quantifieds(&mut used)); + let tparams = tparams + .iter() + .filter(|tparam| used.contains(*tparam)) + .cloned() + .collect(); + drop(used); + Some(body.forall(Arc::new(TParams::new(tparams)))) + } + _ => Some(bound), + } + } + /// Bind a `__init__` method for constructor callable conversion. /// Strips the first parameter and sets the return type to the first param's type. /// Does not instantiate type variables (they should be inferred at the call site). diff --git a/pyrefly/lib/solver/subset.rs b/pyrefly/lib/solver/subset.rs index 5486feaaf8..54940616fd 100644 --- a/pyrefly/lib/solver/subset.rs +++ b/pyrefly/lib/solver/subset.rs @@ -2332,6 +2332,12 @@ impl<'a, Ans: LookupAnswer> Subset<'a, Ans> { { self.is_subset_eq(&self.type_order.constructor_to_callable(got_cls), want) } + (Type::ClassDef(got), Type::BoundMethod(_) | Type::Callable(_) | Type::Function(_)) + if let Some(constructor) = + self.type_order.constructor_to_callable_for_class_def(got) => + { + self.is_subset_eq(&constructor, want) + } (Type::ClassDef(got), Type::BoundMethod(_) | Type::Callable(_) | Type::Function(_)) => { self.is_subset_eq(&Type::type_of(self.type_order.promote_silently(got)), want) } diff --git a/pyrefly/lib/solver/type_order.rs b/pyrefly/lib/solver/type_order.rs index aead7dd884..948c187353 100644 --- a/pyrefly/lib/solver/type_order.rs +++ b/pyrefly/lib/solver/type_order.rs @@ -172,6 +172,11 @@ impl<'a, Ans: LookupAnswer> TypeOrder<'a, Ans> { self.0.constructor_to_callable(cls) } + pub fn constructor_to_callable_for_class_def(self, cls: &Class) -> Option { + self.0 + .constructor_to_callable_for_class_def(&self.0.as_class_type_unchecked(cls)) + } + pub fn instantiate_fresh_forall(self, forall: Forall) -> (QuantifiedHandle, Type) { self.0.instantiate_fresh_forall(forall) } diff --git a/pyrefly/lib/test/callable.rs b/pyrefly/lib/test/callable.rs index d65da56b62..962ad5da96 100644 --- a/pyrefly/lib/test/callable.rs +++ b/pyrefly/lib/test/callable.rs @@ -1401,7 +1401,6 @@ def test(p4: Proto4[...], p7: Proto7): ); testcase!( - bug = "conformance: Constructor to Callable conversion issues with overloads and __new__", test_constructor_callable_conversion, r#" from typing import Callable, ParamSpec, TypeVar, Self, assert_type, overload, Generic @@ -1435,8 +1434,8 @@ class Class8(Generic[T]): return super().__new__(cls) r8 = accepts_callable(Class8) -# pyrefly incorrectly errors on this - should be OK -assert_type(r8([""], [""]), Class8[str]) # E: assert_type(Class8[Unknown], Class8[str]) failed +assert_type(r8([""], [""]), Class8[str]) +r8([1], [""]) # E: Argument `list[str]` is not assignable to parameter `y` with type `list[int]` "#, );