diff --git a/docs/Decimal.md b/docs/Decimal.md index a443ba1..dd16ad5 100644 --- a/docs/Decimal.md +++ b/docs/Decimal.md @@ -223,6 +223,50 @@ assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { ( ## Recommended Usage Rules +## Decimal With Stdlib Math Functions + +Core math helpers such as `abs`, `floor`, `ceil`, `round`, `sin`, `exp`, `ln`, `sqrt`, `log10`, `log2`, and `pow` +now also accept `BigDecimal`. + +Current behavior is intentionally split: + +- exact decimal implementation: + - `abs(x)` + - `floor(x)` + - `ceil(x)` + - `round(x)` + - `pow(x, y)` when `x` is `BigDecimal` and `y` is an integral exponent +- temporary bridge through `Real`: + - `sin`, `cos`, `tan` + - `asin`, `acos`, `atan` + - `sinh`, `cosh`, `tanh` + - `asinh`, `acosh`, `atanh` + - `exp`, `ln`, `log10`, `log2` + - `sqrt` + - `pow` for the remaining non-integral decimal exponent cases + +The temporary bridge is: + +```lyng +BigDecimal -> Real -> host math -> BigDecimal +``` + +This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based +paths over time. + +Examples: + +```lyng +import lyng.decimal + +assertEquals("2.5", (abs("-2.5".d) as BigDecimal).toStringExpanded()) +assertEquals("2", (floor("2.9".d) as BigDecimal).toStringExpanded()) + +// Temporary Real bridge: +assertEquals((exp(1.25) as Real).d.toStringExpanded(), (exp("1.25".d) as BigDecimal).toStringExpanded()) +assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), (sqrt("2".d) as BigDecimal).toStringExpanded()) +``` + If you care about exact decimal source text: ```lyng diff --git a/docs/ai_stdlib_reference.md b/docs/ai_stdlib_reference.md index 0119d2d..f003711 100644 --- a/docs/ai_stdlib_reference.md +++ b/docs/ai_stdlib_reference.md @@ -15,6 +15,10 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s - Preconditions: `require`, `check`. - Async/concurrency: `launch`, `yield`, `flow`, `delay`. - Math: `floor`, `ceil`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `ln`, `log10`, `log2`, `pow`, `sqrt`, `abs`, `clamp`. + - These helpers also accept `lyng.decimal.BigDecimal`. + - Exact Decimal path today: `abs`, `floor`, `ceil`, `round`, and `pow` with integral exponent. + - Temporary Decimal path for the rest: convert `BigDecimal -> Real`, compute, then convert back to `BigDecimal`. + - Treat that bridge as temporary; prefer native Decimal implementations when they become available. ## 3. Core Global Constants/Types - Values: `Unset`, `π`. diff --git a/docs/math.md b/docs/math.md index 7d4f7b9..c0cbe0d 100644 --- a/docs/math.md +++ b/docs/math.md @@ -60,8 +60,13 @@ but: ## Round and range -The following functions return its argument if it is `Int`, -or transformed `Real` otherwise. +The following functions return the argument unchanged if it is `Int`. + +For `BigDecimal`: +- `floor(x)`, `ceil(x)`, and `round(x)` currently use exact decimal operations +- the result stays `BigDecimal` + +For `Real`, the result is a transformed `Real`. | name | description | |----------------|--------------------------------------------------------| @@ -72,6 +77,14 @@ or transformed `Real` otherwise. ## Lyng math functions +Decimal note: +- all scalar math helpers accept `BigDecimal` +- `abs(x)` stays exact for `BigDecimal` +- `pow(x, y)` is exact for `BigDecimal` when `y` is an integral exponent +- the remaining `BigDecimal` cases currently use a temporary bridge: + `BigDecimal -> Real -> host math -> BigDecimal` +- this is temporary; native decimal implementations are planned + | name | meaning | |-----------|------------------------------------------------------| | sin(x) | sine | @@ -91,7 +104,7 @@ or transformed `Real` otherwise. | log10(x) | $log_{10}(x)$ | | pow(x, y) | ${x^y}$ | | sqrt(x) | $ \sqrt {x}$ | -| abs(x) | absolute value of x. Int if x is Int, Real otherwise | +| abs(x) | absolute value of x. Int if x is Int, BigDecimal if x is BigDecimal, Real otherwise | | clamp(x, range) | limit x to be inside range boundaries | For example: @@ -104,6 +117,13 @@ For example: assert( abs(-1) is Int) assert( abs(-2.21) == 2.21 ) + import lyng.decimal + + // Decimal-aware math works too. Some functions are exact, some still bridge through Real temporarily: + assert( (abs("-2.5".d) as BigDecimal).toStringExpanded() == "2.5" ) + assert( (floor("2.9".d) as BigDecimal).toStringExpanded() == "2" ) + assert( sin("0.5".d) is BigDecimal ) + // clamp() limits value to the range: assert( clamp(15, 0..10) == 10 ) assert( clamp(-5, 0..10) == 0 ) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 163d3b1..ef3f8a9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -252,6 +252,44 @@ class Script( companion object { + private suspend fun ScopeFacade.numberToDouble(value: Obj): Double = + ObjBigDecimalSupport.toDoubleOrNull(value) ?: value.toDouble() + + private suspend fun ScopeFacade.decimalAwareUnaryMath( + value: Obj, + exactDecimal: (suspend ScopeFacade.(Obj) -> Obj?)? = null, + fallback: (Double) -> Double + ): Obj { + 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") + } + return ObjReal(fallback(numberToDouble(value))) + } + + private suspend fun ScopeFacade.decimalAwareRoundLike( + value: Obj, + exactDecimal: suspend ScopeFacade.(Obj) -> Obj?, + realFallback: (Double) -> Double + ): Obj { + if (value is ObjInt) return value + exactDecimal(value)?.let { return it } + return ObjReal(realFallback(numberToDouble(value))) + } + + 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") + } + return ObjReal(numberToDouble(base).pow(numberToDouble(exponent))) + } + /** * Create new scope using a standard safe set of modules, using [defaultImportManager]. It is * suspended as first time invocation requires compilation of standard library or other @@ -289,87 +327,81 @@ class Script( } addFn("floor") { val x = args.firstAndOnly() - (if (x is ObjInt) x - else ObjReal(floor(x.toDouble()))) + decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor) } addFn("ceil") { val x = args.firstAndOnly() - (if (x is ObjInt) x - else ObjReal(ceil(x.toDouble()))) + decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil) } addFn("round") { val x = args.firstAndOnly() - (if (x is ObjInt) x - else ObjReal(round(x.toDouble()))) + decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round) } addFn("sin") { - ObjReal(sin(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin) } addFn("cos") { - ObjReal(cos(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos) } addFn("tan") { - ObjReal(tan(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan) } addFn("asin") { - ObjReal(asin(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin) } addFn("acos") { - ObjReal(acos(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos) } addFn("atan") { - ObjReal(atan(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan) } addFn("sinh") { - ObjReal(sinh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh) } addFn("cosh") { - ObjReal(cosh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh) } addFn("tanh") { - ObjReal(tanh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh) } addFn("asinh") { - ObjReal(asinh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh) } addFn("acosh") { - ObjReal(acosh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh) } addFn("atanh") { - ObjReal(atanh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh) } addFn("exp") { - ObjReal(exp(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp) } addFn("ln") { - ObjReal(ln(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln) } addFn("log10") { - ObjReal(log10(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10) } addFn("log2") { - ObjReal(log2(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2) } addFn("pow") { requireExactCount(2) - ObjReal( - (args[0].toDouble()).pow(args[1].toDouble()) - ) + decimalAwarePow(args[0], args[1]) } addFn("sqrt") { - ObjReal( - sqrt(args.firstAndOnly().toDouble()) - ) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt) } addFn("abs") { val x = args.firstAndOnly() - if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue) + if (x is ObjInt) ObjInt(x.value.absoluteValue) + else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue } } addFnDoc( @@ -629,78 +661,75 @@ class Script( ) ensureFn("floor") { val x = args.firstAndOnly() - if (x is ObjInt) x else ObjReal(floor(x.toDouble())) + decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor) } ensureFn("ceil") { val x = args.firstAndOnly() - if (x is ObjInt) x else ObjReal(ceil(x.toDouble())) + decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil) } ensureFn("round") { val x = args.firstAndOnly() - if (x is ObjInt) x else ObjReal(round(x.toDouble())) + decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round) } ensureFn("sin") { - ObjReal(sin(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin) } ensureFn("cos") { - ObjReal(cos(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos) } ensureFn("tan") { - ObjReal(tan(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan) } ensureFn("asin") { - ObjReal(asin(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin) } ensureFn("acos") { - ObjReal(acos(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos) } ensureFn("atan") { - ObjReal(atan(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan) } ensureFn("sinh") { - ObjReal(sinh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh) } ensureFn("cosh") { - ObjReal(cosh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh) } ensureFn("tanh") { - ObjReal(tanh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh) } ensureFn("asinh") { - ObjReal(asinh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh) } ensureFn("acosh") { - ObjReal(acosh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh) } ensureFn("atanh") { - ObjReal(atanh(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh) } ensureFn("exp") { - ObjReal(exp(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp) } ensureFn("ln") { - ObjReal(ln(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln) } ensureFn("log10") { - ObjReal(log10(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10) } ensureFn("log2") { - ObjReal(log2(args.firstAndOnly().toDouble())) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2) } ensureFn("pow") { requireExactCount(2) - ObjReal( - (args[0].toDouble()).pow(args[1].toDouble()) - ) + decimalAwarePow(args[0], args[1]) } ensureFn("sqrt") { - ObjReal( - sqrt(args.firstAndOnly().toDouble()) - ) + decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt) } ensureFn("abs") { val x = args.firstAndOnly() - if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue) + if (x is ObjInt) ObjInt(x.value.absoluteValue) + else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue } } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt index 49c5a39..e6c049d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -448,37 +448,37 @@ private fun buildStdlibDocs(): List { // Math helpers (scalar versions) fun math1(name: String) = mod.funDoc( name = name, - doc = StdlibInlineDocIndex.topFunDoc(name) ?: "Compute $name(x).", + 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.", 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.", params = listOf(ParamDoc("x", type("lyng.Number")))) - mod.funDoc(name = "ceil", doc = StdlibInlineDocIndex.topFunDoc("ceil") ?: "Round up the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number")))) - mod.funDoc(name = "round", doc = StdlibInlineDocIndex.topFunDoc("round") ?: "Round the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number")))) + 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")))) // 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.", params = listOf(ParamDoc("x", type("lyng.Number")))) - mod.funDoc(name = "ln", doc = StdlibInlineDocIndex.topFunDoc("ln") ?: "Natural logarithm (base e).", params = listOf(ParamDoc("x", type("lyng.Number")))) - mod.funDoc(name = "log10", doc = StdlibInlineDocIndex.topFunDoc("log10") ?: "Logarithm base 10.", params = listOf(ParamDoc("x", type("lyng.Number")))) - mod.funDoc(name = "log2", doc = StdlibInlineDocIndex.topFunDoc("log2") ?: "Logarithm base 2.", params = listOf(ParamDoc("x", type("lyng.Number")))) + 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")))) // Power/roots and absolute value mod.funDoc( name = "pow", - doc = StdlibInlineDocIndex.topFunDoc("pow") ?: "Raise `x` to the power `y`.", + 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.", params = listOf(ParamDoc("x", type("lyng.Number")), ParamDoc("y", type("lyng.Number"))) ) mod.funDoc( name = "sqrt", - doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `x`.", + doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `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 = "abs", - doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number (works for Int and Real).", + doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number. Int stays Int, and BigDecimal stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))) ) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt index 7a1ec0f..9f399ee 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt @@ -128,6 +128,37 @@ object ObjBigDecimalSupport { 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 @@ -160,6 +191,12 @@ object ObjBigDecimalSupport { 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) @@ -248,6 +285,12 @@ object ObjBigDecimalSupport { } } + 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", diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt index 3719a8b..e69abe0 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt @@ -146,4 +146,84 @@ class BigDecimalModuleTest { assertEquals("0.2", s0.toString()) """.trimIndent()) } + + @Test + fun testDecimalMathHelpersUseExactImplementationsWhenAvailable() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.decimal + + val absValue = abs("-2.5".d) as BigDecimal + val floorPos = floor("2.9".d) as BigDecimal + val floorNeg = floor("-2.1".d) as BigDecimal + val ceilPos = ceil("2.1".d) as BigDecimal + val ceilNeg = ceil("-2.1".d) as BigDecimal + val roundPos = round("2.5".d) as BigDecimal + val roundNeg = round("-2.5".d) as BigDecimal + val powInt = pow("1.5".d, 2) as BigDecimal + + assertEquals("2.5", absValue.toStringExpanded()) + assertEquals("2", floorPos.toStringExpanded()) + assertEquals("-3", floorNeg.toStringExpanded()) + assertEquals("3", ceilPos.toStringExpanded()) + assertEquals("-2", ceilNeg.toStringExpanded()) + assertEquals("3", roundPos.toStringExpanded()) + assertEquals("-2", roundNeg.toStringExpanded()) + assertEquals("2.25", powInt.toStringExpanded()) + """.trimIndent() + ) + } + + @Test + fun testDecimalMathHelpersFallbackThroughRealTemporarily() = runTest { + val scope = Script.newScope() + scope.eval( + """ + import lyng.decimal + + val sinDecimal = sin("0.5".d) as BigDecimal + val expDecimal = exp("1.25".d) as BigDecimal + val sqrtDecimal = sqrt("2".d) as BigDecimal + val lnDecimal = ln("2".d) as BigDecimal + val log10Decimal = log10("2".d) as BigDecimal + val log2Decimal = log2("2".d) as BigDecimal + val powDecimal = pow("2".d, "0.5".d) as BigDecimal + + assertEquals((sin(0.5) as Real).d.toStringExpanded(), sinDecimal.toStringExpanded()) + assertEquals((exp(1.25) as Real).d.toStringExpanded(), expDecimal.toStringExpanded()) + assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), sqrtDecimal.toStringExpanded()) + assertEquals((ln(2.0) as Real).d.toStringExpanded(), lnDecimal.toStringExpanded()) + assertEquals((log10(2.0) as Real).d.toStringExpanded(), log10Decimal.toStringExpanded()) + assertEquals((log2(2.0) as Real).d.toStringExpanded(), log2Decimal.toStringExpanded()) + assertEquals((pow(2.0, 0.5) as Real).d.toStringExpanded(), powDecimal.toStringExpanded()) + """.trimIndent() + ) + } + + @Test + fun decimalMustBeObj() = runTest { + eval(""" + import lyng.decimal + + val decimal = 42.d + val context = DecimalContext(12) + + assert(decimal is BigDecimal) + assertEquals(BigDecimal, decimal::class) + + assert(context is DecimalContext) + assertEquals(DecimalContext, context::class) + """.trimIndent()) + } + + @Test + fun testFromRealLife1() = runTest { + eval(""" + import lyng.decimal + var X = 42.d + X += 11 + assertEquals(53.d, X) + """) + } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt index 189544f..bca0f94 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/MatrixModuleTest.kt @@ -152,4 +152,20 @@ class MatrixModuleTest { """.trimIndent() ) } + + @Test + fun matrixAndVectorMustBeObjs() = runTest { + eval(""" + import lyng.matrix + + val matrixValue = matrix([[1, 2], [3, 4]]) + val vectorValue = vector([1, 2, 3]) + + assert(matrixValue is Matrix) + assertEquals(Matrix, matrixValue::class) + + assert(vectorValue is Vector) + assertEquals(Vector, vectorValue::class) + """.trimIndent()) + } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/RandomModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/RandomModuleTest.kt new file mode 100644 index 0000000..cb4985f --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/RandomModuleTest.kt @@ -0,0 +1,33 @@ +/* + * 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 + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class RandomModuleTest { + @Test + fun seededRandomMustBeObj() = runTest { + eval(""" + val rng = Random.seeded(12345) + + assert(rng is SeededRandom) + assertEquals(SeededRandom, rng::class) + """.trimIndent()) + } +} diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 1aedcc7..4d79828 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -77,10 +77,17 @@ extern class MapEntry : Array { } // Built-in math helpers (implemented in host runtime). -extern fun abs(x: Object): Real -extern fun ln(x: Object): Real -extern fun pow(x: Object, y: Object): Real -extern fun sqrt(x: Object): Real +// +// Decimal note: +// - these helpers accept `BigDecimal` values too +// - `abs`, `floor`, `ceil`, `round`, and `pow(x, y)` with integral `y` keep decimal arithmetic +// - the remaining decimal cases currently use a temporary bridge: +// `BigDecimal -> Real -> host math -> BigDecimal` +// - this is temporary and will be replaced with dedicated decimal implementations +extern fun abs(x: Object): Object +extern fun ln(x: Object): Object +extern fun pow(x: Object, y: Object): Object +extern fun sqrt(x: Object): Object extern fun clamp(value: T, range: Range): T class SeededRandom { diff --git a/notes/decimal_math_todo.md b/notes/decimal_math_todo.md new file mode 100644 index 0000000..9bf39d3 --- /dev/null +++ b/notes/decimal_math_todo.md @@ -0,0 +1,36 @@ +# Decimal Math TODO + +These stdlib math helpers currently accept `BigDecimal`, but some still use the temporary compatibility path + +`BigDecimal -> Real -> host math -> BigDecimal` + +instead of a native decimal implementation. + +## Still Using The Temporary Real Bridge + +- `sin(x)` +- `cos(x)` +- `tan(x)` +- `asin(x)` +- `acos(x)` +- `atan(x)` +- `sinh(x)` +- `cosh(x)` +- `tanh(x)` +- `asinh(x)` +- `acosh(x)` +- `atanh(x)` +- `exp(x)` +- `ln(x)` +- `log10(x)` +- `log2(x)` +- `sqrt(x)` +- `pow(x, y)` when the decimal case is not reducible to an integral exponent + +## Already Native For Decimal + +- `abs(x)` +- `floor(x)` +- `ceil(x)` +- `round(x)` +- `pow(x, y)` with integral exponent `y`