Skip to content

Commit 7c1be6f

Browse files
martindemellometa-codesync[bot]
authored andcommitted
Load pyrefly type info from the types directory
Reviewed By: DinoV Differential Revision: D95990791 fbshipit-source-id: 4032268c51011f84b4f77525762855e219464c70
1 parent 53497a9 commit 7c1be6f

5 files changed

Lines changed: 180 additions & 11 deletions

File tree

cinderx/PythonLib/cinderx/compiler/static/pyrefly_compiler.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@
55
import sys
66
from typing import Callable, Iterable
77

8-
from cinderx.compiler.static.pyrefly_type_binder import (
9-
PyreflyTypeBinder,
10-
PyreflyTypeInfo,
11-
)
8+
from cinderx.compiler.static.pyrefly_info import EMPTY_TYPE_INFO, Pyrefly
9+
from cinderx.compiler.static.pyrefly_type_binder import PyreflyTypeBinder
1210
from cinderx.compiler.static.type_binder import TypeBinder
13-
from cinderx.compiler.strict.compiler import Compiler, TIMING_LOGGER_TYPE
11+
from cinderx.compiler.strict.compiler import Compiler
1412
from cinderx.compiler.strict.flag_extractor import Flags
1513
from cinderx.compiler.symbols import SymbolVisitor
1614

1715

1816
class PyreflyCompiler(Compiler):
1917
def __init__(
2018
self,
21-
type_info: PyreflyTypeInfo | None = None,
19+
pyrefly: Pyrefly | None = None,
2220
static_opt_out: set[str] | None = None,
2321
static_opt_in: set[str] | None = None,
2422
path: Iterable[str] | None = None,
@@ -40,8 +38,7 @@ def __init__(
4038
use_py_compiler,
4139
allow_list_regex,
4240
)
43-
assert type_info is not None
44-
self.type_info = type_info
41+
self.pyrefly = pyrefly
4542
self.static_opt_in = static_opt_in
4643
self.static_opt_out = static_opt_out or set()
4744

@@ -66,12 +63,15 @@ def make_type_binder(
6663
optimize: int,
6764
enable_patching: bool = False,
6865
) -> TypeBinder:
66+
type_info = EMPTY_TYPE_INFO
67+
if self.pyrefly is not None:
68+
type_info = self.pyrefly.load_type_info(module_name) or EMPTY_TYPE_INFO
6969
return PyreflyTypeBinder(
7070
symbols,
7171
filename,
7272
compiler,
7373
module_name,
7474
optimize,
7575
enable_patching,
76-
self.type_info,
76+
type_info,
7777
)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
# pyre-strict
4+
5+
from __future__ import annotations
6+
7+
import json
8+
import os
9+
from ast import AST
10+
11+
12+
class PyreflyTypeInfo:
13+
"""Loads and indexes a pyrefly type trace JSON file.
14+
15+
The JSON file contains:
16+
- type_table: array of type entries (class, literal, callable)
17+
- locations: array of {loc: {start_line, start_col, end_line, end_col}, type: index}
18+
19+
Locations map source positions to type_table indices. Positions use
20+
1-based lines and 0-based columns (end_col is exclusive), matching
21+
Python AST conventions.
22+
"""
23+
24+
def __init__(self, data: dict[str, object]) -> None:
25+
self._type_table: list[dict[str, object]] = data["type_table"]
26+
self._locations: dict[tuple[int, int, int, int], int] = {}
27+
for entry in data["locations"]:
28+
loc = entry["loc"]
29+
key = (
30+
loc["start_line"],
31+
loc["start_col"],
32+
loc["end_line"],
33+
loc["end_col"],
34+
)
35+
self._locations[key] = entry["type"]
36+
37+
def _type_to_str(self, type_index: int) -> str:
38+
"""Convert a type_table entry to a Python annotation string."""
39+
entry = self._type_table[type_index]
40+
kind = entry["kind"]
41+
if kind == "literal":
42+
return ""
43+
elif kind == "class":
44+
qname = str(entry["qname"])
45+
args = entry.get("args", [])
46+
assert isinstance(args, list)
47+
if not args:
48+
return qname
49+
arg_strs = [self._type_to_str(a) for a in args]
50+
if any(not s for s in arg_strs):
51+
return qname
52+
return f"{qname}[{', '.join(arg_strs)}]"
53+
elif kind == "callable":
54+
params = entry.get("params", [])
55+
assert isinstance(params, list)
56+
ret = entry.get("return_type")
57+
param_strs = [self._type_to_str(p) for p in params]
58+
ret_str = self._type_to_str(ret) if isinstance(ret, int) else ""
59+
if any(not s for s in param_strs) or not ret_str:
60+
return ""
61+
return f"Callable[[{', '.join(param_strs)}], {ret_str}]"
62+
return ""
63+
64+
def lookup(self, node: AST) -> str:
65+
"""Look up the type string for an AST node by its source position."""
66+
key = (
67+
node.lineno, # pyre-ignore[16]
68+
node.col_offset, # pyre-ignore[16]
69+
node.end_lineno, # pyre-ignore[16]
70+
node.end_col_offset, # pyre-ignore[16]
71+
)
72+
type_index = self._locations.get(key)
73+
if type_index is None:
74+
return ""
75+
return self._type_to_str(type_index)
76+
77+
@classmethod
78+
def load_json(cls, json_path: str) -> PyreflyTypeInfo:
79+
with open(json_path) as f:
80+
data = json.load(f)
81+
return cls(data)
82+
83+
@classmethod
84+
def empty(cls) -> PyreflyTypeInfo:
85+
return cls({"type_table": [], "locations": []})
86+
87+
88+
class Pyrefly:
89+
"""Manages type information emitted by pyrefly."""
90+
91+
def __init__(self, type_dir: str):
92+
self.type_dir = type_dir
93+
94+
def load_type_info(self, module_name: str) -> PyreflyTypeInfo | None:
95+
if self.type_dir is None:
96+
return None
97+
json_path = os.path.join(self.type_dir, "types", f"{module_name}.json")
98+
if not os.path.isfile(json_path):
99+
return None
100+
return PyreflyTypeInfo.load_json(json_path)
101+
102+
103+
EMPTY_TYPE_INFO = PyreflyTypeInfo.empty()

cinderx/PythonLib/cinderx/compiler/static/pyrefly_type_binder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ..errors import TypedSyntaxError
1313
from ..symbols import SymbolVisitor
1414
from .effects import NarrowingEffect
15+
from .pyrefly_info import PyreflyTypeInfo
1516
from .type_binder import TypeBinder
1617

1718
if TYPE_CHECKING:

cinderx/PythonLib/cinderx/compiler/strict/pyrefly_loader.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from typing import Callable, Iterable, Mapping
1515

1616
from cinderx.compiler.static.pyrefly_compiler import PyreflyCompiler
17-
from cinderx.compiler.static.pyrefly_type_binder import PyreflyTypeInfo
17+
from cinderx.compiler.static.pyrefly_info import Pyrefly
1818

1919
from .compiler import Compiler, TIMING_LOGGER_TYPE
2020
from .loader import StrictSourceFileLoader
@@ -29,6 +29,8 @@
2929

3030

3131
class PyreflyLoader(StrictSourceFileLoader):
32+
pyrefly_type_dir: str | None = None
33+
3234
@classmethod
3335
def ensure_compiler(
3436
cls,
@@ -41,6 +43,10 @@ def ensure_compiler(
4143
allow_list_regex: Iterable[str] | None = None,
4244
) -> Compiler:
4345
if (comp := cls.compiler) is None:
46+
if cls.pyrefly_type_dir is not None:
47+
pyrefly = Pyrefly(cls.pyrefly_type_dir)
48+
else:
49+
pyrefly = None
4450
comp = cls.compiler = PyreflyCompiler(
4551
type_info=EMPTY_TYPE_INFO,
4652
static_opt_out=None,
@@ -49,6 +55,7 @@ def ensure_compiler(
4955
stub_path=stub_path,
5056
allow_list_prefix=allow_list_prefix,
5157
allow_list_exact=allow_list_exact,
58+
pyrefly=pyrefly,
5259
log_time_func=log_time_func,
5360
enable_patching=enable_patching,
5461
allow_list_regex=allow_list_regex or [],
@@ -114,8 +121,9 @@ def _get_supported_file_loaders(
114121
return [extensions, source, bytecode]
115122

116123

117-
def install(enable_patching: bool = False) -> None:
124+
def install(enable_patching: bool = False, pyrefly_type_dir: str | None = None) -> None:
118125
"""Installs a loader which is capable of loading and validating strict modules"""
126+
PyreflyLoader.pyrefly_type_dir = pyrefly_type_dir
119127
supported_loaders = _get_supported_file_loaders(enable_patching)
120128

121129
for index, hook in enumerate(sys.path_hooks):
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
import ast
4+
import unittest
5+
from types import CodeType
6+
7+
from cinderx.compiler.pycodegen import CodeGenerator
8+
from cinderx.compiler.static import Compiler, StaticCodeGenBase
9+
from cinderx.compiler.static.pyrefly_compiler import PyreflyCompiler
10+
11+
from .common import StaticTestBase
12+
13+
14+
class PyreBinderTests(StaticTestBase):
15+
def test_simple(self):
16+
code = self.compile_one("x = 1 + 2")
17+
18+
def test_force_static(self):
19+
compiler = PyreflyCompiler(non_static_modules=set())
20+
code, strict, static = compiler.load_compiled_module_from_source(
21+
"def f(x: int): return x.is_integer()", "foo.py", "foo", 0
22+
)
23+
self.assertTrue(static)
24+
self.assertFalse(strict)
25+
self.assertInBytecode(self.find_code(code, "f"), "INVOKE_METHOD")
26+
27+
def test_non_force_static(self):
28+
compiler = PyreflyCompiler(non_static_modules={"foo"})
29+
code, strict, static = compiler.load_compiled_module_from_source(
30+
"def f(x: int): return x.is_integer()", "foo.py", "foo", 0
31+
)
32+
self.assertFalse(static)
33+
self.assertFalse(strict)
34+
self.assertNotInBytecode(self.find_code(code, "f"), "INVOKE_METHOD")
35+
36+
def compile_one(
37+
self,
38+
code: str,
39+
modname: str = "<module>",
40+
optimize: int = 0,
41+
ast_optimizer_enabled: bool = True,
42+
enable_patching: bool = False,
43+
) -> CodeType:
44+
compiler = PyreflyCompiler()
45+
tree = ast.parse(self.clean_code(code))
46+
return compiler.compile(
47+
modname,
48+
f"{modname}.py",
49+
tree,
50+
code,
51+
optimize,
52+
enable_patching=enable_patching,
53+
)
54+
55+
56+
if __name__ == "__main__":
57+
unittest.main()

0 commit comments

Comments
 (0)