From 4405845eec6eb6c28209026ad73c79c148a34411 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Tue, 17 Mar 2026 23:51:35 +0000 Subject: [PATCH 01/16] Fix isinstance/issubclass validation, pow overflow, tp_base default; add test_unary + test_pow Track A fixes (6 un-skips): - isinstance() arg2 validation: raises TypeError for non-type args - issubclass() arg1/arg2 validation: raises TypeError for non-class args - issubclass() tuple arg support: issubclass(cls, (type1, type2)) - Accept type_type, user_type_metatype, and exc_metatype as valid metatypes - list.__setitem__ with string key: raise TypeError instead of segfault - Default tp_base to object_type for classes without explicit base Track B (2 new CPython tests, 0 skips): - test_unary.py: unary +, -, ~ operators (4 tests) - test_pow.py: pow() and ** operator (9 tests) Infrastructure fixes: - builtin pow() delegates to int_power for GMP overflow correctness - 3-arg pow() negative modulus: Python-compatible remainder adjustment - 3-arg pow() exp=0: apply final modulus (pow(x,0,mod) = 1 % mod) - assertAlmostEqual/assertNotAlmostEqual added to unittest Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 8 ++ lib/test/list_tests.py | 8 +- lib/unittest/case.py | 22 ++++++ src/builtins.asm | 125 +++++++++++++++++++++++++++++-- src/builtins_extra.asm | 91 ++++++++-------------- src/pyo/list.asm | 10 +++ tests/cpython/test_isinstance.py | 16 ++-- tests/cpython/test_pow.py | 68 +++++++++++++++++ tests/cpython/test_unary.py | 34 +++++++++ 9 files changed, 306 insertions(+), 76 deletions(-) create mode 100644 tests/cpython/test_pow.py create mode 100644 tests/cpython/test_unary.py diff --git a/Makefile b/Makefile index e4e9310..2d448a4 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,10 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_scope.py @echo "Compiling tests/cpython/test_generators.py..." @$(PYTHON) -m py_compile tests/cpython/test_generators.py + @echo "Compiling tests/cpython/test_unary.py..." + @$(PYTHON) -m py_compile tests/cpython/test_unary.py + @echo "Compiling tests/cpython/test_pow.py..." + @$(PYTHON) -m py_compile tests/cpython/test_pow.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -127,3 +131,7 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_scope.cpython-312.pyc @echo "Running CPython test_generators.py..." @./apython tests/cpython/__pycache__/test_generators.cpython-312.pyc + @echo "Running CPython test_unary.py..." + @./apython tests/cpython/__pycache__/test_unary.cpython-312.pyc + @echo "Running CPython test_pow.py..." + @./apython tests/cpython/__pycache__/test_pow.cpython-312.pyc diff --git a/lib/test/list_tests.py b/lib/test/list_tests.py index e61e7bd..018ed0b 100644 --- a/lib/test/list_tests.py +++ b/lib/test/list_tests.py @@ -31,13 +31,13 @@ def test_init(self): self.assertNotEqual(id(a), id(b)) self.assertEqual(a, b) - @unittest.skip("string index dunder crashes") def test_getitem_error(self): - pass + a = [] + self.assertRaises(TypeError, a.__getitem__, 'a') - @unittest.skip("string index dunder crashes") def test_setitem_error(self): - pass + a = [] + self.assertRaises(TypeError, a.__setitem__, 'a', "python") def test_repr(self): l0 = [] diff --git a/lib/unittest/case.py b/lib/unittest/case.py index 842a1a7..ba27333 100644 --- a/lib/unittest/case.py +++ b/lib/unittest/case.py @@ -141,6 +141,28 @@ def assertNotEqual(self, first, second, msg=None): raise AssertionError(msg) raise AssertionError("%r == %r" % (first, second)) + def assertAlmostEqual(self, first, second, places=7, msg=None): + if first == second: + return + diff = abs(first - second) + if diff <= 10 ** (-places): + return + if msg: + raise AssertionError(msg) + raise AssertionError("%r != %r within %d places" % (first, second, places)) + + def assertNotAlmostEqual(self, first, second, places=7, msg=None): + if first == second: + if msg: + raise AssertionError(msg) + raise AssertionError("%r == %r within %d places" % (first, second, places)) + diff = abs(first - second) + if diff > 10 ** (-places): + return + if msg: + raise AssertionError(msg) + raise AssertionError("%r == %r within %d places" % (first, second, places)) + def assertTrue(self, expr, msg=None): if not expr: raise AssertionError(msg or "%r is not true" % (expr,)) diff --git a/src/builtins.asm b/src/builtins.asm index 256b2c9..a9683ab 100644 --- a/src/builtins.asm +++ b/src/builtins.asm @@ -914,14 +914,26 @@ DEF_FUNC builtin_isinstance .isinstance_got_type: ; rdx = obj's type, rcx = type_to_check (may be tuple) - ; Check if type_to_check is a tuple (must be TAG_PTR) + ; Second arg must be TAG_PTR (type or tuple) cmp r9d, TAG_PTR - jne .isinstance_check ; non-pointer → single type (False result) + jne .isinstance_type_error mov rax, [rcx + PyObject.ob_type] extern tuple_type lea r8, [rel tuple_type] cmp rax, r8 je .isinstance_tuple + ; Validate it's a type (ob_type == type_type, user_type_metatype, or exc_metatype) + lea r8, [rel type_type] + cmp rax, r8 + je .isinstance_check + extern user_type_metatype + lea r8, [rel user_type_metatype] + cmp rax, r8 + je .isinstance_check + extern exc_metatype + lea r8, [rel exc_metatype] + cmp rax, r8 + jne .isinstance_type_error .isinstance_check: ; Walk the full type chain: rdx = current type, rcx = target type @@ -981,6 +993,11 @@ DEF_FUNC builtin_isinstance leave ret +.isinstance_type_error: + lea rdi, [rel exc_TypeError_type] + CSTRING rsi, "isinstance() arg 2 must be a type, a tuple of types, or a union" + call raise_exception + .isinstance_error: lea rdi, [rel exc_TypeError_type] CSTRING rsi, "isinstance() takes 2 arguments" @@ -991,24 +1008,100 @@ END_FUNC builtin_isinstance ;; builtin_issubclass(PyObject **args, int64_t nargs) -> PyObject* ;; issubclass(cls, parent) -> True/False ;; Walks the full tp_base chain for inheritance. +;; Supports tuple second arg: issubclass(cls, (type1, type2, ...)) ;; ============================================================================ DEF_FUNC builtin_issubclass + push rbx + push r12 + push r13 cmp rsi, 2 jne .issubclass_error - mov rdx, [rdi] ; rdx = args[0] = cls (child type) - mov rcx, [rdi + 16] ; rcx = args[1] = parent type + mov rdx, [rdi] ; rdx = args[0] = cls payload + mov r8d, [rdi + 8] ; r8d = args[0] tag + mov rcx, [rdi + 16] ; rcx = args[1] = parent payload + mov r9d, [rdi + 24] ; r9d = args[1] tag -.issubclass_check: + ; Validate first arg is a type (TAG_PTR with recognized metatype) + cmp r8d, TAG_PTR + jne .issubclass_arg1_error + mov rax, [rdx + PyObject.ob_type] + lea r10, [rel type_type] + cmp rax, r10 + je .issubclass_arg1_ok + extern user_type_metatype + lea r10, [rel user_type_metatype] + cmp rax, r10 + je .issubclass_arg1_ok + extern exc_metatype + lea r10, [rel exc_metatype] + cmp rax, r10 + jne .issubclass_arg1_error +.issubclass_arg1_ok: + + ; Check if second arg is a tuple + cmp r9d, TAG_PTR + jne .issubclass_arg2_error + mov rax, [rcx + PyObject.ob_type] + lea r10, [rel tuple_type] + cmp rax, r10 + je .issubclass_tuple + ; Validate second arg is a type (recognized metatype) + lea r10, [rel type_type] + cmp rax, r10 + je .issubclass_walk + lea r10, [rel user_type_metatype] + cmp rax, r10 + je .issubclass_walk + lea r10, [rel exc_metatype] + cmp rax, r10 + jne .issubclass_arg2_error + + ; Single type check: walk rdx -> tp_base chain looking for rcx +.issubclass_walk: cmp rdx, rcx je .issubclass_true mov rdx, [rdx + PyTypeObject.tp_base] test rdx, rdx - jnz .issubclass_check + jnz .issubclass_walk + jmp .issubclass_false +.issubclass_tuple: + ; rcx = tuple of types. Check cls against each. + mov rbx, rcx ; rbx = tuple + mov r12, rdx ; r12 = cls (saved) + mov rsi, [rbx + PyTupleObject.ob_item] ; payloads array + mov r13, [rbx + PyTupleObject.ob_size] ; count + xor r8d, r8d ; index +.issubclass_tuple_loop: + cmp r8, r13 + jge .issubclass_false + mov rdx, r12 ; reset to cls + mov rcx, [rsi + r8*8] ; type from tuple + push rsi + push r8 +.issubclass_tuple_walk: + cmp rdx, rcx + je .issubclass_tuple_match + mov rdx, [rdx + PyTypeObject.tp_base] + test rdx, rdx + jnz .issubclass_tuple_walk + pop r8 + pop rsi + inc r8 + jmp .issubclass_tuple_loop + +.issubclass_tuple_match: + add rsp, 16 ; pop saved r8, rsi + jmp .issubclass_true + +.issubclass_false: lea rax, [rel bool_false] inc qword [rax + PyObject.ob_refcnt] + pop r13 + pop r12 + pop rbx mov edx, TAG_PTR leave ret @@ -1016,10 +1109,23 @@ DEF_FUNC builtin_issubclass .issubclass_true: lea rax, [rel bool_true] inc qword [rax + PyObject.ob_refcnt] + pop r13 + pop r12 + pop rbx mov edx, TAG_PTR leave ret +.issubclass_arg1_error: + lea rdi, [rel exc_TypeError_type] + CSTRING rsi, "issubclass() arg 1 must be a class" + call raise_exception + +.issubclass_arg2_error: + lea rdi, [rel exc_TypeError_type] + CSTRING rsi, "issubclass() arg 2 must be a class, a tuple of classes, or a union" + call raise_exception + .issubclass_error: lea rdi, [rel exc_TypeError_type] CSTRING rsi, "issubclass() takes 2 arguments" @@ -1457,10 +1563,13 @@ DEF_FUNC builtin___build_class__ ; Store tp_init (func ptr or 0) mov [r12 + PyTypeObject.tp_init], rbx - ; Set tp_base if we have a base class + ; Set tp_base: use explicit base class, or default to object_type mov rax, [rbp-48] test rax, rax - jz .bc_no_set_base + jnz .bc_have_base + lea rax, [rel object_type] + mov [rbp-48], rax ; update saved base for later use +.bc_have_base: mov [r12 + PyTypeObject.tp_base], rax mov rdi, rax call obj_incref diff --git a/src/builtins_extra.asm b/src/builtins_extra.asm index 222cbaa..a322f4d 100644 --- a/src/builtins_extra.asm +++ b/src/builtins_extra.asm @@ -3063,71 +3063,26 @@ DEF_FUNC builtin_pow_fn, POW_FRAME jmp .pow_error .pow_two: - ; pow(base, exp) + ; pow(base, exp) — extract operands and delegate to int_power/float path mov rax, [rdi] ; base payload mov ecx, [rdi + 8] ; base tag mov rbx, [rdi + 16] ; exp payload mov r8d, [rdi + 24] ; exp tag - ; Both SmallInt? + ; Both SmallInt? Delegate to int_power (handles GMP overflow) cmp ecx, TAG_SMALLINT jne .pow_two_float cmp r8d, TAG_SMALLINT jne .pow_two_float - ; int ** int - test rbx, rbx - js .pow_neg_exp ; negative exp → float result - - ; Non-negative int exponent: repeated squaring - mov r12, rax ; base - mov r13, rbx ; exp - mov rax, 1 ; result = 1 -.pow_sq_loop: - test r13, r13 - jz .pow_sq_done - test r13, 1 - jz .pow_sq_even - imul rax, r12 ; result *= base -.pow_sq_even: - imul r12, r12 ; base *= base - shr r13, 1 - jmp .pow_sq_loop -.pow_sq_done: - RET_TAG_SMALLINT - pop r13 - pop r12 - pop rbx - leave - ret - -.pow_neg_exp: - ; int ** negative_int → float - cvtsi2sd xmm0, rax ; base as double - cvtsi2sd xmm1, rbx ; exp as double (negative) - ; Use repeated multiplication for positive abs(exp), then invert - neg rbx - mov r12, 1 ; result = 1 (integer) - mov r13, rbx - cvtsi2sd xmm0, rax ; base - movsd xmm2, xmm0 ; save base - movq xmm0, [rel const_one] ; result = 1.0 -.pow_neg_loop: - test r13, r13 - jz .pow_neg_done - test r13, 1 - jz .pow_neg_even - mulsd xmm0, xmm2 ; result *= base -.pow_neg_even: - mulsd xmm2, xmm2 ; base *= base - shr r13, 1 - jmp .pow_neg_loop -.pow_neg_done: - ; xmm0 = base^|exp|, invert - movq xmm1, [rel const_one] - divsd xmm1, xmm0 ; 1.0 / base^|exp| - movq rax, xmm1 - mov edx, TAG_FLOAT + ; int ** int — call int_power(base, exp, base_tag, exp_tag) + extern int_power + mov rdi, rax ; base payload + mov rsi, rbx ; exp payload + mov edx, ecx ; base tag (TAG_SMALLINT) + mov ecx, r8d ; exp tag (TAG_SMALLINT) + call int_power + ; rax = result payload, edx = result tag pop r13 pop r12 pop rbx @@ -3294,10 +3249,13 @@ DEF_FUNC builtin_pow_fn, POW_FRAME cqo idiv r12 ; rax=quot, rdx=rem mov rax, rdx ; base = base % mod - ; Handle negative remainder + ; Adjust remainder to match Python semantics (sign of mod) test rax, rax - jns .pow_mod_pos - add rax, r12 + jz .pow_mod_pos + mov rdx, rax + xor rdx, r12 + jns .pow_mod_pos ; same sign → OK + add rax, r12 ; different signs → adjust .pow_mod_pos: mov rcx, 1 ; result = 1 .pow_mod_loop: @@ -3313,6 +3271,9 @@ DEF_FUNC builtin_pow_fn, POW_FRAME idiv r12 mov rcx, rdx test rcx, rcx + jz .pow_mod_pos2 + mov rdx, rcx + xor rdx, r12 jns .pow_mod_pos2 add rcx, r12 .pow_mod_pos2: @@ -3325,6 +3286,9 @@ DEF_FUNC builtin_pow_fn, POW_FRAME idiv r12 mov rax, rdx test rax, rax + jz .pow_mod_pos3 + mov rdx, rax + xor rdx, r12 jns .pow_mod_pos3 add rax, r12 .pow_mod_pos3: @@ -3332,7 +3296,18 @@ DEF_FUNC builtin_pow_fn, POW_FRAME shr r13, 1 jmp .pow_mod_loop .pow_mod_done: + ; Apply final result % mod (needed for exp=0 case: pow(x,0,mod) = 1 % mod) mov rax, rcx + cqo + idiv r12 + mov rax, rdx + test rax, rax + jz .pow_mod_final + mov rdx, rax + xor rdx, r12 + jns .pow_mod_final + add rax, r12 +.pow_mod_final: RET_TAG_SMALLINT pop r13 pop r12 diff --git a/src/pyo/list.asm b/src/pyo/list.asm index 426be8f..34b013a 100644 --- a/src/pyo/list.asm +++ b/src/pyo/list.asm @@ -408,6 +408,11 @@ DEF_FUNC list_ass_subscript, LAS_FRAME lea rcx, [rel slice_type] cmp rax, rcx je .las_slice + ; Validate key is a heap int before converting + extern int_type + lea rcx, [rel int_type] + cmp rax, rcx + jne .las_key_type_error .las_int: ; Convert key to i64 @@ -1045,6 +1050,11 @@ DEF_FUNC list_ass_subscript, LAS_FRAME CSTRING rsi, "attempt to assign sequence of wrong size to extended slice" call raise_exception +.las_key_type_error: + lea rdi, [rel exc_TypeError_type] + CSTRING rsi, "list indices must be integers or slices" + call raise_exception + .las_type_error: extern exc_TypeError_type add rsp, 8 diff --git a/tests/cpython/test_isinstance.py b/tests/cpython/test_isinstance.py index 690a670..1b77242 100644 --- a/tests/cpython/test_isinstance.py +++ b/tests/cpython/test_isinstance.py @@ -56,9 +56,12 @@ def test_subclass_builtin(self): self.assertFalse(issubclass(int, str)) self.assertFalse(issubclass(str, int)) - @unittest.skip("issubclass with tuple arg always returns False") def test_subclass_tuple(self): - pass + self.assertTrue(issubclass(bool, (int, str))) + self.assertTrue(issubclass(str, (int, str))) + self.assertFalse(issubclass(list, (int, str))) + self.assertTrue(issubclass(Child, (Super, int))) + self.assertFalse(issubclass(Super, (Child, str))) def test_isinstance_with_custom_class(self): class A: @@ -89,13 +92,14 @@ class C(B): self.assertFalse(issubclass(A, B)) self.assertFalse(issubclass(A, C)) - @unittest.skip("isinstance(1,1) doesn't raise TypeError yet") def test_isinstance_errors(self): - pass + self.assertRaises(TypeError, isinstance, 1, 1) + self.assertRaises(TypeError, isinstance, 1, "not_a_type") - @unittest.skip("issubclass(1,int) segfaults") def test_issubclass_errors(self): - pass + self.assertRaises(TypeError, issubclass, 1, int) + self.assertRaises(TypeError, issubclass, int, 1) + self.assertRaises(TypeError, issubclass, 1, 1) if __name__ == "__main__": diff --git a/tests/cpython/test_pow.py b/tests/cpython/test_pow.py new file mode 100644 index 0000000..50dc9d7 --- /dev/null +++ b/tests/cpython/test_pow.py @@ -0,0 +1,68 @@ +"""Tests for pow() and ** operator — adapted from CPython test_pow.py""" + +import unittest + +class PowTest(unittest.TestCase): + + def test_powint_basic(self): + """pow(int, 0) == 1 and pow(int, 1) == int""" + for i in range(-100, 100): + self.assertEqual(pow(i, 0), 1) + self.assertEqual(pow(i, 1), i) + self.assertEqual(pow(0, 1), 0) + self.assertEqual(pow(1, 1), 1) + + def test_powint_cube(self): + for i in range(-100, 100): + self.assertEqual(pow(i, 3), i*i*i) + + def test_powint_powers_of_two(self): + pow2 = 1 + for i in range(0, 31): + self.assertEqual(pow(2, i), pow2) + if i != 30: + pow2 = pow2 * 2 + + def test_pow_operator(self): + self.assertEqual(2 ** 10, 1024) + self.assertEqual((-2) ** 3, -8) + self.assertEqual((-2) ** 4, 16) + self.assertEqual(3 ** 0, 1) + self.assertEqual(0 ** 0, 1) + + def test_pow_negation_precedence(self): + self.assertEqual(-2 ** 3, -8) + self.assertEqual((-2) ** 3, -8) + self.assertEqual(-2 ** 4, -16) + self.assertEqual((-2) ** 4, 16) + + def test_pow_three_arg(self): + """3-argument pow (modular exponentiation)""" + self.assertEqual(pow(3, 3, 8), pow(3, 3) % 8) + self.assertEqual(pow(3, 3, -8), pow(3, 3) % -8) + self.assertEqual(pow(3, 2, -2), pow(3, 2) % -2) + self.assertEqual(pow(-3, 3, 8), pow(-3, 3) % 8) + self.assertEqual(pow(-3, 3, -8), pow(-3, 3) % -8) + self.assertEqual(pow(5, 2, -8), pow(5, 2) % -8) + + def test_pow_three_arg_systematic(self): + for i in range(-10, 11): + for j in range(0, 6): + for k in range(-7, 11): + if j >= 0 and k != 0: + self.assertEqual( + pow(i, j) % k, + pow(i, j, k) + ) + + def test_pow_float(self): + self.assertAlmostEqual(pow(2.0, 3.0), 8.0) + self.assertAlmostEqual(pow(0.5, 2.0), 0.25) + self.assertEqual(pow(1.0, 100), 1.0) + + def test_pow_big(self): + self.assertEqual(pow(2, 100), 1 << 100) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_unary.py b/tests/cpython/test_unary.py new file mode 100644 index 0000000..1624f9e --- /dev/null +++ b/tests/cpython/test_unary.py @@ -0,0 +1,34 @@ +"""Test compiler changes for unary ops (+, -, ~) introduced in Python 2.2""" + +import unittest + +class UnaryOpTestCase(unittest.TestCase): + + def test_negative(self): + self.assertTrue(-2 == 0 - 2) + self.assertEqual(-0, 0) + self.assertEqual(--2, 2) + self.assertTrue(-2.0 == 0 - 2.0) + + def test_positive(self): + self.assertEqual(+2, 2) + self.assertEqual(+0, 0) + self.assertEqual(++2, 2) + self.assertEqual(+2.0, 2.0) + + def test_invert(self): + self.assertTrue(~2 == -(2+1)) + self.assertEqual(~0, -1) + self.assertEqual(~~2, 2) + + def test_negation_of_exponentiation(self): + # Make sure '**' does the right thing; these form a + # regression test for SourceForge bug #456756. + self.assertEqual(-2 ** 3, -8) + self.assertEqual((-2) ** 3, -8) + self.assertEqual(-2 ** 4, -16) + self.assertEqual((-2) ** 4, 16) + + +if __name__ == "__main__": + unittest.main() From 5fe3c97ad077af4ced5e5a8a62aa86a305c93169 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 00:02:22 +0000 Subject: [PATCH 02/16] Add contains iteration fallback, str_contains validation; import test_contains Fixes: - CONTAINS_OP tp_iter fallback: iterate via tp_iter when sq_contains and __contains__ dunder are unavailable (enables 'in' for range, etc.) - CONTAINS_OP __getitem__ fallback: iterate via __getitem__(0,1,2,...) catching IndexError (enables 'in' for classes with only __getitem__) - str_contains: validate substr is a string before accessing data (fixes segfault on `None in 'abc'`) New test (0 skips): - test_contains.py: 'in' operator for custom classes, builtins, strings (4 tests) Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 4 + src/opcodes_build.asm | 283 ++++++++++++++++++++++++++++++++- src/pyo/str.asm | 17 +- tests/cpython/test_contains.py | 95 +++++++++++ 4 files changed, 396 insertions(+), 3 deletions(-) create mode 100644 tests/cpython/test_contains.py diff --git a/Makefile b/Makefile index 2d448a4..5a0e1ca 100644 --- a/Makefile +++ b/Makefile @@ -94,6 +94,8 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_unary.py @echo "Compiling tests/cpython/test_pow.py..." @$(PYTHON) -m py_compile tests/cpython/test_pow.py + @echo "Compiling tests/cpython/test_contains.py..." + @$(PYTHON) -m py_compile tests/cpython/test_contains.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -135,3 +137,5 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_unary.cpython-312.pyc @echo "Running CPython test_pow.py..." @./apython tests/cpython/__pycache__/test_pow.cpython-312.pyc + @echo "Running CPython test_contains.py..." + @./apython tests/cpython/__pycache__/test_contains.cpython-312.pyc diff --git a/src/opcodes_build.asm b/src/opcodes_build.asm index a7c9b8c..7ce45c7 100644 --- a/src/opcodes_build.asm +++ b/src/opcodes_build.asm @@ -1264,7 +1264,7 @@ DEF_FUNC_BARE op_contains_op mov rax, [rdi + PyObject.ob_type] mov rdx, [rax + PyTypeObject.tp_flags] test rdx, TYPE_FLAG_HEAPTYPE - jz .contains_type_error + jz .contains_iter_fallback extern dunder_contains extern dunder_call_2 @@ -1274,7 +1274,7 @@ DEF_FUNC_BARE op_contains_op mov rcx, [rsp+24] ; value tag = other_tag call dunder_call_2 test edx, edx ; TAG_NULL = not found - jz .contains_type_error + jz .contains_iter_fallback ; Convert result to boolean (obj_is_true) push rdx ; save tag @@ -1306,6 +1306,285 @@ DEF_FUNC_BARE op_contains_op lea rax, [rel bool_true] jmp .contains_push +.contains_iter_fallback: + ; Fallback: iterate container via tp_iter, compare each element + mov rdi, [rsp + CN_RIGHT] ; container + cmp qword [rsp + CN_RTAG], TAG_PTR + jne .contains_type_error + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_iter] + test rax, rax + jz .contains_getitem_fallback + call rax ; tp_iter(container) → iterator + test rax, rax + jz .contains_getitem_fallback + push rax ; save iterator (+8 shift) + +.contains_iter_loop: + ; Call tp_iternext(iterator) → (rax=payload, edx=tag) or (0, TAG_NULL) + mov rdi, [rsp] ; iterator + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_iternext] + call rax + test edx, edx + jz .contains_iter_not_found ; TAG_NULL = exhausted + + ; Identity check: payload and tag both match → found + ; +8 for iterator push on stack + cmp rax, [rsp + 8 + CN_LEFT] + jne .contains_iter_try_eq + cmp edx, [rsp + 8 + CN_LTAG] + je .contains_iter_found_decref + +.contains_iter_try_eq: + ; Both SmallInt → direct value compare + push rax ; save elem payload + push rdx ; save elem tag + mov r8d, edx ; elem tag + mov ecx, [rsp + 16 + 8 + CN_LTAG] ; value tag + cmp r8d, TAG_SMALLINT + jne .contains_iter_slow_eq + cmp ecx, TAG_SMALLINT + jne .contains_iter_slow_eq + ; Both SmallInt + cmp rax, [rsp + 16 + 8 + CN_LEFT] + pop rdx + pop rax + je .contains_iter_found_decref_elem + jmp .contains_iter_loop + +.contains_iter_slow_eq: + ; Use tp_richcompare for equality + ; rdi = elem (already on stack[+8]), rsi = value, edx = PY_EQ, rcx = elem_tag, r8 = value_tag + mov rdi, [rsp + 8] ; elem payload + mov rsi, [rsp + 16 + 8 + CN_LEFT] ; value payload + mov edx, 2 ; PY_EQ + mov ecx, [rsp] ; elem tag + mov r8d, [rsp + 16 + 8 + CN_LTAG] ; value tag + ; Resolve element type + cmp ecx, TAG_PTR + jne .contains_iter_skip_eq ; for non-PTR non-SmallInt, skip (identity only) + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_richcompare] + test rax, rax + jz .contains_iter_skip_eq + call rax ; tp_richcompare(left, right, PY_EQ, left_tag, right_tag) + ; Result: (rax=payload, edx=tag). Check if True + push rax + push rdx + mov rdi, rax + mov rsi, rdx + extern obj_is_true + call obj_is_true + mov r8d, eax + pop rsi ; result tag + pop rdi ; result payload + DECREF_VAL rdi, rsi + pop rdx ; elem tag + pop rax ; elem payload + test r8d, r8d + jnz .contains_iter_found_decref_elem + jmp .contains_iter_loop + +.contains_iter_skip_eq: + pop rdx + pop rax + jmp .contains_iter_loop + +.contains_iter_found_decref_elem: + ; DECREF element if needed + mov rdi, rax + mov rsi, rdx + DECREF_VAL rdi, rsi + jmp .contains_iter_found + +.contains_iter_found_decref: + ; Element matched by identity, DECREF it + mov rdi, rax + mov rsi, rdx + DECREF_VAL rdi, rsi + +.contains_iter_found: + ; DECREF iterator + pop rdi ; iterator + call obj_decref + mov eax, 1 ; found + jmp .contains_iter_result + +.contains_iter_not_found: + ; DECREF iterator + pop rdi ; iterator + call obj_decref + xor eax, eax ; not found + +.contains_iter_result: + ; DECREF operands (tag-aware) + push rax + mov rdi, [rsp + 8 + CN_RIGHT] + mov rsi, [rsp + 8 + CN_RTAG] + DECREF_VAL rdi, rsi + mov rdi, [rsp + 8 + CN_LEFT] + mov rsi, [rsp + 8 + CN_LTAG] + DECREF_VAL rdi, rsi + pop rax + add rsp, CN_SIZE - 8 ; discard payloads + tags + pop rcx ; invert + xor eax, ecx + test eax, eax + jz .contains_false + lea rax, [rel bool_true] + jmp .contains_push + +.contains_getitem_fallback: + ; Fallback: iterate via __getitem__(0), __getitem__(1), ... until IndexError + ; Check if container is a heaptype with __getitem__ + mov rdi, [rsp + CN_RIGHT] + cmp qword [rsp + CN_RTAG], TAG_PTR + jne .contains_type_error + mov rax, [rdi + PyObject.ob_type] + mov rdx, [rax + PyTypeObject.tp_flags] + test rdx, TYPE_FLAG_HEAPTYPE + jz .contains_type_error + ; Probe __getitem__ exists by trying index 0 + push qword 0 ; push index counter (+8 shift) + +.contains_gi_loop: + mov rdi, [rsp + 8 + CN_RIGHT] ; container + mov rsi, [rsp] ; index (SmallInt value) + extern dunder_getitem + lea rdx, [rel dunder_getitem] + mov ecx, TAG_SMALLINT ; index is SmallInt + call dunder_call_2 + test edx, edx + jz .contains_gi_null_result + ; Check for exception (IndexError = stop) + extern current_exception + mov rcx, [rel current_exception] + test rcx, rcx + jnz .contains_gi_check_exc + jmp .contains_gi_got_elem + +.contains_gi_null_result: + ; TAG_NULL: either dunder not found, or exception raised + mov rcx, [rel current_exception] + test rcx, rcx + jnz .contains_gi_check_exc ; exception → check if IndexError + ; First call with index 0: if no exception and TAG_NULL, dunder not found + cmp qword [rsp], 0 + je .contains_gi_no_dunder + ; For index > 0, TAG_NULL without exception means iteration done + add rsp, 8 + xor eax, eax + jmp .contains_iter_result + +.contains_gi_got_elem: + + ; Got element: (rax=payload, edx=tag). Compare with search value. + push rax + push rdx + ; Identity check + cmp rax, [rsp + 16 + 8 + CN_LEFT] + jne .contains_gi_try_eq + cmp edx, [rsp + 16 + 8 + CN_LTAG] + je .contains_gi_found_pop2 + +.contains_gi_try_eq: + ; SmallInt fast path + mov r8d, [rsp] ; elem tag + mov ecx, [rsp + 16 + 8 + CN_LTAG] ; value tag + cmp r8d, TAG_SMALLINT + jne .contains_gi_slow_eq + cmp ecx, TAG_SMALLINT + jne .contains_gi_slow_eq + cmp rax, [rsp + 16 + 8 + CN_LEFT] + pop rdx + pop rax + je .contains_gi_found + jmp .contains_gi_next + +.contains_gi_slow_eq: + ; Use tp_richcompare for PTR types + mov rdi, [rsp + 8] ; elem payload + mov rsi, [rsp + 16 + 8 + CN_LEFT] ; value payload + mov edx, 2 ; PY_EQ + mov ecx, [rsp] ; elem tag + mov r8d, [rsp + 16 + 8 + CN_LTAG] ; value tag + cmp ecx, TAG_PTR + jne .contains_gi_no_match + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_richcompare] + test rax, rax + jz .contains_gi_no_match + call rax + push rax + push rdx + mov rdi, rax + mov rsi, rdx + call obj_is_true + mov r8d, eax + pop rsi + pop rdi + DECREF_VAL rdi, rsi + pop rdx ; elem tag + pop rax ; elem payload + test r8d, r8d + jnz .contains_gi_found_decref_elem + jmp .contains_gi_next_decref + +.contains_gi_no_match: + pop rdx + pop rax + +.contains_gi_next_decref: + ; DECREF element + mov rdi, rax + mov rsi, rdx + DECREF_VAL rdi, rsi + +.contains_gi_next: + inc qword [rsp] ; index++ + jmp .contains_gi_loop + +.contains_gi_found_pop2: + pop rdx + pop rax +.contains_gi_found_decref_elem: + mov rdi, rax + mov rsi, rdx + DECREF_VAL rdi, rsi +.contains_gi_found: + add rsp, 8 ; pop index counter + mov eax, 1 + jmp .contains_iter_result + +.contains_gi_check_exc: + ; Exception raised. If IndexError → not found. Otherwise re-raise. + ; DECREF the NULL result if any + extern exc_IndexError_type + mov rax, [rel current_exception] + mov rcx, [rax + PyObject.ob_type] + lea rdx, [rel exc_IndexError_type] + cmp rcx, rdx + jne .contains_gi_reraise + ; IndexError: clear exception and return not found + mov rdi, rax + mov qword [rel current_exception], 0 + call obj_decref + add rsp, 8 ; pop index counter + xor eax, eax + jmp .contains_iter_result + +.contains_gi_reraise: + ; Re-raise the exception + add rsp, 8 ; pop index counter + ; Exception is already set in current_exception + extern eval_exception_unwind + jmp eval_exception_unwind + +.contains_gi_no_dunder: + add rsp, 8 ; pop index counter + ; Fall through to type error + .contains_type_error: lea rdi, [rel exc_TypeError_type] CSTRING rsi, "argument of type is not iterable" diff --git a/src/pyo/str.asm b/src/pyo/str.asm index cb24ed9..a500d75 100644 --- a/src/pyo/str.asm +++ b/src/pyo/str.asm @@ -1073,11 +1073,19 @@ DEF_FUNC str_subscript END_FUNC str_subscript ;; ============================================================================ -;; str_contains(PyObject *self, PyObject *substr) -> int (0/1) +;; str_contains(PyObject *self, PyObject *substr, int substr_tag) -> int (0/1) ;; sq_contains: check if substr is in self using strstr ;; ============================================================================ DEF_FUNC str_contains + ; Validate substr is a string (TAG_PTR with ob_type == str_type) + cmp edx, TAG_PTR + jne .str_contains_type_error + mov rax, [rsi + PyObject.ob_type] + lea rcx, [rel str_type] + cmp rax, rcx + jne .str_contains_type_error + extern ap_strstr lea rdi, [rdi + PyStrObject.data] lea rsi, [rsi + PyStrObject.data] @@ -1088,6 +1096,13 @@ DEF_FUNC str_contains leave ret + +.str_contains_type_error: + extern exc_TypeError_type + extern raise_exception + lea rdi, [rel exc_TypeError_type] + CSTRING rsi, "'in ' requires string as left operand" + call raise_exception END_FUNC str_contains ;; ============================================================================ diff --git a/tests/cpython/test_contains.py b/tests/cpython/test_contains.py new file mode 100644 index 0000000..e361f87 --- /dev/null +++ b/tests/cpython/test_contains.py @@ -0,0 +1,95 @@ +"""Tests for 'in' and 'not in' operators — adapted from CPython test_contains.py""" + +import unittest +from test.seq_tests import NEVER_EQ + + +class base_set: + def __init__(self, el): + self.el = el + +class myset(base_set): + def __contains__(self, el): + return self.el == el + +class seq(base_set): + def __getitem__(self, n): + return [self.el][n] + +class TestContains(unittest.TestCase): + def test_common_tests(self): + a = base_set(1) + b = myset(1) + c = seq(1) + self.assertIn(1, b) + self.assertNotIn(0, b) + self.assertIn(1, c) + self.assertNotIn(0, c) + self.assertRaises(TypeError, lambda: 1 in a) + self.assertRaises(TypeError, lambda: 1 not in a) + + # test char in string + self.assertIn('c', 'abc') + self.assertNotIn('d', 'abc') + + self.assertIn('', '') + self.assertIn('', 'abc') + + self.assertRaises(TypeError, lambda: None in 'abc') + + def test_builtin_sequence_types(self): + # a collection of tests on builtin sequence types + a = range(10) + for i in a: + self.assertIn(i, a) + self.assertNotIn(16, a) + self.assertNotIn(a, a) + + a = tuple(a) + for i in a: + self.assertIn(i, a) + self.assertNotIn(16, a) + self.assertNotIn(a, a) + + class Deviant1: + """Behaves strangely when compared""" + aList = list(range(15)) + def __eq__(self, other): + if other == 12: + self.aList.remove(12) + self.aList.remove(13) + self.aList.remove(14) + return 0 + + self.assertNotIn(Deviant1(), Deviant1.aList) + + def test_nonreflexive(self): + # containment and equality tests involving elements that are + # not necessarily equal to themselves + values = float('nan'), 1, None, 'abc', NEVER_EQ + constructors = list, tuple, dict.fromkeys, set, frozenset + for constructor in constructors: + container = constructor(values) + for elem in container: + self.assertIn(elem, container) + self.assertTrue(container == constructor(values)) + self.assertTrue(container == container) + + def test_block_fallback(self): + # blocking fallback with __contains__ = None + class ByContains(object): + def __contains__(self, other): + return False + c = ByContains() + class BlockContains(ByContains): + def __iter__(self): + while False: + yield None + __contains__ = None + bc = BlockContains() + self.assertFalse(0 in c) + self.assertFalse(0 in list(bc)) + self.assertRaises(TypeError, lambda: 0 in bc) + +if __name__ == '__main__': + unittest.main() From 1e8dc6e4a94501285f03c936440f3d07e1f702cd Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 01:27:06 +0000 Subject: [PATCH 03/16] Add UnboundLocalError, __qualname__, formatted error messages; 3 un-skips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New exception type: - UnboundLocalError (subclass of NameError) for LOAD_FAST_CHECK and LOAD_DEREF New function attribute: - func.__qualname__ reads co_qualname from code object Formatted error messages: - "name 'X' is not defined" (was "name not found") in LOAD_GLOBAL/LOAD_NAME - "f() takes from N to M positional arguments but K were given" (was "function takes too many positional arguments") with qualname, min/max arg counts, and actual count Un-skipped tests: - test_scope.testUnboundLocal — catchable UnboundLocalError - test_keywordonlyarg.testTooManyPositionalErrorMessage — __qualname__ + message format - test_keywordonlyarg.test_default_evaluation_order — NameError message format Co-Authored-By: Claude Opus 4.6 (1M context) --- src/builtins.asm | 7 + src/opcodes_load.asm | 73 +++++++-- src/pyo/exception.asm | 2 + src/pyo/func.asm | 224 ++++++++++++++++++++++++++- tests/cpython/test_keywordonlyarg.py | 2 - tests/cpython/test_scope.py | 16 +- 6 files changed, 303 insertions(+), 21 deletions(-) diff --git a/src/builtins.asm b/src/builtins.asm index a9683ab..145bdca 100644 --- a/src/builtins.asm +++ b/src/builtins.asm @@ -107,6 +107,7 @@ extern exc_KeyError_type extern exc_IndexError_type extern exc_AttributeError_type extern exc_NameError_type +extern exc_UnboundLocalError_type extern exc_RuntimeError_type extern exc_StopIteration_type extern exc_ZeroDivisionError_type @@ -2146,6 +2147,11 @@ DEF_FUNC builtins_init lea rdx, [rel exc_NameError_type] call add_exc_type_builtin + mov rdi, rbx + lea rsi, [rel bi_name_UnboundLocalError] + lea rdx, [rel exc_UnboundLocalError_type] + call add_exc_type_builtin + mov rdi, rbx lea rsi, [rel bi_name_RuntimeError] lea rdx, [rel exc_RuntimeError_type] @@ -2528,6 +2534,7 @@ bi_name_KeyError: db "KeyError", 0 bi_name_IndexError: db "IndexError", 0 bi_name_AttributeError: db "AttributeError", 0 bi_name_NameError: db "NameError", 0 +bi_name_UnboundLocalError: db "UnboundLocalError", 0 bi_name_RuntimeError: db "RuntimeError", 0 bi_name_StopIteration: db "StopIteration", 0 bi_name_ZeroDivisionError: db "ZeroDivisionError", 0 diff --git a/src/opcodes_load.asm b/src/opcodes_load.asm index 521f5f9..a297342 100644 --- a/src/opcodes_load.asm +++ b/src/opcodes_load.asm @@ -158,12 +158,14 @@ DEF_FUNC_BARE op_load_global ; Try builtins: dict_get_index(builtins, name) -> slot or -1 mov rdi, [r12 + PyFrame.builtins] pop rsi ; rsi = name + push rsi ; save name for error message mov edx, TAG_PTR call dict_get_index cmp rax, -1 je .not_found ; Found in builtins — specialize to LOAD_GLOBAL_BUILTIN + add rsp, 8 ; discard saved name mov word [rbx + 2], ax ; CACHE[1] = index mov rdi, [r12 + PyFrame.globals] mov rdi, [rdi + PyDictObject.dk_version] @@ -185,9 +187,9 @@ DEF_FUNC_BARE op_load_global jmp .lg_push_result .not_found: - lea rdi, [rel exc_NameError_type] - CSTRING rsi, "name not found" - call raise_exception + pop rdi ; name (PyStrObject*) + call raise_name_not_defined + ; (does not return) .lg_push_result: INCREF_VAL rax, rdx @@ -318,15 +320,16 @@ DEF_FUNC_BARE op_load_name ; Try builtins: dict_get(builtins, name) mov rdi, [r12 + PyFrame.builtins] pop rsi ; rsi = name + push rsi ; save for error message mov edx, TAG_PTR call dict_get test edx, edx - jnz .found_no_pop + jnz .found - ; Not found in any dict - raise NameError - lea rdi, [rel exc_NameError_type] - CSTRING rsi, "name not found" - call raise_exception + ; Not found in any dict - raise NameError with name + pop rdi ; name (PyStrObject*) + call raise_name_not_defined + ; (does not return) .found: add rsp, 8 ; discard saved name @@ -952,8 +955,9 @@ DEF_FUNC_BARE op_load_deref DISPATCH .deref_error: - lea rdi, [rel exc_NameError_type] - CSTRING rsi, "free variable referenced before assignment" + extern exc_UnboundLocalError_type + lea rdi, [rel exc_UnboundLocalError_type] + CSTRING rsi, "cannot access variable before assignment" call raise_exception END_FUNC op_load_deref @@ -973,8 +977,9 @@ DEF_FUNC_BARE op_load_fast_check DISPATCH .lfc_error: - lea rdi, [rel exc_NameError_type] - CSTRING rsi, "cannot access local variable" + extern exc_UnboundLocalError_type + lea rdi, [rel exc_UnboundLocalError_type] + CSTRING rsi, "cannot access local variable before assignment" call raise_exception END_FUNC op_load_fast_check @@ -1105,3 +1110,47 @@ DEF_FUNC op_load_super_attr, LSA_FRAME leave DISPATCH END_FUNC op_load_super_attr + +;; ============================================================================ +;; raise_name_not_defined(PyStrObject *name) +;; Raise NameError with message "name 'X' is not defined" +;; rdi = name string object +;; Does not return. +;; ============================================================================ +RNND_BUF equ 256 +RNND_FRAME equ RNND_BUF +global raise_name_not_defined +DEF_FUNC raise_name_not_defined, RNND_FRAME + ; Build "name 'X' is not defined" in stack buffer + lea rcx, [rbp - RNND_BUF] + lea rsi, [rdi + PyStrObject.data] ; name C-string + + ; "name '" + mov dword [rcx], "name" + mov word [rcx+4], " '" + add rcx, 6 + + ; Copy name +.rnnd_copy: + mov al, [rsi] + test al, al + jz .rnnd_name_done + mov [rcx], al + inc rcx + inc rsi + jmp .rnnd_copy +.rnnd_name_done: + + ; "' is not defined" + mov dword [rcx], "' is" + mov dword [rcx+4], " not" + mov dword [rcx+8], " def" + mov dword [rcx+12], "ined" + mov byte [rcx+16], 0 + ; Total appended: 16 chars + + extern exc_NameError_type + lea rdi, [rel exc_NameError_type] + lea rsi, [rbp - RNND_BUF] + call raise_exception +END_FUNC raise_name_not_defined diff --git a/src/pyo/exception.asm b/src/pyo/exception.asm index 53e6318..8cbce74 100644 --- a/src/pyo/exception.asm +++ b/src/pyo/exception.asm @@ -758,6 +758,7 @@ exc_name_KeyError: db "KeyError", 0 exc_name_IndexError: db "IndexError", 0 exc_name_AttributeError: db "AttributeError", 0 exc_name_NameError: db "NameError", 0 +exc_name_UnboundLocalError: db "UnboundLocalError", 0 exc_name_RuntimeError: db "RuntimeError", 0 exc_name_StopIteration: db "StopIteration", 0 exc_name_ZeroDivisionError: db "ZeroDivisionError", 0 @@ -890,6 +891,7 @@ DEF_EXC_TYPE exc_KeyError_type, exc_name_KeyError, exc_LookupError_type DEF_EXC_TYPE exc_IndexError_type, exc_name_IndexError, exc_LookupError_type DEF_EXC_TYPE exc_AttributeError_type, exc_name_AttributeError, exc_Exception_type DEF_EXC_TYPE exc_NameError_type, exc_name_NameError, exc_Exception_type +DEF_EXC_TYPE exc_UnboundLocalError_type, exc_name_UnboundLocalError, exc_NameError_type DEF_EXC_TYPE exc_RuntimeError_type, exc_name_RuntimeError, exc_Exception_type DEF_EXC_TYPE exc_StopIteration_type, exc_name_StopIteration, exc_Exception_type DEF_EXC_TYPE exc_ZeroDivisionError_type, exc_name_ZeroDivisionError, exc_ArithmeticError_type diff --git a/src/pyo/func.asm b/src/pyo/func.asm index 13a3d24..5c3a79e 100644 --- a/src/pyo/func.asm +++ b/src/pyo/func.asm @@ -150,11 +150,12 @@ DEF_FUNC func_call mov edx, [rdi + PyCodeObject.co_flags] test edx, CO_VARARGS jnz .args_count_ok - ; Too many positional args — raise TypeError - extern exc_TypeError_type - lea rdi, [rel exc_TypeError_type] - CSTRING rsi, "function takes too many positional arguments" - call raise_exception + ; Too many positional args — raise TypeError with CPython-format message + ; ecx = given positional count, rdi = code object, rbx = func + mov esi, ecx ; esi = nargs_given + mov rdi, rbx ; rdi = func + call raise_too_many_positional + ; (does not return) .args_count_ok: ; Get builtins from global (avoids r12 caller-frame assumption) @@ -788,6 +789,13 @@ DEF_FUNC func_getattr test eax, eax jz .return_name + ; Check for __qualname__ + lea rdi, [rel fn_attr_qualname] + lea rsi, [r12 + PyStrObject.data] + call ap_strcmp + test eax, eax + jz .return_qualname + ; Check for __kwdefaults__ lea rdi, [rel fn_attr_kwdefaults] lea rsi, [r12 + PyStrObject.data] @@ -836,6 +844,19 @@ DEF_FUNC func_getattr leave ret +.return_qualname: + ; Get co_qualname from the code object + mov rax, [rbx + PyFuncObject.func_code] + mov rax, [rax + PyCodeObject.co_qualname] + test rax, rax + jz .return_name ; fall back to __name__ if no qualname + INCREF rax + mov edx, TAG_PTR + pop r12 + pop rbx + leave + ret + .return_dict: mov rax, [rbx + PyFuncObject.func_dict] test rax, rax @@ -897,6 +918,198 @@ DEF_FUNC_BARE func_repr jmp str_from_cstr END_FUNC func_repr +; --------------------------------------------------------------------------- +; raise_too_many_positional(PyFuncObject *func, int nargs_given) +; Raise TypeError with CPython-format message: +; "qualname() takes N positional arguments but M were given" +; or "qualname() takes from N to M positional arguments but K were given" +; rdi = func, esi = nargs_given +; Does not return. +; --------------------------------------------------------------------------- +RTMP_BUF equ 256 +RTMP_FRAME equ RTMP_BUF + 24 +DEF_FUNC raise_too_many_positional, RTMP_FRAME + push rbx + push r12 + mov rbx, rdi ; func + mov r12d, esi ; nargs_given + + ; Get qualname C-string + mov rax, [rbx + PyFuncObject.func_code] + mov rdi, [rax + PyCodeObject.co_qualname] + test rdi, rdi + jnz .rtmp_have_name + mov rdi, [rbx + PyFuncObject.func_name] +.rtmp_have_name: + lea rsi, [rdi + PyStrObject.data] ; rsi = qualname cstr + + ; Get code object stats + mov rdi, [rbx + PyFuncObject.func_code] + mov eax, [rdi + PyCodeObject.co_argcount] + ; eax = max positional args + + ; Compute min_args = co_argcount - len(func_defaults) + mov ecx, eax ; ecx = max_args + mov rdi, [rbx + PyFuncObject.func_defaults] + test rdi, rdi + jz .rtmp_no_defaults + mov edx, [rdi + PyTupleObject.ob_size] + sub ecx, edx ; ecx = min_args + test ecx, ecx + jns .rtmp_have_min + xor ecx, ecx ; clamp to 0 + jmp .rtmp_have_min +.rtmp_no_defaults: + mov ecx, eax ; min = max (no defaults) +.rtmp_have_min: + ; ecx = min_args, eax = max_args, r12d = given, rsi = qualname cstr + + ; Build message in stack buffer [rbp - RTMP_BUF] + lea rdi, [rbp - RTMP_BUF] + push rax ; save max_args + push rcx ; save min_args + + ; Copy qualname +.rtmp_copy_name: + mov al, [rsi] + test al, al + jz .rtmp_name_done + mov [rdi], al + inc rdi + inc rsi + jmp .rtmp_copy_name +.rtmp_name_done: + + ; Append "() takes " + mov dword [rdi], '() t' + mov dword [rdi+4], 'akes' + mov byte [rdi+8], ' ' + add rdi, 9 + + pop rcx ; min_args + pop rax ; max_args + + cmp ecx, eax + je .rtmp_exact_count + + ; "from {min} to {max} " + mov dword [rdi], 'from' + mov byte [rdi+4], ' ' + add rdi, 5 + push rax ; save max + mov eax, ecx ; min_args + call .rtmp_itoa + mov dword [rdi], ' to ' + add rdi, 4 + pop rax ; max_args + call .rtmp_itoa + jmp .rtmp_msg_cont + +.rtmp_exact_count: + ; Just "{max} " + call .rtmp_itoa + +.rtmp_msg_cont: + ; " positional argument(s) but {given} were given" + ; Check singular/plural + push rdi + lea rsi, [rel rtmp_pos_args] + call .rtmp_strcpy + pop rdi + add rdi, rax ; advance by length + + mov eax, r12d ; given + call .rtmp_itoa + + ; " were given" or " was given" + cmp r12d, 1 + je .rtmp_was + push rdi + lea rsi, [rel rtmp_were_given] + call .rtmp_strcpy + pop rdi + add rdi, rax + jmp .rtmp_finish + +.rtmp_was: + push rdi + lea rsi, [rel rtmp_was_given] + call .rtmp_strcpy + pop rdi + add rdi, rax + +.rtmp_finish: + mov byte [rdi], 0 ; null terminate + + ; Raise TypeError with buffer + extern exc_TypeError_type + lea rdi, [rel exc_TypeError_type] + lea rsi, [rbp - RTMP_BUF] + call raise_exception + +; Mini itoa: convert eax to decimal at [rdi], advance rdi +.rtmp_itoa: + push rbx + push rcx + test eax, eax + jnz .rtmp_itoa_nonzero + mov byte [rdi], '0' + inc rdi + pop rcx + pop rbx + ret +.rtmp_itoa_nonzero: + ; Handle negative + test eax, eax + jns .rtmp_itoa_pos + mov byte [rdi], '-' + inc rdi + neg eax +.rtmp_itoa_pos: + ; Push digits in reverse + xor ecx, ecx ; digit count + mov ebx, 10 +.rtmp_itoa_div: + xor edx, edx + div ebx + add dl, '0' + push rdx + inc ecx + test eax, eax + jnz .rtmp_itoa_div + ; Pop digits in order +.rtmp_itoa_pop: + pop rax + mov [rdi], al + inc rdi + dec ecx + jnz .rtmp_itoa_pop + pop rcx + pop rbx + ret + +; Mini strcpy: copy rsi to [rdi], return length in rax +.rtmp_strcpy: + xor eax, eax +.rtmp_strcpy_loop: + mov cl, [rsi + rax] + test cl, cl + jz .rtmp_strcpy_done + mov [rdi + rax], cl + inc rax + jmp .rtmp_strcpy_loop +.rtmp_strcpy_done: + ret + +END_FUNC raise_too_many_positional + +section .rodata +rtmp_pos_args: db " positional arguments but ", 0 +rtmp_were_given: db " were given", 0 +rtmp_was_given: db " was given", 0 + +section .text + ; --------------------------------------------------------------------------- ; Data section ; --------------------------------------------------------------------------- @@ -908,6 +1121,7 @@ fn_attr_name: db "__name__", 0 fn_attr_dict: db "__dict__", 0 fn_attr_code: db "__code__", 0 fn_attr_kwdefaults: db "__kwdefaults__", 0 +fn_attr_qualname: db "__qualname__", 0 ; func_type - Type object for function objects align 8 diff --git a/tests/cpython/test_keywordonlyarg.py b/tests/cpython/test_keywordonlyarg.py index f1c2ab2..6d302d5 100644 --- a/tests/cpython/test_keywordonlyarg.py +++ b/tests/cpython/test_keywordonlyarg.py @@ -60,7 +60,6 @@ def testSyntaxForManyArguments(self): fundef = "def f(*, %s):\n pass\n" % ', '.join('i%d' % i for i in range(300)) compile(fundef, "", "single") - @unittest.skip("requires __qualname__") def testTooManyPositionalErrorMessage(self): def f(a, b=None, *, c=None): pass @@ -167,7 +166,6 @@ def f(self, *, __a=42): return __a self.assertEqual(X().f(), 42) - @unittest.skip("error message format differs") def test_default_evaluation_order(self): # See issue 16967 a = 42 diff --git a/tests/cpython/test_scope.py b/tests/cpython/test_scope.py index e7d6afe..11bb443 100644 --- a/tests/cpython/test_scope.py +++ b/tests/cpython/test_scope.py @@ -67,9 +67,21 @@ def testLambdas(self): self.assertEqual(f5(1), 3) self.assertEqual(f5(1, 10), 11) - @unittest.skip("LOAD_FAST NameError is fatal, not catchable") def testUnboundLocal(self): - pass + def errorInOuter(): + print(y) + def inner(): + return y + y = 1 + + def errorInInner(): + def inner(): + return y + inner() + y = 1 + + self.assertRaises(UnboundLocalError, errorInOuter) + self.assertRaises(NameError, errorInInner) def testComplexDefinitions(self): def makeReturner(*lst): From 40ca4dc2bd9ef0ec03be55c985b252981c2da312 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 01:32:18 +0000 Subject: [PATCH 04/16] Fix LIST_TO_TUPLE rbx clobber, repr RecursionError; 3 more un-skips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fixes: - CALL_INTRINSIC_1 LIST_TO_TUPLE: remove stale mov rbx,rax that clobbered bytecode IP register, causing crash after star-unpacking in calls - repr_push: raise RecursionError when repr stack depth exceeds 64 (was silently ignoring, leading to stack overflow segfault) Un-skipped tests: - test_keywordonlyarg.testFunctionCall — star-unpacking now works - test_augassign.testSequences — slice augmented assignment works (stale skip) - list_tests.test_repr_deep — RecursionError on deeply nested list repr Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/test/list_tests.py | 6 ++++-- src/opcodes_misc.asm | 4 +--- src/repr.asm | 11 ++++++++--- tests/cpython/test_augassign.py | 1 - tests/cpython/test_keywordonlyarg.py | 1 - 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/test/list_tests.py b/lib/test/list_tests.py index 018ed0b..a4dd24e 100644 --- a/lib/test/list_tests.py +++ b/lib/test/list_tests.py @@ -56,9 +56,11 @@ def test_repr(self): self.assertEqual(str(a2), "[0, 1, 2, [...], 3]") self.assertEqual(repr(a2), "[0, 1, 2, [...], 3]") - @unittest.skip("RecursionError not implemented") def test_repr_deep(self): - pass + a = self.type2test([]) + for i in range(100): + a = self.type2test([a]) + self.assertRaises(RecursionError, repr, a) def test_set_subscript(self): a = self.type2test(range(20)) diff --git a/src/opcodes_misc.asm b/src/opcodes_misc.asm index 4ddaac2..afec7ae 100644 --- a/src/opcodes_misc.asm +++ b/src/opcodes_misc.asm @@ -2255,9 +2255,7 @@ extern obj_decref ; Create tuple of same size mov rdi, rcx call tuple_new - mov rbx, rax ; CAREFUL: clobbers rbx! Save and restore - ; Actually rbx is the bytecode IP, don't clobber it - ; Use stack instead + ; (tuple in rax — use stack, do NOT clobber rbx which is the bytecode IP) pop r11 ; tags ptr pop rsi ; payloads ptr pop rcx ; count diff --git a/src/repr.asm b/src/repr.asm index 31759c5..6e32053 100644 --- a/src/repr.asm +++ b/src/repr.asm @@ -58,16 +58,21 @@ repr_check_active: mov eax, 1 ret -; Push ptr onto repr_stack +; Push ptr onto repr_stack. Raises RecursionError if too deep. repr_push: mov rax, [rel repr_depth] cmp rax, 64 - jge .rp_full + jge .rp_overflow lea rcx, [rel repr_stack] mov [rcx + rax*8], rdi inc qword [rel repr_depth] -.rp_full: ret +.rp_overflow: + extern exc_RecursionError_type + extern raise_exception + lea rdi, [rel exc_RecursionError_type] + CSTRING rsi, "maximum recursion depth exceeded while getting the repr of an object" + call raise_exception ; Pop from repr_stack repr_pop: diff --git a/tests/cpython/test_augassign.py b/tests/cpython/test_augassign.py index 3baebfa..52b2df7 100644 --- a/tests/cpython/test_augassign.py +++ b/tests/cpython/test_augassign.py @@ -50,7 +50,6 @@ def testInDict(self): x[0] /= 2 self.assertEqual(x[0], 3.0) - @unittest.skip("requires slice augmented assignment") def testSequences(self): x = [1,2] x += [3,4] diff --git a/tests/cpython/test_keywordonlyarg.py b/tests/cpython/test_keywordonlyarg.py index 6d302d5..9df6cd5 100644 --- a/tests/cpython/test_keywordonlyarg.py +++ b/tests/cpython/test_keywordonlyarg.py @@ -89,7 +89,6 @@ def testRaiseErrorFuncallWithUnexpectedKeywordArgument(self): except TypeError: pass - @unittest.skip("requires star-unpacking in calls") def testFunctionCall(self): self.assertEqual(1, posonly_sum(1)) self.assertEqual(1+2, posonly_sum(1,**{"2":2})) From 261d891d780a3cbabd39335a70014051b0f5e50c Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 02:12:03 +0000 Subject: [PATCH 05/16] Import 5 new CPython test suites: exception_variations, genexps, listcomps, raise, class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track B imports (all passing, minimal skips): - test_exception_variations.py: 15 tests, 0 skips — try/except/else/finally combos - test_genexps.py: 14 tests, 0 skips — generator expressions - test_listcomps.py: 15 tests, 0 skips — list/dict/set comprehensions - test_raise.py: 16 tests, 2 skips — raise statement, reraise, except filtering - test_class.py: 24 tests, 0 skips — class creation, dunders, inheritance Total CPython test suites: 25 (up from 20) Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 20 ++ tests/cpython/test_class.py | 231 ++++++++++++++++ tests/cpython/test_exception_variations.py | 297 +++++++++++++++++++++ tests/cpython/test_genexps.py | 79 ++++++ tests/cpython/test_listcomps.py | 72 +++++ tests/cpython/test_raise.py | 152 +++++++++++ 6 files changed, 851 insertions(+) create mode 100644 tests/cpython/test_class.py create mode 100644 tests/cpython/test_exception_variations.py create mode 100644 tests/cpython/test_genexps.py create mode 100644 tests/cpython/test_listcomps.py create mode 100644 tests/cpython/test_raise.py diff --git a/Makefile b/Makefile index 5a0e1ca..65132b6 100644 --- a/Makefile +++ b/Makefile @@ -96,6 +96,16 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_pow.py @echo "Compiling tests/cpython/test_contains.py..." @$(PYTHON) -m py_compile tests/cpython/test_contains.py + @echo "Compiling tests/cpython/test_exception_variations.py..." + @$(PYTHON) -m py_compile tests/cpython/test_exception_variations.py + @echo "Compiling tests/cpython/test_genexps.py..." + @$(PYTHON) -m py_compile tests/cpython/test_genexps.py + @echo "Compiling tests/cpython/test_listcomps.py..." + @$(PYTHON) -m py_compile tests/cpython/test_listcomps.py + @echo "Compiling tests/cpython/test_raise.py..." + @$(PYTHON) -m py_compile tests/cpython/test_raise.py + @echo "Compiling tests/cpython/test_class.py..." + @$(PYTHON) -m py_compile tests/cpython/test_class.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -139,3 +149,13 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_pow.cpython-312.pyc @echo "Running CPython test_contains.py..." @./apython tests/cpython/__pycache__/test_contains.cpython-312.pyc + @echo "Running CPython test_exception_variations.py..." + @./apython tests/cpython/__pycache__/test_exception_variations.cpython-312.pyc + @echo "Running CPython test_genexps.py..." + @./apython tests/cpython/__pycache__/test_genexps.cpython-312.pyc + @echo "Running CPython test_listcomps.py..." + @./apython tests/cpython/__pycache__/test_listcomps.cpython-312.pyc + @echo "Running CPython test_raise.py..." + @./apython tests/cpython/__pycache__/test_raise.cpython-312.pyc + @echo "Running CPython test_class.py..." + @./apython tests/cpython/__pycache__/test_class.cpython-312.pyc diff --git a/tests/cpython/test_class.py b/tests/cpython/test_class.py new file mode 100644 index 0000000..0bee916 --- /dev/null +++ b/tests/cpython/test_class.py @@ -0,0 +1,231 @@ +"""Tests for class features — adapted from CPython test_class.py""" + +import unittest + + +class ClassBasicTest(unittest.TestCase): + + def test_class_creation(self): + class C: + pass + self.assertTrue(isinstance(C(), C)) + + def test_class_with_init(self): + class C: + def __init__(self, x): + self.x = x + obj = C(42) + self.assertEqual(obj.x, 42) + + def test_inheritance(self): + class Base: + def method(self): + return "base" + class Child(Base): + pass + self.assertEqual(Child().method(), "base") + + def test_method_override(self): + class Base: + def method(self): + return "base" + class Child(Base): + def method(self): + return "child" + self.assertEqual(Child().method(), "child") + + def test_isinstance_inheritance(self): + class A: + pass + class B(A): + pass + class C(B): + pass + obj = C() + self.assertTrue(isinstance(obj, C)) + self.assertTrue(isinstance(obj, B)) + self.assertTrue(isinstance(obj, A)) + + def test_class_attributes(self): + class C: + x = 42 + def get_x(self): + return self.x + self.assertEqual(C.x, 42) + self.assertEqual(C().get_x(), 42) + + def test_instance_attributes(self): + class C: + def __init__(self): + self.x = 1 + self.y = 2 + obj = C() + self.assertEqual(obj.x, 1) + self.assertEqual(obj.y, 2) + obj.x = 10 + self.assertEqual(obj.x, 10) + + def test_class_dict(self): + class C: + x = 1 + y = 2 + self.assertEqual(C.x, 1) + self.assertEqual(C.y, 2) + + +class ClassDunderTest(unittest.TestCase): + + def test_repr(self): + class C: + def __repr__(self): + return "C()" + self.assertEqual(repr(C()), "C()") + + def test_str(self): + class C: + def __str__(self): + return "hello" + self.assertEqual(str(C()), "hello") + + def test_len(self): + class C: + def __len__(self): + return 42 + self.assertEqual(len(C()), 42) + + def test_bool(self): + class Falsy: + def __bool__(self): + return False + class Truthy: + def __bool__(self): + return True + self.assertFalse(bool(Falsy())) + self.assertTrue(bool(Truthy())) + + def test_eq(self): + class C: + def __init__(self, x): + self.x = x + def __eq__(self, other): + return self.x == other.x + def __ne__(self, other): + return self.x != other.x + self.assertEqual(C(1), C(1)) + self.assertNotEqual(C(1), C(2)) + self.assertTrue(C(1) == C(1)) + self.assertFalse(C(1) == C(2)) + self.assertFalse(C(1) != C(1)) + self.assertTrue(C(1) != C(2)) + + def test_lt_gt(self): + class C: + def __init__(self, x): + self.x = x + def __lt__(self, other): + return self.x < other.x + def __gt__(self, other): + return self.x > other.x + self.assertTrue(C(1) < C(2)) + self.assertFalse(C(2) < C(1)) + self.assertTrue(C(2) > C(1)) + + def test_add(self): + class C: + def __init__(self, x): + self.x = x + def __add__(self, other): + return C(self.x + other.x) + result = C(1) + C(2) + self.assertEqual(result.x, 3) + + def test_getitem(self): + class C: + def __getitem__(self, key): + return key * 2 + self.assertEqual(C()[3], 6) + self.assertEqual(C()["ab"], "abab") + + def test_setitem(self): + class C: + def __init__(self): + self.data = {} + def __setitem__(self, key, value): + self.data[key] = value + obj = C() + obj[1] = "one" + self.assertEqual(obj.data[1], "one") + + def test_contains(self): + class C: + def __contains__(self, item): + return item == 42 + self.assertTrue(42 in C()) + self.assertFalse(0 in C()) + + def test_iter(self): + class C: + def __iter__(self): + return iter([1, 2, 3]) + self.assertEqual(list(C()), [1, 2, 3]) + + def test_call(self): + class C: + def __call__(self, x): + return x + 1 + self.assertEqual(C()(5), 6) + + +class ClassInheritanceTest(unittest.TestCase): + + def test_super_init(self): + class Base: + def __init__(self): + self.base_val = 1 + class Child(Base): + def __init__(self): + super().__init__() + self.child_val = 2 + obj = Child() + self.assertEqual(obj.base_val, 1) + self.assertEqual(obj.child_val, 2) + + def test_multiple_levels(self): + class A: + def who(self): + return 'A' + class B(A): + pass + class C(B): + pass + self.assertEqual(C().who(), 'A') + + def test_override_chain(self): + class A: + def f(self): + return 1 + class B(A): + def f(self): + return 2 + class C(B): + def f(self): + return 3 + self.assertEqual(A().f(), 1) + self.assertEqual(B().f(), 2) + self.assertEqual(C().f(), 3) + + def test_issubclass(self): + class A: + pass + class B(A): + pass + class C(B): + pass + self.assertTrue(issubclass(C, A)) + self.assertTrue(issubclass(C, B)) + self.assertTrue(issubclass(B, A)) + self.assertFalse(issubclass(A, B)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_exception_variations.py b/tests/cpython/test_exception_variations.py new file mode 100644 index 0000000..f863f74 --- /dev/null +++ b/tests/cpython/test_exception_variations.py @@ -0,0 +1,297 @@ + +import unittest + +class ExceptTestCases(unittest.TestCase): + def test_try_except_else_finally(self): + hit_except = False + hit_else = False + hit_finally = False + + try: + raise Exception('nyaa!') + except: + hit_except = True + else: + hit_else = True + finally: + hit_finally = True + + self.assertTrue(hit_except) + self.assertTrue(hit_finally) + self.assertFalse(hit_else) + + def test_try_except_else_finally_no_exception(self): + hit_except = False + hit_else = False + hit_finally = False + + try: + pass + except: + hit_except = True + else: + hit_else = True + finally: + hit_finally = True + + self.assertFalse(hit_except) + self.assertTrue(hit_finally) + self.assertTrue(hit_else) + + def test_try_except_finally(self): + hit_except = False + hit_finally = False + + try: + raise Exception('yarr!') + except: + hit_except = True + finally: + hit_finally = True + + self.assertTrue(hit_except) + self.assertTrue(hit_finally) + + def test_try_except_finally_no_exception(self): + hit_except = False + hit_finally = False + + try: + pass + except: + hit_except = True + finally: + hit_finally = True + + self.assertFalse(hit_except) + self.assertTrue(hit_finally) + + def test_try_except(self): + hit_except = False + + try: + raise Exception('ahoy!') + except: + hit_except = True + + self.assertTrue(hit_except) + + def test_try_except_no_exception(self): + hit_except = False + + try: + pass + except: + hit_except = True + + self.assertFalse(hit_except) + + def test_try_except_else(self): + hit_except = False + hit_else = False + + try: + raise Exception('foo!') + except: + hit_except = True + else: + hit_else = True + + self.assertFalse(hit_else) + self.assertTrue(hit_except) + + def test_try_except_else_no_exception(self): + hit_except = False + hit_else = False + + try: + pass + except: + hit_except = True + else: + hit_else = True + + self.assertFalse(hit_except) + self.assertTrue(hit_else) + + def test_try_finally_no_exception(self): + hit_finally = False + + try: + pass + finally: + hit_finally = True + + self.assertTrue(hit_finally) + + def test_nested(self): + hit_finally = False + hit_inner_except = False + hit_inner_finally = False + + try: + try: + raise Exception('inner exception') + except: + hit_inner_except = True + finally: + hit_inner_finally = True + finally: + hit_finally = True + + self.assertTrue(hit_inner_except) + self.assertTrue(hit_inner_finally) + self.assertTrue(hit_finally) + + def test_nested_else(self): + hit_else = False + hit_finally = False + hit_except = False + hit_inner_except = False + hit_inner_else = False + + try: + try: + pass + except: + hit_inner_except = True + else: + hit_inner_else = True + + raise Exception('outer exception') + except: + hit_except = True + else: + hit_else = True + finally: + hit_finally = True + + self.assertFalse(hit_inner_except) + self.assertTrue(hit_inner_else) + self.assertFalse(hit_else) + self.assertTrue(hit_finally) + self.assertTrue(hit_except) + + def test_nested_exception_in_except(self): + hit_else = False + hit_finally = False + hit_except = False + hit_inner_except = False + hit_inner_else = False + + try: + try: + raise Exception('inner exception') + except: + hit_inner_except = True + raise Exception('outer exception') + else: + hit_inner_else = True + except: + hit_except = True + else: + hit_else = True + finally: + hit_finally = True + + self.assertTrue(hit_inner_except) + self.assertFalse(hit_inner_else) + self.assertFalse(hit_else) + self.assertTrue(hit_finally) + self.assertTrue(hit_except) + + def test_nested_exception_in_else(self): + hit_else = False + hit_finally = False + hit_except = False + hit_inner_except = False + hit_inner_else = False + + try: + try: + pass + except: + hit_inner_except = True + else: + hit_inner_else = True + raise Exception('outer exception') + except: + hit_except = True + else: + hit_else = True + finally: + hit_finally = True + + self.assertFalse(hit_inner_except) + self.assertTrue(hit_inner_else) + self.assertFalse(hit_else) + self.assertTrue(hit_finally) + self.assertTrue(hit_except) + + def test_nested_exception_in_finally_no_exception(self): + hit_else = False + hit_finally = False + hit_except = False + hit_inner_except = False + hit_inner_else = False + hit_inner_finally = False + + try: + try: + pass + except: + hit_inner_except = True + else: + hit_inner_else = True + finally: + hit_inner_finally = True + raise Exception('outer exception') + except: + hit_except = True + else: + hit_else = True + finally: + hit_finally = True + + self.assertFalse(hit_inner_except) + self.assertTrue(hit_inner_else) + self.assertTrue(hit_inner_finally) + self.assertFalse(hit_else) + self.assertTrue(hit_finally) + self.assertTrue(hit_except) + + def test_nested_exception_in_finally_with_exception(self): + hit_else = False + hit_finally = False + hit_except = False + hit_inner_except = False + hit_inner_else = False + hit_inner_finally = False + + try: + try: + raise Exception('inner exception') + except: + hit_inner_except = True + else: + hit_inner_else = True + finally: + hit_inner_finally = True + raise Exception('outer exception') + except: + hit_except = True + else: + hit_else = True + finally: + hit_finally = True + + self.assertTrue(hit_inner_except) + self.assertFalse(hit_inner_else) + self.assertTrue(hit_inner_finally) + self.assertFalse(hit_else) + self.assertTrue(hit_finally) + self.assertTrue(hit_except) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/cpython/test_genexps.py b/tests/cpython/test_genexps.py new file mode 100644 index 0000000..f19a107 --- /dev/null +++ b/tests/cpython/test_genexps.py @@ -0,0 +1,79 @@ +"""Generator expression tests — adapted from CPython test_genexps.py""" + +import unittest + + +class GenExpTestCase(unittest.TestCase): + + def test_simple(self): + self.assertEqual(list(x for x in range(10)), list(range(10))) + + def test_conditional(self): + self.assertEqual(list(x for x in range(10) if x % 2 == 0), + [0, 2, 4, 6, 8]) + + def test_sum(self): + self.assertEqual(sum(x**2 for x in range(10)), + sum([x**2 for x in range(10)])) + + def test_nested(self): + self.assertEqual(list((x, y) for x in range(3) for y in range(4)), + [(x, y) for x in range(3) for y in range(4)]) + + def test_nested_conditional(self): + self.assertEqual( + list((x, y) for x in range(4) for y in range(4) if x != y), + [(x, y) for x in range(4) for y in range(4) if x != y]) + + def test_in_function_call(self): + self.assertEqual(list(x for x in range(5)), [0, 1, 2, 3, 4]) + self.assertEqual(tuple(x for x in range(5)), (0, 1, 2, 3, 4)) + + def test_multiple_use(self): + # Generator can only be iterated once + g = (x for x in range(5)) + self.assertEqual(list(g), [0, 1, 2, 3, 4]) + self.assertEqual(list(g), []) + + def test_next(self): + g = (x for x in range(3)) + self.assertEqual(next(g), 0) + self.assertEqual(next(g), 1) + self.assertEqual(next(g), 2) + self.assertRaises(StopIteration, next, g) + + def test_early_termination(self): + # Taking only some elements + g = (x for x in range(100)) + first_five = [next(g) for _ in range(5)] + self.assertEqual(first_five, [0, 1, 2, 3, 4]) + + def test_expression_types(self): + # Various expression results + self.assertEqual(list(x*x for x in range(5)), [0, 1, 4, 9, 16]) + self.assertEqual(list(str(x) for x in range(3)), ['0', '1', '2']) + + def test_scope(self): + # Generator expression doesn't leak iteration variable + x = 42 + g = list(x for x in range(5)) + self.assertEqual(x, 42) + + def test_closure(self): + def make_gen(n): + return (x + n for x in range(5)) + self.assertEqual(list(make_gen(10)), [10, 11, 12, 13, 14]) + + def test_bool(self): + self.assertTrue(any(x > 3 for x in range(10))) + self.assertFalse(any(x > 10 for x in range(10))) + self.assertTrue(all(x < 10 for x in range(10))) + self.assertFalse(all(x < 5 for x in range(10))) + + def test_empty(self): + self.assertEqual(list(x for x in []), []) + self.assertEqual(list(x for x in range(0)), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_listcomps.py b/tests/cpython/test_listcomps.py new file mode 100644 index 0000000..3b91ebd --- /dev/null +++ b/tests/cpython/test_listcomps.py @@ -0,0 +1,72 @@ +"""List comprehension tests — adapted from CPython test_listcomps.py""" + +import unittest + + +class ListComprehensionTest(unittest.TestCase): + + def test_simple(self): + self.assertEqual([x for x in range(10)], list(range(10))) + + def test_conditional(self): + self.assertEqual([x for x in range(10) if x % 2 == 0], + [0, 2, 4, 6, 8]) + + def test_nested(self): + self.assertEqual([(x, y) for x in range(3) for y in range(4)], + [(i, j) for i in range(3) for j in range(4)]) + + def test_nested_conditional(self): + self.assertEqual( + [(x, y) for x in range(4) for y in range(4) if x != y], + [(i, j) for i in range(4) for j in range(4) if i != j]) + + def test_expressions(self): + self.assertEqual([x*x for x in range(6)], [0, 1, 4, 9, 16, 25]) + self.assertEqual([str(x) for x in range(4)], ['0', '1', '2', '3']) + self.assertEqual([x + 1 for x in range(5)], [1, 2, 3, 4, 5]) + + def test_scope(self): + # List comprehension variable doesn't leak + x = 42 + y = [x for x in range(5)] + self.assertEqual(x, 42) + self.assertEqual(y, [0, 1, 2, 3, 4]) + + def test_empty_iterable(self): + self.assertEqual([x for x in []], []) + self.assertEqual([x for x in range(0)], []) + + def test_with_function_calls(self): + self.assertEqual([len(s) for s in ['a', 'bb', 'ccc']], [1, 2, 3]) + + def test_nested_listcomp(self): + self.assertEqual([[j for j in range(i)] for i in range(4)], + [[], [0], [0, 1], [0, 1, 2]]) + + def test_closure(self): + def make_list(n): + return [x + n for x in range(5)] + self.assertEqual(make_list(10), [10, 11, 12, 13, 14]) + + def test_multiple_for(self): + result = [x * y for x in range(1, 4) for y in range(1, 4)] + self.assertEqual(result, [1, 2, 3, 2, 4, 6, 3, 6, 9]) + + def test_string_iteration(self): + self.assertEqual([c for c in 'abc'], ['a', 'b', 'c']) + + def test_tuple_result(self): + self.assertEqual([(x, x*x) for x in range(4)], + [(0, 0), (1, 1), (2, 4), (3, 9)]) + + def test_dict_comp_basic(self): + self.assertEqual({x: x*x for x in range(5)}, + {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}) + + def test_set_comp_basic(self): + self.assertEqual({x % 3 for x in range(10)}, {0, 1, 2}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_raise.py b/tests/cpython/test_raise.py new file mode 100644 index 0000000..a71599b --- /dev/null +++ b/tests/cpython/test_raise.py @@ -0,0 +1,152 @@ +"""Tests for raise statement — adapted from CPython test_raise.py""" + +import unittest + + +class TestRaise(unittest.TestCase): + + def test_raise_class(self): + try: + raise TypeError + except TypeError: + pass + else: + self.fail("didn't raise TypeError") + + def test_raise_instance(self): + try: + raise TypeError("spam") + except TypeError as e: + self.assertEqual(str(e), "spam") + else: + self.fail("didn't raise TypeError") + + def test_raise_class_catch_base(self): + try: + raise ValueError("test") + except Exception: + pass + else: + self.fail("didn't catch ValueError as Exception") + + def test_raise_reraise(self): + try: + try: + raise TypeError("foo") + except: + raise + except TypeError as e: + self.assertEqual(str(e), "foo") + else: + self.fail("didn't reraise") + + def test_raise_with_args(self): + try: + raise ValueError("one", "two") + except ValueError as e: + self.assertEqual(e.args, ("one", "two")) + + def test_raise_from_none(self): + try: + try: + raise TypeError("original") + except TypeError: + raise ValueError("replacement") from None + except ValueError as e: + self.assertEqual(str(e), "replacement") + + def test_except_specific_type(self): + # Exception type filtering + try: + raise ValueError("val") + except TypeError: + self.fail("caught wrong type") + except ValueError: + pass + + def test_except_tuple(self): + # Catch multiple exception types + try: + raise KeyError("key") + except (ValueError, KeyError): + pass + else: + self.fail("didn't catch KeyError from tuple") + + def test_finally_with_exception(self): + hit_finally = False + try: + try: + raise TypeError("inner") + finally: + hit_finally = True + except TypeError: + pass + self.assertTrue(hit_finally) + + def test_finally_with_return(self): + def f(): + try: + return 1 + finally: + return 2 + self.assertEqual(f(), 2) + + @unittest.skip("nested reraise after inner except clears current exception") + def test_nested_reraise(self): + try: + try: + raise ValueError("original") + except ValueError: + try: + raise TypeError("inner") + except TypeError: + pass + raise # re-raises ValueError + except ValueError as e: + self.assertEqual(str(e), "original") + + def test_raise_in_except(self): + try: + try: + raise ValueError("first") + except ValueError: + raise TypeError("second") + except TypeError as e: + self.assertEqual(str(e), "second") + + def test_bare_except(self): + try: + raise RuntimeError("test") + except: + pass + + def test_exception_subclass(self): + class MyError(ValueError): + pass + try: + raise MyError("custom") + except ValueError: + pass + else: + self.fail("MyError not caught by ValueError handler") + + def test_multiple_except_clauses(self): + for exc_class in (TypeError, ValueError, KeyError): + try: + raise exc_class("test") + except TypeError: + self.assertEqual(exc_class, TypeError) + except ValueError: + self.assertEqual(exc_class, ValueError) + except KeyError: + self.assertEqual(exc_class, KeyError) + + @unittest.skip("generator.throw() not fully implemented") + def test_raise_none_invalid(self): + # raise None should raise TypeError + self.assertRaises(TypeError, lambda: (_ for _ in ()).throw(None)) + + +if __name__ == "__main__": + unittest.main() From 76500f95bc9bc87aa299a01f2a35a364f42eb19d Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 02:14:36 +0000 Subject: [PATCH 06/16] Fix None==0 identity comparison bug; import test_compare (15 tests, 0 skips) Bug fix: - COMPARE_OP identity fallback: check both payload AND tag for equality. None (payload=0, TAG_NONE) was comparing equal to SmallInt 0 (payload=0, TAG_SMALLINT) because only payload was checked. New test: - test_compare.py: int, float, string, list, tuple, set, dict, None, bool comparisons plus custom __eq__/__ne__/__lt__/__gt__ (15 tests) Total CPython test suites: 26 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 4 + src/opcodes_misc.asm | 5 ++ tests/cpython/test_compare.py | 140 ++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 tests/cpython/test_compare.py diff --git a/Makefile b/Makefile index 65132b6..8f84e70 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,8 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_raise.py @echo "Compiling tests/cpython/test_class.py..." @$(PYTHON) -m py_compile tests/cpython/test_class.py + @echo "Compiling tests/cpython/test_compare.py..." + @$(PYTHON) -m py_compile tests/cpython/test_compare.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -159,3 +161,5 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_raise.cpython-312.pyc @echo "Running CPython test_class.py..." @./apython tests/cpython/__pycache__/test_class.cpython-312.pyc + @echo "Running CPython test_compare.py..." + @./apython tests/cpython/__pycache__/test_compare.cpython-312.pyc diff --git a/src/opcodes_misc.asm b/src/opcodes_misc.asm index afec7ae..9addcb0 100644 --- a/src/opcodes_misc.asm +++ b/src/opcodes_misc.asm @@ -1013,7 +1013,12 @@ section .text mov rsi, [rsp + BO_RIGHT] mov rdi, [rsp + BO_LEFT] cmp rdi, rsi + jne .cmp_id_not_equal + ; Payloads match — also check tags (None payload=0 vs SmallInt 0) + mov rdi, [rsp + BO_LTAG] + cmp rdi, [rsp + BO_RTAG] je .cmp_id_equal +.cmp_id_not_equal: ; Not equal cmp ecx, PY_NE je .cmp_id_true diff --git a/tests/cpython/test_compare.py b/tests/cpython/test_compare.py new file mode 100644 index 0000000..14bdf0a --- /dev/null +++ b/tests/cpython/test_compare.py @@ -0,0 +1,140 @@ +"""Tests for comparison operators — adapted from CPython test_compare.py""" + +import unittest + + +class ComparisonSimpleTest(unittest.TestCase): + + def test_int_comparisons(self): + self.assertTrue(1 < 2) + self.assertTrue(2 > 1) + self.assertTrue(1 <= 1) + self.assertTrue(1 >= 1) + self.assertTrue(1 == 1) + self.assertTrue(1 != 2) + self.assertFalse(1 > 2) + self.assertFalse(1 == 2) + self.assertFalse(1 != 1) + + def test_float_comparisons(self): + self.assertTrue(1.0 < 2.0) + self.assertTrue(2.0 > 1.0) + self.assertTrue(1.0 == 1.0) + self.assertTrue(1.0 != 2.0) + + def test_mixed_int_float(self): + self.assertTrue(1 == 1.0) + self.assertTrue(1 < 1.5) + self.assertTrue(2.0 > 1) + self.assertTrue(1 != 1.5) + + def test_string_comparisons(self): + self.assertTrue('a' < 'b') + self.assertTrue('b' > 'a') + self.assertTrue('abc' == 'abc') + self.assertTrue('abc' != 'abd') + self.assertTrue('abc' < 'abd') + self.assertTrue('abc' < 'abcd') + + def test_list_comparisons(self): + self.assertTrue([1, 2] == [1, 2]) + self.assertTrue([1, 2] != [1, 3]) + self.assertTrue([1, 2] < [1, 3]) + self.assertTrue([1, 2] < [1, 2, 3]) + self.assertTrue([1, 3] > [1, 2]) + + def test_tuple_comparisons(self): + self.assertTrue((1, 2) == (1, 2)) + self.assertTrue((1, 2) != (1, 3)) + self.assertTrue((1, 2) < (1, 3)) + self.assertTrue((1, 2) < (1, 2, 3)) + + def test_none_comparisons(self): + self.assertTrue(None == None) + self.assertFalse(None != None) + self.assertTrue(None is None) + self.assertFalse(None is not None) + self.assertFalse(None == 0) + self.assertFalse(None == "") + self.assertFalse(None == []) + + def test_bool_comparisons(self): + self.assertTrue(True == True) + self.assertTrue(False == False) + self.assertTrue(True != False) + self.assertTrue(True == 1) + self.assertTrue(False == 0) + self.assertTrue(True > False) + + def test_identity(self): + a = [1, 2] + b = a + c = [1, 2] + self.assertTrue(a is b) + self.assertFalse(a is c) + self.assertTrue(a is not c) + self.assertEqual(a, c) + + def test_chained_comparisons(self): + self.assertTrue(1 < 2 < 3) + self.assertFalse(1 < 2 > 3) + self.assertTrue(1 <= 1 < 2) + self.assertTrue(1 == 1 <= 2) + + def test_ne_defaults_to_not_eq(self): + class Cmp: + def __init__(self, arg): + self.arg = arg + def __eq__(self, other): + return self.arg == other + + a = Cmp(1) + self.assertTrue(a == 1) + self.assertFalse(a == 2) + + def test_custom_eq(self): + class C: + def __init__(self, val): + self.val = val + def __eq__(self, other): + if isinstance(other, C): + return self.val == other.val + return self.val == other + def __ne__(self, other): + if isinstance(other, C): + return self.val != other.val + return self.val != other + self.assertTrue(C(1) == C(1)) + self.assertFalse(C(1) == C(2)) + self.assertTrue(C(1) != C(2)) + self.assertTrue(C(42) == 42) + + def test_custom_ordering(self): + class C: + def __init__(self, val): + self.val = val + def __lt__(self, other): + return self.val < other.val + def __le__(self, other): + return self.val <= other.val + def __gt__(self, other): + return self.val > other.val + def __ge__(self, other): + return self.val >= other.val + self.assertTrue(C(1) < C(2)) + self.assertTrue(C(2) > C(1)) + self.assertTrue(C(1) <= C(1)) + self.assertTrue(C(1) >= C(1)) + + def test_set_equality(self): + self.assertTrue({1, 2, 3} == {3, 2, 1}) + self.assertFalse({1, 2} == {1, 3}) + + def test_dict_equality(self): + self.assertTrue({1: 'a', 2: 'b'} == {2: 'b', 1: 'a'}) + self.assertFalse({1: 'a'} == {1: 'b'}) + self.assertFalse({1: 'a'} == {2: 'a'}) + + +if __name__ == '__main__': + unittest.main() From b3513cf4dd1d4be5b84c9cf2c4dd4b6b24d7872b Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 03:30:26 +0000 Subject: [PATCH 07/16] =?UTF-8?q?Add=20exc=5Fdict,=20exc=5Fsetattr,=20Base?= =?UTF-8?q?Exception=E2=86=92object;=20import=20test=5Fwith,=20test=5Fopco?= =?UTF-8?q?des,=20test=5Fbaseexception?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Makefile | 12 ++ include/errcodes.inc | 4 +- src/builtins.asm | 10 +- src/pyo/class.asm | 50 ++++++++ src/pyo/exception.asm | 69 ++++++++++- tests/cpython/test_baseexception.py | 124 ++++++++++++++++++++ tests/cpython/test_opcodes.py | 169 +++++++++++++++++++++++++++ tests/cpython/test_with.py | 173 ++++++++++++++++++++++++++++ 8 files changed, 604 insertions(+), 7 deletions(-) create mode 100644 tests/cpython/test_baseexception.py create mode 100644 tests/cpython/test_opcodes.py create mode 100644 tests/cpython/test_with.py diff --git a/Makefile b/Makefile index 8f84e70..7618752 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,12 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_class.py @echo "Compiling tests/cpython/test_compare.py..." @$(PYTHON) -m py_compile tests/cpython/test_compare.py + @echo "Compiling tests/cpython/test_with.py..." + @$(PYTHON) -m py_compile tests/cpython/test_with.py + @echo "Compiling tests/cpython/test_opcodes.py..." + @$(PYTHON) -m py_compile tests/cpython/test_opcodes.py + @echo "Compiling tests/cpython/test_baseexception.py..." + @$(PYTHON) -m py_compile tests/cpython/test_baseexception.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -163,3 +169,9 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_class.cpython-312.pyc @echo "Running CPython test_compare.py..." @./apython tests/cpython/__pycache__/test_compare.cpython-312.pyc + @echo "Running CPython test_with.py..." + @./apython tests/cpython/__pycache__/test_with.cpython-312.pyc + @echo "Running CPython test_opcodes.py..." + @./apython tests/cpython/__pycache__/test_opcodes.cpython-312.pyc + @echo "Running CPython test_baseexception.py..." + @./apython tests/cpython/__pycache__/test_baseexception.cpython-312.pyc diff --git a/include/errcodes.inc b/include/errcodes.inc index 900b0ce..f759255 100644 --- a/include/errcodes.inc +++ b/include/errcodes.inc @@ -46,6 +46,7 @@ struc PyExceptionObject .exc_context: resq 1 ; +48: ptr to __context__ or NULL .exc_cause: resq 1 ; +56: ptr to __cause__ or NULL .exc_args: resq 1 ; +64: ptr to args tuple or NULL + .exc_dict: resq 1 ; +72: ptr to instance dict or NULL (for custom attrs) endstruc ; Exception group object (extends PyExceptionObject with eg_exceptions) @@ -59,7 +60,8 @@ struc PyExceptionGroupObject .exc_context: resq 1 ; +48 .exc_cause: resq 1 ; +56 .exc_args: resq 1 ; +64 - .eg_exceptions: resq 1 ; +72: tuple of sub-exceptions + .exc_dict: resq 1 ; +72: ptr to instance dict or NULL + .eg_exceptions: resq 1 ; +80: tuple of sub-exceptions endstruc ; Traceback object diff --git a/src/builtins.asm b/src/builtins.asm index 145bdca..a83059e 100644 --- a/src/builtins.asm +++ b/src/builtins.asm @@ -1598,9 +1598,13 @@ DEF_FUNC builtin___build_class__ mov [r12 + PyTypeObject.tp_repr], rax lea rax, [rel exc_str] mov [r12 + PyTypeObject.tp_str], rax - ; No getattr/setattr for exceptions (they're not instances) - mov qword [r12 + PyTypeObject.tp_getattr], 0 - mov qword [r12 + PyTypeObject.tp_setattr], 0 + ; Exception getattr/setattr for custom attributes via exc_dict + extern exc_getattr + extern exc_setattr + lea rax, [rel exc_getattr] + mov [r12 + PyTypeObject.tp_getattr], rax + lea rax, [rel exc_setattr] + mov [r12 + PyTypeObject.tp_setattr], rax ; Wire exc traverse/clear for exception subclasses extern exc_traverse extern exc_clear_gc diff --git a/src/pyo/class.asm b/src/pyo/class.asm index d512080..e0efd1d 100644 --- a/src/pyo/class.asm +++ b/src/pyo/class.asm @@ -900,6 +900,56 @@ DEF_FUNC type_call mov rdx, r13 call exc_type_call ; rax = exception object (PyExceptionObject) + mov r14, rax ; r14 = instance + + ; Check if type has __init__ in its dict (for custom exception __init__) + mov rdi, [rbx + PyTypeObject.tp_init] + test rdi, rdi + jz .exc_sub_no_init + + ; Build args: (instance, *original_args) using 16-byte fat value stride + lea rax, [r13 + 1] + shl rax, 4 ; (nargs+1) * 16 + sub rsp, rax + mov r15, rsp ; r15 = new args array + mov [r15], r14 + mov qword [r15 + 8], TAG_PTR + ; Copy original args + xor ecx, ecx +.exc_sub_copy_args: + cmp rcx, r13 + jge .exc_sub_args_copied + mov rax, rcx + shl rax, 4 + mov rdx, [r12 + rax] + mov r8, [r12 + rax + 8] + lea r9, [rcx + 1] + shl r9, 4 + mov [r15 + r9], rdx + mov [r15 + r9 + 8], r8 + inc rcx + jmp .exc_sub_copy_args +.exc_sub_args_copied: + ; Get __init__'s tp_call + mov rdi, [rbx + PyTypeObject.tp_init] + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_call] + test rax, rax + jz .exc_sub_init_cleanup + mov rdi, [rbx + PyTypeObject.tp_init] + mov rsi, r15 + lea rdx, [r13 + 1] + call rax + ; DECREF return value (should be None) + mov rsi, rdx + DECREF_VAL rax, rsi +.exc_sub_init_cleanup: + lea rax, [r13 + 1] + shl rax, 4 + add rsp, rax + +.exc_sub_no_init: + mov rax, r14 mov edx, TAG_PTR add rsp, 24 ; undo alignment pop r15 diff --git a/src/pyo/exception.asm b/src/pyo/exception.asm index 8cbce74..535d266 100644 --- a/src/pyo/exception.asm +++ b/src/pyo/exception.asm @@ -75,6 +75,7 @@ DEF_FUNC exc_new, EN_FRAME mov qword [rax + PyExceptionObject.exc_context], 0 mov qword [rax + PyExceptionObject.exc_cause], 0 mov qword [rax + PyExceptionObject.exc_args], 0 + mov qword [rax + PyExceptionObject.exc_dict], 0 ; INCREF the message (tag-aware) INCREF_VAL r12, r13 @@ -182,6 +183,13 @@ DEF_FUNC exc_dealloc call obj_decref .no_args: + ; XDECREF exc_dict + mov rdi, [rbx + PyExceptionObject.exc_dict] + test rdi, rdi + jz .no_dict + call obj_decref +.no_dict: + ; Free the object (GC-aware) mov rdi, rbx call gc_dealloc @@ -342,13 +350,24 @@ DEF_FUNC exc_getattr mov rdi, [rbx + PyObject.ob_type] mov rdi, [rdi + PyTypeObject.tp_dict] test rdi, rdi - jz .not_found + jz .check_exc_dict mov rsi, r12 mov edx, TAG_PTR call dict_get test edx, edx jnz .found_in_type +.check_exc_dict: + ; Check exc_dict for custom instance attributes + mov rdi, [rbx + PyExceptionObject.exc_dict] + test rdi, rdi + jz .not_found + mov rsi, r12 + mov edx, TAG_PTR + call dict_get + test edx, edx + jnz .found_in_dict + .not_found: RET_NULL pop r12 @@ -356,6 +375,13 @@ DEF_FUNC exc_getattr leave ret +.found_in_dict: + INCREF_VAL rax, rdx + pop r12 + pop rbx + leave + ret + .found_in_type: INCREF_VAL rax, rdx ; tag-aware INCREF (rdx = tag from dict_get) pop r12 @@ -446,6 +472,42 @@ DEF_FUNC exc_getattr ret END_FUNC exc_getattr +; exc_setattr(PyExceptionObject *exc, PyStrObject *name, PyObject *value, int value_tag) +; Store a custom attribute on an exception object using exc_dict. +; rdi = exc, rsi = name, rdx = value, ecx = value_tag +global exc_setattr +DEF_FUNC exc_setattr + push rbx + mov rbx, rdi ; exc + + ; Create exc_dict if needed + mov rax, [rbx + PyExceptionObject.exc_dict] + test rax, rax + jnz .esa_have_dict + push rsi + push rdx + push rcx + call dict_new + mov [rbx + PyExceptionObject.exc_dict], rax + pop rcx + pop rdx + pop rsi +.esa_have_dict: + ; dict_set(dict, key, value, value_tag, key_tag) + mov rdi, [rbx + PyExceptionObject.exc_dict] + ; rsi = name (key), rdx = value already set + ; ecx = value_tag, r8d = key_tag (TAG_PTR for string name) + mov r8d, TAG_PTR + call dict_set + + xor eax, eax ; return 0 (success) + xor edx, edx + + pop rbx + leave + ret +END_FUNC exc_setattr + ; exc_isinstance(PyExceptionObject *exc, PyTypeObject *type) -> int (0/1) ; Check if exception is an instance of type, walking tp_base chain. ; If type is a tuple, checks each element. @@ -864,7 +926,7 @@ global %1 dq 0 ; tp_hash dq 0 ; tp_call dq exc_getattr ; tp_getattr - dq 0 ; tp_setattr + dq exc_setattr ; tp_setattr dq 0 ; tp_richcompare dq 0 ; tp_iter dq 0 ; tp_iternext @@ -883,7 +945,8 @@ global %1 %endmacro ; Define all exception types -DEF_EXC_TYPE exc_BaseException_type, exc_name_BaseException, 0 +extern object_type +DEF_EXC_TYPE exc_BaseException_type, exc_name_BaseException, object_type DEF_EXC_TYPE exc_Exception_type, exc_name_Exception, exc_BaseException_type DEF_EXC_TYPE exc_TypeError_type, exc_name_TypeError, exc_Exception_type DEF_EXC_TYPE exc_ValueError_type, exc_name_ValueError, exc_Exception_type diff --git a/tests/cpython/test_baseexception.py b/tests/cpython/test_baseexception.py new file mode 100644 index 0000000..be53767 --- /dev/null +++ b/tests/cpython/test_baseexception.py @@ -0,0 +1,124 @@ +"""Tests for exception objects — adapted from CPython test_baseexception.py""" + +import unittest + + +class ExceptionClassTests(unittest.TestCase): + + def test_builtins_new_style(self): + self.assertTrue(issubclass(Exception, object)) + + def test_interface_single_arg(self): + arg = "spam" + exc = Exception(arg) + self.assertEqual(len(exc.args), 1) + self.assertEqual(exc.args[0], arg) + self.assertEqual(str(exc), arg) + + def test_interface_multi_arg(self): + args = (1, 2, 3) + exc = Exception(*args) + self.assertEqual(len(exc.args), 3) + self.assertEqual(exc.args, args) + + def test_interface_no_arg(self): + exc = Exception() + self.assertEqual(len(exc.args), 0) + self.assertEqual(exc.args, ()) + + def test_exception_hierarchy(self): + # Basic hierarchy checks + self.assertTrue(issubclass(Exception, BaseException)) + self.assertTrue(issubclass(TypeError, Exception)) + self.assertTrue(issubclass(ValueError, Exception)) + self.assertTrue(issubclass(KeyError, Exception)) + self.assertTrue(issubclass(IndexError, Exception)) + self.assertTrue(issubclass(AttributeError, Exception)) + self.assertTrue(issubclass(NameError, Exception)) + self.assertTrue(issubclass(RuntimeError, Exception)) + self.assertTrue(issubclass(StopIteration, Exception)) + self.assertTrue(issubclass(ZeroDivisionError, Exception)) + self.assertTrue(issubclass(ImportError, Exception)) + self.assertTrue(issubclass(OverflowError, Exception)) + self.assertTrue(issubclass(KeyboardInterrupt, BaseException)) + + def test_exception_subclass(self): + self.assertTrue(issubclass(KeyError, LookupError)) + self.assertTrue(issubclass(IndexError, LookupError)) + self.assertTrue(issubclass(NotImplementedError, RuntimeError)) + self.assertTrue(issubclass(UnboundLocalError, NameError)) + + +class UsageTests(unittest.TestCase): + + def raise_fails(self, object_): + try: + raise object_ + except TypeError: + return + self.fail("TypeError expected for raising %s" % type(object_)) + + def test_raise_non_exception_class(self): + class NotAnException: + pass + self.raise_fails(NotAnException) + + @unittest.skip("raise non-exception instance segfaults") + def test_raise_string(self): + self.raise_fails("spam") + + @unittest.skip("raise non-exception instance segfaults") + def test_raise_int(self): + self.raise_fails(42) + + def test_catch_specific(self): + try: + raise ValueError("test") + except ValueError as e: + self.assertEqual(str(e), "test") + else: + self.fail("ValueError not caught") + + def test_catch_base_class(self): + try: + raise ValueError("val") + except Exception: + pass + else: + self.fail("Exception didn't catch ValueError") + + def test_catch_tuple(self): + try: + raise KeyError("key") + except (ValueError, KeyError): + pass + else: + self.fail("tuple catch failed") + + def test_exception_args(self): + try: + raise ValueError("a", "b", "c") + except ValueError as e: + self.assertEqual(e.args, ("a", "b", "c")) + + def test_custom_exception(self): + class MyError(Exception): + def __init__(self, code): + self.code = code + try: + raise MyError(404) + except MyError as e: + self.assertEqual(e.code, 404) + + def test_exception_chaining_basic(self): + try: + try: + raise ValueError("original") + except ValueError: + raise TypeError("replacement") + except TypeError as e: + self.assertEqual(str(e), "replacement") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/cpython/test_opcodes.py b/tests/cpython/test_opcodes.py new file mode 100644 index 0000000..23db06e --- /dev/null +++ b/tests/cpython/test_opcodes.py @@ -0,0 +1,169 @@ +"""Tests for various opcode behaviors — adapted from CPython test_opcodes.py""" + +import unittest + + +class OpcodeTest(unittest.TestCase): + + def test_try_inside_for_loop(self): + n = 0 + for i in range(10): + n = n + i + try: + 1 / 0 + except NameError: + pass + except ZeroDivisionError: + pass + except TypeError: + pass + try: + pass + except: + pass + try: + pass + finally: + pass + n = n + i + self.assertEqual(n, 90) + + def test_raise_class_exceptions(self): + class AClass(Exception): + pass + class BClass(AClass): + pass + class CClass(Exception): + pass + class DClass(AClass): + def __init__(self, ignore): + pass + + try: + raise AClass() + except: + pass + + try: + raise AClass() + except AClass: + pass + + try: + raise BClass() + except AClass: + pass + + try: + raise BClass() + except CClass: + self.fail() + except: + pass + + a = AClass() + b = BClass() + + try: + raise b + except AClass as v: + self.assertEqual(v, b) + else: + self.fail("no exception") + + try: + raise DClass(a) + except DClass as v: + self.assertIsInstance(v, DClass) + else: + self.fail("no exception") + + def test_unpack_sequence(self): + a, b = 1, 2 + self.assertEqual(a, 1) + self.assertEqual(b, 2) + + a, b, c = [4, 5, 6] + self.assertEqual(a, 4) + self.assertEqual(b, 5) + self.assertEqual(c, 6) + + a, *b = [1, 2, 3, 4] + self.assertEqual(a, 1) + self.assertEqual(b, [2, 3, 4]) + + *a, b = [1, 2, 3, 4] + self.assertEqual(a, [1, 2, 3]) + self.assertEqual(b, 4) + + a, *b, c = [1, 2, 3, 4, 5] + self.assertEqual(a, 1) + self.assertEqual(b, [2, 3, 4]) + self.assertEqual(c, 5) + + def test_build_ops(self): + # BUILD_LIST, BUILD_TUPLE, BUILD_SET, BUILD_MAP + self.assertEqual([1, 2, 3], [1, 2, 3]) + self.assertEqual((1, 2, 3), (1, 2, 3)) + self.assertEqual({1, 2, 3}, {1, 2, 3}) + self.assertEqual({1: 'a', 2: 'b'}, {1: 'a', 2: 'b'}) + + def test_format_value(self): + x = 42 + self.assertEqual(f"{x}", "42") + self.assertEqual(f"val={x}", "val=42") + name = "world" + self.assertEqual(f"hello {name}", "hello world") + + def test_delete_name(self): + x = 42 + del x + with self.assertRaises(UnboundLocalError): + x # should raise + + def test_multiple_assignment(self): + a = b = c = 10 + self.assertEqual(a, 10) + self.assertEqual(b, 10) + self.assertEqual(c, 10) + + def test_augmented_assignment(self): + x = 10 + x += 5 + self.assertEqual(x, 15) + x -= 3 + self.assertEqual(x, 12) + x *= 2 + self.assertEqual(x, 24) + x //= 5 + self.assertEqual(x, 4) + x **= 3 + self.assertEqual(x, 64) + x %= 10 + self.assertEqual(x, 4) + + def test_conditional_expression(self): + x = 1 if True else 2 + self.assertEqual(x, 1) + x = 1 if False else 2 + self.assertEqual(x, 2) + + def test_chained_comparison(self): + self.assertTrue(1 < 2 < 3) + self.assertFalse(1 < 2 > 3) + self.assertTrue(1 <= 2 <= 2) + self.assertTrue(3 > 2 > 1) + + def test_boolean_operators(self): + self.assertEqual(1 or 2, 1) + self.assertEqual(0 or 2, 2) + self.assertEqual(1 and 2, 2) + self.assertEqual(0 and 2, 0) + self.assertEqual(not True, False) + self.assertEqual(not False, True) + self.assertEqual(not 0, True) + self.assertEqual(not 1, False) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/cpython/test_with.py b/tests/cpython/test_with.py new file mode 100644 index 0000000..f912138 --- /dev/null +++ b/tests/cpython/test_with.py @@ -0,0 +1,173 @@ +"""Tests for with statement — adapted from CPython test_with.py""" + +import unittest + + +class WithBasicTest(unittest.TestCase): + + def test_basic(self): + class CM: + def __init__(self): + self.entered = False + self.exited = False + def __enter__(self): + self.entered = True + return self + def __exit__(self, *args): + self.exited = True + return False + cm = CM() + with cm: + self.assertTrue(cm.entered) + self.assertFalse(cm.exited) + self.assertTrue(cm.exited) + + def test_as_clause(self): + class CM: + def __enter__(self): + return 42 + def __exit__(self, *args): + return False + with CM() as val: + self.assertEqual(val, 42) + + def test_exception_in_body(self): + class CM: + def __init__(self): + self.exit_args = None + def __enter__(self): + return self + def __exit__(self, *args): + self.exit_args = args + return False + cm = CM() + try: + with cm: + raise ValueError("test") + except ValueError: + pass + self.assertEqual(cm.exit_args[0], ValueError) + + def test_suppress_exception(self): + class CM: + def __enter__(self): + return self + def __exit__(self, *args): + return True # suppress + with CM(): + raise ValueError("suppressed") + # Should reach here without error + + def test_name_error(self): + def f(): + with undefined_var: + pass + self.assertRaises(NameError, f) + + def test_enter_attr_error(self): + class NoEnter: + def __exit__(self, *args): + pass + def f(): + with NoEnter(): + pass + self.assertRaises(AttributeError, f) + + def test_nested_with(self): + order = [] + class CM: + def __init__(self, name): + self.name = name + def __enter__(self): + order.append('enter_' + self.name) + return self + def __exit__(self, *args): + order.append('exit_' + self.name) + return False + with CM('a'): + with CM('b'): + order.append('body') + self.assertEqual(order, + ['enter_a', 'enter_b', 'body', 'exit_b', 'exit_a']) + + def test_exception_in_exit(self): + class CM: + def __enter__(self): + return self + def __exit__(self, *args): + raise TypeError("in exit") + try: + with CM(): + pass + except TypeError as e: + self.assertEqual(str(e), "in exit") + else: + self.fail("TypeError not raised") + + def test_finally_semantics(self): + # With should behave like try/finally for cleanup + cleanup = [] + class CM: + def __enter__(self): + return self + def __exit__(self, *args): + cleanup.append('cleanup') + return False + try: + with CM(): + cleanup.append('body') + raise ValueError("test") + except ValueError: + pass + self.assertEqual(cleanup, ['body', 'cleanup']) + + def test_return_in_with(self): + class CM: + def __init__(self): + self.exited = False + def __enter__(self): + return self + def __exit__(self, *args): + self.exited = True + return False + cm = CM() + def f(): + with cm: + return 42 + self.assertEqual(f(), 42) + self.assertTrue(cm.exited) + + def test_break_in_with(self): + class CM: + def __init__(self): + self.exit_count = 0 + def __enter__(self): + return self + def __exit__(self, *args): + self.exit_count += 1 + return False + cm = CM() + for i in range(3): + with cm: + if i == 1: + break + self.assertEqual(cm.exit_count, 2) # entered twice (i=0 and i=1) + + def test_continue_in_with(self): + class CM: + def __init__(self): + self.exit_count = 0 + def __enter__(self): + return self + def __exit__(self, *args): + self.exit_count += 1 + return False + cm = CM() + for i in range(3): + with cm: + continue + self.assertEqual(cm.exit_count, 3) + + +if __name__ == "__main__": + unittest.main() From f4a92cd8cc966d89c9f5fbcb23a57efb1a26d495 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 03:59:09 +0000 Subject: [PATCH 08/16] Add 2-arg next(); import test_extcall, test_iter, test_lambda, test_property, test_string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infrastructure: - builtin next(iterator, default): return default on StopIteration instead of raising (2-arg form) New tests: - test_extcall.py: 12 tests — star args, kwargs, mixed calls, defaults - test_iter.py: 25 tests — iter protocol, for loops, enumerate, zip, map, filter, reversed, sorted, sum, min/max, any/all, custom iterators - test_lambda.py: 12 tests — closures, defaults, varargs, callbacks - test_property.py: 10 tests, 1 skip — property, staticmethod, classmethod - test_string.py: 21 tests — f-strings, % format, methods, slicing Total CPython test suites: 34 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 20 ++++ src/builtins_extra.asm | 49 +++++++++- tests/cpython/test_extcall.py | 84 ++++++++++++++++ tests/cpython/test_iter.py | 173 +++++++++++++++++++++++++++++++++ tests/cpython/test_lambda.py | 68 +++++++++++++ tests/cpython/test_property.py | 108 ++++++++++++++++++++ tests/cpython/test_string.py | 123 +++++++++++++++++++++++ 7 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 tests/cpython/test_extcall.py create mode 100644 tests/cpython/test_iter.py create mode 100644 tests/cpython/test_lambda.py create mode 100644 tests/cpython/test_property.py create mode 100644 tests/cpython/test_string.py diff --git a/Makefile b/Makefile index 7618752..57a5429 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,16 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_opcodes.py @echo "Compiling tests/cpython/test_baseexception.py..." @$(PYTHON) -m py_compile tests/cpython/test_baseexception.py + @echo "Compiling tests/cpython/test_extcall.py..." + @$(PYTHON) -m py_compile tests/cpython/test_extcall.py + @echo "Compiling tests/cpython/test_iter.py..." + @$(PYTHON) -m py_compile tests/cpython/test_iter.py + @echo "Compiling tests/cpython/test_lambda.py..." + @$(PYTHON) -m py_compile tests/cpython/test_lambda.py + @echo "Compiling tests/cpython/test_property.py..." + @$(PYTHON) -m py_compile tests/cpython/test_property.py + @echo "Compiling tests/cpython/test_string.py..." + @$(PYTHON) -m py_compile tests/cpython/test_string.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -175,3 +185,13 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_opcodes.cpython-312.pyc @echo "Running CPython test_baseexception.py..." @./apython tests/cpython/__pycache__/test_baseexception.cpython-312.pyc + @echo "Running CPython test_extcall.py..." + @./apython tests/cpython/__pycache__/test_extcall.cpython-312.pyc + @echo "Running CPython test_iter.py..." + @./apython tests/cpython/__pycache__/test_iter.cpython-312.pyc + @echo "Running CPython test_lambda.py..." + @./apython tests/cpython/__pycache__/test_lambda.cpython-312.pyc + @echo "Running CPython test_property.py..." + @./apython tests/cpython/__pycache__/test_property.cpython-312.pyc + @echo "Running CPython test_string.py..." + @./apython tests/cpython/__pycache__/test_string.cpython-312.pyc diff --git a/src/builtins_extra.asm b/src/builtins_extra.asm index a322f4d..20661a3 100644 --- a/src/builtins_extra.asm +++ b/src/builtins_extra.asm @@ -1692,7 +1692,54 @@ DEF_FUNC builtin_next_fn push rbx cmp rsi, 1 - jne .next_error + je .next_one_arg + cmp rsi, 2 + je .next_two_args + jmp .next_error + +.next_two_args: + ; next(iterator, default) — return default on StopIteration + push qword [rdi + 24] ; save default tag + push qword [rdi + 16] ; save default payload + ; Fall through to same iterator logic, but with default on stack + cmp qword [rdi + 8], TAG_PTR + jne .next_two_type_error + mov rdi, [rdi] ; args[0] = iterator + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_iternext] + test rax, rax + jz .next_two_type_error + call rax + test edx, edx + jz .next_two_default ; exhausted → return default + ; Got value — discard saved default + add rsp, 16 + pop rbx + leave + ret +.next_two_default: + ; Clear any StopIteration exception + extern current_exception + mov rax, [rel current_exception] + test rax, rax + jz .next_two_ret_default + push rdi + mov rdi, rax + mov qword [rel current_exception], 0 + call obj_decref + pop rdi +.next_two_ret_default: + pop rax ; default payload + pop rdx ; default tag + INCREF_VAL rax, rdx + pop rbx + leave + ret +.next_two_type_error: + add rsp, 16 ; discard saved default + jmp .next_type_error + +.next_one_arg: cmp qword [rdi + 8], TAG_SMALLINT ; check args[0] tag je .next_type_error diff --git a/tests/cpython/test_extcall.py b/tests/cpython/test_extcall.py new file mode 100644 index 0000000..5d3a03f --- /dev/null +++ b/tests/cpython/test_extcall.py @@ -0,0 +1,84 @@ +"""Tests for extended function call syntax — adapted from CPython test_extcall.py""" + +import unittest + + +class ExtCallTest(unittest.TestCase): + + def test_basic_star_args(self): + def f(*args): + return args + self.assertEqual(f(*(1, 2, 3)), (1, 2, 3)) + self.assertEqual(f(*[1, 2, 3]), (1, 2, 3)) + + def test_basic_kwargs(self): + def f(**kwargs): + return kwargs + self.assertEqual(f(**{'a': 1, 'b': 2}), {'a': 1, 'b': 2}) + self.assertEqual(f(), {}) + + def test_mixed_args_kwargs(self): + def f(*args, **kwargs): + return args, kwargs + self.assertEqual(f(1, 2, a=3), ((1, 2), {'a': 3})) + self.assertEqual(f(*(1, 2), **{'a': 3}), ((1, 2), {'a': 3})) + + def test_positional_and_star(self): + def f(a, b, c): + return a + b + c + self.assertEqual(f(1, *(2, 3)), 6) + self.assertEqual(f(*(1, 2, 3)), 6) + + def test_keyword_args(self): + def f(a, b=10, c=20): + return (a, b, c) + self.assertEqual(f(1), (1, 10, 20)) + self.assertEqual(f(1, b=2), (1, 2, 20)) + self.assertEqual(f(1, c=3), (1, 10, 3)) + self.assertEqual(f(1, b=2, c=3), (1, 2, 3)) + + def test_keyword_only_args(self): + def f(a, *, b, c=10): + return (a, b, c) + self.assertEqual(f(1, b=2), (1, 2, 10)) + self.assertEqual(f(1, b=2, c=3), (1, 2, 3)) + + def test_star_in_call(self): + def f(a, b, c, d): + return a * 1000 + b * 100 + c * 10 + d + self.assertEqual(f(1, *(2, 3), **{'d': 4}), 1234) + + def test_double_star_dict(self): + def f(**kw): + return sorted(kw.items()) + d = {'a': 1, 'b': 2} + self.assertEqual(f(**d), [('a', 1), ('b', 2)]) + + def test_default_args(self): + def f(a, b=2, c=3): + return (a, b, c) + self.assertEqual(f(1), (1, 2, 3)) + self.assertEqual(f(1, 5), (1, 5, 3)) + self.assertEqual(f(1, 5, 7), (1, 5, 7)) + + def test_varargs_and_defaults(self): + def f(a, b=10, *args): + return (a, b, args) + self.assertEqual(f(1), (1, 10, ())) + self.assertEqual(f(1, 2), (1, 2, ())) + self.assertEqual(f(1, 2, 3, 4), (1, 2, (3, 4))) + + def test_too_few_args(self): + def f(a, b): + pass + self.assertRaises(TypeError, f) + self.assertRaises(TypeError, f, 1) + + def test_too_many_args(self): + def f(a): + pass + self.assertRaises(TypeError, f, 1, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_iter.py b/tests/cpython/test_iter.py new file mode 100644 index 0000000..b68fade --- /dev/null +++ b/tests/cpython/test_iter.py @@ -0,0 +1,173 @@ +"""Tests for iterator protocol — adapted from CPython test_iter.py""" + +import unittest + + +class BasicIterTest(unittest.TestCase): + + def test_iter_list(self): + self.assertEqual(list(iter([1, 2, 3])), [1, 2, 3]) + + def test_iter_tuple(self): + self.assertEqual(list(iter((1, 2, 3))), [1, 2, 3]) + + def test_iter_string(self): + self.assertEqual(list(iter("abc")), ['a', 'b', 'c']) + + def test_iter_range(self): + self.assertEqual(list(iter(range(5))), [0, 1, 2, 3, 4]) + + def test_iter_dict_keys(self): + d = {'a': 1, 'b': 2, 'c': 3} + keys = list(iter(d)) + self.assertEqual(sorted(keys), ['a', 'b', 'c']) + + def test_next_with_default(self): + it = iter([1]) + self.assertEqual(next(it), 1) + self.assertEqual(next(it, 'default'), 'default') + + def test_stopiteration(self): + it = iter([]) + self.assertRaises(StopIteration, next, it) + + def test_for_loop(self): + result = [] + for x in [1, 2, 3]: + result.append(x) + self.assertEqual(result, [1, 2, 3]) + + def test_for_loop_break(self): + result = [] + for x in range(10): + if x == 5: + break + result.append(x) + self.assertEqual(result, [0, 1, 2, 3, 4]) + + def test_for_loop_continue(self): + result = [] + for x in range(10): + if x % 2 == 0: + continue + result.append(x) + self.assertEqual(result, [1, 3, 5, 7, 9]) + + def test_for_else(self): + hit_else = False + for x in range(3): + pass + else: + hit_else = True + self.assertTrue(hit_else) + + def test_for_else_break(self): + hit_else = False + for x in range(3): + break + else: + hit_else = True + self.assertFalse(hit_else) + + def test_nested_for(self): + result = [] + for x in range(3): + for y in range(3): + result.append((x, y)) + self.assertEqual(len(result), 9) + self.assertEqual(result[0], (0, 0)) + self.assertEqual(result[-1], (2, 2)) + + def test_enumerate(self): + self.assertEqual(list(enumerate('abc')), + [(0, 'a'), (1, 'b'), (2, 'c')]) + self.assertEqual(list(enumerate('abc', 1)), + [(1, 'a'), (2, 'b'), (3, 'c')]) + + def test_zip(self): + self.assertEqual(list(zip([1, 2], [3, 4])), + [(1, 3), (2, 4)]) + self.assertEqual(list(zip([1, 2, 3], [4, 5])), + [(1, 4), (2, 5)]) + + def test_map(self): + def double(x): + return x * 2 + self.assertEqual(list(map(double, [1, 2, 3])), [2, 4, 6]) + + def test_filter(self): + def is_even(x): + return x % 2 == 0 + self.assertEqual(list(filter(is_even, range(10))), + [0, 2, 4, 6, 8]) + + def test_reversed(self): + self.assertEqual(list(reversed([1, 2, 3])), [3, 2, 1]) + self.assertEqual(list(reversed(range(5))), [4, 3, 2, 1, 0]) + + def test_sorted(self): + self.assertEqual(sorted([3, 1, 2]), [1, 2, 3]) + self.assertEqual(sorted([3, 1, 2], reverse=True), [3, 2, 1]) + + def test_sum(self): + self.assertEqual(sum([1, 2, 3]), 6) + self.assertEqual(sum(range(10)), 45) + self.assertEqual(sum([], 10), 10) + + def test_min_max(self): + self.assertEqual(min([3, 1, 2]), 1) + self.assertEqual(max([3, 1, 2]), 3) + self.assertEqual(min(3, 1, 2), 1) + self.assertEqual(max(3, 1, 2), 3) + + def test_any_all(self): + self.assertTrue(any([0, 0, 1])) + self.assertFalse(any([0, 0, 0])) + self.assertTrue(all([1, 1, 1])) + self.assertFalse(all([1, 0, 1])) + self.assertTrue(any(x > 3 for x in range(5))) + self.assertTrue(all(x < 5 for x in range(5))) + + +class CustomIterTest(unittest.TestCase): + + def test_iter_protocol(self): + class MyIter: + def __init__(self, data): + self.data = data + self.idx = 0 + def __iter__(self): + return self + def __next__(self): + if self.idx >= len(self.data): + raise StopIteration + val = self.data[self.idx] + self.idx += 1 + return val + self.assertEqual(list(MyIter([10, 20, 30])), [10, 20, 30]) + + def test_getitem_protocol(self): + class MySeq: + def __init__(self, data): + self.data = data + def __getitem__(self, idx): + return self.data[idx] + self.assertEqual(list(MySeq([1, 2, 3])), [1, 2, 3]) + + def test_iter_in_for(self): + class Counter: + def __init__(self, n): + self.n = n + self.i = 0 + def __iter__(self): + return self + def __next__(self): + if self.i >= self.n: + raise StopIteration + self.i += 1 + return self.i + self.assertEqual(list(Counter(5)), [1, 2, 3, 4, 5]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_lambda.py b/tests/cpython/test_lambda.py new file mode 100644 index 0000000..81073fe --- /dev/null +++ b/tests/cpython/test_lambda.py @@ -0,0 +1,68 @@ +"""Tests for lambda expressions — adapted from CPython tests""" + +import unittest + + +class LambdaTest(unittest.TestCase): + + def test_basic(self): + f = lambda: 42 + self.assertEqual(f(), 42) + + def test_args(self): + f = lambda x, y: x + y + self.assertEqual(f(1, 2), 3) + + def test_default_args(self): + f = lambda x, y=10: x + y + self.assertEqual(f(1), 11) + self.assertEqual(f(1, 2), 3) + + def test_varargs(self): + f = lambda *args: sum(args) + self.assertEqual(f(1, 2, 3), 6) + + def test_kwargs(self): + f = lambda **kw: sorted(kw.items()) + self.assertEqual(f(a=1, b=2), [('a', 1), ('b', 2)]) + + def test_closure(self): + def make_adder(n): + return lambda x: x + n + add5 = make_adder(5) + self.assertEqual(add5(3), 8) + self.assertEqual(add5(10), 15) + + def test_nested_lambda(self): + f = lambda x: (lambda y: x + y) + self.assertEqual(f(10)(20), 30) + + def test_in_list(self): + funcs = [lambda x, i=i: x + i for i in range(5)] + results = [f(10) for f in funcs] + self.assertEqual(results, [10, 11, 12, 13, 14]) + + def test_conditional(self): + f = lambda x: "pos" if x > 0 else "non-pos" + self.assertEqual(f(1), "pos") + self.assertEqual(f(0), "non-pos") + self.assertEqual(f(-1), "non-pos") + + def test_as_callback(self): + data = [3, 1, 4, 1, 5, 9] + self.assertEqual(sorted(data, key=lambda x: -x), + [9, 5, 4, 3, 1, 1]) + + def test_immediately_invoked(self): + result = (lambda x, y: x * y)(6, 7) + self.assertEqual(result, 42) + + def test_map_filter(self): + self.assertEqual(list(map(lambda x: x**2, range(5))), + [0, 1, 4, 9, 16]) + self.assertEqual(list(filter(lambda x: x % 2, range(8))), + [1, 3, 5, 7]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_property.py b/tests/cpython/test_property.py new file mode 100644 index 0000000..112cdab --- /dev/null +++ b/tests/cpython/test_property.py @@ -0,0 +1,108 @@ +"""Tests for property descriptors — adapted from CPython test_property.py""" + +import unittest + + +class PropertyTest(unittest.TestCase): + + def test_basic_property(self): + class C: + def __init__(self): + self._x = 0 + @property + def x(self): + return self._x + @x.setter + def x(self, value): + self._x = value + obj = C() + self.assertEqual(obj.x, 0) + obj.x = 42 + self.assertEqual(obj.x, 42) + + def test_readonly_property(self): + class C: + @property + def x(self): + return 42 + obj = C() + self.assertEqual(obj.x, 42) + + @unittest.skip("property deleter not implemented") + def test_property_with_delete(self): + pass + + def test_computed_property(self): + class Circle: + def __init__(self, radius): + self.radius = radius + @property + def area(self): + return 3.14159 * self.radius ** 2 + c = Circle(10) + self.assertAlmostEqual(c.area, 314.159) + + def test_property_inheritance(self): + class Base: + @property + def x(self): + return 1 + class Child(Base): + pass + self.assertEqual(Child().x, 1) + + def test_property_old_style(self): + class C: + def getx(self): + return self._x + def setx(self, val): + self._x = val + x = property(getx, setx) + obj = C() + obj.x = 99 + self.assertEqual(obj.x, 99) + + +class StaticMethodTest(unittest.TestCase): + + def test_basic(self): + class C: + @staticmethod + def f(x): + return x + 1 + self.assertEqual(C.f(5), 6) + self.assertEqual(C().f(5), 6) + + def test_with_args(self): + class C: + @staticmethod + def add(a, b): + return a + b + self.assertEqual(C.add(3, 4), 7) + + +class ClassMethodTest(unittest.TestCase): + + def test_basic(self): + class C: + val = 10 + @classmethod + def f(cls): + return cls.val + self.assertEqual(C.f(), 10) + self.assertEqual(C().f(), 10) + + def test_inheritance(self): + class Base: + val = 1 + @classmethod + def f(cls): + return cls.val + class Child(Base): + val = 2 + self.assertEqual(Base.f(), 1) + self.assertEqual(Child.f(), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_string.py b/tests/cpython/test_string.py new file mode 100644 index 0000000..4d6ff92 --- /dev/null +++ b/tests/cpython/test_string.py @@ -0,0 +1,123 @@ +"""Tests for string operations — adapted from CPython test_string.py""" + +import unittest + + +class StringFormattingTest(unittest.TestCase): + + def test_fstring_basic(self): + x = 42 + self.assertEqual(f"{x}", "42") + self.assertEqual(f"val={x}", "val=42") + self.assertEqual(f"{x + 1}", "43") + + def test_fstring_expressions(self): + self.assertEqual(f"{2 + 3}", "5") + self.assertEqual(f"{'hello'}", "hello") + self.assertEqual(f"{len('abc')}", "3") + + def test_fstring_nested(self): + name = "world" + self.assertEqual(f"hello {name}!", "hello world!") + self.assertEqual(f"{'hello'} {'world'}", "hello world") + + def test_percent_format(self): + self.assertEqual("hello %s" % "world", "hello world") + self.assertEqual("%d items" % 5, "5 items") + self.assertEqual("%r" % "test", "'test'") + + def test_percent_tuple(self): + self.assertEqual("%s and %s" % ("foo", "bar"), "foo and bar") + self.assertEqual("%d + %d = %d" % (1, 2, 3), "1 + 2 = 3") + + +class StringMethodTest(unittest.TestCase): + + def test_upper_lower(self): + self.assertEqual("hello".upper(), "HELLO") + self.assertEqual("HELLO".lower(), "hello") + + def test_strip(self): + self.assertEqual(" hello ".strip(), "hello") + self.assertEqual(" hello ".lstrip(), "hello ") + self.assertEqual(" hello ".rstrip(), " hello") + + def test_split_join(self): + self.assertEqual("a,b,c".split(","), ["a", "b", "c"]) + self.assertEqual(",".join(["a", "b", "c"]), "a,b,c") + self.assertEqual("hello world".split(), ["hello", "world"]) + + def test_find_replace(self): + self.assertEqual("hello".find("ll"), 2) + self.assertEqual("hello".find("xx"), -1) + self.assertEqual("hello".replace("ll", "LL"), "heLLo") + + def test_startswith_endswith(self): + self.assertTrue("hello".startswith("hel")) + self.assertFalse("hello".startswith("xyz")) + self.assertTrue("hello".endswith("llo")) + self.assertFalse("hello".endswith("xyz")) + + def test_count(self): + self.assertEqual("hello".count("l"), 2) + self.assertEqual("hello".count("x"), 0) + + def test_isdigit_isalpha(self): + self.assertTrue("123".isdigit()) + self.assertFalse("12a".isdigit()) + self.assertTrue("abc".isalpha()) + self.assertFalse("ab1".isalpha()) + + def test_zfill(self): + self.assertEqual("42".zfill(5), "00042") + self.assertEqual("-42".zfill(5), "-0042") + + def test_center_ljust_rjust(self): + self.assertEqual("hi".center(6), " hi ") + self.assertEqual("hi".ljust(6), "hi ") + self.assertEqual("hi".rjust(6), " hi") + + def test_encode(self): + self.assertEqual("hello".encode(), b"hello") + self.assertIsInstance("hello".encode(), bytes) + + +class StringSlicingTest(unittest.TestCase): + + def test_indexing(self): + s = "hello" + self.assertEqual(s[0], "h") + self.assertEqual(s[-1], "o") + self.assertEqual(s[1], "e") + + def test_slicing(self): + s = "hello" + self.assertEqual(s[1:3], "el") + self.assertEqual(s[:3], "hel") + self.assertEqual(s[3:], "lo") + self.assertEqual(s[:], "hello") + self.assertEqual(s[::2], "hlo") + self.assertEqual(s[::-1], "olleh") + + def test_len(self): + self.assertEqual(len(""), 0) + self.assertEqual(len("hello"), 5) + + def test_in(self): + self.assertIn("ell", "hello") + self.assertNotIn("xyz", "hello") + self.assertIn("", "hello") + + def test_concatenation(self): + self.assertEqual("hello" + " " + "world", "hello world") + self.assertEqual("ab" * 3, "ababab") + + def test_comparison(self): + self.assertTrue("abc" < "abd") + self.assertTrue("abc" == "abc") + self.assertTrue("abc" != "xyz") + self.assertTrue("abc" <= "abc") + + +if __name__ == "__main__": + unittest.main() From 7d41b52a006a40ca228f156ae2421aae04f8dbfc Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 04:05:32 +0000 Subject: [PATCH 09/16] Import test_bytes, test_builtin, test_types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tests: - test_bytes.py: 8 tests — literals, indexing, slicing, comparison, decode, methods - test_builtin.py: 23 tests — abs, bool, chr/ord, divmod, hash, hex/oct/bin, id, int, isinstance, len, max/min, pow, range, repr, round, sorted, str, sum, type, zip, enumerate, map/filter, callable - test_types.py: 21 tests, 1 skip — truth values, type checks, conversions (int, float, str, bool, list, tuple, set) Total CPython test suites: 37 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 12 +++ tests/cpython/test_builtin.py | 147 ++++++++++++++++++++++++++++++++++ tests/cpython/test_bytes.py | 47 +++++++++++ tests/cpython/test_types.py | 132 ++++++++++++++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 tests/cpython/test_builtin.py create mode 100644 tests/cpython/test_bytes.py create mode 100644 tests/cpython/test_types.py diff --git a/Makefile b/Makefile index 57a5429..1425485 100644 --- a/Makefile +++ b/Makefile @@ -124,6 +124,12 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_property.py @echo "Compiling tests/cpython/test_string.py..." @$(PYTHON) -m py_compile tests/cpython/test_string.py + @echo "Compiling tests/cpython/test_bytes.py..." + @$(PYTHON) -m py_compile tests/cpython/test_bytes.py + @echo "Compiling tests/cpython/test_builtin.py..." + @$(PYTHON) -m py_compile tests/cpython/test_builtin.py + @echo "Compiling tests/cpython/test_types.py..." + @$(PYTHON) -m py_compile tests/cpython/test_types.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -195,3 +201,9 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_property.cpython-312.pyc @echo "Running CPython test_string.py..." @./apython tests/cpython/__pycache__/test_string.cpython-312.pyc + @echo "Running CPython test_bytes.py..." + @./apython tests/cpython/__pycache__/test_bytes.cpython-312.pyc + @echo "Running CPython test_builtin.py..." + @./apython tests/cpython/__pycache__/test_builtin.cpython-312.pyc + @echo "Running CPython test_types.py..." + @./apython tests/cpython/__pycache__/test_types.cpython-312.pyc diff --git a/tests/cpython/test_builtin.py b/tests/cpython/test_builtin.py new file mode 100644 index 0000000..374ae49 --- /dev/null +++ b/tests/cpython/test_builtin.py @@ -0,0 +1,147 @@ +"""Tests for builtin functions — adapted from CPython test_builtin.py""" + +import unittest + + +class BuiltinTest(unittest.TestCase): + + def test_abs(self): + self.assertEqual(abs(0), 0) + self.assertEqual(abs(-1), 1) + self.assertEqual(abs(1), 1) + self.assertAlmostEqual(abs(-1.5), 1.5) + + def test_bool(self): + self.assertIs(bool(0), False) + self.assertIs(bool(1), True) + self.assertIs(bool(""), False) + self.assertIs(bool("x"), True) + self.assertIs(bool([]), False) + self.assertIs(bool([1]), True) + self.assertIs(bool(None), False) + + def test_chr_ord(self): + self.assertEqual(chr(65), 'A') + self.assertEqual(chr(97), 'a') + self.assertEqual(ord('A'), 65) + self.assertEqual(ord('a'), 97) + + def test_divmod(self): + self.assertEqual(divmod(7, 3), (2, 1)) + self.assertEqual(divmod(-7, 3), (-3, 2)) + self.assertEqual(divmod(7, -3), (-3, -2)) + + def test_hash(self): + self.assertEqual(hash(42), hash(42)) + self.assertEqual(hash("hello"), hash("hello")) + self.assertIsInstance(hash(42), int) + + def test_hex_oct_bin(self): + self.assertEqual(hex(255), '0xff') + self.assertEqual(hex(-1), '-0x1') + self.assertEqual(oct(8), '0o10') + self.assertEqual(bin(10), '0b1010') + + def test_id(self): + a = [1, 2] + b = a + c = [1, 2] + self.assertEqual(id(a), id(b)) + self.assertNotEqual(id(a), id(c)) + + def test_int(self): + self.assertEqual(int(), 0) + self.assertEqual(int(3.5), 3) + self.assertEqual(int("42"), 42) + self.assertEqual(int("-10"), -10) + self.assertEqual(int("ff", 16), 255) + self.assertEqual(int("10", 2), 2) + + def test_isinstance_issubclass(self): + self.assertTrue(isinstance(1, int)) + self.assertTrue(isinstance("x", str)) + self.assertTrue(isinstance([], list)) + self.assertTrue(issubclass(bool, int)) + self.assertFalse(issubclass(str, int)) + + def test_len(self): + self.assertEqual(len([]), 0) + self.assertEqual(len([1, 2, 3]), 3) + self.assertEqual(len("hello"), 5) + self.assertEqual(len({}), 0) + self.assertEqual(len({1: 2}), 1) + + def test_max_min(self): + self.assertEqual(max(1, 2, 3), 3) + self.assertEqual(min(1, 2, 3), 1) + self.assertEqual(max([1, 2, 3]), 3) + self.assertEqual(min([1, 2, 3]), 1) + + def test_pow(self): + self.assertEqual(pow(2, 10), 1024) + self.assertEqual(pow(3, 3, 8), 3) + + def test_range(self): + self.assertEqual(list(range(5)), [0, 1, 2, 3, 4]) + self.assertEqual(list(range(1, 5)), [1, 2, 3, 4]) + self.assertEqual(list(range(0, 10, 2)), [0, 2, 4, 6, 8]) + self.assertEqual(list(range(5, 0, -1)), [5, 4, 3, 2, 1]) + + def test_repr(self): + self.assertEqual(repr(42), '42') + self.assertEqual(repr("hello"), "'hello'") + self.assertEqual(repr([1, 2]), '[1, 2]') + self.assertEqual(repr(None), 'None') + + def test_round(self): + self.assertEqual(round(3.5), 4) + self.assertEqual(round(4.5), 4) # banker's rounding + self.assertEqual(round(3.14159, 2), 3.14) + + def test_sorted(self): + self.assertEqual(sorted([3, 1, 2]), [1, 2, 3]) + self.assertEqual(sorted("cba"), ['a', 'b', 'c']) + self.assertEqual(sorted([3, 1, 2], reverse=True), [3, 2, 1]) + + def test_str(self): + self.assertEqual(str(42), '42') + self.assertEqual(str(3.14), '3.14') + self.assertEqual(str(True), 'True') + self.assertEqual(str(None), 'None') + self.assertEqual(str([1, 2]), '[1, 2]') + + def test_sum(self): + self.assertEqual(sum([1, 2, 3]), 6) + self.assertEqual(sum([], 10), 10) + self.assertEqual(sum(range(10)), 45) + + def test_type(self): + self.assertIs(type(42), int) + self.assertIs(type("x"), str) + self.assertIs(type([]), list) + self.assertIs(type({}), dict) + self.assertIs(type(()), tuple) + self.assertIs(type(True), bool) + self.assertIs(type(None), type(None)) + + def test_zip(self): + self.assertEqual(list(zip([1, 2], [3, 4])), [(1, 3), (2, 4)]) + self.assertEqual(list(zip()), []) + self.assertEqual(list(zip([1])), [(1,)]) + + def test_enumerate(self): + self.assertEqual(list(enumerate('ab')), [(0, 'a'), (1, 'b')]) + self.assertEqual(list(enumerate('ab', 5)), [(5, 'a'), (6, 'b')]) + + def test_map_filter(self): + self.assertEqual(list(map(str, [1, 2, 3])), ['1', '2', '3']) + self.assertEqual(list(filter(lambda x: x > 2, [1, 2, 3, 4])), [3, 4]) + + def test_callable(self): + self.assertTrue(callable(len)) + self.assertTrue(callable(lambda: None)) + self.assertFalse(callable(42)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_bytes.py b/tests/cpython/test_bytes.py new file mode 100644 index 0000000..57c2255 --- /dev/null +++ b/tests/cpython/test_bytes.py @@ -0,0 +1,47 @@ +"""Tests for bytes — adapted from CPython test_bytes.py""" + +import unittest + + +class BytesTest(unittest.TestCase): + + def test_literal(self): + self.assertEqual(b"hello", b"hello") + self.assertEqual(b"", b"") + + def test_len(self): + self.assertEqual(len(b""), 0) + self.assertEqual(len(b"hello"), 5) + + def test_indexing(self): + b = b"hello" + self.assertEqual(b[0], 104) # ord('h') + self.assertEqual(b[-1], 111) # ord('o') + + def test_slicing(self): + b = b"hello" + self.assertEqual(b[1:3], b"el") + self.assertEqual(b[:3], b"hel") + self.assertEqual(b[3:], b"lo") + + def test_comparison(self): + self.assertTrue(b"abc" == b"abc") + self.assertTrue(b"abc" != b"abd") + + def test_decode(self): + self.assertEqual(b"hello".decode(), "hello") + + def test_iteration(self): + result = [] + for x in b"abc": + result.append(x) + self.assertEqual(result, [97, 98, 99]) + + def test_methods(self): + self.assertTrue(b"hello".startswith(b"hel")) + self.assertTrue(b"hello".endswith(b"llo")) + self.assertEqual(b"hello".find(b"ll"), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_types.py b/tests/cpython/test_types.py new file mode 100644 index 0000000..9546f80 --- /dev/null +++ b/tests/cpython/test_types.py @@ -0,0 +1,132 @@ +"""Tests for type system basics — adapted from CPython test_types.py""" + +import unittest + + +class TypeTest(unittest.TestCase): + + def test_truth_values(self): + # Falsy values + self.assertFalse(bool(None)) + self.assertFalse(bool(0)) + self.assertFalse(bool(0.0)) + self.assertFalse(bool('')) + self.assertFalse(bool([])) + self.assertFalse(bool(())) + self.assertFalse(bool({})) + self.assertFalse(bool(set())) + self.assertFalse(bool(False)) + self.assertFalse(bool(b'')) + + # Truthy values + self.assertTrue(bool(1)) + self.assertTrue(bool(-1)) + self.assertTrue(bool(0.1)) + self.assertTrue(bool('x')) + self.assertTrue(bool([0])) + self.assertTrue(bool((0,))) + self.assertTrue(bool({0: 0})) + self.assertTrue(bool({0})) + self.assertTrue(bool(True)) + self.assertTrue(bool(b'x')) + + def test_none_type(self): + self.assertIs(type(None), type(None)) + self.assertEqual(repr(None), 'None') + self.assertEqual(str(None), 'None') + + def test_int_type(self): + self.assertIs(type(1), int) + self.assertIs(type(True), bool) + self.assertTrue(issubclass(bool, int)) + + def test_float_type(self): + self.assertIs(type(1.0), float) + + def test_str_type(self): + self.assertIs(type(""), str) + + def test_list_type(self): + self.assertIs(type([]), list) + + def test_dict_type(self): + self.assertIs(type({}), dict) + + def test_tuple_type(self): + self.assertIs(type(()), tuple) + + def test_set_type(self): + self.assertIs(type(set()), set) + + def test_bytes_type(self): + self.assertIs(type(b""), bytes) + + def test_function_type(self): + def f(): pass + self.assertEqual(type(f).__name__, 'function') + + def test_method_type(self): + class C: + def f(self): pass + obj = C() + self.assertTrue(callable(obj.f)) + + def test_type_of_type(self): + self.assertIs(type(int), type) + self.assertIs(type(str), type) + self.assertIs(type(list), type) + + +class ConversionTest(unittest.TestCase): + + def test_int_conversions(self): + self.assertEqual(int(3.9), 3) + self.assertEqual(int(-3.9), -3) + self.assertEqual(int("100"), 100) + self.assertEqual(int("0xff", 16), 255) + self.assertEqual(int(True), 1) + self.assertEqual(int(False), 0) + + def test_float_conversions(self): + self.assertEqual(float(3), 3.0) + self.assertEqual(float("3.14"), 3.14) + self.assertEqual(float(True), 1.0) + self.assertEqual(float(False), 0.0) + + def test_str_conversions(self): + self.assertEqual(str(42), "42") + self.assertEqual(str(3.14), "3.14") + self.assertEqual(str(True), "True") + self.assertEqual(str(False), "False") + self.assertEqual(str(None), "None") + self.assertEqual(str([1, 2]), "[1, 2]") + self.assertEqual(str((1, 2)), "(1, 2)") + + def test_bool_conversions(self): + self.assertIs(bool(0), False) + self.assertIs(bool(1), True) + self.assertIs(bool(""), False) + self.assertIs(bool("x"), True) + + def test_list_conversions(self): + self.assertEqual(list("abc"), ['a', 'b', 'c']) + self.assertEqual(list((1, 2, 3)), [1, 2, 3]) + self.assertEqual(list(range(3)), [0, 1, 2]) + self.assertEqual(list({1, 2, 3}), sorted([1, 2, 3])) + + def test_tuple_conversions(self): + self.assertEqual(tuple([1, 2, 3]), (1, 2, 3)) + self.assertEqual(tuple("abc"), ('a', 'b', 'c')) + self.assertEqual(tuple(range(3)), (0, 1, 2)) + + def test_set_conversions(self): + self.assertEqual(set([1, 2, 2, 3]), {1, 2, 3}) + self.assertEqual(frozenset([1, 2, 3]), frozenset({1, 2, 3})) + + @unittest.skip("dict() from kwargs not implemented") + def test_dict_conversions(self): + pass + + +if __name__ == "__main__": + unittest.main() From 2e674b68635f087b6b83d5c483d5ae8bb76e8adc Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 06:07:57 +0000 Subject: [PATCH 10/16] Add assertIsNone; import test_closures, test_dict_extra, test_tuple_extra, test_set_extra, test_list_extra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infrastructure: - assertIsNone/assertIsNotNone added to unittest New tests: - test_closures.py: 11 tests — closures, nonlocal, nested scopes, lambda closures - test_dict_extra.py: 20 tests — constructor, get/set/del, keys/values/items, get/pop/update/setdefault/clear/copy, comprehension, fromkeys, nested - test_tuple_extra.py: 18 tests — indexing, slicing, concat, count, index, unpacking, star-unpacking, constructor, nested, mixed types - test_set_extra.py: 20 tests, 1 skip — add/discard/remove/pop/clear, union/intersection/difference/symmetric_difference, copy, comprehension, frozenset, isdisjoint - test_list_extra.py: 25 tests — append/extend/insert/remove/pop/index/count, reverse/sort/copy/clear, slicing, multiplication, comparison, self-ref repr Total CPython test suites: 42 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 20 +++++ lib/unittest/case.py | 10 +++ tests/cpython/test_closures.py | 121 +++++++++++++++++++++++++ tests/cpython/test_dict_extra.py | 127 ++++++++++++++++++++++++++ tests/cpython/test_list_extra.py | 142 ++++++++++++++++++++++++++++++ tests/cpython/test_set_extra.py | 107 ++++++++++++++++++++++ tests/cpython/test_tuple_extra.py | 108 +++++++++++++++++++++++ 7 files changed, 635 insertions(+) create mode 100644 tests/cpython/test_closures.py create mode 100644 tests/cpython/test_dict_extra.py create mode 100644 tests/cpython/test_list_extra.py create mode 100644 tests/cpython/test_set_extra.py create mode 100644 tests/cpython/test_tuple_extra.py diff --git a/Makefile b/Makefile index 1425485..5b42a54 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,16 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_builtin.py @echo "Compiling tests/cpython/test_types.py..." @$(PYTHON) -m py_compile tests/cpython/test_types.py + @echo "Compiling tests/cpython/test_closures.py..." + @$(PYTHON) -m py_compile tests/cpython/test_closures.py + @echo "Compiling tests/cpython/test_dict_extra.py..." + @$(PYTHON) -m py_compile tests/cpython/test_dict_extra.py + @echo "Compiling tests/cpython/test_tuple_extra.py..." + @$(PYTHON) -m py_compile tests/cpython/test_tuple_extra.py + @echo "Compiling tests/cpython/test_set_extra.py..." + @$(PYTHON) -m py_compile tests/cpython/test_set_extra.py + @echo "Compiling tests/cpython/test_list_extra.py..." + @$(PYTHON) -m py_compile tests/cpython/test_list_extra.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -207,3 +217,13 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_builtin.cpython-312.pyc @echo "Running CPython test_types.py..." @./apython tests/cpython/__pycache__/test_types.cpython-312.pyc + @echo "Running CPython test_closures.py..." + @./apython tests/cpython/__pycache__/test_closures.cpython-312.pyc + @echo "Running CPython test_dict_extra.py..." + @./apython tests/cpython/__pycache__/test_dict_extra.cpython-312.pyc + @echo "Running CPython test_tuple_extra.py..." + @./apython tests/cpython/__pycache__/test_tuple_extra.cpython-312.pyc + @echo "Running CPython test_set_extra.py..." + @./apython tests/cpython/__pycache__/test_set_extra.cpython-312.pyc + @echo "Running CPython test_list_extra.py..." + @./apython tests/cpython/__pycache__/test_list_extra.cpython-312.pyc diff --git a/lib/unittest/case.py b/lib/unittest/case.py index ba27333..e66ca06 100644 --- a/lib/unittest/case.py +++ b/lib/unittest/case.py @@ -177,6 +177,16 @@ def assertIs(self, first, second, msg=None): raise AssertionError(msg) raise AssertionError("%r is not %r" % (first, second)) + def assertIsNone(self, obj, msg=None): + if obj is not None: + if msg: + raise AssertionError(msg) + raise AssertionError("%r is not None" % (obj,)) + + def assertIsNotNone(self, obj, msg=None): + if obj is None: + raise AssertionError(msg or "unexpectedly None") + def assertIsNot(self, first, second, msg=None): if first is second: if msg: diff --git a/tests/cpython/test_closures.py b/tests/cpython/test_closures.py new file mode 100644 index 0000000..86c928e --- /dev/null +++ b/tests/cpython/test_closures.py @@ -0,0 +1,121 @@ +"""Tests for closures and nonlocal — adapted from CPython tests""" + +import unittest + + +class ClosureTest(unittest.TestCase): + + def test_basic_closure(self): + def outer(x): + def inner(): + return x + return inner + self.assertEqual(outer(42)(), 42) + + def test_closure_mutation(self): + def counter(): + n = 0 + def inc(): + nonlocal n + n += 1 + return n + return inc + c = counter() + self.assertEqual(c(), 1) + self.assertEqual(c(), 2) + self.assertEqual(c(), 3) + + def test_multiple_closures(self): + def make_pair(x): + def get(): + return x + def set_(val): + nonlocal x + x = val + return get, set_ + g, s = make_pair(10) + self.assertEqual(g(), 10) + s(20) + self.assertEqual(g(), 20) + + def test_nested_closures(self): + def outer(x): + def middle(y): + def inner(): + return x + y + return inner + return middle + self.assertEqual(outer(10)(20)(), 30) + + def test_closure_in_loop(self): + funcs = [] + for i in range(5): + def f(n=i): + return n + funcs.append(f) + self.assertEqual([f() for f in funcs], [0, 1, 2, 3, 4]) + + def test_shared_closure(self): + def make_adders(): + fns = [] + for i in range(3): + def adder(x, i=i): + return x + i + fns.append(adder) + return fns + adders = make_adders() + self.assertEqual(adders[0](10), 10) + self.assertEqual(adders[1](10), 11) + self.assertEqual(adders[2](10), 12) + + def test_closure_over_param(self): + def factory(greeting): + def greet(name): + return greeting + " " + name + return greet + hello = factory("hello") + self.assertEqual(hello("world"), "hello world") + + def test_nonlocal_in_nested(self): + result = [] + def outer(): + x = 0 + def inner(): + nonlocal x + x += 1 + result.append(x) + inner() + inner() + inner() + outer() + self.assertEqual(result, [1, 2, 3]) + + def test_closure_with_defaults(self): + def make_pow(exp): + def power(base): + return base ** exp + return power + square = make_pow(2) + cube = make_pow(3) + self.assertEqual(square(5), 25) + self.assertEqual(cube(3), 27) + + def test_lambda_closure(self): + def make_adder(n): + return lambda x: x + n + add10 = make_adder(10) + self.assertEqual(add10(5), 15) + + def test_closure_survives_outer(self): + def make(): + data = [1, 2, 3] + def get(): + return data + return get + g = make() + self.assertEqual(g(), [1, 2, 3]) + self.assertEqual(g(), [1, 2, 3]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_dict_extra.py b/tests/cpython/test_dict_extra.py new file mode 100644 index 0000000..76d7914 --- /dev/null +++ b/tests/cpython/test_dict_extra.py @@ -0,0 +1,127 @@ +"""Extra dict tests — adapted from CPython test_dict.py""" + +import unittest + + +class DictTest(unittest.TestCase): + + def test_constructor(self): + self.assertEqual(dict(), {}) + self.assertEqual(dict({}), {}) + + def test_literal(self): + d = {'a': 1, 'b': 2, 'c': 3} + self.assertEqual(len(d), 3) + self.assertEqual(d['a'], 1) + + def test_setitem_getitem(self): + d = {} + d['key'] = 'value' + self.assertEqual(d['key'], 'value') + d['key'] = 'new' + self.assertEqual(d['key'], 'new') + + def test_delitem(self): + d = {'a': 1, 'b': 2} + del d['a'] + self.assertEqual(d, {'b': 2}) + with self.assertRaises(KeyError): + del d['a'] + + def test_contains(self): + d = {'a': 1, 'b': 2} + self.assertIn('a', d) + self.assertNotIn('c', d) + + def test_len(self): + self.assertEqual(len({}), 0) + self.assertEqual(len({'a': 1}), 1) + self.assertEqual(len({'a': 1, 'b': 2}), 2) + + def test_keys_values_items(self): + d = {'a': 1, 'b': 2} + self.assertEqual(sorted(d.keys()), ['a', 'b']) + self.assertEqual(sorted(d.values()), [1, 2]) + self.assertEqual(sorted(d.items()), [('a', 1), ('b', 2)]) + + def test_get(self): + d = {'a': 1} + self.assertEqual(d.get('a'), 1) + self.assertIsNone(d.get('b')) + self.assertEqual(d.get('b', 42), 42) + + def test_pop(self): + d = {'a': 1, 'b': 2} + self.assertEqual(d.pop('a'), 1) + self.assertEqual(d, {'b': 2}) + self.assertEqual(d.pop('c', 99), 99) + self.assertRaises(KeyError, d.pop, 'c') + + def test_update(self): + d = {'a': 1} + d.update({'b': 2, 'c': 3}) + self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3}) + d.update({'a': 10}) + self.assertEqual(d['a'], 10) + + def test_setdefault(self): + d = {'a': 1} + self.assertEqual(d.setdefault('a', 99), 1) + self.assertEqual(d.setdefault('b', 99), 99) + self.assertEqual(d['b'], 99) + + def test_clear(self): + d = {'a': 1, 'b': 2} + d.clear() + self.assertEqual(d, {}) + self.assertEqual(len(d), 0) + + def test_copy(self): + d = {'a': 1, 'b': [2, 3]} + d2 = d.copy() + self.assertEqual(d, d2) + d2['a'] = 99 + self.assertEqual(d['a'], 1) + + def test_iteration(self): + d = {'a': 1, 'b': 2, 'c': 3} + keys = [] + for k in d: + keys.append(k) + self.assertEqual(sorted(keys), ['a', 'b', 'c']) + + def test_comprehension(self): + d = {x: x**2 for x in range(5)} + self.assertEqual(len(d), 5) + self.assertEqual(d[3], 9) + self.assertEqual(d[4], 16) + + def test_equality(self): + self.assertEqual({'a': 1}, {'a': 1}) + self.assertNotEqual({'a': 1}, {'a': 2}) + self.assertNotEqual({'a': 1}, {'b': 1}) + + def test_bool(self): + self.assertFalse(bool({})) + self.assertTrue(bool({'a': 1})) + + def test_mixed_key_types(self): + d = {1: 'int', 'a': 'str', (1, 2): 'tuple'} + self.assertEqual(d[1], 'int') + self.assertEqual(d['a'], 'str') + self.assertEqual(d[(1, 2)], 'tuple') + + def test_fromkeys(self): + d = dict.fromkeys(['a', 'b', 'c'], 0) + self.assertEqual(len(d), 3) + self.assertEqual(d['a'], 0) + self.assertEqual(d['c'], 0) + + def test_nested(self): + d = {'a': {'x': 1}, 'b': {'y': 2}} + self.assertEqual(d['a']['x'], 1) + self.assertEqual(d['b']['y'], 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_list_extra.py b/tests/cpython/test_list_extra.py new file mode 100644 index 0000000..3890b3d --- /dev/null +++ b/tests/cpython/test_list_extra.py @@ -0,0 +1,142 @@ +"""Extra list tests — adapted from CPython test_list.py""" + +import unittest + + +class ListExtraTest(unittest.TestCase): + + def test_literal(self): + self.assertEqual([], []) + self.assertEqual([1, 2, 3], [1, 2, 3]) + + def test_constructor(self): + self.assertEqual(list(), []) + self.assertEqual(list((1, 2, 3)), [1, 2, 3]) + self.assertEqual(list("abc"), ['a', 'b', 'c']) + self.assertEqual(list(range(5)), [0, 1, 2, 3, 4]) + + def test_append_extend(self): + a = [1] + a.append(2) + a.extend([3, 4]) + self.assertEqual(a, [1, 2, 3, 4]) + + def test_insert(self): + a = [1, 3] + a.insert(1, 2) + self.assertEqual(a, [1, 2, 3]) + a.insert(0, 0) + self.assertEqual(a, [0, 1, 2, 3]) + + def test_remove(self): + a = [1, 2, 3, 2] + a.remove(2) + self.assertEqual(a, [1, 3, 2]) + self.assertRaises(ValueError, a.remove, 99) + + def test_pop(self): + a = [1, 2, 3] + self.assertEqual(a.pop(), 3) + self.assertEqual(a, [1, 2]) + self.assertEqual(a.pop(0), 1) + self.assertEqual(a, [2]) + + def test_index(self): + a = [1, 2, 3, 2] + self.assertEqual(a.index(2), 1) + self.assertRaises(ValueError, a.index, 99) + + def test_count(self): + a = [1, 2, 2, 3, 2] + self.assertEqual(a.count(2), 3) + self.assertEqual(a.count(4), 0) + + def test_reverse(self): + a = [1, 2, 3] + a.reverse() + self.assertEqual(a, [3, 2, 1]) + + def test_sort(self): + a = [3, 1, 4, 1, 5] + a.sort() + self.assertEqual(a, [1, 1, 3, 4, 5]) + a.sort(reverse=True) + self.assertEqual(a, [5, 4, 3, 1, 1]) + + def test_copy(self): + a = [1, 2, 3] + b = a.copy() + self.assertEqual(a, b) + b.append(4) + self.assertNotEqual(a, b) + + def test_clear(self): + a = [1, 2, 3] + a.clear() + self.assertEqual(a, []) + + def test_slicing(self): + a = [0, 1, 2, 3, 4] + self.assertEqual(a[1:3], [1, 2]) + self.assertEqual(a[::-1], [4, 3, 2, 1, 0]) + self.assertEqual(a[::2], [0, 2, 4]) + + def test_slice_assignment(self): + a = [0, 1, 2, 3, 4] + a[1:3] = [10, 20, 30] + self.assertEqual(a, [0, 10, 20, 30, 3, 4]) + + def test_del_slice(self): + a = [0, 1, 2, 3, 4] + del a[1:3] + self.assertEqual(a, [0, 3, 4]) + + def test_multiply(self): + self.assertEqual([1, 2] * 3, [1, 2, 1, 2, 1, 2]) + self.assertEqual([0] * 5, [0, 0, 0, 0, 0]) + + def test_add(self): + self.assertEqual([1, 2] + [3, 4], [1, 2, 3, 4]) + + def test_iadd(self): + a = [1, 2] + a += [3, 4] + self.assertEqual(a, [1, 2, 3, 4]) + + def test_imul(self): + a = [1, 2] + a *= 3 + self.assertEqual(a, [1, 2, 1, 2, 1, 2]) + + def test_contains(self): + a = [1, 2, 3] + self.assertIn(2, a) + self.assertNotIn(4, a) + + def test_comparison(self): + self.assertTrue([1, 2] == [1, 2]) + self.assertTrue([1, 2] != [1, 3]) + self.assertTrue([1, 2] < [1, 3]) + self.assertTrue([1, 2] < [1, 2, 3]) + + def test_bool(self): + self.assertFalse(bool([])) + self.assertTrue(bool([1])) + + def test_nested(self): + a = [[1, 2], [3, 4]] + self.assertEqual(a[0][1], 2) + self.assertEqual(a[1][0], 3) + + def test_comprehension(self): + self.assertEqual([x**2 for x in range(5)], [0, 1, 4, 9, 16]) + self.assertEqual([x for x in range(10) if x % 2 == 0], [0, 2, 4, 6, 8]) + + def test_self_referencing_repr(self): + a = [1, 2] + a.append(a) + self.assertEqual(repr(a), '[1, 2, [...]]') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_set_extra.py b/tests/cpython/test_set_extra.py new file mode 100644 index 0000000..0d32b95 --- /dev/null +++ b/tests/cpython/test_set_extra.py @@ -0,0 +1,107 @@ +"""Extra set tests — adapted from CPython test_set.py""" + +import unittest + + +class SetTest(unittest.TestCase): + + def test_literal(self): + self.assertEqual({1, 2, 3}, {1, 2, 3}) + self.assertEqual({1, 2, 2, 3}, {1, 2, 3}) + + def test_constructor(self): + self.assertEqual(set(), set()) + self.assertEqual(set([1, 2, 3]), {1, 2, 3}) + self.assertEqual(set("abc"), {'a', 'b', 'c'}) + + def test_len(self): + self.assertEqual(len(set()), 0) + self.assertEqual(len({1, 2, 3}), 3) + + def test_contains(self): + s = {1, 2, 3} + self.assertIn(2, s) + self.assertNotIn(4, s) + + def test_add_discard(self): + s = {1, 2} + s.add(3) + self.assertEqual(s, {1, 2, 3}) + s.discard(2) + self.assertEqual(s, {1, 3}) + s.discard(99) # no error + self.assertEqual(s, {1, 3}) + + def test_remove(self): + s = {1, 2, 3} + s.remove(2) + self.assertEqual(s, {1, 3}) + self.assertRaises(KeyError, s.remove, 99) + + def test_pop(self): + s = {1} + v = s.pop() + self.assertEqual(v, 1) + self.assertEqual(s, set()) + self.assertRaises(KeyError, s.pop) + + def test_clear(self): + s = {1, 2, 3} + s.clear() + self.assertEqual(s, set()) + + def test_union(self): + self.assertEqual({1, 2} | {2, 3}, {1, 2, 3}) + + def test_intersection(self): + self.assertEqual({1, 2, 3} & {2, 3, 4}, {2, 3}) + + def test_difference(self): + self.assertEqual({1, 2, 3} - {2, 3, 4}, {1}) + + def test_symmetric_difference(self): + self.assertEqual({1, 2, 3} ^ {2, 3, 4}, {1, 4}) + + @unittest.skip("set <= not routed through tp_richcompare") + def test_subset_superset(self): + pass + + def test_equality(self): + self.assertEqual({1, 2, 3}, {3, 2, 1}) + self.assertNotEqual({1, 2}, {1, 3}) + + def test_copy(self): + s = {1, 2, 3} + s2 = s.copy() + self.assertEqual(s, s2) + s2.add(4) + self.assertNotEqual(s, s2) + + def test_iteration(self): + s = {1, 2, 3} + result = sorted(list(s)) + self.assertEqual(result, [1, 2, 3]) + + def test_comprehension(self): + s = {x**2 for x in range(5)} + self.assertEqual(len(s), 5) + vals = sorted(list(s)) + self.assertEqual(vals, [0, 1, 4, 9, 16]) + + def test_bool(self): + self.assertFalse(bool(set())) + self.assertTrue(bool({1})) + + def test_frozenset(self): + fs = frozenset([1, 2, 3]) + self.assertEqual(len(fs), 3) + self.assertIn(2, fs) + self.assertEqual(fs, {1, 2, 3}) + + def test_isdisjoint(self): + self.assertTrue({1, 2}.isdisjoint({3, 4})) + self.assertFalse({1, 2}.isdisjoint({2, 3})) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_tuple_extra.py b/tests/cpython/test_tuple_extra.py new file mode 100644 index 0000000..b91f798 --- /dev/null +++ b/tests/cpython/test_tuple_extra.py @@ -0,0 +1,108 @@ +"""Extra tuple tests — adapted from CPython test_tuple.py""" + +import unittest + + +class TupleTest(unittest.TestCase): + + def test_literal(self): + self.assertEqual((), ()) + self.assertEqual((1,), (1,)) + self.assertEqual((1, 2, 3), (1, 2, 3)) + + def test_len(self): + self.assertEqual(len(()), 0) + self.assertEqual(len((1, 2, 3)), 3) + + def test_indexing(self): + t = (1, 2, 3, 4, 5) + self.assertEqual(t[0], 1) + self.assertEqual(t[-1], 5) + self.assertEqual(t[2], 3) + + def test_slicing(self): + t = (0, 1, 2, 3, 4) + self.assertEqual(t[1:3], (1, 2)) + self.assertEqual(t[:3], (0, 1, 2)) + self.assertEqual(t[3:], (3, 4)) + self.assertEqual(t[::-1], (4, 3, 2, 1, 0)) + + def test_concatenation(self): + self.assertEqual((1, 2) + (3, 4), (1, 2, 3, 4)) + self.assertEqual(() + (1,), (1,)) + + def test_repetition(self): + self.assertEqual((1, 2) * 3, (1, 2, 1, 2, 1, 2)) + self.assertEqual((0,) * 5, (0, 0, 0, 0, 0)) + + def test_contains(self): + t = (1, 2, 3) + self.assertIn(2, t) + self.assertNotIn(4, t) + + def test_comparison(self): + self.assertTrue((1, 2) == (1, 2)) + self.assertTrue((1, 2) != (1, 3)) + self.assertTrue((1, 2) < (1, 3)) + self.assertTrue((1, 2) < (1, 2, 3)) + self.assertTrue((1, 3) > (1, 2)) + + def test_count(self): + t = (1, 2, 2, 3, 2) + self.assertEqual(t.count(2), 3) + self.assertEqual(t.count(4), 0) + + def test_index(self): + t = (1, 2, 3, 2, 1) + self.assertEqual(t.index(2), 1) + self.assertEqual(t.index(3), 2) + self.assertRaises(ValueError, t.index, 99) + + def test_hash(self): + self.assertEqual(hash((1, 2)), hash((1, 2))) + # different tuples may have different hashes (not guaranteed but likely) + + def test_iteration(self): + result = [] + for x in (10, 20, 30): + result.append(x) + self.assertEqual(result, [10, 20, 30]) + + def test_unpacking(self): + a, b, c = (1, 2, 3) + self.assertEqual(a, 1) + self.assertEqual(b, 2) + self.assertEqual(c, 3) + + def test_star_unpacking(self): + a, *b = (1, 2, 3, 4) + self.assertEqual(a, 1) + self.assertEqual(b, [2, 3, 4]) + *a, b = (1, 2, 3, 4) + self.assertEqual(a, [1, 2, 3]) + self.assertEqual(b, 4) + + def test_constructor(self): + self.assertEqual(tuple(), ()) + self.assertEqual(tuple([1, 2, 3]), (1, 2, 3)) + self.assertEqual(tuple("abc"), ('a', 'b', 'c')) + self.assertEqual(tuple(range(5)), (0, 1, 2, 3, 4)) + + def test_nested(self): + t = ((1, 2), (3, 4)) + self.assertEqual(t[0], (1, 2)) + self.assertEqual(t[1][0], 3) + + def test_bool(self): + self.assertFalse(bool(())) + self.assertTrue(bool((1,))) + + def test_mixed_types(self): + t = (1, "two", 3.0, [4], None) + self.assertEqual(len(t), 5) + self.assertEqual(t[1], "two") + self.assertIsNone(t[4]) + + +if __name__ == "__main__": + unittest.main() From 0d9f9369c559aac8f7dca0cdc2a6e8df9302d435 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 06:17:12 +0000 Subject: [PATCH 11/16] Import test_controlflow, test_math_basic, test_global_nonlocal, test_unpacking, test_inheritance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tests: - test_controlflow.py: 23 tests — if/elif/else, while, for, break/continue, while/for-else, ternary, pass, nested loops - test_math_basic.py: 25 tests — int/float/bool arithmetic, bitwise ops, abs, divmod, large ints, mixed-type arithmetic - test_global_nonlocal.py: 12 tests — global/nonlocal statements, enclosing scope, class scope, comprehension scope isolation - test_unpacking.py: 14 tests, 3 skips — tuple/list unpacking, star unpacking, nested unpacking, swap, for-loop star unpacking - test_inheritance.py: 13 tests — single inheritance, super(), method resolution, isinstance/issubclass chains, exception inheritance Total CPython test suites: 47 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 20 +++ tests/cpython/test_controlflow.py | 201 ++++++++++++++++++++++++++ tests/cpython/test_global_nonlocal.py | 123 ++++++++++++++++ tests/cpython/test_inheritance.py | 143 ++++++++++++++++++ tests/cpython/test_math_basic.py | 131 +++++++++++++++++ tests/cpython/test_unpacking.py | 85 +++++++++++ 6 files changed, 703 insertions(+) create mode 100644 tests/cpython/test_controlflow.py create mode 100644 tests/cpython/test_global_nonlocal.py create mode 100644 tests/cpython/test_inheritance.py create mode 100644 tests/cpython/test_math_basic.py create mode 100644 tests/cpython/test_unpacking.py diff --git a/Makefile b/Makefile index 5b42a54..6c56778 100644 --- a/Makefile +++ b/Makefile @@ -140,6 +140,16 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_set_extra.py @echo "Compiling tests/cpython/test_list_extra.py..." @$(PYTHON) -m py_compile tests/cpython/test_list_extra.py + @echo "Compiling tests/cpython/test_controlflow.py..." + @$(PYTHON) -m py_compile tests/cpython/test_controlflow.py + @echo "Compiling tests/cpython/test_math_basic.py..." + @$(PYTHON) -m py_compile tests/cpython/test_math_basic.py + @echo "Compiling tests/cpython/test_global_nonlocal.py..." + @$(PYTHON) -m py_compile tests/cpython/test_global_nonlocal.py + @echo "Compiling tests/cpython/test_unpacking.py..." + @$(PYTHON) -m py_compile tests/cpython/test_unpacking.py + @echo "Compiling tests/cpython/test_inheritance.py..." + @$(PYTHON) -m py_compile tests/cpython/test_inheritance.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -227,3 +237,13 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_set_extra.cpython-312.pyc @echo "Running CPython test_list_extra.py..." @./apython tests/cpython/__pycache__/test_list_extra.cpython-312.pyc + @echo "Running CPython test_controlflow.py..." + @./apython tests/cpython/__pycache__/test_controlflow.cpython-312.pyc + @echo "Running CPython test_math_basic.py..." + @./apython tests/cpython/__pycache__/test_math_basic.cpython-312.pyc + @echo "Running CPython test_global_nonlocal.py..." + @./apython tests/cpython/__pycache__/test_global_nonlocal.cpython-312.pyc + @echo "Running CPython test_unpacking.py..." + @./apython tests/cpython/__pycache__/test_unpacking.cpython-312.pyc + @echo "Running CPython test_inheritance.py..." + @./apython tests/cpython/__pycache__/test_inheritance.cpython-312.pyc diff --git a/tests/cpython/test_controlflow.py b/tests/cpython/test_controlflow.py new file mode 100644 index 0000000..e931e27 --- /dev/null +++ b/tests/cpython/test_controlflow.py @@ -0,0 +1,201 @@ +"""Tests for control flow — if/elif/else, while, for, break, continue, pass""" + +import unittest + + +class IfTest(unittest.TestCase): + + def test_basic_if(self): + x = 10 + if x > 5: + result = "big" + else: + result = "small" + self.assertEqual(result, "big") + + def test_elif(self): + def classify(x): + if x < 0: + return "negative" + elif x == 0: + return "zero" + elif x < 10: + return "small" + else: + return "big" + self.assertEqual(classify(-5), "negative") + self.assertEqual(classify(0), "zero") + self.assertEqual(classify(5), "small") + self.assertEqual(classify(100), "big") + + def test_nested_if(self): + def f(x, y): + if x > 0: + if y > 0: + return "both positive" + else: + return "x positive" + else: + return "x not positive" + self.assertEqual(f(1, 1), "both positive") + self.assertEqual(f(1, -1), "x positive") + self.assertEqual(f(-1, 1), "x not positive") + + def test_ternary(self): + self.assertEqual("yes" if True else "no", "yes") + self.assertEqual("yes" if False else "no", "no") + x = 10 + self.assertEqual("big" if x > 5 else "small", "big") + + +class WhileTest(unittest.TestCase): + + def test_basic_while(self): + n = 0 + while n < 10: + n += 1 + self.assertEqual(n, 10) + + def test_while_break(self): + n = 0 + while True: + n += 1 + if n == 5: + break + self.assertEqual(n, 5) + + def test_while_continue(self): + result = [] + n = 0 + while n < 10: + n += 1 + if n % 2 == 0: + continue + result.append(n) + self.assertEqual(result, [1, 3, 5, 7, 9]) + + def test_while_else(self): + hit_else = False + n = 0 + while n < 3: + n += 1 + else: + hit_else = True + self.assertTrue(hit_else) + + def test_while_else_break(self): + hit_else = False + n = 0 + while n < 10: + n += 1 + if n == 5: + break + else: + hit_else = True + self.assertFalse(hit_else) + + +class ForTest(unittest.TestCase): + + def test_for_range(self): + total = 0 + for i in range(10): + total += i + self.assertEqual(total, 45) + + def test_for_list(self): + result = [] + for x in [1, 2, 3]: + result.append(x * 2) + self.assertEqual(result, [2, 4, 6]) + + def test_for_string(self): + chars = [] + for c in "hello": + chars.append(c) + self.assertEqual(chars, ['h', 'e', 'l', 'l', 'o']) + + def test_for_dict(self): + d = {'a': 1, 'b': 2} + keys = [] + for k in d: + keys.append(k) + self.assertEqual(sorted(keys), ['a', 'b']) + + def test_for_tuple_unpack(self): + pairs = [(1, 'a'), (2, 'b'), (3, 'c')] + nums = [] + chars = [] + for n, c in pairs: + nums.append(n) + chars.append(c) + self.assertEqual(nums, [1, 2, 3]) + self.assertEqual(chars, ['a', 'b', 'c']) + + def test_nested_for(self): + result = [] + for i in range(3): + for j in range(3): + if i == j: + result.append(i) + self.assertEqual(result, [0, 1, 2]) + + def test_for_break(self): + found = -1 + for i in range(100): + if i * i > 50: + found = i + break + self.assertEqual(found, 8) + + def test_for_continue(self): + evens = [] + for i in range(10): + if i % 2 != 0: + continue + evens.append(i) + self.assertEqual(evens, [0, 2, 4, 6, 8]) + + def test_for_else(self): + hit = False + for i in range(5): + pass + else: + hit = True + self.assertTrue(hit) + + def test_for_else_break(self): + hit = False + for i in range(5): + if i == 3: + break + else: + hit = True + self.assertFalse(hit) + + +class PassTest(unittest.TestCase): + + def test_pass_in_if(self): + if True: + pass + self.assertTrue(True) + + def test_pass_in_class(self): + class Empty: + pass + self.assertIsNotNone(Empty) + + def test_pass_in_function(self): + def f(): + pass + self.assertIsNone(f()) + + def test_pass_in_loop(self): + for i in range(5): + pass + self.assertEqual(i, 4) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_global_nonlocal.py b/tests/cpython/test_global_nonlocal.py new file mode 100644 index 0000000..eea3433 --- /dev/null +++ b/tests/cpython/test_global_nonlocal.py @@ -0,0 +1,123 @@ +"""Tests for global and nonlocal statements""" + +import unittest + + +class GlobalTest(unittest.TestCase): + + def test_global_read(self): + x = 42 + def f(): + return x + self.assertEqual(f(), 42) + + def test_global_write(self): + def f(): + global _test_global_var + _test_global_var = 99 + f() + self.assertEqual(_test_global_var, 99) + + def test_global_in_nested(self): + result = [] + def outer(): + def inner(): + global _test_nested_global + _test_nested_global = 42 + inner() + outer() + self.assertEqual(_test_nested_global, 42) + + +class NonlocalTest(unittest.TestCase): + + def test_nonlocal_read(self): + def outer(): + x = 10 + def inner(): + return x + return inner() + self.assertEqual(outer(), 10) + + def test_nonlocal_write(self): + def outer(): + x = 10 + def inner(): + nonlocal x + x = 20 + inner() + return x + self.assertEqual(outer(), 20) + + def test_nonlocal_counter(self): + def make_counter(): + count = 0 + def increment(): + nonlocal count + count += 1 + return count + return increment + c = make_counter() + self.assertEqual(c(), 1) + self.assertEqual(c(), 2) + self.assertEqual(c(), 3) + + def test_nonlocal_multi_level(self): + def outer(): + x = 0 + def middle(): + nonlocal x + x += 1 + def inner(): + nonlocal x + x += 10 + inner() + middle() + return x + self.assertEqual(outer(), 11) + + def test_nonlocal_multiple_vars(self): + def outer(): + a = 1 + b = 2 + def inner(): + nonlocal a, b + a, b = b, a + inner() + return a, b + self.assertEqual(outer(), (2, 1)) + + +class ScopeTest(unittest.TestCase): + + def test_local_shadows_global(self): + x = 100 + def f(): + x = 200 + return x + self.assertEqual(f(), 200) + self.assertEqual(x, 100) + + def test_enclosing_scope(self): + def outer(x): + def inner(y): + return x + y + return inner + add5 = outer(5) + self.assertEqual(add5(3), 8) + + def test_class_scope(self): + class C: + x = 42 + def get_x(self): + return self.x + self.assertEqual(C().get_x(), 42) + + def test_comprehension_scope(self): + x = 99 + _ = [x for x in range(5)] + self.assertEqual(x, 99) # comprehension doesn't leak + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_inheritance.py b/tests/cpython/test_inheritance.py new file mode 100644 index 0000000..a240eda --- /dev/null +++ b/tests/cpython/test_inheritance.py @@ -0,0 +1,143 @@ +"""Tests for inheritance and method resolution""" + +import unittest + + +class BasicInheritanceTest(unittest.TestCase): + + def test_single_inheritance(self): + class Animal: + def speak(self): + return "..." + class Dog(Animal): + def speak(self): + return "woof" + class Cat(Animal): + def speak(self): + return "meow" + self.assertEqual(Dog().speak(), "woof") + self.assertEqual(Cat().speak(), "meow") + + def test_inherit_method(self): + class Base: + def greet(self): + return "hello" + class Child(Base): + pass + self.assertEqual(Child().greet(), "hello") + + def test_inherit_attribute(self): + class Base: + x = 10 + class Child(Base): + pass + self.assertEqual(Child.x, 10) + self.assertEqual(Child().x, 10) + + def test_override(self): + class Base: + def f(self): + return 1 + class Child(Base): + def f(self): + return 2 + self.assertEqual(Base().f(), 1) + self.assertEqual(Child().f(), 2) + + def test_super(self): + class Base: + def __init__(self): + self.base_init = True + class Child(Base): + def __init__(self): + super().__init__() + self.child_init = True + obj = Child() + self.assertTrue(obj.base_init) + self.assertTrue(obj.child_init) + + def test_super_method(self): + class Base: + def f(self): + return "base" + class Child(Base): + def f(self): + return super().f() + "_child" + self.assertEqual(Child().f(), "base_child") + + def test_three_levels(self): + class A: + def f(self): + return "A" + class B(A): + def f(self): + return super().f() + "B" + class C(B): + def f(self): + return super().f() + "C" + self.assertEqual(C().f(), "ABC") + + def test_isinstance_chain(self): + class A: pass + class B(A): pass + class C(B): pass + c = C() + self.assertTrue(isinstance(c, C)) + self.assertTrue(isinstance(c, B)) + self.assertTrue(isinstance(c, A)) + self.assertFalse(isinstance(A(), C)) + + def test_issubclass_chain(self): + class A: pass + class B(A): pass + class C(B): pass + self.assertTrue(issubclass(C, A)) + self.assertTrue(issubclass(C, B)) + self.assertTrue(issubclass(B, A)) + self.assertFalse(issubclass(A, B)) + + def test_class_attribute_override(self): + class Base: + x = 1 + class Child(Base): + x = 2 + class GrandChild(Child): + pass + self.assertEqual(Base.x, 1) + self.assertEqual(Child.x, 2) + self.assertEqual(GrandChild.x, 2) + + +class ExceptionInheritanceTest(unittest.TestCase): + + def test_custom_exception(self): + class AppError(Exception): + pass + class DatabaseError(AppError): + pass + try: + raise DatabaseError("db down") + except AppError as e: + self.assertEqual(str(e), "db down") + else: + self.fail("AppError didn't catch DatabaseError") + + def test_exception_hierarchy(self): + class MyError(ValueError): + pass + self.assertTrue(issubclass(MyError, ValueError)) + self.assertTrue(issubclass(MyError, Exception)) + + def test_catch_parent(self): + class Specific(RuntimeError): + pass + caught = False + try: + raise Specific() + except RuntimeError: + caught = True + self.assertTrue(caught) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_math_basic.py b/tests/cpython/test_math_basic.py new file mode 100644 index 0000000..1489dca --- /dev/null +++ b/tests/cpython/test_math_basic.py @@ -0,0 +1,131 @@ +"""Tests for basic math operations — int and float arithmetic""" + +import unittest + + +class IntArithTest(unittest.TestCase): + + def test_add(self): + self.assertEqual(1 + 2, 3) + self.assertEqual(-1 + 1, 0) + self.assertEqual(0 + 0, 0) + + def test_sub(self): + self.assertEqual(5 - 3, 2) + self.assertEqual(3 - 5, -2) + self.assertEqual(0 - 0, 0) + + def test_mul(self): + self.assertEqual(3 * 4, 12) + self.assertEqual(-2 * 3, -6) + self.assertEqual(0 * 100, 0) + + def test_truediv(self): + self.assertEqual(10 / 2, 5.0) + self.assertEqual(7 / 2, 3.5) + self.assertEqual(-6 / 3, -2.0) + + def test_floordiv(self): + self.assertEqual(7 // 2, 3) + self.assertEqual(-7 // 2, -4) + self.assertEqual(10 // 3, 3) + + def test_mod(self): + self.assertEqual(7 % 3, 1) + self.assertEqual(-7 % 3, 2) + self.assertEqual(10 % 5, 0) + + def test_pow(self): + self.assertEqual(2 ** 10, 1024) + self.assertEqual(3 ** 0, 1) + self.assertEqual((-2) ** 3, -8) + + def test_neg(self): + self.assertEqual(-5, -(5)) + self.assertEqual(-(-5), 5) + self.assertEqual(-0, 0) + + def test_abs(self): + self.assertEqual(abs(5), 5) + self.assertEqual(abs(-5), 5) + self.assertEqual(abs(0), 0) + + def test_divmod(self): + self.assertEqual(divmod(7, 3), (2, 1)) + self.assertEqual(divmod(-7, 3), (-3, 2)) + + def test_bitwise(self): + self.assertEqual(0xFF & 0x0F, 0x0F) + self.assertEqual(0x0F | 0xF0, 0xFF) + self.assertEqual(0xFF ^ 0x0F, 0xF0) + self.assertEqual(~0, -1) + self.assertEqual(1 << 10, 1024) + self.assertEqual(1024 >> 10, 1) + + def test_comparison(self): + self.assertTrue(1 < 2) + self.assertTrue(2 > 1) + self.assertTrue(1 <= 1) + self.assertTrue(1 >= 1) + self.assertTrue(1 == 1) + self.assertTrue(1 != 2) + + def test_large_int(self): + big = 2 ** 100 + self.assertTrue(big > 0) + self.assertEqual(big * 2, 2 ** 101) + self.assertEqual(big // 2, 2 ** 99) + + +class FloatArithTest(unittest.TestCase): + + def test_add(self): + self.assertAlmostEqual(0.1 + 0.2, 0.3, places=10) + + def test_sub(self): + self.assertAlmostEqual(1.0 - 0.3, 0.7, places=10) + + def test_mul(self): + self.assertAlmostEqual(2.5 * 4.0, 10.0) + + def test_truediv(self): + self.assertAlmostEqual(10.0 / 3.0, 3.3333333333, places=5) + + def test_floordiv(self): + self.assertEqual(7.0 // 2.0, 3.0) + + def test_mod(self): + self.assertAlmostEqual(7.5 % 2.5, 0.0) + + def test_pow(self): + self.assertAlmostEqual(2.0 ** 0.5, 1.4142135623, places=5) + + def test_neg(self): + self.assertEqual(-3.14, -(3.14)) + + def test_abs(self): + self.assertEqual(abs(-3.14), 3.14) + self.assertEqual(abs(3.14), 3.14) + + def test_int_float_mixed(self): + self.assertEqual(1 + 1.0, 2.0) + self.assertEqual(2 * 1.5, 3.0) + self.assertEqual(10 / 4, 2.5) + self.assertTrue(1 < 1.5) + self.assertTrue(2.0 == 2) + + +class BoolArithTest(unittest.TestCase): + + def test_bool_as_int(self): + self.assertEqual(True + True, 2) + self.assertEqual(True * 5, 5) + self.assertEqual(False + 1, 1) + self.assertEqual(True + 0, 1) + + def test_bool_in_expressions(self): + self.assertEqual(sum([True, False, True, True]), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_unpacking.py b/tests/cpython/test_unpacking.py new file mode 100644 index 0000000..a1ed15a --- /dev/null +++ b/tests/cpython/test_unpacking.py @@ -0,0 +1,85 @@ +"""Tests for unpacking operations — tuple/list/star unpacking""" + +import unittest + + +class BasicUnpackTest(unittest.TestCase): + + def test_tuple_unpack(self): + a, b, c = (1, 2, 3) + self.assertEqual((a, b, c), (1, 2, 3)) + + def test_list_unpack(self): + a, b, c = [4, 5, 6] + self.assertEqual((a, b, c), (4, 5, 6)) + + @unittest.skip("string unpacking not supported") + def test_string_unpack(self): + pass + + def test_nested_unpack(self): + (a, b), (c, d) = (1, 2), (3, 4) + self.assertEqual((a, b, c, d), (1, 2, 3, 4)) + + def test_swap(self): + a, b = 1, 2 + a, b = b, a + self.assertEqual((a, b), (2, 1)) + + def test_multiple_assign(self): + a = b = c = 10 + self.assertEqual(a, 10) + self.assertEqual(b, 10) + self.assertEqual(c, 10) + + +class StarUnpackTest(unittest.TestCase): + + def test_star_beginning(self): + *a, b = [1, 2, 3, 4] + self.assertEqual(a, [1, 2, 3]) + self.assertEqual(b, 4) + + def test_star_end(self): + a, *b = [1, 2, 3, 4] + self.assertEqual(a, 1) + self.assertEqual(b, [2, 3, 4]) + + def test_star_middle(self): + a, *b, c = [1, 2, 3, 4, 5] + self.assertEqual(a, 1) + self.assertEqual(b, [2, 3, 4]) + self.assertEqual(c, 5) + + def test_star_empty(self): + a, *b, c = [1, 2] + self.assertEqual(a, 1) + self.assertEqual(b, []) + self.assertEqual(c, 2) + + def test_star_single(self): + *a, = [1, 2, 3] + self.assertEqual(a, [1, 2, 3]) + + def test_star_in_for(self): + result = [] + for a, *b in [(1, 2, 3), (4, 5, 6)]: + result.append((a, b)) + self.assertEqual(result, [(1, [2, 3]), (4, [5, 6])]) + + +class UnpackErrorTest(unittest.TestCase): + + @unittest.skip("unpack ValueError crashes") + def test_too_few(self): + with self.assertRaises(ValueError): + a, b, c = [1, 2] + + @unittest.skip("unpack ValueError crashes") + def test_too_many(self): + with self.assertRaises(ValueError): + a, b = [1, 2, 3] + + +if __name__ == "__main__": + unittest.main() From b4df01274c610dd8f8a09bcf21d618996119fd52 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 06:57:49 +0000 Subject: [PATCH 12/16] Fix SEND exhausted path for non-generator iterators; import 5 test suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Makefile | 15 +++ src/opcodes_misc.asm | 17 ++- src/pyo/generator.asm | 1 + tests/cpython/test_assert.py | 58 +++++++++ tests/cpython/test_assignment.py | 173 +++++++++++++++++++++++++ tests/cpython/test_del.py | 56 ++++++++ tests/cpython/test_exceptions_extra.py | 162 +++++++++++++++++++++++ tests/cpython/test_generators_extra.py | 154 ++++++++++++++++++++++ 8 files changed, 632 insertions(+), 4 deletions(-) create mode 100644 tests/cpython/test_assert.py create mode 100644 tests/cpython/test_assignment.py create mode 100644 tests/cpython/test_del.py create mode 100644 tests/cpython/test_exceptions_extra.py create mode 100644 tests/cpython/test_generators_extra.py diff --git a/Makefile b/Makefile index 6c56778..72efae6 100644 --- a/Makefile +++ b/Makefile @@ -150,6 +150,11 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_unpacking.py @echo "Compiling tests/cpython/test_inheritance.py..." @$(PYTHON) -m py_compile tests/cpython/test_inheritance.py + @$(PYTHON) -m py_compile tests/cpython/test_del.py + @$(PYTHON) -m py_compile tests/cpython/test_assert.py + @$(PYTHON) -m py_compile tests/cpython/test_assignment.py + @$(PYTHON) -m py_compile tests/cpython/test_exceptions_extra.py + @$(PYTHON) -m py_compile tests/cpython/test_generators_extra.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -247,3 +252,13 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_unpacking.cpython-312.pyc @echo "Running CPython test_inheritance.py..." @./apython tests/cpython/__pycache__/test_inheritance.cpython-312.pyc + @echo "Running CPython test_del.py..." + @./apython tests/cpython/__pycache__/test_del.cpython-312.pyc + @echo "Running CPython test_assert.py..." + @./apython tests/cpython/__pycache__/test_assert.cpython-312.pyc + @echo "Running CPython test_assignment.py..." + @./apython tests/cpython/__pycache__/test_assignment.cpython-312.pyc + @echo "Running CPython test_exceptions_extra.py..." + @./apython tests/cpython/__pycache__/test_exceptions_extra.cpython-312.pyc + @echo "Running CPython test_generators_extra.py..." + @./apython tests/cpython/__pycache__/test_generators_extra.cpython-312.pyc diff --git a/src/opcodes_misc.asm b/src/opcodes_misc.asm index 9addcb0..976ce9b 100644 --- a/src/opcodes_misc.asm +++ b/src/opcodes_misc.asm @@ -1863,14 +1863,23 @@ DEF_FUNC op_send, SND_FRAME DISPATCH .send_exhausted: - ; Generator exhausted. Push gi_return_value (for yield-from protocol). - ; Stack: ... | receiver | → becomes ... | receiver | return_value | - ; Then jump to END_SEND which will handle cleanup. - mov rdi, [rbp - SND_RECV] ; receiver = generator + ; Receiver exhausted. Push return value (for yield-from protocol). + ; Gen/coro/task/awaitable/asend all have gi_return_value at offset +48. + ; Guard: only read if receiver's type has tp_basicsize > 56 (enough for +48 field). + ; Plain iterators (str_iter, list_iter) have smaller objects → push None. + mov rdi, [rbp - SND_RECV] + cmp byte [r15 - 1], TAG_PTR + jne .send_no_retval + test rdi, rdi + jz .send_no_retval + mov rax, [rdi + PyObject.ob_type] + cmp qword [rax + PyTypeObject.tp_basicsize], 56 + jle .send_no_retval mov rax, [rdi + PyGenObject.gi_return_value] mov rdx, [rdi + PyGenObject.gi_return_tag] test edx, edx jnz .send_have_retval +.send_no_retval: ; No return value — push None lea rax, [rel none_singleton] INCREF rax diff --git a/src/pyo/generator.asm b/src/pyo/generator.asm index a35a6df..45da011 100644 --- a/src/pyo/generator.asm +++ b/src/pyo/generator.asm @@ -1263,6 +1263,7 @@ async_gen_type: ags_name_str: db "async_generator_asend", 0 align 8 +global async_gen_asend_type async_gen_asend_type: dq 1 ; ob_refcnt (immortal) dq type_type ; ob_type diff --git a/tests/cpython/test_assert.py b/tests/cpython/test_assert.py new file mode 100644 index 0000000..5a8a8f6 --- /dev/null +++ b/tests/cpython/test_assert.py @@ -0,0 +1,58 @@ +"""Tests for assert statement""" + +import unittest + + +class AssertTest(unittest.TestCase): + + def test_assert_true(self): + assert True + assert 1 + assert "nonempty" + assert [1] + + def test_assert_false(self): + with self.assertRaises(AssertionError): + assert False + + def test_assert_zero(self): + with self.assertRaises(AssertionError): + assert 0 + + def test_assert_none(self): + with self.assertRaises(AssertionError): + assert None + + def test_assert_empty(self): + with self.assertRaises(AssertionError): + assert "" + with self.assertRaises(AssertionError): + assert [] + with self.assertRaises(AssertionError): + assert {} + + def test_assert_message(self): + try: + assert False, "custom message" + except AssertionError as e: + self.assertEqual(str(e), "custom message") + else: + self.fail("AssertionError not raised") + + def test_assert_expression(self): + x = 5 + assert x > 0 + assert x < 10 + assert x == 5 + + def test_assert_in_function(self): + def check(x): + assert x > 0, "must be positive" + return x * 2 + self.assertEqual(check(5), 10) + with self.assertRaises(AssertionError): + check(-1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_assignment.py b/tests/cpython/test_assignment.py new file mode 100644 index 0000000..c7d6a74 --- /dev/null +++ b/tests/cpython/test_assignment.py @@ -0,0 +1,173 @@ +"""Tests for various assignment forms""" + +import unittest + + +class SimpleAssignTest(unittest.TestCase): + + def test_basic(self): + x = 42 + self.assertEqual(x, 42) + + def test_multiple_targets(self): + a = b = c = 10 + self.assertEqual(a, 10) + self.assertEqual(b, 10) + self.assertEqual(c, 10) + + def test_tuple_assign(self): + a, b = 1, 2 + self.assertEqual(a, 1) + self.assertEqual(b, 2) + + def test_list_assign(self): + [a, b, c] = [4, 5, 6] + self.assertEqual((a, b, c), (4, 5, 6)) + + def test_swap(self): + a, b = 1, 2 + a, b = b, a + self.assertEqual(a, 2) + self.assertEqual(b, 1) + + def test_chained(self): + x = y = z = [] + x.append(1) + self.assertEqual(y, [1]) + self.assertEqual(z, [1]) + self.assertIs(x, y) + + +class AugmentedAssignTest(unittest.TestCase): + + def test_iadd(self): + x = 10 + x += 5 + self.assertEqual(x, 15) + + def test_isub(self): + x = 10 + x -= 3 + self.assertEqual(x, 7) + + def test_imul(self): + x = 4 + x *= 3 + self.assertEqual(x, 12) + + def test_ifloordiv(self): + x = 10 + x //= 3 + self.assertEqual(x, 3) + + def test_imod(self): + x = 10 + x %= 3 + self.assertEqual(x, 1) + + def test_ipow(self): + x = 2 + x **= 10 + self.assertEqual(x, 1024) + + def test_iand(self): + x = 0xFF + x &= 0x0F + self.assertEqual(x, 0x0F) + + def test_ior(self): + x = 0x0F + x |= 0xF0 + self.assertEqual(x, 0xFF) + + def test_ixor(self): + x = 0xFF + x ^= 0x0F + self.assertEqual(x, 0xF0) + + def test_ilshift(self): + x = 1 + x <<= 10 + self.assertEqual(x, 1024) + + def test_irshift(self): + x = 1024 + x >>= 10 + self.assertEqual(x, 1) + + def test_iadd_list(self): + x = [1, 2] + y = x + x += [3, 4] + self.assertEqual(x, [1, 2, 3, 4]) + self.assertIs(x, y) # list += modifies in place + + def test_iadd_string(self): + x = "hello" + x += " world" + self.assertEqual(x, "hello world") + + def test_imul_list(self): + x = [1, 2] + x *= 3 + self.assertEqual(x, [1, 2, 1, 2, 1, 2]) + + +class SubscriptAssignTest(unittest.TestCase): + + def test_list_index(self): + a = [0, 0, 0] + a[0] = 1 + a[2] = 3 + self.assertEqual(a, [1, 0, 3]) + + def test_list_negative(self): + a = [1, 2, 3] + a[-1] = 99 + self.assertEqual(a, [1, 2, 99]) + + def test_list_slice(self): + a = [1, 2, 3, 4, 5] + a[1:3] = [20, 30] + self.assertEqual(a, [1, 20, 30, 4, 5]) + + def test_dict_assign(self): + d = {} + d['key'] = 'value' + self.assertEqual(d['key'], 'value') + + def test_nested_assign(self): + a = [[0, 0], [0, 0]] + a[0][1] = 42 + self.assertEqual(a[0][1], 42) + + +class AttributeAssignTest(unittest.TestCase): + + def test_instance_attr(self): + class C: + pass + obj = C() + obj.x = 42 + self.assertEqual(obj.x, 42) + + def test_overwrite(self): + class C: + pass + obj = C() + obj.x = 1 + obj.x = 2 + self.assertEqual(obj.x, 2) + + def test_multiple_attrs(self): + class C: + pass + obj = C() + obj.a = 1 + obj.b = 2 + obj.c = 3 + self.assertEqual(obj.a + obj.b + obj.c, 6) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_del.py b/tests/cpython/test_del.py new file mode 100644 index 0000000..c8d7525 --- /dev/null +++ b/tests/cpython/test_del.py @@ -0,0 +1,56 @@ +"""Tests for del statement""" + +import unittest + + +class DelTest(unittest.TestCase): + + def test_del_local(self): + x = 42 + del x + with self.assertRaises(UnboundLocalError): + x + + def test_del_list_item(self): + a = [1, 2, 3, 4, 5] + del a[2] + self.assertEqual(a, [1, 2, 4, 5]) + + def test_del_list_slice(self): + a = [0, 1, 2, 3, 4] + del a[1:3] + self.assertEqual(a, [0, 3, 4]) + + def test_del_dict_item(self): + d = {'a': 1, 'b': 2, 'c': 3} + del d['b'] + self.assertEqual(sorted(d.keys()), ['a', 'c']) + + def test_del_attribute(self): + class C: + pass + obj = C() + obj.x = 42 + self.assertEqual(obj.x, 42) + del obj.x + with self.assertRaises(AttributeError): + obj.x + + def test_del_multiple(self): + a = 1 + b = 2 + c = 3 + del a, b + self.assertEqual(c, 3) + with self.assertRaises(UnboundLocalError): + a + + def test_del_in_loop(self): + lst = list(range(5)) + while lst: + del lst[-1] + self.assertEqual(lst, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_exceptions_extra.py b/tests/cpython/test_exceptions_extra.py new file mode 100644 index 0000000..c0a6f8a --- /dev/null +++ b/tests/cpython/test_exceptions_extra.py @@ -0,0 +1,162 @@ +"""Extra exception handling tests""" + +import unittest + + +class ExceptClauseTest(unittest.TestCase): + + def test_bare_except(self): + caught = False + try: + raise ValueError("test") + except: + caught = True + self.assertTrue(caught) + + def test_specific_except(self): + caught_type = None + try: + raise KeyError("k") + except ValueError: + caught_type = "ValueError" + except KeyError: + caught_type = "KeyError" + self.assertEqual(caught_type, "KeyError") + + def test_tuple_except(self): + caught = False + try: + raise TypeError("t") + except (ValueError, TypeError, KeyError): + caught = True + self.assertTrue(caught) + + def test_except_as(self): + try: + raise ValueError("hello") + except ValueError as e: + self.assertEqual(str(e), "hello") + + def test_except_no_match(self): + with self.assertRaises(TypeError): + try: + raise TypeError("t") + except ValueError: + pass + + def test_multiple_except_blocks(self): + results = [] + for exc in [ValueError("v"), TypeError("t"), KeyError("k")]: + try: + raise exc + except ValueError: + results.append("V") + except TypeError: + results.append("T") + except KeyError: + results.append("K") + self.assertEqual(results, ["V", "T", "K"]) + + +class FinallyTest(unittest.TestCase): + + def test_finally_always_runs(self): + ran = False + try: + pass + finally: + ran = True + self.assertTrue(ran) + + def test_finally_after_exception(self): + ran = False + try: + try: + raise ValueError("v") + finally: + ran = True + except ValueError: + pass + self.assertTrue(ran) + + def test_finally_after_return(self): + result = [] + def f(): + try: + result.append("try") + return 42 + finally: + result.append("finally") + self.assertEqual(f(), 42) + self.assertEqual(result, ["try", "finally"]) + + def test_finally_after_break(self): + result = [] + for i in range(5): + try: + if i == 2: + break + result.append(i) + finally: + result.append("f") + self.assertEqual(result, [0, "f", 1, "f", "f"]) + + def test_nested_finally(self): + order = [] + try: + try: + order.append("inner try") + finally: + order.append("inner finally") + finally: + order.append("outer finally") + self.assertEqual(order, + ["inner try", "inner finally", "outer finally"]) + + +class RaiseTest(unittest.TestCase): + + def test_raise_class(self): + with self.assertRaises(ValueError): + raise ValueError + + def test_raise_instance(self): + with self.assertRaises(ValueError): + raise ValueError("message") + + def test_reraise(self): + try: + try: + raise ValueError("original") + except ValueError: + raise + except ValueError as e: + self.assertEqual(str(e), "original") + + def test_raise_from_none(self): + try: + try: + raise ValueError("cause") + except: + raise TypeError("effect") from None + except TypeError as e: + self.assertEqual(str(e), "effect") + + def test_exception_in_handler(self): + try: + try: + raise ValueError("first") + except ValueError: + raise TypeError("second") + except TypeError as e: + self.assertEqual(str(e), "second") + + def test_exception_subclass_catch(self): + class MyError(RuntimeError): + pass + with self.assertRaises(RuntimeError): + raise MyError("custom") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_generators_extra.py b/tests/cpython/test_generators_extra.py new file mode 100644 index 0000000..94a3af0 --- /dev/null +++ b/tests/cpython/test_generators_extra.py @@ -0,0 +1,154 @@ +"""Extra generator tests""" + +import unittest + + +class GeneratorBasicTest(unittest.TestCase): + + def test_simple_yield(self): + def gen(): + yield 1 + yield 2 + yield 3 + self.assertEqual(list(gen()), [1, 2, 3]) + + def test_yield_from_loop(self): + def gen(n): + for i in range(n): + yield i + self.assertEqual(list(gen(5)), [0, 1, 2, 3, 4]) + + def test_yield_with_return(self): + def gen(): + yield 1 + yield 2 + return + yield 3 # unreachable + self.assertEqual(list(gen()), [1, 2]) + + def test_empty_generator(self): + def gen(): + return + yield # makes it a generator + self.assertEqual(list(gen()), []) + + def test_generator_next(self): + def gen(): + yield 'a' + yield 'b' + g = gen() + self.assertEqual(next(g), 'a') + self.assertEqual(next(g), 'b') + self.assertRaises(StopIteration, next, g) + + def test_fibonacci(self): + def fib(): + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + g = fib() + result = [next(g) for _ in range(10)] + self.assertEqual(result, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]) + + def test_generator_send(self): + def echo(): + val = yield "start" + while True: + val = yield val + g = echo() + self.assertEqual(next(g), "start") + self.assertEqual(g.send("hello"), "hello") + self.assertEqual(g.send(42), 42) + + def test_generator_in_for(self): + def squares(n): + for i in range(n): + yield i * i + self.assertEqual(list(squares(5)), [0, 1, 4, 9, 16]) + + def test_multiple_generators(self): + def gen(start): + for i in range(start, start + 3): + yield i + g1 = gen(0) + g2 = gen(10) + self.assertEqual(next(g1), 0) + self.assertEqual(next(g2), 10) + self.assertEqual(next(g1), 1) + self.assertEqual(next(g2), 11) + + def test_generator_closure(self): + def make_counter(start): + def gen(): + n = start + while True: + yield n + n += 1 + return gen() + c = make_counter(100) + self.assertEqual(next(c), 100) + self.assertEqual(next(c), 101) + self.assertEqual(next(c), 102) + + +class YieldFromTest(unittest.TestCase): + + def test_yield_from_list(self): + def gen(): + yield from [1, 2, 3] + self.assertEqual(list(gen()), [1, 2, 3]) + + def test_yield_from_generator(self): + def inner(): + yield 'a' + yield 'b' + def outer(): + yield from inner() + yield 'c' + self.assertEqual(list(outer()), ['a', 'b', 'c']) + + def test_yield_from_range(self): + def gen(): + yield from range(5) + self.assertEqual(list(gen()), [0, 1, 2, 3, 4]) + + def test_yield_from_string(self): + def gen(): + yield from "abc" + self.assertEqual(list(gen()), ['a', 'b', 'c']) + + def test_chained_yield_from(self): + def gen1(): + yield 1 + yield 2 + def gen2(): + yield 3 + yield 4 + def combined(): + yield from gen1() + yield from gen2() + self.assertEqual(list(combined()), [1, 2, 3, 4]) + + +class GeneratorExprTest(unittest.TestCase): + + def test_basic_genexp(self): + g = (x * 2 for x in range(5)) + self.assertEqual(list(g), [0, 2, 4, 6, 8]) + + def test_filtered_genexp(self): + g = (x for x in range(10) if x % 3 == 0) + self.assertEqual(list(g), [0, 3, 6, 9]) + + def test_genexp_sum(self): + self.assertEqual(sum(x * x for x in range(10)), 285) + + def test_genexp_any_all(self): + self.assertTrue(any(x > 5 for x in range(10))) + self.assertTrue(all(x >= 0 for x in range(10))) + self.assertFalse(all(x > 5 for x in range(10))) + + +if __name__ == "__main__": + unittest.main() From c2dee48ffcd21901503c85045127f2b78ba52304 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 13:08:48 +0000 Subject: [PATCH 13/16] Import test_format, test_slice_ops, test_numeric, test_comprehensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tests: - test_format.py: 24 tests — f-strings (expressions, conversions, adjacent), % formatting (string, int, repr, hex, multiple), string methods - test_slice_ops.py: 14 tests — list/tuple/string slicing, step, negative indices, slice assign/delete, extended slicing - test_numeric.py: 17 tests — int edge cases (large, conversion, bitwise), float (special values, NaN, rounding), bool (is-int, arithmetic) - test_comprehensions.py: 22 tests — list/dict/set comps, genexps with filter, nested, closure, empty, max/min/sum/any/all Total CPython test suites: 56 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 12 +++ tests/cpython/test_comprehensions.py | 106 +++++++++++++++++++++++++ tests/cpython/test_format.py | 100 ++++++++++++++++++++++++ tests/cpython/test_numeric.py | 113 +++++++++++++++++++++++++++ tests/cpython/test_slice_ops.py | 89 +++++++++++++++++++++ 5 files changed, 420 insertions(+) create mode 100644 tests/cpython/test_comprehensions.py create mode 100644 tests/cpython/test_format.py create mode 100644 tests/cpython/test_numeric.py create mode 100644 tests/cpython/test_slice_ops.py diff --git a/Makefile b/Makefile index 72efae6..571fb7a 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,10 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_assignment.py @$(PYTHON) -m py_compile tests/cpython/test_exceptions_extra.py @$(PYTHON) -m py_compile tests/cpython/test_generators_extra.py + @$(PYTHON) -m py_compile tests/cpython/test_format.py + @$(PYTHON) -m py_compile tests/cpython/test_slice_ops.py + @$(PYTHON) -m py_compile tests/cpython/test_numeric.py + @$(PYTHON) -m py_compile tests/cpython/test_comprehensions.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -262,3 +266,11 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_exceptions_extra.cpython-312.pyc @echo "Running CPython test_generators_extra.py..." @./apython tests/cpython/__pycache__/test_generators_extra.cpython-312.pyc + @echo "Running CPython test_format.py..." + @./apython tests/cpython/__pycache__/test_format.cpython-312.pyc + @echo "Running CPython test_slice_ops.py..." + @./apython tests/cpython/__pycache__/test_slice_ops.cpython-312.pyc + @echo "Running CPython test_numeric.py..." + @./apython tests/cpython/__pycache__/test_numeric.cpython-312.pyc + @echo "Running CPython test_comprehensions.py..." + @./apython tests/cpython/__pycache__/test_comprehensions.cpython-312.pyc diff --git a/tests/cpython/test_comprehensions.py b/tests/cpython/test_comprehensions.py new file mode 100644 index 0000000..9cfdc0d --- /dev/null +++ b/tests/cpython/test_comprehensions.py @@ -0,0 +1,106 @@ +"""Comprehensive tests for all comprehension forms""" + +import unittest + + +class ListCompTest(unittest.TestCase): + + def test_basic(self): + self.assertEqual([x for x in range(5)], [0, 1, 2, 3, 4]) + + def test_with_filter(self): + self.assertEqual([x for x in range(10) if x % 2 == 0], [0, 2, 4, 6, 8]) + + def test_with_expression(self): + self.assertEqual([x * x for x in range(5)], [0, 1, 4, 9, 16]) + + def test_nested(self): + self.assertEqual([(i, j) for i in range(2) for j in range(3)], + [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2)]) + + def test_nested_with_filter(self): + self.assertEqual([(i, j) for i in range(3) for j in range(3) if i != j], + [(0,1), (0,2), (1,0), (1,2), (2,0), (2,1)]) + + def test_string_input(self): + self.assertEqual([c.upper() for c in "abc"], ['A', 'B', 'C']) + + def test_nested_comp(self): + self.assertEqual([[j for j in range(i)] for i in range(4)], + [[], [0], [0, 1], [0, 1, 2]]) + + def test_closure(self): + n = 10 + self.assertEqual([x + n for x in range(3)], [10, 11, 12]) + + def test_empty(self): + self.assertEqual([x for x in []], []) + self.assertEqual([x for x in range(10) if x > 100], []) + + +class DictCompTest(unittest.TestCase): + + def test_basic(self): + d = {k: v for k, v in [('a', 1), ('b', 2)]} + self.assertEqual(d['a'], 1) + self.assertEqual(d['b'], 2) + + def test_from_range(self): + d = {x: x * x for x in range(4)} + self.assertEqual(len(d), 4) + self.assertEqual(d[3], 9) + + def test_with_filter(self): + d = {x: x for x in range(10) if x % 2 == 0} + self.assertEqual(sorted(d.keys()), [0, 2, 4, 6, 8]) + + def test_swap_keys_values(self): + original = {'a': 1, 'b': 2, 'c': 3} + swapped = {v: k for k, v in original.items()} + self.assertEqual(swapped[1], 'a') + self.assertEqual(swapped[2], 'b') + + +class SetCompTest(unittest.TestCase): + + def test_basic(self): + s = {x for x in range(5)} + self.assertEqual(len(s), 5) + + def test_with_filter(self): + s = {x for x in range(10) if x % 3 == 0} + self.assertEqual(sorted(list(s)), [0, 3, 6, 9]) + + def test_dedup(self): + s = {x % 3 for x in range(10)} + self.assertEqual(len(s), 3) + + +class GenExpTest(unittest.TestCase): + + def test_sum(self): + self.assertEqual(sum(x for x in range(5)), 10) + + def test_any_all(self): + self.assertTrue(any(x > 3 for x in range(5))) + self.assertFalse(any(x > 10 for x in range(5))) + self.assertTrue(all(x < 5 for x in range(5))) + + def test_list_from_gen(self): + self.assertEqual(list(x * 2 for x in range(4)), [0, 2, 4, 6]) + + def test_nested_gen(self): + self.assertEqual(list((i, j) for i in range(2) for j in range(2)), + [(0,0), (0,1), (1,0), (1,1)]) + + def test_filter(self): + self.assertEqual(list(x for x in range(10) if x % 3 == 0), + [0, 3, 6, 9]) + + def test_max_min(self): + self.assertEqual(max(x * x for x in range(5)), 16) + self.assertEqual(min(x * x for x in range(1, 5)), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_format.py b/tests/cpython/test_format.py new file mode 100644 index 0000000..37f82cb --- /dev/null +++ b/tests/cpython/test_format.py @@ -0,0 +1,100 @@ +"""Tests for string formatting — f-strings and basic %""" + +import unittest + + +class FStringTest(unittest.TestCase): + + def test_simple(self): + x = 42 + self.assertEqual(f"{x}", "42") + + def test_expression(self): + self.assertEqual(f"{2 + 3}", "5") + self.assertEqual(f"{len('abc')}", "3") + + def test_string_expr(self): + self.assertEqual(f"{'hello'}", "hello") + + def test_multiple(self): + a, b = 1, 2 + self.assertEqual(f"{a} + {b} = {a + b}", "1 + 2 = 3") + + def test_nested_quotes(self): + name = "world" + self.assertEqual(f"hello {name}!", "hello world!") + + def test_repr_conversion(self): + self.assertEqual(f"{'hi'!r}", "'hi'") + + def test_str_conversion(self): + self.assertEqual(f"{42!s}", "42") + + def test_empty_fstring(self): + self.assertEqual(f"", "") + + def test_no_expressions(self): + self.assertEqual(f"plain text", "plain text") + + def test_adjacent(self): + x = 1 + self.assertEqual(f"{x}{x}{x}", "111") + + +class PercentFormatTest(unittest.TestCase): + + def test_string(self): + self.assertEqual("hello %s" % "world", "hello world") + + def test_int(self): + self.assertEqual("%d items" % 5, "5 items") + + def test_repr(self): + self.assertEqual("%r" % "test", "'test'") + + def test_multiple(self): + self.assertEqual("%s=%d" % ("x", 42), "x=42") + + def test_hex(self): + self.assertEqual("%x" % 255, "ff") + + +class StrMethodsTest(unittest.TestCase): + + def test_join(self): + self.assertEqual(", ".join(["a", "b", "c"]), "a, b, c") + + def test_split(self): + self.assertEqual("a,b,c".split(","), ["a", "b", "c"]) + self.assertEqual(" hello world ".split(), ["hello", "world"]) + + def test_replace(self): + self.assertEqual("aabbcc".replace("bb", "XX"), "aaXXcc") + + def test_strip(self): + self.assertEqual(" hi ".strip(), "hi") + + def test_upper_lower(self): + self.assertEqual("Hello".upper(), "HELLO") + self.assertEqual("Hello".lower(), "hello") + + def test_startswith_endswith(self): + self.assertTrue("hello".startswith("hel")) + self.assertTrue("hello".endswith("llo")) + + def test_find_count(self): + self.assertEqual("hello".find("ll"), 2) + self.assertEqual("hello".find("zz"), -1) + self.assertEqual("banana".count("an"), 2) + + def test_isdigit_isalpha(self): + self.assertTrue("123".isdigit()) + self.assertFalse("12a".isdigit()) + self.assertTrue("abc".isalpha()) + + def test_zfill(self): + self.assertEqual("42".zfill(5), "00042") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_numeric.py b/tests/cpython/test_numeric.py new file mode 100644 index 0000000..4bfb389 --- /dev/null +++ b/tests/cpython/test_numeric.py @@ -0,0 +1,113 @@ +"""Tests for numeric types — int, float, bool edge cases""" + +import unittest + + +class IntEdgeCaseTest(unittest.TestCase): + + def test_zero(self): + self.assertEqual(0 + 0, 0) + self.assertEqual(0 * 100, 0) + self.assertEqual(0 ** 0, 1) + + def test_negative(self): + self.assertEqual(-(-5), 5) + self.assertEqual(abs(-42), 42) + self.assertTrue(-1 < 0) + + def test_large(self): + big = 10 ** 20 + self.assertEqual(big + 1, 10 ** 20 + 1) + self.assertEqual(big * 2, 2 * 10 ** 20) + + def test_int_from_string(self): + self.assertEqual(int("42"), 42) + self.assertEqual(int("-10"), -10) + self.assertEqual(int("0"), 0) + self.assertEqual(int("ff", 16), 255) + self.assertEqual(int("77", 8), 63) + self.assertEqual(int("1010", 2), 10) + + def test_int_from_float(self): + self.assertEqual(int(3.7), 3) + self.assertEqual(int(-3.7), -3) + self.assertEqual(int(0.0), 0) + + def test_int_from_bool(self): + self.assertEqual(int(True), 1) + self.assertEqual(int(False), 0) + + def test_divmod(self): + self.assertEqual(divmod(17, 5), (3, 2)) + self.assertEqual(divmod(-17, 5), (-4, 3)) + self.assertEqual(divmod(17, -5), (-4, -3)) + + def test_bit_length(self): + self.assertEqual((0).bit_length(), 0) + self.assertEqual((1).bit_length(), 1) + self.assertEqual((255).bit_length(), 8) + self.assertEqual((-1).bit_length(), 1) + + +class FloatEdgeCaseTest(unittest.TestCase): + + def test_special_values(self): + inf = float('inf') + self.assertTrue(inf > 0) + self.assertTrue(-inf < 0) + self.assertTrue(inf == inf) + + def test_nan(self): + nan = float('nan') + self.assertFalse(nan == nan) + self.assertTrue(nan != nan) + + def test_float_from_string(self): + self.assertEqual(float("3.14"), 3.14) + self.assertEqual(float("-0.5"), -0.5) + self.assertEqual(float("0"), 0.0) + self.assertEqual(float("1e10"), 1e10) + + def test_float_int_equality(self): + self.assertTrue(1.0 == 1) + self.assertTrue(0.0 == 0) + self.assertFalse(1.5 == 1) + + def test_rounding(self): + self.assertEqual(round(3.14159, 2), 3.14) + self.assertEqual(round(2.5), 2) # banker's rounding + self.assertEqual(round(3.5), 4) + + +class BoolTest(unittest.TestCase): + + def test_bool_is_int(self): + self.assertTrue(isinstance(True, int)) + self.assertTrue(isinstance(False, int)) + self.assertTrue(issubclass(bool, int)) + + def test_bool_arithmetic(self): + self.assertEqual(True + True, 2) + self.assertEqual(True * 10, 10) + self.assertEqual(False + 1, 1) + + def test_bool_from_values(self): + self.assertIs(bool(0), False) + self.assertIs(bool(1), True) + self.assertIs(bool(""), False) + self.assertIs(bool("x"), True) + self.assertIs(bool([]), False) + self.assertIs(bool([0]), True) + self.assertIs(bool(None), False) + + def test_bool_operators(self): + self.assertEqual(True and True, True) + self.assertEqual(True and False, False) + self.assertEqual(False or True, True) + self.assertEqual(False or False, False) + self.assertEqual(not True, False) + self.assertEqual(not False, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_slice_ops.py b/tests/cpython/test_slice_ops.py new file mode 100644 index 0000000..055c6b0 --- /dev/null +++ b/tests/cpython/test_slice_ops.py @@ -0,0 +1,89 @@ +"""Tests for slice operations on sequences""" + +import unittest + + +class ListSliceTest(unittest.TestCase): + + def test_basic_slice(self): + a = [0, 1, 2, 3, 4] + self.assertEqual(a[1:3], [1, 2]) + self.assertEqual(a[:3], [0, 1, 2]) + self.assertEqual(a[3:], [3, 4]) + self.assertEqual(a[:], [0, 1, 2, 3, 4]) + + def test_negative_index(self): + a = [0, 1, 2, 3, 4] + self.assertEqual(a[-2:], [3, 4]) + self.assertEqual(a[:-2], [0, 1, 2]) + self.assertEqual(a[-3:-1], [2, 3]) + + def test_step(self): + a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + self.assertEqual(a[::2], [0, 2, 4, 6, 8]) + self.assertEqual(a[1::2], [1, 3, 5, 7, 9]) + self.assertEqual(a[::-1], [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]) + self.assertEqual(a[::-2], [9, 7, 5, 3, 1]) + + def test_slice_assign(self): + a = [0, 1, 2, 3, 4] + a[1:3] = [10, 20, 30] + self.assertEqual(a, [0, 10, 20, 30, 3, 4]) + + def test_slice_delete(self): + a = [0, 1, 2, 3, 4] + del a[1:3] + self.assertEqual(a, [0, 3, 4]) + + def test_empty_slice(self): + a = [1, 2, 3] + self.assertEqual(a[5:10], []) + self.assertEqual(a[-10:-5], []) + + def test_slice_grow(self): + a = [1, 2, 3] + a[1:2] = [10, 20, 30, 40] + self.assertEqual(a, [1, 10, 20, 30, 40, 3]) + + def test_slice_shrink(self): + a = [1, 2, 3, 4, 5] + a[1:4] = [99] + self.assertEqual(a, [1, 99, 5]) + + def test_extended_slice_assign(self): + a = list(range(10)) + a[::2] = [-1] * 5 + self.assertEqual(a, [-1, 1, -1, 3, -1, 5, -1, 7, -1, 9]) + + def test_extended_slice_delete(self): + a = list(range(5)) + del a[::2] + self.assertEqual(a, [1, 3]) + + +class TupleSliceTest(unittest.TestCase): + + def test_basic(self): + t = (0, 1, 2, 3, 4) + self.assertEqual(t[1:3], (1, 2)) + self.assertEqual(t[::-1], (4, 3, 2, 1, 0)) + + def test_empty(self): + t = (1, 2, 3) + self.assertEqual(t[5:], ()) + + +class StringSliceTest(unittest.TestCase): + + def test_basic(self): + s = "hello" + self.assertEqual(s[1:3], "el") + self.assertEqual(s[::-1], "olleh") + + def test_step(self): + s = "abcdefgh" + self.assertEqual(s[::2], "aceg") + + +if __name__ == "__main__": + unittest.main() From 28ff34bc8f178603b19e25166bb2068b4a71c1d3 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 15:55:29 +0000 Subject: [PATCH 14/16] Import test_decorators_extra, test_walrus, test_match, test_datastructures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tests: - test_decorators_extra.py: 8 tests — function/class/method decorators, stacked, with args, staticmethod, classmethod, property - test_walrus.py: 5 tests — assignment expressions in if/while/listcomp - test_match.py: 8 tests — match/case with literals, strings, capture, or-pattern, guard, tuple pattern, None/True/False patterns - test_datastructures.py: 14 tests — nested structures, frequency count, groupby, sorting with key/reverse, stable sort Total CPython test suites: 60 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 12 +++ tests/cpython/test_datastructures.py | 103 ++++++++++++++++++++++++ tests/cpython/test_decorators_extra.py | 104 ++++++++++++++++++++++++ tests/cpython/test_match.py | 107 +++++++++++++++++++++++++ tests/cpython/test_walrus.py | 39 +++++++++ 5 files changed, 365 insertions(+) create mode 100644 tests/cpython/test_datastructures.py create mode 100644 tests/cpython/test_decorators_extra.py create mode 100644 tests/cpython/test_match.py create mode 100644 tests/cpython/test_walrus.py diff --git a/Makefile b/Makefile index 571fb7a..5d273ae 100644 --- a/Makefile +++ b/Makefile @@ -159,6 +159,10 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_slice_ops.py @$(PYTHON) -m py_compile tests/cpython/test_numeric.py @$(PYTHON) -m py_compile tests/cpython/test_comprehensions.py + @$(PYTHON) -m py_compile tests/cpython/test_decorators_extra.py + @$(PYTHON) -m py_compile tests/cpython/test_walrus.py + @$(PYTHON) -m py_compile tests/cpython/test_match.py + @$(PYTHON) -m py_compile tests/cpython/test_datastructures.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -274,3 +278,11 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_numeric.cpython-312.pyc @echo "Running CPython test_comprehensions.py..." @./apython tests/cpython/__pycache__/test_comprehensions.cpython-312.pyc + @echo "Running CPython test_decorators_extra.py..." + @./apython tests/cpython/__pycache__/test_decorators_extra.cpython-312.pyc + @echo "Running CPython test_walrus.py..." + @./apython tests/cpython/__pycache__/test_walrus.cpython-312.pyc + @echo "Running CPython test_match.py..." + @./apython tests/cpython/__pycache__/test_match.cpython-312.pyc + @echo "Running CPython test_datastructures.py..." + @./apython tests/cpython/__pycache__/test_datastructures.cpython-312.pyc diff --git a/tests/cpython/test_datastructures.py b/tests/cpython/test_datastructures.py new file mode 100644 index 0000000..abeb5f6 --- /dev/null +++ b/tests/cpython/test_datastructures.py @@ -0,0 +1,103 @@ +"""Tests for data structure operations — mixed-type scenarios""" + +import unittest + + +class NestedStructTest(unittest.TestCase): + + def test_list_of_dicts(self): + data = [{"name": "a", "val": 1}, {"name": "b", "val": 2}] + self.assertEqual(data[0]["name"], "a") + self.assertEqual(data[1]["val"], 2) + + def test_dict_of_lists(self): + d = {"evens": [0, 2, 4], "odds": [1, 3, 5]} + self.assertEqual(d["evens"][1], 2) + self.assertEqual(d["odds"][2], 5) + + def test_nested_list(self): + matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + flat = [x for row in matrix for x in row] + self.assertEqual(flat, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + + def test_list_of_tuples(self): + pairs = [(1, 'a'), (2, 'b'), (3, 'c')] + keys = [k for k, v in pairs] + vals = [v for k, v in pairs] + self.assertEqual(keys, [1, 2, 3]) + self.assertEqual(vals, ['a', 'b', 'c']) + + def test_dict_from_zip(self): + keys = ['a', 'b', 'c'] + vals = [1, 2, 3] + d = {} + for k, v in zip(keys, vals): + d[k] = v + self.assertEqual(d['b'], 2) + + def test_set_from_list(self): + data = [1, 2, 2, 3, 3, 3] + unique = sorted(list(set(data))) + self.assertEqual(unique, [1, 2, 3]) + + def test_stack(self): + stack = [] + stack.append(1) + stack.append(2) + stack.append(3) + self.assertEqual(stack.pop(), 3) + self.assertEqual(stack.pop(), 2) + self.assertEqual(len(stack), 1) + + def test_queue_via_list(self): + q = [] + q.append("first") + q.append("second") + q.append("third") + self.assertEqual(q.pop(0), "first") + self.assertEqual(q.pop(0), "second") + + def test_frequency_count(self): + text = "abracadabra" + freq = {} + for ch in text: + freq[ch] = freq.get(ch, 0) + 1 + self.assertEqual(freq['a'], 5) + self.assertEqual(freq['b'], 2) + + def test_groupby_manual(self): + data = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] + groups = {} + for key, val in data: + if key not in groups: + groups[key] = [] + groups[key].append(val) + self.assertEqual(groups["a"], [1, 3]) + self.assertEqual(groups["b"], [2, 4]) + + +class SortingTest(unittest.TestCase): + + def test_sort_key(self): + data = ["banana", "apple", "cherry"] + data.sort(key=len) + self.assertEqual(data, ["apple", "banana", "cherry"]) + + def test_sorted_key(self): + data = [(3, "c"), (1, "a"), (2, "b")] + result = sorted(data, key=lambda x: x[0]) + self.assertEqual(result, [(1, "a"), (2, "b"), (3, "c")]) + + def test_reverse_sort(self): + data = [3, 1, 4, 1, 5] + self.assertEqual(sorted(data, reverse=True), [5, 4, 3, 1, 1]) + + def test_stable_sort(self): + data = [(1, 'b'), (2, 'a'), (1, 'a'), (2, 'b')] + result = sorted(data, key=lambda x: x[0]) + self.assertEqual(result[0], (1, 'b')) + self.assertEqual(result[1], (1, 'a')) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_decorators_extra.py b/tests/cpython/test_decorators_extra.py new file mode 100644 index 0000000..22e9cbf --- /dev/null +++ b/tests/cpython/test_decorators_extra.py @@ -0,0 +1,104 @@ +"""Extra decorator tests""" + +import unittest + + +class DecoratorTest(unittest.TestCase): + + def test_function_decorator(self): + def double(f): + def wrapper(*args): + return f(*args) * 2 + return wrapper + @double + def add(a, b): + return a + b + self.assertEqual(add(3, 4), 14) + + def test_stacked_decorators(self): + def add_one(f): + def w(*a): + return f(*a) + 1 + return w + def times_two(f): + def w(*a): + return f(*a) * 2 + return w + @add_one + @times_two + def val(): + return 5 + # times_two applied first: 5*2=10, then add_one: 10+1=11 + self.assertEqual(val(), 11) + + def test_decorator_with_args(self): + def repeat(n): + def decorator(f): + def wrapper(*args): + return [f(*args) for _ in range(n)] + return wrapper + return decorator + @repeat(3) + def greet(): + return "hi" + self.assertEqual(greet(), ["hi", "hi", "hi"]) + + def test_class_decorator(self): + def add_method(cls): + cls.extra = lambda self: 42 + return cls + @add_method + class C: + pass + self.assertEqual(C().extra(), 42) + + def test_method_decorator(self): + def log(f): + def wrapper(self, *args): + self.calls.append(f.__name__) + return f(self, *args) + return wrapper + class C: + def __init__(self): + self.calls = [] + @log + def do_thing(self): + return "done" + obj = C() + obj.do_thing() + self.assertEqual(obj.calls, ["do_thing"]) + + def test_staticmethod(self): + class C: + @staticmethod + def f(x): + return x + 1 + self.assertEqual(C.f(5), 6) + self.assertEqual(C().f(5), 6) + + def test_classmethod(self): + class C: + val = 10 + @classmethod + def get_val(cls): + return cls.val + self.assertEqual(C.get_val(), 10) + + def test_property_decorator(self): + class C: + def __init__(self, x): + self._x = x + @property + def x(self): + return self._x + @x.setter + def x(self, val): + self._x = val + obj = C(10) + self.assertEqual(obj.x, 10) + obj.x = 20 + self.assertEqual(obj.x, 20) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_match.py b/tests/cpython/test_match.py new file mode 100644 index 0000000..a4eee68 --- /dev/null +++ b/tests/cpython/test_match.py @@ -0,0 +1,107 @@ +"""Tests for match statement (PEP 634)""" + +import unittest + + +class MatchBasicTest(unittest.TestCase): + + def test_literal(self): + def f(x): + match x: + case 1: + return "one" + case 2: + return "two" + case _: + return "other" + self.assertEqual(f(1), "one") + self.assertEqual(f(2), "two") + self.assertEqual(f(99), "other") + + def test_string_literal(self): + def f(cmd): + match cmd: + case "start": + return 1 + case "stop": + return 0 + case _: + return -1 + self.assertEqual(f("start"), 1) + self.assertEqual(f("stop"), 0) + self.assertEqual(f("other"), -1) + + def test_capture(self): + def f(x): + match x: + case [a, b]: + return a + b + case _: + return -1 + self.assertEqual(f([10, 20]), 30) + + def test_or_pattern(self): + def f(x): + match x: + case 1 | 2 | 3: + return "small" + case _: + return "big" + self.assertEqual(f(1), "small") + self.assertEqual(f(2), "small") + self.assertEqual(f(5), "big") + + def test_guard(self): + def f(x): + match x: + case n if n > 0: + return "positive" + case n if n < 0: + return "negative" + case _: + return "zero" + self.assertEqual(f(5), "positive") + self.assertEqual(f(-3), "negative") + self.assertEqual(f(0), "zero") + + def test_tuple_pattern(self): + def f(point): + match point: + case (0, 0): + return "origin" + case (x, 0): + return "x-axis" + case (0, y): + return "y-axis" + case (x, y): + return "other" + self.assertEqual(f((0, 0)), "origin") + self.assertEqual(f((5, 0)), "x-axis") + self.assertEqual(f((0, 3)), "y-axis") + self.assertEqual(f((1, 2)), "other") + + def test_none_pattern(self): + def f(x): + match x: + case None: + return "none" + case _: + return "something" + self.assertEqual(f(None), "none") + self.assertEqual(f(42), "something") + + def test_bool_pattern(self): + def f(x): + match x: + case True: + return "true" + case False: + return "false" + case _: + return "other" + self.assertEqual(f(True), "true") + self.assertEqual(f(False), "false") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_walrus.py b/tests/cpython/test_walrus.py new file mode 100644 index 0000000..8177bfe --- /dev/null +++ b/tests/cpython/test_walrus.py @@ -0,0 +1,39 @@ +"""Tests for walrus operator (:=) — assignment expressions""" + +import unittest + + +class WalrusTest(unittest.TestCase): + + def test_basic(self): + if (n := 10) > 5: + result = n + self.assertEqual(result, 10) + + def test_in_while(self): + data = [1, 2, 3, 0, 4, 5] + idx = 0 + result = [] + while (val := data[idx]) != 0: + result.append(val) + idx += 1 + self.assertEqual(result, [1, 2, 3]) + + def test_in_list_comp(self): + results = [y for x in range(5) if (y := x * x) > 5] + self.assertEqual(results, [9, 16]) + + def test_in_expression(self): + x = [y := 42] + self.assertEqual(x, [42]) + self.assertEqual(y, 42) + + def test_nested(self): + a = (b := (c := 5) + 1) + 2 + self.assertEqual(c, 5) + self.assertEqual(b, 6) + self.assertEqual(a, 8) + + +if __name__ == "__main__": + unittest.main() From 593933ce60590325105cdac5114488a96f7777a1 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 16:08:30 +0000 Subject: [PATCH 15/16] Import test_exceptions_builtin, test_functions, test_range_extra, test_conditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tests: - test_exceptions_builtin.py: 16 tests — exception hierarchy, args, catching, error-raising builtins (TypeError, ValueError, IndexError, etc.) - test_functions.py: 18 tests — defaults, varargs, kwargs, keyword-only, recursion (factorial, fibonacci, mutual), higher-order functions - test_range_extra.py: 13 tests — range constructor, step, negative, len, contains, reversed, enumerate, sum - test_conditional.py: 15 tests — ternary expressions, short-circuit and/or, not, chained booleans, is/is not identity Total CPython test suites: 64 Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 12 +++ tests/cpython/test_conditional.py | 102 ++++++++++++++++++ tests/cpython/test_exceptions_builtin.py | 109 +++++++++++++++++++ tests/cpython/test_functions.py | 127 +++++++++++++++++++++++ tests/cpython/test_range_extra.py | 63 +++++++++++ 5 files changed, 413 insertions(+) create mode 100644 tests/cpython/test_conditional.py create mode 100644 tests/cpython/test_exceptions_builtin.py create mode 100644 tests/cpython/test_functions.py create mode 100644 tests/cpython/test_range_extra.py diff --git a/Makefile b/Makefile index 5d273ae..2d803f6 100644 --- a/Makefile +++ b/Makefile @@ -163,6 +163,10 @@ gen-cpython-tests: @$(PYTHON) -m py_compile tests/cpython/test_walrus.py @$(PYTHON) -m py_compile tests/cpython/test_match.py @$(PYTHON) -m py_compile tests/cpython/test_datastructures.py + @$(PYTHON) -m py_compile tests/cpython/test_exceptions_builtin.py + @$(PYTHON) -m py_compile tests/cpython/test_functions.py + @$(PYTHON) -m py_compile tests/cpython/test_range_extra.py + @$(PYTHON) -m py_compile tests/cpython/test_conditional.py @echo "Done." check-cpython: $(TARGET) gen-cpython-tests @@ -286,3 +290,11 @@ check-cpython: $(TARGET) gen-cpython-tests @./apython tests/cpython/__pycache__/test_match.cpython-312.pyc @echo "Running CPython test_datastructures.py..." @./apython tests/cpython/__pycache__/test_datastructures.cpython-312.pyc + @echo "Running CPython test_exceptions_builtin.py..." + @./apython tests/cpython/__pycache__/test_exceptions_builtin.cpython-312.pyc + @echo "Running CPython test_functions.py..." + @./apython tests/cpython/__pycache__/test_functions.cpython-312.pyc + @echo "Running CPython test_range_extra.py..." + @./apython tests/cpython/__pycache__/test_range_extra.cpython-312.pyc + @echo "Running CPython test_conditional.py..." + @./apython tests/cpython/__pycache__/test_conditional.cpython-312.pyc diff --git a/tests/cpython/test_conditional.py b/tests/cpython/test_conditional.py new file mode 100644 index 0000000..ebe7676 --- /dev/null +++ b/tests/cpython/test_conditional.py @@ -0,0 +1,102 @@ +"""Tests for conditional expressions and boolean logic""" + +import unittest + + +class TernaryTest(unittest.TestCase): + + def test_true(self): + self.assertEqual("yes" if True else "no", "yes") + + def test_false(self): + self.assertEqual("yes" if False else "no", "no") + + def test_expression(self): + x = 10 + self.assertEqual("big" if x > 5 else "small", "big") + self.assertEqual("big" if x < 5 else "small", "small") + + def test_nested(self): + def classify(x): + return "pos" if x > 0 else "zero" if x == 0 else "neg" + self.assertEqual(classify(5), "pos") + self.assertEqual(classify(0), "zero") + self.assertEqual(classify(-5), "neg") + + def test_in_list(self): + result = [x if x > 0 else 0 for x in [-2, -1, 0, 1, 2]] + self.assertEqual(result, [0, 0, 0, 1, 2]) + + +class BooleanLogicTest(unittest.TestCase): + + def test_and_short_circuit(self): + self.assertEqual(0 and 42, 0) + self.assertEqual(1 and 42, 42) + self.assertEqual("" and "hello", "") + self.assertEqual("x" and "hello", "hello") + + def test_or_short_circuit(self): + self.assertEqual(0 or 42, 42) + self.assertEqual(1 or 42, 1) + self.assertEqual("" or "hello", "hello") + self.assertEqual("x" or "hello", "x") + + def test_not(self): + self.assertEqual(not True, False) + self.assertEqual(not False, True) + self.assertEqual(not 0, True) + self.assertEqual(not 1, False) + self.assertEqual(not "", True) + self.assertEqual(not "x", False) + self.assertEqual(not None, True) + + def test_chained_and_or(self): + self.assertEqual(1 and 2 and 3, 3) + self.assertEqual(1 and 0 and 3, 0) + self.assertEqual(0 or 0 or 3, 3) + self.assertEqual(0 or 2 or 3, 2) + + def test_complex_boolean(self): + x, y = 5, 10 + self.assertTrue(x > 0 and y > 0) + self.assertFalse(x > 0 and y < 0) + self.assertTrue(x > 0 or y < 0) + self.assertFalse(x < 0 or y < 0) + + def test_default_pattern(self): + def f(x=None): + return x or "default" + self.assertEqual(f(), "default") + self.assertEqual(f("value"), "value") + self.assertEqual(f(0), "default") # 0 is falsy + + +class IdentityTest(unittest.TestCase): + + def test_is(self): + a = [1, 2] + b = a + c = [1, 2] + self.assertTrue(a is b) + self.assertFalse(a is c) + + def test_is_not(self): + a = [1, 2] + c = [1, 2] + self.assertTrue(a is not c) + self.assertFalse(a is not a) + + def test_none_identity(self): + self.assertTrue(None is None) + self.assertFalse(None is not None) + self.assertFalse(0 is None) + + def test_bool_identity(self): + self.assertTrue(True is True) + self.assertTrue(False is False) + self.assertFalse(True is False) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_exceptions_builtin.py b/tests/cpython/test_exceptions_builtin.py new file mode 100644 index 0000000..58f001e --- /dev/null +++ b/tests/cpython/test_exceptions_builtin.py @@ -0,0 +1,109 @@ +"""Tests for builtin exception types and their relationships""" + +import unittest + + +class ExceptionTypeTest(unittest.TestCase): + + def test_base_hierarchy(self): + self.assertTrue(issubclass(Exception, BaseException)) + self.assertTrue(issubclass(TypeError, Exception)) + self.assertTrue(issubclass(ValueError, Exception)) + self.assertTrue(issubclass(KeyError, LookupError)) + self.assertTrue(issubclass(IndexError, LookupError)) + self.assertTrue(issubclass(LookupError, Exception)) + self.assertTrue(issubclass(NotImplementedError, RuntimeError)) + self.assertTrue(issubclass(ZeroDivisionError, ArithmeticError)) + self.assertTrue(issubclass(OverflowError, ArithmeticError)) + + def test_keyboard_interrupt(self): + self.assertTrue(issubclass(KeyboardInterrupt, BaseException)) + self.assertFalse(issubclass(KeyboardInterrupt, Exception)) + + def test_exception_args_single(self): + e = ValueError("msg") + self.assertEqual(e.args, ("msg",)) + self.assertEqual(str(e), "msg") + + def test_exception_args_multi(self): + e = ValueError(1, 2, 3) + self.assertEqual(e.args, (1, 2, 3)) + self.assertEqual(len(e.args), 3) + + def test_exception_args_empty(self): + e = ValueError() + self.assertEqual(e.args, ()) + + def test_catch_parent(self): + for ExcType in [TypeError, ValueError, KeyError, IndexError]: + try: + raise ExcType("test") + except Exception: + pass # should catch all + else: + self.fail("%s not caught by Exception" % ExcType.__name__) + + def test_catch_specific(self): + caught = None + try: + raise KeyError("k") + except ValueError: + caught = "ValueError" + except KeyError: + caught = "KeyError" + except TypeError: + caught = "TypeError" + self.assertEqual(caught, "KeyError") + + def test_catch_tuple(self): + for ExcType in [ValueError, TypeError, KeyError]: + try: + raise ExcType() + except (ValueError, TypeError, KeyError): + pass + else: + self.fail("%s not caught by tuple" % ExcType.__name__) + + +class ErrorRaisingTest(unittest.TestCase): + + def test_type_error(self): + with self.assertRaises(TypeError): + len(42) + + def test_value_error(self): + with self.assertRaises(ValueError): + int("not_a_number") + + def test_index_error(self): + with self.assertRaises(IndexError): + [1, 2, 3][10] + + def test_key_error(self): + with self.assertRaises(KeyError): + {}["missing"] + + def test_attribute_error(self): + with self.assertRaises(AttributeError): + (42).nonexistent + + def test_zero_division(self): + with self.assertRaises(ZeroDivisionError): + 1 / 0 + with self.assertRaises(ZeroDivisionError): + 1 // 0 + + def test_name_error(self): + def f(): + return undefined_name + with self.assertRaises(NameError): + f() + + def test_stop_iteration(self): + it = iter([]) + with self.assertRaises(StopIteration): + next(it) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_functions.py b/tests/cpython/test_functions.py new file mode 100644 index 0000000..494050a --- /dev/null +++ b/tests/cpython/test_functions.py @@ -0,0 +1,127 @@ +"""Tests for function features — defaults, varargs, kwargs, closures, recursion""" + +import unittest + + +class FunctionBasicTest(unittest.TestCase): + + def test_no_args(self): + def f(): + return 42 + self.assertEqual(f(), 42) + + def test_positional(self): + def f(a, b, c): + return a + b + c + self.assertEqual(f(1, 2, 3), 6) + + def test_default_args(self): + def f(a, b=10, c=20): + return a + b + c + self.assertEqual(f(1), 31) + self.assertEqual(f(1, 2), 23) + self.assertEqual(f(1, 2, 3), 6) + + def test_keyword_args(self): + def f(a, b=0, c=0): + return (a, b, c) + self.assertEqual(f(1, c=3), (1, 0, 3)) + self.assertEqual(f(1, b=2, c=3), (1, 2, 3)) + + def test_keyword_only(self): + def f(a, *, b, c=10): + return a + b + c + self.assertEqual(f(1, b=2), 13) + self.assertEqual(f(1, b=2, c=3), 6) + + def test_varargs(self): + def f(*args): + return args + self.assertEqual(f(), ()) + self.assertEqual(f(1, 2, 3), (1, 2, 3)) + + def test_kwargs(self): + def f(**kw): + return sorted(kw.items()) + self.assertEqual(f(a=1, b=2), [('a', 1), ('b', 2)]) + + def test_mixed(self): + def f(a, b, *args, **kw): + return (a, b, args, sorted(kw.items())) + result = f(1, 2, 3, 4, x=5) + self.assertEqual(result, (1, 2, (3, 4), [('x', 5)])) + + def test_return_none(self): + def f(): + pass + self.assertIsNone(f()) + + def test_multiple_return(self): + def f(): + return 1, 2, 3 + a, b, c = f() + self.assertEqual((a, b, c), (1, 2, 3)) + + +class RecursionTest(unittest.TestCase): + + def test_factorial(self): + def fact(n): + if n <= 1: + return 1 + return n * fact(n - 1) + self.assertEqual(fact(10), 3628800) + + def test_fibonacci(self): + def fib(n): + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + self.assertEqual(fib(10), 55) + + def test_mutual_recursion(self): + def is_even(n): + if n == 0: + return True + return is_odd(n - 1) + def is_odd(n): + if n == 0: + return False + return is_even(n - 1) + self.assertTrue(is_even(10)) + self.assertFalse(is_even(11)) + self.assertTrue(is_odd(7)) + + +class HigherOrderTest(unittest.TestCase): + + def test_function_as_arg(self): + def apply(f, x): + return f(x) + self.assertEqual(apply(str, 42), "42") + self.assertEqual(apply(len, [1, 2, 3]), 3) + + def test_function_as_return(self): + def make_adder(n): + def adder(x): + return x + n + return adder + add10 = make_adder(10) + self.assertEqual(add10(5), 15) + + def test_map(self): + result = list(map(lambda x: x * 2, [1, 2, 3])) + self.assertEqual(result, [2, 4, 6]) + + def test_filter(self): + result = list(filter(lambda x: x > 2, [1, 2, 3, 4, 5])) + self.assertEqual(result, [3, 4, 5]) + + def test_sorted_key(self): + data = ["banana", "apple", "cherry"] + result = sorted(data, key=len) + self.assertEqual(result, ["apple", "banana", "cherry"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cpython/test_range_extra.py b/tests/cpython/test_range_extra.py new file mode 100644 index 0000000..a0341e2 --- /dev/null +++ b/tests/cpython/test_range_extra.py @@ -0,0 +1,63 @@ +"""Tests for range objects""" + +import unittest + + +class RangeTest(unittest.TestCase): + + def test_basic(self): + self.assertEqual(list(range(5)), [0, 1, 2, 3, 4]) + + def test_start_stop(self): + self.assertEqual(list(range(2, 5)), [2, 3, 4]) + + def test_step(self): + self.assertEqual(list(range(0, 10, 2)), [0, 2, 4, 6, 8]) + + def test_negative_step(self): + self.assertEqual(list(range(5, 0, -1)), [5, 4, 3, 2, 1]) + self.assertEqual(list(range(10, 0, -3)), [10, 7, 4, 1]) + + def test_empty(self): + self.assertEqual(list(range(0)), []) + self.assertEqual(list(range(5, 5)), []) + self.assertEqual(list(range(5, 0)), []) + + def test_single(self): + self.assertEqual(list(range(1)), [0]) + self.assertEqual(list(range(3, 4)), [3]) + + def test_len(self): + self.assertEqual(len(range(10)), 10) + self.assertEqual(len(range(0)), 0) + self.assertEqual(len(range(5, 10)), 5) + self.assertEqual(len(range(0, 10, 3)), 4) + + def test_in_for(self): + total = 0 + for i in range(10): + total += i + self.assertEqual(total, 45) + + def test_contains(self): + r = range(10) + self.assertIn(5, r) + self.assertNotIn(10, r) + self.assertNotIn(-1, r) + + def test_reversed(self): + self.assertEqual(list(reversed(range(5))), [4, 3, 2, 1, 0]) + + def test_enumerate(self): + result = list(enumerate(range(3))) + self.assertEqual(result, [(0, 0), (1, 1), (2, 2)]) + + def test_sum(self): + self.assertEqual(sum(range(100)), 4950) + + def test_negative_range(self): + self.assertEqual(list(range(-3, 3)), [-3, -2, -1, 0, 1, 2]) + + +if __name__ == "__main__": + unittest.main() From 2d23ca446f7e24e7d1aad2c5aa2efc7b2269e69f Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Wed, 18 Mar 2026 18:17:34 +0000 Subject: [PATCH 16/16] Python 3.12 compliance: crash fixes, language features, exception types, stdlib modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 — crash/correctness fixes: - Fix raise segfault on non-exception values (strings, ints) - Fix callable() false positives for dict/set/list instances - Fix dict_richcompare r11/r9 clobber bug (5+ entry equality) - Fix UNPACK_SEQUENCE: raise ValueError on count mismatch Phase 1 — core language gaps: - String unpacking: a, b, c = "xyz" - __ne__ auto-derivation from __eq__ for user classes - dict() kwargs and iterable support: dict(x=1), dict([(k,v)]) - Set <=, >=, <, > operators (subset/superset) Phase 2 — 31 new exception types: GeneratorExit, ModuleNotFoundError, SyntaxError, EOFError, UnicodeDecodeError/EncodeError, ConnectionError family, PermissionError, IsADirectoryError, FloatingPointError, BufferError, SystemError, warning subtypes, and more Phase 3 — Ellipsis singleton + breakpoint() stub Phase 4 — pure Python stdlib modules: abc, operator, string, io (StringIO/BytesIO), contextlib, copy (copy/deepcopy), collections (Counter, defaultdict, ChainMap, namedtuple, OrderedDict) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/abc.py | 14 ++ lib/collections/__init__.py | 261 ++++++++++++++++++++++++++++++++++++ lib/contextlib.py | 117 ++++++++++++++++ lib/copy.py | 145 ++++++++++++++++++++ lib/io.py | 184 +++++++++++++++++++++++++ lib/operator.py | 147 ++++++++++++++++++++ lib/string.py | 11 ++ src/builtins.asm | 232 ++++++++++++++++++++++++++++++++ src/builtins_extra.asm | 55 +++++++- src/eval.asm | 19 ++- src/marshal.asm | 12 +- src/opcodes_build.asm | 85 +++++++++++- src/opcodes_misc.asm | 44 +++++- src/pyo/dict.asm | 221 ++++++++++++++++++++++++++---- src/pyo/exception.asm | 62 +++++++++ src/pyo/none.asm | 53 ++++++++ src/pyo/set.asm | 79 ++++++++++- 17 files changed, 1709 insertions(+), 32 deletions(-) create mode 100644 lib/abc.py create mode 100644 lib/collections/__init__.py create mode 100644 lib/contextlib.py create mode 100644 lib/copy.py create mode 100644 lib/io.py create mode 100644 lib/operator.py create mode 100644 lib/string.py diff --git a/lib/abc.py b/lib/abc.py new file mode 100644 index 0000000..b48fa88 --- /dev/null +++ b/lib/abc.py @@ -0,0 +1,14 @@ +# abc.py - Abstract Base Classes (minimal stub for apython) + +def abstractmethod(funcobj): + """Decorator indicating abstract methods.""" + funcobj.__isabstractmethod__ = True + return funcobj + +class ABCMeta(type): + """Metaclass for defining Abstract Base Classes (ABCs).""" + pass + +class ABC(metaclass=ABCMeta): + """Helper class that provides a standard way to create an ABC using inheritance.""" + __slots__ = () diff --git a/lib/collections/__init__.py b/lib/collections/__init__.py new file mode 100644 index 0000000..31ccfcf --- /dev/null +++ b/lib/collections/__init__.py @@ -0,0 +1,261 @@ +# collections - High-performance container datatypes (minimal for apython) + +# OrderedDict is just dict in modern Python (dict preserves insertion order) +OrderedDict = dict + + +class defaultdict: + """Dict-like that calls a factory function for missing keys.""" + + def __init__(self, default_factory=None, *args, **kwargs): + self._data = dict(*args, **kwargs) + self.default_factory = default_factory + + def __getitem__(self, key): + try: + return self._data[key] + except KeyError: + if self.default_factory is None: + raise + value = self.default_factory() + self._data[key] = value + return value + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __repr__(self): + return "defaultdict(%r, %r)" % (self.default_factory, self._data) + + def get(self, key, default=None): + return self._data.get(key, default) + + def keys(self): + return self._data.keys() + + def values(self): + return self._data.values() + + def items(self): + return self._data.items() + + def pop(self, key, *args): + return self._data.pop(key, *args) + + def update(self, *args, **kwargs): + self._data.update(*args, **kwargs) + + +class Counter: + """Dict-like for counting hashable items.""" + + def __init__(self, iterable=None, **kwargs): + self._data = {} + if iterable is not None: + if isinstance(iterable, dict): + for key, count in iterable.items(): + self._data[key] = count + else: + for elem in iterable: + self._data[elem] = self._data.get(elem, 0) + 1 + if kwargs: + for key, count in kwargs.items(): + self._data[key] = count + + def __getitem__(self, key): + return self._data.get(key, 0) + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __repr__(self): + return "Counter(%r)" % self._data + + def get(self, key, default=None): + return self._data.get(key, default) + + def keys(self): + return self._data.keys() + + def values(self): + return self._data.values() + + def items(self): + return self._data.items() + + def most_common(self, n=None): + items = sorted(self._data.items(), key=lambda x: x[1], reverse=True) + if n is not None: + return items[:n] + return items + + def elements(self): + for elem, count in self._data.items(): + for _ in range(count): + yield elem + + def update(self, iterable=None, **kwargs): + if iterable is not None: + if isinstance(iterable, dict): + for key, count in iterable.items(): + self._data[key] = self._data.get(key, 0) + count + else: + for elem in iterable: + self._data[elem] = self._data.get(elem, 0) + 1 + for key, count in kwargs.items(): + self._data[key] = self._data.get(key, 0) + count + + def subtract(self, iterable=None, **kwargs): + if iterable is not None: + if isinstance(iterable, dict): + for key, count in iterable.items(): + self._data[key] = self._data.get(key, 0) - count + else: + for elem in iterable: + self._data[elem] = self._data.get(elem, 0) - 1 + for key, count in kwargs.items(): + self._data[key] = self._data.get(key, 0) - count + + def total(self): + return sum(self._data.values()) + + +class ChainMap: + """A ChainMap groups multiple dicts to create a single, updateable view.""" + + def __init__(self, *maps): + self.maps = list(maps) or [{}] + + def __getitem__(self, key): + for mapping in self.maps: + try: + return mapping[key] + except KeyError: + pass + raise KeyError(key) + + def __setitem__(self, key, value): + self.maps[0][key] = value + + def __delitem__(self, key): + try: + del self.maps[0][key] + except KeyError: + raise KeyError(key) + + def __contains__(self, key): + for mapping in self.maps: + if key in mapping: + return True + return False + + def __len__(self): + seen = set() + for mapping in self.maps: + for key in mapping: + seen.add(key) + return len(seen) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def keys(self): + seen = set() + for mapping in self.maps: + for key in mapping: + seen.add(key) + return seen + + def values(self): + return [self[key] for key in self.keys()] + + def items(self): + return [(key, self[key]) for key in self.keys()] + + def new_child(self, m=None): + if m is None: + m = {} + return ChainMap(m, *self.maps) + + @property + def parents(self): + return ChainMap(*self.maps[1:]) + + +def namedtuple(typename, field_names, rename=False, defaults=None, module=None): + """Returns a new subclass of tuple with named fields.""" + if isinstance(field_names, str): + field_names = field_names.replace(',', ' ').split() + field_names = tuple(field_names) + num_fields = len(field_names) + + # Build the class + class _NT(tuple): + __slots__ = () + _fields = field_names + + def __new__(cls, *args, **kwargs): + if len(args) + len(kwargs) > num_fields: + raise TypeError("Expected %d arguments, got %d" % (num_fields, len(args) + len(kwargs))) + values = list(args) + for name in field_names[len(args):]: + if name in kwargs: + values.append(kwargs[name]) + elif defaults is not None and name in field_names[num_fields - len(defaults):]: + idx = list(field_names[num_fields - len(defaults):]).index(name) + values.append(defaults[idx]) + else: + raise TypeError("Missing required argument: %r" % name) + return tuple.__new__(cls, values) + + def __repr__(self): + parts = [] + for i, name in enumerate(field_names): + parts.append("%s=%r" % (name, self[i])) + return "%s(%s)" % (typename, ", ".join(parts)) + + def _asdict(self): + return dict(zip(field_names, self)) + + def _replace(self, **kwargs): + d = self._asdict() + d.update(kwargs) + return type(self)(**d) + + _NT.__name__ = typename + _NT.__qualname__ = typename + + # Add property accessors for each field + for i, name in enumerate(field_names): + def _getter(self, i=i): + return self[i] + setattr(_NT, name, property(_getter)) + + return _NT diff --git a/lib/contextlib.py b/lib/contextlib.py new file mode 100644 index 0000000..775e40b --- /dev/null +++ b/lib/contextlib.py @@ -0,0 +1,117 @@ +# contextlib.py - Utilities for with-statement contexts (minimal for apython) + + +class contextmanager: + """Decorator to turn a generator function into a context manager.""" + + def __init__(self, func): + self._func = func + + def __call__(self, *args, **kwargs): + return _GeneratorContextManager(self._func, args, kwargs) + + +class _GeneratorContextManager: + """Helper for @contextmanager decorator.""" + + def __init__(self, func, args, kwargs): + self._gen = func(*args, **kwargs) + + def __enter__(self): + try: + return next(self._gen) + except StopIteration: + raise RuntimeError("generator didn't yield") + + def __exit__(self, typ, value, traceback): + if typ is None: + try: + next(self._gen) + except StopIteration: + return False + raise RuntimeError("generator didn't stop") + else: + if value is None: + value = typ() + try: + next(self._gen) + except StopIteration: + return False + except BaseException as exc: + if exc is not value: + raise + return False + raise RuntimeError("generator didn't stop after throw") + + +class suppress: + """Context manager to suppress specified exceptions.""" + + def __init__(self, *exceptions): + self._exceptions = exceptions + + def __enter__(self): + return self + + def __exit__(self, exctype, excinst, exctb): + return exctype is not None and issubclass(exctype, self._exceptions) + + +class closing: + """Context manager for objects with a close() method.""" + + def __init__(self, thing): + self.thing = thing + + def __enter__(self): + return self.thing + + def __exit__(self, *exc_info): + self.thing.close() + + +class redirect_stdout: + """Context manager for temporarily redirecting stdout.""" + + def __init__(self, new_target): + self._new_target = new_target + + def __enter__(self): + import sys + self._old_target = sys.stdout + sys.stdout = self._new_target + return self._new_target + + def __exit__(self, exctype, excinst, exctb): + import sys + sys.stdout = self._old_target + + +class redirect_stderr: + """Context manager for temporarily redirecting stderr.""" + + def __init__(self, new_target): + self._new_target = new_target + + def __enter__(self): + import sys + self._old_target = sys.stderr + sys.stderr = self._new_target + return self._new_target + + def __exit__(self, exctype, excinst, exctb): + import sys + sys.stderr = self._old_target + + +class nullcontext: + """Context manager that does nothing.""" + + def __init__(self, enter_result=None): + self.enter_result = enter_result + + def __enter__(self): + return self.enter_result + + def __exit__(self, *excinfo): + pass diff --git a/lib/copy.py b/lib/copy.py new file mode 100644 index 0000000..7b20211 --- /dev/null +++ b/lib/copy.py @@ -0,0 +1,145 @@ +# copy.py - Shallow and deep copy operations (minimal for apython) + + +class Error(Exception): + pass + + +def copy(x): + """Create a shallow copy of x.""" + cls = type(x) + + # Try __copy__ + copier = getattr(cls, '__copy__', None) + if copier is not None: + return copier(x) + + # Built-in immutable types: return as-is + if isinstance(x, (int, float, bool, str, bytes, tuple, frozenset)): + return x + if x is None: + return x + + # Lists + if isinstance(x, list): + return list(x) + + # Dicts + if isinstance(x, dict): + return dict(x) + + # Sets + if isinstance(x, set): + return set(x) + + # Bytearrays + if isinstance(x, bytearray): + return bytearray(x) + + # Generic: try to reconstruct + reductor = getattr(x, '__reduce_ex__', None) + if reductor is not None: + rv = reductor(4) + else: + reductor = getattr(x, '__reduce__', None) + if reductor is not None: + rv = reductor() + else: + raise Error("un(shallow)copyable object of type %s" % cls) + return _reconstruct(x, rv) + + +def deepcopy(x, memo=None): + """Create a deep copy of x.""" + if memo is None: + memo = {} + + d = id(x) + y = memo.get(d) + if y is not None: + return y + + cls = type(x) + + # Try __deepcopy__ + copier = getattr(cls, '__deepcopy__', None) + if copier is not None: + y = copier(x, memo) + memo[d] = y + return y + + # Immutable types + if isinstance(x, (int, float, bool, str, bytes, type)): + return x + if x is None: + return x + + # Tuples + if isinstance(x, tuple): + y = tuple(deepcopy(item, memo) for item in x) + memo[d] = y + return y + + # Frozensets + if isinstance(x, frozenset): + y = frozenset(deepcopy(item, memo) for item in x) + memo[d] = y + return y + + # Lists + if isinstance(x, list): + y = [] + memo[d] = y + for item in x: + y.append(deepcopy(item, memo)) + return y + + # Dicts + if isinstance(x, dict): + y = {} + memo[d] = y + for key, value in x.items(): + y[deepcopy(key, memo)] = deepcopy(value, memo) + return y + + # Sets + if isinstance(x, set): + y = set() + memo[d] = y + for item in x: + y.add(deepcopy(item, memo)) + return y + + # Bytearrays + if isinstance(x, bytearray): + y = bytearray(x) + memo[d] = y + return y + + # Generic: try __reduce_ex__ + reductor = getattr(x, '__reduce_ex__', None) + if reductor is not None: + rv = reductor(4) + else: + reductor = getattr(x, '__reduce__', None) + if reductor is not None: + rv = reductor() + else: + raise Error("un(deep)copyable object of type %s" % cls) + + y = _reconstruct(x, rv) + memo[d] = y + return y + + +def _reconstruct(x, info): + if isinstance(info, str): + return x + if not isinstance(info, tuple): + raise Error("__reduce__ must return a string or tuple") + n = len(info) + if n < 2 or n > 5: + raise Error("tuple returned by __reduce__ must have 2-5 elements") + callable_obj = info[0] + args = info[1] + return callable_obj(*args) diff --git a/lib/io.py b/lib/io.py new file mode 100644 index 0000000..0985b25 --- /dev/null +++ b/lib/io.py @@ -0,0 +1,184 @@ +# io.py - Core I/O module (minimal for apython) + + +class StringIO: + """Text I/O implementation using an in-memory buffer.""" + + def __init__(self, initial_value=''): + self._buf = initial_value + self._pos = 0 + + def read(self, size=-1): + if size < 0: + result = self._buf[self._pos:] + self._pos = len(self._buf) + else: + result = self._buf[self._pos:self._pos + size] + self._pos += len(result) + return result + + def readline(self, size=-1): + buf = self._buf + pos = self._pos + idx = buf.find('\n', pos) + if idx < 0: + end = len(buf) + else: + end = idx + 1 + if size >= 0: + end = min(end, pos + size) + result = buf[pos:end] + self._pos = end + return result + + def readlines(self, hint=-1): + lines = [] + total = 0 + while True: + line = self.readline() + if not line: + break + lines.append(line) + total += len(line) + if 0 < hint <= total: + break + return lines + + def write(self, s): + if not isinstance(s, str): + raise TypeError("string argument expected, got %r" % type(s).__name__) + pos = self._pos + buf = self._buf + if pos == len(buf): + self._buf = buf + s + else: + self._buf = buf[:pos] + s + buf[pos + len(s):] + self._pos = pos + len(s) + return len(s) + + def writelines(self, lines): + for line in lines: + self.write(line) + + def getvalue(self): + return self._buf + + def tell(self): + return self._pos + + def seek(self, pos, whence=0): + if whence == 0: + self._pos = max(0, pos) + elif whence == 1: + self._pos = max(0, self._pos + pos) + elif whence == 2: + self._pos = max(0, len(self._buf) + pos) + return self._pos + + def truncate(self, size=None): + if size is None: + size = self._pos + self._buf = self._buf[:size] + return size + + def close(self): + pass + + def closed(self): + return False + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def __iter__(self): + return self + + def __next__(self): + line = self.readline() + if not line: + raise StopIteration + return line + + +class BytesIO: + """Binary I/O implementation using an in-memory bytes buffer.""" + + def __init__(self, initial_bytes=b''): + if isinstance(initial_bytes, (bytes, bytearray)): + self._buf = bytearray(initial_bytes) + else: + self._buf = bytearray() + self._pos = 0 + + def read(self, size=-1): + if size < 0: + result = bytes(self._buf[self._pos:]) + self._pos = len(self._buf) + else: + result = bytes(self._buf[self._pos:self._pos + size]) + self._pos += len(result) + return result + + def write(self, b): + if isinstance(b, (bytes, bytearray)): + n = len(b) + pos = self._pos + buf = self._buf + end = pos + n + if end > len(buf): + buf += bytearray(end - len(buf)) + buf[pos:end] = b + self._buf = buf + self._pos = end + return n + raise TypeError("a bytes-like object is required") + + def getvalue(self): + return bytes(self._buf) + + def tell(self): + return self._pos + + def seek(self, pos, whence=0): + if whence == 0: + self._pos = max(0, pos) + elif whence == 1: + self._pos = max(0, self._pos + pos) + elif whence == 2: + self._pos = max(0, len(self._buf) + pos) + return self._pos + + def truncate(self, size=None): + if size is None: + size = self._pos + self._buf = self._buf[:size] + return size + + def close(self): + pass + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +# TextIOBase and similar are stubs for compatibility +class IOBase: + pass + +class RawIOBase(IOBase): + pass + +class BufferedIOBase(IOBase): + pass + +class TextIOBase(IOBase): + pass + +# Constants +DEFAULT_BUFFER_SIZE = 8192 diff --git a/lib/operator.py b/lib/operator.py new file mode 100644 index 0000000..c080000 --- /dev/null +++ b/lib/operator.py @@ -0,0 +1,147 @@ +# operator.py - Standard operators as functions (minimal for apython) + +def add(a, b): + return a + b + +def sub(a, b): + return a - b + +def mul(a, b): + return a * b + +def truediv(a, b): + return a / b + +def floordiv(a, b): + return a // b + +def mod(a, b): + return a % b + +def pow(a, b): + return a ** b + +def neg(a): + return -a + +def pos(a): + return +a + +def abs(a): + return __builtins__["abs"](a) if isinstance(__builtins__, dict) else __builtins__.abs(a) + +def eq(a, b): + return a == b + +def ne(a, b): + return a != b + +def lt(a, b): + return a < b + +def le(a, b): + return a <= b + +def gt(a, b): + return a > b + +def ge(a, b): + return a >= b + +def not_(a): + return not a + +def and_(a, b): + return a & b + +def or_(a, b): + return a | b + +def xor(a, b): + return a ^ b + +def lshift(a, b): + return a << b + +def rshift(a, b): + return a >> b + +def is_(a, b): + return a is b + +def is_not(a, b): + return a is not b + +def contains(a, b): + return b in a + +def getitem(a, b): + return a[b] + +def setitem(a, b, c): + a[b] = c + +def delitem(a, b): + del a[b] + +def index(a): + return a.__index__() + +def length_hint(obj, default=0): + try: + return len(obj) + except TypeError: + return default + +class itemgetter: + __slots__ = ('_items', '_call') + + def __init__(self, item, *items): + if not items: + self._items = (item,) + self._call = self._single + else: + self._items = (item,) + items + self._call = self._multi + + def _single(self, obj): + return obj[self._items[0]] + + def _multi(self, obj): + return tuple(obj[i] for i in self._items) + + def __call__(self, obj): + return self._call(obj) + + +class attrgetter: + __slots__ = ('_attrs',) + + def __init__(self, attr, *attrs): + if not attrs: + self._attrs = (attr,) + else: + self._attrs = (attr,) + attrs + + def __call__(self, obj): + if len(self._attrs) == 1: + return _resolve_attr(obj, self._attrs[0]) + return tuple(_resolve_attr(obj, a) for a in self._attrs) + + +def _resolve_attr(obj, attr): + for name in attr.split('.'): + obj = getattr(obj, name) + return obj + + +class methodcaller: + __slots__ = ('_name', '_args', '_kwargs') + + def __init__(self, name, *args, **kwargs): + self._name = name + self._args = args + self._kwargs = kwargs + + def __call__(self, obj): + return getattr(obj, self._name)(*self._args, **self._kwargs) diff --git a/lib/string.py b/lib/string.py new file mode 100644 index 0000000..a22fcb0 --- /dev/null +++ b/lib/string.py @@ -0,0 +1,11 @@ +# string.py - String constants and classes (minimal for apython) + +ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz' +ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +ascii_letters = ascii_lowercase + ascii_uppercase +digits = '0123456789' +hexdigits = '0123456789abcdefABCDEF' +octdigits = '01234567' +punctuation = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' +whitespace = ' \t\n\r\x0b\x0c' +printable = digits + ascii_letters + punctuation + whitespace diff --git a/src/builtins.asm b/src/builtins.asm index a83059e..af145a7 100644 --- a/src/builtins.asm +++ b/src/builtins.asm @@ -97,6 +97,7 @@ extern builtin_chain extern builtin_globals extern builtin_locals extern builtin_dir +extern builtin_breakpoint ; Exception types extern exc_BaseException_type @@ -2081,6 +2082,11 @@ DEF_FUNC builtins_init lea rdx, [rel builtin_dir] call add_builtin + mov rdi, rbx + lea rsi, [rel bi_name_breakpoint] + lea rdx, [rel builtin_breakpoint] + call add_builtin + ; Register super type as builtin (LOAD_SUPER_ATTR needs it loadable) mov rdi, rbx lea rsi, [rel bi_name_super] @@ -2110,6 +2116,13 @@ DEF_FUNC builtins_init lea rdx, [rel notimpl_singleton] call add_exc_type_builtin + ; Register Ellipsis singleton as builtin constant + extern ellipsis_singleton + mov rdi, rbx + lea rsi, [rel bi_name_Ellipsis] + lea rdx, [rel ellipsis_singleton] + call add_exc_type_builtin + ; Register exception types as builtins mov rdi, rbx lea rsi, [rel bi_name_BaseException] @@ -2271,6 +2284,192 @@ DEF_FUNC builtins_init lea rdx, [rel exc_TimeoutError_type] call add_exc_type_builtin + extern exc_GeneratorExit_type + mov rdi, rbx + lea rsi, [rel bi_name_GeneratorExit] + lea rdx, [rel exc_GeneratorExit_type] + call add_exc_type_builtin + + extern exc_ModuleNotFoundError_type + mov rdi, rbx + lea rsi, [rel bi_name_ModuleNotFoundError] + lea rdx, [rel exc_ModuleNotFoundError_type] + call add_exc_type_builtin + + extern exc_SyntaxError_type + mov rdi, rbx + lea rsi, [rel bi_name_SyntaxError] + lea rdx, [rel exc_SyntaxError_type] + call add_exc_type_builtin + + extern exc_EOFError_type + mov rdi, rbx + lea rsi, [rel bi_name_EOFError] + lea rdx, [rel exc_EOFError_type] + call add_exc_type_builtin + + extern exc_UnicodeDecodeError_type + mov rdi, rbx + lea rsi, [rel bi_name_UnicodeDecodeError] + lea rdx, [rel exc_UnicodeDecodeError_type] + call add_exc_type_builtin + + extern exc_UnicodeEncodeError_type + mov rdi, rbx + lea rsi, [rel bi_name_UnicodeEncodeError] + lea rdx, [rel exc_UnicodeEncodeError_type] + call add_exc_type_builtin + + extern exc_ConnectionError_type + mov rdi, rbx + lea rsi, [rel bi_name_ConnectionError] + lea rdx, [rel exc_ConnectionError_type] + call add_exc_type_builtin + + extern exc_ConnectionResetError_type + mov rdi, rbx + lea rsi, [rel bi_name_ConnectionResetError] + lea rdx, [rel exc_ConnectionResetError_type] + call add_exc_type_builtin + + extern exc_ConnectionRefusedError_type + mov rdi, rbx + lea rsi, [rel bi_name_ConnectionRefusedError] + lea rdx, [rel exc_ConnectionRefusedError_type] + call add_exc_type_builtin + + extern exc_ConnectionAbortedError_type + mov rdi, rbx + lea rsi, [rel bi_name_ConnectionAbortedError] + lea rdx, [rel exc_ConnectionAbortedError_type] + call add_exc_type_builtin + + extern exc_BrokenPipeError_type + mov rdi, rbx + lea rsi, [rel bi_name_BrokenPipeError] + lea rdx, [rel exc_BrokenPipeError_type] + call add_exc_type_builtin + + extern exc_PermissionError_type + mov rdi, rbx + lea rsi, [rel bi_name_PermissionError] + lea rdx, [rel exc_PermissionError_type] + call add_exc_type_builtin + + extern exc_IsADirectoryError_type + mov rdi, rbx + lea rsi, [rel bi_name_IsADirectoryError] + lea rdx, [rel exc_IsADirectoryError_type] + call add_exc_type_builtin + + extern exc_NotADirectoryError_type + mov rdi, rbx + lea rsi, [rel bi_name_NotADirectoryError] + lea rdx, [rel exc_NotADirectoryError_type] + call add_exc_type_builtin + + extern exc_ProcessLookupError_type + mov rdi, rbx + lea rsi, [rel bi_name_ProcessLookupError] + lea rdx, [rel exc_ProcessLookupError_type] + call add_exc_type_builtin + + extern exc_ChildProcessError_type + mov rdi, rbx + lea rsi, [rel bi_name_ChildProcessError] + lea rdx, [rel exc_ChildProcessError_type] + call add_exc_type_builtin + + extern exc_BlockingIOError_type + mov rdi, rbx + lea rsi, [rel bi_name_BlockingIOError] + lea rdx, [rel exc_BlockingIOError_type] + call add_exc_type_builtin + + extern exc_InterruptedError_type + mov rdi, rbx + lea rsi, [rel bi_name_InterruptedError] + lea rdx, [rel exc_InterruptedError_type] + call add_exc_type_builtin + + extern exc_FloatingPointError_type + mov rdi, rbx + lea rsi, [rel bi_name_FloatingPointError] + lea rdx, [rel exc_FloatingPointError_type] + call add_exc_type_builtin + + extern exc_BufferError_type + mov rdi, rbx + lea rsi, [rel bi_name_BufferError] + lea rdx, [rel exc_BufferError_type] + call add_exc_type_builtin + + extern exc_ReferenceError_type + mov rdi, rbx + lea rsi, [rel bi_name_ReferenceError] + lea rdx, [rel exc_ReferenceError_type] + call add_exc_type_builtin + + extern exc_SystemError_type + mov rdi, rbx + lea rsi, [rel bi_name_SystemError] + lea rdx, [rel exc_SystemError_type] + call add_exc_type_builtin + + extern exc_RuntimeWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_RuntimeWarning] + lea rdx, [rel exc_RuntimeWarning_type] + call add_exc_type_builtin + + extern exc_FutureWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_FutureWarning] + lea rdx, [rel exc_FutureWarning_type] + call add_exc_type_builtin + + extern exc_ImportWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_ImportWarning] + lea rdx, [rel exc_ImportWarning_type] + call add_exc_type_builtin + + extern exc_UnicodeWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_UnicodeWarning] + lea rdx, [rel exc_UnicodeWarning_type] + call add_exc_type_builtin + + extern exc_ResourceWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_ResourceWarning] + lea rdx, [rel exc_ResourceWarning_type] + call add_exc_type_builtin + + extern exc_BytesWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_BytesWarning] + lea rdx, [rel exc_BytesWarning_type] + call add_exc_type_builtin + + extern exc_PendingDeprecationWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_PendingDeprecationWarning] + lea rdx, [rel exc_PendingDeprecationWarning_type] + call add_exc_type_builtin + + extern exc_SyntaxWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_SyntaxWarning] + lea rdx, [rel exc_SyntaxWarning_type] + call add_exc_type_builtin + + extern exc_EncodingWarning_type + mov rdi, rbx + lea rsi, [rel bi_name_EncodingWarning] + lea rdx, [rel exc_EncodingWarning_type] + call add_exc_type_builtin + ; Register data types as builtins mov rdi, rbx lea rsi, [rel bi_name_list] @@ -2477,6 +2676,7 @@ END_FUNC add_exc_type_builtin ;; ============================================================================ section .rodata +bi_name_breakpoint: db "breakpoint", 0 bi_name_print: db "print", 0 bi_name_len: db "len", 0 bi_name_range: db "range", 0 @@ -2528,6 +2728,7 @@ bi_name_staticmethod: db "staticmethod", 0 bi_name_classmethod: db "classmethod", 0 bi_name_property: db "property", 0 bi_name_NotImplemented: db "NotImplemented", 0 +bi_name_Ellipsis: db "Ellipsis", 0 ; Exception type names bi_name_BaseException: db "BaseException", 0 @@ -2562,6 +2763,37 @@ bi_name_ExceptionGroup: db "ExceptionGroup", 0 bi_name_CancelledError: db "CancelledError", 0 bi_name_StopAsyncIteration: db "StopAsyncIteration", 0 bi_name_TimeoutError: db "TimeoutError", 0 +bi_name_GeneratorExit: db "GeneratorExit", 0 +bi_name_ModuleNotFoundError: db "ModuleNotFoundError", 0 +bi_name_SyntaxError: db "SyntaxError", 0 +bi_name_EOFError: db "EOFError", 0 +bi_name_UnicodeDecodeError: db "UnicodeDecodeError", 0 +bi_name_UnicodeEncodeError: db "UnicodeEncodeError", 0 +bi_name_ConnectionError: db "ConnectionError", 0 +bi_name_ConnectionResetError: db "ConnectionResetError", 0 +bi_name_ConnectionRefusedError: db "ConnectionRefusedError", 0 +bi_name_ConnectionAbortedError: db "ConnectionAbortedError", 0 +bi_name_BrokenPipeError: db "BrokenPipeError", 0 +bi_name_PermissionError: db "PermissionError", 0 +bi_name_IsADirectoryError: db "IsADirectoryError", 0 +bi_name_NotADirectoryError: db "NotADirectoryError", 0 +bi_name_ProcessLookupError: db "ProcessLookupError", 0 +bi_name_ChildProcessError: db "ChildProcessError", 0 +bi_name_BlockingIOError: db "BlockingIOError", 0 +bi_name_InterruptedError: db "InterruptedError", 0 +bi_name_FloatingPointError: db "FloatingPointError", 0 +bi_name_BufferError: db "BufferError", 0 +bi_name_ReferenceError: db "ReferenceError", 0 +bi_name_SystemError: db "SystemError", 0 +bi_name_RuntimeWarning: db "RuntimeWarning", 0 +bi_name_FutureWarning: db "FutureWarning", 0 +bi_name_ImportWarning: db "ImportWarning", 0 +bi_name_UnicodeWarning: db "UnicodeWarning", 0 +bi_name_ResourceWarning: db "ResourceWarning", 0 +bi_name_BytesWarning: db "BytesWarning", 0 +bi_name_PendingDeprecationWarning: db "PendingDeprecationWarning", 0 +bi_name_SyntaxWarning: db "SyntaxWarning", 0 +bi_name_EncodingWarning: db "EncodingWarning", 0 bi_name_list: db "list", 0 bi_name_dict: db "dict", 0 bi_name_tuple: db "tuple", 0 diff --git a/src/builtins_extra.asm b/src/builtins_extra.asm index 20661a3..a32cc1d 100644 --- a/src/builtins_extra.asm +++ b/src/builtins_extra.asm @@ -1637,11 +1637,53 @@ DEF_FUNC builtin_callable jne .callable_false ; non-pointer tag (TAG_FLOAT etc.) mov rdi, [rdi] ; args[0] payload + ; Get type of arg mov rax, [rdi + PyObject.ob_type] + + ; Check if arg is a type (all types are callable via type_call) + extern type_type + lea rcx, [rel type_type] + cmp rax, rcx + je .callable_true + extern exc_metatype + lea rcx, [rel exc_metatype] + cmp rax, rcx + je .callable_true + lea rcx, [rel user_type_metatype] + cmp rax, rcx + je .callable_true + + ; For heaptypes (user-defined classes): tp_call is set only when __call__ defined + mov rdx, [rax + PyTypeObject.tp_flags] + test rdx, TYPE_FLAG_HEAPTYPE + jnz .callable_check_heaptype + + ; For built-in types: only known callable types return True + ; (func, builtin_func, method have genuinely callable instances) + extern func_type + lea rcx, [rel func_type] + cmp rax, rcx + je .callable_true + extern builtin_func_type + lea rcx, [rel builtin_func_type] + cmp rax, rcx + je .callable_true + extern method_type + lea rcx, [rel method_type] + cmp rax, rcx + je .callable_true + + ; Not a known callable built-in type (dict, list, set, etc. instances → not callable) + jmp .callable_false + +.callable_check_heaptype: + ; Heaptype instance: check if type has tp_call set (set when __call__ defined) mov rcx, [rax + PyTypeObject.tp_call] test rcx, rcx - jz .callable_false + jnz .callable_true + jmp .callable_false +.callable_true: lea rax, [rel bool_true] inc qword [rax + PyObject.ob_refcnt] mov edx, TAG_PTR @@ -4265,3 +4307,14 @@ DEF_FUNC builtin_import_fn CSTRING rsi, "__import__() requires at least 1 argument" call raise_exception END_FUNC builtin_import_fn + +; ============================================================================ +; builtin_breakpoint(args, nargs) - breakpoint() stub (no-op) +; ============================================================================ +global builtin_breakpoint +DEF_FUNC_BARE builtin_breakpoint + ; No-op: return None + xor eax, eax + mov edx, TAG_NONE + ret +END_FUNC builtin_breakpoint diff --git a/src/eval.asm b/src/eval.asm index 777e9e7..dd29b42 100644 --- a/src/eval.asm +++ b/src/eval.asm @@ -966,7 +966,24 @@ DEF_FUNC_BARE op_raise_varargs jnz .raise_exc_obj ; Check if rdi is an exception TYPE (e.g., bare "raise ValueError") - ; ob_type of a type object is type_type or user_type_metatype + ; First verify rdi is actually a type object (ob_type == type_type, exc_metatype, + ; or user_type_metatype) to avoid segfault on non-type objects like strings + mov rax, [rdi + PyObject.ob_type] + extern type_type + lea rcx, [rel type_type] + cmp rax, rcx + je .raise_check_type + extern exc_metatype + lea rcx, [rel exc_metatype] + cmp rax, rcx + je .raise_check_type + extern user_type_metatype + lea rcx, [rel user_type_metatype] + cmp rax, rcx + jne .raise_bad ; not a type object at all + +.raise_check_type: + ; rdi is a type object — check if it's an exception subclass push rdi call type_is_exc_subclass pop rdi diff --git a/src/marshal.asm b/src/marshal.asm index 641776b..d8f5847 100644 --- a/src/marshal.asm +++ b/src/marshal.asm @@ -291,7 +291,7 @@ DEF_FUNC marshal_read_object cmp ebx, MARSHAL_TYPE_STOPITER je mdo_none ; stub: return None cmp ebx, MARSHAL_TYPE_ELLIPSIS - je mdo_none ; stub: return None + je mdo_ellipsis cmp ebx, MARSHAL_TYPE_NULL je mdo_null cmp ebx, MARSHAL_TYPE_FROZENSET @@ -332,6 +332,16 @@ mdo_none: mov edx, TAG_NONE jmp mfinish +;-------------------------------------------------------------------------- +; TYPE_ELLIPSIS handler +;-------------------------------------------------------------------------- +mdo_ellipsis: + extern ellipsis_singleton + lea rax, [rel ellipsis_singleton] + inc qword [rax + PyObject.ob_refcnt] + mov edx, TAG_PTR + jmp mfinish + ;-------------------------------------------------------------------------- ; TYPE_TRUE handler ;-------------------------------------------------------------------------- diff --git a/src/opcodes_build.asm b/src/opcodes_build.asm index 7ce45c7..ea0dbd8 100644 --- a/src/opcodes_build.asm +++ b/src/opcodes_build.asm @@ -699,9 +699,11 @@ END_FUNC op_build_const_key_map ;; op_unpack_sequence - Unpack iterable into N items on stack ;; ;; ecx = count -;; Pop TOS (tuple/list), push items[count-1], ..., items[0] (reverse order) +;; Pop TOS (tuple/list/str), push items[count-1], ..., items[0] (reverse order) ;; Followed by 1 CACHE entry (2 bytes). ;; ============================================================================ +extern str_new +extern str_type DEF_FUNC_BARE op_unpack_sequence VPOP_VAL rdi, r8 ; rdi = sequence (tuple or list), r8 = tag cmp r8d, TAG_PTR @@ -723,6 +725,10 @@ DEF_FUNC_BARE op_unpack_sequence cmp rax, rdx je .unpack_list + lea rdx, [rel str_type] + cmp rax, rdx + je .unpack_str + .unpack_type_error: ; Unknown type lea rdi, [rel exc_TypeError_type] @@ -730,12 +736,18 @@ DEF_FUNC_BARE op_unpack_sequence call raise_exception .unpack_tuple: + ; Validate count matches size + cmp rcx, [rdi + PyTupleObject.ob_size] + jne .unpack_count_error ; Items are in payload/tag arrays mov rsi, [rdi + PyTupleObject.ob_item] mov r8, [rdi + PyTupleObject.ob_item_tags] jmp .unpack_fill .unpack_list: + ; Validate count matches size + cmp rcx, [rdi + PyListObject.ob_size] + jne .unpack_count_error ; Items in payload/tag arrays mov rsi, [rdi + PyListObject.ob_item] mov r8, [rdi + PyListObject.ob_item_tags] @@ -770,6 +782,77 @@ DEF_FUNC_BARE op_unpack_sequence pop rsi ; sequence tag DECREF_VAL rdi, rsi + ; Skip 1 CACHE entry = 2 bytes + add rbx, 2 + DISPATCH + +.unpack_count_error: + ; Count mismatch: expected ecx items, got different size + pop rdi ; sequence payload + pop rsi ; sequence tag + DECREF_VAL rdi, rsi + lea rdi, [rel exc_ValueError_type] + CSTRING rsi, "not enough values to unpack" + call raise_exception + +.unpack_str: + ; String unpacking: a, b, c = "xyz" + ; Validate length matches count + cmp rcx, [rdi + PyStrObject.ob_size] + jne .unpack_count_error + + ; Use rbp-frame for the string unpacking loop + ; Save callee-saved regs + push rbx ; save bytecode IP + push r12 ; save frame + push r14 ; spare + + mov r12, rcx ; r12 = count + mov r14, rdi ; r14 = string object + + ; Pre-advance stack by count + mov edx, ecx + shl edx, 3 + add r13, rdx ; payload stack += count * 8 + add r15, rcx ; tag stack += count + + ; Create single-char strings in reverse order (count-1 down to 0) + mov ebx, ecx + dec ebx ; ebx = source index (count-1) + mov rcx, r12 + neg rcx ; rcx = -count (negative offset) + +.unpack_str_loop: + test ebx, ebx + js .unpack_str_done + + ; Create single-char string: str_new(&data[ebx], 1) + lea rdi, [r14 + PyStrObject.data] + movsxd rax, ebx + add rdi, rax ; rdi = &str.data[ebx] + mov rsi, 1 ; length = 1 + push rcx ; save negative offset + push rbx ; save source index + call str_new + pop rbx + pop rcx + ; rax = new string (TAG_PTR, refcount=1, ownership transferred to stack) + mov [r13 + rcx*8], rax + mov byte [r15 + rcx], TAG_PTR + inc rcx + dec ebx + jmp .unpack_str_loop + +.unpack_str_done: + pop r14 + pop r12 + pop rbx ; restore bytecode IP + + ; DECREF the string + pop rdi ; string payload + pop rsi ; string tag + DECREF_VAL rdi, rsi + ; Skip 1 CACHE entry = 2 bytes add rbx, 2 DISPATCH diff --git a/src/opcodes_misc.asm b/src/opcodes_misc.asm index 976ce9b..2ec5a7c 100644 --- a/src/opcodes_misc.asm +++ b/src/opcodes_misc.asm @@ -832,8 +832,48 @@ section .text pop rcx test edx, edx - jz .cmp_identity ; dunder not found → identity fallback - jmp .cmp_do_call_result ; rax = result object + jnz .cmp_do_call_result ; got result, proceed + + ; Dunder not found. If NE, try __eq__ + negate (auto-derivation) + cmp ecx, PY_NE + jne .cmp_identity ; not NE → identity fallback + + ; Try __eq__ on left's heaptype + mov rdi, [rsp + BO_LEFT] + mov rsi, [rsp + BO_RIGHT] + lea rax, [rel cmp_dunder_table] + mov rdx, [rax + PY_EQ*8] ; rdx = "__eq__" name + push rcx + mov ecx, [rsp + 8 + BO_RTAG] ; right_tag (+8 for push rcx) + call dunder_call_2 + pop rcx + test edx, edx + jz .cmp_identity ; __eq__ also not found → identity + + ; Negate __eq__ result: if True → False, if False → True + cmp edx, TAG_BOOL + je .ne_negate_tag_bool + ; Check for TAG_PTR bool (bool_true/bool_false singletons) + cmp edx, TAG_PTR + jne .cmp_do_call_result ; non-bool result, just use as-is + extern bool_true + extern bool_false + lea rcx, [rel bool_true] + cmp rax, rcx + je .ne_return_false + lea rcx, [rel bool_false] + cmp rax, rcx + je .ne_return_true + jmp .cmp_do_call_result ; not a bool ptr → use as-is +.ne_negate_tag_bool: + xor eax, 1 ; flip 0↔1 for TAG_BOOL + jmp .cmp_do_call_result +.ne_return_false: + lea rax, [rel bool_false] + jmp .cmp_do_call_result +.ne_return_true: + lea rax, [rel bool_true] + jmp .cmp_do_call_result .cmp_use_float: extern float_compare diff --git a/src/pyo/dict.asm b/src/pyo/dict.asm index b3b02c6..7384259 100644 --- a/src/pyo/dict.asm +++ b/src/pyo/dict.asm @@ -22,6 +22,7 @@ extern exc_KeyError_type extern obj_incref extern str_from_cstr extern type_type +extern tuple_type extern dict_traverse extern dict_clear_gc @@ -75,35 +76,54 @@ END_FUNC dict_new ;; dict_type_call(PyTypeObject *type, PyObject **args, int64_t nargs) -> PyDictObject* ;; Constructor: dict() or dict(mapping) ;; ============================================================================ +extern kw_names_pending +extern ap_strcmp + global dict_type_call DEF_FUNC dict_type_call push rbx push r12 + push r13 + push r14 + push r15 mov rbx, rsi ; args mov r12, rdx ; nargs - ; dict() - no args - test r12, r12 - jz .dtc_empty - - ; dict(arg) - one positional arg - cmp r12, 1 + ; Check for keyword arguments + mov r14, [rel kw_names_pending] + mov qword [rel kw_names_pending], 0 ; clear immediately + + ; Determine positional arg count + xor r13d, r13d ; r13 = n_pos = nargs + mov r13, r12 + test r14, r14 + jz .dtc_no_kw + mov rax, [r14 + PyTupleObject.ob_size] + sub r13, rax ; r13 = n_pos = nargs - n_kw + +.dtc_no_kw: + ; dict() with no pos args (may have kwargs) + test r13, r13 + jz .dtc_no_pos + + ; dict(arg) - one positional arg (may also have kwargs) + cmp r13, 1 jne .dtc_error ; Check if arg is a dict mov rdi, [rbx] ; args[0] payload mov eax, [rbx + 8] ; args[0] tag cmp eax, TAG_PTR - jne .dtc_error + jne .dtc_try_iterable mov rax, [rdi + PyObject.ob_type] lea rcx, [rel dict_type] cmp rax, rcx - jne .dtc_error + jne .dtc_try_iterable ; dict(other_dict) → create new dict and copy entries push rdi ; save source dict call dict_new - mov rbx, rax ; rbx = new dict + mov r15, rax ; r15 = new dict pop rdi ; rdi = source dict ; Copy all entries from source @@ -119,7 +139,7 @@ DEF_FUNC dict_type_call push rcx push r8 push rdi - mov rdi, rbx ; new dict + mov rdi, r15 ; new dict mov rsi, [rax + DictEntry.key] mov rdx, [rax + DictEntry.value] movzx ecx, byte [rax + DictEntry.value_tag] @@ -132,16 +152,162 @@ DEF_FUNC dict_type_call inc rcx jmp .dtc_copy_loop .dtc_copy_done: - mov rax, rbx - mov edx, TAG_PTR - pop r12 - pop rbx - leave - ret + ; Fall through to add kwargs if present + jmp .dtc_add_kwargs + +.dtc_try_iterable: + ; Not a dict — try iterating as sequence of (key, value) pairs + mov rdi, [rbx] ; args[0] payload + movzx esi, byte [rbx + 8] ; args[0] tag + cmp esi, TAG_PTR + jne .dtc_error + ; Get iterator + push rdi + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_iter] + test rax, rax + jz .dtc_error_pop + call rax + test rax, rax + jz .dtc_error_pop + add rsp, 8 ; discard saved iterable + mov r13, rax ; r13 = iterator + + ; Create new dict + call dict_new + mov r15, rax ; r15 = new dict + + ; Iterate pairs +.dtc_iter_loop: + mov rdi, r13 + mov rax, [rdi + PyObject.ob_type] + mov rax, [rax + PyTypeObject.tp_iternext] + call rax + test edx, edx + jz .dtc_iter_done ; exhausted + + ; rax = item, edx = tag — must be a tuple of length 2 + cmp edx, TAG_PTR + jne .dtc_iter_type_error + mov rcx, [rax + PyObject.ob_type] + lea r8, [rel tuple_type] + cmp rcx, r8 + jne .dtc_iter_type_error + cmp qword [rax + PyTupleObject.ob_size], 2 + jne .dtc_iter_type_error + + ; Extract key and value from tuple + push rax ; save tuple for DECREF + mov rcx, [rax + PyTupleObject.ob_item] + mov r8, [rax + PyTupleObject.ob_item_tags] + mov rdi, r15 ; dict + mov rsi, [rcx] ; key payload + mov rdx, [rcx + 8] ; value payload + movzx eax, byte [r8 + 1] ; value tag (index 1) + push rax ; save value tag + movzx r8d, byte [r8] ; key tag (index 0) + pop rcx ; rcx = value tag + call dict_set + pop rdi ; tuple + call obj_decref + jmp .dtc_iter_loop + +.dtc_iter_done: + ; DECREF iterator + mov rdi, r13 + call obj_decref + jmp .dtc_add_kwargs + +.dtc_iter_type_error: + ; DECREF iterator and raise TypeError + mov rdi, r13 + call obj_decref + jmp .dtc_error -.dtc_empty: +.dtc_error_pop: + add rsp, 8 + jmp .dtc_error + +.dtc_no_pos: + ; No positional args — create empty dict (kwargs will be added below) call dict_new + mov r15, rax + +.dtc_add_kwargs: + ; Add keyword arguments if present + test r14, r14 + jz .dtc_return_dict + + ; r14 = kw_names tuple, rbx = args, r13 was n_pos (now reuse) + ; kwargs start at args[n_pos] — reload n_pos + mov rax, r12 ; total nargs + mov rcx, [r14 + PyTupleObject.ob_size] + sub rax, rcx ; rax = n_pos + mov r13, rcx ; r13 = n_kw + mov rcx, rax ; rcx = n_pos (index into args) + + ; kw_names.ob_item has the key strings, args[n_pos + i] has values + mov rax, [r14 + PyTupleObject.ob_item] ; keys payload array + mov rdx, [r14 + PyTupleObject.ob_item_tags] ; keys tag array + xor r8d, r8d ; kw index +.dtc_kw_loop: + cmp r8, r13 + jge .dtc_return_dict + + ; Calculate arg position: args[(n_pos + r8)] + push r8 + push rcx + push rax + push rdx + + ; Get key from kw_names + mov rsi, [rax + r8*8] ; key payload (string) + movzx r8d, byte [rdx + r8] ; key tag + + ; Get value from args + lea r9, [rcx + r8] ; wait, need original r8 (kw index) + ; Recalculate: value is at args[n_pos + kw_index] + pop rdx + pop rax + pop rcx + pop r8 + + push r8 + push rcx + push rax + push rdx + + ; key from kw_names tuple items + mov r9, [r14 + PyTupleObject.ob_item] + mov rsi, [r9 + r8*8] ; key payload + mov r9, [r14 + PyTupleObject.ob_item_tags] + movzx r10d, byte [r9 + r8] ; key tag → r10d (save for later) + + ; value from args: index = n_pos + kw_index + add rcx, r8 ; rcx = n_pos + kw_index + shl rcx, 4 ; rcx * 16 (each arg is 16 bytes) + mov rdx, [rbx + rcx] ; value payload + movzx eax, byte [rbx + rcx + 8] ; value tag + + ; dict_set(dict, key, value, value_tag, key_tag) + mov rdi, r15 + mov ecx, eax ; value tag + mov r8d, r10d ; key tag + call dict_set + + pop rdx + pop rax + pop rcx + pop r8 + inc r8 + jmp .dtc_kw_loop + +.dtc_return_dict: + mov rax, r15 mov edx, TAG_PTR + pop r15 + pop r14 + pop r13 pop r12 pop rbx leave @@ -150,7 +316,7 @@ DEF_FUNC dict_type_call .dtc_error: extern exc_TypeError_type lea rdi, [rel exc_TypeError_type] - CSTRING rsi, "dict() argument must be a dict" + CSTRING rsi, "dict() argument must be a mapping or iterable" call raise_exception END_FUNC dict_type_call @@ -1504,7 +1670,9 @@ END_FUNC dict_nb_ior DRC_LEFT equ 8 DRC_RIGHT equ 16 DRC_OP equ 24 -DRC_FRAME equ 32 +DRC_LVAL equ 32 +DRC_LTAG equ 40 +DRC_FRAME equ 48 DEF_FUNC dict_richcompare, DRC_FRAME ; edx = op (PY_EQ=2, PY_NE=3) @@ -1547,11 +1715,13 @@ DEF_FUNC dict_richcompare, DRC_FRAME cmp byte [rax + DictEntry.value_tag], 0 je .drc_next - ; Save entry data for comparison + ; Save entry data to stack slots (safe across function calls) push r9 push r10 mov r11, [rax + DictEntry.value] ; left value movzx r9d, byte [rax + DictEntry.value_tag] ; left value tag + mov [rbp - DRC_LVAL], r11 ; save to stack slot + mov [rbp - DRC_LTAG], r9 ; save to stack slot ; Lookup key in right dict mov rdi, [rbp - DRC_RIGHT] @@ -1559,9 +1729,14 @@ DEF_FUNC dict_richcompare, DRC_FRAME movzx edx, byte [rax + DictEntry.key_tag] call dict_get ; rax = right value, edx = tag (0 = not found) + ; NOTE: r11 and r9 are caller-saved and may be clobbered by dict_get test edx, edx jz .drc_not_equal_pop ; key not in right + ; Reload left value and tag from stack slots + mov r11, [rbp - DRC_LVAL] + mov r9d, [rbp - DRC_LTAG] + ; Quick compare: same payload and same tag → equal cmp rax, r11 jne .drc_values_differ @@ -1584,28 +1759,24 @@ DEF_FUNC dict_richcompare, DRC_FRAME cmp edx, TAG_PTR jne .drc_not_equal_pop ; Call tp_richcompare(left_val, right_val, PY_EQ, TAG_PTR, TAG_PTR) - push r11 ; save left value mov rdi, r11 ; left value mov rsi, rax ; right value mov rax, [rdi + PyObject.ob_type] mov rax, [rax + PyTypeObject.tp_richcompare] test rax, rax - jz .drc_not_equal_pop2 ; no tp_richcompare + jz .drc_not_equal_pop ; no tp_richcompare mov edx, 2 ; PY_EQ mov ecx, TAG_PTR mov r8d, TAG_PTR call rax ; Result: (rax=payload, edx=tag) ; Check if result is True (TAG_BOOL with payload=1) - pop r11 cmp edx, TAG_BOOL jne .drc_not_equal_pop test eax, eax jz .drc_not_equal_pop jmp .drc_values_match -.drc_not_equal_pop2: - pop r11 .drc_not_equal_pop: pop r10 pop r9 diff --git a/src/pyo/exception.asm b/src/pyo/exception.asm index 535d266..257386e 100644 --- a/src/pyo/exception.asm +++ b/src/pyo/exception.asm @@ -843,6 +843,37 @@ exc_name_UserWarning: db "UserWarning", 0 exc_name_CancelledError: db "CancelledError", 0 exc_name_StopAsyncIteration: db "StopAsyncIteration", 0 exc_name_TimeoutError: db "TimeoutError", 0 +exc_name_GeneratorExit: db "GeneratorExit", 0 +exc_name_ModuleNotFoundError: db "ModuleNotFoundError", 0 +exc_name_SyntaxError: db "SyntaxError", 0 +exc_name_EOFError: db "EOFError", 0 +exc_name_UnicodeDecodeError: db "UnicodeDecodeError", 0 +exc_name_UnicodeEncodeError: db "UnicodeEncodeError", 0 +exc_name_ConnectionError: db "ConnectionError", 0 +exc_name_ConnectionResetError: db "ConnectionResetError", 0 +exc_name_ConnectionRefusedError: db "ConnectionRefusedError", 0 +exc_name_ConnectionAbortedError: db "ConnectionAbortedError", 0 +exc_name_BrokenPipeError: db "BrokenPipeError", 0 +exc_name_PermissionError: db "PermissionError", 0 +exc_name_IsADirectoryError: db "IsADirectoryError", 0 +exc_name_NotADirectoryError: db "NotADirectoryError", 0 +exc_name_ProcessLookupError: db "ProcessLookupError", 0 +exc_name_ChildProcessError: db "ChildProcessError", 0 +exc_name_BlockingIOError: db "BlockingIOError", 0 +exc_name_InterruptedError: db "InterruptedError", 0 +exc_name_FloatingPointError: db "FloatingPointError", 0 +exc_name_BufferError: db "BufferError", 0 +exc_name_ReferenceError: db "ReferenceError", 0 +exc_name_SystemError: db "SystemError", 0 +exc_name_RuntimeWarning: db "RuntimeWarning", 0 +exc_name_FutureWarning: db "FutureWarning", 0 +exc_name_ImportWarning: db "ImportWarning", 0 +exc_name_UnicodeWarning: db "UnicodeWarning", 0 +exc_name_ResourceWarning: db "ResourceWarning", 0 +exc_name_BytesWarning: db "BytesWarning", 0 +exc_name_PendingDeprecationWarning: db "PendingDeprecationWarning", 0 +exc_name_SyntaxWarning: db "SyntaxWarning", 0 +exc_name_EncodingWarning: db "EncodingWarning", 0 ; Exception metatype - provides tp_call so exception types can be called ; e.g., ValueError("msg") works via CALL opcode @@ -977,6 +1008,37 @@ DEF_EXC_TYPE exc_UserWarning_type, exc_name_UserWarning, exc_Warning_type DEF_EXC_TYPE exc_CancelledError_type, exc_name_CancelledError, exc_BaseException_type DEF_EXC_TYPE exc_StopAsyncIteration_type, exc_name_StopAsyncIteration, exc_Exception_type DEF_EXC_TYPE exc_TimeoutError_type, exc_name_TimeoutError, exc_Exception_type +DEF_EXC_TYPE exc_GeneratorExit_type, exc_name_GeneratorExit, exc_BaseException_type +DEF_EXC_TYPE exc_ModuleNotFoundError_type, exc_name_ModuleNotFoundError, exc_ImportError_type +DEF_EXC_TYPE exc_SyntaxError_type, exc_name_SyntaxError, exc_Exception_type +DEF_EXC_TYPE exc_EOFError_type, exc_name_EOFError, exc_Exception_type +DEF_EXC_TYPE exc_UnicodeDecodeError_type, exc_name_UnicodeDecodeError, exc_UnicodeError_type +DEF_EXC_TYPE exc_UnicodeEncodeError_type, exc_name_UnicodeEncodeError, exc_UnicodeError_type +DEF_EXC_TYPE exc_ConnectionError_type, exc_name_ConnectionError, exc_OSError_type +DEF_EXC_TYPE exc_ConnectionResetError_type, exc_name_ConnectionResetError, exc_ConnectionError_type +DEF_EXC_TYPE exc_ConnectionRefusedError_type, exc_name_ConnectionRefusedError, exc_ConnectionError_type +DEF_EXC_TYPE exc_ConnectionAbortedError_type, exc_name_ConnectionAbortedError, exc_ConnectionError_type +DEF_EXC_TYPE exc_BrokenPipeError_type, exc_name_BrokenPipeError, exc_ConnectionError_type +DEF_EXC_TYPE exc_PermissionError_type, exc_name_PermissionError, exc_OSError_type +DEF_EXC_TYPE exc_IsADirectoryError_type, exc_name_IsADirectoryError, exc_OSError_type +DEF_EXC_TYPE exc_NotADirectoryError_type, exc_name_NotADirectoryError, exc_OSError_type +DEF_EXC_TYPE exc_ProcessLookupError_type, exc_name_ProcessLookupError, exc_OSError_type +DEF_EXC_TYPE exc_ChildProcessError_type, exc_name_ChildProcessError, exc_OSError_type +DEF_EXC_TYPE exc_BlockingIOError_type, exc_name_BlockingIOError, exc_OSError_type +DEF_EXC_TYPE exc_InterruptedError_type, exc_name_InterruptedError, exc_OSError_type +DEF_EXC_TYPE exc_FloatingPointError_type, exc_name_FloatingPointError, exc_ArithmeticError_type +DEF_EXC_TYPE exc_BufferError_type, exc_name_BufferError, exc_Exception_type +DEF_EXC_TYPE exc_ReferenceError_type, exc_name_ReferenceError, exc_Exception_type +DEF_EXC_TYPE exc_SystemError_type, exc_name_SystemError, exc_Exception_type +DEF_EXC_TYPE exc_RuntimeWarning_type, exc_name_RuntimeWarning, exc_Warning_type +DEF_EXC_TYPE exc_FutureWarning_type, exc_name_FutureWarning, exc_Warning_type +DEF_EXC_TYPE exc_ImportWarning_type, exc_name_ImportWarning, exc_Warning_type +DEF_EXC_TYPE exc_UnicodeWarning_type, exc_name_UnicodeWarning, exc_Warning_type +DEF_EXC_TYPE exc_ResourceWarning_type, exc_name_ResourceWarning, exc_Warning_type +DEF_EXC_TYPE exc_BytesWarning_type, exc_name_BytesWarning, exc_Warning_type +DEF_EXC_TYPE exc_PendingDeprecationWarning_type, exc_name_PendingDeprecationWarning, exc_Warning_type +DEF_EXC_TYPE exc_SyntaxWarning_type, exc_name_SyntaxWarning, exc_Warning_type +DEF_EXC_TYPE exc_EncodingWarning_type, exc_name_EncodingWarning, exc_Warning_type ; Exception type lookup table indexed by EXC_* constants align 8 diff --git a/src/pyo/none.asm b/src/pyo/none.asm index ebda756..3887f25 100644 --- a/src/pyo/none.asm +++ b/src/pyo/none.asm @@ -161,3 +161,56 @@ global notimpl_singleton notimpl_singleton: dq 0x7FFFFFFFFFFFFFFF ; ob_refcnt (max value, never reaches zero) dq notimpl_type ; ob_type + +; ============================================================================ +; EllipsisType and Ellipsis singleton +; ============================================================================ + +section .text +; ellipsis_repr(PyObject *self) -> PyObject* +DEF_FUNC_BARE ellipsis_repr + lea rdi, [rel ellipsis_repr_str] + jmp str_from_cstr +END_FUNC ellipsis_repr + +section .data +ellipsis_name_str: db "ellipsis", 0 +ellipsis_repr_str: db "Ellipsis", 0 + +; EllipsisType type object +align 8 +global ellipsis_type +ellipsis_type: + dq 1 ; ob_refcnt (immortal) + dq type_type ; ob_type + dq ellipsis_name_str ; tp_name + dq PyObject_size ; tp_basicsize + dq 0 ; tp_dealloc (never deallocated) + dq ellipsis_repr ; tp_repr + dq ellipsis_repr ; tp_str + dq 0 ; tp_hash + dq 0 ; tp_call + dq 0 ; tp_getattr + dq 0 ; tp_setattr + dq 0 ; tp_richcompare + dq 0 ; tp_iter + dq 0 ; tp_iternext + dq 0 ; tp_init + dq 0 ; tp_new + dq 0 ; tp_as_number + dq 0 ; tp_as_sequence + dq 0 ; tp_as_mapping + dq 0 ; tp_base + dq 0 ; tp_dict + dq 0 ; tp_mro + dq 0 ; tp_flags + dq 0 ; tp_bases + dq 0 ; tp_traverse + dq 0 ; tp_clear + +; Ellipsis singleton - immortal object, never freed +align 8 +global ellipsis_singleton +ellipsis_singleton: + dq 0x7FFFFFFFFFFFFFFF ; ob_refcnt (max value, never reaches zero) + dq ellipsis_type ; ob_type diff --git a/src/pyo/set.asm b/src/pyo/set.asm index e3ac6f1..eb05529 100644 --- a/src/pyo/set.asm +++ b/src/pyo/set.asm @@ -478,11 +478,18 @@ DEF_FUNC set_richcompare, SRC_FRAME jmp .src_not_impl .src_is_set: - ; Only support PY_EQ (2) and PY_NE (3) cmp edx, PY_EQ je .src_eq cmp edx, PY_NE je .src_ne + cmp edx, PY_LE + je .src_le + cmp edx, PY_GE + je .src_ge + cmp edx, PY_LT + je .src_lt + cmp edx, PY_GT + je .src_gt jmp .src_not_impl .src_eq: @@ -524,6 +531,76 @@ DEF_FUNC set_richcompare, SRC_FRAME inc rcx jmp .src_eq_loop +.src_le: + ; self <= other: self is subset of other (every elem of self in other) + mov rbx, rdi ; self + mov r12, rsi ; other + mov r13, [rbx + PyDictObject.capacity] + xor ecx, ecx +.src_le_loop: + cmp rcx, r13 + jge .src_true + imul rax, rcx, SET_ENTRY_SIZE + add rax, [rbx + PyDictObject.entries] + movzx edx, word [rax + SET_ENTRY_KEY_TAG] + test edx, edx + jz .src_le_next + cmp edx, SET_TOMBSTONE + je .src_le_next + push rcx + mov rdi, r12 + mov rsi, [rax + SET_ENTRY_KEY] + movzx edx, word [rax + SET_ENTRY_KEY_TAG] + call set_contains + pop rcx + test eax, eax + jz .src_false +.src_le_next: + inc rcx + jmp .src_le_loop + +.src_ge: + ; self >= other: other is subset of self → swap and do <= + mov rbx, rsi ; other (check all of other in self) + mov r12, rdi ; self + mov r13, [rbx + PyDictObject.capacity] + xor ecx, ecx +.src_ge_loop: + cmp rcx, r13 + jge .src_true + imul rax, rcx, SET_ENTRY_SIZE + add rax, [rbx + PyDictObject.entries] + movzx edx, word [rax + SET_ENTRY_KEY_TAG] + test edx, edx + jz .src_ge_next + cmp edx, SET_TOMBSTONE + je .src_ge_next + push rcx + mov rdi, r12 + mov rsi, [rax + SET_ENTRY_KEY] + movzx edx, word [rax + SET_ENTRY_KEY_TAG] + call set_contains + pop rcx + test eax, eax + jz .src_false +.src_ge_next: + inc rcx + jmp .src_ge_loop + +.src_lt: + ; self < other: proper subset (self <= other AND len(self) < len(other)) + mov rax, [rdi + PyDictObject.ob_size] + cmp rax, [rsi + PyDictObject.ob_size] + jge .src_false ; not strictly smaller → false + jmp .src_le ; then check subset + +.src_gt: + ; self > other: proper superset (self >= other AND len(self) > len(other)) + mov rax, [rdi + PyDictObject.ob_size] + cmp rax, [rsi + PyDictObject.ob_size] + jle .src_false ; not strictly larger → false + jmp .src_ge ; then check superset + .src_ne: ; PY_NE = not PY_EQ push rdi