Add decimal module and operator interop registry
This commit is contained in:
parent
4269310beb
commit
580256d520
250
docs/Decimal.md
Normal file
250
docs/Decimal.md
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
# Decimal (`lyng.decimal`)
|
||||||
|
|
||||||
|
`lyng.decimal` adds an arbitrary-precision decimal type to Lyng as a normal library module.
|
||||||
|
|
||||||
|
Import it when you need decimal arithmetic that should not inherit `Real`'s binary floating-point behavior:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
```
|
||||||
|
|
||||||
|
## What `BigDecimal` Is For
|
||||||
|
|
||||||
|
Use `BigDecimal` when values are fundamentally decimal:
|
||||||
|
|
||||||
|
- money
|
||||||
|
- human-entered quantities
|
||||||
|
- exact decimal text
|
||||||
|
- predictable decimal rounding
|
||||||
|
- user-facing formatting and tests
|
||||||
|
|
||||||
|
Do not use it just because a number has a fractional part. `Real` is still the right type for ordinary double-precision numeric work.
|
||||||
|
|
||||||
|
## Creating Decimal Values
|
||||||
|
|
||||||
|
There are three supported conversions:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
val a = 1.d
|
||||||
|
val b = 2.2.d
|
||||||
|
val c = "2.2".d
|
||||||
|
|
||||||
|
assertEquals("1", a.toStringExpanded())
|
||||||
|
assertEquals("2.2", b.toStringExpanded())
|
||||||
|
assertEquals("2.2", c.toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
The three forms mean different things:
|
||||||
|
|
||||||
|
- `1.d`: convert `Int -> BigDecimal`
|
||||||
|
- `2.2.d`: convert `Real -> BigDecimal`
|
||||||
|
- `"2.2".d`: parse exact decimal text
|
||||||
|
|
||||||
|
That distinction is intentional.
|
||||||
|
|
||||||
|
### `Real.d` vs `"..." .d`
|
||||||
|
|
||||||
|
`Real.d` preserves the current `Real` value. It does not pretend the source code was exact decimal text.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded())
|
||||||
|
assertEquals("0.3", "0.3".d.toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
This follows the "minimal confusion" rule:
|
||||||
|
|
||||||
|
- if you start from a `Real`, you get a decimal representation of that `Real`
|
||||||
|
- if you want exact decimal source text, use a `String`
|
||||||
|
|
||||||
|
## Factory Functions
|
||||||
|
|
||||||
|
The explicit factory methods are:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
BigDecimal.fromInt(10)
|
||||||
|
BigDecimal.fromReal(2.5)
|
||||||
|
BigDecimal.fromString("12.34")
|
||||||
|
```
|
||||||
|
|
||||||
|
These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code.
|
||||||
|
|
||||||
|
## Core Operations
|
||||||
|
|
||||||
|
`BigDecimal` supports:
|
||||||
|
|
||||||
|
- `+`
|
||||||
|
- `-`
|
||||||
|
- `*`
|
||||||
|
- `/`
|
||||||
|
- `%`
|
||||||
|
- unary `-`
|
||||||
|
- comparison operators
|
||||||
|
- equality operators
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("3.75", ("1.5".d + "2.25".d).toStringExpanded())
|
||||||
|
assertEquals("1.25", ("2.5".d - "1.25".d).toStringExpanded())
|
||||||
|
assertEquals("3.0", ("1.5".d * 2.d).toStringExpanded())
|
||||||
|
assertEquals("0.5", (1.d / 2.d).toStringExpanded())
|
||||||
|
assert("2.0".d > "1.5".d)
|
||||||
|
assert("2.0".d == 2.d)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interoperability With `Int` and `Real`
|
||||||
|
|
||||||
|
The decimal module registers mixed-operand operator bridges so both sides read naturally:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals(3.d, 1 + 2.d)
|
||||||
|
assertEquals(3.d, 2.d + 1)
|
||||||
|
assertEquals(1.5.d, 1.d + 0.5)
|
||||||
|
assertEquals(1.5.d, 0.5 + 1.d)
|
||||||
|
assert(2 == 2.d)
|
||||||
|
assert(3 > 2.d)
|
||||||
|
```
|
||||||
|
|
||||||
|
Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + BigDecimal` and `Real + BigDecimal`.
|
||||||
|
|
||||||
|
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
||||||
|
|
||||||
|
## String Representation
|
||||||
|
|
||||||
|
Use `toStringExpanded()` when you want plain decimal output without scientific notation:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("12.34", "12.34".d.toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the recommended representation for:
|
||||||
|
|
||||||
|
- tests
|
||||||
|
- user-visible diagnostics
|
||||||
|
- decimal formatting checks
|
||||||
|
|
||||||
|
## Conversions Back To Built-ins
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals(2, "2.9".d.toInt())
|
||||||
|
assertEquals(2.9, "2.9".d.toReal())
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `toReal()` only when you are willing to return to binary floating-point semantics.
|
||||||
|
|
||||||
|
## Division Context
|
||||||
|
|
||||||
|
Division is the operation where precision and rounding matter most.
|
||||||
|
|
||||||
|
By default, decimal division uses:
|
||||||
|
|
||||||
|
- precision: `34` significant digits
|
||||||
|
- rounding: `HalfEven`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||||
|
assertEquals("0.6666666666666666666666666666666667", ("2".d / 3.d).toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
## `withDecimalContext(...)`
|
||||||
|
|
||||||
|
Use `withDecimalContext(...)` to override decimal division rules inside a block:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"0.3333333333",
|
||||||
|
withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass an explicit context object:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
val ctx = DecimalContext(6, DecimalRounding.HalfAwayFromZero)
|
||||||
|
assertEquals("0.666667", withDecimalContext(ctx) { ("2".d / 3.d).toStringExpanded() })
|
||||||
|
```
|
||||||
|
|
||||||
|
The context is dynamic and local to the block. After the block exits, the previous context is restored.
|
||||||
|
|
||||||
|
## Rounding Modes
|
||||||
|
|
||||||
|
Available rounding modes:
|
||||||
|
|
||||||
|
- `HalfEven`
|
||||||
|
- `HalfAwayFromZero`
|
||||||
|
- `HalfTowardsZero`
|
||||||
|
- `Ceiling`
|
||||||
|
- `Floor`
|
||||||
|
- `AwayFromZero`
|
||||||
|
- `TowardsZero`
|
||||||
|
|
||||||
|
Tie example at precision `2`:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
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.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.Floor) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
```
|
||||||
|
|
||||||
|
Negative values follow the same named policy in the obvious direction:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommended Usage Rules
|
||||||
|
|
||||||
|
If you care about exact decimal source text:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
"12.34".d
|
||||||
|
```
|
||||||
|
|
||||||
|
If you intentionally convert an existing binary floating-point value:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
someReal.d
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want local control over division:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
withDecimalContext(precision, rounding) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want custom mixed operators for your own type, follow the same pattern as Decimal and use the operator registry:
|
||||||
|
|
||||||
|
- define the operators on your own class
|
||||||
|
- choose a common class
|
||||||
|
- register mixed operand bridges
|
||||||
|
|
||||||
|
See [OperatorInterop.md](OperatorInterop.md).
|
||||||
309
docs/OperatorInterop.md
Normal file
309
docs/OperatorInterop.md
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
# Operator Interop Registry
|
||||||
|
|
||||||
|
`lyng.operators` provides a runtime registry for mixed-class binary operators.
|
||||||
|
|
||||||
|
Import it when you want expressions such as:
|
||||||
|
|
||||||
|
- `1 + MyType(...)`
|
||||||
|
- `2 < MyType(...)`
|
||||||
|
- `3 == MyType(...)`
|
||||||
|
|
||||||
|
to work without modifying the built-in `Int` or `Real` classes.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.operators
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
If your class defines:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Amount(val value: Int) {
|
||||||
|
fun plus(other: Amount) = Amount(value + other.value)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
then:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
Amount(1) + Amount(2)
|
||||||
|
```
|
||||||
|
|
||||||
|
works, because the left operand already knows how to add another `Amount`.
|
||||||
|
|
||||||
|
But:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
1 + Amount(2)
|
||||||
|
```
|
||||||
|
|
||||||
|
does not naturally work, because `Int` has not been rewritten to know about `Amount`.
|
||||||
|
|
||||||
|
The operator interop registry solves exactly that problem.
|
||||||
|
|
||||||
|
## Mental Model
|
||||||
|
|
||||||
|
Registration describes a mixed pair:
|
||||||
|
|
||||||
|
- left class `L`
|
||||||
|
- right class `R`
|
||||||
|
- common class `C`
|
||||||
|
|
||||||
|
When Lyng sees `L op R`, it:
|
||||||
|
|
||||||
|
1. converts `L -> C`
|
||||||
|
2. converts `R -> C`
|
||||||
|
3. evaluates the operator as `C op C`
|
||||||
|
|
||||||
|
So the registry is a bridge, not a separate arithmetic engine.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
OperatorInterop.register(
|
||||||
|
leftClass,
|
||||||
|
rightClass,
|
||||||
|
commonClass,
|
||||||
|
operators,
|
||||||
|
leftToCommon,
|
||||||
|
rightToCommon
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
- `leftClass`: original left operand class
|
||||||
|
- `rightClass`: original right operand class
|
||||||
|
- `commonClass`: class that will actually execute the operator methods
|
||||||
|
- `operators`: list of operators enabled for this pair
|
||||||
|
- `leftToCommon`: conversion from left operand to common class
|
||||||
|
- `rightToCommon`: conversion from right operand to common class
|
||||||
|
|
||||||
|
## Supported Operators
|
||||||
|
|
||||||
|
`BinaryOperator` values:
|
||||||
|
|
||||||
|
- `Plus`
|
||||||
|
- `Minus`
|
||||||
|
- `Mul`
|
||||||
|
- `Div`
|
||||||
|
- `Mod`
|
||||||
|
- `Compare`
|
||||||
|
- `Equals`
|
||||||
|
|
||||||
|
Meaning:
|
||||||
|
|
||||||
|
- `Compare` enables `<`, `<=`, `>`, `>=`, and `<=>`
|
||||||
|
- `Equals` enables `==` and `!=`
|
||||||
|
|
||||||
|
## Minimal Working Example
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
package test.decimalbox
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class DecimalBox(val value: Int) {
|
||||||
|
fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
fun minus(other: DecimalBox) = DecimalBox(value - other.value)
|
||||||
|
fun mul(other: DecimalBox) = DecimalBox(value * other.value)
|
||||||
|
fun div(other: DecimalBox) = DecimalBox(value / other.value)
|
||||||
|
fun mod(other: DecimalBox) = DecimalBox(value % other.value)
|
||||||
|
fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
DecimalBox,
|
||||||
|
DecimalBox,
|
||||||
|
[
|
||||||
|
BinaryOperator.Plus,
|
||||||
|
BinaryOperator.Minus,
|
||||||
|
BinaryOperator.Mul,
|
||||||
|
BinaryOperator.Div,
|
||||||
|
BinaryOperator.Mod,
|
||||||
|
BinaryOperator.Compare,
|
||||||
|
BinaryOperator.Equals
|
||||||
|
],
|
||||||
|
{ x: Int -> DecimalBox(x) },
|
||||||
|
{ x: DecimalBox -> x }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import test.decimalbox
|
||||||
|
|
||||||
|
assertEquals(DecimalBox(3), 1 + DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 3 - DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(8), 4 * DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(4), 8 / DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 7 % DecimalBox(2))
|
||||||
|
assert(1 < DecimalBox(2))
|
||||||
|
assert(2 <= DecimalBox(2))
|
||||||
|
assert(3 > DecimalBox(2))
|
||||||
|
assert(2 == DecimalBox(2))
|
||||||
|
assert(2 != DecimalBox(3))
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Decimal Uses It
|
||||||
|
|
||||||
|
`lyng.decimal` uses this same mechanism so that:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
1 + 2.d
|
||||||
|
0.5 + 1.d
|
||||||
|
2 == 2.d
|
||||||
|
3 > 2.d
|
||||||
|
```
|
||||||
|
|
||||||
|
work naturally even though `Int` and `Real` themselves were not edited to know `BigDecimal`.
|
||||||
|
|
||||||
|
The shape is:
|
||||||
|
|
||||||
|
- `leftClass = Int` or `Real`
|
||||||
|
- `rightClass = BigDecimal`
|
||||||
|
- `commonClass = BigDecimal`
|
||||||
|
- convert built-ins into `BigDecimal`
|
||||||
|
- leave `BigDecimal` values unchanged
|
||||||
|
|
||||||
|
## Step-By-Step Pattern For Your Own Type
|
||||||
|
|
||||||
|
### 1. Pick the common class
|
||||||
|
|
||||||
|
Choose one class that will be the actual arithmetic domain.
|
||||||
|
|
||||||
|
For numeric-like types, that is usually your own class:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Rational(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Implement operators on that class
|
||||||
|
|
||||||
|
The common class should define the operations you plan to register.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Rational(val num: Int, val den: Int) {
|
||||||
|
fun plus(other: Rational) = Rational(num * other.den + other.num * den, den * other.den)
|
||||||
|
fun minus(other: Rational) = Rational(num * other.den - other.num * den, den * other.den)
|
||||||
|
fun mul(other: Rational) = Rational(num * other.num, den * other.den)
|
||||||
|
fun div(other: Rational) = Rational(num * other.den, den * other.num)
|
||||||
|
fun compareTo(other: Rational) = (num * other.den) <=> (other.num * den)
|
||||||
|
|
||||||
|
static fun fromInt(value: Int) = Rational(value, 1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Register the mixed pair
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
Rational,
|
||||||
|
Rational,
|
||||||
|
[
|
||||||
|
BinaryOperator.Plus,
|
||||||
|
BinaryOperator.Minus,
|
||||||
|
BinaryOperator.Mul,
|
||||||
|
BinaryOperator.Div,
|
||||||
|
BinaryOperator.Compare,
|
||||||
|
BinaryOperator.Equals
|
||||||
|
],
|
||||||
|
{ x: Int -> Rational.fromInt(x) },
|
||||||
|
{ x: Rational -> x }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Use it
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
assertEquals(Rational(3, 2), 1 + Rational(1, 2))
|
||||||
|
assert(Rational(3, 2) == Rational(3, 2))
|
||||||
|
assert(2 > Rational(3, 2))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registering More Than One Built-in Type
|
||||||
|
|
||||||
|
If you want both `Int + MyType` and `Real + MyType`, register both pairs explicitly:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
MyType,
|
||||||
|
MyType,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> MyType.fromInt(x) },
|
||||||
|
{ x: MyType -> x }
|
||||||
|
)
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Real,
|
||||||
|
MyType,
|
||||||
|
MyType,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Real -> MyType.fromReal(x) },
|
||||||
|
{ x: MyType -> x }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each mixed pair is independent.
|
||||||
|
|
||||||
|
## Pure Lyng Registration
|
||||||
|
|
||||||
|
This mechanism is intentionally useful from pure Lyng code, not only from Kotlin-backed modules.
|
||||||
|
|
||||||
|
That means you can:
|
||||||
|
|
||||||
|
- declare a class in Lyng
|
||||||
|
- define its operators in Lyng
|
||||||
|
- register mixed operand bridges in Lyng
|
||||||
|
|
||||||
|
without touching compiler internals.
|
||||||
|
|
||||||
|
## Where To Register
|
||||||
|
|
||||||
|
Register once during module initialization.
|
||||||
|
|
||||||
|
Top-level module code is a good place:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
package my.rational
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class Rational(...)
|
||||||
|
|
||||||
|
OperatorInterop.register(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
That keeps registration close to the type declaration and makes importing the module enough to activate the interop.
|
||||||
|
|
||||||
|
## What Registration Does Not Do
|
||||||
|
|
||||||
|
The registry does not:
|
||||||
|
|
||||||
|
- invent operators your common class does not implement
|
||||||
|
- change the original `Int`, `Real`, or other built-ins
|
||||||
|
- automatically cover every class pair
|
||||||
|
- replace normal method overload resolution when the left-hand class already knows what to do
|
||||||
|
|
||||||
|
It only teaches Lyng how to bridge a specific mixed pair into a common class for the listed operators.
|
||||||
|
|
||||||
|
## Recommended Design Rules
|
||||||
|
|
||||||
|
If you want interop to feel natural:
|
||||||
|
|
||||||
|
- choose one obvious common class
|
||||||
|
- make conversions explicit and unsurprising
|
||||||
|
- implement `compareTo` if you want ordering operators
|
||||||
|
- register `Equals` whenever mixed equality should work
|
||||||
|
- keep the registered operator list minimal and accurate
|
||||||
|
|
||||||
|
For decimal-like semantics, also read [Decimal.md](Decimal.md).
|
||||||
@ -5,6 +5,73 @@ For a programmer-focused migration summary, see `docs/whats_new_1_5.md`.
|
|||||||
|
|
||||||
## Language Features
|
## Language Features
|
||||||
|
|
||||||
|
### Decimal Arithmetic Module (`lyng.decimal`)
|
||||||
|
Lyng now ships a first-class decimal module built as a regular extension library rather than a deep core special case.
|
||||||
|
|
||||||
|
It provides:
|
||||||
|
|
||||||
|
- `BigDecimal`
|
||||||
|
- convenient `.d` conversions from `Int`, `Real`, and `String`
|
||||||
|
- mixed arithmetic with `Int` and `Real`
|
||||||
|
- local division precision and rounding control via `withDecimalContext(...)`
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("3", (1 + 2.d).toStringExpanded())
|
||||||
|
assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded())
|
||||||
|
assertEquals("0.3", "0.3".d.toStringExpanded())
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"0.3333333333",
|
||||||
|
withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The distinction between `Real -> Decimal` and exact decimal parsing is explicit by design:
|
||||||
|
|
||||||
|
- `2.2.d` converts the current `Real` value
|
||||||
|
- `"2.2".d` parses exact decimal text
|
||||||
|
|
||||||
|
See [Decimal](Decimal.md).
|
||||||
|
|
||||||
|
### Binary Operator Interop Registry
|
||||||
|
Lyng now provides a general mechanism for mixed binary operators through `lyng.operators`.
|
||||||
|
|
||||||
|
This solves cases like:
|
||||||
|
|
||||||
|
- `Int + MyType`
|
||||||
|
- `Real < MyType`
|
||||||
|
- `Int == MyType`
|
||||||
|
|
||||||
|
without requiring changes to built-in classes.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class DecimalBox(val value: Int) {
|
||||||
|
fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
DecimalBox,
|
||||||
|
DecimalBox,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> DecimalBox(x) },
|
||||||
|
{ x: DecimalBox -> x }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(DecimalBox(3), 1 + DecimalBox(2))
|
||||||
|
assert(1 < DecimalBox(2))
|
||||||
|
assert(2 == DecimalBox(2))
|
||||||
|
```
|
||||||
|
|
||||||
|
`lyng.decimal` uses this same mechanism internally to interoperate with `Int` and `Real`.
|
||||||
|
|
||||||
|
See [Operator Interop Registry](OperatorInterop.md).
|
||||||
|
|
||||||
### Class Properties with Accessors
|
### Class Properties with Accessors
|
||||||
Classes now support properties with custom `get()` and `set()` accessors. Properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
Classes now support properties with custom `get()` and `set()` accessors. Properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ android-compileSdk = "34"
|
|||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
kotlinx-datetime = "0.6.1"
|
kotlinx-datetime = "0.6.1"
|
||||||
mp_bintools = "0.3.2"
|
mp_bintools = "0.3.2"
|
||||||
|
ionspin-bignum = "0.3.10"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
okioVersion = "3.10.2"
|
||||||
compiler = "3.2.0-alpha11"
|
compiler = "3.2.0-alpha11"
|
||||||
@ -22,6 +23,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t
|
|||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
||||||
|
ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" }
|
||||||
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||||
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
||||||
|
|||||||
@ -101,6 +101,7 @@ kotlin {
|
|||||||
//put your multiplatform dependencies here
|
//put your multiplatform dependencies here
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
api(libs.mp.bintools)
|
api(libs.mp.bintools)
|
||||||
|
implementation(libs.ionspin.bignum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
|
|||||||
@ -4667,6 +4667,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
is MethodCallRef -> methodReturnTypeDeclByRef[ref]
|
is MethodCallRef -> methodReturnTypeDeclByRef[ref]
|
||||||
is CallRef -> callReturnTypeDeclByRef[ref]
|
is CallRef -> callReturnTypeDeclByRef[ref]
|
||||||
|
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
|
||||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -4730,6 +4731,7 @@ class Compiler(
|
|||||||
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||||
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||||
is CallRef -> inferCallReturnClass(ref)
|
is CallRef -> inferCallReturnClass(ref)
|
||||||
|
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
||||||
is FieldRef -> {
|
is FieldRef -> {
|
||||||
val targetClass = resolveReceiverClassForMember(ref.target)
|
val targetClass = resolveReceiverClassForMember(ref.target)
|
||||||
inferFieldReturnClass(targetClass, ref.name)
|
inferFieldReturnClass(targetClass, ref.name)
|
||||||
@ -4744,10 +4746,105 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun typeDeclOfClass(objClass: ObjClass): TypeDecl = TypeDecl.Simple(objClass.className, false)
|
||||||
|
|
||||||
|
private fun binaryOpMethodName(op: BinOp): String? = when (op) {
|
||||||
|
BinOp.PLUS -> "plus"
|
||||||
|
BinOp.MINUS -> "minus"
|
||||||
|
BinOp.STAR -> "mul"
|
||||||
|
BinOp.SLASH -> "div"
|
||||||
|
BinOp.PERCENT -> "mod"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interopOperatorFor(op: BinOp): InteropOperator? = when (op) {
|
||||||
|
BinOp.PLUS -> InteropOperator.Plus
|
||||||
|
BinOp.MINUS -> InteropOperator.Minus
|
||||||
|
BinOp.STAR -> InteropOperator.Mul
|
||||||
|
BinOp.SLASH -> InteropOperator.Div
|
||||||
|
BinOp.PERCENT -> InteropOperator.Mod
|
||||||
|
BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE -> InteropOperator.Compare
|
||||||
|
BinOp.EQ, BinOp.NEQ -> InteropOperator.Equals
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sameClassArithmeticFallback(leftClass: ObjClass, rightClass: ObjClass, op: BinOp): TypeDecl? {
|
||||||
|
if (leftClass !== rightClass) return null
|
||||||
|
val methodName = binaryOpMethodName(op) ?: return null
|
||||||
|
if (leftClass.getInstanceMemberOrNull(methodName, includeAbstract = true) == null) return null
|
||||||
|
return typeDeclOfClass(leftClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inferBinaryOpReturnTypeDecl(ref: BinaryOpRef): TypeDecl? {
|
||||||
|
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
|
||||||
|
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
|
||||||
|
val boolType = typeDeclOfClass(ObjBool.type)
|
||||||
|
val intType = typeDeclOfClass(ObjInt.type)
|
||||||
|
val realType = typeDeclOfClass(ObjReal.type)
|
||||||
|
val stringType = typeDeclOfClass(ObjString.type)
|
||||||
|
|
||||||
|
when (ref.op) {
|
||||||
|
BinOp.OR, BinOp.AND,
|
||||||
|
BinOp.EQARROW, BinOp.EQ, BinOp.NEQ, BinOp.REF_EQ, BinOp.REF_NEQ, BinOp.MATCH, BinOp.NOTMATCH,
|
||||||
|
BinOp.LTE, BinOp.LT, BinOp.GTE, BinOp.GT,
|
||||||
|
BinOp.IN, BinOp.NOTIN,
|
||||||
|
BinOp.IS, BinOp.NOTIS -> return boolType
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftClass == null || rightClass == null) return null
|
||||||
|
|
||||||
|
val leftIsInt = leftClass == ObjInt.type
|
||||||
|
val rightIsInt = rightClass == ObjInt.type
|
||||||
|
val leftIsReal = leftClass == ObjReal.type
|
||||||
|
val rightIsReal = rightClass == ObjReal.type
|
||||||
|
val leftIsNumeric = leftIsInt || leftIsReal
|
||||||
|
val rightIsNumeric = rightIsInt || rightIsReal
|
||||||
|
|
||||||
|
return when (ref.op) {
|
||||||
|
BinOp.PLUS -> when {
|
||||||
|
leftIsInt && rightIsInt -> intType
|
||||||
|
leftIsNumeric && rightIsNumeric -> realType
|
||||||
|
leftClass == ObjString.type -> stringType
|
||||||
|
interopOperatorFor(ref.op)?.let {
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, it)
|
||||||
|
} != null -> typeDeclOfClass(
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, interopOperatorFor(ref.op)!!)!!
|
||||||
|
)
|
||||||
|
else -> binaryOpMethodName(ref.op)?.let { classMethodReturnTypeDecl(leftClass, it) }
|
||||||
|
?: sameClassArithmeticFallback(leftClass, rightClass, ref.op)
|
||||||
|
}
|
||||||
|
BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT -> when {
|
||||||
|
leftIsInt && rightIsInt -> intType
|
||||||
|
leftIsNumeric && rightIsNumeric -> realType
|
||||||
|
ref.op == BinOp.STAR && leftClass == ObjString.type && rightIsNumeric -> stringType
|
||||||
|
interopOperatorFor(ref.op)?.let {
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, it)
|
||||||
|
} != null -> typeDeclOfClass(
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, interopOperatorFor(ref.op)!!)!!
|
||||||
|
)
|
||||||
|
else -> binaryOpMethodName(ref.op)?.let { classMethodReturnTypeDecl(leftClass, it) }
|
||||||
|
?: sameClassArithmeticFallback(leftClass, rightClass, ref.op)
|
||||||
|
}
|
||||||
|
BinOp.BAND, BinOp.BOR, BinOp.BXOR, BinOp.SHL, BinOp.SHR ->
|
||||||
|
if (leftIsInt && rightIsInt) intType else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun inferBinaryOpReturnClass(ref: BinaryOpRef): ObjClass? {
|
private fun inferBinaryOpReturnClass(ref: BinaryOpRef): ObjClass? {
|
||||||
|
inferBinaryOpReturnTypeDecl(ref)?.let { declared ->
|
||||||
|
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||||
|
}
|
||||||
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
|
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
|
||||||
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
|
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
|
||||||
if (leftClass == null || rightClass == null) return null
|
if (leftClass == null || rightClass == null) return null
|
||||||
|
interopOperatorFor(ref.op)?.let { op ->
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, op)?.let { return it }
|
||||||
|
}
|
||||||
|
sameClassArithmeticFallback(leftClass, rightClass, ref.op)?.let { declared ->
|
||||||
|
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||||
|
}
|
||||||
return when (ref.op) {
|
return when (ref.op) {
|
||||||
BinOp.PLUS, BinOp.MINUS -> when {
|
BinOp.PLUS, BinOp.MINUS -> when {
|
||||||
leftClass == ObjInstant.type && rightClass == ObjInstant.type && ref.op == BinOp.MINUS -> ObjDuration.type
|
leftClass == ObjInstant.type && rightClass == ObjInstant.type && ref.op == BinOp.MINUS -> ObjDuration.type
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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 net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
|
||||||
|
internal enum class InteropOperator(val memberName: String?) {
|
||||||
|
Plus("plus"),
|
||||||
|
Minus("minus"),
|
||||||
|
Mul("mul"),
|
||||||
|
Div("div"),
|
||||||
|
Mod("mod"),
|
||||||
|
Compare("compareTo"),
|
||||||
|
Equals("equals");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromName(name: String): InteropOperator =
|
||||||
|
entries.firstOrNull { it.name == name }
|
||||||
|
?: throw IllegalArgumentException("unknown interop operator: $name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class InteropRule(
|
||||||
|
val commonClass: ObjClass,
|
||||||
|
val operators: Set<InteropOperator>,
|
||||||
|
val leftToCommon: Obj,
|
||||||
|
val rightToCommon: Obj
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class PromotedOperands(val left: Obj, val right: Obj)
|
||||||
|
|
||||||
|
internal object OperatorInteropRegistry {
|
||||||
|
private val rules = mutableMapOf<Pair<ObjClass, ObjClass>, InteropRule>()
|
||||||
|
|
||||||
|
fun register(
|
||||||
|
leftClass: ObjClass,
|
||||||
|
rightClass: ObjClass,
|
||||||
|
commonClass: ObjClass,
|
||||||
|
operatorNames: List<String>,
|
||||||
|
leftToCommon: Obj,
|
||||||
|
rightToCommon: Obj
|
||||||
|
) {
|
||||||
|
val operators = operatorNames.mapTo(linkedSetOf(), InteropOperator::fromName)
|
||||||
|
rules[leftClass to rightClass] = InteropRule(commonClass, operators, leftToCommon, rightToCommon)
|
||||||
|
if (leftClass !== rightClass) {
|
||||||
|
rules[rightClass to leftClass] = InteropRule(commonClass, operators, rightToCommon, leftToCommon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun promote(scope: Scope, left: Obj, right: Obj, operator: InteropOperator): PromotedOperands? {
|
||||||
|
val leftValue = unwrap(left)
|
||||||
|
val rightValue = unwrap(right)
|
||||||
|
val rule = rules[leftValue.objClass to rightValue.objClass] ?: return null
|
||||||
|
if (operator !in rule.operators) return null
|
||||||
|
val promotedLeft = rule.leftToCommon.invoke(scope, ObjVoid, Arguments(leftValue))
|
||||||
|
val promotedRight = rule.rightToCommon.invoke(scope, ObjVoid, Arguments(rightValue))
|
||||||
|
if (promotedLeft.objClass !== rule.commonClass || promotedRight.objClass !== rule.commonClass) {
|
||||||
|
scope.raiseIllegalState(
|
||||||
|
"Operator interop promotion must return ${rule.commonClass.className}, " +
|
||||||
|
"got ${promotedLeft.objClass.className} and ${promotedRight.objClass.className}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return PromotedOperands(promotedLeft, promotedRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun invokeBinary(scope: Scope, left: Obj, right: Obj, operator: InteropOperator): Obj? {
|
||||||
|
val promoted = promote(scope, left, right, operator) ?: return null
|
||||||
|
val memberName = operator.memberName ?: return null
|
||||||
|
return promoted.left.invokeInstanceMethod(scope, memberName, Arguments(promoted.right))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun invokeCompare(scope: Scope, left: Obj, right: Obj): Int? {
|
||||||
|
val promoted = promote(scope, left, right, InteropOperator.Compare) ?: return null
|
||||||
|
return promoted.left.invokeInstanceMethod(scope, "compareTo", Arguments(promoted.right))
|
||||||
|
.cast<ObjInt>(scope)
|
||||||
|
.value
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commonClassFor(leftClass: ObjClass, rightClass: ObjClass, operator: InteropOperator): ObjClass? {
|
||||||
|
val rule = rules[leftClass to rightClass] ?: return null
|
||||||
|
if (operator !in rule.operators) return null
|
||||||
|
return rule.commonClass
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun unwrap(obj: Obj): Obj = when (obj) {
|
||||||
|
is FrameSlotRef -> obj.read()
|
||||||
|
is RecordSlotRef -> obj.read()
|
||||||
|
else -> obj
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,7 +27,9 @@ import net.sergeych.lyng.bytecode.CmdVm
|
|||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.stdlib_included.decimalLyng
|
||||||
import net.sergeych.lyng.stdlib_included.observableLyng
|
import net.sergeych.lyng.stdlib_included.observableLyng
|
||||||
|
import net.sergeych.lyng.stdlib_included.operatorsLyng
|
||||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||||
import net.sergeych.lynon.ObjLynonClass
|
import net.sergeych.lynon.ObjLynonClass
|
||||||
import net.sergeych.mp_tools.globalDefer
|
import net.sergeych.mp_tools.globalDefer
|
||||||
@ -727,6 +729,36 @@ class Script(
|
|||||||
module.addConst("ChangeRejectionException", ObjChangeRejectionExceptionClass)
|
module.addConst("ChangeRejectionException", ObjChangeRejectionExceptionClass)
|
||||||
module.eval(Source("lyng.observable", observableLyng))
|
module.eval(Source("lyng.observable", observableLyng))
|
||||||
}
|
}
|
||||||
|
addPackage("lyng.operators") { module ->
|
||||||
|
module.eval(Source("lyng.operators", operatorsLyng))
|
||||||
|
module.bindObject("OperatorInterop") {
|
||||||
|
addFun("register") {
|
||||||
|
val leftClass = requiredArg<ObjClass>(0)
|
||||||
|
val rightClass = requiredArg<ObjClass>(1)
|
||||||
|
val commonClass = requiredArg<ObjClass>(2)
|
||||||
|
val operators = requiredArg<ObjList>(3).list.map { value ->
|
||||||
|
val entry = value as? ObjEnumEntry
|
||||||
|
?: requireScope().raiseIllegalArgument(
|
||||||
|
"OperatorInterop.register expects BinaryOperator enum entries"
|
||||||
|
)
|
||||||
|
entry.name.value
|
||||||
|
}
|
||||||
|
OperatorInteropRegistry.register(
|
||||||
|
leftClass = leftClass,
|
||||||
|
rightClass = rightClass,
|
||||||
|
commonClass = commonClass,
|
||||||
|
operatorNames = operators,
|
||||||
|
leftToCommon = args[4],
|
||||||
|
rightToCommon = args[5]
|
||||||
|
)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addPackage("lyng.decimal") { module ->
|
||||||
|
module.eval(Source("lyng.decimal", decimalLyng))
|
||||||
|
ObjBigDecimalSupport.bindTo(module)
|
||||||
|
}
|
||||||
addPackage("lyng.buffer") {
|
addPackage("lyng.buffer") {
|
||||||
it.addConstDoc(
|
it.addConstDoc(
|
||||||
name = "Buffer",
|
name = "Buffer",
|
||||||
|
|||||||
@ -25,6 +25,8 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
import kotlinx.serialization.json.JsonNull
|
import kotlinx.serialization.json.JsonNull
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
import net.sergeych.lyng.miniast.ParamDoc
|
import net.sergeych.lyng.miniast.ParamDoc
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
import net.sergeych.lyng.miniast.type
|
import net.sergeych.lyng.miniast.type
|
||||||
@ -175,7 +177,8 @@ open class Obj {
|
|||||||
if (other === this) return 0
|
if (other === this) return 0
|
||||||
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
|
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
|
||||||
return invokeInstanceMethod(scope, "compareTo", Arguments(other)) {
|
return invokeInstanceMethod(scope, "compareTo", Arguments(other)) {
|
||||||
scope.raiseNotImplemented("compareTo for ${objClass.className}")
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { ObjInt.of(it.toLong()) }
|
||||||
|
?: scope.raiseNotImplemented("compareTo for ${objClass.className}")
|
||||||
}.cast<ObjInt>(scope).toInt()
|
}.cast<ObjInt>(scope).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +287,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.plus(scope, otherValue)
|
if (self !== this) return self.plus(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "plus", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "plus", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("plus for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Plus)
|
||||||
|
?: scope.raiseNotImplemented("plus for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,7 +305,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.minus(scope, otherValue)
|
if (self !== this) return self.minus(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "minus", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "minus", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("minus for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Minus)
|
||||||
|
?: scope.raiseNotImplemented("minus for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +329,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.mul(scope, otherValue)
|
if (self !== this) return self.mul(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "mul", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "mul", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("mul for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Mul)
|
||||||
|
?: scope.raiseNotImplemented("mul for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +347,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.div(scope, otherValue)
|
if (self !== this) return self.div(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "div", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "div", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("div for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Div)
|
||||||
|
?: scope.raiseNotImplemented("div for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,7 +365,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.mod(scope, otherValue)
|
if (self !== this) return self.mod(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "mod", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "mod", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("mod for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Mod)
|
||||||
|
?: scope.raiseNotImplemented("mod for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,313 @@
|
|||||||
|
/*
|
||||||
|
* 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.BigDecimal as IonBigDecimal
|
||||||
|
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.Arguments
|
||||||
|
import net.sergeych.lyng.FrameSlotRef
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
|
import net.sergeych.lyng.RecordSlotRef
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.TypeDecl
|
||||||
|
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||||
|
import net.sergeych.lyng.miniast.type
|
||||||
|
import net.sergeych.lyng.requiredArg
|
||||||
|
|
||||||
|
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("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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -364,6 +364,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
onNotFoundResult: (suspend () -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj {
|
): Obj {
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
|
val methodScope = scope.applyClosure(instanceScope)
|
||||||
|
|
||||||
// Fast path for public members when outside any class context
|
// Fast path for public members when outside any class context
|
||||||
if (caller == null) {
|
if (caller == null) {
|
||||||
@ -373,7 +374,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -384,7 +385,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -394,7 +395,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -409,7 +410,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, c)
|
return rec.value.invoke(methodScope, this, args, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -418,7 +419,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, c)
|
return rec.value.invoke(methodScope, this, args, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -427,7 +428,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, c)
|
return rec.value.invoke(methodScope, this, args, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -439,7 +440,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name) ?: objClass
|
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name) ?: objClass
|
||||||
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
|
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
|
||||||
if (canAccessMember(rec.visibility, decl, effectiveCaller, name)) {
|
if (canAccessMember(rec.visibility, decl, effectiveCaller, name)) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -474,7 +475,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(
|
return rec.value.invoke(
|
||||||
instanceScope,
|
methodScope,
|
||||||
this,
|
this,
|
||||||
args,
|
args,
|
||||||
decl
|
decl
|
||||||
@ -586,8 +587,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
if (other !is ObjInstance) return -1
|
if (other !is ObjInstance || other.objClass != objClass) {
|
||||||
if (other.objClass != objClass) return -1
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||||
|
return -1
|
||||||
|
}
|
||||||
for (f in comparableVars) {
|
for (f in comparableVars) {
|
||||||
val a = f.value.value
|
val a = f.value.value
|
||||||
val b = other.instanceScope.objects[f.key]?.value ?: scope.raiseError("Internal error: field ${f.key} not found in other instance")
|
val b = other.instanceScope.objects[f.key]?.value ?: scope.raiseError("Internal error: field ${f.key} not found in other instance")
|
||||||
|
|||||||
@ -19,6 +19,8 @@ package net.sergeych.lyng.obj
|
|||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
@ -54,10 +56,11 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
if (other !is Numeric) return -2
|
|
||||||
return if (other is ObjInt) {
|
return if (other is ObjInt) {
|
||||||
value.compareTo(other.value)
|
value.compareTo(other.value)
|
||||||
} else {
|
} else {
|
||||||
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||||
|
if (other !is Numeric) return -2
|
||||||
doubleValue.compareTo(other.doubleValue)
|
doubleValue.compareTo(other.doubleValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,28 +73,39 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value + other.value)
|
of(this.value + other.value)
|
||||||
else
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus)
|
||||||
|
?:
|
||||||
ObjReal.of(this.doubleValue + other.toDouble())
|
ObjReal.of(this.doubleValue + other.toDouble())
|
||||||
|
|
||||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value - other.value)
|
of(this.value - other.value)
|
||||||
else
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus)
|
||||||
|
?:
|
||||||
ObjReal.of(this.doubleValue - other.toDouble())
|
ObjReal.of(this.doubleValue - other.toDouble())
|
||||||
|
|
||||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) {
|
if (other is ObjInt) {
|
||||||
of(this.value * other.value)
|
of(this.value * other.value)
|
||||||
} else ObjReal.of(this.value * other.toDouble())
|
} else {
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mul)
|
||||||
|
?: ObjReal.of(this.value * other.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value / other.value)
|
of(this.value / other.value)
|
||||||
else ObjReal.of(this.value / other.toDouble())
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Div)
|
||||||
|
?: ObjReal.of(this.value / other.toDouble())
|
||||||
|
|
||||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value % other.value)
|
of(this.value % other.value)
|
||||||
else ObjReal.of(this.value.toDouble() % other.toDouble())
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod)
|
||||||
|
?: ObjReal.of(this.value.toDouble() % other.toDouble())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Numbers are now immutable, so we can't do in-place assignment.
|
* Numbers are now immutable, so we can't do in-place assignment.
|
||||||
|
|||||||
@ -19,6 +19,8 @@ package net.sergeych.lyng.obj
|
|||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.miniast.addConstDoc
|
import net.sergeych.lyng.miniast.addConstDoc
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
@ -40,6 +42,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
override fun byValueCopy(): Obj = this
|
override fun byValueCopy(): Obj = this
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
|
if (other is ObjReal) return value.compareTo(other.value)
|
||||||
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||||
if (other !is Numeric) return -2
|
if (other !is Numeric) return -2
|
||||||
return value.compareTo(other.doubleValue)
|
return value.compareTo(other.doubleValue)
|
||||||
}
|
}
|
||||||
@ -63,19 +67,24 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value + other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus)
|
||||||
|
?: of(this.value + other.toDouble())
|
||||||
|
|
||||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value - other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus)
|
||||||
|
?: of(this.value - other.toDouble())
|
||||||
|
|
||||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value * other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mul)
|
||||||
|
?: of(this.value * other.toDouble())
|
||||||
|
|
||||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value / other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Div)
|
||||||
|
?: of(this.value / other.toDouble())
|
||||||
|
|
||||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value % other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod)
|
||||||
|
?: of(this.value % other.toDouble())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns unboxed Double value
|
* Returns unboxed Double value
|
||||||
|
|||||||
@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* 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.stdlib_included
|
||||||
|
|
||||||
|
@Suppress("Unused", "MemberVisibilityCanBePrivate")
|
||||||
|
internal val decimalLyng = """
|
||||||
|
package lyng.decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounding policies used by [DecimalContext] and `withDecimalContext(...)`.
|
||||||
|
*
|
||||||
|
* These modes currently affect decimal division. They are designed to be explicit and readable in Lyng code.
|
||||||
|
*
|
||||||
|
* Common examples at precision `2`:
|
||||||
|
* - `HalfEven`: `1.d / 8.d -> 0.12`
|
||||||
|
* - `HalfAwayFromZero`: `1.d / 8.d -> 0.13`, `-1.d / 8.d -> -0.13`
|
||||||
|
* - `HalfTowardsZero`: `1.d / 8.d -> 0.12`, `-1.d / 8.d -> -0.12`
|
||||||
|
* - `Ceiling`: rounds toward positive infinity
|
||||||
|
* - `Floor`: rounds toward negative infinity
|
||||||
|
* - `AwayFromZero`: always increases magnitude when rounding is needed
|
||||||
|
* - `TowardsZero`: always truncates toward zero
|
||||||
|
*/
|
||||||
|
enum DecimalRounding {
|
||||||
|
HalfEven,
|
||||||
|
HalfAwayFromZero,
|
||||||
|
HalfTowardsZero,
|
||||||
|
Ceiling,
|
||||||
|
Floor,
|
||||||
|
AwayFromZero,
|
||||||
|
TowardsZero
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic decimal arithmetic settings.
|
||||||
|
*
|
||||||
|
* A decimal context is not attached permanently to a `BigDecimal` value. Instead, it is applied dynamically
|
||||||
|
* inside `withDecimalContext(...)`, which makes the rule local to a block of code.
|
||||||
|
*
|
||||||
|
* Default context:
|
||||||
|
* - precision: `34` significant digits
|
||||||
|
* - rounding: `DecimalRounding.HalfEven`
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* (1.d / 3.d).toStringExpanded()
|
||||||
|
* >>> "0.3333333333333333333333333333333333"
|
||||||
|
*
|
||||||
|
* withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
|
||||||
|
* >>> "0.3333333333"
|
||||||
|
*
|
||||||
|
* withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() }
|
||||||
|
* >>> "0.13"
|
||||||
|
*/
|
||||||
|
class DecimalContext(
|
||||||
|
val precision: Int = 34,
|
||||||
|
val rounding: DecimalRounding = DecimalRounding.HalfEven
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arbitrary-precision decimal value.
|
||||||
|
*
|
||||||
|
* `BigDecimal` is intended for decimal arithmetic where binary floating-point (`Real`) is the wrong tool:
|
||||||
|
* - money
|
||||||
|
* - human-entered decimal values
|
||||||
|
* - ratios that should round in decimal, not in binary
|
||||||
|
* - reproducible decimal formatting
|
||||||
|
*
|
||||||
|
* Creating values:
|
||||||
|
*
|
||||||
|
* - `1.d` converts `Int -> BigDecimal`
|
||||||
|
* - `2.2.d` converts `Real -> BigDecimal` by preserving the current IEEE-754 value
|
||||||
|
* - `"2.2".d` parses exact decimal text
|
||||||
|
* - `BigDecimal.fromInt(...)`, `fromReal(...)`, `fromString(...)` are explicit factory forms
|
||||||
|
*
|
||||||
|
* Important distinction:
|
||||||
|
*
|
||||||
|
* - `2.2.d` means "take the current `Real` value and convert it"
|
||||||
|
* - `"2.2".d` means "parse this exact decimal literal text"
|
||||||
|
*
|
||||||
|
* Therefore:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* 2.2.d.toStringExpanded()
|
||||||
|
* >>> "2.2"
|
||||||
|
*
|
||||||
|
* (0.1 + 0.2).d.toStringExpanded()
|
||||||
|
* >>> "0.30000000000000004"
|
||||||
|
*
|
||||||
|
* "0.3".d.toStringExpanded()
|
||||||
|
* >>> "0.3"
|
||||||
|
*
|
||||||
|
* Mixed arithmetic:
|
||||||
|
*
|
||||||
|
* `BigDecimal` defines its own operators against decimal-compatible values, and the decimal module also registers
|
||||||
|
* interop bridges so built-in left-hand operands work naturally:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* 1.d + 2
|
||||||
|
* >>> 3.d
|
||||||
|
*
|
||||||
|
* 1 + 2.d
|
||||||
|
* >>> 3.d
|
||||||
|
*
|
||||||
|
* 0.5 + 1.d
|
||||||
|
* >>> 1.5.d
|
||||||
|
*
|
||||||
|
* Precision and rounding:
|
||||||
|
*
|
||||||
|
* - division uses the default decimal context unless overridden
|
||||||
|
* - use `withDecimalContext(...)` to apply a local precision/rounding policy
|
||||||
|
*
|
||||||
|
* Exact decimal literal style:
|
||||||
|
*
|
||||||
|
* If you want the source text itself to be the decimal value, use a string:
|
||||||
|
*
|
||||||
|
* "2.2".d
|
||||||
|
*
|
||||||
|
* That is the precise form. `2.2.d` remains a `Real -> BigDecimal` conversion by design.
|
||||||
|
*/
|
||||||
|
extern class BigDecimal() {
|
||||||
|
/** Add another decimal-compatible value. */
|
||||||
|
extern fun plus(other: Object): BigDecimal
|
||||||
|
/** Subtract another decimal-compatible value. */
|
||||||
|
extern fun minus(other: Object): BigDecimal
|
||||||
|
/** Multiply by another decimal-compatible value. */
|
||||||
|
extern fun mul(other: Object): BigDecimal
|
||||||
|
/**
|
||||||
|
* Divide by another decimal-compatible value.
|
||||||
|
*
|
||||||
|
* Division uses the current decimal context:
|
||||||
|
* - by default: `34` significant digits, `HalfEven`
|
||||||
|
* - inside `withDecimalContext(...)`: the context active for the current block
|
||||||
|
*/
|
||||||
|
extern fun div(other: Object): BigDecimal
|
||||||
|
/** Remainder with another decimal-compatible value. */
|
||||||
|
extern fun mod(other: Object): BigDecimal
|
||||||
|
/** Compare with another decimal-compatible value. */
|
||||||
|
extern fun compareTo(other: Object): Int
|
||||||
|
/** Unary minus. */
|
||||||
|
extern fun negate(): BigDecimal
|
||||||
|
/** Convert to `Int` by dropping the fractional part according to backend conversion rules. */
|
||||||
|
extern fun toInt(): Int
|
||||||
|
/** Convert to `Real`. */
|
||||||
|
extern fun toReal(): Real
|
||||||
|
/**
|
||||||
|
* Convert to a plain decimal string without scientific notation.
|
||||||
|
*
|
||||||
|
* This is the preferred representation for user-facing decimal tests and diagnostics.
|
||||||
|
*/
|
||||||
|
extern fun toStringExpanded(): String
|
||||||
|
|
||||||
|
/** Create a decimal from an `Int`. */
|
||||||
|
static extern fun fromInt(value: Int): BigDecimal
|
||||||
|
/**
|
||||||
|
* Create a decimal from a `Real`.
|
||||||
|
*
|
||||||
|
* This preserves the current IEEE-754 value using a round-trip-safe decimal conversion.
|
||||||
|
* It does not try to recover the original source text.
|
||||||
|
*/
|
||||||
|
static extern fun fromReal(value: Real): BigDecimal
|
||||||
|
/** Parse exact decimal text. */
|
||||||
|
static extern fun fromString(value: String): BigDecimal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run [block] with the provided decimal context.
|
||||||
|
*
|
||||||
|
* This is the main way to control decimal division precision and rounding locally without changing global behavior.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* withDecimalContext(10) {
|
||||||
|
* (1.d / 3.d).toStringExpanded()
|
||||||
|
* }
|
||||||
|
* >>> "0.3333333333"
|
||||||
|
*
|
||||||
|
* Contexts are dynamic and block-local. After the block finishes, the previous context is restored.
|
||||||
|
*/
|
||||||
|
extern fun withDecimalContext<T>(context: DecimalContext, block: ()->T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience overload for changing only precision.
|
||||||
|
*
|
||||||
|
* Equivalent to `withDecimalContext(DecimalContext(precision, DecimalRounding.HalfEven), block)`.
|
||||||
|
*/
|
||||||
|
extern fun withDecimalContext<T>(precision: Int, block: ()->T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience overload for changing precision and rounding explicitly.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* withDecimalContext(2, DecimalRounding.HalfAwayFromZero) {
|
||||||
|
* (1.d / 8.d).toStringExpanded()
|
||||||
|
* }
|
||||||
|
* >>> "0.13"
|
||||||
|
*/
|
||||||
|
extern fun withDecimalContext<T>(precision: Int, rounding: DecimalRounding, block: ()->T): T
|
||||||
|
""".trimIndent()
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* 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.stdlib_included
|
||||||
|
|
||||||
|
@Suppress("Unused", "MemberVisibilityCanBePrivate")
|
||||||
|
internal val operatorsLyng = """
|
||||||
|
package lyng.operators
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binary operators that can be bridged between two different operand classes.
|
||||||
|
*
|
||||||
|
* Registering a pair means:
|
||||||
|
* - the runtime can evaluate `left op right` when `left` has class `L` and `right` has class `R`
|
||||||
|
* - both operands are first converted to a shared "common" class `C`
|
||||||
|
* - the actual operator implementation is then looked up on `C`
|
||||||
|
*
|
||||||
|
* This is primarily useful when:
|
||||||
|
* - you add a new numeric-like type in a library or in pure Lyng code
|
||||||
|
* - your type already implements operators against itself
|
||||||
|
* - you also want existing left-hand types such as `Int` or `Real` to work when your type is on the right
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.operators
|
||||||
|
*
|
||||||
|
* class DecimalBox(val value: Int) {
|
||||||
|
* fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
* fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* OperatorInterop.register(
|
||||||
|
* Int,
|
||||||
|
* DecimalBox,
|
||||||
|
* DecimalBox,
|
||||||
|
* [BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
* { x: Int -> DecimalBox(x) },
|
||||||
|
* { x: DecimalBox -> x }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* After registration:
|
||||||
|
* - `1 + DecimalBox(2)` works
|
||||||
|
* - `1 < DecimalBox(2)` works
|
||||||
|
* - `2 == DecimalBox(2)` works
|
||||||
|
*
|
||||||
|
* But this registration does not replace methods on the original classes. It only teaches
|
||||||
|
* Lyng how to bridge a mixed pair into a common class for the listed operators.
|
||||||
|
*/
|
||||||
|
enum BinaryOperator {
|
||||||
|
/** `a + b` */
|
||||||
|
Plus,
|
||||||
|
/** `a - b` */
|
||||||
|
Minus,
|
||||||
|
/** `a * b` */
|
||||||
|
Mul,
|
||||||
|
/** `a / b` */
|
||||||
|
Div,
|
||||||
|
/** `a % b` */
|
||||||
|
Mod,
|
||||||
|
/**
|
||||||
|
* Ordering comparisons.
|
||||||
|
*
|
||||||
|
* Registering `Compare` enables `<`, `<=`, `>`, `>=`, and the shuttle operator `<=>`
|
||||||
|
* for the mixed operand pair.
|
||||||
|
*/
|
||||||
|
Compare,
|
||||||
|
/**
|
||||||
|
* Equality comparisons.
|
||||||
|
*
|
||||||
|
* Registering `Equals` enables `==` and `!=` for the mixed operand pair.
|
||||||
|
*/
|
||||||
|
Equals
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime registry for mixed-class binary operators.
|
||||||
|
*
|
||||||
|
* `register(L, R, C, ...)` defines how Lyng should evaluate expressions where:
|
||||||
|
* - the left operand has class `L`
|
||||||
|
* - the right operand has class `R`
|
||||||
|
* - the actual operation should be executed as if both were values of class `C`
|
||||||
|
*
|
||||||
|
* The registry is symmetric for the converted values, but not for the original syntax.
|
||||||
|
* Its job is specifically to fill the gap where your custom type appears on the right:
|
||||||
|
*
|
||||||
|
* - `myDecimal + 1` usually already works if `BigDecimal.plus(Int)` exists
|
||||||
|
* - `1 + myDecimal` needs registration because `Int` itself is not rewritten
|
||||||
|
*
|
||||||
|
* Typical pattern for a custom type:
|
||||||
|
*
|
||||||
|
* import lyng.operators
|
||||||
|
*
|
||||||
|
* class Rational(val num: Int, val den: Int) {
|
||||||
|
* fun plus(other: Rational) = Rational(num * other.den + other.num * den, den * other.den)
|
||||||
|
* fun minus(other: Rational) = Rational(num * other.den - other.num * den, den * other.den)
|
||||||
|
* fun mul(other: Rational) = Rational(num * other.num, den * other.den)
|
||||||
|
* fun div(other: Rational) = Rational(num * other.den, den * other.num)
|
||||||
|
* fun compareTo(other: Rational) = (num * other.den) <=> (other.num * den)
|
||||||
|
*
|
||||||
|
* static fun fromInt(value: Int) = Rational(value, 1)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* OperatorInterop.register(
|
||||||
|
* Int,
|
||||||
|
* Rational,
|
||||||
|
* Rational,
|
||||||
|
* [
|
||||||
|
* BinaryOperator.Plus,
|
||||||
|
* BinaryOperator.Minus,
|
||||||
|
* BinaryOperator.Mul,
|
||||||
|
* BinaryOperator.Div,
|
||||||
|
* BinaryOperator.Compare,
|
||||||
|
* BinaryOperator.Equals
|
||||||
|
* ],
|
||||||
|
* { x: Int -> Rational.fromInt(x) },
|
||||||
|
* { x: Rational -> x }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* Then:
|
||||||
|
* - `1 + Rational(1, 2)` works
|
||||||
|
* - `3 > Rational(5, 2)` works
|
||||||
|
* - `2 == Rational(2, 1)` works
|
||||||
|
*
|
||||||
|
* Decimal uses the same mechanism internally to make `Int + BigDecimal` and `Real + BigDecimal`
|
||||||
|
* work without changing the built-in `Int` or `Real` classes.
|
||||||
|
*/
|
||||||
|
extern object OperatorInterop {
|
||||||
|
/**
|
||||||
|
* Register a mixed-operand operator bridge.
|
||||||
|
*
|
||||||
|
* @param leftClass class of the original left operand
|
||||||
|
* @param rightClass class of the original right operand
|
||||||
|
* @param commonClass class that will actually execute the operator methods
|
||||||
|
* @param operators operators supported for this mixed pair
|
||||||
|
* @param leftToCommon conversion from `L` to `C`
|
||||||
|
* @param rightToCommon conversion from `R` to `C`
|
||||||
|
*
|
||||||
|
* Requirements for `commonClass`:
|
||||||
|
* - if you register `Plus`, `C` should implement `fun plus(other: C): C` or equivalent accepted result type
|
||||||
|
* - if you register `Minus`, `C` should implement `fun minus(other: C): ...`
|
||||||
|
* - if you register `Mul`, `C` should implement `fun mul(other: C): ...`
|
||||||
|
* - if you register `Div`, `C` should implement `fun div(other: C): ...`
|
||||||
|
* - if you register `Mod`, `C` should implement `fun mod(other: C): ...`
|
||||||
|
* - if you register `Compare`, `C` should implement `fun compareTo(other: C): Int`
|
||||||
|
*
|
||||||
|
* `Equals` reuses comparison/equality semantics of the promoted values.
|
||||||
|
*
|
||||||
|
* Registration is usually done once at module initialization time:
|
||||||
|
*
|
||||||
|
* package my.rational
|
||||||
|
* import lyng.operators
|
||||||
|
*
|
||||||
|
* class Rational(val num: Int, val den: Int) {
|
||||||
|
* fun plus(other: Rational) = Rational(num * other.den + other.num * den, den * other.den)
|
||||||
|
* fun compareTo(other: Rational) = (num * other.den) <=> (other.num * den)
|
||||||
|
* static fun fromInt(value: Int) = Rational(value, 1)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* OperatorInterop.register(
|
||||||
|
* Int,
|
||||||
|
* Rational,
|
||||||
|
* Rational,
|
||||||
|
* [BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
* { x: Int -> Rational.fromInt(x) },
|
||||||
|
* { x: Rational -> x }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
extern fun register<L, R, C>(
|
||||||
|
leftClass: Class<L>,
|
||||||
|
rightClass: Class<R>,
|
||||||
|
commonClass: Class<C>,
|
||||||
|
operators: List<BinaryOperator>,
|
||||||
|
leftToCommon: (L)->C,
|
||||||
|
rightToCommon: (R)->C
|
||||||
|
): Void
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* 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 OperatorInteropTest {
|
||||||
|
@Test
|
||||||
|
fun testPureLyngOperatorInteropRegistration() = runTest {
|
||||||
|
val im = Script.defaultImportManager.copy()
|
||||||
|
im.addPackage("test.decimalbox") { scope ->
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
package test.decimalbox
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class DecimalBox(val value: Int) {
|
||||||
|
fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
fun minus(other: DecimalBox) = DecimalBox(value - other.value)
|
||||||
|
fun mul(other: DecimalBox) = DecimalBox(value * other.value)
|
||||||
|
fun div(other: DecimalBox) = DecimalBox(value / other.value)
|
||||||
|
fun mod(other: DecimalBox) = DecimalBox(value % other.value)
|
||||||
|
fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
DecimalBox,
|
||||||
|
DecimalBox,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Minus, BinaryOperator.Mul, BinaryOperator.Div, BinaryOperator.Mod, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> DecimalBox(x) },
|
||||||
|
{ x: DecimalBox -> x }
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = im.newStdScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import test.decimalbox
|
||||||
|
|
||||||
|
assertEquals(DecimalBox(3), 1 + DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 3 - DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(8), 4 * DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(4), 8 / DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 7 % DecimalBox(2))
|
||||||
|
assert(1 < DecimalBox(2))
|
||||||
|
assert(2 <= DecimalBox(2))
|
||||||
|
assert(3 > DecimalBox(2))
|
||||||
|
assert(2 >= DecimalBox(2))
|
||||||
|
assert(2 == DecimalBox(2))
|
||||||
|
assert(2 != DecimalBox(3))
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRealInteropRegistrationUsesTopLevelModuleCode() = runTest {
|
||||||
|
val im = Script.defaultImportManager.copy()
|
||||||
|
im.addPackage("test.realbox") { scope ->
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
package test.realbox
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class RealBox(val value: Real) {
|
||||||
|
fun plus(other: RealBox) = RealBox(value + other.value)
|
||||||
|
fun compareTo(other: RealBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Real,
|
||||||
|
RealBox,
|
||||||
|
RealBox,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Real -> RealBox(x) },
|
||||||
|
{ x: RealBox -> x }
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = im.newStdScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import test.realbox
|
||||||
|
|
||||||
|
assertEquals(RealBox(1.75), 0.5 + RealBox(1.25))
|
||||||
|
assert(1.5 < RealBox(2.0))
|
||||||
|
assert(2.0 == RealBox(2.0))
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,74 +27,85 @@ import org.jetbrains.compose.web.dom.*
|
|||||||
fun HomePage() {
|
fun HomePage() {
|
||||||
val samples = remember {
|
val samples = remember {
|
||||||
listOf(
|
listOf(
|
||||||
|
"""
|
||||||
|
// Decimal arithmetic with explicit local precision
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
val exact = "0.3".d
|
||||||
|
val fromReal = (0.1 + 0.2).d
|
||||||
|
|
||||||
|
println(exact.toStringExpanded())
|
||||||
|
println(fromReal.toStringExpanded())
|
||||||
|
println(withDecimalContext(10) { (1.d / 3.d).toStringExpanded() })
|
||||||
|
""".trimIndent(),
|
||||||
|
"""
|
||||||
|
// Mixed operators for your own type
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class Coins(val amount: Int) {
|
||||||
|
fun plus(other: Coins) = Coins(amount + other.amount)
|
||||||
|
fun compareTo(other: Coins) = amount <=> other.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
Coins,
|
||||||
|
Coins,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> Coins(x) },
|
||||||
|
{ x: Coins -> x }
|
||||||
|
)
|
||||||
|
|
||||||
|
println(1 + Coins(2))
|
||||||
|
println(3 > Coins(2))
|
||||||
|
""".trimIndent(),
|
||||||
|
"""
|
||||||
|
// Non-local returns from closures
|
||||||
|
fun findFirst<T>(list: Iterable<T>, predicate: (T)->Bool): T? {
|
||||||
|
list.forEach {
|
||||||
|
if (predicate(it)) return@findFirst it
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val found: Int? = findFirst([1, 5, 8, 12]) { it > 10 }
|
||||||
|
println("Found: " + found)
|
||||||
|
""".trimIndent(),
|
||||||
|
"""
|
||||||
|
// Implicit coroutines: parallelism without ceremony
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
|
val d1 = launch {
|
||||||
|
delay(100.milliseconds)
|
||||||
|
"Task A finished"
|
||||||
|
}
|
||||||
|
val d2 = launch {
|
||||||
|
delay(50.milliseconds)
|
||||||
|
"Task B finished"
|
||||||
|
}
|
||||||
|
|
||||||
|
println(d1.await())
|
||||||
|
println(d2.await())
|
||||||
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Everything is an expression
|
// Everything is an expression
|
||||||
val x: Int = 10
|
val x: Int = 10
|
||||||
val status: String = if (x > 0) "Positive" else "Zero or Negative"
|
val status: String = if (x > 0) "Positive" else "Zero or Negative"
|
||||||
|
|
||||||
// Even loops return values!
|
|
||||||
val result = for (i in 1..5) {
|
val result = for (i in 1..5) {
|
||||||
if (i == 3) break "Found 3!"
|
if (i == 3) break "Found 3!"
|
||||||
} else "Not found"
|
} else "Not found"
|
||||||
|
|
||||||
println("Result: " + result)
|
println(status)
|
||||||
|
println(result)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Functional power with generics and collections
|
// Functional collections with strict static types
|
||||||
val squares: List<Int> = (1..10)
|
val squares: List<Int> = (1..10)
|
||||||
.filter { it % 2 == 0 }
|
.filter { it % 2 == 0 }
|
||||||
.map { it * it }
|
.map { it * it }
|
||||||
|
|
||||||
println("Even squares: " + squares)
|
println("Even squares: " + squares)
|
||||||
// Output: [4, 16, 36, 64, 100]
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Generics and type aliases
|
|
||||||
type Num = Int | Real
|
|
||||||
|
|
||||||
class Box<out T: Num>(val value: T) {
|
|
||||||
fun get(): T = value
|
|
||||||
}
|
|
||||||
|
|
||||||
val intBox = Box(42)
|
|
||||||
val realBox = Box(3.14)
|
|
||||||
println("Boxes: " + intBox.get() + ", " + realBox.get())
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Strict compile-time types and symbol resolution
|
|
||||||
fun greet(name: String, count: Int) {
|
|
||||||
for (i in 1..count) {
|
|
||||||
println("Hello, " + name + "!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
greet("Lyng", 3)
|
|
||||||
// greet(10, "error") // This would be a compile-time error!
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Flexible map literals and shorthands
|
|
||||||
val id = 101
|
|
||||||
val name = "Lyng"
|
|
||||||
val base = { id:, name: } // Shorthand for id: id, name: name
|
|
||||||
|
|
||||||
val full = { ...base, version: "1.5.0-SNAPSHOT", status: "active" }
|
|
||||||
println(full)
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Modern null safety
|
|
||||||
var config: Map<String, Int>? = null
|
|
||||||
config ?= { timeout: 30 } // Assign only if null
|
|
||||||
|
|
||||||
val timeout = config?["timeout"] ?: 60
|
|
||||||
println("Timeout is: " + timeout)
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Destructuring with splat operator
|
|
||||||
val [first, middle..., last] = [1, 2, 3, 4, 5, 6]
|
|
||||||
|
|
||||||
println("First: " + first)
|
|
||||||
println("Middle: " + middle)
|
|
||||||
println("Last: " + last)
|
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Diamond-safe Multiple Inheritance (C3 MRO)
|
// Diamond-safe Multiple Inheritance (C3 MRO)
|
||||||
@ -123,18 +134,6 @@ fun HomePage() {
|
|||||||
println([10, 20, 30].second)
|
println([10, 20, 30].second)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Non-local returns from closures
|
|
||||||
fun findFirst<T>(list: Iterable<T>, predicate: (T)->Bool): T? {
|
|
||||||
list.forEach {
|
|
||||||
if (predicate(it)) return@findFirst it
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val found: Int? = findFirst([1, 5, 8, 12]) { it > 10 }
|
|
||||||
println("Found: " + found)
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Easy operator overloading
|
// Easy operator overloading
|
||||||
class Vector(val x: Real, val y: Real) {
|
class Vector(val x: Real, val y: Real) {
|
||||||
fun plus(other: Vector): Vector = Vector(x + other.x, y + other.y)
|
fun plus(other: Vector): Vector = Vector(x + other.x, y + other.y)
|
||||||
@ -156,20 +155,13 @@ fun HomePage() {
|
|||||||
println("User name: " + u.name)
|
println("User name: " + u.name)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Implicit coroutines: parallelism without ceremony
|
// Flexible map literals and shorthands
|
||||||
import lyng.time
|
val id = 101
|
||||||
|
val name = "Lyng"
|
||||||
|
val base = { id:, name: }
|
||||||
|
val full = { ...base, status: "active", tags: ["typed", "portable"] }
|
||||||
|
|
||||||
val d1 = launch {
|
println(full)
|
||||||
delay(100.milliseconds)
|
|
||||||
"Task A finished"
|
|
||||||
}
|
|
||||||
val d2 = launch {
|
|
||||||
delay(50.milliseconds)
|
|
||||||
"Task B finished"
|
|
||||||
}
|
|
||||||
|
|
||||||
println(d1.await())
|
|
||||||
println(d2.await())
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -195,6 +187,8 @@ fun HomePage() {
|
|||||||
Div({ classes("d-flex", "justify-content-center", "gap-2", "flex-wrap", "mb-4") }) {
|
Div({ classes("d-flex", "justify-content-center", "gap-2", "flex-wrap", "mb-4") }) {
|
||||||
// Benefits pills
|
// Benefits pills
|
||||||
listOf(
|
listOf(
|
||||||
|
"Decimal arithmetic",
|
||||||
|
"Operator interop",
|
||||||
"Strict static typing",
|
"Strict static typing",
|
||||||
"Generics & Type Aliases",
|
"Generics & Type Aliases",
|
||||||
"Implicit coroutines",
|
"Implicit coroutines",
|
||||||
@ -233,6 +227,26 @@ fun HomePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Div({ classes("row", "g-3", "mb-4") }) {
|
||||||
|
listOf(
|
||||||
|
Triple("New: Decimal", "Exact decimal values, local precision control, and mixed operators with Int and Real.", "#/docs/Decimal.md"),
|
||||||
|
Triple("New: Operator Interop", "Teach Lyng how your custom types interact with built-ins on the left-hand side.", "#/docs/OperatorInterop.md"),
|
||||||
|
Triple("What Changed", "Recent language, stdlib, and tooling improvements in one place.", "#/docs/whats_new.md")
|
||||||
|
).forEach { (title, text, href) ->
|
||||||
|
Div({ classes("col-12", "col-lg-4") }) {
|
||||||
|
A(attrs = {
|
||||||
|
classes("text-decoration-none")
|
||||||
|
attr("href", href)
|
||||||
|
}) {
|
||||||
|
Div({ classes("h-100", "p-3", "border", "rounded-3", "bg-body-tertiary") }) {
|
||||||
|
H3({ classes("h5", "mb-2", "text-body") }) { Text(title) }
|
||||||
|
P({ classes("mb-0", "text-muted") }) { Text(text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Code sample slideshow
|
// Code sample slideshow
|
||||||
Div({
|
Div({
|
||||||
classes("markdown-body", "mt-0", "slide-container", "position-relative")
|
classes("markdown-body", "mt-0", "slide-container", "position-relative")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user