Skip to content

Commit

Permalink
Ensure keys are sorted.
Browse files Browse the repository at this point in the history
  • Loading branch information
wasabii committed Aug 3, 2024
1 parent 4f8d8c8 commit 5403fe3
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 0 deletions.
221 changes: 221 additions & 0 deletions src/IKVM.ByteCode.Tests/Writing/InstructionEncoderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
using System.Buffers;

using FluentAssertions;

using IKVM.ByteCode.Buffers;
using IKVM.ByteCode.Writing;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace IKVM.ByteCode.Tests.Writing
{

[TestClass]
public class InstructionEncoderTests
{

[TestMethod]
public void CanMarkShortLabel()
{
var blob = new BlobBuilder();
var code = new InstructionEncoder(blob);
var label = code.DefineLabel();

var branchOffset = code.Offset;
code.Branch(OpCode._goto, label);

var labelOffset = code.Offset;
code.MarkLabel(label);
code.OpCode(OpCode._return);

var b = new SequenceReader<byte>(new ReadOnlySequence<byte>(blob.ToArray()));

// start with instruction
b.TryRead(out byte opcode).Should().BeTrue();
opcode.Should().Be((byte)OpCode._goto);

// argument of branch instruction should be offset to label
b.TryReadBigEndian(out short argument).Should().BeTrue();
argument.Should().Be(checked((short)(labelOffset - branchOffset)));

// last value should be return instruction
b.TryRead(out byte returnInstruction).Should().BeTrue();
returnInstruction.Should().Be((byte)OpCode._return);
}

[TestMethod]
public void CanMarkLongLabel()
{
var blob = new BlobBuilder();
var code = new InstructionEncoder(blob);
var label = code.DefineLabel();

var branchOffset = code.Offset;
code.Branch(OpCode._goto_w, label);

var labelOffset = code.Offset;
code.MarkLabel(label);
code.OpCode(OpCode._return);

var b = new SequenceReader<byte>(new ReadOnlySequence<byte>(blob.ToArray()));

// start with instruction
b.TryRead(out byte opcode).Should().BeTrue();
opcode.Should().Be((byte)OpCode._goto_w);

// argument of branch instruction should be offset to label
b.TryReadBigEndian(out int argument).Should().BeTrue();
argument.Should().Be(labelOffset - branchOffset);

// last value should be return instruction
b.TryRead(out byte returnInstruction).Should().BeTrue();
returnInstruction.Should().Be((byte)OpCode._return);
}

[TestMethod]
public void CanRepeatLabel()
{
var blob = new BlobBuilder();
new InstructionEncoder(blob)
.DefineLabel(out var label)
.Label(label, 4)
.Label(label, 4)
.Label(label, 4)
.Label(label, 4)
.MarkLabel(label);

var b = new SequenceReader<byte>(new ReadOnlySequence<byte>(blob.ToArray()));

b.TryReadBigEndian(out int l1).Should().BeTrue();
b.TryReadBigEndian(out int l2).Should().BeTrue();
b.TryReadBigEndian(out int l3).Should().BeTrue();
b.TryReadBigEndian(out int l4).Should().BeTrue();

// labels values should be progrssively lower as we approach the end
l1.Should().Be(16);
l2.Should().Be(12);
l3.Should().Be(8);
l4.Should().Be(4);
}

[TestMethod]
public void CanEncodeTableSwitch()
{
var blob = new BlobBuilder();
var code = new InstructionEncoder(blob);

var defaultLabel = code.DefineLabel();
var case1Label = code.DefineLabel();
var case2Label = code.DefineLabel();

// tableswitch
var tableOffset = code.Offset;
code.TableSwitch(defaultLabel, 0, t => t.Case(case1Label).Case(case2Label));

// return code, all labels arrive here
var labelOffset = code.Offset;
code.MarkLabel(defaultLabel);
code.MarkLabel(case1Label);
code.MarkLabel(case2Label);
code.OpCode(OpCode._return);

var b = new SequenceReader<byte>(new ReadOnlySequence<byte>(blob.ToArray()));

// start with instruction
b.TryRead(out byte opcode).Should().BeTrue();
opcode.Should().Be((byte)OpCode._tableswitch);

// advance past padding
b.TryRead(out byte _).Should().BeTrue();
b.TryRead(out byte _).Should().BeTrue();
b.TryRead(out byte _).Should().BeTrue();

// default value should be 32 bit signed integer which is offset from instruction to label
b.TryReadBigEndian(out int defaultValue).Should().BeTrue();
defaultValue.Should().Be(labelOffset - tableOffset);

// low should be starting value: 0
b.TryReadBigEndian(out int low).Should().BeTrue();
low.Should().Be(0);

// high should be ending value: 1
b.TryReadBigEndian(out int high).Should().BeTrue();
high.Should().Be(1);

// first case should be distance from instruction to return
b.TryReadBigEndian(out int case1Offset).Should().BeTrue();
case1Offset.Should().Be(labelOffset - tableOffset);

// second case should be distance from instruction to return
b.TryReadBigEndian(out int case2Offset).Should().BeTrue();
case2Offset.Should().Be(labelOffset - tableOffset);

// last value should be return instruction
b.TryRead(out byte returnInstruction).Should().BeTrue();
returnInstruction.Should().Be((byte)OpCode._return);
}

[TestMethod]
public void CanEncodeLookupSwitch()
{
var blob = new BlobBuilder();
var code = new InstructionEncoder(blob);

var defaultLabel = code.DefineLabel();
var case1Label = code.DefineLabel();
var case2Label = code.DefineLabel();

// tableswitch
var lookupOffset = code.Offset;
code.LookupSwitch(defaultLabel, e => e.Case(1, case1Label).Case(2, case2Label));

// return code, all labels arrive here
var labelOffset = code.Offset;
code.MarkLabel(defaultLabel);
code.MarkLabel(case1Label);
code.MarkLabel(case2Label);
code.OpCode(OpCode._return);

var b = new SequenceReader<byte>(new ReadOnlySequence<byte>(blob.ToArray()));

// start with instruction
b.TryRead(out byte opcode).Should().BeTrue();
opcode.Should().Be((byte)OpCode._lookupswitch);

// advance past padding
b.TryRead(out byte _).Should().BeTrue();
b.TryRead(out byte _).Should().BeTrue();
b.TryRead(out byte _).Should().BeTrue();

// default value should be 32 bit signed integer which is offset from instruction to label
b.TryReadBigEndian(out int defaultValue).Should().BeTrue();
defaultValue.Should().Be(labelOffset - lookupOffset);

// npairs, count of pairs
b.TryReadBigEndian(out int npairs).Should().BeTrue();
npairs.Should().Be(2);

// first case key
b.TryReadBigEndian(out int case1Key).Should().BeTrue();
case1Key.Should().Be(1);

// first case offset should be distance from instruction to return
b.TryReadBigEndian(out int case1Offset).Should().BeTrue();
case1Offset.Should().Be(labelOffset - lookupOffset);

// second case key
b.TryReadBigEndian(out int case2Key).Should().BeTrue();
case2Key.Should().Be(2);

// second case offset should be distance from instruction to return
b.TryReadBigEndian(out int case2Offset).Should().BeTrue();
case2Offset.Should().Be(labelOffset - lookupOffset);

// last value should be return instruction
b.TryRead(out byte returnInstruction).Should().BeTrue();
returnInstruction.Should().Be((byte)OpCode._return);
}

}

}
6 changes: 6 additions & 0 deletions src/IKVM.ByteCode/Writing/EncoderAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace IKVM.ByteCode.Writing
{

public delegate void EncoderAction<T>(in T encoder) where T : struct;

}
64 changes: 64 additions & 0 deletions src/IKVM.ByteCode/Writing/LookupSwitchInstructionEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Buffers.Binary;

using IKVM.ByteCode.Buffers;

namespace IKVM.ByteCode.Writing
{

/// <summary>
/// Encodes a lookupswitch instruction.
/// </summary>
public struct LookupSwitchInstructionEncoder
{

readonly InstructionEncoder _encoder;
readonly int _offset;
readonly Blob _npairsBlob;
int _npairs;
long _lastKey = long.MinValue;

/// <summary>
/// Initializes a new instance.
/// </summary>
/// <param name="encoder"></param>
/// <param name="defaultLabel"></param>
/// <param name="low"></param>
/// <exception cref="ArgumentNullException"></exception>
public LookupSwitchInstructionEncoder(InstructionEncoder encoder, LabelHandle defaultLabel)
{
_encoder = encoder ?? throw new ArgumentNullException(nameof(encoder));

// header of table switch is instruction, alignment, then default
_offset = _encoder.Offset;
_encoder.OpCode(OpCode._lookupswitch);
_encoder.Align(4);
_encoder.Label(defaultLabel, 4, _offset);

// reserve space for high value and start at low
_npairs = 0;
_npairsBlob = encoder.ReserveBytes(4);
BinaryPrimitives.WriteInt32BigEndian(_npairsBlob.GetBytes(), _npairs);
}

/// <summary>
/// Adds a new case to the lookup switch.
/// </summary>
/// <param name="key"></param>
/// <param name="label"></param>
/// <returns></returns>
public LookupSwitchInstructionEncoder Case(int key, LabelHandle label)
{
if (key < _lastKey)
throw new ArgumentOutOfRangeException("Key must be greater than previos key.", nameof(key));

_lastKey = key;
_encoder.WriteInt32(key);
_encoder.Label(label, 4, _offset);
BinaryPrimitives.WriteInt32BigEndian(_npairsBlob.GetBytes(), ++_npairs);
return this;
}

}

}
72 changes: 72 additions & 0 deletions src/IKVM.ByteCode/Writing/TableSwitchInstructionEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Buffers.Binary;

using IKVM.ByteCode.Buffers;

namespace IKVM.ByteCode.Writing
{

/// <summary>
/// Encodes multiple table switch arguments.
/// </summary>
public class TableSwitchInstructionEncoder
{

readonly InstructionEncoder _encoder;
readonly int _offset;
readonly int _low;
readonly Blob _highBlob;
int _high;

/// <summary>
/// Initializes a new instance.
/// </summary>
/// <param name="encoder"></param>
/// <param name="defaultLabel"></param>
/// <param name="low"></param>
/// <exception cref="ArgumentNullException"></exception>
public TableSwitchInstructionEncoder(InstructionEncoder encoder, LabelHandle defaultLabel, int low)
{
_encoder = encoder ?? throw new ArgumentNullException(nameof(encoder));

// header of table switch is instruction, alignment, then default
_offset = _encoder.Offset;
_encoder.OpCode(OpCode._tableswitch);
_encoder.Align(4);
_encoder.Label(defaultLabel, 4, _offset);

// write low value
_low = low;
encoder.WriteInt32(_low);

// reserve space for high value and start at low
_high = low - 1;
_highBlob = encoder.ReserveBytes(4);
BinaryPrimitives.WriteInt32BigEndian(_highBlob.GetBytes(), _high);
}

/// <summary>
/// Adds a new case to the table switch.
/// </summary>
/// <param name="label"></param>
/// <returns></returns>
public TableSwitchInstructionEncoder Case(LabelHandle label)
{
_encoder.Label(label, 4, _offset);
BinaryPrimitives.WriteInt32BigEndian(_highBlob.GetBytes(), ++_high);
return this;
}

/// <summary>
/// Validates whether we have written correct values.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public void Validate()
{
if (_high < _low)
throw new InvalidOperationException("TableSwitch requires at least one item.");
}

}

}

0 comments on commit 5403fe3

Please sign in to comment.