Skip to content

Commit b4df012

Browse files
jgarzikclaude
andcommitted
Fix SEND exhausted path for non-generator iterators; import 5 test suites
Bug fix: - op_send exhausted: guard gi_return_value read with tp_basicsize > 56. Plain iterators (str_iter, list_iter) have smaller objects without gi_return_value field. Was crashing on yield-from-string and silently returning garbage for some iterator types. - Export async_gen_asend_type for use by SEND type checks. New tests (0 skips unless noted): - test_del.py: 7 tests — del local/list/dict/attribute/multiple - test_assert.py: 8 tests — assert true/false/message/expression - test_assignment.py: 28 tests — simple/augmented/subscript/attribute assign - test_exceptions_extra.py: 17 tests — except clauses, finally, raise/reraise - test_generators_extra.py: 19 tests — yield, send, yield-from, genexps Total CPython test suites: 52 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d9f936 commit b4df012

8 files changed

Lines changed: 632 additions & 4 deletions

File tree

Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ gen-cpython-tests:
150150
@$(PYTHON) -m py_compile tests/cpython/test_unpacking.py
151151
@echo "Compiling tests/cpython/test_inheritance.py..."
152152
@$(PYTHON) -m py_compile tests/cpython/test_inheritance.py
153+
@$(PYTHON) -m py_compile tests/cpython/test_del.py
154+
@$(PYTHON) -m py_compile tests/cpython/test_assert.py
155+
@$(PYTHON) -m py_compile tests/cpython/test_assignment.py
156+
@$(PYTHON) -m py_compile tests/cpython/test_exceptions_extra.py
157+
@$(PYTHON) -m py_compile tests/cpython/test_generators_extra.py
153158
@echo "Done."
154159

155160
check-cpython: $(TARGET) gen-cpython-tests
@@ -247,3 +252,13 @@ check-cpython: $(TARGET) gen-cpython-tests
247252
@./apython tests/cpython/__pycache__/test_unpacking.cpython-312.pyc
248253
@echo "Running CPython test_inheritance.py..."
249254
@./apython tests/cpython/__pycache__/test_inheritance.cpython-312.pyc
255+
@echo "Running CPython test_del.py..."
256+
@./apython tests/cpython/__pycache__/test_del.cpython-312.pyc
257+
@echo "Running CPython test_assert.py..."
258+
@./apython tests/cpython/__pycache__/test_assert.cpython-312.pyc
259+
@echo "Running CPython test_assignment.py..."
260+
@./apython tests/cpython/__pycache__/test_assignment.cpython-312.pyc
261+
@echo "Running CPython test_exceptions_extra.py..."
262+
@./apython tests/cpython/__pycache__/test_exceptions_extra.cpython-312.pyc
263+
@echo "Running CPython test_generators_extra.py..."
264+
@./apython tests/cpython/__pycache__/test_generators_extra.cpython-312.pyc

src/opcodes_misc.asm

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,14 +1863,23 @@ DEF_FUNC op_send, SND_FRAME
18631863
DISPATCH
18641864

18651865
.send_exhausted:
1866-
; Generator exhausted. Push gi_return_value (for yield-from protocol).
1867-
; Stack: ... | receiver | → becomes ... | receiver | return_value |
1868-
; Then jump to END_SEND which will handle cleanup.
1869-
mov rdi, [rbp - SND_RECV] ; receiver = generator
1866+
; Receiver exhausted. Push return value (for yield-from protocol).
1867+
; Gen/coro/task/awaitable/asend all have gi_return_value at offset +48.
1868+
; Guard: only read if receiver's type has tp_basicsize > 56 (enough for +48 field).
1869+
; Plain iterators (str_iter, list_iter) have smaller objects → push None.
1870+
mov rdi, [rbp - SND_RECV]
1871+
cmp byte [r15 - 1], TAG_PTR
1872+
jne .send_no_retval
1873+
test rdi, rdi
1874+
jz .send_no_retval
1875+
mov rax, [rdi + PyObject.ob_type]
1876+
cmp qword [rax + PyTypeObject.tp_basicsize], 56
1877+
jle .send_no_retval
18701878
mov rax, [rdi + PyGenObject.gi_return_value]
18711879
mov rdx, [rdi + PyGenObject.gi_return_tag]
18721880
test edx, edx
18731881
jnz .send_have_retval
1882+
.send_no_retval:
18741883
; No return value — push None
18751884
lea rax, [rel none_singleton]
18761885
INCREF rax

src/pyo/generator.asm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,7 @@ async_gen_type:
12631263
ags_name_str: db "async_generator_asend", 0
12641264

12651265
align 8
1266+
global async_gen_asend_type
12661267
async_gen_asend_type:
12671268
dq 1 ; ob_refcnt (immortal)
12681269
dq type_type ; ob_type

tests/cpython/test_assert.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Tests for assert statement"""
2+
3+
import unittest
4+
5+
6+
class AssertTest(unittest.TestCase):
7+
8+
def test_assert_true(self):
9+
assert True
10+
assert 1
11+
assert "nonempty"
12+
assert [1]
13+
14+
def test_assert_false(self):
15+
with self.assertRaises(AssertionError):
16+
assert False
17+
18+
def test_assert_zero(self):
19+
with self.assertRaises(AssertionError):
20+
assert 0
21+
22+
def test_assert_none(self):
23+
with self.assertRaises(AssertionError):
24+
assert None
25+
26+
def test_assert_empty(self):
27+
with self.assertRaises(AssertionError):
28+
assert ""
29+
with self.assertRaises(AssertionError):
30+
assert []
31+
with self.assertRaises(AssertionError):
32+
assert {}
33+
34+
def test_assert_message(self):
35+
try:
36+
assert False, "custom message"
37+
except AssertionError as e:
38+
self.assertEqual(str(e), "custom message")
39+
else:
40+
self.fail("AssertionError not raised")
41+
42+
def test_assert_expression(self):
43+
x = 5
44+
assert x > 0
45+
assert x < 10
46+
assert x == 5
47+
48+
def test_assert_in_function(self):
49+
def check(x):
50+
assert x > 0, "must be positive"
51+
return x * 2
52+
self.assertEqual(check(5), 10)
53+
with self.assertRaises(AssertionError):
54+
check(-1)
55+
56+
57+
if __name__ == "__main__":
58+
unittest.main()

tests/cpython/test_assignment.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Tests for various assignment forms"""
2+
3+
import unittest
4+
5+
6+
class SimpleAssignTest(unittest.TestCase):
7+
8+
def test_basic(self):
9+
x = 42
10+
self.assertEqual(x, 42)
11+
12+
def test_multiple_targets(self):
13+
a = b = c = 10
14+
self.assertEqual(a, 10)
15+
self.assertEqual(b, 10)
16+
self.assertEqual(c, 10)
17+
18+
def test_tuple_assign(self):
19+
a, b = 1, 2
20+
self.assertEqual(a, 1)
21+
self.assertEqual(b, 2)
22+
23+
def test_list_assign(self):
24+
[a, b, c] = [4, 5, 6]
25+
self.assertEqual((a, b, c), (4, 5, 6))
26+
27+
def test_swap(self):
28+
a, b = 1, 2
29+
a, b = b, a
30+
self.assertEqual(a, 2)
31+
self.assertEqual(b, 1)
32+
33+
def test_chained(self):
34+
x = y = z = []
35+
x.append(1)
36+
self.assertEqual(y, [1])
37+
self.assertEqual(z, [1])
38+
self.assertIs(x, y)
39+
40+
41+
class AugmentedAssignTest(unittest.TestCase):
42+
43+
def test_iadd(self):
44+
x = 10
45+
x += 5
46+
self.assertEqual(x, 15)
47+
48+
def test_isub(self):
49+
x = 10
50+
x -= 3
51+
self.assertEqual(x, 7)
52+
53+
def test_imul(self):
54+
x = 4
55+
x *= 3
56+
self.assertEqual(x, 12)
57+
58+
def test_ifloordiv(self):
59+
x = 10
60+
x //= 3
61+
self.assertEqual(x, 3)
62+
63+
def test_imod(self):
64+
x = 10
65+
x %= 3
66+
self.assertEqual(x, 1)
67+
68+
def test_ipow(self):
69+
x = 2
70+
x **= 10
71+
self.assertEqual(x, 1024)
72+
73+
def test_iand(self):
74+
x = 0xFF
75+
x &= 0x0F
76+
self.assertEqual(x, 0x0F)
77+
78+
def test_ior(self):
79+
x = 0x0F
80+
x |= 0xF0
81+
self.assertEqual(x, 0xFF)
82+
83+
def test_ixor(self):
84+
x = 0xFF
85+
x ^= 0x0F
86+
self.assertEqual(x, 0xF0)
87+
88+
def test_ilshift(self):
89+
x = 1
90+
x <<= 10
91+
self.assertEqual(x, 1024)
92+
93+
def test_irshift(self):
94+
x = 1024
95+
x >>= 10
96+
self.assertEqual(x, 1)
97+
98+
def test_iadd_list(self):
99+
x = [1, 2]
100+
y = x
101+
x += [3, 4]
102+
self.assertEqual(x, [1, 2, 3, 4])
103+
self.assertIs(x, y) # list += modifies in place
104+
105+
def test_iadd_string(self):
106+
x = "hello"
107+
x += " world"
108+
self.assertEqual(x, "hello world")
109+
110+
def test_imul_list(self):
111+
x = [1, 2]
112+
x *= 3
113+
self.assertEqual(x, [1, 2, 1, 2, 1, 2])
114+
115+
116+
class SubscriptAssignTest(unittest.TestCase):
117+
118+
def test_list_index(self):
119+
a = [0, 0, 0]
120+
a[0] = 1
121+
a[2] = 3
122+
self.assertEqual(a, [1, 0, 3])
123+
124+
def test_list_negative(self):
125+
a = [1, 2, 3]
126+
a[-1] = 99
127+
self.assertEqual(a, [1, 2, 99])
128+
129+
def test_list_slice(self):
130+
a = [1, 2, 3, 4, 5]
131+
a[1:3] = [20, 30]
132+
self.assertEqual(a, [1, 20, 30, 4, 5])
133+
134+
def test_dict_assign(self):
135+
d = {}
136+
d['key'] = 'value'
137+
self.assertEqual(d['key'], 'value')
138+
139+
def test_nested_assign(self):
140+
a = [[0, 0], [0, 0]]
141+
a[0][1] = 42
142+
self.assertEqual(a[0][1], 42)
143+
144+
145+
class AttributeAssignTest(unittest.TestCase):
146+
147+
def test_instance_attr(self):
148+
class C:
149+
pass
150+
obj = C()
151+
obj.x = 42
152+
self.assertEqual(obj.x, 42)
153+
154+
def test_overwrite(self):
155+
class C:
156+
pass
157+
obj = C()
158+
obj.x = 1
159+
obj.x = 2
160+
self.assertEqual(obj.x, 2)
161+
162+
def test_multiple_attrs(self):
163+
class C:
164+
pass
165+
obj = C()
166+
obj.a = 1
167+
obj.b = 2
168+
obj.c = 3
169+
self.assertEqual(obj.a + obj.b + obj.c, 6)
170+
171+
172+
if __name__ == "__main__":
173+
unittest.main()

tests/cpython/test_del.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests for del statement"""
2+
3+
import unittest
4+
5+
6+
class DelTest(unittest.TestCase):
7+
8+
def test_del_local(self):
9+
x = 42
10+
del x
11+
with self.assertRaises(UnboundLocalError):
12+
x
13+
14+
def test_del_list_item(self):
15+
a = [1, 2, 3, 4, 5]
16+
del a[2]
17+
self.assertEqual(a, [1, 2, 4, 5])
18+
19+
def test_del_list_slice(self):
20+
a = [0, 1, 2, 3, 4]
21+
del a[1:3]
22+
self.assertEqual(a, [0, 3, 4])
23+
24+
def test_del_dict_item(self):
25+
d = {'a': 1, 'b': 2, 'c': 3}
26+
del d['b']
27+
self.assertEqual(sorted(d.keys()), ['a', 'c'])
28+
29+
def test_del_attribute(self):
30+
class C:
31+
pass
32+
obj = C()
33+
obj.x = 42
34+
self.assertEqual(obj.x, 42)
35+
del obj.x
36+
with self.assertRaises(AttributeError):
37+
obj.x
38+
39+
def test_del_multiple(self):
40+
a = 1
41+
b = 2
42+
c = 3
43+
del a, b
44+
self.assertEqual(c, 3)
45+
with self.assertRaises(UnboundLocalError):
46+
a
47+
48+
def test_del_in_loop(self):
49+
lst = list(range(5))
50+
while lst:
51+
del lst[-1]
52+
self.assertEqual(lst, [])
53+
54+
55+
if __name__ == "__main__":
56+
unittest.main()

0 commit comments

Comments
 (0)