From 71fb635db429ef00785c2e03139262e9be5c545d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 11:07:07 +0000 Subject: [PATCH 1/2] Complex in C#: a RubyComplex type, its arithmetic and Kernel#Complex; Math.hypot rounds as libm (#89) A Complex was a plain Numeric with @real and @image, and every operation went through complex.rb's helpers: a * b cost ~3.7 us, Complex(1, 2) ~1.7 (CRuby ~140 and ~70 ns). - RubyComplex (Src/Libraries/Builtins/RubyComplex.cs) is the Complex class's CLR type: two fields, frozen from birth, #instance_variables empty. #real, #imaginary/#imag, __raw__ and the __ir_set_parts__ #marshal_load uses are C#. - +, -, *, /, quo, ==, #hash and #abs/#magnitude are C# for Integer and Float parts and operands, following complex.c's f_add/f_sub/f_mul, safe_mul, comp_mul and f_divide (an exact 0 or 1 operand skipped, a Float times a zero reduced to its sign first). Rational parts, Bignums, integer division (which makes Rationals) and coercion go to complex.rb's __ir_*_slow__ methods, which are the old Ruby methods renamed. Comparable's #== is no longer undefined in Complex: Complex's own is already in the table. - Kernel#Complex is C# for Integers and Floats; the rest is complex.rb's __ir_Complex_slow__. Integer#to_c and Float#to_c are C#. - Math.hypot rounds as libm does: the squares and their sum to about twice double precision, one Newton step on the root, powers-of-two scaling for squares that would overflow or underflow. Of 20,000 random cases 9 differ from CRuby in the last place, against ~900 with the scaled formula, which made Complex(3.5, -1).abs one unit off. Warmed-up, Release, net8.0 (CRuby 4.0): Complex(1, 2) 1739 -> 51 ns (71) a + b 1080 -> 76 ns (63) a * b 3664 -> 121 ns (141) af / bf 4039 -> 68 ns (172) a == b 301 -> 19 ns (13) af.abs 387 -> 49 ns (17) a.hash 234 -> 34 ns (21) 5.to_c 280 -> 64 ns (35) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPwZf7MsYwQdhLBCZCkH2C --- Src/IronRuby.Tests/RubyTests.cs | 1 + Src/IronRuby.Tests/Runtime/MiscTests.cs | 55 ++++ Src/Libraries/Builtins/FloatOps.cs | 6 + Src/Libraries/Builtins/Integer.cs | 6 + Src/Libraries/Builtins/KernelOps.cs | 50 ++++ Src/Libraries/Builtins/RubyComplex.cs | 336 ++++++++++++++++++++++++ Src/Libraries/Builtins/RubyMath.cs | 37 ++- Src/Libraries/Initializers.Generated.cs | 105 ++++++++ Src/StdLib/ironruby/ironruby/complex.rb | 178 ++++++------- 9 files changed, 677 insertions(+), 97 deletions(-) create mode 100644 Src/Libraries/Builtins/RubyComplex.cs diff --git a/Src/IronRuby.Tests/RubyTests.cs b/Src/IronRuby.Tests/RubyTests.cs index 9bbd69bb6..28b825ee7 100644 --- a/Src/IronRuby.Tests/RubyTests.cs +++ b/Src/IronRuby.Tests/RubyTests.cs @@ -876,6 +876,7 @@ public Tests(Driver/*!*/ driver) { Range_MinMaxInclude1, Integer_Power1, Rational_CSharp1, + Complex_CSharp1, Method_LightFrame1, Method_LightFrame2, Hash_SelfKey1, diff --git a/Src/IronRuby.Tests/Runtime/MiscTests.cs b/Src/IronRuby.Tests/Runtime/MiscTests.cs index 4b6c911b9..cac3cf152 100644 --- a/Src/IronRuby.Tests/Runtime/MiscTests.cs +++ b/Src/IronRuby.Tests/Runtime/MiscTests.cs @@ -3171,6 +3171,61 @@ p r "); } + /// + /// Complex as a C# type (RubyComplex) with +, -, *, /, ==, #hash and #abs in C# for Integer and Float + /// parts, Kernel#Complex and #to_c; Rational parts, coercion and the rest left to complex.rb; Math.hypot + /// rounded as libm's is. The expected output is CRuby's. + /// + public void Complex_CSharp1() { + TestOutput(@" +require 'ruby4' +def t(l); r = begin; yield; rescue => e; [e.class, e.message]; end; puts ""#{l}: #{r.inspect}""; end +a = Complex(1, 2); b = Complex(3.5, -1); r = Complex(Rational(1, 2), 3); z = Complex(0, 0) +t('basic') { [a, a.real, a.imaginary, a.imag, a.frozen?, a.instance_variables, a.class.superclass, Complex(1), Complex(1.5), Complex(""1+2i"")] } +t('add') { [a + b, a + 1, a + 2.5, a + r, a - b, a - 1, a + Rational(1, 3), -a, a + Complex(0, 0)] } +t('mul') { [a * b, a * 2, a * 0.5, a * r, a * Complex(0, 1), Complex(Float::INFINITY, 0) * 0, a * z] } +t('div') { [a / b, a / 2, a / 0.5, a.quo(a), a / r, a.fdiv(2), Complex(1, 1) / Complex(1, -1)] } +t('div0') { a / 0 } +t('div0f') { a / 0.0 } +t('pow') { [a ** 2, a ** -1, a ** 0.5, a ** a, a ** Rational(1, 2), Complex(0, 1) ** 2] } +t('eq') { [a == Complex(1, 2), a == b, Complex(3) == 3, Complex(3, 0.0) == 3, a == ""x"", a.eql?(Complex(1, 2)), a.eql?(Complex(1.0, 2)), Complex(1, 0) <=> 2, a <=> 1] } +t('hash') { [a.hash == Complex(1, 2).hash, {a => 1}[Complex(1, 2)], a.hash == Complex(1.0, 2).hash] } +t('abs') { [a.abs, a.abs2, a.arg, a.polar, a.rectangular, a.conjugate, b.abs, Complex(3, 4).abs, Complex(-0.0, 0).arg] } +t('fmt') { [a.to_s, a.inspect, b.to_s, r.inspect, Complex(1, Float::NAN).to_s, Complex(1, -0.0).inspect] } +t('marshal') { [Marshal.load(Marshal.dump(a)), Marshal.load(Marshal.dump(r))] } +o = Object.new; def o.coerce(x) = [x, Complex(1, 1)] +t('coerce') { [a + o, a * o] } +t('bad') { a + ""x"" } +t('int ops') { [1 + a, 2 * a, 3 - a, 4 / a, 1.5 * a, Rational(1, 2) + a, 2 ** a] } +t('misc') { [a.real?, 1.real?, a.to_c, 5.to_c, 2.5.to_c, Rational(1, 2).to_c, a.finite?, Complex(Float::INFINITY, 0).infinite?, Complex.polar(1, Math::PI / 2).real.round(10), Complex.rectangular(1, 2)] } +t('Complex()') { [Complex(1, 2), Complex(""3""), Complex(nil, exception: false), Complex(""x"", exception: false), Complex(1, 2.0)] } +t('cerr') { Complex(nil) } +t('hypot') { [Math.hypot(3.5, 1), Math.hypot(3, 4), Math.hypot(1e300, 1e300), Math.hypot(1e-300, 1e-300), Math.hypot(-0.0, 0), Math.hypot(Float::INFINITY, Float::NAN), Math.hypot(5e-324, 5e-324)] } +t('undef') { [Complex.instance_method(:==).owner, (Complex(1) < 2 rescue $!.class)] } +", @" +basic: [(1+2i), 1, 2, 2, true, [], Numeric, (1+0i), (1.5+0i), (1+2i)] +add: [(4.5+1i), (2+2i), (3.5+2i), ((3/2)+5i), (-2.5+3i), (0+2i), ((4/3)+2i), (-1-2i), (1+2i)] +mul: [(5.5+6.0i), (2+4i), (0.5+1.0i), ((-11/2)+(4/1)*i), (-2+1i), (NaN+0i), (0+0i)] +div: [(0.11320754716981132+0.6037735849056602i), ((1/2)+1i), (2.0+4.0i), (1+0i), ((26/37)-(8/37)*i), (0.5+1.0i), (0+1i)] +div0: [ZeroDivisionError, ""divided by 0""] +div0f: (Infinity+Infinity*i) +pow: [(-3+4i), ((1/5)-(2/5)*i), (1.272019649514069+0.7861513777574233i), (-0.22251715680177267+0.10070913113607541i), (1.272019649514069+0.7861513777574233i), (-1+0i)] +eq: [true, false, true, true, false, true, false, -1, nil] +hash: [true, 1, false] +abs: [2.23606797749979, 5, 1.1071487177940904, [2.23606797749979, 1.1071487177940904], [1, 2], (1-2i), 3.640054944640259, 5.0, 3.141592653589793] +fmt: [""1+2i"", ""(1+2i)"", ""3.5-1i"", ""((1/2)+3i)"", ""1+NaN*i"", ""(1-0.0i)""] +marshal: [(1+2i), ((1/2)+3i)] +coerce: [(2+3i), (-1+3i)] +bad: [TypeError, ""String can't be coerced into Complex""] +int ops: [(2+2i), (2+4i), (2-2i), ((4/5)-(8/5)*i), (1.5+3.0i), ((3/2)+2i), (0.36691394948660344+1.9660554808224875i)] +misc: [false, true, (1+2i), (5+0i), (2.5+0i), ((1/2)+0i), true, 1, 0.0, (1+2i)] +Complex(): [(1+2i), (3+0i), nil, nil, (1+2.0i)] +cerr: [TypeError, ""can't convert nil into Complex""] +hypot: [3.640054944640259, 5.0, 1.4142135623730952e+300, 1.414213562373095e-300, 0.0, Infinity, 5.0e-324] +undef: [Complex, NoMethodError] +"); + } + /// /// Rational as a C# type (RubyRational) with its arithmetic, comparison, #to_f and #hash in C#, and /// Kernel#Rational: Integer, Float, Rational and big operands, coercion, division by zero, NaN, diff --git a/Src/Libraries/Builtins/FloatOps.cs b/Src/Libraries/Builtins/FloatOps.cs index 1dceec6e4..1acdd4884 100644 --- a/Src/Libraries/Builtins/FloatOps.cs +++ b/Src/Libraries/Builtins/FloatOps.cs @@ -23,6 +23,12 @@ namespace IronRuby.Builtins { [RubyClass("Float", Extends = typeof(double), Inherits = typeof(Numeric)), Includes(typeof(ClrFloat), Copy = true)] [UndefineMethod("new", IsStatic = true)] public static class FloatOps { + /// numeric_to_c: self + 0i (complex.rb's Numeric#to_c, for a Float). + [RubyMethod("to_c")] + public static RubyComplex/*!*/ ToComplex(RubyContext/*!*/ context, double self) { + return new RubyComplex(context.GetClass(typeof(RubyComplex)), self, ScriptingRuntimeHelpers.Int32ToObject(0)); + } + #region Constants /// diff --git a/Src/Libraries/Builtins/Integer.cs b/Src/Libraries/Builtins/Integer.cs index d7bba8ec1..df947fc35 100644 --- a/Src/Libraries/Builtins/Integer.cs +++ b/Src/Libraries/Builtins/Integer.cs @@ -216,6 +216,12 @@ private static BigInteger ToBigInteger(object/*!*/ self) { #endregion + /// numeric_to_c: self + 0i (complex.rb's Numeric#to_c, for the commonest receivers). + [RubyMethod("to_c")] + public static RubyComplex/*!*/ ToComplex(RubyContext/*!*/ context, object/*!*/ self) { + return new RubyComplex(context.GetClass(typeof(RubyComplex)), self, ScriptingRuntimeHelpers.Int32ToObject(0)); + } + #region numerator/ord, denominator [RubyMethod("numerator")] diff --git a/Src/Libraries/Builtins/KernelOps.cs b/Src/Libraries/Builtins/KernelOps.cs index 98eccf4bf..9aacebdc2 100644 --- a/Src/Libraries/Builtins/KernelOps.cs +++ b/Src/Libraries/Builtins/KernelOps.cs @@ -679,6 +679,56 @@ public static object ToRational(object self) { throw ConversionArity(0, "1..2"); } + // Kernel#Complex(x[, y], exception: true) (MRI's nucomp_f_complex): Integers and Floats make + // their Complex here; anything else - Strings, Rationals, a Complex, objects, exception: - is + // complex.rb's __ir_Complex_slow__, whose `*args, exception:' cost ~1.7 us a call. + + private static bool IsIntegerOrFloat(object value) { + return value is int || value is long || value is double; + } + + [RubyMethod("Complex", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Complex", RubyMethodAttributes.PublicSingleton)] + public static object ToComplex(CallSiteStorage>/*!*/ slow, RubyContext/*!*/ context, + object self, object value) { + if (IsIntegerOrFloat(value)) { + return new RubyComplex(context.GetClass(typeof(RubyComplex)), value, ScriptingRuntimeHelpers.Int32ToObject(0)); + } + var site = slow.GetCallSite("__ir_Complex_slow__", 1); + return site.Target(site, self, value); + } + + [RubyMethod("Complex", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Complex", RubyMethodAttributes.PublicSingleton)] + public static object ToComplex(CallSiteStorage>/*!*/ slow, RubyContext/*!*/ context, + object self, object real, object imaginary) { + if (IsIntegerOrFloat(real) && IsIntegerOrFloat(imaginary)) { + return new RubyComplex(context.GetClass(typeof(RubyComplex)), real, imaginary); + } + var site = slow.GetCallSite("__ir_Complex_slow__", 2); + return site.Target(site, self, real, imaginary); + } + + [RubyMethod("Complex", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Complex", RubyMethodAttributes.PublicSingleton)] + public static object ToComplex(CallSiteStorage>/*!*/ slow, + object self, object real, object imaginary, object options) { + var site = slow.GetCallSite("__ir_Complex_slow__", 3); + return site.Target(site, self, real, imaginary, options); + } + + [RubyMethod("Complex", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Complex", RubyMethodAttributes.PublicSingleton)] + public static object ToComplex(object self, object a, object b, object c, object d, params object[]/*!*/ more) { + throw ConversionArity(4 + more.Length, "1..2"); + } + + [RubyMethod("Complex", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Complex", RubyMethodAttributes.PublicSingleton)] + public static object ToComplex(object self) { + throw ConversionArity(0, "1..2"); + } + // The Rational class of the context that last asked (one per process in practice). private static RubyClass _rationalClass; diff --git a/Src/Libraries/Builtins/RubyComplex.cs b/Src/Libraries/Builtins/RubyComplex.cs new file mode 100644 index 000000000..dd6d4b7b9 --- /dev/null +++ b/Src/Libraries/Builtins/RubyComplex.cs @@ -0,0 +1,336 @@ +/* **************************************************************************** + * + * Complex: its representation. The methods are complex.rb's (Src/StdLib/ironruby/ + * ironruby/complex.rb, a port of MRI 4.0's complex.c), apart from the ones moved here; + * what is here is what that file builds on - the real and imaginary parts, read and + * made without a Ruby method call or an instance variable lookup. + * + * ***************************************************************************/ + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using IronRuby.Runtime; +using IronRuby.Runtime.Calls; +using Microsoft.Scripting.Runtime; + +namespace IronRuby.Builtins { + + /// + /// A Complex: a real and an imaginary part, each a real Numeric, frozen from birth (MRI's struct + /// RComplex). They were @real and @image of a plain Numeric. + /// + [RubyClass("Complex", Inherits = typeof(Numeric))] + public class RubyComplex : Numeric { + // null until the parts are set: an instance Marshal allocates before #marshal_load runs + private object _real; + private object _imaginary; + + public RubyComplex(RubyClass/*!*/ cls) + : base(cls) { + } + + public RubyComplex(RubyClass/*!*/ cls, object real, object imaginary) + : base(cls) { + _real = real; + _imaginary = imaginary; + Freeze(); + } + + public object Real { get { return _real; } } + public object Imaginary { get { return _imaginary; } } + + [RubyMethod("real")] + public static object GetReal(RubyComplex/*!*/ self) { + return self._real; + } + + [RubyMethod("imaginary"), RubyMethod("imag")] + public static object GetImaginary(RubyComplex/*!*/ self) { + return self._imaginary; + } + + /// MRI's nucomp_s_new_internal: no canonicalisation, no checks; frozen. + [RubyMethod("__raw__", RubyMethodAttributes.PublicSingleton)] + public static RubyComplex/*!*/ Raw(RubyClass/*!*/ self, object real, object imaginary) { + return new RubyComplex(self, real, imaginary); + } + + #region arithmetic on Integer and Float parts (complex.c's f_add, f_sub, f_mul, safe_mul, comp_mul) + + // The fast paths take parts and operands that are an Int32-sized Integer or a Float; those + // are combined in a long (an Integer stays one) or a double. Anything else - a Rational part, + // a Bignum, another kind of operand - is complex.rb's (the __ir_*_slow__ methods), which + // these follow step for step: complex.c's helpers skip an exact 0 or 1 operand, and that is + // what keeps an exact zero from turning 0.0 into 0, which ruby/spec observes. + + private static bool IsSimple(object x) { + if (x is double) { return true; } + long v; + return TryLong(x, out v) && v >= Int32.MinValue && v <= Int32.MaxValue; + } + + private static bool TryLong(object x, out long v) { + if (x is int) { v = (int)x; return true; } + if (x is long) { v = (long)x; return true; } + v = 0; + return false; + } + + // an exact Integer 0 (FIXNUM_ZERO_P) + private static bool FixZero(object x) { + long v; + return TryLong(x, out v) && v == 0; + } + + private static bool FixOne(object x) { + long v; + return TryLong(x, out v) && v == 1; + } + + // f_zero_p: 0.0 counts + private static bool Zero(object x) { + if (x is double) { return (double)x == 0.0; } + return FixZero(x); + } + + private static double D(object x) { + if (x is double) { return (double)x; } + long v; + TryLong(x, out v); + return v; + } + + private static object Plus(object x, object y) { + long a, b; + if (TryLong(x, out a) && TryLong(y, out b)) { return Protocols.Normalize(a + b); } + return D(x) + D(y); + } + + private static object Minus(object x, object y) { + long a, b; + if (TryLong(x, out a) && TryLong(y, out b)) { return Protocols.Normalize(a - b); } + return D(x) - D(y); + } + + private static object Times(object x, object y) { + long a, b; + if (TryLong(x, out a) && TryLong(y, out b)) { return Protocols.Normalize(a * b); } + return D(x) * D(y); + } + + private static object FAdd(object x, object y) { + if (!(x is double)) { + if (FixZero(x)) { return y; } + if (FixZero(y)) { return x; } + } else if (FixZero(y)) { + return x; + } + return Plus(x, y); + } + + private static object FSub(object x, object y) { + return FixZero(y) ? x : Minus(x, y); + } + + private static object FMul(object x, object y) { + if (!(x is double)) { + if (FixZero(y)) { return ScriptingRuntimeHelpers.Int32ToObject(0); } + if (FixZero(x) && !(y is double)) { return ScriptingRuntimeHelpers.Int32ToObject(0); } + if (FixOne(x)) { return y; } + if (FixOne(y)) { return x; } + } else if (FixOne(y)) { + return x; + } + return Times(x, y); + } + + private static bool SignBit(double x) { + return !Double.IsNaN(x) && (x < 0.0 || (x == 0.0 && Double.IsNegative(x))); + } + + // safe_mul: a Float multiplied by a zero is reduced to its sign first + private static object SafeMul(object a, object b, bool az, bool bz) { + if (!az && bz && a is double && !Double.IsNaN((double)a)) { + a = SignBit((double)a) ? -1.0 : 1.0; + } + if (!bz && az && b is double && !Double.IsNaN((double)b)) { + b = SignBit((double)b) ? -1.0 : 1.0; + } + return FMul(a, b); + } + + private RubyComplex/*!*/ Make(object real, object imaginary) { + return new RubyComplex(ImmediateClass.GetNonSingletonClass(), real, imaginary); + } + + private bool HasSimpleParts { + get { return IsSimple(_real) && IsSimple(_imaginary); } + } + + private static object Slow(CallSiteStorage>/*!*/ slow, string/*!*/ name, RubyComplex/*!*/ self, object other) { + var site = slow.GetCallSite(name, 1); + return site.Target(site, self, other); + } + + [RubyMethod("+")] + public static object Add(CallSiteStorage>/*!*/ slow, RubyComplex/*!*/ self, object other) { + if (self.HasSimpleParts) { + var c = other as RubyComplex; + if (c != null) { + if (c.HasSimpleParts) { + return self.Make(FAdd(self._real, c._real), FAdd(self._imaginary, c._imaginary)); + } + } else if (IsSimple(other)) { + return self.Make(FAdd(self._real, other), self._imaginary); + } + } + return Slow(slow, "__ir_add_slow__", self, other); + } + + [RubyMethod("-")] + public static object Subtract(CallSiteStorage>/*!*/ slow, RubyComplex/*!*/ self, object other) { + if (self.HasSimpleParts) { + var c = other as RubyComplex; + if (c != null) { + if (c.HasSimpleParts) { + return self.Make(FSub(self._real, c._real), FSub(self._imaginary, c._imaginary)); + } + } else if (IsSimple(other)) { + return self.Make(FSub(self._real, other), self._imaginary); + } + } + return Slow(slow, "__ir_sub_slow__", self, other); + } + + [RubyMethod("*")] + public static object Multiply(CallSiteStorage>/*!*/ slow, RubyComplex/*!*/ self, object other) { + if (self.HasSimpleParts) { + var c = other as RubyComplex; + if (c != null) { + if (c.HasSimpleParts) { + // comp_mul + object ar = self._real, ai = self._imaginary, br = c._real, bi = c._imaginary; + bool arz = Zero(ar), aiz = Zero(ai), brz = Zero(br), biz = Zero(bi); + object real = FSub(SafeMul(ar, br, arz, brz), SafeMul(ai, bi, aiz, biz)); + object imag = FAdd(SafeMul(ar, bi, arz, biz), SafeMul(ai, br, aiz, brz)); + return self.Make(real, imag); + } + } else if (IsSimple(other)) { + return self.Make(FMul(self._real, other), FMul(self._imaginary, other)); + } + } + return Slow(slow, "__ir_mul_slow__", self, other); + } + + // f_divide with f_quo, for Float parts throughout: the quotients are Floats, as they are + // there (Integer parts make Rationals, which complex.rb makes). + [RubyMethod("/"), RubyMethod("quo")] + public static object Divide(CallSiteStorage>/*!*/ slow, RubyComplex/*!*/ self, object other) { + if (self._real is double && self._imaginary is double) { + double ar = (double)self._real, ai = (double)self._imaginary; + var c = other as RubyComplex; + if (c != null) { + if (c._real is double && c._imaginary is double) { + double br = (double)c._real, bi = (double)c._imaginary; + double x, y; + if (Math.Abs(br) > Math.Abs(bi)) { + double r = bi / br; + double n = br * (1 + r * r); + x = (ar + ai * r) / n; + y = (ai - ar * r) / n; + } else { + double r = br / bi; + double n = bi * (1 + r * r); + x = (ar * r + ai) / n; + y = (ai * r - ar) / n; + } + return self.Make(x, y); + } + } else if (other is double) { + return self.Make(ar / (double)other, ai / (double)other); + } + } + return Slow(slow, "__ir_div_slow__", self, other); + } + + // nucomp_eqeq_p, with f_eqeq_p: a Float on either side compares as Floats + private static bool PartEqual(object x, object y) { + long a, b; + if (TryLong(x, out a) && TryLong(y, out b)) { return a == b; } + return D(x) == D(y); + } + + [RubyMethod("==")] + public static object Equal(CallSiteStorage>/*!*/ slow, RubyComplex/*!*/ self, object other) { + if (self.HasSimpleParts) { + var c = other as RubyComplex; + if (c != null) { + if (c.HasSimpleParts) { + return ScriptingRuntimeHelpers.BooleanToObject(PartEqual(self._real, c._real) && PartEqual(self._imaginary, c._imaginary)); + } + } else if (IsSimple(other)) { + return ScriptingRuntimeHelpers.BooleanToObject(PartEqual(self._real, other) && Zero(self._imaginary)); + } + } + return Slow(slow, "__ir_eqeq_slow__", self, other); + } + + // nucomp_hash: a function of the parts and their classes (#eql? tells 1 from 1.0) + [RubyMethod("hash")] + public static object Hash(CallSiteStorage>/*!*/ slow, RubyComplex/*!*/ self) { + if (self.HasSimpleParts) { + return ScriptingRuntimeHelpers.Int32ToObject(unchecked((PartHash(self._real) * 31 + PartHash(self._imaginary)) * 31 + 0x436f6d70)); + } + var site = slow.GetCallSite("__ir_hash_slow__", 0); + return site.Target(site, self); + } + + private static int PartHash(object x) { + if (x is double) { return ((double)x).GetHashCode() ^ 0x5a5a5a5a; } + long v; + TryLong(x, out v); + return v.GetHashCode(); + } + + // rb_complex_abs + [RubyMethod("abs"), RubyMethod("magnitude")] + public static object Abs(CallSiteStorage>/*!*/ slow, RubyComplex/*!*/ self) { + if (self.HasSimpleParts) { + object re = self._real, im = self._imaginary; + if (Zero(re)) { + object a = AbsPart(im); + return (re is double && !(im is double)) ? (object)D(a) : a; + } + if (Zero(im)) { + object a = AbsPart(re); + return (!(re is double) && im is double) ? (object)D(a) : a; + } + return RubyMath.HypotDouble(D(re), D(im)); + } + var site = slow.GetCallSite("__ir_abs_slow__", 0); + return site.Target(site, self); + } + + private static object AbsPart(object x) { + if (x is double) { return Math.Abs((double)x); } + long v; + TryLong(x, out v); + return Protocols.Normalize(Math.Abs(v)); + } + + #endregion + + /// The parts of the instance Marshal allocates for #marshal_load, which is then frozen. + [RubyMethod("__ir_set_parts__", RubyMethodAttributes.PrivateInstance)] + public static RubyComplex/*!*/ SetParts(RubyContext/*!*/ context, RubyComplex/*!*/ self, object real, object imaginary) { + if (self.IsFrozen) { + throw RubyExceptions.CreateObjectFrozenError(context, self); + } + self._real = real; + self._imaginary = imaginary; + self.Freeze(); + return self; + } + } +} diff --git a/Src/Libraries/Builtins/RubyMath.cs b/Src/Libraries/Builtins/RubyMath.cs index 2577ee9fc..4fa31b466 100644 --- a/Src/Libraries/Builtins/RubyMath.cs +++ b/Src/Libraries/Builtins/RubyMath.cs @@ -277,6 +277,15 @@ public static object Hypot(CallSiteStorage + /// C's hypot(): x*x + y*y with one rounding (a fused multiply-add) while neither square can + /// overflow or underflow - the scaled form below rounds twice more, and came out a unit in + /// the last place off libm's for (3.5).hypot(1) - and scaled by the larger one otherwise. + /// + internal static double HypotDouble(double dx, double dy) { dx = SM.Abs(dx); dy = SM.Abs(dy); // an infinity wins over a NaN, which is what C's hypot() promises @@ -284,9 +293,31 @@ public static object Hypot(CallSiteStorage 1e150) { + scale = 4.149515568880993e+180; // 2**600 + dx *= 2.409919865102884e-181; // 2**-600 + dy *= 2.409919865102884e-181; + } else if (dy < 1e-150) { + scale = 2.409919865102884e-181; + dx *= 4.149515568880993e+180; + dy *= 4.149515568880993e+180; + } + // x*x + y*y to about twice double precision: each square exactly as a sum of two + // doubles (the low half by a fused multiply-add), the four added without losing the + // small ones; then one Newton step on the square root, whose own square is again + // taken exactly. That is within a hair of the correctly rounded result libm gives. + double sx = dx * dx, ex = SM.FusedMultiplyAdd(dx, dx, -sx); + double sy = dy * dy, ey = SM.FusedMultiplyAdd(dy, dy, -sy); + double sum = sx + sy; + double err = ((sx - sum) + sy) + (ex + ey); + double h = SM.Sqrt(sum + err); + double hh = h * h, he = SM.FusedMultiplyAdd(h, h, -hh); + return (h + (((sum - hh) - he) + err) / (2.0 * h)) * scale; } [RubyMethodAttribute("frexp", RubyMethodAttributes.PrivateInstance)] diff --git a/Src/Libraries/Initializers.Generated.cs b/Src/Libraries/Initializers.Generated.cs index 87702146d..9b7fba10f 100644 --- a/Src/Libraries/Initializers.Generated.cs +++ b/Src/Libraries/Initializers.Generated.cs @@ -223,6 +223,7 @@ protected override void LoadModules() { Context.TrueClass = DefineGlobalClass("TrueClass", typeof(IronRuby.Builtins.TrueClass), 0x0000000F, Context.ObjectClass, LoadTrueClass_Instance, null, null, IronRuby.Builtins.RubyModule.EmptyArray); DefineGlobalClass("UnboundMethod", typeof(IronRuby.Builtins.UnboundMethod), 0x0000000F, Context.ObjectClass, LoadUnboundMethod_Instance, null, null, IronRuby.Builtins.RubyModule.EmptyArray); // Skipped primitive: Class + DefineGlobalClass("Complex", typeof(IronRuby.Builtins.RubyComplex), 0x0000000F, def70, LoadComplex_Instance, LoadComplex_Class, null, IronRuby.Builtins.RubyModule.EmptyArray); IronRuby.Builtins.RubyClass def42 = DefineGlobalClass("File", typeof(IronRuby.Builtins.RubyFile), 0x00000007, def45, LoadFile_Instance, LoadFile_Class, LoadFile_Constants, IronRuby.Builtins.RubyModule.EmptyArray, new Func>, IronRuby.Runtime.ConversionStorage>, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.ConversionStorage, IronRuby.Builtins.RubyClass, System.Object, System.Object, System.Object, System.Collections.Generic.IDictionary, IronRuby.Builtins.RubyFile>(IronRuby.Builtins.RubyFileOps.CreateFile) ); @@ -822,6 +823,82 @@ private static void LoadComparable_Instance(IronRuby.Builtins.RubyModule/*!*/ mo } + private static void LoadComplex_Instance(IronRuby.Builtins.RubyModule/*!*/ module) { + DefineLibraryMethod(module, "__ir_set_parts__", 0x52, + 0x00000000U, + new Func(IronRuby.Builtins.RubyComplex.SetParts) + ); + + DefineLibraryMethod(module, "-", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object, System.Object>(IronRuby.Builtins.RubyComplex.Subtract) + ); + + DefineLibraryMethod(module, "*", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object, System.Object>(IronRuby.Builtins.RubyComplex.Multiply) + ); + + DefineLibraryMethod(module, "/", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object, System.Object>(IronRuby.Builtins.RubyComplex.Divide) + ); + + DefineLibraryMethod(module, "+", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object, System.Object>(IronRuby.Builtins.RubyComplex.Add) + ); + + DefineLibraryMethod(module, "==", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object, System.Object>(IronRuby.Builtins.RubyComplex.Equal) + ); + + DefineLibraryMethod(module, "abs", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object>(IronRuby.Builtins.RubyComplex.Abs) + ); + + DefineLibraryMethod(module, "hash", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object>(IronRuby.Builtins.RubyComplex.Hash) + ); + + DefineLibraryMethod(module, "imag", 0x51, + 0x00000000U, + new Func(IronRuby.Builtins.RubyComplex.GetImaginary) + ); + + DefineLibraryMethod(module, "imaginary", 0x51, + 0x00000000U, + new Func(IronRuby.Builtins.RubyComplex.GetImaginary) + ); + + DefineLibraryMethod(module, "magnitude", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object>(IronRuby.Builtins.RubyComplex.Abs) + ); + + DefineLibraryMethod(module, "quo", 0x51, + 0x00000000U, + new Func>, IronRuby.Builtins.RubyComplex, System.Object, System.Object>(IronRuby.Builtins.RubyComplex.Divide) + ); + + DefineLibraryMethod(module, "real", 0x51, + 0x00000000U, + new Func(IronRuby.Builtins.RubyComplex.GetReal) + ); + + } + + private static void LoadComplex_Class(IronRuby.Builtins.RubyModule/*!*/ module) { + DefineLibraryMethod(module, "__raw__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.RubyComplex.Raw) + ); + + } + private static void LoadDir_Instance(IronRuby.Builtins.RubyModule/*!*/ module) { DefineLibraryMethod(module, "close", 0x51, 0x00000000U, @@ -2934,6 +3011,11 @@ private static void LoadFloat_Instance(IronRuby.Builtins.RubyModule/*!*/ module) new Func(IronRuby.Builtins.FloatOps.Eql) ); + DefineLibraryMethod(module, "to_c", 0x51, + 0x00000000U, + new Func(IronRuby.Builtins.FloatOps.ToComplex) + ); + } private static void LoadFloat_Class(IronRuby.Builtins.RubyModule/*!*/ module) { @@ -3256,6 +3338,11 @@ private static void LoadInteger_Instance(IronRuby.Builtins.RubyModule/*!*/ modul new Func(IronRuby.Builtins.Integer.Times) ); + DefineLibraryMethod(module, "to_c", 0x51, + 0x00000000U, + new Func(IronRuby.Builtins.Integer.ToComplex) + ); + DefineLibraryMethod(module, "to_i", 0x51, 0x00000000U, new Func(IronRuby.Builtins.Integer.ToInteger) @@ -4887,6 +4974,15 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module new Func(IronRuby.Builtins.KernelOps.GetClrMember) ); + DefineLibraryMethod(module, "Complex", 0x52, + new[] { 0x00000000U, 0x00000000U, 0x00000000U, 0x80000000U, 0x00000000U}, + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToComplex), + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToComplex), + new Func>, System.Object, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToComplex), + new Func(IronRuby.Builtins.KernelOps.ToComplex), + new Func(IronRuby.Builtins.KernelOps.ToComplex) + ); + DefineLibraryMethod(module, "define_singleton_method", 0x51, new[] { 0x0002000cU, 0x0000000cU, 0x0002000cU, 0x0000000cU, 0x0004000aU, 0x0000000aU, 0x0002000cU, 0x0000000cU}, new Func(IronRuby.Builtins.KernelOps.DefineSingletonMethod), @@ -5443,6 +5539,15 @@ private static void LoadKernel_Class(IronRuby.Builtins.RubyModule/*!*/ module) { new Func(IronRuby.Builtins.KernelOps.Catch) ); + DefineLibraryMethod(module, "Complex", 0x61, + new[] { 0x00000000U, 0x00000000U, 0x00000000U, 0x80000000U, 0x00000000U}, + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToComplex), + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToComplex), + new Func>, System.Object, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToComplex), + new Func(IronRuby.Builtins.KernelOps.ToComplex), + new Func(IronRuby.Builtins.KernelOps.ToComplex) + ); + DefineLibraryMethod(module, "eval", 0x61, 0x00020014U, 0x0002001cU, new Func(IronRuby.Builtins.KernelOps.Evaluate), diff --git a/Src/StdLib/ironruby/ironruby/complex.rb b/Src/StdLib/ironruby/ironruby/complex.rb index d60549ae9..f80c42901 100644 --- a/Src/StdLib/ironruby/ironruby/complex.rb +++ b/Src/StdLib/ironruby/ironruby/complex.rb @@ -2,7 +2,7 @@ # on Numeric, Float and String, and Kernel#Complex. Loaded by ruby4.rb as part of the # core prelude, after rational.rb. # -# A Complex is a real part in @real and an imaginary part in @image (MRI's names, which +# A Complex is a real part in self.real and an imaginary part in self.imaginary (MRI's names, which # its marshal format carries), frozen from birth. complex.c undefines Complex.new and # Complex.allocate; instances are made by __raw__ (nucomp_s_new_internal). # @@ -13,23 +13,13 @@ class Complex < Numeric class << self - alias_method :__ir_allocate__, :allocate - private :__ir_allocate__ undef_method :allocate undef_method :new end - # nucomp_s_new_internal: no canonicalisation, no checks; OBJ_FREEZE. - def self.__raw__(real, imag) - __ir_allocate__.__send__(:__ir_set_parts__, real, imag) - end - - def __ir_set_parts__(real, imag) - @real = real - @image = imag - freeze - end - private :__ir_set_parts__ + # A Complex's parts are fields of the C# RubyComplex (Src/Libraries/Builtins/RubyComplex.cs), + # read by #real and #imaginary; __raw__ (nucomp_s_new_internal: no canonicalisation, no + # checks, frozen) and the __ir_set_parts__ #marshal_load uses are C# too. # --- complex.c's arithmetic helpers ---------------------------------------- @@ -315,44 +305,43 @@ def self.__sinpi__(w) # `Complex(1) < 2` is a NoMethodError, not a comparison of magnitudes. The undefinition # is an entry in Complex's own table, so it also hides what ruby4.rb adds to Numeric # later; a name nothing defines yet is given a definition here to undefine. - ((::Comparable.instance_methods(false) + ::Comparable.private_instance_methods(false)) + + # Comparable's #== is the exception: Complex has its own, which is C# (RubyComplex) and so + # already in the table. + ((::Comparable.instance_methods(false) + ::Comparable.private_instance_methods(false) - [:==]) + %i[% div divmod floor ceil modulo remainder round step truncate i positive? negative?]).each do |m| define_method(m) { |*| } unless method_defined?(m) || private_method_defined?(m) undef_method(m) end # --- instance methods -------------------------------------------------------- + # + # +, -, *, /, quo, ==, #hash and #abs are C# (RubyComplex) for Integer and Float parts and + # operands; the __ir_*_slow__ methods below are the rest of each, which C# calls. - attr_reader :real - - def imaginary - @image - end - alias imag imaginary # rb_complex_uminus def -@ - ::Complex.__new2__(-@real, -@image) + ::Complex.__new2__(-self.real, -self.imaginary) end # rb_complex_plus - def +(other) + def __ir_add_slow__(other) if ::Complex === other - return ::Complex.__new2__(::Complex.__f_add__(@real, other.real), ::Complex.__f_add__(@image, other.imaginary)) + return ::Complex.__new2__(::Complex.__f_add__(self.real, other.real), ::Complex.__f_add__(self.imaginary, other.imaginary)) end if ::Numeric === other && ::Complex.__real_p__(other) - return ::Complex.__new2__(::Complex.__f_add__(@real, other), @image) + return ::Complex.__new2__(::Complex.__f_add__(self.real, other), self.imaginary) end __ir_num_coerce_bin__(other, :+) end # rb_complex_minus - def -(other) + def __ir_sub_slow__(other) if ::Complex === other - return ::Complex.__new2__(::Complex.__f_sub__(@real, other.real), ::Complex.__f_sub__(@image, other.imaginary)) + return ::Complex.__new2__(::Complex.__f_sub__(self.real, other.real), ::Complex.__f_sub__(self.imaginary, other.imaginary)) end if ::Numeric === other && ::Complex.__real_p__(other) - return ::Complex.__new2__(::Complex.__f_sub__(@real, other), @image) + return ::Complex.__new2__(::Complex.__f_sub__(self.real, other), self.imaginary) end __ir_num_coerce_bin__(other, :-) end @@ -383,13 +372,13 @@ def self.__comp_mul__(areal, aimag, breal, bimag) end # rb_complex_mul - def *(other) + def __ir_mul_slow__(other) if ::Complex === other - real, imag = ::Complex.__comp_mul__(@real, @image, other.real, other.imaginary) + real, imag = ::Complex.__comp_mul__(self.real, self.imaginary, other.real, other.imaginary) return ::Complex.__new2__(real, imag) end if ::Numeric === other && ::Complex.__real_p__(other) - return ::Complex.__new2__(::Complex.__f_mul__(@real, other), ::Complex.__f_mul__(@image, other)) + return ::Complex.__new2__(::Complex.__f_mul__(self.real, other), ::Complex.__f_mul__(self.imaginary, other)) end __ir_num_coerce_bin__(other, :*) end @@ -398,8 +387,8 @@ def *(other) def __ir_divide__(other, func, id) c = ::Complex if ::Complex === other - ar = @real - ai = @image + ar = self.real + ai = self.imaginary br = other.real bi = other.imaginary flo = ::Float === ar || ::Float === ai || ::Float === br || ::Float === bi @@ -421,8 +410,8 @@ def __ir_divide__(other, func, id) return c.__new2__(x, y) end if ::Numeric === other && c.__real_p__(other) - x = ::Rational.__canonicalize_value__(c.__send__(func, @real, other)) - y = ::Rational.__canonicalize_value__(c.__send__(func, @image, other)) + x = ::Rational.__canonicalize_value__(c.__send__(func, self.real, other)) + y = ::Rational.__canonicalize_value__(c.__send__(func, self.imaginary, other)) return c.__new2__(x, y) end __ir_num_coerce_bin__(other, id) @@ -430,10 +419,9 @@ def __ir_divide__(other, func, id) private :__ir_divide__ # rb_complex_div - def /(other) + def __ir_div_slow__(other) __ir_divide__(other, :__f_quo__, :quo) end - alias quo / # nucomp_fdiv def fdiv(other) @@ -456,17 +444,17 @@ def __ir_pow_special_angle__(other) return nil unless ::Integer === other c = ::Complex x = nil - if c.__zero__(@image) - x = @real + if c.__zero__(self.imaginary) + x = self.real dir = 0 - elsif c.__zero__(@real) - x = @image + elsif c.__zero__(self.real) + x = self.imaginary dir = 2 - elsif c.__eqeq__(@real, @image) - x = @real + elsif c.__eqeq__(self.real, self.imaginary) + x = self.real dir = 1 - elsif c.__eqeq__(@real, c.__f_negate__(@image)) - x = @image + elsif c.__eqeq__(self.real, c.__f_negate__(self.imaginary)) + x = self.imaginary dir = 3 else dir = 0 @@ -506,7 +494,7 @@ def **(other) other = other.real end if ::Integer === other && other == 1 - return c.__raw__(@real, @image) + return c.__raw__(self.real, self.imaginary) end result = __ir_pow_special_angle__(other) @@ -565,13 +553,13 @@ def **(other) end # nucomp_eqeq_p - def ==(other) + def __ir_eqeq_slow__(other) c = ::Complex if ::Complex === other - return c.__eqeq__(@real, other.real) && c.__eqeq__(@image, other.imaginary) + return c.__eqeq__(self.real, other.real) && c.__eqeq__(self.imaginary, other.imaginary) end if ::Numeric === other && c.__real_p__(other) - return c.__eqeq__(@real, other) && c.__zero__(@image) + return c.__eqeq__(self.real, other) && c.__zero__(self.imaginary) end c.__eqeq__(other, self) end @@ -580,13 +568,13 @@ def ==(other) def <=>(other) c = ::Complex return __ir_num_coerce_cmp__(other, :<=>) unless ::Numeric === other - return nil unless c.__zero__(@image) + return nil unless c.__zero__(self.imaginary) if ::Complex === other - return @real <=> other.real if c.__zero__(other.imaginary) + return self.real <=> other.real if c.__zero__(other.imaginary) elsif c.__real_p__(other) - return @real <=> other + return self.real <=> other else - return @real.__send__(:__ir_num_coerce_cmp__, other, :<=>) + return self.real.__send__(:__ir_num_coerce_cmp__, other, :<=>) end nil end @@ -601,37 +589,36 @@ def coerce(other) end # rb_complex_abs - def abs + def __ir_abs_slow__ c = ::Complex - if c.__zero__(@real) - a = @image.abs - a = a.to_f if ::Float === @real && !(::Float === @image) + if c.__zero__(self.real) + a = self.imaginary.abs + a = a.to_f if ::Float === self.real && !(::Float === self.imaginary) return a end - if c.__zero__(@image) - a = @real.abs - a = a.to_f if !(::Float === @real) && ::Float === @image + if c.__zero__(self.imaginary) + a = self.real.abs + a = a.to_f if !(::Float === self.real) && ::Float === self.imaginary return a end - ::Math.hypot(@real, @image) + ::Math.hypot(self.real, self.imaginary) end - alias magnitude abs # nucomp_abs2 def abs2 c = ::Complex - c.__f_add__(c.__f_mul__(@real, @real), c.__f_mul__(@image, @image)) + c.__f_add__(c.__f_mul__(self.real, self.real), c.__f_mul__(self.imaginary, self.imaginary)) end # rb_complex_arg def arg - ::Math.atan2(@image, @real) + ::Math.atan2(self.imaginary, self.real) end alias angle arg alias phase arg def rectangular - [@real, @image] + [self.real, self.imaginary] end alias rect rectangular @@ -641,7 +628,7 @@ def polar # rb_complex_conjugate def conjugate - ::Complex.__new2__(@real, -@image) + ::Complex.__new2__(self.real, -self.imaginary) end alias conj conjugate @@ -651,34 +638,34 @@ def real? # nucomp_denominator / nucomp_numerator def denominator - ::Complex.__denominator__(@real).lcm(::Complex.__denominator__(@image)) + ::Complex.__denominator__(self.real).lcm(::Complex.__denominator__(self.imaginary)) end def numerator c = ::Complex cd = denominator - c.__new2__(c.__f_mul__(c.__numerator__(@real), c.__f_div__(cd, c.__denominator__(@real))), - c.__f_mul__(c.__numerator__(@image), c.__f_div__(cd, c.__denominator__(@image)))) + c.__new2__(c.__f_mul__(c.__numerator__(self.real), c.__f_div__(cd, c.__denominator__(self.real))), + c.__f_mul__(c.__numerator__(self.imaginary), c.__f_div__(cd, c.__denominator__(self.imaginary)))) end # nucomp_hash - def hash - [@real, @image, ::Complex].hash + def __ir_hash_slow__ + [self.real, self.imaginary, ::Complex].hash end # nucomp_eql_p def eql?(other) return false unless ::Complex === other - @real.class == other.real.class && @image.class == other.imaginary.class && self == other + self.real.class == other.real.class && self.imaginary.class == other.imaginary.class && self == other end # f_format: the imaginary part's sign, its magnitude, then "i" - with a "*" in front # when the magnitude does not end in a digit, as in "(3/4)*i" or "Infinity*i". def __ir_format__(s, inspecting) - impos = !::Complex.__signbit__(@image) - s << (inspecting ? @real.inspect : ::Kernel.String(@real)) + impos = !::Complex.__signbit__(self.imaginary) + s << (inspecting ? self.real.inspect : ::Kernel.String(self.real)) s << (impos ? "+" : "-") - mag = @image.abs + mag = self.imaginary.abs s << (inspecting ? mag.inspect : ::Kernel.String(mag)) s << "*" unless s.end_with?("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") s << "i" @@ -695,49 +682,49 @@ def inspect # rb_complex_finite_p / rb_complex_infinite_p def finite? - ::Complex.__finite__(@real) && ::Complex.__finite__(@image) + ::Complex.__finite__(self.real) && ::Complex.__finite__(self.imaginary) end def infinite? - return nil if !::Complex.__infinite__(@real) && !::Complex.__infinite__(@image) + return nil if !::Complex.__infinite__(self.real) && !::Complex.__infinite__(self.imaginary) 1 end # nucomp_to_i / nucomp_to_f / nucomp_to_r / nucomp_rationalize def to_i - unless ::Complex.__exact_zero__(@image) + unless ::Complex.__exact_zero__(self.imaginary) ::Kernel.raise(::RangeError, "can't convert #{self} into Integer") end - @real.to_i + self.real.to_i end def to_f - unless ::Complex.__exact_zero__(@image) + unless ::Complex.__exact_zero__(self.imaginary) ::Kernel.raise(::RangeError, "can't convert #{self} into Float") end - @real.to_f + self.real.to_f end def to_r - if ::Float === @image && @image == 0.0 + if ::Float === self.imaginary && self.imaginary == 0.0 # a Float zero imaginary part converts as well as an exact one - elsif !::Complex.__exact_zero__(@image) - imag = ::Rational.__check_to_rational__(@image) rescue nil + elsif !::Complex.__exact_zero__(self.imaginary) + imag = ::Rational.__check_to_rational__(self.imaginary) rescue nil if imag.nil? || !(::Rational === imag) || !::Complex.__exact_zero__(imag) ::Kernel.raise(::RangeError, "can't convert #{self} into Rational") end end - @real.to_r + self.real.to_r end def rationalize(*args) if args.size > 1 ::Kernel.raise(::ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..1)") end - unless ::Complex.__exact_zero__(@image) + unless ::Complex.__exact_zero__(self.imaginary) ::Kernel.raise(::RangeError, "can't convert #{self} into Rational") end - @real.rationalize(*args) + self.real.rationalize(*args) end def to_c @@ -758,7 +745,7 @@ def clone(freeze: nil) # nucomp_marshal_dump / nucomp_marshal_load (by way of Complex::compatible in MRI). def marshal_dump - [@real, @image] + [self.real, self.imaginary] end private :marshal_dump @@ -767,10 +754,7 @@ def marshal_load(a) if a.size != 2 ::Kernel.raise(::ArgumentError, "marshaled complex must have an array whose length is 2 but #{a.size}") end - @real = a[0] - @image = a[1] - freeze - self + __ir_set_parts__(a[0], a[1]) end private :marshal_load @@ -1060,7 +1044,8 @@ def self.convert(*args) module Kernel # nucomp_f_complex. Complex(1) is (1+0i) while Complex(1, nil) is a TypeError, so the # arity is read off *args rather than from a default. - def Complex(*args, exception: true) + # Kernel#Complex is C# (KernelOps.ToComplex) for Integers and Floats; the rest comes here. + def __ir_Complex_slow__(*args, exception: true) if args.size < 1 || args.size > 2 ::Kernel.raise(::ArgumentError, "wrong number of arguments (given #{args.size}, expected 1..2)") end @@ -1068,7 +1053,7 @@ def Complex(*args, exception: true) return args[0] if args.size == 1 && ::Complex.equal?(::Kernel.instance_method(:class).bind(args[0]).call) ::Complex.__convert__(args[0], args[1], args.size == 2, raise_) end - module_function :Complex + module_function :__ir_Complex_slow__ end class Numeric @@ -1119,3 +1104,8 @@ def to_c ::Complex.__parse_string__(self, false) end end + +class Complex + private :__ir_add_slow__, :__ir_sub_slow__, :__ir_mul_slow__, :__ir_div_slow__, + :__ir_eqeq_slow__, :__ir_hash_slow__, :__ir_abs_slow__ +end From 25c068384e8a6cd9dbffb7ad77105def5e668bd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 11:17:24 +0000 Subject: [PATCH 2/2] Complex: a sum or difference of two Int32 products past a long is a Bignum comp_mul adds and subtracts products of two Int32-sized parts, which can pass a long: Complex(-2**31, -2**31) ** 2 came out with -2**63 for its imaginary part, where it is 2**63. Plus and Minus detect the overflow and take a BigInteger. Rational_CSharp1 also gains cases at the Int32 edges, where RubyRational leaves long arithmetic for BigInteger. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KPwZf7MsYwQdhLBCZCkH2C --- Src/IronRuby.Tests/Runtime/MiscTests.cs | 4 ++++ Src/Libraries/Builtins/RubyComplex.cs | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Src/IronRuby.Tests/Runtime/MiscTests.cs b/Src/IronRuby.Tests/Runtime/MiscTests.cs index cac3cf152..4e41ae55f 100644 --- a/Src/IronRuby.Tests/Runtime/MiscTests.cs +++ b/Src/IronRuby.Tests/Runtime/MiscTests.cs @@ -3202,6 +3202,7 @@ public void Complex_CSharp1() { t('cerr') { Complex(nil) } t('hypot') { [Math.hypot(3.5, 1), Math.hypot(3, 4), Math.hypot(1e300, 1e300), Math.hypot(1e-300, 1e-300), Math.hypot(-0.0, 0), Math.hypot(Float::INFINITY, Float::NAN), Math.hypot(5e-324, 5e-324)] } t('undef') { [Complex.instance_method(:==).owner, (Complex(1) < 2 rescue $!.class)] } +t('int32 edge') { m = -2147483648; x = 2147483647; [Complex(m, m) * Complex(m, m), Complex(x, x) * Complex(x, -x), Complex(m, x) * Complex(x, m), Complex(m, 0) * m + Complex(m, 0) * m, Complex(x, x) + Complex(x, x), Complex(m, m) - Complex(x, x)] } ", @" basic: [(1+2i), 1, 2, 2, true, [], Numeric, (1+0i), (1.5+0i), (1+2i)] add: [(4.5+1i), (2+2i), (3.5+2i), ((3/2)+5i), (-2.5+3i), (0+2i), ((4/3)+2i), (-1-2i), (1+2i)] @@ -3223,6 +3224,7 @@ public void Complex_CSharp1() { cerr: [TypeError, ""can't convert nil into Complex""] hypot: [3.640054944640259, 5.0, 1.4142135623730952e+300, 1.414213562373095e-300, 0.0, Infinity, 5.0e-324] undef: [Complex, NoMethodError] +int32 edge: [(0+9223372036854775808i), (9223372028264841218+0i), (0+9223372032559808513i), (9223372036854775808+0i), (4294967294+4294967294i), (-4294967295-4294967295i)] "); } @@ -3266,6 +3268,7 @@ public void Rational_CSharp1() { t('badkw') { Rational(1, exception: 1) } t('Kernel.') { Kernel.Rational(3, 9) } t('arity') { method(:Rational).arity } +t('int32 edge') { [Rational(2**31 - 1, 2**31 - 2) + Rational(-2**31, 2**31 - 1), Rational(-2**31, 1) * Rational(-2**31, 1), Rational(2**31, 3) / Rational(-2**31, 7), Rational(-2**31, 2**31 - 1) <=> Rational(2**31 - 1, 2**31 - 2), Rational(2**31 - 1) + (2**31 - 1), Rational(2**31, 1) + 1, Rational(-2**31, 3) - Rational(2**31 - 1, 5), Rational(2**31 - 1, -(2**31)), (-2**31) ** -1, (-2**31) ** -2] } t('class') { [Rational(1, 3).class, Rational(4, 2).denominator.class] } ", @" add: [(-1/15), (4/3), (3541774862152233910273/3), 0.8333333333333333, (1180591620717411303425/3), (2361183241434822606848/3), (11/15), (-8/3), (-11/15), (0/1)] @@ -3297,6 +3300,7 @@ public void Rational_CSharp1() { badkw: [ArgumentError, ""expected true or false as exception: 1""] Kernel.: (1/3) arity: -1 +int32 edge: [(1/4611686011984936962), (4611686018427387904/1), (-7/3), -1, (4294967294/1), (2147483649/1), (-17179869181/15), (-2147483647/2147483648), (-1/2147483648), (1/4611686018427387904)] class: [Rational, Integer] "); } diff --git a/Src/Libraries/Builtins/RubyComplex.cs b/Src/Libraries/Builtins/RubyComplex.cs index dd6d4b7b9..48c5749e6 100644 --- a/Src/Libraries/Builtins/RubyComplex.cs +++ b/Src/Libraries/Builtins/RubyComplex.cs @@ -101,15 +101,25 @@ private static double D(object x) { return v; } + // The operands can be products of two Int32-sized parts (comp_mul), so a sum or difference of + // two of them can pass a long: Complex(-2**31, -2**31) ** 2 has 2**63 for its imaginary part. private static object Plus(object x, object y) { long a, b; - if (TryLong(x, out a) && TryLong(y, out b)) { return Protocols.Normalize(a + b); } + if (TryLong(x, out a) && TryLong(y, out b)) { + long r = unchecked(a + b); + if (((a ^ r) & (b ^ r)) < 0) { return Protocols.Normalize((BigInteger)a + b); } + return Protocols.Normalize(r); + } return D(x) + D(y); } private static object Minus(object x, object y) { long a, b; - if (TryLong(x, out a) && TryLong(y, out b)) { return Protocols.Normalize(a - b); } + if (TryLong(x, out a) && TryLong(y, out b)) { + long r = unchecked(a - b); + if (((a ^ b) & (a ^ r)) < 0) { return Protocols.Normalize((BigInteger)a - b); } + return Protocols.Normalize(r); + } return D(x) - D(y); }