fixed some issues with Decimals

This commit is contained in:
Sergey Chernov 2026-03-29 00:39:48 +03:00
parent e55d9c835a
commit c7c333b71a
11 changed files with 385 additions and 73 deletions

View File

@ -223,6 +223,50 @@ assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (
## Recommended Usage Rules
## Decimal With Stdlib Math Functions
Core math helpers such as `abs`, `floor`, `ceil`, `round`, `sin`, `exp`, `ln`, `sqrt`, `log10`, `log2`, and `pow`
now also accept `BigDecimal`.
Current behavior is intentionally split:
- exact decimal implementation:
- `abs(x)`
- `floor(x)`
- `ceil(x)`
- `round(x)`
- `pow(x, y)` when `x` is `BigDecimal` and `y` is an integral exponent
- temporary bridge through `Real`:
- `sin`, `cos`, `tan`
- `asin`, `acos`, `atan`
- `sinh`, `cosh`, `tanh`
- `asinh`, `acosh`, `atanh`
- `exp`, `ln`, `log10`, `log2`
- `sqrt`
- `pow` for the remaining non-integral decimal exponent cases
The temporary bridge is:
```lyng
BigDecimal -> Real -> host math -> BigDecimal
```
This is a compatibility step, not the long-term design. Native decimal implementations will replace these bridge-based
paths over time.
Examples:
```lyng
import lyng.decimal
assertEquals("2.5", (abs("-2.5".d) as BigDecimal).toStringExpanded())
assertEquals("2", (floor("2.9".d) as BigDecimal).toStringExpanded())
// Temporary Real bridge:
assertEquals((exp(1.25) as Real).d.toStringExpanded(), (exp("1.25".d) as BigDecimal).toStringExpanded())
assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), (sqrt("2".d) as BigDecimal).toStringExpanded())
```
If you care about exact decimal source text:
```lyng

View File

@ -15,6 +15,10 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
- Preconditions: `require`, `check`.
- Async/concurrency: `launch`, `yield`, `flow`, `delay`.
- Math: `floor`, `ceil`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `ln`, `log10`, `log2`, `pow`, `sqrt`, `abs`, `clamp`.
- These helpers also accept `lyng.decimal.BigDecimal`.
- Exact Decimal path today: `abs`, `floor`, `ceil`, `round`, and `pow` with integral exponent.
- Temporary Decimal path for the rest: convert `BigDecimal -> Real`, compute, then convert back to `BigDecimal`.
- Treat that bridge as temporary; prefer native Decimal implementations when they become available.
## 3. Core Global Constants/Types
- Values: `Unset`, `π`.

View File

@ -60,8 +60,13 @@ but:
## Round and range
The following functions return its argument if it is `Int`,
or transformed `Real` otherwise.
The following functions return the argument unchanged if it is `Int`.
For `BigDecimal`:
- `floor(x)`, `ceil(x)`, and `round(x)` currently use exact decimal operations
- the result stays `BigDecimal`
For `Real`, the result is a transformed `Real`.
| name | description |
|----------------|--------------------------------------------------------|
@ -72,6 +77,14 @@ or transformed `Real` otherwise.
## Lyng math functions
Decimal note:
- all scalar math helpers accept `BigDecimal`
- `abs(x)` stays exact for `BigDecimal`
- `pow(x, y)` is exact for `BigDecimal` when `y` is an integral exponent
- the remaining `BigDecimal` cases currently use a temporary bridge:
`BigDecimal -> Real -> host math -> BigDecimal`
- this is temporary; native decimal implementations are planned
| name | meaning |
|-----------|------------------------------------------------------|
| sin(x) | sine |
@ -91,7 +104,7 @@ or transformed `Real` otherwise.
| log10(x) | $log_{10}(x)$ |
| pow(x, y) | ${x^y}$ |
| sqrt(x) | $ \sqrt {x}$ |
| abs(x) | absolute value of x. Int if x is Int, Real otherwise |
| abs(x) | absolute value of x. Int if x is Int, BigDecimal if x is BigDecimal, Real otherwise |
| clamp(x, range) | limit x to be inside range boundaries |
For example:
@ -104,6 +117,13 @@ For example:
assert( abs(-1) is Int)
assert( abs(-2.21) == 2.21 )
import lyng.decimal
// Decimal-aware math works too. Some functions are exact, some still bridge through Real temporarily:
assert( (abs("-2.5".d) as BigDecimal).toStringExpanded() == "2.5" )
assert( (floor("2.9".d) as BigDecimal).toStringExpanded() == "2" )
assert( sin("0.5".d) is BigDecimal )
// clamp() limits value to the range:
assert( clamp(15, 0..10) == 10 )
assert( clamp(-5, 0..10) == 0 )

View File

@ -252,6 +252,44 @@ class Script(
companion object {
private suspend fun ScopeFacade.numberToDouble(value: Obj): Double =
ObjBigDecimalSupport.toDoubleOrNull(value) ?: value.toDouble()
private suspend fun ScopeFacade.decimalAwareUnaryMath(
value: Obj,
exactDecimal: (suspend ScopeFacade.(Obj) -> Obj?)? = null,
fallback: (Double) -> Double
): Obj {
exactDecimal?.let { exact ->
exact(value)?.let { return it }
}
if (ObjBigDecimalSupport.isDecimalValue(value)) {
return ObjBigDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value)))
?: raiseIllegalState("failed to convert Real result back to BigDecimal")
}
return ObjReal(fallback(numberToDouble(value)))
}
private suspend fun ScopeFacade.decimalAwareRoundLike(
value: Obj,
exactDecimal: suspend ScopeFacade.(Obj) -> Obj?,
realFallback: (Double) -> Double
): Obj {
if (value is ObjInt) return value
exactDecimal(value)?.let { return it }
return ObjReal(realFallback(numberToDouble(value)))
}
private suspend fun ScopeFacade.decimalAwarePow(base: Obj, exponent: Obj): Obj {
ObjBigDecimalSupport.exactPow(this, base, exponent)?.let { return it }
if (ObjBigDecimalSupport.isDecimalValue(base) || ObjBigDecimalSupport.isDecimalValue(exponent)) {
return ObjBigDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent)))
?: ObjBigDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent)))
?: raiseIllegalState("failed to convert Real pow result back to BigDecimal")
}
return ObjReal(numberToDouble(base).pow(numberToDouble(exponent)))
}
/**
* Create new scope using a standard safe set of modules, using [defaultImportManager]. It is
* suspended as first time invocation requires compilation of standard library or other
@ -289,87 +327,81 @@ class Script(
}
addFn("floor") {
val x = args.firstAndOnly()
(if (x is ObjInt) x
else ObjReal(floor(x.toDouble())))
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
}
addFn("ceil") {
val x = args.firstAndOnly()
(if (x is ObjInt) x
else ObjReal(ceil(x.toDouble())))
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
}
addFn("round") {
val x = args.firstAndOnly()
(if (x is ObjInt) x
else ObjReal(round(x.toDouble())))
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
}
addFn("sin") {
ObjReal(sin(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
}
addFn("cos") {
ObjReal(cos(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos)
}
addFn("tan") {
ObjReal(tan(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan)
}
addFn("asin") {
ObjReal(asin(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin)
}
addFn("acos") {
ObjReal(acos(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos)
}
addFn("atan") {
ObjReal(atan(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan)
}
addFn("sinh") {
ObjReal(sinh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh)
}
addFn("cosh") {
ObjReal(cosh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh)
}
addFn("tanh") {
ObjReal(tanh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh)
}
addFn("asinh") {
ObjReal(asinh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh)
}
addFn("acosh") {
ObjReal(acosh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh)
}
addFn("atanh") {
ObjReal(atanh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh)
}
addFn("exp") {
ObjReal(exp(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp)
}
addFn("ln") {
ObjReal(ln(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln)
}
addFn("log10") {
ObjReal(log10(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10)
}
addFn("log2") {
ObjReal(log2(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2)
}
addFn("pow") {
requireExactCount(2)
ObjReal(
(args[0].toDouble()).pow(args[1].toDouble())
)
decimalAwarePow(args[0], args[1])
}
addFn("sqrt") {
ObjReal(
sqrt(args.firstAndOnly().toDouble())
)
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt)
}
addFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
if (x is ObjInt) ObjInt(x.value.absoluteValue)
else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue }
}
addFnDoc(
@ -629,78 +661,75 @@ class Script(
)
ensureFn("floor") {
val x = args.firstAndOnly()
if (x is ObjInt) x else ObjReal(floor(x.toDouble()))
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
}
ensureFn("ceil") {
val x = args.firstAndOnly()
if (x is ObjInt) x else ObjReal(ceil(x.toDouble()))
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
}
ensureFn("round") {
val x = args.firstAndOnly()
if (x is ObjInt) x else ObjReal(round(x.toDouble()))
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
}
ensureFn("sin") {
ObjReal(sin(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
}
ensureFn("cos") {
ObjReal(cos(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos)
}
ensureFn("tan") {
ObjReal(tan(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan)
}
ensureFn("asin") {
ObjReal(asin(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin)
}
ensureFn("acos") {
ObjReal(acos(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos)
}
ensureFn("atan") {
ObjReal(atan(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan)
}
ensureFn("sinh") {
ObjReal(sinh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh)
}
ensureFn("cosh") {
ObjReal(cosh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh)
}
ensureFn("tanh") {
ObjReal(tanh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh)
}
ensureFn("asinh") {
ObjReal(asinh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh)
}
ensureFn("acosh") {
ObjReal(acosh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh)
}
ensureFn("atanh") {
ObjReal(atanh(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh)
}
ensureFn("exp") {
ObjReal(exp(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp)
}
ensureFn("ln") {
ObjReal(ln(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln)
}
ensureFn("log10") {
ObjReal(log10(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10)
}
ensureFn("log2") {
ObjReal(log2(args.firstAndOnly().toDouble()))
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2)
}
ensureFn("pow") {
requireExactCount(2)
ObjReal(
(args[0].toDouble()).pow(args[1].toDouble())
)
decimalAwarePow(args[0], args[1])
}
ensureFn("sqrt") {
ObjReal(
sqrt(args.firstAndOnly().toDouble())
)
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt)
}
ensureFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
if (x is ObjInt) ObjInt(x.value.absoluteValue)
else decimalAwareUnaryMath(x, ObjBigDecimalSupport::exactAbs) { it.absoluteValue }
}
}
}

View File

@ -448,37 +448,37 @@ private fun buildStdlibDocs(): List<MiniDecl> {
// Math helpers (scalar versions)
fun math1(name: String) = mod.funDoc(
name = name,
doc = StdlibInlineDocIndex.topFunDoc(name) ?: "Compute $name(x).",
doc = StdlibInlineDocIndex.topFunDoc(name) ?: "Compute $name(x). Accepts Int, Real, and BigDecimal. BigDecimal currently uses a temporary Real bridge and will get native decimal implementations later.",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
math1("sin"); math1("cos"); math1("tan"); math1("asin"); math1("acos"); math1("atan")
mod.funDoc(name = "floor", doc = StdlibInlineDocIndex.topFunDoc("floor") ?: "Round down the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ceil", doc = StdlibInlineDocIndex.topFunDoc("ceil") ?: "Round up the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "round", doc = StdlibInlineDocIndex.topFunDoc("round") ?: "Round the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "floor", doc = StdlibInlineDocIndex.topFunDoc("floor") ?: "Round down the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ceil", doc = StdlibInlineDocIndex.topFunDoc("ceil") ?: "Round up the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "round", doc = StdlibInlineDocIndex.topFunDoc("round") ?: "Round the number to the nearest integer. BigDecimal is handled directly and stays BigDecimal.", params = listOf(ParamDoc("x", type("lyng.Number"))))
// Hyperbolic and inverse hyperbolic
math1("sinh"); math1("cosh"); math1("tanh"); math1("asinh"); math1("acosh"); math1("atanh")
// Exponentials and logarithms
mod.funDoc(name = "exp", doc = StdlibInlineDocIndex.topFunDoc("exp") ?: "Euler's exponential e^x.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ln", doc = StdlibInlineDocIndex.topFunDoc("ln") ?: "Natural logarithm (base e).", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log10", doc = StdlibInlineDocIndex.topFunDoc("log10") ?: "Logarithm base 10.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log2", doc = StdlibInlineDocIndex.topFunDoc("log2") ?: "Logarithm base 2.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "exp", doc = StdlibInlineDocIndex.topFunDoc("exp") ?: "Euler's exponential e^x. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ln", doc = StdlibInlineDocIndex.topFunDoc("ln") ?: "Natural logarithm (base e). BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log10", doc = StdlibInlineDocIndex.topFunDoc("log10") ?: "Logarithm base 10. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log2", doc = StdlibInlineDocIndex.topFunDoc("log2") ?: "Logarithm base 2. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.", params = listOf(ParamDoc("x", type("lyng.Number"))))
// Power/roots and absolute value
mod.funDoc(
name = "pow",
doc = StdlibInlineDocIndex.topFunDoc("pow") ?: "Raise `x` to the power `y`.",
doc = StdlibInlineDocIndex.topFunDoc("pow") ?: "Raise `x` to the power `y`. BigDecimal with integral `y` is handled directly; other BigDecimal cases currently use a temporary Real bridge.",
params = listOf(ParamDoc("x", type("lyng.Number")), ParamDoc("y", type("lyng.Number")))
)
mod.funDoc(
name = "sqrt",
doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `x`.",
doc = StdlibInlineDocIndex.topFunDoc("sqrt") ?: "Square root of `x`. BigDecimal currently uses a temporary Real bridge and will get a native decimal implementation later.",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
mod.funDoc(
name = "abs",
doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number (works for Int and Real).",
doc = StdlibInlineDocIndex.topFunDoc("abs") ?: "Absolute value of a number. Int stays Int, and BigDecimal stays BigDecimal.",
params = listOf(ParamDoc("x", type("lyng.Number")))
)

View File

@ -128,6 +128,37 @@ object ObjBigDecimalSupport {
registerInterop(decimalClass)
}
fun isDecimalValue(value: Obj): Boolean =
value is ObjInstance && value.objClass.className == "BigDecimal"
suspend fun exactAbs(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.abs()) }
suspend fun exactFloor(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.floor()) }
suspend fun exactCeil(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let { scope.newInstanceLikeDecimal(value, it.ceil()) }
suspend fun exactRound(scope: ScopeFacade, value: Obj): Obj? =
decimalValueOrNull(value)?.let {
scope.newInstanceLikeDecimal(value, it.roundToDigitPositionAfterDecimalPoint(0, RoundingMode.ROUND_HALF_CEILING))
}
suspend fun exactPow(scope: ScopeFacade, base: Obj, exponent: Obj): Obj? {
val decimal = decimalValueOrNull(base) ?: return null
val intExponent = exponent as? ObjInt ?: return null
return scope.newInstanceLikeDecimal(base, decimal.pow(intExponent.value))
}
suspend fun fromRealLike(scope: ScopeFacade, sample: Obj, value: Double): Obj? {
if (!isDecimalValue(sample)) return null
return scope.newInstanceLikeDecimal(sample, IonBigDecimal.fromDouble(value, realConversionMode))
}
fun toDoubleOrNull(value: Obj): Double? =
decimalValueOrNull(value)?.doubleValue(false)
private fun valueOf(obj: Obj): IonBigDecimal {
val instance = obj as? ObjInstance ?: error("BigDecimal receiver must be an object instance")
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
@ -160,6 +191,12 @@ object ObjBigDecimalSupport {
return instance
}
private suspend fun ScopeFacade.newInstanceLikeDecimal(sample: Obj, value: IonBigDecimal): ObjInstance {
val decimalClass = (sample as? ObjInstance)?.objClass
?: raiseIllegalState("BigDecimal sample must be an object instance")
return newInstance(decimalClass, value)
}
private fun coerceArg(scope: Scope, value: Obj): IonBigDecimal = when (value) {
is ObjInt -> IonBigDecimal.fromLongAsSignificand(value.value)
is ObjReal -> IonBigDecimal.fromDouble(value.value, realConversionMode)
@ -248,6 +285,12 @@ object ObjBigDecimalSupport {
}
}
private fun decimalValueOrNull(value: Obj): IonBigDecimal? {
if (!isDecimalValue(value)) return null
val instance = value as ObjInstance
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
}
private fun registerBuiltinConversions(decimalClass: ObjClass) {
ObjInt.type.addPropertyDoc(
name = "d",

View File

@ -146,4 +146,84 @@ class BigDecimalModuleTest {
assertEquals("0.2", s0.toString())
""".trimIndent())
}
@Test
fun testDecimalMathHelpersUseExactImplementationsWhenAvailable() = runTest {
val scope = Script.newScope()
scope.eval(
"""
import lyng.decimal
val absValue = abs("-2.5".d) as BigDecimal
val floorPos = floor("2.9".d) as BigDecimal
val floorNeg = floor("-2.1".d) as BigDecimal
val ceilPos = ceil("2.1".d) as BigDecimal
val ceilNeg = ceil("-2.1".d) as BigDecimal
val roundPos = round("2.5".d) as BigDecimal
val roundNeg = round("-2.5".d) as BigDecimal
val powInt = pow("1.5".d, 2) as BigDecimal
assertEquals("2.5", absValue.toStringExpanded())
assertEquals("2", floorPos.toStringExpanded())
assertEquals("-3", floorNeg.toStringExpanded())
assertEquals("3", ceilPos.toStringExpanded())
assertEquals("-2", ceilNeg.toStringExpanded())
assertEquals("3", roundPos.toStringExpanded())
assertEquals("-2", roundNeg.toStringExpanded())
assertEquals("2.25", powInt.toStringExpanded())
""".trimIndent()
)
}
@Test
fun testDecimalMathHelpersFallbackThroughRealTemporarily() = runTest {
val scope = Script.newScope()
scope.eval(
"""
import lyng.decimal
val sinDecimal = sin("0.5".d) as BigDecimal
val expDecimal = exp("1.25".d) as BigDecimal
val sqrtDecimal = sqrt("2".d) as BigDecimal
val lnDecimal = ln("2".d) as BigDecimal
val log10Decimal = log10("2".d) as BigDecimal
val log2Decimal = log2("2".d) as BigDecimal
val powDecimal = pow("2".d, "0.5".d) as BigDecimal
assertEquals((sin(0.5) as Real).d.toStringExpanded(), sinDecimal.toStringExpanded())
assertEquals((exp(1.25) as Real).d.toStringExpanded(), expDecimal.toStringExpanded())
assertEquals((sqrt(2.0) as Real).d.toStringExpanded(), sqrtDecimal.toStringExpanded())
assertEquals((ln(2.0) as Real).d.toStringExpanded(), lnDecimal.toStringExpanded())
assertEquals((log10(2.0) as Real).d.toStringExpanded(), log10Decimal.toStringExpanded())
assertEquals((log2(2.0) as Real).d.toStringExpanded(), log2Decimal.toStringExpanded())
assertEquals((pow(2.0, 0.5) as Real).d.toStringExpanded(), powDecimal.toStringExpanded())
""".trimIndent()
)
}
@Test
fun decimalMustBeObj() = runTest {
eval("""
import lyng.decimal
val decimal = 42.d
val context = DecimalContext(12)
assert(decimal is BigDecimal)
assertEquals(BigDecimal, decimal::class)
assert(context is DecimalContext)
assertEquals(DecimalContext, context::class)
""".trimIndent())
}
@Test
fun testFromRealLife1() = runTest {
eval("""
import lyng.decimal
var X = 42.d
X += 11
assertEquals(53.d, X)
""")
}
}

View File

@ -152,4 +152,20 @@ class MatrixModuleTest {
""".trimIndent()
)
}
@Test
fun matrixAndVectorMustBeObjs() = runTest {
eval("""
import lyng.matrix
val matrixValue = matrix([[1, 2], [3, 4]])
val vectorValue = vector([1, 2, 3])
assert(matrixValue is Matrix)
assertEquals(Matrix, matrixValue::class)
assert(vectorValue is Vector)
assertEquals(Vector, vectorValue::class)
""".trimIndent())
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
class RandomModuleTest {
@Test
fun seededRandomMustBeObj() = runTest {
eval("""
val rng = Random.seeded(12345)
assert(rng is SeededRandom)
assertEquals(SeededRandom, rng::class)
""".trimIndent())
}
}

View File

@ -77,10 +77,17 @@ extern class MapEntry<K,V> : Array<Object> {
}
// Built-in math helpers (implemented in host runtime).
extern fun abs(x: Object): Real
extern fun ln(x: Object): Real
extern fun pow(x: Object, y: Object): Real
extern fun sqrt(x: Object): Real
//
// Decimal note:
// - these helpers accept `BigDecimal` values too
// - `abs`, `floor`, `ceil`, `round`, and `pow(x, y)` with integral `y` keep decimal arithmetic
// - the remaining decimal cases currently use a temporary bridge:
// `BigDecimal -> Real -> host math -> BigDecimal`
// - this is temporary and will be replaced with dedicated decimal implementations
extern fun abs(x: Object): Object
extern fun ln(x: Object): Object
extern fun pow(x: Object, y: Object): Object
extern fun sqrt(x: Object): Object
extern fun clamp<T>(value: T, range: Range<T>): T
class SeededRandom {

View File

@ -0,0 +1,36 @@
# Decimal Math TODO
These stdlib math helpers currently accept `BigDecimal`, but some still use the temporary compatibility path
`BigDecimal -> Real -> host math -> BigDecimal`
instead of a native decimal implementation.
## Still Using The Temporary Real Bridge
- `sin(x)`
- `cos(x)`
- `tan(x)`
- `asin(x)`
- `acos(x)`
- `atan(x)`
- `sinh(x)`
- `cosh(x)`
- `tanh(x)`
- `asinh(x)`
- `acosh(x)`
- `atanh(x)`
- `exp(x)`
- `ln(x)`
- `log10(x)`
- `log2(x)`
- `sqrt(x)`
- `pow(x, y)` when the decimal case is not reducible to an integral exponent
## Already Native For Decimal
- `abs(x)`
- `floor(x)`
- `ceil(x)`
- `round(x)`
- `pow(x, y)` with integral exponent `y`