Skip to content

Commit c888eb9

Browse files
committed
Promote immutable RInstances to value types
1 parent 3afff6e commit c888eb9

File tree

7 files changed

+101
-11
lines changed

7 files changed

+101
-11
lines changed

mypyc/codegen/emitclass.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Callable, Mapping, Tuple
66

77
from mypyc.codegen.emit import Emitter, HeaderDeclaration, ReturnHandler
8-
from mypyc.codegen.emitfunc import native_function_header
8+
from mypyc.codegen.emitfunc import native_function_header, struct_type
99
from mypyc.codegen.emitwrapper import (
1010
generate_bin_op_wrapper,
1111
generate_bool_wrapper,
@@ -21,7 +21,7 @@
2121
from mypyc.common import BITMAP_BITS, BITMAP_TYPE, NATIVE_PREFIX, PREFIX, REG_PREFIX
2222
from mypyc.ir.class_ir import ClassIR, VTableEntries
2323
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR
24-
from mypyc.ir.rtypes import RTuple, RType, object_rprimitive
24+
from mypyc.ir.rtypes import RTuple, RType, object_rprimitive, RInstance
2525
from mypyc.namegen import NameGenerator
2626
from mypyc.sametype import is_same_type
2727

@@ -394,9 +394,16 @@ def generate_object_struct(cl: ClassIR, emitter: Emitter) -> None:
394394
if attr not in bitmap_attrs:
395395
lines.append(f"{BITMAP_TYPE} {attr};")
396396
bitmap_attrs.append(attr)
397+
397398
for attr, rtype in base.attributes.items():
398399
if (attr, rtype) not in seen_attrs:
399-
lines.append(f"{emitter.ctype_spaced(rtype)}{emitter.attr(attr)};")
400+
if isinstance(rtype, RInstance) and rtype.class_ir.is_value_type:
401+
# value types use structs directly
402+
rtype_struct = struct_type(rtype.class_ir, emitter)
403+
lines.append(f"{emitter.ctype_spaced(rtype_struct)}{emitter.attr(attr)};")
404+
else:
405+
lines.append(f"{emitter.ctype_spaced(rtype)}{emitter.attr(attr)};")
406+
400407
seen_attrs.add((attr, rtype))
401408

402409
if isinstance(rtype, RTuple):

mypyc/codegen/emitfunc.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
)
7272
from mypyc.ir.pprint import generate_names_for_ir
7373
from mypyc.ir.rtypes import (
74+
c_pyssize_t_rprimitive,
7475
RArray,
7576
RInstance,
7677
RStruct,
@@ -81,9 +82,19 @@
8182
is_int_rprimitive,
8283
is_pointer_rprimitive,
8384
is_tagged,
85+
PyObject,
8486
)
8587

8688

89+
def struct_type(class_ir: ClassIR, emitter: Emitter) -> RStruct:
90+
"""Return the struct type for this instance type."""
91+
python_fields: list[tuple[str, RType]] = [("head", PyObject), ("vtable", c_pyssize_t_rprimitive)]
92+
class_fields = list(class_ir.attributes.items())
93+
attr_names = [emitter.attr(name) for name, _ in python_fields + class_fields]
94+
attr_types = [rtype for _, rtype in python_fields + class_fields]
95+
return RStruct(class_ir.struct_name(emitter.names), attr_names, attr_types)
96+
97+
8798
def native_function_type(fn: FuncIR, emitter: Emitter) -> str:
8899
args = ", ".join(emitter.ctype(arg.type) for arg in fn.args) or "void"
89100
ret = emitter.ctype(fn.ret_type)
@@ -383,8 +394,18 @@ def visit_get_attr(self, op: GetAttr) -> None:
383394
else:
384395
# Otherwise, use direct or offset struct access.
385396
attr_expr = self.get_attr_expr(obj, op, decl_cl)
386-
self.emitter.emit_line(f"{dest} = {attr_expr};")
387397
always_defined = cl.is_always_defined(op.attr)
398+
# This steals the reference to src, so we don't need to increment the arg
399+
if isinstance(attr_rtype, RInstance) and attr_rtype.class_ir.is_value_type:
400+
# special case for value types, it is unboxed in the struct
401+
struct_name = attr_rtype.class_ir.struct_name(self.names)
402+
temp = self.emitter.temp_name()
403+
self.emitter.emit_line(f"{struct_name} {temp} = {attr_expr};")
404+
self.emitter.emit_line(f"{dest} = (PyObject *)&{temp};")
405+
always_defined = True
406+
else:
407+
self.emitter.emit_line(f"{dest} = {attr_expr};")
408+
388409
merged_branch = None
389410
if not always_defined:
390411
self.emitter.emit_undefined_attr_check(
@@ -481,7 +502,12 @@ def visit_set_attr(self, op: SetAttr) -> None:
481502
self.emitter.emit_attr_bitmap_set(src, obj, attr_rtype, cl, op.attr)
482503

483504
# This steals the reference to src, so we don't need to increment the arg
484-
self.emitter.emit_line(f"{attr_expr} = {src};")
505+
if isinstance(attr_rtype, RInstance) and attr_rtype.class_ir.is_value_type:
506+
# special case for value types, it is unboxed in the struct
507+
struct_name = attr_rtype.class_ir.struct_name(self.names)
508+
self.emitter.emit_line(f"{attr_expr} = *({struct_name} *)({src});")
509+
else:
510+
self.emitter.emit_line(f"{attr_expr} = {src};")
485511
if op.error_kind == ERR_FALSE:
486512
self.emitter.emit_line(f"{dest} = 1;")
487513

@@ -522,8 +548,42 @@ def get_dest_assign(self, dest: Value) -> str:
522548
else:
523549
return ""
524550

551+
def try_emit_new_value_type_call(self, op: Call) -> bool:
552+
if not isinstance(op.type, RInstance):
553+
return False
554+
555+
cl = op.type.class_ir
556+
if op.fn.fullname != op.type.name or not cl.is_value_type:
557+
return False
558+
559+
assert not op.type.is_refcounted, "Value types must not be refcounted"
560+
dest = self.get_dest_assign(op)
561+
struct_name = cl.struct_name(self.names)
562+
temp_name = self.emitter.temp_name()
563+
temp_name2 = self.emitter.temp_name()
564+
self.emit_line(f"{struct_name} {temp_name};")
565+
init_fn = cl.get_method("__init__")
566+
if init_fn:
567+
args = [self.reg(arg) for arg in op.args]
568+
self.emitter.emit_line(
569+
"char {} = {}{}{}({});".format(
570+
temp_name2,
571+
self.emitter.get_group_prefix(init_fn.decl),
572+
NATIVE_PREFIX,
573+
init_fn.cname(self.emitter.names),
574+
", ".join([f"&{temp_name}"] + args),
575+
)
576+
)
577+
self.emit_line(f"{dest}{temp_name2} != 2 ? (PyObject *)&{temp_name} : NULL;")
578+
else:
579+
self.emit_line(f"{dest}(PyObject *)&{temp_name};")
580+
return True
581+
525582
def visit_call(self, op: Call) -> None:
526583
"""Call native function."""
584+
if self.try_emit_new_value_type_call(op):
585+
return
586+
527587
dest = self.get_dest_assign(op)
528588
args = ", ".join(self.reg(arg) for arg in op.args)
529589
lib = self.emitter.get_group_prefix(op.fn)

mypyc/ir/class_ir.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def __init__(
9494
is_abstract: bool = False,
9595
is_ext_class: bool = True,
9696
is_final_class: bool = False,
97+
is_immutable: bool = False,
9798
) -> None:
9899
self.name = name
99100
self.module_name = module_name
@@ -102,6 +103,7 @@ def __init__(
102103
self.is_abstract = is_abstract
103104
self.is_ext_class = is_ext_class
104105
self.is_final_class = is_final_class
106+
self.is_immutable = is_immutable
105107
# An augmented class has additional methods separate from what mypyc generates.
106108
# Right now the only one is dataclasses.
107109
self.is_augmented = False
@@ -202,14 +204,18 @@ def __repr__(self) -> str:
202204
"name={self.name}, module_name={self.module_name}, "
203205
"is_trait={self.is_trait}, is_generated={self.is_generated}, "
204206
"is_abstract={self.is_abstract}, is_ext_class={self.is_ext_class}, "
205-
"is_final_class={self.is_final_class}"
207+
"is_final_class={self.is_final_class}, is_immutable={self.is_immutable}"
206208
")".format(self=self)
207209
)
208210

209211
@property
210212
def fullname(self) -> str:
211213
return f"{self.module_name}.{self.name}"
212214

215+
@property
216+
def is_value_type(self) -> bool:
217+
return self.is_ext_class and self.is_immutable and not self.has_dict
218+
213219
def real_base(self) -> ClassIR | None:
214220
"""Return the actual concrete base class, if there is one."""
215221
if len(self.mro) > 1 and not self.mro[1].is_trait:
@@ -352,6 +358,7 @@ def serialize(self) -> JsonDict:
352358
"is_generated": self.is_generated,
353359
"is_augmented": self.is_augmented,
354360
"is_final_class": self.is_final_class,
361+
"is_immutable": self.is_immutable,
355362
"inherits_python": self.inherits_python,
356363
"has_dict": self.has_dict,
357364
"allow_interpreted_subclasses": self.allow_interpreted_subclasses,
@@ -408,6 +415,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
408415
ir.is_ext_class = data["is_ext_class"]
409416
ir.is_augmented = data["is_augmented"]
410417
ir.is_final_class = data["is_final_class"]
418+
ir.is_immutable = data["is_immutable"]
411419
ir.inherits_python = data["inherits_python"]
412420
ir.has_dict = data["has_dict"]
413421
ir.allow_interpreted_subclasses = data["allow_interpreted_subclasses"]

mypyc/ir/rtypes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> RStruct:
798798
class RInstance(RType):
799799
"""Instance of user-defined class (compiled to C extension class).
800800
801-
The runtime representation is 'PyObject *', and these are always
801+
The runtime representation is typically 'PyObject *', and these are
802802
boxed and thus reference-counted.
803803
804804
These support fast method calls and fast attribute access using
@@ -819,6 +819,7 @@ def __init__(self, class_ir: ClassIR) -> None:
819819
self.name = class_ir.fullname
820820
self.class_ir = class_ir
821821
self._ctype = "PyObject *"
822+
self.is_refcounted = not class_ir.is_value_type
822823

823824
def accept(self, visitor: RTypeVisitor[T]) -> T:
824825
return visitor.visit_rinstance(self)

mypyc/irbuild/classdef.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from __future__ import annotations
44

5-
import typing_extensions
65
from abc import abstractmethod
76
from typing import Callable, Final
87

8+
import typing_extensions
9+
910
from mypy.nodes import (
1011
TYPE_VAR_TUPLE_KIND,
1112
AssignmentStmt,
@@ -68,7 +69,6 @@
6869
get_func_def,
6970
is_constant,
7071
is_dataclass_decorator,
71-
is_final_class,
7272
)
7373
from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op
7474
from mypyc.primitives.generic_ops import (
@@ -301,7 +301,6 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None:
301301
self.builder.init_final_static(lvalue, value, self.cdef.name)
302302

303303
def finalize(self, ir: ClassIR) -> None:
304-
ir.is_final_class = is_final_class(self.cdef)
305304
attrs_with_defaults, default_assignments = find_attr_initializers(
306305
self.builder, self.cdef, self.skip_attr_default
307306
)

mypyc/irbuild/prepare.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
is_dataclass,
5959
is_extension_class,
6060
is_trait,
61+
is_immutable,
6162
)
6263
from mypyc.options import CompilerOptions
6364
from mypyc.sametype import is_same_type
@@ -81,7 +82,12 @@ def build_type_map(
8182
# references even if there are import cycles.
8283
for module, cdef in classes:
8384
class_ir = ClassIR(
84-
cdef.name, module.fullname, is_trait(cdef), is_abstract=cdef.info.is_abstract
85+
cdef.name,
86+
module.fullname,
87+
is_trait(cdef),
88+
is_abstract=cdef.info.is_abstract,
89+
is_immutable=is_immutable(cdef),
90+
is_final_class=cdef.info.is_final,
8591
)
8692
class_ir.is_ext_class = is_extension_class(cdef)
8793
if class_ir.is_ext_class:

mypyc/irbuild/util.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ def dataclass_type(cdef: ClassDef) -> str | None:
8383
return None
8484

8585

86+
def is_immutable(cdef: ClassDef) -> bool:
87+
"""Check if a class is immutable by checking if all its variables are marked as Final."""
88+
for v in cdef.info.names.values():
89+
if isinstance(v.node, Var) and v.node.name not in ("__slots__",):
90+
if not v.node.is_final and not v.node.is_property:
91+
return False
92+
return True
93+
94+
8695
def get_mypyc_attr_literal(e: Expression) -> Any:
8796
"""Convert an expression from a mypyc_attr decorator to a value.
8897

0 commit comments

Comments
 (0)