fixes on Decimals and bound kotlin classes

This commit is contained in:
sergeych committed 2026-03-29 01:20:20 +03:00
1 parent c7c333b71a
commit 05d7432b37
14 files changed
+114 -667

No files matched your search

@@ -17,6 +17,7 @@
package net.sergeych.lyng
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import net.sergeych.lyng.obj.*
/**
@@ -133,3 +134,6 @@ fun ScopeFacade.raiseIllegalOperation(message: String = "Operation is illegal"):
fun ScopeFacade.raiseIterationFinished(): Nothing =
raiseError(ObjIterationFinishedException(requireScope()))
suspend fun ScopeFacade.newDecimal(value: BigDecimal): ObjInstance =
ObjDecimalSupport.newDecimal(this, value)
@@ -253,7 +253,7 @@ class Script(
companion object {
private suspend fun ScopeFacade.numberToDouble(value: Obj): Double =
ObjBigDecimalSupport.toDoubleOrNull(value) ?: value.toDouble()
ObjDecimalSupport.toDoubleOrNull(value) ?: value.toDouble()
private suspend fun ScopeFacade.decimalAwareUnaryMath(
value: Obj,
@@ -263,9 +263,9 @@ class Script(
exactDecimal?.let { exact ->
exact(value)?.let { return it }
}
if (ObjBigDecimalSupport.isDecimalValue(value)) {
return ObjBigDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value)))
?: raiseIllegalState("failed to convert Real result back to BigDecimal")
if (ObjDecimalSupport.isDecimalValue(value)) {
return ObjDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value)))
?: raiseIllegalState("failed to convert Real result back to Decimal")
}
return ObjReal(fallback(numberToDouble(value)))
}
@@ -281,11 +281,11 @@ class Script(
}
private suspend fun ScopeFacade.decimalAwarePow(base: Obj, exponent: Obj): Obj {
ObjBigDecimalSupport.exactPow(this, base, exponent)?.let { return it }
if (ObjBigDecimalSupport.isDecimalValue(base) || ObjBigDecimalSupport.isDecimalValue(exponent)) {
return ObjBigDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent)))
?: ObjBigDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent)))
?: raiseIllegalState("failed to convert Real pow result back to BigDecimal")
ObjDecimalSupport.exactPow(this, base, exponent)?.let { return it }
if (ObjDecimalSupport.isDecimalValue(base) || ObjDecimalSupport.isDecimalValue(exponent)) {
return ObjDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent)))
?: ObjDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent)))
?: raiseIllegalState("failed to convert Real pow result back to Decimal")
}
return ObjReal(numberToDouble(base).pow(numberToDouble(exponent)))
}
@@ -327,15 +327,15 @@ class Script(
}
addFn("floor") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
}
addFn("ceil") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
}
addFn("round") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
}
addFn("sin") {
@@ -401,7 +401,7 @@ class Script(
addFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue)
else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue }
else decimalAwareUnaryMath(x, ObjDecimalSupport::exactAbs) { it.absoluteValue }
}
addFnDoc(
@@ -661,15 +661,15 @@ class Script(
)
ensureFn("floor") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
}
ensureFn("ceil") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
}
ensureFn("round") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
}
ensureFn("sin") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
@@ -729,7 +729,7 @@ class Script(
ensureFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue)
else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue }
else decimalAwareUnaryMath(x, ObjDecimalSupport::exactAbs) { it.absoluteValue }
}
}
}
@@ -867,7 +867,7 @@ class Script(
}
addPackage("lyng.decimal") { module ->
module.eval(Source("lyng.decimal", decimalLyng))
ObjBigDecimalSupport.bindTo(module)
ObjDecimalSupport.bindTo(module)
}
addPackage("lyng.matrix") { module ->
module.eval(Source("lyng.matrix", matrixLyng))
@@ -448,37 +448,37 @@ private fun buildStdlibDocs(): List<MiniDecl> {
// Math helpers (scalar versions)
fun math1(name: String) = mod.funDoc(
name = name,
doc = StdlibInlineDocIndex.topFunDoc(name) ?: "Compute $name(x). Accepts Int, Real, and BigDecimal. BigDecimal currently uses a temporary Real bridge and will get native decimal implementations later.",
doc = StdlibInlineDocIndex.topFunDoc(name) ?: "Compute $name(x). Accepts Int, Real, and Decimal. Decimal currently uses a temporary Real bridge and will get native decimal implementations later.",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
math1("sin"); math1("cos"); math1("tan"); math1("asin"); math1("acos"); math1("atan")
mod.funDoc(name = "floor", doc = StdlibInlineDocIndex.topFunDoc("floor") ?: "Round down the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ceil", doc = StdlibInlineDocIndex.topFunDoc("ceil") ?: "Round up the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "round", doc = StdlibInlineDocIndex.topFunDoc("round") ?: "Round the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "floor", doc = StdlibInlineDocIndex.topFunDoc("floor") ?: "Round down the number to the nearest integer. Decimal is handled directly and stays Decimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ceil", doc = StdlibInlineDocIndex.topFunDoc("ceil") ?: "Round up the number to the nearest integer. Decimal is handled directly and stays Decimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "round", doc = StdlibInlineDocIndex.topFunDoc("round") ?: "Round the number to the nearest integer. Decimal is handled directly and stays Decimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
// Hyperbolic and inverse hyperbolic
math1("sinh"); math1("cosh"); math1("tanh"); math1("asinh"); math1("acosh"); math1("atanh")
// Exponentials and logarithms
mod.funDoc(name = "exp", doc = StdlibInlineDocIndex.topFunDoc("exp") ?: "Euler's exponential e^x. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ln", doc = StdlibInlineDocIndex.topFunDoc("ln") ?: "Natural logarithm (base e). BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log10", doc = StdlibInlineDocIndex.topFunDoc("log10") ?: "Logarithm base 10. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log2", doc = StdlibInlineDocIndex.topFunDoc("log2") ?: "Logarithm base 2. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "exp", doc = StdlibInlineDocIndex.topFunDoc("exp") ?: "Euler's exponential e^x. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ln", doc = StdlibInlineDocIndex.topFunDoc("ln") ?: "Natural logarithm (base e). Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log10", doc = StdlibInlineDocIndex.topFunDoc("log10") ?: "Logarithm base 10. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log2", doc = StdlibInlineDocIndex.topFunDoc("log2") ?: "Logarithm base 2. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
// Power/roots and absolute value
mod.funDoc(
name = "pow",
doc = StdlibInlineDocIndex.topFunDoc("pow") ?: "Raise `x` to the power `y`. BigDecimal with integral `y` is handled directly; other BigDecimal cases currently use a temporary Real bridge.",
doc = StdlibInlineDocIndex.topFunDoc("pow") ?: "Raise `x` to the power `y`. Decimal with integral `y` is handled directly; other Decimal cases currently use a temporary Real bridge.",
params = listOf(ParamDoc("x", type("lyng.Number")), ParamDoc("y", type("lyng.Number")))
)
mod.funDoc(
name = "sqrt",
doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `x`. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.",
doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `x`. Decimal currently uses a temporary Real bridge and will get a native decimal implementation later.",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
mod.funDoc(
name = "abs",
doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number. Int stays Int, and BigDecimal stays BigDecimal.",
doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number. Int stays Int, and Decimal stays Decimal.",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
@@ -1,351 +0,0 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.obj
import com.ionspin.kotlin.bignum.decimal.DecimalMode
import com.ionspin.kotlin.bignum.decimal.RoundingMode
import com.ionspin.kotlin.bignum.integer.BigInteger
import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lyng.requiredArg
import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal
object ObjBigDecimalSupport {
private const val decimalContextVar = "__lyng_decimal_context__"
// For Real -> BigDecimal, preserve the actual IEEE-754 Double value using a
// round-trip-safe precision. This intentionally does not try to recover source text.
private val realConversionMode = DecimalMode(17L, RoundingMode.ROUND_HALF_TO_EVEN)
// Division needs an explicit stopping rule for non-terminating results. Use a
// decimal128-like default context until Lyng exposes per-operation contexts.
private val defaultDivisionMode = DecimalMode(34L, RoundingMode.ROUND_HALF_TO_EVEN)
private val zero: IonBigDecimal = IonBigDecimal.ZERO
private val decimalTypeDecl = TypeDecl.Simple("lyng.decimal.BigDecimal", false)
private object BoundMarker
private data class DecimalRuntimeContext(
val precision: Long,
val rounding: RoundingMode
) : Obj()
suspend fun bindTo(module: ModuleScope) {
val decimalClass = module.requireClass("BigDecimal")
if (decimalClass.kotlinClassData === BoundMarker) return
decimalClass.kotlinClassData = BoundMarker
decimalClass.isAbstract = false
val hooks = decimalClass.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
decimalClass.bridgeInitHooks = it
}
hooks += { _, instance ->
instance.kotlinInstanceData = zero
}
decimalClass.addFn("plus") {
newInstance(decimalClass, valueOf(thisObj).plus(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("minus") {
newInstance(decimalClass, valueOf(thisObj).minus(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("mul") {
newInstance(decimalClass, valueOf(thisObj).times(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("div") {
newInstance(decimalClass, divideWithContext(valueOf(thisObj), coerceArg(requireScope(), args.firstAndOnly()), currentDivisionMode(requireScope())))
}
decimalClass.addFn("mod") {
newInstance(decimalClass, valueOf(thisObj).rem(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("compareTo") {
ObjInt.of(valueOf(thisObj).compareTo(coerceArg(requireScope(), args.firstAndOnly())).toLong())
}
decimalClass.addFn("negate") {
newInstance(decimalClass, valueOf(thisObj).unaryMinus())
}
decimalClass.addFn("toInt") {
ObjInt.of(valueOf(thisObj).longValue(false))
}
decimalClass.addFn("toReal") {
ObjReal.of(valueOf(thisObj).doubleValue(false))
}
decimalClass.addFn("toString") {
ObjString(valueOf(thisObj).toStringExpanded())
}
decimalClass.addFn("toStringExpanded") {
ObjString(valueOf(thisObj).toStringExpanded())
}
decimalClass.addClassFn("fromInt") {
val value = requiredArg<ObjInt>(0).value
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
}
decimalClass.addClassFn("fromReal") {
val value = requiredArg<ObjReal>(0).value
newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode))
}
decimalClass.addClassFn("fromString") {
val value = requiredArg<ObjString>(0).value
try {
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
} catch (e: Throwable) {
requireScope().raiseIllegalArgument("invalid BigDecimal string: $value")
}
}
module.addFn("withDecimalContext") {
val (context, block) = when (args.list.size) {
2 -> {
val first = args[0]
val block = args[1]
if (first is ObjInt) {
DecimalRuntimeContext(first.value, RoundingMode.ROUND_HALF_TO_EVEN) to block
} else {
normalizeContext(requireScope(), first) to block
}
}
3 -> {
val precision = requiredArg<ObjInt>(0).value
val rounding = roundingModeFromObj(requireScope(), args[1])
DecimalRuntimeContext(precision, rounding) to args[2]
}
else -> requireScope().raiseIllegalArgument("withDecimalContext expects (context, block), (precision, block), or (precision, rounding, block)")
}
val child = requireScope().createChildScope()
child.addConst(decimalContextVar, context)
block.callOn(child)
}
registerBuiltinConversions(decimalClass)
registerInterop(decimalClass)
}
fun isDecimalValue(value: Obj): Boolean =
value is ObjInstance && value.objClass.className == "BigDecimal"
suspend fun exactAbs(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.abs()) }
suspend fun exactFloor(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.floor()) }
suspend fun exactCeil(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.ceil()) }
suspend fun exactRound(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let {
scope.newInstanceLikeDecimal(value, it.roundToDigitPositionAfterDecimalPoint(0, RoundingMode.ROUND_HALF_CEILING))
}
suspend fun exactPow(scope: ScopeFacade, base: Obj, exponent: Obj): Obj? {
val decimal = decimalValueOrNull(base) ?: return null
val intExponent = exponent as? ObjInt ?: return null
return scope.newInstanceLikeDecimal(base, decimal.pow(intExponent.value))
}
suspend fun fromRealLike(scope: ScopeFacade, sample: Obj, value: Double): Obj? {
if (!isDecimalValue(sample)) return null
return scope.newInstanceLikeDecimal(sample, IonBigDecimal.fromDouble(value, realConversionMode))
}
fun toDoubleOrNull(value: Obj): Double? =
decimalValueOrNull(value)?.doubleValue(false)
private fun valueOf(obj: Obj): IonBigDecimal {
val instance = obj as? ObjInstance ?: error("BigDecimal receiver must be an object instance")
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
}
private suspend fun currentDivisionMode(scope: Scope): DecimalMode {
val context = findContextObject(scope) ?: return defaultDivisionMode
return DecimalMode(context.precision, context.rounding)
}
private fun divideWithContext(left: IonBigDecimal, right: IonBigDecimal, mode: DecimalMode): IonBigDecimal {
if (mode.decimalPrecision <= 0L) {
return stripMode(left.divide(right, mode))
}
val exactLeft = stripMode(left)
val exactRight = stripMode(right)
val guardMode = DecimalMode(mode.decimalPrecision + 2, RoundingMode.TOWARDS_ZERO)
var guarded = stripMode(exactLeft.divide(exactRight, guardMode))
val hasMoreTail = !stripMode(exactLeft - stripMode(guarded * exactRight)).isZero()
if (hasMoreTail && isHalfRounding(mode.roundingMode) && looksLikeExactHalf(guarded, mode.decimalPrecision)) {
guarded = nudgeLastDigitAwayFromZero(guarded)
}
return stripMode(guarded.roundSignificand(mode))
}
private suspend fun ScopeFacade.newInstance(decimalClass: ObjClass, value: IonBigDecimal): ObjInstance {
val instance = call(decimalClass) as? ObjInstance
?: raiseIllegalState("BigDecimal() did not return an object instance")
instance.kotlinInstanceData = value
return instance
}
private suspend fun ScopeFacade.newInstanceLikeDecimal(sample: Obj, value: IonBigDecimal): ObjInstance {
val decimalClass = (sample as? ObjInstance)?.objClass
?: raiseIllegalState("BigDecimal sample must be an object instance")
return newInstance(decimalClass, value)
}
private fun coerceArg(scope: Scope, value: Obj): IonBigDecimal = when (value) {
is ObjInt -> IonBigDecimal.fromLongAsSignificand(value.value)
is ObjReal -> IonBigDecimal.fromDouble(value.value, realConversionMode)
is ObjInstance -> {
if (value.objClass.className != "BigDecimal") {
scope.raiseIllegalArgument("expected BigDecimal-compatible value, got ${value.objClass.className}")
}
value.kotlinInstanceData as? IonBigDecimal ?: zero
}
else -> scope.raiseIllegalArgument("expected BigDecimal-compatible value, got ${value.objClass.className}")
}
private suspend fun normalizeContext(scope: Scope, value: Obj): DecimalRuntimeContext {
val instance = value as? ObjInstance
?: scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
if (instance.objClass.className != "DecimalContext") {
scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
}
return decimalRuntimeContextFromInstance(scope, instance)
}
private fun findContextObject(scope: Scope): DecimalRuntimeContext? {
var current: Scope? = scope
while (current != null) {
val record = current.objects[decimalContextVar] ?: current.localBindings[decimalContextVar]
val value = when (val raw = record?.value) {
is FrameSlotRef -> raw.peekValue()
is RecordSlotRef -> raw.peekValue()
else -> raw
}
when (value) {
is DecimalRuntimeContext -> return value
}
current = current.parent
}
return null
}
private suspend fun decimalRuntimeContextFromInstance(scope: Scope, context: ObjInstance): DecimalRuntimeContext {
val precision = context.readField(scope, "precision").value as? ObjInt
?: scope.raiseClassCastError("DecimalContext.precision must be Int")
if (precision.value <= 0L) {
scope.raiseIllegalArgument("DecimalContext precision must be positive")
}
val rounding = roundingModeFromObj(scope, context.readField(scope, "rounding").value)
return DecimalRuntimeContext(precision.value, rounding)
}
private fun stripMode(value: IonBigDecimal): IonBigDecimal =
IonBigDecimal.fromBigIntegerWithExponent(value.significand, value.exponent)
private fun isHalfRounding(mode: RoundingMode): Boolean = when (mode) {
RoundingMode.ROUND_HALF_TO_EVEN,
RoundingMode.ROUND_HALF_AWAY_FROM_ZERO,
RoundingMode.ROUND_HALF_TOWARDS_ZERO,
RoundingMode.ROUND_HALF_CEILING,
RoundingMode.ROUND_HALF_FLOOR,
RoundingMode.ROUND_HALF_TO_ODD -> true
else -> false
}
private fun looksLikeExactHalf(value: IonBigDecimal, targetPrecision: Long): Boolean {
val digits = value.significand.abs().toString(10)
if (digits.length <= targetPrecision) return false
val discarded = digits.substring(targetPrecision.toInt())
return discarded[0] == '5' && discarded.drop(1).all { it == '0' }
}
private fun nudgeLastDigitAwayFromZero(value: IonBigDecimal): IonBigDecimal {
val ulpExponent = value.exponent - value.precision + 1
val ulp = IonBigDecimal.fromBigIntegerWithExponent(BigInteger.ONE, ulpExponent)
return if (value.significand.signum() < 0) value - ulp else value + ulp
}
private fun roundingModeFromObj(scope: Scope, value: Obj): RoundingMode {
val entry = value as? ObjEnumEntry ?: scope.raiseClassCastError("DecimalContext.rounding must be DecimalRounding")
return when (entry.name.value) {
"HalfEven" -> RoundingMode.ROUND_HALF_TO_EVEN
"HalfAwayFromZero" -> RoundingMode.ROUND_HALF_AWAY_FROM_ZERO
"HalfTowardsZero" -> RoundingMode.ROUND_HALF_TOWARDS_ZERO
"Ceiling" -> RoundingMode.CEILING
"Floor" -> RoundingMode.FLOOR
"AwayFromZero" -> RoundingMode.AWAY_FROM_ZERO
"TowardsZero" -> RoundingMode.TOWARDS_ZERO
else -> scope.raiseIllegalArgument("unsupported DecimalRounding: ${entry.name.value}")
}
}
private fun decimalValueOrNull(value: Obj): IonBigDecimal? {
if (!isDecimalValue(value)) return null
val instance = value as ObjInstance
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
}
private fun registerBuiltinConversions(decimalClass: ObjClass) {
ObjInt.type.addPropertyDoc(
name = "d",
doc = "Convert this integer to a BigDecimal.",
type = type("lyng.decimal.BigDecimal"),
moduleName = "lyng.decimal",
getter = { newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(thisAs<ObjInt>().value)) }
)
ObjInt.type.members["d"] = ObjInt.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
ObjReal.type.addPropertyDoc(
name = "d",
doc = "Convert this real number to a BigDecimal by preserving the current IEEE-754 value with 17 significant digits and half-even rounding.",
type = type("lyng.decimal.BigDecimal"),
moduleName = "lyng.decimal",
getter = { newInstance(decimalClass, IonBigDecimal.fromDouble(thisAs<ObjReal>().value, realConversionMode)) }
)
ObjReal.type.members["d"] = ObjReal.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
ObjString.type.addPropertyDoc(
name = "d",
doc = "Parse this string as a BigDecimal.",
type = type("lyng.decimal.BigDecimal"),
moduleName = "lyng.decimal",
getter = {
val value = thisAs<ObjString>().value
try {
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
} catch (e: Throwable) {
requireScope().raiseIllegalArgument("invalid BigDecimal string: $value")
}
}
)
ObjString.type.members["d"] = ObjString.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
}
private fun registerInterop(decimalClass: ObjClass) {
OperatorInteropRegistry.register(
leftClass = ObjInt.type,
rightClass = decimalClass,
commonClass = decimalClass,
operatorNames = listOf(
InteropOperator.Plus.name,
InteropOperator.Minus.name,
InteropOperator.Mul.name,
InteropOperator.Div.name,
InteropOperator.Mod.name,
InteropOperator.Compare.name,
InteropOperator.Equals.name
),
leftToCommon = ObjExternCallable.fromBridge {
val value = requiredArg<ObjInt>(0).value
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
},
rightToCommon = ObjExternCallable.fromBridge {
requiredArg<Obj>(0)
}
)
}
}
@@ -48,7 +48,7 @@ enum DecimalRounding {
/**
* Dynamic decimal arithmetic settings.
*
* A decimal context is not attached permanently to a `BigDecimal` value. Instead, it is applied dynamically
* A decimal context is not attached permanently to a `Decimal` value. Instead, it is applied dynamically
* inside `withDecimalContext(...)`, which makes the rule local to a block of code.
*
* Default context:
@@ -76,7 +76,7 @@ class DecimalContext(
/**
* Arbitrary-precision decimal value.
*
* `BigDecimal` is intended for decimal arithmetic where binary floating-point (`Real`) is the wrong tool:
* `Decimal` is intended for decimal arithmetic where binary floating-point (`Real`) is the wrong tool:
* - money
* - human-entered decimal values
* - ratios that should round in decimal, not in binary
@@ -84,10 +84,10 @@ class DecimalContext(
*
* Creating values:
*
* - `1.d` converts `Int -> BigDecimal`
* - `2.2.d` converts `Real -> BigDecimal` by preserving the current IEEE-754 value
* - `1.d` converts `Int -> Decimal`
* - `2.2.d` converts `Real -> Decimal` by preserving the current IEEE-754 value
* - `"2.2".d` parses exact decimal text
* - `BigDecimal.fromInt(...)`, `fromReal(...)`, `fromString(...)` are explicit factory forms
* - `Decimal.fromInt(...)`, `fromReal(...)`, `fromString(...)` are explicit factory forms
*
* Important distinction:
*
@@ -109,7 +109,7 @@ class DecimalContext(
*
* Mixed arithmetic:
*
* `BigDecimal` defines its own operators against decimal-compatible values, and the decimal module also registers
* `Decimal` defines its own operators against decimal-compatible values, and the decimal module also registers
* interop bridges so built-in left-hand operands work naturally:
*
* import lyng.decimal
@@ -134,15 +134,15 @@ class DecimalContext(
*
* "2.2".d
*
* That is the precise form. `2.2.d` remains a `Real -> BigDecimal` conversion by design.
* That is the precise form. `2.2.d` remains a `Real -> Decimal` conversion by design.
*/
extern class BigDecimal() {
extern class Decimal() {
/** Add another decimal-compatible value. */
extern fun plus(other: Object): BigDecimal
extern fun plus(other: Object): Decimal
/** Subtract another decimal-compatible value. */
extern fun minus(other: Object): BigDecimal
extern fun minus(other: Object): Decimal
/** Multiply by another decimal-compatible value. */
extern fun mul(other: Object): BigDecimal
extern fun mul(other: Object): Decimal
/**
* Divide by another decimal-compatible value.
*
@@ -150,13 +150,13 @@ extern class BigDecimal() {
* - by default: `34` significant digits, `HalfEven`
* - inside `withDecimalContext(...)`: the context active for the current block
*/
extern fun div(other: Object): BigDecimal
extern fun div(other: Object): Decimal
/** Remainder with another decimal-compatible value. */
extern fun mod(other: Object): BigDecimal
extern fun mod(other: Object): Decimal
/** Compare with another decimal-compatible value. */
extern fun compareTo(other: Object): Int
/** Unary minus. */
extern fun negate(): BigDecimal
extern fun negate(): Decimal
/** Convert to `Int` by dropping the fractional part according to backend conversion rules. */
extern fun toInt(): Int
/** Convert to `Real`. */
@@ -169,16 +169,16 @@ extern class BigDecimal() {
extern fun toStringExpanded(): String
/** Create a decimal from an `Int`. */
static extern fun fromInt(value: Int): BigDecimal
static extern fun fromInt(value: Int): Decimal
/**
* Create a decimal from a `Real`.
*
* This preserves the current IEEE-754 value using a round-trip-safe decimal conversion.
* It does not try to recover the original source text.
*/
static extern fun fromReal(value: Real): BigDecimal
static extern fun fromReal(value: Real): Decimal
/** Parse exact decimal text. */
static extern fun fromString(value: String): BigDecimal
static extern fun fromString(value: String): Decimal
}
/**
@@ -97,7 +97,7 @@ enum BinaryOperator {
* The registry is symmetric for the converted values, but not for the original syntax.
* Its job is specifically to fill the gap where your custom type appears on the right:
*
* - `myDecimal + 1` usually already works if `BigDecimal.plus(Int)` exists
* - `myDecimal + 1` usually already works if `Decimal.plus(Int)` exists
* - `1 + myDecimal` needs registration because `Int` itself is not rewritten
*
* Typical pattern for a custom type:
@@ -135,7 +135,7 @@ enum BinaryOperator {
* - `3 > Rational(5, 2)` works
* - `2 == Rational(2, 1)` works
*
* Decimal uses the same mechanism internally to make `Int + BigDecimal` and `Real + BigDecimal`
* Decimal uses the same mechanism internally to make `Int + Decimal` and `Real + Decimal`
* work without changing the built-in `Int` or `Real` classes.
*/
extern object OperatorInterop {