Skip to content

Commit c4ee0e2

Browse files
committed
[GR-20462] Implement endless ranges.
PullRequest: truffleruby/1272
2 parents ae2ce9c + f4f55ab commit c4ee0e2

File tree

20 files changed

+4148
-3996
lines changed

20 files changed

+4148
-3996
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ Compatibility:
108108
* Implemented `Kernel#system` `exception: true` option.
109109
* Implemented `Random.bytes`.
110110
* Implemented `Random.random_number`.
111+
* Added the ability to parse endless ranges.
112+
* Made `Range#to_a` compatible with endless ranges.
113+
* Made `Array#[]` and `Array#[]= ` compatible with endless ranges.
111114

112115
Performance:
113116

spec/ruby/core/array/element_set_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,14 @@ def obj.to_ary() [1, 2, 3] end
396396
a.should == [1, 2, 3, 8, 4, 5]
397397
end
398398

399+
it "inserts at the end if m > the array size" do
400+
a = [1, 2, 3]
401+
a[3..3] = [4]
402+
a.should == [1, 2, 3, 4]
403+
a[5..7] = [6]
404+
a.should == [1, 2, 3, 4, nil, 6]
405+
end
406+
399407
describe "Range subclasses" do
400408
before :each do
401409
@range_incl = ArraySpecs::MyRange.new(1, 2)
@@ -425,6 +433,43 @@ def obj.to_ary() [1, 2, 3] end
425433
end
426434
end
427435

436+
describe "Array#[]= with [m..]" do
437+
438+
it "just sets the section defined by range to nil even if the rhs is nil" do
439+
a = [1, 2, 3, 4, 5]
440+
a[2..] = nil
441+
a.should == [1, 2, nil]
442+
end
443+
444+
it "just sets the section defined by range to nil if m and n < 0 and the rhs is nil" do
445+
a = [1, 2, 3, 4, 5]
446+
a[-3..] = nil
447+
a.should == [1, 2, nil]
448+
end
449+
450+
it "replaces the section defined by range" do
451+
a = [6, 5, 4, 3, 2, 1]
452+
a[3...] = 9
453+
a.should == [6, 5, 4, 9]
454+
a[2..] = [7, 7, 7]
455+
a.should == [6, 5, 7, 7, 7]
456+
end
457+
458+
it "replaces the section if m and n < 0" do
459+
a = [1, 2, 3, 4, 5]
460+
a[-3..] = [7, 8, 9]
461+
a.should == [1, 2, 7, 8, 9]
462+
end
463+
464+
it "inserts at the end if m > the array size" do
465+
a = [1, 2, 3]
466+
a[3..] = [4]
467+
a.should == [1, 2, 3, 4]
468+
a[5..] = [6]
469+
a.should == [1, 2, 3, 4, nil, 6]
470+
end
471+
end
472+
428473
describe "Array#[] after a shift" do
429474
it "works for insertion" do
430475
a = [1,2]

spec/ruby/core/array/to_a_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@
2121
array = ArraySpecs.recursive_array
2222
array.to_a.should == array
2323
end
24+
25+
it "throws an exception for endless ranges" do
26+
-> { (1..).to_a }.should raise_error(RangeError)
27+
end
2428
end

spec/tags/core/range/new_tags.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.

spec/tags/language/range_tags.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/main/java/org/truffleruby/core/array/ArrayIndexNode.java

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ public abstract class ArrayIndexNode extends ArrayCoreMethodNode {
2525

2626
@Child private ArrayReadDenormalizedNode readNode;
2727
@Child private ArrayReadSliceDenormalizedNode readSliceNode;
28-
@Child private ArrayReadSliceNormalizedNode readNormalizedSliceNode;
2928
@Child private AllocateObjectNode allocateObjectNode = AllocateObjectNode.create();
3029

3130
@Specialization
@@ -54,32 +53,26 @@ protected DynamicObject slice(DynamicObject array, int start, int length) {
5453
@Specialization(guards = "isIntRange(range)")
5554
protected DynamicObject slice(DynamicObject array, DynamicObject range, NotProvided len,
5655
@Cached("createBinaryProfile()") ConditionProfile negativeBeginProfile,
57-
@Cached("createBinaryProfile()") ConditionProfile negativeEndProfile) {
56+
@Cached("createBinaryProfile()") ConditionProfile negativeEndProfile,
57+
@Cached ArrayReadSliceNormalizedNode readNormalizedSliceNode) {
5858
final int size = getSize(array);
59-
final int normalizedIndex = ArrayOperations
59+
final int normalizedBegin = ArrayOperations
6060
.normalizeIndex(size, Layouts.INT_RANGE.getBegin(range), negativeBeginProfile);
6161

62-
if (normalizedIndex < 0 || normalizedIndex > size) {
62+
if (normalizedBegin < 0 || normalizedBegin > size) {
6363
return nil();
6464
} else {
6565
final int end = ArrayOperations
6666
.normalizeIndex(size, Layouts.INT_RANGE.getEnd(range), negativeEndProfile);
6767
final int exclusiveEnd = ArrayOperations
6868
.clampExclusiveIndex(size, Layouts.INT_RANGE.getExcludedEnd(range) ? end : end + 1);
6969

70-
if (exclusiveEnd <= normalizedIndex) {
70+
if (exclusiveEnd <= normalizedBegin) {
7171
return allocateObjectNode
7272
.allocate(Layouts.BASIC_OBJECT.getLogicalClass(array), ArrayStrategy.NULL_ARRAY_STORE, 0);
7373
}
7474

75-
final int length = exclusiveEnd - normalizedIndex;
76-
77-
if (readNormalizedSliceNode == null) {
78-
CompilerDirectives.transferToInterpreterAndInvalidate();
79-
readNormalizedSliceNode = insert(ArrayReadSliceNormalizedNodeGen.create());
80-
}
81-
82-
return readNormalizedSliceNode.executeReadSlice(array, normalizedIndex, length);
75+
return readNormalizedSliceNode.executeReadSlice(array, normalizedBegin, exclusiveEnd - normalizedBegin);
8376
}
8477
}
8578

src/main/java/org/truffleruby/core/array/ArrayIndexSetNode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,15 @@ protected Object setRange(DynamicObject array, DynamicObject range, Object value
219219
@Specialization(guards = { "isIntRange(range)", "!isRubyArray(value)" })
220220
protected Object setRangeWithNonArray(DynamicObject array, DynamicObject range, Object value,
221221
NotProvided unused) {
222+
// the fallback will recurse after converting the object to an array
222223
return fallback(array, range, value, unused);
223224
}
224225

225226
// array[start..end] = object_or_array (non-int range)
226227

227228
@Specialization(guards = { "!isIntRange(range)", "isRubyRange(range)" })
228229
protected Object setOtherRange(DynamicObject array, DynamicObject range, Object value, NotProvided unused) {
230+
// the fallback will recurse after converting the range to an int range
229231
return fallback(array, range, value, unused);
230232
}
231233

src/main/java/org/truffleruby/core/array/ArrayNodes.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ protected Object fallback(DynamicObject array, Object index, Object length, Obje
278278
argumentNames = { "index_start_or_range", "length_or_value", "value" })
279279
public abstract static class IndexSetPrimitiveNode extends ArrayIndexSetNode {
280280

281+
// This primitive inherits from the same base as IndexSetNode and is called in its fallback.
282+
// Hence we need to avoid infinite recursion on fallback.
283+
281284
protected abstract RubyNode[] getArguments();
282285

283286
@Override

src/main/java/org/truffleruby/core/range/RangeNodes.java

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,8 @@ protected DynamicObject toA(DynamicObject range) {
386386
}
387387
}
388388

389-
@Specialization(guards = "isObjectRange(range)")
390-
protected Object toA(VirtualFrame frame, DynamicObject range) {
389+
@Specialization(guards = { "isObjectRange(range)", "!isEndlessRange(getContext(), range)" })
390+
protected Object boundedToA(VirtualFrame frame, DynamicObject range) {
391391
if (toAInternalCall == null) {
392392
CompilerDirectives.transferToInterpreterAndInvalidate();
393393
toAInternalCall = insert(CallDispatchHeadNode.createPrivate());
@@ -396,34 +396,53 @@ protected Object toA(VirtualFrame frame, DynamicObject range) {
396396
return toAInternalCall.call(range, "to_a_internal");
397397
}
398398

399+
@Specialization(guards = { "isObjectRange(range)", "isEndlessRange(getContext(), range)" })
400+
protected Object endlessToA(VirtualFrame frame, DynamicObject range) {
401+
throw new RaiseException(getContext(), coreExceptions().rangeError(
402+
"cannot convert endless range to an array",
403+
this));
404+
}
405+
399406
}
400407

408+
/**
409+
* Returns a conversion of the range into an int range, with regard to the supplied array
410+
* (the array is necessary to handle endless ranges).
411+
*/
401412
@Primitive(name = "range_to_int_range")
402413
public abstract static class ToIntRangeNode extends PrimitiveArrayArgumentsNode {
403414

404415
@Child private ToIntNode toIntNode;
405416

406417
@Specialization(guards = "isIntRange(range)")
407-
protected DynamicObject intRange(DynamicObject range) {
418+
protected DynamicObject intRange(DynamicObject range, DynamicObject array) {
408419
return range;
409420
}
410421

411422
@Specialization(guards = "isLongRange(range)")
412-
protected DynamicObject longRange(DynamicObject range) {
423+
protected DynamicObject longRange(DynamicObject range, DynamicObject array) {
413424
int begin = toInt(Layouts.LONG_RANGE.getBegin(range));
414425
int end = toInt(Layouts.LONG_RANGE.getEnd(range));
415426
boolean excludedEnd = Layouts.LONG_RANGE.getExcludedEnd(range);
416427
return Layouts.INT_RANGE.createIntRange(coreLibrary().intRangeFactory, excludedEnd, begin, end);
417428
}
418429

419-
@Specialization(guards = "isObjectRange(range)")
420-
protected DynamicObject objectRange(DynamicObject range) {
430+
@Specialization(guards = { "isObjectRange(range)", "!isEndlessRange(getContext(), range)" })
431+
protected DynamicObject boundedObjectRange(DynamicObject range, DynamicObject array) {
421432
int begin = toInt(Layouts.OBJECT_RANGE.getBegin(range));
422433
int end = toInt(Layouts.OBJECT_RANGE.getEnd(range));
423434
boolean excludedEnd = Layouts.OBJECT_RANGE.getExcludedEnd(range);
424435
return Layouts.INT_RANGE.createIntRange(coreLibrary().intRangeFactory, excludedEnd, begin, end);
425436
}
426437

438+
@Specialization(guards = { "isObjectRange(range)", "isEndlessRange(getContext(), range)" })
439+
protected DynamicObject endlessObjectRange(DynamicObject range, DynamicObject array) {
440+
int begin = toInt(Layouts.OBJECT_RANGE.getBegin(range));
441+
int end = Layouts.ARRAY.getSize(array);
442+
boolean excludedEnd = true;
443+
return Layouts.INT_RANGE.createIntRange(coreLibrary().intRangeFactory, excludedEnd, begin, end);
444+
}
445+
427446
private int toInt(Object indexObject) {
428447
if (toIntNode == null) {
429448
CompilerDirectives.transferToInterpreterAndInvalidate();
@@ -495,14 +514,7 @@ protected Object objectRange(
495514
allocateNode = insert(AllocateObjectNode.create());
496515
}
497516

498-
final Object cmpResult;
499-
try {
500-
cmpResult = cmpNode.call(begin, "<=>", end);
501-
} catch (RaiseException e) {
502-
throw new RaiseException(getContext(), coreExceptions().argumentError("bad value for range", this));
503-
}
504-
505-
if (cmpResult == nil()) {
517+
if (cmpNode.call(begin, "<=>", end) == nil() && end != nil()) {
506518
throw new RaiseException(getContext(), coreExceptions().argumentError("bad value for range", this));
507519
}
508520

src/main/java/org/truffleruby/language/RubyGuards.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ public static boolean isObjectRange(DynamicObject object) {
127127
return Layouts.OBJECT_RANGE.isObjectRange(object);
128128
}
129129

130+
public static boolean isEndlessRange(RubyContext context, DynamicObject object) {
131+
assert isObjectRange(object);
132+
return isNil(context, Layouts.OBJECT_RANGE.getEnd(object));
133+
}
134+
130135
public static boolean isRubyRange(Object value) {
131136
return isIntRange(value) || isLongRange(value) || isObjectRange(value);
132137
}

0 commit comments

Comments
 (0)