Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Src/IronRuby.Tests/RubyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,8 @@ public Tests(Driver/*!*/ driver) {
Io_BuiltinReaders1,
Io_LineReaders2,
Io_LineReaders3,
StringIO_LineReaders1,
Kernel_ConversionsInjectZip1,
Method_LightFrame1,
Method_LightFrame2,
Hash_SelfKey1,
Expand Down
182 changes: 182 additions & 0 deletions Src/IronRuby.Tests/Runtime/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3147,6 +3147,188 @@ private static bool CanCreateSymbolicLinks() {
}
}

/// <summary>
/// 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.
/// </summary>
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]]]
");
}

/// <summary>
/// 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.
/// </summary>
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]]
");
}

/// <summary>
/// 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
Expand Down
41 changes: 41 additions & 0 deletions Src/Libraries/Builtins/Enumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,11 @@ internal static object Inject(CallSiteStorage<EachSite>/*!*/ 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);
Expand All @@ -616,6 +621,42 @@ internal static object Inject(CallSiteStorage<EachSite>/*!*/ each, BlockParam op
return result != Missing.Value ? result : null;
}

/// <summary>
/// #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.
/// </summary>
private static object InjectList(RubyArray/*!*/ list, BlockParam operation, RubyScope scope, string operatorName,
CallSite<Func<CallSite, RubyScope, object, object, object>> 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
Expand Down
54 changes: 54 additions & 0 deletions Src/Libraries/Builtins/KernelOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ public static object ToFloat(ConversionStorage<double>/*!*/ 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) {
Expand All @@ -138,6 +157,7 @@ public static object ToFloat(ConversionStorage<double>/*!*/ 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
Expand All @@ -162,6 +182,40 @@ public static object ToFloat(ConversionStorage<double>/*!*/ 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.
Expand Down
16 changes: 16 additions & 0 deletions Src/Libraries/Extensions/IListOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2771,6 +2771,22 @@ public static object GetMaximum(ConversionStorage<int>/*!*/ fixnumCast, Comparis
return GetExtremes(fixnumCast, comparisonStorage, comparer, self, count, +1);
}

/// <summary>
/// Array#minmax is [min, max] - MRI's ary_minmax calls the two, so a custom &lt;=&gt; sees
/// the same comparisons - and gets their Fixnum fast paths. With a block it is
/// Enumerable#minmax, as MRI hands it to super.
/// </summary>
[RubyMethod("minmax")]
public static object/*!*/ GetMinMax(CallSiteStorage<Func<CallSite, object, Proc, object>>/*!*/ 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, "<=>");
}
Expand Down
Loading
Loading