Skip to content

Commit b83ffef

Browse files
migeed-zmeta-codesync[bot]
authored andcommitted
Promote implicit literals for module-level variables
Summary: Module-level assignments like `timeout = 100` infer `Literal[100]`, but protocols expect `timeout: int`. This caused the `protocols_modules.py` conformance test to fail. We fix this by promoting implicit literals at two use sites where module-level variables are read from outside their defining scope: 1. Cross-barrier function reads: when a function reads a module-level variable (across a flow barrier), we use `Binding::PromoteForward` instead of `Binding::Forward`. This widens implicit literals to their base types, so `def foo(): reveal_type(x)` gives `int` for `x = 42`. The `is_module_scope` flag on `NameReadInfo::Anywhere` ensures we only promote reads from module scope, not from enclosing functions. 2. Module exports: unannotated exports promote implicit literals so that cross-module access like `import mod; mod.x` gives `int`. Flow-sensitive reads at the same level are unchanged — `x = 42; reveal_type(x)` still gives `Literal[42]`. ALL_CAPS names are excluded from promotion as conventional constants. Annotated exports (including `Final`) are unchanged since the annotation determines the type. Reviewed By: rchen152 Differential Revision: D98803805 fbshipit-source-id: e7784ad9be3b7a8a32bfd280b31f6e1e2f273069
1 parent 400db01 commit b83ffef

21 files changed

Lines changed: 231 additions & 78 deletions

File tree

conformance/third_party/conformance.exp

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9655,22 +9655,11 @@
96559655
}
96569656
],
96579657
"protocols_modules.py": [
9658-
{
9659-
"code": -2,
9660-
"column": 17,
9661-
"concise_description": "`Module[_protocols_modules1]` is not assignable to `Options1`",
9662-
"description": "`Module[_protocols_modules1]` is not assignable to `Options1`\n `Module[_protocols_modules1].timeout` has type `Literal[100]`, which is not consistent with `int` in `Options1.timeout` (the type of read-write attributes cannot be changed)",
9663-
"line": 25,
9664-
"name": "bad-assignment",
9665-
"severity": "error",
9666-
"stop_column": 36,
9667-
"stop_line": 25
9668-
},
96699658
{
96709659
"code": -2,
96719660
"column": 17,
96729661
"concise_description": "`Module[_protocols_modules1]` is not assignable to `Options2`",
9673-
"description": "`Module[_protocols_modules1]` is not assignable to `Options2`\n `Module[_protocols_modules1].timeout` has type `Literal[100]`, which is not consistent with `str` in `Options2.timeout` (the type of read-write attributes cannot be changed)",
9662+
"description": "`Module[_protocols_modules1]` is not assignable to `Options2`\n `Module[_protocols_modules1].timeout` has type `int`, which is not consistent with `str` in `Options2.timeout` (the type of read-write attributes cannot be changed)",
96749663
"line": 26,
96759664
"name": "bad-assignment",
96769665
"severity": "error",

conformance/third_party/conformance.result

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,7 @@
147147
"protocols_explicit.py": [],
148148
"protocols_generic.py": [],
149149
"protocols_merging.py": [],
150-
"protocols_modules.py": [
151-
"Line 25: Unexpected errors ['`Module[_protocols_modules1]` is not assignable to `Options1`\\n `Module[_protocols_modules1].timeout` has type `Literal[100]`, which is not consistent with `int` in `Options1.timeout` (the type of read-write attributes cannot be changed)']"
152-
],
150+
"protocols_modules.py": [],
153151
"protocols_recursive.py": [],
154152
"protocols_runtime_checkable.py": [],
155153
"protocols_self.py": [],

conformance/third_party/results.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"total": 139,
3-
"pass": 126,
4-
"fail": 13,
3+
"pass": 127,
4+
"fail": 12,
55
"pass_rate": 0.91,
6-
"differences": 32,
6+
"differences": 31,
77
"passing": [
88
"aliases_explicit.py",
99
"aliases_newtype.py",
@@ -101,6 +101,7 @@
101101
"protocols_explicit.py",
102102
"protocols_generic.py",
103103
"protocols_merging.py",
104+
"protocols_modules.py",
104105
"protocols_recursive.py",
105106
"protocols_runtime_checkable.py",
106107
"protocols_self.py",
@@ -144,7 +145,6 @@
144145
"generics_self_basic.py": 2,
145146
"generics_self_usage.py": 1,
146147
"generics_typevartuple_basic.py": 1,
147-
"protocols_modules.py": 1,
148148
"typeforms_typeform.py": 2
149149
},
150150
"comment": "@generated"

crates/pyrefly_types/src/types.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,29 @@ impl Type {
17011701
sigs
17021702
}
17031703

1704+
fn widen_one_implicit_literal(ty: &mut Type, stdlib: &Stdlib) {
1705+
match &*ty {
1706+
Type::Literal(lit) if lit.style == LitStyle::Implicit => {
1707+
*ty = lit.value.general_class_type(stdlib).clone().to_type()
1708+
}
1709+
Type::LiteralString(LitStyle::Implicit) => *ty = stdlib.str().clone().to_type(),
1710+
_ => {}
1711+
}
1712+
}
1713+
1714+
/// Like `promote_implicit_literals` but only recurses into unions.
1715+
pub fn promote_shallow_implicit_literals(mut self, stdlib: &Stdlib) -> Type {
1716+
match &mut self {
1717+
Type::Union(union) => {
1718+
for member in &mut union.members {
1719+
Self::widen_one_implicit_literal(member, stdlib);
1720+
}
1721+
}
1722+
_ => Self::widen_one_implicit_literal(&mut self, stdlib),
1723+
}
1724+
self
1725+
}
1726+
17041727
pub fn promote_implicit_literals(mut self, stdlib: &Stdlib) -> Type {
17051728
fn g(ty: &mut Type, f: &mut dyn FnMut(&mut Type)) {
17061729
// Don't recurse into NNModule fields — they carry captured constructor
@@ -1712,12 +1735,8 @@ impl Type {
17121735
ty.recurse_mut(&mut |ty| g(ty, f));
17131736
f(ty);
17141737
}
1715-
g(&mut self, &mut |ty| match &ty {
1716-
Type::Literal(lit) if lit.style == LitStyle::Implicit => {
1717-
*ty = lit.value.general_class_type(stdlib).clone().to_type()
1718-
}
1719-
Type::LiteralString(LitStyle::Implicit) => *ty = stdlib.str().clone().to_type(),
1720-
_ => {}
1738+
g(&mut self, &mut |ty| {
1739+
Self::widen_one_implicit_literal(ty, stdlib)
17211740
});
17221741
self
17231742
}

pyrefly/lib/alt/expr.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,15 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
286286
if Ast::is_synthesized_empty_name(x) {
287287
TypeInfo::of_ty(self.heap.mk_any_error())
288288
} else {
289-
self.get(&Key::BoundName(ShortIdentifier::expr_name(x)))
290-
.arc_clone()
289+
let result = self
290+
.get(&Key::BoundName(ShortIdentifier::expr_name(x)))
291+
.arc_clone();
292+
// Complements PromoteForward for seeded captures.
293+
if self.bindings().should_promote_at_range(x.range) {
294+
result.map_ty(|ty| ty.promote_shallow_implicit_literals(self.stdlib))
295+
} else {
296+
result
297+
}
291298
}
292299
}
293300
Expr::Attribute(x) => {

pyrefly/lib/alt/function.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1592,7 +1592,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
15921592
fn step_pred(&self, pred: &mut Option<Idx<Key>>) -> Option<DecoratedFunction> {
15931593
let pred_idx = (*pred)?;
15941594
let mut b = self.bindings().get(pred_idx);
1595-
while let Binding::Forward(k) | Binding::ForwardToFirstUse(k) = b {
1595+
while let Binding::Forward(k) | Binding::PromoteForward(k) | Binding::ForwardToFirstUse(k) =
1596+
b
1597+
{
15961598
b = self.bindings().get(*k);
15971599
}
15981600
if let Binding::Function(idx, pred_, _) = b {

pyrefly/lib/alt/solve.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,6 +1948,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
19481948
if let Binding::Forward(fwd) | Binding::ForwardToFirstUse(fwd) = binding {
19491949
return self.get_idx(*fwd);
19501950
}
1951+
if let Binding::PromoteForward(fwd) = binding {
1952+
return Arc::new(self.resolve_promote_forward(*fwd));
1953+
}
19511954
// Inline first-use pinning for NameAssign.
19521955
let mut type_info = if let Binding::NameAssign(na) = binding
19531956
&& self.solver().infer_with_first_use
@@ -4266,9 +4269,16 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
42664269
type_info
42674270
}
42684271

4272+
fn resolve_promote_forward(&self, fwd: Idx<Key>) -> TypeInfo {
4273+
self.get_idx(fwd)
4274+
.arc_clone()
4275+
.map_ty(|ty| ty.promote_shallow_implicit_literals(self.stdlib))
4276+
}
4277+
42694278
fn binding_to_type_info(&self, binding: &Binding, errors: &ErrorCollector) -> TypeInfo {
42704279
match binding {
42714280
Binding::Forward(k) => self.get_idx(*k).arc_clone(),
4281+
Binding::PromoteForward(k) => self.resolve_promote_forward(*k),
42724282
Binding::ForwardToFirstUse(k) => {
42734283
if let Some(def_idx) = self.def_idx_for_forward_to_first_use(*k)
42744284
&& let Some(type_info) = self.check_partial_answer(def_idx)
@@ -4691,6 +4701,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
46914701
fn binding_to_type(&self, binding: &Binding, errors: &ErrorCollector) -> Type {
46924702
match binding {
46934703
Binding::Forward(..)
4704+
| Binding::PromoteForward(..)
46944705
| Binding::ForwardToFirstUse(..)
46954706
| Binding::Phi(..)
46964707
| Binding::LoopPhi(..)

pyrefly/lib/alt/traits.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ impl<Ans: LookupAnswer> Solve<Ans> for KeyExport {
237237
) -> Arc<Type> {
238238
let inner = match binding {
239239
BindingExport::Forward(idx) => Binding::Forward(*idx),
240+
BindingExport::PromoteForward(idx) => Binding::PromoteForward(*idx),
240241
BindingExport::AnnotatedForward(ann, idx) => {
241242
Binding::AnnotatedType(*ann, Box::new(Binding::Forward(*idx)))
242243
}

pyrefly/lib/binding/binding.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ use crate::binding::django::DjangoFieldInfo;
7070
use crate::binding::narrow::NarrowOp;
7171
use crate::binding::narrow::NarrowingSubject;
7272
use crate::binding::pydantic::PydanticConfigDict;
73+
use crate::binding::scope::is_constant_name;
7374
use crate::binding::table::TableKeyed;
7475
use crate::export::special::SpecialExport;
7576
use crate::module::module_info::ModuleInfo;
@@ -2089,6 +2090,8 @@ pub enum Binding {
20892090
ClassDef(Idx<KeyClass>, Box<[Idx<KeyDecorator>]>),
20902091
/// A forward reference to another binding.
20912092
Forward(Idx<Key>),
2093+
/// Like Forward, but widens implicit literals.
2094+
PromoteForward(Idx<Key>),
20922095
/// A forward reference produced during first-use resolution of a partial type.
20932096
/// Behaves identically to `Forward` but marks that this indirection came from
20942097
/// the partial-type / first-use machinery.
@@ -2259,6 +2262,7 @@ impl DisplayWith<Bindings> for Binding {
22592262
}
22602263
Self::ClassDef(x, _) => write!(f, "ClassDef({})", ctx.display(*x)),
22612264
Self::Forward(k) => write!(f, "Forward({})", ctx.display(*k)),
2265+
Self::PromoteForward(k) => write!(f, "PromoteForward({})", ctx.display(*k)),
22622266
Self::ForwardToFirstUse(k) => {
22632267
write!(f, "ForwardToFirstUse({})", ctx.display(*k))
22642268
}
@@ -2513,6 +2517,7 @@ impl Binding {
25132517
| Binding::None
25142518
| Binding::Any(_)
25152519
| Binding::Forward(_)
2520+
| Binding::PromoteForward(_)
25162521
| Binding::ForwardToFirstUse(_)
25172522
| Binding::Phi(_, _)
25182523
| Binding::LoopPhi(_, _)
@@ -2538,14 +2543,23 @@ impl Binding {
25382543
#[derive(Clone, Debug)]
25392544
pub enum BindingExport {
25402545
Forward(Idx<Key>),
2546+
PromoteForward(Idx<Key>),
25412547
AnnotatedForward(Idx<KeyAnnotation>, Idx<Key>),
25422548
}
25432549

25442550
impl BindingExport {
2551+
pub fn forward_maybe_promote(idx: Idx<Key>, name: &Name) -> Self {
2552+
if is_constant_name(name) {
2553+
Self::Forward(idx)
2554+
} else {
2555+
Self::PromoteForward(idx)
2556+
}
2557+
}
2558+
25452559
/// The forwarded key index that this export points to.
25462560
pub fn key_idx(&self) -> Idx<Key> {
25472561
match self {
2548-
Self::Forward(idx) | Self::AnnotatedForward(_, idx) => *idx,
2562+
Self::Forward(idx) | Self::PromoteForward(idx) | Self::AnnotatedForward(_, idx) => *idx,
25492563
}
25502564
}
25512565
}
@@ -2554,6 +2568,9 @@ impl DisplayWith<Bindings> for BindingExport {
25542568
fn fmt(&self, f: &mut fmt::Formatter<'_>, ctx: &Bindings) -> fmt::Result {
25552569
match self {
25562570
Self::Forward(idx) => write!(f, "BindingExport::Forward({})", ctx.display(*idx)),
2571+
Self::PromoteForward(idx) => {
2572+
write!(f, "BindingExport::PromoteForward({})", ctx.display(*idx))
2573+
}
25572574
Self::AnnotatedForward(ann, idx) => {
25582575
write!(
25592576
f,

0 commit comments

Comments
 (0)