fixed some issues with Decimals
This commit is contained in:
parent
e55d9c835a
commit
c7c333b71a
@ -223,6 +223,50 @@ assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (
|
|||||||
|
|
||||||
## Recommended Usage Rules
|
## 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:
|
If you care about exact decimal source text:
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
|
|||||||
@ -15,6 +15,10 @@ 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`.
|
||||||
|
- 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
|
## 3. Core Global Constants/Types
|
||||||
- Values: `Unset`, `π`.
|
- Values: `Unset`, `π`.
|
||||||
|
|||||||
26
docs/math.md
26
docs/math.md
@ -60,8 +60,13 @@ but:
|
|||||||
|
|
||||||
## Round and range
|
## Round and range
|
||||||
|
|
||||||
The following functions return its argument if it is `Int`,
|
The following functions return the argument unchanged if it is `Int`.
|
||||||
or transformed `Real` otherwise.
|
|
||||||
|
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 |
|
| name | description |
|
||||||
|----------------|--------------------------------------------------------|
|
|----------------|--------------------------------------------------------|
|
||||||
@ -72,6 +77,14 @@ or transformed `Real` otherwise.
|
|||||||
|
|
||||||
## Lyng math functions
|
## 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 |
|
| name | meaning |
|
||||||
|-----------|------------------------------------------------------|
|
|-----------|------------------------------------------------------|
|
||||||
| sin(x) | sine |
|
| sin(x) | sine |
|
||||||
@ -91,7 +104,7 @@ or transformed `Real` otherwise.
|
|||||||
| 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, 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 |
|
| clamp(x, range) | limit x to be inside range boundaries |
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
@ -104,6 +117,13 @@ For example:
|
|||||||
assert( abs(-1) is Int)
|
assert( abs(-1) is Int)
|
||||||
assert( abs(-2.21) == 2.21 )
|
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:
|
// clamp() limits value to the range:
|
||||||
assert( clamp(15, 0..10) == 10 )
|
assert( clamp(15, 0..10) == 10 )
|
||||||
assert( clamp(-5, 0..10) == 0 )
|
assert( clamp(-5, 0..10) == 0 )
|
||||||
|
|||||||
@ -252,6 +252,44 @@ class Script(
|
|||||||
|
|
||||||
companion object {
|
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
|
* 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
|
* suspended as first time invocation requires compilation of standard library or other
|
||||||
@ -289,87 +327,81 @@ class Script(
|
|||||||
}
|
}
|
||||||
addFn("floor") {
|
addFn("floor") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
(if (x is ObjInt) x
|
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
|
||||||
else ObjReal(floor(x.toDouble())))
|
|
||||||
}
|
}
|
||||||
addFn("ceil") {
|
addFn("ceil") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
(if (x is ObjInt) x
|
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
|
||||||
else ObjReal(ceil(x.toDouble())))
|
|
||||||
}
|
}
|
||||||
addFn("round") {
|
addFn("round") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
(if (x is ObjInt) x
|
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
|
||||||
else ObjReal(round(x.toDouble())))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("sin") {
|
addFn("sin") {
|
||||||
ObjReal(sin(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
|
||||||
}
|
}
|
||||||
addFn("cos") {
|
addFn("cos") {
|
||||||
ObjReal(cos(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos)
|
||||||
}
|
}
|
||||||
addFn("tan") {
|
addFn("tan") {
|
||||||
ObjReal(tan(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan)
|
||||||
}
|
}
|
||||||
addFn("asin") {
|
addFn("asin") {
|
||||||
ObjReal(asin(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin)
|
||||||
}
|
}
|
||||||
addFn("acos") {
|
addFn("acos") {
|
||||||
ObjReal(acos(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos)
|
||||||
}
|
}
|
||||||
addFn("atan") {
|
addFn("atan") {
|
||||||
ObjReal(atan(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("sinh") {
|
addFn("sinh") {
|
||||||
ObjReal(sinh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh)
|
||||||
}
|
}
|
||||||
addFn("cosh") {
|
addFn("cosh") {
|
||||||
ObjReal(cosh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh)
|
||||||
}
|
}
|
||||||
addFn("tanh") {
|
addFn("tanh") {
|
||||||
ObjReal(tanh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh)
|
||||||
}
|
}
|
||||||
addFn("asinh") {
|
addFn("asinh") {
|
||||||
ObjReal(asinh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh)
|
||||||
}
|
}
|
||||||
addFn("acosh") {
|
addFn("acosh") {
|
||||||
ObjReal(acosh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh)
|
||||||
}
|
}
|
||||||
addFn("atanh") {
|
addFn("atanh") {
|
||||||
ObjReal(atanh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("exp") {
|
addFn("exp") {
|
||||||
ObjReal(exp(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp)
|
||||||
}
|
}
|
||||||
addFn("ln") {
|
addFn("ln") {
|
||||||
ObjReal(ln(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("log10") {
|
addFn("log10") {
|
||||||
ObjReal(log10(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("log2") {
|
addFn("log2") {
|
||||||
ObjReal(log2(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFn("pow") {
|
addFn("pow") {
|
||||||
requireExactCount(2)
|
requireExactCount(2)
|
||||||
ObjReal(
|
decimalAwarePow(args[0], args[1])
|
||||||
(args[0].toDouble()).pow(args[1].toDouble())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
addFn("sqrt") {
|
addFn("sqrt") {
|
||||||
ObjReal(
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt)
|
||||||
sqrt(args.firstAndOnly().toDouble())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
addFn("abs") {
|
addFn("abs") {
|
||||||
val x = args.firstAndOnly()
|
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(
|
addFnDoc(
|
||||||
@ -629,78 +661,75 @@ class Script(
|
|||||||
)
|
)
|
||||||
ensureFn("floor") {
|
ensureFn("floor") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
if (x is ObjInt) x else ObjReal(floor(x.toDouble()))
|
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactFloor, ::floor)
|
||||||
}
|
}
|
||||||
ensureFn("ceil") {
|
ensureFn("ceil") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
if (x is ObjInt) x else ObjReal(ceil(x.toDouble()))
|
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactCeil, ::ceil)
|
||||||
}
|
}
|
||||||
ensureFn("round") {
|
ensureFn("round") {
|
||||||
val x = args.firstAndOnly()
|
val x = args.firstAndOnly()
|
||||||
if (x is ObjInt) x else ObjReal(round(x.toDouble()))
|
decimalAwareRoundLike(x, ObjBigDecimalSupport::exactRound, ::round)
|
||||||
}
|
}
|
||||||
ensureFn("sin") {
|
ensureFn("sin") {
|
||||||
ObjReal(sin(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
|
||||||
}
|
}
|
||||||
ensureFn("cos") {
|
ensureFn("cos") {
|
||||||
ObjReal(cos(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos)
|
||||||
}
|
}
|
||||||
ensureFn("tan") {
|
ensureFn("tan") {
|
||||||
ObjReal(tan(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan)
|
||||||
}
|
}
|
||||||
ensureFn("asin") {
|
ensureFn("asin") {
|
||||||
ObjReal(asin(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin)
|
||||||
}
|
}
|
||||||
ensureFn("acos") {
|
ensureFn("acos") {
|
||||||
ObjReal(acos(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos)
|
||||||
}
|
}
|
||||||
ensureFn("atan") {
|
ensureFn("atan") {
|
||||||
ObjReal(atan(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan)
|
||||||
}
|
}
|
||||||
ensureFn("sinh") {
|
ensureFn("sinh") {
|
||||||
ObjReal(sinh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh)
|
||||||
}
|
}
|
||||||
ensureFn("cosh") {
|
ensureFn("cosh") {
|
||||||
ObjReal(cosh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh)
|
||||||
}
|
}
|
||||||
ensureFn("tanh") {
|
ensureFn("tanh") {
|
||||||
ObjReal(tanh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh)
|
||||||
}
|
}
|
||||||
ensureFn("asinh") {
|
ensureFn("asinh") {
|
||||||
ObjReal(asinh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh)
|
||||||
}
|
}
|
||||||
ensureFn("acosh") {
|
ensureFn("acosh") {
|
||||||
ObjReal(acosh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh)
|
||||||
}
|
}
|
||||||
ensureFn("atanh") {
|
ensureFn("atanh") {
|
||||||
ObjReal(atanh(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh)
|
||||||
}
|
}
|
||||||
ensureFn("exp") {
|
ensureFn("exp") {
|
||||||
ObjReal(exp(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp)
|
||||||
}
|
}
|
||||||
ensureFn("ln") {
|
ensureFn("ln") {
|
||||||
ObjReal(ln(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln)
|
||||||
}
|
}
|
||||||
ensureFn("log10") {
|
ensureFn("log10") {
|
||||||
ObjReal(log10(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10)
|
||||||
}
|
}
|
||||||
ensureFn("log2") {
|
ensureFn("log2") {
|
||||||
ObjReal(log2(args.firstAndOnly().toDouble()))
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2)
|
||||||
}
|
}
|
||||||
ensureFn("pow") {
|
ensureFn("pow") {
|
||||||
requireExactCount(2)
|
requireExactCount(2)
|
||||||
ObjReal(
|
decimalAwarePow(args[0], args[1])
|
||||||
(args[0].toDouble()).pow(args[1].toDouble())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
ensureFn("sqrt") {
|
ensureFn("sqrt") {
|
||||||
ObjReal(
|
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt)
|
||||||
sqrt(args.firstAndOnly().toDouble())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
ensureFn("abs") {
|
ensureFn("abs") {
|
||||||
val x = args.firstAndOnly()
|
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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).",
|
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")))
|
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.", 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.", 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.", 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
|
// 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.", 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).", 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.", 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.", 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
|
// Power/roots and absolute value
|
||||||
mod.funDoc(
|
mod.funDoc(
|
||||||
name = "pow",
|
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")))
|
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`.",
|
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")))
|
params = listOf(ParamDoc("x", type("lyng.Number")))
|
||||||
)
|
)
|
||||||
mod.funDoc(
|
mod.funDoc(
|
||||||
name = "abs",
|
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")))
|
params = listOf(ParamDoc("x", type("lyng.Number")))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -128,6 +128,37 @@ object ObjBigDecimalSupport {
|
|||||||
registerInterop(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 {
|
private fun valueOf(obj: Obj): IonBigDecimal {
|
||||||
val instance = obj as? ObjInstance ?: error("BigDecimal receiver must be an object instance")
|
val instance = obj as? ObjInstance ?: error("BigDecimal receiver must be an object instance")
|
||||||
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
|
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
|
||||||
@ -160,6 +191,12 @@ object ObjBigDecimalSupport {
|
|||||||
return instance
|
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) {
|
private fun coerceArg(scope: Scope, value: Obj): IonBigDecimal = when (value) {
|
||||||
is ObjInt -> IonBigDecimal.fromLongAsSignificand(value.value)
|
is ObjInt -> IonBigDecimal.fromLongAsSignificand(value.value)
|
||||||
is ObjReal -> IonBigDecimal.fromDouble(value.value, realConversionMode)
|
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) {
|
private fun registerBuiltinConversions(decimalClass: ObjClass) {
|
||||||
ObjInt.type.addPropertyDoc(
|
ObjInt.type.addPropertyDoc(
|
||||||
name = "d",
|
name = "d",
|
||||||
|
|||||||
@ -146,4 +146,84 @@ class BigDecimalModuleTest {
|
|||||||
assertEquals("0.2", s0.toString())
|
assertEquals("0.2", s0.toString())
|
||||||
""".trimIndent())
|
""".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)
|
||||||
|
""")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -152,4 +152,20 @@ class MatrixModuleTest {
|
|||||||
""".trimIndent()
|
""".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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -77,10 +77,17 @@ extern class MapEntry<K,V> : Array<Object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Built-in math helpers (implemented in host runtime).
|
// Built-in math helpers (implemented in host runtime).
|
||||||
extern fun abs(x: Object): Real
|
//
|
||||||
extern fun ln(x: Object): Real
|
// Decimal note:
|
||||||
extern fun pow(x: Object, y: Object): Real
|
// - these helpers accept `BigDecimal` values too
|
||||||
extern fun sqrt(x: Object): Real
|
// - `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
|
extern fun clamp<T>(value: T, range: Range<T>): T
|
||||||
|
|
||||||
class SeededRandom {
|
class SeededRandom {
|
||||||
|
|||||||
36
notes/decimal_math_todo.md
Normal file
36
notes/decimal_math_todo.md
Normal 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`
|
||||||
Loading…
x
Reference in New Issue
Block a user