Skip to content

Commit c870d09

Browse files
committed
Support buffer keyword argument to Array#pack
* Fixes #3559
1 parent c2e5209 commit c870d09

File tree

10 files changed

+95
-25
lines changed

10 files changed

+95
-25
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Compatibility:
2121
* Allow null encoding pointer in `rb_enc_interned_str_cstr` (@thomasmarshall).
2222
* Allow anonymous memberless Struct (@simonlevasseur).
2323
* Set `$!` when a `Kernel#at_exit` hook raises an exception (#3535, @andrykonchin).
24+
* Support `:buffer` keyword argument to `Array#pack` (#3559, @andrykonchyn).
2425

2526
Performance:
2627
* Fix inline caching for Regexp creation from Strings (#3492, @andrykonchin, @eregon).

spec/ruby/core/array/pack/buffer_spec.rb

+10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
TypeError, "buffer must be String, not Array")
2929
end
3030

31+
it "raise FrozenError if buffer is frozen" do
32+
-> { [65].pack("c", buffer: "frozen-string".freeze) }.should raise_error(FrozenError)
33+
end
34+
35+
it "preserves the encoding of the given buffer" do
36+
buffer = ''.encode(Encoding::ISO_8859_1)
37+
[65, 66, 67].pack("ccc", buffer: buffer)
38+
buffer.encoding.should == Encoding::ISO_8859_1
39+
end
40+
3141
context "offset (@) is specified" do
3242
it 'keeps buffer content if it is longer than offset' do
3343
n = [ 65, 66, 67 ]

spec/tags/core/array/pack/buffer_tags.txt

-6
This file was deleted.

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

+40-12
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.truffleruby.core.encoding.RubyEncoding;
5858
import org.truffleruby.core.format.BytesResult;
5959
import org.truffleruby.core.format.FormatExceptionTranslator;
60+
import org.truffleruby.core.format.FormatRootNode;
6061
import org.truffleruby.core.format.exceptions.FormatException;
6162
import org.truffleruby.core.format.pack.PackCompiler;
6263
import org.truffleruby.core.hash.HashingNodes;
@@ -1491,15 +1492,15 @@ public void accept(Node node, CallBlockNode yieldNode, RubyArray array, Object s
14911492

14921493
}
14931494

1494-
@CoreMethod(names = "pack", required = 1, split = Split.ALWAYS)
1495-
public abstract static class ArrayPackNode extends CoreMethodArrayArgumentsNode {
1495+
@Primitive(name = "array_pack", lowerFixnum = 1)
1496+
public abstract static class ArrayPackPrimitiveNode extends PrimitiveArrayArgumentsNode {
14961497

14971498
@Specialization
1498-
RubyString pack(RubyArray array, Object format,
1499+
RubyString pack(RubyArray array, Object format, Object buffer,
14991500
@Cached ToStrNode toStrNode,
15001501
@Cached PackNode packNode) {
15011502
final var formatAsString = toStrNode.execute(this, format);
1502-
return packNode.execute(this, array, formatAsString);
1503+
return packNode.execute(this, array, formatAsString, buffer);
15031504
}
15041505
}
15051506

@@ -1508,28 +1509,35 @@ RubyString pack(RubyArray array, Object format,
15081509
@ReportPolymorphism
15091510
public abstract static class PackNode extends RubyBaseNode {
15101511

1511-
public abstract RubyString execute(Node node, RubyArray array, Object format);
1512+
public abstract RubyString execute(Node node, RubyArray array, Object format, Object buffer);
15121513

15131514
@Specialization(
15141515
guards = {
15151516
"libFormat.isRubyString(format)",
1517+
"libBuffer.isRubyString(buffer)",
15161518
"equalNode.execute(libFormat, format, cachedFormat, cachedEncoding)" },
15171519
limit = "getCacheLimit()")
1518-
static RubyString packCached(Node node, RubyArray array, Object format,
1520+
static RubyString packCached(Node node, RubyArray array, Object format, Object buffer,
15191521
@Cached @Shared InlinedBranchProfile exceptionProfile,
15201522
@Cached @Shared InlinedConditionProfile resizeProfile,
15211523
@Cached @Shared RubyStringLibrary libFormat,
1524+
@Cached @Shared RubyStringLibrary libBuffer,
15221525
@Cached @Shared WriteObjectFieldNode writeAssociatedNode,
15231526
@Cached @Shared TruffleString.FromByteArrayNode fromByteArrayNode,
1527+
@Cached @Shared TruffleString.CopyToByteArrayNode copyToByteArrayNode,
15241528
@Cached("asTruffleStringUncached(format)") TruffleString cachedFormat,
15251529
@Cached("libFormat.getEncoding(format)") RubyEncoding cachedEncoding,
15261530
@Cached("cachedFormat.byteLength(cachedEncoding.tencoding)") int cachedFormatLength,
1527-
@Cached("create(compileFormat(node, getJavaString(format)))") DirectCallNode callPackNode,
1531+
@Cached("compileFormat(node, getJavaString(format))") RootCallTarget formatCallTarget,
1532+
@Cached("create(formatCallTarget)") DirectCallNode callPackNode,
15281533
@Cached StringHelperNodes.EqualNode equalNode) {
1534+
final byte[] bytes = initOutputBytes(buffer, libBuffer, formatCallTarget, copyToByteArrayNode);
1535+
15291536
final BytesResult result;
1537+
15301538
try {
15311539
result = (BytesResult) callPackNode.call(
1532-
new Object[]{ array.getStore(), array.size, false, null });
1540+
new Object[]{ array.getStore(), array.size, bytes, libBuffer.byteLength(buffer) });
15331541
} catch (FormatException e) {
15341542
exceptionProfile.enter(node);
15351543
throw FormatExceptionTranslator.translate(getContext(node), node, e);
@@ -1538,22 +1546,28 @@ static RubyString packCached(Node node, RubyArray array, Object format,
15381546
return finishPack(node, cachedFormatLength, result, resizeProfile, writeAssociatedNode, fromByteArrayNode);
15391547
}
15401548

1541-
@Specialization(guards = { "libFormat.isRubyString(format)" }, replaces = "packCached")
1542-
static RubyString packUncached(Node node, RubyArray array, Object format,
1549+
@Specialization(guards = { "libFormat.isRubyString(format)", "libBuffer.isRubyString(buffer)" },
1550+
replaces = "packCached")
1551+
static RubyString packUncached(Node node, RubyArray array, Object format, Object buffer,
15431552
@Cached @Shared InlinedBranchProfile exceptionProfile,
15441553
@Cached @Shared InlinedConditionProfile resizeProfile,
15451554
@Cached @Shared RubyStringLibrary libFormat,
1555+
@Cached @Shared RubyStringLibrary libBuffer,
15461556
@Cached @Shared WriteObjectFieldNode writeAssociatedNode,
15471557
@Cached @Shared TruffleString.FromByteArrayNode fromByteArrayNode,
1558+
@Cached @Shared TruffleString.CopyToByteArrayNode copyToByteArrayNode,
15481559
@Cached ToJavaStringNode toJavaStringNode,
15491560
@Cached IndirectCallNode callPackNode) {
15501561
final String formatString = toJavaStringNode.execute(node, format);
1562+
final RootCallTarget formatCallTarget = compileFormat(node, formatString);
1563+
final byte[] bytes = initOutputBytes(buffer, libBuffer, formatCallTarget, copyToByteArrayNode);
15511564

15521565
final BytesResult result;
1566+
15531567
try {
15541568
result = (BytesResult) callPackNode.call(
1555-
compileFormat(node, formatString),
1556-
new Object[]{ array.getStore(), array.size, false, null });
1569+
formatCallTarget,
1570+
new Object[]{ array.getStore(), array.size, bytes, libBuffer.byteLength(buffer) });
15571571
} catch (FormatException e) {
15581572
exceptionProfile.enter(node);
15591573
throw FormatExceptionTranslator.translate(getContext(node), node, e);
@@ -1591,6 +1605,20 @@ protected static RootCallTarget compileFormat(Node node, String format) {
15911605
}
15921606
}
15931607

1608+
private static byte[] initOutputBytes(Object buffer, RubyStringLibrary libBuffer,
1609+
RootCallTarget formatCallTarget, TruffleString.CopyToByteArrayNode copyToByteArrayNode) {
1610+
int bufferLength = libBuffer.byteLength(buffer);
1611+
var formatRootNode = (FormatRootNode) formatCallTarget.getRootNode();
1612+
int expectedLength = formatRootNode.getExpectedLength();
1613+
1614+
// output buffer should be at least expectedLength to not mess up the expectedLength's logic
1615+
final int length = Math.max(bufferLength, expectedLength);
1616+
final byte[] bytes = new byte[length];
1617+
copyToByteArrayNode.execute(libBuffer.getTString(buffer), 0, bytes, 0, bufferLength,
1618+
libBuffer.getTEncoding(buffer));
1619+
return bytes;
1620+
}
1621+
15941622
protected int getCacheLimit() {
15951623
return getLanguage().options.PACK_CACHE;
15961624
}

src/main/java/org/truffleruby/core/format/FormatRootNode.java

+20-3
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,39 @@ public final class FormatRootNode extends RubyBaseRootNode implements InternalRo
3030
@Child private FormatNode child;
3131

3232
@CompilationFinal private int expectedLength = 0;
33+
private final boolean acceptOutput;
34+
private final boolean acceptOutputPosition;
3335

3436
public FormatRootNode(
3537
RubyLanguage language,
3638
SourceSection sourceSection,
3739
FormatEncoding encoding,
38-
FormatNode child) {
40+
FormatNode child,
41+
boolean acceptOutput,
42+
boolean acceptOutputPosition) {
3943
super(language, FormatFrameDescriptor.FRAME_DESCRIPTOR, sourceSection);
4044
this.encoding = encoding;
4145
this.child = child;
46+
this.acceptOutput = acceptOutput;
47+
this.acceptOutputPosition = acceptOutputPosition;
4248
}
4349

50+
/** Accepts the following arguments stored in a frame: source array, its length, output buffer as a bytes array,
51+
* (optional) position in the output buffer to start from */
4452
@SuppressWarnings("unchecked")
4553
@Override
4654
public Object execute(VirtualFrame frame) {
4755
frame.setObject(FormatFrameDescriptor.SOURCE_SLOT, frame.getArguments()[0]);
4856
frame.setInt(FormatFrameDescriptor.SOURCE_END_POSITION_SLOT, (int) frame.getArguments()[1]);
4957
frame.setInt(FormatFrameDescriptor.SOURCE_START_POSITION_SLOT, 0);
5058
frame.setInt(FormatFrameDescriptor.SOURCE_POSITION_SLOT, 0);
51-
frame.setObject(FormatFrameDescriptor.OUTPUT_SLOT, new byte[expectedLength]);
52-
frame.setInt(FormatFrameDescriptor.OUTPUT_POSITION_SLOT, 0);
59+
60+
final byte[] outputInit = acceptOutput ? (byte[]) frame.getArguments()[2] : new byte[expectedLength];
61+
frame.setObject(FormatFrameDescriptor.OUTPUT_SLOT, outputInit);
62+
63+
final int outputPosition = acceptOutputPosition ? (int) frame.getArguments()[3] : 0;
64+
frame.setInt(FormatFrameDescriptor.OUTPUT_POSITION_SLOT, outputPosition);
65+
5366
frame.setObject(FormatFrameDescriptor.ASSOCIATED_SLOT, null);
5467

5568
child.execute(frame);
@@ -95,6 +108,10 @@ public String getName() {
95108
return "format";
96109
}
97110

111+
public int getExpectedLength() {
112+
return expectedLength;
113+
}
114+
98115
@Override
99116
public String toString() {
100117
return getName();

src/main/java/org/truffleruby/core/format/pack/PackCompiler.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ public RootCallTarget compile(String format) throws DeferredRaiseException {
4646
language,
4747
currentNode.getEncapsulatingSourceSection(),
4848
builder.getEncoding(),
49-
builder.getNode()).getCallTarget();
49+
builder.getNode(),
50+
true,
51+
true).getCallTarget();
5052
}
5153

5254
}

src/main/java/org/truffleruby/core/format/printf/PrintfCompiler.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ public RootCallTarget compile(AbstractTruffleString tstring, RubyEncoding encodi
4646
language,
4747
currentNode.getEncapsulatingSourceSection(),
4848
new FormatEncoding(encoding),
49-
builder.getNode()).getCallTarget();
49+
builder.getNode(),
50+
false,
51+
false).getCallTarget();
5052
}
5153

5254
}

src/main/java/org/truffleruby/core/format/rbsprintf/RBSprintfCompiler.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ public RootCallTarget compile(AbstractTruffleString formatTString, RubyEncoding
5151
language,
5252
currentNode.getEncapsulatingSourceSection(),
5353
new FormatEncoding(formatEncoding),
54-
builder.getNode()).getCallTarget();
54+
builder.getNode(),
55+
false,
56+
false).getCallTarget();
5557
}
5658

5759
private static int SIGN = 0x10;

src/main/ruby/truffleruby/core/array.rb

+14
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,20 @@ def last(n = undefined)
719719
Array.new self[-n..-1]
720720
end
721721

722+
def pack(format, buffer: nil)
723+
if Primitive.nil? buffer
724+
Primitive.array_pack(self, format, '')
725+
else
726+
unless Primitive.is_a?(buffer, String)
727+
raise TypeError, "buffer must be String, not #{Primitive.class(buffer)}"
728+
end
729+
730+
string = Primitive.array_pack(self, format, buffer)
731+
buffer.replace string.force_encoding(buffer.encoding)
732+
end
733+
end
734+
Truffle::Graal.always_split instance_method(:pack)
735+
722736
def permutation(num = undefined, &block)
723737
unless block_given?
724738
return to_enum(:permutation, num) do
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/pack.rb:13:in `to_str': message (RuntimeError)
2-
from /pack.rb:18:in `pack'
2+
from <internal:core> core/array.rb:LINE:in `pack'
33
from /pack.rb:18:in `block in <main>'
44
from /backtraces.rb:17:in `check'
55
from /pack.rb:17:in `<main>'

0 commit comments

Comments
 (0)