Skip to content

Commit 67869e4

Browse files
eendebakptclaude
andcommitted
Fix truediv type propagation for non-numeric types
Only set the result of NB_TRUE_DIVIDE to float when both operands are known int/float. Types like Fraction and Decimal override __truediv__ and return non-float results. The unconditional type propagation caused _POP_TOP_FLOAT to be emitted for Fraction results, crashing with an assertion failure. Fixes the segfault in test_math.testRemainder and test_random.test_binomialvariate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 97889f5 commit 67869e4

File tree

3 files changed

+40
-23
lines changed

3 files changed

+40
-23
lines changed

Lib/test/test_capi/test_opt.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3268,9 +3268,11 @@ def testfunc(args):
32683268
self.assertIn("_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT", uops)
32693269

32703270
def test_float_truediv_type_propagation(self):
3271-
# (a/b) + (c/d): inner divisions are generic _BINARY_OP but
3272-
# type propagation marks their results as float, so the +
3273-
# is specialized and the += uses inplace on the unique result
3271+
# (a/b) + (c/d): inner divisions are generic _BINARY_OP.
3272+
# Type propagation only marks results as float when operand
3273+
# types are known int/float (to avoid mistyping Fraction etc.).
3274+
# With unknown-type locals, the + is specialized via tier 1
3275+
# guards, not via optimizer type propagation.
32743276
def testfunc(args):
32753277
a, b, c, d, n = args
32763278
total = 0.0
@@ -3283,29 +3285,42 @@ def testfunc(args):
32833285
self.assertAlmostEqual(res, expected)
32843286
self.assertIsNotNone(ex)
32853287
uops = get_opnames(ex)
3286-
# The + between the two division results should use inplace
3287-
# (the a/b result is unique from type propagation)
3288-
self.assertIn("_BINARY_OP_ADD_FLOAT_INPLACE", uops)
3289-
# The += should also use inplace (the + result is unique)
3288+
# The + between the two division results is specialized
3289+
self.assertIn("_BINARY_OP_ADD_FLOAT", uops)
3290+
# The += uses inplace (the + result is unique)
32903291
self.assertIn("_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT", uops)
32913292

3292-
def test_float_truediv_unique_result_enables_inplace_add(self):
3293-
# a / b: the generic division result is marked as unique float
3294-
# by type propagation, so total += (a / b) uses inplace add
3293+
def test_float_truediv_non_float_type_no_crash(self):
3294+
# Fraction / Fraction goes through _BINARY_OP with NB_TRUE_DIVIDE
3295+
# but returns Fraction, not float. The optimizer must not assume
3296+
# the result is float for non-int/float operands. See gh-146306.
3297+
from fractions import Fraction
32953298
def testfunc(args):
32963299
a, b, n = args
3297-
total = 0.0
3300+
total = Fraction(0)
32983301
for _ in range(n):
32993302
total += a / b
3300-
return total
3303+
return float(total)
33013304

3302-
res, ex = self._run_with_optimizer(testfunc, (10.0, 3.0, TIER2_THRESHOLD))
3303-
expected = TIER2_THRESHOLD * (10.0 / 3.0)
3305+
res, ex = self._run_with_optimizer(testfunc, (Fraction(10), Fraction(3), TIER2_THRESHOLD))
3306+
expected = float(TIER2_THRESHOLD * Fraction(10, 3))
33043307
self.assertAlmostEqual(res, expected)
3305-
self.assertIsNotNone(ex)
3306-
uops = get_opnames(ex)
3307-
# The += uses inplace because the division result is unique
3308-
self.assertIn("_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT", uops)
3308+
3309+
def test_float_truediv_mixed_float_fraction_no_crash(self):
3310+
# float / Fraction: lhs is known float from a prior guard,
3311+
# but rhs is Fraction. The guard insertion for rhs should
3312+
# deopt cleanly at runtime, not crash.
3313+
from fractions import Fraction
3314+
def testfunc(args):
3315+
a, b, c, n = args
3316+
total = 0.0
3317+
for _ in range(n):
3318+
total += (a + b) / c # (a+b) is float, c is Fraction
3319+
return total
3320+
3321+
res, ex = self._run_with_optimizer(testfunc, (2.0, 3.0, Fraction(4), TIER2_THRESHOLD))
3322+
expected = TIER2_THRESHOLD * (5.0 / Fraction(4))
3323+
self.assertAlmostEqual(res, float(expected))
33093324

33103325
def test_load_attr_instance_value(self):
33113326
def testfunc(n):

Python/optimizer_bytecodes.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,11 @@ dummy_func(void) {
280280
}
281281
res = PyJitRef_MakeUnique(sym_new_type(ctx, &PyFloat_Type));
282282
}
283-
else if (oparg == NB_TRUE_DIVIDE || oparg == NB_INPLACE_TRUE_DIVIDE) {
284-
// True division always returns a new float, regardless of operand
285-
// types. Set type for downstream ops. Don't mark unique here —
286-
// the generic _BINARY_OP handles its own l/r outputs.
283+
else if ((oparg == NB_TRUE_DIVIDE || oparg == NB_INPLACE_TRUE_DIVIDE)
284+
&& (lhs_int || lhs_float) && (rhs_int || rhs_float)) {
285+
// True division of int/float operands always returns a float.
286+
// Only set the type when both operands are known int/float;
287+
// other types (Fraction, Decimal, etc.) may return non-float.
287288
res = sym_new_type(ctx, &PyFloat_Type);
288289
}
289290
else if (!((lhs_int || lhs_float) && (rhs_int || rhs_float))) {

Python/optimizer_cases.c.h

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)