Skip to content

Commit 3fb39d7

Browse files
rchen152meta-codesync[bot]
authored andcommitted
Do not treat Any in type variable bounds as hints
Summary: Fixes #3265. ___ landed-with-radar-review Differential Revision: D128999763 fbshipit-source-id: 06485aed9ac0e503c3e305c7ba457358b0c8b12d
1 parent 052d997 commit 3fb39d7

5 files changed

Lines changed: 123 additions & 116 deletions

File tree

‎pyrefly/lib/alt/expr.rs‎

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ use crate::alt::shape_extension::is_int_tuple_bound;
100100
use crate::alt::solve::TypeFormContext;
101101
use crate::alt::solve::UntypeContext;
102102
use crate::alt::unwrap::HintRef;
103-
use crate::alt::unwrap::ListElementHint;
104103
use crate::binding::binding::Binding;
105104
use crate::binding::binding::BindingClass;
106105
use crate::binding::binding::Key;
@@ -1261,23 +1260,14 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
12611260
hint,
12621261
|hint| self.decompose_list(hint),
12631262
|elem_hint, hint| {
1264-
let (elem_hint, partial_fallback) = elem_hint
1265-
.map(ListElementHint::into_parts)
1266-
.unwrap_or_default();
12671263
self.ifs_infer(&x.generators, errors);
12681264
let elem_ty = self.expr_infer_with_hint_promote(
12691265
&x.elt,
12701266
HintRef::with_ty_opt(hint, elem_hint.as_ref()),
12711267
errors,
12721268
HintCoercion::BestEffort,
12731269
);
1274-
let ty = self.heap.mk_class_type(self.stdlib.list(elem_ty));
1275-
if let Some(partial_fallback) = partial_fallback {
1276-
self.solver()
1277-
.replace_unresolved_partials(ty, &partial_fallback)
1278-
} else {
1279-
ty
1280-
}
1270+
self.heap.mk_class_type(self.stdlib.list(elem_ty))
12811271
},
12821272
),
12831273
Expr::SetComp(x) => self.infer_with_decomposed_hint(
@@ -3521,37 +3511,25 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
35213511
fn list_literal_infer(
35223512
&self,
35233513
list: &ExprList,
3524-
elt_hint: Option<ListElementHint>,
3514+
elt_hint: Option<Type>,
35253515
hint: Option<HintRef>,
35263516
errors: &ErrorCollector,
35273517
) -> Type {
35283518
if list.is_empty() {
3529-
let elem_ty = match elt_hint {
3530-
Some(ListElementHint::Hint(elem_hint)) => elem_hint,
3531-
Some(ListElementHint::UninformativeAny(_)) | None => self
3532-
.solver()
3519+
let elem_ty = elt_hint.unwrap_or_else(|| {
3520+
self.solver()
35333521
.fresh_partial_contained(self.uniques, list.range)
3534-
.to_type(self.heap),
3535-
};
3522+
.to_type(self.heap)
3523+
});
35363524
self.heap.mk_class_type(self.stdlib.list(elem_ty))
35373525
} else {
3538-
let (elt_hint, partial_fallback) = elt_hint
3539-
.map(ListElementHint::into_parts)
3540-
.unwrap_or_default();
35413526
let elem_tys = self.elts_infer(
35423527
&list.elts,
35433528
HintRef::with_ty_opt(hint, elt_hint.as_ref()),
35443529
errors,
35453530
);
3546-
let ty = self
3547-
.heap
3548-
.mk_class_type(self.stdlib.list(self.unions(elem_tys)));
3549-
if let Some(partial_fallback) = partial_fallback {
3550-
self.solver()
3551-
.replace_unresolved_partials(ty, &partial_fallback)
3552-
} else {
3553-
ty
3554-
}
3531+
self.heap
3532+
.mk_class_type(self.stdlib.list(self.unions(elem_tys)))
35553533
}
35563534
}
35573535

‎pyrefly/lib/alt/unwrap.rs‎

Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,6 @@ use crate::types::types::Var;
2727
/// individually, as doing so would be prohibitively expensive.
2828
pub const MAX_HINT_WIDTH: usize = 32;
2929

30-
/// A contextual element hint for list literals and comprehensions.
31-
pub(crate) enum ListElementHint {
32-
/// A hint that should be applied while inferring the element.
33-
Hint(Type),
34-
/// An `Any` hint that is ignored for inference but retained as a fallback.
35-
///
36-
/// The contained type is always `Any`, preserving its original `AnyStyle`.
37-
UninformativeAny(Type),
38-
}
39-
40-
impl ListElementHint {
41-
/// Split the hint into an inference hint and a fallback.
42-
pub(crate) fn into_parts(self) -> (Option<Type>, Option<Type>) {
43-
match self {
44-
Self::Hint(ty) => (Some(ty), None),
45-
Self::UninformativeAny(ty) => (None, Some(ty)),
46-
}
47-
}
48-
}
49-
5030
// The error collector is None for a "soft" type hint, where we try to
5131
// match an expression against a hint, but fall back to the inferred type
5232
// without any errors if the hint is incompatible.
@@ -367,27 +347,13 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
367347
}
368348
}
369349

370-
pub(crate) fn decompose_list(&self, hint: &Type) -> Option<ListElementHint> {
350+
pub fn decompose_list(&self, hint: &Type) -> Option<Type> {
371351
let elem = self.fresh_var();
372352
let list_type = self
373353
.heap
374354
.mk_class_type(self.stdlib.list(elem.to_type(self.heap)));
375355
if self.is_subset_eq(&list_type, hint) {
376-
match self.resolve_var_opt(hint, elem) {
377-
Some(elem_hint)
378-
if elem_hint.is_any()
379-
&& hint
380-
.collect_maybe_placeholder_vars()
381-
.into_iter()
382-
.any(|var| self.solver().var_is_quantified(var)) =>
383-
{
384-
// An `Any` element hint obtained while the container hint still has an unsolved
385-
// generic variable carries no information about the literal's elements.
386-
Some(ListElementHint::UninformativeAny(elem_hint))
387-
}
388-
Some(elem_hint) => Some(ListElementHint::Hint(elem_hint)),
389-
None => None,
390-
}
356+
self.resolve_var_opt(hint, elem)
391357
} else {
392358
None
393359
}
@@ -544,26 +510,41 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
544510
}
545511
}
546512

547-
pub fn decompose_hint<'b, D>(
513+
pub fn decompose_hint<D>(
548514
&self,
549-
hint: HintRef<'_, 'b>,
550-
decompose: impl Fn(&'b Type) -> Option<D>,
515+
hint: HintRef,
516+
decompose: impl Fn(&Type) -> Option<D>,
551517
) -> Vec<D> {
552518
hint.types()
553519
.iter()
554520
.filter_map(|hint| self.decompose_one_hint(hint, &decompose))
555521
.collect()
556522
}
557523

558-
fn decompose_one_hint<'b, D>(
524+
fn decompose_one_hint<D>(
559525
&self,
560-
hint: &'b Type,
561-
decompose: impl FnOnce(&'b Type) -> Option<D>,
526+
hint: &Type,
527+
decompose: impl FnOnce(&Type) -> Option<D>,
562528
) -> Option<D> {
529+
let hint_vars = hint.collect_maybe_placeholder_vars();
563530
// Decomposing a hint should not have any side effects.
564-
let snapshot = self
565-
.solver()
566-
.snapshot_exact_vars(&hint.collect_maybe_placeholder_vars());
531+
let snapshot = self.solver().snapshot_exact_vars(&hint_vars);
532+
// An `Any` in an unconstrained type variable's bound supplies no contextual information.
533+
// Fresh vars in those positions preserve the bound's structural constraints.
534+
for var in hint_vars {
535+
if let Some(bound) = self.solver().unconstrained_typevar_bound(var) {
536+
let mut transformed = false;
537+
let bound = bound.transform(&mut |ty| {
538+
if ty.is_any() {
539+
*ty = self.fresh_var().to_type(self.heap);
540+
transformed = true;
541+
}
542+
});
543+
if transformed {
544+
self.solver().set_typevar_bound(var, bound);
545+
}
546+
}
547+
}
567548
let ret = decompose(hint);
568549
self.solver().restore_vars(snapshot);
569550
ret

‎pyrefly/lib/solver/solver.rs‎

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -795,30 +795,6 @@ impl Solver {
795795
)
796796
}
797797

798-
/// Replace unresolved empty-container element types with `fallback` in a copy of `ty`.
799-
pub(crate) fn replace_unresolved_partials(&self, mut ty: Type, fallback: &Type) -> Type {
800-
self.expand_mut(&mut ty);
801-
let partials: SmallSet<_> = {
802-
let variables = self.variables.lock();
803-
ty.collect_maybe_placeholder_vars()
804-
.into_iter()
805-
.filter(|var| {
806-
matches!(
807-
&*variables.get(*var),
808-
Variable::PartialQuantified(_) | Variable::PartialContained(_)
809-
)
810-
})
811-
.collect()
812-
};
813-
ty.transform_mut(&mut |ty| {
814-
if matches!(ty, Type::Var(var) if partials.contains(var)) {
815-
*ty = fallback.clone();
816-
}
817-
});
818-
self.simplify_mut(&mut ty);
819-
ty
820-
}
821-
822798
/// Returns true if the given type is a Var that points to a partial variable.
823799
pub fn is_partial(&self, ty: &Type) -> bool {
824800
if let Type::Var(v) = ty {
@@ -835,6 +811,33 @@ impl Solver {
835811
matches!(&*variables.get(var), Variable::Quantified { .. })
836812
}
837813

814+
/// The bound of the type variable that `var` stands for, if inference has not yet given it a
815+
/// solution or bounds of its own.
816+
pub(crate) fn unconstrained_typevar_bound(&self, var: Var) -> Option<Type> {
817+
let variables = self.variables.lock();
818+
match &*variables.get(var) {
819+
Variable::Quantified { quantified, bounds } if bounds.is_empty() => {
820+
match quantified.restriction() {
821+
Restriction::Bound(bound) => Some(bound.clone()),
822+
_ => None,
823+
}
824+
}
825+
_ => None,
826+
}
827+
}
828+
829+
/// Replace a type variable's declared bound during speculative hint decomposition.
830+
pub(crate) fn set_typevar_bound(&self, var: Var, bound: Type) {
831+
let variables = self.variables.lock();
832+
let mut variable = variables.get_mut(var);
833+
let Variable::Quantified { quantified, .. } = &mut *variable else {
834+
unreachable!("only an unconstrained type variable has its bound replaced")
835+
};
836+
*quantified = quantified
837+
.clone()
838+
.with_restriction(Restriction::Bound(bound));
839+
}
840+
838841
/// The vars an argument could fill in that are still waiting for an answer.
839842
pub(crate) fn unsolved_argument_vars(&self, argument: &MatchedArgument) -> Vec<Var> {
840843
let variables = self.variables.lock();

‎pyrefly/lib/test/calls.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,14 +1021,13 @@ reduce(max, [1,2])
10211021
// https://github.com/facebook/pyrefly/issues/3265
10221022
// All of these iterators should have element type int without diagnostics.
10231023
testcase!(
1024-
bug = "iter loses element types for set and dict literals",
10251024
test_iter_container_literal,
10261025
r#"
10271026
from typing import Iterator, assert_type
10281027
10291028
assert_type(iter([0]), Iterator[int])
1030-
assert_type(iter({0}), Iterator[int]) # E: assert_type(Iterator[Any], Iterator[int]) failed
1031-
assert_type(iter({0: 0}), Iterator[int]) # E: assert_type(Iterator[Any], Iterator[int]) failed
1029+
assert_type(iter({0}), Iterator[int])
1030+
assert_type(iter({0: 0}), Iterator[int])
10321031
assert_type(iter([x for x in [0]]), Iterator[int])
10331032
assert_type(iter({x for x in [0]}), Iterator[int])
10341033
assert_type(iter({x: x for x in [0]}), Iterator[int])

‎pyrefly/lib/test/contextual.rs‎

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -236,46 +236,92 @@ assert_type(keep([]), list[Any])
236236
"#,
237237
);
238238

239+
testcase!(
240+
test_any_bounded_typevar_preserves_structure,
241+
r#"
242+
from typing import Any, assert_type, overload
243+
244+
class Array[S, D]: ...
245+
246+
@overload
247+
def stack[S: tuple[int, int, *tuple[Any, ...]], D](
248+
xs: list[Array[S, D]],
249+
) -> Array[S, D]: ...
250+
@overload
251+
def stack[D](xs: list[Array[tuple[int], D]]) -> Array[tuple[int, int], D]: ...
252+
def stack(xs):
253+
return Array()
254+
255+
a: Array[tuple[int], int] = Array()
256+
assert_type(stack([a, a]), Array[tuple[int, int], int])
257+
b: Array[tuple[int, int, int], str] = Array()
258+
assert_type(stack([b, b]), Array[tuple[int, int, int], str])
259+
260+
def keep[T: dict[float, Any]](x: T) -> T: ...
261+
assert_type(keep({0: "a"}), dict[float, str])
262+
"#,
263+
);
264+
265+
testcase!(
266+
test_any_bounded_typevar_preserves_subclass,
267+
r#"
268+
from typing import Any, assert_type
269+
270+
class Scalar[T]:
271+
def item(self) -> T: ...
272+
class Float32(Scalar[float]): ...
273+
class Array[S, D]:
274+
def scalar(self) -> D: ...
275+
276+
def stack[T: Scalar[Any]](
277+
xs: list[T | Array[tuple[int], T]],
278+
) -> Array[tuple[int, int], T]: ...
279+
280+
a: Array[tuple[int], Float32] = Array()
281+
assert_type(stack([a, a]), Array[tuple[int, int], Float32])
282+
"#,
283+
);
284+
285+
// The `Any` in the element bound supplies no hint for a nested empty literal.
239286
testcase!(
240287
test_any_bounded_typevar_nested_empty_literal,
241288
r#"
242289
from typing import Any, reveal_type
243290
244291
def keep[T: list[Any]](x: T) -> T: ...
245292
246-
reveal_type(keep([[]])) # E: revealed type: list[list[Any]]
247-
reveal_type(keep([[], [1]])) # E: revealed type: list[list[int] | list[Any]]
248-
reveal_type(keep([[] for _ in [0]])) # E: revealed type: list[list[Any]]
249-
reveal_type(keep([set()])) # E: revealed type: list[set[Any]]
250-
reveal_type(keep([list()])) # E: revealed type: list[list[Any]]
251-
reveal_type(keep([{}])) # E: revealed type: list[dict[Any, Any]]
293+
reveal_type(keep([[]])) # E: revealed type: list[list[@_]]
294+
reveal_type(keep([[], [1]])) # E: revealed type: list[list[int] | list[@_]]
295+
reveal_type(keep([[] for _ in [0]])) # E: revealed type: list[list[@_]]
296+
reveal_type(keep([set()])) # E: revealed type: list[set[@_]]
297+
reveal_type(keep([list()])) # E: revealed type: list[list[@_]]
298+
reveal_type(keep([{}])) # E: revealed type: list[dict[@_, @_]]
252299
"#,
253300
);
254301

255302
// https://github.com/facebook/pyrefly/issues/3265
256303
// Any in a bound should preserve the concrete element types asserted below.
257304
testcase!(
258-
bug = "Any bounds erase inferred container element types",
259305
test_any_bounded_typevar_container_hints,
260306
r#"
261307
from typing import Any, Iterator, Protocol, assert_type
262308
263309
def keep_set[T: set[Any]](x: T) -> T: ...
264310
def keep_dict[T: dict[Any, Any]](x: T) -> T: ...
265-
assert_type(keep_set({0}), set[int]) # E: assert_type(set[Any], set[int]) failed
266-
assert_type(keep_set({x for x in [0]}), set[int]) # E: assert_type(set[Any], set[int]) failed
267-
assert_type(keep_dict({0: "a"}), dict[int, str]) # E: assert_type(dict[Any, Any], dict[int, str]) failed
268-
assert_type(keep_dict({x: str(x) for x in [0]}), dict[int, str]) # E: assert_type(dict[Any, Any], dict[int, str]) failed
311+
assert_type(keep_set({0}), set[int])
312+
assert_type(keep_set({x for x in [0]}), set[int])
313+
assert_type(keep_dict({0: "a"}), dict[int, str])
314+
assert_type(keep_dict({x: str(x) for x in [0]}), dict[int, str])
269315
assert_type(keep_dict({}), dict[Any, Any])
270-
assert_type(keep_dict({0: []}), dict[int, list[Any]]) # E: assert_type(dict[Any, Any], dict[int, list[Any]]) failed
316+
assert_type(keep_dict({0: []}), dict[int, list[Any]])
271317
272318
class ReturnsIterator[T](Protocol):
273319
def __iter__(self) -> T: ...
274320
275321
def iterator[I: Iterator[Any]](x: ReturnsIterator[I]) -> I: ...
276322
assert_type(iterator([0]), Iterator[int])
277-
assert_type(iterator({0}), Iterator[int]) # E: assert_type(Iterator[Any], Iterator[int]) failed
278-
assert_type(iterator({0: "a"}), Iterator[int]) # E: assert_type(Iterator[Any], Iterator[int]) failed
323+
assert_type(iterator({0}), Iterator[int])
324+
assert_type(iterator({0: "a"}), Iterator[int])
279325
280326
# Concrete bounds and explicit Any annotations still supply contextual types.
281327
def floats[T: list[float]](x: T) -> T: ...

0 commit comments

Comments
 (0)