BigMath::exp modifies its first argument
I noticed this when creating the patch at https://github.com/ruby/ruby/pull/332. I believe it affects everything from ruby 1.8 up.
BigMath::exp is implemented for negative numbers according to the identity E ** -x == 1 / (E ** x). At bigdecimal.c:2742 (in GitHub commit 258acf3, in the implementation of BigMath_s_exp), the code flips the sign on the input argument for negative numbers. Later, it returns the reciprocal of the result.
Problem: When the first argument is a BigDecimal (or, presumably, Bignum), the original argument is modified, so its sign bit remains flipped. Hence:
2.0.0-p195 :001 > require 'bigdecimal/math'
2.0.0-p195 :002 > x = BigDecimal(-1)
2.0.0-p195 :003 > BigMath.exp(x, 20)
2.0.0-p195 :004 > x
Here, x is modified when used as an argument to BigMath.exp AND the answer is wrong. I have already submitted the patch mentioned above for the latter problem, but I'm not sure what the appropriate fix would be for the sign modification. Just resetting the sign bit on exit would be easy but not thread safe; BigMath.exp really shouldn't be modifying the argument at all. But copying the whole argument is potentially wasteful if the precision is high.
I suspect that the special calculation track for negative values is actually not needed at all. Without the patch I just submitted, BigMath.exp is reliably returning the reciprocal of the correct answer, which means that it's properly calculating the correct answer by using the (negative) VALUE x passed in as the original argument -- at least for immediate values. So perhaps the basic iteration loop is just as valid for negative exponents as it is already implemented.
#1 [ruby-core:55551] Updated by GSnyder (Garth Snyder) over 4 years ago
It appears that the iteration loop in BigMath_s_exp is just calculating the series expansion (X ** N) / N! from N = 0 until the incremental term vanishes below the precision threshold. I don't see any reason this would not be equally valid for negative X. The code already checks the increment d with VpIsZero(), which accommodates either negative or positive zero.
#3 [ruby-core:55567] Updated by GSnyder (Garth Snyder) over 4 years ago
In the original report, I stated that the answer was wrong; that is incorrect. It's wrong (in the current trunk) if the first argument is a negative, immediate type, but BigDecimal arguments give correct answers. They just become modified in the process.
I experimented with just removing the special handling for negative arguments in BigMath.exp(), but there is in fact something odd about the calculation mechanism that gives incorrect results.