Skip to content

Commit 757e0d4

Browse files
authored
[mypyc] Support inheriting native int attributes from traits (#14490)
Regular trait attributes are accessed via looking up the field offset from a vtable. This doesn't work with native ints, since they may also require access to a defined attributes bitmap, and also looking that up from a vtable would be too complicated. Work around this may always accessing trait native int attributes using accessor methods. These can raise an exception if an attribute is undefined. Add empty accessor methods in traits for each attribute with overlapping error values. Also synthesize real accessors in each concrete subclass. When accessing the attribute using a concrete subclass, still prefer direct field and bitmap access. Only attribute access through a trait type requires accessors to be used. Work on mypyc/mypyc#837.
1 parent 8b30913 commit 757e0d4

File tree

8 files changed

+119
-13
lines changed

8 files changed

+119
-13
lines changed

mypyc/analysis/attrdefined.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,9 @@ def update_always_defined_attrs_using_subclasses(cl: ClassIR, seen: set[ClassIR]
415415

416416

417417
def detect_undefined_bitmap(cl: ClassIR, seen: Set[ClassIR]) -> None:
418+
if cl.is_trait:
419+
return
420+
418421
if cl in seen:
419422
return
420423
seen.add(cl)
@@ -426,3 +429,9 @@ def detect_undefined_bitmap(cl: ClassIR, seen: Set[ClassIR]) -> None:
426429
for n, t in cl.attributes.items():
427430
if t.error_overlap and not cl.is_always_defined(n):
428431
cl.bitmap_attrs.append(n)
432+
433+
for base in cl.mro[1:]:
434+
if base.is_trait:
435+
for n, t in base.attributes.items():
436+
if t.error_overlap and not cl.is_always_defined(n) and n not in cl.bitmap_attrs:
437+
cl.bitmap_attrs.append(n)

mypyc/codegen/emitfunc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,8 @@ def visit_get_attr(self, op: GetAttr) -> None:
330330
rtype = op.class_type
331331
cl = rtype.class_ir
332332
attr_rtype, decl_cl = cl.attr_details(op.attr)
333-
if cl.get_method(op.attr):
333+
prefer_method = cl.is_trait and attr_rtype.error_overlap
334+
if cl.get_method(op.attr, prefer_method=prefer_method):
334335
# Properties are essentially methods, so use vtable access for them.
335336
version = "_TRAIT" if cl.is_trait else ""
336337
self.emit_line(

mypyc/ir/class_ir.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,13 @@ def get_method_and_class(
294294

295295
return None
296296

297-
def get_method(self, name: str) -> FuncIR | None:
298-
res = self.get_method_and_class(name)
297+
def get_method(self, name: str, *, prefer_method: bool = False) -> FuncIR | None:
298+
res = self.get_method_and_class(name, prefer_method=prefer_method)
299299
return res[0] if res else None
300300

301+
def has_method_decl(self, name: str) -> bool:
302+
return any(name in ir.method_decls for ir in self.mro)
303+
301304
def subclasses(self) -> set[ClassIR] | None:
302305
"""Return all subclasses of this class, both direct and indirect.
303306

mypyc/irbuild/classdef.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,16 @@ def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None:
161161
# Generate implicit property setters/getters
162162
for name, decl in ir.method_decls.items():
163163
if decl.implicit and decl.is_prop_getter:
164-
getter_ir = gen_property_getter_ir(builder, decl, cdef)
164+
getter_ir = gen_property_getter_ir(builder, decl, cdef, ir.is_trait)
165165
builder.functions.append(getter_ir)
166166
ir.methods[getter_ir.decl.name] = getter_ir
167167

168168
setter_ir = None
169169
setter_name = PROPSET_PREFIX + name
170170
if setter_name in ir.method_decls:
171-
setter_ir = gen_property_setter_ir(builder, ir.method_decls[setter_name], cdef)
171+
setter_ir = gen_property_setter_ir(
172+
builder, ir.method_decls[setter_name], cdef, ir.is_trait
173+
)
172174
builder.functions.append(setter_ir)
173175
ir.methods[setter_name] = setter_ir
174176

mypyc/irbuild/function.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,21 +1028,28 @@ def get_native_impl_ids(builder: IRBuilder, singledispatch_func: FuncDef) -> dic
10281028
return {impl: i for i, (typ, impl) in enumerate(impls) if not is_decorated(builder, impl)}
10291029

10301030

1031-
def gen_property_getter_ir(builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef) -> FuncIR:
1031+
def gen_property_getter_ir(
1032+
builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef, is_trait: bool
1033+
) -> FuncIR:
10321034
"""Generate an implicit trivial property getter for an attribute.
10331035
10341036
These are used if an attribute can also be accessed as a property.
10351037
"""
10361038
name = func_decl.name
10371039
builder.enter(name)
10381040
self_reg = builder.add_argument("self", func_decl.sig.args[0].type)
1039-
value = builder.builder.get_attr(self_reg, name, func_decl.sig.ret_type, -1)
1040-
builder.add(Return(value))
1041+
if not is_trait:
1042+
value = builder.builder.get_attr(self_reg, name, func_decl.sig.ret_type, -1)
1043+
builder.add(Return(value))
1044+
else:
1045+
builder.add(Unreachable())
10411046
args, _, blocks, ret_type, fn_info = builder.leave()
10421047
return FuncIR(func_decl, args, blocks)
10431048

10441049

1045-
def gen_property_setter_ir(builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef) -> FuncIR:
1050+
def gen_property_setter_ir(
1051+
builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef, is_trait: bool
1052+
) -> FuncIR:
10461053
"""Generate an implicit trivial property setter for an attribute.
10471054
10481055
These are used if an attribute can also be accessed as a property.
@@ -1053,7 +1060,8 @@ def gen_property_setter_ir(builder: IRBuilder, func_decl: FuncDecl, cdef: ClassD
10531060
value_reg = builder.add_argument("value", func_decl.sig.args[1].type)
10541061
assert name.startswith(PROPSET_PREFIX)
10551062
attr_name = name[len(PROPSET_PREFIX) :]
1056-
builder.add(SetAttr(self_reg, attr_name, value_reg, -1))
1063+
if not is_trait:
1064+
builder.add(SetAttr(self_reg, attr_name, value_reg, -1))
10571065
builder.add(Return(builder.none()))
10581066
args, _, blocks, ret_type, fn_info = builder.leave()
10591067
return FuncIR(func_decl, args, blocks)

mypyc/irbuild/prepare.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,15 @@ def prepare_methods_and_attributes(
305305
if isinstance(node.node, Var):
306306
assert node.node.type, "Class member %s missing type" % name
307307
if not node.node.is_classvar and name not in ("__slots__", "__deletable__"):
308-
ir.attributes[name] = mapper.type_to_rtype(node.node.type)
308+
attr_rtype = mapper.type_to_rtype(node.node.type)
309+
if ir.is_trait and attr_rtype.error_overlap:
310+
# Traits don't have attribute definedness bitmaps, so use
311+
# property accessor methods to access attributes that need them.
312+
# We will generate accessor implementations that use the class bitmap
313+
# for any concrete subclasses.
314+
add_getter_declaration(ir, name, attr_rtype, module_name)
315+
add_setter_declaration(ir, name, attr_rtype, module_name)
316+
ir.attributes[name] = attr_rtype
309317
elif isinstance(node.node, (FuncDef, Decorator)):
310318
prepare_method_def(ir, module_name, cdef, mapper, node.node)
311319
elif isinstance(node.node, OverloadedFuncDef):
@@ -329,11 +337,20 @@ def prepare_methods_and_attributes(
329337
def prepare_implicit_property_accessors(
330338
info: TypeInfo, ir: ClassIR, module_name: str, mapper: Mapper
331339
) -> None:
340+
concrete_attributes = set()
332341
for base in ir.base_mro:
333342
for name, attr_rtype in base.attributes.items():
343+
concrete_attributes.add(name)
334344
add_property_methods_for_attribute_if_needed(
335345
info, ir, name, attr_rtype, module_name, mapper
336346
)
347+
for base in ir.mro[1:]:
348+
if base.is_trait:
349+
for name, attr_rtype in base.attributes.items():
350+
if name not in concrete_attributes:
351+
add_property_methods_for_attribute_if_needed(
352+
info, ir, name, attr_rtype, module_name, mapper
353+
)
337354

338355

339356
def add_property_methods_for_attribute_if_needed(
@@ -350,6 +367,7 @@ def add_property_methods_for_attribute_if_needed(
350367
"""
351368
for base in info.mro[1:]:
352369
if base in mapper.type_to_ir:
370+
base_ir = mapper.type_to_ir[base]
353371
n = base.names.get(attr_name)
354372
if n is None:
355373
continue
@@ -361,6 +379,9 @@ def add_property_methods_for_attribute_if_needed(
361379
# Defined as a read-write property in base class/trait
362380
add_getter_declaration(ir, attr_name, attr_rtype, module_name)
363381
add_setter_declaration(ir, attr_name, attr_rtype, module_name)
382+
elif base_ir.is_trait and attr_rtype.error_overlap:
383+
add_getter_declaration(ir, attr_name, attr_rtype, module_name)
384+
add_setter_declaration(ir, attr_name, attr_rtype, module_name)
364385

365386

366387
def add_getter_declaration(

mypyc/irbuild/vtable.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def compute_vtable(cls: ClassIR) -> None:
4040
for t in [cls] + cls.traits:
4141
for fn in itertools.chain(t.methods.values()):
4242
# TODO: don't generate a new entry when we overload without changing the type
43-
if fn == cls.get_method(fn.name):
43+
if fn == cls.get_method(fn.name, prefer_method=True):
4444
cls.vtable[fn.name] = len(entries)
4545
# If the class contains a glue method referring to itself, that is a
4646
# shadow glue method to support interpreted subclasses.
@@ -60,7 +60,7 @@ def specialize_parent_vtable(cls: ClassIR, parent: ClassIR) -> VTableEntries:
6060
for entry in parent.vtable_entries:
6161
# Find the original method corresponding to this vtable entry.
6262
# (This may not be the method in the entry, if it was overridden.)
63-
orig_parent_method = entry.cls.get_method(entry.name)
63+
orig_parent_method = entry.cls.get_method(entry.name, prefer_method=True)
6464
assert orig_parent_method
6565
method_cls = cls.get_method_and_class(entry.name, prefer_method=True)
6666
if method_cls:

mypyc/test-data/run-i64.test

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,3 +1433,65 @@ def test_read_only_property_in_trait_implemented_as_property() -> None:
14331433
assert t.x == 6
14341434
with assertRaises(TypeError):
14351435
t.y
1436+
1437+
@trait
1438+
class T2:
1439+
x: i64
1440+
y: i64
1441+
1442+
class C2(T2):
1443+
pass
1444+
1445+
def test_inherit_trait_attribute() -> None:
1446+
c = C2()
1447+
c.x = 5
1448+
assert c.x == 5
1449+
c.x = MAGIC
1450+
assert c.x == MAGIC
1451+
with assertRaises(AttributeError):
1452+
c.y
1453+
c.y = 6
1454+
assert c.y == 6
1455+
t: T2 = C2()
1456+
with assertRaises(AttributeError):
1457+
t.y
1458+
t = c
1459+
assert t.x == MAGIC
1460+
c.x = 55
1461+
assert t.x == 55
1462+
assert t.y == 6
1463+
a: Any = c
1464+
assert a.x == 55
1465+
assert a.y == 6
1466+
a.x = 7
1467+
a.y = 8
1468+
assert a.x == 7
1469+
assert a.y == 8
1470+
1471+
class D2(T2):
1472+
x: i64
1473+
y: i64 = 4
1474+
1475+
def test_implement_trait_attribute() -> None:
1476+
d = D2()
1477+
d.x = 5
1478+
assert d.x == 5
1479+
d.x = MAGIC
1480+
assert d.x == MAGIC
1481+
assert d.y == 4
1482+
d.y = 6
1483+
assert d.y == 6
1484+
t: T2 = D2()
1485+
assert t.y == 4
1486+
t = d
1487+
assert t.x == MAGIC
1488+
d.x = 55
1489+
assert t.x == 55
1490+
assert t.y == 6
1491+
a: Any = d
1492+
assert a.x == 55
1493+
assert a.y == 6
1494+
a.x = 7
1495+
a.y = 8
1496+
assert a.x == 7
1497+
assert a.y == 8

0 commit comments

Comments
 (0)