Skip to content

Commit cde4779

Browse files
authored
[mypyc] Fix non-deterministic class struct layout under separate=True (#21530)
The helper function `detect_undefined_bitmap` builds the list of attributes that need a per-instance "is set?" bit (`cl.bitmap_attrs`). It walks from a subclass up into its base and `.append()`s entries. The walk dedupes within one call via `seen`, but the function is called once per SCC; Under `separate=True`, every subclass of a shared base lives in its own SCC, so the base is visited multiple times and gets the same entries re-appended on every pass. After N visits, `base.bitmap_attrs` contains N duplicate copies of the same names, so attribute offsets shift between builds and not-rebuilt subclasses end up reading the wrong bytes. The added test case shows that on master branch `base.bitmap_attrs` has been populated with `["i"] * 11` The fix: Build a fresh local list and assign once at the end. The function becomes idempotent and the struct layout remains identical after each incremental build.
1 parent 6f0e77b commit cde4779

2 files changed

Lines changed: 31 additions & 4 deletions

File tree

‎mypyc/analysis/attrdefined.py‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -424,14 +424,20 @@ def detect_undefined_bitmap(cl: ClassIR, seen: set[ClassIR]) -> None:
424424
for base in cl.base_mro[1:]:
425425
detect_undefined_bitmap(base, seen)
426426

427+
# Build fresh and assign once. This function is called per SCC and `seen`
428+
# only dedupes within a single call, so appending in place to a shared base
429+
# would accumulate duplicates across SCCs and produce non-deterministic
430+
# struct layouts under separate=True.
431+
new_attrs: list[str] = []
427432
if len(cl.base_mro) > 1:
428-
cl.bitmap_attrs.extend(cl.base_mro[1].bitmap_attrs)
433+
new_attrs.extend(cl.base_mro[1].bitmap_attrs)
429434
for n, t in cl.attributes.items():
430435
if t.error_overlap and not cl.is_always_defined(n):
431-
cl.bitmap_attrs.append(n)
436+
new_attrs.append(n)
432437

433438
for base in cl.mro[1:]:
434439
if base.is_trait:
435440
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)
441+
if t.error_overlap and not cl.is_always_defined(n) and n not in new_attrs:
442+
new_attrs.append(n)
443+
cl.bitmap_attrs = new_attrs

‎mypyc/test/test_emitclass.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import unittest
44

5+
from mypyc.analysis.attrdefined import detect_undefined_bitmap
56
from mypyc.codegen.emitclass import getter_name, setter_name, slot_key
67
from mypyc.ir.class_ir import ClassIR
8+
from mypyc.ir.rtypes import int32_rprimitive
79
from mypyc.namegen import NameGenerator
810

911

@@ -33,3 +35,22 @@ def test_getter_name(self) -> None:
3335
generator = NameGenerator([["mod"]])
3436

3537
assert getter_name(cls, "down", generator) == "testing___SomeClass_get_down"
38+
39+
def test_bitmap_attrs_stable_across_repeat_analysis(self) -> None:
40+
# Regression: detect_undefined_bitmap used to mutate cl.bitmap_attrs
41+
# in place, so under separate=True (one SCC per group) a shared base
42+
# class would accumulate duplicate entries as each subclass's SCC
43+
# walked into it, growing the emitted struct between builds.
44+
base = ClassIR("Base", "mod")
45+
base.attributes = {"i": int32_rprimitive}
46+
sub = ClassIR("Sub", "mod")
47+
sub.attributes = {"j": int32_rprimitive}
48+
base.mro = base.base_mro = [base]
49+
sub.mro = sub.base_mro = [sub, base]
50+
base.children = [sub]
51+
52+
detect_undefined_bitmap(sub, seen=set())
53+
for _ in range(10):
54+
detect_undefined_bitmap(sub, seen=set())
55+
assert base.bitmap_attrs == ["i"]
56+
assert sub.bitmap_attrs == ["i", "j"]

0 commit comments

Comments
 (0)