diff --git a/src/main/java/com/amazon/ion/bytecode/BytecodeIonReader.kt b/src/main/java/com/amazon/ion/bytecode/BytecodeIonReader.kt index 8ad878fe9..a5428f12b 100644 --- a/src/main/java/com/amazon/ion/bytecode/BytecodeIonReader.kt +++ b/src/main/java/com/amazon/ion/bytecode/BytecodeIonReader.kt @@ -2,13 +2,613 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.bytecode -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import com.amazon.ion.Decimal +import com.amazon.ion.IntegerSize +import com.amazon.ion.IonException +import com.amazon.ion.IonReader +import com.amazon.ion.IonType +import com.amazon.ion.SymbolTable +import com.amazon.ion.SymbolToken +import com.amazon.ion.Timestamp +import com.amazon.ion.bytecode.BytecodeIonReader.AnnotationHelper.EMPTY_ANNOTATIONS +import com.amazon.ion.bytecode.ir.Debugger +import com.amazon.ion.bytecode.ir.Instructions +import com.amazon.ion.bytecode.ir.Instructions.I_REFILL +import com.amazon.ion.bytecode.ir.Operation +import com.amazon.ion.bytecode.ir.OperationKind +import com.amazon.ion.bytecode.util.ByteSlice +import com.amazon.ion.bytecode.util.BytecodeBuffer +import com.amazon.ion.bytecode.util.ConstantPool +import com.amazon.ion.impl._Private_RecyclingStack +import com.amazon.ion.impl._Private_Utils +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Date /** - * TODO: This class should implement [IonReader] for the Bytecode IR. + * This class implements [IonReader] for the Bytecode IR. + * + * TODO: When should the value accessor functions return null? + * The existing text, tree, and binary readers are not consistent with each other. */ -internal class BytecodeIonReader -@SuppressFBWarnings("URF_UNREAD_FIELD", justification = "field will be read once this class is implemented") -constructor( - private var bytecodeGenerator: BytecodeGenerator, -) +internal class BytecodeIonReader(private var generator: BytecodeGenerator) : IonReader { + + private var bytecodeI = 0 + + internal var minorVersion: Byte = 0 + private set + + private val context = EncodingContextManager() + private var symbolTable: Array = EncodingContextManager.SYSTEM_SYMBOLS + + private var bytecodeIntList = BytecodeBuffer().also { it.add(I_REFILL) } + private var bytecode = bytecodeIntList.unsafeGetArray() + + private var constantPool = ConstantPool(256) + private var firstLocalConstant = 0 + + private var instruction = 0 + private var fieldNameIndex = -1 + private var annotationsIndex = -1 + private var annotationCount: Byte = 0 + + private var labelIndex = -1 + + private var isInStruct = false + private var isNextAlreadyLoaded = false + + private val containerStack = _Private_RecyclingStack(10) { ContainerInfo() } + + // This has higher overhead than using standard library methods. Only use this when there's a chance that + // the conversion in fallible and/or lossy. + private val scalarConverter = ScalarConversionHelper() + + data class ContainerInfo( + @JvmField var isStruct: Boolean = false, + /** The index of the first instruction after this container. */ + @JvmField var bytecodeI: Int = -1, + ) + + companion object { + const val INSTRUCTION_NOT_SET = 0 + } + + override fun close() {} + + private fun refillBytecode(): IntArray { + val bytecodeIntList = bytecodeIntList + bytecodeIntList.clear() + val constantPool = constantPool + constantPool.truncate(firstLocalConstant) + val context = context + generator.refill( + bytecodeIntList, + constantPool, + context.getEffectiveMacroTableBytecode(), + context.getEffectiveMacroTableOffsets(), + symbolTable + ) + bytecodeIntList.add(I_REFILL) + val bytecodeArray = bytecodeIntList.unsafeGetArray() + this.bytecode = bytecodeArray + return bytecodeArray + } + + override fun hasNext(): Boolean { + val result = next() != null + isNextAlreadyLoaded = true + return result + } + + override fun next(): IonType? { + if (isNextAlreadyLoaded) { + isNextAlreadyLoaded = false + return type + } + + var bytecode = bytecode + var i = bytecodeI + var instruction = instruction + var operationKind: Int + + var annotationStart = 0 // Stores the start of the annotations, offset by 1 (for bit-twiddling reasons). + var annotationFlag = -1 // -1 if not set, 0 if set. Used to avoid branches in the annotation case. + var annotationCount = 0 + var labelIndex = labelIndex + + do { + // Move `i` to point to the next instruction. + val length = Instructions.getData(instruction) + val operandCountBits = Instructions.getOperandCountBits(instruction) + // equivalent to `i += if (operandsToSkip == 3) length else operandsToSkip` + // `useOperandCount` is all zeros if `operandsToSkip` is 3, and all ones if `operandsToCount` is smaller than 3. + val useOperandCount = ((operandCountBits - 3) shr 2) + i += (operandCountBits and useOperandCount) or (length and useOperandCount.inv()) + + // Load the next instruction + instruction = bytecode[i++] + operationKind = Operation.toOperationKind(Instructions.toOperation(instruction)) + + when (operationKind) { + OperationKind.NULL, + OperationKind.BOOL, + OperationKind.INT, + OperationKind.FLOAT, + OperationKind.DECIMAL, + OperationKind.TIMESTAMP, + OperationKind.STRING, + OperationKind.SYMBOL, + OperationKind.BLOB, + OperationKind.CLOB, + OperationKind.LIST, + OperationKind.SEXP, + OperationKind.STRUCT -> break + OperationKind.END -> { + // TODO(perf): See if there is any benefit to splitting these into different operation kinds. + when (instruction) { + // return so that we don't update the current state so that if the user keeps calling `next()` they keep encountering this. + Instructions.I_END_CONTAINER -> return null + // break so that we do update the current state so that if the user calls `next()`, we can check the generator for more data. + Instructions.I_END_OF_INPUT -> break + else -> TODO("${Debugger.renderSingleInstruction(instruction)} at ${i - 1}") + } + } + OperationKind.FIELD_NAME -> fieldNameIndex = i - 1 + OperationKind.ANNOTATIONS -> { + annotationStart = (annotationStart or (i and annotationFlag)) + annotationCount++ + annotationFlag = 0 + } + OperationKind.IVM -> handleIvm(instruction) + OperationKind.REFILL -> { + bytecode = refillBytecode() + i = 0 + } + OperationKind.DIRECTIVE -> { + i = handleSystemValue(instruction, i) + } + OperationKind.ARGUMENT -> { + // In other words, an absent argument. + annotationStart = 0 + annotationFlag = -1 + annotationCount = 0 + } + OperationKind.METADATA -> { + labelIndex = i - 1 + continue + } + else -> TODO("${OperationKind.nameOf(operationKind)} at ${i - 1}") + } + } while (true) + + if (labelIndex != -1) { + this.labelIndex = labelIndex + } + + this.bytecodeI = i + this.instruction = instruction + this.annotationsIndex = annotationStart - 1 + this.annotationCount = annotationCount.toByte() + + return OperationKind.ionTypeOf(operationKind) + } + + private fun handleIvm(instruction: Int) { + val ionVersionInt = Instructions.getData(instruction) + minorVersion = ionVersionInt.toByte() + generator = generator.getGeneratorForMinorVersion(ionVersionInt) + symbolTable = EncodingContextManager.SYSTEM_SYMBOLS + constantPool.clear() + context.reset() + } + + private fun handleSystemValue(instruction: Int, nextI: Int): Int { + TODO("Implement directive handler") + } + + override fun getType(): IonType? = OperationKind.ionTypeOf(Operation.toOperationKind(Instructions.toOperation(instruction))) + + override fun stepIn() { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val steppingIntoStruct = when (op) { + Operation.OP_LIST_START, + Operation.OP_SEXP_START -> false + Operation.OP_STRUCT_START -> true + else -> throw IonException("Not positioned on a container") + } + + val length = Instructions.getData(instruction) + containerStack.push { + it.bytecodeI = bytecodeI + length + it.isStruct = isInStruct + } + this.isInStruct = steppingIntoStruct + this.instruction = INSTRUCTION_NOT_SET + this.fieldNameIndex = -1 + this.annotationCount = 0 + this.annotationsIndex = -1 + } + + override fun stepOut() { + val top = containerStack.pop() ?: throw IonException("Nothing to step out of.") + this.bytecodeI = top.bytecodeI + this.isInStruct = top.isStruct + this.instruction = INSTRUCTION_NOT_SET + this.fieldNameIndex = -1 + this.annotationCount = 0 + this.annotationsIndex = -1 + } + + override fun isInStruct(): Boolean = isInStruct + + override fun getDepth(): Int = containerStack.size() + + private object AnnotationHelper { + @JvmStatic + val EMPTY_ANNOTATIONS: Array = emptyArray() + @JvmStatic + val EMPTY_ITERATOR: Iterator = object : Iterator { + override fun hasNext(): Boolean = false + override fun next(): Nothing = throw NoSuchElementException() + } + } + + override fun getTypeAnnotations(): Array { + val nAnnotations = annotationCount.toInt() + if (nAnnotations == 0) { + return EMPTY_ANNOTATIONS + } + val result = arrayOfNulls(nAnnotations) + var p = annotationsIndex + val bytecode = this.bytecode + for (i in 0 until nAnnotations) { + val instruction = bytecode[p++] + val data = Instructions.getData(instruction) + result[i] = when (Instructions.toOperation(instruction)) { + Operation.OP_ANNOTATION_CP -> constantPool[data] as String? + Operation.OP_ANNOTATION_SID -> symbolTable[data] + Operation.OP_ANNOTATION_REF -> { + val position = bytecode[p++] + generator.readTextReference(position, data) + } + else -> throw IllegalStateException("annotation $i does not point to an annotation; was ${Debugger.renderSingleInstruction(instruction)}") + } + } + return result + } + + private fun createSymbolToken(text: String?, sid: Int): SymbolToken = _Private_Utils.newSymbolToken(text, sid) + + override fun getTypeAnnotationSymbols(): Array { + + val nAnnotations = annotationCount.toInt() + var p = annotationsIndex + val bytecode = this.bytecode + val result = Array(nAnnotations) { i -> + + val instruction = bytecode[p++] + val data = Instructions.getData(instruction) + when (Instructions.toOperation(instruction)) { + Operation.OP_ANNOTATION_CP -> { + val text = constantPool[data] as String? + createSymbolToken(text, -1) + } + Operation.OP_ANNOTATION_SID -> { + val text = symbolTable[data] + createSymbolToken(text, data) + } + Operation.OP_ANNOTATION_REF -> { + val position = bytecode[p++] + val text = generator.readTextReference(position, data) + createSymbolToken(text, -1) + } + else -> throw IllegalStateException("annotation $i does not point to an annotation; was ${Debugger.renderSingleInstruction(instruction)}") + } + } + return result + } + + // TODO(perf): We could make this into lazy iterator. + override fun iterateTypeAnnotations(): Iterator = if (annotationCount.toInt() == 0) AnnotationHelper.EMPTY_ITERATOR else typeAnnotations.iterator() + + override fun getFieldId(): Int { + val fieldName = fieldNameIndex + if (fieldName < 0) return -1 + val fieldInstruction = bytecode[fieldName] + return when (Instructions.toOperation(fieldInstruction)) { + Operation.OP_FIELD_NAME_CP -> -1 + Operation.OP_FIELD_NAME_SID -> Instructions.getData(fieldInstruction) + Operation.OP_FIELD_NAME_REF -> -1 + else -> throw IllegalStateException("Field name index does not point to a field name; was ${Debugger.renderSingleInstruction(instruction)}") + } + } + + override fun getFieldName(): String? { + val fieldName = fieldNameIndex + if (fieldName < 0) return null + val fieldInstruction = bytecode[fieldName] + val data = Instructions.getData(fieldInstruction) + return when (Instructions.toOperation(fieldInstruction)) { + Operation.OP_FIELD_NAME_CP -> constantPool[data] as String? + Operation.OP_FIELD_NAME_SID -> symbolTable[data] + Operation.OP_FIELD_NAME_REF -> { + val position = bytecode[fieldName + 1] + generator.readTextReference(position, data) + } + else -> throw IllegalStateException("Field name index does not point to a field name; was ${Debugger.renderSingleInstruction(instruction)}") + } + } + + override fun getFieldNameSymbol(): SymbolToken? { + val fieldName = fieldNameIndex + if (fieldName < 0) return null + val fieldInstruction = bytecode[fieldName] + val data = Instructions.getData(fieldInstruction) + return when (Instructions.toOperation(fieldInstruction)) { + Operation.OP_FIELD_NAME_CP -> { + val text = constantPool[data] as String? + createSymbolToken(text, -1) + } + Operation.OP_FIELD_NAME_SID -> { + val text = symbolTable[data] + createSymbolToken(text, data) + } + Operation.OP_FIELD_NAME_REF -> { + val position = bytecode[fieldName + 1] + val text = generator.readTextReference(position, data) + createSymbolToken(text, -1) + } + else -> throw IllegalStateException("Field name index does not point to a field name; was ${Debugger.renderSingleInstruction(instruction)}") + } + } + + override fun isNullValue(): Boolean { + val op = Instructions.toOperation(instruction) + return Operation.NULL_VARIANT == (op and Operation.NULL_VARIANT) + } + + override fun booleanValue(): Boolean { + val instruction = this.instruction + if (Instructions.toOperation(instruction) != Operation.OP_BOOL) { + throw IonException("Not positioned on a boolean") + } + val bool = (instruction and 1) == 1 + return bool + } + + override fun getIntegerSize(): IntegerSize? { + val instruction = instruction + return when (Instructions.toOperation(instruction)) { + Operation.OP_INT_I16 -> IntegerSize.INT + Operation.OP_INT_I32 -> IntegerSize.INT + Operation.OP_INT_I64 -> IntegerSize.LONG + Operation.OP_INT_CP -> IntegerSize.BIG_INTEGER + Operation.OP_INT_REF -> { + val length = Instructions.getData(instruction) + when (length) { + // TODO: Check the size of the materialized value to see how big it really is? + // TODO: This might not be accurate for Ion 1.0 since it has unsigned integer encodings. + 1, 2, 3, 4 -> IntegerSize.INT + 5, 6, 7, 8 -> IntegerSize.LONG + else -> IntegerSize.BIG_INTEGER + } + } + else -> null + } + } + + override fun intValue(): Int { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val i = bytecodeI + val int = when (op) { + Operation.OP_INT_I16 -> Instructions.getData(instruction) + Operation.OP_INT_I32 -> bytecode[i] + Operation.OP_INT_I64 -> scalarConverter.from(longValue()).intoInt() + Operation.OP_INT_CP, + Operation.OP_INT_REF -> scalarConverter.from(bigIntegerValue()).intoInt() + Operation.OP_DECIMAL_CP, + Operation.OP_DECIMAL_REF -> scalarConverter.from(decimalValue()).intoInt() + Operation.OP_FLOAT_F32, + Operation.OP_FLOAT_F64 -> scalarConverter.from(doubleValue()).intoInt() + // TODO: Other numeric value types + else -> throw IonException("Not positioned on an Int value") + } + return int + } + + override fun longValue(): Long { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + var i = bytecodeI + val long = when (op) { + Operation.OP_INT_I16 -> Instructions.getData(instruction).toLong() + Operation.OP_INT_I32 -> bytecode[i].toLong() + Operation.OP_INT_I64 -> { + val msb = bytecode[i++].toLong() and 0xFFFFFFFF + val lsb = bytecode[i].toLong() and 0xFFFFFFFF + (msb shl 32) or lsb + } + Operation.OP_INT_CP, + Operation.OP_INT_REF -> scalarConverter.from(bigIntegerValue()).intoLong() + Operation.OP_DECIMAL_CP, + Operation.OP_DECIMAL_REF -> scalarConverter.from(decimalValue()).intoLong() + Operation.OP_FLOAT_F32, + Operation.OP_FLOAT_F64 -> scalarConverter.from(doubleValue()).intoLong() + // TODO: Other numeric value types + else -> throw IonException("Not positioned on an Int value") + } + return long + } + + override fun bigIntegerValue(): BigInteger { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + var i = bytecodeI + val bigInt = when (op) { + Operation.OP_INT_I16 -> Instructions.getData(instruction).toBigInteger() + Operation.OP_INT_I32 -> bytecode[i].toLong().toBigInteger() + Operation.OP_INT_I64 -> { + val msb = bytecode[i++].toLong() and 0xFFFFFFFF + val lsb = bytecode[i].toLong() and 0xFFFFFFFF + ((msb shl 32) or lsb).toBigInteger() + } + Operation.OP_INT_CP -> { + val cpIndex = Instructions.getData(instruction) + constantPool[cpIndex] as BigInteger + } + Operation.OP_INT_REF -> { + val length = Instructions.getData(instruction) + val position = bytecode[bytecodeI] + generator.readBigIntegerReference(position, length) + } + Operation.OP_DECIMAL_CP, + Operation.OP_DECIMAL_REF -> scalarConverter.from(decimalValue()).intoBigInteger() + Operation.OP_FLOAT_F32, + Operation.OP_FLOAT_F64 -> scalarConverter.from(doubleValue()).intoBigInteger() + else -> throw IonException("Not positioned on an Int value") + } + return bigInt + } + + override fun doubleValue(): Double { + var i = bytecodeI + val op = Instructions.toOperation(instruction) + val bytecode = bytecode + val double = when (op) { + Operation.OP_FLOAT_F64 -> { + val msb = bytecode[i++].toLong() and 0xFFFFFFFF + val lsb = bytecode[i].toLong() and 0xFFFFFFFF + Double.fromBits((msb shl 32) or lsb) + } + Operation.OP_FLOAT_F32 -> Float.fromBits(bytecode[i]).toDouble() + Operation.OP_INT_I16, + Operation.OP_INT_I32 -> scalarConverter.from(intValue()).intoDouble() + Operation.OP_INT_I64 -> scalarConverter.from(longValue()).intoDouble() + Operation.OP_INT_CP, + Operation.OP_INT_REF -> scalarConverter.from(bigIntegerValue()).intoDouble() + Operation.OP_DECIMAL_CP, + Operation.OP_DECIMAL_REF -> scalarConverter.from(decimalValue()).intoDouble() + else -> throw IonException("Not positioned on an Float value") + } + return double + } + + override fun bigDecimalValue(): BigDecimal? = decimalValue() + + override fun decimalValue(): Decimal? { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val data = Instructions.getData(instruction) + return when (op) { + Operation.OP_DECIMAL_REF -> Decimal.valueOf(generator.readDecimalReference(position = bytecode[bytecodeI], length = data)) + Operation.OP_DECIMAL_CP -> Decimal.valueOf(constantPool[data] as BigDecimal) + Operation.OP_NULL_DECIMAL -> null + // Other numeric types + Operation.OP_INT_I16, + Operation.OP_INT_I32, + Operation.OP_INT_I64 -> Decimal.valueOf(longValue()) + Operation.OP_INT_CP, + Operation.OP_INT_REF -> Decimal.valueOf(bigIntegerValue()) + Operation.OP_FLOAT_F32, + Operation.OP_FLOAT_F64 -> scalarConverter.from(doubleValue()).intoDecimal() + else -> throw IonException("Not positioned on a decimal value") + } + } + + override fun dateValue(): Date? = timestampValue()?.dateValue() + + override fun timestampValue(): Timestamp? { + val i = bytecodeI + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val data = Instructions.getData(instruction) + return when (op) { + Operation.OP_TIMESTAMP_CP -> constantPool[data] as Timestamp + Operation.OP_SHORT_TIMESTAMP_REF -> generator.readShortTimestampReference(position = bytecode[i], opcode = data) + Operation.OP_TIMESTAMP_REF -> generator.readTimestampReference(position = bytecode[i], length = data) + Operation.OP_NULL_TIMESTAMP -> null + else -> throw IonException("Not positioned on a timestamp value") + } + } + + override fun stringValue(): String? { + val i = bytecodeI + val data = Instructions.getData(instruction) + return when (Instructions.toOperation(instruction)) { + Operation.OP_NULL_STRING, + Operation.OP_NULL_SYMBOL -> null + Operation.OP_STRING_CP, + Operation.OP_SYMBOL_CP -> constantPool[data] as String? + Operation.OP_SYMBOL_REF, + Operation.OP_STRING_REF -> generator.readTextReference(position = bytecode[i], length = data) + Operation.OP_SYMBOL_CHAR -> data.toChar().toString() + Operation.OP_SYMBOL_SID -> symbolTable[data] + else -> throw IonException("Not positioned on a string or symbol value") + } + } + + override fun symbolValue(): SymbolToken? { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val data = Instructions.getData(instruction) + val i = bytecodeI + return when (op) { + Operation.OP_NULL_SYMBOL -> null + Operation.OP_SYMBOL_CP -> createSymbolToken(constantPool[data] as String, -1) + Operation.OP_SYMBOL_REF -> createSymbolToken(generator.readTextReference(position = bytecode[i], length = data), -1) + Operation.OP_SYMBOL_CHAR -> createSymbolToken(data.toChar().toString(), -1) + Operation.OP_SYMBOL_SID -> createSymbolToken(symbolTable[data], data) + else -> throw IonException("Not positioned on a symbol") + } + } + + // TODO: don't return null + override fun getSymbolTable(): SymbolTable? = null + + override fun byteSize(): Int { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val data = Instructions.getData(instruction) + return when (op) { + Operation.OP_BLOB_CP, + Operation.OP_CLOB_CP -> (constantPool[data] as ByteSlice).length + Operation.OP_BLOB_REF, + Operation.OP_CLOB_REF -> data + else -> throw IonException("Not positioned on a lob value") + } + } + + override fun newBytes(): ByteArray { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val data = Instructions.getData(instruction) + val i = bytecodeI + return when (op) { + Operation.OP_BLOB_CP, + Operation.OP_CLOB_CP -> (constantPool[data] as ByteSlice).newByteArray() + Operation.OP_BLOB_REF, + Operation.OP_CLOB_REF -> generator.readBytesReference(position = bytecode[i], length = data).newByteArray() + else -> throw IonException("Not positioned on a lob value") + } + } + + override fun getBytes(buffer: ByteArray, offset: Int, len: Int): Int { + val instruction = this.instruction + val op = Instructions.toOperation(instruction) + val data = Instructions.getData(instruction) + val i = bytecodeI + val slice = when (op) { + Operation.OP_BLOB_CP, + Operation.OP_CLOB_CP -> (constantPool[data] as ByteSlice) + Operation.OP_BLOB_REF, + Operation.OP_CLOB_REF -> generator.readBytesReference(position = bytecode[i], length = data) + else -> throw IonException("Not positioned on a lob value") + } + slice.bytes.copyInto(buffer, offset, slice.startInclusive, slice.endExclusive) + return slice.length + } + + override fun asFacet(facetType: Class?): T = TODO("Not yet implemented") +} diff --git a/src/main/java/com/amazon/ion/bytecode/EncodingContextManager.kt b/src/main/java/com/amazon/ion/bytecode/EncodingContextManager.kt new file mode 100644 index 000000000..8afb94a25 --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/EncodingContextManager.kt @@ -0,0 +1,145 @@ +// 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.bytecode.util.BytecodeBuffer +import com.amazon.ion.bytecode.util.ConstantPool +import com.amazon.ion.ion_1_1.MacroImpl +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +/** + * TODO: + * Write more documentation. + * Implement stubbed out methods. + * + * Notes: + * + * It is never safe to remove or modify any existing data in the effective tables. It is safe to append data to those + * tables for an `add_symbols`, `add_macros`, or `use` directive (as long as the active encoding modules are just `$ion` and `_`). + */ +internal class EncodingContextManager { + + companion object { + val SYSTEM_SYMBOLS = arrayOf( + null, + "\$ion", + "\$ion_1_0", + "\$ion_symbol_table", + "name", + "version", + "imports", + "symbols", + "max_id", + "\$ion_shared_symbol_table", + ) + } + + // These make up the effective macro table and effective symbol table + private var macroBytecode = BytecodeBuffer() + private var macroOffsets = BytecodeBuffer() + private var macroNames = ConstantPool() + private var symbols = mutableListOf().apply { SYSTEM_SYMBOLS.forEach { add(it) } } + + // TODO: Do we need the constant pool here? + private var constants = ConstantPool() + + private class Module( + val symbols: Array, + val macros: Array, + val macroNames: Array + ) + + // Tracks only modules _other_ than the system module and default module + private val additionalAvailableModules = mutableMapOf() + // Tracks only modules _other_ than the system module and default module + private var additionalActiveModules = mutableListOf() + + @SuppressFBWarnings("IE_EXPOSE_REP", justification = "array is accessible for performance") + fun getEffectiveMacroTableBytecode(): IntArray = macroBytecode.unsafeGetArray() + + @SuppressFBWarnings("IE_EXPOSE_REP", justification = "array is accessible for performance") + fun getEffectiveMacroTableOffsets(): IntArray = macroOffsets.unsafeGetArray() + + fun getEffectiveSymbolTable(): Array = symbols.toTypedArray() + + @SuppressFBWarnings("IE_EXPOSE_REP", justification = "array is accessible for performance") + fun getEffectiveConstantPool(): Array = constants.unsafeGetArray() + + /** Called when encountering an IVM */ + fun reset() { + additionalActiveModules.clear() + additionalAvailableModules.clear() + macroBytecode.clear() + macroOffsets.clear() + macroNames.clear() + symbols.clear() + SYSTEM_SYMBOLS.forEach { symbols.add(it) } + constants.clear() + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + */ + fun readSetSymbolsDirective(reader: BytecodeIonReader) { + TODO() + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + */ + fun readAddSymbols(reader: BytecodeIonReader) { + TODO() + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + */ + fun readSetMacrosDirective(reader: BytecodeIonReader) { + TODO() + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + */ + fun readAddMacrosDirective(reader: BytecodeIonReader) { + TODO() + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + */ + fun readUseDirective(reader: BytecodeIonReader) { + TODO("Shared symbol tables and shared modules not supported yet.") + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + */ + fun readModuleDirective(reader: BytecodeIonReader) { + TODO("Module definitions not supported yet.") + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + */ + fun readImportDirective(reader: BytecodeIonReader) { + TODO("Shared symbol tables and shared modules not supported yet.") + } + + /** + * The [BytecodeIonReader] should be positioned in the directive, but not on the first value yet. + * When this method returns, the [BytecodeIonReader] will be positioned at the end of the directive, but not stepped out. + * + * Content should be a list of module names, as symbols. + */ + fun readEncodingDirective(reader: BytecodeIonReader) { + TODO() + } +} diff --git a/src/main/java/com/amazon/ion/bytecode/ScalarConversionHelper.kt b/src/main/java/com/amazon/ion/bytecode/ScalarConversionHelper.kt new file mode 100644 index 000000000..5e7cd52fe --- /dev/null +++ b/src/main/java/com/amazon/ion/bytecode/ScalarConversionHelper.kt @@ -0,0 +1,46 @@ +// 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.impl._Private_ScalarConversions +import com.amazon.ion.impl._Private_ScalarConversions.ValueVariant +import java.math.BigInteger + +/** + * Wraps [_Private_ScalarConversions] to cut down on repeated code in [BytecodeIonReader]. + * + * This abstraction probably adds overhead, but it should only be used in the non-ideal paths—performing **lossy** + * conversions on scalar values. + */ +internal class ScalarConversionHelper { + private val scalarConverter = ValueVariant() + private val preparedConverter = ThisPreparedConverter() + + private inline fun initConversion(startType: Int, addValueFn: ValueVariant.() -> Unit): PreparedConverter { + val converter = scalarConverter + converter.clear() + converter.addValueFn() + converter.authoritativeType = startType + return preparedConverter + } + + fun from(value: Int) = initConversion(_Private_ScalarConversions.AS_TYPE.int_value) { addValue(value) } + fun from(value: Long) = initConversion(_Private_ScalarConversions.AS_TYPE.long_value) { addValue(value) } + fun from(value: BigInteger?) = initConversion(_Private_ScalarConversions.AS_TYPE.bigInteger_value) { addValue(value) } + fun from(value: Double) = initConversion(_Private_ScalarConversions.AS_TYPE.double_value) { addValue(value) } + fun from(value: Decimal?) = initConversion(_Private_ScalarConversions.AS_TYPE.decimal_value) { addValue(value) } + + sealed class PreparedConverter(private val converter: ValueVariant) { + + private fun doConversion(toType: Int): ValueVariant = converter.apply { cast(get_conversion_fnid(toType)) } + + fun intoInt(): Int = doConversion(_Private_ScalarConversions.AS_TYPE.int_value).int + fun intoLong(): Long = doConversion(_Private_ScalarConversions.AS_TYPE.long_value).long + fun intoBigInteger(): BigInteger = doConversion(_Private_ScalarConversions.AS_TYPE.bigInteger_value).bigInteger + fun intoDecimal(): Decimal = doConversion(_Private_ScalarConversions.AS_TYPE.decimal_value).decimal + fun intoDouble(): Double = doConversion(_Private_ScalarConversions.AS_TYPE.double_value).double + } + + private inner class ThisPreparedConverter : PreparedConverter(this@ScalarConversionHelper.scalarConverter) +} 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 475320741..144c43ad5 100644 --- a/src/main/java/com/amazon/ion/bytecode/ir/Debugger.kt +++ b/src/main/java/com/amazon/ion/bytecode/ir/Debugger.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.bytecode.ir +import com.amazon.ion.bytecode.ir.Debugger.invoke import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import java.util.function.Consumer @@ -31,6 +32,15 @@ import java.util.function.Consumer ) internal object Debugger { + @OptIn(ExperimentalStdlibApi::class) + fun renderSingleInstruction(instruction: Int): String { + val operationInt = Instructions.toOperation(instruction) + val instructionInfo = InstructionInfo.entries.singleOrNull { it.operation == operationInt } + + instructionInfo ?: return "UNKNOWN ${instruction.toHexString()}" + return "${instructionInfo.name} ${instructionInfo.dataType.formatter(Instructions.getData(instruction))}" + } + /** * Helper function to render bytecode as an `Array` to make it easier to read in the IntelliJ debugger. */ diff --git a/src/main/java/com/amazon/ion/bytecode/ir/OperationKind.kt b/src/main/java/com/amazon/ion/bytecode/ir/OperationKind.kt index d5870ad37..390f06984 100644 --- a/src/main/java/com/amazon/ion/bytecode/ir/OperationKind.kt +++ b/src/main/java/com/amazon/ion/bytecode/ir/OperationKind.kt @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.bytecode.ir +import com.amazon.ion.IonType + /** * Constants defining the different categories of operations for the bytecode instruction set. * @@ -98,4 +100,27 @@ internal object OperationKind { else -> throw IllegalArgumentException("Not a valid instruction kind: $instructionKind") } } + + /** + * Returns the [IonType] corresponding to the operation kind, if any. + */ + @JvmStatic + fun ionTypeOf(operationKind: Int): IonType? { + return when (operationKind) { + NULL -> IonType.NULL + BOOL -> IonType.BOOL + INT -> IonType.INT + FLOAT -> IonType.FLOAT + DECIMAL -> IonType.DECIMAL + TIMESTAMP -> IonType.TIMESTAMP + STRING -> IonType.STRING + SYMBOL -> IonType.SYMBOL + CLOB -> IonType.CLOB + BLOB -> IonType.BLOB + LIST -> IonType.LIST + SEXP -> IonType.SEXP + STRUCT -> IonType.STRUCT + else -> null + } + } } diff --git a/src/main/java/com/amazon/ion/bytecode/ir/instruction_reference.md b/src/main/java/com/amazon/ion/bytecode/ir/instruction_reference.md index 094ed12e7..d993f4982 100644 --- a/src/main/java/com/amazon/ion/bytecode/ir/instruction_reference.md +++ b/src/main/java/com/amazon/ion/bytecode/ir/instruction_reference.md @@ -79,9 +79,9 @@ This instruction indicates that there is a String value, and the text of the str | NULL_BLOB | `0x57` | `01010` | `111` | `00` | - | - | | | LIST_START | `0x58` | `01011` | `000` | `11` | bytecode_length (u22) | - | Length must include the END_CONTAINER instruction | | NULL_LIST | `0x5F` | `01011` | `111` | `00` | - | - | | -| SEXP_START | `0x60` | `01100` | `000` | `11` | bytecode_length (u22) | - | Length must include the END_CONTAINER instruction | +| SEXP_START | `0x60` | `01100` | `000` | `11` | bytecode_length (u22) | - | Length must include the END_CONTAINER instruction | | NULL_SEXP | `0x67` | `01100` | `111` | `00` | - | - | | -| STRUCT_START | `0x68` | `01101` | `000` | `11` | bytecode_length (u22) | - | Length must include the END_CONTAINER instruction | +| STRUCT_START | `0x68` | `01101` | `000` | `11` | bytecode_length (u22) | - | Length must include the END_CONTAINER instruction | | NULL_STRUCT | `0x6F` | `01101` | `111` | `00` | - | - | | | ANNOTATION_CP | `0x70` | `01110` | `000` | `00` | cp_index (u22) | - | Non-null [String] in constant pool | | ANNOTATION_REF | `0x71` | `01110` | `001` | `01` | ref_length (u22) | offset (u32) | Reference to UTF-8 bytes | @@ -90,7 +90,7 @@ This instruction indicates that there is a String value, and the text of the str | FIELD_NAME_REF | `0x79` | `01111` | `001` | `01` | ref_length (u22) | offset (u32) | Reference to UTF-8 bytes | | FIELD_NAME_SID | `0x7A` | `01111` | `010` | `00` | sid (u22) | - | | | IVM | `0x80` | `10000` | `000` | `00` | version (u8, u8) | - | version is packed as u8 major, u8 minor | -| DIRECTIVE_SET_SYMBOLS | `0x88` | `10001` | `000` | `00` | - | - | Must have END_CONTAINER instruction to delimit end of directive | +| DIRECTIVE_SET_SYMBOLS | `0x88` | `10001` | `000` | `00` | - | - | Must have END_CONTAINER instruction to delimit end of directive | | DIRECTIVE_ADD_SYMBOLS | `0x89` | `10001` | `001` | `00` | - | - | Must have END_CONTAINER instruction to delimit end of directive | | DIRECTIVE_SET_MACROS | `0x8A` | `10001` | `010` | `00` | - | - | Must have END_CONTAINER instruction to delimit end of directive | | DIRECTIVE_ADD_MACROS | `0x8B` | `10001` | `011` | `00` | - | - | Must have END_CONTAINER instruction to delimit end of directive | @@ -127,6 +127,18 @@ Possible TODOs: from inside a lengthy NOP. Comments that are longer than u22 max value could be encoded using multiple comment instructions. The span should include the comment-delimiting characters. +## Integers + +* All eagerly-read integer values MUST be encoded using the instruction with the smallest integer size in which the integer value can fit. +* The length value for an `INT_REF` instruction must include the sign bit. + +## Exposing unevaluated macro invocations + +* Generators may be configured to expose some or all macro invocations. +* Generators should expose macro invocations using the `INVOKE` instruction +* The `INVOKE` instruction is followed by one argument for each parameter in the signature (making any "no argument" expressions explicit). +* The arguments are followed by the `END_CONTAINER` instruction. (TODO: Should we have a distinct `END_INVOKE` instruction?) + ## Directive Content ### `SET_SYMBOLS`, `ADD_SYMBOLS` diff --git a/src/main/java/com/amazon/ion/bytecode/util/AppendableConstantPoolView.kt b/src/main/java/com/amazon/ion/bytecode/util/AppendableConstantPoolView.kt index 89e70a969..616953398 100644 --- a/src/main/java/com/amazon/ion/bytecode/util/AppendableConstantPoolView.kt +++ b/src/main/java/com/amazon/ion/bytecode/util/AppendableConstantPoolView.kt @@ -9,7 +9,7 @@ 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? + operator fun get(i: Int): Any? val size: Int } diff --git a/src/main/java/com/amazon/ion/bytecode/util/BytecodeBuffer.kt b/src/main/java/com/amazon/ion/bytecode/util/BytecodeBuffer.kt index e72dc0f2d..ff7d16538 100644 --- a/src/main/java/com/amazon/ion/bytecode/util/BytecodeBuffer.kt +++ b/src/main/java/com/amazon/ion/bytecode/util/BytecodeBuffer.kt @@ -142,10 +142,22 @@ internal class BytecodeBuffer private constructor( * @param length the number of bytecode instructions to copy */ fun addSlice(values: BytecodeBuffer, startInclusive: Int, length: Int) { + addSlice(values.data, startInclusive, length) + } + + /** + * Appends a slice of bytecode instructions from an [IntArray] to this buffer. + * The buffer will automatically grow if necessary to accommodate the new instructions. + * + * @param values the source `IntArray` to copy from + * @param startInclusive the starting index in the source array (inclusive) + * @param length the number of bytecode instructions to copy + */ + fun addSlice(values: IntArray, startInclusive: Int, length: Int) { val thisNumberOfValues = this.numberOfValues val newNumberOfValues = thisNumberOfValues + length val data = ensureCapacity(newNumberOfValues) - System.arraycopy(values.data, startInclusive, data, thisNumberOfValues, length) + System.arraycopy(values, startInclusive, data, thisNumberOfValues, length) this.numberOfValues = newNumberOfValues } diff --git a/src/main/java/com/amazon/ion/bytecode/util/ConstantPool.kt b/src/main/java/com/amazon/ion/bytecode/util/ConstantPool.kt index 0aaa1069d..231375bbd 100644 --- a/src/main/java/com/amazon/ion/bytecode/util/ConstantPool.kt +++ b/src/main/java/com/amazon/ion/bytecode/util/ConstantPool.kt @@ -49,7 +49,7 @@ internal class ConstantPool private constructor( /** * Returns the `i`th int in the list. */ - override fun get(i: Int): Any? { + override operator fun get(i: Int): Any? { if (i < 0 || i >= numberOfValues) { throw IndexOutOfBoundsException("Invalid index $i requested from IntList with $numberOfValues values.") } diff --git a/src/test/java/com/amazon/ion/bytecode/BytecodeIonReaderTest.kt b/src/test/java/com/amazon/ion/bytecode/BytecodeIonReaderTest.kt new file mode 100644 index 000000000..bd1a16580 --- /dev/null +++ b/src/test/java/com/amazon/ion/bytecode/BytecodeIonReaderTest.kt @@ -0,0 +1,1707 @@ +// 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.IntegerSize +import com.amazon.ion.IonException +import com.amazon.ion.IonReader +import com.amazon.ion.IonType +import com.amazon.ion.SymbolToken +import com.amazon.ion.Timestamp +import com.amazon.ion.bytecode.ir.Instructions.I_ANNOTATION_CP +import com.amazon.ion.bytecode.ir.Instructions.I_ANNOTATION_REF +import com.amazon.ion.bytecode.ir.Instructions.I_ANNOTATION_SID +import com.amazon.ion.bytecode.ir.Instructions.I_ARGUMENT_NONE +import com.amazon.ion.bytecode.ir.Instructions.I_BLOB_CP +import com.amazon.ion.bytecode.ir.Instructions.I_BLOB_REF +import com.amazon.ion.bytecode.ir.Instructions.I_BOOL +import com.amazon.ion.bytecode.ir.Instructions.I_CLOB_CP +import com.amazon.ion.bytecode.ir.Instructions.I_CLOB_REF +import com.amazon.ion.bytecode.ir.Instructions.I_DECIMAL_CP +import com.amazon.ion.bytecode.ir.Instructions.I_DECIMAL_REF +import com.amazon.ion.bytecode.ir.Instructions.I_END_CONTAINER +import com.amazon.ion.bytecode.ir.Instructions.I_END_OF_INPUT +import com.amazon.ion.bytecode.ir.Instructions.I_FIELD_NAME_CP +import com.amazon.ion.bytecode.ir.Instructions.I_FIELD_NAME_REF +import com.amazon.ion.bytecode.ir.Instructions.I_FIELD_NAME_SID +import com.amazon.ion.bytecode.ir.Instructions.I_FLOAT_F32 +import com.amazon.ion.bytecode.ir.Instructions.I_FLOAT_F64 +import com.amazon.ion.bytecode.ir.Instructions.I_INT_CP +import com.amazon.ion.bytecode.ir.Instructions.I_INT_I16 +import com.amazon.ion.bytecode.ir.Instructions.I_INT_I32 +import com.amazon.ion.bytecode.ir.Instructions.I_INT_I64 +import com.amazon.ion.bytecode.ir.Instructions.I_INT_REF +import com.amazon.ion.bytecode.ir.Instructions.I_IVM +import com.amazon.ion.bytecode.ir.Instructions.I_LIST_START +import com.amazon.ion.bytecode.ir.Instructions.I_SEXP_START +import com.amazon.ion.bytecode.ir.Instructions.I_STRING_CP +import com.amazon.ion.bytecode.ir.Instructions.I_STRING_REF +import com.amazon.ion.bytecode.ir.Instructions.I_STRUCT_START +import com.amazon.ion.bytecode.ir.Instructions.I_SYMBOL_CHAR +import com.amazon.ion.bytecode.ir.Instructions.I_SYMBOL_CP +import com.amazon.ion.bytecode.ir.Instructions.I_SYMBOL_REF +import com.amazon.ion.bytecode.ir.Instructions.I_SYMBOL_SID +import com.amazon.ion.bytecode.ir.Instructions.I_TIMESTAMP_CP +import com.amazon.ion.bytecode.ir.Instructions.I_TIMESTAMP_REF +import com.amazon.ion.bytecode.ir.Instructions.packInstructionData +import com.amazon.ion.bytecode.util.AppendableConstantPoolView +import com.amazon.ion.bytecode.util.ByteSlice +import com.amazon.ion.bytecode.util.BytecodeBuffer +import com.amazon.ion.bytecode.util.ConstantPool +import com.amazon.ion.impl._Private_Utils +import com.amazon.ion.system.IonSystemBuilder +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import java.math.BigDecimal +import java.math.BigInteger + +class BytecodeIonReaderTest { + + private val ION = IonSystemBuilder.standard().build() + + @Test + fun `read I_BOOL`() { + val generator = MockGenerator( + I_BOOL.packInstructionData(0), + I_BOOL.packInstructionData(1), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.BOOL + type shouldBe IonType.BOOL + booleanValue() shouldBe false + + next() shouldBe IonType.BOOL + booleanValue() shouldBe true + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Nested + inner class `INT operations` { + + @Test + fun `read I_INT_16`() { + val generator = MockGenerator( + I_INT_I16.packInstructionData(123), + I_END_OF_INPUT + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + isNullValue shouldBe false + + integerSize shouldBe IntegerSize.INT + + intValue() shouldBe 123 + longValue() shouldBe 123L + bigIntegerValue() shouldBe 123.toBigInteger() + doubleValue() shouldBe 123.toDouble() + decimalValue() shouldBe Decimal.valueOf(123) + bigDecimalValue() shouldBe 123.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_INT_32`() { + val generator = MockGenerator( + I_INT_I32, + 234, + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + isNullValue shouldBe false + + integerSize shouldBe IntegerSize.INT + + intValue() shouldBe 234 + longValue() shouldBe 234L + bigIntegerValue() shouldBe 234.toBigInteger() + doubleValue() shouldBe 234.toDouble() + decimalValue() shouldBe Decimal.valueOf(234) + bigDecimalValue() shouldBe 234.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_INT_64`() { + val generator = MockGenerator( + I_INT_I64, + 0x12345678, + 0x13579ACE, + I_END_OF_INPUT, + ) + + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + isNullValue shouldBe false + + integerSize shouldBe IntegerSize.LONG + + // Because it's too large for an Int + shouldThrow { intValue() } + + longValue() shouldBe 0x12345678_13579ACEL + bigIntegerValue() shouldBe 0x12345678_13579ACE.toBigInteger() + doubleValue() shouldBe 0x12345678_13579ACE.toDouble() + decimalValue() shouldBe Decimal.valueOf(0x12345678_13579ACE) + bigDecimalValue() shouldBe 0x12345678_13579ACE.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_INT_CP`() { + val expectedBigInteger = BigInteger("999999999999999999999999999999") + + val generator = MockGenerator( + I_INT_CP.packInstructionData(4), + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(4) { add(null) } + add(expectedBigInteger) + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + isNullValue shouldBe false + + integerSize shouldBe IntegerSize.BIG_INTEGER + + shouldThrow { intValue() } + shouldThrow { longValue() } + bigIntegerValue() shouldBe expectedBigInteger + doubleValue() shouldBe expectedBigInteger.toDouble() + decimalValue() shouldBe Decimal.valueOf(expectedBigInteger) + bigDecimalValue() shouldBe expectedBigInteger.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_INT_REF`() { + val generator = MockGenerator( + I_INT_REF.packInstructionData(10), + 123, + I_END_OF_INPUT, + references = mapOf(123 to BigInteger.TEN) + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + isNullValue shouldBe false + + integerSize shouldBe IntegerSize.BIG_INTEGER + + intValue() shouldBe 10 + longValue() shouldBe 10L + bigIntegerValue() shouldBe 10.toBigInteger() + doubleValue() shouldBe 10.toDouble() + decimalValue() shouldBe Decimal.valueOf(10) + bigDecimalValue() shouldBe 10.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `FLOAT operations` { + + @Test + fun `read I_FLOAT_F32`() { + val generator = MockGenerator( + I_FLOAT_F32, + 0x40040000, + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.FLOAT + isNullValue shouldBe false + intValue() shouldBe 2 + longValue() shouldBe 2L + bigIntegerValue() shouldBe 2.toBigInteger() + doubleValue() shouldBe 2.0625 + decimalValue() shouldBe Decimal.valueOf(2.0625) + bigDecimalValue() shouldBe 2.0625.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_FLOAT_F64`() { + val generator = MockGenerator( + I_FLOAT_F64, + 0x40000800, + 0x00000000, + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.FLOAT + isNullValue shouldBe false + intValue() shouldBe 2 + longValue() shouldBe 2L + bigIntegerValue() shouldBe 2.toBigInteger() + doubleValue() shouldBe 2.00390625 + decimalValue() shouldBe Decimal.valueOf(2.00390625) + bigDecimalValue() shouldBe 2.00390625.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `DECIMAL operations` { + + @Test + fun `read I_DECIMAL_CP`() { + val generator = MockGenerator( + I_DECIMAL_CP.packInstructionData(4), + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(4) { add(null) } + add(Decimal.TEN) + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.DECIMAL + isNullValue shouldBe false + intValue() shouldBe 10 + longValue() shouldBe 10L + bigIntegerValue() shouldBe 10.toBigInteger() + doubleValue() shouldBe 10.toDouble() + decimalValue() shouldBe Decimal.valueOf(10) + bigDecimalValue() shouldBe 10.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_DECIMAL_REF`() { + val generator = MockGenerator( + I_DECIMAL_REF.packInstructionData(4), + 123, + I_END_OF_INPUT, + references = mapOf(123 to Decimal.TEN) + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.DECIMAL + isNullValue shouldBe false + intValue() shouldBe 10 + longValue() shouldBe 10L + bigIntegerValue() shouldBe 10.toBigInteger() + doubleValue() shouldBe 10.toDouble() + decimalValue() shouldBe Decimal.valueOf(10) + bigDecimalValue() shouldBe 10.toBigDecimal() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `TIMESTAMP operations` { + val THE_TIMESTAMP = Timestamp.valueOf("2025-10-27T") + + @Test + fun `read I_TIMESTAMP_CP`() { + val generator = MockGenerator( + I_TIMESTAMP_CP.packInstructionData(4), + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(4) { add(null) } + add(THE_TIMESTAMP) + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.TIMESTAMP + isNullValue shouldBe false + timestampValue() shouldBe THE_TIMESTAMP + dateValue() shouldBe THE_TIMESTAMP.dateValue() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_TIMESTAMP_REF`() { + val generator = MockGenerator( + I_TIMESTAMP_REF.packInstructionData(4), + 123, + I_END_OF_INPUT, + references = mapOf(123 to THE_TIMESTAMP) + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.TIMESTAMP + isNullValue shouldBe false + timestampValue() shouldBe THE_TIMESTAMP + dateValue() shouldBe THE_TIMESTAMP.dateValue() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_SHORT_TIMESTAMP_REF`() { + val generator = MockGenerator( + I_TIMESTAMP_REF.packInstructionData(4), + 123, + I_END_OF_INPUT, + references = mapOf(123 to THE_TIMESTAMP) + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.TIMESTAMP + isNullValue shouldBe false + timestampValue() shouldBe THE_TIMESTAMP + dateValue() shouldBe THE_TIMESTAMP.dateValue() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `STRING operations` { + @Test + fun `read I_STRING_CP`() { + val generator = MockGenerator( + I_STRING_CP.packInstructionData(4), + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(4) { add(null) } + add("hello world") + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRING + isNullValue shouldBe false + stringValue() shouldBe "hello world" + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_STRING_REF`() { + val generator = MockGenerator( + I_STRING_REF.packInstructionData(4), + 123, + I_END_OF_INPUT, + references = mapOf(123 to "hello world") + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRING + isNullValue shouldBe false + stringValue() shouldBe "hello world" + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `SYMBOL operations` { + + @Test + fun `read I_SYMBOL_CP`() { + val generator = MockGenerator( + I_SYMBOL_CP.packInstructionData(4), + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(4) { add(null) } + add("hello world") + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SYMBOL + isNullValue shouldBe false + stringValue() shouldBe "hello world" + symbolValue() shouldBe symbolToken("hello world", -1) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_SYMBOL_REF`() { + val generator = MockGenerator( + I_SYMBOL_REF.packInstructionData(4), + 123, + I_END_OF_INPUT, + references = mapOf(123 to "hello world") + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SYMBOL + isNullValue shouldBe false + stringValue() shouldBe "hello world" + symbolValue() shouldBe symbolToken("hello world", -1) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_SYMBOL_SID`() { + val generator = MockGenerator( + I_SYMBOL_SID.packInstructionData(4), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SYMBOL + isNullValue shouldBe false + stringValue() shouldBe "name" + symbolValue() shouldBe symbolToken("name", 4) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_SYMBOL_SID for $0`() { + val generator = MockGenerator( + I_SYMBOL_SID.packInstructionData(0), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SYMBOL + isNullValue shouldBe false + stringValue() shouldBe null + symbolValue() shouldBe symbolToken(null, 0) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_SYMBOL_CHAR`() { + val generator = MockGenerator( + I_SYMBOL_CHAR.packInstructionData('a'.code), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SYMBOL + isNullValue shouldBe false + stringValue() shouldBe "a" + symbolValue() shouldBe symbolToken("a", -1) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `LOB operations` { + + private val THE_BYTES = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8) + private val THE_BYTES_COPIED_INTO_ARRAY = byteArrayOf(0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0) + + @Test + fun `read I_CLOB_CP`() { + val generator = MockGenerator( + I_CLOB_CP.packInstructionData(4), + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(4) { add(null) } + add(ByteSlice(THE_BYTES, 0, 8)) + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.CLOB + isNullValue shouldBe false + byteSize() shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES, newBytes()) + val bytes = ByteArray(16) + val numBytesCopied = getBytes(bytes, 4, bytes.size) + numBytesCopied shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES_COPIED_INTO_ARRAY, bytes) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_CLOB_REF`() { + val generator = MockGenerator( + I_CLOB_REF.packInstructionData(8), + 123, + I_END_OF_INPUT, + references = mapOf(123 to ByteSlice(THE_BYTES, 0, 8)) + ) + + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.CLOB + isNullValue shouldBe false + byteSize() shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES, newBytes()) + val bytes = ByteArray(16) + val numBytesCopied = getBytes(bytes, 4, bytes.size) + numBytesCopied shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES_COPIED_INTO_ARRAY, bytes) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_BLOB_CP`() { + val generator = MockGenerator( + I_BLOB_CP.packInstructionData(4), + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(4) { add(null) } + add(ByteSlice(THE_BYTES, 0, 8)) + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.BLOB + isNullValue shouldBe false + byteSize() shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES, newBytes()) + val bytes = ByteArray(16) + val numBytesCopied = getBytes(bytes, 4, bytes.size) + numBytesCopied shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES_COPIED_INTO_ARRAY, bytes) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_BLOB_REF`() { + val generator = MockGenerator( + I_BLOB_REF.packInstructionData(8), + 123, + I_END_OF_INPUT, + references = mapOf(123 to ByteSlice(THE_BYTES, 0, 8)) + ) + + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.BLOB + isNullValue shouldBe false + byteSize() shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES, newBytes()) + val bytes = ByteArray(16) + val numBytesCopied = getBytes(bytes, 4, bytes.size) + numBytesCopied shouldBe THE_BYTES.size + assertArrayEquals(THE_BYTES_COPIED_INTO_ARRAY, bytes) + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `LIST operations` { + + @Test + fun `read a list`() { + val generator = MockGenerator( + I_LIST_START.packInstructionData(3), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.LIST + isNullValue shouldBe false + + depth shouldBe 0 + stepIn() + depth shouldBe 1 + + next() shouldBe IonType.INT + intValue() shouldBe 0 + next() shouldBe IonType.INT + intValue() shouldBe 1 + + // End of list + next() shouldBe null + // Even if we try again, it should still be the end of the list. + next() shouldBe null + + depth shouldBe 1 + stepOut() + depth shouldBe 0 + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `step out early from a list`() { + val generator = MockGenerator( + I_LIST_START.packInstructionData(3), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.LIST + isNullValue shouldBe false + + depth shouldBe 0 + stepIn() + depth shouldBe 1 + + next() shouldBe IonType.INT + intValue() shouldBe 0 + + depth shouldBe 1 + stepOut() + depth shouldBe 0 + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `skip a list`() { + val generator = MockGenerator( + I_LIST_START.packInstructionData(3), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.LIST + isNullValue shouldBe false + depth shouldBe 0 + next() shouldBe IonType.INT + depth shouldBe 0 + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read an empty list`() { + val generator = MockGenerator( + I_LIST_START.packInstructionData(1), + I_END_CONTAINER, + // value here helps test that we are properly respecting the container boundaries. + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.LIST + isNullValue shouldBe false + + stepIn() + // End of list + next() shouldBe null + // Even if we try again, it should still be the end of the list. + next() shouldBe null + stepOut() + + next() shouldBe IonType.INT + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `step out early from an empty list`() { + val generator = MockGenerator( + I_LIST_START.packInstructionData(1), + I_END_CONTAINER, + // value here helps test that we are properly respecting the container boundaries. + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.LIST + isNullValue shouldBe false + + stepIn() + // Stepping out early in that we didn't even look to see if there is a value in the list + stepOut() + + next() shouldBe IonType.INT + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `skip an empty list`() { + val generator = MockGenerator( + I_LIST_START.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.LIST + isNullValue shouldBe false + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `SEXP operations` { + + @Test + fun `read a sexp`() { + val generator = MockGenerator( + I_SEXP_START.packInstructionData(3), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SEXP + isNullValue shouldBe false + + depth shouldBe 0 + stepIn() + depth shouldBe 1 + + next() shouldBe IonType.INT + intValue() shouldBe 0 + next() shouldBe IonType.INT + intValue() shouldBe 1 + + // End of sexp + next() shouldBe null + // Even if we try again, it should still be the end of the sexp. + next() shouldBe null + + depth shouldBe 1 + stepOut() + depth shouldBe 0 + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `step out early from a sexp`() { + val generator = MockGenerator( + I_SEXP_START.packInstructionData(3), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SEXP + isNullValue shouldBe false + + depth shouldBe 0 + stepIn() + depth shouldBe 1 + + next() shouldBe IonType.INT + intValue() shouldBe 0 + + depth shouldBe 1 + stepOut() + depth shouldBe 0 + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `skip a sexp`() { + val generator = MockGenerator( + I_SEXP_START.packInstructionData(3), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SEXP + isNullValue shouldBe false + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read an empty sexp`() { + val generator = MockGenerator( + I_SEXP_START.packInstructionData(1), + I_END_CONTAINER, + // value here helps test that we are properly respecting the container boundaries. + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SEXP + isNullValue shouldBe false + + stepIn() + // End of sexp + next() shouldBe null + // Even if we try again, it should still be the end of the sexp. + next() shouldBe null + stepOut() + + next() shouldBe IonType.INT + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `step out early from an empty sexp`() { + val generator = MockGenerator( + I_SEXP_START.packInstructionData(1), + I_END_CONTAINER, + // value here helps test that we are properly respecting the container boundaries. + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SEXP + isNullValue shouldBe false + + stepIn() + // Stepping out early in that we didn't even look to see if there is a value in the sexp + stepOut() + + next() shouldBe IonType.INT + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `skip an empty sexp`() { + val generator = MockGenerator( + I_SEXP_START.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.SEXP + isNullValue shouldBe false + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `STRUCT operations` { + + private fun IonReader.checkFieldName(text: String?, sid: Int) { + fieldName shouldBe text + fieldId shouldBe sid + fieldNameSymbol shouldBe symbolToken(text, sid) + } + + // Some of these tests do not check the field name. That is intentional so that different field name instructions + // can be tested somewhat independently of the struct traversal logic. + + @Test + fun `read a struct`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(5), + I_FIELD_NAME_SID.packInstructionData(4), + I_INT_I16.packInstructionData(0), + I_FIELD_NAME_SID.packInstructionData(5), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + isInStruct shouldBe false + depth shouldBe 0 + + stepIn() + + isInStruct shouldBe true + depth shouldBe 1 + + next() shouldBe IonType.INT + intValue() shouldBe 0 + next() shouldBe IonType.INT + intValue() shouldBe 1 + + // End of struct + next() shouldBe null + // Even if we try again, it should still be the end of the struct. + next() shouldBe null + stepOut() + + isInStruct shouldBe false + depth shouldBe 0 + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `step out early from a struct`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(5), + I_FIELD_NAME_SID.packInstructionData(4), + I_INT_I16.packInstructionData(0), + I_FIELD_NAME_SID.packInstructionData(5), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + isInStruct shouldBe false + depth shouldBe 0 + + stepIn() + + isInStruct shouldBe true + depth shouldBe 1 + + next() shouldBe IonType.INT + intValue() shouldBe 0 + stepOut() + + isInStruct shouldBe false + depth shouldBe 0 + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `skip a struct`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(5), + I_FIELD_NAME_SID.packInstructionData(4), + I_INT_I16.packInstructionData(0), + I_FIELD_NAME_SID.packInstructionData(5), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + + isInStruct shouldBe false + depth shouldBe 0 + + next() shouldBe IonType.INT + + isInStruct shouldBe false + depth shouldBe 0 + + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read an empty struct`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(1), + I_END_CONTAINER, + // value here helps test that we are properly respecting the container boundaries. + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + + stepIn() + // End of struct + next() shouldBe null + // Even if we try again, it should still be the end of the struct. + next() shouldBe null + stepOut() + + next() shouldBe IonType.INT + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `step out early from an empty struct`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(1), + I_END_CONTAINER, + // value here helps test that we are properly respecting the container boundaries. + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + + stepIn() + // Stepping out early in that we didn't even look to see if there is a value in the struct + stepOut() + + next() shouldBe IonType.INT + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `skip an empty struct`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(1), + I_END_CONTAINER, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + + next() shouldBe IonType.INT + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_FIELD_NAME_SID`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(7), + I_FIELD_NAME_SID.packInstructionData(4), + I_INT_I16.packInstructionData(0), + I_FIELD_NAME_SID.packInstructionData(5), + I_INT_I16.packInstructionData(1), + I_FIELD_NAME_SID.packInstructionData(0), + I_INT_I16.packInstructionData(2), + I_END_CONTAINER, + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + + stepIn() + + next() shouldBe IonType.INT + checkFieldName("name", 4) + intValue() shouldBe 0 + + next() shouldBe IonType.INT + checkFieldName("version", 5) + intValue() shouldBe 1 + + next() shouldBe IonType.INT + checkFieldName(null, 0) + intValue() shouldBe 2 + + next() shouldBe null + stepOut() + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_FIELD_NAME_CP`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(5), + I_FIELD_NAME_CP.packInstructionData(3), + I_INT_I16.packInstructionData(0), + I_FIELD_NAME_CP.packInstructionData(4), + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_END_OF_INPUT, + constants = ConstantPool().apply { + repeat(3) { add(null) } + add("foo") + add("bar") + } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + + stepIn() + + next() shouldBe IonType.INT + checkFieldName("foo", -1) + intValue() shouldBe 0 + next() shouldBe IonType.INT + checkFieldName("bar", -1) + intValue() shouldBe 1 + + next() shouldBe null + stepOut() + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_FIELD_NAME_REF`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(7), + I_FIELD_NAME_REF.packInstructionData(3), + 10, + I_INT_I16.packInstructionData(0), + I_FIELD_NAME_REF.packInstructionData(3), + 15, + I_INT_I16.packInstructionData(1), + I_END_CONTAINER, + I_END_OF_INPUT, + references = mapOf( + 10 to "foo", + 15 to "bar", + ) + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + isNullValue shouldBe false + + stepIn() + + next() shouldBe IonType.INT + checkFieldName("foo", -1) + intValue() shouldBe 0 + next() shouldBe IonType.INT + checkFieldName("bar", -1) + intValue() shouldBe 1 + + next() shouldBe null + stepOut() + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `ANNOTATION operations` { + + @Test + fun `read I_ANNOTATION_SID`() { + val generator = MockGenerator( + I_ANNOTATION_SID.packInstructionData(5), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + intValue() shouldBe 0 + typeAnnotations shouldBe arrayOf("version") + typeAnnotationSymbols shouldBe arrayOf(symbolToken("version", 5)) + iterateTypeAnnotations().asSequence().toList() shouldBe listOf("version") + + next() shouldBe IonType.INT + intValue() shouldBe 1 + // No annotations should leak into this value. + typeAnnotations shouldBe emptyArray() + typeAnnotationSymbols shouldBe emptyArray() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_ANNOTATION_SID for $0`() { + val generator = MockGenerator( + I_ANNOTATION_SID.packInstructionData(0), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + intValue() shouldBe 0 + typeAnnotations shouldBe arrayOf(null) + typeAnnotationSymbols shouldBe arrayOf(symbolToken(null, 0)) + iterateTypeAnnotations().asSequence().toList() shouldBe listOf(null) + + next() shouldBe IonType.INT + intValue() shouldBe 1 + // No annotations should leak into this value. + typeAnnotations shouldBe emptyArray() + typeAnnotationSymbols shouldBe emptyArray() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_ANNOTATION_CP`() { + val generator = MockGenerator( + I_ANNOTATION_CP.packInstructionData(0), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_OF_INPUT, + constants = ConstantPool().apply { add("foo") } + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + intValue() shouldBe 0 + typeAnnotations shouldBe arrayOf("foo") + typeAnnotationSymbols shouldBe arrayOf(symbolToken("foo", -1)) + iterateTypeAnnotations().asSequence().toList() shouldBe listOf("foo") + + next() shouldBe IonType.INT + intValue() shouldBe 1 + // No annotations should leak into this value. + typeAnnotations shouldBe emptyArray() + typeAnnotationSymbols shouldBe emptyArray() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read I_ANNOTATION_REF`() { + val generator = MockGenerator( + I_ANNOTATION_REF.packInstructionData(3), + 5, + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_OF_INPUT, + references = mapOf(5 to "foo"), + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + intValue() shouldBe 0 + typeAnnotations shouldBe arrayOf("foo") + typeAnnotationSymbols shouldBe arrayOf(symbolToken("foo", -1)) + iterateTypeAnnotations().asSequence().toList() shouldBe listOf("foo") + + next() shouldBe IonType.INT + intValue() shouldBe 1 + // No annotations should leak into this value. + typeAnnotations shouldBe emptyArray() + typeAnnotationSymbols shouldBe emptyArray() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read multiple annotations on a single value`() { + val generator = MockGenerator( + I_ANNOTATION_SID.packInstructionData(4), + I_ANNOTATION_SID.packInstructionData(5), + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_OF_INPUT, + references = mapOf(5 to "foo"), + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + intValue() shouldBe 0 + typeAnnotations shouldBe arrayOf("name", "version") + typeAnnotationSymbols shouldBe arrayOf(symbolToken("name", 4), symbolToken("version", 5)) + iterateTypeAnnotations().asSequence().toList() shouldBe listOf("name", "version") + + next() shouldBe IonType.INT + intValue() shouldBe 1 + // No annotations should leak into this value. + typeAnnotations shouldBe emptyArray() + typeAnnotationSymbols shouldBe emptyArray() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `read multiple types of annotations on a single value`() { + val generator = MockGenerator( + I_ANNOTATION_SID.packInstructionData(4), + I_ANNOTATION_CP.packInstructionData(1), + I_ANNOTATION_REF.packInstructionData(3), + 10, + I_INT_I16.packInstructionData(0), + I_INT_I16.packInstructionData(1), + I_END_OF_INPUT, + constants = ConstantPool().apply { add(null); add("foo") }, + references = mapOf(10 to "bar"), + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + intValue() shouldBe 0 + typeAnnotations shouldBe arrayOf("name", "foo", "bar") + typeAnnotationSymbols shouldBe arrayOf(symbolToken("name", 4), symbolToken("foo", -1), symbolToken("bar", -1)) + iterateTypeAnnotations().asSequence().toList() shouldBe listOf("name", "foo", "bar") + + next() shouldBe IonType.INT + intValue() shouldBe 1 + // No annotations should leak into this value. + typeAnnotations shouldBe emptyArray() + typeAnnotationSymbols shouldBe emptyArray() + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Nested + inner class `I_REFILL and I_END_OF_INPUT cases` { + @Test + fun `handle I_REFILL`() { + val generator = MockGenerator( + MockGenerator.Refill(I_INT_I16.packInstructionData(1)), + // REFILL is automatically appended by the BytecodeIonReader. + MockGenerator.Refill( + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + ) + BytecodeIonReader(generator) shouldProduce "1 2" + generator.assertAllRefillsUsed() + } + + @Test + fun `reading I_END_OF_INPUT indicates no value`() { + val generator = MockGenerator(I_END_OF_INPUT) + with(BytecodeIonReader(generator)) { + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `after reading I_END_OF_INPUT, it should be possible to check for more input`() { + val generator = MockGenerator( + MockGenerator.Refill(I_END_OF_INPUT), + MockGenerator.Refill( + I_INT_I16.packInstructionData(1), + I_END_OF_INPUT, + ) + ) + with(BytecodeIonReader(generator)) { + next() shouldBe null + next() shouldBe IonType.INT + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + @Test + fun `handle I_IVM`() { + val generator = MockGenerator( + I_INT_I16.packInstructionData(1), + I_IVM.packInstructionData(0x0101), + I_INT_I16.packInstructionData(2), + I_IVM.packInstructionData(0x0100), + I_INT_I16.packInstructionData(3), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + minorVersion shouldBe 0 + next() shouldBe IonType.INT + minorVersion shouldBe 1 + next() shouldBe IonType.INT + minorVersion shouldBe 0 + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Nested + inner class `I_ARGUMENT_NONE cases` { + + @Test + fun `discard I_ARGUMENT_NONE`() { + val generator = MockGenerator( + I_INT_I16.packInstructionData(1), + I_ARGUMENT_NONE, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + intValue() shouldBe 1 + next() shouldBe IonType.INT + intValue() shouldBe 2 + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `discard any annotations preceding I_ARGUMENT_NONE`() { + val generator = MockGenerator( + I_INT_I16.packInstructionData(1), + I_ANNOTATION_SID.packInstructionData(4), + I_ARGUMENT_NONE, + I_INT_I16.packInstructionData(2), + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.INT + typeAnnotations shouldBe emptyArray() + intValue() shouldBe 1 + + next() shouldBe IonType.INT + typeAnnotations shouldBe emptyArray() + intValue() shouldBe 2 + + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `discard I_ARGUMENT_NONE in a list`() { + val generator = MockGenerator( + I_LIST_START.packInstructionData(4), + I_INT_I16.packInstructionData(1), + I_ARGUMENT_NONE, + I_INT_I16.packInstructionData(2), + I_END_CONTAINER, + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.LIST + stepIn() + + next() shouldBe IonType.INT + intValue() shouldBe 1 + next() shouldBe IonType.INT + intValue() shouldBe 2 + next() shouldBe null + + stepOut() + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + + @Test + fun `discard a field name preceding I_ARGUMENT_NONE`() { + val generator = MockGenerator( + I_STRUCT_START.packInstructionData(7), + I_FIELD_NAME_SID.packInstructionData(4), + I_INT_I16.packInstructionData(1), + I_FIELD_NAME_SID.packInstructionData(5), + I_ARGUMENT_NONE, + I_FIELD_NAME_SID.packInstructionData(6), + I_INT_I16.packInstructionData(2), + I_END_CONTAINER, + I_END_OF_INPUT, + ) + with(BytecodeIonReader(generator)) { + next() shouldBe IonType.STRUCT + stepIn() + + next() shouldBe IonType.INT + fieldName shouldBe "name" + intValue() shouldBe 1 + + next() shouldBe IonType.INT + fieldName shouldBe "imports" + intValue() shouldBe 2 + + next() shouldBe null + + stepOut() + next() shouldBe null + } + generator.assertAllRefillsUsed() + } + } + + /* + TODO: Test cases for + I_DIRECTIVE_SET_SYMBOLS + I_DIRECTIVE_ADD_SYMBOLS + I_DIRECTIVE_SET_MACROS + I_DIRECTIVE_ADD_MACROS + I_DIRECTIVE_USE + I_DIRECTIVE_MODULE + I_DIRECTIVE_ENCODING + I_INVOKE -- if we are exposing macro invocations + I_META_OFFSET + I_META_ROWCOL + I_META_COMMENT + */ + + /** Helper function that creates a [SymbolToken] instance. */ + private fun symbolToken(text: String?, id: Int): SymbolToken = _Private_Utils.newSymbolToken(text, id) + + /** Helper function that asserts an action throws a particular exception. */ + private inline fun shouldThrow(action: Executable) = assertThrows(T::class.java, action) + + /** Asserts that [this] is equal to [expected]. */ + private infix fun T.shouldBe(expected: T) = assertEquals(expected, this) + + /** Asserts that [this] array's contents are equal to the [expected] array's content. */ + private infix fun Array.shouldBe(expected: Array) = assertArrayEquals(expected, this) + + /** Asserts that this [BytecodeIonReader] produces the [expected] Ion data. */ + private infix fun BytecodeIonReader.shouldProduce(expected: String) { + val iter10 = ION.iterate(ION.newReader(expected)) + val iter11 = ION.iterate(this) + while (iter10.hasNext() && iter11.hasNext()) { + assertEquals(iter10.next(), iter11.next()) + } + assertEquals(iter10.hasNext(), iter11.hasNext()) + } + + /** + * A mock [BytecodeGenerator]. Supplied by one or more [Refill]s. + */ + private class MockGenerator(vararg refills: Refill) : BytecodeGenerator { + + class Refill( + vararg val bytecode: Int, + val constants: ConstantPool = ConstantPool(), + val references: Map = emptyMap(), + val minorVersion: Int = 1, + ) + + /** Convenience constructor that constructs a [MockGenerator] with exactly one [Refill] */ + constructor( + vararg bytecode: Int, + constants: ConstantPool = ConstantPool(), + references: Map = emptyMap(), + minorVersion: Int = 1, + ) : this(Refill(*bytecode, constants = constants, references = references, minorVersion = minorVersion)) + + /** Asserts that all refills for this [MockGenerator] have been used */ + fun assertAllRefillsUsed() = assertTrue(refills.isEmpty()) + + private val refills = ArrayDeque().apply { addAll(refills) } + private lateinit var current: Refill + + override fun refill( + destination: BytecodeBuffer, + constantPool: AppendableConstantPoolView, + macroSrc: IntArray, + macroIndices: IntArray, + symTab: Array + ) { + val current = refills.removeFirst() + this.current = current + val bytecode = current.bytecode + destination.addSlice(bytecode, 0, bytecode.size) + current.constants.toArray().forEach { c -> constantPool.add(c) } + } + + override fun readBigIntegerReference(position: Int, length: Int): BigInteger = current.references[position] as BigInteger + override fun readDecimalReference(position: Int, length: Int): Decimal = Decimal.valueOf(current.references[position] as BigDecimal) + override fun readShortTimestampReference(position: Int, opcode: Int): Timestamp = current.references[position] as Timestamp + override fun readTimestampReference(position: Int, length: Int): Timestamp = current.references[position] as Timestamp + override fun readTextReference(position: Int, length: Int): String = current.references[position] as String + override fun readBytesReference(position: Int, length: Int): ByteSlice = current.references[position] as ByteSlice + override fun ionMinorVersion(): Int = current.minorVersion + override fun getGeneratorForMinorVersion(minorVersion: Int): BytecodeGenerator = this + } +}