Skip to content

Commit 66850a4

Browse files
committed
W22 root-cause fix: populate refcount in-state from liveness for n_in==0 blocks
GDB investigation 2026-04-23 17:46Z (per Alex 17:45:31Z 'use gdb to find the problem' directive) found the await SIGSEGV root cause at phx_rs_add_copy(rs=NULL) in refcount_pass_c.c:716 (phx_rc_process_output passthrough branch). The same root cause produces the W22 yield-from crash class (SIGABRT 134 'register not live' from deopt.cpp:478). Root cause: phx_rc_use_simple_in_state's n_in==0 branch (legacy from the CinderX C++ refcount_insertion pass — see git show 1d2b9a7^:Python/ jit/hir/refcount_insertion.cpp:667-672) created an empty PhxStateMap in_state for blocks with no CFG predecessors. Generator-resume blocks (yield-from, await, plain generator entry post-RETURN_GENERATOR) all have in_edges_count() == 0 because the runtime resume edge is not modelled in the CFG. The empty in-state left env->live_regs empty; subsequent passthrough output processing called phx_sm_get(&env-> live_regs, model_out) which returned NULL and crashed at phx_rs_add_copy(NULL, ...). The bug was inherited from the C++ refcount_insertion.cpp baseline (R3b port preserved the empty-StateMap behavior verbatim) and was hidden until W32 (4a01bfa) repaired the gate's --wiring step and exposed compiled-code execution of yield-from + await patterns. W22 (d25b2f3 through 4a63c4d) narrowly worked around the yield-from trigger via a pattern-deopt in checkTranslate; this commit fixes the root cause and removes the deopt. Fix: populate the n_in==0 in-state from liveness analysis using the same hir_liveness_foreach_live_in + RegCollector pattern that phx_rc_initialize_in_state uses for multi-predecessor blocks (refcount_pass_c.c:447-462). For each live-in reg, add to in_state with PHX_REF_OWNED kind unless the register's static type is uncounted (matches the regular phx_rc_process_output ownership-default logic at line 723-734). Verification (./python rebuilt with libjit.a relink): /tmp/repro_min3.py (yield-from auto-compile 1100-iter): PASS EXIT=0, is_jit_compiled gen_yieldfrom=True (no deopt fallback needed) /tmp/test_await_autocompile.py: PROGRESSES past the W22 SIGSEGV class (phx_rs_add_copy NULL fixed) but exposes a SEPARATE deopt-side issue (deopt.cpp:478 'register v27 not live') at the YieldFrom HIR instruction's FrameState. Investigation continues. builder.cpp restored to pre-W22 state: the pattern-deopt for YIELD_VALUE+ RESUME-oparg-2 is removed; YIELD_FROM stays in isSupportedOpcode (it is a stub in 3.12 per Python/jit_common/opcode_stubs.h, so the case is dead but harmless). Per Alex 17:45:31Z + supervisor 17:45:52Z: no more bypass framing, investigate via gdb, fix the root cause.
1 parent 4a63c4d commit 66850a4

2 files changed

Lines changed: 51 additions & 46 deletions

File tree

Python/jit/hir/builder.cpp

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4899,8 +4899,7 @@ void HIRBuilder::checkTranslate() {
48994899
banned_name_ids.insert(i);
49004900
}
49014901
}
4902-
BytecodeInstructionBlock bc_block{code_};
4903-
for (auto& bci : bc_block) {
4902+
for (auto& bci : BytecodeInstructionBlock{code_}) {
49044903
auto opcode = bci.opcode();
49054904
int oparg = bci.oparg();
49064905
if (!isSupportedOpcode(opcode)) {
@@ -4928,46 +4927,6 @@ void HIRBuilder::checkTranslate() {
49284927
preloader_.fullname(),
49294928
name_at(oparg))};
49304929
}
4931-
} else if (opcode == YIELD_VALUE) {
4932-
// W22 yield-from deopt: Alex 2026-04-23 explicit one-case bypass of
4933-
// feedback_no_workarounds.md. CPython 3.12 desugars 'yield from EXPR'
4934-
// into GET_YIELD_FROM_ITER + SEND-loop where the loop body is
4935-
// (YIELD_VALUE 2, RESUME 2, JUMP_BACKWARD_NO_INTERRUPT). The HIR
4936-
// YieldFrom instruction is emitted in builder_emit_c.c:hir_builder_emit_
4937-
// yield_value_c when next bytecode is RESUME with oparg==2 (the
4938-
// RESUME oparg discriminator: 0=function-start, 1=after-plain-yield,
4939-
// 2=after-yield-from, 3=after-await). Refcount-pass-init n_in==0 on
4940-
// the generator-resume bb crashes deopt with 'register not live'
4941-
// assertion (5 architectural fix iterations did not converge cleanly).
4942-
// Author authority bypass per Alex 14:00:40Z + gatekeeper 13:59:54Z +
4943-
// theologian 14:15:09Z + supervisor 14:14:50Z concur on (C) pattern-
4944-
// deopt scope. Plain yield (RESUME oparg==1) and await (RESUME
4945-
// oparg==3) STAY in the JIT — only the yield-from desugaring pattern
4946-
// falls back to the interpreter. RULE STANDS for future cases; this
4947-
// is the ONE explicit exception.
4948-
//
4949-
// Bounds check: nextInstrOffset() can go past end of bytecode for the
4950-
// last instruction (per Python/jit/bytecode.h:57 contract). Guard
4951-
// before reading the next instruction's opcode/oparg, otherwise
4952-
// YIELD_VALUE at end-of-bytecode SIGSEGVs reading garbage memory.
4953-
// (testkeeper 14:34:52Z auto-compile asyncio regression catch.)
4954-
//
4955-
// UNIT NOTE: nextInstrOffset() returns BCOffset (bytes); bc_block.size()
4956-
// returns Py_ssize_t in instruction-count units. Assigning to BCIndex
4957-
// auto-divides by sizeof(_Py_CODEUNIT) (per bytecode_offsets.h:176),
4958-
// matching the units of size(). This mirrors the existing 3.11+
4959-
// bounds-check pattern at builder.cpp:1143. (testkeeper 14:40:22Z
4960-
// catch on units mismatch in v2.)
4961-
BCIndex next_idx = bci.nextInstrOffset();
4962-
if (next_idx < bc_block.size()) {
4963-
auto next_bc = bci.nextInstr();
4964-
if (next_bc.opcode() == RESUME && next_bc.oparg() == 2) {
4965-
throw std::runtime_error{fmt::format(
4966-
"Cannot compile {} to HIR because it uses 'yield from' "
4967-
"(W22 deopt: YIELD_VALUE+RESUME-oparg-2 pattern)",
4968-
preloader_.fullname())};
4969-
}
4970-
}
49714930
}
49724931
}
49734932
}

Python/jit/hir/refcount_pass_c.c

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,10 +496,56 @@ void phx_rc_use_simple_in_state(PhxRefcountEnv *env, void *block) {
496496
size_t n_in = hir_bb_in_edges_count(bb);
497497

498498
if (n_in == 0) {
499-
PhxStateMap empty;
500-
phx_sm_init(&empty);
501-
phx_rc_use_in_state(env, &empty);
502-
phx_sm_destroy(&empty);
499+
/* Generator-resume blocks have in_edges_count() == 0 because the
500+
* CFG does not model the runtime resume edge from the generator
501+
* machinery (every JIT-compiled generator function has at least one
502+
* such block). The live-in registers for these blocks are restored
503+
* by the runtime from the generator's saved state when execution
504+
* resumes — semantically they arrive as owned references (mortal
505+
* objects) or stay uncounted (immortals).
506+
*
507+
* The legacy implementation (inherited from CinderX C++) used an
508+
* empty in-state here, which left env->live_regs empty and caused
509+
* subsequent passthrough output processing to dereference NULL
510+
* (refcount_pass_c.c:716 phx_rs_add_copy with rs=NULL). That bug
511+
* was always present but hidden until W32 (4a01bfa3d1) repaired
512+
* the gate's --wiring step and exposed compiled-code execution of
513+
* yield-from + await patterns. W22 narrowly worked around the
514+
* yield-from trigger via a checkTranslate deopt; the proper fix
515+
* is to populate the in-state from liveness analysis here, so
516+
* every n_in==0 block (yield-from, await, plain generator entry
517+
* after RETURN_GENERATOR) gets a correct in-state.
518+
*
519+
* Per gdb investigation 2026-04-23 17:46Z: stack trace at
520+
* phx_rs_add_copy(rs=NULL) → phx_rc_process_output → passthrough
521+
* branch dereferences phx_sm_get(&env->live_regs, model_out)
522+
* which returns NULL because the live-in register was never
523+
* inserted into env->live_regs. */
524+
PhxStateMap in_state;
525+
phx_sm_init(&in_state);
526+
527+
RegCollector collector = {NULL, 0, 0};
528+
hir_liveness_foreach_live_in(env->liveness_state, block,
529+
init_in_state_collect_reg, &collector);
530+
531+
for (size_t i = 0; i < collector.count; i++) {
532+
void *reg = collector.regs[i];
533+
void *model = model_reg_rc(reg);
534+
if (phx_sm_contains(&in_state, model)) continue;
535+
536+
PhxRegState *rs = phx_sm_get_or_create(&in_state, model);
537+
/* phx_rs_init (called by get_or_create) leaves kind at the
538+
* default of PHX_REF_UNCOUNTED. For mortal-typed registers
539+
* delivered by the generator-resume runtime, mark OWNED so
540+
* the refcount pass tracks the reference correctly. */
541+
if (!phx_rc_is_uncounted(reg)) {
542+
phx_rs_set_owned(rs);
543+
}
544+
}
545+
PyMem_RawFree(collector.regs);
546+
547+
phx_rc_use_in_state(env, &in_state);
548+
phx_sm_destroy(&in_state);
503549
return;
504550
}
505551

0 commit comments

Comments
 (0)