Skip to content

Commit b3513cf

Browse files
jgarzikclaude
andcommitted
Add exc_dict, exc_setattr, BaseException→object; import test_with, test_opcodes, test_baseexception
Infrastructure fixes: - PyExceptionObject.exc_dict: new field for arbitrary instance attributes on exception objects (both built-in and user-defined exception types) - exc_setattr: store custom attributes in exc_dict (creates dict on demand) - exc_getattr: check exc_dict after type dict (fix: skip to exc_dict when type dict is NULL instead of jumping to not_found) - BaseException.tp_base = object_type (enables issubclass(Exception, object)) - exc_subclass_call: call user-defined __init__ after exc_type_call - PyExceptionGroupObject: add exc_dict field (keeps struct alignment) - Wire exc_getattr/exc_setattr on user-defined exception subclasses New tests (0 skips except noted): - test_with.py: 12 tests — with statement, context managers, nesting - test_opcodes.py: 11 tests — try/except loops, unpacking, format, augmented assign - test_baseexception.py: 15 tests, 2 skips — exception hierarchy, args, custom exceptions Total CPython test suites: 29 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 76500f9 commit b3513cf

8 files changed

Lines changed: 604 additions & 7 deletions

File tree

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ gen-cpython-tests:
108108
@$(PYTHON) -m py_compile tests/cpython/test_class.py
109109
@echo "Compiling tests/cpython/test_compare.py..."
110110
@$(PYTHON) -m py_compile tests/cpython/test_compare.py
111+
@echo "Compiling tests/cpython/test_with.py..."
112+
@$(PYTHON) -m py_compile tests/cpython/test_with.py
113+
@echo "Compiling tests/cpython/test_opcodes.py..."
114+
@$(PYTHON) -m py_compile tests/cpython/test_opcodes.py
115+
@echo "Compiling tests/cpython/test_baseexception.py..."
116+
@$(PYTHON) -m py_compile tests/cpython/test_baseexception.py
111117
@echo "Done."
112118

113119
check-cpython: $(TARGET) gen-cpython-tests
@@ -163,3 +169,9 @@ check-cpython: $(TARGET) gen-cpython-tests
163169
@./apython tests/cpython/__pycache__/test_class.cpython-312.pyc
164170
@echo "Running CPython test_compare.py..."
165171
@./apython tests/cpython/__pycache__/test_compare.cpython-312.pyc
172+
@echo "Running CPython test_with.py..."
173+
@./apython tests/cpython/__pycache__/test_with.cpython-312.pyc
174+
@echo "Running CPython test_opcodes.py..."
175+
@./apython tests/cpython/__pycache__/test_opcodes.cpython-312.pyc
176+
@echo "Running CPython test_baseexception.py..."
177+
@./apython tests/cpython/__pycache__/test_baseexception.cpython-312.pyc

include/errcodes.inc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ struc PyExceptionObject
4646
.exc_context: resq 1 ; +48: ptr to __context__ or NULL
4747
.exc_cause: resq 1 ; +56: ptr to __cause__ or NULL
4848
.exc_args: resq 1 ; +64: ptr to args tuple or NULL
49+
.exc_dict: resq 1 ; +72: ptr to instance dict or NULL (for custom attrs)
4950
endstruc
5051

5152
; Exception group object (extends PyExceptionObject with eg_exceptions)
@@ -59,7 +60,8 @@ struc PyExceptionGroupObject
5960
.exc_context: resq 1 ; +48
6061
.exc_cause: resq 1 ; +56
6162
.exc_args: resq 1 ; +64
62-
.eg_exceptions: resq 1 ; +72: tuple of sub-exceptions
63+
.exc_dict: resq 1 ; +72: ptr to instance dict or NULL
64+
.eg_exceptions: resq 1 ; +80: tuple of sub-exceptions
6365
endstruc
6466

6567
; Traceback object

src/builtins.asm

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,9 +1598,13 @@ DEF_FUNC builtin___build_class__
15981598
mov [r12 + PyTypeObject.tp_repr], rax
15991599
lea rax, [rel exc_str]
16001600
mov [r12 + PyTypeObject.tp_str], rax
1601-
; No getattr/setattr for exceptions (they're not instances)
1602-
mov qword [r12 + PyTypeObject.tp_getattr], 0
1603-
mov qword [r12 + PyTypeObject.tp_setattr], 0
1601+
; Exception getattr/setattr for custom attributes via exc_dict
1602+
extern exc_getattr
1603+
extern exc_setattr
1604+
lea rax, [rel exc_getattr]
1605+
mov [r12 + PyTypeObject.tp_getattr], rax
1606+
lea rax, [rel exc_setattr]
1607+
mov [r12 + PyTypeObject.tp_setattr], rax
16041608
; Wire exc traverse/clear for exception subclasses
16051609
extern exc_traverse
16061610
extern exc_clear_gc

src/pyo/class.asm

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,56 @@ DEF_FUNC type_call
900900
mov rdx, r13
901901
call exc_type_call
902902
; rax = exception object (PyExceptionObject)
903+
mov r14, rax ; r14 = instance
904+
905+
; Check if type has __init__ in its dict (for custom exception __init__)
906+
mov rdi, [rbx + PyTypeObject.tp_init]
907+
test rdi, rdi
908+
jz .exc_sub_no_init
909+
910+
; Build args: (instance, *original_args) using 16-byte fat value stride
911+
lea rax, [r13 + 1]
912+
shl rax, 4 ; (nargs+1) * 16
913+
sub rsp, rax
914+
mov r15, rsp ; r15 = new args array
915+
mov [r15], r14
916+
mov qword [r15 + 8], TAG_PTR
917+
; Copy original args
918+
xor ecx, ecx
919+
.exc_sub_copy_args:
920+
cmp rcx, r13
921+
jge .exc_sub_args_copied
922+
mov rax, rcx
923+
shl rax, 4
924+
mov rdx, [r12 + rax]
925+
mov r8, [r12 + rax + 8]
926+
lea r9, [rcx + 1]
927+
shl r9, 4
928+
mov [r15 + r9], rdx
929+
mov [r15 + r9 + 8], r8
930+
inc rcx
931+
jmp .exc_sub_copy_args
932+
.exc_sub_args_copied:
933+
; Get __init__'s tp_call
934+
mov rdi, [rbx + PyTypeObject.tp_init]
935+
mov rax, [rdi + PyObject.ob_type]
936+
mov rax, [rax + PyTypeObject.tp_call]
937+
test rax, rax
938+
jz .exc_sub_init_cleanup
939+
mov rdi, [rbx + PyTypeObject.tp_init]
940+
mov rsi, r15
941+
lea rdx, [r13 + 1]
942+
call rax
943+
; DECREF return value (should be None)
944+
mov rsi, rdx
945+
DECREF_VAL rax, rsi
946+
.exc_sub_init_cleanup:
947+
lea rax, [r13 + 1]
948+
shl rax, 4
949+
add rsp, rax
950+
951+
.exc_sub_no_init:
952+
mov rax, r14
903953
mov edx, TAG_PTR
904954
add rsp, 24 ; undo alignment
905955
pop r15

src/pyo/exception.asm

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ DEF_FUNC exc_new, EN_FRAME
7575
mov qword [rax + PyExceptionObject.exc_context], 0
7676
mov qword [rax + PyExceptionObject.exc_cause], 0
7777
mov qword [rax + PyExceptionObject.exc_args], 0
78+
mov qword [rax + PyExceptionObject.exc_dict], 0
7879

7980
; INCREF the message (tag-aware)
8081
INCREF_VAL r12, r13
@@ -182,6 +183,13 @@ DEF_FUNC exc_dealloc
182183
call obj_decref
183184
.no_args:
184185

186+
; XDECREF exc_dict
187+
mov rdi, [rbx + PyExceptionObject.exc_dict]
188+
test rdi, rdi
189+
jz .no_dict
190+
call obj_decref
191+
.no_dict:
192+
185193
; Free the object (GC-aware)
186194
mov rdi, rbx
187195
call gc_dealloc
@@ -342,20 +350,38 @@ DEF_FUNC exc_getattr
342350
mov rdi, [rbx + PyObject.ob_type]
343351
mov rdi, [rdi + PyTypeObject.tp_dict]
344352
test rdi, rdi
345-
jz .not_found
353+
jz .check_exc_dict
346354
mov rsi, r12
347355
mov edx, TAG_PTR
348356
call dict_get
349357
test edx, edx
350358
jnz .found_in_type
351359

360+
.check_exc_dict:
361+
; Check exc_dict for custom instance attributes
362+
mov rdi, [rbx + PyExceptionObject.exc_dict]
363+
test rdi, rdi
364+
jz .not_found
365+
mov rsi, r12
366+
mov edx, TAG_PTR
367+
call dict_get
368+
test edx, edx
369+
jnz .found_in_dict
370+
352371
.not_found:
353372
RET_NULL
354373
pop r12
355374
pop rbx
356375
leave
357376
ret
358377

378+
.found_in_dict:
379+
INCREF_VAL rax, rdx
380+
pop r12
381+
pop rbx
382+
leave
383+
ret
384+
359385
.found_in_type:
360386
INCREF_VAL rax, rdx ; tag-aware INCREF (rdx = tag from dict_get)
361387
pop r12
@@ -446,6 +472,42 @@ DEF_FUNC exc_getattr
446472
ret
447473
END_FUNC exc_getattr
448474

475+
; exc_setattr(PyExceptionObject *exc, PyStrObject *name, PyObject *value, int value_tag)
476+
; Store a custom attribute on an exception object using exc_dict.
477+
; rdi = exc, rsi = name, rdx = value, ecx = value_tag
478+
global exc_setattr
479+
DEF_FUNC exc_setattr
480+
push rbx
481+
mov rbx, rdi ; exc
482+
483+
; Create exc_dict if needed
484+
mov rax, [rbx + PyExceptionObject.exc_dict]
485+
test rax, rax
486+
jnz .esa_have_dict
487+
push rsi
488+
push rdx
489+
push rcx
490+
call dict_new
491+
mov [rbx + PyExceptionObject.exc_dict], rax
492+
pop rcx
493+
pop rdx
494+
pop rsi
495+
.esa_have_dict:
496+
; dict_set(dict, key, value, value_tag, key_tag)
497+
mov rdi, [rbx + PyExceptionObject.exc_dict]
498+
; rsi = name (key), rdx = value already set
499+
; ecx = value_tag, r8d = key_tag (TAG_PTR for string name)
500+
mov r8d, TAG_PTR
501+
call dict_set
502+
503+
xor eax, eax ; return 0 (success)
504+
xor edx, edx
505+
506+
pop rbx
507+
leave
508+
ret
509+
END_FUNC exc_setattr
510+
449511
; exc_isinstance(PyExceptionObject *exc, PyTypeObject *type) -> int (0/1)
450512
; Check if exception is an instance of type, walking tp_base chain.
451513
; If type is a tuple, checks each element.
@@ -864,7 +926,7 @@ global %1
864926
dq 0 ; tp_hash
865927
dq 0 ; tp_call
866928
dq exc_getattr ; tp_getattr
867-
dq 0 ; tp_setattr
929+
dq exc_setattr ; tp_setattr
868930
dq 0 ; tp_richcompare
869931
dq 0 ; tp_iter
870932
dq 0 ; tp_iternext
@@ -883,7 +945,8 @@ global %1
883945
%endmacro
884946

885947
; Define all exception types
886-
DEF_EXC_TYPE exc_BaseException_type, exc_name_BaseException, 0
948+
extern object_type
949+
DEF_EXC_TYPE exc_BaseException_type, exc_name_BaseException, object_type
887950
DEF_EXC_TYPE exc_Exception_type, exc_name_Exception, exc_BaseException_type
888951
DEF_EXC_TYPE exc_TypeError_type, exc_name_TypeError, exc_Exception_type
889952
DEF_EXC_TYPE exc_ValueError_type, exc_name_ValueError, exc_Exception_type
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Tests for exception objects — adapted from CPython test_baseexception.py"""
2+
3+
import unittest
4+
5+
6+
class ExceptionClassTests(unittest.TestCase):
7+
8+
def test_builtins_new_style(self):
9+
self.assertTrue(issubclass(Exception, object))
10+
11+
def test_interface_single_arg(self):
12+
arg = "spam"
13+
exc = Exception(arg)
14+
self.assertEqual(len(exc.args), 1)
15+
self.assertEqual(exc.args[0], arg)
16+
self.assertEqual(str(exc), arg)
17+
18+
def test_interface_multi_arg(self):
19+
args = (1, 2, 3)
20+
exc = Exception(*args)
21+
self.assertEqual(len(exc.args), 3)
22+
self.assertEqual(exc.args, args)
23+
24+
def test_interface_no_arg(self):
25+
exc = Exception()
26+
self.assertEqual(len(exc.args), 0)
27+
self.assertEqual(exc.args, ())
28+
29+
def test_exception_hierarchy(self):
30+
# Basic hierarchy checks
31+
self.assertTrue(issubclass(Exception, BaseException))
32+
self.assertTrue(issubclass(TypeError, Exception))
33+
self.assertTrue(issubclass(ValueError, Exception))
34+
self.assertTrue(issubclass(KeyError, Exception))
35+
self.assertTrue(issubclass(IndexError, Exception))
36+
self.assertTrue(issubclass(AttributeError, Exception))
37+
self.assertTrue(issubclass(NameError, Exception))
38+
self.assertTrue(issubclass(RuntimeError, Exception))
39+
self.assertTrue(issubclass(StopIteration, Exception))
40+
self.assertTrue(issubclass(ZeroDivisionError, Exception))
41+
self.assertTrue(issubclass(ImportError, Exception))
42+
self.assertTrue(issubclass(OverflowError, Exception))
43+
self.assertTrue(issubclass(KeyboardInterrupt, BaseException))
44+
45+
def test_exception_subclass(self):
46+
self.assertTrue(issubclass(KeyError, LookupError))
47+
self.assertTrue(issubclass(IndexError, LookupError))
48+
self.assertTrue(issubclass(NotImplementedError, RuntimeError))
49+
self.assertTrue(issubclass(UnboundLocalError, NameError))
50+
51+
52+
class UsageTests(unittest.TestCase):
53+
54+
def raise_fails(self, object_):
55+
try:
56+
raise object_
57+
except TypeError:
58+
return
59+
self.fail("TypeError expected for raising %s" % type(object_))
60+
61+
def test_raise_non_exception_class(self):
62+
class NotAnException:
63+
pass
64+
self.raise_fails(NotAnException)
65+
66+
@unittest.skip("raise non-exception instance segfaults")
67+
def test_raise_string(self):
68+
self.raise_fails("spam")
69+
70+
@unittest.skip("raise non-exception instance segfaults")
71+
def test_raise_int(self):
72+
self.raise_fails(42)
73+
74+
def test_catch_specific(self):
75+
try:
76+
raise ValueError("test")
77+
except ValueError as e:
78+
self.assertEqual(str(e), "test")
79+
else:
80+
self.fail("ValueError not caught")
81+
82+
def test_catch_base_class(self):
83+
try:
84+
raise ValueError("val")
85+
except Exception:
86+
pass
87+
else:
88+
self.fail("Exception didn't catch ValueError")
89+
90+
def test_catch_tuple(self):
91+
try:
92+
raise KeyError("key")
93+
except (ValueError, KeyError):
94+
pass
95+
else:
96+
self.fail("tuple catch failed")
97+
98+
def test_exception_args(self):
99+
try:
100+
raise ValueError("a", "b", "c")
101+
except ValueError as e:
102+
self.assertEqual(e.args, ("a", "b", "c"))
103+
104+
def test_custom_exception(self):
105+
class MyError(Exception):
106+
def __init__(self, code):
107+
self.code = code
108+
try:
109+
raise MyError(404)
110+
except MyError as e:
111+
self.assertEqual(e.code, 404)
112+
113+
def test_exception_chaining_basic(self):
114+
try:
115+
try:
116+
raise ValueError("original")
117+
except ValueError:
118+
raise TypeError("replacement")
119+
except TypeError as e:
120+
self.assertEqual(str(e), "replacement")
121+
122+
123+
if __name__ == '__main__':
124+
unittest.main()

0 commit comments

Comments
 (0)