@@ -38,6 +38,47 @@ def assert_no_opcode(self, f, opname):
3838 opnames = {instruction .opname for instruction in instructions }
3939 self .assertNotIn (opname , opnames )
4040
41+ def adaptive_counters (self , f ):
42+ """Map each specialized instruction in f to its adaptive counter."""
43+ counters = {}
44+ for instruction in dis .get_instructions (f , adaptive = True ):
45+ if instruction .opname == instruction .baseopname :
46+ continue
47+ if instruction .baseopname in ("RESUME" , "JUMP_BACKWARD" ):
48+ continue
49+ cache = {name : data for name , _ , data in instruction .cache_info }
50+ if "counter" in cache :
51+ counters [instruction .offset ] = (instruction .opname ,
52+ cache ["counter" ])
53+ return counters
54+
55+ def assert_specialization_stable (self , f , * args , calls = 10 ):
56+ """Assert that no specialized instruction in f misses its guard.
57+
58+ A specialized instruction whose guard holds leaves its adaptive
59+ counter untouched. One whose guard fails on every execution
60+ (the specializer accepted a site its guard then rejects) advances
61+ the counter on each miss, so comparing the counters before and
62+ after a few more calls detects the mismatch without needing a
63+ pystats build.
64+
65+ f must start from a fresh code object (see reset_code()): a nested
66+ function reuses its code object across calls of the enclosing test,
67+ so a specialization left over from a previous run with different
68+ operands would show up here as a miss.
69+ """
70+ before = self .adaptive_counters (f )
71+ self .assertTrue (before , f"{ f .__qualname__ } has no specialized "
72+ "instructions" )
73+ for _ in range (calls ):
74+ f (* args )
75+ after = self .adaptive_counters (f )
76+ moved = [f"{ op } at offset { off } : counter { before [off ][1 ]} -> { c } "
77+ for off , (op , c ) in after .items () if before [off ] != (op , c )]
78+ self .assertEqual (moved , [],
79+ f"specialized instructions in { f .__qualname__ } "
80+ f"missed their guard during { calls } calls" )
81+
4182
4283class TestLoadSuperAttrCache (unittest .TestCase ):
4384 def test_descriptor_not_double_executed_on_spec_fail (self ):
@@ -2156,6 +2197,7 @@ def list_append(l):
21562197 for _ in r :
21572198 l .append (1 )
21582199
2200+ reset_code (list_append )
21592201 list_append ([])
21602202 self .assert_specialized (list_append , "CALL_LIST_APPEND" )
21612203 self .assert_no_opcode (list_append , "CALL_METHOD_DESCRIPTOR_O" )
@@ -2166,10 +2208,47 @@ def my_list_append(l):
21662208 l .append (1 )
21672209
21682210 class MyList (list ): pass
2169- my_list_append (MyList ())
2211+ my_list = MyList ()
2212+ reset_code (my_list_append )
2213+ my_list_append (my_list )
21702214 self .assert_specialized (my_list_append , "CALL_METHOD_DESCRIPTOR_O" )
21712215 self .assert_no_opcode (my_list_append , "CALL_LIST_APPEND" )
21722216 self .assert_no_opcode (my_list_append , "CALL" )
2217+ self .assert_specialization_stable (my_list_append , my_list )
2218+
2219+ @cpython_only
2220+ @requires_specialization
2221+ @requires_jit_disabled # JIT-compiled code stops moving tier-1 counters
2222+ def test_call_method_descriptor_subclass_instance (self ):
2223+ # A C method inherited from a built-in type is specialized for
2224+ # subclass instances too; each guard must accept the subtype
2225+ # rather than miss on every call. One case per guard, plus the
2226+ # unbound form where the receiver is the first argument.
2227+ class MyStr (str ): pass
2228+ Point = collections .namedtuple ("Point" , "x y" )
2229+ counts = collections .defaultdict (int , {1 : 2 })
2230+
2231+ def noargs (s ): return s .upper ()
2232+ def o (p ): return p .count (1 )
2233+ def fast (d ): return d .get (1 )
2234+ def fast_with_keywords (s ): return s .split ()
2235+ def unbound (d ): return dict .get (d , 1 )
2236+
2237+ cases = [
2238+ (noargs , MyStr ("abc" ), "CALL_METHOD_DESCRIPTOR_NOARGS" ),
2239+ (o , Point (1 , 2 ), "CALL_METHOD_DESCRIPTOR_O" ),
2240+ (fast , counts , "CALL_METHOD_DESCRIPTOR_FAST" ),
2241+ (fast_with_keywords , MyStr ("a b" ),
2242+ "CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS" ),
2243+ (unbound , counts , "CALL_METHOD_DESCRIPTOR_FAST" ),
2244+ ]
2245+ for f , obj , opname in cases :
2246+ with self .subTest (f = f .__name__ , obj = type (obj ).__name__ ):
2247+ reset_code (f )
2248+ for _ in range (_testinternalcapi .SPECIALIZATION_THRESHOLD ):
2249+ f (obj )
2250+ self .assert_specialized (f , opname )
2251+ self .assert_specialization_stable (f , obj )
21732252
21742253 @cpython_only
21752254 @requires_specialization
0 commit comments