diff --git a/docs/Decimal.md b/docs/Decimal.md index dd16ad5..4a4b1e6 100644 --- a/docs/Decimal.md +++ b/docs/Decimal.md @@ -8,9 +8,9 @@ Import it when you need decimal arithmetic that should not inherit `Real`'s bina import lyng.decimal ``` -## What `BigDecimal` Is For +## What `Decimal` Is For -Use `BigDecimal` when values are fundamentally decimal: +Use `Decimal` when values are fundamentally decimal: - money - human-entered quantities @@ -38,8 +38,8 @@ assertEquals("2.2", c.toStringExpanded()) The three forms mean different things: -- `1.d`: convert `Int -> BigDecimal` -- `2.2.d`: convert `Real -> BigDecimal` +- `1.d`: convert `Int -> Decimal` +- `2.2.d`: convert `Real -> Decimal` - `"2.2".d`: parse exact decimal text That distinction is intentional. @@ -67,16 +67,36 @@ The explicit factory methods are: ```lyng import lyng.decimal -BigDecimal.fromInt(10) -BigDecimal.fromReal(2.5) -BigDecimal.fromString("12.34") +Decimal.fromInt(10) +Decimal.fromReal(2.5) +Decimal.fromString("12.34") ``` These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code. +## From Kotlin + +If you already have an ionspin `BigDecimal` on the host side, the simplest supported way to create a Lyng `Decimal` is: + +```kotlin +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import net.sergeych.lyng.Script +import net.sergeych.lyng.asFacade +import net.sergeych.lyng.newDecimal + +val scope = Script.newScope() +val decimal = scope.asFacade().newDecimal(BigDecimal.parseStringWithMode("12.34")) +``` + +Notes: + +- `newDecimal(...)` loads `lyng.decimal` if needed +- it returns a real Lyng `Decimal` object instance +- this is the preferred Kotlin-side construction path when you already hold a host `BigDecimal` + ## Core Operations -`BigDecimal` supports: +`Decimal` supports: - `+` - `-` @@ -115,7 +135,7 @@ assert(2 == 2.d) assert(3 > 2.d) ``` -Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + BigDecimal` and `Real + BigDecimal`. +Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + Decimal` and `Real + Decimal`. See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that. @@ -226,7 +246,7 @@ assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { ( ## 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`. +now also accept `Decimal`. Current behavior is intentionally split: @@ -235,7 +255,7 @@ Current behavior is intentionally split: - `floor(x)` - `ceil(x)` - `round(x)` - - `pow(x, y)` when `x` is `BigDecimal` and `y` is an integral exponent + - `pow(x, y)` when `x` is `Decimal` and `y` is an integral exponent - temporary bridge through `Real`: - `sin`, `cos`, `tan` - `asin`, `acos`, `atan` @@ -248,7 +268,7 @@ Current behavior is intentionally split: The temporary bridge is: ```lyng -BigDecimal -> Real -> host math -> BigDecimal +Decimal -> Real -> host math -> Decimal ``` This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based @@ -259,12 +279,12 @@ Examples: ```lyng import lyng.decimal -assertEquals("2.5", (abs("-2.5".d) as BigDecimal).toStringExpanded()) -assertEquals("2", (floor("2.9".d) as BigDecimal).toStringExpanded()) +assertEquals("2.5", (abs("-2.5".d) as Decimal).toStringExpanded()) +assertEquals("2", (floor("2.9".d) as Decimal).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()) +assertEquals((exp(1.25) as Real).d.toStringExpanded(), (exp("1.25".d) as Decimal).toStringExpanded()) +assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), (sqrt("2".d) as Decimal).toStringExpanded()) ``` If you care about exact decimal source text: diff --git a/docs/OperatorInterop.md b/docs/OperatorInterop.md index cf244d0..06cf2c2 100644 --- a/docs/OperatorInterop.md +++ b/docs/OperatorInterop.md @@ -160,15 +160,15 @@ import lyng.decimal 3 > 2.d ``` -work naturally even though `Int` and `Real` themselves were not edited to know `BigDecimal`. +work naturally even though `Int` and `Real` themselves were not edited to know `Decimal`. The shape is: - `leftClass = Int` or `Real` -- `rightClass = BigDecimal` -- `commonClass = BigDecimal` -- convert built-ins into `BigDecimal` -- leave `BigDecimal` values unchanged +- `rightClass = Decimal` +- `commonClass = Decimal` +- convert built-ins into `Decimal` +- leave `Decimal` values unchanged ## Step-By-Step Pattern For Your Own Type diff --git a/docs/ai_stdlib_reference.md b/docs/ai_stdlib_reference.md index f003711..cc4863a 100644 --- a/docs/ai_stdlib_reference.md +++ b/docs/ai_stdlib_reference.md @@ -15,9 +15,9 @@ 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`. + - These helpers also accept `lyng.decimal.Decimal`. - 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`. + - Temporary Decimal path for the rest: convert `Decimal -> Real`, compute, then convert back to `Decimal`. - Treat that bridge as temporary; prefer native Decimal implementations when they become available. ## 3. Core Global Constants/Types @@ -60,6 +60,9 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s ## 5. Additional Built-in Modules (import explicitly) - `import lyng.observable` - `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`. +- `import lyng.decimal` + - `Decimal`, `DecimalContext`, `DecimalRounding`, `withDecimalContext(...)`. + - Kotlin host helper: `ScopeFacade.newDecimal(BigDecimal)` wraps an ionspin host decimal as a Lyng `Decimal`. - `import lyng.complex` - `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`. - `import lyng.matrix` diff --git a/docs/math.md b/docs/math.md index c0cbe0d..fa9eb21 100644 --- a/docs/math.md +++ b/docs/math.md @@ -62,9 +62,9 @@ but: The following functions return the argument unchanged if it is `Int`. -For `BigDecimal`: +For `Decimal`: - `floor(x)`, `ceil(x)`, and `round(x)` currently use exact decimal operations -- the result stays `BigDecimal` +- the result stays `Decimal` For `Real`, the result is a transformed `Real`. @@ -78,11 +78,11 @@ For `Real`, the result is a transformed `Real`. ## 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` +- all scalar math helpers accept `Decimal` +- `abs(x)` stays exact for `Decimal` +- `pow(x, y)` is exact for `Decimal` when `y` is an integral exponent +- the remaining `Decimal` cases currently use a temporary bridge: + `Decimal -> Real -> host math -> Decimal` - this is temporary; native decimal implementations are planned | name | meaning | @@ -104,7 +104,7 @@ Decimal note: | 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, BigDecimal if x is BigDecimal, Real otherwise | +| abs(x) | absolute value of x. Int if x is Int, Decimal if x is Decimal, Real otherwise | | clamp(x, range) | limit x to be inside range boundaries | For example: @@ -120,9 +120,9 @@ For example: 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 ) + assert( (abs("-2.5".d) as Decimal).toStringExpanded() == "2.5" ) + assert( (floor("2.9".d) as Decimal).toStringExpanded() == "2" ) + assert( sin("0.5".d) is Decimal ) // clamp() limits value to the range: assert( clamp(15, 0..10) == 10 ) diff --git a/docs/whats_new.md b/docs/whats_new.md index 9fd98ab..66ab1c2 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -57,7 +57,7 @@ Lyng now ships a first-class decimal module built as a regular extension library It provides: -- `BigDecimal` +- `Decimal` - convenient `.d` conversions from `Int`, `Real`, and `String` - mixed arithmetic with `Int` and `Real` - local division precision and rounding control via `withDecimalContext(...)` diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt index b33f60f..d264285 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index ef3f8a9..155643c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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)) 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 e6c049d..1e45c95 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). 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"))) ) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt deleted file mode 100644 index 9f399ee..0000000 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBigDecimalSupport.kt +++ /dev/null @@ -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 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(0).value - newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value)) - } - decimalClass.addClassFn("fromReal") { - val value = requiredArg(0).value - newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode)) - } - decimalClass.addClassFn("fromString") { - val value = requiredArg(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(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().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().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().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(0).value - newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value)) - }, - rightToCommon = ObjExternCallable.fromBridge { - requiredArg(0) - } - ) - } -} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/decimal_lyng.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/decimal_lyng.kt index 5c3e457..34c8886 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/decimal_lyng.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/decimal_lyng.kt @@ -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 } /** diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/operators_lyng.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/operators_lyng.kt index 08f5ede..f235643 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/operators_lyng.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/operators_lyng.kt @@ -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 { diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt deleted file mode 100644 index e69abe0..0000000 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/BigDecimalModuleTest.kt +++ /dev/null @@ -1,229 +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 - -import kotlinx.coroutines.test.runTest -import kotlin.test.Test - -class BigDecimalModuleTest { - @Test - fun testDecimalModuleFactoriesAndConversions() = runTest { - val scope = Script.newScope() - scope.eval( - """ - import lyng.decimal - - assertEquals("12.34", BigDecimal.fromString("12.34").toStringExpanded()) - assertEquals("1", BigDecimal.fromInt(1).toStringExpanded()) - assertEquals("2.5", "2.5".d.toStringExpanded()) - assertEquals("1", 1.d.toStringExpanded()) - assertEquals("2.2", 2.2.d.toStringExpanded()) - assertEquals("3", (1 + 2).d.toStringExpanded()) - assertEquals("1.5", (1 + 0.5).d.toStringExpanded()) - assertEquals("3", (1 + 2.d).toStringExpanded()) - assertEquals("3", (2.d + 1).toStringExpanded()) - assertEquals(2.5, "2.5".d.toReal()) - assertEquals(2, "2.5".d.toInt()) - assertEquals(2.2, 2.2.d.toReal()) - assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded()) - """.trimIndent() - ) - } - - @Test - fun testDecimalModuleMixedIntOperators() = runTest { - val scope = Script.newScope() - scope.eval( - """ - import lyng.decimal - - assertEquals(3.d, 1 + 2.d) - assertEquals(3.d, 2.d + 1) - assertEquals(1.d, 3 - 2.d) - assertEquals(1.d, 3.d - 2) - assertEquals(8.d, 4 * 2.d) - assertEquals(8.d, 4.d * 2) - assertEquals(4.d, 8 / 2.d) - assertEquals(4.d, 8.d / 2) - assertEquals(1.d, 7 % 2.d) - assertEquals(1.d, 7.d % 2) - assert(1 < 2.d) - assert(2 <= 2.d) - assert(3 > 2.d) - assert(3.d > 2) - assert(2 == 2.d) - assert(2.d == 2) - """.trimIndent() - ) - } - - @Test - fun testDecimalDivisionUsesDefaultContext() = runTest { - val scope = Script.newScope() - scope.eval( - """ - import lyng.decimal - - assertEquals("0.125", (1.d / 8.d).toStringExpanded()) - assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded()) - assertEquals("0.6666666666666666666666666666666667", ("2".d / 3.d).toStringExpanded()) - """.trimIndent() - ) - } - - @Test - fun testWithDecimalContextOverridesDivisionContext() = runTest { - val scope = Script.newScope() - scope.eval( - """ - import lyng.decimal - - assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded()) - assertEquals("0.3333333333", withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }) - assertEquals("0.666667", withDecimalContext(6) { ("2".d / 3.d).toStringExpanded() }) - assertEquals("0.666667", withDecimalContext(DecimalContext(6)) { ("2".d / 3.d).toStringExpanded() }) - assertEquals("0.12", withDecimalContext(2) { (1.d / 8.d).toStringExpanded() }) - assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() }) - assertEquals("0.13", withDecimalContext(DecimalContext(2, DecimalRounding.HalfAwayFromZero)) { (1.d / 8.d).toStringExpanded() }) - assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded()) - """.trimIndent() - ) - } - - @Test - fun testDecimalDivisionRoundingMatrix() = runTest { - val scope = Script.newScope() - scope.eval( - """ - import lyng.decimal - - assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfEven) { (1.d / 8.d).toStringExpanded() }) - assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfEven) { (-1.d / 8.d).toStringExpanded() }) - - assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() }) - assertEquals("-0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (-1.d / 8.d).toStringExpanded() }) - - assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (1.d / 8.d).toStringExpanded() }) - assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (-1.d / 8.d).toStringExpanded() }) - - assertEquals("0.13", withDecimalContext(2, DecimalRounding.Ceiling) { (1.d / 8.d).toStringExpanded() }) - assertEquals("-0.12", withDecimalContext(2, DecimalRounding.Ceiling) { (-1.d / 8.d).toStringExpanded() }) - - assertEquals("0.12", withDecimalContext(2, DecimalRounding.Floor) { (1.d / 8.d).toStringExpanded() }) - assertEquals("-0.13", withDecimalContext(2, DecimalRounding.Floor) { (-1.d / 8.d).toStringExpanded() }) - - assertEquals("0.13", withDecimalContext(2, DecimalRounding.AwayFromZero) { (1.d / 8.d).toStringExpanded() }) - assertEquals("-0.13", withDecimalContext(2, DecimalRounding.AwayFromZero) { (-1.d / 8.d).toStringExpanded() }) - - assertEquals("0.12", withDecimalContext(2, DecimalRounding.TowardsZero) { (1.d / 8.d).toStringExpanded() }) - assertEquals("-0.12", withDecimalContext(2, DecimalRounding.TowardsZero) { (-1.d / 8.d).toStringExpanded() }) - """.trimIndent() - ) - } - - @Test - fun testDefaultToString() = runTest { - eval(""" - import lyng.decimal - - var s0 = "0.1".d + "0.1".d - assertEquals("0.2", s0.toStringExpanded()) - 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/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 4d79828..bd812a4 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -79,10 +79,10 @@ extern class MapEntry : Array { // Built-in math helpers (implemented in host runtime). // // Decimal note: -// - these helpers accept `BigDecimal` values too +// - these helpers accept `Decimal` 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` +// `Decimal -> Real -> host math -> Decimal` // - this is temporary and will be replaced with dedicated decimal implementations extern fun abs(x: Object): Object extern fun ln(x: Object): Object diff --git a/notes/decimal_math_todo.md b/notes/decimal_math_todo.md index 9bf39d3..4d84983 100644 --- a/notes/decimal_math_todo.md +++ b/notes/decimal_math_todo.md @@ -1,8 +1,8 @@ # Decimal Math TODO -These stdlib math helpers currently accept `BigDecimal`, but some still use the temporary compatibility path +These stdlib math helpers currently accept `Decimal`, but some still use the temporary compatibility path -`BigDecimal -> Real -> host math -> BigDecimal` +`Decimal -> Real -> host math -> Decimal` instead of a native decimal implementation.