diff --git a/Src/IronRuby.Tests/RubyTests.cs b/Src/IronRuby.Tests/RubyTests.cs index be37b0546..25b27970f 100644 --- a/Src/IronRuby.Tests/RubyTests.cs +++ b/Src/IronRuby.Tests/RubyTests.cs @@ -870,6 +870,8 @@ public Tests(Driver/*!*/ driver) { Io_LineReaders3, StringIO_LineReaders1, Kernel_ConversionsInjectZip1, + Kernel_ConversionsRoundingStep1, + Method_OptionalAndKeywordParameters1, Method_LightFrame1, Method_LightFrame2, Hash_SelfKey1, diff --git a/Src/IronRuby.Tests/Runtime/MiscTests.cs b/Src/IronRuby.Tests/Runtime/MiscTests.cs index 34ad997f6..d24d8d166 100644 --- a/Src/IronRuby.Tests/Runtime/MiscTests.cs +++ b/Src/IronRuby.Tests/Runtime/MiscTests.cs @@ -3147,6 +3147,192 @@ private static bool CanCreateSymbolicLinks() { } } + /// + /// A method with optional positional parameters and keywords (`def m(a, b = 2, k: 3)') keeps its + /// optionals as optional parameters; a keyword call that leaves one out has its keyword hash moved + /// into place before the defaults run, in the generic body and in the JIT's entry (the loop at the + /// end runs the methods specialized). Arity errors name the positionals and required keywords as + /// MRI does, and #parameters lists required keywords first. The expected output is CRuby's. + /// + public void Method_OptionalAndKeywordParameters1() { + TestOutput(@" +require 'ruby4' +def t(l); r = begin; yield; rescue => e; [e.class, e.message]; end; puts ""#{l}: #{r.inspect}""; end +def m1(a, b = 2, k: 3) = [a, b, k] +def m2(a, b = 2, c = a + b, k: 3, j:) = [a, b, c, k, j] +def m3(a, b = {x: 1}, **kw) = [a, b, kw] +def m4(b = 1, k: 2) = [b, k] +def m5(a, b = nil, exception: true) = [a, b, exception] +t(1) { [m1(1), m1(1, 5), m1(1, k: 9), m1(1, 5, k: 9)] } +t(2) { m1(1, {k: 9}) } +t(3) { m1(1, 5, {k: 9}) } +t(4) { m1(1, 5, 6) } +t(5) { m1(k: 1) } +t(6) { m1() } +t(7) { m1(1, z: 2) } +t(8) { [m2(1, j: 0), m2(1, 4, j: 0), m2(1, 4, 5, j: 0, k: 7)] } +t(9) { m2(1, 4) } +t(10) { m2(1, 4, 5, 6, j: 0) } +t(11) { [m3(1), m3(1, z: 2), m3(1, {y: 2}), m3(1, {y: 2}, z: 3), m3(1, **{})] } +t(12) { h = {k: 7}; [m1(1, **h), m1(1, 2, **h), m4, m4(5), m4(k: 6), m4(5, k: 6), m4({k: 6})] } +t(13) { [m5(1), m5(""1"", 16), m5(1, exception: false), m5(1, 2, exception: false)] } +t(14) { [method(:m1).arity, method(:m2).arity, method(:m3).arity, method(:m4).arity, method(:m1).parameters, method(:m2).parameters] } +class P; def s(a, b = 2, k: 3) = [:p, a, b, k]; end +class C < P; def s(a, b = 5, k: 4); super; end; end +class D < P; def s(a, b = 5, k: 4); b = 9; k = 8; super; end; end +t(15) { [C.new.s(1), C.new.s(1, 2), C.new.s(1, k: 0), D.new.s(1)] } +def nil_kw(a, b = 1, k: 2) = [a, b, k] +t(16) { [nil_kw(1, nil), nil_kw(nil), nil_kw(1, nil, k: nil)] } +t(18) { send(:m1, 1, k: 4) } +t(19) { method(:m1).call(1, k: 4) } +t(20) { pr = method(:m1).to_proc; [pr.(1), pr.(1, k: 2)] } +o = Object.new +def o.each_pair(x = 1, sep: "",""); ""#{x}#{sep}""; end +t(21) { [o.each_pair, o.each_pair(2), o.each_pair(sep: "";""), o.each_pair(3, sep: ""|"")] } +def opt_hash_default(a, b = {}, k: 1) = [a, b, k] +t(22) { [opt_hash_default(1), opt_hash_default(1, {z: 1}), opt_hash_default(1, k: 2)] } +1000.times { m1(1, k: 2); m1(1); m1(1, 2) } +t(23) { [m1(1), m1(1, 5), m1(1, k: 9), m1(1, 5, k: 9)] } +# every case again after the JIT has specialized the methods +def n1(a, b = 2, k: 3) = [a, b, k] +def n2(a, b = 2, c = 7, k: 3, j:) = [a, b, c, k, j] +def n4(b = 1, k: 2) = [b, k] +def m6(a, b = 1.5, k: :x) = [a, b, k] +def mi(a, b = 2, k: 3) = a + b + k +r = nil +3000.times do |i| + r = [n1(1), n1(1, 5), n1(1, k: 9), n1(1, 5, k: 9), n2(1, j: 0), n2(1, 4, j: 0), n2(1, 4, 5, j: 0, k: 7), + m4, n4(5), n4(k: 6), n4(5, k: 6), n4({k: 6}), m6(1), m6(1, 2.5), m6(1, k: :y), mi(1), mi(1, 2, k: 4), mi(i, k: i)] +end +p r +t(1) { n1(k: 1) } +t(2) { n1(1, {k: 9}) } +t(3) { n1(1, 5, 6) } +t(4) { n1(1, z: 2) } +t(5) { n2(1, 4) } +t(6) { mi(1, ""x"") } +t(7) { mi(""a"", k: 1) } +t(8) { n1({k: 1}) } +t(9) { n4({k: 6}, k: 1) } +def kwreq_order(a, k: 1, j:) = 1 +t(24) { method(:kwreq_order).parameters } +def mand_only(a, k: 1) = 1 +t(25) { [(mand_only rescue $!.message), (mand_only(1, 2) rescue $!.message)] } +", @" +1: [[1, 2, 3], [1, 5, 3], [1, 2, 9], [1, 5, 9]] +2: [1, {k: 9}, 3] +3: [ArgumentError, ""wrong number of arguments (given 3, expected 1..2)""] +4: [ArgumentError, ""wrong number of arguments (given 3, expected 1..2)""] +5: [ArgumentError, ""wrong number of arguments (given 0, expected 1..2)""] +6: [ArgumentError, ""wrong number of arguments (given 0, expected 1..2)""] +7: [ArgumentError, ""unknown keyword: :z""] +8: [[1, 2, 3, 3, 0], [1, 4, 5, 3, 0], [1, 4, 5, 7, 0]] +9: [ArgumentError, ""missing keyword: :j""] +10: [ArgumentError, ""wrong number of arguments (given 4, expected 1..3; required keyword: j)""] +11: [[1, {x: 1}, {}], [1, {x: 1}, {z: 2}], [1, {y: 2}, {}], [1, {y: 2}, {z: 3}], [1, {x: 1}, {}]] +12: [[1, 2, 7], [1, 2, 7], [1, 2], [5, 2], [1, 6], [5, 6], [{k: 6}, 2]] +13: [[1, nil, true], [""1"", 16, true], [1, nil, false], [1, 2, false]] +14: [-2, -3, -2, -1, [[:req, :a], [:opt, :b], [:key, :k]], [[:req, :a], [:opt, :b], [:opt, :c], [:keyreq, :j], [:key, :k]]] +15: [[:p, 1, 5, 4], [:p, 1, 2, 4], [:p, 1, 5, 0], [:p, 1, 9, 8]] +16: [[1, nil, 2], [nil, 1, 2], [1, nil, nil]] +18: [1, 2, 4] +19: [1, 2, 4] +20: [[1, 2, 3], [1, 2, 2]] +21: [""1,"", ""2,"", ""1;"", ""3|""] +22: [[1, {}, 1], [1, {z: 1}, 1], [1, {}, 2]] +23: [[1, 2, 3], [1, 5, 3], [1, 2, 9], [1, 5, 9]] +[[1, 2, 3], [1, 5, 3], [1, 2, 9], [1, 5, 9], [1, 2, 7, 3, 0], [1, 4, 7, 3, 0], [1, 4, 5, 7, 0], [1, 2], [5, 2], [1, 6], [5, 6], [{k: 6}, 2], [1, 1.5, :x], [1, 2.5, :x], [1, 1.5, :y], 6, 7, 6000] +1: [ArgumentError, ""wrong number of arguments (given 0, expected 1..2)""] +2: [1, {k: 9}, 3] +3: [ArgumentError, ""wrong number of arguments (given 3, expected 1..2)""] +4: [ArgumentError, ""unknown keyword: :z""] +5: [ArgumentError, ""missing keyword: :j""] +6: [TypeError, ""String can't be coerced into Integer""] +7: [TypeError, ""no implicit conversion of Integer into String""] +8: [{k: 1}, 2, 3] +9: [{k: 6}, 1] +24: [[:req, :a], [:keyreq, :j], [:key, :k]] +25: [""wrong number of arguments (given 0, expected 1)"", ""wrong number of arguments (given 2, expected 1)""] +"); + } + + /// + /// Kernel#Integer and #Float in C# (the cases they answer themselves and the ones they hand to + /// the prelude), Integer#round/floor/ceil/truncate of a Fixnum in long arithmetic, Range#first + /// and #last without a count, the lazily inspected ArithmeticSequence, and the Float loop of + /// #step in C#, break and return included. The expected output is CRuby's. + /// + public void Kernel_ConversionsRoundingStep1() { + TestOutput(@" +require 'ruby4' +def t(l); r = begin; yield; rescue => e; [e.class, e.message]; end; puts ""#{l}: #{r.inspect}""; end +t('Integer nil radix') { Integer(""10"", nil) } +t('Integer float radix') { [Integer(""10"", 8.9), Integer(""z"", 8.9, exception: false)] } +o = Object.new; def o.to_int; 16; end +t('Integer to_int radix') { Integer(""ff"", o) } +t('Integer radix non-string') { [Integer(12, 16, exception: false), (Integer(12, 16) rescue $!.message)] } +t('Integer nil') { [Integer(nil, exception: false), (Integer(nil) rescue $!.message)] } +t('Integer 3 args') { Integer(""1"", 2, 3) } +t('Integer 4 args') { Integer(""1"", 2, 3, 4) } +t('Float 2 args') { Float(1, 2) } +t('Integer unknown keyword') { Integer(""1"", foo: 1) } +t('Float unknown keyword') { Float(""1"", foo: 1) } +t('Integer braced hash') { Integer(""1"", {exception: false}) } +t('arity') { [method(:Integer).arity, method(:Float).arity] } +t('Integer values') { [Integer(4.7), Integer(-4.7), Integer(1e30), Integer(2**70), Integer(""0x1f""), Integer(""1_000""), Integer("" 42 "")] } +t('Integer failures') { [Integer(Float::NAN, exception: false), (Integer(Float::NAN) rescue $!.class), Integer(""zz"", exception: false), Integer(""zz"", 36)] } +t('Float values') { [Float(1), Float(2**70), Float(""1.5""), Float(""x"", exception: false), Float(nil, exception: false), (Float(nil) rescue $!.class), Float(""1e3""), (Float("""") rescue $!.class)] } +f = 1.5 +t('Float identity') { Float(f).equal?(f) } +t('Kernel.') { [Kernel.Integer(""7""), Kernel.Float(""1"")] } +t('bad exception') { Integer(""1"", exception: 1) } +t('true radix') { Integer(""1"", true) } +t('utf16') { Integer(""12"".encode(""UTF-16LE""), exception: false) } +t('to_i') { r = Object.new; def r.to_i; 3; end; [Integer(r), Integer(Rational(7, 2)), Float(Rational(7, 2))] } +t('round') { [12345.round(-2), 12350.round(-2), -12350.round(-2), 12350.round(-2, half: :even), 12350.round(-2, half: :down), 25.round(-1, half: :even), 2147483647.round(-9), -2147483648.floor(-9), 5.ceil(-18), 5.ceil(-19), -5.floor(-19), 5.round(-25), 12345.truncate(-3), -12345.truncate(-3), 7.round(2)] } +t('round as a hash key') { {-3000000000 => :hit}[-2147483648.floor(-9)] } +t('first/last') { [(1..10).first, (1...10).last, (1..10).first(3), (1..10).last(3), (..5).last(2), (...5).last(3), ('a'..'e').last(2)] } +t('beginless first') { (..5).first } +t('endless last') { (1..).last } +t('step inspect') { r = (1..10); [(r % 3).inspect, r.step(3).inspect, r.step.inspect, 1.step(10, 3).inspect, 1.step(by: 2, to: 5).inspect, 1.0.step(2.0, 0.5).inspect, (r % 3).to_a] } +t('float step') { a = []; 1.0.step(2.0, 0.3) { |x| a << x }; 2.0.step(1.0, -0.5) { |x| a << x }; (1.0...2.0).step(0.5) { |x| a << x }; a } +t('float step break') { [1.0.step(2.0, 0.5) { |x| break x * 10 if x > 1.2 }, 1.0.step(2.0, 0.5) { }, (1.0..2.0).step(0.5).map { |x| x * 2 }] } +def float_step_return; 1.0.step(5.0, 1.0) { |x| return x if x == 3.0 }; :no; end +t('float step return') { float_step_return } +", @" +Integer nil radix: [TypeError, ""no implicit conversion from nil to integer""] +Integer float radix: [8, nil] +Integer to_int radix: 255 +Integer radix non-string: [nil, ""base specified for non string value""] +Integer nil: [nil, ""can't convert nil into Integer""] +Integer 3 args: [ArgumentError, ""wrong number of arguments (given 3, expected 1..2)""] +Integer 4 args: [ArgumentError, ""wrong number of arguments (given 4, expected 1..2)""] +Float 2 args: [ArgumentError, ""wrong number of arguments (given 2, expected 1)""] +Integer unknown keyword: [ArgumentError, ""unknown keyword: :foo""] +Float unknown keyword: [ArgumentError, ""unknown keyword: :foo""] +Integer braced hash: [TypeError, ""no implicit conversion of Hash into Integer""] +arity: [-2, -2] +Integer values: [4, -4, 1000000000000000019884624838656, 1180591620717411303424, 31, 1000, 42] +Integer failures: [nil, FloatDomainError, nil, 1295] +Float values: [1.0, 1.1805916207174113e+21, 1.5, nil, nil, TypeError, 1000.0, ArgumentError] +Float identity: true +Kernel.: [7, 1.0] +bad exception: [ArgumentError, ""expected true or false as exception: 1""] +true radix: [TypeError, ""no implicit conversion of true into Integer""] +utf16: [Encoding::CompatibilityError, ""ASCII incompatible encoding: UTF-16LE""] +to_i: [3, 3, 3.5] +round: [12300, 12400, -12400, 12400, 12300, 20, 2000000000, -3000000000, 1000000000000000000, 10000000000000000000, -10000000000000000000, 0, 12000, -12000, 7] +round as a hash key: :hit +first/last: [1, 10, [1, 2, 3], [8, 9, 10], [4, 5], [2, 3, 4], [""d"", ""e""]] +beginless first: [RangeError, ""cannot get the first element of beginless range""] +endless last: [RangeError, ""cannot get the last element of endless range""] +step inspect: [""((1..10).%(3))"", ""((1..10).step(3))"", ""((1..10).step)"", ""(1.step(10, 3))"", ""(1.step(by: 2, to: 5))"", ""(1.0.step(2.0, 0.5))"", [1, 4, 7, 10]] +float step: [1.0, 1.3, 1.6, 1.9, 2.0, 1.5, 1.0, 1.0, 1.5] +float step break: [15.0, 1.0, [2.0, 3.0, 4.0]] +float step return: 3.0 +"); + } + /// /// Kernel#Integer and #Float with exception: false answer nil from the C# parser instead of /// raising and rescuing; a String in an ASCII-incompatible encoding is refused either way, @@ -3584,7 +3770,7 @@ public void Range_IntegerFastPaths1() { first 1..3.5 2: [1, 2] last 1..3.5 2: [2, 3] first ..5 2: ""RangeError: cannot get the first element of beginless range"" -last ..5 2: ""TypeError: can't iterate from NilClass"" +last ..5 2: [4, 5] sub first: [:x] sub last: [9, 10] big last: [999999998, 999999999, 1000000000] diff --git a/Src/Libraries/Builtins/Integer.cs b/Src/Libraries/Builtins/Integer.cs index 1c7b24d3b..2d1845d65 100644 --- a/Src/Libraries/Builtins/Integer.cs +++ b/Src/Libraries/Builtins/Integer.cs @@ -121,6 +121,36 @@ public static object ToInteger(object/*!*/ self) { return self; } + // A Fixnum and an Integer ndigits: long arithmetic (NumericRounding.TryRoundFixnum). + + [RubyMethod("round")] + public static object Round(int self, int ndigits) { + object result; + return NumericRounding.TryRoundFixnum(self, ndigits, NumericRounding.IntegerRounding.Round, NumericRounding.Half.Up, out result) + ? result : NumericRounding.RoundInteger(new BigInteger(self), ndigits, NumericRounding.Half.Up); + } + + [RubyMethod("floor")] + public static object Floor(int self, int ndigits) { + object result; + return NumericRounding.TryRoundFixnum(self, ndigits, NumericRounding.IntegerRounding.Floor, NumericRounding.Half.Up, out result) + ? result : NumericRounding.FloorInteger(new BigInteger(self), ndigits); + } + + [RubyMethod("ceil")] + public static object Ceil(int self, int ndigits) { + object result; + return NumericRounding.TryRoundFixnum(self, ndigits, NumericRounding.IntegerRounding.Ceil, NumericRounding.Half.Up, out result) + ? result : NumericRounding.CeilInteger(new BigInteger(self), ndigits); + } + + [RubyMethod("truncate")] + public static object Truncate(int self, int ndigits) { + object result; + return NumericRounding.TryRoundFixnum(self, ndigits, NumericRounding.IntegerRounding.Truncate, NumericRounding.Half.Up, out result) + ? result : NumericRounding.TruncateInteger(new BigInteger(self), ndigits); + } + /// /// Rounds self to a multiple of 10**(-ndigits). A non-negative ndigits returns self. /// Accepts the half: option (:up, :down or :even). @@ -143,6 +173,10 @@ public static object Round(ConversionStorage/*!*/ integerCast, obj if (nd >= 0) { return self; } + object fixnumResult; + if (self is int && NumericRounding.TryRoundFixnum((int)self, nd, NumericRounding.IntegerRounding.Round, half, out fixnumResult)) { + return fixnumResult; + } return NumericRounding.RoundInteger(ToBigInteger(self), nd, half); } diff --git a/Src/Libraries/Builtins/KernelOps.cs b/Src/Libraries/Builtins/KernelOps.cs index b0bada6c5..2170bc61e 100644 --- a/Src/Libraries/Builtins/KernelOps.cs +++ b/Src/Libraries/Builtins/KernelOps.cs @@ -114,14 +114,246 @@ public static object InitializeClone( return result; } + #region Kernel#Integer, Kernel#Float + + // Kernel#Integer(arg, base = nil, exception: true) and #Float(arg, exception: true). The + // cases a program spends its time in - an Integer, a finite Float, a String with or without + // a radix, the exception: keyword alone - are answered here. Everything else goes to the + // prelude (__ir_Integer_slow__, __ir_Float_slow__ in ruby4.rb), which knows the coercion + // protocol and the errors. A Ruby wrapper with an optional parameter and a keyword cost + // ~0.5 us a call: such a signature is lowered to a splat, which the JIT does not take. + + private static readonly object NoException = new object(); + + // The exception: keyword of a trailing keyword hash: true or false, or NoException when the + // hash has anything else in it (another key, a value that is not true or false), which the + // prelude reports. Null when the argument is not a keyword hash at all. + private static object ExceptionOption(RubyContext/*!*/ context, object options) { + var hash = options as Hash; + if (hash == null || !hash.IsKeywordArguments) { + return null; + } + if (hash.Count == 0) { + return ScriptingRuntimeHelpers.True; + } + object value; + if (hash.Count == 1 && hash.TryGetValue(context.CreateAsciiSymbol("exception"), out value) && value is bool) { + return value; + } + return NoException; + } + + // ruby4.rb is not loaded (an embedding that does without it, a test): the conversion + // protocol of the C# library is all there is. + private static bool HasPrelude(RubyContext/*!*/ context, string/*!*/ name) { + return context.KernelModule.ResolveMethod(name, VisibilityContext.AllVisible).Found; + } + + private static Exception/*!*/ ConversionArity(int given, string/*!*/ expected) { + return RubyExceptions.CreateArgumentError("wrong number of arguments (given {0}, expected {1})", given, expected); + } + + [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] + public static object ToInteger(CallSiteStorage>/*!*/ slow, + ConversionStorage/*!*/ integerConversion, object self, object value) { + object result; + if (TryToIntegerFast(self, value, null, true, out result)) { + return result; + } + if (!HasPrelude(integerConversion.Context, "__ir_Integer_slow__")) { + return ToInteger(integerConversion, self, value); + } + var site = slow.GetCallSite("__ir_Integer_slow__", 2); + return site.Target(site, self, value, null); + } + + [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] + public static object ToInteger(CallSiteStorage>/*!*/ slow, + CallSiteStorage>/*!*/ slowWithOptions, + RubyContext/*!*/ context, object self, object value, object radixOrOptions) { + + object result; + object exception = ExceptionOption(context, radixOrOptions); + if (exception == null) { + // Integer(value, radix) + RequireRadix(radixOrOptions); + if (TryToIntegerFast(self, value, radixOrOptions, true, out result)) { + return result; + } + var site = slow.GetCallSite("__ir_Integer_slow__", 2); + return site.Target(site, self, value, radixOrOptions); + } + if (exception != NoException && TryToIntegerFast(self, value, null, (bool)exception, out result)) { + return result; + } + var optionsSite = slowWithOptions.GetCallSite("__ir_Integer_slow__", 3); + return optionsSite.Target(optionsSite, self, value, null, radixOrOptions); + } + + [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] + public static object ToInteger(CallSiteStorage>/*!*/ slowWithOptions, + RubyContext/*!*/ context, object self, object value, object radix, object options) { + + object exception = ExceptionOption(context, options); + if (exception == null) { + throw ConversionArity(3, "1..2"); + } + RequireRadix(radix); + object result; + if (exception != NoException && TryToIntegerFast(self, value, radix, (bool)exception, out result)) { + return result; + } + var site = slowWithOptions.GetCallSite("__ir_Integer_slow__", 3); + return site.Target(site, self, value, radix, options); + } + + // A radix given as nil is not a radix left out (MRI's NUM2INT), whatever exception: says. + private static void RequireRadix(object radix) { + if (radix == null) { + throw RubyExceptions.CreateTypeError("no implicit conversion from nil to integer"); + } + } + + // Any other number of arguments: the error MRI words, which the binder would not (it + // counts the keyword hash as one more parameter). + [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] + public static object ToInteger(object self, object value, object radix, object options, object extra, params object[]/*!*/ more) { + throw ConversionArity(4 + more.Length, "1..2"); + } + + private static bool TryToIntegerFast(object self, object value, object radix, bool exception, out object result) { + if (radix == null) { + if (value is int || value is BigInteger) { + result = value; + return true; + } + if (value is double) { + double d = (double)value; + if (Double.IsNaN(d) || Double.IsInfinity(d)) { + // FloatDomainError, which the prelude raises - or nil + result = null; + return !exception; + } + result = ClrFloat.ToInt(d); + return true; + } + } + var str = value as MutableString; + if (str != null && (radix == null || radix is int)) { + int r = (radix == null) ? 0 : (int)radix; + result = exception ? ToInteger(self, str, r) : TryToInteger(self, str, r); + return true; + } + result = null; + return false; + } + + [RubyMethod("Float", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Float", RubyMethodAttributes.PublicSingleton)] + public static object ToFloat(CallSiteStorage>/*!*/ slow, + ConversionStorage/*!*/ floatConversion, RubyContext/*!*/ context, object self, object value) { + + object result; + if (TryToFloatFast(context, self, value, true, out result)) { + return result; + } + if (!HasPrelude(context, "__ir_Float_slow__")) { + return ToFloat(floatConversion, self, value); + } + var site = slow.GetCallSite("__ir_Float_slow__", 1); + return site.Target(site, self, value); + } + + [RubyMethod("Float", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Float", RubyMethodAttributes.PublicSingleton)] + public static object ToFloat(CallSiteStorage>/*!*/ slowWithOptions, + RubyContext/*!*/ context, object self, object value, object options) { + + object exception = ExceptionOption(context, options); + if (exception == null) { + throw ConversionArity(2, "1"); + } + object result; + if (exception != NoException && TryToFloatFast(context, self, value, (bool)exception, out result)) { + return result; + } + var site = slowWithOptions.GetCallSite("__ir_Float_slow__", 2); + return site.Target(site, self, value, options); + } + + [RubyMethod("Float", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("Float", RubyMethodAttributes.PublicSingleton)] + public static object ToFloat(object self, object value, object options, object extra, params object[]/*!*/ more) { + throw ConversionArity(3 + more.Length, "1"); + } + + private static bool TryToFloatFast(RubyContext/*!*/ context, object self, object value, bool exception, out object result) { + if (value is double) { + // the very same object, see below + result = value; + return true; + } + if (value is int) { + result = (double)(int)value; + return true; + } + var str = value as MutableString; + if (str != null) { + result = exception ? ToFloat(context, self, str) : TryToFloat(self, str); + return true; + } + result = null; + return false; + } + + #endregion + + // The Float loop of Numeric#step and Range#step (ArithmeticSequence.__step_each__ in + // ruby4.rb, after MRI's ruby_float_step): n values beg, beg + step, ..., the last one held + // to stop. A method that yields is not one the method JIT takes, so the loop cost ~0.25 us + // a value in Ruby. + [RubyMethod("__ir_float_step__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_float_step__", RubyMethodAttributes.PublicSingleton)] + public static object FloatStep(BlockParam/*!*/ block, object self, double beg, double stop, double step, double n) { + object result; + if (Double.IsInfinity(step)) { + if (n > 0 && block.Yield(beg, out result)) { + return result; + } + } else if (step == 0) { + while (true) { + if (block.Yield(beg, out result)) { + return result; + } + } + } else { + // n may be NaN (Infinity.step(Infinity, 1)); 0 < NaN is false, so nothing is + // yielded, which is what MRI's for(;i= 0 ? stop < d : d < stop) { + d = stop; + } + if (block.Yield(d, out result)) { + return result; + } + } + } + return null; + } + /// /// A Float given to Float() comes back as the very same object. Taking it as a double and /// handing it back boxes it afresh, and #equal? on a boxed double compares the boxes - so /// Float(f).equal?(f) was false, including for the NaN and the Infinity that have no /// other way of being told apart from any other NaN or Infinity. /// - [RubyMethod("Float", RubyMethodAttributes.PrivateInstance)] - [RubyMethod("Float", RubyMethodAttributes.PublicSingleton)] + [RubyMethod("__ir_Float__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_Float__", RubyMethodAttributes.PublicSingleton)] public static object ToFloat(ConversionStorage/*!*/ floatConversion, object self, object value) { if (value is double) { return value; @@ -132,8 +364,8 @@ public static object ToFloat(ConversionStorage/*!*/ floatConversion, obj // A String has to be readable as a number at all: in an ASCII-compatible encoding - MRI // checks that before anything else, and exception: false does not change it - and, for // Float, without a NUL, which MRI names rather than calling the value invalid. - [RubyMethod("Float", RubyMethodAttributes.PrivateInstance)] - [RubyMethod("Float", RubyMethodAttributes.PublicSingleton)] + [RubyMethod("__ir_Float__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_Float__", RubyMethodAttributes.PublicSingleton)] public static object ToFloat(RubyContext/*!*/ context, object self, [NotNull]MutableString/*!*/ value) { RequireAsciiCompatible(value); if (value.IndexOf('\0') >= 0) { @@ -148,14 +380,14 @@ private static void RequireAsciiCompatible(MutableString/*!*/ value) { } } - [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] - [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] + [RubyMethod("__ir_Integer__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_Integer__", RubyMethodAttributes.PublicSingleton)] public static object/*!*/ ToInteger(object self, [NotNull]MutableString/*!*/ value) { return ToInteger(self, value, 0); } - [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] - [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] + [RubyMethod("__ir_Integer__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_Integer__", RubyMethodAttributes.PublicSingleton)] public static object/*!*/ ToInteger(object self, [NotNull]MutableString/*!*/ value, [DefaultProtocol]int radix) { RequireAsciiCompatible(value); var str = value.ConvertToString(); @@ -370,8 +602,8 @@ private static bool TryParseRubyInteger(string/*!*/ str, int requestedRadix, out #endregion - [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] - [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] + [RubyMethod("__ir_Integer__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_Integer__", RubyMethodAttributes.PublicSingleton)] public static object/*!*/ ToInteger(ConversionStorage/*!*/ integerConversion, object self, object value) { var integer = Protocols.ConvertToInteger(integerConversion, value); return integer.IsFixnum ? ScriptingRuntimeHelpers.Int32ToObject(integer.Fixnum) : integer.Bignum; diff --git a/Src/Libraries/Builtins/RangeOps.cs b/Src/Libraries/Builtins/RangeOps.cs index d9b57e519..943f92f8a 100644 --- a/Src/Libraries/Builtins/RangeOps.cs +++ b/Src/Libraries/Builtins/RangeOps.cs @@ -98,15 +98,47 @@ public static object Reinitialize(BinaryOpStorage/*!*/ comparisonStorage, /// /// Returns the first object in self /// - [RubyMethod("begin"), RubyMethod("first")] + [RubyMethod("begin")] public static object Begin([NotNull]Range/*!*/ self) { return self.Begin; } + // #first and #last without a count answer the endpoint; with one, ruby4.rb counts + // (__first_n__, __last_n__). They were a Ruby `def first(*args)', whose rest parameter + // put every call on the slow path: (1..100).first cost ~0.2 us. + + [RubyMethod("first")] + public static object First([NotNull]Range/*!*/ self) { + if (self.Begin == null) { + throw RubyExceptions.CreateRangeError("cannot get the first element of beginless range"); + } + return self.Begin; + } + + [RubyMethod("first")] + public static object First(CallSiteStorage>/*!*/ firstN, [NotNull]Range/*!*/ self, object count) { + var site = firstN.GetCallSite("__first_n__", 1); + return site.Target(site, self, count); + } + + [RubyMethod("last")] + public static object Last([NotNull]Range/*!*/ self) { + if (self.End == null) { + throw RubyExceptions.CreateRangeError("cannot get the last element of endless range"); + } + return self.End; + } + + [RubyMethod("last")] + public static object Last(CallSiteStorage>/*!*/ lastN, [NotNull]Range/*!*/ self, object count) { + var site = lastN.GetCallSite("__last_n__", 1); + return site.Target(site, self, count); + } + /// /// Returns the object that defines the end of self /// - [RubyMethod("end"), RubyMethod("last")] + [RubyMethod("end")] public static object End([NotNull]Range/*!*/ self) { return self.End; } diff --git a/Src/Libraries/Extensions/NumericRounding.cs b/Src/Libraries/Extensions/NumericRounding.cs index 76696bb1b..0acb543ab 100644 --- a/Src/Libraries/Extensions/NumericRounding.cs +++ b/Src/Libraries/Extensions/NumericRounding.cs @@ -367,6 +367,70 @@ private static bool RoundsAwayEntirely(BigInteger value, int ndigits) { return -(long)ndigits > digits; } + // ---- a Fixnum and a negative ndigits, in long arithmetic ------------------------------- + // + // What RoundInteger and its siblings answer, without a BigInteger in sight: 12345.round(-2) + // cost ~0.5 us, nearly all of it BigInteger arithmetic on numbers that fit in a long. A + // power of ten past 10**18 does not fit, and the BigInteger versions take that. + + internal enum IntegerRounding { Round, Floor, Ceil, Truncate } + + private static readonly long[] _powersOf10 = BuildPowersOf10(); + + private static long[] BuildPowersOf10() { + var result = new long[19]; + long p = 1; + for (int i = 0; i < result.Length; i++) { + result[i] = p; + p *= 10; + } + return result; + } + + internal static bool TryRoundFixnum(int value, int ndigits, IntegerRounding kind, Half mode, out object result) { + if (ndigits >= 0) { + result = ScriptingRuntimeHelpers.Int32ToObject(value); + return true; + } + if (ndigits < -18) { + result = null; + return false; + } + long f = _powersOf10[-ndigits]; + long v = value; + long r = v % f; // the sign of v, as C#'s % has it + long down = v - r; // towards zero + long rounded; + switch (kind) { + case IntegerRounding.Truncate: + rounded = down; + break; + case IntegerRounding.Floor: + rounded = (r < 0) ? down - f : down; + break; + case IntegerRounding.Ceil: + rounded = (r > 0) ? down + f : down; + break; + default: + long twice = 2 * Math.Abs(r); + bool away; + if (twice != f) { + away = twice > f; + } else { + switch (mode) { + case Half.Down: away = false; break; + case Half.Even: away = ((down / f) & 1) != 0; break; + default: away = true; break; + } + } + rounded = away ? (v < 0 ? down - f : down + f) : down; + break; + } + // an Int32 when it fits and an Int64 otherwise, as every other Integer is represented + result = Protocols.Normalize(rounded); + return true; + } + private static BigInteger Pow10(int exponent) { return BigInteger.Pow(10, exponent); } diff --git a/Src/Libraries/Initializers.Generated.cs b/Src/Libraries/Initializers.Generated.cs index fe84aab70..5a8573ec4 100644 --- a/Src/Libraries/Initializers.Generated.cs +++ b/Src/Libraries/Initializers.Generated.cs @@ -3107,8 +3107,9 @@ private static void LoadInteger_Instance(IronRuby.Builtins.RubyModule/*!*/ modul ); DefineLibraryMethod(module, "ceil", 0x51, - 0x00000000U, 0x00000000U, + 0x00000000U, 0x00000000U, 0x00000000U, new Func(IronRuby.Builtins.Integer.ToInteger), + new Func(IronRuby.Builtins.Integer.Ceil), new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.Integer.Ceil) ); @@ -3145,8 +3146,9 @@ private static void LoadInteger_Instance(IronRuby.Builtins.RubyModule/*!*/ modul ); DefineLibraryMethod(module, "floor", 0x51, - 0x00000000U, 0x00000000U, + 0x00000000U, 0x00000000U, 0x00000000U, new Func(IronRuby.Builtins.Integer.ToInteger), + new Func(IronRuby.Builtins.Integer.Floor), new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.Integer.Floor) ); @@ -3229,8 +3231,9 @@ private static void LoadInteger_Instance(IronRuby.Builtins.RubyModule/*!*/ modul ); DefineLibraryMethod(module, "round", 0x51, - 0x00000000U, 0x00000000U, + 0x00000000U, 0x00000000U, 0x00000000U, new Func(IronRuby.Builtins.Integer.ToInteger), + new Func(IronRuby.Builtins.Integer.Round), new Func, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.Integer.Round) ); @@ -3268,8 +3271,9 @@ private static void LoadInteger_Instance(IronRuby.Builtins.RubyModule/*!*/ modul ); DefineLibraryMethod(module, "truncate", 0x51, - 0x00000000U, 0x00000000U, + 0x00000000U, 0x00000000U, 0x00000000U, new Func(IronRuby.Builtins.Integer.ToInteger), + new Func(IronRuby.Builtins.Integer.Truncate), new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.Integer.Truncate) ); @@ -4733,6 +4737,17 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module new Func>, IronRuby.Runtime.CallSiteStorage>, System.Object, System.Boolean, System.Object>(IronRuby.Builtins.KernelOps.CloneWithFreeze) ); + DefineLibraryMethod(module, "__ir_Float__", 0x52, + 0x00000000U, 0x00000004U, + new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func(IronRuby.Builtins.KernelOps.ToFloat) + ); + + DefineLibraryMethod(module, "__ir_float_step__", 0x52, + 0x00000000U, + new Func(IronRuby.Builtins.KernelOps.FloatStep) + ); + DefineLibraryMethod(module, "__ir_hidden_ivar__", 0x52, 0x00000004U, new Func(IronRuby.Builtins.KernelOps.GetHiddenInstanceVariable) @@ -4743,6 +4758,13 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module new Func(IronRuby.Builtins.KernelOps.InspectForDisplay) ); + DefineLibraryMethod(module, "__ir_Integer__", 0x52, + 0x00000002U, 0x00020002U, 0x00000000U, + new Func(IronRuby.Builtins.KernelOps.ToInteger), + new Func(IronRuby.Builtins.KernelOps.ToInteger), + new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger) + ); + DefineLibraryMethod(module, "__ir_lazy_grep__", 0x52, 0x00000000U, new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.LazyGrep) @@ -4929,9 +4951,10 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module ); DefineLibraryMethod(module, "Float", 0x52, - 0x00000000U, 0x00000004U, - new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), - new Func(IronRuby.Builtins.KernelOps.ToFloat) + 0x00000000U, 0x00000000U, 0x80000000U, + new Func>, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func(IronRuby.Builtins.KernelOps.ToFloat) ); DefineLibraryMethod(module, "fork", 0x52, @@ -5023,10 +5046,11 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module ); DefineLibraryMethod(module, "Integer", 0x52, - 0x00000002U, 0x00020002U, 0x00000000U, - new Func(IronRuby.Builtins.KernelOps.ToInteger), - new Func(IronRuby.Builtins.KernelOps.ToInteger), - new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger) + 0x00000000U, 0x00000000U, 0x00000000U, 0x80000000U, + new Func>, IronRuby.Runtime.ConversionStorage, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger), + new Func>, IronRuby.Runtime.CallSiteStorage>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger), + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger), + new Func(IronRuby.Builtins.KernelOps.ToInteger) ); DefineLibraryMethod(module, "is_a?", 0x51, @@ -5325,6 +5349,24 @@ private static void LoadKernel_Class(IronRuby.Builtins.RubyModule/*!*/ module) { new Func(IronRuby.Builtins.KernelOps.GetCurrentCalleeName) ); + DefineLibraryMethod(module, "__ir_Float__", 0x61, + 0x00000000U, 0x00000004U, + new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func(IronRuby.Builtins.KernelOps.ToFloat) + ); + + DefineLibraryMethod(module, "__ir_float_step__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.KernelOps.FloatStep) + ); + + DefineLibraryMethod(module, "__ir_Integer__", 0x61, + 0x00000002U, 0x00020002U, 0x00000000U, + new Func(IronRuby.Builtins.KernelOps.ToInteger), + new Func(IronRuby.Builtins.KernelOps.ToInteger), + new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger) + ); + DefineLibraryMethod(module, "__ir_try_Float__", 0x61, 0x00000002U, new Func(IronRuby.Builtins.KernelOps.TryToFloat) @@ -5417,9 +5459,10 @@ private static void LoadKernel_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "Float", 0x61, - 0x00000000U, 0x00000004U, - new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), - new Func(IronRuby.Builtins.KernelOps.ToFloat) + 0x00000000U, 0x00000000U, 0x80000000U, + new Func>, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func(IronRuby.Builtins.KernelOps.ToFloat) ); DefineLibraryMethod(module, "fork", 0x61, @@ -5449,10 +5492,11 @@ private static void LoadKernel_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "Integer", 0x61, - 0x00000002U, 0x00020002U, 0x00000000U, - new Func(IronRuby.Builtins.KernelOps.ToInteger), - new Func(IronRuby.Builtins.KernelOps.ToInteger), - new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger) + 0x00000000U, 0x00000000U, 0x00000000U, 0x80000000U, + new Func>, IronRuby.Runtime.ConversionStorage, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger), + new Func>, IronRuby.Runtime.CallSiteStorage>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger), + new Func>, IronRuby.Runtime.RubyContext, System.Object, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToInteger), + new Func(IronRuby.Builtins.KernelOps.ToInteger) ); DefineLibraryMethod(module, "iterator?", 0x61, @@ -7639,8 +7683,9 @@ private static void LoadRange_Instance(IronRuby.Builtins.RubyModule/*!*/ module) ); DefineLibraryMethod(module, "first", 0x51, - 0x00000001U, - new Func(IronRuby.Builtins.RangeOps.Begin) + 0x00000001U, 0x00000002U, + new Func(IronRuby.Builtins.RangeOps.First), + new Func>, IronRuby.Builtins.Range, System.Object, System.Object>(IronRuby.Builtins.RangeOps.First) ); DefineLibraryMethod(module, "hash", 0x51, @@ -7669,8 +7714,9 @@ private static void LoadRange_Instance(IronRuby.Builtins.RubyModule/*!*/ module) ); DefineLibraryMethod(module, "last", 0x51, - 0x00000001U, - new Func(IronRuby.Builtins.RangeOps.End) + 0x00000001U, 0x00000002U, + new Func(IronRuby.Builtins.RangeOps.Last), + new Func>, IronRuby.Builtins.Range, System.Object, System.Object>(IronRuby.Builtins.RangeOps.Last) ); DefineLibraryMethod(module, "member?", 0x51, diff --git a/Src/Prism/PrismAstBridge.cs b/Src/Prism/PrismAstBridge.cs index 690a2a672..3cd5adcc5 100644 --- a/Src/Prism/PrismAstBridge.cs +++ b/Src/Prism/PrismAstBridge.cs @@ -2236,16 +2236,18 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List (post as Pm.RequiredParameterNode)?.Name)); } + // MRI lists the required keywords first, whatever order they were declared in: + // `def m(k: 1, j:)' has [[:keyreq, :j], [:key, :k]]. int requiredKeywordCount = 0; foreach (var keyword in node.Keywords) { - switch (keyword) { - case Pm.RequiredKeywordParameterNode required: - parameters.Add(new RubyParameterSignature.Parameter("keyreq", required.Name)); - requiredKeywordCount++; - break; - case Pm.OptionalKeywordParameterNode optional: - parameters.Add(new RubyParameterSignature.Parameter("key", optional.Name)); - break; + if (keyword is Pm.RequiredKeywordParameterNode required) { + parameters.Add(new RubyParameterSignature.Parameter("keyreq", required.Name)); + requiredKeywordCount++; + } + } + foreach (var keyword in node.Keywords) { + if (keyword is Pm.OptionalKeywordParameterNode optional) { + parameters.Add(new RubyParameterSignature.Parameter("key", optional.Name)); } } @@ -2330,23 +2332,43 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List } } + bool keywordsAfterOptionals = false; + // keyword arguments: with only mandatory positionals a trailing optional hash // is equivalent, so use that (it keeps arity and Method#parameters sane). // Anything richer needs positional binding done by hand, because keywords do // not have their own calling-convention slot here. if (node.Keywords.Length > 0 || node.KeywordRest is Pm.KeywordRestParameterNode) { + // A method with optional positionals and nothing else past them keeps them as + // optional parameters, the keywords lowered after them as below: the keyword hash + // of a call that leaves an optional out is moved into place before the defaults run + // (Parameters.KeywordsAfterOptionals). The general lowering puts everything in one + // splat, which no call can bind without allocating, and the method JIT does not take. + // // A block's keyword parameters must not count towards the auto-splat decision: // `{ |a, **k| }` takes one positional parameter, so a single Array argument is // passed whole. The general lowering is the one that knows that, since it binds // the positionals itself. - if (optional.Count > 0 || unsplat != null || node.Posts.Length > 0 || autoSplat) { + if (isMethod && optional.Count > 0 && unsplat == null && node.Posts.Length == 0 && !autoSplat) { + int optionalPositionals = optional.Count; + // `super' passes the positionals by name: the mandatory ones are locals (a + // destructured one a hidden local), and so are the optional ones + var positionals = new List(mandatory); + foreach (var opt in optional) { + positionals.Add((Expression)opt.Left); + } + prologue = LowerKeywords(node, optional, true, mandatory, isMethod, span, optionalPositionals); + _zsuperArguments = ZSuperArguments(node, positionals); + keywordsAfterOptionals = true; + } else if (optional.Count > 0 || unsplat != null || node.Posts.Length > 0 || autoSplat) { return LowerGeneralParameters(node, autoSplat, isMethod, span, out prologue); - } - // a lambda checks arity as strictly as a method does; a plain block does not - prologue = LowerKeywords(node, optional, isMethod || !autoSplat, mandatory, isMethod, span); - if (isMethod) { - // only leading mandatory parameters can be here, and they are all locals - _zsuperArguments = ZSuperArguments(node, new List(mandatory)); + } else { + // a lambda checks arity as strictly as a method does; a plain block does not + prologue = LowerKeywords(node, optional, isMethod || !autoSplat, mandatory, isMethod, span, 0); + if (isMethod) { + // only leading mandatory parameters can be here, and they are all locals + _zsuperArguments = ZSuperArguments(node, new List(mandatory)); + } } } else if (node.KeywordRest is Pm.ForwardingParameterNode) { // def f(...) => def f(*?fwd?, &?fwdblk?); calls with `...` splat them back @@ -2384,7 +2406,9 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List } return new Parameters(mandatory.ToArray(), leadingMandatoryCount, - optional.Count > 0 ? optional.ToArray() : null, unsplat, blockParam, span); + optional.Count > 0 ? optional.ToArray() : null, unsplat, blockParam, span) { + KeywordsAfterOptionals = keywordsAfterOptionals + }; } /// @@ -2543,7 +2567,7 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List /// rest = ?kw?.dup ; rest.delete(:j) ; rest.delete(:k) /// private Statements/*!*/ LowerKeywords(Pm.ParametersNode/*!*/ node, List/*!*/ optional, - bool strict, List/*!*/ mandatory, bool isMethod, SourceSpan span) { + bool strict, List/*!*/ mandatory, bool isMethod, SourceSpan span, int optionalPositionals) { var kwVar = CurrentScope.AddVariable("?kw?", span); // The default stands for "the caller passed no keywords", so it is marked as keyword @@ -2562,7 +2586,7 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List } if (strict) { - foreach (var check in KeywordSeparationChecks(kwVar, mandatory, true, span)) { + foreach (var check in KeywordSeparationChecks(kwVar, mandatory, optionalPositionals, true, span)) { prologue.Add(check); } } @@ -2584,24 +2608,25 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List /// m("a" => 1, b: 2) # the keywords landed in a: given 0, expected 1 /// private List/*!*/ KeywordSeparationChecks(LocalVariable/*!*/ kwVar, List/*!*/ mandatory, - bool acceptsKeywords, SourceSpan span) { + int optionalPositionals, bool acceptsKeywords, SourceSpan span) { var checks = new List(); int count = mandatory.Count; + string expected = (optionalPositionals > 0) ? count + ".." + (count + optionalPositionals) : count.ToString(); var lastMandatory = count > 0 ? mandatory[count - 1] as LocalVariable : null; if (lastMandatory != null) { checks.Add(new IfExpression( new KeywordArgumentsTest(lastMandatory, span), new Statements(acceptsKeywords - ? RaiseArityError(count - 1, count, span) + ? RaiseArityError(count - 1, expected, span) : RaiseError("ArgumentError", "no keywords accepted", span)), new List(), span)); } checks.Add(new UnlessExpression( new KeywordArgumentsTest(kwVar, span), - new Statements(RaiseArityError(count + 1, count, span)), null, span)); + new Statements(RaiseArityError(count + optionalPositionals + 1, expected, span)), null, span)); if (!acceptsKeywords) { checks.Add(new UnlessExpression( @@ -2621,7 +2646,7 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List optional.Add(new SimpleAssignmentExpression(kwVar, new HashConstructor(new Maplet[0], true, span), null, span)); var prologue = new Statements(); - foreach (var check in KeywordSeparationChecks(kwVar, mandatory, false, span)) { + foreach (var check in KeywordSeparationChecks(kwVar, mandatory, 0, false, span)) { prologue.Add(check); } return prologue; @@ -2638,6 +2663,10 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List } private Expression/*!*/ RaiseArityError(int given, int expected, SourceSpan span) { + return RaiseArityError(given, expected.ToString(), span); + } + + private Expression/*!*/ RaiseArityError(int given, string/*!*/ expected, SourceSpan span) { return RaiseError("ArgumentError", "wrong number of arguments (given " + given + ", expected " + expected + ")", span); } diff --git a/Src/Ruby/Compiler/Ast/Parameters.cs b/Src/Ruby/Compiler/Ast/Parameters.cs index ece1f17b1..0717c8604 100644 --- a/Src/Ruby/Compiler/Ast/Parameters.cs +++ b/Src/Ruby/Compiler/Ast/Parameters.cs @@ -73,6 +73,17 @@ internal LeftValue SuperSplat { set { _superSplat = value; } } + // Set for a method whose keywords were lowered onto a trailing optional parameter after + // optional positional ones (`def m(a, b = 1, k: 2)' => `def m(a, b = 1, ?kw? = {})'). A + // keyword call that leaves b out passes its keyword hash where b is: before any default + // is evaluated, the hash is moved to the last parameter (TransformOptionalsInitialization). + private bool _keywordsAfterOptionals; + + internal bool KeywordsAfterOptionals { + get { return _keywordsAfterOptionals; } + set { _keywordsAfterOptionals = value; } + } + public LeftValue/*!*/[]/*!*/ Mandatory { get { return _mandatory; } } @@ -120,6 +131,55 @@ public Parameters(LeftValue/*!*/[] mandatory, int leadingMandatoryCount, SimpleA MSA.Expression singleton = gen.CurrentScope.DefineHiddenVariable("#default", typeof(object)); MSA.Expression result = AstUtils.Empty(); + + // The keyword hash in an optional positional: the arguments a call passes fill the + // parameters from the left, and the ones it leaves out get the default, so when the + // keyword parameter got the default the call's last argument is in the rightmost + // optional that did not. If that is the hash a keyword call made, it is the keywords: + // if kw == #default + // if b != #default then (kw, b = b, #default) if keyword_hash?(b) + // elsif a != #default then ... + MSA.Expression shift = null; + if (_keywordsAfterOptionals && _optional.Length > 1) { + var keywords = _optional[_optional.Length - 1].Left; + MSA.Expression chain = AstUtils.Empty(); + for (int i = 0; i < _optional.Length - 1; i++) { + var positional = _optional[i].Left; + chain = Ast.IfThenElse( + Ast.NotEqual(positional.TransformRead(gen), singleton), + Ast.IfThen( + Methods.IsKeywordArgumentsHash.OpCall(AstUtils.Box(positional.TransformRead(gen))), + Ast.Block( + keywords.TransformWrite(gen, positional.TransformRead(gen)), + positional.TransformWrite(gen, singleton), + AstUtils.Empty() + ) + ), + chain + ); + } + shift = Ast.IfThen(Ast.Equal(keywords.TransformRead(gen), singleton), chain); + } + // Once the keywords have been moved out of a positional, the parameters that got their + // default are no longer a suffix of the list, which the nesting below relies on: test + // each one in turn instead, still left to right. + if (shift != null) { + var defaults = new List(); + defaults.Add(Ast.Assign(singleton, Ast.Field(null, Fields.DefaultArgument))); + defaults.Add(shift); + for (int i = 0; i < _optional.Length; i++) { + defaults.Add(AstUtils.IfThen( + Ast.Equal(_optional[i].Left.TransformRead(gen), singleton), + Ast.Block( + _optional[i].Left.TransformWrite(gen, AstUtils.Constant(null, typeof(object))), + _optional[i].TransformRead(gen) // assignment + ) + )); + } + defaults.Add(AstUtils.Empty()); + return Ast.Block(defaults); + } + for (int i = 0; i < _optional.Length; i++) { result = AstUtils.IfThen( Ast.Equal(_optional[i].Left.TransformRead(gen), singleton), @@ -136,6 +196,7 @@ public Parameters(LeftValue/*!*/[] mandatory, int leadingMandatoryCount, SimpleA return Ast.Block( Ast.Assign(singleton, Ast.Field(null, Fields.DefaultArgument)), + shift ?? AstUtils.Empty(), result, AstUtils.Empty() ); diff --git a/Src/Ruby/Compiler/ReflectionCache.Generated.cs b/Src/Ruby/Compiler/ReflectionCache.Generated.cs index ebb1184fa..5384de9ae 100644 --- a/Src/Ruby/Compiler/ReflectionCache.Generated.cs +++ b/Src/Ruby/Compiler/ReflectionCache.Generated.cs @@ -532,6 +532,8 @@ public static partial class Methods { private static MethodInfo _MakeVirtualClassInstantiatedError; public static MethodInfo/*!*/ MakeWrongNumberOfArgumentsError { get { return _MakeWrongNumberOfArgumentsError ?? (_MakeWrongNumberOfArgumentsError = CallInstruction.CacheFunc(RubyOps.MakeWrongNumberOfArgumentsError)); } } private static MethodInfo _MakeWrongNumberOfArgumentsError; + public static MethodInfo/*!*/ MakeKeywordMethodArityError { get { return _MakeKeywordMethodArityError ?? (_MakeKeywordMethodArityError = CallInstruction.CacheFunc(RubyOps.MakeKeywordMethodArityError)); } } + private static MethodInfo _MakeKeywordMethodArityError; public static MethodInfo/*!*/ MakeWrongNumberOfArgumentsErrorN { get { return _MakeWrongNumberOfArgumentsErrorN ?? (_MakeWrongNumberOfArgumentsErrorN = CallInstruction.CacheFunc(RubyOps.MakeWrongNumberOfArgumentsErrorN)); } } private static MethodInfo _MakeWrongNumberOfArgumentsErrorN; public static MethodInfo/*!*/ MarkAliasCall { get { return _MarkAliasCall ?? (_MarkAliasCall = CallInstruction.CacheFunc(RubyOps.MarkAliasCall)); } } diff --git a/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs b/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs index fba68c711..94b26d23b 100644 --- a/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs +++ b/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs @@ -269,6 +269,22 @@ private MemberDispatcher GetDispatcher(Type/*!*/ delegateType, RubyCallSignature internal override void BuildCallNoFlow(MetaObjectBuilder/*!*/ metaBuilder, CallArguments/*!*/ args, string/*!*/ name) { Assert.NotNull(metaBuilder, args, name); + // A method with keyword parameters takes them in one more, hidden, optional parameter + // (PrismAstBridge.LowerKeywords), which ArgsBuilder would count as one more positional. + var signature = _body.Ast.Parameters.Signature; + if (signature != null && TakesOrRefusesKeywords && !args.Signature.HasSplattedArgument && !args.Signature.HasRhsArgument && + Parameters.Unsplat == null) { + int count = args.SimpleArgumentCount; + if (count < Parameters.Mandatory.Length || count > Parameters.Mandatory.Length + Parameters.Optional.Length) { + metaBuilder.SetError(Methods.MakeKeywordMethodArityError.OpCall( + AstUtils.Constant(signature, typeof(object)), + AstUtils.Constant(count), + (count > 0) ? AstUtils.Box(args.GetSimpleArgumentExpression(count - 1)) : AstUtils.Constant(null) + )); + return; + } + } + // 2 implicit args: self, block var argsBuilder = new ArgsBuilder(2, Parameters.Mandatory.Length, Parameters.LeadingMandatoryCount, Parameters.Optional.Length, Parameters.Unsplat != null); argsBuilder.SetImplicit(0, AstUtils.Box(args.TargetExpression)); diff --git a/Src/Ruby/Runtime/Jit/JitCompiler.cs b/Src/Ruby/Runtime/Jit/JitCompiler.cs index 9c403f262..c0a03f5b9 100644 --- a/Src/Ruby/Runtime/Jit/JitCompiler.cs +++ b/Src/Ruby/Runtime/Jit/JitCompiler.cs @@ -149,23 +149,25 @@ private JitCompiler(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, JT[]/* // one, then the optional ones, whose defaults the entry has already filled in (JitStub) - // or, for a method with keywords, the mandatory ones and then its keyword locals. var optional = ast.Parameters.Optional; - int expected = mandatory.Length + ((keywords != null) ? keywords.Count : (_supplied >= 0) ? _supplied : optional.Length); + // the positionals of a keyword method: the mandatory ones, then any optional ones before the keywords + int positional = mandatory.Length + ((keywords != null) ? keywords.OptionalPositionals : 0); + int expected = (keywords != null) ? positional + keywords.Count : mandatory.Length + ((_supplied >= 0) ? _supplied : optional.Length); _paramNames = new string[paramTypes.Length]; _paramExprs = new MSA.ParameterExpression[paramTypes.Length]; _resolvedTypes = (JT[])paramTypes.Clone(); if (paramTypes.Length != expected) { throw JitBailout.Instance; } for (int i = 0; i < paramTypes.Length; i++) { string name; - if (i < mandatory.Length || keywords == null) { + if (i < positional || keywords == null) { var lv = (i < mandatory.Length) ? mandatory[i] as LocalVariable : optional[i - mandatory.Length].Left as LocalVariable; if (lv == null) { throw JitBailout.Instance; } name = lv.Name; } else { - name = keywords.LocalNames[i - mandatory.Length]; + name = keywords.LocalNames[i - positional]; } if (_vars[0].ContainsKey(name)) { throw JitBailout.Instance; } _paramNames[i] = name; - if (keywords != null && i >= mandatory.Length && keywords.HasDefaultExpr(i - mandatory.Length)) { + if (keywords != null && i >= positional && keywords.HasDefaultExpr(i - positional)) { // the boxed value or RubyOps.DefaultArgument; the local is bound by EmitKeywordDefaults _paramExprs[i] = Ast.Parameter(typeof(object), "?" + name); continue; @@ -565,7 +567,7 @@ internal static JitCode TryCompile(MethodDeclaration/*!*/ ast, RubyContext/*!*/ private static JT[]/*!*/ LambdaParameterTypes(JT[]/*!*/ paramTypes, JitKeywords keywords, MethodDeclaration/*!*/ ast) { if (keywords == null || keywords.DefaultExprs == null) { return paramTypes; } var result = (JT[])paramTypes.Clone(); - int first = ast.Parameters.Mandatory.Length; + int first = ast.Parameters.Mandatory.Length + keywords.OptionalPositionals; for (int j = 0; j < keywords.Count && first + j < result.Length; j++) { if (keywords.HasDefaultExpr(j)) { result[first + j] = JT.Obj; } } @@ -645,7 +647,7 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { /// private List EmitKeywordDefaults() { if (_keywords.DefaultExprs == null) { return null; } - int first = _ast.Parameters.Mandatory.Length; + int first = _ast.Parameters.Mandatory.Length + _keywords.OptionalPositionals; var parts = new List(); var defaultArgument = Ast.Field(null, typeof(RubyOps).GetField("DefaultArgument")); for (int j = 0; j < _keywords.Count; j++) { diff --git a/Src/Ruby/Runtime/Jit/JitKeywords.cs b/Src/Ruby/Runtime/Jit/JitKeywords.cs index 11d3a4ab6..c8f2b95aa 100644 --- a/Src/Ruby/Runtime/Jit/JitKeywords.cs +++ b/Src/Ruby/Runtime/Jit/JitKeywords.cs @@ -67,14 +67,36 @@ internal bool HasDefaultExpr(int index) { internal int Count { get { return LocalNames.Length; } } + // The optional positional parameters before the keyword hash (Parameters.KeywordsAfterOptionals): + // `def m(a, b = 1, k: 2)' has one. Each has a literal default (OptionalDefaults); a call that + // leaves one out may have put its keyword hash there, which the entry moves into place first. + internal int OptionalPositionals; + internal object[] OptionalDefaults; + /// /// The keyword signature of a method whose parameters are mandatory positionals plus /// keywords lowered by LowerKeywords, or null if the method is anything else. /// internal static JitKeywords TryRecognize(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context) { var ps = ast.Parameters; - if (ps.Optional.Length != 1 || ps.Unsplat != null || ps.Block != null) { return null; } - var kwVar = ps.Optional[0].Left as LocalVariable; + if (ps.Unsplat != null || ps.Block != null) { return null; } + if (ps.Optional.Length != 1 && !(ps.KeywordsAfterOptionals && ps.Optional.Length > 1)) { return null; } + int optionalPositionals = ps.Optional.Length - 1; + object[] optionalDefaults = null; + if (optionalPositionals > 0) { + // only defaults the entry can fill in up front: nil, true, false, a Fixnum or a Float + optionalDefaults = new object[optionalPositionals]; + for (int k = 0; k < optionalPositionals; k++) { + var opt = ps.Optional[k]; + var literal = opt.Right as Literal; + object v = (literal != null) ? literal.Value : null; + if (!(opt.Left is LocalVariable) || literal == null || !(v == null || v is bool || v is int || v is double)) { + return null; + } + optionalDefaults[k] = v; + } + } + var kwVar = ps.Optional[optionalPositionals].Left as LocalVariable; if (kwVar == null || kwVar.Name != "?kw?") { return null; } var body = ast.Body; @@ -121,6 +143,8 @@ internal static JitKeywords TryRecognize(MethodDeclaration/*!*/ ast, RubyContext if (names.Length == 0 || names.Length > MaxKeywords) { return null; } var result = new JitKeywords(); + result.OptionalPositionals = optionalPositionals; + result.OptionalDefaults = optionalDefaults; result.RequiredCount = match.RequiredCountOrPosition; result.MatchNames = new RubySymbol[names.Length]; for (int j = 0; j < names.Length; j++) { @@ -187,6 +211,25 @@ internal static JitKeywords TryRecognize(MethodDeclaration/*!*/ ast, RubyContext return result; } + /// + /// What the generic body does first (Parameters.TransformOptionalsInitialization): a call that + /// passed no keyword hash where it belongs but left an optional positional out may have put it + /// in the last optional positional it did pass. holds the delegate's + /// positional parameters and then the keyword hash, at . + /// + internal static void ShiftKeywords(object[]/*!*/ args, int firstOptional, int keywordIndex) { + if (args[keywordIndex] != RubyOps.DefaultArgument) { return; } + for (int i = keywordIndex - 1; i >= firstOptional; i--) { + if (args[i] != RubyOps.DefaultArgument) { + if (RubyOps.IsKeywordArgumentsHash(args[i])) { + args[keywordIndex] = args[i]; + args[i] = RubyOps.DefaultArgument; + } + return; + } + } + } + private static bool IsVar(RExpr node, LocalVariable/*!*/ kwVar) { var lv = node as LocalVariable; return lv != null && lv.Name == kwVar.Name && lv.DefinitionLexicalDepth == kwVar.DefinitionLexicalDepth; diff --git a/Src/Ruby/Runtime/Jit/JitStub.cs b/Src/Ruby/Runtime/Jit/JitStub.cs index b511c6bdf..409e6bfbf 100644 --- a/Src/Ruby/Runtime/Jit/JitStub.cs +++ b/Src/Ruby/Runtime/Jit/JitStub.cs @@ -186,6 +186,9 @@ internal Specialization(JitStub/*!*/ stub) { protected void Profile(object self, object[] args) { if (_state != Profiling) { return; } + if (_keywords != null && _keywords.OptionalPositionals > 0) { + JitKeywords.ShiftKeywords(args, _positional - _keywords.OptionalPositionals, _positional); + } if (_keywords != null) { // what the prologue binds the keywords to, if it binds them at all object kw = args[_positional]; @@ -581,7 +584,7 @@ private bool TrySpecialize() { var spec = new Specialization(this); spec.ConstantVersion = constantVersion; spec.Entry = BuildEntry(code, callee, version, needsClassGuard ? _receiverClass : null, spec).Compile(); - if (_keywords != null && !needsClassGuard && _positional <= JitKeywordCall.MaxPositional && + if (_keywords != null && _keywords.OptionalPositionals == 0 && !needsClassGuard && _positional <= JitKeywordCall.MaxPositional && _keywords.Count <= JitKeywordCall.MaxKeywords) { spec.Direct = BuildDirectEntry(code, callee, version, spec).Compile(); } @@ -732,6 +735,22 @@ private bool SelfCallResolvesHere(RubyClass/*!*/ cls, RefinementActivation/*!*/ var values = new MSA.Expression[_typedCount]; var defaulted = new List(); var fillDefaults = new List(); + if (_keywords != null && _keywords.OptionalPositionals > 0) { + // a keyword hash that landed in an optional positional is moved to where it belongs, + // as the generic body would move it (JitKeywords.ShiftKeywords) - which it then finds + // already done, should the guards below send the call there + var defaultArgument = Ast.Field(null, typeof(RubyOps).GetField("DefaultArgument")); + MSA.Expression chain = Ast.Empty(); + for (int i = _positional - _keywords.OptionalPositionals; i < _positional; i++) { + chain = Ast.IfThenElse( + Ast.NotEqual(args[i], defaultArgument), + Ast.IfThen( + Ast.Call(typeof(RubyOps).GetMethod("IsKeywordArgumentsHash"), args[i]), + Ast.Block(Ast.Assign(args[_positional], args[i]), Ast.Assign(args[i], defaultArgument))), + chain); + } + fillDefaults.Add(Ast.IfThen(Ast.Equal(args[_positional], defaultArgument), chain)); + } for (int i = 0; i < _positional; i++) { if (_hasDefault != null && _hasDefault[i]) { var v = Ast.Variable(typeof(object), "v" + i); @@ -757,9 +776,11 @@ private bool SelfCallResolvesHere(RubyClass/*!*/ cls, RefinementActivation/*!*/ MSA.ParameterExpression[] keywordValues = null; if (_keywords != null) { MSA.Expression kw = args[_positional]; - // the separation check on the last positional: `m("a" => 1)' gave it the keywords - if (_positional > 0 && code.ParameterTypes[_positional - 1] == JT.Obj) { - test = And(test, Ast.Not(Ast.Call(typeof(RubyOps).GetMethod("IsKeywordArgumentsHash"), args[_positional - 1]))); + // the separation check on the last mandatory positional: `m("a" => 1)' gave it the + // keywords (an optional one cannot hold them any more, they were moved above) + int lastMandatory = _positional - _keywords.OptionalPositionals - 1; + if (lastMandatory >= 0 && code.ParameterTypes[lastMandatory] == JT.Obj) { + test = And(test, Ast.Not(Ast.Call(typeof(RubyOps).GetMethod("IsKeywordArgumentsHash"), args[lastMandatory]))); } keywordValues = new MSA.ParameterExpression[_keywords.Count]; for (int j = 0; j < keywordValues.Length; j++) { @@ -839,7 +860,7 @@ private bool SelfCallResolvesHere(RubyClass/*!*/ cls, RefinementActivation/*!*/ if (keywordValues != null) { body = Ast.Block(typeof(object), keywordValues, body); } - if (defaulted.Count > 0) { + if (fillDefaults.Count > 0) { fillDefaults.Add(body); body = Ast.Block(typeof(object), defaulted, fillDefaults); } @@ -1167,7 +1188,18 @@ private static bool Screen(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, hasDefault = null; defaults = null; } + } else if (keywords != null && keywords.OptionalPositionals > 0) { + // optional positionals before the keywords: the entry fills in their literal defaults + hasDefault = new bool[arity]; + defaults = new object[arity]; + for (int i = 0; i < keywords.OptionalPositionals; i++) { + hasDefault[mandatoryCount + i] = true; + defaults[mandatoryCount + i] = keywords.OptionalDefaults[i]; + } } + // A method whose keywords follow optional positionals is only taken as a keyword method: + // the entry has to move a misplaced keyword hash first, which only that kind does. + if (ps.KeywordsAfterOptionals && keywords == null) { return false; } // A body the compiler can never accept, whatever types it is called with, is not // wrapped at all. Once wrapped, the trampoline is what the runtime caches and bakes // into call site rules, so a method declined later still pays one extra indirect @@ -1178,10 +1210,12 @@ private static bool Screen(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, } // over the typed body's parameters: the positionals, then a keyword method's keyword locals - int typedCount = mandatoryCount + ((keywords != null) ? keywords.Count : ps.Optional.Length); + int positionalCount = mandatoryCount + ((keywords != null) ? keywords.OptionalPositionals : 0); + int typedCount = positionalCount + ((keywords != null) ? keywords.Count : ps.Optional.Length); for (int i = 0; i < typedCount; i++) { string name = (i < mandatoryCount) ? ((LocalVariable)ps.Mandatory[i]).Name - : (keywords != null) ? keywords.LocalNames[i - mandatoryCount] + : (i < positionalCount) ? ((LocalVariable)ps.Optional[i - mandatoryCount].Left).Name + : (keywords != null) ? keywords.LocalNames[i - positionalCount] : ((LocalVariable)ps.Optional[i - mandatoryCount].Left).Name; if (!otherUses.Contains(name)) { if (storedOnly == null) { storedOnly = new bool[typedCount]; } diff --git a/Src/Ruby/Runtime/RubyOps.cs b/Src/Ruby/Runtime/RubyOps.cs index db012fb08..9ec308baa 100644 --- a/Src/Ruby/Runtime/RubyOps.cs +++ b/Src/Ruby/Runtime/RubyOps.cs @@ -3368,6 +3368,26 @@ private static void TraceRescue(RubyScope/*!*/ scope, Exception exception) { return new ArgumentException(String.Format("wrong number of arguments (given {0}, expected {1})", actual, expected)); } + /// + /// The arity error of a call to a method with keyword parameters, worded from the declared + /// signature as MRI words it: the keyword hash a call made is not one of the arguments + /// given, the hidden parameter it arrives in is not one of those expected, and a required + /// keyword is named ("given 4, expected 1..3; required keyword: j"). + /// + [Emitted] + public static ArgumentException/*!*/ MakeKeywordMethodArityError(object/*!*/ signature, int count, object lastArgument) { + var parameters = (RubyParameterSignature)signature; + var keywords = lastArgument as Hash; + if (keywords != null && !keywords.IsKeywordArguments) { + keywords = null; + } + int positional = count - (keywords != null ? 1 : 0); + int min = parameters.MinPositionalCount, max = parameters.MaxPositionalCount; + var inv = System.Globalization.CultureInfo.InvariantCulture; + string expected = (min == max) ? min.ToString(inv) : min.ToString(inv) + (max < 0 ? "+" : ".." + max.ToString(inv)); + return MakeWrongNumberOfArgumentsErrorN(positional, expected + MissingRequiredKeywordsNote(parameters, keywords)); + } + /// /// Same, but with MRI's variable-arity spelling of the expected count: "1..3", "1+" or "2, 3, or 5". /// The description is a compile-time constant produced by the binder. diff --git a/Src/StdLib/ironruby/ruby4.rb b/Src/StdLib/ironruby/ruby4.rb index 67d024d6a..cb4bc851c 100644 --- a/Src/StdLib/ironruby/ruby4.rb +++ b/Src/StdLib/ironruby/ruby4.rb @@ -933,20 +933,13 @@ def __reverse_each_size__ end private :__reverse_each_size__ - # The built-in #first/#last only answer the no-argument form, and because they - # are defined on Range they hide Enumerable#first(n) rather than falling - # through to it - so `(0..Float::INFINITY).first(3)` was an ArgumentError. - unless method_defined?(:first_without_count) - alias :first_without_count :first - alias :last_without_count :last - - def first(*args) - if args.empty? - raise RangeError, "cannot get the first element of beginless range" if self.begin.nil? - return first_without_count - end + # #first(n) and #last(n): the C# #first and #last (RangeOps) answer the form without a + # count and hand the count here - an Enumerable#first(n) they would otherwise hide, so + # `(0..Float::INFINITY).first(3)` was an ArgumentError. + unless private_method_defined?(:__first_n__) + def __first_n__(count) raise RangeError, "cannot get the first element of beginless range" if self.begin.nil? - n = __to_count__(args[0]) + n = __to_count__(count) raise ArgumentError, "negative array size (or size too big)" if n < 0 b = self.begin e = self.end @@ -969,23 +962,20 @@ def first(*args) result end - def last(*args) - if args.empty? - raise RangeError, "cannot get the last element of endless range" if self.end.nil? - return last_without_count - end + def __last_n__(count) raise RangeError, "cannot get the last element of endless range" if self.end.nil? - n = __to_count__(args[0]) + n = __to_count__(count) raise ArgumentError, "negative array size" if n < 0 b = self.begin e = self.end # MRI's rb_int_range_last: the last n of an Integer range, without building all of it # first (to_a.last(n) made (1..10**6).last(3) a million-element Array). MRI takes this # path for a subclass too, whatever its #each. - if ::Integer === b && ::Integer === e + # (..5).last(2) too: a beginless Integer range counts down from its end. + if (b.nil? || ::Integer === b) && ::Integer === e top = exclude_end? ? e - 1 : e first_value = top - n + 1 - first_value = b if first_value < b + first_value = b if !b.nil? && first_value < b return __int_run__(first_value, top) end to_a.last(n) @@ -1004,6 +994,7 @@ def __int_run__(from, to) result end private :__int_run__ + private :__first_n__, :__last_n__ # #first and #last take a count, not an arbitrary object: anything that is # not an Integer has to offer #to_int and have it answer one. @@ -3147,130 +3138,102 @@ def __opts_exception__(value) private :__opts_exception__ module_function :__opts_exception__ - # Kernel#Integer and #Float: the C# side parses the string grammar (and, since - # this branch, an explicit radix); the arity, the exception: keyword and the - # coercion protocol are easier to get exactly right here. + # Kernel#Integer and #Float are C# (KernelOps.ToInteger, ToFloat): an Integer, a finite + # Float, a String with or without a radix, and the exception: keyword alone are answered + # there. Everything else comes here - the coercion protocol, the errors, exception: false + # around an object's own conversion - by way of these two, which the C# side calls with + # the base as given (nil when there was none) and the keywords as given. # # MRI's order for Integer(): an Integer or Float is converted directly, a # String is parsed, and anything else is asked for #to_int, then #to_i, with # #to_str consulted first when the object offers one. - if private_method_defined?(:Integer) || method_defined?(:Integer) - alias :__ir_Integer__ :Integer - private :__ir_Integer__ - - def Integer(arg, base = nil, exception: true) - # The common case - a String, no base, failures raised - is what the block below - # does first anyway; going straight there skips the block and its rescue frame, - # which cost many times the parse itself. - if base.nil? && true.equal?(exception) && !arg.nil? && arg.is_a?(::String) - return __ir_Integer__(arg) - end - # and an Integer is answered as it is, as the block below would - return arg if base.nil? && ::Integer === arg && true.equal?(exception) - # a finite Float is truncated, as below, without the block - return arg.to_i if base.nil? && ::Float === arg && true.equal?(exception) && arg.finite? - # exception: false on a String: the C# parser answers nil for what it cannot read, - # where raising and rescuing cost ~55 us a call - if false.equal?(exception) && ::String === arg - return base.nil? ? __ir_try_Integer__(arg) : __ir_try_Integer__(arg, base) - end - exception = ::Kernel.__opts_exception__(exception) - __numeric_conversion__(exception) do - # a base goes with a String, and that is checked before anything else - Integer(nil, 8) - # is this error, not the nil one - if base && !arg.is_a?(::String) && !arg.respond_to?(:to_str) - raise ::ArgumentError, "base specified for non string value" - end + def __ir_Integer_slow__(arg, base, exception: true) + exception = ::Kernel.__opts_exception__(exception) + # The radix is converted first, and outside the rescue: exception: false does not + # turn a radix that is not a number into nil (MRI's NUM2INT in rb_f_integer). + unless base.nil? || ::Integer === base + radix = base.respond_to?(:to_int) ? base.to_int : nil + unless ::Integer === radix + raise ::TypeError, "no implicit conversion of #{true.equal?(base) || false.equal?(base) ? base : base.class} into Integer" + end + base = radix + end + __numeric_conversion__(exception) do + # a base goes with a String, and that is checked before anything else - Integer(nil, 8) + # is this error, not the nil one + if base && !arg.is_a?(::String) && !arg.respond_to?(:to_str) + raise ::ArgumentError, "base specified for non string value" + end - if arg.nil? - raise ::TypeError, "can't convert nil into Integer" - end + if arg.nil? + raise ::TypeError, "can't convert nil into Integer" + end - if arg.is_a?(::String) - __ir_check_numeric_string__(arg) - next base ? __ir_Integer__(arg, base) : __ir_Integer__(arg) - end + if arg.is_a?(::String) + __ir_check_numeric_string__(arg) + next base ? __ir_Integer__(arg, base) : __ir_Integer__(arg) + end - if arg.is_a?(::Integer) - next arg - end + if arg.is_a?(::Integer) + next arg + end - if arg.is_a?(::Float) - if arg.nan? || arg.infinite? - raise ::FloatDomainError, arg.to_s - end - next arg.to_i + if arg.is_a?(::Float) + if arg.nan? || arg.infinite? + raise ::FloatDomainError, arg.to_s end + next arg.to_i + end - # #to_int first, then #to_str, then #to_i; a non-Integer from #to_int is - # not an error by itself, it just falls through to the next candidate. - if arg.respond_to?(:to_int) - value = arg.to_int - next value if value.is_a?(::Integer) - end + # #to_int first, then #to_str, then #to_i; a non-Integer from #to_int is + # not an error by itself, it just falls through to the next candidate. + if arg.respond_to?(:to_int) + value = arg.to_int + next value if value.is_a?(::Integer) + end - if arg.respond_to?(:to_str) - text = arg.to_str - unless text.is_a?(::String) - raise ::TypeError, "can't convert #{arg.class} into Integer" - end - __ir_check_numeric_string__(text) - next base ? __ir_Integer__(text, base) : __ir_Integer__(text) + if arg.respond_to?(:to_str) + text = arg.to_str + unless text.is_a?(::String) + raise ::TypeError, "can't convert #{arg.class} into Integer" end + __ir_check_numeric_string__(text) + next base ? __ir_Integer__(text, base) : __ir_Integer__(text) + end - unless arg.respond_to?(:to_i) - raise ::TypeError, "can't convert #{__conversion_class_name__(arg)} into Integer" - end + unless arg.respond_to?(:to_i) + raise ::TypeError, "can't convert #{__conversion_class_name__(arg)} into Integer" + end - value = arg.to_i - unless value.is_a?(::Integer) - raise ::TypeError, - "can't convert #{__conversion_class_name__(arg)} into Integer " \ - "(#{arg.class}#to_i gives #{__ir_conversion_result_name__(value)})" - end - value + value = arg.to_i + unless value.is_a?(::Integer) + raise ::TypeError, + "can't convert #{__conversion_class_name__(arg)} into Integer " \ + "(#{arg.class}#to_i gives #{__ir_conversion_result_name__(value)})" end + value end - module_function :Integer - end - - if private_method_defined?(:Float) || method_defined?(:Float) - alias :__ir_Float__ :Float - private :__ir_Float__ - - def Float(arg, exception: true) - # A String with failures raised goes to the C# parser below in any case; see Integer. - if true.equal?(exception) && !arg.nil? && ::String === arg - return __ir_Float__(arg) - end - # A Float is its own answer and an Integer is the C# conversion, as below, without the - # block, its rescue frame or the class tests. - if true.equal?(exception) - return arg if ::Float === arg - return __ir_Float__(arg) if ::Integer === arg - end - # exception: false on a String, as Integer does above - if false.equal?(exception) && ::String === arg - return __ir_try_Float__(arg) - end - exception = ::Kernel.__opts_exception__(exception) - __numeric_conversion__(exception) do - raise ::TypeError, "can't convert nil into Float" if arg.nil? - __ir_check_numeric_string__(arg, "Float") if ::String === arg - if ::Kernel === arg && !(::Integer === arg || ::Float === arg || ::Rational === arg || - ::String === arg || true.equal?(arg) || false.equal?(arg)) && arg.respond_to?(:to_f, true) - # MRI hands back the very Float #to_f returned (the same NaN, say), so the - # result must not go through the C# conversion, which reboxes it. - result = arg.__send__(:to_f) - return result if ::Float === result - name = arg.class.to_s - raise ::TypeError, "can't convert #{name} to Float (#{name}#to_f gives #{result.class})" - end - __ir_Float__(arg) + end + module_function :__ir_Integer_slow__ + + def __ir_Float_slow__(arg, exception: true) + exception = ::Kernel.__opts_exception__(exception) + __numeric_conversion__(exception) do + raise ::TypeError, "can't convert nil into Float" if arg.nil? + __ir_check_numeric_string__(arg, "Float") if ::String === arg + if ::Kernel === arg && !(::Integer === arg || ::Float === arg || ::Rational === arg || + ::String === arg || true.equal?(arg) || false.equal?(arg)) && arg.respond_to?(:to_f, true) + # MRI hands back the very Float #to_f returned (the same NaN, say), so the + # result must not go through the C# conversion, which reboxes it. + result = arg.__send__(:to_f) + return result if ::Float === result + name = arg.class.to_s + raise ::TypeError, "can't convert #{name} to Float (#{name}#to_f gives #{result.class})" end + __ir_Float__(arg) end - module_function :Float end + module_function :__ir_Float_slow__ # A String has to be readable as a number at all: ASCII-compatible, no NUL. def __ir_check_numeric_string__(text, what = "Integer") @@ -4409,9 +4372,7 @@ def step(*args, **kw, &block) unit = 1 if unit.nil? ::Kernel.raise(::ArgumentError, "step can't be 0") if unit == 0 if (to.nil? || to.is_a?(::Numeric)) && unit.is_a?(::Numeric) - shown = args.empty? && (kw.nil? || kw.empty?) ? "" : "(#{(args.map(&:inspect) + (kw || {}).map { |k, v| "#{k}: #{v.inspect}" }).join(', ')})" - return ::Enumerator::ArithmeticSequence.__build__( - self, to, unit, false, self, "(#{inspect}.step#{shown})") + return ::Enumerator::ArithmeticSequence.__build__(self, to, unit, false, self, [:num, args, kw]) end recv = self e = ::Enumerator.new { |y| recv.step(*args, **kw) { |v| y << v } } @@ -7375,21 +7336,8 @@ def self.__step_each__(from, to, unit, excl, &block) stop = fin.to_f step = unit.to_f n = __float_step_size__(beg, stop, step, excl) - if step.infinite? - block.call(beg) if n > 0 - elsif step == 0 - loop { block.call(beg) } - else - i = 0 - # n may be NaN (Infinity.step(Infinity, 1)); 0 < NaN is false, so - # nothing is yielded, which is what MRI's for(;i= 0 ? stop < d : d < stop - block.call(d) - i += 1 - end - end + # the loop is C# (KernelOps.FloatStep) + ::Kernel.__ir_float_step__(beg, stop, step, n.to_f, &block) return end @@ -7397,7 +7345,7 @@ def self.__step_each__(from, to, unit, excl, &block) cmp = desc ? :< : :> loop do break if excl ? (i == fin || i.__send__(cmp, fin)) : i.__send__(cmp, fin) - block.call(i) + yield(i) i += unit end end @@ -7492,9 +7440,27 @@ def hash [self.begin, self.end, step, exclude_end?].hash end + # @inspect_str is the text, or how the sequence was written - worked out only when asked + # for, since building it was a good part of what making a sequence cost def inspect - return @inspect_str if @inspect_str - "((#{@source.inspect}).%(#{@step.inspect}))" + form = @inspect_str + case form + when ::String then form + when :step then "((#{@source.inspect}).step)" + when ::Array + case form[0] + when :step then "((#{@source.inspect}).step(#{form[1].inspect}))" + when :% then "((#{@source.inspect}).%(#{form[1].inspect}))" + else + # Numeric#step: [:num, args, kw] + args = form[1] + kw = form[2] + shown = args.empty? && (kw.nil? || kw.empty?) ? "" : "(#{(args.map(&:inspect) + (kw || {}).map { |k, v| "#{k}: #{v.inspect}" }).join(', ')})" + "(#{@source.inspect}.step#{shown})" + end + else + "((#{@source.inspect}).%(#{@step.inspect}))" + end end alias :to_s :inspect end @@ -12797,8 +12763,7 @@ def step(*args, &block) if block ::Kernel.raise(::ArgumentError, "#step iteration for beginless ranges is meaningless") end - written = n.nil? ? "((#{inspect}).step)" : "((#{inspect}).step(#{n.inspect}))" - return ::Enumerator::ArithmeticSequence.__build__(b, e, unit, exclude_end?, self, written) + return ::Enumerator::ArithmeticSequence.__build__(b, e, unit, exclude_end?, self, n.nil? ? :step : [:step, n]) end ::Kernel.raise(::ArgumentError, "step can't be 0") if numeric && unit == 0 @@ -12806,8 +12771,7 @@ def step(*args, &block) unless block if numeric # inspected the way it was written: ((1..10).step) or ((1..10).step(2)) - written = n.nil? ? "((#{inspect}).step)" : "((#{inspect}).step(#{n.inspect}))" - return ::Enumerator::ArithmeticSequence.__build__(b, e, unit, exclude_end?, self, written) + return ::Enumerator::ArithmeticSequence.__build__(b, e, unit, exclude_end?, self, n.nil? ? :step : [:step, n]) end range = self return ::Enumerator.new { |y| range.step(*args) { |x| y << x } }.__set_inspect__(self, :step, args) @@ -12831,7 +12795,7 @@ def step(*args, &block) def %(n) seq = step(n) - seq.instance_variable_set(:@inspect_str, "((#{inspect}).%(#{n.inspect}))") if seq.is_a?(::Enumerator::ArithmeticSequence) + seq.instance_variable_set(:@inspect_str, [:%, n]) if seq.is_a?(::Enumerator::ArithmeticSequence) seq end