Do not round `a**b` to infinity

... instead, just calculate the value unless it is too big.
Also, this change raises an ArgumentError if it is expected to exceed
16 GB in a 64-bit environment.

(It is possible to calculate it straightforward, but it would likely be
out-of-memory, so I didn't think it would make sense.)

[Feature #20811]
This commit is contained in:
Yusuke Endoh 2024-11-08 14:46:35 +09:00
Родитель f7b334e002
Коммит 45cd4a8296
7 изменённых файлов: 122 добавлений и 61 удалений

Просмотреть файл

@ -6287,8 +6287,7 @@ rb_big_pow(VALUE x, VALUE y)
y = bignorm(y);
if (FIXNUM_P(y))
goto again;
rb_warn("in a**b, b may be too big");
d = rb_big2dbl(y);
rb_raise(rb_eArgError, "exponent is too large");
}
else if (FIXNUM_P(y)) {
yy = FIX2LONG(y);
@ -6304,13 +6303,16 @@ rb_big_pow(VALUE x, VALUE y)
VALUE z = 0;
SIGNED_VALUE mask;
const size_t xbits = rb_absint_numwords(x, 1, NULL);
const size_t BIGLEN_LIMIT = 32*1024*1024;
#if SIZEOF_SIZE_T == 4
const size_t BIGLEN_LIMIT = 1ULL << 31; // 2 GB
#else // SIZEOF_SIZE_T == 8
const size_t BIGLEN_LIMIT = 1ULL << 34; // 16 GB
#endif
if (xbits == (size_t)-1 ||
(xbits > BIGLEN_LIMIT) ||
(xbits * yy > BIGLEN_LIMIT)) {
rb_warn("in a**b, b may be too big");
d = (double)yy;
rb_raise(rb_eArgError, "exponent is too large");
}
else {
for (mask = FIXNUM_MAX + 1; mask; mask >>= 1) {

Просмотреть файл

@ -1048,8 +1048,7 @@ rb_rational_pow(VALUE self, VALUE other)
}
}
else if (RB_BIGNUM_TYPE_P(other)) {
rb_warn("in a**b, b may be too big");
return rb_float_pow(nurat_to_f(self), other);
rb_raise(rb_eArgError, "exponent is too large");
}
else if (RB_FLOAT_TYPE_P(other) || RB_TYPE_P(other, T_RATIONAL)) {
return rb_float_pow(nurat_to_f(self), other);

Просмотреть файл

@ -48,10 +48,18 @@ describe :integer_exponent, shared: true do
(-1).send(@method, 4611686018427387905).should eql(-1)
end
it "returns Float::INFINITY when the number is too big" do
-> {
2.send(@method, 427387904).should == Float::INFINITY
}.should complain(/warning: in a\*\*b, b may be too big/)
ruby_version_is ""..."3.4" do
it "returns Float::INFINITY when the number is too big" do
-> {
2.send(@method, 427387904).should == Float::INFINITY
}.should complain(/warning: in a\*\*b, b may be too big/)
end
end
ruby_version_is "3.4" do
it "raises an ArgumentError when the number is too big" do
-> { 100000000.send(@method, 1000000000) }.should raise_error(ArgumentError)
end
end
it "raises a ZeroDivisionError for 0 ** -1" do
@ -108,13 +116,23 @@ describe :integer_exponent, shared: true do
-> { @bignum.send(@method, :symbol) }.should raise_error(TypeError)
end
it "switch to a Float when the values is too big" do
flt = nil
-> {
flt = @bignum.send(@method, @bignum)
}.should complain(/warning: in a\*\*b, b may be too big/)
flt.should be_kind_of(Float)
flt.infinite?.should == 1
ruby_version_is ""..."3.4" do
it "switch to a Float when the values is too big" do
flt = nil
-> {
flt = @bignum.send(@method, @bignum)
}.should complain(/warning: in a\*\*b, b may be too big/)
flt.should be_kind_of(Float)
flt.infinite?.should == 1
end
end
ruby_version_is "3.4" do
it "does not switch to a Float when the values is too big" do
-> {
@bignum.send(@method, @bignum)
}.should raise_error(ArgumentError)
end
end
it "returns a complex number when negative and raised to a fractional power" do

Просмотреть файл

@ -84,45 +84,89 @@ describe :rational_exponent, shared: true do
(Rational(-1) ** bignum_value(3)).should eql(Rational(-1))
end
it "returns positive Infinity when self is > 1" do
-> {
(Rational(2) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_max) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
ruby_version_is ""..."3.4" do
it "returns positive Infinity when self is > 1" do
-> {
(Rational(2) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_max) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
end
it "returns 0.0 when self is > 1 and the exponent is negative" do
-> {
(Rational(2) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_max) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
end
end
it "returns 0.0 when self is > 1 and the exponent is negative" do
-> {
(Rational(2) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_max) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
ruby_version_is "3.4" do
it "raises an ArgumentError when self is > 1" do
-> {
(Rational(2) ** bignum_value)
}.should raise_error(ArgumentError)
-> {
(Rational(fixnum_max) ** bignum_value)
}.should raise_error(ArgumentError)
end
it "raises an ArgumentError when self is > 1 and the exponent is negative" do
-> {
(Rational(2) ** -bignum_value)
}.should raise_error(ArgumentError)
-> {
(Rational(fixnum_max) ** -bignum_value)
}.should raise_error(ArgumentError)
end
end
# Fails on linux due to pow() bugs in glibc: http://sources.redhat.com/bugzilla/show_bug.cgi?id=3866
platform_is_not :linux do
it "returns positive Infinity when self < -1" do
-> {
(Rational(-2) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(-2) ** (bignum_value + 1)).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_min) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
ruby_version_is ""..."3.4" do
it "returns positive Infinity when self < -1" do
-> {
(Rational(-2) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(-2) ** (bignum_value + 1)).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_min) ** bignum_value).infinite?.should == 1
}.should complain(/warning: in a\*\*b, b may be too big/)
end
it "returns 0.0 when self is < -1 and the exponent is negative" do
-> {
(Rational(-2) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_min) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
end
end
it "returns 0.0 when self is < -1 and the exponent is negative" do
-> {
(Rational(-2) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
-> {
(Rational(fixnum_min) ** -bignum_value).should eql(0.0)
}.should complain(/warning: in a\*\*b, b may be too big/)
ruby_version_is "3.4" do
it "returns positive Infinity when self < -1" do
-> {
(Rational(-2) ** bignum_value)
}.should raise_error(ArgumentError)
-> {
(Rational(fixnum_min) ** bignum_value)
}.should raise_error(ArgumentError)
end
it "returns 0.0 when self is < -1 and the exponent is negative" do
-> {
(Rational(-2) ** -bignum_value)
}.should raise_error(ArgumentError)
-> {
(Rational(fixnum_min) ** -bignum_value)
}.should raise_error(ArgumentError)
end
end
end
end

Просмотреть файл

@ -476,8 +476,8 @@ class TestBignum < Test::Unit::TestCase
def test_pow
assert_equal(1.0, T32 ** 0.0)
assert_equal(1.0 / T32, T32 ** -1)
assert_equal(1, assert_warning(/may be too big/) {T32 ** T32}.infinite?)
assert_equal(1, assert_warning(/may be too big/) {T32 ** (2**30-1)}.infinite?)
assert_raise(ArgumentError) { T32 ** T32 }
assert_raise(ArgumentError) { T32 ** (2**30-1) }
### rational changes the behavior of Bignum#**
#assert_raise(TypeError) { T32**"foo" }

Просмотреть файл

@ -57,20 +57,19 @@ class TestInteger < Test::Unit::TestCase
nil
end, "[ruby-dev:32084] [ruby-dev:34547]")
x = EnvUtil.suppress_warning {2 ** -0x4000000000000000}
assert_in_delta(0.0, (x / 2), Float::EPSILON)
assert_raise(ArgumentError) {2 ** -0x4000000000000000}
<<~EXPRS.each_line.with_index(__LINE__+1) do |expr, line|
crash01: 111r+11**-11111161111111
crash02: 1118111111111**-1111111111111111**1+1==11111
crash03: -1111111**-1111*11 - -1111111** -111111111
crash03: -1111111**-1111*11 - -11** -11111111
crash04: 1118111111111** -1111111111111111**1+11111111111**1 ===111
crash05: 11** -111155555555555555 -55 !=5-555
crash07: 1 + 111111111**-1111811111
crash08: 18111111111**-1111111111111111**1 + 1111111111**-1111**1
crash10: -7 - -1111111** -1111**11
crash12: 1118111111111** -1111111111111111**1 + 1111 - -1111111** -1111*111111111119
crash13: 1.0i - -1111111** -111111111
crash13: 1.0i - -11** -11111111
crash14: 11111**111111111**111111 * -11111111111111111111**-111111111111
crash15: ~1**1111 + -~1**~1**111
crash17: 11** -1111111**1111 /11i
@ -80,7 +79,7 @@ class TestInteger < Test::Unit::TestCase
crash21: 11**-10111111119-1i -1r
EXPRS
name, expr = expr.split(':', 2)
assert_ruby_status(%w"-W0", expr, name)
assert_ruby_status(%w"-W0", "begin; #{ expr }; rescue ArgumentError; end", name)
end
end

Просмотреть файл

@ -1066,11 +1066,10 @@ class Rational_Test < Test::Unit::TestCase
end
def test_power_overflow
bug = '[ruby-core:79686] [Bug #13242]: Infinity due to overflow'
x = EnvUtil.suppress_warning {4r**40000000}
assert_predicate x, :infinite?, bug
x = EnvUtil.suppress_warning {(1/4r)**40000000}
assert_equal 0, x, bug
assert_raise(ArgumentError) { 4r**400000000000000000000 }
exp = 4**40000000
assert_equal exp, 4r**40000000
assert_equal 1r/exp, (1/4r)**40000000
end
def test_positive_p