Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Kotlin/Native implementation of BigDecimal and BigInteger #1200

Open
wants to merge 6 commits into
base: kn-main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ kotlin.code.style=official
kotlin.incremental.js=true
kotlin.incremental.multiplatform=true
kotlin.mpp.stability.nowarn=true
kotlin.native.binary.sourceInfoType=libbacktrace
Copy link
Contributor

Choose a reason for hiding this comment

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

Were you able to get better stack traces with this enabled? I remember you said it didn't help, so I'd rather leave it off if it's not working as expected

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, will remove. I eventually discovered that full stack traces are available in the build artifacts (e.g., build/reports/tests/linuxX64Test/index.html) just not in the console output.

kotlin.native.ignoreDisabledTargets=true

# atomicfu
Expand All @@ -16,4 +17,4 @@ org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G
sdkVersion=1.3.30-SNAPSHOT

# codegen
codegenVersion=0.33.30-SNAPSHOT
codegenVersion=0.33.30-SNAPSHOT
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ slf4j-version = "2.0.16"
slf4j-v1x-version = "1.7.36"
crt-kotlin-version = "0.8.10"
micrometer-version = "1.13.6"
kotlin-multiplatform-bignum-version = "0.3.10"

# codegen
smithy-version = "1.51.0"
Expand Down Expand Up @@ -101,6 +102,8 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-ve
kaml = { module = "com.charleskorn.kaml:kaml", version.ref = "kaml-version" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup-version" }

kotlin-multiplatform-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "kotlin-multiplatform-bignum-version" }

[plugins]
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka-version"}
kotlin-jvm = {id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" }
Expand Down
21 changes: 21 additions & 0 deletions runtime/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile> {
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
),
)
}
}
}
}
6 changes: 6 additions & 0 deletions runtime/runtime-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ kotlin {
}
}

nativeMain {
dependencies {
implementation(libs.kotlin.multiplatform.bignum)
}
}

all {
languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,94 @@ package aws.smithy.kotlin.runtime.content
public expect class BigDecimal(value: String) :
Number,
Comparable<BigDecimal> {

/**
* 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,86 @@ package aws.smithy.kotlin.runtime.content
public expect class BigInteger(value: String) :
Number,
Comparable<BigInteger> {

/**
* 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ class BigIntegerTest {
"0x123456789abcdef0" to "1311768467463790320",
"0x00ffffffffffffffffffffffffffffffec" to "340282366920938463463374607431768211436",
"0x81445edf51ddc07216da5621c727bfd379d400f3da08018d45749a" to "-52134902384590238490284023839028330923830129830129301234239834982",

)

tests.forEach { (hex, expected) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BigDecimal> {
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()
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually 31 is multiplied in these hashCode implementations, is this intentional addition? Is there any reason we can't just take the delegate's hash code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I intentionally used addition instead of multiplication although either should work. I wanted to avoid using the unmodified hash code from the delegate because that would bin them the same if they were both added to a hashed container (e.g., Set). That's unlikely but also trivial to avoid.


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)
Comment on lines +48 to +52
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice 👍


actual override fun compareTo(other: BigDecimal): Int = delegate.compareTo(other.delegate)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,47 @@
*/
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<BigInteger> {
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()
public actual override fun toShort(): Short = delegate.toShort()
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()
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question about adding 17, can we just take the delegate's hash code?

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()
}
Loading
Loading