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.
Files changed (68) hide show
  1. __static__/__init__.py +641 -0
  2. __static__/compiler_flags.py +8 -0
  3. __static__/enum.py +160 -0
  4. __static__/native_utils.py +77 -0
  5. __static__/type_code.py +48 -0
  6. __strict__/__init__.py +39 -0
  7. _cinderx.so +0 -0
  8. cinderx/__init__.py +577 -0
  9. cinderx/__pycache__/__init__.cpython-314.pyc +0 -0
  10. cinderx/_asyncio.py +156 -0
  11. cinderx/compileall.py +710 -0
  12. cinderx/compiler/__init__.py +40 -0
  13. cinderx/compiler/__main__.py +137 -0
  14. cinderx/compiler/config.py +7 -0
  15. cinderx/compiler/consts.py +72 -0
  16. cinderx/compiler/debug.py +70 -0
  17. cinderx/compiler/dis_stable.py +283 -0
  18. cinderx/compiler/errors.py +151 -0
  19. cinderx/compiler/flow_graph_optimizer.py +1287 -0
  20. cinderx/compiler/future.py +91 -0
  21. cinderx/compiler/misc.py +32 -0
  22. cinderx/compiler/opcode_cinder.py +18 -0
  23. cinderx/compiler/opcode_static.py +100 -0
  24. cinderx/compiler/opcodebase.py +158 -0
  25. cinderx/compiler/opcodes.py +991 -0
  26. cinderx/compiler/optimizer.py +547 -0
  27. cinderx/compiler/pyassem.py +3711 -0
  28. cinderx/compiler/pycodegen.py +7660 -0
  29. cinderx/compiler/pysourceloader.py +62 -0
  30. cinderx/compiler/static/__init__.py +1404 -0
  31. cinderx/compiler/static/compiler.py +629 -0
  32. cinderx/compiler/static/declaration_visitor.py +335 -0
  33. cinderx/compiler/static/definite_assignment_checker.py +280 -0
  34. cinderx/compiler/static/effects.py +160 -0
  35. cinderx/compiler/static/module_table.py +666 -0
  36. cinderx/compiler/static/type_binder.py +2176 -0
  37. cinderx/compiler/static/types.py +10580 -0
  38. cinderx/compiler/static/util.py +81 -0
  39. cinderx/compiler/static/visitor.py +91 -0
  40. cinderx/compiler/strict/__init__.py +69 -0
  41. cinderx/compiler/strict/class_conflict_checker.py +249 -0
  42. cinderx/compiler/strict/code_gen_base.py +409 -0
  43. cinderx/compiler/strict/common.py +507 -0
  44. cinderx/compiler/strict/compiler.py +352 -0
  45. cinderx/compiler/strict/feature_extractor.py +130 -0
  46. cinderx/compiler/strict/flag_extractor.py +97 -0
  47. cinderx/compiler/strict/loader.py +827 -0
  48. cinderx/compiler/strict/preprocessor.py +11 -0
  49. cinderx/compiler/strict/rewriter/__init__.py +5 -0
  50. cinderx/compiler/strict/rewriter/remove_annotations.py +84 -0
  51. cinderx/compiler/strict/rewriter/rewriter.py +975 -0
  52. cinderx/compiler/strict/runtime.py +77 -0
  53. cinderx/compiler/symbols.py +1754 -0
  54. cinderx/compiler/unparse.py +414 -0
  55. cinderx/compiler/visitor.py +194 -0
  56. cinderx/jit.py +230 -0
  57. cinderx/opcode.py +202 -0
  58. cinderx/static.py +113 -0
  59. cinderx/strictmodule.py +6 -0
  60. cinderx/test_support.py +341 -0
  61. cinderx-2026.1.16.2.dist-info/METADATA +15 -0
  62. cinderx-2026.1.16.2.dist-info/RECORD +68 -0
  63. cinderx-2026.1.16.2.dist-info/WHEEL +6 -0
  64. cinderx-2026.1.16.2.dist-info/licenses/LICENSE +21 -0
  65. cinderx-2026.1.16.2.dist-info/top_level.txt +5 -0
  66. opcodes/__init__.py +0 -0
  67. opcodes/assign_opcode_numbers.py +272 -0
  68. opcodes/cinderx_opcodes.py +121 -0
@@ -0,0 +1,975 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ # pyre-strict
4
+
5
+ from __future__ import annotations
6
+
7
+ import ast
8
+ import sys
9
+ from ast import (
10
+ alias,
11
+ AnnAssign,
12
+ arg,
13
+ Assign,
14
+ AST,
15
+ AsyncFunctionDef,
16
+ Attribute,
17
+ Call,
18
+ ClassDef,
19
+ Constant,
20
+ copy_location,
21
+ DictComp,
22
+ expr,
23
+ FunctionDef,
24
+ GeneratorExp,
25
+ Global,
26
+ Import,
27
+ ImportFrom,
28
+ Lambda,
29
+ ListComp,
30
+ Module,
31
+ Name,
32
+ NodeVisitor,
33
+ SetComp,
34
+ stmt,
35
+ Try,
36
+ )
37
+ from symtable import SymbolTable
38
+ from types import ModuleType
39
+ from typing import (
40
+ cast,
41
+ final,
42
+ Generic,
43
+ Iterable,
44
+ List,
45
+ Mapping,
46
+ MutableMapping,
47
+ Optional,
48
+ Sequence,
49
+ TypeVar,
50
+ Union,
51
+ )
52
+
53
+ from ..common import (
54
+ AstRewriter,
55
+ get_symbol_map,
56
+ imported_name,
57
+ lineinfo,
58
+ mangle_priv_name,
59
+ ScopeStack,
60
+ SymbolMap,
61
+ SymbolScope,
62
+ )
63
+
64
+
65
+ # We want to transform a module which looks something like:
66
+ #
67
+ # from strict_modules import cached_property
68
+ #
69
+ # x = 1
70
+ # def f():
71
+ # return min(x, 20)
72
+ #
73
+ # class C:
74
+ # @cached_property
75
+ # def f(self): return 42
76
+ #
77
+ # C.foo = 42
78
+ #
79
+ # def g():
80
+ # class C: pass
81
+ # return C
82
+ #
83
+ # Into something like:
84
+ #
85
+ # min = <builtins>.min # we have a fix set of builtins we execute against
86
+ #
87
+ # # We want some helper methods to be available to us:
88
+ # <strict-modules> = <fixed-modules>["strict_modules"]
89
+ #
90
+ # x = 1
91
+ # def f():
92
+ # return min(x, 20)
93
+ #
94
+ # @<init-cached-properties>({'f': '_f_impl'})
95
+ # class C:
96
+ # __slots__ = ('f', )
97
+
98
+ # def _f_impl(self):
99
+ # return 42
100
+ #
101
+ #
102
+ # C.foo = 42
103
+ #
104
+ # def g():
105
+ # class C: pass
106
+ # return C
107
+ # <classes> = []
108
+ #
109
+ #
110
+ #
111
+ # # Top-level assigned names
112
+ #
113
+ # Note: we use non-valid Python identifeirs for anything which we introduce like
114
+ # <strict-module> and use valid Python identifiers for variables which will be accessible
115
+ # from within the module.
116
+
117
+
118
+ def make_arg(name: str) -> arg:
119
+ return lineinfo(ast.arg(name, None))
120
+
121
+
122
+ TAst = TypeVar("TAst", bound=AST)
123
+
124
+
125
+ def make_assign(*a: object, **kw: object) -> Assign:
126
+ # pyre-fixme[6]: For 1st argument expected `List[expr]` but got `object`.
127
+ # pyre-fixme[6]: For 2nd argument expected `expr` but got `object`.
128
+ # pyre-fixme[6]: For 3rd argument expected `Optional[str]` but got `object`.
129
+ node = Assign(*a, **kw)
130
+ node.type_comment = None
131
+ return node
132
+
133
+
134
+ def copyline(from_node: AST, to_node: TAst) -> TAst:
135
+ # pyre-fixme[16]: `AST` has no attribute `lineno`.
136
+ to_node.lineno = from_node.lineno
137
+ # pyre-fixme[16]: `AST` has no attribute `col_offset`.
138
+ to_node.col_offset = from_node.col_offset
139
+ return to_node
140
+
141
+
142
+ _IMPLICIT_GLOBALS = [
143
+ "__name__",
144
+ "__loader__",
145
+ "__package__",
146
+ "__spec__",
147
+ "__path__",
148
+ "__file__",
149
+ "__cached__",
150
+ ]
151
+
152
+
153
+ def make_function(name: str, pos_args: list[arg]) -> FunctionDef:
154
+ # pyre-fixme[20]: Argument `args` expected.
155
+ func = lineinfo(ast.FunctionDef())
156
+ func.name = name
157
+ # pyre-fixme[20]: Argument `args` expected.
158
+ args = ast.arguments()
159
+ args.kwonlyargs = []
160
+ args.kw_defaults = []
161
+ args.defaults = []
162
+ args.args = pos_args
163
+ args.posonlyargs = []
164
+ func.args = args
165
+ args.kwarg = None
166
+ args.vararg = None
167
+ func.decorator_list = []
168
+ func.returns = None
169
+ func.type_comment = ""
170
+ return func
171
+
172
+
173
+ def make_assign_empty_list(name: str) -> Assign:
174
+ return lineinfo(
175
+ make_assign(
176
+ [lineinfo(ast.Name(name, ast.Store()))],
177
+ lineinfo(ast.List([], ast.Load())),
178
+ )
179
+ )
180
+
181
+
182
+ GLOBALS_HELPER_ALIAS = "<globals-helper>"
183
+
184
+
185
+ @final
186
+ class StrictModuleRewriter:
187
+ """rewrites a module body so that all global variables are transformed into
188
+ local variables, and are closed over by the enclosing functions. This will
189
+ ultimately remove all LOAD_GLOBAL/STORE_GLOBAL opcodes, and therefore will
190
+ also have the side effect of making the module read-only as globals will
191
+ not be exposed."""
192
+
193
+ def __init__(
194
+ self,
195
+ root: Module,
196
+ table: SymbolTable,
197
+ filename: str,
198
+ modname: str,
199
+ mode: str,
200
+ optimize: int,
201
+ builtins: ModuleType | Mapping[str, object] = __builtins__,
202
+ is_static: bool = False,
203
+ ) -> None:
204
+ if not isinstance(builtins, dict):
205
+ builtins = builtins.__dict__
206
+
207
+ self.root = root
208
+ self.table = table
209
+ self.filename = filename
210
+ self.modname = modname
211
+ self.mode = mode
212
+ self.optimize = optimize
213
+ self.builtins: Mapping[str, object] = builtins
214
+ self.symbol_map: SymbolMap = get_symbol_map(root, table)
215
+ scope: SymbolScope[None, None] = SymbolScope(table, None)
216
+ self.visitor = ImmutableVisitor(ScopeStack(scope, symbol_map=self.symbol_map))
217
+ # Top-level statements in the returned code object...
218
+ self.code_stmts: list[stmt] = []
219
+ self.is_static = is_static
220
+
221
+ def transform(self) -> ast.Module:
222
+ original_first_node = self.root.body[0] if self.root.body else None
223
+ self.visitor.visit(self.root)
224
+ self.visitor.global_sets.update(_IMPLICIT_GLOBALS)
225
+
226
+ for argname in _IMPLICIT_GLOBALS:
227
+ self.visitor.globals.add(argname)
228
+
229
+ # pyre-fixme[20]: Argument `type_ignores` expected.
230
+ mod = ast.Module(
231
+ [
232
+ *self.get_future_imports(),
233
+ *self.transform_body(),
234
+ ]
235
+ )
236
+ if mod.body and original_first_node:
237
+ # this isn't obvious but the new mod body is empty
238
+ # if the original module body is empty. Therefore there
239
+ # is always a location to copy
240
+ copy_location(mod.body[0], original_first_node)
241
+
242
+ mod.type_ignores = []
243
+ return mod
244
+
245
+ def get_future_imports(self) -> Iterable[stmt]:
246
+ if self.visitor.future_imports:
247
+ yield lineinfo(
248
+ ImportFrom("__future__", list(self.visitor.future_imports), 0)
249
+ )
250
+
251
+ def del_global(self, name: str) -> stmt:
252
+ return lineinfo(ast.Delete([lineinfo(ast.Name(name, ast.Del()))]))
253
+
254
+ def store_global(self, name: str, value: expr) -> stmt:
255
+ return lineinfo(make_assign([lineinfo(ast.Name(name, ast.Store()))], value))
256
+
257
+ def load_global(self, name: str) -> expr:
258
+ return lineinfo(ast.Name(name, ast.Load()))
259
+
260
+ def create_annotations(self) -> stmt:
261
+ return self.store_global("__annotations__", lineinfo(ast.Dict([], [])))
262
+
263
+ def make_transformer(
264
+ self,
265
+ scopes: ScopeStack[None, ScopeData],
266
+ ) -> ImmutableTransformer:
267
+ return ImmutableTransformer(
268
+ scopes,
269
+ self.modname,
270
+ self.builtins,
271
+ self.visitor.globals,
272
+ self.visitor.global_sets,
273
+ self.visitor.global_dels,
274
+ self.visitor.future_imports,
275
+ self.is_static,
276
+ )
277
+
278
+ def transform_body(self) -> Iterable[stmt]:
279
+ scopes = ScopeStack(
280
+ SymbolScope(self.table, ScopeData()), symbol_map=self.symbol_map
281
+ )
282
+ transformer = self.make_transformer(scopes)
283
+ body = transformer.visit(self.root).body
284
+
285
+ return body
286
+
287
+
288
+ def rewrite(
289
+ root: Module,
290
+ table: SymbolTable,
291
+ filename: str,
292
+ modname: str,
293
+ mode: str = "exec",
294
+ optimize: int = -1,
295
+ builtins: ModuleType | Mapping[str, object] = __builtins__,
296
+ is_static: bool = False,
297
+ ) -> Module:
298
+ return StrictModuleRewriter(
299
+ root,
300
+ table,
301
+ filename,
302
+ modname,
303
+ mode,
304
+ optimize,
305
+ builtins,
306
+ is_static=is_static,
307
+ ).transform()
308
+
309
+
310
+ TTransformedStmt = Union[Optional[AST], List[AST]]
311
+ TVar = TypeVar("TScope")
312
+ TScopeData = TypeVar("TData")
313
+
314
+
315
+ class SymbolVisitor(Generic[TVar, TScopeData], NodeVisitor):
316
+ def __init__(self, scopes: ScopeStack[TVar, TScopeData]) -> None:
317
+ self.scopes = scopes
318
+
319
+ @property
320
+ def skip_annotations(self) -> bool:
321
+ return False
322
+
323
+ def is_global(self, name: str) -> bool:
324
+ return self.scopes.is_global(name)
325
+
326
+ def scope_for(self, name: str) -> SymbolScope[TVar, TScopeData]:
327
+ return self.scopes.scope_for(name)
328
+
329
+ def visit_Comp_Outer(
330
+ self,
331
+ node: ListComp | SetComp | GeneratorExp | DictComp,
332
+ update: bool = False,
333
+ ) -> None:
334
+ iter = self.visit(node.generators[0].iter)
335
+ if update:
336
+ node.generators[0].iter = iter
337
+
338
+ def visit_Try(self, node: Try) -> TTransformedStmt:
339
+ # Need to match the order the symbol visitor constructs these in, which
340
+ # walks orelse before handlers.
341
+ for val in node.body:
342
+ self.visit(val)
343
+
344
+ for val in node.orelse:
345
+ self.visit(val)
346
+
347
+ for val in node.handlers:
348
+ self.visit(val)
349
+
350
+ for val in node.finalbody:
351
+ self.visit(val)
352
+
353
+ return node
354
+
355
+ def visit_Comp_Inner(
356
+ self,
357
+ node: ListComp | SetComp | GeneratorExp | DictComp,
358
+ update: bool = False,
359
+ scope_node: ListComp | SetComp | GeneratorExp | DictComp | None = None,
360
+ ) -> None:
361
+ scope_node = scope_node or node
362
+ with self.scopes.with_node_scope(scope_node):
363
+ if isinstance(node, DictComp):
364
+ key = self.visit(node.key)
365
+ if update:
366
+ node.key = key
367
+ value = self.visit(node.value)
368
+ if update:
369
+ node.value = value
370
+ else:
371
+ elt = self.visit(node.elt)
372
+ if update:
373
+ node.elt = elt
374
+
375
+ self.walk_many(node.generators[0].ifs, update)
376
+
377
+ target = self.visit(node.generators[0].target)
378
+ if update:
379
+ # pyre-fixme[16]: `DictComp` has no attribute `target`.
380
+ # pyre-fixme[16]: `GeneratorExp` has no attribute `target`.
381
+ # pyre-fixme[16]: `ListComp` has no attribute `target`.
382
+ # pyre-fixme[16]: `SetComp` has no attribute `target`.
383
+ node.target = target
384
+
385
+ gens = node.generators[1:]
386
+ self.walk_many(gens, update)
387
+ if update:
388
+ node.generators[1:] = gens
389
+
390
+ def visit_Func_Outer(
391
+ self, node: AsyncFunctionDef | FunctionDef | Lambda, update: bool = False
392
+ ) -> None:
393
+ args = self.visit(node.args)
394
+ if update:
395
+ node.args = args
396
+
397
+ if isinstance(node, (AsyncFunctionDef, FunctionDef)):
398
+ retnode = node.returns
399
+ if retnode and not self.skip_annotations:
400
+ returns = self.visit(retnode)
401
+ if update:
402
+ node.returns = returns
403
+
404
+ self.walk_many(node.decorator_list, update)
405
+
406
+ def visit_Func_Inner(
407
+ self,
408
+ node: AsyncFunctionDef | FunctionDef | Lambda,
409
+ update: bool = False,
410
+ scope_node: AsyncFunctionDef | FunctionDef | Lambda | None = None,
411
+ ) -> SymbolScope[TVar, TScopeData]:
412
+ scope_node = scope_node or node
413
+ with self.scopes.with_node_scope(scope_node) as next:
414
+ assert next
415
+ assert isinstance(node, Lambda) or node.name == next.symbols.get_name()
416
+
417
+ # visit body in function scope
418
+ if isinstance(node, Lambda):
419
+ new = self.visit(node.body)
420
+ if update:
421
+ node.body = new
422
+ else:
423
+ self.walk_many(node.body, update)
424
+
425
+ return next
426
+
427
+ def visit_Class_Outer(self, node: ClassDef, update: bool = False) -> None:
428
+ self.walk_many(node.bases, update)
429
+ self.walk_many(node.keywords, update)
430
+ self.walk_many(node.decorator_list, update)
431
+
432
+ def visit_Class_Inner(
433
+ self,
434
+ node: ClassDef,
435
+ update: bool = False,
436
+ scope_node: ClassDef | None = None,
437
+ ) -> SymbolScope[TVar, TScopeData]:
438
+ scope_node = scope_node or node
439
+ with self.scopes.with_node_scope(scope_node) as next:
440
+ assert next
441
+ assert node.name == next.symbols.get_name()
442
+ # visit body in class scope
443
+ self.walk_many(node.body, update)
444
+
445
+ return next
446
+
447
+ def walk_many(
448
+ self,
449
+ stmts: list[TAst],
450
+ update: bool = False,
451
+ ) -> None:
452
+ new_stmts = []
453
+ for statement in stmts:
454
+ new = self.visit(statement)
455
+ if isinstance(new, ast.AST):
456
+ new_stmts.append(new)
457
+ elif new is not None:
458
+ new_stmts.extend(new)
459
+ if update:
460
+ stmts[:] = new_stmts
461
+
462
+ def visit_Lambda(self, node: Lambda) -> TTransformedStmt:
463
+ self.visit_Func_Outer(node)
464
+
465
+ self.visit_Func_Inner(node)
466
+
467
+ def visit_comp(
468
+ self, node: ListComp | SetComp | DictComp | GeneratorExp
469
+ ) -> expr | None:
470
+ self.visit_Comp_Outer(node)
471
+
472
+ self.visit_Comp_Inner(node)
473
+
474
+ return node
475
+
476
+ def visit_ListComp(self, node: ListComp) -> expr | None:
477
+ return self.visit_comp(node)
478
+
479
+ def visit_SetComp(self, node: SetComp) -> expr | None:
480
+ return self.visit_comp(node)
481
+
482
+ def visit_GeneratorExp(self, node: GeneratorExp) -> expr | None:
483
+ return self.visit_comp(node)
484
+
485
+ def visit_DictComp(self, node: DictComp) -> expr | None:
486
+ return self.visit_comp(node)
487
+
488
+
489
+ @final
490
+ class ImmutableVisitor(SymbolVisitor[None, None]):
491
+ def __init__(self, scopes: ScopeStack[None, None]) -> None:
492
+ super().__init__(scopes)
493
+ self.globals: set[str] = set()
494
+ self.global_sets: set[str] = set()
495
+ self.global_dels: set[str] = set()
496
+ self.future_imports: set[alias] = set()
497
+
498
+ def future_annotations(self) -> bool:
499
+ for a in self.future_imports:
500
+ if a.name == "annotations":
501
+ return True
502
+ return False
503
+
504
+ @property
505
+ def skip_annotations(self) -> bool:
506
+ return self.future_annotations() or sys.version_info >= (3, 14)
507
+
508
+ def load_name(self, name: str) -> None:
509
+ if self.is_global(name):
510
+ self.globals.add(name)
511
+
512
+ def store_name(self, name: str) -> None:
513
+ if self.is_global(name):
514
+ self.globals.add(name)
515
+ self.global_sets.add(name)
516
+
517
+ def del_name(self, name: str) -> None:
518
+ if self.is_global(name):
519
+ self.globals.add(name)
520
+ self.global_sets.add(name)
521
+ self.global_dels.add(name)
522
+
523
+ def visit_Global(self, node: Global) -> None:
524
+ for name in node.names:
525
+ self.globals.add(name)
526
+
527
+ def visit_Name(self, node: Name) -> None:
528
+ if isinstance(node.ctx, ast.Load):
529
+ self.load_name(node.id)
530
+ elif isinstance(node.ctx, ast.Store):
531
+ self.store_name(node.id)
532
+ elif isinstance(node.ctx, ast.Del):
533
+ self.del_name(node.id)
534
+
535
+ def visit_ImportFrom(self, node: ImportFrom) -> None:
536
+ if node.module == "__future__":
537
+ self.future_imports.update(node.names)
538
+
539
+ for name in node.names:
540
+ self.store_name(name.asname or name.name)
541
+
542
+ def visit_Import(self, node: Import) -> None:
543
+ for name in node.names:
544
+ self.store_name(imported_name(name))
545
+
546
+ def visit_Call(self, node: Call) -> None:
547
+ func = node.func
548
+ if isinstance(func, ast.Name):
549
+ # We don't currently allow aliasing or shadowing exec/eval
550
+ # so this check is currently sufficient.
551
+ if (func.id == "exec" or func.id == "eval") and len(node.args) < 2:
552
+ # We'll need access to our globals() helper when we transform
553
+ # the ast
554
+ self.globals.add("globals")
555
+ self.globals.add("locals")
556
+ self.generic_visit(node)
557
+
558
+ def visit_Try(self, node: Try) -> None:
559
+ super().visit_Try(node)
560
+
561
+ for handler in node.handlers:
562
+ name = handler.name
563
+ if name is None:
564
+ continue
565
+ self.del_name(name)
566
+
567
+ def visit_ClassDef(self, node: ClassDef) -> None:
568
+ self.visit_Class_Outer(node)
569
+
570
+ self.store_name(node.name)
571
+
572
+ self.visit_Class_Inner(node)
573
+
574
+ def visit_FunctionDef(self, node: FunctionDef) -> None:
575
+ self.visit_Func_Outer(node)
576
+
577
+ self.store_name(node.name)
578
+
579
+ self.visit_Func_Inner(node)
580
+
581
+ def visit_arg(self, node: ast.arg) -> None:
582
+ if not self.skip_annotations:
583
+ return self.generic_visit(node)
584
+
585
+ def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> None:
586
+ self.visit_Func_Outer(node)
587
+
588
+ self.store_name(node.name)
589
+
590
+ self.visit_Func_Inner(node)
591
+
592
+ def visit_AnnAssign(self, node: AnnAssign) -> None:
593
+ self.visit(node.target)
594
+ if not self.skip_annotations:
595
+ self.visit(node.annotation)
596
+ value = node.value
597
+ if value:
598
+ self.visit(value)
599
+
600
+
601
+ class ScopeData:
602
+ def __init__(self) -> None:
603
+ self.has_classdefs = False
604
+
605
+ def visit_Assign(self, node: Assign) -> None:
606
+ pass
607
+
608
+ def visit_AnnAssign(self, node: AnnAssign) -> None:
609
+ pass
610
+
611
+ def visit_decorators(self, node: ClassDef | FunctionDef | AsyncFunctionDef) -> None:
612
+ # visit the current node held by the scope data
613
+ pass
614
+
615
+
616
+ @final
617
+ class ClassScope(ScopeData):
618
+ def __init__(self, node: ClassDef) -> None:
619
+ super().__init__()
620
+ self.node = node
621
+ self.instance_fields: set[str] = set()
622
+ self.cached_props: dict[str, object] = {}
623
+ self.slots_enabled: bool = False
624
+ self.loose_slots: bool = False
625
+ self.extra_slots: list[str] | None = None
626
+
627
+ def visit_AnnAssign(self, node: AnnAssign) -> None:
628
+ target = node.target
629
+ if node.value is None and isinstance(target, Name):
630
+ self.instance_fields.add(target.id)
631
+
632
+
633
+ @final
634
+ class FunctionScope(ScopeData):
635
+ def __init__(self, node: AsyncFunctionDef | FunctionDef, parent: ScopeData) -> None:
636
+ super().__init__()
637
+ self.node = node
638
+ self.parent = parent
639
+ self.is_cached_prop: bool = False
640
+ self.cached_prop_value: object = None
641
+
642
+ def visit_AnnAssign(self, node: AnnAssign) -> None:
643
+ parent = self.parent
644
+ target = node.target
645
+ if (
646
+ self.node.name == "__init__"
647
+ and self.node.args.args
648
+ and isinstance(parent, ClassScope)
649
+ and isinstance(target, Attribute)
650
+ ):
651
+ self.add_attr_name(target, parent)
652
+
653
+ def add_attr_name(self, target: Attribute, parent: ClassScope) -> None:
654
+ """records self.name = ... when salf matches the 1st parameter"""
655
+ value = target.value
656
+ node = self.node
657
+ if isinstance(value, Name) and value.id == node.args.args[0].arg:
658
+ parent.instance_fields.add(target.attr)
659
+
660
+ def assign_worker(self, targets: Sequence[AST], parent: ClassScope) -> None:
661
+ for target in targets:
662
+ if isinstance(target, Attribute):
663
+ self.add_attr_name(target, parent)
664
+ elif isinstance(target, (ast.Tuple, ast.List)):
665
+ self.assign_worker(target.elts, parent)
666
+
667
+ def visit_Assign(self, node: Assign) -> None:
668
+ parent = self.parent
669
+ if (
670
+ self.node.name == "__init__"
671
+ and isinstance(parent, ClassScope)
672
+ and self.node.args.args
673
+ ):
674
+ self.assign_worker(node.targets, parent)
675
+
676
+
677
+ @final
678
+ class ImmutableTransformer(SymbolVisitor[None, ScopeData], AstRewriter):
679
+ def __init__(
680
+ self,
681
+ symbols: ScopeStack[None, ScopeData],
682
+ modname: str,
683
+ builtins: Mapping[str, object],
684
+ globals: set[str],
685
+ global_sets: set[str],
686
+ global_dels: set[str],
687
+ future_imports: set[alias],
688
+ is_static: bool,
689
+ ) -> None:
690
+ super().__init__(symbols)
691
+ symbols.scope_factory = self.make_scope
692
+ self.modname = modname
693
+ self.future_imports = future_imports
694
+ self.is_static = is_static
695
+
696
+ def make_scope(
697
+ self,
698
+ symtable: SymbolTable,
699
+ node: AST,
700
+ vars: MutableMapping[str, None] | None = None,
701
+ ) -> SymbolScope[None, ScopeData]:
702
+ if isinstance(node, (FunctionDef, AsyncFunctionDef)):
703
+ data: ScopeData = FunctionScope(node, self.scopes.scopes[-1].scope_data)
704
+ elif isinstance(node, ClassDef):
705
+ data: ScopeData = ClassScope(node)
706
+ else:
707
+ data: ScopeData = ScopeData()
708
+ return SymbolScope(symtable, data)
709
+
710
+ def visit_Assign(self, node: Assign) -> TTransformedStmt:
711
+ self.scopes.scopes[-1].scope_data.visit_Assign(node)
712
+ node = self.update_node(
713
+ node,
714
+ targets=self.walk_list(node.targets),
715
+ value=self.visit(node.value),
716
+ )
717
+ return node
718
+
719
+ def visit_AnnAssign(self, node: AnnAssign) -> TTransformedStmt:
720
+ self.scopes.scopes[-1].scope_data.visit_AnnAssign(node)
721
+ return self.generic_visit(node)
722
+
723
+ def make_base_class_dict_test_stmts(
724
+ self, bases: list[TAst], instance_fields: set[str], location_node: ast.ClassDef
725
+ ) -> stmt:
726
+ slots_stmt = self.make_slots_stmt(instance_fields)
727
+ # if there are non-names in the bases of the class, give up and just create slots
728
+ if not all(isinstance(b, ast.Name) for b in bases):
729
+ return slots_stmt
730
+ # if __dict__ is not added to instance fields (no loose slots), just slotify
731
+ if "__dict__" not in instance_fields:
732
+ return slots_stmt
733
+ # generate code that decide whether __dict__ should be included
734
+ # if any('__dict__' in getattr(_t, '__dict__', ()) for b in <bases> for _t in b.mro()):
735
+ # __slots__ = <slots without __dict__>
736
+ # else:
737
+ # __slots__ = <slots with __dict__>
738
+ names = [lineinfo(ast.Name(cast(ast.Name, n).id, ast.Load())) for n in bases]
739
+ condition = lineinfo(
740
+ ast.Call(
741
+ lineinfo(ast.Name("any", ast.Load())),
742
+ [
743
+ lineinfo(
744
+ ast.GeneratorExp(
745
+ lineinfo(
746
+ ast.Compare(
747
+ lineinfo(Constant("__dict__")),
748
+ [lineinfo(ast.In())],
749
+ [
750
+ lineinfo(
751
+ ast.Call(
752
+ lineinfo(
753
+ ast.Name("getattr", ast.Load())
754
+ ),
755
+ [
756
+ lineinfo(
757
+ ast.Name("_t", ast.Load())
758
+ ),
759
+ lineinfo(Constant("__dict__")),
760
+ lineinfo(ast.Tuple([], ast.Load())),
761
+ ],
762
+ [],
763
+ )
764
+ )
765
+ ],
766
+ )
767
+ ),
768
+ [
769
+ lineinfo(
770
+ ast.comprehension(
771
+ lineinfo(ast.Name("b", ast.Store())),
772
+ # pyre-fixme[6]: For 1st argument expected
773
+ # `List[expr]` but got `List[Name]`.
774
+ lineinfo(ast.List(names, ast.Load())),
775
+ [],
776
+ 0,
777
+ )
778
+ ),
779
+ lineinfo(
780
+ ast.comprehension(
781
+ lineinfo(ast.Name("_t", ast.Store())),
782
+ lineinfo(
783
+ ast.Call(
784
+ lineinfo(
785
+ ast.Attribute(
786
+ lineinfo(
787
+ ast.Name("b", ast.Load())
788
+ ),
789
+ "mro",
790
+ ast.Load(),
791
+ )
792
+ ),
793
+ [],
794
+ [],
795
+ )
796
+ ),
797
+ [],
798
+ 0,
799
+ )
800
+ ),
801
+ ],
802
+ ),
803
+ target=location_node,
804
+ )
805
+ ],
806
+ [],
807
+ )
808
+ )
809
+ slots_stmt_without_dict = self.make_slots_stmt(instance_fields - {"__dict__"})
810
+ return lineinfo(ast.If(condition, [slots_stmt_without_dict], [slots_stmt]))
811
+
812
+ def make_slots_stmt(self, instance_fields: set[str]) -> Assign:
813
+ return lineinfo(
814
+ make_assign(
815
+ [lineinfo(Name("__slots__", ast.Store()))],
816
+ lineinfo(
817
+ ast.Tuple(
818
+ [lineinfo(Constant(name)) for name in instance_fields],
819
+ ast.Load(),
820
+ )
821
+ ),
822
+ )
823
+ )
824
+
825
+ def visit_ClassDef(self, node: ClassDef) -> TTransformedStmt:
826
+ outer_scope_data = self.scopes.scopes[-1].scope_data
827
+ outer_scope_data.has_classdefs = True
828
+ orig_node = node
829
+ node = self.clone_node(node)
830
+ self.visit_Class_Outer(node, True)
831
+ class_scope = self.visit_Class_Inner(node, True, scope_node=orig_node)
832
+ scope_data = class_scope.scope_data
833
+ assert isinstance(scope_data, ClassScope), type(class_scope).__name__
834
+ scope_data.visit_decorators(node)
835
+
836
+ slots_enabled = scope_data.slots_enabled and not self.is_static
837
+ if slots_enabled:
838
+ if scope_data.loose_slots:
839
+ scope_data.instance_fields.add("__dict__")
840
+ scope_data.instance_fields.add("__loose_slots__")
841
+ extra_slots = scope_data.extra_slots
842
+ if extra_slots:
843
+ scope_data.instance_fields.update(extra_slots)
844
+
845
+ node.body.append(
846
+ self.make_base_class_dict_test_stmts(
847
+ node.bases, scope_data.instance_fields, node
848
+ )
849
+ )
850
+
851
+ if scope_data.cached_props and slots_enabled:
852
+ # Add a decorator which replaces our name-mangled cache properties
853
+ # with a class-level decorator that converts them. We apply it as
854
+ # a decorator so that we get to initialize these before other
855
+ # decorators see the class.
856
+ node.decorator_list.append(
857
+ self.make_cached_property_init_decorator(scope_data, class_scope)
858
+ )
859
+
860
+ return node
861
+
862
+ def make_cached_property_init_decorator(
863
+ self,
864
+ scope_data: ClassScope,
865
+ class_scope: SymbolScope[TVar, TScopeData],
866
+ ) -> expr:
867
+ return lineinfo(
868
+ ast.Call(
869
+ lineinfo(ast.Name("<init-cached-properties>", ast.Load())),
870
+ [
871
+ lineinfo(
872
+ ast.Dict(
873
+ [
874
+ lineinfo(
875
+ Constant(mangle_priv_name(name, [class_scope]))
876
+ )
877
+ for name in scope_data.cached_props
878
+ ],
879
+ [
880
+ lineinfo(
881
+ ast.Tuple(
882
+ [
883
+ lineinfo(
884
+ Constant(
885
+ mangle_priv_name(
886
+ self.mangle_cached_prop(name),
887
+ [class_scope],
888
+ )
889
+ )
890
+ ),
891
+ lineinfo(Constant(value)),
892
+ ],
893
+ ast.Load(),
894
+ )
895
+ )
896
+ for name, value in scope_data.cached_props.items()
897
+ ],
898
+ )
899
+ )
900
+ ],
901
+ [],
902
+ )
903
+ )
904
+
905
+ def mangle_cached_prop(self, name: str) -> str:
906
+ return "_" + name + "_impl"
907
+
908
+ def visit_FunctionDef(self, node: FunctionDef) -> TTransformedStmt:
909
+ outer_scope = self.scopes.scopes[-1].scope_data
910
+ orig_node = node
911
+ node = self.clone_node(node)
912
+ self.visit_Func_Outer(node, True)
913
+
914
+ scope_data = self.visit_Func_Inner(node, True, scope_node=orig_node).scope_data
915
+ scope_data.visit_decorators(node)
916
+
917
+ res: TTransformedStmt = node
918
+
919
+ self.check_cached_prop(node, scope_data, outer_scope)
920
+
921
+ return res
922
+
923
+ def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> TTransformedStmt:
924
+ outer_scope = self.scopes.scopes[-1].scope_data
925
+ orig_node = node
926
+ node = self.clone_node(node)
927
+ self.visit_Func_Outer(node, True)
928
+
929
+ scope_data = self.visit_Func_Inner(node, True, scope_node=orig_node).scope_data
930
+ scope_data.visit_decorators(node)
931
+
932
+ self.check_cached_prop(node, scope_data, outer_scope)
933
+
934
+ return node
935
+
936
+ def check_cached_prop(
937
+ self,
938
+ node: AsyncFunctionDef | FunctionDef,
939
+ func_scope: ScopeData,
940
+ outer_scope: ScopeData,
941
+ ) -> None:
942
+ if isinstance(func_scope, FunctionScope) and isinstance(
943
+ outer_scope, ClassScope
944
+ ):
945
+ if func_scope.is_cached_prop:
946
+ # preprocessor already checked that outer scope is slotified
947
+ outer_scope.instance_fields.add(node.name)
948
+ outer_scope.cached_props[node.name] = func_scope.cached_prop_value
949
+ node.name = self.mangle_cached_prop(node.name)
950
+
951
+ def visit_Lambda(self, node: Lambda) -> TTransformedStmt:
952
+ orig_node = node
953
+ node = self.clone_node(node)
954
+ self.visit_Func_Outer(node, True)
955
+
956
+ self.visit_Func_Inner(node, True, scope_node=orig_node)
957
+
958
+ return node
959
+
960
+ def visit_ImportFrom(self, node: ImportFrom) -> TTransformedStmt:
961
+ if node.module == "__future__":
962
+ # We push these to the top of the module where they're required to be
963
+ return None
964
+ return node
965
+
966
+ def visit_comp(
967
+ self, node: ListComp | SetComp | DictComp | GeneratorExp
968
+ ) -> expr | None:
969
+ orig_node = node
970
+ node = self.clone_node(node)
971
+ self.visit_Comp_Outer(node, True)
972
+
973
+ self.visit_Comp_Inner(node, True, scope_node=orig_node)
974
+
975
+ return node