Skip to content

Commit 44aa041

Browse files
stroxlermeta-codesync[bot]
authored andcommitted
Add test fixture and bug tests for __static__ primitive types
Summary: The cinderx compiler needs a new kind of contextual typing where primitive literals can be inferred as subtypes of the primitive type - this isn't a thing in interpreted python (`5` is always an `int`), but the compiler needs to actually compile literal types to be optimized unboxed ctypes in some cases. We probably will want to make Pyrefly core handle this via contextual typing in the long run assuming the current flow remains, but for now it makes sense to bolt it on as a post-processing phase because we don't want to disturb Pyrefly core. This diff sets up a test fixture to demonstrate where we get undesirable builtin types today, which we can use in stacked diffs to start supporting static primitive types in specific scenarios Add a minimal `__static__` module stub and tests demonstrating that literal values assigned to `__static__` primitive-typed variables (e.g. `x: int64 = 42`) currently get builtins types in the CinderX report instead of the contextual `__static__` type. These tests document the current (buggy) behavior and will be updated when the post-processing pass is implemented. Reviewed By: martindemello Differential Revision: D96493198 fbshipit-source-id: 83a77d9c1cf426b061226cee6352b9522ae97d77
1 parent f2a1a7e commit 44aa041

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed

pyrefly/lib/test/cinderx.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@ use crate::report::cinderx::write_results;
1515
use crate::state::require::Require;
1616
use crate::test::util::TestEnv;
1717

18+
/// Minimal `__static__` stub for CinderX primitive type tests.
19+
/// In production, the real `__static__` package is provided by CinderX.
20+
const STATIC_MODULE_STUB: &str = r#"
21+
class int8(int): pass
22+
class int16(int): pass
23+
class int32(int): pass
24+
class int64(int): pass
25+
class uint8(int): pass
26+
class uint16(int): pass
27+
class uint32(int): pass
28+
class uint64(int): pass
29+
class cbool(int): pass
30+
class char(int): pass
31+
class double(float): pass
32+
class single(float): pass
33+
"#;
34+
1835
/// Create a type-checked state from a single module's Python source.
1936
fn create_state(module_name: &str, python_code: &str) -> crate::state::state::State {
2037
let mut test_env = TestEnv::new();
@@ -25,6 +42,17 @@ fn create_state(module_name: &str, python_code: &str) -> crate::state::state::St
2542
state
2643
}
2744

45+
/// Create a type-checked state with the `__static__` stub and a test module.
46+
fn create_state_with_static(module_name: &str, python_code: &str) -> crate::state::state::State {
47+
let mut test_env = TestEnv::new();
48+
test_env.add("__static__", STATIC_MODULE_STUB);
49+
test_env.add(module_name, python_code);
50+
let (state, _) = test_env
51+
.with_default_require_level(Require::Everything)
52+
.to_state();
53+
state
54+
}
55+
2856
/// Find the handle for a module by name in a transaction.
2957
fn get_handle(
3058
module_name: &str,
@@ -750,6 +778,149 @@ def f(t: tuple[Inner, str]) -> None:
750778
);
751779
}
752780

781+
/// BUG: When a literal int is assigned to a variable annotated with `__static__.int64`,
782+
/// the CinderX report should record the contextual type `__static__.int64` for the
783+
/// literal expression, but currently it records `Literal[42]` / `builtins.int` instead.
784+
#[test]
785+
fn test_static_int64_literal_contextual_type() {
786+
let state = create_state_with_static(
787+
"test",
788+
r#"
789+
from __static__ import int64
790+
791+
x: int64 = 42
792+
"#,
793+
);
794+
let transaction = state.transaction();
795+
let handle = get_handle("test", &transaction);
796+
797+
let data = collect_module_types(&transaction, &handle).expect("should collect types");
798+
799+
// The type table should contain `__static__.int64` as a class entry,
800+
// proving that the annotation resolved correctly.
801+
let has_int64_class = data.entries.iter().any(|entry| {
802+
matches!(
803+
&entry.ty,
804+
StructuredType::Class { qname, .. } if qname == "__static__.int64"
805+
)
806+
});
807+
assert!(
808+
has_int64_class,
809+
"expected `__static__.int64` in the type table, got: {:#?}",
810+
data.entries,
811+
);
812+
813+
// BUG: The literal `42` should have contextual type `__static__.int64`, but
814+
// currently it gets `Literal[42]` with promoted type `builtins.int`.
815+
// Check that a Literal entry for "42" exists (the current, wrong behavior).
816+
let has_literal_42 = data.entries.iter().any(|entry| {
817+
matches!(
818+
&entry.ty,
819+
StructuredType::Literal { value, .. } if value == "42"
820+
)
821+
});
822+
assert!(
823+
has_literal_42,
824+
"expected Literal(42) in the type table (current buggy behavior), got: {:#?}",
825+
data.entries,
826+
);
827+
828+
// Verify the literal's promoted type is builtins.int, NOT __static__.int64.
829+
// This confirms the bug: the contextual __static__ type is lost.
830+
let literal_entry = data
831+
.entries
832+
.iter()
833+
.find(|entry| matches!(&entry.ty, StructuredType::Literal { value, .. } if value == "42"))
834+
.expect("Literal(42) should exist");
835+
let promoted_idx = match &literal_entry.ty {
836+
StructuredType::Literal { promoted_type, .. } => *promoted_type,
837+
_ => unreachable!("already matched as Literal"),
838+
};
839+
let promoted_entry = &data.entries[promoted_idx];
840+
// BUG: This should be __static__.int64 but is currently builtins.int.
841+
assert!(
842+
matches!(
843+
&promoted_entry.ty,
844+
StructuredType::Class { qname, .. } if qname == "builtins.int"
845+
),
846+
"expected promoted_type to be builtins.int (current buggy behavior), got: {:#?}",
847+
promoted_entry.ty,
848+
);
849+
}
850+
851+
/// BUG: When a literal float is assigned to a variable annotated with `__static__.double`,
852+
/// the CinderX report should record the contextual type `__static__.double` for the
853+
/// literal expression, but currently it records `builtins.float` instead.
854+
#[test]
855+
fn test_static_double_literal_contextual_type() {
856+
let state = create_state_with_static(
857+
"test",
858+
r#"
859+
from __static__ import double
860+
861+
y: double = 3.14
862+
"#,
863+
);
864+
let transaction = state.transaction();
865+
let handle = get_handle("test", &transaction);
866+
867+
let data = collect_module_types(&transaction, &handle).expect("should collect types");
868+
869+
// The type table should contain `__static__.double` as a class entry,
870+
// proving that the annotation resolved correctly.
871+
let has_double_class = data.entries.iter().any(|entry| {
872+
matches!(
873+
&entry.ty,
874+
StructuredType::Class { qname, .. } if qname == "__static__.double"
875+
)
876+
});
877+
assert!(
878+
has_double_class,
879+
"expected `__static__.double` in the type table, got: {:#?}",
880+
data.entries,
881+
);
882+
883+
// BUG: The literal `3.14` should have contextual type `__static__.double`, but
884+
// currently the located type for the literal expression resolves to `builtins.float`.
885+
// Check that a builtins.float class entry exists (used for the literal's type).
886+
let has_float_class = data.entries.iter().any(|entry| {
887+
matches!(
888+
&entry.ty,
889+
StructuredType::Class { qname, args, .. } if qname == "builtins.float" && args.is_empty()
890+
)
891+
});
892+
// BUG: This should be __static__.double but is currently builtins.float.
893+
assert!(
894+
has_float_class,
895+
"expected `builtins.float` in the type table (current buggy behavior), got: {:#?}",
896+
data.entries,
897+
);
898+
899+
// Find the located type entries whose type resolves to builtins.float.
900+
// At least one of these should correspond to the literal `3.14`, confirming
901+
// the bug that the contextual __static__.double type is lost.
902+
let float_class_idx = data
903+
.entries
904+
.iter()
905+
.position(|entry| {
906+
matches!(
907+
&entry.ty,
908+
StructuredType::Class { qname, args, .. } if qname == "builtins.float" && args.is_empty()
909+
)
910+
})
911+
.expect("builtins.float should exist");
912+
let float_locations: Vec<_> = data
913+
.locations
914+
.iter()
915+
.filter(|loc| loc.type_index == float_class_idx)
916+
.collect();
917+
assert!(
918+
!float_locations.is_empty(),
919+
"expected at least one location with type builtins.float (the literal 3.14), got locations: {:#?}",
920+
data.locations,
921+
);
922+
}
923+
753924
#[test]
754925
fn test_literal_promoted_type() {
755926
let state = create_state("test", "x = 42");

0 commit comments

Comments
 (0)