diff --git a/build.gradle.kts b/build.gradle.kts index 5e161c1ff1..5725892261 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,6 +82,9 @@ dependencies { testImplementation("pl.pragmatists:JUnitParams:1.1.1") testImplementation("com.google.code.tempus-fugit:tempus-fugit:1.1") testImplementation("com.github.luben:zstd-jni:1.5.6-5") + + // For @SuppressFBWarning annotation. Only needed for SpotBugs static analysis. + compileOnly("com.github.spotbugs:spotbugs-annotations:4.9.5") } group = "com.amazon.ion" diff --git a/config/proguard/rules.pro b/config/proguard/rules.pro index a06cdf2f31..0d1d56cd96 100644 --- a/config/proguard/rules.pro +++ b/config/proguard/rules.pro @@ -3,3 +3,5 @@ -dontoptimize -dontobfuscate -dontwarn java.sql.Timestamp +# We don't need this at runtime, so it's okay if it's missing. +-dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings diff --git a/src/main/java/com/amazon/ion/_private_/SuppressFBWarnings.kt b/src/main/java/com/amazon/ion/_private_/SuppressFBWarnings.kt deleted file mode 100644 index 806cf3638f..0000000000 --- a/src/main/java/com/amazon/ion/_private_/SuppressFBWarnings.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.amazon.ion._private_ - -/** - * Suppress individual Spotbugs warnings. - */ -annotation class SuppressFBWarnings(val value: Array, val justification: String) diff --git a/src/main/java/com/amazon/ion/bytecode/BytecodeGenerator.kt b/src/main/java/com/amazon/ion/bytecode/BytecodeGenerator.kt new file mode 100644 index 0000000000..ce60270c05 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/BytecodeGenerator.kt @@ -0,0 +1,97 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode + +import com.amazon.ion.Decimal +import com.amazon.ion.Timestamp +import com.amazon.ion.bytecode.util.AppendableConstantPoolView +import com.amazon.ion.bytecode.util.BytecodeBuffer +import java.math.BigInteger + +/** + * Abstracts a particular input source (e.g. [ByteArray], [ByteBuffer][java.nio.ByteBuffer]) and Ion version out + * of the [BytecodeIonReader]. + * + * The BytecodeGenerator serves as an adapter layer between the [BytecodeIonReader] and various Ion data sources, + * enabling efficient reading of Ion data by converting it to bytecode instructions. This abstraction allows the + * reader to work with different input formats and Ion versions while minimizing branching in the hot paths. It + * also allows the source data to be read using a "push" model (in chunks), decoupling it from the "pull" model + * of the IonReader API. + * + * ## Usage Pattern + * + * The typical interaction flow is: + * 1. [BytecodeIonReader] calls [refill] to populate its bytecode buffer with instructions + * 2. As the reader processes bytecode, it calls the appropriate `read*Reference` methods to resolve + * scalar values that reference the original source data + * 3. When an IVM is encountered, [getGeneratorForMinorVersion] may be called to switch versions + * + * ## Implementation Notes + * + * In the future, it might be possible to push all the encoding context management into this layer, which _might_ + * provide some performance benefits by reducing the need to switch context between the BytecodeGenerator and the BytecodeIonReader. + * + * It would be possible to simplify this interface by replacing all the `read*` functions with a single function, such as this: + * ``` + * fun readReference(instruction: Int, operand: Int): T + * ``` + * However, this seems like it would probably have a negative impact on the throughput of the reader because the call-site + * for the function is already in a branch for that specific kind of reference, so the single function approach would + * require additional branching inside the implementation of `readReference`. + * + * If we add references with int64 positions, add overrides of the `read*` methods that support a `long` position. + */ +internal interface BytecodeGenerator { + + // TODO: Does this method need to return the symbol table and/or constant pool as well? + // No, we're not going to update the encoding context in the bytecode generator. + // That might limit the applicability because the bytecode needs to contain directives + // for all possible Ion Versions... but we'll deal with that later. + /** + * Refills [destination] with one or more top-level user values and optionally + * one system value. When a system value (symbol table, directive, IVM) is encountered, + * no more top-level values may be filled into the destination. + * + * If there is incomplete data, and no values can be returned, it should + * throw IncompleteDataException (if streaming source) or IonException (if + * fully-buffered source). + */ + fun refill( + /** The BytecodeBuffer that is to be filled with the bytecode */ + destination: BytecodeBuffer, + /** A container for holding instances of eagerly materialized values, such as those in template definitions. */ + constantPool: AppendableConstantPoolView, + /** Bytecode for each macro in the effective macro table */ + macroSrc: IntArray, + /** + * A lookup table indicating for each macro address, where to find the first + * instruction for that macro in [macroSrc]. For example, to read the bytecode for the macro, + * you would do something like this: + * ``` + * var i = macroIndices[macroAddress] + * var currentInstruction = macroSrc[i++] + * while (currentInstruction. + * ``` + */ + macroIndices: IntArray, + /** The current symbol table */ + symTab: Array, + ) + + fun readBigIntegerReference(position: Int, length: Int): BigInteger + fun readDecimalReference(position: Int, length: Int): Decimal + fun readShortTimestampReference(position: Int, opcode: Int): Timestamp + fun readTimestampReference(position: Int, length: Int): Timestamp + fun readTextReference(position: Int, length: Int): String + fun readBytesReference(position: Int, length: Int): ByteArray + + /** The Ion Minor Version supported by this [BytecodeGenerator] */ + fun ionMinorVersion(): Int + + /** + * When the [BytecodeIonReader] encounters an IVM that requires a version change, it will call this method. + * Implementations must return a BytecodeGenerator that supports the requested version and is positioned + * to continue compiling bytecode exactly where this BytecodeGenerator left off. + */ + fun getGeneratorForMinorVersion(minorVersion: Int): BytecodeGenerator = throw UnsupportedOperationException() +} diff --git a/src/main/java/com/amazon/ion/bytecode/BytecodeIonReader.kt b/src/main/java/com/amazon/ion/bytecode/BytecodeIonReader.kt new file mode 100644 index 0000000000..8ad878fe95 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/BytecodeIonReader.kt @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +/** + * TODO: This class should implement [IonReader] for the Bytecode IR. + */ +internal class BytecodeIonReader +@SuppressFBWarnings("URF_UNREAD_FIELD", justification = "field will be read once this class is implemented") +constructor( + private var bytecodeGenerator: BytecodeGenerator, +) diff --git a/src/main/java/com/amazon/ion/bytecode/bin10/ByteArrayBytecodeGenerator10.kt b/src/main/java/com/amazon/ion/bytecode/bin10/ByteArrayBytecodeGenerator10.kt new file mode 100644 index 0000000000..73964cbd49 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin10/ByteArrayBytecodeGenerator10.kt @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +@SuppressFBWarnings("EI_EXPOSE_REP2", justification = "constructor does not make a defensive copy of source as a performance optimization") +internal class ByteArrayBytecodeGenerator10 +@SuppressFBWarnings("URF_UNREAD_FIELD", justification = "field will be read once this class is implemented") +constructor( + private val source: ByteArray, + private var i: Int, +) { + // TODO: This should implement BytecodeGenerator +} diff --git a/src/main/java/com/amazon/ion/bytecode/bin10/InputStreamBytecodeGenerator10.kt b/src/main/java/com/amazon/ion/bytecode/bin10/InputStreamBytecodeGenerator10.kt new file mode 100644 index 0000000000..42383fcb83 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin10/InputStreamBytecodeGenerator10.kt @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin10 + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import java.io.InputStream + +internal class InputStreamBytecodeGenerator10 +@SuppressFBWarnings("URF_UNREAD_FIELD", justification = "field will be read once this class is implemented") +constructor( + private val source: InputStream, + private var i: Long, +) { + // TODO: This should implement BytecodeGenerator +} diff --git a/src/main/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11.kt b/src/main/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11.kt new file mode 100644 index 0000000000..e28c6ab483 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin11/ByteArrayBytecodeGenerator11.kt @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin11 + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +@SuppressFBWarnings("EI_EXPOSE_REP2", justification = "constructor does not make a defensive copy of source as a performance optimization") +internal class ByteArrayBytecodeGenerator11 +@SuppressFBWarnings("URF_UNREAD_FIELD", justification = "field will be read once this class is implemented") +constructor( + private val source: ByteArray, + private var i: Int, +) { + // TODO: This should implement BytecodeGenerator +} diff --git a/src/main/java/com/amazon/ion/bytecode/bin11/InputStreamBytecodeGenerator11.kt b/src/main/java/com/amazon/ion/bytecode/bin11/InputStreamBytecodeGenerator11.kt new file mode 100644 index 0000000000..f7b9d9de91 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin11/InputStreamBytecodeGenerator11.kt @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin11 + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import java.io.InputStream + +internal class InputStreamBytecodeGenerator11 +@SuppressFBWarnings("URF_UNREAD_FIELD", justification = "field will be read once this class is implemented") +constructor( + private val source: InputStream, + private var i: Long, +) { + // TODO: This should implement BytecodeGenerator +} diff --git a/src/main/java/com/amazon/ion/bytecode/bin11/OpCode.kt b/src/main/java/com/amazon/ion/bytecode/bin11/OpCode.kt new file mode 100644 index 0000000000..ea06da069a --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/bin11/OpCode.kt @@ -0,0 +1,142 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.bin11 + +/** + * Constants representing Ion 1.1 opcode values. + * + * Not all opcodes are represented here—when there are a range of opcodes for a particular thing (macro addresses or + * length prefixed values), we don't necessarily want to write code for each of these opcodes individually. + * + * For dealing with tagless encodings, there are a few supplemental/replacement opcodes that have a `TE` prefix (for + * tagless encoding) on the name of the constant. See also [TaglessScalarType]. + * + * TODO: Consider whether there is some other location more suitable for this class. + */ +internal object OpCode { + const val MACRO_0 = 0x00 + + const val MACRO_47 = 0x47 + + const val EXTENSIBLE_MACRO_ADDRESS_0 = 0x48 + const val EXTENSIBLE_MACRO_ADDRESS_7 = 0x4F + + const val SYMBOL_SID_FLEX_0 = 0x50 + const val SYMBOL_SID_FLEX_7 = 0x57 + + const val ANNOTATION_SID = 0x58 + const val ANNOTATION_TEXT = 0x59 + + const val RESERVED_5A = 0x5A + + const val TAGLESS_ELEMENT_LIST = 0x5B + const val TAGLESS_ELEMENT_SEXP = 0x5C + + const val RESERVED_5D = 0x5D + const val RESERVED_5E = 0x5E + const val RESERVED_5F = 0x5F + + const val INT_0 = 0x60 + const val INT_8 = 0x61 + const val INT_16 = 0x62 + const val INT_24 = 0x63 + const val INT_32 = 0x64 + const val INT_40 = 0x65 + const val INT_48 = 0x66 + const val INT_56 = 0x67 + const val INT_64 = 0x68 + + const val RESERVED_69 = 0x69 + + const val FLOAT_0 = 0x6A + const val FLOAT_16 = 0x6B + const val FLOAT_32 = 0x6C + const val FLOAT_64 = 0x6D + + const val BOOL_TRUE = 0x6E + const val BOOL_FALSE = 0x6F + + const val DECIMAL_0 = 0x70 + const val DECIMAL_LENGTH_15 = 0x7F + + const val TIMESTAMP_YEAR_PRECISION = 0x80 + const val TIMESTAMP_MONTH_PRECISION = 0x81 + const val TIMESTAMP_DAY_PRECISION = 0x82 + const val TIMESTAMP_MINUTE_PRECISION = 0x83 + const val TIMESTAMP_SECOND_PRECISION = 0x84 + const val TIMESTAMP_MILLIS_PRECISION = 0x85 + const val TIMESTAMP_MICROS_PRECISION = 0x86 + const val TIMESTAMP_NANOS_PRECISION = 0x87 + const val TIMESTAMP_MINUTE_PRECISION_WITH_OFFSET = 0x88 + const val TIMESTAMP_SECOND_PRECISION_WITH_OFFSET = 0x89 + const val TIMESTAMP_MILLIS_PRECISION_WITH_OFFSET = 0x8A + const val TIMESTAMP_MICROS_PRECISION_WITH_OFFSET = 0x8B + const val TIMESTAMP_NANOS_PRECISION_WITH_OFFSET = 0x8C + + const val RESERVED_8D = 0x8D + + const val NULL_NULL = 0x8E + const val TYPED_NULL = 0x8F + + const val STRING_LENGTH_0 = 0x90 + const val STRING_LENGTH_15 = 0x9F + + const val SYMBOL_LENGTH_0 = 0xA0 + const val SYMBOL_LENGTH_15 = 0xAF + + const val LIST_LENGTH_0 = 0xB0 + const val LIST_LENGTH_15 = 0xBF + + const val SEXP_LENGTH_0 = 0xC0 + const val SEXP_LENGTH_15 = 0xCF + + const val STRUCT_LENGTH_0 = 0xD0 + const val RESERVED_D1 = 0xD1 + const val STRUCT_LENGTH_15 = 0xDF + + const val IVM = 0xE0 + const val DIRECTIVE_SET_SYMBOLS = 0xE1 + const val DIRECTIVE_ADD_SYMBOLS = 0xE2 + const val DIRECTIVE_SET_MACROS = 0xE3 + const val DIRECTIVE_ADD_MACROS = 0xE4 + const val DIRECTIVE_USE = 0xE5 + const val DIRECTIVE_MODULE = 0xE6 + const val DIRECTIVE_ENCODING = 0xE7 + const val TAGGED_PLACEHOLDER = 0xE8 + const val TAGGED_PLACEHOLDER_WITH_DEFAULT = 0xE9 + const val TAGLESS_PLACEHOLDER = 0xEA + + const val NO_ARGUMENT = 0xEB + const val NOP = 0xEC + const val NOP_L = 0xED + + const val STRUCT_SWITCH_MODES = 0xEE + const val DELIMITED_CONTAINER_END = 0xEF + + const val DELIMITED_LIST = 0xF0 + const val DELIMITED_SEXP = 0xF1 + const val DELIMITED_STRUCT_SID_MODE = 0xF2 + const val DELIMITED_STRUCT_FS_MODE = 0xF3 + + const val LENGTH_PREFIXED_MACRO_INVOCATION = 0xF4 + const val VARIABLE_LENGTH_INTEGER = 0xF5 + const val VARIABLE_LENGTH_DECIMAL = 0xF6 + const val VARIABLE_LENGTH_TIMESTAMP = 0xF7 + const val VARIABLE_LENGTH_STRING = 0xF8 + const val VARIABLE_LENGTH_SYMBOL = 0xF9 + const val VARIABLE_LENGTH_LIST = 0xFA + const val VARIABLE_LENGTH_SEXP = 0xFB + const val VARIABLE_LENGTH_STRUCT_SID_MODE = 0xFC + const val VARIABLE_LENGTH_STRUCT_FS_MODE = 0xFD + const val VARIABLE_LENGTH_BLOB = 0xFE + const val VARIABLE_LENGTH_CLOB = 0xFF + + const val TE_FLEX_INT = 0x60 + const val TE_FLEX_UINT = 0xE0 + const val TE_UINT_8 = 0xE1 + const val TE_UINT_16 = 0xE2 + const val TE_UINT_32 = 0xE4 + const val TE_UINT_64 = 0xE8 + const val TE_SYMBOL_SID = 0xEA + const val TE_SYMBOL_FS = 0xEE +} diff --git a/src/main/java/com/amazon/ion/bytecode/ir/Debugger.kt b/src/main/java/com/amazon/ion/bytecode/ir/Debugger.kt index 8701df59d5..2814916642 100644 --- a/src/main/java/com/amazon/ion/bytecode/ir/Debugger.kt +++ b/src/main/java/com/amazon/ion/bytecode/ir/Debugger.kt @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.bytecode.ir -import com.amazon.ion._private_.SuppressFBWarnings +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import java.util.function.Consumer /** diff --git a/src/main/java/com/amazon/ion/bytecode/util/AppendableConstantPoolView.kt b/src/main/java/com/amazon/ion/bytecode/util/AppendableConstantPoolView.kt new file mode 100644 index 0000000000..1338a56759 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/util/AppendableConstantPoolView.kt @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.util + +/** + * A view of a [ConstantPool] that allows read and append operations. + */ +interface AppendableConstantPoolView { + /** Adds a value to the constant pool, returning the index assigned to the value. */ + fun add(value: Any?): Int + /** Retrieves a value from the constant pool. */ + fun get(i: Int): Any? +} diff --git a/src/main/java/com/amazon/ion/bytecode/util/ByteSlice.kt b/src/main/java/com/amazon/ion/bytecode/util/ByteSlice.kt new file mode 100644 index 0000000000..7b2903183f --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/util/ByteSlice.kt @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.util + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +/** + * Light-weight representation of a portion of a [ByteArray]. + * + * Positions are relative to `bytes`, not the underlying data stream. + * + * This is not intended to be exposed publicly, but it might end up needing to be exposed in order to expose a + * template-building API. If it does get exposed, we need to revisit the location of this class. + */ +@SuppressFBWarnings( + value = ["EI_EXPOSE_REP"], + justification = "This class is only for internal use. Exposing the internal array is intentional to avoid the performance overhead of copying it." +) +internal class ByteSlice( + val bytes: ByteArray, + val startInclusive: Int, + val endExclusive: Int +) { + val length = endExclusive - startInclusive + + /** + * Convenience method to create a new array that is a copy of this data represented by this [ByteSlice]. + */ + fun newByteArray() = bytes.copyOfRange(startInclusive, endExclusive) +} diff --git a/src/main/java/com/amazon/ion/bytecode/util/BytecodeBuffer.kt b/src/main/java/com/amazon/ion/bytecode/util/BytecodeBuffer.kt new file mode 100644 index 0000000000..5c8c4ce3e8 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/util/BytecodeBuffer.kt @@ -0,0 +1,276 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.util + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +/** + * This is a custom collection that allows unsafe access to the backing array for storing bytecode instructions. + * + * It allows us to build up the bytecode with the convenience of appending to a list, but when it comes time to read + * the bytecode, we can read it with the access efficiency of an array. + * + * It provides specialized methods for adding single values, pairs, triples, and slices of bytecode instructions + * efficiently. The buffer automatically grows as needed using a growth multiplier strategy. + * + * This class looks very similar to [ConstantPool], but this class is backed by an array of primitive integers + * rather + * + * Potential Performance Improvement: Consider exposing raw, `@JvmField`-annotated fields for `size` and `capacity` if it will improve the performance. + */ +internal class BytecodeBuffer private constructor( + private var data: IntArray, + private var numberOfValues: Int, +) { + companion object { + private const val GROWTH_MULTIPLIER = 2 + } + + /** + * Creates a new empty BytecodeBuffer with the specified initial capacity. + */ + constructor(initialCapacity: Int) : this(IntArray(initialCapacity), 0) + + @SuppressFBWarnings( + value = ["IE_EXPOSE_REP", "IE_EXPOSE_REP2"], + justification = "unsafeGetArray() intentionally exposes internal representation as a performance optimization" + ) + constructor() : this(IntArray(16), 0) + + private var capacity: Int = data.size + + /** + * Returns the current capacity of this `BytecodeBuffer`. + * The capacity represents the maximum number of elements that can be stored without reallocating the backing array. + */ + fun capacity() = capacity + + /** + * Returns the number of bytecode instructions currently stored in this `BytecodeBuffer`. + */ + fun size(): Int { + return numberOfValues + } + + /** + * Returns `true` if this `BytecodeBuffer` contains no bytecode instructions, `false` otherwise. + */ + fun isEmpty(): Boolean { + return numberOfValues == 0 + } + + /** + * Returns the bytecode instruction at the specified index. + * + * @param i the index of the bytecode instruction to retrieve + * @return the bytecode instruction at the specified index + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || index >= size()) + */ + fun get(i: Int): Int { + if (i < 0 || i >= numberOfValues) { + throw IndexOutOfBoundsException( + "Invalid index $i requested from BytecodeBuffer with $numberOfValues values." + ) + } + return data[i] + } + + /** + * Reserves space for one bytecode instruction without setting its value. + * This increases the size of the buffer by 1 and returns the index of the reserved position. + * + * @return the index of the reserved position that can be set later using [set] + */ + fun reserve(): Int { + return numberOfValues++ + } + + /** + * Appends a single bytecode instruction to this `BytecodeBuffer`. + * The buffer will automatically grow if necessary to accommodate the new instruction. + * + * @param value the bytecode instruction to add + */ + fun add(value: Int) { + val n = numberOfValues + val newNumberOfValues = n + 1 + val data: IntArray = ensureCapacity(newNumberOfValues) + data[n] = value + numberOfValues = newNumberOfValues + } + + /** + * Appends two bytecode instructions to this `BytecodeBuffer` in a single operation. + * This is more efficient than calling [add] twice. The buffer will automatically grow if necessary. + * + * @param value0 the first bytecode instruction to add + * @param value1 the second bytecode instruction to add + */ + fun add2(value0: Int, value1: Int) { + val n = numberOfValues + val newNumberOfValues = n + 2 + val data: IntArray = ensureCapacity(newNumberOfValues) + data[n] = value0 + data[n + 1] = value1 + numberOfValues = newNumberOfValues + } + + /** + * Appends three bytecode instructions to this `BytecodeBuffer` in a single operation. + * This is more efficient than calling [add] three times. The buffer will automatically grow if necessary. + * + * @param value0 the first bytecode instruction to add + * @param value1 the second bytecode instruction to add + * @param value2 the third bytecode instruction to add + */ + fun add3(value0: Int, value1: Int, value2: Int) { + val n = numberOfValues + val newNumberOfValues = n + 3 + val data: IntArray = ensureCapacity(newNumberOfValues) + data[n] = value0 + data[n + 1] = value1 + data[n + 2] = value2 + numberOfValues = newNumberOfValues + } + + /** + * Appends a slice of bytecode instructions from another `BytecodeBuffer` to this buffer. + * The buffer will automatically grow if necessary to accommodate the new instructions. + * + * @param values the source `BytecodeBuffer` to copy from + * @param startInclusive the starting index in the source buffer (inclusive) + * @param length the number of bytecode instructions to copy + */ + fun addSlice(values: BytecodeBuffer, startInclusive: Int, length: Int) { + val thisNumberOfValues = this.numberOfValues + val newNumberOfValues = thisNumberOfValues + length + val data = ensureCapacity(newNumberOfValues) + System.arraycopy(values.data, startInclusive, data, thisNumberOfValues, length) + this.numberOfValues = newNumberOfValues + } + + /** + * Empties this `BytecodeBuffer`, allowing bytecode instructions to be inserted at the beginning again. + * Note that this method does not shrink the size of the backing data store or modify the backing data store in any other way. + */ + fun clear() { + numberOfValues = 0 + } + + /** + * Truncates the bytecode buffer to the specified length, allowing new instructions to be inserted starting at that position. + * Note that this method does not shrink the size of the backing data store or modify the backing data store in any other way. + * + * @param n the new length of the buffer (must not exceed the current size) + * @throws IllegalArgumentException if n exceeds the current number of values + */ + fun truncate(n: Int) { + require(n <= numberOfValues) { "length exceeds number of values" } + this.numberOfValues = n + } + + /** + * Sets the bytecode instruction at the specified index to the given value. + * This is typically used in conjunction with [reserve] to set values at previously reserved positions. + * + * @param index the index of the bytecode instruction to set + * @param value the new bytecode instruction value + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || index >= size()) + */ + fun set(index: Int, value: Int) { + if (index < 0 || index >= numberOfValues) { + throw java.lang.IndexOutOfBoundsException() + } + data[index] = value + } + + private fun ensureCapacity(minCapacity: Int): IntArray { + val capacity: Int = this.capacity + if (minCapacity > capacity) { + return grow(minCapacity) + } + return data + } + + private fun grow(minCapacity: Int): IntArray { + // TODO: Consider making it grow to the next power of 2 instead of just growing to double the required capacity. + val newCapacity = minCapacity * GROWTH_MULTIPLIER + val newData = IntArray(newCapacity) + System.arraycopy(data, 0, newData, 0, capacity) + this.data = newData + this.capacity = newCapacity + return newData + } + + /** + * Returns an array that is the same size as this `BytecodeBuffer`. The returned array is a defensive copy, and will + * not be modified by any mutating methods of this `BytecodeBuffer`. + */ + fun toArray(): IntArray { + val thisNumberOfValues = this.numberOfValues + val copy = IntArray(thisNumberOfValues) + System.arraycopy(data, 0, copy, 0, thisNumberOfValues) + return copy + } + + /** + * Gets the backing array without any safeguards—i.e. defensive copying—to enable faster read-only access to the + * bytecode instructions in this BytecodeBuffer. + * + * Changes to this `BytecodeBuffer` might be reflected in the returned array, and vice versa, but that behavior cannot + * be guaranteed because this `BytecodeBuffer` might have to grow (and therefore reallocate) the backing data array. + * The size of the returned array reflects the `capacity` rather than the `size` of this `BytecodeBuffer`. + * + * This is safe to use as long as (a) you don't modify the returned array, (b) you don't modify this `BytecodeBuffer` + * while you still have a reference to the returned array, and (c) you don't read past the end of the bytecode (which + * should be denoted by one of the "end" instructions). + */ + @SuppressFBWarnings( + value = ["EI_EXPOSE_REP"], + justification = "unsafeGetArray intentionally exposes internal representation as a performance optimization" + ) + fun unsafeGetArray(): IntArray { + return data + } + + override fun toString(): String { + val numberOfValues = this.numberOfValues + + val builder = StringBuilder() + builder.append("BytecodeBuffer(data=[") + if (numberOfValues > 0) { + for (m in 0 until numberOfValues) { + builder.append(data[m]).append(",") + } + } + builder.append("])") + return builder.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as BytecodeBuffer + + val numberOfValues = this.numberOfValues + val thisData = this.data + val otherData = other.data + + if (numberOfValues != other.numberOfValues) return false + for (m in 0 until numberOfValues) { + if (thisData[m] != otherData[m]) return false + } + return true + } + + override fun hashCode(): Int { + val numberOfValues = this.numberOfValues + val data = this.data + + var result = numberOfValues + for (m in 0 until numberOfValues) { + result = result * 31 + data[m].hashCode() + } + return result + } +} diff --git a/src/main/java/com/amazon/ion/bytecode/util/ConstantPool.kt b/src/main/java/com/amazon/ion/bytecode/util/ConstantPool.kt new file mode 100644 index 0000000000..59ea7bbd0e --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/util/ConstantPool.kt @@ -0,0 +1,154 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.util + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +/** + * This is a custom collection that allows unsafe access to the backing array. + * + * It has a specialized [add] method that allows adding something to a [ConstantPool] and getting the CP_INDEX all in + * one method call. + * This class also manages the growth of the backing array, making it easy to add to the constant pool, but when we need + * to read the bytecode, we can access the backing array directly for more efficiency. + */ +internal class ConstantPool private constructor( + private var data: Array, + private var numberOfValues: Int, +) : AppendableConstantPoolView { + companion object { + const val GROWTH_MULTIPLIER: Int = 2 + } + + constructor(initialCapacity: Int) : this(data = arrayOfNulls(initialCapacity), numberOfValues = 0) + + val size: Int + get() = numberOfValues + + fun isEmpty(): Boolean = numberOfValues == 0 + + /** + * Empties this `ConstantPool`, allowing items to be inserted at the beginning again. + * Note that this method does not shrink the size of the backing data store or modify the backing data store in any other way. + */ + fun clear() { + numberOfValues = 0 + } + + /** + * Truncates the constant pool to length of `n`, allowing new items to be inserted starting at `n`. + * Note that this method does not shrink the size of the backing data store or modify the backing data store in any other way. + */ + fun truncate(n: Int) { + require(n <= numberOfValues) { "length exceeds number of values" } + numberOfValues = n + } + + /** + * Returns the `i`th int in the list. + */ + override fun get(i: Int): Any? { + if (i < 0 || i >= numberOfValues) { + throw IndexOutOfBoundsException("Invalid index $i requested from IntList with $numberOfValues values.") + } + return data[i] + } + + /** + * Appends a value to this `ConstantPool`, returning the index of the newly added item. + */ + override fun add(value: Any?): Int { + val n = numberOfValues + val newNumberOfValues = n + 1 + val data = ensureCapacity(newNumberOfValues) + data[n] = value + numberOfValues = newNumberOfValues + return n + } + + private fun ensureCapacity(minCapacity: Int): Array { + val data: Array = this.data + val capacity = data.size + if (minCapacity > capacity) { + // TODO: Consider making it grow to the next power of 2 instead of just growing to double the required capacity. + val newCapacity = minCapacity * GROWTH_MULTIPLIER + val newData: Array = arrayOfNulls(newCapacity) + System.arraycopy(data, 0, newData, 0, capacity) + this.data = newData + return newData + } + return data + } + + /** + * Returns an array that is the same size as this `ConstantPool`. The returned array is a defensive copy, and will + * not be modified by any mutating methods of this `ConstantPool`. + */ + fun toArray(): Array { + val thisNumberOfValues = this.numberOfValues + val copy = arrayOfNulls(thisNumberOfValues) + System.arraycopy(data, 0, copy, 0, thisNumberOfValues) + return copy + } + + /** + * Gets the backing array without any safeguards—i.e. defensive copying—to enable faster read-only access to the + * elements of this ConstantPool. + * + * Changes to this `ConstantPool` might be reflected in the returned array, and vice versa, but that behavior cannot + * be guaranteed because this `ConstantPool` might have to grow (and therefore reallocate) the backing data array. + * The size of the returned array reflects the `capacity` rather than the `size` of this `ConstantPool`. + * + * This is safe to use as long as (a) you don't modify the returned array, (b) you don't modify this `ConstantPool` + * while you still have a reference to the returned array, and (c) you can ensure that all array access indices + * are valid given the current `size` of the constant pool. + */ + @SuppressFBWarnings( + value = ["EI_EXPOSE_REP"], + justification = "unsafeGetArray intentionally exposes internal representation as a performance optimization" + ) + fun unsafeGetArray(): Array { + return data + } + + override fun toString(): String { + val numberOfValues = this.numberOfValues + + val builder = StringBuilder() + builder.append("ConstantPool(data=[") + if (numberOfValues > 0) { + for (m in 0 until numberOfValues) { + builder.append(data[m]).append(",") + } + } + builder.append("])") + return builder.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as ConstantPool + + val numberOfValues = this.numberOfValues + val thisData = this.data + val otherData = other.data + + if (numberOfValues != other.numberOfValues) return false + for (m in 0 until numberOfValues) { + if (thisData[m] != otherData[m]) return false + } + return true + } + + override fun hashCode(): Int { + val numberOfValues = this.numberOfValues + val data = this.data + + var result = numberOfValues + for (m in 0 until numberOfValues) { + result = result * 31 + data[m].hashCode() + } + return result + } +} diff --git a/src/test/java/com/amazon/ion/bytecode/util/BytecodeBufferTest.kt b/src/test/java/com/amazon/ion/bytecode/util/BytecodeBufferTest.kt new file mode 100644 index 0000000000..4622de049c --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/util/BytecodeBufferTest.kt @@ -0,0 +1,714 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class BytecodeBufferTest { + + @Test + fun `constructor creates empty buffer with correct initial capacity`() { + val buffer = BytecodeBuffer() + assertEquals(0, buffer.size()) + assertTrue(buffer.isEmpty()) + assertTrue(buffer.capacity() > 0) + } + + @Test + fun `size returns correct number of elements`() { + val buffer = BytecodeBuffer() + assertEquals(0, buffer.size()) + + buffer.add(42) + assertEquals(1, buffer.size()) + + buffer.add(100) + assertEquals(2, buffer.size()) + } + + @Test + fun `isEmpty returns true for empty buffer and false for non-empty buffer`() { + val buffer = BytecodeBuffer() + assertTrue(buffer.isEmpty()) + + buffer.add(42) + assertFalse(buffer.isEmpty()) + + buffer.clear() + assertTrue(buffer.isEmpty()) + } + + @Test + fun `capacity returns current capacity`() { + val buffer = BytecodeBuffer() + val initialCapacity = buffer.capacity() + assertTrue(initialCapacity > 0) + + // Add elements to potentially trigger growth + for (i in 0 until initialCapacity + 1) { + buffer.add(i) + } + + // Capacity should have grown + assertTrue(buffer.capacity() > initialCapacity) + } + + @Test + fun `add stores single value correctly`() { + val buffer = BytecodeBuffer() + buffer.add(42) + assertEquals(42, buffer.get(0)) + assertEquals(1, buffer.size()) + + buffer.add(100) + assertEquals(100, buffer.get(1)) + assertEquals(2, buffer.size()) + } + + @Test + fun `add2 stores two values correctly`() { + val buffer = BytecodeBuffer() + buffer.add2(10, 20) + + assertEquals(10, buffer.get(0)) + assertEquals(20, buffer.get(1)) + assertEquals(2, buffer.size()) + + buffer.add2(30, 40) + assertEquals(30, buffer.get(2)) + assertEquals(40, buffer.get(3)) + assertEquals(4, buffer.size()) + } + + @Test + fun `add3 stores three values correctly`() { + val buffer = BytecodeBuffer() + buffer.add3(10, 20, 30) + + assertEquals(10, buffer.get(0)) + assertEquals(20, buffer.get(1)) + assertEquals(30, buffer.get(2)) + assertEquals(3, buffer.size()) + + buffer.add3(40, 50, 60) + assertEquals(40, buffer.get(3)) + assertEquals(50, buffer.get(4)) + assertEquals(60, buffer.get(5)) + assertEquals(6, buffer.size()) + } + + @Test + fun `addSlice copies values from another buffer correctly`() { + val sourceBuffer = BytecodeBuffer() + sourceBuffer.add(10) + sourceBuffer.add(20) + sourceBuffer.add(30) + sourceBuffer.add(40) + sourceBuffer.add(50) + + val targetBuffer = BytecodeBuffer() + targetBuffer.add(1) + targetBuffer.add(2) + + // Copy slice from index 1, length 3 (values 20, 30, 40) + targetBuffer.addSlice(sourceBuffer, 1, 3) + + assertEquals(5, targetBuffer.size()) + assertEquals(1, targetBuffer.get(0)) + assertEquals(2, targetBuffer.get(1)) + assertEquals(20, targetBuffer.get(2)) + assertEquals(30, targetBuffer.get(3)) + assertEquals(40, targetBuffer.get(4)) + } + + @Test + fun `addSlice with zero length does nothing`() { + val sourceBuffer = BytecodeBuffer() + sourceBuffer.add(10) + sourceBuffer.add(20) + + val targetBuffer = BytecodeBuffer() + targetBuffer.add(1) + + targetBuffer.addSlice(sourceBuffer, 0, 0) + + assertEquals(1, targetBuffer.size()) + assertEquals(1, targetBuffer.get(0)) + } + + @Test + fun `get throws IndexOutOfBoundsException for negative index`() { + val buffer = BytecodeBuffer() + buffer.add(42) + + val exception = assertThrows { + buffer.get(-1) + } + assertTrue(exception.message!!.contains("Invalid index -1")) + } + + @Test + fun `get throws IndexOutOfBoundsException for index greater than or equal to size`() { + val buffer = BytecodeBuffer() + buffer.add(42) + + val exception = assertThrows { + buffer.get(1) + } + assertTrue(exception.message!!.contains("Invalid index 1")) + + val exception2 = assertThrows { + buffer.get(10) + } + assertTrue(exception2.message!!.contains("Invalid index 10")) + } + + @Test + fun `get throws IndexOutOfBoundsException for empty buffer`() { + val buffer = BytecodeBuffer() + val exception = assertThrows { + buffer.get(0) + } + assertTrue(exception.message!!.contains("Invalid index 0")) + } + + @Test + fun `reserve increases size and returns correct index`() { + val buffer = BytecodeBuffer() + buffer.add(10) + + val reservedIndex = buffer.reserve() + assertEquals(1, reservedIndex) + assertEquals(2, buffer.size()) + + val anotherReservedIndex = buffer.reserve() + assertEquals(2, anotherReservedIndex) + assertEquals(3, buffer.size()) + } + + @Test + fun `set works correctly with reserved positions`() { + val buffer = BytecodeBuffer() + buffer.add(10) + + val reservedIndex = buffer.reserve() + buffer.set(reservedIndex, 42) + + assertEquals(10, buffer.get(0)) + assertEquals(42, buffer.get(1)) + } + + @Test + fun `set throws IndexOutOfBoundsException for invalid indices`() { + val buffer = BytecodeBuffer() + buffer.add(10) + + assertThrows { + buffer.set(-1, 42) + } + + assertThrows { + buffer.set(1, 42) + } + + assertThrows { + buffer.set(10, 42) + } + } + + @Test + fun `set updates existing values correctly`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + buffer.add(30) + + buffer.set(1, 99) + + assertEquals(10, buffer.get(0)) + assertEquals(99, buffer.get(1)) + assertEquals(30, buffer.get(2)) + assertEquals(3, buffer.size()) + } + + @Test + fun `clear empties the buffer`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + buffer.add(30) + assertEquals(3, buffer.size()) + assertFalse(buffer.isEmpty()) + + buffer.clear() + assertEquals(0, buffer.size()) + assertTrue(buffer.isEmpty()) + + // Should be able to add new items starting from index 0 + buffer.add(42) + assertEquals(42, buffer.get(0)) + assertEquals(1, buffer.size()) + } + + @Test + fun `truncate reduces size to specified length`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + buffer.add(30) + buffer.add(40) + assertEquals(4, buffer.size()) + + buffer.truncate(2) + assertEquals(2, buffer.size()) + assertEquals(10, buffer.get(0)) + assertEquals(20, buffer.get(1)) + + // Should throw exception when trying to access truncated items + assertThrows { + buffer.get(2) + } + + // Should be able to add new items starting from truncated size + buffer.add(99) + assertEquals(99, buffer.get(2)) + assertEquals(3, buffer.size()) + } + + @Test + fun `truncate to zero makes buffer empty`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + + buffer.truncate(0) + assertEquals(0, buffer.size()) + assertTrue(buffer.isEmpty()) + } + + @Test + fun `truncate with length greater than size throws IllegalArgumentException`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + + val exception = assertThrows { + buffer.truncate(5) + } + assertTrue(exception.message!!.contains("length exceeds number of values")) + + // Buffer should remain unchanged after exception + assertEquals(2, buffer.size()) + assertEquals(10, buffer.get(0)) + assertEquals(20, buffer.get(1)) + } + + @Test + fun `truncate allows truncating to current size`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + + // Should not change anything + buffer.truncate(2) + assertEquals(2, buffer.size()) + assertEquals(10, buffer.get(0)) + assertEquals(20, buffer.get(1)) + } + + @Test + fun `buffer grows automatically when capacity is exceeded`() { + val buffer = BytecodeBuffer() + val initialCapacity = buffer.capacity() + + // Add items beyond initial capacity + for (i in 0 until initialCapacity + 5) { + buffer.add(i) + } + + assertEquals(initialCapacity + 5, buffer.size()) + assertTrue(buffer.capacity() > initialCapacity) + + // Verify all items are accessible + for (i in 0 until initialCapacity + 5) { + assertEquals(i, buffer.get(i)) + } + } + + @Test + fun `toArray returns defensive copy with correct size`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + buffer.add(30) + + val array = buffer.toArray() + assertEquals(3, array.size) + assertEquals(10, array[0]) + assertEquals(20, array[1]) + assertEquals(30, array[2]) + + // Verify it's a defensive copy by modifying the returned array + array[0] = 999 + assertEquals(10, buffer.get(0)) // Original should be unchanged + } + + @Test + fun `toArray returns empty array for empty buffer`() { + val buffer = BytecodeBuffer() + val array = buffer.toArray() + assertEquals(0, array.size) + } + + @Test + fun `unsafeGetArray returns backing array`() { + val buffer = BytecodeBuffer() + buffer.add(10) + buffer.add(20) + + val array = buffer.unsafeGetArray() + + // Array should contain the items + assertEquals(10, array[0]) + assertEquals(20, array[1]) + + // Array size should be capacity, not just the number of items + assertEquals(buffer.capacity(), array.size) + + // If we make changes it should be reflected in the BytecodeBuffer + // DON'T ACTUALLY DO THIS OUTSIDE OF TEST CODE! + array[0] = 999 + assertEquals(999, buffer.get(0)) + } + + @Test + fun `toString returns correct string representation`() { + val buffer = BytecodeBuffer() + val emptyString = buffer.toString() + assertEquals("BytecodeBuffer(data=[])", emptyString) + + buffer.add(10) + buffer.add(20) + buffer.add(30) + + assertEquals("BytecodeBuffer(data=[10,20,30,])", buffer.toString()) + } + + @Test + fun `equals returns true for identical buffers`() { + val buffer1 = BytecodeBuffer() + val buffer2 = BytecodeBuffer() + + // Empty buffers should be equal + assertEquals(buffer1, buffer2) + + // Add same items to both + buffer1.add(10) + buffer1.add(20) + buffer1.add(30) + + buffer2.add(10) + buffer2.add(20) + buffer2.add(30) + + assertEquals(buffer1, buffer2) + } + + @Test + fun `equals returns false for buffers with different content`() { + val buffer1 = BytecodeBuffer() + val buffer2 = BytecodeBuffer() + + buffer1.add(10) + buffer2.add(20) + + assertNotEquals(buffer1, buffer2) + } + + @Test + fun `equals returns false for buffers with different sizes`() { + val buffer1 = BytecodeBuffer() + val buffer2 = BytecodeBuffer() + + buffer1.add(10) + buffer2.add(10) + buffer2.add(20) + + assertNotEquals(buffer1, buffer2) + } + + @Test + fun `equals returns true for same instance`() { + val buffer = BytecodeBuffer() + assertEquals(buffer, buffer) + } + + @Test + fun `equals returns false for null and different types`() { + val buffer = BytecodeBuffer() + assertNotEquals(buffer, null) + assertNotEquals(buffer, "not a bytecode buffer") + assertNotEquals(buffer, listOf()) + } + + @Test + fun `hashCode is consistent with equals`() { + val buffer1 = BytecodeBuffer() + val buffer2 = BytecodeBuffer() + + // Empty buffers + assertEquals(buffer1.hashCode(), buffer2.hashCode()) + + // Add same content + buffer1.add(10) + buffer1.add(20) + + buffer2.add(10) + buffer2.add(20) + + assertEquals(buffer1.hashCode(), buffer2.hashCode()) + } + + @Test + fun `hashCode differs for different content`() { + val buffer1 = BytecodeBuffer() + val buffer2 = BytecodeBuffer() + + buffer1.add(10) + buffer2.add(20) + + assertNotEquals(buffer1.hashCode(), buffer2.hashCode()) + } + + @Test + fun `growth multiplier is applied correctly`() { + val buffer = BytecodeBuffer() + val initialCapacity = buffer.capacity() + + // Add items to force growth + for (i in 0 until initialCapacity + 1) { + buffer.add(i) + } + + // Verify all items are accessible + for (i in 0 until initialCapacity + 1) { + assertEquals(i, buffer.get(i)) + } + + // The backing array should have grown by GROWTH_MULTIPLIER (which is 2) + val newCapacity = buffer.capacity() + assertTrue(newCapacity >= (initialCapacity + 1) * 2) + } + + @Test + fun `large number of items can be stored and retrieved`() { + val buffer = BytecodeBuffer() + val itemCount = 1000 + + // Add many items + for (i in 0 until itemCount) { + buffer.add(i) + } + + assertEquals(itemCount, buffer.size()) + + // Verify all items can be retrieved + for (i in 0 until itemCount) { + assertEquals(i, buffer.get(i)) + } + } + + @Test + fun `operations work correctly after clear`() { + val buffer = BytecodeBuffer() + // Add some items + buffer.add(10) + buffer.add(20) + assertEquals(2, buffer.size()) + + // Clear and verify + buffer.clear() + assertEquals(0, buffer.size()) + assertTrue(buffer.isEmpty()) + + // Add new items after clear + buffer.add(30) + buffer.add(40) + + assertEquals(30, buffer.get(0)) + assertEquals(40, buffer.get(1)) + assertEquals(2, buffer.size()) + } + + @Test + fun `operations work correctly after truncate`() { + val buffer = BytecodeBuffer() + // Add some items + buffer.add(10) + buffer.add(20) + buffer.add(30) + buffer.add(40) + assertEquals(4, buffer.size()) + + // Truncate to 2 + buffer.truncate(2) + assertEquals(2, buffer.size()) + + // Add new items after truncate + buffer.add(50) + buffer.add(60) + + assertEquals(10, buffer.get(0)) + assertEquals(20, buffer.get(1)) + assertEquals(50, buffer.get(2)) + assertEquals(60, buffer.get(3)) + assertEquals(4, buffer.size()) + } + + @Test + fun `mixed operations work correctly together`() { + val buffer = BytecodeBuffer() + + // Test add, add2, add3 together + buffer.add(1) + buffer.add2(2, 3) + buffer.add3(4, 5, 6) + + assertEquals(6, buffer.size()) + for (i in 0 until 6) { + assertEquals(i + 1, buffer.get(i)) + } + + // Test reserve and set + val reservedIndex = buffer.reserve() + buffer.set(reservedIndex, 99) + + assertEquals(7, buffer.size()) + assertEquals(99, buffer.get(6)) + + // Test addSlice + val sourceBuffer = BytecodeBuffer() + sourceBuffer.add(100) + sourceBuffer.add(200) + + buffer.addSlice(sourceBuffer, 0, 2) + + assertEquals(9, buffer.size()) + assertEquals(100, buffer.get(7)) + assertEquals(200, buffer.get(8)) + } + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 5, 10]) + fun `truncate works correctly with valid lengths`(length: Int) { + val buffer = BytecodeBuffer() + + // Add 10 items + for (i in 0 until 10) { + buffer.add(i) + } + + buffer.truncate(length) + assertEquals(length, buffer.size()) + + // Verify remaining items are correct + for (i in 0 until length) { + assertEquals(i, buffer.get(i)) + } + } + + @Test + fun `truncate throws IllegalArgumentException for length exceeding size`() { + val buffer = BytecodeBuffer() + + // Add 5 items + for (i in 0 until 5) { + buffer.add(i) + } + + // Try to truncate to length greater than size + val exception = assertThrows { + buffer.truncate(10) + } + assertTrue(exception.message!!.contains("length exceeds number of values")) + + // Buffer should remain unchanged + assertEquals(5, buffer.size()) + for (i in 0 until 5) { + assertEquals(i, buffer.get(i)) + } + } + + @Test + fun `buffer handles negative values correctly`() { + val buffer = BytecodeBuffer() + buffer.add(-1) + buffer.add(-100) + buffer.add(0) + buffer.add(100) + + assertEquals(-1, buffer.get(0)) + assertEquals(-100, buffer.get(1)) + assertEquals(0, buffer.get(2)) + assertEquals(100, buffer.get(3)) + } + + @Test + fun `buffer handles maximum and minimum integer values`() { + val buffer = BytecodeBuffer() + buffer.add(Int.MAX_VALUE) + buffer.add(Int.MIN_VALUE) + + assertEquals(Int.MAX_VALUE, buffer.get(0)) + assertEquals(Int.MIN_VALUE, buffer.get(1)) + } + + @Test + fun `addSlice handles edge cases correctly`() { + val sourceBuffer = BytecodeBuffer() + sourceBuffer.add(10) + sourceBuffer.add(20) + sourceBuffer.add(30) + + val targetBuffer = BytecodeBuffer() + + // Add slice from the end + targetBuffer.addSlice(sourceBuffer, 2, 1) + assertEquals(1, targetBuffer.size()) + assertEquals(30, targetBuffer.get(0)) + + // Add slice from the beginning + targetBuffer.addSlice(sourceBuffer, 0, 2) + assertEquals(3, targetBuffer.size()) + assertEquals(30, targetBuffer.get(0)) + assertEquals(10, targetBuffer.get(1)) + assertEquals(20, targetBuffer.get(2)) + } + + @Test + fun `capacity increases correctly with multiple growth cycles`() { + val buffer = BytecodeBuffer() + val initialCapacity = buffer.capacity() + + // Force multiple growth cycles + val itemsToAdd = initialCapacity * 4 + for (i in 0 until itemsToAdd) { + buffer.add(i) + } + + assertEquals(itemsToAdd, buffer.size()) + assertTrue(buffer.capacity() >= itemsToAdd) + + // Verify all items are still accessible + for (i in 0 until itemsToAdd) { + assertEquals(i, buffer.get(i)) + } + } +} diff --git a/src/test/java/com/amazon/ion/bytecode/util/ConstantPoolTest.kt b/src/test/java/com/amazon/ion/bytecode/util/ConstantPoolTest.kt new file mode 100644 index 0000000000..80790d7258 --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/util/ConstantPoolTest.kt @@ -0,0 +1,481 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.bytecode.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ConstantPoolTest { + + @Test + fun `constructor creates empty constant pool with correct initial capacity`() { + val pool = ConstantPool(5) + assertEquals(0, pool.size) + assertTrue(pool.isEmpty()) + } + + @Test + fun `size returns correct number of elements`() { + val constantPool = ConstantPool(10) + assertEquals(0, constantPool.size) + + constantPool.add("test") + assertEquals(1, constantPool.size) + + constantPool.add(42) + assertEquals(2, constantPool.size) + } + + @Test + fun `isEmpty returns true for empty pool and false for non-empty pool`() { + val constantPool = ConstantPool(10) + assertTrue(constantPool.isEmpty()) + + constantPool.add("test") + assertFalse(constantPool.isEmpty()) + + constantPool.clear() + assertTrue(constantPool.isEmpty()) + } + + @Test + fun `add returns correct index and stores value`() { + val constantPool = ConstantPool(10) + val index1 = constantPool.add("first") + assertEquals(0, index1) + assertEquals("first", constantPool.get(0)) + + val index2 = constantPool.add("second") + assertEquals(1, index2) + assertEquals("second", constantPool.get(1)) + + val index3 = constantPool.add(null) + assertEquals(2, index3) + assertNull(constantPool.get(2)) + } + + @Test + fun `add can store various types of objects`() { + val constantPool = ConstantPool(10) + val stringIndex = constantPool.add("string") + val intIndex = constantPool.add(42) + val listIndex = constantPool.add(listOf(1, 2, 3)) + val nullIndex = constantPool.add(null) + + assertEquals("string", constantPool.get(stringIndex)) + assertEquals(42, constantPool.get(intIndex)) + assertEquals(listOf(1, 2, 3), constantPool.get(listIndex)) + assertNull(constantPool.get(nullIndex)) + } + + @Test + fun `get throws IndexOutOfBoundsException for negative index`() { + val constantPool = ConstantPool(10) + constantPool.add("test") + + val exception = assertThrows { + constantPool.get(-1) + } + assertTrue(exception.message!!.contains("Invalid index -1")) + } + + @Test + fun `get throws IndexOutOfBoundsException for index greater than or equal to size`() { + val constantPool = ConstantPool(10) + constantPool.add("test") + + val exception = assertThrows { + constantPool.get(1) + } + assertTrue(exception.message!!.contains("Invalid index 1")) + + val exception2 = assertThrows { + constantPool.get(10) + } + assertTrue(exception2.message!!.contains("Invalid index 10")) + } + + @Test + fun `get throws IndexOutOfBoundsException for empty pool`() { + val constantPool = ConstantPool(10) + val exception = assertThrows { + constantPool.get(0) + } + assertTrue(exception.message!!.contains("Invalid index 0")) + } + + @Test + fun `clear empties the constant pool`() { + val constantPool = ConstantPool(10) + constantPool.add("test1") + constantPool.add("test2") + constantPool.add("test3") + assertEquals(3, constantPool.size) + assertFalse(constantPool.isEmpty()) + + constantPool.clear() + assertEquals(0, constantPool.size) + assertTrue(constantPool.isEmpty()) + + // Should be able to add new items starting from index 0 + val index = constantPool.add("new item") + assertEquals(0, index) + assertEquals("new item", constantPool.get(0)) + } + + @Test + fun `truncate reduces size to specified length`() { + val constantPool = ConstantPool(10) + constantPool.add("item0") + constantPool.add("item1") + constantPool.add("item2") + constantPool.add("item3") + assertEquals(4, constantPool.size) + + constantPool.truncate(2) + assertEquals(2, constantPool.size) + assertEquals("item0", constantPool.get(0)) + assertEquals("item1", constantPool.get(1)) + + // Should throw exception when trying to access truncated items + assertThrows { + constantPool.get(2) + } + + // Should be able to add new items starting from truncated size + val newIndex = constantPool.add("new item") + assertEquals(2, newIndex) + assertEquals("new item", constantPool.get(2)) + } + + @Test + fun `truncate to zero makes pool empty`() { + val constantPool = ConstantPool(10) + constantPool.add("item1") + constantPool.add("item2") + + constantPool.truncate(0) + assertEquals(0, constantPool.size) + assertTrue(constantPool.isEmpty()) + } + + @Test + fun `truncate throws exception when length exceeds number of values`() { + val constantPool = ConstantPool(10) + constantPool.add("item1") + constantPool.add("item2") + + val exception = assertThrows { + constantPool.truncate(3) + } + assertEquals("length exceeds number of values", exception.message) + } + + @Test + fun `truncate allows truncating to current size`() { + val constantPool = ConstantPool(10) + constantPool.add("item1") + constantPool.add("item2") + + // Should not throw exception + constantPool.truncate(2) + assertEquals(2, constantPool.size) + } + + @Test + fun `pool grows automatically when capacity is exceeded`() { + val smallPool = ConstantPool(2) + + // Add items beyond initial capacity + for (i in 0..5) { + val index = smallPool.add("item$i") + assertEquals(i, index) + } + + assertEquals(6, smallPool.size) + + // Verify all items are accessible + for (i in 0..5) { + assertEquals("item$i", smallPool.get(i)) + } + } + + @ParameterizedTest + @ValueSource(ints = [1, 5, 10, 100, 1000]) + fun `pool handles various initial capacities`(initialCapacity: Int) { + val pool = ConstantPool(initialCapacity) + assertTrue(pool.isEmpty()) + assertEquals(0, pool.size) + + // Add one item to verify it works + val index = pool.add("test") + assertEquals(0, index) + assertEquals("test", pool.get(0)) + } + + @Test + fun `toArray returns defensive copy with correct size`() { + val constantPool = ConstantPool(10) + constantPool.add("item1") + constantPool.add("item2") + constantPool.add(null) + + val array = constantPool.toArray() + assertEquals(3, array.size) + assertEquals("item1", array[0]) + assertEquals("item2", array[1]) + assertNull(array[2]) + + // Verify it's a defensive copy by modifying the returned array + array[0] = "modified" + assertEquals("item1", constantPool.get(0)) // Original should be unchanged + } + + @Test + fun `toArray returns empty array for empty pool`() { + val constantPool = ConstantPool(10) + val array = constantPool.toArray() + assertEquals(0, array.size) + } + + @Test + fun `unsafeGetArray returns backing array`() { + val constantPool = ConstantPool(10) + constantPool.add("item1") + constantPool.add("item2") + + val array = constantPool.unsafeGetArray() + + // Array should contain the items + assertEquals("item1", array[0]) + assertEquals("item2", array[1]) + + // If we make changes it should be reflected in the BytecodeBuffer + // DON'T ACTUALLY DO THIS OUTSIDE OF TEST CODE! + array[0] = "item3" + assertEquals("item3", constantPool.get(0)) + } + + @Test + fun `toString returns correct string representation`() { + val constantPool = ConstantPool(10) + val emptyString = constantPool.toString() + assertEquals("ConstantPool(data=[])", emptyString) + + constantPool.add("test") + constantPool.add(42) + + val string = constantPool.toString() + assertEquals("ConstantPool(data=[test,42,])", string) + } + + @Test + fun `equals returns true for identical pools`() { + val pool1 = ConstantPool(5) + val pool2 = ConstantPool(10) // Different capacity + + // Empty pools should be equal + assertEquals(pool1, pool2) + + // Add same items to both + pool1.add("test") + pool1.add(42) + pool1.add(null) + + pool2.add("test") + pool2.add(42) + pool2.add(null) + + assertEquals(pool1, pool2) + } + + @Test + fun `equals returns false for pools with different content`() { + val pool1 = ConstantPool(5) + val pool2 = ConstantPool(5) + + pool1.add("test1") + pool2.add("test2") + + assertNotEquals(pool1, pool2) + } + + @Test + fun `equals returns false for pools with different sizes`() { + val pool1 = ConstantPool(5) + val pool2 = ConstantPool(5) + + pool1.add("test") + pool2.add("test") + pool2.add("extra") + + assertNotEquals(pool1, pool2) + } + + @Test + fun `equals returns true for same instance`() { + val constantPool = ConstantPool(10) + assertEquals(constantPool, constantPool) + } + + @Test + fun `equals returns false for null and different types`() { + val constantPool = ConstantPool(10) + assertNotEquals(constantPool, null) + assertNotEquals(constantPool, "not a constant pool") + assertNotEquals(constantPool, listOf()) + } + + @Test + fun `hashCode is consistent with equals`() { + val pool1 = ConstantPool(5) + val pool2 = ConstantPool(10) + + // Empty pools + assertEquals(pool1.hashCode(), pool2.hashCode()) + + // Add same content + pool1.add("test") + pool1.add(42) + + pool2.add("test") + pool2.add(42) + + assertEquals(pool1.hashCode(), pool2.hashCode()) + } + + @Test + fun `hashCode differs for different content`() { + val pool1 = ConstantPool(5) + val pool2 = ConstantPool(5) + + pool1.add("test1") + pool2.add("test2") + + assertNotEquals(pool1.hashCode(), pool2.hashCode()) + } + + @Test + fun `constant pool implements AppendableConstantPoolView interface correctly`() { + val constantPool = ConstantPool(10) + val view: AppendableConstantPoolView = constantPool + + val index = view.add("test") + assertEquals(0, index) + assertEquals("test", view.get(0)) + } + + @Test + fun `growth multiplier is applied correctly`() { + // Create a small pool to test growth + val smallPool = ConstantPool(1) + + // Add items to force growth + smallPool.add("item1") + smallPool.add("item2") // This should trigger growth + + // Verify both items are accessible + assertEquals("item1", smallPool.get(0)) + assertEquals("item2", smallPool.get(1)) + assertEquals(2, smallPool.size) + + // The backing array should have grown by GROWTH_MULTIPLIER + val backingArray = smallPool.unsafeGetArray() + assertTrue(backingArray.size >= 2) // Should be at least 2 (1 * GROWTH_MULTIPLIER) + } + + @Test + fun `large number of items can be stored and retrieved`() { + val largePool = ConstantPool(10) + val itemCount = 1000 + + // Add many items + for (i in 0 until itemCount) { + val index = largePool.add("item$i") + assertEquals(i, index) + } + + assertEquals(itemCount, largePool.size) + + // Verify all items can be retrieved + for (i in 0 until itemCount) { + assertEquals("item$i", largePool.get(i)) + } + } + + @Test + fun `pool handles null values correctly`() { + val constantPool = ConstantPool(10) + val index1 = constantPool.add(null) + val index2 = constantPool.add("not null") + val index3 = constantPool.add(null) + + assertEquals(0, index1) + assertEquals(1, index2) + assertEquals(2, index3) + + assertNull(constantPool.get(0)) + assertEquals("not null", constantPool.get(1)) + assertNull(constantPool.get(2)) + + assertEquals(3, constantPool.size) + } + + @Test + fun `operations work correctly after clear`() { + val constantPool = ConstantPool(10) + // Add some items + constantPool.add("item1") + constantPool.add("item2") + assertEquals(2, constantPool.size) + + // Clear and verify + constantPool.clear() + assertEquals(0, constantPool.size) + assertTrue(constantPool.isEmpty()) + + // Add new items after clear + val index1 = constantPool.add("new1") + val index2 = constantPool.add("new2") + + assertEquals(0, index1) + assertEquals(1, index2) + assertEquals("new1", constantPool.get(0)) + assertEquals("new2", constantPool.get(1)) + assertEquals(2, constantPool.size) + } + + @Test + fun `operations work correctly after truncate`() { + val constantPool = ConstantPool(10) + // Add some items + constantPool.add("item1") + constantPool.add("item2") + constantPool.add("item3") + constantPool.add("item4") + assertEquals(4, constantPool.size) + + // Truncate to 2 + constantPool.truncate(2) + assertEquals(2, constantPool.size) + + // Add new items after truncate + val index1 = constantPool.add("new1") + val index2 = constantPool.add("new2") + + assertEquals(2, index1) + assertEquals(3, index2) + assertEquals("item1", constantPool.get(0)) + assertEquals("item2", constantPool.get(1)) + assertEquals("new1", constantPool.get(2)) + assertEquals("new2", constantPool.get(3)) + assertEquals(4, constantPool.size) + } +}