From fa87839ec17d38d7c19c515e42bdaa079bfdad1f Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 7 Apr 2026 15:48:50 +0200 Subject: [PATCH 1/2] Fix JIT+pystats crash: is_terminator fails for base opcodes Two bugs when building with --enable-experimental-jit --enable-pystats: 1. `is_terminator()` uses `_PyUop_Uncached[opcode]` to map replicated variants back to base opcodes, but the array only contains entries for replicated variants (e.g. `_JUMP_TO_TOP_r00`). For base opcodes like `_JUMP_TO_TOP` itself, it returns 0, so the terminator is not recognized. This causes `effective_trace_length()` to hit `Py_FatalError("No terminating instruction")`. Fix: fall back to the raw opcode when `_PyUop_Uncached` returns 0. 2. `jit.c` references `jit_got_size` in `OPT_STAT_ADD` but the field is missing from `OptimizationStats` in `pystats.h`, causing a build failure. Fix: add the missing `jit_got_size` field. Co-Authored-By: Claude Opus 4.6 (1M context) --- Include/cpython/pystats.h | 1 + Python/optimizer.c | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Include/cpython/pystats.h b/Include/cpython/pystats.h index e473110eca7415..3a8616fa138807 100644 --- a/Include/cpython/pystats.h +++ b/Include/cpython/pystats.h @@ -162,6 +162,7 @@ typedef struct _optimization_stats { uint64_t jit_code_size; uint64_t jit_trampoline_size; uint64_t jit_data_size; + uint64_t jit_got_size; uint64_t jit_padding_size; uint64_t jit_freed_memory_size; uint64_t trace_total_memory_hist[_Py_UOP_HIST_SIZE]; diff --git a/Python/optimizer.c b/Python/optimizer.c index f09bf778587b12..71248cda45e9b0 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -602,6 +602,9 @@ static int is_terminator(const _PyUOpInstruction *uop) { int opcode = _PyUop_Uncached[uop->opcode]; + if (opcode == 0) { + opcode = uop->opcode; + } return ( opcode == _EXIT_TRACE || opcode == _DEOPT || From 301d58bd8e7bb0910fb0eed40c29ca59b04922b4 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 7 Apr 2026 22:58:15 +0200 Subject: [PATCH 2/2] Address review: remove _PyUop_Uncached from is_terminator is_terminator() is only called before stack_allocate, where opcodes are always base opcodes. Use the raw opcode directly with an assert. In sanity_check (which sees replicated opcodes post-stack_allocate), inline the terminator check using the already-computed base_opcode. Co-Authored-By: Claude Opus 4.6 (1M context) --- Python/optimizer.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Python/optimizer.c b/Python/optimizer.c index 71248cda45e9b0..316044437bf663 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -601,10 +601,8 @@ add_to_trace( static int is_terminator(const _PyUOpInstruction *uop) { - int opcode = _PyUop_Uncached[uop->opcode]; - if (opcode == 0) { - opcode = uop->opcode; - } + int opcode = uop->opcode; + assert(opcode <= MAX_UOP_ID); return ( opcode == _EXIT_TRACE || opcode == _DEOPT || @@ -1348,7 +1346,10 @@ sanity_check(_PyExecutorObject *executor) CHECK(inst->format == UOP_FORMAT_JUMP); CHECK(inst->error_target < executor->code_size); } - if (is_terminator(inst)) { + if (base_opcode == _EXIT_TRACE || + base_opcode == _DEOPT || + base_opcode == _JUMP_TO_TOP || + base_opcode == _DYNAMIC_EXIT) { ended = true; i++; break;