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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 55 additions & 13 deletions pyrefly/lib/solver/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use crate::types::module::ModuleType;
use crate::types::simplify::simplify_tuples;
use crate::types::simplify::unions;
use crate::types::simplify::unions_with_literals;
use crate::types::type_var::Restriction;
use crate::types::types::TParams;
use crate::types::types::Type;
use crate::types::types::Var;
Expand Down Expand Up @@ -1552,20 +1553,61 @@ impl<'a, Ans: LookupAnswer> Subset<'a, Ans> {
}
Variable::Quantified(q) | Variable::PartialQuantified(q) => {
let name = q.name.clone();
let bound = q.restriction().as_type(self.type_order.stdlib());
let restriction = q.restriction().clone();
drop(v1_ref);
variables.update(*v1, Variable::Answer(t2.clone()));
drop(variables);
if let Err(e) = self.is_subset_eq(t2, &bound) {
self.solver.instantiation_errors.write().insert(
*v1,
TypeVarSpecializationError {
name,
got: t2.clone(),
want: bound,
error: e,
},
);

// Per PEP 484, constrained TypeVars must be inferred to exactly one of
// their constraint types. When checking `Var(TypeVar) <: ConcreteType`,
// we find a constraint that satisfies this relationship.
//
// Example: For `AnyStr = TypeVar("AnyStr", str, bytes)` checking
// `AnyStr <: Buffer`, we find `bytes <: Buffer` succeeds, so we
// infer `AnyStr = bytes` rather than the invalid `AnyStr = Buffer`.
//
// See: https://github.com/facebook/pyrefly/issues/2221
match &restriction {
Restriction::Constraints(constraints) => {
// Find a constraint C such that C <: t2
let valid_constraint = constraints
.iter()
.find(|c| self.is_subset_eq(c, t2).is_ok());

if let Some(constraint) = valid_constraint {
variables.update(*v1, Variable::Answer(constraint.clone()));
drop(variables);
} else {
// No constraint satisfies the relationship - record error
let bound = restriction.as_type(self.type_order.stdlib());
variables.update(*v1, Variable::Answer(t2.clone()));
drop(variables);
self.solver.instantiation_errors.write().insert(
*v1,
TypeVarSpecializationError {
name,
got: t2.clone(),
want: bound,
error: SubsetError::Other,
},
);
}
}
Restriction::Bound(_) | Restriction::Unrestricted => {
// For bounded or unrestricted TypeVars, check t2 against bound
let bound = restriction.as_type(self.type_order.stdlib());
variables.update(*v1, Variable::Answer(t2.clone()));
drop(variables);
if let Err(e) = self.is_subset_eq(t2, &bound) {
self.solver.instantiation_errors.write().insert(
*v1,
TypeVarSpecializationError {
name,
got: t2.clone(),
want: bound,
error: e,
},
);
}
}
}
Ok(())
}
Expand Down
61 changes: 61 additions & 0 deletions pyrefly/lib/test/generic_restrictions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,3 +1040,64 @@ def f[T, U: int, V = str](x: T, y: U, z: V) -> tuple[T, U, V]: ...
reveal_type(f) # E: revealed type: [T, U: int, V = str](x: T, y: U, z: V) -> tuple[T, U, V]
"#,
);

// https://github.com/facebook/pyrefly/issues/2221
// Tests that constrained TypeVars are correctly inferred when checking Var <: ConcreteType.
// When AnyStr (constrained to str | bytes) is checked against Buffer,
// we should find that bytes <: Buffer and infer AnyStr = bytes,
// rather than trying to assign Buffer to AnyStr directly.
testcase!(
test_constrained_typevar_protocol_inference,
r#"
import shutil
import urllib.request as request

# urlopen returns Any, so the first argument contributes no constraint
# BufferedWriter.write accepts Buffer, and bytes <: Buffer
# So AnyStr should be inferred as bytes (not Buffer), and this should not error
with request.urlopen("https://example.com") as remote, open("out.html", 'wb') as local:
shutil.copyfileobj(remote, local)
"#,
);

// Same test but more minimal - directly using Any
testcase!(
test_constrained_typevar_with_any_argument,
r#"
from typing import Any, TypeVar, Protocol
from typing_extensions import Buffer

AnyStr = TypeVar("AnyStr", str, bytes)

class SupportsRead(Protocol[AnyStr]):
def read(self, length: int = ...) -> AnyStr: ...

class SupportsWrite(Protocol[AnyStr]):
def write(self, s: AnyStr) -> object: ...

def copyfileobj(fsrc: SupportsRead[AnyStr], fdst: SupportsWrite[AnyStr]) -> None: ...

def test(remote: Any, local: Any) -> None:
# Both are Any, so no constraint inference happens
copyfileobj(remote, local)
"#,
);

// Test that errors are still reported when no constraint satisfies the subtype relationship
testcase!(
test_constrained_typevar_no_valid_constraint,
r#"
from typing import TypeVar

T = TypeVar("T", str, int)

def accept_t(x: T) -> T:
return x

def get_float() -> float:
return 1.0

# Neither str nor int is a subtype of float, so this should error
accept_t(get_float()) # E: `float` is not assignable to upper bound `int | str` of type variable `T`
"#,
);
Loading