fixes on Decimals and bound kotlin classes

This commit is contained in:
Sergey Chernov 2026-03-29 01:20:20 +03:00
parent c7c333b71a
commit 05d7432b37
14 changed files with 114 additions and 667 deletions

View File

@ -8,9 +8,9 @@ Import it when you need decimal arithmetic that should not inherit `Real`'s bina
import lyng.decimal 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 - money
- human-entered quantities - human-entered quantities
@ -38,8 +38,8 @@ assertEquals("2.2", c.toStringExpanded())
The three forms mean different things: The three forms mean different things:
- `1.d`: convert `Int -> BigDecimal` - `1.d`: convert `Int -> Decimal`
- `2.2.d`: convert `Real -> BigDecimal` - `2.2.d`: convert `Real -> Decimal`
- `"2.2".d`: parse exact decimal text - `"2.2".d`: parse exact decimal text
That distinction is intentional. That distinction is intentional.
@ -67,16 +67,36 @@ The explicit factory methods are:
```lyng ```lyng
import lyng.decimal import lyng.decimal
BigDecimal.fromInt(10) Decimal.fromInt(10)
BigDecimal.fromReal(2.5) Decimal.fromReal(2.5)
BigDecimal.fromString("12.34") Decimal.fromString("12.34")
``` ```
These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code. 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 ## Core Operations
`BigDecimal` supports: `Decimal` supports:
- `+` - `+`
- `-` - `-`
@ -115,7 +135,7 @@ assert(2 == 2.d)
assert(3 > 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. 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 ## Decimal With Stdlib Math Functions
Core math helpers such as `abs`, `floor`, `ceil`, `round`, `sin`, `exp`, `ln`, `sqrt`, `log10`, `log2`, and `pow` 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: Current behavior is intentionally split:
@ -235,7 +255,7 @@ Current behavior is intentionally split:
- `floor(x)` - `floor(x)`
- `ceil(x)` - `ceil(x)`
- `round(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`: - temporary bridge through `Real`:
- `sin`, `cos`, `tan` - `sin`, `cos`, `tan`
- `asin`, `acos`, `atan` - `asin`, `acos`, `atan`
@ -248,7 +268,7 @@ Current behavior is intentionally split:
The temporary bridge is: The temporary bridge is:
```lyng ```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 This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based
@ -259,12 +279,12 @@ Examples:
```lyng ```lyng
import lyng.decimal import lyng.decimal
assertEquals("2.5", (abs("-2.5".d) as BigDecimal).toStringExpanded()) assertEquals("2.5", (abs("-2.5".d) as Decimal).toStringExpanded())
assertEquals("2", (floor("2.9".d) as BigDecimal).toStringExpanded()) assertEquals("2", (floor("2.9".d) as Decimal).toStringExpanded())
// Temporary Real bridge: // Temporary Real bridge:
assertEquals((exp(1.25) as Real).d.toStringExpanded(), (exp("1.25".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 BigDecimal).toStringExpanded()) assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), (sqrt("2".d) as Decimal).toStringExpanded())
``` ```
If you care about exact decimal source text: If you care about exact decimal source text:

View File

@ -160,15 +160,15 @@ import lyng.decimal
3 > 2.d 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: The shape is:
- `leftClass = Int` or `Real` - `leftClass = Int` or `Real`
- `rightClass = BigDecimal` - `rightClass = Decimal`
- `commonClass = BigDecimal` - `commonClass = Decimal`
- convert built-ins into `BigDecimal` - convert built-ins into `Decimal`
- leave `BigDecimal` values unchanged - leave `Decimal` values unchanged
## Step-By-Step Pattern For Your Own Type ## Step-By-Step Pattern For Your Own Type

View File

@ -15,9 +15,9 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
- Preconditions: `require`, `check`. - Preconditions: `require`, `check`.
- Async/concurrency: `launch`, `yield`, `flow`, `delay`. - 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`. - 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. - 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. - Treat that bridge as temporary; prefer native Decimal implementations when they become available.
## 3. Core Global Constants/Types ## 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) ## 5. Additional Built-in Modules (import explicitly)
- `import lyng.observable` - `import lyng.observable`
- `Observable`, `Subscription`, `ObservableList`, `ListChange` and change subtypes, `ChangeRejectionException`. - `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` - `import lyng.complex`
- `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`. - `Complex`, `complex(re, im)`, `cis(angle)`, and numeric embedding extensions such as `2.i` / `3.re`.
- `import lyng.matrix` - `import lyng.matrix`

View File

@ -62,9 +62,9 @@ but:
The following functions return the argument unchanged if it is `Int`. 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 - `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`. For `Real`, the result is a transformed `Real`.
@ -78,11 +78,11 @@ For `Real`, the result is a transformed `Real`.
## Lyng math functions ## Lyng math functions
Decimal note: Decimal note:
- all scalar math helpers accept `BigDecimal` - all scalar math helpers accept `Decimal`
- `abs(x)` stays exact for `BigDecimal` - `abs(x)` stays exact for `Decimal`
- `pow(x, y)` is exact for `BigDecimal` when `y` is an integral exponent - `pow(x, y)` is exact for `Decimal` when `y` is an integral exponent
- the remaining `BigDecimal` cases currently use a temporary bridge: - the remaining `Decimal` cases currently use a temporary bridge:
`BigDecimal -> Real -> host math -> BigDecimal` `Decimal -> Real -> host math -> Decimal`
- this is temporary; native decimal implementations are planned - this is temporary; native decimal implementations are planned
| name | meaning | | name | meaning |
@ -104,7 +104,7 @@ Decimal note:
| log10(x) | $log_{10}(x)$ | | log10(x) | $log_{10}(x)$ |
| pow(x, y) | ${x^y}$ | | pow(x, y) | ${x^y}$ |
| sqrt(x) | $ \sqrt {x}$ | | 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 | | clamp(x, range) | limit x to be inside range boundaries |
For example: For example:
@ -120,9 +120,9 @@ For example:
import lyng.decimal import lyng.decimal
// Decimal-aware math works too. Some functions are exact, some still bridge through Real temporarily: // 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( (abs("-2.5".d) as Decimal).toStringExpanded() == "2.5" )
assert( (floor("2.9".d) as BigDecimal).toStringExpanded() == "2" ) assert( (floor("2.9".d) as Decimal).toStringExpanded() == "2" )
assert( sin("0.5".d) is BigDecimal ) assert( sin("0.5".d) is Decimal )
// clamp() limits value to the range: // clamp() limits value to the range:
assert( clamp(15, 0..10) == 10 ) assert( clamp(15, 0..10) == 10 )

View File

@ -57,7 +57,7 @@ Lyng now ships a first-class decimal module built as a regular extension library
It provides: It provides:
- `BigDecimal` - `Decimal`
- convenient `.d` conversions from `Int`, `Real`, and `String` - convenient `.d` conversions from `Int`, `Real`, and `String`
- mixed arithmetic with `Int` and `Real` - mixed arithmetic with `Int` and `Real`
- local division precision and rounding control via `withDecimalContext(...)` - local division precision and rounding control via `withDecimalContext(...)`

View File

@ -17,6 +17,7 @@
package net.sergeych.lyng package net.sergeych.lyng
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import net.sergeych.lyng.obj.* import net.sergeych.lyng.obj.*
/** /**
@ -133,3 +134,6 @@ fun ScopeFacade.raiseIllegalOperation(message: String = "Operation is illegal"):
fun ScopeFacade.raiseIterationFinished(): Nothing = fun ScopeFacade.raiseIterationFinished(): Nothing =
raiseError(ObjIterationFinishedException(requireScope())) raiseError(ObjIterationFinishedException(requireScope()))
suspend fun ScopeFacade.newDecimal(value: BigDecimal): ObjInstance =
ObjDecimalSupport.newDecimal(this, value)

View File

@ -253,7 +253,7 @@ class Script(
companion object { companion object {
private suspend fun ScopeFacade.numberToDouble(value: Obj): Double = private suspend fun ScopeFacade.numberToDouble(value: Obj): Double =
ObjBigDecimalSupport.toDoubleOrNull(value) ?: value.toDouble() ObjDecimalSupport.toDoubleOrNull(value) ?: value.toDouble()
private suspend fun ScopeFacade.decimalAwareUnaryMath( private suspend fun ScopeFacade.decimalAwareUnaryMath(
value: Obj, value: Obj,
@ -263,9 +263,9 @@ class Script(
exactDecimal?.let { exact -> exactDecimal?.let { exact ->
exact(value)?.let { return it } exact(value)?.let { return it }
} }
if (ObjBigDecimalSupport.isDecimalValue(value)) { if (ObjDecimalSupport.isDecimalValue(value)) {
return ObjBigDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value))) return ObjDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value)))
?: raiseIllegalState("failed to convert Real result back to BigDecimal") ?: raiseIllegalState("failed to convert Real result back to Decimal")
} }
return ObjReal(fallback(numberToDouble(value))) return ObjReal(fallback(numberToDouble(value)))
} }
@ -281,11 +281,11 @@ class Script(
} }
private suspend fun ScopeFacade.decimalAwarePow(base: Obj, exponent: Obj): Obj { private suspend fun ScopeFacade.decimalAwarePow(base: Obj, exponent: Obj): Obj {
ObjBigDecimalSupport.exactPow(this, base, exponent)?.let { return it } ObjDecimalSupport.exactPow(this, base, exponent)?.let { return it }
if (ObjBigDecimalSupport.isDecimalValue(base) || ObjBigDecimalSupport.isDecimalValue(exponent)) { if (ObjDecimalSupport.isDecimalValue(base) || ObjDecimalSupport.isDecimalValue(exponent)) {
return ObjBigDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent))) return ObjDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent)))
?: ObjBigDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent))) ?: ObjDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent)))
?: raiseIllegalState("failed to convert Real pow result back to BigDecimal") ?: raiseIllegalState("failed to convert Real pow result back to Decimal")
} }
return ObjReal(numberToDouble(base).pow(numberToDouble(exponent))) return ObjReal(numberToDouble(base).pow(numberToDouble(exponent)))
} }
@ -327,15 +327,15 @@ class Script(
} }
addFn("floor") { addFn("floor") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor) decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
} }
addFn("ceil") { addFn("ceil") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil) decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
} }
addFn("round") { addFn("round") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round) decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
} }
addFn("sin") { addFn("sin") {
@ -401,7 +401,7 @@ class Script(
addFn("abs") { addFn("abs") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue) if (x is ObjInt) ObjInt(x.value.absoluteValue)
else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue } else decimalAwareUnaryMath(x, ObjDecimalSupport::exactAbs) { it.absoluteValue }
} }
addFnDoc( addFnDoc(
@ -661,15 +661,15 @@ class Script(
) )
ensureFn("floor") { ensureFn("floor") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor) decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
} }
ensureFn("ceil") { ensureFn("ceil") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil) decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
} }
ensureFn("round") { ensureFn("round") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round) decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
} }
ensureFn("sin") { ensureFn("sin") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin) decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
@ -729,7 +729,7 @@ class Script(
ensureFn("abs") { ensureFn("abs") {
val x = args.firstAndOnly() val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue) 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 -> addPackage("lyng.decimal") { module ->
module.eval(Source("lyng.decimal", decimalLyng)) module.eval(Source("lyng.decimal", decimalLyng))
ObjBigDecimalSupport.bindTo(module) ObjDecimalSupport.bindTo(module)
} }
addPackage("lyng.matrix") { module -> addPackage("lyng.matrix") { module ->
module.eval(Source("lyng.matrix", matrixLyng)) module.eval(Source("lyng.matrix", matrixLyng))

View File

@ -448,37 +448,37 @@ private fun buildStdlibDocs(): List<MiniDecl> {
// Math helpers (scalar versions) // Math helpers (scalar versions)
fun math1(name: String) = mod.funDoc( fun math1(name: String) = mod.funDoc(
name = name, 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"))) params = listOf(ParamDoc("x", type("lyng.Number")))
) )
math1("sin"); math1("cos"); math1("tan"); math1("asin"); math1("acos"); math1("atan") 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 = "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. 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. 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. 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. Decimal is handled directly and stays Decimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
// Hyperbolic and inverse hyperbolic // Hyperbolic and inverse hyperbolic
math1("sinh"); math1("cosh"); math1("tanh"); math1("asinh"); math1("acosh"); math1("atanh") math1("sinh"); math1("cosh"); math1("tanh"); math1("asinh"); math1("acosh"); math1("atanh")
// Exponentials and logarithms // 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 = "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). 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). 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. 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. 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. 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. 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 // Power/roots and absolute value
mod.funDoc( mod.funDoc(
name = "pow", 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"))) params = listOf(ParamDoc("x", type("lyng.Number")), ParamDoc("y", type("lyng.Number")))
) )
mod.funDoc( mod.funDoc(
name = "sqrt", 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"))) params = listOf(ParamDoc("x", type("lyng.Number")))
) )
mod.funDoc( mod.funDoc(
name = "abs", 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"))) params = listOf(ParamDoc("x", type("lyng.Number")))
) )

View File

@ -1,351 +0,0 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.obj
import com.ionspin.kotlin.bignum.decimal.DecimalMode
import com.ionspin.kotlin.bignum.decimal.RoundingMode
import com.ionspin.kotlin.bignum.integer.BigInteger
import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lyng.requiredArg
import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal
object ObjBigDecimalSupport {
private const val decimalContextVar = "__lyng_decimal_context__"
// For Real -> BigDecimal, preserve the actual IEEE-754 Double value using a
// round-trip-safe precision. This intentionally does not try to recover source text.
private val realConversionMode = DecimalMode(17L, RoundingMode.ROUND_HALF_TO_EVEN)
// Division needs an explicit stopping rule for non-terminating results. Use a
// decimal128-like default context until Lyng exposes per-operation contexts.
private val defaultDivisionMode = DecimalMode(34L, RoundingMode.ROUND_HALF_TO_EVEN)
private val zero: IonBigDecimal = IonBigDecimal.ZERO
private val decimalTypeDecl = TypeDecl.Simple("lyng.decimal.BigDecimal", false)
private object BoundMarker
private data class DecimalRuntimeContext(
val precision: Long,
val rounding: RoundingMode
) : Obj()
suspend fun bindTo(module: ModuleScope) {
val decimalClass = module.requireClass("BigDecimal")
if (decimalClass.kotlinClassData === BoundMarker) return
decimalClass.kotlinClassData = BoundMarker
decimalClass.isAbstract = false
val hooks = decimalClass.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
decimalClass.bridgeInitHooks = it
}
hooks += { _, instance ->
instance.kotlinInstanceData = zero
}
decimalClass.addFn("plus") {
newInstance(decimalClass, valueOf(thisObj).plus(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("minus") {
newInstance(decimalClass, valueOf(thisObj).minus(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("mul") {
newInstance(decimalClass, valueOf(thisObj).times(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("div") {
newInstance(decimalClass, divideWithContext(valueOf(thisObj), coerceArg(requireScope(), args.firstAndOnly()), currentDivisionMode(requireScope())))
}
decimalClass.addFn("mod") {
newInstance(decimalClass, valueOf(thisObj).rem(coerceArg(requireScope(), args.firstAndOnly())))
}
decimalClass.addFn("compareTo") {
ObjInt.of(valueOf(thisObj).compareTo(coerceArg(requireScope(), args.firstAndOnly())).toLong())
}
decimalClass.addFn("negate") {
newInstance(decimalClass, valueOf(thisObj).unaryMinus())
}
decimalClass.addFn("toInt") {
ObjInt.of(valueOf(thisObj).longValue(false))
}
decimalClass.addFn("toReal") {
ObjReal.of(valueOf(thisObj).doubleValue(false))
}
decimalClass.addFn("toString") {
ObjString(valueOf(thisObj).toStringExpanded())
}
decimalClass.addFn("toStringExpanded") {
ObjString(valueOf(thisObj).toStringExpanded())
}
decimalClass.addClassFn("fromInt") {
val value = requiredArg<ObjInt>(0).value
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
}
decimalClass.addClassFn("fromReal") {
val value = requiredArg<ObjReal>(0).value
newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode))
}
decimalClass.addClassFn("fromString") {
val value = requiredArg<ObjString>(0).value
try {
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
} catch (e: Throwable) {
requireScope().raiseIllegalArgument("invalid BigDecimal string: $value")
}
}
module.addFn("withDecimalContext") {
val (context, block) = when (args.list.size) {
2 -> {
val first = args[0]
val block = args[1]
if (first is ObjInt) {
DecimalRuntimeContext(first.value, RoundingMode.ROUND_HALF_TO_EVEN) to block
} else {
normalizeContext(requireScope(), first) to block
}
}
3 -> {
val precision = requiredArg<ObjInt>(0).value
val rounding = roundingModeFromObj(requireScope(), args[1])
DecimalRuntimeContext(precision, rounding) to args[2]
}
else -> requireScope().raiseIllegalArgument("withDecimalContext expects (context, block), (precision, block), or (precision, rounding, block)")
}
val child = requireScope().createChildScope()
child.addConst(decimalContextVar, context)
block.callOn(child)
}
registerBuiltinConversions(decimalClass)
registerInterop(decimalClass)
}
fun isDecimalValue(value: Obj): Boolean =
value is ObjInstance && value.objClass.className == "BigDecimal"
suspend fun exactAbs(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.abs()) }
suspend fun exactFloor(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.floor()) }
suspend fun exactCeil(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.ceil()) }
suspend fun exactRound(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let {
scope.newInstanceLikeDecimal(value, it.roundToDigitPositionAfterDecimalPoint(0, RoundingMode.ROUND_HALF_CEILING))
}
suspend fun exactPow(scope: ScopeFacade, base: Obj, exponent: Obj): Obj? {
val decimal = decimalValueOrNull(base) ?: return null
val intExponent = exponent as? ObjInt ?: return null
return scope.newInstanceLikeDecimal(base, decimal.pow(intExponent.value))
}
suspend fun fromRealLike(scope: ScopeFacade, sample: Obj, value: Double): Obj? {
if (!isDecimalValue(sample)) return null
return scope.newInstanceLikeDecimal(sample, IonBigDecimal.fromDouble(value, realConversionMode))
}
fun toDoubleOrNull(value: Obj): Double? =
decimalValueOrNull(value)?.doubleValue(false)
private fun valueOf(obj: Obj): IonBigDecimal {
val instance = obj as? ObjInstance ?: error("BigDecimal receiver must be an object instance")
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
}
private suspend fun currentDivisionMode(scope: Scope): DecimalMode {
val context = findContextObject(scope) ?: return defaultDivisionMode
return DecimalMode(context.precision, context.rounding)
}
private fun divideWithContext(left: IonBigDecimal, right: IonBigDecimal, mode: DecimalMode): IonBigDecimal {
if (mode.decimalPrecision <= 0L) {
return stripMode(left.divide(right, mode))
}
val exactLeft = stripMode(left)
val exactRight = stripMode(right)
val guardMode = DecimalMode(mode.decimalPrecision + 2, RoundingMode.TOWARDS_ZERO)
var guarded = stripMode(exactLeft.divide(exactRight, guardMode))
val hasMoreTail = !stripMode(exactLeft - stripMode(guarded * exactRight)).isZero()
if (hasMoreTail && isHalfRounding(mode.roundingMode) && looksLikeExactHalf(guarded, mode.decimalPrecision)) {
guarded = nudgeLastDigitAwayFromZero(guarded)
}
return stripMode(guarded.roundSignificand(mode))
}
private suspend fun ScopeFacade.newInstance(decimalClass: ObjClass, value: IonBigDecimal): ObjInstance {
val instance = call(decimalClass) as? ObjInstance
?: raiseIllegalState("BigDecimal() did not return an object instance")
instance.kotlinInstanceData = value
return instance
}
private suspend fun ScopeFacade.newInstanceLikeDecimal(sample: Obj, value: IonBigDecimal): ObjInstance {
val decimalClass = (sample as? ObjInstance)?.objClass
?: raiseIllegalState("BigDecimal sample must be an object instance")
return newInstance(decimalClass, value)
}
private fun coerceArg(scope: Scope, value: Obj): IonBigDecimal = when (value) {
is ObjInt -> IonBigDecimal.fromLongAsSignificand(value.value)
is ObjReal -> IonBigDecimal.fromDouble(value.value, realConversionMode)
is ObjInstance -> {
if (value.objClass.className != "BigDecimal") {
scope.raiseIllegalArgument("expected BigDecimal-compatible value, got ${value.objClass.className}")
}
value.kotlinInstanceData as? IonBigDecimal ?: zero
}
else -> scope.raiseIllegalArgument("expected BigDecimal-compatible value, got ${value.objClass.className}")
}
private suspend fun normalizeContext(scope: Scope, value: Obj): DecimalRuntimeContext {
val instance = value as? ObjInstance
?: scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
if (instance.objClass.className != "DecimalContext") {
scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
}
return decimalRuntimeContextFromInstance(scope, instance)
}
private fun findContextObject(scope: Scope): DecimalRuntimeContext? {
var current: Scope? = scope
while (current != null) {
val record = current.objects[decimalContextVar] ?: current.localBindings[decimalContextVar]
val value = when (val raw = record?.value) {
is FrameSlotRef -> raw.peekValue()
is RecordSlotRef -> raw.peekValue()
else -> raw
}
when (value) {
is DecimalRuntimeContext -> return value
}
current = current.parent
}
return null
}
private suspend fun decimalRuntimeContextFromInstance(scope: Scope, context: ObjInstance): DecimalRuntimeContext {
val precision = context.readField(scope, "precision").value as? ObjInt
?: scope.raiseClassCastError("DecimalContext.precision must be Int")
if (precision.value <= 0L) {
scope.raiseIllegalArgument("DecimalContext precision must be positive")
}
val rounding = roundingModeFromObj(scope, context.readField(scope, "rounding").value)
return DecimalRuntimeContext(precision.value, rounding)
}
private fun stripMode(value: IonBigDecimal): IonBigDecimal =
IonBigDecimal.fromBigIntegerWithExponent(value.significand, value.exponent)
private fun isHalfRounding(mode: RoundingMode): Boolean = when (mode) {
RoundingMode.ROUND_HALF_TO_EVEN,
RoundingMode.ROUND_HALF_AWAY_FROM_ZERO,
RoundingMode.ROUND_HALF_TOWARDS_ZERO,
RoundingMode.ROUND_HALF_CEILING,
RoundingMode.ROUND_HALF_FLOOR,
RoundingMode.ROUND_HALF_TO_ODD -> true
else -> false
}
private fun looksLikeExactHalf(value: IonBigDecimal, targetPrecision: Long): Boolean {
val digits = value.significand.abs().toString(10)
if (digits.length <= targetPrecision) return false
val discarded = digits.substring(targetPrecision.toInt())
return discarded[0] == '5' && discarded.drop(1).all { it == '0' }
}
private fun nudgeLastDigitAwayFromZero(value: IonBigDecimal): IonBigDecimal {
val ulpExponent = value.exponent - value.precision + 1
val ulp = IonBigDecimal.fromBigIntegerWithExponent(BigInteger.ONE, ulpExponent)
return if (value.significand.signum() < 0) value - ulp else value + ulp
}
private fun roundingModeFromObj(scope: Scope, value: Obj): RoundingMode {
val entry = value as? ObjEnumEntry ?: scope.raiseClassCastError("DecimalContext.rounding must be DecimalRounding")
return when (entry.name.value) {
"HalfEven" -> RoundingMode.ROUND_HALF_TO_EVEN
"HalfAwayFromZero" -> RoundingMode.ROUND_HALF_AWAY_FROM_ZERO
"HalfTowardsZero" -> RoundingMode.ROUND_HALF_TOWARDS_ZERO
"Ceiling" -> RoundingMode.CEILING
"Floor" -> RoundingMode.FLOOR
"AwayFromZero" -> RoundingMode.AWAY_FROM_ZERO
"TowardsZero" -> RoundingMode.TOWARDS_ZERO
else -> scope.raiseIllegalArgument("unsupported DecimalRounding: ${entry.name.value}")
}
}
private fun decimalValueOrNull(value: Obj): IonBigDecimal? {
if (!isDecimalValue(value)) return null
val instance = value as ObjInstance
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
}
private fun registerBuiltinConversions(decimalClass: ObjClass) {
ObjInt.type.addPropertyDoc(
name = "d",
doc = "Convert this integer to a BigDecimal.",
type = type("lyng.decimal.BigDecimal"),
moduleName = "lyng.decimal",
getter = { newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(thisAs<ObjInt>().value)) }
)
ObjInt.type.members["d"] = ObjInt.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
ObjReal.type.addPropertyDoc(
name = "d",
doc = "Convert this real number to a BigDecimal by preserving the current IEEE-754 value with 17 significant digits and half-even rounding.",
type = type("lyng.decimal.BigDecimal"),
moduleName = "lyng.decimal",
getter = { newInstance(decimalClass, IonBigDecimal.fromDouble(thisAs<ObjReal>().value, realConversionMode)) }
)
ObjReal.type.members["d"] = ObjReal.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
ObjString.type.addPropertyDoc(
name = "d",
doc = "Parse this string as a BigDecimal.",
type = type("lyng.decimal.BigDecimal"),
moduleName = "lyng.decimal",
getter = {
val value = thisAs<ObjString>().value
try {
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
} catch (e: Throwable) {
requireScope().raiseIllegalArgument("invalid BigDecimal string: $value")
}
}
)
ObjString.type.members["d"] = ObjString.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
}
private fun registerInterop(decimalClass: ObjClass) {
OperatorInteropRegistry.register(
leftClass = ObjInt.type,
rightClass = decimalClass,
commonClass = decimalClass,
operatorNames = listOf(
InteropOperator.Plus.name,
InteropOperator.Minus.name,
InteropOperator.Mul.name,
InteropOperator.Div.name,
InteropOperator.Mod.name,
InteropOperator.Compare.name,
InteropOperator.Equals.name
),
leftToCommon = ObjExternCallable.fromBridge {
val value = requiredArg<ObjInt>(0).value
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
},
rightToCommon = ObjExternCallable.fromBridge {
requiredArg<Obj>(0)
}
)
}
}

View File

@ -48,7 +48,7 @@ enum DecimalRounding {
/** /**
* Dynamic decimal arithmetic settings. * 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. * inside `withDecimalContext(...)`, which makes the rule local to a block of code.
* *
* Default context: * Default context:
@ -76,7 +76,7 @@ class DecimalContext(
/** /**
* Arbitrary-precision decimal value. * 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 * - money
* - human-entered decimal values * - human-entered decimal values
* - ratios that should round in decimal, not in binary * - ratios that should round in decimal, not in binary
@ -84,10 +84,10 @@ class DecimalContext(
* *
* Creating values: * Creating values:
* *
* - `1.d` converts `Int -> BigDecimal` * - `1.d` converts `Int -> Decimal`
* - `2.2.d` converts `Real -> BigDecimal` by preserving the current IEEE-754 value * - `2.2.d` converts `Real -> Decimal` by preserving the current IEEE-754 value
* - `"2.2".d` parses exact decimal text * - `"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: * Important distinction:
* *
@ -109,7 +109,7 @@ class DecimalContext(
* *
* Mixed arithmetic: * 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: * interop bridges so built-in left-hand operands work naturally:
* *
* import lyng.decimal * import lyng.decimal
@ -134,15 +134,15 @@ class DecimalContext(
* *
* "2.2".d * "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. */ /** Add another decimal-compatible value. */
extern fun plus(other: Object): BigDecimal extern fun plus(other: Object): Decimal
/** Subtract another decimal-compatible value. */ /** Subtract another decimal-compatible value. */
extern fun minus(other: Object): BigDecimal extern fun minus(other: Object): Decimal
/** Multiply by another decimal-compatible value. */ /** Multiply by another decimal-compatible value. */
extern fun mul(other: Object): BigDecimal extern fun mul(other: Object): Decimal
/** /**
* Divide by another decimal-compatible value. * Divide by another decimal-compatible value.
* *
@ -150,13 +150,13 @@ extern class BigDecimal() {
* - by default: `34` significant digits, `HalfEven` * - by default: `34` significant digits, `HalfEven`
* - inside `withDecimalContext(...)`: the context active for the current block * - 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. */ /** Remainder with another decimal-compatible value. */
extern fun mod(other: Object): BigDecimal extern fun mod(other: Object): Decimal
/** Compare with another decimal-compatible value. */ /** Compare with another decimal-compatible value. */
extern fun compareTo(other: Object): Int extern fun compareTo(other: Object): Int
/** Unary minus. */ /** Unary minus. */
extern fun negate(): BigDecimal extern fun negate(): Decimal
/** Convert to `Int` by dropping the fractional part according to backend conversion rules. */ /** Convert to `Int` by dropping the fractional part according to backend conversion rules. */
extern fun toInt(): Int extern fun toInt(): Int
/** Convert to `Real`. */ /** Convert to `Real`. */
@ -169,16 +169,16 @@ extern class BigDecimal() {
extern fun toStringExpanded(): String extern fun toStringExpanded(): String
/** Create a decimal from an `Int`. */ /** 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`. * Create a decimal from a `Real`.
* *
* This preserves the current IEEE-754 value using a round-trip-safe decimal conversion. * This preserves the current IEEE-754 value using a round-trip-safe decimal conversion.
* It does not try to recover the original source text. * 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. */ /** Parse exact decimal text. */
static extern fun fromString(value: String): BigDecimal static extern fun fromString(value: String): Decimal
} }
/** /**

View File

@ -97,7 +97,7 @@ enum BinaryOperator {
* The registry is symmetric for the converted values, but not for the original syntax. * 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: * 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 * - `1 + myDecimal` needs registration because `Int` itself is not rewritten
* *
* Typical pattern for a custom type: * Typical pattern for a custom type:
@ -135,7 +135,7 @@ enum BinaryOperator {
* - `3 > Rational(5, 2)` works * - `3 > Rational(5, 2)` works
* - `2 == Rational(2, 1)` 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. * work without changing the built-in `Int` or `Real` classes.
*/ */
extern object OperatorInterop { extern object OperatorInterop {

View File

@ -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)
""")
}
}

View File

@ -79,10 +79,10 @@ extern class MapEntry<K,V> : Array<Object> {
// Built-in math helpers (implemented in host runtime). // Built-in math helpers (implemented in host runtime).
// //
// Decimal note: // 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 // - `abs`, `floor`, `ceil`, `round`, and `pow(x, y)` with integral `y` keep decimal arithmetic
// - the remaining decimal cases currently use a temporary bridge: // - 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 // - this is temporary and will be replaced with dedicated decimal implementations
extern fun abs(x: Object): Object extern fun abs(x: Object): Object
extern fun ln(x: Object): Object extern fun ln(x: Object): Object

View File

@ -1,8 +1,8 @@
# Decimal Math TODO # 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. instead of a native decimal implementation.