Skip to content

Commit 68dc6a9

Browse files
jgarzikclaude
andcommitted
Fix set_traverse/set_clear_gc using wrong entry stride and layout
set entries are 24-byte (hash+key+key_tag_qword) with SET_ENTRY_SIZE=24, while dict entries are 32-byte (hash+key+value+byte_tags+padding). The old code tail-jumped to dict_traverse/dict_clear_gc which iterate with DICT_ENTRY_SIZE=32 stride and read byte key_tag at offset 24 — completely wrong offsets for set entries after the first slot. Implement dedicated set_traverse and set_clear_gc that use the correct 24-byte stride, qword key_tag checks (0 for empty, 0xDEAD for tombstone), and only visit/decref the key (no value field in sets). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 17824c4 commit 68dc6a9

1 file changed

Lines changed: 82 additions & 7 deletions

File tree

src/gc.asm

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -925,15 +925,90 @@ DEF_FUNC dict_clear_gc
925925
END_FUNC dict_clear_gc
926926

927927
; ---- set_traverse / set_clear ----
928-
; Set uses dict internally — traverse the dict's entries
929-
DEF_FUNC_BARE set_traverse
930-
; rdi = set obj; set uses a dict at [rdi + 16] (SetObject -> dict ptr)
931-
; Actually set objects ARE dicts — same layout as PyDictObject
932-
jmp dict_traverse
928+
; Set entries are 24 bytes (hash+key+key_tag_qword), distinct from DictEntry (32 bytes).
929+
SET_ENTRY_SIZE_GC equ 24
930+
SET_ENTRY_KEY_GC equ 8
931+
SET_ENTRY_KEY_TAG_GC equ 16
932+
SET_TOMBSTONE_GC equ 0xDEAD
933+
934+
DEF_FUNC set_traverse
935+
push rbx
936+
push r12
937+
push r13
938+
939+
mov rbx, rdi
940+
mov r12, [rbx + PyDictObject.entries] ; set reuses PyDictObject layout for header
941+
mov r13, [rbx + PyDictObject.capacity]
942+
test r13, r13
943+
jz .st_done
944+
.st_loop:
945+
dec r13
946+
; Check for empty (key_tag == 0) or tombstone (key_tag == 0xDEAD)
947+
cmp qword [r12 + SET_ENTRY_KEY_TAG_GC], 0
948+
je .st_next
949+
cmp qword [r12 + SET_ENTRY_KEY_TAG_GC], SET_TOMBSTONE_GC
950+
je .st_next
951+
952+
; Visit key
953+
mov rdi, [r12 + SET_ENTRY_KEY_GC]
954+
movzx esi, byte [r12 + SET_ENTRY_KEY_TAG_GC]
955+
VISIT_FAT rdi, rsi
956+
957+
.st_next:
958+
add r12, SET_ENTRY_SIZE_GC
959+
test r13, r13
960+
jnz .st_loop
961+
.st_done:
962+
pop r13
963+
pop r12
964+
pop rbx
965+
leave
966+
ret
933967
END_FUNC set_traverse
934968

935-
DEF_FUNC_BARE set_clear_gc
936-
jmp dict_clear_gc
969+
DEF_FUNC set_clear_gc
970+
push rbx
971+
push r12
972+
push r13
973+
974+
mov rbx, rdi
975+
mov r12, [rbx + PyDictObject.entries]
976+
mov r13, [rbx + PyDictObject.capacity]
977+
978+
test r13, r13
979+
jz .sc_done
980+
.sc_loop:
981+
dec r13
982+
cmp qword [r12 + SET_ENTRY_KEY_TAG_GC], 0
983+
je .sc_next
984+
cmp qword [r12 + SET_ENTRY_KEY_TAG_GC], SET_TOMBSTONE_GC
985+
je .sc_next
986+
987+
; DECREF key
988+
push r12
989+
push r13
990+
mov rdi, [r12 + SET_ENTRY_KEY_GC]
991+
movzx esi, byte [r12 + SET_ENTRY_KEY_TAG_GC]
992+
DECREF_VAL rdi, rsi
993+
pop r13
994+
pop r12
995+
996+
; Clear entry
997+
mov qword [r12 + SET_ENTRY_KEY_TAG_GC], 0
998+
mov qword [r12 + SET_ENTRY_KEY_GC], 0
999+
1000+
.sc_next:
1001+
add r12, SET_ENTRY_SIZE_GC
1002+
test r13, r13
1003+
jnz .sc_loop
1004+
.sc_done:
1005+
mov qword [rbx + PyDictObject.ob_size], 0
1006+
1007+
pop r13
1008+
pop r12
1009+
pop rbx
1010+
leave
1011+
ret
9371012
END_FUNC set_clear_gc
9381013

9391014
; ---- func_traverse / func_clear ----

0 commit comments

Comments
 (0)