Skip to content

Commit b44f075

Browse files
committed
builder: emitInlineExceptionMatch → C (100/144 → 101/144 = 70.1%)
Convert HIRBuilder::emitInlineExceptionMatch — the largest, most complex method in the original 7-method remaining set (185-line C++ source with inline bytecode dispatch + 5-block + D-1774910012 invariant). Mirrors C++ HIRBuilder::emitInlineExceptionMatch @ builder.cpp:1329 (pre-conversion). Tier 5 milestone: 70.1%. emitInlineExceptionMatch is the FIRST method shipping with both encoding-clean AND invariant-preserved validation per pythia python#73 (chat 14:39:something) gap closure: D-1774910012 falsifier (3 tests, 595734c) landed pre-port and preserved post-port per testkeeper 8-test gate at chat 15:31:50Z. W25-defer-aware A1 stub-heavy bytecode-parse design (theologian chat 14:48:10Z pre-audit): ZERO new bridges. Pre-audit bridge-count gate per supervisor 14:16:55Z policy (>1 → HALT for Option B reconsideration); this method passed at 0 ≤ 1. 5 phases per pre-audit: P1: getitem CallStatic dispatch (pointer pre-resolved by C++ stub via JITRT_DictGetItem vs PyObject_GetItem) P2: exc_tc setup with depth-trim + LoadConst exc_type + JITRT_MatchAndClearException CallStatic + match_block / deopt_block CondBranch P3: match_tc setup + Py_None placeholder push (D-1774910012 PA) + bytecode dispatch loop (10 cases + default) P4: deopt_tc setup with re-push left/right + Snapshot + Deopt P5: ok block — RefineType in-place SSA-rename 3 separate PhxTranslationContext objects (PB): exc_tc copies from tc.frame match_tc copies from EXC_TC.FRAME (depth-trimmed) deopt_tc copies from TC.FRAME (original, NOT exc_tc) Approach: C++ stub iterates info.except_body bytecodes via BytecodeInstruction.nextInstr() and builds an opcode array with pre-resolved const_obj (LOAD_CONST/RETURN_CONST) and jump_target_block (JUMP_BACKWARD*). C body switches on the array. Sibling C emit functions (hir_builder_emit_swap_c / load_fast_c / store_fast_c / binary_op_c) called directly with extern decls. OpcodeArrayEntry struct mirrored on C++ + C side (5 fields per entry: opcode, oparg, base_offset, const_obj, jump_target_block). C++ static_assert on size sanity check. D-1774910012 invariant (PA pitfall): Py_None placeholder pushed on match_tc.frame.stack BEFORE dispatch loop; POP_EXCEPT case pops the placeholder. Mirrors the inline exception handler deopt fix at 28b4ee1 from 2026-04-01. Failure mode: closure LOAD_DEREF in except blocks → deopt stack mismatch → exc_info chain corruption post-threshold (only triggers post-1000 calls). Preserved via the falsifier in test_phoenix_jit_inline_except_closure.py (push 595734c). Bytecode dispatch loop opcodes (10 handled + default): POP_EXCEPT (pops placeholder per PA), POP_TOP (pops TOS), SWAP, LOAD_FAST/_CHECK/_AND_CLEAR (3 cases), LOAD_CONST, STORE_FAST, BINARY_OP, RETURN_CONST, RETURN_VALUE, JUMP_BACKWARD/_NO_INTERRUPT (2 cases), default → deopt Pitfalls addressed inline (theologian chat 14:48:10Z 8 pitfalls): PA: D-1774910012 Py_None placeholder + POP_EXCEPT pops PB: 3-TC frame-copy origins (each from correct source frame) PC: handler.depth depth-trim while-loop in P2 PD: dispatch loop default → deopt (silent path; supervisor chat 15:30:08Z (b) JIT_DCHECK addition deferred to follow-up commit pending design clarification) PE: emitted_terminator semantics replaced by `goto match_loop_done` pattern in C PF: phx_frame_state_destroy on ALL 3 TCs at function end PG: sibling C emit functions take match_tc explicitly PH: stub-built opcode array with 5 fields per entry JITRT_MatchAndClearException pointer passed from C++ stub via void* (vs C-side direct reference) — avoids C-vs-C++ name mangling at link time. Same pattern as getitem_fn. testkeeper iter cycle: iter 1: 5 errors (extern decl signature mismatches on sibling emit functions + arg counts on LOAD_FAST/STORE_FAST) iter 2: 2 errors (binary_op_c PhxTranslationContext* sig + BCOffset→int conversion) iter 3: 1 link error (JITRT_MatchAndClearException name mangling) iter 4: PASS — JIT_BUILD_EXIT=0, full --clean BUILD_EXIT=0, 8-test gate including D-1774910012 falsifier 8/8 PASS W26 gate-hardening not triggered (clean build path). ZERO new bridges held. Per pythia python#74 (chat 15:29:something): PA invariant validated by falsifier; PB-PH rely on theologian's spec + reviewer inspection. Mutation test per supervisor chat 15:30:08Z (a) is gating push 59 PUSH AUTHORIZED — testkeeper to execute before dual-arch.
1 parent 595734c commit b44f075

2 files changed

Lines changed: 299 additions & 167 deletions

File tree

‎Python/jit/hir/builder.cpp‎

Lines changed: 78 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,192 +1326,103 @@ bool HIRBuilder::getSimpleExceptInfo(
13261326
return true;
13271327
}
13281328

1329+
// OpcodeArrayEntry — mirror of the C-side struct in builder_emit_c.c.
1330+
// 5 fields per entry. Layout MUST match (verified via static_assert).
1331+
struct OpcodeArrayEntry_CXX {
1332+
int opcode;
1333+
int oparg;
1334+
int base_offset;
1335+
void* const_obj;
1336+
void* jump_target_block;
1337+
};
1338+
static_assert(sizeof(OpcodeArrayEntry_CXX) <= 64,
1339+
"OpcodeArrayEntry size sanity");
1340+
1341+
extern "C" void hir_builder_emit_inline_exception_match_c(
1342+
void* tc, void* func, void* builder,
1343+
int bc_base_offset,
1344+
int handler_depth,
1345+
void* exc_type_obj,
1346+
int except_body_offset,
1347+
HirType return_type,
1348+
void* left, void* right, void* result,
1349+
void* getitem_fn,
1350+
void* match_and_clear_fn,
1351+
const OpcodeArrayEntry_CXX* opcodes,
1352+
size_t opcode_count);
1353+
13291354
void HIRBuilder::emitInlineExceptionMatch(
1330-
CFG& cfg,
1355+
CFG& /*cfg*/,
13311356
TranslationContext& tc,
13321357
const jit::BytecodeInstruction& bc_instr,
13331358
const ExceptionTableEntry& handler,
13341359
const SimpleExceptInfo& info,
13351360
Register* left,
13361361
Register* right,
13371362
Register* result) {
1338-
// Emit dict lookup via CallStatic. For BINARY_SUBSCR_DICT (known dict
1339-
// type), use JITRT_DictGetItem which calls PyDict_GetItemWithError
1340-
// directly, avoiding PyObject_GetItem's generic type dispatch overhead.
1363+
// Pre-resolve getitem_fn pointer per BINARY_SUBSCR_DICT specialization.
13411364
void* getitem_fn = (bc_instr.opcode() == BINARY_SUBSCR_DICT)
13421365
? reinterpret_cast<void*>(JITRT_DictGetItem)
13431366
: reinterpret_cast<void*>(PyObject_GetItem);
1344-
auto call = tc.emitCallStatic(2, result, getitem_fn, TOptObject);
1345-
call->SetOperand(0, left);
1346-
call->SetOperand(1, right);
1347-
1348-
BasicBlock* ok_block = cfg.AllocateBlock();
1349-
BasicBlock* exc_match_block = cfg.AllocateBlock();
13501367

1351-
// Branch: non-null -> success, null -> exception path
1352-
tc.emitCondBranch(result, ok_block, exc_match_block);
1353-
1354-
// === Exception match block ===
1355-
// Use a SEPARATE TranslationContext so we do not corrupt tc.frame.stack.
1356-
{
1357-
TranslationContext exc_tc{exc_match_block, tc.frame};
1358-
1359-
// Pop excess stack items above handler depth.
1360-
// Refcount insertion will handle decrement automatically.
1361-
while (static_cast<int>(exc_tc.frame.stack.count) > handler.depth) {
1362-
static_cast<Register*>(phx_ptr_arr_pop(&exc_tc.frame.stack));
1368+
// Iterate except_body bytecodes, build opcode array. Mirrors C++ original
1369+
// loop at builder.cpp:1404-1483 (pre-conversion). Pre-resolves const_obj
1370+
// (LOAD_CONST/RETURN_CONST) and jump_target_block (JUMP_BACKWARD*) to
1371+
// keep PyCodeObject + getBlockAtOff access on the C++ side.
1372+
std::vector<OpcodeArrayEntry_CXX> opcodes;
1373+
BytecodeInstruction ebc{code_, info.except_body};
1374+
bool emitted_terminator = false;
1375+
while (!emitted_terminator) {
1376+
OpcodeArrayEntry_CXX entry{
1377+
ebc.opcode(),
1378+
ebc.oparg(),
1379+
ebc.baseOffset().value(),
1380+
nullptr,
1381+
nullptr};
1382+
int op = entry.opcode;
1383+
if (op == LOAD_CONST || op == RETURN_CONST) {
1384+
entry.const_obj = PyTuple_GET_ITEM(code_->co_consts, entry.oparg);
13631385
}
1364-
1365-
// Load exception type as a constant (resolved at compile time).
1366-
Register* exc_type_reg = temps_.AllocateNonStack();
1367-
exc_tc.emitLoadConst(exc_type_reg, Type::fromObject(info.exc_type));
1368-
1369-
// Call JITRT_MatchAndClearException(exc_type) via CallStatic.
1370-
// Returns int (TCInt32): 1 = matched, 0 = no match.
1371-
Register* match_result = temps_.AllocateNonStack();
1372-
auto match_call = exc_tc.emitCallStatic(
1373-
1, match_result,
1374-
reinterpret_cast<void*>(JITRT_MatchAndClearException),
1375-
TCInt32);
1376-
match_call->SetOperand(0, exc_type_reg);
1377-
1378-
BasicBlock* match_block = cfg.AllocateBlock();
1379-
BasicBlock* deopt_block = cfg.AllocateBlock();
1380-
exc_tc.emitCondBranch(match_result, match_block, deopt_block);
1381-
1382-
// === Match block: emit except body bytecodes inline ===
1383-
// Delegate to existing emit* methods to handle all edge cases.
1384-
{
1385-
TranslationContext match_tc{match_block, exc_tc.frame};
1386-
match_tc.frame.cur_instr_offs = info.except_body;
1387-
1388-
// Push Py_None as the "previous exception" placeholder.
1389-
// In CPython's interpreter, PUSH_EXC_INFO pushes the old
1390-
// exc_info->exc_value onto the stack so POP_EXCEPT can restore
1391-
// it. The JIT skips PUSH_EXC_INFO but the handler body bytecodes
1392-
// (SWAP, POP_EXCEPT) still expect prev_exc on the stack. If the
1393-
// handler body hits an unsupported opcode and deopts, the
1394-
// interpreter must find a valid prev_exc here — otherwise
1395-
// POP_EXCEPT writes a garbage pointer into exc_info->exc_value,
1396-
// corrupting the exception context chain.
1397-
Register* prev_exc_reg = temps_.AllocateStack();
1398-
match_tc.emitLoadConst(prev_exc_reg, TNoneType);
1399-
phx_ptr_arr_push(&match_tc.frame.stack, prev_exc_reg);
1400-
1401-
BytecodeInstruction ebc{code_, info.except_body};
1402-
bool emitted_terminator = false;
1403-
1404-
while (!emitted_terminator) {
1405-
switch (ebc.opcode()) {
1406-
case POP_EXCEPT:
1407-
// Pop the prev_exc placeholder we pushed above.
1408-
// In CPython, POP_EXCEPT pops the saved exception and
1409-
// restores it to exc_info->exc_value. Our placeholder is
1410-
// Py_None, which is correct: there was no active exception
1411-
// before the JIT's inline handler.
1412-
static_cast<Register*>(phx_ptr_arr_pop(&match_tc.frame.stack));
1413-
break;
1414-
1415-
case POP_TOP:
1416-
static_cast<Register*>(phx_ptr_arr_pop(&match_tc.frame.stack));
1417-
break;
1418-
1419-
case SWAP:
1420-
emitSwap(match_tc, ebc.oparg());
1421-
break;
1422-
1423-
case LOAD_FAST:
1424-
case LOAD_FAST_CHECK:
1425-
case LOAD_FAST_AND_CLEAR:
1426-
emitLoadFast(match_tc, ebc);
1427-
break;
1428-
1429-
case LOAD_CONST: {
1430-
Register* reg = temps_.AllocateStack();
1431-
Type type = Type::fromObject(
1432-
PyTuple_GET_ITEM(code_->co_consts, ebc.oparg()));
1433-
match_tc.emitLoadConst(reg, type);
1434-
phx_ptr_arr_push(&match_tc.frame.stack, reg);
1435-
break;
1436-
}
1437-
1438-
case STORE_FAST:
1439-
emitStoreFast(match_tc, ebc);
1440-
break;
1441-
1442-
case BINARY_OP:
1443-
emitBinaryOp(cfg, match_tc, ebc);
1444-
break;
1445-
1446-
case RETURN_CONST: {
1447-
Register* ret_reg = temps_.AllocateStack();
1448-
Type type = Type::fromObject(
1449-
PyTuple_GET_ITEM(code_->co_consts, ebc.oparg()));
1450-
match_tc.emitLoadConst(ret_reg, type);
1451-
match_tc.emitReturn(ret_reg, type);
1452-
emitted_terminator = true;
1453-
break;
1454-
}
1455-
1456-
case RETURN_VALUE: {
1457-
Register* ret_val = static_cast<Register*>(phx_ptr_arr_pop(&match_tc.frame.stack));
1458-
match_tc.emitReturn(ret_val, preloader_.returnType());
1459-
emitted_terminator = true;
1460-
break;
1461-
}
1462-
1463-
case JUMP_BACKWARD:
1464-
case JUMP_BACKWARD_NO_INTERRUPT: {
1465-
BCOffset class="pl-c1">getJumpTarget();
1466-
auto* target_block = getBlockAtOff(target);
1467-
match_tc.emitBranch(target_block);
1468-
emitted_terminator = true;
1469-
break;
1470-
}
1471-
1472-
default:
1473-
// Unsupported opcode: deopt to interpreter.
1474-
match_tc.frame.cur_instr_offs = ebc.baseOffset();
1475-
match_tc.emitSnapshot();
1476-
match_tc.emitDeopt();
1477-
emitted_terminator = true;
1478-
break;
1479-
}
1480-
1481-
if (!emitted_terminator) {
1482-
ebc = ebc.nextInstr();
1483-
}
1484-
}
1386+
if (op == JUMP_BACKWARD || op == JUMP_BACKWARD_NO_INTERRUPT) {
1387+
entry.jump_target_block =
1388+
static_cast<void*>(getBlockAtOff(ebc.getJumpTarget()));
14851389
}
1486-
1487-
// === Deopt block (no match -- let interpreter handle) ===
1488-
// JITRT_MatchAndClearException returned 0 and restored the pending
1489-
// exception via PyErr_SetRaisedException. We deopt back to the
1490-
// interpreter at the BINARY_SUBSCR offset with left/right re-pushed
1491-
// (emitBinaryOp already popped them, but the interpreter expects them
1492-
// on stack at this offset for correct exception_unwind depth).
1493-
//
1494-
// Deopt uses kUnhandledException (default for Deopt instruction),
1495-
// which causes the interpreter to enter the error handler directly
1496-
// (throwflag=1). The interpreter calls exception_unwind, walks
1497-
// co_exceptiontable, and finds the correct handler -- including
1498-
// outer handlers for nested try blocks.
1499-
{
1500-
TranslationContext deopt_tc{deopt_block, tc.frame};
1501-
// Push left (container) and right (key) back onto the stack.
1502-
phx_ptr_arr_push(&deopt_tc.frame.stack, left);
1503-
phx_ptr_arr_push(&deopt_tc.frame.stack, right);
1504-
deopt_tc.frame.cur_instr_offs = bc_instr.baseOffset();
1505-
deopt_tc.emitSnapshot();
1506-
deopt_tc.emitDeopt();
1390+
// Detect terminator (matches C++ original's emitted_terminator logic).
1391+
if (op == RETURN_VALUE || op == RETURN_CONST
1392+
|| op == JUMP_BACKWARD || op == JUMP_BACKWARD_NO_INTERRUPT) {
1393+
emitted_terminator = true;
1394+
} else if (op != POP_EXCEPT && op != POP_TOP && op != SWAP
1395+
&& op != LOAD_FAST && op != LOAD_FAST_CHECK
1396+
&& op != LOAD_FAST_AND_CLEAR && op != LOAD_CONST
1397+
&& op != STORE_FAST && op != BINARY_OP) {
1398+
// Default → deopt → terminator.
1399+
emitted_terminator = true;
1400+
}
1401+
opcodes.push_back(entry);
1402+
if (!emitted_terminator) {
1403+
ebc = ebc.nextInstr();
15071404
}
15081405
}
15091406

1510-
// === OK block (no exception, result is non-null) ===
1511-
tc.block = ok_block;
1512-
tc.emitRefineType(result, TObject, result);
1407+
hir_builder_emit_inline_exception_match_c(
1408+
static_cast<void*>(&tc),
1409+
static_cast<void*>(current_func_),
1410+
static_cast<void*>(this),
1411+
bc_instr.baseOffset().value(),
1412+
static_cast<int>(handler.depth),
1413+
static_cast<void*>(info.exc_type),
1414+
info.except_body.value(),
1415+
Type::toHirType(preloader_.returnType()),
1416+
static_cast<void*>(left),
1417+
static_cast<void*>(right),
1418+
static_cast<void*>(result),
1419+
getitem_fn,
1420+
reinterpret_cast<void*>(JITRT_MatchAndClearException),
1421+
opcodes.data(),
1422+
opcodes.size());
15131423
}
15141424

1425+
15151426
void HIRBuilder::emitCallExceptionHandler(
15161427
CFG& cfg,
15171428
TranslationContext& tc,

0 commit comments

Comments
 (0)