Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jruby/jruby
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: f3e4eb737303
Choose a base ref
...
head repository: jruby/jruby
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 8ebd02ec60eb
Choose a head ref
  • 7 commits
  • 12 files changed
  • 1 contributor

Commits on Jan 21, 2016

  1. Copy the full SHA
    85f483b View commit details
  2. [Truffle] Updated Rope#toString to be encoding-aware and avoid materi…

    …alizing a byte[] if not necessary.
    nirvdrum committed Jan 21, 2016
    Copy the full SHA
    737c687 View commit details
  3. Copy the full SHA
    fbef03c View commit details
  4. Copy the full SHA
    7689def View commit details
  5. Copy the full SHA
    57da56e View commit details
  6. Copy the full SHA
    b6c964c View commit details
  7. Copy the full SHA
    8ebd02e View commit details
Original file line number Diff line number Diff line change
@@ -32,7 +32,9 @@
import org.jruby.truffle.runtime.core.EncodingOperations;
import org.jruby.truffle.runtime.core.StringOperations;
import org.jruby.truffle.runtime.layouts.Layouts;
import org.jruby.truffle.runtime.rope.Rope;
import org.jruby.util.ByteList;
import org.jruby.util.StringSupport;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
@@ -128,7 +130,7 @@ public DynamicObject isCompatibleStringStringCached(DynamicObject first, Dynamic
"isRubyString(first)", "isRubyString(second)"
}, contains = "isCompatibleStringStringCached")
public DynamicObject isCompatibleStringStringUncached(DynamicObject first, DynamicObject second) {
final Encoding compatibleEncoding = areCompatible(first, second);
final Encoding compatibleEncoding = compatibleEncodingForStrings(first, second);

if (compatibleEncoding != null) {
return getEncoding(compatibleEncoding);
@@ -248,18 +250,41 @@ public Object isCompatibleStringEncoding(DynamicObject first, DynamicObject seco
}

@TruffleBoundary
public static Encoding areCompatible(DynamicObject first, DynamicObject second) {
public static Encoding compatibleEncodingForStrings(DynamicObject first, DynamicObject second) {
// Taken from org.jruby.RubyEncoding#areCompatible.

assert RubyGuards.isRubyString(first);
assert RubyGuards.isRubyString(second);

final Encoding firstEncoding = Layouts.STRING.getRope(first).getEncoding();
final Encoding secondEncoding = Layouts.STRING.getRope(second).getEncoding();
final Rope firstRope = StringOperations.rope(first);
final Rope secondRope = StringOperations.rope(second);

final Encoding firstEncoding = firstRope.getEncoding();
final Encoding secondEncoding = secondRope.getEncoding();

if (firstEncoding == null || secondEncoding == null) return null;
if (firstEncoding == secondEncoding) return firstEncoding;

if (secondRope.isEmpty()) return firstEncoding;
if (firstRope.isEmpty()) {
return firstEncoding.isAsciiCompatible() && isAsciiOnly(secondRope) ? firstEncoding : secondEncoding;
}

if (!firstEncoding.isAsciiCompatible() || !secondEncoding.isAsciiCompatible()) return null;

if (firstEncoding == secondEncoding) {
return firstEncoding;
if (firstRope.getCodeRange() != secondRope.getCodeRange()) {
if (firstRope.getCodeRange() == StringSupport.CR_7BIT) return secondEncoding;
if (secondRope.getCodeRange() == StringSupport.CR_7BIT) return firstEncoding;
}
if (secondRope.getCodeRange() == StringSupport.CR_7BIT) return firstEncoding;
if (firstRope.getCodeRange() == StringSupport.CR_7BIT) return secondEncoding;

return org.jruby.RubyEncoding.areCompatible(StringOperations.getCodeRangeableReadOnly(first), StringOperations.getCodeRangeableReadOnly(second));
return null;
}

@TruffleBoundary
private static boolean isAsciiOnly(Rope rope) {
return rope.getEncoding().isAsciiCompatible() && rope.getCodeRange() == StringSupport.CR_7BIT;
}

protected Encoding extractEncoding(DynamicObject string) {
Original file line number Diff line number Diff line change
@@ -440,7 +440,6 @@ public abstract static class GetIndexNode extends CoreMethodArrayArgumentsNode {
@Child private ToIntNode toIntNode;
@Child private CallDispatchHeadNode includeNode;
@Child private CallDispatchHeadNode dupNode;
@Child private SizeNode sizeNode;
@Child private StringPrimitiveNodes.StringSubstringPrimitiveNode substringNode;
@Child private AllocateObjectNode allocateObjectNode;

@@ -453,7 +452,7 @@ public GetIndexNode(RubyContext context, SourceSection sourceSection) {

@Specialization(guards = "wasNotProvided(length) || isRubiniusUndefined(length)")
public Object getIndex(VirtualFrame frame, DynamicObject string, int index, Object length) {
final int stringLength = getSizeNode().executeInteger(frame, string);
final int stringLength = StringOperations.rope(string).characterLength();
int normalizedIndex = StringOperations.normalizeIndex(stringLength, index);

if (normalizedIndex < 0 || normalizedIndex >= StringOperations.byteLength(string)) {
@@ -492,7 +491,7 @@ public Object sliceObjectRange(VirtualFrame frame, DynamicObject string, Dynamic
private Object sliceRange(VirtualFrame frame, DynamicObject string, int begin, int end, boolean doesExcludeEnd) {
assert RubyGuards.isRubyString(string);

final int stringLength = getSizeNode().executeInteger(frame, string);
final int stringLength = StringOperations.rope(string).characterLength();
begin = StringOperations.normalizeIndex(stringLength, begin);

if (begin < 0 || begin > stringLength) {
@@ -594,15 +593,6 @@ protected boolean isRubiniusUndefined(Object object) {
return object == getContext().getCoreLibrary().getRubiniusUndefined();
}

private SizeNode getSizeNode() {
if (sizeNode == null) {
CompilerDirectives.transferToInterpreter();
sizeNode = insert(StringNodesFactory.SizeNodeFactory.create(getContext(), getSourceSection(), new RubyNode[]{null}));
}

return sizeNode;
}

}

@CoreMethod(names = "ascii_only?")
@@ -1326,14 +1316,11 @@ public Object initializeCopy(DynamicObject self, DynamicObject from) {
public abstract static class InsertNode extends CoreMethodNode {

@Child private CallDispatchHeadNode appendNode;
@Child private StringPrimitiveNodes.CharacterByteIndexNode characterByteIndexNode;
@Child private SizeNode sizeNode;
@Child private TaintResultNode taintResultNode;
@Child private StringPrimitiveNodes.CharacterByteIndexNode characterByteIndexNode;@Child private TaintResultNode taintResultNode;

public InsertNode(RubyContext context, SourceSection sourceSection) {
super(context, sourceSection);
characterByteIndexNode = StringPrimitiveNodesFactory.CharacterByteIndexNodeFactory.create(context, sourceSection, new RubyNode[] {});
sizeNode = StringNodesFactory.SizeNodeFactory.create(context, sourceSection, new RubyNode[] {});
taintResultNode = new TaintResultNode(context, sourceSection);
}

@@ -1350,7 +1337,7 @@ public Object insertPrepend(DynamicObject string, int index, DynamicObject other
final Rope left = rope(other);
final Rope right = rope(string);

final Encoding compatibleEncoding = EncodingNodes.CompatibleQueryNode.areCompatible(string, other);
final Encoding compatibleEncoding = EncodingNodes.CompatibleQueryNode.compatibleEncodingForStrings(string, other);

if (compatibleEncoding == null) {
CompilerDirectives.transferToInterpreter();
@@ -1386,15 +1373,15 @@ public Object insert(VirtualFrame frame, DynamicObject string, int index, Dynami

final Rope source = rope(string);
final Rope insert = rope(other);
final Encoding compatibleEncoding = EncodingNodes.CompatibleQueryNode.areCompatible(string, other);
final Encoding compatibleEncoding = EncodingNodes.CompatibleQueryNode.compatibleEncodingForStrings(string, other);

if (compatibleEncoding == null) {
CompilerDirectives.transferToInterpreter();
throw new RaiseException(getContext().getCoreLibrary().encodingCompatibilityError(
String.format("incompatible encodings: %s and %s", source.getEncoding(), insert.getEncoding()), this));
}

final int stringLength = sizeNode.executeInteger(frame, string);
final int stringLength = source.characterLength();
final int normalizedIndex = StringNodesHelper.checkIndex(stringLength, index, this);
final int byteIndex = characterByteIndexNode.executeInt(frame, string, normalizedIndex, 0);

Original file line number Diff line number Diff line change
@@ -104,21 +104,18 @@ public int size(DynamicObject bytes) {
@CoreMethod(names = "locate", required = 3, lowerFixnumParameters = {1, 2})
public abstract static class LocateNode extends CoreMethodArrayArgumentsNode {

@Child private StringNodes.SizeNode sizeNode;

public LocateNode(RubyContext context, SourceSection sourceSection) {
super(context, sourceSection);
sizeNode = StringNodesFactory.SizeNodeFactory.create(context, sourceSection, new RubyNode[] {});
}

@Specialization(guards = "isRubyString(pattern)")
public Object getByte(VirtualFrame frame, DynamicObject bytes, DynamicObject pattern, int start, int length) {
public Object getByte(DynamicObject bytes, DynamicObject pattern, int start, int length) {
final int index = new ByteList(Layouts.BYTE_ARRAY.getBytes(bytes), start, length).indexOf(StringOperations.getByteListReadOnly(pattern));

if (index == -1) {
return nil();
} else {
return start + index + sizeNode.executeInteger(frame, pattern);
return start + index + StringOperations.rope(pattern).characterLength();
}
}

Original file line number Diff line number Diff line change
@@ -160,7 +160,7 @@ public DynamicObject stringAppend(VirtualFrame frame, DynamicObject string, Dyna
final Rope left = rope(string);
final Rope right = rope(other);

final Encoding compatibleEncoding = EncodingNodes.CompatibleQueryNode.areCompatible(string, other);
final Encoding compatibleEncoding = EncodingNodes.CompatibleQueryNode.compatibleEncodingForStrings(string, other);

if (compatibleEncoding == null) {
CompilerDirectives.transferToInterpreter();
@@ -263,7 +263,6 @@ public static abstract class StringByteSubstringPrimitiveNode extends RubiniusPr

@Child private TaintResultNode taintResultNode;
@Child private AllocateObjectNode allocateObjectNode;
@Child private StringNodes.SizeNode sizeNode;

public StringByteSubstringPrimitiveNode(RubyContext context, SourceSection sourceSection) {
super(context, sourceSection);
@@ -293,7 +292,7 @@ public Object stringByteSubstring(VirtualFrame frame, DynamicObject string, int
}

final Rope rope = rope(string);
final int stringLength = getSizeNode().executeInteger(frame, string);
final int stringLength = rope.characterLength();
final int normalizedIndex = StringOperations.normalizeIndex(stringLength, index);

if (normalizedIndex < 0 || normalizedIndex > rope.byteLength()) {
@@ -393,14 +392,6 @@ public Object stringByteSubstring(DynamicObject string, DynamicObject index, Obj
return null;
}

private StringNodes.SizeNode getSizeNode() {
if (sizeNode == null) {
CompilerDirectives.transferToInterpreter();
sizeNode = insert(StringNodesFactory.SizeNodeFactory.create(getContext(), getSourceSection(), new RubyNode[]{null}));
}

return sizeNode;
}
}

@RubiniusPrimitive(name = "string_check_null_safe", needsSelf = false)
@@ -475,19 +466,16 @@ protected static boolean indexOutOfBounds(DynamicObject string, int byteIndex) {
@RubiniusPrimitive(name = "string_compare_substring")
public static abstract class StringCompareSubstringPrimitiveNode extends RubiniusPrimitiveNode {

@Child private StringNodes.SizeNode sizeNode;

public StringCompareSubstringPrimitiveNode(RubyContext context, SourceSection sourceSection) {
super(context, sourceSection);
sizeNode = StringNodesFactory.SizeNodeFactory.create(context, sourceSection, new RubyNode[] { null });
}

@Specialization(guards = "isRubyString(other)")
public int stringCompareSubstring(VirtualFrame frame, DynamicObject string, DynamicObject other, int start, int size) {
// Transliterated from Rubinius C++.

final int stringLength = sizeNode.executeInteger(frame, string);
final int otherLength = sizeNode.executeInteger(frame, other);
final int stringLength = StringOperations.rope(string).characterLength();
final int otherLength = StringOperations.rope(other).characterLength();

if (start < 0) {
start += otherLength;
@@ -521,11 +509,12 @@ public int stringCompareSubstring(VirtualFrame frame, DynamicObject string, Dyna
size = stringLength;
}

final ByteList bytes = StringOperations.getByteListReadOnly(string);
final ByteList otherBytes = StringOperations.getByteListReadOnly(other);
final Rope rope = StringOperations.rope(string);
final Rope otherRope = StringOperations.rope(other);

return ByteList.memcmp(bytes.getUnsafeBytes(), bytes.getBegin(), size,
otherBytes.getUnsafeBytes(), otherBytes.getBegin() + start, size);
// TODO (nirvdrum 21-Jan-16): Reimplement with something more friendly to rope byte[] layout?
return ByteList.memcmp(rope.getBytes(), rope.getBegin(), size,
otherRope.getBytes(), otherRope.getBegin() + start, size);
}

}
@@ -911,17 +900,19 @@ public Object stringCharacterIndex(DynamicObject string, DynamicObject pattern,
return nil();
}

final Rope stringRope = Layouts.STRING.getRope(string);
final Rope stringRope = rope(string);
final Rope patternRope = rope(pattern);

final int total = stringRope.byteLength();
int p = StringOperations.getByteListReadOnly(string).getBegin();
int p = stringRope.begin();
final int e = p + total;
int pp = StringOperations.getByteListReadOnly(pattern).getBegin();
final int pe = pp + Layouts.STRING.getRope(pattern).byteLength();
int pp = patternRope.begin();
final int pe = pp + patternRope.byteLength();
int s;
int ss;

final byte[] stringBytes = StringOperations.getByteListReadOnly(string).getUnsafeBytes();
final byte[] patternBytes = StringOperations.getByteListReadOnly(pattern).getUnsafeBytes();
final byte[] stringBytes = stringRope.getBytes();
final byte[] patternBytes = patternRope.getBytes();

if (stringRope.isSingleByteOptimizable()) {
for(s = p += offset, ss = pp; p < e; s = ++p) {
@@ -943,7 +934,7 @@ public Object stringCharacterIndex(DynamicObject string, DynamicObject pattern,
return nil();
}

final Encoding enc = Layouts.STRING.getRope(string).getEncoding();
final Encoding enc = stringRope.getEncoding();
int index = 0;
int c;

@@ -1125,11 +1116,11 @@ public Object stringPreviousByteIndex(DynamicObject string, int index) {
throw new RaiseException(getContext().getCoreLibrary().argumentError("negative index given", this));
}

final ByteList bytes = StringOperations.getByteListReadOnly(string);
final int p = bytes.getBegin();
final int end = p + bytes.getRealSize();
final Rope rope = rope(string);
final int p = rope.getBegin();
final int end = p + rope.getRealSize();

final int b = bytes.getEncoding().prevCharHead(bytes.getUnsafeBytes(), p, p + index, end);
final int b = rope.getEncoding().prevCharHead(rope.getBytes(), p, p + index, end);

if (b == -1) {
return nil();
@@ -1159,8 +1150,8 @@ public DynamicObject stringCopyFrom(DynamicObject string, DynamicObject other, i
int dst = dest;
int cnt = size;

final ByteList otherBytes = StringOperations.getByteListReadOnly(other);
int osz = otherBytes.length();
final Rope otherRope = rope(other);
int osz = otherRope.byteLength();
if(negativeStartOffsetProfile.profile(src < 0)) src = 0;
if(sizeTooLargeInReplacementProfile.profile(cnt > osz - src)) cnt = osz - src;

@@ -1169,7 +1160,7 @@ public DynamicObject stringCopyFrom(DynamicObject string, DynamicObject other, i
if(negativeDestinationOffsetProfile.profile(dst < 0)) dst = 0;
if(sizeTooLargeInStringProfile.profile(cnt > sz - dst)) cnt = sz - dst;

System.arraycopy(otherBytes.unsafeBytes(), otherBytes.begin() + src, stringBytes.getUnsafeBytes(), stringBytes.begin() + dest, cnt);
System.arraycopy(otherRope.getBytes(), otherRope.begin() + src, stringBytes.getUnsafeBytes(), stringBytes.begin() + dest, cnt);

Layouts.STRING.setRope(string, StringOperations.ropeFromByteList(stringBytes));

@@ -1193,8 +1184,10 @@ protected boolean offsetTooLargeRaw(int offset, DynamicObject string) {
// This bounds checks on the total capacity rather than the virtual
// size() of the String. This allows for string adjustment within
// the capacity without having to change the virtual size first.
final ByteList byteList = StringOperations.getByteListReadOnly(string);
return offset >= (byteList.unsafeBytes().length - byteList.begin());

// TODO (nirvdrum 21-Jan-16) Verify whether we still need this method as we never have spare capacity allocated with ropes.
final Rope rope = rope(string);
return offset >= (rope.byteLength() - rope.begin());
}

}
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
import org.jruby.truffle.nodes.RubyGuards;
import org.jruby.truffle.runtime.core.*;
import org.jruby.truffle.runtime.layouts.Layouts;
import org.jruby.truffle.runtime.rope.RopeOperations;

public class RubyObjectType extends ObjectType {

@@ -25,7 +26,7 @@ public String toString(DynamicObject object) {
CompilerAsserts.neverPartOfCompilation();

if (RubyGuards.isRubyString(object)) {
return Helpers.decodeByteList(getContext().getRuntime(), StringOperations.getByteListReadOnly(object));
return RopeOperations.decodeRope(getContext().getRuntime(), StringOperations.rope(object));
} else if (RubyGuards.isRubySymbol(object)) {
return Layouts.SYMBOL.getString(object);
} else if (RubyGuards.isRubyException(object)) {
Original file line number Diff line number Diff line change
@@ -80,4 +80,8 @@ public ByteList getByteList() {
return StringOperations.getByteList(string);
}


public DynamicObject getString() {
return string;
}
}
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@
import org.jruby.RubyString;
import org.jruby.runtime.Helpers;
import org.jruby.truffle.nodes.RubyGuards;
import org.jruby.truffle.nodes.core.EncodingNodes;
import org.jruby.truffle.runtime.RubyContext;
import org.jruby.truffle.runtime.control.RaiseException;
import org.jruby.truffle.runtime.layouts.Layouts;
@@ -58,9 +59,9 @@ public static DynamicObject createString(RubyContext context, Rope rope) {
}

// Since ByteList.toString does not decode properly
@TruffleBoundary
@CompilerDirectives.TruffleBoundary
public static String getString(RubyContext context, DynamicObject string) {
return Helpers.decodeByteList(context.getRuntime(), StringOperations.getByteListReadOnly(string));
return RopeOperations.decodeRope(context.getRuntime(), StringOperations.rope(string));
}

public static StringCodeRangeableWrapper getCodeRangeable(DynamicObject string) {
@@ -159,12 +160,13 @@ public static void modifyAndKeepCodeRange(DynamicObject string) {
keepCodeRange(string);
}

@TruffleBoundary
@CompilerDirectives.TruffleBoundary
public static Encoding checkEncoding(DynamicObject string, CodeRangeable other) {
final Encoding encoding = StringSupport.areCompatible(getCodeRangeableReadOnly(string), other);
final Encoding encoding = EncodingNodes.CompatibleQueryNode.compatibleEncodingForStrings(string, ((StringCodeRangeableWrapper) other).getString());

// TODO (nirvdrum 23-Mar-15) We need to raise a proper Truffle+JRuby exception here, rather than a non-Truffle JRuby exception.
if (encoding == null) {
CompilerDirectives.transferToInterpreter();
throw Layouts.MODULE.getFields(Layouts.BASIC_OBJECT.getLogicalClass(string)).getContext().getRuntime().newEncodingCompatibilityError(
String.format("incompatible character encodings: %s and %s",
Layouts.STRING.getRope(string).getEncoding().toString(),
@@ -174,7 +176,7 @@ public static Encoding checkEncoding(DynamicObject string, CodeRangeable other)
return encoding;
}

@TruffleBoundary
@CompilerDirectives.TruffleBoundary
private static int slowCodeRangeScan(DynamicObject string) {
final ByteList byteList = StringOperations.getByteListReadOnly(string);
return StringSupport.codeRangeScan(byteList.getEncoding(), byteList);
@@ -192,10 +194,12 @@ public static int normalizeIndex(int length, int index) {

public static int clampExclusiveIndex(DynamicObject string, int index) {
assert RubyGuards.isRubyString(string);
return ArrayOperations.clampExclusiveIndex(StringOperations.getByteListReadOnly(string).length(), index);

// TODO (nirvdrum 21-Jan-16): Verify this is supposed to be the byteLength and not the characterLength.
return ArrayOperations.clampExclusiveIndex(StringOperations.rope(string).byteLength(), index);
}

@TruffleBoundary
@CompilerDirectives.TruffleBoundary
public static Encoding checkEncoding(RubyContext context, DynamicObject string, CodeRangeable other, Node node) {
final Encoding encoding = StringSupport.areCompatible(getCodeRangeableReadOnly(string), other);

Original file line number Diff line number Diff line change
@@ -103,4 +103,10 @@ public Rope getLeft() {
public Rope getRight() {
return right;
}

@Override
public String toString() {
// This should be used for debugging only.
return RopeOperations.decodeUTF8(left) + RopeOperations.decodeUTF8(right);
}
}
Original file line number Diff line number Diff line change
@@ -63,4 +63,10 @@ public boolean equals(Object o) {

return false;
}

@Override
public String toString() {
// This should be used for debugging only.
return RopeOperations.decodeUTF8(this);
}
}
Original file line number Diff line number Diff line change
@@ -72,12 +72,6 @@ public final int depth() {
return ropeDepth;
}

@Override
public String toString() {
// This should be used for debugging only.
return new String(getBytes());
}

public int begin() {
return 0;
}
Original file line number Diff line number Diff line change
@@ -6,6 +6,10 @@
* Eclipse Public License version 1.0
* GNU General Public License version 2
* GNU Lesser General Public License version 2.1
*
*
* Some of the code in this class is modified from org.jruby.runtime.Helpers,
* licensed under the same EPL1.0/GPL 2.0/LGPL 2.1 used throughout.
*/
package org.jruby.truffle.runtime.rope;

@@ -14,10 +18,14 @@
import org.jcodings.Encoding;
import org.jcodings.specific.ASCIIEncoding;
import org.jcodings.specific.UTF8Encoding;
import org.jruby.Ruby;
import org.jruby.RubyEncoding;
import org.jruby.util.StringSupport;
import org.jruby.util.io.EncodingUtils;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;

public class RopeOperations {

public static final Rope EMPTY_ASCII_8BIT_ROPE = create(new byte[] {}, ASCIIEncoding.INSTANCE, StringSupport.CR_7BIT);
@@ -122,6 +130,30 @@ public static String decodeUTF8(Rope rope) {
return RubyEncoding.decodeUTF8(rope.getBytes(), 0, rope.byteLength());
}

@TruffleBoundary
public static String decodeRope(Ruby runtime, Rope value) {
int begin = value.getBegin();
int length = value.byteLength();

Encoding encoding = value.getEncoding();

if (encoding == UTF8Encoding.INSTANCE) {
return RubyEncoding.decodeUTF8(value.getBytes(), begin, length);
}

Charset charset = runtime.getEncodingService().charsetForEncoding(encoding);

if (charset == null) {
try {
return new String(value.getBytes(), begin, length, encoding.toString());
} catch (UnsupportedEncodingException uee) {
return value.toString();
}
}

return RubyEncoding.decode(value.getBytes(), begin, length, charset);
}

// MRI: get_actual_encoding
@TruffleBoundary
public static Encoding STR_ENC_GET(Rope rope) {
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@

package org.jruby.truffle.runtime.rope;

import org.jruby.RubyEncoding;

public class SubstringRope extends Rope {

private final Rope child;
@@ -48,4 +50,9 @@ public int getOffset() {
return offset;
}

@Override
public String toString() {
// This should be used for debugging only.
return RubyEncoding.decodeUTF8(child.getBytes(), offset, byteLength());
}
}