Skip to content

Commit 2f68cf7

Browse files
jgarzikclaude
andcommitted
Import CPython test_set.py, implement set_richcompare
Import adapted CPython set tests (29 tests) and fix: - Implement set_richcompare for PY_EQ and PY_NE on set and frozenset - tp_richcompare was NULL, so == always returned False Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7de9f2b commit 2f68cf7

4 files changed

Lines changed: 337 additions & 2 deletions

File tree

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ gen-cpython-tests:
8080
@$(PYTHON) -m py_compile tests/cpython/test_tuple.py
8181
@echo "Compiling tests/cpython/test_dict.py..."
8282
@$(PYTHON) -m py_compile tests/cpython/test_dict.py
83+
@echo "Compiling tests/cpython/test_set.py..."
84+
@$(PYTHON) -m py_compile tests/cpython/test_set.py
8385
@echo "Done."
8486

8587
check-cpython: $(TARGET) gen-cpython-tests
@@ -107,3 +109,5 @@ check-cpython: $(TARGET) gen-cpython-tests
107109
@./apython tests/cpython/__pycache__/test_tuple.cpython-312.pyc
108110
@echo "Running CPython test_dict.py..."
109111
@./apython tests/cpython/__pycache__/test_dict.cpython-312.pyc
112+
@echo "Running CPython test_set.py..."
113+
@./apython tests/cpython/__pycache__/test_set.cpython-312.pyc

bugs.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,25 @@
7171
**Symptom**: Same as #6 but for tuples
7272
**Fix**: Same pattern as list.index - check nargs, extract start/stop.
7373

74+
### 15. set_richcompare completely unimplemented (set.asm)
75+
**Symptom**: `{1,2} == {1,2}` returns False, `set() == set()` returns False
76+
**Root cause**: `tp_richcompare = 0` for set_type and frozenset_type. The `==` operator fell through to identity comparison.
77+
**Fix**: Implement `set_richcompare` for PY_EQ and PY_NE. PY_EQ checks len equality then verifies every element of self is in other. PY_NE negates PY_EQ result.
78+
79+
### 16. dict() constructor creates broken dict (dict.asm)
80+
**Symptom**: `dict() == {}` returns False, adding items to `dict()` crashes with "hash table full"
81+
**Root cause**: `dict_type.tp_call = 0`, so `type_call` used generic `instance_new` which allocated a PyDictObject-sized block but didn't initialize the hash table entries array.
82+
**Fix**: Implement `dict_type_call` that calls `dict_new` for no-args case and copies entries for dict(other_dict) case.
83+
84+
### Known Bugs Not Yet Fixed
85+
- `dict.update()` with no args segfaults (methods.asm)
86+
- `dict.update(x=1, y=2)` with kwargs segfaults (methods.asm)
87+
- `dict.popitem()` segfaults (methods.asm)
88+
- `set.update()` method not implemented (use |= instead)
89+
- `repr(d.keys())` returns wrong value (dict view repr not implemented)
90+
- `(1,1) in d.items()` fails (dict_items __contains__ not implemented)
91+
- `tuple(t) is t` identity optimization not implemented
92+
7493
## New Infrastructure Added
7594
- `list_copy()` - standalone shallow copy function
7695
- `ALWAYS_EQ`, `NEVER_EQ`, `C_RECURSION_LIMIT` in `lib/test/support/__init__.py`

src/pyo/set.asm

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,127 @@ DEF_FUNC set_contains
448448
ret
449449
END_FUNC set_contains
450450

451+
;; ============================================================================
452+
;; set_richcompare(self, other, op, self_tag, other_tag) -> (rax, edx) fat value
453+
;; Compares two sets. Only PY_EQ and PY_NE implemented.
454+
;; ============================================================================
455+
SRC_SELF equ 8
456+
SRC_OTHER equ 16
457+
SRC_OP equ 24
458+
SRC_FRAME equ 24
459+
DEF_FUNC set_richcompare, SRC_FRAME
460+
push rbx
461+
push r12
462+
push r13
463+
464+
mov [rbp - SRC_SELF], rdi
465+
mov [rbp - SRC_OTHER], rsi
466+
mov [rbp - SRC_OP], rdx
467+
468+
; Check other is a set or frozenset
469+
test r8d, TAG_RC_BIT
470+
jz .src_not_impl
471+
mov rax, [rsi + PyObject.ob_type]
472+
lea rcx, [rel set_type]
473+
cmp rax, rcx
474+
je .src_is_set
475+
lea rcx, [rel frozenset_type]
476+
cmp rax, rcx
477+
je .src_is_set
478+
jmp .src_not_impl
479+
480+
.src_is_set:
481+
; Only support PY_EQ (2) and PY_NE (3)
482+
cmp edx, PY_EQ
483+
je .src_eq
484+
cmp edx, PY_NE
485+
je .src_ne
486+
jmp .src_not_impl
487+
488+
.src_eq:
489+
; Check lengths
490+
mov rax, [rdi + PyDictObject.ob_size]
491+
cmp rax, [rsi + PyDictObject.ob_size]
492+
jne .src_false
493+
494+
; Every element of self must be in other
495+
mov rbx, rdi ; self (set)
496+
mov r12, rsi ; other (set)
497+
mov r13, [rbx + PyDictObject.capacity]
498+
xor ecx, ecx ; index
499+
.src_eq_loop:
500+
cmp rcx, r13
501+
jge .src_true
502+
503+
; Get entry at index
504+
imul rax, rcx, SET_ENTRY_SIZE
505+
add rax, [rbx + PyDictObject.entries]
506+
; Check if occupied (key_tag != 0 and != tombstone)
507+
movzx edx, word [rax + SET_ENTRY_KEY_TAG]
508+
test edx, edx
509+
jz .src_eq_next
510+
cmp edx, SET_TOMBSTONE
511+
je .src_eq_next
512+
513+
; Entry is occupied — check if key is in other set
514+
push rcx
515+
mov rdi, r12 ; other set
516+
mov rsi, [rax + SET_ENTRY_KEY] ; key
517+
movzx edx, word [rax + SET_ENTRY_KEY_TAG]
518+
call set_contains
519+
pop rcx
520+
test eax, eax
521+
jz .src_false ; not found → not equal
522+
523+
.src_eq_next:
524+
inc rcx
525+
jmp .src_eq_loop
526+
527+
.src_ne:
528+
; PY_NE = not PY_EQ
529+
push rdi
530+
push rsi
531+
mov edx, PY_EQ
532+
call set_richcompare
533+
pop rsi
534+
pop rdi
535+
; Negate: if bool_true → return bool_false, vice versa
536+
test edx, edx
537+
jz .src_not_impl ; NULL result → propagate
538+
lea rcx, [rel bool_true]
539+
cmp rax, rcx
540+
je .src_false ; EQ was True → NE is False
541+
jmp .src_true ; EQ was False → NE is True
542+
543+
.src_true:
544+
extern bool_true
545+
lea rax, [rel bool_true]
546+
mov edx, TAG_PTR
547+
pop r13
548+
pop r12
549+
pop rbx
550+
leave
551+
ret
552+
553+
.src_false:
554+
extern bool_false
555+
lea rax, [rel bool_false]
556+
mov edx, TAG_PTR
557+
pop r13
558+
pop r12
559+
pop rbx
560+
leave
561+
ret
562+
563+
.src_not_impl:
564+
RET_NULL
565+
pop r13
566+
pop r12
567+
pop rbx
568+
leave
569+
ret
570+
END_FUNC set_richcompare
571+
451572
;; ============================================================================
452573
;; set_contains_sq(self, key) -> int (0/1)
453574
;; sq_contains wrapper for the sequence methods (for "in" operator)
@@ -1054,7 +1175,7 @@ set_type:
10541175
dq set_type_call ; tp_call
10551176
dq 0 ; tp_getattr
10561177
dq 0 ; tp_setattr
1057-
dq 0 ; tp_richcompare
1178+
dq set_richcompare ; tp_richcompare
10581179
dq set_tp_iter ; tp_iter
10591180
dq 0 ; tp_iternext
10601181
dq 0 ; tp_init
@@ -1085,7 +1206,7 @@ frozenset_type:
10851206
dq frozenset_type_call ; tp_call
10861207
dq 0 ; tp_getattr
10871208
dq 0 ; tp_setattr
1088-
dq 0 ; tp_richcompare
1209+
dq set_richcompare ; tp_richcompare
10891210
dq set_tp_iter ; tp_iter (reuse set iter)
10901211
dq 0 ; tp_iternext
10911212
dq 0 ; tp_init

tests/cpython/test_set.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""CPython test_set.py adapted for apython."""
2+
import unittest
3+
4+
class TestSet(unittest.TestCase):
5+
6+
def test_uniquification(self):
7+
s = set([1, 1, 2, 2, 3, 3])
8+
self.assertEqual(sorted(s), [1, 2, 3])
9+
10+
def test_len(self):
11+
self.assertEqual(len(set()), 0)
12+
self.assertEqual(len(set([1, 2, 3])), 3)
13+
self.assertEqual(len({1, 2, 3}), 3)
14+
15+
def test_contains(self):
16+
s = {1, 2, 3}
17+
self.assertIn(1, s)
18+
self.assertIn(2, s)
19+
self.assertIn(3, s)
20+
self.assertNotIn(4, s)
21+
self.assertNotIn('a', s)
22+
23+
def test_union(self):
24+
a = {1, 2, 3}
25+
b = {2, 3, 4}
26+
self.assertEqual(sorted(a | b), [1, 2, 3, 4])
27+
self.assertEqual(sorted(a.union(b)), [1, 2, 3, 4])
28+
# Union with empty
29+
self.assertEqual(a | set(), a)
30+
self.assertEqual(set() | a, a)
31+
32+
def test_intersection(self):
33+
a = {1, 2, 3}
34+
b = {2, 3, 4}
35+
self.assertEqual(sorted(a & b), [2, 3])
36+
self.assertEqual(sorted(a.intersection(b)), [2, 3])
37+
# Intersection with empty
38+
self.assertEqual(a & set(), set())
39+
40+
def test_difference(self):
41+
a = {1, 2, 3}
42+
b = {2, 3, 4}
43+
self.assertEqual(sorted(a - b), [1])
44+
self.assertEqual(sorted(a.difference(b)), [1])
45+
46+
def test_symmetric_difference(self):
47+
a = {1, 2, 3}
48+
b = {2, 3, 4}
49+
self.assertEqual(sorted(a ^ b), [1, 4])
50+
self.assertEqual(sorted(a.symmetric_difference(b)), [1, 4])
51+
52+
def test_isdisjoint(self):
53+
self.assertTrue({1, 2}.isdisjoint({3, 4}))
54+
self.assertFalse({1, 2}.isdisjoint({2, 3}))
55+
self.assertTrue(set().isdisjoint(set()))
56+
57+
def test_issubset(self):
58+
self.assertTrue({1, 2}.issubset({1, 2, 3}))
59+
self.assertTrue(set().issubset({1}))
60+
self.assertFalse({1, 2, 3}.issubset({1, 2}))
61+
self.assertTrue({1, 2}.issubset({1, 2}))
62+
63+
def test_issuperset(self):
64+
self.assertTrue({1, 2, 3}.issuperset({1, 2}))
65+
self.assertTrue({1}.issuperset(set()))
66+
self.assertFalse({1, 2}.issuperset({1, 2, 3}))
67+
self.assertTrue({1, 2}.issuperset({1, 2}))
68+
69+
def test_equality(self):
70+
self.assertEqual(set(), set())
71+
self.assertEqual({1, 2, 3}, {1, 2, 3})
72+
self.assertEqual({1, 2, 3}, {3, 2, 1})
73+
self.assertNotEqual({1, 2}, {1, 2, 3})
74+
self.assertNotEqual({1, 2, 3}, {1, 2})
75+
76+
def test_clear(self):
77+
s = {1, 2, 3}
78+
s.clear()
79+
self.assertEqual(len(s), 0)
80+
self.assertEqual(s, set())
81+
82+
def test_copy(self):
83+
s = {1, 2, 3}
84+
t = s.copy()
85+
self.assertEqual(s, t)
86+
self.assertIsNot(s, t)
87+
88+
def test_add(self):
89+
s = set()
90+
s.add(1)
91+
s.add(2)
92+
s.add(2) # duplicate
93+
self.assertEqual(sorted(s), [1, 2])
94+
95+
def test_remove(self):
96+
s = {1, 2, 3}
97+
s.remove(2)
98+
self.assertEqual(sorted(s), [1, 3])
99+
try:
100+
s.remove(99)
101+
self.fail("Expected KeyError")
102+
except KeyError:
103+
pass
104+
105+
def test_discard(self):
106+
s = {1, 2, 3}
107+
s.discard(2)
108+
self.assertEqual(sorted(s), [1, 3])
109+
s.discard(99) # should not raise
110+
self.assertEqual(sorted(s), [1, 3])
111+
112+
def test_pop(self):
113+
s = {1}
114+
v = s.pop()
115+
self.assertEqual(v, 1)
116+
self.assertEqual(len(s), 0)
117+
try:
118+
s.pop()
119+
self.fail("Expected KeyError")
120+
except KeyError:
121+
pass
122+
123+
def test_set_literal(self):
124+
s = {1, 2, 3}
125+
self.assertEqual(sorted(s), [1, 2, 3])
126+
self.assertIsInstance(s, set)
127+
128+
def test_iteration(self):
129+
s = {1, 2, 3}
130+
result = []
131+
for x in s:
132+
result.append(x)
133+
self.assertEqual(sorted(result), [1, 2, 3])
134+
135+
def test_set_of_strings(self):
136+
s = {'a', 'b', 'c'}
137+
self.assertEqual(sorted(s), ['a', 'b', 'c'])
138+
self.assertIn('a', s)
139+
self.assertNotIn('d', s)
140+
141+
def test_bool(self):
142+
self.assertFalse(set())
143+
self.assertTrue({1})
144+
145+
def test_repr(self):
146+
# Empty set
147+
self.assertEqual(repr(set()), 'set()')
148+
# Single element
149+
self.assertEqual(repr({1}), '{1}')
150+
151+
def test_large_set(self):
152+
s = set()
153+
for i in range(1000):
154+
s.add(i)
155+
self.assertEqual(len(s), 1000)
156+
for i in range(1000):
157+
self.assertIn(i, s)
158+
159+
def test_update(self):
160+
# set.update via |= (update method not available)
161+
s = {1, 2}
162+
s |= {3, 4}
163+
self.assertEqual(sorted(s), [1, 2, 3, 4])
164+
165+
def test_set_from_list(self):
166+
s = set([1, 2, 3, 2, 1])
167+
self.assertEqual(sorted(s), [1, 2, 3])
168+
169+
def test_set_from_string(self):
170+
s = set('abcabc')
171+
self.assertEqual(sorted(s), ['a', 'b', 'c'])
172+
173+
def test_frozenset_basic(self):
174+
fs = frozenset([1, 2, 3])
175+
self.assertEqual(sorted(fs), [1, 2, 3])
176+
self.assertIn(1, fs)
177+
self.assertNotIn(4, fs)
178+
self.assertEqual(len(fs), 3)
179+
180+
def test_frozenset_equality(self):
181+
self.assertEqual(frozenset(), frozenset())
182+
self.assertEqual(frozenset([1, 2]), frozenset([2, 1]))
183+
self.assertNotEqual(frozenset([1]), frozenset([1, 2]))
184+
185+
def test_mixed_set_frozenset_equality(self):
186+
self.assertEqual({1, 2, 3}, frozenset([1, 2, 3]))
187+
self.assertEqual(frozenset([1, 2, 3]), {1, 2, 3})
188+
189+
190+
if __name__ == "__main__":
191+
unittest.main()

0 commit comments

Comments
 (0)