Repository navigation
Adds some utilities for Ion 1.0 BytecodeGenerators #1124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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()}") | ||
| } | ||
| } | ||
| } | ||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| } | ||
| 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") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
There was a problem hiding this comment.
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:
0x40with low nibble0x01-0x03,0x05-0x07, and0x09-0x0Eare illegal - only L=4 (for FP32), L=8 (FP64), L=0 (0e0) and L=15 (null) are supported for float0x60with low nibble0x00/0x01is illegal - timestamps require at least offset and year0xE0with low nibble0x01-0x02is illegal - annotations require the annot_length field, at least one annotation and the valueThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!