diff --git a/Src/IronRuby.Tests/RubyTests.cs b/Src/IronRuby.Tests/RubyTests.cs index f17f6c8d..be37b054 100644 --- a/Src/IronRuby.Tests/RubyTests.cs +++ b/Src/IronRuby.Tests/RubyTests.cs @@ -868,6 +868,8 @@ public Tests(Driver/*!*/ driver) { Io_BuiltinReaders1, Io_LineReaders2, Io_LineReaders3, + StringIO_LineReaders1, + Kernel_ConversionsInjectZip1, Method_LightFrame1, Method_LightFrame2, Hash_SelfKey1, diff --git a/Src/IronRuby.Tests/Runtime/MiscTests.cs b/Src/IronRuby.Tests/Runtime/MiscTests.cs index 71fe5e37..34ad997f 100644 --- a/Src/IronRuby.Tests/Runtime/MiscTests.cs +++ b/Src/IronRuby.Tests/Runtime/MiscTests.cs @@ -3147,6 +3147,188 @@ private static bool CanCreateSymbolicLinks() { } } + /// + /// 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, + /// and Float names a NUL. Array#inject over Fixnums, Array#minmax as [min, max], and + /// Enumerable#zip with an Integer Range worked out from the index. + /// + public void Kernel_ConversionsInjectZip1() { + TestOutput(@" +require 'ruby4' +def t(label) + r = begin; yield; rescue => e; [e.class, e.message]; end + puts ""#{label}: #{r.inspect}"" +end +['123', ' 0x1A ', '0b101', '1_000', '1__0', '', ' ', 'x', '12x', ""1\0"", '-0o17', '0d19', '08', '+5', 'ff'].each do |s| + t(""Integer #{s.inspect}"") { [Integer(s, exception: false), Integer(s, 16, exception: false), Float(s, exception: false)] } +end +['1.5', '1e3', '.5', '1_0.5', 'Infinity', 'NaN', '0x1p3', ' 2.5 ', '1e', ""1\0""].each do |s| + t(""Float #{s.inspect}"") { Float(s, exception: false) } + t(""Float! #{s.inspect}"") { Float(s) } +end +t('radix 99') { Integer('1', 99, exception: false) } +u = '12'.encode('UTF-16LE') +t('utf16') { [(Integer(u) rescue $!.class), (Integer(u, exception: false) rescue $!.class), (Float(u, exception: false) rescue $!.class)] } +t('nil') { Integer(nil, exception: false) } +[1.5, -2.9, 1e20, Float::NAN, Float::INFINITY].each { |f| t(""Integer(#{f})"") { [(Integer(f) rescue $!.class), Integer(f, exception: false)] } } +a = [1, 2, 3, 2**62, 2**62] +t('inject +') { [a.inject(:+), a.inject(10, :+), [].inject(:+), [].inject(5, :+), [1, 2.5, 3].inject(:+), ([1, 'a'].inject(:+) rescue $!.class)] } +t('inject sym') { [[2, 3, 4].inject(:*), [1, 2].inject('+'), %w[a b c].inject(:+), (1..4).inject(:+), [3].inject(:+)] } +t('inject block') { [[1, 2, 3].inject { |s, x| s * 10 + x }, [1, 2, 3].inject(100) { |s, x| s - x }, [1, 2, 3].inject { |s, x| break s * 7 if x == 3; s + x }] } +t('inject subclass') { Class.new(Array) { def each; yield 9; end }.new([1, 2]).inject(:+) } +t('minmax') { [[3, 1, 2].minmax, [].minmax, %w[b a].minmax, [1.5, 1, 2**70].minmax, [3, 1, 2].minmax { |x, y| y <=> x }, ([1, nil].minmax rescue $!.class)] } +t('zip') { [(1..5).zip(1..3), (1..5).zip(2...4), (1..4).zip(10..), (1..3).zip(5..1), (1..3).zip(2**64..2**64 + 1), (1..3).zip('a'..'b')] } +t('zip block') { r = []; [(1..3).zip(4..6) { |x| r << x }, r] } +t('zip subclass') { (1..2).zip(Class.new(Range) { def each; yield 42; end }.new(1, 5)) } +t('zip singleton') { r = (1..3).dup; def r.each; yield 42; end; (1..2).zip(r) } +t('zip readers') { [(r = (1..3).dup; def r.begin; 10; end; (0..1).zip(r)), (r = (1..3).dup; def r.end; 1; end; (0..2).zip(r)), (r = (1..3).dup; def r.exclude_end?; true; end; (0..3).zip(r))] } +", @" +Integer ""123"": [123, 291, 123.0] +Integer "" 0x1A "": [26, 26, 26.0] +Integer ""0b101"": [5, 45313, nil] +Integer ""1_000"": [1000, 4096, 1000.0] +Integer ""1__0"": [nil, nil, nil] +Integer """": [nil, nil, nil] +Integer "" "": [nil, nil, nil] +Integer ""x"": [nil, nil, nil] +Integer ""12x"": [nil, nil, nil] +Integer ""1\u0000"": [nil, nil, nil] +Integer ""-0o17"": [-15, nil, nil] +Integer ""0d19"": [19, 3353, nil] +Integer ""08"": [nil, 8, 8.0] +Integer ""+5"": [5, 5, 5.0] +Integer ""ff"": [nil, 255, nil] +Float ""1.5"": 1.5 +Float! ""1.5"": 1.5 +Float ""1e3"": 1000.0 +Float! ""1e3"": 1000.0 +Float "".5"": 0.5 +Float! "".5"": 0.5 +Float ""1_0.5"": 10.5 +Float! ""1_0.5"": 10.5 +Float ""Infinity"": nil +Float! ""Infinity"": [ArgumentError, ""invalid value for Float(): \""Infinity\""""] +Float ""NaN"": nil +Float! ""NaN"": [ArgumentError, ""invalid value for Float(): \""NaN\""""] +Float ""0x1p3"": 8.0 +Float! ""0x1p3"": 8.0 +Float "" 2.5 "": 2.5 +Float! "" 2.5 "": 2.5 +Float ""1e"": nil +Float! ""1e"": [ArgumentError, ""invalid value for Float(): \""1e\""""] +Float ""1\u0000"": nil +Float! ""1\u0000"": [ArgumentError, ""string for Float contains null byte""] +radix 99: [ArgumentError, ""invalid radix 99""] +utf16: [Encoding::CompatibilityError, Encoding::CompatibilityError, Encoding::CompatibilityError] +nil: nil +Integer(1.5): [1, 1] +Integer(-2.9): [-2, -2] +Integer(1.0e+20): [100000000000000000000, 100000000000000000000] +Integer(NaN): [FloatDomainError, nil] +Integer(Infinity): [FloatDomainError, nil] +inject +: [9223372036854775814, 9223372036854775824, nil, 5, 6.5, TypeError] +inject sym: [24, 3, ""abc"", 10, 3] +inject block: [123, 94, 21] +inject subclass: 9 +minmax: [[1, 3], [nil, nil], [""a"", ""b""], [1, 1180591620717411303424], [3, 1], ArgumentError] +zip: [[[1, 1], [2, 2], [3, 3], [4, nil], [5, nil]], [[1, 2], [2, 3], [3, nil], [4, nil], [5, nil]], [[1, 10], [2, 11], [3, 12], [4, 13]], [[1, nil], [2, nil], [3, nil]], [[1, 18446744073709551616], [2, 18446744073709551617], [3, nil]], [[1, ""a""], [2, ""b""], [3, nil]]] +zip block: [nil, [[1, 4], [2, 5], [3, 6]]] +zip subclass: [[1, 42], [2, nil]] +zip singleton: [[1, 42], [2, nil]] +zip readers: [[[0, 1], [1, 2]], [[0, 1], [1, 2], [2, 3]], [[0, 1], [1, 2], [2, 3], [3, nil]]] +"); + } + + /// + /// StringIO's line readers after the bare gets got an overload of its own, a one-byte + /// separator is looked for as a byte, and chomp: cuts the line where it is; lineno counts + /// the lines read and not the nil at the end. Also the byte side of a fresh literal, which + /// learns it is ASCII rather than being switched to bytes to find out. + /// + public void StringIO_LineReaders1() { + TestOutput(@" +require 'ruby4' +require 'stringio' +def t(label) + r = begin; yield; rescue => e; [e.class, e.message]; end + puts ""#{label}: #{r.inspect}"" +end +src = ""one\r\ntwo\n\n\nthréeéx\nlast"" +[[], [""\n""], [""e""], [""é""], [""éx""], [""""], [nil], [3], [""o"", 2], [""\r\n""]].each do |a| + t(""gets #{a.inspect}"") { s = StringIO.new(src); r = []; while l = s.gets(*a); r << l; end; [r, $_, s.lineno] } + t(""gets chomp #{a.inspect}"") { s = StringIO.new(src); r = []; while l = s.gets(*a, chomp: true); r << l; end; r } + t(""readlines chomp #{a.inspect}"") { StringIO.new(src).readlines(*a, chomp: true) } + t(""each_line chomp #{a.inspect}"") { StringIO.new(src).each_line(*a, chomp: true).to_a } +end +t(""binary sep"") { StringIO.new(""a\xFFb\xFFc"".b).readlines(""\xFF"".b, chomp: true) } +t(""gets bare"") { s = StringIO.new(""x\ny""); [s.gets, s.gets, s.gets, $_] } +t(""frozen line chomp"") { s = StringIO.new(""ab\n"".freeze); l = s.gets(chomp: true); [l, l.frozen?, s.string] } +t(""chomp keeps source"") { str = +""a\nb\n""; s = StringIO.new(str); s.readlines(chomp: true); str } +# literal byte access +[""hello"", ""héllo"", """", ""\u{1F600}x""].each do |lit| + t(""bytes #{lit.inspect}"") { x = lit.dup; [x.bytesize, x.getbyte(0), x.getbyte(1), x.bytes, x.size, x.ascii_only?, x.b, x.byteslice(1, 2)] } +end +t(""iso literal"") { x = ""caf\xE9"".force_encoding(""iso-8859-1""); [x.bytesize, x.getbyte(3), x.bytes] } +t(""utf16"") { x = ""ab"".encode(""UTF-16LE""); [x.bytesize, x.getbyte(0), x.getbyte(1), x.bytes] } +t(""io write literal"") { s = StringIO.new(+""""); s.write(""abc""); s.write(""dé""); [s.string, s.string.bytesize, s.pos] } +t(""string << then bytes"") { x = +""ab""; x << ""c""; [x.bytesize, x.getbyte(2), x.bytes] } +", @" +gets []: [[""one\r\n"", ""two\n"", ""\n"", ""\n"", ""thréeéx\n"", ""last""], nil, 6] +gets chomp []: [""one"", ""two"", """", """", ""thréeéx"", ""last""] +readlines chomp []: [""one"", ""two"", """", """", ""thréeéx"", ""last""] +each_line chomp []: [""one"", ""two"", """", """", ""thréeéx"", ""last""] +gets [""\n""]: [[""one\r\n"", ""two\n"", ""\n"", ""\n"", ""thréeéx\n"", ""last""], nil, 6] +gets chomp [""\n""]: [""one"", ""two"", """", """", ""thréeéx"", ""last""] +readlines chomp [""\n""]: [""one"", ""two"", """", """", ""thréeéx"", ""last""] +each_line chomp [""\n""]: [""one"", ""two"", """", """", ""thréeéx"", ""last""] +gets [""e""]: [[""one"", ""\r\ntwo\n\n\nthrée"", ""éx\nlast""], nil, 3] +gets chomp [""e""]: [""on"", ""\r\ntwo\n\n\nthré"", ""éx\nlast""] +readlines chomp [""e""]: [""on"", ""\r\ntwo\n\n\nthré"", ""éx\nlast""] +each_line chomp [""e""]: [""on"", ""\r\ntwo\n\n\nthré"", ""éx\nlast""] +gets [""é""]: [[""one\r\ntwo\n\n\nthré"", ""eé"", ""x\nlast""], nil, 3] +gets chomp [""é""]: [""one\r\ntwo\n\n\nthr"", ""e"", ""x\nlast""] +readlines chomp [""é""]: [""one\r\ntwo\n\n\nthr"", ""e"", ""x\nlast""] +each_line chomp [""é""]: [""one\r\ntwo\n\n\nthr"", ""e"", ""x\nlast""] +gets [""éx""]: [[""one\r\ntwo\n\n\nthréeéx"", ""\nlast""], nil, 2] +gets chomp [""éx""]: [""one\r\ntwo\n\n\nthrée"", ""\nlast""] +readlines chomp [""éx""]: [""one\r\ntwo\n\n\nthrée"", ""\nlast""] +each_line chomp [""éx""]: [""one\r\ntwo\n\n\nthrée"", ""\nlast""] +gets [""""]: [[""one\r\ntwo\n\n\n"", ""thréeéx\nlast""], nil, 2] +gets chomp [""""]: [""one\r\ntwo"", ""thréeéx\nlast""] +readlines chomp [""""]: [""one\r\ntwo"", ""thréeéx\nlast""] +each_line chomp [""""]: [""one\r\ntwo"", ""thréeéx\nlast""] +gets [nil]: [[""one\r\ntwo\n\n\nthréeéx\nlast""], nil, 1] +gets chomp [nil]: [""one\r\ntwo\n\n\nthréeéx\nlast""] +readlines chomp [nil]: [""one\r\ntwo\n\n\nthréeéx\nlast""] +each_line chomp [nil]: [""one\r\ntwo\n\n\nthréeéx\nlast""] +gets [3]: [[""one"", ""\r\n"", ""two"", ""\n"", ""\n"", ""\n"", ""thr"", ""ée"", ""éx"", ""\n"", ""las"", ""t""], nil, 12] +gets chomp [3]: [""one"", """", ""two"", """", """", """", ""thr"", ""ée"", ""éx"", """", ""las"", ""t""] +readlines chomp [3]: [""one"", """", ""two"", """", """", """", ""thr"", ""ée"", ""éx"", """", ""las"", ""t""] +each_line chomp [3]: [""one"", """", ""two"", """", """", """", ""thr"", ""ée"", ""éx"", """", ""las"", ""t""] +gets [""o"", 2]: [[""o"", ""ne"", ""\r\n"", ""tw"", ""o"", ""\n\n"", ""\nt"", ""hr"", ""é"", ""eé"", ""x\n"", ""la"", ""st""], nil, 13] +gets chomp [""o"", 2]: ["""", ""ne"", ""\r\n"", ""tw"", """", ""\n\n"", ""\nt"", ""hr"", ""é"", ""eé"", ""x\n"", ""la"", ""st""] +readlines chomp [""o"", 2]: ["""", ""ne"", ""\r\n"", ""tw"", """", ""\n\n"", ""\nt"", ""hr"", ""é"", ""eé"", ""x\n"", ""la"", ""st""] +each_line chomp [""o"", 2]: ["""", ""ne"", ""\r\n"", ""tw"", """", ""\n\n"", ""\nt"", ""hr"", ""é"", ""eé"", ""x\n"", ""la"", ""st""] +gets [""\r\n""]: [[""one\r\n"", ""two\n\n\nthréeéx\nlast""], nil, 2] +gets chomp [""\r\n""]: [""one"", ""two\n\n\nthréeéx\nlast""] +readlines chomp [""\r\n""]: [""one"", ""two\n\n\nthréeéx\nlast""] +each_line chomp [""\r\n""]: [""one"", ""two\n\n\nthréeéx\nlast""] +binary sep: [""a"", ""b"", ""c""] +gets bare: [""x\n"", ""y"", nil, nil] +frozen line chomp: [""ab"", false, ""ab\n""] +chomp keeps source: ""a\nb\n"" +bytes ""hello"": [5, 104, 101, [104, 101, 108, 108, 111], 5, true, ""hello"", ""el""] +bytes ""héllo"": [6, 104, 195, [104, 195, 169, 108, 108, 111], 5, false, ""h\xC3\xA9llo"", ""é""] +bytes """": [0, nil, nil, [], 0, true, """", nil] +bytes ""😀x"": [5, 240, 159, [240, 159, 152, 128, 120], 2, false, ""\xF0\x9F\x98\x80x"", ""\x9F\x98""] +iso literal: [4, 233, [99, 97, 102, 233]] +utf16: [4, 97, 0, [97, 0, 98, 0]] +io write literal: [""abcdé"", 6, 6] +string << then bytes: [3, 99, [97, 98, 99]] +"); + } + /// /// Text mode's universal newline decorator - "\r\n" and a lone "\r" are folded before the /// line is split - and the newline options that are decorators for the way out only diff --git a/Src/Libraries/Builtins/Enumerable.cs b/Src/Libraries/Builtins/Enumerable.cs index 2f792366..b63492d7 100644 --- a/Src/Libraries/Builtins/Enumerable.cs +++ b/Src/Libraries/Builtins/Enumerable.cs @@ -591,6 +591,11 @@ internal static object Inject(CallSiteStorage/*!*/ each, BlockParam op operatorName, new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) ); + var list = self as RubyArray; + if (list != null && IsPlainList(each.Context, self)) { + return InjectList(list, operation, scope, operatorName, site, initial); + } + object result = initial; Each(each, self, Proc.Create(each.Context, 0, delegate(BlockParam/*!*/ selfBlock, object _, object[] __, RubyArray/*!*/ args) { Debug.Assert(__.Length == 0); @@ -616,6 +621,42 @@ internal static object Inject(CallSiteStorage/*!*/ each, BlockParam op return result != Missing.Value ? result : null; } + /// + /// #inject over a plain list (see IsPlainList): the elements are taken from the list, + /// with no #each call, Proc or argument array for each one. inject(:+) over Fixnums adds + /// them here while Integer#+ is the built-in, as MRI's ary_inject_op does, and goes back to + /// calling #+ at the first element that is not a Fixnum. + /// + private static object InjectList(RubyArray/*!*/ list, BlockParam operation, RubyScope scope, string operatorName, + CallSite> site, object initial) { + + object result = initial; + int i = 0; + if (result == Missing.Value) { + if (list.Count == 0) { + return null; + } + result = list[0]; + i = 1; + } + + if (site != null && operatorName == "+" && IListOps.IsBuiltinMethod(scope.RubyContext.IntegerClass, "+")) { + for (; i < list.Count && result is int && list[i] is int; i++) { + result = ClrInteger.Add((int)result, (int)list[i]); + } + } + + for (; i < list.Count; i++) { + object value = list[i]; + if (site != null) { + result = site.Target(site, scope, result, value); + } else if (operation.Yield(result, value, out result)) { + return result; + } + } + return result; + } + #endregion #region min, max, minmax diff --git a/Src/Libraries/Builtins/KernelOps.cs b/Src/Libraries/Builtins/KernelOps.cs index e75823d7..b0bada6c 100644 --- a/Src/Libraries/Builtins/KernelOps.cs +++ b/Src/Libraries/Builtins/KernelOps.cs @@ -129,6 +129,25 @@ public static object ToFloat(ConversionStorage/*!*/ floatConversion, obj return Protocols.CastToFloat(floatConversion, value); } + // 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)] + public static object ToFloat(RubyContext/*!*/ context, object self, [NotNull]MutableString/*!*/ value) { + RequireAsciiCompatible(value); + if (value.IndexOf('\0') >= 0) { + throw RubyExceptions.CreateArgumentError("string for Float contains null byte"); + } + return RubyOps.ConvertMutableStringToFloat(context, value); + } + + private static void RequireAsciiCompatible(MutableString/*!*/ value) { + if (!value.Encoding.IsAsciiIdentity) { + throw new EncodingCompatibilityError("ASCII incompatible encoding: " + value.Encoding.Name); + } + } + [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] public static object/*!*/ ToInteger(object self, [NotNull]MutableString/*!*/ value) { @@ -138,6 +157,7 @@ public static object ToFloat(ConversionStorage/*!*/ floatConversion, obj [RubyMethod("Integer", RubyMethodAttributes.PrivateInstance)] [RubyMethod("Integer", RubyMethodAttributes.PublicSingleton)] public static object/*!*/ ToInteger(object self, [NotNull]MutableString/*!*/ value, [DefaultProtocol]int radix) { + RequireAsciiCompatible(value); var str = value.ConvertToString(); // A negative radix means "this is only a hint, a prefix wins"; zero and the @@ -162,6 +182,40 @@ public static object ToFloat(ConversionStorage/*!*/ floatConversion, obj throw InvalidIntegerValue(str); } + // Kernel#Integer and #Float with exception: false (ruby4.rb): a String that does not read + // as a number answers nil here, rather than raising an ArgumentError for the prelude to + // rescue - which cost ~55 us a failure. An invalid radix still raises, as it does in MRI + // whatever exception: says. + + [RubyMethod("__ir_try_Integer__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_try_Integer__", RubyMethodAttributes.PublicSingleton)] + public static object TryToInteger(object self, [NotNull]MutableString/*!*/ value) { + return TryToInteger(self, value, 0); + } + + [RubyMethod("__ir_try_Integer__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_try_Integer__", RubyMethodAttributes.PublicSingleton)] + public static object TryToInteger(object self, [NotNull]MutableString/*!*/ value, [DefaultProtocol]int radix) { + RequireAsciiCompatible(value); + var str = value.ConvertToString(); + if (!HasIntegerDigits(str)) { + return null; + } + if (radix > 0 && (radix < 2 || radix > 36)) { + throw RubyExceptions.CreateArgumentError("invalid radix {0}", radix); + } + object result; + return TryParseRubyInteger(str, radix, out result) ? result : null; + } + + [RubyMethod("__ir_try_Float__", RubyMethodAttributes.PrivateInstance)] + [RubyMethod("__ir_try_Float__", RubyMethodAttributes.PublicSingleton)] + public static object TryToFloat(object self, [NotNull]MutableString/*!*/ value) { + RequireAsciiCompatible(value); + double result; + return RubyOps.TryParseRubyFloat(value.ConvertToString(), out result) ? (object)result : null; + } + #region Kernel#Integer string grammar // Kernel#Integer is stricter than String#to_i: it consumes the whole string or fails. diff --git a/Src/Libraries/Extensions/IListOps.cs b/Src/Libraries/Extensions/IListOps.cs index b50f101c..4f31d754 100644 --- a/Src/Libraries/Extensions/IListOps.cs +++ b/Src/Libraries/Extensions/IListOps.cs @@ -2771,6 +2771,22 @@ public static object GetMaximum(ConversionStorage/*!*/ fixnumCast, Comparis return GetExtremes(fixnumCast, comparisonStorage, comparer, self, count, +1); } + /// + /// Array#minmax is [min, max] - MRI's ary_minmax calls the two, so a custom <=> sees + /// the same comparisons - and gets their Fixnum fast paths. With a block it is + /// Enumerable#minmax, as MRI hands it to super. + /// + [RubyMethod("minmax")] + public static object/*!*/ GetMinMax(CallSiteStorage>/*!*/ each, + ComparisonStorage/*!*/ comparisonStorage, BlockParam comparer, IList/*!*/ self) { + if (comparer != null) { + return Enumerable.GetExtremes(each, comparisonStorage, comparer, self); + } + object min = GetExtreme(comparisonStorage, null, self, -1); + object max = GetExtreme(comparisonStorage, null, self, +1); + return new RubyArray(2) { min, max }; + } + private static bool IsBuiltinIntegerComparison(RubyContext/*!*/ context) { return IsBuiltinMethod(context.IntegerClass, "<=>"); } diff --git a/Src/Libraries/Initializers.Generated.cs b/Src/Libraries/Initializers.Generated.cs index f1fe1128..fe84aab7 100644 --- a/Src/Libraries/Initializers.Generated.cs +++ b/Src/Libraries/Initializers.Generated.cs @@ -4763,6 +4763,17 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module new Func(IronRuby.Builtins.KernelOps.SetHiddenInstanceVariable) ); + DefineLibraryMethod(module, "__ir_try_Float__", 0x52, + 0x00000002U, + new Func(IronRuby.Builtins.KernelOps.TryToFloat) + ); + + DefineLibraryMethod(module, "__ir_try_Integer__", 0x52, + 0x00000002U, 0x00020002U, + new Func(IronRuby.Builtins.KernelOps.TryToInteger), + new Func(IronRuby.Builtins.KernelOps.TryToInteger) + ); + DefineLibraryMethod(module, "__method__", 0x52, 0x00000000U, new Func(IronRuby.Builtins.KernelOps.GetCurrentMethodName) @@ -4918,8 +4929,9 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module ); DefineLibraryMethod(module, "Float", 0x52, - 0x00000000U, - new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat) + 0x00000000U, 0x00000004U, + new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func(IronRuby.Builtins.KernelOps.ToFloat) ); DefineLibraryMethod(module, "fork", 0x52, @@ -5313,6 +5325,17 @@ private static void LoadKernel_Class(IronRuby.Builtins.RubyModule/*!*/ module) { new Func(IronRuby.Builtins.KernelOps.GetCurrentCalleeName) ); + DefineLibraryMethod(module, "__ir_try_Float__", 0x61, + 0x00000002U, + new Func(IronRuby.Builtins.KernelOps.TryToFloat) + ); + + DefineLibraryMethod(module, "__ir_try_Integer__", 0x61, + 0x00000002U, 0x00020002U, + new Func(IronRuby.Builtins.KernelOps.TryToInteger), + new Func(IronRuby.Builtins.KernelOps.TryToInteger) + ); + DefineLibraryMethod(module, "__method__", 0x61, 0x00000000U, new Func(IronRuby.Builtins.KernelOps.GetCurrentMethodName) @@ -5394,8 +5417,9 @@ private static void LoadKernel_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "Float", 0x61, - 0x00000000U, - new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat) + 0x00000000U, 0x00000004U, + new Func, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.ToFloat), + new Func(IronRuby.Builtins.KernelOps.ToFloat) ); DefineLibraryMethod(module, "fork", 0x61, @@ -9524,6 +9548,11 @@ private static void LoadSystem__Collections__IList_Instance(IronRuby.Builtins.Ru new Func, IronRuby.Runtime.ComparisonStorage, IronRuby.Runtime.BlockParam, System.Collections.IList, System.Object, System.Object>(IronRuby.Builtins.IListOps.GetMinimum) ); + DefineLibraryMethod(module, "minmax", 0x51, + 0x00000000U, + new Func>, IronRuby.Runtime.ComparisonStorage, IronRuby.Runtime.BlockParam, System.Collections.IList, System.Object>(IronRuby.Builtins.IListOps.GetMinMax) + ); + DefineLibraryMethod(module, "nitems", 0x51, 0x00000000U, new Func(IronRuby.Builtins.IListOps.NumberOfNonNilItems) @@ -13471,7 +13500,8 @@ private static void LoadStringIO_Instance(IronRuby.Builtins.RubyModule/*!*/ modu ); DefineLibraryMethod(module, "gets", 0x11, - 0x00400000U, + 0x00000000U, 0x00400000U, + new Func(IronRuby.StandardLibrary.StringIO.StringIO.Gets), new Func>, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.RubyScope, IronRuby.StandardLibrary.StringIO.StringIO, System.Object, System.Object, System.Collections.Generic.IDictionary, IronRuby.Builtins.MutableString>(IronRuby.StandardLibrary.StringIO.StringIO.Gets) ); diff --git a/Src/Libraries/StringIO/StringIO.cs b/Src/Libraries/StringIO/StringIO.cs index e418315b..bd53aa8b 100644 --- a/Src/Libraries/StringIO/StringIO.cs +++ b/Src/Libraries/StringIO/StringIO.cs @@ -1010,9 +1010,17 @@ private static MutableString Chomp(MutableString line, MutableString separator, } int separatorLength = separator.GetByteCount(); - if (separatorLength == 0 || length < separatorLength || !line.EndsWith(separator)) { + if (separatorLength == 0 || length < separatorLength) { return line; } + // The separator is compared as bytes - the line is a slice of the content and its + // separator was found as bytes - and the line, which nothing else holds yet, is cut + // where it is rather than sliced into another string. + for (int i = 0; i < separatorLength; i++) { + if (line.GetByte(length - separatorLength + i) != separator.GetByte(i)) { + return line; + } + } int cut = length - separatorLength; // A line ended by "\n" gives up the "\r" in front of it too - MRI chomps the pair - @@ -1020,22 +1028,17 @@ private static MutableString Chomp(MutableString line, MutableString separator, if (separatorLength == 1 && separator.GetByte(0) == (byte)'\n' && cut > 0 && line.GetByte(cut - 1) == (byte)'\r') { cut--; } - return line.GetSlice(0, cut); + line.SetByteCount(cut); + return line; } + // The bare call - nearly every gets - binds without the optional arguments' conversions. [RubyMethod("gets")] - public static MutableString Gets(ConversionStorage>/*!*/ toHash, - ConversionStorage/*!*/ toStr, ConversionStorage/*!*/ toInt, - RubyScope/*!*/ scope, StringIO/*!*/ self, - [Optional]object separatorOrLimit, [Optional]object limitOrOptions, - [DefaultParameterValue(null), DefaultProtocol]IDictionary options) { - - MutableString separator; - int limit; - bool chomp; - ParseLineArguments(toHash, toStr, toInt, scope.RubyContext, separatorOrLimit, limitOrOptions, options, - out separator, out limit, out chomp); + public static MutableString Gets(RubyScope/*!*/ scope, StringIO/*!*/ self) { + return Gets(scope, self, scope.RubyContext.InputSeparator, -1, false); + } + private static MutableString Gets(RubyScope/*!*/ scope, StringIO/*!*/ self, MutableString separator, int limit, bool chomp) { var content = self.GetReadableContent(); int position = self._position; @@ -1045,6 +1048,8 @@ public static MutableString Gets(ConversionStorage>/ // $_ is frame local, and the frame it belongs in is the caller's - which is why this // is not wrapped in Ruby the way the chomp: option once was. scope.GetInnerMostClosureScope().LastInputLine = result; + // Every read that answers a line counts, cut short by a limit or not - but the nil at + // the end is not a line, and neither is the "" of gets(0) (MRI's strio_gets). if (result != null && limit != 0) { self._lineNumber++; } @@ -1052,6 +1057,21 @@ public static MutableString Gets(ConversionStorage>/ return result; } + [RubyMethod("gets")] + public static MutableString Gets(ConversionStorage>/*!*/ toHash, + ConversionStorage/*!*/ toStr, ConversionStorage/*!*/ toInt, + RubyScope/*!*/ scope, StringIO/*!*/ self, + [Optional]object separatorOrLimit, [Optional]object limitOrOptions, + [DefaultParameterValue(null), DefaultProtocol]IDictionary options) { + + MutableString separator; + int limit; + bool chomp; + ParseLineArguments(toHash, toStr, toInt, scope.RubyContext, separatorOrLimit, limitOrOptions, options, + out separator, out limit, out chomp); + return Gets(scope, self, separator, limit, chomp); + } + [RubyMethod("readline")] public static MutableString/*!*/ ReadLine(ConversionStorage>/*!*/ toHash, ConversionStorage/*!*/ toStr, ConversionStorage/*!*/ toInt, @@ -1138,6 +1158,10 @@ private static MutableString ReadLine(MutableString/*!*/ content, MutableString position++; } } + } else if (separator.GetByteCount() == 1) { + // the usual "\n": one byte to look for, and no copy of the separator to look with + position = content.IndexOf(separator.GetByte(0), oldPosition); + position = (position != -1) ? position + 1 : length; } else { // Positions are byte offsets, so the separator is looked for as bytes too: searching // for it as a string switched a non-ASCII content to characters, giving a character diff --git a/Src/Ruby/Builtins/MutableString.StringContent.cs b/Src/Ruby/Builtins/MutableString.StringContent.cs index ec268a23..8f63435a 100644 --- a/Src/Ruby/Builtins/MutableString.StringContent.cs +++ b/Src/Ruby/Builtins/MutableString.StringContent.cs @@ -55,15 +55,35 @@ internal string/*!*/ Data { return SwitchToMutable(); } + // ASCII text in an ASCII-compatible encoding is its own bytes. Narrowing it is one + // vectorized pass; the escaping encoder, which has to be ready for characters that are + // not valid, costs a few hundred nanoseconds even on a short literal - and a literal + // written to an IO, or asked for its bytesize, is switched to bytes this way first. + private bool IsAsciiData() { + return _owner._encoding.IsAsciiIdentity && _owner.IsAscii(); + } + internal byte[]/*!*/ DataToBytes() { - return _data.Length > 0 ? _owner._encoding.EscapingEncoding.GetBytes(_data) : Utils.EmptyBytes; + if (_data.Length == 0) { + return Utils.EmptyBytes; + } + if (IsAsciiData()) { + var bytes = new byte[_data.Length]; + System.Text.Ascii.FromUtf16(_data, bytes, out _); + return bytes; + } + return _owner._encoding.EscapingEncoding.GetBytes(_data); } internal int GetDataByteCount() { - return _owner._encoding.EscapingEncoding.GetByteCount(_data); + return IsAsciiData() ? _data.Length : _owner._encoding.EscapingEncoding.GetByteCount(_data); } internal void GetDataBytes(byte[]/*!*/ bytes, int start) { + if (IsAsciiData()) { + System.Text.Ascii.FromUtf16(_data, new Span(bytes, start, _data.Length), out _); + return; + } _owner._encoding.EscapingEncoding.GetBytes(_data, 0, _data.Length, bytes, start); } @@ -100,7 +120,9 @@ public override int GetCharacterCount() { } public override int GetByteCount() { - if (_data.Length == 0 || _owner.HasSingleByteCharacters && !_owner.HasSurrogates()) { + // A fresh string has not been looked at yet; finding out that it is ASCII (or in a + // single-byte encoding) is cheaper than switching it to bytes to count them. + if (_data.Length == 0 || _owner.DetectSingleByteCharacters() && !_owner.HasSurrogates()) { return _data.Length; } return SwitchToBinary().GetByteCount(); @@ -217,7 +239,7 @@ public override char GetChar(int index) { } public override byte GetByte(int index) { - if (index == 0 || _owner.HasSingleByteCharacters && !_owner.HasSurrogates()) { + if (index == 0 || _owner.DetectSingleByteCharacters() && !_owner.HasSurrogates()) { if (index >= _data.Length) { throw new IndexOutOfRangeException(); } diff --git a/Src/Ruby/Runtime/RubyOps.cs b/Src/Ruby/Runtime/RubyOps.cs index 7316a11b..db012fb0 100644 --- a/Src/Ruby/Runtime/RubyOps.cs +++ b/Src/Ruby/Runtime/RubyOps.cs @@ -4111,7 +4111,7 @@ private static bool ScanExponent(string/*!*/ str, int end, ref int index, out in return true; } - internal static bool TryParseRubyFloat(string/*!*/ str, out double result) { + public static bool TryParseRubyFloat(string/*!*/ str, out double result) { result = 0.0; int index = 0; diff --git a/Src/StdLib/ironruby/ruby4.rb b/Src/StdLib/ironruby/ruby4.rb index c6ccb305..67d024d6 100644 --- a/Src/StdLib/ironruby/ruby4.rb +++ b/Src/StdLib/ironruby/ruby4.rb @@ -786,12 +786,33 @@ def each_slice(n, &block) unless method_defined?(:zip_without_conversion) alias :zip_without_conversion :zip + # An Integer Range whose #each is the built-in one yields begin, begin + 1, ... so the + # element a row wants is worked out from its index. Going through Enumerator#next for + # every element cost ~15 us each: (1..100).zip(1..100) was ~40x MRI. + ZipIntegerRange__ = ::Struct.new(:first, :last) do + def at(index) + value = first + index + last.nil? || value <= last ? value : nil + end + end + RANGE_EACH__ = ::Range.instance_method(:each) + # the readers the indexed path asks; Range#each itself uses the stored endpoints, so a + # Range that overrides one of them - (1..3).dup can - has to be enumerated instead + RANGE_READERS__ = [:begin, :end, :exclude_end?].map { |name| [name, ::Range.instance_method(name)] }.freeze + def __zip_source__(other) return other if other.is_a?(Array) if other.respond_to?(:to_ary) converted = other.to_ary return converted if converted.is_a?(Array) end + if other.instance_of?(::Range) && other.method(:each).unbind == RANGE_EACH__ && + RANGE_READERS__.all? { |name, method| other.method(name).unbind == method } && + ::Integer === other.begin && (other.end.nil? || ::Integer === other.end) + last = other.end + last -= 1 if last && other.exclude_end? + return ZipIntegerRange__.new(other.begin, last) + end unless other.respond_to?(:each) raise TypeError, "wrong argument type #{__ir_message_type_name__(other)} (must respond to :each)" end @@ -808,6 +829,8 @@ def zip(*others, &block) sources.each do |source| row << if source.is_a?(Array) source[index] + elsif ZipIntegerRange__ === source + source.at(index) else begin source.next @@ -3144,6 +3167,13 @@ def Integer(arg, base = nil, exception: true) 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) @@ -3219,6 +3249,10 @@ def Float(arg, exception: true) 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?