Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 543dbce

Browse files
authored
[mypyc] Use shared value for Final attribute initialized in class body (#21932)
Use a module-level static for Final attributes initialized in class body: ``` class C: X: Final = [1] # Only one list allocated ``` Previously such final attributes where stored in the per-instance struct, and a new object was allocated for each instance, which deviated from Python semantics and was inefficient. Now the initializer is only run once. Final attributes that have a type with an overlapping error value like `i64` are still not supported properly. This is left as follow-up work. I used coding agent assist but reviewed the result manually and iterated on it.
1 parent 8f26230 commit 543dbce

11 files changed

Lines changed: 575 additions & 24 deletions

mypyc/ir/class_ir.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,11 @@ def __init__(
162162
# Attributes that must survive generator/coroutine completion because
163163
# escaped nested functions may still read them as closure variables.
164164
self.attrs_to_keep_alive_on_completion: set[str] = set()
165-
# Final attributes defined in the class (not inherited)
165+
# Final attributes initialized in the __init__ method (not inherited)
166166
self.final_attributes: set[str] = set()
167+
# Final attributes defined in class body as "X: Final = <value>" (not inherited).
168+
# They get no slot in the instance struct. The value lives in a module-level static.
169+
self.class_final_attributes: dict[str, RType] = {}
167170
# Deletable attributes
168171
self.deletable: list[str] = []
169172
# We populate method_types with the signatures of every method before
@@ -302,6 +305,25 @@ def is_final_attr(self, name: str) -> bool:
302305
return False
303306
return False
304307

308+
def class_final_attr_details(self, name: str) -> tuple[RType, ClassIR] | None:
309+
"""Look up a (possibly inherited) class-body Final attribute.
310+
311+
Returns the attribute type and the class that defines it, or None if this
312+
class has no such attribute. The defining class is what identifies the
313+
static holding the value, so callers need it to build the static's name.
314+
315+
Deliberately kept out of attr_details()/has_attr(): these attributes have no
316+
instance slot, so callers that want to read or write one must not treat them
317+
as ordinary attributes.
318+
"""
319+
for ir in self.mro:
320+
if name in ir.class_final_attributes:
321+
return ir.class_final_attributes[name], ir
322+
if name in ir.attributes or name in ir.property_types:
323+
# Shadowed by a real attribute or property closer in the MRO.
324+
return None
325+
return None
326+
305327
def method_decl(self, name: str) -> FuncDecl:
306328
for ir in self.mro:
307329
if name in ir.method_decls:
@@ -435,6 +457,9 @@ def serialize(self) -> JsonDict:
435457
"attributes": [(k, t.serialize()) for k, t in self.attributes.items()],
436458
"attrs_to_keep_alive_on_completion": sorted(self.attrs_to_keep_alive_on_completion),
437459
"final_attributes": sorted(self.final_attributes),
460+
"class_final_attributes": [
461+
(k, t.serialize()) for k, t in self.class_final_attributes.items()
462+
],
438463
# We try to serialize a name reference, but if the decl isn't in methods
439464
# then we can't be sure that will work so we serialize the whole decl.
440465
"method_decls": [
@@ -499,6 +524,9 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
499524
ir.attributes = {k: deserialize_type(t, ctx) for k, t in data["attributes"]}
500525
ir.attrs_to_keep_alive_on_completion = set(data["attrs_to_keep_alive_on_completion"])
501526
ir.final_attributes = set(data["final_attributes"])
527+
ir.class_final_attributes = {
528+
k: deserialize_type(t, ctx) for k, t in data["class_final_attributes"]
529+
}
502530
ir.method_decls = {
503531
k: ctx.functions[v].decl if isinstance(v, str) else FuncDecl.deserialize(v, ctx)
504532
for k, v in data["method_decls"]

mypyc/irbuild/builder.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,15 +1226,18 @@ def is_synthetic_type(self, typ: TypeInfo) -> bool:
12261226
return typ.is_named_tuple or typ.is_newtype or typ.typeddict_type is not None
12271227

12281228
def get_final_ref(self, expr: MemberExpr) -> tuple[str, Var, bool] | None:
1229-
"""Check if `expr` is a final class or module attribute.
1229+
"""Check if `expr` is a final class, module or instance attribute.
12301230
1231-
Return False for instance attributes.
1232-
1233-
This needs to be done differently for class and module attributes to
1231+
This needs to be done differently for class, module and instance attributes to
12341232
correctly determine fully qualified name. Return a tuple that consists of
12351233
the qualified name, the corresponding Var node, and a flag indicating whether
12361234
the final name was defined in a compiled module. Return None if `expr` does not
12371235
refer to a final attribute.
1236+
1237+
Instance attributes only qualify if they are class-body Finals ("X: Final = ..."
1238+
in the class body), which have no instance slot -- see
1239+
ClassIR.class_final_attributes. Ordinary instance attributes, including
1240+
"self.x: Final = ..." set in __init__, return None.
12381241
"""
12391242
final_var = None
12401243
if isinstance(expr.expr, RefExpr) and isinstance(expr.expr.node, TypeInfo):
@@ -1254,10 +1257,69 @@ def get_final_ref(self, expr: MemberExpr) -> tuple[str, Var, bool] | None:
12541257
final_var = expr.node
12551258
fullname = expr.node.fullname
12561259
native = self.is_native_ref_expr(expr)
1260+
else:
1261+
# Possibly a class-body Final read through an instance ("self.X"). These
1262+
# have no instance slot, so read them exactly like "Cls.X".
1263+
var = self.get_class_final_var(expr)
1264+
if var is not None:
1265+
final_var = var
1266+
fullname = f"{var.info.fullname}.{var.name}"
1267+
native = self.is_native_module(var.info.module_name)
12571268
if final_var is not None:
12581269
return fullname, final_var, native
12591270
return None
12601271

1272+
def get_class_final_var(self, expr: MemberExpr) -> Var | None:
1273+
"""Return the Var for a class-body Final read through an instance expression.
1274+
1275+
Return None if `expr` isn't such a read; the caller then falls back to an
1276+
ordinary attribute read, which finds these names on the type object via
1277+
py_get_attr (correct, just slower).
1278+
1279+
A union-typed object qualifies only when every item resolves to the same
1280+
declaration, since otherwise the value differs per item.
1281+
"""
1282+
instance_type = get_proper_type(self.types.get(expr.expr))
1283+
if isinstance(instance_type, UnionType):
1284+
items = [get_proper_type(item) for item in instance_type.items]
1285+
else:
1286+
items = [instance_type]
1287+
found: Var | None = None
1288+
for item in items:
1289+
if not isinstance(item, Instance):
1290+
return None
1291+
var = self.class_final_var_of_instance(item, expr.name)
1292+
if var is None:
1293+
return None
1294+
if found is not None and var is not found:
1295+
return None
1296+
found = var
1297+
return found
1298+
1299+
def class_final_var_of_instance(self, instance: Instance, name: str) -> Var | None:
1300+
"""Look up a class-body Final attribute on a single instance type.
1301+
1302+
ClassIR is the authority on which attributes were compiled as class-body
1303+
Finals (mypyc.irbuild.util's is_class_body_final decides, and the answer
1304+
survives serialization), so we gate on it and only use the mypy symbol table
1305+
to find the defining class.
1306+
"""
1307+
class_ir = self.mapper.type_to_ir.get(instance.type)
1308+
if class_ir is None:
1309+
return None
1310+
details = class_ir.class_final_attr_details(name)
1311+
if details is None:
1312+
return None
1313+
_, defining_ir = details
1314+
sym = instance.type.get(name)
1315+
if sym is None or not isinstance(sym.node, Var) or not sym.node.is_final:
1316+
return None
1317+
# mypy's MRO and mypyc's can differ (traits), so only proceed when both agree
1318+
# on which class defines the attribute; that class names the static.
1319+
if sym.node.info.fullname != defining_ir.fullname:
1320+
return None
1321+
return sym.node
1322+
12611323
def emit_load_final(
12621324
self, final_var: Var, fullname: str, name: str, native: bool, typ: Type, line: int
12631325
) -> Value | None:

mypyc/irbuild/prepare.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
default_attr_name,
7676
get_func_def,
7777
get_mypyc_attrs,
78+
is_class_body_final,
7879
is_dataclass,
7980
is_extension_class,
8081
is_trait,
@@ -637,6 +638,11 @@ def prepare_methods_and_attributes(
637638
assert node.node.type, "Class member %s missing type" % name
638639
if not node.node.is_classvar and name not in ("__slots__", "__deletable__"):
639640
attr_rtype = mapper.type_to_rtype(node.node.type)
641+
if is_class_body_final(node.node, ir, cdef, attr_rtype):
642+
# No instance slot: the value lives in a module-level static
643+
# and on the type object. See ClassIR.class_final_attributes.
644+
ir.class_final_attributes[name] = attr_rtype
645+
continue
640646
if ir.is_trait and attr_rtype.error_overlap:
641647
# Traits don't have attribute definedness bitmaps, so use
642648
# property accessor methods to access attributes that need them.

mypyc/irbuild/util.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from mypy.types import FINAL_DECORATOR_NAMES
3636
from mypyc.errors import Errors
3737
from mypyc.ir.class_ir import ClassIR
38-
from mypyc.ir.rtypes import is_none_rprimitive, is_object_rprimitive, is_optional_type
38+
from mypyc.ir.rtypes import RType, is_none_rprimitive, is_object_rprimitive, is_optional_type
3939

4040
MYPYC_ATTRS: Final[frozenset[MypycAttr]] = frozenset(
4141
["native_class", "allow_interpreted_subclasses", "serializable", "free_list_len", "acyclic"]
@@ -107,6 +107,42 @@ def dataclass_type(cdef: ClassDef) -> str | None:
107107
return None
108108

109109

110+
def is_class_body_final(var: Var, ir: ClassIR, cdef: ClassDef, attr_rtype: RType) -> bool:
111+
"""Is a member a final attribute initialized in class body ("X: Final = <value>")?
112+
113+
The value is the same for every instance, so we don't give these struct
114+
slots. Instead, we let the general final-name machinery own it: the value
115+
is stored once in a module-level static and set on the type object (see
116+
ExtClassBuilder.add_attr), and reads are constant folded or loaded from
117+
the static.
118+
119+
See ClassIR.class_final_attributes.
120+
"""
121+
if not var.is_final or not var.has_explicit_value:
122+
return False
123+
if var.final_set_in_init:
124+
# "self.x: Final = ..." in __init__ varies per instance.
125+
return False
126+
if not ir.is_ext_class:
127+
# Non-extension classes keep class-level values in the class dict anyway.
128+
return False
129+
if ir.builtin_base:
130+
# These have no attribute slots at all (see prepare_methods_and_attributes),
131+
# so reads go through the class dict and honour shadowing by an interpreted
132+
# subclass, which folding would break.
133+
return False
134+
if dataclass_type(cdef) is not None:
135+
# Dataclass attribute definitions are special.
136+
return False
137+
if attr_rtype.error_overlap:
138+
# A static of type like this can't distinguish unset from a value equal to
139+
# the error sentinel.
140+
#
141+
# TODO: Support thesse by having a separate initialized flag
142+
return False
143+
return True
144+
145+
110146
def _defaults_skip(stmt: AssignmentStmt, cls_type: str | None) -> bool:
111147
"""Whether a class-level default assignment is skipped when emitting
112148
__mypyc_defaults_setup, based on class type.

mypyc/test-data/alwaysdefined.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ BinaryOps: [a, b, c, d, e, f]
318318
LocalsAndGlobals: [a, g]
319319
Booleans: [a, b, c, d, e]
320320
ModuleFinal: [a, b]
321-
ClassFinal: [F, a]
321+
ClassFinal: [a]
322322
Literals: [a, b, c]
323323
ListComprehension: [a]
324324
Helper: [x]

mypyc/test-data/irbuild-basic.test

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3027,12 +3027,6 @@ def f(a: bool) -> int:
30273027
else:
30283028
return C.y
30293029
[out]
3030-
def C.__mypyc_defaults_setup(__mypyc_self__):
3031-
__mypyc_self__ :: __main__.C
3032-
L0:
3033-
__mypyc_self__.x = 2
3034-
__mypyc_self__.y = 4
3035-
return 1
30363030
def f(a):
30373031
a :: bool
30383032
L0:

mypyc/test-data/irbuild-constant-fold.test

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,6 @@ class C:
229229
def f() -> None:
230230
a = C.X + 1
231231
[out]
232-
def C.__mypyc_defaults_setup(__mypyc_self__):
233-
__mypyc_self__ :: __main__.C
234-
L0:
235-
__mypyc_self__.X = 10
236-
return 1
237232
def f():
238233
a :: int
239234
L0:

mypyc/test-data/irbuild-final.test

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,127 @@ L0:
250250
r3 = 'a'
251251
r4 = PyUnicode_Concat(r2, r3)
252252
return r4
253+
254+
[case testClassBodyFinalConstantFoldedThroughInstance]
255+
from typing import Final
256+
257+
class C:
258+
X: Final = 23
259+
260+
def get(self) -> int:
261+
return self.X
262+
263+
class D(C):
264+
def get_inherited(self) -> int:
265+
return self.X
266+
267+
def f(c: C) -> int:
268+
return c.X
269+
[out]
270+
def C.get(self):
271+
self :: __main__.C
272+
L0:
273+
return 46
274+
def D.get_inherited(self):
275+
self :: __main__.D
276+
L0:
277+
return 46
278+
def f(c):
279+
c :: __main__.C
280+
L0:
281+
return 46
282+
283+
[case testClassBodyFinalReadFromStaticThroughInstance]
284+
from typing import Final
285+
286+
def make() -> int:
287+
return 3
288+
289+
class C:
290+
X: Final = make()
291+
292+
def get(self) -> int:
293+
return self.X
294+
[out]
295+
def make():
296+
L0:
297+
return 6
298+
def C.get(self):
299+
self :: __main__.C
300+
r0 :: int
301+
r1 :: bool
302+
L0:
303+
r0 = __main__.C.X :: static
304+
if is_error(r0) goto L1 else goto L2
305+
L1:
306+
r1 = raise NameError('value for final name "X" was not set')
307+
unreachable
308+
L2:
309+
return r0
310+
311+
[case testInstanceFinalStillUsesAttribute]
312+
from typing import Final
313+
314+
class C:
315+
def __init__(self, x: int) -> None:
316+
self.x: Final = x
317+
318+
def get(self) -> int:
319+
return self.x
320+
[out]
321+
def C.__init__(self, x):
322+
self :: __main__.C
323+
x :: int
324+
L0:
325+
self.x = x
326+
return 1
327+
def C.get(self):
328+
self :: __main__.C
329+
r0 :: int
330+
L0:
331+
r0 = self.x
332+
return r0
333+
334+
[case testClassBodyFinalNotOptimizedInBuiltinBaseSubclass]
335+
from typing import Final
336+
337+
class MyError(Exception):
338+
# Deliberately not turned into a class-level Final, since an interpreted subclass
339+
# can shadow it. See is_class_body_final() in mypyc/irbuild/util.py.
340+
CODE: Final = 5
341+
342+
def get(self) -> int:
343+
return self.CODE
344+
[out]
345+
def MyError.get(self):
346+
self :: __main__.MyError
347+
r0 :: str
348+
r1 :: object
349+
r2 :: int
350+
L0:
351+
r0 = 'CODE'
352+
r1 = CPyObject_GetAttr(self, r0)
353+
r2 = unbox(int, r1)
354+
return r2
355+
356+
[case testClassBodyFinalThroughUnion]
357+
from typing import Final, Union
358+
359+
class C:
360+
X: Final = 23
361+
362+
class E1(C):
363+
pass
364+
365+
class E2(C):
366+
pass
367+
368+
def same_decl(x: Union[E1, E2]) -> int:
369+
# Neither item is the declaring class itself, but both inherit the same
370+
# declaration, so this can still be folded.
371+
return x.X
372+
[out]
373+
def same_decl(x):
374+
x :: union[__main__.E1, __main__.E2]
375+
L0:
376+
return 46

mypyc/test-data/irbuild-int.test

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,6 @@ def f4() -> int:
141141
def f5() -> int:
142142
return C.B
143143
[out]
144-
def C.__mypyc_defaults_setup(__mypyc_self__):
145-
__mypyc_self__ :: __main__.C
146-
L0:
147-
__mypyc_self__.A = 2
148-
__mypyc_self__.B = -2
149-
return 1
150144
def f1():
151145
L0:
152146
return -2

0 commit comments

Comments
 (0)