From 5c49c13ac8b7b535eff439657c39d838c4c69ae6 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 27 Apr 2026 22:43:37 +0200 Subject: [PATCH 1/4] Fix stack underflow for BINARY_OP in the jit --- Python/optimizer_bytecodes.c | 7 +++++++ Python/optimizer_cases.c.h | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index daebef4a04320b..d3de3d087b2116 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -334,15 +334,20 @@ dummy_func(void) { } res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } + // The branches above emit a specialized binary op; every branch below + // must explicitly emit _BINARY_OP. else if (is_truediv && (lhs_int || lhs_float) && (rhs_int || rhs_float)) { + ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } else if (!((lhs_int || lhs_float) && (rhs_int || rhs_float))) { // There's something other than an int or float involved: + ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_unknown(ctx); } else if (oparg == NB_POWER || oparg == NB_INPLACE_POWER) { + ADD_OP(_BINARY_OP, oparg, 0); // This one's fun... the *type* of the result depends on the // *values* being exponentiated. However, exponents with one // constant part are reasonably common, so it's probably worth @@ -377,9 +382,11 @@ dummy_func(void) { } } else if (lhs_int && rhs_int) { + ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_type(ctx, &PyLong_Type); } else { + ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index c3c889e9de9a7e..729c69adec78cd 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -5271,12 +5271,15 @@ } else if (is_truediv && (lhs_int || lhs_float) && (rhs_int || rhs_float)) { + ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } else if (!((lhs_int || lhs_float) && (rhs_int || rhs_float))) { + ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_unknown(ctx); } else if (oparg == NB_POWER || oparg == NB_INPLACE_POWER) { + ADD_OP(_BINARY_OP, oparg, 0); if (rhs_float) { res = sym_new_unknown(ctx); } @@ -5294,9 +5297,11 @@ } } else if (lhs_int && rhs_int) { + ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_type(ctx, &PyLong_Type); } else { + ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } CHECK_STACK_BOUNDS(1); From e1e6adbbb5201430ffe6bf49f4336ecc81900ab5 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 27 Apr 2026 22:43:54 +0200 Subject: [PATCH 2/4] add regression test --- Lib/test/test_capi/test_opt.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 39075fc64cf02b..11deae580d5231 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -3919,6 +3919,38 @@ def testfunc(args): expected = TIER2_THRESHOLD * (5.0 / Fraction(4)) self.assertAlmostEqual(res, float(expected)) + def test_float_truediv_partial_float_no_stack_underflow(self): + # gh-149049: a speculative _GUARD_*_FLOAT for a partially-float + # truediv/remainder must not drop the original _BINARY_OP. + def truediv(args): + n, = args + nan = float("nan") + def victim(a=0, b=nan, c=2): + return (a + b) / c + for _ in range(n): + victim() + + def remainder(args): + n, = args + nan = float("nan") + def victim(a=0, b=nan, c=2): + return (a + b) % c + for _ in range(n): + victim() + + for testfunc in (truediv, remainder): + with self.subTest(op=testfunc.__name__): + # Iterations must be high enough that the buggy trace + # is not only built but executed (where it underflows). + _, ex = self._run_with_optimizer( + testfunc, (TIER2_THRESHOLD * 10,)) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertTrue( + "_GUARD_TOS_FLOAT" in uops or "_GUARD_NOS_FLOAT" in uops, + uops, + ) + def test_int_add_inplace_unique_lhs(self): # a * b produces a unique compact int; adding c reuses it in place def testfunc(args): From 7a020228c5c4b4af5efd5245cd75828efcd6f223 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 28 Apr 2026 14:42:32 +0200 Subject: [PATCH 3/4] use emit_op --- Python/optimizer_bytecodes.c | 15 +++++---------- Python/optimizer_cases.c.h | 13 +++++-------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index d3de3d087b2116..29f67953c81f6e 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -293,6 +293,7 @@ dummy_func(void) { || oparg == NB_INPLACE_TRUE_DIVIDE); bool is_remainder = (oparg == NB_REMAINDER || oparg == NB_INPLACE_REMAINDER); + int emit_op = _BINARY_OP; // Promote probable-float operands to known floats via speculative // guards. _RECORD_TOS_TYPE / _RECORD_NOS_TYPE in the BINARY_OP macro // record the observed operand type during tracing, which @@ -318,36 +319,31 @@ dummy_func(void) { } if (is_truediv && lhs_float && rhs_float) { if (PyJitRef_IsUnique(lhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE; l = sym_new_null(ctx); r = rhs; } else if (PyJitRef_IsUnique(rhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT; l = lhs; r = sym_new_null(ctx); } else { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT; l = lhs; r = rhs; } res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } - // The branches above emit a specialized binary op; every branch below - // must explicitly emit _BINARY_OP. else if (is_truediv && (lhs_int || lhs_float) && (rhs_int || rhs_float)) { - ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } else if (!((lhs_int || lhs_float) && (rhs_int || rhs_float))) { // There's something other than an int or float involved: - ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_unknown(ctx); } else if (oparg == NB_POWER || oparg == NB_INPLACE_POWER) { - ADD_OP(_BINARY_OP, oparg, 0); // This one's fun... the *type* of the result depends on the // *values* being exponentiated. However, exponents with one // constant part are reasonably common, so it's probably worth @@ -382,13 +378,12 @@ dummy_func(void) { } } else if (lhs_int && rhs_int) { - ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_type(ctx, &PyLong_Type); } else { - ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } + ADD_OP(emit_op, oparg, 0); } op(_BINARY_OP_ADD_INT, (left, right -- res, l, r)) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 729c69adec78cd..e4619088c8c628 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -5237,6 +5237,7 @@ || oparg == NB_INPLACE_TRUE_DIVIDE); bool is_remainder = (oparg == NB_REMAINDER || oparg == NB_INPLACE_REMAINDER); + int emit_op = _BINARY_OP; if (is_truediv || is_remainder) { if (!sym_has_type(rhs) && sym_get_probable_type(rhs) == &PyFloat_Type) { @@ -5253,17 +5254,17 @@ } if (is_truediv && lhs_float && rhs_float) { if (PyJitRef_IsUnique(lhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE; l = sym_new_null(ctx); r = rhs; } else if (PyJitRef_IsUnique(rhs)) { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT; l = lhs; r = sym_new_null(ctx); } else { - ADD_OP(_BINARY_OP_TRUEDIV_FLOAT, 0, 0); + emit_op = _BINARY_OP_TRUEDIV_FLOAT; l = lhs; r = rhs; } @@ -5271,15 +5272,12 @@ } else if (is_truediv && (lhs_int || lhs_float) && (rhs_int || rhs_float)) { - ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } else if (!((lhs_int || lhs_float) && (rhs_int || rhs_float))) { - ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_unknown(ctx); } else if (oparg == NB_POWER || oparg == NB_INPLACE_POWER) { - ADD_OP(_BINARY_OP, oparg, 0); if (rhs_float) { res = sym_new_unknown(ctx); } @@ -5297,13 +5295,12 @@ } } else if (lhs_int && rhs_int) { - ADD_OP(_BINARY_OP, oparg, 0); res = sym_new_type(ctx, &PyLong_Type); } else { - ADD_OP(_BINARY_OP, oparg, 0); res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type)); } + ADD_OP(emit_op, oparg, 0); CHECK_STACK_BOUNDS(1); stack_pointer[-2] = res; stack_pointer[-1] = l; From 901ad116e3d4faf142ca01f41d0dae074ce945b8 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:19:27 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-04-28-21-19-21.gh-issue-149049.98u2Ib.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-28-21-19-21.gh-issue-149049.98u2Ib.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-28-21-19-21.gh-issue-149049.98u2Ib.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-28-21-19-21.gh-issue-149049.98u2Ib.rst new file mode 100644 index 00000000000000..4c8f7e08a44859 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-28-21-19-21.gh-issue-149049.98u2Ib.rst @@ -0,0 +1 @@ +Fix stack underflow for ``BINARY_OP`` in tier 2.