From 850efedb72dc6b9be0d8f50e265cac50c9c86609 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 31 Mar 2026 16:57:18 +0300 Subject: [PATCH] Handle non-finite real/decimal arithmetic --- .../kotlin/net/sergeych/lyng/obj/Obj.kt | 1 + .../sergeych/lyng/obj/ObjDecimalSupport.kt | 105 ++++++++++++++---- .../kotlin/net/sergeych/lyng/obj/ObjReal.kt | 16 ++- lynglib/src/commonTest/kotlin/ScriptTest.kt | 30 +++++ .../net/sergeych/lyng/DecimalModuleTest.kt | 89 +++++++++++++++ 5 files changed, 215 insertions(+), 26 deletions(-) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 001fdad..cf95717 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -1079,6 +1079,7 @@ interface Numeric { fun Obj.toDouble(): Double = (this as? Numeric)?.doubleValue + ?: ObjDecimalSupport.toDoubleOrNull(this) ?: (this as? ObjString)?.value?.toDouble() ?: throw IllegalArgumentException("cannot convert to double $this") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDecimalSupport.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDecimalSupport.kt index 5b92444..4513be8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDecimalSupport.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDecimalSupport.kt @@ -54,30 +54,36 @@ object ObjDecimalSupport { instance.kotlinInstanceData = zero } decimalClass.addFn("plus") { - ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Plus) + mixedRealDecimalArithmeticFallback(thisObj, args.firstAndOnly(), InteropOperator.Plus) + ?: ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Plus) ?: OperatorInteropRegistry.invokeBinary(requireScope(), thisObj, args.firstAndOnly(), InteropOperator.Plus) ?: newInstance(decimalClass, valueOf(thisObj).plus(coerceArg(requireScope(), args.firstAndOnly()))) } decimalClass.addFn("minus") { - ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Minus) + mixedRealDecimalArithmeticFallback(thisObj, args.firstAndOnly(), InteropOperator.Minus) + ?: ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Minus) ?: OperatorInteropRegistry.invokeBinary(requireScope(), thisObj, args.firstAndOnly(), InteropOperator.Minus) ?: newInstance(decimalClass, valueOf(thisObj).minus(coerceArg(requireScope(), args.firstAndOnly()))) } decimalClass.addFn("mul") { - ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Mul) + mixedRealDecimalArithmeticFallback(thisObj, args.firstAndOnly(), InteropOperator.Mul) + ?: ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Mul) ?: OperatorInteropRegistry.invokeBinary(requireScope(), thisObj, args.firstAndOnly(), InteropOperator.Mul) ?: newInstance(decimalClass, valueOf(thisObj).times(coerceArg(requireScope(), args.firstAndOnly()))) } decimalClass.addFn("div") { - ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Div) + mixedRealDecimalArithmeticFallback(thisObj, args.firstAndOnly(), InteropOperator.Div) + ?: ObjComplexSupport.decimalBinary(this, thisObj, args.firstAndOnly(), InteropOperator.Div) ?: OperatorInteropRegistry.invokeBinary(requireScope(), thisObj, args.firstAndOnly(), InteropOperator.Div) ?: newInstance(decimalClass, divideWithContext(valueOf(thisObj), coerceArg(requireScope(), args.firstAndOnly()), currentDivisionMode(requireScope()))) } decimalClass.addFn("mod") { - newInstance(decimalClass, valueOf(thisObj).rem(coerceArg(requireScope(), args.firstAndOnly()))) + mixedRealDecimalArithmeticFallback(thisObj, args.firstAndOnly(), InteropOperator.Mod) + ?: newInstance(decimalClass, valueOf(thisObj).rem(coerceArg(requireScope(), args.firstAndOnly()))) } decimalClass.addFn("compareTo") { - ObjInt.of(valueOf(thisObj).compareTo(coerceArg(requireScope(), args.firstAndOnly())).toLong()) + mixedRealDecimalCompareFallback(thisObj, args.firstAndOnly())?.toObj() + ?: ObjInt.of(valueOf(thisObj).compareTo(coerceArg(requireScope(), args.firstAndOnly())).toLong()) } decimalClass.addFn("negate") { newInstance(decimalClass, valueOf(thisObj).unaryMinus()) @@ -100,7 +106,7 @@ object ObjDecimalSupport { } decimalClass.addClassFn("fromReal") { val value = requiredArg(0).value - newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode)) + newInstanceFromFiniteReal(decimalClass, value) } decimalClass.addClassFn("fromString") { val value = requiredArg(0).value @@ -161,12 +167,35 @@ object ObjDecimalSupport { suspend fun fromRealLike(scope: ScopeFacade, sample: Obj, value: Double): Obj? { if (!isDecimalValue(sample)) return null + if (!value.isFinite()) return ObjReal.of(value) return scope.newInstanceLikeDecimal(sample, IonBigDecimal.fromDouble(value, realConversionMode)) } fun toDoubleOrNull(value: Obj): Double? = decimalValueOrNull(value)?.doubleValue(false) + internal fun mixedRealDecimalArithmeticFallback(left: Obj, right: Obj, operator: InteropOperator): Obj? { + if (!isMixedRealDecimal(left, right)) return null + val leftDouble = numericDoubleOrNull(left) ?: return null + val rightDouble = numericDoubleOrNull(right) ?: return null + val result = when (operator) { + InteropOperator.Plus -> leftDouble + rightDouble + InteropOperator.Minus -> leftDouble - rightDouble + InteropOperator.Mul -> leftDouble * rightDouble + InteropOperator.Div -> leftDouble / rightDouble + InteropOperator.Mod -> leftDouble % rightDouble + else -> return null + } + return if (result.isFinite()) null else ObjReal.of(result) + } + + internal fun mixedRealDecimalCompareFallback(left: Obj, right: Obj): Int? { + if (!isMixedRealDecimal(left, right)) return null + val leftDouble = numericDoubleOrNull(left) ?: return null + val rightDouble = numericDoubleOrNull(right) ?: return null + return if (leftDouble.isFinite() && rightDouble.isFinite()) null else leftDouble.compareTo(rightDouble) + } + suspend fun newDecimal(scope: ScopeFacade, value: IonBigDecimal): ObjInstance { val decimalModule = scope.requireScope().currentImportProvider.createModuleScope(scope.pos, "lyng.decimal") val decimalClass = decimalModule.requireClass("Decimal") @@ -205,6 +234,13 @@ object ObjDecimalSupport { return instance } + private suspend fun ScopeFacade.newInstanceFromFiniteReal(decimalClass: ObjClass, value: Double): ObjInstance { + if (!value.isFinite()) { + requireScope().raiseIllegalArgument("cannot convert non-finite Real to Decimal: $value") + } + return newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode)) + } + private suspend fun ScopeFacade.newInstanceLikeDecimal(sample: Obj, value: IonBigDecimal): ObjInstance { val decimalClass = (sample as? ObjInstance)?.objClass ?: raiseIllegalState("Decimal sample must be an object instance") @@ -213,7 +249,12 @@ object ObjDecimalSupport { private fun coerceArg(scope: Scope, value: Obj): IonBigDecimal = when (value) { is ObjInt -> IonBigDecimal.fromLong(value.value) - is ObjReal -> IonBigDecimal.fromDouble(value.value, realConversionMode) + is ObjReal -> { + if (!value.value.isFinite()) { + scope.raiseIllegalArgument("cannot convert non-finite Real to Decimal: ${value.value}") + } + IonBigDecimal.fromDouble(value.value, realConversionMode) + } is ObjInstance -> { if (value.objClass.className != "Decimal") { scope.raiseIllegalArgument("expected Decimal-compatible value, got ${value.objClass.className}") @@ -319,7 +360,7 @@ object ObjDecimalSupport { doc = "Convert this real number to a Decimal by preserving the current IEEE-754 value with 17 significant digits and half-even rounding.", type = type("lyng.decimal.Decimal"), moduleName = "lyng.decimal", - getter = { newInstance(decimalClass, IonBigDecimal.fromDouble(thisAs().value, realConversionMode)) } + getter = { newInstanceFromFiniteReal(decimalClass, thisAs().value) } ) ObjReal.type.members["d"] = ObjReal.type.members.getValue("d").copy(typeDecl = decimalTypeDecl) ObjString.type.addPropertyDoc( @@ -340,26 +381,48 @@ object ObjDecimalSupport { } private fun registerInterop(decimalClass: ObjClass) { + val decimalIdentity = ObjExternCallable.fromBridge { + requiredArg(0) + } + val numericOperators = listOf( + InteropOperator.Plus.name, + InteropOperator.Minus.name, + InteropOperator.Mul.name, + InteropOperator.Div.name, + InteropOperator.Mod.name, + InteropOperator.Compare.name, + InteropOperator.Equals.name + ) 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 - ), + operatorNames = numericOperators, leftToCommon = ObjExternCallable.fromBridge { val value = requiredArg(0).value newInstance(decimalClass, IonBigDecimal.fromLong(value)) }, - rightToCommon = ObjExternCallable.fromBridge { - requiredArg(0) - } + rightToCommon = decimalIdentity + ) + OperatorInteropRegistry.register( + leftClass = ObjReal.type, + rightClass = decimalClass, + commonClass = decimalClass, + operatorNames = numericOperators, + leftToCommon = ObjExternCallable.fromBridge { + val value = requiredArg(0).value + newInstanceFromFiniteReal(decimalClass, value) + }, + rightToCommon = decimalIdentity ) } + + private fun isMixedRealDecimal(left: Obj, right: Obj): Boolean = + (left is ObjReal && isDecimalValue(right)) || (right is ObjReal && isDecimalValue(left)) + + private fun numericDoubleOrNull(value: Obj): Double? = when (value) { + is Numeric -> value.doubleValue + is ObjInstance -> toDoubleOrNull(value) + else -> null + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt index c3465ec..422700f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjReal.kt @@ -43,6 +43,7 @@ data class ObjReal(val value: Double) : Obj(), Numeric { override suspend fun compareTo(scope: Scope, other: Obj): Int { if (other is ObjReal) return value.compareTo(other.value) + ObjDecimalSupport.mixedRealDecimalCompareFallback(this, other)?.let { return it } OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it } if (other !is Numeric) return -2 return value.compareTo(other.doubleValue) @@ -67,23 +68,28 @@ data class ObjReal(val value: Double) : Obj(), Numeric { } override suspend fun plus(scope: Scope, other: Obj): Obj = - OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus) + ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Plus) + ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus) ?: of(this.value + other.toDouble()) override suspend fun minus(scope: Scope, other: Obj): Obj = - OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus) + ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Minus) + ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus) ?: of(this.value - other.toDouble()) override suspend fun mul(scope: Scope, other: Obj): Obj = - OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mul) + ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Mul) + ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mul) ?: of(this.value * other.toDouble()) override suspend fun div(scope: Scope, other: Obj): Obj = - OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Div) + ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Div) + ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Div) ?: of(this.value / other.toDouble()) override suspend fun mod(scope: Scope, other: Obj): Obj = - OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod) + ObjDecimalSupport.mixedRealDecimalArithmeticFallback(this, other, InteropOperator.Mod) + ?: OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod) ?: of(this.value % other.toDouble()) /** diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 21cc08d..b4a1b0f 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -5460,4 +5460,34 @@ class ScriptTest { """.trimIndent() ) } + +// @Test +// fun testFromCalcrus1() = runTest { +// eval($$""" +// import lyng.decimal +// var x = 7.0.d +// // глубина по звуку падения +// val m = 1 // kg +// val d = 0.06 // 6 cm +// val c = 340 // скор. звука +// val g = 9.82 +// var cnt = 0 +// var h = 0.0 +// var t = x +// var message = "" +// +// while(true){ +// val h0 = 0 +// h = c*c/h*(1 + g*t/c -sqrt(1+2*g*t/c)) +// message = "iter ${cnt++}" +// if( cnt > 100 ) { +// message= "ошибка" +// break 0 +// } +// x = h +// if( abs(h-h0)/h > 0.08 ) break h +// } +// println(x) +// """.trimIndent()) +// } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DecimalModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DecimalModuleTest.kt index 0e6cdf8..08d40ca 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DecimalModuleTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DecimalModuleTest.kt @@ -19,6 +19,7 @@ package net.sergeych.lyng import com.ionspin.kotlin.bignum.decimal.BigDecimal import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.obj.ObjException import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -77,6 +78,94 @@ class DecimalModuleTest { ) } + @Test + fun testDecimalModuleMixedRealOperators() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.decimal + + assertEquals(3.5.d, 1.5 + 2.d) + assertEquals(3.5.d, 2.d + 1.5) + assertEquals(1.5.d, 3.5 - 2.d) + assertEquals(1.5.d, 3.5.d - 2.0) + assertEquals(7.d, 3.5 * 2.d) + assertEquals(7.d, 3.5.d * 2.0) + assertEquals(1.75.d, 3.5 / 2.d) + assertEquals(1.75.d, 3.5.d / 2.0) + assert(1.5 < 2.d) + assert(2.5.d > 2.0) + assert(2.5 == 2.5.d) + assert(2.5.d == 2.5) + """.trimIndent() + ) + } + + @Test + fun testMixedRealDecimalNonFiniteResultsStayReal() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.decimal + + val inf1 = 1.0 / 0.d + val inf2 = 1.d / 0.0 + val inf3 = (1.0 / 0.0) * 2.d + val negInf = -1.d / 0.0 + val nan1 = 0.0 / 0.d + val nan2 = (0.0 / 0.0) + 1.d + + assert(inf1 is Real) + assert(inf2 is Real) + assert(inf3 is Real) + assert(negInf is Real) + assert(nan1 is Real) + assert(nan2 is Real) + + assertEquals("Infinity", inf1.toString()) + assertEquals("Infinity", inf2.toString()) + assertEquals("Infinity", inf3.toString()) + assertEquals("-Infinity", negInf.toString()) + assertEquals("NaN", nan1.toString()) + assertEquals("NaN", nan2.toString()) + + assert(inf1 > 999999999.d) + assert(negInf < -999999999.d) + assert(!(nan1 == 1.d)) + """.trimIndent() + ) + } + + @Test + fun testDecimalRejectsExplicitNonFiniteRealConversions() = runTest { + val scope = Script.newScope() + val ex1 = assertFailsWith { + scope.eval( + """ + import lyng.decimal + (1.0 / 0.0).d + """.trimIndent() + ) + } + val ex2 = assertFailsWith { + scope.eval( + """ + import lyng.decimal + Decimal.fromReal(0.0 / 0.0) + """.trimIndent() + ) + } + + assertEquals( + "cannot convert non-finite Real to Decimal: Infinity", + (ex1.errorObject as ObjException).message.value + ) + assertEquals( + "cannot convert non-finite Real to Decimal: NaN", + (ex2.errorObject as ObjException).message.value + ) + } + @Test fun testDecimalDivisionUsesDefaultContext() = runTest { val scope = Script.newScope()