diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_binary_arithmetic.py b/graalpython/com.oracle.graal.python.test/src/tests/test_binary_arithmetic.py index c4e64727dc..15463ab4cc 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_binary_arithmetic.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_binary_arithmetic.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018, 2024, Oracle and/or its affiliates. +# Copyright (c) 2018, 2026, Oracle and/or its affiliates. # Copyright (c) 2013, Regents of the University of California # # All rights reserved. @@ -195,6 +195,24 @@ def test_floor_div(): assert_exception(lambda: 5.4 // 0.0, ZeroDivisionError) +def test_large_int_floor_div(): + divisor = 2**130 + 5 + quotient = 2**80 + 3 + remainder = 2**70 + 7 + dividend = quotient * divisor + remainder + + assert dividend // divisor == quotient + assert -dividend // divisor == -quotient - 1 + assert dividend // -divisor == -quotient - 1 + assert -dividend // -divisor == quotient + + exact_dividend = quotient * divisor + assert exact_dividend // divisor == quotient + assert -exact_dividend // divisor == -quotient + assert exact_dividend // -divisor == -quotient + assert -exact_dividend // -divisor == quotient + + def test_true_div(): assert 1 / 2 == 0.5 assert 108086391056891904 / 30023997515803307 == 3.6 @@ -211,6 +229,10 @@ def test_int_rfloor_div(): def test_divmod(): + assert divmod(7, 3) == (2, 1) + assert divmod(True, True) == (1, 0) + assert int.__divmod__(1, 2.0) is NotImplemented + class Floatable: def __init__(self, val): self.val = val @@ -226,6 +248,38 @@ def doDivmod(a, b): assert_exception(lambda: doDivmod(*args), TypeError) +def test_large_int_divmod(): + divisor = 2**130 + 5 + quotient = 2**80 + 3 + remainder = 2**70 + 7 + dividend = quotient * divisor + remainder + + assert divmod(dividend, divisor) == (quotient, remainder) + assert divmod(-dividend, divisor) == (-quotient - 1, divisor - remainder) + assert divmod(dividend, -divisor) == (-quotient - 1, remainder - divisor) + assert divmod(-dividend, -divisor) == (quotient, -remainder) + + exact_dividend = quotient * divisor + assert divmod(exact_dividend, divisor) == (quotient, 0) + assert divmod(-exact_dividend, divisor) == (-quotient, 0) + assert divmod(exact_dividend, -divisor) == (-quotient, 0) + assert divmod(-exact_dividend, -divisor) == (quotient, 0) + + assert divmod(3 * divisor + 7, divisor) == (3, 7) + + long_divisor = 2**40 + 3 + large_quotient = 2**100 + 3 + assert divmod(large_quotient * long_divisor + 7, long_divisor) == (large_quotient, 7) + + long_dividend = 2**40 + 3 + assert divmod(long_dividend, divisor) == (0, long_dividend) + + long_quotient = 2**31 + 3 + long_divisor = 2**31 + 5 + assert divmod(long_quotient * long_divisor + 7, long_divisor) == (long_quotient, 7) + assert divmod(-(2**63), -1) == (2**63, 0) + + def test_subclass_ordered_binop(): class A(int): def __add__(self, other): diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_exception.py b/graalpython/com.oracle.graal.python.test/src/tests/test_exception.py index dfa1802d9e..6ec99a3a4b 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_exception.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_exception.py @@ -34,6 +34,9 @@ def fun4(test_obj): class ExceptionTests(unittest.TestCase): + def test_errno_enotsup(self): + self.assertEqual(errno.errorcode[errno.ENOTSUP], "ENOTSUP") + def test_exc_info(self): typ, val, tb = (None,) * 3 try: diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_float.py b/graalpython/com.oracle.graal.python.test/src/tests/test_float.py index 70f4895178..ae3a90f55b 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_float.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_float.py @@ -76,6 +76,9 @@ def test_rounding(self): assert round(1.123, 2) == 1.12 assert round(1.123, 3) == 1.123 assert round(1.123, 100) == 1.123 + assert round(1e23) == 99999999999999991611392 + assert round(-1e23) == -99999999999999991611392 + assert round(8.98846567431158e307) == 2**1023 import sys if sys.version_info.minor >= 6: assert 1.123.__round__(None) == 1 @@ -928,6 +931,37 @@ def test_format(self): self.assertEqual(format(NAN, 'F'), 'NAN') self.assertEqual(format(INF, 'f'), 'inf') self.assertEqual(format(INF, 'F'), 'INF') + self.assertEqual(format(INF, '07,f'), '0000inf') + self.assertEqual(format(NAN, '07,f'), '0000nan') + self.assertEqual(format(-INF, '07_f'), '-000inf') + + def test_negative_zero_format(self): + self.assertEqual(format(0.0, 'zf'), '0.000000') + self.assertEqual(format(-0.0, 'z.1f'), '0.0') + self.assertEqual(format(-0.01, 'z.1f'), '0.0') + self.assertEqual(format(-0.09, 'z.1f'), '-0.1') + self.assertEqual(format(-0.001, 'z.2e'), '-1.00e-03') + self.assertEqual(format(-0.001, 'z.2g'), '-0.001') + self.assertEqual(format(-0.001, 'z.2%'), '-0.10%') + + self.assertEqual(format(-0.0, ' z.0f'), ' 0') + self.assertEqual(format(-0.0, '+z.0f'), '+0') + self.assertEqual(format(-0.0, '-z.0f'), '0') + self.assertEqual(format(-0.0, 'z>6.1f'), 'zz-0.0') + self.assertEqual(format(-0.0, 'z>z6.1f'), 'zzz0.0') + + self.assertEqual(format(-0, 'z.1f'), '0.0') + self.assertEqual(format(complex(0.0, -0.01), 'z.1f'), '0.0+0.0j') + self.assertEqual(format(complex(-0.0, -0.0), 'z'), '(0+0j)') + + with self.assertRaisesRegex(ValueError, 'Negative zero coercion'): + format(0, 'zd') + with self.assertRaisesRegex(ValueError, 'Negative zero coercion'): + format('x', 'zs') + with self.assertRaises(ValueError): + format(0.0, 'z+f') + with self.assertRaises(ValueError): + format(0.0, 'fz') def test_format_testfile(self): with open(format_testfile) as testfile: diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_formatting.py b/graalpython/com.oracle.graal.python.test/src/tests/test_formatting.py index 0e234d1a72..cdf7b71c37 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_formatting.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_formatting.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # The Universal Permissive License (UPL), Version 1.0 @@ -123,6 +123,23 @@ def test_complex_formatting(): assert format(MyComplex(3j), "") == "42" assert format(MyComplex(3j), " <5") == "3j " assert format(complex(2**53 + 1, 0)) == '(9007199254740992+0j)' + assert format(1j, "+.1") == "+1j" + assert format(1j, " .2") == " 1j" + assert format(1000j, ",.4") == "1,000j" + assert format(1000+2000j, ",") == "(1,000+2,000j)" + assert format(1000j, "#") == "1000.j" + assert format(1+2j, "#") == "(1.+2.j)" + assert format(0j, "+.0") == "+0j" + assert format(1+2j, "+") == "(+1+2j)" + + +def test_alternate_float_formatting(): + assert format(0.0, ".0") == "0e+00" + assert format(1000.0, "#") == "1000.0" + assert format(6.103515625e-05, "#.11g") == "6.1035156250e-05" + assert format(2.220446049250313e-16, "#.038g") == "2.2204460492503130808472633361816406250e-16" + assert format(2.220446049250313e-16j, "#.038g") == ( + "0.0000000000000000000000000000000000000+2.2204460492503130808472633361816406250e-16j") class AnyRepr: diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/MathModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/MathModuleBuiltins.java index b56a70e834..c7bf6599d6 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/MathModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/MathModuleBuiltins.java @@ -2697,27 +2697,7 @@ static Object doGeneral(VirtualFrame frame, Object x, @TruffleBoundary private static BigInteger op(BigInteger x) { // assumes x >= 0 - if (x.equals(BigInteger.ZERO) || x.equals(BigInteger.ONE)) { - return x; - } - BigInteger start = BigInteger.ONE; - BigInteger end = x; - BigInteger result = BigInteger.ZERO; - BigInteger two = BigInteger.valueOf(2); - while (start.compareTo(end) <= 0) { - BigInteger mid = (start.add(end).divide(two)); - int cmp = mid.multiply(mid).compareTo(x); - if (cmp == 0) { - return mid; - } - if (cmp < 0) { - start = mid.add(BigInteger.ONE); - result = mid; - } else { - end = mid.subtract(BigInteger.ONE); - } - } - return result; + return x.sqrt(); } private static void raiseIfNegative(Node inliningTarget, boolean condition, PRaiseNode raiseNode) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/exception/OSErrorEnum.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/exception/OSErrorEnum.java index 705032dc51..a401c07a98 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/exception/OSErrorEnum.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/exception/OSErrorEnum.java @@ -180,7 +180,7 @@ public enum OSErrorEnum { EPROTONOSUPPORT(platformSpecific(93, 43), tsLiteral("Protocol not supported")), ESOCKTNOSUPPORT(platformSpecific(94, 44), tsLiteral("Socket type not supported")), EOPNOTSUPP(platformSpecific(95, 102), tsLiteral("Operation not supported on transport endpoint")), - ENOTSUP(platformSpecific(95, 102), tsLiteral("Operation not supported")), + ENOTSUP(platformSpecific(95, 45), tsLiteral("Operation not supported")), EPFNOSUPPORT(platformSpecific(96, 46), tsLiteral("Protocol family not supported")), EAFNOSUPPORT(platformSpecific(97, 47), tsLiteral("Address family not supported by protocol")), EADDRINUSE(platformSpecific(98, 48), tsLiteral("Address already in use")), diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java index f0fc8e72c2..8a25b39ce5 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/floats/FloatBuiltins.java @@ -328,7 +328,7 @@ protected static boolean isPrimitiveFloat(Node inliningTarget, Object cls, Built @Slot(value = SlotKind.tp_repr, isComplex = true) @GenerateNodeFactory public abstract static class StrNode extends AbstractNumericUnaryBuiltin { - public static final Spec spec = new Spec(' ', '>', Spec.NONE, false, Spec.UNSPECIFIED, Spec.NONE, 0, 'r'); + public static final Spec spec = new Spec(' ', '>', Spec.NONE, false, false, Spec.UNSPECIFIED, Spec.NONE, 0, 'r'); @Override protected TruffleString op(double self) { @@ -823,11 +823,10 @@ static Object round(VirtualFrame frame, Object x, Object n, @Specialization static Object round(Object xObj, @SuppressWarnings("unused") PNone none, @Bind Node inliningTarget, - @Bind PythonLanguage language, @Exclusive @Cached CastToJavaDoubleNode cast, @Cached InlinedConditionProfile nanProfile, @Cached InlinedConditionProfile infProfile, - @Cached InlinedConditionProfile isLongProfile, + @Cached PyLongFromDoubleNode longFromDoubleNode, @Exclusive @Cached PRaiseNode raiseNode) { double x = castToDoubleChecked(inliningTarget, xObj, cast); if (nanProfile.profile(inliningTarget, Double.isNaN(x))) { @@ -837,16 +836,7 @@ static Object round(Object xObj, @SuppressWarnings("unused") PNone none, throw raiseNode.raise(inliningTarget, PythonErrorType.OverflowError, ErrorMessages.CANNOT_CONVERT_S_TO_INT, "float infinity"); } double result = round(x, 0, inliningTarget, raiseNode); - if (isLongProfile.profile(inliningTarget, result > Long.MAX_VALUE || result < Long.MIN_VALUE)) { - return PFactory.createInt(language, toBigInteger(result)); - } else { - return (long) result; - } - } - - @TruffleBoundary - private static BigInteger toBigInteger(double d) { - return BigDecimal.valueOf(d).toBigInteger(); + return longFromDoubleNode.execute(inliningTarget, result); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/ints/IntBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/ints/IntBuiltins.java index a1c1e600da..00d95041ad 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/ints/IntBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/ints/IntBuiltins.java @@ -135,6 +135,7 @@ import com.oracle.graal.python.nodes.object.BuiltinClassProfiles.IsBuiltinClassExactProfile; import com.oracle.graal.python.nodes.object.GetClassNode.GetPythonObjectClassNode; import com.oracle.graal.python.nodes.truffle.PythonIntegerTypes; +import com.oracle.graal.python.nodes.util.NarrowBigIntegerNode; import com.oracle.graal.python.runtime.ExecutionContext.BoundaryCallContext; import com.oracle.graal.python.runtime.IndirectCallData.BoundaryCallData; import com.oracle.graal.python.runtime.PythonContext; @@ -890,12 +891,14 @@ static PInt doPiPi(PInt left, PInt right, @TruffleBoundary static BigInteger op(BigInteger left, BigInteger right) { // Math.floorDiv for BigInteger - BigInteger r = left.divide(right); - // if the signs are different and modulo not zero, round down - if ((left.xor(right)).signum() < 0 && (r.multiply(right).compareTo(left)) != 0) { - r = r.subtract(BigInteger.ONE); + int leftSign = left.signum(); + if (leftSign == 0 || leftSign == right.signum()) { + return left.divide(right); } - return r; + BigInteger[] quotientAndRemainder = left.divideAndRemainder(right); + return quotientAndRemainder[1].signum() == 0 + ? quotientAndRemainder[0] + : quotientAndRemainder[0].subtract(BigInteger.ONE); } @SuppressWarnings("unused") @@ -911,20 +914,80 @@ public static FloorDivNode create() { } @Slot(value = SlotKind.nb_divmod, isComplex = true) + @TypeSystemReference(PythonIntegerTypes.class) @GenerateNodeFactory abstract static class DivModNode extends BinaryOpBuiltinNode { + private static final BigInteger LONG_OVERFLOW_VALUE = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + @Specialization - static Object doGeneric(VirtualFrame frame, Object left, Object right, + static Object doLongLong(long left, long right, @Bind Node inliningTarget, - @Cached FloorDivNode floorDivNode, - @Cached ModNode modNode) { - Object div = floorDivNode.execute(frame, left, right); - if (div == PNotImplemented.NOT_IMPLEMENTED) { - return PNotImplemented.NOT_IMPLEMENTED; + @Shared @Cached PRaiseNode raiseNode) { + raiseDivisionByZero(inliningTarget, right == 0, raiseNode); + Object quotient; + if (left == Long.MIN_VALUE && right == -1) { + quotient = PFactory.createInt(PythonLanguage.get(inliningTarget), LONG_OVERFLOW_VALUE); + } else { + quotient = Math.floorDiv(left, right); + } + return PFactory.createTuple(PythonLanguage.get(inliningTarget), + new Object[]{quotient, Math.floorMod(left, right)}); + } + + @Specialization + static Object doPIntLong(PInt left, long right, + @Bind Node inliningTarget, + @Shared @Cached NarrowBigIntegerNode narrowQuotient, + @Shared @Cached NarrowBigIntegerNode narrowRemainder, + @Shared @Cached PRaiseNode raiseNode) { + raiseDivisionByZero(inliningTarget, right == 0, raiseNode); + return doBigInteger(inliningTarget, left.getValue(), PInt.longToBigInteger(right), + narrowQuotient, narrowRemainder); + } + + @Specialization + static Object doLongPInt(long left, PInt right, + @Bind Node inliningTarget, + @Shared @Cached NarrowBigIntegerNode narrowQuotient, + @Shared @Cached NarrowBigIntegerNode narrowRemainder, + @Shared @Cached PRaiseNode raiseNode) { + raiseDivisionByZero(inliningTarget, right.isZero(), raiseNode); + return doBigInteger(inliningTarget, PInt.longToBigInteger(left), right.getValue(), + narrowQuotient, narrowRemainder); + } + + @Specialization + static Object doPIntPInt(PInt left, PInt right, + @Bind Node inliningTarget, + @Shared @Cached NarrowBigIntegerNode narrowQuotient, + @Shared @Cached NarrowBigIntegerNode narrowRemainder, + @Shared @Cached PRaiseNode raiseNode) { + raiseDivisionByZero(inliningTarget, right.isZero(), raiseNode); + return doBigInteger(inliningTarget, left.getValue(), right.getValue(), narrowQuotient, narrowRemainder); + } + + private static Object doBigInteger(Node inliningTarget, BigInteger left, BigInteger right, + NarrowBigIntegerNode narrowQuotient, NarrowBigIntegerNode narrowRemainder) { + BigInteger[] quotientAndRemainder = divmod(left, right); + Object quotient = narrowQuotient.execute(inliningTarget, quotientAndRemainder[0]); + Object remainder = narrowRemainder.execute(inliningTarget, quotientAndRemainder[1]); + return PFactory.createTuple(PythonLanguage.get(inliningTarget), new Object[]{quotient, remainder}); + } + + @TruffleBoundary + private static BigInteger[] divmod(BigInteger left, BigInteger right) { + BigInteger[] quotientAndRemainder = left.divideAndRemainder(right); + if (quotientAndRemainder[1].signum() != 0 && left.signum() != right.signum()) { + quotientAndRemainder[0] = quotientAndRemainder[0].subtract(BigInteger.ONE); + quotientAndRemainder[1] = quotientAndRemainder[1].add(right); } - Object mod = modNode.execute(frame, left, right); - return PFactory.createTuple(PythonLanguage.get(inliningTarget), new Object[]{div, mod}); + return quotientAndRemainder; + } + + @Fallback + static PNotImplemented doGeneric(@SuppressWarnings("unused") Object left, @SuppressWarnings("unused") Object right) { + return PNotImplemented.NOT_IMPLEMENTED; } } @@ -1058,11 +1121,12 @@ static BigInteger opNeg(BigInteger a, BigInteger b) { if (a.signum() == 0) { return BigInteger.ZERO; } - BigInteger mod = a.mod(b.negate()); + BigInteger positiveB = b.negate(); + BigInteger mod = a.mod(positiveB); if (mod.signum() == 0) { return BigInteger.ZERO; } - return a.mod(b.negate()).subtract(b.negate()); + return mod.subtract(positiveB); } @SuppressWarnings("unused") @@ -2686,6 +2750,9 @@ private static void validateIntegerSpec(Node inliningTarget, PRaiseNode raiseNod if (Spec.specified(spec.precision)) { throw raiseNode.raise(inliningTarget, ValueError, ErrorMessages.PRECISION_NOT_ALLOWED_FOR_INT); } + if (spec.noNegativeZero && (spec.type == 'b' || spec.type == 'c' || spec.type == 'd' || spec.type == 'o' || spec.type == 'x' || spec.type == 'X' || spec.type == 'n')) { + throw raiseNode.raise(inliningTarget, ValueError, ErrorMessages.NEGATIVE_ZERO_COERCION_NOT_ALLOWED_IN_INT_FMT); + } if (spec.type == 'c') { if (Spec.specified(spec.sign)) { throw raiseNode.raise(inliningTarget, ValueError, ErrorMessages.SIGN_NOT_ALLOWED_WITH_C_FOR_INT); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/str/StringBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/str/StringBuiltins.java index 5c5ffad650..7988d7a1d4 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/str/StringBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/str/StringBuiltins.java @@ -443,6 +443,9 @@ private static Spec getAndValidateSpec(Node inliningTarget, TruffleString format throw raiseNode.raise(inliningTarget, ValueError, ErrorMessages.SIGN_NOT_ALLOWED_FOR_STRING_FMT); } } + if (spec.noNegativeZero) { + throw raiseNode.raise(inliningTarget, ValueError, ErrorMessages.NEGATIVE_ZERO_COERCION_NOT_ALLOWED_IN_STRING_FMT); + } if (spec.alternate) { throw raiseNode.raise(inliningTarget, ValueError, ErrorMessages.ALTERNATE_NOT_ALLOWED_WITH_STRING_FMT); } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java index 8ea2f836f4..4de41f7f84 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java @@ -799,6 +799,8 @@ public abstract class ErrorMessages { public static final TruffleString ZERO_PADDING_NOT_ALLOWED_FOR_COMPLEX_FMT = tsLiteral("Zero padding is not allowed in complex format specifier"); public static final TruffleString POW_THIRD_ARG_CANNOT_BE_ZERO = tsLiteral("pow() 3rd argument cannot be 0"); public static final TruffleString PRECISION_NOT_ALLOWED_FOR_INT = tsLiteral("Precision not allowed in integer format specifier"); + public static final TruffleString NEGATIVE_ZERO_COERCION_NOT_ALLOWED_IN_INT_FMT = tsLiteral("Negative zero coercion (z) not allowed in integer format specifier"); + public static final TruffleString NEGATIVE_ZERO_COERCION_NOT_ALLOWED_IN_STRING_FMT = tsLiteral("Negative zero coercion (z) not allowed in string format specifier"); public static final TruffleString SIGN_NOT_ALLOWED_WITH_C_FOR_INT = tsLiteral("Sign not allowed with integer format specifier 'c'"); public static final TruffleString ALTERNATE_NOT_ALLOWED_WITH_C_FOR_INT = tsLiteral("Alternate form (#) not allowed with integer format specifier 'c'"); public static final TruffleString ALTERNATE_NOT_ALLOWED_WITH_STRING_FMT = tsLiteral("Alternate form (#) not allowed in string format specifier"); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/ComplexFormatter.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/ComplexFormatter.java index 4b8fccc51a..630df7d130 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/ComplexFormatter.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/ComplexFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -53,7 +53,7 @@ protected ComplexFormatter(FormattingBuffer result, Spec spec, Node raisingNode) Spec imSpec; if (hasNoSpecType()) { // no type spec: should be like the default __str__ - reSpec = getComponentSpecForNoSpecType(spec, InternalFormat.Spec.NONE); + reSpec = getComponentSpecForNoSpecType(spec, spec.sign); imSpec = getComponentSpecForNoSpecType(spec, '+'); } else { // Turn off any flags that should apply to the result as a whole and not to the @@ -75,6 +75,7 @@ private static Spec getComponentSpec(Spec spec, char sign) { '\0', // (fill) '<', // (align) sign, // + spec.noNegativeZero, // spec.alternate, // -1, // (width) spec.grouping, // @@ -87,12 +88,10 @@ private static Spec getComponentSpecForNoSpecType(Spec spec, char sign) { // are printed without the decimal part, which is mostly what "g" does int precision = spec.precision; char type = 'r'; - if (precision < 0) { - precision = 0; - } else { + if (precision >= 0) { type = 'g'; } - return new InternalFormat.Spec(' ', '>', sign, false, InternalFormat.Spec.UNSPECIFIED, Spec.NONE, precision, type); + return new InternalFormat.Spec(' ', '>', sign, spec.noNegativeZero, spec.alternate, InternalFormat.Spec.UNSPECIFIED, spec.grouping, precision, type); } private boolean hasNoSpecType() { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FloatFormatter.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FloatFormatter.java index 56c56bfaa7..332cf7dbdc 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FloatFormatter.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FloatFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. * Copyright (c) 2016 Jython Developers * * Licensed under PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 @@ -39,6 +39,8 @@ public class FloatFormatter extends InternalFormat.Formatter { private int lenExponent; /** if >=0, minimum digits to follow decimal point (where consulted) */ private int minFracDigits; + /** Preserve a decimal point in alternate repr-like formats without padding to full precision. */ + private boolean forceDecimalPoint; /** * Construct the formatter from a client-supplied buffer, to which the result will be appended, @@ -51,7 +53,14 @@ public class FloatFormatter extends InternalFormat.Formatter { */ public FloatFormatter(FormattingBuffer result, Spec spec, boolean addDot0, Node raisingNode) { super(result, spec, raisingNode); - if (!addDot0 && spec.type == 'r') { + boolean reprLike = spec.type == 'r' || spec.type == Spec.NONE && !specified(spec.precision); + if (spec.alternate && reprLike) { + // In repr-like formats, alternate form only preserves the decimal point. A regular + // float still gets the fractional zero requested by Py_DTSF_ADD_DOT_0, while a + // complex component does not. + minFracDigits = addDot0 ? 1 : 0; + forceDecimalPoint = true; + } else if (!addDot0 && spec.type == 'r') { minFracDigits = 0; } else if (spec.alternate) { // Alternate form means do not trim the zero fractional digits. @@ -234,17 +243,51 @@ public FloatFormatter format(double value, String positivePrefixString) { throw unknownFormat(spec.type, "float", raisingNode); } + if (spec.noNegativeZero) { + coerceNegativeZero(positivePrefix); + } + // If the format type is an upper-case letter, convert the result to upper case. if (Character.isUpperCase(spec.type)) { uppercase(); } - // If required to, group the whole-part digits. - groupWholePartIfRequired(); + if (!Double.isFinite(value)) { + // Infinity and NaN have no integral part. In particular, sign-aware zero padding must + // not apply grouping to the padding inserted before the marker. + actualGrouping = Spec.NONE; + } else { + // If required to, group the whole-part digits. + groupWholePartIfRequired(); + } return this; } + /** Remove the negative sign if conversion and rounding produced zero. */ + private void coerceNegativeZero(String positivePrefix) { + if (lenSign != 1 || result.charAt(start) != '-' || lenWhole == 0 || lenMarker > 1) { + return; + } + int numericStart = start + lenSign; + int wholeEnd = numericStart + lenWhole; + for (int i = numericStart; i < wholeEnd; i++) { + if (result.charAt(i) != '0') { + return; + } + } + int fractionStart = wholeEnd + lenPoint; + int fractionEnd = fractionStart + lenFraction; + for (int i = fractionStart; i < fractionEnd; i++) { + if (result.charAt(i) != '0') { + return; + } + } + String replacement = positivePrefix == null ? "" : positivePrefix; + result.replace(start, start + 1, replacement); + lenSign = replacement.length(); + } + /** * Convert just the letters in the representation of the current number (in {@link #result}) to * upper case. (That's the exponent marker or the "inf" or "nan".) @@ -527,7 +570,7 @@ private void format_g(double value, String positivePrefix, int precision, int ex } else { // Finish the job as e-format. - appendExponential(pointlessDigits, exp); + appendExponential(pointlessDigits, exp, prec); } } } @@ -566,7 +609,7 @@ private void format_r(double value, String positivePrefix) { } else { // Finish the job as e-format. - appendExponential(pointlessBuffer, exp); + appendExponential(pointlessBuffer, exp, precision); } } } @@ -586,10 +629,13 @@ private void zeroHelper(int precision, int expThreshold) { if (minFracDigits < 0) { // In "alternate format", we won't economise trailing zeros. appendPointAndTrailingZeros(precision - 1); - } else if (lenFraction < minFracDigits) { + } else if (expThreshold > 0 && lenFraction < minFracDigits) { // Otherwise, it should be at least the stated minimum length. appendTrailingZeros(minFracDigits); } + if (forceDecimalPoint && lenPoint == 0) { + appendPointAndTrailingZeros(minFracDigits); + } // And just occasionally (in none-format) we go exponential even when exp = 0... if (0 >= expThreshold) { @@ -666,6 +712,9 @@ private void appendFixed(CharSequence digits, int exp, int precision) { removeTrailingZeros(minFracDigits); } } + if (forceDecimalPoint && lenPoint == 0) { + appendPointAndTrailingZeros(minFracDigits); + } } /** @@ -678,8 +727,9 @@ private void appendFixed(CharSequence digits, int exp, int precision) { * * @param digits from converting the value at a given precision. * @param exp would be the exponent (in e-format), used to position the decimal point. + * @param precision total number of significant digits. */ - private void appendExponential(CharSequence digits, int exp) { + private void appendExponential(CharSequence digits, int exp, int precision) { // The whole-part is the first digit. result.append(digits.charAt(0)); @@ -695,6 +745,13 @@ private void appendExponential(CharSequence digits, int exp) { if (minFracDigits >= 0) { // Note positive minFracDigits only applies to fixed formats. removeTrailingZeros(0); + } else { + // BigDecimal does not retain a trailing zero produced by rounding, but alternate + // g-format requires exactly the requested number of significant digits. + appendPointAndTrailingZeros(precision - 1); + } + if (forceDecimalPoint && lenPoint == 0) { + appendPointAndTrailingZeros(0); } // Finally, append the exponent as e+nn. diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FormatProcessor.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FormatProcessor.java index 921e286a94..64269ee015 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FormatProcessor.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/FormatProcessor.java @@ -412,7 +412,7 @@ private T formatImpl(Object args1) { * Encode as an InternalFormat.Spec. The values in the constructor always have specified * values, except for sign, width and precision. */ - InternalFormat.Spec spec = new InternalFormat.Spec(fill, align, sign, altFlag, width, Spec.NONE, precision, c); + InternalFormat.Spec spec = new InternalFormat.Spec(fill, align, sign, false, altFlag, width, Spec.NONE, precision, c); /* * Process argument according to format specification decoded from the string. It is diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/InternalFormat.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/InternalFormat.java index 8dcdf8d98d..1d2c8153f3 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/InternalFormat.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/formatting/InternalFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. * Copyright (c) -2016 Jython Developers * * Licensed under PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 @@ -541,7 +541,7 @@ public PException precisionTooLarge(String type) { * specification is: * *
-     * [[fill]align][sign][#][0][width][,][.precision][type]
+     * [[fill]align][sign][z][#][0][width][,][.precision][type]
      * 
* * A typical idiom is: @@ -568,6 +568,8 @@ public static class Spec { * U+FFFF if unspecified. */ public final char sign; + /** Coerce negative zero to positive zero after rounding. */ + public final boolean noNegativeZero; /** The alternative format flag '#' was given. */ public final boolean alternate; /** Width to which to pad the result, or -1 if unspecified. */ @@ -610,23 +612,25 @@ public static boolean specified(int value) { * Constructor to set all the fields in the format specifier. * *
-         * [[fill]align][sign][#][0][width][,][.precision][type]
+         * [[fill]align][sign][z][#][0][width][,][.precision][type]
          * 
* * @param fill fill character (or {@link #NONE} * @param align alignment indicator, one of {'<', '^', '>', '=' * @param sign policy, one of '+', '-', or ' '. + * @param noNegativeZero true to coerce negative zero to positive zero after rounding * @param alternate true to request alternate formatting mode ('#' flag). * @param width of field after padding or -1 to default * @param grouping true to request comma-separated groups * @param precision (e.g. decimal places) or -1 to default * @param type indicator character */ - public Spec(char fill, char align, char sign, boolean alternate, int width, + public Spec(char fill, char align, char sign, boolean noNegativeZero, boolean alternate, int width, char grouping, int precision, char type) { this.fill = fill; this.align = align; this.sign = sign; + this.noNegativeZero = noNegativeZero; this.alternate = alternate; this.width = width; this.grouping = grouping; @@ -649,6 +653,9 @@ public String toString() { if (specified(sign)) { buf.append(sign); } + if (noNegativeZero) { + buf.append('z'); + } if (alternate) { buf.append('#'); } @@ -678,7 +685,7 @@ public String toString() { * @param type indicator character */ public Spec(int precision, char type) { - this(' ', '>', NONE, false, UNSPECIFIED, NONE, precision, type); + this(' ', '>', NONE, false, false, UNSPECIFIED, NONE, precision, type); } /** The alignment from the parsed format specification, or default. */ @@ -749,13 +756,14 @@ private static class Parser { */ /* * This method is the equivalent of CPython's parse_internal_render_format_spec() in - * ~/Objects/stringlib/formatter.h. + * Python/formatter_unicode.c. */ Spec parse(char defaultType, char defaultAlignment, Node raisingNode) { char type = defaultType; char align = NONE; char fill = NONE; char sign = NONE; + boolean noNegativeZero; boolean alternate; char grouping = NONE; int width = Spec.UNSPECIFIED, precision = Spec.UNSPECIFIED; @@ -782,6 +790,9 @@ Spec parse(char defaultType, char defaultAlignment, Node raisingNode) { sign = spec.charAt(ptr++); } + // Scan [z] ... (PEP 682: suppress the sign of negative zero after rounding) + noNegativeZero = scanPast('z'); + // Scan [#] ... alternate = scanPast('#'); @@ -881,7 +892,7 @@ Spec parse(char defaultType, char defaultAlignment, Node raisingNode) { } // Create a specification - return new Spec(fill, align, sign, alternate, width, grouping, precision, type); + return new Spec(fill, align, sign, noNegativeZero, alternate, width, grouping, precision, type); } /** diff --git a/mx.graalpython/downstream_tests.py b/mx.graalpython/downstream_tests.py index 775e8b6586..a9415d007e 100644 --- a/mx.graalpython/downstream_tests.py +++ b/mx.graalpython/downstream_tests.py @@ -82,7 +82,14 @@ def downstream_test_hpy(graalpy, testdir=None, args=None, env=None, check=True, hpy_test_root = hpy_root / "test" venv = testdir / 'hpy_venv' run([graalpy, "-m", "venv", str(venv)]) - run_in_venv(venv, ["pip", "install", "pytest", "pytest-xdist", "pytest-rerunfailures!=16.0", "filelock"]) + run_in_venv(venv, [ + "pip", + "install", + "pytest==9.1.1", + "pytest-xdist==3.8.0", + "pytest-rerunfailures==16.4", + "filelock==3.30.0", + ]) env = env or os.environ.copy() env["SETUPTOOLS_SCM_PRETEND_VERSION"] = "0.9.0" run_in_venv(venv, ["pip", "install", "-e", "."], cwd=str(hpy_root), env=env)