From 6d3d275d3ef0bed0d5660ab91740166ad25a3279 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sun, 29 May 2022 11:23:16 +0100 Subject: [PATCH] [mypyc] Track definedness of native int attributes using a bitmap Since native ints can't support a reserved value to mark an undefined attribute, use a separate bitmap attribute (or attributes) to store information about defined/undefined attributes with native int types. The bitmap is only defined if we can't infer that an attribute is always defined, and it's only needed for native int attributes. We only access the bitmap if the runtime value of an attribute is equal to the (overlapping) error value. This way the performance cost of the bitmap is pretty low on average. I'll add support for traits in a follow-up PR to keep this PR simple. Work on mypyc/mypyc#837. --- mypyc/analysis/attrdefined.py | 21 ++- mypyc/codegen/emit.py | 71 ++++++++- mypyc/codegen/emitclass.py | 32 +++- mypyc/codegen/emitfunc.py | 14 +- mypyc/common.py | 5 + mypyc/ir/class_ir.py | 9 +- mypyc/ir/ops.py | 2 +- mypyc/irbuild/main.py | 6 +- mypyc/test-data/run-i64.test | 285 +++++++++++++++++++++++++++++++++- mypyc/test/test_emitfunc.py | 75 ++++++++- 10 files changed, 503 insertions(+), 17 deletions(-) diff --git a/mypyc/analysis/attrdefined.py b/mypyc/analysis/attrdefined.py index 170c0029ba04..dc871a93eba1 100644 --- a/mypyc/analysis/attrdefined.py +++ b/mypyc/analysis/attrdefined.py @@ -91,7 +91,7 @@ def foo(self) -> int: SetMem, Unreachable, ) -from mypyc.ir.rtypes import RInstance +from mypyc.ir.rtypes import RInstance, is_fixed_width_rtype # If True, print out all always-defined attributes of native classes (to aid # debugging and testing) @@ -120,6 +120,11 @@ def analyze_always_defined_attrs(class_irs: list[ClassIR]) -> None: for cl in class_irs: update_always_defined_attrs_using_subclasses(cl, seen) + # Final pass: detect attributes that need to use a bitmap to track definedness + seen = set() + for cl in class_irs: + detect_undefined_bitmap(cl, seen) + def analyze_always_defined_attrs_in_class(cl: ClassIR, seen: set[ClassIR]) -> None: if cl in seen: @@ -407,3 +412,17 @@ def update_always_defined_attrs_using_subclasses(cl: ClassIR, seen: set[ClassIR] removed.add(attr) cl._always_initialized_attrs -= removed seen.add(cl) + + +def detect_undefined_bitmap(cl: ClassIR, seen: Set[ClassIR]) -> None: + if cl in seen: + return + seen.add(cl) + for base in cl.base_mro[1:]: + detect_undefined_bitmap(cl, seen) + + if len(cl.base_mro) > 1: + cl.bitmap_attrs.extend(cl.base_mro[1].bitmap_attrs) + for n, t in cl.attributes.items(): + if is_fixed_width_rtype(t) and not cl.is_always_defined(n): + cl.bitmap_attrs.append(n) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 3fd48dcd1cb8..90919665a0d2 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -8,6 +8,7 @@ from mypyc.codegen.literals import Literals from mypyc.common import ( + ATTR_BITMAP_BITS, ATTR_PREFIX, FAST_ISINSTANCE_MAX_SUBCLASSES, NATIVE_PREFIX, @@ -329,21 +330,81 @@ def tuple_c_declaration(self, rtuple: RTuple) -> list[str]: return result + def bitmap_field(self, index: int) -> str: + """Return C field name used for attribute bitmap.""" + n = index // ATTR_BITMAP_BITS + if n == 0: + return "bitmap" + return f"bitmap{n + 1}" + + def attr_bitmap_expr(self, obj: str, cl: ClassIR, index: int) -> str: + """Return reference to the attribute definedness bitmap.""" + cast = f"({cl.struct_name(self.names)} *)" + attr = self.bitmap_field(index) + return f"({cast}{obj})->{attr}" + + def emit_attr_bitmap_set( + self, value: str, obj: str, rtype: RType, cl: ClassIR, attr: str + ) -> None: + """Mark an attribute as defined in the attribute bitmap. + + Assumes that the attribute is tracked in the bitmap (only some attributes + use the bitmap). If 'value' is not equal to the error value, do nothing. + """ + self._emit_attr_bitmap_update(value, obj, rtype, cl, attr, clear=False) + + def emit_attr_bitmap_clear(self, obj: str, rtype: RType, cl: ClassIR, attr: str) -> None: + """Mark an attribute as undefined in the attribute bitmap. + + Unlike emit_attr_bitmap_set, clear unconditionally. + """ + self._emit_attr_bitmap_update("", obj, rtype, cl, attr, clear=True) + + def _emit_attr_bitmap_update( + self, value: str, obj: str, rtype: RType, cl: ClassIR, attr: str, clear: bool + ) -> None: + if value: + self.emit_line(f"if (unlikely({value} == {self.c_undefined_value(rtype)})) {{") + index = cl.bitmap_attrs.index(attr) + mask = 1 << (index & (ATTR_BITMAP_BITS - 1)) + bitmap = self.attr_bitmap_expr(obj, cl, index) + if clear: + self.emit_line(f"{bitmap} &= ~{mask};") + else: + self.emit_line(f"{bitmap} |= {mask};") + if value: + self.emit_line("}") + def use_vectorcall(self) -> bool: return use_vectorcall(self.capi_version) def emit_undefined_attr_check( - self, rtype: RType, attr_expr: str, compare: str, unlikely: bool = False + self, + rtype: RType, + attr_expr: str, + compare: str, + obj: str, + attr: str, + cl: ClassIR, + *, + unlikely: bool = False, ) -> None: if isinstance(rtype, RTuple): - check = "({})".format( + check = "{}".format( self.tuple_undefined_check_cond(rtype, attr_expr, self.c_undefined_value, compare) ) else: - check = f"({attr_expr} {compare} {self.c_undefined_value(rtype)})" + undefined = self.c_undefined_value(rtype) + check = f"{attr_expr} {compare} {undefined}" if unlikely: - check = f"(unlikely{check})" - self.emit_line(f"if {check} {{") + check = f"unlikely({check})" + if is_fixed_width_rtype(rtype): + index = cl.bitmap_attrs.index(attr) + bit = 1 << (index & (ATTR_BITMAP_BITS - 1)) + attr = self.bitmap_field(index) + obj_expr = f"({cl.struct_name(self.names)} *){obj}" + check = f"{check} && !(({obj_expr})->{attr} & {bit})" + self.emit_line(f"if ({check}) {{") def tuple_undefined_check_cond( self, diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 5434b5c01219..a93ef1b57a1e 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -17,10 +17,17 @@ generate_richcompare_wrapper, generate_set_del_item_wrapper, ) -from mypyc.common import NATIVE_PREFIX, PREFIX, REG_PREFIX, use_fastcall +from mypyc.common import ( + ATTR_BITMAP_BITS, + ATTR_BITMAP_TYPE, + NATIVE_PREFIX, + PREFIX, + REG_PREFIX, + use_fastcall, +) from mypyc.ir.class_ir import ClassIR, VTableEntries from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR -from mypyc.ir.rtypes import RTuple, RType, object_rprimitive +from mypyc.ir.rtypes import RTuple, RType, is_fixed_width_rtype, object_rprimitive from mypyc.namegen import NameGenerator from mypyc.sametype import is_same_type @@ -367,8 +374,17 @@ def generate_object_struct(cl: ClassIR, emitter: Emitter) -> None: lines += ["typedef struct {", "PyObject_HEAD", "CPyVTableItem *vtable;"] if cl.has_method("__call__") and emitter.use_vectorcall(): lines.append("vectorcallfunc vectorcall;") + bitmap_attrs = [] for base in reversed(cl.base_mro): if not base.is_trait: + if base.bitmap_attrs: + # Do we need another attribute bitmap field? + if emitter.bitmap_field(len(base.bitmap_attrs) - 1) not in bitmap_attrs: + for i in range(0, len(base.bitmap_attrs), ATTR_BITMAP_BITS): + attr = emitter.bitmap_field(i) + if attr not in bitmap_attrs: + lines.append(f"{ATTR_BITMAP_TYPE} {attr};") + bitmap_attrs.append(attr) for attr, rtype in base.attributes.items(): if (attr, rtype) not in seen_attrs: lines.append(f"{emitter.ctype_spaced(rtype)}{emitter.attr(attr)};") @@ -546,6 +562,9 @@ def generate_setup_for_class( emitter.emit_line("}") else: emitter.emit_line(f"self->vtable = {vtable_name};") + for i in range(0, len(cl.bitmap_attrs), ATTR_BITMAP_BITS): + field = emitter.bitmap_field(i) + emitter.emit_line(f"self->{field} = 0;") if cl.has_method("__call__") and emitter.use_vectorcall(): name = cl.method_decl("__call__").cname(emitter.names) @@ -887,7 +906,7 @@ def generate_getter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N always_defined = cl.is_always_defined(attr) and not rtype.is_refcounted if not always_defined: - emitter.emit_undefined_attr_check(rtype, attr_expr, "==", unlikely=True) + emitter.emit_undefined_attr_check(rtype, attr_expr, "==", "self", attr, cl, unlikely=True) emitter.emit_line("PyErr_SetString(PyExc_AttributeError,") emitter.emit_line(f' "attribute {repr(attr)} of {repr(cl.name)} undefined");') emitter.emit_line("return NULL;") @@ -926,7 +945,7 @@ def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N if rtype.is_refcounted: attr_expr = f"self->{attr_field}" if not always_defined: - emitter.emit_undefined_attr_check(rtype, attr_expr, "!=") + emitter.emit_undefined_attr_check(rtype, attr_expr, "!=", "self", attr, cl) emitter.emit_dec_ref(f"self->{attr_field}", rtype) if not always_defined: emitter.emit_line("}") @@ -943,9 +962,14 @@ def generate_setter(cl: ClassIR, attr: str, rtype: RType, emitter: Emitter) -> N emitter.emit_lines("if (!tmp)", " return -1;") emitter.emit_inc_ref("tmp", rtype) emitter.emit_line(f"self->{attr_field} = tmp;") + if is_fixed_width_rtype(rtype) and not always_defined: + emitter.emit_attr_bitmap_set("tmp", "self", rtype, cl, attr) + if deletable: emitter.emit_line("} else") emitter.emit_line(f" self->{attr_field} = {emitter.c_undefined_value(rtype)};") + if is_fixed_width_rtype(rtype): + emitter.emit_attr_bitmap_clear("self", rtype, cl, attr) emitter.emit_line("return 0;") emitter.emit_line("}") diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index c0aaff2c5f99..2c096655f41e 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -60,6 +60,7 @@ RStruct, RTuple, RType, + is_fixed_width_rtype, is_int32_rprimitive, is_int64_rprimitive, is_int_rprimitive, @@ -353,7 +354,9 @@ def visit_get_attr(self, op: GetAttr) -> None: always_defined = cl.is_always_defined(op.attr) merged_branch = None if not always_defined: - self.emitter.emit_undefined_attr_check(attr_rtype, dest, "==", unlikely=True) + self.emitter.emit_undefined_attr_check( + attr_rtype, dest, "==", obj, op.attr, cl, unlikely=True + ) branch = self.next_branch() if branch is not None: if ( @@ -433,10 +436,17 @@ def visit_set_attr(self, op: SetAttr) -> None: # previously undefined), so decref the old value. always_defined = cl.is_always_defined(op.attr) if not always_defined: - self.emitter.emit_undefined_attr_check(attr_rtype, attr_expr, "!=") + self.emitter.emit_undefined_attr_check( + attr_rtype, attr_expr, "!=", obj, op.attr, cl + ) self.emitter.emit_dec_ref(attr_expr, attr_rtype) if not always_defined: self.emitter.emit_line("}") + elif is_fixed_width_rtype(attr_rtype) and not cl.is_always_defined(op.attr): + # If there is overlap with the error value, update bitmap to mark + # attribute as defined. + self.emitter.emit_attr_bitmap_set(src, obj, attr_rtype, cl, op.attr) + # This steals the reference to src, so we don't need to increment the arg self.emitter.emit_line(f"{attr_expr} = {src};") if op.error_kind == ERR_FALSE: diff --git a/mypyc/common.py b/mypyc/common.py index 8083d83c9d6a..e0202eaa3edc 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -53,6 +53,11 @@ MAX_LITERAL_SHORT_INT: Final = sys.maxsize >> 1 if not IS_MIXED_32_64_BIT_BUILD else 2**30 - 1 MIN_LITERAL_SHORT_INT: Final = -MAX_LITERAL_SHORT_INT - 1 +# Decription of the C type used to track definedness of attributes +# that have types with overlapping error values +ATTR_BITMAP_TYPE: Final = "uint32_t" +ATTR_BITMAP_BITS: Final = 32 + # Runtime C library files RUNTIME_C_FILES: Final = [ "init.c", diff --git a/mypyc/ir/class_ir.py b/mypyc/ir/class_ir.py index dca19e5a2e3c..7f55decfd754 100644 --- a/mypyc/ir/class_ir.py +++ b/mypyc/ir/class_ir.py @@ -130,7 +130,7 @@ def __init__( self.builtin_base: str | None = None # Default empty constructor self.ctor = FuncDecl(name, None, module_name, FuncSignature([], RInstance(self))) - + # Attributes defined in the class (not inherited) self.attributes: dict[str, RType] = {} # Deletable attributes self.deletable: list[str] = [] @@ -184,6 +184,13 @@ def __init__( # If True, __init__ can make 'self' visible to unanalyzed/arbitrary code self.init_self_leak = False + # Definedness of these attributes is backed by a bitmap. Index in the list + # indicates the bit number. Includes inherited attributes. We need the + # bitmap for types such as native ints that can't have a dedicated error + # value that doesn't overlap a valid value. The bitmap is used if the + # value of an attribute is the same as the error value. + self.bitmap_attrs: List[str] = [] + def __repr__(self) -> str: return ( "ClassIR(" diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 56a1e6103acf..361221f5b710 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -633,7 +633,7 @@ def __init__(self, obj: Value, attr: str, line: int, *, borrow: bool = False) -> attr_type = obj.type.attr_type(attr) self.type = attr_type if is_fixed_width_rtype(attr_type): - self.error_kind = ERR_NEVER + self.error_kind = ERR_MAGIC_OVERLAPPING self.is_borrowed = borrow and attr_type.is_refcounted def sources(self) -> list[Value]: diff --git a/mypyc/irbuild/main.py b/mypyc/irbuild/main.py index e20872979b7a..9bbb90aad207 100644 --- a/mypyc/irbuild/main.py +++ b/mypyc/irbuild/main.py @@ -57,7 +57,11 @@ def build_ir( options: CompilerOptions, errors: Errors, ) -> ModuleIRs: - """Build IR for a set of modules that have been type-checked by mypy.""" + """Build basic IR for a set of modules that have been type-checked by mypy. + + The returned IR is not complete and requires additional + transformations, such as the insertion of refcount handling. + """ build_type_map(mapper, modules, graph, types, options, errors) singledispatch_info = find_singledispatch_register_impls(modules, errors) diff --git a/mypyc/test-data/run-i64.test b/mypyc/test-data/run-i64.test index d7bba82493b2..e91c24185f4f 100644 --- a/mypyc/test-data/run-i64.test +++ b/mypyc/test-data/run-i64.test @@ -335,10 +335,13 @@ def test_mixed_arithmetic_and_bitwise_ops() -> None: with assertRaises(OverflowError): assert int_too_small & i64_3 -[case testI64ErrorValues] +[case testI64ErrorValuesAndUndefined] from typing import Any import sys +from mypy_extensions import mypyc_attr +from typing_extensions import Final + MYPY = False if MYPY: from mypy_extensions import i64 @@ -390,3 +393,283 @@ def test_unbox_int_fails() -> None: o3: Any = -(1 << 63 + 1) with assertRaises(OverflowError, "int too large to convert to i64"): z: i64 = o3 + +class Uninit: + x: i64 + y: i64 = 0 + z: i64 + +class Derived(Uninit): + a: i64 = 1 + b: i64 + c: i64 = 2 + +class Derived2(Derived): + h: i64 + +def test_uninitialized_attr() -> None: + o = Uninit() + assert o.y == 0 + with assertRaises(AttributeError): + o.x + with assertRaises(AttributeError): + o.z + o.x = 1 + assert o.x == 1 + with assertRaises(AttributeError): + o.z + o.z = 2 + assert o.z == 2 + +# This is the error value, but it's also a valid normal value +MAGIC: Final = -113 + +def test_magic_value() -> None: + o = Uninit() + o.x = MAGIC + assert o.x == MAGIC + with assertRaises(AttributeError): + o.z + o.z = MAGIC + assert o.x == MAGIC + assert o.z == MAGIC + +def test_magic_value_via_any() -> None: + o: Any = Uninit() + with assertRaises(AttributeError): + o.x + with assertRaises(AttributeError): + o.z + o.x = MAGIC + assert o.x == MAGIC + with assertRaises(AttributeError): + o.z + o.z = MAGIC + assert o.z == MAGIC + +def test_magic_value_and_inheritance() -> None: + o = Derived2() + o.x = MAGIC + assert o.x == MAGIC + with assertRaises(AttributeError): + o.z + with assertRaises(AttributeError): + o.b + with assertRaises(AttributeError): + o.h + o.z = MAGIC + assert o.z == MAGIC + with assertRaises(AttributeError): + o.b + with assertRaises(AttributeError): + o.h + o.h = MAGIC + assert o.h == MAGIC + with assertRaises(AttributeError): + o.b + o.b = MAGIC + assert o.b == MAGIC + +@mypyc_attr(allow_interpreted_subclasses=True) +class MagicInit: + x: i64 = MAGIC + +def test_magic_value_as_initializer() -> None: + o = MagicInit() + assert o.x == MAGIC + +class ManyUninit: + a1: i64 + a2: i64 + a3: i64 + a4: i64 + a5: i64 + a6: i64 + a7: i64 + a8: i64 + a9: i64 + a10: i64 + a11: i64 + a12: i64 + a13: i64 + a14: i64 + a15: i64 + a16: i64 + a17: i64 + a18: i64 + a19: i64 + a20: i64 + a21: i64 + a22: i64 + a23: i64 + a24: i64 + a25: i64 + a26: i64 + a27: i64 + a28: i64 + a29: i64 + a30: i64 + a31: i64 + a32: i64 + a33: i64 + a34: i64 + a35: i64 + a36: i64 + a37: i64 + a38: i64 + a39: i64 + a40: i64 + a41: i64 + a42: i64 + a43: i64 + a44: i64 + a45: i64 + a46: i64 + a47: i64 + a48: i64 + a49: i64 + a50: i64 + a51: i64 + a52: i64 + a53: i64 + a54: i64 + a55: i64 + a56: i64 + a57: i64 + a58: i64 + a59: i64 + a60: i64 + a61: i64 + a62: i64 + a63: i64 + a64: i64 + a65: i64 + a66: i64 + a67: i64 + a68: i64 + a69: i64 + a70: i64 + a71: i64 + a72: i64 + a73: i64 + a74: i64 + a75: i64 + a76: i64 + a77: i64 + a78: i64 + a79: i64 + a80: i64 + a81: i64 + a82: i64 + a83: i64 + a84: i64 + a85: i64 + a86: i64 + a87: i64 + a88: i64 + a89: i64 + a90: i64 + a91: i64 + a92: i64 + a93: i64 + a94: i64 + a95: i64 + a96: i64 + a97: i64 + a98: i64 + a99: i64 + a100: i64 + +def test_many_uninitialized_attributes() -> None: + o = ManyUninit() + with assertRaises(AttributeError): + o.a1 + with assertRaises(AttributeError): + o.a10 + with assertRaises(AttributeError): + o.a20 + with assertRaises(AttributeError): + o.a30 + with assertRaises(AttributeError): + o.a31 + with assertRaises(AttributeError): + o.a32 + with assertRaises(AttributeError): + o.a33 + with assertRaises(AttributeError): + o.a40 + with assertRaises(AttributeError): + o.a50 + with assertRaises(AttributeError): + o.a60 + with assertRaises(AttributeError): + o.a62 + with assertRaises(AttributeError): + o.a63 + with assertRaises(AttributeError): + o.a64 + with assertRaises(AttributeError): + o.a65 + with assertRaises(AttributeError): + o.a80 + with assertRaises(AttributeError): + o.a100 + o.a30 = MAGIC + assert o.a30 == MAGIC + o.a31 = MAGIC + assert o.a31 == MAGIC + o.a32 = MAGIC + assert o.a32 == MAGIC + o.a33 = MAGIC + assert o.a33 == MAGIC + with assertRaises(AttributeError): + o.a34 + o.a62 = MAGIC + assert o.a62 == MAGIC + o.a63 = MAGIC + assert o.a63 == MAGIC + o.a64 = MAGIC + assert o.a64 == MAGIC + o.a65 = MAGIC + assert o.a65 == MAGIC + with assertRaises(AttributeError): + o.a66 + +class BaseNoBitmap: + x: int = 5 + +class DerivedBitmap(BaseNoBitmap): + # Subclass needs a bitmap, but base class doesn't have it. + y: i64 + +def test_derived_adds_bitmap() -> None: + d = DerivedBitmap() + d.x = 643 + b: BaseNoBitmap = d + assert b.x == 643 + +class Delete: + __deletable__ = ['x', 'y'] + x: i64 + y: i64 + +def test_del() -> None: + o = Delete() + o.x = MAGIC + o.y = -1 + assert o.x == MAGIC + assert o.y == -1 + del o.x + with assertRaises(AttributeError): + o.x + assert o.y == -1 + del o.y + with assertRaises(AttributeError): + o.y + o.x = 5 + assert o.x == 5 + with assertRaises(AttributeError): + o.y + del o.x + with assertRaises(AttributeError): + o.x diff --git a/mypyc/test/test_emitfunc.py b/mypyc/test/test_emitfunc.py index 5be1e61cba8d..d7dcf3be532b 100644 --- a/mypyc/test/test_emitfunc.py +++ b/mypyc/test/test_emitfunc.py @@ -9,6 +9,7 @@ from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FuncDecl, FuncIR, FuncSignature, RuntimeArg from mypyc.ir.ops import ( + ERR_NEVER, Assign, AssignMulti, BasicBlock, @@ -103,7 +104,13 @@ def add_local(name: str, rtype: RType) -> Register: "tt", RTuple([RTuple([int_rprimitive, bool_rprimitive]), bool_rprimitive]) ) ir = ClassIR("A", "mod") - ir.attributes = {"x": bool_rprimitive, "y": int_rprimitive} + ir.attributes = { + "x": bool_rprimitive, + "y": int_rprimitive, + "i1": int64_rprimitive, + "i2": int32_rprimitive, + } + ir.bitmap_attrs = ["i1", "i2"] compute_vtable(ir) ir.mro = [ir] self.r = add_local("r", RInstance(ir)) @@ -397,6 +404,16 @@ def test_get_attr_merged(self) -> None: skip_next=True, ) + def test_get_attr_with_bitmap(self) -> None: + self.assert_emit( + GetAttr(self.r, "i1", 1), + """cpy_r_r0 = ((mod___AObject *)cpy_r_r)->_i1; + if (unlikely(cpy_r_r0 == -113) && !(((mod___AObject *)cpy_r_r)->bitmap & 1)) { + PyErr_SetString(PyExc_AttributeError, "attribute 'i1' of 'A' undefined"); + } + """, + ) + def test_set_attr(self) -> None: self.assert_emit( SetAttr(self.r, "y", self.m, 1), @@ -416,6 +433,62 @@ def test_set_attr_non_refcounted(self) -> None: """, ) + def test_set_attr_no_error(self) -> None: + op = SetAttr(self.r, "y", self.m, 1) + op.error_kind = ERR_NEVER + self.assert_emit( + op, + """if (((mod___AObject *)cpy_r_r)->_y != CPY_INT_TAG) { + CPyTagged_DECREF(((mod___AObject *)cpy_r_r)->_y); + } + ((mod___AObject *)cpy_r_r)->_y = cpy_r_m; + """, + ) + + def test_set_attr_non_refcounted_no_error(self) -> None: + op = SetAttr(self.r, "x", self.b, 1) + op.error_kind = ERR_NEVER + self.assert_emit( + op, + """((mod___AObject *)cpy_r_r)->_x = cpy_r_b; + """, + ) + + def test_set_attr_with_bitmap(self) -> None: + # For some rtypes the error value overlaps a valid value, so we need + # to use a separate bitmap to track defined attributes. + self.assert_emit( + SetAttr(self.r, "i1", self.i64, 1), + """if (unlikely(cpy_r_i64 == -113)) { + ((mod___AObject *)cpy_r_r)->bitmap |= 1; + } + ((mod___AObject *)cpy_r_r)->_i1 = cpy_r_i64; + cpy_r_r0 = 1; + """, + ) + self.assert_emit( + SetAttr(self.r, "i2", self.i32, 1), + """if (unlikely(cpy_r_i32 == -113)) { + ((mod___AObject *)cpy_r_r)->bitmap |= 2; + } + ((mod___AObject *)cpy_r_r)->_i2 = cpy_r_i32; + cpy_r_r0 = 1; + """, + ) + + def test_set_attr_init_with_bitmap(self) -> None: + op = SetAttr(self.r, "i1", self.i64, 1) + op.is_init = True + self.assert_emit( + op, + """if (unlikely(cpy_r_i64 == -113)) { + ((mod___AObject *)cpy_r_r)->bitmap |= 1; + } + ((mod___AObject *)cpy_r_r)->_i1 = cpy_r_i64; + cpy_r_r0 = 1; + """, + ) + def test_dict_get_item(self) -> None: self.assert_emit( CallC(