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