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
114 changes: 114 additions & 0 deletions src/main/java/com/amazon/ion/bytecode/bin10/TypeIdHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion.bytecode.bin10

import com.amazon.ion.IonType
import com.amazon.ion.bytecode.ir.OperationKind
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings

internal object TypeIdHelper {

/**
* Returns the IonType for a legal Ion 1.0 typeId.
*
* The returned value is undefined when attempting to use this function for illegal type IDs.
*/
@JvmStatic
fun ionTypeForTypeId(typeId: Int): IonType? {
return when (typeId shr 4) {
0x0 -> if (typeId == 0x0F) IonType.NULL else null
0x1 -> IonType.BOOL
0x2, 0x3 -> IonType.INT
0x4 -> IonType.FLOAT
0x5 -> IonType.DECIMAL
0x6 -> IonType.TIMESTAMP
0x7 -> IonType.SYMBOL
0x8 -> IonType.STRING
0x9 -> IonType.CLOB
0xA -> IonType.BLOB
0xB -> IonType.LIST
0xC -> IonType.SEXP
0xD -> IonType.STRUCT
else -> null
}
}

/**
* Returns the [OperationKind] for an Ion 1.0 typeId.
*/
@JvmStatic
fun operationKindForTypeId(typeId: Int): Int = typeToOperationKindLookup[typeId]

private val typeToOperationKindLookup = IntArray(256) { initOperationKindForType(it) }

private fun initOperationKindForType(state: Int): Int {
return when (state) {
in 0x00..0x0E -> OperationKind.UNSET
0x0F -> OperationKind.NULL
0x10, 0x11, 0x1F -> OperationKind.BOOL
in 0x20..0x2F -> OperationKind.INT
in 0x31..0x3F -> OperationKind.INT
0x40, 0x44, 0x48, 0x4F -> OperationKind.FLOAT
in 0x50..0x5F -> OperationKind.DECIMAL
in 0x62..0x6F -> OperationKind.TIMESTAMP
in 0x70..0x7F -> OperationKind.SYMBOL
in 0x80..0x8F -> OperationKind.STRING
in 0x90..0x9F -> OperationKind.CLOB
in 0xA0..0xAF -> OperationKind.BLOB
in 0xB0..0xBF -> OperationKind.LIST
in 0xC0..0xCF -> OperationKind.SEXP
0xD0, in 0xD2..0xDF -> OperationKind.STRUCT
0xE0 -> OperationKind.IVM
in 0xE3..0xEE -> OperationKind.ANNOTATIONS
// Everything else: 12..1E, 30, D1, EF, F0..FF, illegal timestamp, float, and annotations sizes
else -> OperationKind.UNSET
}
}
Comment on lines +44 to +66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are excluding some illegal typeIDs here (int 0 with type code 3, bools that are not true/false/null, null annotation, etc). but there are still some more that you are not excluding:

  • 0x40 with low nibble 0x01-0x03, 0x05-0x07, and 0x09-0x0E are illegal - only L=4 (for FP32), L=8 (FP64), L=0 (0e0) and L=15 (null) are supported for float
  • 0x60 with low nibble 0x00/0x01 is illegal - timestamps require at least offset and year
  • 0xE0 with low nibble 0x01-0x02 is illegal - annotations require the annot_length field, at least one annotation and the value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!


/**
* A lookup table containing the value length for Ion 1.0 type IDs.
* A value of -1 indicates that the length follows as a `VarUInt`.
* A value of -2 indicates that the typeId is not a valid Ion 1.0 typeId.
*
* TODO(perf): If there's not a noticeable amount of overhead, we should hide this behind a method instead of
* exposing it directly and suppressing the spotbugs error.
*/
@JvmStatic
@get:SuppressFBWarnings("MS_EXPOSE_REP", justification = "it is exposed for internal use only as a performance optimization")
val TYPE_LENGTHS = IntArray(256) { initTypeLength(it) }

@JvmStatic
@OptIn(ExperimentalStdlibApi::class)
private fun initTypeLength(typeId: Int): Int {
return when (typeId) {
// The length of all of these is determined entirely by the low-nibble of the typeId.
0x00, 0x10, 0x20, 0x30, 0x40, 0x50, /* */ 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0 /* */ -> 0
0x01, /* */ 0x21, 0x31, /* */ 0x51, /* */ 0x71, 0x81, 0x91, 0xA1, 0xB1, 0xC1 /* */ /* */ -> 1
0x02, 0x12, 0x22, 0x32, /* */ 0x52, 0x62, 0x72, 0x82, 0x92, 0xA2, 0xB2, 0xC2, 0xD2 /* */ -> 2
0x03, 0x13, 0x23, 0x33, /* */ 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3 -> 3
0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4, 0xB4, 0xC4, 0xD4, 0xE4 -> 4
0x05, 0x15, 0x25, 0x35, /* */ 0x55, 0x65, 0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5 -> 5
0x06, 0x16, 0x26, 0x36, /* */ 0x56, 0x66, 0x76, 0x86, 0x96, 0xA6, 0xB6, 0xC6, 0xD6, 0xE6 -> 6
0x07, 0x17, 0x27, 0x37, /* */ 0x57, 0x67, 0x77, 0x87, 0x97, 0xA7, 0xB7, 0xC7, 0xD7, 0xE7 -> 7
0x08, 0x18, 0x28, 0x38, 0x48, 0x58, 0x68, 0x78, 0x88, 0x98, 0xA8, 0xB8, 0xC8, 0xD8, 0xE8 -> 8
0x09, 0x19, 0x29, 0x39, /* */ 0x59, 0x69, 0x79, 0x89, 0x99, 0xA9, 0xB9, 0xC9, 0xD9, 0xE9 -> 9
0x0A, 0x1A, 0x2A, 0x3A, /* */ 0x5A, 0x6A, 0x7A, 0x8A, 0x9A, 0xAA, 0xBA, 0xCA, 0xDA, 0xEA -> 10
0x0B, 0x1B, 0x2B, 0x3B, /* */ 0x5B, 0x6B, 0x7B, 0x8B, 0x9B, 0xAB, 0xBB, 0xCB, 0xDB, 0xEB -> 11
0x0C, 0x1C, 0x2C, 0x3C, /* */ 0x5C, 0x6C, 0x7C, 0x8C, 0x9C, 0xAC, 0xBC, 0xCC, 0xDC, 0xEC -> 12
0x0D, 0x1D, 0x2D, 0x3D, /* */ 0x5D, 0x6D, 0x7D, 0x8D, 0x9D, 0xAD, 0xBD, 0xCD, 0xDD, 0xED -> 13
0x0E, 0x1E, 0x2E, 0x3E, /* */ 0x5E, 0x6E, 0x7E, 0x8E, 0x9E, 0xAE, 0xBE, 0xCE, 0xDE, 0xEE -> -1
// Bool True
0x11 -> 0
// Nulls
0x0F, 0x1F, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x7F, 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF -> 0
// IVM
0xE0 -> 3 // ...3 more than the typeId byte.

// Reserved and/or illegal typeIds
0xEF, 0x60, 0x61, 0xD1, 0xE1, 0xE2,
in 0x41..0x4E, // Illegal float lengths. 0x44 and 0x48 are trapped in the earlier condition.
in 0xF0..0xFF -> -2
else -> TODO("This should be unreachable: ${typeId.toHexString()}")
}
}
}
134 changes: 134 additions & 0 deletions src/main/java/com/amazon/ion/bytecode/bin10/ValueHelpers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion.bytecode.bin10

import com.amazon.ion.Decimal
import com.amazon.ion.IonException
import com.amazon.ion.Timestamp
import java.math.BigDecimal
import java.math.BigInteger
import kotlin.experimental.and

/**
* Given a typeId in the range 0x20..0x3F, returns either -1 or 1.
* This uses some clever bit twiddling to avoid any branching.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to have some quantification of the benefit as an interesting learning. Not blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a quantification of the benefit. However, I've been trying to minimize hard-to-predict branching. I'll add a TODO to go and benchmark this vs branching later.

*
* (Yes, it's obtuse looking, but it works.)
*
* TODO(perf): Once reader is implemented enough for benchmarks, benchmark this vs using if/else.
*/
internal fun signForIntTypeId(typeId: Int): Int = (((typeId shr 4) shl 31) shr 31) or 1

/**
* Return either -1 or 1 based on the sign bit of the given byte. This uses some bit manipulation to avoid any branching.
*/
internal fun getSignumValueFromLeadingSignBit(byte: Byte): Int = byte.toInt().shr(7).shl(1) + 1

/**
* Reads a timestamp value from the given byte array.
*/
internal fun readTimestampReference(valueBytes: ByteArray, position: Int, length: Int): Timestamp {
var p = position
val end = position + length

val offset: Int? = if (valueBytes[p].toInt() and 0xFF == 0xC0) {
p++
null
} else {
val offsetValueAndLength = VarIntHelper.readVarIntValueAndLength(valueBytes, p)
p += offsetValueAndLength.toInt() and 0xFF
(offsetValueAndLength shr 8).toInt()
}
val yearValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p)
p += yearValueAndLength.toInt() and 0xFF
val year = (yearValueAndLength shr 8).toInt()
var month = 0
var day = 0
var hour = 0
var minute = 0
var second = 0
var fractionalSecond: BigDecimal? = null
var precision = Timestamp.Precision.YEAR
if (p < end) {
val monthValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p)
p += monthValueAndLength.toInt() and 0xFF
month = (monthValueAndLength shr 8).toInt()
precision = Timestamp.Precision.MONTH
if (p < end) {
val dayValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p)
p += dayValueAndLength.toInt() and 0xFF
day = (dayValueAndLength shr 8).toInt()
precision = Timestamp.Precision.DAY
if (p < end) {
val hourValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p)
p += hourValueAndLength.toInt() and 0xFF
hour = (hourValueAndLength shr 8).toInt()
if (p >= end) {
throw IonException("Timestamps may not specify hour without specifying minute.")
}

val minuteValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p)
p += minuteValueAndLength.toInt() and 0xFF
minute = (minuteValueAndLength shr 8).toInt()
precision = Timestamp.Precision.MINUTE
if (p < end) {
val secondValueAndLength = VarIntHelper.readVarUIntValueAndLength(valueBytes, p)
p += secondValueAndLength.toInt() and 0xFF
second = (secondValueAndLength shr 8).toInt()
precision = Timestamp.Precision.SECOND
if (p < end) {
fractionalSecond = readDecimalReference(valueBytes, p, end)
if (fractionalSecond.scale() < 0) {
fractionalSecond = fractionalSecond.setScale(0)
}
}
}
}
}
}
try {
return Timestamp.createFromUtcFields(
precision,
year,
month,
day,
hour,
minute,
second,
fractionalSecond,
offset
)
} catch (e: IllegalArgumentException) {
throw IonException("Illegal timestamp encoding at $position.", e)
}
}

/**
* Reads a Decimal value from the given byte array.
*/
internal fun readDecimalReference(valueBytes: ByteArray, position: Int, end: Int): Decimal {
var p = position
val exponentValueAndLength = VarIntHelper.readVarIntValueAndLength(valueBytes, p)
p += exponentValueAndLength.toInt() and 0xFF
val scale = -(exponentValueAndLength shr 8).toInt()

val coefficientLength = end - p
return if (coefficientLength > 0) {
// TODO: See if we can have a shared set of reusable buffers for this instead of allocating a copy.
val bytes = valueBytes.copyOfRange(p, p + coefficientLength)
Comment on lines +117 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One can dream...


// Get the signum
val signum = getSignumValueFromLeadingSignBit(bytes[0])
// Clear the sign bit
bytes[0] = bytes[0] and 0x7F
// Construct the BigInteger
val coefficient = BigInteger(signum, bytes)
if (coefficient == BigInteger.ZERO && signum == -1) {
Decimal.negativeZero(scale)
} else {
Decimal.valueOf(BigInteger(signum, bytes), scale)
}
} else {
Decimal.valueOf(BigInteger.ZERO, scale)
}
}
84 changes: 84 additions & 0 deletions src/main/java/com/amazon/ion/bytecode/bin10/VarIntHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion.bytecode.bin10

import com.amazon.ion.IonException

object VarIntHelper {

private const val TERMINATION_BIT_MASK = 0b10000000
private const val MASK_7_BITS = 0b01111111

/**
* Returns an unsigned integer up to 7 bytes, with an 1 byte integer signifying how many varuint bytes were used in its encoding.
*/
@JvmStatic
fun readVarUIntValueAndLength(source: ByteArray, position: Int): Long {
val currentByte: Int = source[position].toInt()
val result = (currentByte and MASK_7_BITS).toLong()
return if (currentByte and TERMINATION_BIT_MASK != 0) {
(result shl 8) or 1L
} else {
readVarUIntValueAndLength2(source, position + 1, result)
}
}

@JvmStatic
private fun readVarUIntValueAndLength2(source: ByteArray, position: Int, partialResult: Long): Long {
val currentByte: Int = source.get(position).toInt()
val result = (partialResult shl 7) or (currentByte and MASK_7_BITS).toLong()
if (currentByte and TERMINATION_BIT_MASK != 0) {
return (result shl 8) or 2
} else {
return readVarUIntValueAndLength3Plus(source, position + 1, result)
}
}

@JvmStatic
private fun readVarUIntValueAndLength3Plus(source: ByteArray, position: Int, partialResult: Long): Long {
var currentByte: Int
var result = partialResult
var p = position
var length = 2
do {
length++
if (length > 7) throw IonException("VarUInt value is too large")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be fine to put this after the loop.

currentByte = source.get(p++).toInt()
result = (result shl 7) or (currentByte and MASK_7_BITS).toLong()
} while (currentByte and TERMINATION_BIT_MASK == 0)

return (result shl 8) or length.toLong()
}

/**
* Returns a signed integer up to 7 bytes, with an 1 byte integer signifying how many varuint bytes were used in its encoding.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like bad things could happen if the VarInt exceeds 7 bytes, so we probably need to throw if that happens. I know it adds a branch, but I don't know how to avoid it safely. I'd imagine the branch would ~always be predicted correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call.

*/
@JvmStatic
fun readVarIntValueAndLength(source: ByteArray, position: Int): Long {
var p = position

var length = 1
try {
var currentByte = source[p++].toInt() and 0xFF
var result = (currentByte and 0b00111111).toLong()
val sign = getSignumValueFromVarIntSignBit(currentByte)
while (currentByte and TERMINATION_BIT_MASK == 0) {
length++
if (length > 7) throw IonException("VarInt value is too large")
currentByte = source[p++].toInt()
result = (result shl 7) or (currentByte and MASK_7_BITS).toLong()
}
return ((sign * result) shl 8) or length.toLong()
} catch (e: ArrayIndexOutOfBoundsException) {
throw IonException("Incomplete VarInt at position $position", e)
}
}

/**
* Return either -1 or 1 based on the sign bit of the given byte. This uses some bit manipulation to avoid any branching.
*
* Visible only for testing.
*/
@JvmStatic
internal fun getSignumValueFromVarIntSignBit(byte: Int): Int = byte.shl(25).shr(31).shl(1) + 1
}
8 changes: 8 additions & 0 deletions src/test/java/com/amazon/ion/TextToBinaryUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ object TextToBinaryUtils {
return bytesAsBytes
}

/**
* Converts a string of binary octets, such as "10010111 00010011", to a byte array.
*/
@JvmStatic
fun String.binaryStringToByteArray(): ByteArray {
return octetStringToByteArray(this, 2)
}

/**
* Converts a string of hex octets, such as "BE EF", to a byte array.
*/
Expand Down
Loading
Loading