Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 134 additions & 42 deletions pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use pyrefly_types::types::CalleeKind;
use pyrefly_types::types::NNModuleType;
use pyrefly_types::types::TArgs;
use pyrefly_types::types::TParams;
use pyrefly_util::owner::Owner;
use pyrefly_util::prelude::SliceExt;
use pyrefly_util::prelude::VecExt;
use pyrefly_util::visit::Visit;
Expand Down Expand Up @@ -51,6 +52,7 @@ use crate::alt::class::class_field::DescriptorBase;
use crate::alt::class::dataclass::ReplaceKind;
use crate::alt::expr::TypeOrExpr;
use crate::alt::nn_module_specials::is_nn_sequential;
use crate::alt::overload::ArgsExpander;
use crate::alt::unwrap::HintRef;
use crate::alt::unwrap::MAX_HINT_WIDTH;
use crate::binding::binding::Key;
Expand Down Expand Up @@ -1214,7 +1216,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
context: Option<&dyn Fn() -> ErrorContext>,
hint: Option<HintRef>,
construct: impl Fn(Option<&Type>) -> ConstructedInstance,
) -> Type {
) -> (Type, bool) {
if let Some(hint) = hint
&& let hints = hint.types()
&& hints.len() <= MAX_HINT_WIDTH
Expand All @@ -1229,13 +1231,16 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
// Take the first successful match. We require the result to be assignable to the
// hint so that, in a case like `x: list[X] | None = [XChild()]`, we choose the
// `list[X]` branch with contextually typed results.
return ret.take(
arguments_range,
errors,
arg_errors,
return_errors,
context,
self,
return (
ret.take(
arguments_range,
errors,
arg_errors,
return_errors,
context,
self,
),
true,
);
}
if ret_no_match_hint.is_none() {
Expand All @@ -1245,25 +1250,31 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
if let Some(ret) = ret_no_match_hint {
// Even if none of the results were assignable to their hints, we still keep the
// first contextually typed result if it only produced specialization errors.
return ret.take(
arguments_range,
errors,
arg_errors,
return_errors,
context,
self,
return (
ret.take(
arguments_range,
errors,
arg_errors,
return_errors,
context,
self,
),
false,
);
}
}
// If the hint is too wide or always produces non-specialization errors, don't use it.
let ret = construct(None);
ret.take(
arguments_range,
errors,
arg_errors,
return_errors,
context,
self,
(
ret.take(
arguments_range,
errors,
arg_errors,
return_errors,
context,
self,
),
false,
)
}

Expand Down Expand Up @@ -1324,26 +1335,106 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
None => (args, keywords),
};

self.construct_with_hint(
arguments_range,
errors,
arg_errors,
return_errors,
context,
HintRef::filter_for_constructor(hint, cls.targs()),
|hint| {
self.construct_class_inner(
cls.clone(),
constructor_kind.clone(),
args,
keywords,
arguments_range,
callee_range,
context,
hint,
)
},
)
let hint = HintRef::filter_for_constructor(hint, cls.targs());
let construct = |args: &[CallArg],
keywords: &[CallKeyword],
errors: &ErrorCollector,
arg_errors: &ErrorCollector,
return_errors: &ErrorCollector,
hint| {
self.construct_with_hint(
arguments_range,
errors,
arg_errors,
return_errors,
context,
hint,
|hint| {
self.construct_class_inner(
cls.clone(),
constructor_kind.clone(),
args,
keywords,
arguments_range,
callee_range,
context,
hint,
)
},
)
};
let hint = match hint {
Some(hint) if hint.types().len() > 1 && hint.types().len() <= MAX_HINT_WIDTH => hint,
hint => return construct(args, keywords, errors, arg_errors, return_errors, hint).0,
};

let unexpanded_errors = self.error_collector();
let unexpanded_arg_errors = self.error_collector();
let unexpanded_return_errors = self.error_collector();
let (unexpanded, matched_hint) = construct(
args,
keywords,
&unexpanded_errors,
&unexpanded_arg_errors,
&unexpanded_return_errors,
Some(hint),
);
if matched_hint {
errors.extend(unexpanded_errors);
arg_errors.extend(unexpanded_arg_errors);
return_errors.extend(unexpanded_return_errors);
return unexpanded;
}

// As with overloads, a union argument may select a different specialization for each
// member. Accept the call only if every expansion matches a member of the union hint.
let mut args_expander = ArgsExpander::new(args.to_vec(), keywords.to_vec(), self);
let owner = Owner::new();
let expansion_errors = self.error_collector();
'outer: while let Some(arg_lists) = args_expander.expand(&expansion_errors, &owner) {
if expansion_errors.has_hard() {
break;
}
let mut rets = Vec::new();
// Keep diagnostics local until every expanded call succeeds.
let chosen_errors = self.error_collector();
let chosen_arg_errors = self.error_collector();
let chosen_return_errors = self.error_collector();
for (expanded_args, expanded_keywords) in arg_lists {
let expanded_errors = self.error_collector();
let expanded_arg_errors = self.error_collector();
let expanded_return_errors = self.error_collector();
let (ret, matched_hint) = construct(
&expanded_args,
&expanded_keywords,
&expanded_errors,
&expanded_arg_errors,
&expanded_return_errors,
Some(hint),
);
if !matched_hint
|| expanded_errors.has_hard()
|| expanded_arg_errors.has_hard()
|| expanded_return_errors.has_hard()
{
continue 'outer;
}
chosen_errors.extend(expanded_errors);
chosen_arg_errors.extend(expanded_arg_errors);
chosen_return_errors.extend(expanded_return_errors);
rets.push(ret);
}
errors.extend(chosen_errors);
arg_errors.extend(expansion_errors);
arg_errors.extend(chosen_arg_errors);
return_errors.extend(chosen_return_errors);
return self.unions(rets);
}

errors.extend(unexpanded_errors);
arg_errors.extend(unexpanded_arg_errors);
return_errors.extend(unexpanded_return_errors);
unexpanded
}

fn construct_class_inner(
Expand Down Expand Up @@ -1740,6 +1831,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
)
},
)
.0
}

fn construct_typed_dict_inner(
Expand Down
61 changes: 61 additions & 0 deletions pyrefly/lib/test/constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,67 @@ def test(f: type[A | B]) -> A | B:
"#,
);

// Regression test for https://github.com/facebook/pyrefly/issues/1134
testcase!(
test_generic_constructor_union,
r#"
class A[T]:
def __init__(self, value: T):
self.value = value

def f(value: int | str):
_: A[int] | A[str] = A(value)

class Pair[T]:
def __init__(self, x: T, y: T): ...

def g(x: int | str, y: int | str):
# Expanding both arguments includes mixed pairs, so the union is not safe.
_: Pair[int] | Pair[str] = Pair(x, y) # E: `Pair[int | str]` is not assignable to `Pair[int] | Pair[str]`
"#,
);

testcase!(
test_generic_constructor_union_diagnostics,
r#"
from typing import cast
from typing_extensions import deprecated

class A[T]:
@deprecated("old constructor")
def __init__(self, value: T) -> None: ...

class B[T]:
def __init__(self, value: T) -> None: ...

@deprecated("old factory")
def old_factory() -> int | str: ...

def f(value: int | str) -> None:
constructor: A[int] | A[str] = A(value) # E: `A.__init__` is deprecated
factory: B[int] | B[str] = B(old_factory()) # E: `old_factory` is deprecated
redundant_cast: B[int] | B[str] = B(cast(int | str, value)) # E: Redundant cast
keyword_factory: B[int] | B[str] = B(value=old_factory()) # E: `old_factory` is deprecated
keyword_cast: B[int] | B[str] = B(value=cast(int | str, value)) # E: Redundant cast
"#,
);

testcase!(
test_generic_constructor_union_rejected_expansion_diagnostics,
r#"
from typing_extensions import deprecated

class Pair[T, U]:
@deprecated("old constructor")
def __init__(self, x: T, y: U) -> None: ...

def f(x: int | str, y: int | str) -> None:
# Expanding x alone accepts the int branch but rejects the str branch.
# Expanding y as well accepts every branch without retaining rejected call errors.
pair: Pair[int, int | str] | Pair[str, int] | Pair[str, str] = Pair(x, y) # E: `Pair.__init__` is deprecated
"#,
);

testcase!(
test_generic_class,
r#"
Expand Down
Loading