Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion mypyc/ir/class_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,11 @@ def __init__(
# Attributes that must survive generator/coroutine completion because
# escaped nested functions may still read them as closure variables.
self.attrs_to_keep_alive_on_completion: set[str] = set()
# Final attributes defined in the class (not inherited)
# Final attributes initialized in the __init__ method (not inherited)
self.final_attributes: set[str] = set()
# Final attributes defined in class body as "X: Final = <value>" (not inherited).
# They get no slot in the instance struct. The value lives in a module-level static.
self.class_final_attributes: dict[str, RType] = {}
# Deletable attributes
self.deletable: list[str] = []
# We populate method_types with the signatures of every method before
Expand Down Expand Up @@ -302,6 +305,25 @@ def is_final_attr(self, name: str) -> bool:
return False
return False

def class_final_attr_details(self, name: str) -> tuple[RType, ClassIR] | None:
"""Look up a (possibly inherited) class-body Final attribute.

Returns the attribute type and the class that defines it, or None if this
class has no such attribute. The defining class is what identifies the
static holding the value, so callers need it to build the static's name.

Deliberately kept out of attr_details()/has_attr(): these attributes have no
instance slot, so callers that want to read or write one must not treat them
as ordinary attributes.
"""
for ir in self.mro:
if name in ir.class_final_attributes:
return ir.class_final_attributes[name], ir
if name in ir.attributes or name in ir.property_types:
# Shadowed by a real attribute or property closer in the MRO.
return None
return None

def method_decl(self, name: str) -> FuncDecl:
for ir in self.mro:
if name in ir.method_decls:
Expand Down Expand Up @@ -435,6 +457,9 @@ def serialize(self) -> JsonDict:
"attributes": [(k, t.serialize()) for k, t in self.attributes.items()],
"attrs_to_keep_alive_on_completion": sorted(self.attrs_to_keep_alive_on_completion),
"final_attributes": sorted(self.final_attributes),
"class_final_attributes": [
(k, t.serialize()) for k, t in self.class_final_attributes.items()
],
# We try to serialize a name reference, but if the decl isn't in methods
# then we can't be sure that will work so we serialize the whole decl.
"method_decls": [
Expand Down Expand Up @@ -499,6 +524,9 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
ir.attributes = {k: deserialize_type(t, ctx) for k, t in data["attributes"]}
ir.attrs_to_keep_alive_on_completion = set(data["attrs_to_keep_alive_on_completion"])
ir.final_attributes = set(data["final_attributes"])
ir.class_final_attributes = {
k: deserialize_type(t, ctx) for k, t in data["class_final_attributes"]
}
ir.method_decls = {
k: ctx.functions[v].decl if isinstance(v, str) else FuncDecl.deserialize(v, ctx)
for k, v in data["method_decls"]
Expand Down
70 changes: 66 additions & 4 deletions mypyc/irbuild/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1226,15 +1226,18 @@ def is_synthetic_type(self, typ: TypeInfo) -> bool:
return typ.is_named_tuple or typ.is_newtype or typ.typeddict_type is not None

def get_final_ref(self, expr: MemberExpr) -> tuple[str, Var, bool] | None:
"""Check if `expr` is a final class or module attribute.
"""Check if `expr` is a final class, module or instance attribute.

Return False for instance attributes.

This needs to be done differently for class and module attributes to
This needs to be done differently for class, module and instance attributes to
correctly determine fully qualified name. Return a tuple that consists of
the qualified name, the corresponding Var node, and a flag indicating whether
the final name was defined in a compiled module. Return None if `expr` does not
refer to a final attribute.

Instance attributes only qualify if they are class-body Finals ("X: Final = ..."
in the class body), which have no instance slot -- see
ClassIR.class_final_attributes. Ordinary instance attributes, including
"self.x: Final = ..." set in __init__, return None.
"""
final_var = None
if isinstance(expr.expr, RefExpr) and isinstance(expr.expr.node, TypeInfo):
Expand All @@ -1254,10 +1257,69 @@ def get_final_ref(self, expr: MemberExpr) -> tuple[str, Var, bool] | None:
final_var = expr.node
fullname = expr.node.fullname
native = self.is_native_ref_expr(expr)
else:
# Possibly a class-body Final read through an instance ("self.X"). These
# have no instance slot, so read them exactly like "Cls.X".
var = self.get_class_final_var(expr)
if var is not None:
final_var = var
fullname = f"{var.info.fullname}.{var.name}"
native = self.is_native_module(var.info.module_name)
if final_var is not None:
return fullname, final_var, native
return None

def get_class_final_var(self, expr: MemberExpr) -> Var | None:
"""Return the Var for a class-body Final read through an instance expression.

Return None if `expr` isn't such a read; the caller then falls back to an
ordinary attribute read, which finds these names on the type object via
py_get_attr (correct, just slower).

A union-typed object qualifies only when every item resolves to the same
declaration, since otherwise the value differs per item.
"""
instance_type = get_proper_type(self.types.get(expr.expr))
if isinstance(instance_type, UnionType):
items = [get_proper_type(item) for item in instance_type.items]
else:
items = [instance_type]
found: Var | None = None
for item in items:
if not isinstance(item, Instance):
return None
var = self.class_final_var_of_instance(item, expr.name)
if var is None:
return None
if found is not None and var is not found:
return None
found = var
return found

def class_final_var_of_instance(self, instance: Instance, name: str) -> Var | None:
"""Look up a class-body Final attribute on a single instance type.

ClassIR is the authority on which attributes were compiled as class-body
Finals (mypyc.irbuild.util's is_class_body_final decides, and the answer
survives serialization), so we gate on it and only use the mypy symbol table
to find the defining class.
"""
class_ir = self.mapper.type_to_ir.get(instance.type)
if class_ir is None:
return None
details = class_ir.class_final_attr_details(name)
if details is None:
return None
_, defining_ir = details
sym = instance.type.get(name)
if sym is None or not isinstance(sym.node, Var) or not sym.node.is_final:
return None
# mypy's MRO and mypyc's can differ (traits), so only proceed when both agree
# on which class defines the attribute; that class names the static.
if sym.node.info.fullname != defining_ir.fullname:
return None
return sym.node

def emit_load_final(
self, final_var: Var, fullname: str, name: str, native: bool, typ: Type, line: int
) -> Value | None:
Expand Down
6 changes: 6 additions & 0 deletions mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
default_attr_name,
get_func_def,
get_mypyc_attrs,
is_class_body_final,
is_dataclass,
is_extension_class,
is_trait,
Expand Down Expand Up @@ -637,6 +638,11 @@ def prepare_methods_and_attributes(
assert node.node.type, "Class member %s missing type" % name
if not node.node.is_classvar and name not in ("__slots__", "__deletable__"):
attr_rtype = mapper.type_to_rtype(node.node.type)
if is_class_body_final(node.node, ir, cdef, attr_rtype):
# No instance slot: the value lives in a module-level static
# and on the type object. See ClassIR.class_final_attributes.
ir.class_final_attributes[name] = attr_rtype
continue
if ir.is_trait and attr_rtype.error_overlap:
# Traits don't have attribute definedness bitmaps, so use
# property accessor methods to access attributes that need them.
Expand Down
38 changes: 37 additions & 1 deletion mypyc/irbuild/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from mypy.types import FINAL_DECORATOR_NAMES
from mypyc.errors import Errors
from mypyc.ir.class_ir import ClassIR
from mypyc.ir.rtypes import is_none_rprimitive, is_object_rprimitive, is_optional_type
from mypyc.ir.rtypes import RType, is_none_rprimitive, is_object_rprimitive, is_optional_type

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


def is_class_body_final(var: Var, ir: ClassIR, cdef: ClassDef, attr_rtype: RType) -> bool:
"""Is a member a final attribute initialized in class body ("X: Final = <value>")?

The value is the same for every instance, so we don't give these struct
slots. Instead, we let the general final-name machinery own it: the value
is stored once in a module-level static and set on the type object (see
ExtClassBuilder.add_attr), and reads are constant folded or loaded from
the static.

See ClassIR.class_final_attributes.
"""
if not var.is_final or not var.has_explicit_value:
return False
if var.final_set_in_init:
# "self.x: Final = ..." in __init__ varies per instance.
return False
if not ir.is_ext_class:
# Non-extension classes keep class-level values in the class dict anyway.
return False
if ir.builtin_base:
# These have no attribute slots at all (see prepare_methods_and_attributes),
# so reads go through the class dict and honour shadowing by an interpreted
# subclass, which folding would break.
return False
if dataclass_type(cdef) is not None:
# Dataclass attribute definitions are special.
return False
if attr_rtype.error_overlap:
# A static of type like this can't distinguish unset from a value equal to
# the error sentinel.
#
# TODO: Support thesse by having a separate initialized flag
return False
return True


def _defaults_skip(stmt: AssignmentStmt, cls_type: str | None) -> bool:
"""Whether a class-level default assignment is skipped when emitting
__mypyc_defaults_setup, based on class type.
Expand Down
2 changes: 1 addition & 1 deletion mypyc/test-data/alwaysdefined.test
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ BinaryOps: [a, b, c, d, e, f]
LocalsAndGlobals: [a, g]
Booleans: [a, b, c, d, e]
ModuleFinal: [a, b]
ClassFinal: [F, a]
ClassFinal: [a]
Literals: [a, b, c]
ListComprehension: [a]
Helper: [x]
Expand Down
6 changes: 0 additions & 6 deletions mypyc/test-data/irbuild-basic.test
Original file line number Diff line number Diff line change
Expand Up @@ -3027,12 +3027,6 @@ def f(a: bool) -> int:
else:
return C.y
[out]
def C.__mypyc_defaults_setup(__mypyc_self__):
__mypyc_self__ :: __main__.C
L0:
__mypyc_self__.x = 2
__mypyc_self__.y = 4
return 1
def f(a):
a :: bool
L0:
Expand Down
5 changes: 0 additions & 5 deletions mypyc/test-data/irbuild-constant-fold.test
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,6 @@ class C:
def f() -> None:
a = C.X + 1
[out]
def C.__mypyc_defaults_setup(__mypyc_self__):
__mypyc_self__ :: __main__.C
L0:
__mypyc_self__.X = 10
return 1
def f():
a :: int
L0:
Expand Down
124 changes: 124 additions & 0 deletions mypyc/test-data/irbuild-final.test
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,127 @@ L0:
r3 = 'a'
r4 = PyUnicode_Concat(r2, r3)
return r4

[case testClassBodyFinalConstantFoldedThroughInstance]
from typing import Final

class C:
X: Final = 23

def get(self) -> int:
return self.X

class D(C):
def get_inherited(self) -> int:
return self.X

def f(c: C) -> int:
return c.X
[out]
def C.get(self):
self :: __main__.C
L0:
return 46
def D.get_inherited(self):
self :: __main__.D
L0:
return 46
def f(c):
c :: __main__.C
L0:
return 46

[case testClassBodyFinalReadFromStaticThroughInstance]
from typing import Final

def make() -> int:
return 3

class C:
X: Final = make()

def get(self) -> int:
return self.X
[out]
def make():
L0:
return 6
def C.get(self):
self :: __main__.C
r0 :: int
r1 :: bool
L0:
r0 = __main__.C.X :: static
if is_error(r0) goto L1 else goto L2
L1:
r1 = raise NameError('value for final name "X" was not set')
unreachable
L2:
return r0

[case testInstanceFinalStillUsesAttribute]
from typing import Final

class C:
def __init__(self, x: int) -> None:
self.x: Final = x

def get(self) -> int:
return self.x
[out]
def C.__init__(self, x):
self :: __main__.C
x :: int
L0:
self.x = x
return 1
def C.get(self):
self :: __main__.C
r0 :: int
L0:
r0 = self.x
return r0

[case testClassBodyFinalNotOptimizedInBuiltinBaseSubclass]
from typing import Final

class MyError(Exception):
# Deliberately not turned into a class-level Final, since an interpreted subclass
# can shadow it. See is_class_body_final() in mypyc/irbuild/util.py.
CODE: Final = 5

def get(self) -> int:
return self.CODE
[out]
def MyError.get(self):
self :: __main__.MyError
r0 :: str
r1 :: object
r2 :: int
L0:
r0 = 'CODE'
r1 = CPyObject_GetAttr(self, r0)
r2 = unbox(int, r1)
return r2

[case testClassBodyFinalThroughUnion]
from typing import Final, Union

class C:
X: Final = 23

class E1(C):
pass

class E2(C):
pass

def same_decl(x: Union[E1, E2]) -> int:
# Neither item is the declaring class itself, but both inherit the same
# declaration, so this can still be folded.
return x.X
[out]
def same_decl(x):
x :: union[__main__.E1, __main__.E2]
L0:
return 46
6 changes: 0 additions & 6 deletions mypyc/test-data/irbuild-int.test
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,6 @@ def f4() -> int:
def f5() -> int:
return C.B
[out]
def C.__mypyc_defaults_setup(__mypyc_self__):
__mypyc_self__ :: __main__.C
L0:
__mypyc_self__.A = 2
__mypyc_self__.B = -2
return 1
def f1():
L0:
return -2
Expand Down
Loading
Loading