cinderx 2026.1.16.2__cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- __static__/__init__.py +641 -0
- __static__/compiler_flags.py +8 -0
- __static__/enum.py +160 -0
- __static__/native_utils.py +77 -0
- __static__/type_code.py +48 -0
- __strict__/__init__.py +39 -0
- _cinderx.so +0 -0
- cinderx/__init__.py +577 -0
- cinderx/__pycache__/__init__.cpython-314.pyc +0 -0
- cinderx/_asyncio.py +156 -0
- cinderx/compileall.py +710 -0
- cinderx/compiler/__init__.py +40 -0
- cinderx/compiler/__main__.py +137 -0
- cinderx/compiler/config.py +7 -0
- cinderx/compiler/consts.py +72 -0
- cinderx/compiler/debug.py +70 -0
- cinderx/compiler/dis_stable.py +283 -0
- cinderx/compiler/errors.py +151 -0
- cinderx/compiler/flow_graph_optimizer.py +1287 -0
- cinderx/compiler/future.py +91 -0
- cinderx/compiler/misc.py +32 -0
- cinderx/compiler/opcode_cinder.py +18 -0
- cinderx/compiler/opcode_static.py +100 -0
- cinderx/compiler/opcodebase.py +158 -0
- cinderx/compiler/opcodes.py +991 -0
- cinderx/compiler/optimizer.py +547 -0
- cinderx/compiler/pyassem.py +3711 -0
- cinderx/compiler/pycodegen.py +7660 -0
- cinderx/compiler/pysourceloader.py +62 -0
- cinderx/compiler/static/__init__.py +1404 -0
- cinderx/compiler/static/compiler.py +629 -0
- cinderx/compiler/static/declaration_visitor.py +335 -0
- cinderx/compiler/static/definite_assignment_checker.py +280 -0
- cinderx/compiler/static/effects.py +160 -0
- cinderx/compiler/static/module_table.py +666 -0
- cinderx/compiler/static/type_binder.py +2176 -0
- cinderx/compiler/static/types.py +10580 -0
- cinderx/compiler/static/util.py +81 -0
- cinderx/compiler/static/visitor.py +91 -0
- cinderx/compiler/strict/__init__.py +69 -0
- cinderx/compiler/strict/class_conflict_checker.py +249 -0
- cinderx/compiler/strict/code_gen_base.py +409 -0
- cinderx/compiler/strict/common.py +507 -0
- cinderx/compiler/strict/compiler.py +352 -0
- cinderx/compiler/strict/feature_extractor.py +130 -0
- cinderx/compiler/strict/flag_extractor.py +97 -0
- cinderx/compiler/strict/loader.py +827 -0
- cinderx/compiler/strict/preprocessor.py +11 -0
- cinderx/compiler/strict/rewriter/__init__.py +5 -0
- cinderx/compiler/strict/rewriter/remove_annotations.py +84 -0
- cinderx/compiler/strict/rewriter/rewriter.py +975 -0
- cinderx/compiler/strict/runtime.py +77 -0
- cinderx/compiler/symbols.py +1754 -0
- cinderx/compiler/unparse.py +414 -0
- cinderx/compiler/visitor.py +194 -0
- cinderx/jit.py +230 -0
- cinderx/opcode.py +202 -0
- cinderx/static.py +113 -0
- cinderx/strictmodule.py +6 -0
- cinderx/test_support.py +341 -0
- cinderx-2026.1.16.2.dist-info/METADATA +15 -0
- cinderx-2026.1.16.2.dist-info/RECORD +68 -0
- cinderx-2026.1.16.2.dist-info/WHEEL +6 -0
- cinderx-2026.1.16.2.dist-info/licenses/LICENSE +21 -0
- cinderx-2026.1.16.2.dist-info/top_level.txt +5 -0
- opcodes/__init__.py +0 -0
- opcodes/assign_opcode_numbers.py +272 -0
- opcodes/cinderx_opcodes.py +121 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
# pyre-strict
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
from ast import AST, Attribute, BinOp, Call, ClassDef, Constant, Name, Subscript
|
|
9
|
+
from contextlib import nullcontext
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import cast, ContextManager, Optional, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from ..errors import TypedSyntaxError
|
|
14
|
+
from ..symbols import ModuleScope, Scope
|
|
15
|
+
from .types import (
|
|
16
|
+
Callable,
|
|
17
|
+
Class,
|
|
18
|
+
ClassVar,
|
|
19
|
+
CType,
|
|
20
|
+
DataclassDecorator,
|
|
21
|
+
DynamicClass,
|
|
22
|
+
ExactClass,
|
|
23
|
+
FinalClass,
|
|
24
|
+
Function,
|
|
25
|
+
FunctionGroup,
|
|
26
|
+
InitVar,
|
|
27
|
+
KnownBoolean,
|
|
28
|
+
MethodType,
|
|
29
|
+
ModuleInstance,
|
|
30
|
+
NativeDecorator,
|
|
31
|
+
TType,
|
|
32
|
+
TypeDescr,
|
|
33
|
+
UnionType,
|
|
34
|
+
UnknownDecoratedMethod,
|
|
35
|
+
Value,
|
|
36
|
+
)
|
|
37
|
+
from .visitor import GenericVisitor
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from .compiler import Compiler
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# When set to True, allow type alises of the form
|
|
44
|
+
# A = Foo
|
|
45
|
+
# as well as the explicit 3.12+
|
|
46
|
+
# type A = Foo
|
|
47
|
+
# When set to False, only the latter form will be supported.
|
|
48
|
+
#
|
|
49
|
+
# TASK(T214139735): This is a placeholder for a feature flag that will let
|
|
50
|
+
# users opt in to implicit type aliases on a per-module basis.
|
|
51
|
+
ENABLE_IMPLICIT_TYPE_ALIASES = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ModuleFlag(Enum):
|
|
55
|
+
CHECKED_DICTS = 1
|
|
56
|
+
CHECKED_LISTS = 3
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ModuleTableException(Exception):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ReferenceVisitor(GenericVisitor[Optional[Value]]):
|
|
64
|
+
def __init__(self, module: ModuleTable) -> None:
|
|
65
|
+
super().__init__(module)
|
|
66
|
+
self.subscr_nesting = 0
|
|
67
|
+
self.local_names: dict[str, Value] = {}
|
|
68
|
+
|
|
69
|
+
def visitName(self, node: Name) -> Value | None:
|
|
70
|
+
if node.id in self.local_names:
|
|
71
|
+
return self.local_names[node.id]
|
|
72
|
+
return self.module.get_child(
|
|
73
|
+
node.id, self.context_qualname, force_decl=self.force_decl_deps
|
|
74
|
+
) or self.module.compiler.builtins.get_child_intrinsic(node.id)
|
|
75
|
+
|
|
76
|
+
def visitAttribute(self, node: Attribute) -> Value | None:
|
|
77
|
+
val = self.visit(node.value)
|
|
78
|
+
if val is not None:
|
|
79
|
+
return val.resolve_attr(node, self)
|
|
80
|
+
|
|
81
|
+
def add_local_name(self, name: str, value: Value) -> None:
|
|
82
|
+
self.local_names[name] = value
|
|
83
|
+
|
|
84
|
+
def clear_local_names(self) -> None:
|
|
85
|
+
self.local_names = {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AnnotationVisitor(ReferenceVisitor):
|
|
89
|
+
def resolve_annotation(
|
|
90
|
+
self,
|
|
91
|
+
node: ast.AST,
|
|
92
|
+
*,
|
|
93
|
+
is_declaration: bool = False,
|
|
94
|
+
) -> Class | None:
|
|
95
|
+
with self.error_context(node):
|
|
96
|
+
klass = self.visit(node)
|
|
97
|
+
if not isinstance(klass, Class):
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
if self.subscr_nesting or not is_declaration:
|
|
101
|
+
if isinstance(klass, FinalClass):
|
|
102
|
+
raise TypedSyntaxError(
|
|
103
|
+
"Final annotation is only valid in initial declaration "
|
|
104
|
+
"of attribute or module-level constant",
|
|
105
|
+
)
|
|
106
|
+
if isinstance(klass, ClassVar):
|
|
107
|
+
raise TypedSyntaxError(
|
|
108
|
+
"ClassVar is allowed only in class attribute annotations. "
|
|
109
|
+
"Class Finals are inferred ClassVar; do not nest with Final."
|
|
110
|
+
)
|
|
111
|
+
if isinstance(klass, InitVar):
|
|
112
|
+
raise TypedSyntaxError(
|
|
113
|
+
"InitVar is allowed only in class attribute annotations."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if isinstance(klass, ExactClass):
|
|
117
|
+
klass = klass.unwrap().exact_type()
|
|
118
|
+
elif isinstance(klass, FinalClass):
|
|
119
|
+
pass
|
|
120
|
+
else:
|
|
121
|
+
klass = klass.inexact_type()
|
|
122
|
+
# PEP-484 specifies that ints should be treated as a subclass of floats,
|
|
123
|
+
# even though they differ in the runtime. We need to maintain the distinction
|
|
124
|
+
# between the two internally, so we should view user-specified `float` annotations
|
|
125
|
+
# as `float | int`. This widening of the type prevents us from applying
|
|
126
|
+
# optimizations to user-specified floats, but does not affect ints. Since we
|
|
127
|
+
# don't optimize Python floats anyway, we accept this to maintain PEP-484 compatibility.
|
|
128
|
+
|
|
129
|
+
if klass.unwrap() is self.type_env.float:
|
|
130
|
+
klass = self.compiler.type_env.get_union(
|
|
131
|
+
(self.type_env.float, self.type_env.int)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# TODO until we support runtime checking of unions, we must for
|
|
135
|
+
# safety resolve union annotations to dynamic (except for
|
|
136
|
+
# optionals, which we can check at runtime)
|
|
137
|
+
if self.is_unsupported_union_type(klass):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Types like typing.NamedTuple or typing.Protocol should not be
|
|
141
|
+
# used as annotations; if they are, fall back to dynamic rather
|
|
142
|
+
# than raise an error (see T186572841 for context).
|
|
143
|
+
|
|
144
|
+
# TASK(T186572841): Extend to Protocol as well
|
|
145
|
+
special_types = ("NamedTuple", "TypedDict")
|
|
146
|
+
if klass.type_name.module == "typing":
|
|
147
|
+
if klass.type_name.qualname in special_types:
|
|
148
|
+
return self.type_env.dynamic
|
|
149
|
+
|
|
150
|
+
return klass
|
|
151
|
+
|
|
152
|
+
def is_unsupported_union_type(self, klass: Value) -> bool:
|
|
153
|
+
"""Returns True if klass is an unsupported union type."""
|
|
154
|
+
return (
|
|
155
|
+
isinstance(klass, UnionType)
|
|
156
|
+
and klass is not self.type_env.union
|
|
157
|
+
and klass is not self.type_env.optional
|
|
158
|
+
and klass.opt_type is None
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def visitSubscript(self, node: Subscript) -> Value | None:
|
|
162
|
+
target = self.resolve_annotation(node.value, is_declaration=True)
|
|
163
|
+
if target is None:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
self.subscr_nesting += 1
|
|
167
|
+
slice = self.visit(node.slice) or self.type_env.DYNAMIC
|
|
168
|
+
self.subscr_nesting -= 1
|
|
169
|
+
return target.resolve_subscr(node, slice, self) or target
|
|
170
|
+
|
|
171
|
+
def visitBinOp(self, node: BinOp) -> Value | None:
|
|
172
|
+
if isinstance(node.op, ast.BitOr):
|
|
173
|
+
ltype = self.resolve_annotation(node.left)
|
|
174
|
+
rtype = self.resolve_annotation(node.right)
|
|
175
|
+
if ltype is None or rtype is None:
|
|
176
|
+
return None
|
|
177
|
+
return self.module.compiler.type_env.get_union((ltype, rtype))
|
|
178
|
+
|
|
179
|
+
def visitConstant(self, node: Constant) -> Value | None:
|
|
180
|
+
sval = node.value
|
|
181
|
+
if sval is None:
|
|
182
|
+
return self.type_env.none
|
|
183
|
+
elif isinstance(sval, str):
|
|
184
|
+
n = ast.parse(node.value, "", "eval").body
|
|
185
|
+
return self.visit(n)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DepTrackingOptOut:
|
|
189
|
+
"""A reason for opting out of module dependency tracking.
|
|
190
|
+
|
|
191
|
+
This helps ensure our dependency tracking is complete; we can't request type
|
|
192
|
+
information from a module via ModuleTable.get_child(...) without either
|
|
193
|
+
providing a `requester` (to track the dependency) or providing an opt-out
|
|
194
|
+
reason.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, reason: str) -> None:
|
|
198
|
+
self.reason = reason
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
INTRINSIC_OPT_OUT = DepTrackingOptOut(
|
|
202
|
+
"We don't need to track dependencies to intrinsic modules; "
|
|
203
|
+
"they won't change during development and require pyc updates. "
|
|
204
|
+
"If they change, we should bump the static pyc magic number."
|
|
205
|
+
)
|
|
206
|
+
DEFERRED_IMPORT_OPT_OUT = DepTrackingOptOut("Tracked in ModuleTable.declare_import()")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class DeferredImport:
|
|
210
|
+
def __init__(
|
|
211
|
+
self,
|
|
212
|
+
in_module: ModuleTable,
|
|
213
|
+
mod_to_import: str,
|
|
214
|
+
asname: str,
|
|
215
|
+
optimize: int,
|
|
216
|
+
compiler: Compiler,
|
|
217
|
+
name: str | None = None,
|
|
218
|
+
mod_to_return: str | None = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
self.in_module = in_module
|
|
221
|
+
self.mod_to_import = mod_to_import
|
|
222
|
+
self.asname = asname
|
|
223
|
+
self.name = name
|
|
224
|
+
self.mod_to_return = mod_to_return
|
|
225
|
+
self.compiler = compiler
|
|
226
|
+
self.optimize = optimize
|
|
227
|
+
|
|
228
|
+
def import_mod(self, mod_name: str) -> ModuleTable | None:
|
|
229
|
+
return self.compiler.import_module(mod_name, optimize=self.optimize)
|
|
230
|
+
|
|
231
|
+
def resolve(self) -> Value:
|
|
232
|
+
if self.mod_to_import not in self.compiler.modules:
|
|
233
|
+
self.import_mod(self.mod_to_import)
|
|
234
|
+
mod = self.compiler.modules.get(self.mod_to_import)
|
|
235
|
+
if mod is not None:
|
|
236
|
+
if self.name is None:
|
|
237
|
+
return ModuleInstance(
|
|
238
|
+
self.mod_to_return or self.mod_to_import, self.compiler
|
|
239
|
+
)
|
|
240
|
+
val = mod.get_child(self.name, DEFERRED_IMPORT_OPT_OUT)
|
|
241
|
+
if val is not None:
|
|
242
|
+
return val
|
|
243
|
+
try_mod = f"{self.mod_to_import}.{self.name}"
|
|
244
|
+
self.import_mod(try_mod)
|
|
245
|
+
if try_mod in self.compiler.modules:
|
|
246
|
+
return ModuleInstance(try_mod, self.compiler)
|
|
247
|
+
if not mod.first_pass_done:
|
|
248
|
+
raise ModuleTableException(
|
|
249
|
+
f"Cannot find {self.mod_to_import}.{self.name} due to cyclic reference"
|
|
250
|
+
)
|
|
251
|
+
# For unknown modules, we don't need to record each individual object
|
|
252
|
+
# used from them; it doesn't matter, as we won't track transitive deps
|
|
253
|
+
# across them. We just need to record a dependency to the module in
|
|
254
|
+
# general.
|
|
255
|
+
self.in_module.record_dependency(self.asname, (self.mod_to_import, "<any>"))
|
|
256
|
+
return self.compiler.type_env.DYNAMIC
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class UnknownModule:
|
|
260
|
+
def __init__(self, name: str) -> None:
|
|
261
|
+
self.name = name
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ModuleTable:
|
|
265
|
+
def __init__(
|
|
266
|
+
self,
|
|
267
|
+
name: str,
|
|
268
|
+
filename: str,
|
|
269
|
+
compiler: Compiler,
|
|
270
|
+
members: dict[str, Value] | None = None,
|
|
271
|
+
first_pass_done: bool = True,
|
|
272
|
+
) -> None:
|
|
273
|
+
self.name = name
|
|
274
|
+
self.filename = filename
|
|
275
|
+
self._children: dict[str, Value | DeferredImport] = {}
|
|
276
|
+
if members is not None:
|
|
277
|
+
self._children.update(members)
|
|
278
|
+
self.compiler = compiler
|
|
279
|
+
self.types: dict[AST, Value] = {}
|
|
280
|
+
self.node_data: dict[tuple[AST, object], object] = {}
|
|
281
|
+
self.flags: set[ModuleFlag] = set()
|
|
282
|
+
self.decls: list[tuple[AST, str | None, Value | None]] = []
|
|
283
|
+
self.implicit_decl_names: set[str] = set()
|
|
284
|
+
self.type_alias_names: set[str] = set()
|
|
285
|
+
self.compile_non_static: set[AST] = set()
|
|
286
|
+
# {local-name: {(mod, qualname)}} for decl-time deps
|
|
287
|
+
self.decl_deps: dict[str, set[tuple[str, str]]] = {}
|
|
288
|
+
# {local-name: {(mod, qualname)}} for bind-time deps
|
|
289
|
+
self.bind_deps: dict[str, set[tuple[str, str]]] = {}
|
|
290
|
+
# (source module, source name) for every name this module imports-from
|
|
291
|
+
# another static module at top level
|
|
292
|
+
self.imported_from: dict[str, tuple[str, str]] = {}
|
|
293
|
+
# TODO: final constants should be typed to literals, and
|
|
294
|
+
# this should be removed in the future
|
|
295
|
+
self.named_finals: dict[str, ast.Constant] = {}
|
|
296
|
+
# Have we completed our first pass through the module, populating
|
|
297
|
+
# imports and types defined in the module? Until we have, resolving
|
|
298
|
+
# type annotations is not safe.
|
|
299
|
+
self.first_pass_done = first_pass_done
|
|
300
|
+
self.finish_bind_done = False
|
|
301
|
+
self.ann_visitor = AnnotationVisitor(self)
|
|
302
|
+
self.ref_visitor = ReferenceVisitor(self)
|
|
303
|
+
# the prefix children will get on their qualname; always None for
|
|
304
|
+
# modules (the qualname of a class or function is within-module, it
|
|
305
|
+
# doesn't include the module name)
|
|
306
|
+
self.qualname: None = None
|
|
307
|
+
|
|
308
|
+
def __repr__(self) -> str:
|
|
309
|
+
return f"<ModuleTable {self.name}>"
|
|
310
|
+
|
|
311
|
+
def get_dependencies(self) -> set[ModuleTable | UnknownModule]:
|
|
312
|
+
"""Return all modules this module depends on."""
|
|
313
|
+
# We only include bind deps for the current module; for transitive deps
|
|
314
|
+
# we follow only decl_deps
|
|
315
|
+
immediate_deps = {**self.bind_deps, **self.decl_deps}
|
|
316
|
+
all_modules_deps = {
|
|
317
|
+
name: mod.decl_deps for name, mod in self.compiler.modules.items()
|
|
318
|
+
}
|
|
319
|
+
all_modules_deps[self.name] = immediate_deps
|
|
320
|
+
dep_names = find_transitive_deps(self.name, all_modules_deps)
|
|
321
|
+
return {
|
|
322
|
+
self.compiler.modules.get(name) or UnknownModule(name) for name in dep_names
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
def record_dependency(
|
|
326
|
+
self, name: str, target: tuple[str, str], force_decl: bool = False
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Record a dependency from a local name to a name in another module.
|
|
329
|
+
|
|
330
|
+
`name` is the name in this module's namespace. `target` is a (str, str)
|
|
331
|
+
tuple of (source_module, source_name).
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
deps = (
|
|
335
|
+
self.bind_deps
|
|
336
|
+
if (self.finish_bind_done and not force_decl)
|
|
337
|
+
else self.decl_deps
|
|
338
|
+
)
|
|
339
|
+
deps.setdefault(name, set()).add(target)
|
|
340
|
+
|
|
341
|
+
def get_child(
|
|
342
|
+
self, name: str, requester: str | DepTrackingOptOut, force_decl: bool = False
|
|
343
|
+
) -> Value | None:
|
|
344
|
+
if not isinstance(requester, DepTrackingOptOut):
|
|
345
|
+
# Using imported_from here is just an optimization that lets us skip
|
|
346
|
+
# one level of transitive-dependency-following later. If modA has
|
|
347
|
+
# "from modB import B", we already record `modA.B -> modB.B`, so
|
|
348
|
+
# technically if `modA.f -> modA.B`, we could just record that
|
|
349
|
+
# dependency, and following the transitive closure would later give
|
|
350
|
+
# us `modA.f -> modB.B`. But since we have the `imported_from` data
|
|
351
|
+
# available, we use it to just record `modA.f -> modB.B` initially.
|
|
352
|
+
# We don't need `modA.f -> modA.B` recorded as a dependency in that
|
|
353
|
+
# case, since intra-module dependencies have no direct impact on
|
|
354
|
+
# recompilation, they are only needed for following the transitive
|
|
355
|
+
# chain across modules.
|
|
356
|
+
source = self.imported_from.get(name, (self.name, name))
|
|
357
|
+
self.record_dependency(requester, source, force_decl)
|
|
358
|
+
res = self._children.get(name)
|
|
359
|
+
if isinstance(res, DeferredImport):
|
|
360
|
+
self._children[name] = res = res.resolve()
|
|
361
|
+
return res
|
|
362
|
+
|
|
363
|
+
def syntax_error(self, msg: str, node: AST) -> None:
|
|
364
|
+
return self.compiler.error_sink.syntax_error(msg, self.filename, node)
|
|
365
|
+
|
|
366
|
+
def error_context(self, node: AST | None) -> ContextManager[None]:
|
|
367
|
+
if node is None:
|
|
368
|
+
return nullcontext()
|
|
369
|
+
return self.compiler.error_sink.error_context(self.filename, node)
|
|
370
|
+
|
|
371
|
+
def maybe_set_type_alias(
|
|
372
|
+
self,
|
|
373
|
+
# pyre-ignore[11]: Annotation `ast.TypeAlias` is not defined as a type
|
|
374
|
+
node: ast.Assign | ast.TypeAlias,
|
|
375
|
+
name: str,
|
|
376
|
+
*,
|
|
377
|
+
require_type: bool = False,
|
|
378
|
+
) -> None:
|
|
379
|
+
"""
|
|
380
|
+
Check if we are assigning a Class or Union value to a variable at
|
|
381
|
+
module scope, and if so, store it as a type alias.
|
|
382
|
+
"""
|
|
383
|
+
value = self.resolve_type(node.value, name)
|
|
384
|
+
if isinstance(value, Class):
|
|
385
|
+
# This is a type, treat it similarly to a class declaration
|
|
386
|
+
self.decls.append((node, name, value))
|
|
387
|
+
self._children[name] = value
|
|
388
|
+
self.type_alias_names.add(name)
|
|
389
|
+
else:
|
|
390
|
+
# Treat the type as dynamic if it is an assignment,
|
|
391
|
+
# raise an error if it is an explicit type alias.
|
|
392
|
+
if require_type:
|
|
393
|
+
rhs = ast.unparse(node.value)
|
|
394
|
+
raise TypedSyntaxError(f"RHS of type alias {name} is not a type: {rhs}")
|
|
395
|
+
self.implicit_decl_names.add(name)
|
|
396
|
+
|
|
397
|
+
# pyre-ignore[11]: Annotation `ast.TypeAlias` is not defined as a type
|
|
398
|
+
def declare_type_alias(self, node: ast.TypeAlias) -> None:
|
|
399
|
+
self.maybe_set_type_alias(node, node.name.id, require_type=True)
|
|
400
|
+
|
|
401
|
+
def declare_class(self, node: ClassDef, klass: Class) -> None:
|
|
402
|
+
if self.first_pass_done:
|
|
403
|
+
raise ModuleTableException(
|
|
404
|
+
"Attempted to declare a class after the declaration visit"
|
|
405
|
+
)
|
|
406
|
+
self.decls.append((node, node.name, klass))
|
|
407
|
+
self._children[node.name] = klass
|
|
408
|
+
|
|
409
|
+
def declare_function(self, func: Function) -> None:
|
|
410
|
+
if self.first_pass_done:
|
|
411
|
+
raise ModuleTableException(
|
|
412
|
+
"Attempted to declare a function after the declaration visit"
|
|
413
|
+
)
|
|
414
|
+
existing = self._children.get(func.func_name)
|
|
415
|
+
new_member = func
|
|
416
|
+
if existing is not None:
|
|
417
|
+
if isinstance(existing, Function):
|
|
418
|
+
new_member = FunctionGroup([existing, new_member], func.klass.type_env)
|
|
419
|
+
elif isinstance(existing, FunctionGroup):
|
|
420
|
+
existing.functions.append(new_member)
|
|
421
|
+
new_member = existing
|
|
422
|
+
else:
|
|
423
|
+
raise TypedSyntaxError(
|
|
424
|
+
f"function conflicts with other member {func.func_name} in {self.name}"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
self.decls.append((func.node, func.func_name, new_member))
|
|
428
|
+
self._children[func.func_name] = new_member
|
|
429
|
+
|
|
430
|
+
def _get_inferred_type(self, value: ast.expr, requester: str) -> Value | None:
|
|
431
|
+
if not isinstance(value, ast.Name):
|
|
432
|
+
return None
|
|
433
|
+
return self.get_child(value.id, requester)
|
|
434
|
+
|
|
435
|
+
def finish_bind(self) -> None:
|
|
436
|
+
self.first_pass_done = True
|
|
437
|
+
for node, name, value in self.decls:
|
|
438
|
+
with self.error_context(node):
|
|
439
|
+
if value is not None:
|
|
440
|
+
assert name is not None
|
|
441
|
+
new_value = value.finish_bind(self, None)
|
|
442
|
+
if new_value is None:
|
|
443
|
+
# We still need to maintain a name for unknown decorated methods since they
|
|
444
|
+
# will bind to some name, and we want to avoid throwing unknown name errors.
|
|
445
|
+
if isinstance(self.types[node], UnknownDecoratedMethod):
|
|
446
|
+
self._children[name] = self.compiler.type_env.DYNAMIC
|
|
447
|
+
else:
|
|
448
|
+
del self._children[name]
|
|
449
|
+
elif new_value is not value:
|
|
450
|
+
self._children[name] = new_value
|
|
451
|
+
|
|
452
|
+
if isinstance(node, ast.AnnAssign) and isinstance(
|
|
453
|
+
node.target, ast.Name
|
|
454
|
+
):
|
|
455
|
+
# isinstance(target := node.target, ast.Name) above would be
|
|
456
|
+
# nicer, but pyre doesn't narrow the type of target :/
|
|
457
|
+
target = node.target
|
|
458
|
+
assert isinstance(target, ast.Name)
|
|
459
|
+
typ = self.resolve_annotation(
|
|
460
|
+
node.annotation, target.id, is_declaration=True
|
|
461
|
+
)
|
|
462
|
+
if typ is not None:
|
|
463
|
+
# Special case Final[dynamic] to use inferred type.
|
|
464
|
+
instance = typ.instance
|
|
465
|
+
value = node.value
|
|
466
|
+
if isinstance(typ, FinalClass):
|
|
467
|
+
# We keep track of annotated finals in the
|
|
468
|
+
# named_finals field - we can safely remove that
|
|
469
|
+
# information here to ensure that the rest of the
|
|
470
|
+
# type system can safely ignore it.
|
|
471
|
+
unwrapped = typ.unwrap()
|
|
472
|
+
if value is not None and isinstance(
|
|
473
|
+
unwrapped, DynamicClass
|
|
474
|
+
):
|
|
475
|
+
instance = (
|
|
476
|
+
self._get_inferred_type(value, target.id)
|
|
477
|
+
or unwrapped.instance
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
instance = unwrapped.instance
|
|
481
|
+
|
|
482
|
+
self._children[target.id] = instance
|
|
483
|
+
|
|
484
|
+
if isinstance(typ, FinalClass):
|
|
485
|
+
value = node.value
|
|
486
|
+
if not value:
|
|
487
|
+
raise TypedSyntaxError(
|
|
488
|
+
"Must assign a value when declaring a Final"
|
|
489
|
+
)
|
|
490
|
+
elif not isinstance(typ, CType) and isinstance(
|
|
491
|
+
value, ast.Constant
|
|
492
|
+
):
|
|
493
|
+
self.named_finals[target.id] = value
|
|
494
|
+
|
|
495
|
+
for name in self.implicit_decl_names:
|
|
496
|
+
if name not in self._children:
|
|
497
|
+
self._children[name] = self.compiler.type_env.DYNAMIC
|
|
498
|
+
# We don't need these anymore...
|
|
499
|
+
self.implicit_decl_names.clear()
|
|
500
|
+
self.finish_bind_done = True
|
|
501
|
+
|
|
502
|
+
def validate_overrides(self) -> None:
|
|
503
|
+
for _node, name, _value in self.decls:
|
|
504
|
+
if name is None or name in self.type_alias_names:
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
child = self._children.get(name, None)
|
|
508
|
+
if isinstance(child, Value):
|
|
509
|
+
child.validate_overrides(self, None)
|
|
510
|
+
|
|
511
|
+
self.decls.clear()
|
|
512
|
+
|
|
513
|
+
def resolve_type(self, node: ast.AST, requester: str) -> Class | None:
|
|
514
|
+
with self.ann_visitor.temporary_context_qualname(requester):
|
|
515
|
+
try:
|
|
516
|
+
typ = self.ann_visitor.visit(node)
|
|
517
|
+
except SyntaxError:
|
|
518
|
+
# We resolve types via the AnnotationVisitor, which expects
|
|
519
|
+
# string literals to parse as well-formed python type
|
|
520
|
+
# expressions. In the more general case here (where we are
|
|
521
|
+
# resolving a node that is not necessarily an annotation in the
|
|
522
|
+
# source code) the visitor will try to parse arbitrary string
|
|
523
|
+
# literals as python code, and will raise a SyntaxError.
|
|
524
|
+
return None
|
|
525
|
+
if isinstance(typ, Class):
|
|
526
|
+
return typ
|
|
527
|
+
|
|
528
|
+
def resolve_decorator(self, node: ast.AST) -> Value | None:
|
|
529
|
+
if isinstance(node, Call):
|
|
530
|
+
func = self.ref_visitor.visit(node.func)
|
|
531
|
+
if isinstance(func, Class):
|
|
532
|
+
return func.instance
|
|
533
|
+
elif isinstance(func, DataclassDecorator):
|
|
534
|
+
return func
|
|
535
|
+
elif isinstance(func, NativeDecorator):
|
|
536
|
+
return func
|
|
537
|
+
elif isinstance(func, Callable):
|
|
538
|
+
return func.return_type.resolved().instance
|
|
539
|
+
elif isinstance(func, MethodType):
|
|
540
|
+
return func.function.return_type.resolved().instance
|
|
541
|
+
|
|
542
|
+
return self.ref_visitor.visit(node)
|
|
543
|
+
|
|
544
|
+
def resolve_annotation(
|
|
545
|
+
self,
|
|
546
|
+
node: ast.AST,
|
|
547
|
+
requester: str,
|
|
548
|
+
*,
|
|
549
|
+
# annotation on a variable or attribute declaration (could be inside a
|
|
550
|
+
# function, thus not public)
|
|
551
|
+
is_declaration: bool = False,
|
|
552
|
+
# annotation on public API (also includes e.g. function arg/return
|
|
553
|
+
# annotations, does not include function-internal declarations)
|
|
554
|
+
is_decl_dep: bool = False,
|
|
555
|
+
) -> Class | None:
|
|
556
|
+
with self.ann_visitor.temporary_context_qualname(requester, is_decl_dep):
|
|
557
|
+
return self.ann_visitor.resolve_annotation(
|
|
558
|
+
node, is_declaration=is_declaration
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def resolve_name_with_descr(
|
|
562
|
+
self, name: str, requester: str
|
|
563
|
+
) -> tuple[Value | None, TypeDescr | None]:
|
|
564
|
+
if val := self.get_child(name, requester):
|
|
565
|
+
return val, ((self.name,), name)
|
|
566
|
+
elif val := self.compiler.builtins.get_child_intrinsic(name):
|
|
567
|
+
return val, None
|
|
568
|
+
return None, None
|
|
569
|
+
|
|
570
|
+
def get_final_literal(self, node: AST, scope: Scope) -> ast.Constant | None:
|
|
571
|
+
if not isinstance(node, Name):
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
final_val = self.named_finals.get(node.id, None)
|
|
575
|
+
if (
|
|
576
|
+
final_val is not None
|
|
577
|
+
and isinstance(node.ctx, ast.Load)
|
|
578
|
+
and (
|
|
579
|
+
# Ensure the name is not shadowed in the local scope
|
|
580
|
+
isinstance(scope, ModuleScope) or node.id not in scope.defs
|
|
581
|
+
)
|
|
582
|
+
):
|
|
583
|
+
return final_val
|
|
584
|
+
|
|
585
|
+
def declare_import(
|
|
586
|
+
self, name: str, target: tuple[str, str] | None, val: Value | DeferredImport
|
|
587
|
+
) -> None:
|
|
588
|
+
"""Declare a name imported into this module.
|
|
589
|
+
|
|
590
|
+
`name` is the name in this module's namespace. `target` is a (str, str)
|
|
591
|
+
tuple of (source_module, source_name) for an `import from`. For a
|
|
592
|
+
top-level module import, `target` should be `None`.
|
|
593
|
+
|
|
594
|
+
"""
|
|
595
|
+
if self.first_pass_done:
|
|
596
|
+
raise ModuleTableException(
|
|
597
|
+
"Attempted to declare an import after the declaration visit"
|
|
598
|
+
)
|
|
599
|
+
self._children[name] = val
|
|
600
|
+
if target is not None:
|
|
601
|
+
self.imported_from[name] = target
|
|
602
|
+
self.record_dependency(name, target)
|
|
603
|
+
|
|
604
|
+
def declare_variable(self, node: ast.AnnAssign, module: ModuleTable) -> None:
|
|
605
|
+
self.decls.append((node, None, None))
|
|
606
|
+
|
|
607
|
+
def declare_variables(self, node: ast.Assign, module: ModuleTable) -> None:
|
|
608
|
+
targets = node.targets
|
|
609
|
+
if (
|
|
610
|
+
ENABLE_IMPLICIT_TYPE_ALIASES
|
|
611
|
+
and len(targets) == 1
|
|
612
|
+
and isinstance(targets[0], ast.Name)
|
|
613
|
+
):
|
|
614
|
+
# pyre-ignore[16]: `ast.expr` has no attribute `id`
|
|
615
|
+
return self.maybe_set_type_alias(node, targets[0].id)
|
|
616
|
+
|
|
617
|
+
for target in targets:
|
|
618
|
+
if isinstance(target, ast.Name):
|
|
619
|
+
self.implicit_decl_names.add(target.id)
|
|
620
|
+
|
|
621
|
+
def get_node_data(self, key: AST, data_type: type[TType]) -> TType:
|
|
622
|
+
return cast(TType, self.node_data[key, data_type])
|
|
623
|
+
|
|
624
|
+
def get_opt_node_data(self, key: AST, data_type: type[TType]) -> TType | None:
|
|
625
|
+
return cast(Optional[TType], self.node_data.get((key, data_type)))
|
|
626
|
+
|
|
627
|
+
def set_node_data(self, key: AST, data_type: type[TType], value: TType) -> None:
|
|
628
|
+
self.node_data[key, data_type] = value
|
|
629
|
+
|
|
630
|
+
def mark_known_boolean_test(self, node: ast.expr, *, value: bool) -> None:
|
|
631
|
+
"""
|
|
632
|
+
For boolean tests that can be determined during decl-visit, we note the AST nodes
|
|
633
|
+
and the boolean value. This helps us avoid visiting dead code in later passes.
|
|
634
|
+
"""
|
|
635
|
+
self.set_node_data(
|
|
636
|
+
node, KnownBoolean, KnownBoolean.TRUE if value else KnownBoolean.FALSE
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class IntrinsicModuleTable(ModuleTable):
|
|
641
|
+
"""A ModuleTable for modules that are intrinsic in the compiler."""
|
|
642
|
+
|
|
643
|
+
def get_child_intrinsic(self, name: str) -> Value | None:
|
|
644
|
+
return self.get_child(name, INTRINSIC_OPT_OUT)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def find_transitive_deps(
|
|
648
|
+
modname: str, all_deps: dict[str, dict[str, set[tuple[str, str]]]]
|
|
649
|
+
) -> set[str]:
|
|
650
|
+
"""Find all transitive dependency modules of `modname`.
|
|
651
|
+
|
|
652
|
+
Given an `alldeps` dictionary of {modname: {name: {(module, name)}}}, return
|
|
653
|
+
the transitive closure of module names depended on by `modname` (not
|
|
654
|
+
including `modname` itself).
|
|
655
|
+
"""
|
|
656
|
+
worklist = {dep for deps in all_deps.get(modname, {}).values() for dep in deps}
|
|
657
|
+
ret = set()
|
|
658
|
+
seen = set()
|
|
659
|
+
while worklist:
|
|
660
|
+
dep = worklist.pop()
|
|
661
|
+
seen.add(dep)
|
|
662
|
+
mod, name = dep
|
|
663
|
+
ret.add(mod)
|
|
664
|
+
worklist.update(all_deps.get(mod, {}).get(name, set()).difference(seen))
|
|
665
|
+
ret.discard(modname)
|
|
666
|
+
return ret
|