Skip to content

Commit 595734c

Browse files
committed
test: D-1774910012 falsifier — closure LOAD_DEREF in except blocks
Adds Lib/test/test_phoenix_jit_inline_except_closure.py as the regression detector for D-1774910012 (28b4ee1, inline exception handler deopt fix). Original C++ fix shipped without a regression test; project memory notes 'only triggers if closure reaches threshold=1000 (currently doesn't in test suite)'. The C port (push 59 emitInlineExceptionMatch) preserves the same invariant. This test pre-exists the port so push 59's dual-arch gate naturally exercises the falsifier. 3 tests covering theologian's 4-step spec + 2 extensions: - test_load_deref_in_except_basic: closure + LOAD_DEREF in except, WARMUP=1100 - test_load_deref_with_pop_except_chain: nested try/except + exc_info chain integrity (sys.exc_info() must be clean between calls) - test_load_deref_after_pop_except: LOAD_DEREF after POP_EXCEPT verifies Py_None placeholder pop semantics + closure cell mutation correctness Baseline: 3/3 PASS on push 58 binary (8b7b935, pre-port C++ implementation). Test now becomes the regression gate for the C port. scripts/gate_phoenix.sh updated to include the new test in PHOENIX_MODULES gate (was 15 tests, now 16). Per pythia python#73 + theologian + supervisor 14:55:41Z: closes the validation gap before push 59 emitInlineExceptionMatch lands.
1 parent 8b7b935 commit 595734c

2 files changed

Lines changed: 144 additions & 2 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Phoenix JIT — D-1774910012 falsifier: closure LOAD_DEREF in except blocks.
2+
3+
Tests the post-threshold path of emitInlineExceptionMatch under JIT compilation
4+
when the except-clause body uses LOAD_DEREF on a closure variable. This is the
5+
exact failure mode documented at D-1774910012 (28b4ee14b3): closure LOAD_DEREF
6+
in except blocks caused deopt stack mismatch (Py_None placeholder + POP_EXCEPT
7+
semantics). Bug only triggered above auto-compile threshold (1000 calls).
8+
9+
The C++ fix shipped without a regression test. The C port (push 59
10+
emitInlineExceptionMatch) preserves the same invariant. This test is the
11+
regression falsifier per pythia #73 + theologian + supervisor 14:53:28Z.
12+
13+
Test design (4 steps):
14+
1. Define function with closure capturing outer variable
15+
2. except-clause body uses LOAD_DEREF on the closure variable
16+
3. Call 1001+ times to trigger threshold=1000 → JIT compile
17+
4. Verify post-threshold call exercises exception path under JIT-compiled
18+
code with no SegFault, no exc_info corruption, expected result preserved
19+
20+
Run with: ./python -m test test_phoenix_jit_inline_except_closure
21+
"""
22+
23+
import sys
24+
import unittest
25+
26+
try:
27+
import _cinderx
28+
import cinderjit
29+
HAS_JIT = True
30+
except ImportError:
31+
HAS_JIT = False
32+
33+
WARMUP = 1100 # threshold=1000, add margin
34+
35+
36+
@unittest.skipUnless(HAS_JIT, "requires JIT")
37+
class TestJitInlineExceptClosure(unittest.TestCase):
38+
"""Falsifier for D-1774910012 (inline exception handler deopt fix).
39+
40+
Each test defines an outer function that creates an inner closure where
41+
the except-clause body LOAD_DEREFs the captured variable. Warming the
42+
inner past WARMUP triggers the JIT path that previously corrupted
43+
exc_info chain.
44+
"""
45+
46+
def test_load_deref_in_except_basic(self):
47+
"""LOAD_DEREF on closure var inside except: assert no corruption."""
48+
def make_inner():
49+
captured = "captured-value"
50+
def inner(should_raise):
51+
try:
52+
if should_raise:
53+
raise ValueError("boom")
54+
return "ok"
55+
except ValueError:
56+
# LOAD_DEREF on captured (closure cell) inside except body
57+
return f"caught-with-{captured}"
58+
return inner
59+
60+
inner = make_inner()
61+
# Warmup non-raising path 1100 times to trigger JIT
62+
for _ in range(WARMUP):
63+
self.assertEqual(inner(False), "ok")
64+
self.assertTrue(cinderjit.is_jit_compiled(inner),
65+
"inner was NOT JIT-compiled after warmup")
66+
# Now exercise the except path under JIT
67+
self.assertEqual(inner(True), "caught-with-captured-value")
68+
# Run exception path many more times to surface any latent corruption
69+
for _ in range(100):
70+
self.assertEqual(inner(True), "caught-with-captured-value")
71+
self.assertEqual(inner(False), "ok")
72+
73+
def test_load_deref_with_pop_except_chain(self):
74+
"""exc_info chain integrity: nested try/except using LOAD_DEREF in both."""
75+
def make_inner():
76+
outer_msg = "outer-closure"
77+
def inner(level):
78+
try:
79+
if level == 0:
80+
raise ValueError(f"v-{outer_msg}")
81+
elif level == 1:
82+
raise TypeError(f"t-{outer_msg}")
83+
return ("clean", outer_msg)
84+
except ValueError as e:
85+
return ("ve", str(e), outer_msg)
86+
except TypeError as e:
87+
return ("te", str(e), outer_msg)
88+
return inner
89+
90+
inner = make_inner()
91+
# Warmup with all 3 paths
92+
for i in range(WARMUP):
93+
inner(2) # clean path dominates warmup
94+
self.assertTrue(cinderjit.is_jit_compiled(inner),
95+
"inner was NOT JIT-compiled after warmup")
96+
97+
# Exercise each path many times, verify result + exc_info clean
98+
for _ in range(100):
99+
r0 = inner(0)
100+
r1 = inner(1)
101+
r2 = inner(2)
102+
self.assertEqual(r0, ("ve", "v-outer-closure", "outer-closure"))
103+
self.assertEqual(r1, ("te", "t-outer-closure", "outer-closure"))
104+
self.assertEqual(r2, ("clean", "outer-closure"))
105+
# exc_info should be clean between calls (no chain corruption)
106+
self.assertIsNone(sys.exc_info()[1],
107+
"sys.exc_info corrupted post-call")
108+
109+
def test_load_deref_after_pop_except(self):
110+
"""LOAD_DEREF after POP_EXCEPT: prev_exc placeholder pop semantics."""
111+
def make_inner():
112+
shared = [0]
113+
def inner(should_raise):
114+
try:
115+
if should_raise:
116+
raise RuntimeError("trigger")
117+
return ("noraise", shared[0])
118+
except RuntimeError:
119+
pass
120+
# After POP_EXCEPT, LOAD_DEREF on shared (mutates closure)
121+
shared[0] += 1
122+
return ("post-except", shared[0])
123+
return inner
124+
125+
inner = make_inner()
126+
# Warmup non-raising path
127+
for _ in range(WARMUP):
128+
inner(False)
129+
self.assertTrue(cinderjit.is_jit_compiled(inner),
130+
"inner was NOT JIT-compiled after warmup")
131+
132+
# Run exception path; verify shared[0] mutates correctly post-POP_EXCEPT
133+
before = inner(False)[1]
134+
# Exception path must increment shared[0] each call
135+
r1 = inner(True)
136+
self.assertEqual(r1, ("post-except", before + 1))
137+
r2 = inner(True)
138+
self.assertEqual(r2, ("post-except", before + 2))
139+
140+
141+
if __name__ == "__main__":
142+
unittest.main()

scripts/gate_phoenix.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ echo "JIT Smoke: PASS" | tee -a "$RESULTS_FILE"
201201
# Step 3: Phoenix test suite
202202
echo "" | tee -a "$RESULTS_FILE"
203203
echo "--- Step 3: Phoenix Tests ---" | tee -a "$RESULTS_FILE"
204-
PHOENIX_MODULES="test_phoenix_jit_arithmetic test_phoenix_jit_autocompile test_phoenix_jit_comparisons test_phoenix_jit_containers test_phoenix_jit_controlflow test_phoenix_jit_coverage test_phoenix_jit_functions test_phoenix_jit_generators test_phoenix_jit_loadattr_golden test_phoenix_float test_phoenix_hir_type test_phoenix_profiling_hooks test_phoenix_deferred_compile test_phoenix_benchmark_correctness test_phoenix_usetype_float"
204+
PHOENIX_MODULES="test_phoenix_jit_arithmetic test_phoenix_jit_autocompile test_phoenix_jit_comparisons test_phoenix_jit_containers test_phoenix_jit_controlflow test_phoenix_jit_coverage test_phoenix_jit_functions test_phoenix_jit_generators test_phoenix_jit_inline_except_closure test_phoenix_jit_loadattr_golden test_phoenix_float test_phoenix_hir_type test_phoenix_profiling_hooks test_phoenix_deferred_compile test_phoenix_benchmark_correctness test_phoenix_usetype_float"
205205
PHOENIX_OUTPUT=$(JIT_ENABLE=1 ASAN_OPTIONS=detect_leaks=0 "$PYTHON" -m test $PHOENIX_MODULES 2>&1 || true)
206206

207207
PHOENIX_TOTAL=$(echo "$PHOENIX_OUTPUT" | grep -oP 'Total tests: run=\K[0-9]+' || echo 0)
@@ -582,7 +582,7 @@ if [ "$ARM64" -eq 1 ]; then
582582
exit 99;
583583
fi;
584584
echo \"ARM64_BINARY_MATCH: ./python reports \$ARM64_COMMIT (clean) ✓\";
585-
JIT_ENABLE=1 ./python -m test test_phoenix_jit_arithmetic test_phoenix_jit_autocompile test_phoenix_jit_comparisons test_phoenix_jit_containers test_phoenix_jit_controlflow test_phoenix_jit_coverage test_phoenix_jit_functions test_phoenix_jit_generators test_phoenix_jit_loadattr_golden test_phoenix_float test_phoenix_hir_type test_phoenix_benchmark_correctness test_phoenix_deferred_compile test_phoenix_profiling_hooks test_phoenix_usetype_float 2>&1 | tail -10;
585+
JIT_ENABLE=1 ./python -m test test_phoenix_jit_arithmetic test_phoenix_jit_autocompile test_phoenix_jit_comparisons test_phoenix_jit_containers test_phoenix_jit_controlflow test_phoenix_jit_coverage test_phoenix_jit_functions test_phoenix_jit_generators test_phoenix_jit_inline_except_closure test_phoenix_jit_loadattr_golden test_phoenix_float test_phoenix_hir_type test_phoenix_benchmark_correctness test_phoenix_deferred_compile test_phoenix_profiling_hooks test_phoenix_usetype_float 2>&1 | tail -10;
586586
echo ARM64_EXIT=\$?;
587587
echo STASH_POP_BEGIN;
588588
# Restore the original branch BEFORE popping so the stash is applied to

0 commit comments

Comments
 (0)