Hi,

On Thu, Feb 25, 2010 at 6:14 AM, Kenta Murata <muraken / gmail.com> wrote:
> Hi,
>
> I've written a patch for rubyspec.

The previous behavior of Math is impossible to write rubyspecs for
without an extreme amount of pain because it is platform-dependent. By
this I mean that it even depends on the version of libc and at least
on Linux glibc changed whether EDOM or ERANGE is set for atanh(-1)
between versions.

The consequence is that there is no accessible information in Ruby to
determine one version of Linux with a libc that sets EDOM from one
that sets ERANGE, which makes it almost impossible to write a sane
rubyspec guard. I have carefully looked through all rbconfig.rb
settings and other constants. I would be happy to be proven wrong.
Even with the existing platform information used by the rubyspec
guards, the specs are extremely convoluted and difficult.

I am very pleased to see this effort to make Math sane in the face of
platform oddities. But this still leaves the question of how to
provide rubyspecs for the behavior of Math prior to 1.9.2. Here is my
proposal.

I have added two new guards to rubyspec: #unspecified, and
#specified_on. The #unspecified guard never yields on MRI (regardless
of version). It is used to contain and document specs for particular
behaviors that MRI has either specifically said are "unspecified" or
"implementation-specific" or are impossible to spec in a consistent
manner. The #specified_on guard takes one or more implementation
designators and yields on those implementations.

For the Math specs, this is what I propose:

1. For all the standard behaviors (ie inputs in the domain), a single
set of rubyspecs for each method should be consistent across MRI
versions. These behaviors are spec'd normally.
2. For exceptional behaviors, the specs are written for the new MRI
platform-independent behaviors (eg using Math::DomainError exception).
3. The existing platform-dependent behaviors in 1.8.x are explicitly
"unspecified" in the specs.
4. Implementations are free to opt-in to the sane behaviors being
established for 1.9.2

This is an example of how the specs for Math.atanh would appear

describe "Math.atanh" do
  # standard specs for basic behaviors
  # ...

  ruby_version_is ""..."1.9" do
    unspecified do
      specified_on :rubinius, :jruby, :ironruby do
        it_behaves_like :math_atanh_exceptional, :atanh, Math
      end
    end
  end

  ruby_version_is "1.9" do
    it_behaves_like :math_atanh_exceptional, :atanh, Math
  end
end

Please note the following:
* I am not proposing the Math behavior be *generally* unspecified on
MRI versions before 1.9.2. Only the exceptional behaviors that are
currently wildly platform-dependent.
* I am not proposing anything really different than the existing MRI
behaviors since they are so platform-dependent they appear almost
random to normal Ruby code. I would only be formalizing that in the
specs with the #unspecified guard.
* Alternate implementations must have some specs to guide their
implementation. Only saying the behavior is implementation-specific
with no specs is not acceptable for rubyspec.
* I am not proposing anything really different for existing
alternative implementations. For example, Java already has platform
independent behaviors for eg atanh() and that is exposed in JRuby,
thereby requiring multiple "not_compliant_on :jruby" and "compliant_on
:jruby" guards which further complicate the specs and are rather
unfair (is it fair to say that since JRuby has nice, consistent
behavior when MRI has weird platform-dependent behavior, that JRuby is
"non-comforming"? Technically yes, but the whole point of rubyspec is
to provide consistent behavior to Ruby code!)
* The exceptional behaviors should impact much less code than the
normal behaviors. If an alternative implementation follows the 1.9.2
sane behavior in compatibility versions < 1.9.2, that may result in
bugs with existing Ruby code. Should that occur, the implementation
can decided how to deal with it. The value of having an explicit,
reasonable behavior for Math methods outweighs the possibility that
the consistent behavior may cause compatibility issues.
* This particular area of rubyspec (ie Math methods) is highly
exceptional in that most of MRI does not have such difficult-to-define
behavior.
* I would be using the behavior of 1.9.2 for the #specified_on
behaviors for compatibility versions < 1.9.2. In other words, for
alternative implementations that opt-in, the behaviors would be those
of 1.9.2 not some random other behavior.

Rubyspec patches for the new 1.9.2 platform-independent Math behaviors
are very welcome and much appreciated.

Cheers,
Brian

>
>
> diff --git a/core/math/gamma_spec.rb b/core/math/gamma_spec.rb
> index 4215075..4b291d3 100644
> --- a/core/math/gamma_spec.rb
> +++ b/core/math/gamma_spec.rb
> @@ -33,23 +33,39 @@ ruby_version_is "1.9" do
> =A0 =A0 it "returns good numerical approximation for gamma(-0.00001)" do
> =A0 =A0 =A0 Math.gamma(-0.00001).should be_close(-100000.577225, TOLERANC=
E)
> =A0 =A0 end
> -
> - =A0 =A0it "raises Domain Error given -1" do
> - =A0 =A0 =A0lambda { Math.gamma(-1) }.should raise_error(Errno::EDOM)
> +
> + =A0 =A0ruby_version_is ""..."1.9" do
> + =A0 =A0 =A0it "raises Domain Error given -1" do
> + =A0 =A0 =A0 =A0lambda { Math.gamma(-1) }.should raise_error(Errno::EDOM=
)
> + =A0 =A0 =A0end
> =A0 =A0 end
> -
> +
> + =A0 =A0ruby_version_is "1.9" do
> + =A0 =A0 =A0it "raises Math::DomainError given -1" do
> + =A0 =A0 =A0 =A0lambda { Math.gamma(-1) }.should raise_error(Math::Domai=
nError)
> + =A0 =A0 =A0end
> + =A0 =A0end
> +
> =A0 =A0 # See http://redmine.ruby-lang.org/issues/show/2189
> =A0 =A0 it "returns +infinity given +infinity" do
> =A0 =A0 =A0 Math.gamma(infinity_value).infinite?.should =3D=3D 1
> =A0 =A0 end
> -
> - =A0 =A0it "raises Domain Error given negative infinity" do
> - =A0 =A0 =A0lambda { Math.gamma(-infinity_value) }.should raise_error(Er=
rno::EDOM)
> +
> + =A0 =A0ruby_version_is ""..."1.9" do
> + =A0 =A0 =A0it "raises Domain Error given negative infinity" do
> + =A0 =A0 =A0 =A0lambda { Math.gamma(-infinity_value) }.should raise_erro=
r(Errno::EDOM)
> + =A0 =A0 =A0end
> =A0 =A0 end
> -
> +
> + =A0 =A0ruby_version_is "1.9" do
> + =A0 =A0 =A0it "raises Math::DomainError given negative infinity" do
> + =A0 =A0 =A0 =A0lambda { Math.gamma(-infinity_value) }.should raise_erro=
r(Math::DomainError)
> + =A0 =A0 =A0end
> + =A0 =A0end
> +
> =A0 =A0 it "returns NaN given NaN" do
> =A0 =A0 =A0 Math.gamma(nan_value).nan?.should be_true
> =A0 =A0 end
>
> =A0 end
> -end
> \ No newline at end of file
> +end
> diff --git a/core/math/lgamma_spec.rb b/core/math/lgamma_spec.rb
> index c3bc48e..1ea5bf4 100644
> --- a/core/math/lgamma_spec.rb
> +++ b/core/math/lgamma_spec.rb
> @@ -34,8 +34,16 @@ ruby_version_is "1.9" do
> =A0 =A0 =A0 lg2[1].should =3D=3D 1
> =A0 =A0 end
>
> - =A0 =A0it "returns [Infinity, 1] when passed -Infinity" do
> - =A0 =A0 =A0Math.lgamma(-infinity_value).should =3D=3D [infinity_value, =
1]
> + =A0 =A0ruby_version_is ""..."1.9" do
> + =A0 =A0 =A0it "returns [Infinity, 1] when passed -Infinity" do
> + =A0 =A0 =A0 =A0Math.lgamma(-infinity_value).should =3D=3D [infinity_val=
ue, 1]
> + =A0 =A0 =A0end
> + =A0 =A0end
> +
> + =A0 =A0ruby_version_is "1.9" do
> + =A0 =A0 =A0it "raises Math::DomainError when passed -Infinity" do
> + =A0 =A0 =A0 =A0lambda { Math.lgamma(-infinity_value) }.should raise_err=
or(Math::DomainError)
> + =A0 =A0 =A0end
> =A0 =A0 end
>
> =A0 =A0 # Note: see related issue http://redmine.ruby-lang.org/issues/sho=
w/2189
> @@ -50,15 +58,23 @@ ruby_version_is "1.9" do
> =A0 =A0 # Note: see related issue http://redmine.ruby-lang.org/issues/sho=
w/2189
> =A0 =A0 # If you would like to see simpler Ruby behavior, lobby for Ruby =
to
> =A0 =A0 # have platform-independent Math functions.
> - =A0 =A0platform_is :darwin do
> - =A0 =A0 =A0# JRuby has platform-independent math and behaves as above
> - =A0 =A0 =A0not_compliant_on :jruby do
> - =A0 =A0 =A0 =A0it "raises an Errno::EDOM when passed Infinity" do
> - =A0 =A0 =A0 =A0 =A0lambda { Math.lgamma(infinity_value) }.should raise_=
error(Errno::EDOM)
> + =A0 =A0ruby_version_is ""..."1.9" do
> + =A0 =A0 =A0platform_is :darwin do
> + =A0 =A0 =A0 =A0# JRuby has platform-independent math and behaves as abo=
ve
> + =A0 =A0 =A0 =A0not_compliant_on :jruby do
> + =A0 =A0 =A0 =A0 =A0it "raises an Errno::EDOM when passed Infinity" do
> + =A0 =A0 =A0 =A0 =A0 =A0lambda { Math.lgamma(infinity_value) }.should ra=
ise_error(Errno::EDOM)
> + =A0 =A0 =A0 =A0 =A0end
> =A0 =A0 =A0 =A0 end
> =A0 =A0 =A0 end
> =A0 =A0 end
>
> + =A0 =A0ruby_version_is "1.9" do
> + =A0 =A0 =A0it "returns [Infinity, 1] when passed Infinity" do
> + =A0 =A0 =A0 =A0Math.lgamma(infinity_value).should =3D=3D [infinity_valu=
e, 1]
> + =A0 =A0 =A0end
> + =A0 =A0end
> +
> =A0 =A0 it "returns [NaN, 1] when passed NaN" do
> =A0 =A0 =A0 Math.lgamma(nan_value)[0].nan?.should be_true
> =A0 =A0 =A0 Math.lgamma(nan_value)[1].should =3D=3D 1
> diff --git a/library/mathn/math/shared/rsqrt.rb b/library/mathn/math/shar=
ed/rsqrt.rb
> index ff12abf..3d1b424 100644
> --- a/library/mathn/math/shared/rsqrt.rb
> +++ b/library/mathn/math/shared/rsqrt.rb
> @@ -13,9 +13,19 @@ describe :mathn_math_rsqrt, :shared =3D> true do
> =A0 =A0 @object.send(:rsqrt, 12.34).should =3D=3D Math.sqrt!(12.34)
> =A0 end
>
> - =A0it "raises an Errno::EDOM if the argument is a negative number" do
> - =A0 =A0lambda { @object.send(:rsqrt, -1) }.should raise_error(Errno::ED=
OM)
> - =A0 =A0lambda { @object.send(:rsqrt, -4.0) }.should raise_error(Errno::=
EDOM)
> - =A0 =A0lambda { @object.send(:rsqrt, -16/64) }.should raise_error(Errno=
::EDOM)
> + =A0ruby_version_is ""..."1.9" do
> + =A0 =A0it "raises an Errno::EDOM if the argument is a negative number" =
do
> + =A0 =A0 =A0lambda { @object.send(:rsqrt, -1) }.should raise_error(Errno=
::EDOM)
> + =A0 =A0 =A0lambda { @object.send(:rsqrt, -4.0) }.should raise_error(Err=
no::EDOM)
> + =A0 =A0 =A0lambda { @object.send(:rsqrt, -16/64) }.should raise_error(E=
rrno::EDOM)
> + =A0 =A0end
> + =A0end
> +
> + =A0ruby_version_is "1.9" do
> + =A0 =A0it "raises an Math::DomainError if the argument is a negative nu=
mber" do
> + =A0 =A0 =A0lambda { @object.send(:rsqrt, -1) }.should raise_error(Math:=
:DomainError)
> + =A0 =A0 =A0lambda { @object.send(:rsqrt, -4.0) }.should raise_error(Mat=
h::DomainError)
> + =A0 =A0 =A0lambda { @object.send(:rsqrt, -16/64) }.should raise_error(M=
ath::DomainError)
> + =A0 =A0end
> =A0 end
> =A0end
> diff --git a/shared/math/atanh.rb b/shared/math/atanh.rb
> index b520171..69341af 100644
> --- a/shared/math/atanh.rb
> +++ b/shared/math/atanh.rb
> @@ -120,11 +120,23 @@ describe :math_atanh_private, :shared =3D> true do
> =A0end
>
> =A0describe :math_atanh_no_complex, :shared =3D> true do
> - =A0it "raises an Errno::EDOM for arguments greater than 1.0" do
> - =A0 =A0lambda { @object.send(@method, 1.0 + Float::EPSILON) =A0}.should=
 raise_error(Errno::EDOM)
> + =A0ruby_version_is ""..."1.9" do
> + =A0 =A0it "raises an Errno::EDOM for arguments greater than 1.0" do
> + =A0 =A0 =A0lambda { @object.send(@method, 1.0 + Float::EPSILON) =A0}.sh=
ould raise_error(Errno::EDOM)
> + =A0 =A0end
> +
> + =A0 =A0it "raises an Errno::EDOM for arguments less than -1.0" do
> + =A0 =A0 =A0lambda { @object.send(@method, -1.0 - Float::EPSILON) }.shou=
ld raise_error(Errno::EDOM)
> + =A0 =A0end
> =A0 end
>
> - =A0it "raises an Errno::EDOM for arguments less than -1.0" do
> - =A0 =A0lambda { @object.send(@method, -1.0 - Float::EPSILON) }.should r=
aise_error(Errno::EDOM)
> + =A0ruby_version_is "1.9" do
> + =A0 =A0it "raises an Math::DomainError for arguments greater than 1.0" =
do
> + =A0 =A0 =A0lambda { @object.send(@method, 1.0 + Float::EPSILON) =A0}.sh=
ould raise_error(Math::DomainError)
> + =A0 =A0end
> +
> + =A0 =A0it "raises an Math::DomainError for arguments less than -1.0" do
> + =A0 =A0 =A0lambda { @object.send(@method, -1.0 - Float::EPSILON) }.shou=
ld raise_error(Math::DomainError)
> + =A0 =A0end
> =A0 end
> =A0end
>
>
> --
> Kenta Murata
> OpenPGP FP =3D FA26 35D7 4F98 3498 0810 E0D5 F213 966F E9EB 0BCC
>
> E-mail: mrkn / mrkn.jp
> twitter: http://twitter.com/mrkn/
> blog: http://d.hatena.ne.jp/mrkn/
>
>
>