From ca1c1f1c5d3f6ae95170d72338dcbbe747275e12 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:44:23 +0000 Subject: [PATCH] working implementation of BigInteger/BigDecimal for Kotlin/Native --- runtime/build.gradle.kts | 21 ++++++ .../kotlin/runtime/content/BigDecimal.kt | 74 +++++++++++++++++-- .../kotlin/runtime/content/BigInteger.kt | 63 ++++++++++++++++ .../kotlin/runtime/content/BigDecimalJVM.kt | 51 +++++++++---- .../kotlin/runtime/content/BigIntegerJVM.kt | 36 +++++++-- .../runtime/content/BigDecimalNative.kt | 65 ++++++++-------- .../runtime/content/BigIntegerNative.kt | 26 +++++-- 7 files changed, 270 insertions(+), 66 deletions(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 393b61436..bea1350f5 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -88,6 +88,27 @@ subprojects { tasks.withType { compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") + + // FIXME When building LinuxX64 on AL2 the linker inclues a bunch of dynamic links to unavailable versions + // of zlib. The below workaround forces the linker to statically link zlib but it's a hack because the + // linker will still dynamically link zlib (although the executable will no longer fail at runtime due to + // link resolution failures). The correct solution for this is probably containerized builds similar to + // what we do in aws-crt-kotlin. The following compiler args were helpful in debugging this issue: + // * Enable verbose compiler output : -verbose + // * Increase verbosity during the compiler's linker phase : -Xverbose-phases=Linker + // * Enable verbose linker output from gold : -linker-option --verbose + if (target.contains("linux", ignoreCase = true)) { + freeCompilerArgs.addAll( + listOf( + "-linker-option", // The subsequent argument is for the linker + "-Bstatic", // Enable static linking for the libraries that follow + "-linker-option", // The subsequent argument is for the linker + "-lz", // Link zlib statically (because of -Bstatic above) + "-linker-option", // The subsequent argument is for the linker + "-Bdynamic", // Restore dynamic linking, which is the default + ), + ) + } } } } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt index d9af8f85d..408f4080f 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt @@ -11,32 +11,94 @@ package aws.smithy.kotlin.runtime.content public expect class BigDecimal(value: String) : Number, Comparable { + /** * Create an instance of [BigDecimal] from a mantissa and exponent. - * @param mantissa a [BigInteger] representing the mantissa of this big decimal - * @param exponent an [Int] representing the exponent of this big decimal + * @param mantissa a [BigInteger] representing the [significant digits](https://en.wikipedia.org/wiki/Significand) + * of this decimal value + * @param exponent an [Int] representing the exponent of this decimal value */ public constructor(mantissa: BigInteger, exponent: Int) /** - * The mantissa of this decimal number + * The [significant digits](https://en.wikipedia.org/wiki/Significand) of this decimal value */ public val mantissa: BigInteger /** - * The exponent of this decimal number. - * If zero or positive, this represents the number of digits to the right of the decimal point. - * If negative, the mantissa is multiplied by ten to the power of the negation of the scale. + * The exponent of this decimal number. If zero or positive, this represents the number of digits to the right of + * the decimal point. If negative, the [mantissa] is multiplied by ten to the power of the negation of the scale. */ public val exponent: Int + /** + * Converts this value to a [Byte], which may involve rounding or truncation + */ override fun toByte(): Byte + + /** + * Converts this value to a [Double], which may involve rounding or truncation + */ override fun toDouble(): Double + + /** + * Converts this value to a [Float], which may involve rounding or truncation + */ override fun toFloat(): Float + + /** + * Converts this value to a [Short], which may involve rounding or truncation + */ override fun toShort(): Short + + /** + * Converts this value to an [Int], which may involve rounding or truncation + */ override fun toInt(): Int + + /** + * Converts this value to a [Long], which may involve rounding or truncation + */ override fun toLong(): Long + + /** + * Returns the decimal (i.e., radix-10) string representation of this value in long-form (i.e., _not_ scientific) + * notation + */ public fun toPlainString(): String + + /** + * Returns the decimal (i.e., radix-10) string representation of this value using scientific notation if an exponent + * is needed + */ + override fun toString(): String + + /** + * Returns a hash code for this value + */ + override fun hashCode(): Int + + /** + * Checks if this value is equal to the given object + * @param other The other value to compare against + */ override fun equals(other: Any?): Boolean + + /** + * Returns the sum of this value and the given value + * @param other The other value to add (i.e., the addend) + */ + public operator fun plus(other: BigDecimal): BigDecimal + + /** + * Returns the difference of this value and the given value + * @param other The value to subtract (i.e., the subtrahend) + */ + public operator fun minus(other: BigDecimal): BigDecimal + + /** + * Compare this value to the given value for in/equality + * @param other The value to compare against + */ public override operator fun compareTo(other: BigDecimal): Int } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt index f223bc36c..798c65300 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt @@ -11,23 +11,86 @@ package aws.smithy.kotlin.runtime.content public expect class BigInteger(value: String) : Number, Comparable { + /** * Create an instance of [BigInteger] from a [ByteArray] * @param bytes ByteArray representing the large integer */ public constructor(bytes: ByteArray) + /** + * Converts this value to a [Byte], which may involve rounding or truncation + */ override fun toByte(): Byte + + /** + * Converts this value to a [Long], which may involve rounding or truncation + */ override fun toLong(): Long + + /** + * Converts this value to a [Short], which may involve rounding or truncation + */ override fun toShort(): Short + + /** + * Converts this value to an [Int], which may involve rounding or truncation + */ override fun toInt(): Int + + /** + * Converts this value to a [Float], which may involve rounding or truncation + */ override fun toFloat(): Float + + /** + * Converts this value to a [Double], which may involve rounding or truncation + */ override fun toDouble(): Double + + /** + * Returns the decimal (i.e., radix-10) string representation of this value + */ override fun toString(): String + + /** + * Returns a string representation of this value in the given radix + * @param radix The [numerical base](https://en.wikipedia.org/wiki/Radix) in which to represent the value + */ + public fun toString(radix: Int = 10): String + + /** + * Returns a hash code for this value + */ override fun hashCode(): Int + + /** + * Checks if this value is equal to the given object + * @param other The other value to compare against + */ override fun equals(other: Any?): Boolean + + /** + * Returns the sum of this value and the given value + * @param other The other value to add (i.e., the addend) + */ public operator fun plus(other: BigInteger): BigInteger + + /** + * Returns the difference of this value and the given value + * @param other The value to subtract (i.e., the subtrahend) + */ public operator fun minus(other: BigInteger): BigInteger + + /** + * Returns the [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) binary representation of this + * value + */ public fun toByteArray(): ByteArray + + /** + * Compare this value to the given value for in/equality + * @param other The value to compare against + */ public override operator fun compareTo(other: BigInteger): Int } diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt index 9fb5d4efc..a7de1ab95 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt @@ -4,33 +4,52 @@ */ package aws.smithy.kotlin.runtime.content -public actual class BigDecimal actual constructor(public val value: String) : +import java.math.BigDecimal as JvmBigDecimal + +public actual class BigDecimal private constructor(private val delegate: JvmBigDecimal) : Number(), Comparable { - private val delegate = java.math.BigDecimal(value) - public actual constructor(mantissa: BigInteger, exponent: Int) : this( - java.math.BigDecimal( - java.math.BigInteger(mantissa.toString()), - exponent, - ).toPlainString(), - ) + private companion object { + /** + * Returns a new or existing [BigDecimal] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ + fun coalesceOrCreate(value: JvmBigDecimal, left: BigDecimal, right: BigDecimal): BigDecimal = when (value) { + left.delegate -> left + right.delegate -> right + else -> BigDecimal(value) + } + } + + public actual constructor(value: String) : this(JvmBigDecimal(value)) + public actual constructor(mantissa: BigInteger, exponent: Int) : this(JvmBigDecimal(mantissa.delegate, exponent)) public actual fun toPlainString(): String = delegate.toPlainString() - actual override fun toByte(): Byte = delegate.toByte() - actual override fun toDouble(): Double = delegate.toDouble() - actual override fun toFloat(): Float = delegate.toFloat() - actual override fun toInt(): Int = delegate.toInt() - actual override fun toLong(): Long = delegate.toLong() - actual override fun toShort(): Short = delegate.toShort() + public actual override fun toString(): String = delegate.toString() + public actual override fun toByte(): Byte = delegate.toByte() + public actual override fun toDouble(): Double = delegate.toDouble() + public actual override fun toFloat(): Float = delegate.toFloat() + public actual override fun toInt(): Int = delegate.toInt() + public actual override fun toLong(): Long = delegate.toLong() + public actual override fun toShort(): Short = delegate.toShort() - actual override fun equals(other: Any?): Boolean = other is BigDecimal && delegate == other.delegate + public actual override fun equals(other: Any?): Boolean = other is BigDecimal && delegate == other.delegate + public actual override fun hashCode(): Int = 31 + delegate.hashCode() public actual val mantissa: BigInteger - get() = BigInteger(delegate.unscaledValue().toString()) + get() = BigInteger(delegate.unscaledValue()) public actual val exponent: Int get() = delegate.scale() + public actual operator fun plus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate + other.delegate, this, other) + + public actual operator fun minus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate - other.delegate, this, other) + actual override fun compareTo(other: BigDecimal): Int = delegate.compareTo(other.delegate) } diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt index e89d12c9c..2f864710b 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt @@ -4,12 +4,28 @@ */ package aws.smithy.kotlin.runtime.content -public actual class BigInteger actual constructor(public val value: String) : +import java.math.BigInteger as JvmBigInteger + +public actual class BigInteger internal constructor(internal val delegate: JvmBigInteger) : Number(), Comparable { - private val delegate = java.math.BigInteger(value) - public actual constructor(bytes: ByteArray) : this(java.math.BigInteger(bytes).toString()) + private companion object { + /** + * Returns a new or existing [BigInteger] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ + fun coalesceOrCreate(value: JvmBigInteger, left: BigInteger, right: BigInteger): BigInteger = when (value) { + left.delegate -> left + right.delegate -> right + else -> BigInteger(value) + } + } + + public actual constructor(value: String) : this(JvmBigInteger(value)) + public actual constructor(bytes: ByteArray) : this(JvmBigInteger(bytes)) public actual override fun toByte(): Byte = delegate.toByte() public actual override fun toLong(): Long = delegate.toLong() @@ -17,12 +33,18 @@ public actual class BigInteger actual constructor(public val value: String) : public actual override fun toInt(): Int = delegate.toInt() public actual override fun toFloat(): Float = delegate.toFloat() public actual override fun toDouble(): Double = delegate.toDouble() - public actual override fun toString(): String = delegate.toString() - public actual override fun hashCode(): Int = delegate.hashCode() + public actual override fun toString(): String = toString(10) + public actual fun toString(radix: Int): String = delegate.toString(radix) + + public actual override fun hashCode(): Int = 17 + delegate.hashCode() public actual override fun equals(other: Any?): Boolean = other is BigInteger && delegate == other.delegate - public actual operator fun plus(other: BigInteger): BigInteger = BigInteger((delegate + other.delegate).toString()) - public actual operator fun minus(other: BigInteger): BigInteger = BigInteger((delegate - other.delegate).toString()) + public actual operator fun plus(other: BigInteger): BigInteger = + coalesceOrCreate(delegate + other.delegate, this, other) + + public actual operator fun minus(other: BigInteger): BigInteger = + coalesceOrCreate(delegate - other.delegate, this, other) + public actual override operator fun compareTo(other: BigInteger): Int = delegate.compareTo(other.delegate) public actual fun toByteArray(): ByteArray = delegate.toByteArray() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigDecimalNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigDecimalNative.kt index 202cebe00..b22085d02 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigDecimalNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigDecimalNative.kt @@ -4,46 +4,53 @@ */ package aws.smithy.kotlin.runtime.content -public actual class BigDecimal actual constructor(value: String) : +import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonSpinBigDecimal + +public actual class BigDecimal private constructor(private val delegate: IonSpinBigDecimal) : Number(), Comparable { - public actual fun toPlainString(): String { - TODO("Not yet implemented") - } - actual override fun toByte(): Byte { - TODO("Not yet implemented") + private companion object { + /** + * Returns a new or existing [BigDecimal] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ + fun coalesceOrCreate(value: IonSpinBigDecimal, left: BigDecimal, right: BigDecimal): BigDecimal = when (value) { + left.delegate -> left + right.delegate -> right + else -> BigDecimal(value) + } } - actual override fun toDouble(): Double { - TODO("Not yet implemented") - } + public actual constructor(value: String) : this(IonSpinBigDecimal.parseString(value, 10)) - actual override fun toFloat(): Float { - TODO("Not yet implemented") - } + public actual constructor(mantissa: BigInteger, exponent: Int) : + this(IonSpinBigDecimal.fromBigIntegerWithExponent(mantissa.delegate, exponent.toLong())) - actual override fun toInt(): Int { - TODO("Not yet implemented") - } - - actual override fun toLong(): Long { - TODO("Not yet implemented") - } - - actual override fun toShort(): Short { - TODO("Not yet implemented") - } + actual override fun toByte(): Byte = delegate.byteValue(exactRequired = false) + actual override fun toDouble(): Double = delegate.doubleValue(exactRequired = false) + actual override fun toFloat(): Float = delegate.floatValue(exactRequired = false) + actual override fun toInt(): Int = delegate.intValue(exactRequired = false) + actual override fun toLong(): Long = delegate.longValue(exactRequired = false) + actual override fun toShort(): Short = delegate.shortValue(exactRequired = false) public actual val mantissa: BigInteger - get() = TODO("Not yet implemented") + get() = BigInteger(delegate.significand) public actual val exponent: Int - get() = TODO("Not yet implemented") + get() = delegate.exponent.toInt() - public actual constructor(mantissa: BigInteger, exponent: Int) : this("TODO(Not yet implemented)") { - TODO("Not yet implemented") - } + actual override fun compareTo(other: BigDecimal): Int = delegate.compare(other.delegate) + actual override fun equals(other: Any?): Boolean = other is BigDecimal && other.delegate == delegate + actual override fun hashCode(): Int = 31 + delegate.hashCode() + public actual fun toPlainString(): String = delegate.toPlainString() + actual override fun toString(): String = delegate.toString() + + public actual operator fun plus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate + other.delegate, this, other) - actual override fun compareTo(other: BigDecimal): Int = TODO("Not yet implemented") + public actual operator fun minus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate - other.delegate, this, other) } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt index a1b3f7963..09674a0d2 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt @@ -8,11 +8,17 @@ import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray import com.ionspin.kotlin.bignum.integer.util.toTwosComplementByteArray import com.ionspin.kotlin.bignum.integer.BigInteger as IonSpinBigInteger -public actual class BigInteger private constructor(private val delegate: IonSpinBigInteger) : +public actual class BigInteger internal constructor(internal val delegate: IonSpinBigInteger) : Number(), Comparable { private companion object { + /** + * Returns a new or existing [BigInteger] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ fun coalesceOrCreate(value: IonSpinBigInteger, left: BigInteger, right: BigInteger): BigInteger = when (value) { left.delegate -> left right.delegate -> right @@ -23,12 +29,12 @@ public actual class BigInteger private constructor(private val delegate: IonSpin public actual constructor(value: String) : this(IonSpinBigInteger.parseString(value, 10)) public actual constructor(bytes: ByteArray) : this(IonSpinBigInteger.fromTwosComplementByteArray(bytes)) - actual override fun toByte(): Byte = delegate.byteValue(exactRequired = false) - actual override fun toDouble(): Double = delegate.doubleValue(exactRequired = false) - actual override fun toFloat(): Float = delegate.floatValue(exactRequired = false) - actual override fun toInt(): Int = delegate.intValue(exactRequired = false) - actual override fun toLong(): Long = delegate.longValue(exactRequired = false) - actual override fun toShort(): Short = delegate.shortValue(exactRequired = false) + public actual override fun toByte(): Byte = delegate.byteValue(exactRequired = false) + public actual override fun toDouble(): Double = delegate.doubleValue(exactRequired = false) + public actual override fun toFloat(): Float = delegate.floatValue(exactRequired = false) + public actual override fun toInt(): Int = delegate.intValue(exactRequired = false) + public actual override fun toLong(): Long = delegate.longValue(exactRequired = false) + public actual override fun toShort(): Short = delegate.shortValue(exactRequired = false) public actual operator fun plus(other: BigInteger): BigInteger = coalesceOrCreate(delegate + other.delegate, this, other) @@ -37,5 +43,9 @@ public actual class BigInteger private constructor(private val delegate: IonSpin coalesceOrCreate(delegate - other.delegate, this, other) public actual fun toByteArray(): ByteArray = delegate.toTwosComplementByteArray() - actual override fun compareTo(other: BigInteger): Int = delegate.compare(other.delegate) + public actual override fun compareTo(other: BigInteger): Int = delegate.compare(other.delegate) + public actual override fun equals(other: Any?): Boolean = other is BigInteger && other.delegate == delegate + public actual override fun hashCode(): Int = 17 + delegate.hashCode() + public actual override fun toString(): String = toString(10) + public actual fun toString(radix: Int): String = delegate.toString(radix) }