From 45dbf03f08e0cb29eaf173e24dcf545771acdcd9 Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:24:49 -0500 Subject: [PATCH 01/15] Optimize calls to final classes --- mypyc/codegen/emitclass.py | 22 ++++++++++++-------- mypyc/codegen/emitfunc.py | 42 +++++++++++++++++++++----------------- mypyc/ir/class_ir.py | 5 +++-- mypyc/irbuild/classdef.py | 3 ++- mypyc/irbuild/util.py | 10 ++++++++- 5 files changed, 50 insertions(+), 32 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index ad95a1b0f323..1cbe10a6b825 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -380,7 +380,9 @@ def setter_name(cl: ClassIR, attribute: str, names: NameGenerator) -> str: def generate_object_struct(cl: ClassIR, emitter: Emitter) -> None: seen_attrs: set[tuple[str, RType]] = set() lines: list[str] = [] - lines += ["typedef struct {", "PyObject_HEAD", "CPyVTableItem *vtable;"] + lines += ["typedef struct {", "PyObject_HEAD"] + if not cl.is_final_class: + lines.append("CPyVTableItem *vtable;") if cl.has_method("__call__") and emitter.use_vectorcall(): lines.append("vectorcallfunc vectorcall;") bitmap_attrs = [] @@ -563,14 +565,16 @@ def generate_setup_for_class( emitter.emit_line("if (self == NULL)") emitter.emit_line(" return NULL;") - if shadow_vtable_name: - emitter.emit_line(f"if (type != {emitter.type_struct_name(cl)}) {{") - emitter.emit_line(f"self->vtable = {shadow_vtable_name};") - emitter.emit_line("} else {") - emitter.emit_line(f"self->vtable = {vtable_name};") - emitter.emit_line("}") - else: - emitter.emit_line(f"self->vtable = {vtable_name};") + if not cl.is_final_class: + if shadow_vtable_name: + emitter.emit_line(f"if (type != {emitter.type_struct_name(cl)}) {{") + emitter.emit_line(f"self->vtable = {shadow_vtable_name};") + emitter.emit_line("} else {") + emitter.emit_line(f"self->vtable = {vtable_name};") + emitter.emit_line("}") + else: + emitter.emit_line(f"self->vtable = {vtable_name};") + for i in range(0, len(cl.bitmap_attrs), BITMAP_BITS): field = emitter.bitmap_field(i) emitter.emit_line(f"self->{field} = 0;") diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index d945a28d8481..3f2541caa72f 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -361,21 +361,24 @@ def visit_get_attr(self, op: GetAttr) -> None: attr_rtype, decl_cl = cl.attr_details(op.attr) prefer_method = cl.is_trait and attr_rtype.error_overlap if cl.get_method(op.attr, prefer_method=prefer_method): - # Properties are essentially methods, so use vtable access for them. - version = "_TRAIT" if cl.is_trait else "" - self.emit_line( - "%s = CPY_GET_ATTR%s(%s, %s, %d, %s, %s); /* %s */" - % ( - dest, - version, - obj, - self.emitter.type_struct_name(rtype.class_ir), - rtype.getter_index(op.attr), - rtype.struct_name(self.names), - self.ctype(rtype.attr_type(op.attr)), - op.attr, + if cl.is_method_final(op.attr): + self.emit_method_call(f"{dest} = ", op.obj, op.attr, []) + else: + # Properties are essentially methods, so use vtable access for them. + version = "_TRAIT" if cl.is_trait else "" + self.emit_line( + "%s = CPY_GET_ATTR%s(%s, %s, %d, %s, %s); /* %s */" + % ( + dest, + version, + obj, + self.emitter.type_struct_name(rtype.class_ir), + rtype.getter_index(op.attr), + rtype.struct_name(self.names), + self.ctype(rtype.attr_type(op.attr)), + op.attr, + ) ) - ) else: # Otherwise, use direct or offset struct access. attr_expr = self.get_attr_expr(obj, op, decl_cl) @@ -529,11 +532,12 @@ def visit_call(self, op: Call) -> None: def visit_method_call(self, op: MethodCall) -> None: """Call native method.""" dest = self.get_dest_assign(op) - obj = self.reg(op.obj) + self.emit_method_call(dest, op.obj, op.method, op.args) - rtype = op.receiver_type + def emit_method_call(self, dest: str, op_obj: Value, name: str, op_args: list[Value]) -> None: + obj = self.reg(op_obj) + rtype = op_obj.type class_ir = rtype.class_ir - name = op.method method = rtype.class_ir.get_method(name) assert method is not None @@ -547,7 +551,7 @@ def visit_method_call(self, op: MethodCall) -> None: if method.decl.kind == FUNC_STATICMETHOD else [f"(PyObject *)Py_TYPE({obj})"] if method.decl.kind == FUNC_CLASSMETHOD else [obj] ) - args = ", ".join(obj_args + [self.reg(arg) for arg in op.args]) + args = ", ".join(obj_args + [self.reg(arg) for arg in op_args]) mtype = native_function_type(method, self.emitter) version = "_TRAIT" if rtype.class_ir.is_trait else "" if is_direct: @@ -567,7 +571,7 @@ def visit_method_call(self, op: MethodCall) -> None: rtype.struct_name(self.names), mtype, args, - op.method, + name, ) ) diff --git a/mypyc/ir/class_ir.py b/mypyc/ir/class_ir.py index 18f3cbcff987..38f58b2a309e 100644 --- a/mypyc/ir/class_ir.py +++ b/mypyc/ir/class_ir.py @@ -93,6 +93,7 @@ def __init__( is_generated: bool = False, is_abstract: bool = False, is_ext_class: bool = True, + is_final_class: bool = False, ) -> None: self.name = name self.module_name = module_name @@ -100,6 +101,7 @@ def __init__( self.is_generated = is_generated self.is_abstract = is_abstract self.is_ext_class = is_ext_class + self.is_final_class = is_final_class # An augmented class has additional methods separate from what mypyc generates. # Right now the only one is dataclasses. self.is_augmented = False @@ -248,8 +250,7 @@ def has_method(self, name: str) -> bool: def is_method_final(self, name: str) -> bool: subs = self.subclasses() if subs is None: - # TODO: Look at the final attribute! - return False + return self.is_final_class if self.has_method(name): method_decl = self.method_decl(name) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 7e0a842b1b41..7a4aee172f0f 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -63,7 +63,7 @@ handle_non_ext_method, load_type, ) -from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator +from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator, is_final_class from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op from mypyc.primitives.generic_ops import ( iter_op, @@ -294,6 +294,7 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None: self.builder.init_final_static(lvalue, value, self.cdef.name) def finalize(self, ir: ClassIR) -> None: + ir.is_final_class = is_final_class(self.cdef) attrs_with_defaults, default_assignments = find_attr_initializers( self.builder, self.cdef, self.skip_attr_default ) diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py index ed01a59d1214..f1c0b3a29500 100644 --- a/mypyc/irbuild/util.py +++ b/mypyc/irbuild/util.py @@ -31,6 +31,14 @@ DATACLASS_DECORATORS = {"dataclasses.dataclass", "attr.s", "attr.attrs"} +def is_final_decorator(d: Expression) -> bool: + return isinstance(d, RefExpr) and d.fullname == "typing.final" + + +def is_final_class(cdef: ClassDef) -> bool: + return any(is_final_decorator(d) for d in cdef.decorators) + + def is_trait_decorator(d: Expression) -> bool: return isinstance(d, RefExpr) and d.fullname == "mypy_extensions.trait" @@ -119,7 +127,7 @@ def get_mypyc_attrs(stmt: ClassDef | Decorator) -> dict[str, Any]: def is_extension_class(cdef: ClassDef) -> bool: if any( - not is_trait_decorator(d) and not is_dataclass_decorator(d) and not get_mypyc_attr_call(d) + not is_trait_decorator(d) and not is_dataclass_decorator(d) and not get_mypyc_attr_call(d) and not is_final_decorator(d) for d in cdef.decorators ): return False From 6f16840f035917f926147cfc55b887846e4d1d29 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:32:48 +0000 Subject: [PATCH 02/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/irbuild/classdef.py | 8 +++++++- mypyc/irbuild/util.py | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 7a4aee172f0f..499e07ae7d19 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -63,7 +63,13 @@ handle_non_ext_method, load_type, ) -from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator, is_final_class +from mypyc.irbuild.util import ( + dataclass_type, + get_func_def, + is_constant, + is_dataclass_decorator, + is_final_class, +) from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op from mypyc.primitives.generic_ops import ( iter_op, diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py index f1c0b3a29500..c38c377a3d44 100644 --- a/mypyc/irbuild/util.py +++ b/mypyc/irbuild/util.py @@ -127,7 +127,10 @@ def get_mypyc_attrs(stmt: ClassDef | Decorator) -> dict[str, Any]: def is_extension_class(cdef: ClassDef) -> bool: if any( - not is_trait_decorator(d) and not is_dataclass_decorator(d) and not get_mypyc_attr_call(d) and not is_final_decorator(d) + not is_trait_decorator(d) + and not is_dataclass_decorator(d) + and not get_mypyc_attr_call(d) + and not is_final_decorator(d) for d in cdef.decorators ): return False From 38a995f6adab6ebaf8c8f446928b708c6dcb54df Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 6 Oct 2024 21:41:23 -0500 Subject: [PATCH 03/15] Assert rtype --- mypyc/codegen/emitfunc.py | 2 ++ mypyc/ir/rtypes.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 3f2541caa72f..4599c5e609c4 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -73,6 +73,7 @@ from mypyc.ir.rtypes import ( RArray, RStruct, + RInstance, RTuple, RType, is_int32_rprimitive, @@ -537,6 +538,7 @@ def visit_method_call(self, op: MethodCall) -> None: def emit_method_call(self, dest: str, op_obj: Value, name: str, op_args: list[Value]) -> None: obj = self.reg(op_obj) rtype = op_obj.type + assert isinstance(rtype, RInstance) class_ir = rtype.class_ir method = rtype.class_ir.get_method(name) assert method is not None diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index fecfaee5ef77..53e3cee74e56 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -64,7 +64,7 @@ class RType: @abstractmethod def accept(self, visitor: RTypeVisitor[T]) -> T: - raise NotImplementedError + raise NotImplementedError() def short_name(self) -> str: return short_name(self.name) From 46ce3e687c7029376fa856482d82dfed636719c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 02:41:49 +0000 Subject: [PATCH 04/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/codegen/emitfunc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 4599c5e609c4..2d17c5c1064f 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -72,8 +72,8 @@ from mypyc.ir.pprint import generate_names_for_ir from mypyc.ir.rtypes import ( RArray, - RStruct, RInstance, + RStruct, RTuple, RType, is_int32_rprimitive, From d44bd16bb23b2eb3ddf3021b0d886f0109b59191 Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:14:23 -0500 Subject: [PATCH 05/15] Add test case for @final attribute --- mypyc/test-data/run-classes.test | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 59617714f7e7..b685f9334321 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2503,3 +2503,29 @@ class C: def test_final_attribute() -> None: assert C.A == -1 assert C.a == [-1] + +[case testClassWithFinalAttribute] +from typing import final + +@final +class C: + def a(self) -> int: + return 1 + +def test_class_final_attribute() -> None: + assert C().a() == 1 + +[case testClassWithFinalAttributeInherited] +from typing import final + +class B: + def a(self) -> int: + return 2 + +@final +class C(B): + def a(self) -> int: + return 1 + +def test_class_final_attribute_inherited() -> None: + assert C().a() == 1 From b4b37efb5b6b24b27387ccae5ff5db024f5a835d Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:23:01 -0500 Subject: [PATCH 06/15] Rename new test cases --- mypyc/test-data/run-classes.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index b685f9334321..4698693b381f 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2504,7 +2504,7 @@ def test_final_attribute() -> None: assert C.A == -1 assert C.a == [-1] -[case testClassWithFinalAttribute] +[case testClassWithFinalTypingAttribute] from typing import final @final @@ -2515,7 +2515,7 @@ class C: def test_class_final_attribute() -> None: assert C().a() == 1 -[case testClassWithFinalAttributeInherited] +[case testClassWithFinalTypingAttributeInherited] from typing import final class B: From 92cace581adca75a49d7e6a0f92b181262f9d62f Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 6 Oct 2024 23:03:17 -0500 Subject: [PATCH 07/15] ClassIR serialization --- mypyc/ir/class_ir.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypyc/ir/class_ir.py b/mypyc/ir/class_ir.py index 38f58b2a309e..a835e7b0e750 100644 --- a/mypyc/ir/class_ir.py +++ b/mypyc/ir/class_ir.py @@ -201,7 +201,8 @@ def __repr__(self) -> str: "ClassIR(" "name={self.name}, module_name={self.module_name}, " "is_trait={self.is_trait}, is_generated={self.is_generated}, " - "is_abstract={self.is_abstract}, is_ext_class={self.is_ext_class}" + "is_abstract={self.is_abstract}, is_ext_class={self.is_ext_class}, " + "is_final_class={self.is_final_class}" ")".format(self=self) ) @@ -350,6 +351,7 @@ def serialize(self) -> JsonDict: "is_abstract": self.is_abstract, "is_generated": self.is_generated, "is_augmented": self.is_augmented, + "is_final_class": self.is_final_class, "inherits_python": self.inherits_python, "has_dict": self.has_dict, "allow_interpreted_subclasses": self.allow_interpreted_subclasses, From 1d6cacd622be2a3fd9debe407a80505c31eca226 Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 6 Oct 2024 23:14:50 -0500 Subject: [PATCH 08/15] ClassIR deserialization --- mypyc/ir/class_ir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypyc/ir/class_ir.py b/mypyc/ir/class_ir.py index a835e7b0e750..94bf714b28d4 100644 --- a/mypyc/ir/class_ir.py +++ b/mypyc/ir/class_ir.py @@ -407,6 +407,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR: ir.is_abstract = data["is_abstract"] ir.is_ext_class = data["is_ext_class"] ir.is_augmented = data["is_augmented"] + ir.is_final_class = data["is_final_class"] ir.inherits_python = data["inherits_python"] ir.has_dict = data["has_dict"] ir.allow_interpreted_subclasses = data["allow_interpreted_subclasses"] From 88a2ccf182fe45cd0dcf3d21dd151b7678e77d29 Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:42:17 -0500 Subject: [PATCH 09/15] Restore the placement vtable to preserve the PyObject layout --- mypyc/codegen/emitclass.py | 4 +--- mypyc/irbuild/ll_builder.py | 5 +++-- mypyc/test-data/run-classes.test | 6 ++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 1cbe10a6b825..21b3e0383bc9 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -380,9 +380,7 @@ def setter_name(cl: ClassIR, attribute: str, names: NameGenerator) -> str: def generate_object_struct(cl: ClassIR, emitter: Emitter) -> None: seen_attrs: set[tuple[str, RType]] = set() lines: list[str] = [] - lines += ["typedef struct {", "PyObject_HEAD"] - if not cl.is_final_class: - lines.append("CPyVTableItem *vtable;") + lines += ["typedef struct {", "PyObject_HEAD", "CPyVTableItem *vtable;"] if cl.has_method("__call__") and emitter.use_vectorcall(): lines.append("vectorcallfunc vectorcall;") bitmap_attrs = [] diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 0c9310e6a5ca..b6d517c0369a 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1889,8 +1889,9 @@ def primitive_op( # Does this primitive map into calling a Python C API # or an internal mypyc C API function? if desc.c_function_name: - # TODO: Generate PrimitiOps here and transform them into CallC + # TODO: Generate PrimitiveOps here and transform them into CallC # ops only later in the lowering pass + print(f"primitive_op: {desc.name} {desc.c_function_name} {desc.is_pure} -> {result_type}") c_desc = CFunctionDescription( desc.name, desc.arg_types, @@ -1908,7 +1909,7 @@ def primitive_op( ) return self.call_c(c_desc, args, line, result_type) - # This primitve gets transformed in a lowering pass to + # This primitive gets transformed in a lowering pass to # lower-level IR ops using a custom transform function. coerced = [] diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 4698693b381f..47a3be6cd8a4 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2527,5 +2527,11 @@ class C(B): def a(self) -> int: return 1 +def fn(cl: B) -> int: + return cl.a() + def test_class_final_attribute_inherited() -> None: assert C().a() == 1 + assert fn(C()) == 1 + assert B().a() == 2 + assert fn(B()) == 2 From 59f3f13e716ae3c3d0c0bd433fa9df76e4b6534a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:44:37 +0000 Subject: [PATCH 10/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/irbuild/ll_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index b6d517c0369a..774551dc2e5c 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1891,7 +1891,9 @@ def primitive_op( if desc.c_function_name: # TODO: Generate PrimitiveOps here and transform them into CallC # ops only later in the lowering pass - print(f"primitive_op: {desc.name} {desc.c_function_name} {desc.is_pure} -> {result_type}") + print( + f"primitive_op: {desc.name} {desc.c_function_name} {desc.is_pure} -> {result_type}" + ) c_desc = CFunctionDescription( desc.name, desc.arg_types, From 232db96f90795d4f8710a5316353eb3a19ebf4ee Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:44:43 -0500 Subject: [PATCH 11/15] Restore the placement vtable to preserve the PyObject layout --- mypyc/irbuild/ll_builder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 774551dc2e5c..c98136ce06d2 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1891,9 +1891,6 @@ def primitive_op( if desc.c_function_name: # TODO: Generate PrimitiveOps here and transform them into CallC # ops only later in the lowering pass - print( - f"primitive_op: {desc.name} {desc.c_function_name} {desc.is_pure} -> {result_type}" - ) c_desc = CFunctionDescription( desc.name, desc.arg_types, From f36c774014debb5e1540ed90b3befb9bad04535a Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:23:02 -0500 Subject: [PATCH 12/15] revert to initialize vtable always --- mypyc/codegen/emitclass.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 21b3e0383bc9..3ab6932546a6 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -563,15 +563,14 @@ def generate_setup_for_class( emitter.emit_line("if (self == NULL)") emitter.emit_line(" return NULL;") - if not cl.is_final_class: - if shadow_vtable_name: - emitter.emit_line(f"if (type != {emitter.type_struct_name(cl)}) {{") - emitter.emit_line(f"self->vtable = {shadow_vtable_name};") - emitter.emit_line("} else {") - emitter.emit_line(f"self->vtable = {vtable_name};") - emitter.emit_line("}") - else: - emitter.emit_line(f"self->vtable = {vtable_name};") + if shadow_vtable_name: + emitter.emit_line(f"if (type != {emitter.type_struct_name(cl)}) {{") + emitter.emit_line(f"self->vtable = {shadow_vtable_name};") + emitter.emit_line("} else {") + emitter.emit_line(f"self->vtable = {vtable_name};") + emitter.emit_line("}") + else: + emitter.emit_line(f"self->vtable = {vtable_name};") for i in range(0, len(cl.bitmap_attrs), BITMAP_BITS): field = emitter.bitmap_field(i) From a4872ac81366dd3cb14c10f4dad0af9ba5a361d3 Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:11:04 -0500 Subject: [PATCH 13/15] Address PR comments --- mypyc/codegen/emitfunc.py | 2 +- mypyc/irbuild/classdef.py | 2 -- mypyc/irbuild/prepare.py | 6 +++++- mypyc/irbuild/util.py | 8 +++----- mypyc/test-data/run-classes.test | 22 ++++++++++++++++++++++ 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 2d17c5c1064f..6088fb06dd32 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -362,10 +362,10 @@ def visit_get_attr(self, op: GetAttr) -> None: attr_rtype, decl_cl = cl.attr_details(op.attr) prefer_method = cl.is_trait and attr_rtype.error_overlap if cl.get_method(op.attr, prefer_method=prefer_method): + # Properties are essentially methods, so use vtable access for them. if cl.is_method_final(op.attr): self.emit_method_call(f"{dest} = ", op.obj, op.attr, []) else: - # Properties are essentially methods, so use vtable access for them. version = "_TRAIT" if cl.is_trait else "" self.emit_line( "%s = CPY_GET_ATTR%s(%s, %s, %d, %s, %s); /* %s */" diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 499e07ae7d19..aafbb3aff18d 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -68,7 +68,6 @@ get_func_def, is_constant, is_dataclass_decorator, - is_final_class, ) from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op from mypyc.primitives.generic_ops import ( @@ -300,7 +299,6 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None: self.builder.init_final_static(lvalue, value, self.cdef.name) def finalize(self, ir: ClassIR) -> None: - ir.is_final_class = is_final_class(self.cdef) attrs_with_defaults, default_assignments = find_attr_initializers( self.builder, self.cdef, self.skip_attr_default ) diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index 29e06439abdd..40a40b79df49 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -81,7 +81,11 @@ def build_type_map( # references even if there are import cycles. for module, cdef in classes: class_ir = ClassIR( - cdef.name, module.fullname, is_trait(cdef), is_abstract=cdef.info.is_abstract + cdef.name, + module.fullname, + is_trait(cdef), + is_abstract=cdef.info.is_abstract, + is_final_class=cdef.info.is_final, ) class_ir.is_ext_class = is_extension_class(cdef) if class_ir.is_ext_class: diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py index c38c377a3d44..e27e509ad7fa 100644 --- a/mypyc/irbuild/util.py +++ b/mypyc/irbuild/util.py @@ -27,16 +27,14 @@ UnaryExpr, Var, ) +from mypy.semanal import refers_to_fullname +from mypy.types import FINAL_DECORATOR_NAMES DATACLASS_DECORATORS = {"dataclasses.dataclass", "attr.s", "attr.attrs"} def is_final_decorator(d: Expression) -> bool: - return isinstance(d, RefExpr) and d.fullname == "typing.final" - - -def is_final_class(cdef: ClassDef) -> bool: - return any(is_final_decorator(d) for d in cdef.decorators) + return refers_to_fullname(d, FINAL_DECORATOR_NAMES) def is_trait_decorator(d: Expression) -> bool: diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 47a3be6cd8a4..6531789241ce 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2522,11 +2522,28 @@ class B: def a(self) -> int: return 2 + @property + def b(self) -> int: + return self.a() + 2 + + @property + def c(self) -> int: + return 3 + +def test_class_final_attribute_basic() -> None: + assert B().a() == 2 + assert B().b == 4 + assert B().c == 3 + @final class C(B): def a(self) -> int: return 1 + @property + def b(self) -> int: + return self.a() + 1 + def fn(cl: B) -> int: return cl.a() @@ -2535,3 +2552,8 @@ def test_class_final_attribute_inherited() -> None: assert fn(C()) == 1 assert B().a() == 2 assert fn(B()) == 2 + + assert B().b == 4 + assert C().b == 2 + assert B().c == 3 + assert C().c == 3 From 220cfde32044ee629f56fe926da9df6f8d0e9349 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 00:11:35 +0000 Subject: [PATCH 14/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/irbuild/classdef.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index aafbb3aff18d..7e0a842b1b41 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -63,12 +63,7 @@ handle_non_ext_method, load_type, ) -from mypyc.irbuild.util import ( - dataclass_type, - get_func_def, - is_constant, - is_dataclass_decorator, -) +from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op from mypyc.primitives.generic_ops import ( iter_op, From b664fbae893fdf78ac428179a71d7cfecc44e8d8 Mon Sep 17 00:00:00 2001 From: Jairo Velasco <1904410+jairov4@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:22:13 -0500 Subject: [PATCH 15/15] Add new test case --- mypyc/test-data/run-classes.test | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 6531789241ce..25744e356c3e 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2504,7 +2504,7 @@ def test_final_attribute() -> None: assert C.A == -1 assert C.a == [-1] -[case testClassWithFinalTypingAttribute] +[case testClassWithFinalDecorator] from typing import final @final @@ -2515,7 +2515,28 @@ class C: def test_class_final_attribute() -> None: assert C().a() == 1 -[case testClassWithFinalTypingAttributeInherited] + +[case testClassWithFinalDecoratorCtor] +from typing import final + +@final +class C: + def __init__(self) -> None: + self.a = 1 + + def b(self) -> int: + return 2 + + @property + def c(self) -> int: + return 3 + +def test_class_final_attribute() -> None: + assert C().a == 1 + assert C().b() == 2 + assert C().c == 3 + +[case testClassWithFinalDecoratorInheritedWithProperties] from typing import final class B: