Compare commits
7
Commits
1.5.1
...
bigdecimals
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580256d520 | ||
|
|
4269310beb | ||
|
|
7c059b4741 | ||
|
|
217787e17a | ||
|
|
9ddc7dbee6 | ||
|
|
24937c7cf5 | ||
|
|
ecc4d8b9ed |
No files matched your search
+250
@@ -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).
|
||||
@@ -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).
|
||||
@@ -72,6 +72,14 @@ For module-level APIs, the default workflow is:
|
||||
1. declare globals in Lyng using `extern fun` / `extern val` / `extern var`;
|
||||
2. bind Kotlin implementation via `ModuleScope.globalBinder()`.
|
||||
|
||||
This is also the recommended way to expose a Kotlin-backed value that should behave like a true
|
||||
Lyng global variable/property. If you need `x` to read/write through Kotlin on every access, use
|
||||
`extern var` / `extern val` plus `bindGlobalVar(...)`.
|
||||
|
||||
Do not use `addConst(...)` for this case: `addConst(...)` installs a value, not a Kotlin-backed
|
||||
property accessor. It is appropriate for fixed values and objects, but not for a global that should
|
||||
delegate reads/writes back into Kotlin state.
|
||||
|
||||
```kotlin
|
||||
import net.sergeych.lyng.bridge.*
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
@@ -117,6 +125,12 @@ assertEquals("changed", globalProp)
|
||||
assertEquals("1.0.0", globalVersion)
|
||||
```
|
||||
|
||||
Minimal rule of thumb:
|
||||
|
||||
- use `bindGlobalFun(...)` for global functions
|
||||
- use `bindGlobalVar(...)` for Kotlin-backed global variables/properties
|
||||
- use `addConst(...)` only for fixed values/objects that do not need getter/setter behavior
|
||||
|
||||
For custom argument handling and full runtime access:
|
||||
|
||||
```kotlin
|
||||
@@ -462,14 +476,21 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
||||
module.eval(
|
||||
"""
|
||||
extern val version: String
|
||||
extern var status: String
|
||||
extern fun triple(x: Int): Int
|
||||
""".trimIndent()
|
||||
)
|
||||
val binder = module.globalBinder()
|
||||
var status = "ready"
|
||||
binder.bindGlobalVar(
|
||||
name = "version",
|
||||
get = { "1.0" }
|
||||
)
|
||||
binder.bindGlobalVar(
|
||||
name = "status",
|
||||
get = { status },
|
||||
set = { status = it }
|
||||
)
|
||||
binder.bindGlobalFun1<Int>("triple") { x ->
|
||||
ObjInt.of((x * 3).toLong())
|
||||
}
|
||||
@@ -479,6 +500,7 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
||||
scope.eval("""
|
||||
import my.tools.*
|
||||
val v = triple(14)
|
||||
status = "busy"
|
||||
""")
|
||||
val v = scope.eval("v").toKotlin(scope) // -> 42
|
||||
```
|
||||
|
||||
@@ -5,6 +5,73 @@ For a programmer-focused migration summary, see `docs/whats_new_1_5.md`.
|
||||
|
||||
## 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
|
||||
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-datetime = "0.6.1"
|
||||
mp_bintools = "0.3.2"
|
||||
ionspin-bignum = "0.3.10"
|
||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||
okioVersion = "3.10.2"
|
||||
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-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
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" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
||||
|
||||
@@ -101,6 +101,7 @@ kotlin {
|
||||
//put your multiplatform dependencies here
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.mp.bintools)
|
||||
implementation(libs.ionspin.bignum)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
|
||||
@@ -184,6 +184,7 @@ class Compiler(
|
||||
private val encodedPayloadTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
|
||||
private val objectDeclNames: MutableSet<String> = mutableSetOf()
|
||||
private val externCallableNames: MutableSet<String> = mutableSetOf()
|
||||
private val externBindingNames: MutableSet<String> = mutableSetOf()
|
||||
private val moduleDeclaredNames: MutableSet<String> = mutableSetOf()
|
||||
private var seedingSlotPlan: Boolean = false
|
||||
|
||||
@@ -192,6 +193,7 @@ class Compiler(
|
||||
if (plan.slots.isEmpty()) return emptyMap()
|
||||
val result = LinkedHashMap<String, ForcedLocalSlotInfo>(plan.slots.size)
|
||||
for ((name, entry) in plan.slots) {
|
||||
if (externBindingNames.contains(name)) continue
|
||||
result[name] = ForcedLocalSlotInfo(
|
||||
index = entry.index,
|
||||
isMutable = entry.isMutable,
|
||||
@@ -581,7 +583,7 @@ class Compiler(
|
||||
val baseNames = cls.directParents.map { it.className }
|
||||
val nextFieldId = (fieldIds.values.maxOrNull() ?: -1) + 1
|
||||
val nextMethodId = (methodIds.values.maxOrNull() ?: -1) + 1
|
||||
return CompileClassInfo(name, fieldIds, methodIds, nextFieldId, nextMethodId, baseNames)
|
||||
return CompileClassInfo(name, cls.logicalPackageName, fieldIds, methodIds, nextFieldId, nextMethodId, baseNames)
|
||||
}
|
||||
|
||||
private data class BaseMemberIds(
|
||||
@@ -885,6 +887,7 @@ class Compiler(
|
||||
name = declaredName,
|
||||
isMutable = false,
|
||||
visibility = Visibility.Public,
|
||||
actualExtern = false,
|
||||
initializer = initStmt,
|
||||
isTransient = false,
|
||||
typeDecl = null,
|
||||
@@ -1550,6 +1553,7 @@ class Compiler(
|
||||
|
||||
private data class CompileClassInfo(
|
||||
val name: String,
|
||||
val packageName: String?,
|
||||
val fieldIds: Map<String, Int>,
|
||||
val methodIds: Map<String, Int>,
|
||||
val nextFieldId: Int,
|
||||
@@ -1751,6 +1755,7 @@ class Compiler(
|
||||
enumEntriesByName = enumEntriesByName,
|
||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||
callableReturnTypeByName = callableReturnTypeByName,
|
||||
externBindingNames = externBindingNames,
|
||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||
) as BytecodeStatement
|
||||
unwrapped to bytecodeStmt.bytecodeFunction()
|
||||
@@ -1957,6 +1962,9 @@ class Compiler(
|
||||
val scopeIndex = slotPlanStack.indexOfLast { it.id == slotLoc.scopeId }
|
||||
if (functionIndex >= 0 && scopeIndex >= functionIndex) return null
|
||||
val modulePlan = moduleSlotPlan()
|
||||
if (modulePlan != null && slotLoc.scopeId == modulePlan.id && externBindingNames.contains(name)) {
|
||||
return null
|
||||
}
|
||||
if (useScopeSlots && modulePlan != null && slotLoc.scopeId == modulePlan.id) {
|
||||
return null
|
||||
}
|
||||
@@ -2075,6 +2083,7 @@ class Compiler(
|
||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||
callableReturnTypeByName = callableReturnTypeByName,
|
||||
externCallableNames = externCallableNames,
|
||||
externBindingNames = externBindingNames,
|
||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||
)
|
||||
}
|
||||
@@ -2105,6 +2114,7 @@ class Compiler(
|
||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||
callableReturnTypeByName = callableReturnTypeByName,
|
||||
externCallableNames = externCallableNames,
|
||||
externBindingNames = externBindingNames,
|
||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||
)
|
||||
}
|
||||
@@ -2160,6 +2170,7 @@ class Compiler(
|
||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||
callableReturnTypeByName = callableReturnTypeByName,
|
||||
externCallableNames = externCallableNames,
|
||||
externBindingNames = externBindingNames,
|
||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||
)
|
||||
}
|
||||
@@ -2343,6 +2354,7 @@ class Compiler(
|
||||
stmt.name,
|
||||
stmt.isMutable,
|
||||
stmt.visibility,
|
||||
stmt.actualExtern,
|
||||
init,
|
||||
stmt.isTransient,
|
||||
stmt.typeDecl,
|
||||
@@ -4655,6 +4667,7 @@ class Compiler(
|
||||
}
|
||||
is MethodCallRef -> methodReturnTypeDeclByRef[ref]
|
||||
is CallRef -> callReturnTypeDeclByRef[ref]
|
||||
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
|
||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
||||
else -> null
|
||||
}
|
||||
@@ -4718,6 +4731,7 @@ class Compiler(
|
||||
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||
is CallRef -> inferCallReturnClass(ref)
|
||||
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
||||
is FieldRef -> {
|
||||
val targetClass = resolveReceiverClassForMember(ref.target)
|
||||
inferFieldReturnClass(targetClass, ref.name)
|
||||
@@ -4732,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? {
|
||||
inferBinaryOpReturnTypeDecl(ref)?.let { declared ->
|
||||
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||
}
|
||||
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
|
||||
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
|
||||
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) {
|
||||
BinOp.PLUS, BinOp.MINUS -> when {
|
||||
leftClass == ObjInstant.type && rightClass == ObjInstant.type && ref.op == BinOp.MINUS -> ObjDuration.type
|
||||
@@ -6446,7 +6555,7 @@ class Compiler(
|
||||
WhenStatement(value, cases, elseCase, whenPos)
|
||||
} else {
|
||||
// when { cond -> ... }
|
||||
TODO("when without object is not yet implemented")
|
||||
throw ScriptError(t.pos, "when without subject is not implemented")
|
||||
}
|
||||
return wrapBytecode(stmt)
|
||||
}
|
||||
@@ -6699,6 +6808,7 @@ class Compiler(
|
||||
)
|
||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||
name = qualifiedName,
|
||||
packageName = packageName,
|
||||
fieldIds = fieldIds,
|
||||
methodIds = methodIds,
|
||||
nextFieldId = fieldIds.size,
|
||||
@@ -6821,6 +6931,7 @@ class Compiler(
|
||||
classCtx?.let { ctx ->
|
||||
compileClassInfos[className] = CompileClassInfo(
|
||||
name = className,
|
||||
packageName = packageName,
|
||||
fieldIds = ctx.memberFieldIds.toMap(),
|
||||
methodIds = ctx.memberMethodIds.toMap(),
|
||||
nextFieldId = ctx.nextFieldId,
|
||||
@@ -6862,6 +6973,7 @@ class Compiler(
|
||||
val baseIds = collectBaseMemberIds(baseSpecs.map { it.name })
|
||||
compileClassInfos[className] = CompileClassInfo(
|
||||
name = className,
|
||||
packageName = packageName,
|
||||
fieldIds = baseIds.fieldIds,
|
||||
methodIds = baseIds.methodIds,
|
||||
nextFieldId = baseIds.nextFieldId,
|
||||
@@ -7110,6 +7222,7 @@ class Compiler(
|
||||
}
|
||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||
name = qualifiedName,
|
||||
packageName = packageName,
|
||||
fieldIds = ctx.memberFieldIds.toMap(),
|
||||
methodIds = ctx.memberMethodIds.toMap(),
|
||||
nextFieldId = ctx.nextFieldId,
|
||||
@@ -7125,6 +7238,7 @@ class Compiler(
|
||||
classCtx?.let { ctx ->
|
||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||
name = qualifiedName,
|
||||
packageName = packageName,
|
||||
fieldIds = ctx.memberFieldIds.toMap(),
|
||||
methodIds = ctx.memberMethodIds.toMap(),
|
||||
nextFieldId = ctx.nextFieldId,
|
||||
@@ -7211,6 +7325,7 @@ class Compiler(
|
||||
}
|
||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||
name = qualifiedName,
|
||||
packageName = packageName,
|
||||
fieldIds = ctx.memberFieldIds.toMap(),
|
||||
methodIds = ctx.memberMethodIds.toMap(),
|
||||
nextFieldId = ctx.nextFieldId,
|
||||
@@ -8390,7 +8505,7 @@ class Compiler(
|
||||
is ImplicitThisMethodCallRef -> {
|
||||
when (directRef.methodName()) {
|
||||
"iterator" -> ObjIterator
|
||||
"lazy" -> ObjLazyDelegate.type
|
||||
"lazy" -> resolveClassByName("lazy") ?: inferMethodCallReturnClass(directRef.methodName())
|
||||
else -> inferMethodCallReturnClass(directRef.methodName())
|
||||
}
|
||||
}
|
||||
@@ -8409,7 +8524,7 @@ class Compiler(
|
||||
when {
|
||||
target is LocalSlotRef -> {
|
||||
when (target.name) {
|
||||
"lazy" -> ObjLazyDelegate.type
|
||||
"lazy" -> resolveClassByName("lazy")
|
||||
"iterator" -> ObjIterator
|
||||
"flow" -> ObjFlow.type
|
||||
"launch" -> ObjDeferred.type
|
||||
@@ -8420,7 +8535,7 @@ class Compiler(
|
||||
}
|
||||
target is LocalVarRef -> {
|
||||
when (target.name) {
|
||||
"lazy" -> ObjLazyDelegate.type
|
||||
"lazy" -> resolveClassByName("lazy")
|
||||
"iterator" -> ObjIterator
|
||||
"flow" -> ObjFlow.type
|
||||
"launch" -> ObjDeferred.type
|
||||
@@ -8459,7 +8574,9 @@ class Compiler(
|
||||
?: unwrapDirectRef(initializer)?.let { inferObjClassFromRef(it) }
|
||||
?: throw ScriptError(initializer.pos, "Delegate type must be known at compile time")
|
||||
if (initClass !== delegateClass &&
|
||||
initClass.logicalName != delegateClass.logicalName &&
|
||||
!initClass.allParentsSet.contains(delegateClass) &&
|
||||
!initClass.allParentsSet.any { it.logicalName == delegateClass.logicalName } &&
|
||||
!initClass.allImplementingNames.contains(delegateClass.className)
|
||||
) {
|
||||
throw ScriptError(
|
||||
@@ -8549,7 +8666,9 @@ class Compiler(
|
||||
if (closedParent != null) {
|
||||
throw ScriptError(Pos.builtIn, "can't inherit from closed class ${closedParent.className}")
|
||||
}
|
||||
ObjInstanceClass(info.name, *parents.toTypedArray())
|
||||
ObjInstanceClass(info.name, *parents.toTypedArray()).apply {
|
||||
logicalPackageNameOverride = info.packageName
|
||||
}
|
||||
}
|
||||
if (stub is ObjInstanceClass) {
|
||||
for ((fieldName, fieldId) in info.fieldIds) {
|
||||
@@ -8893,7 +9012,7 @@ class Compiler(
|
||||
val effectiveEqToken = if (isProperty) null else eqToken
|
||||
|
||||
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
|
||||
if (!isStatic && declaringClassNameCaptured == null) declareLocalName(name, isMutable)
|
||||
if (!isStatic && declaringClassNameCaptured == null && !actualExtern) declareLocalName(name, isMutable)
|
||||
val declKind = if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
||||
SymbolKind.MEMBER
|
||||
} else {
|
||||
@@ -8902,6 +9021,8 @@ class Compiler(
|
||||
resolutionSink?.declareSymbol(name, declKind, isMutable, nameStartPos, isOverride = isOverride)
|
||||
if (declKind == SymbolKind.MEMBER && extTypeName == null) {
|
||||
(codeContexts.lastOrNull() as? CodeContext.ClassBody)?.declaredMembers?.add(name)
|
||||
} else if (actualExtern) {
|
||||
externBindingNames.add(name)
|
||||
}
|
||||
|
||||
val isDelegate = if (isAbstract || actualExtern) {
|
||||
@@ -8967,7 +9088,11 @@ class Compiler(
|
||||
|
||||
if (isDelegate && initialExpression != null) {
|
||||
ensureDelegateType(initialExpression)
|
||||
if (isMutable && resolveInitializerObjClass(initialExpression) == ObjLazyDelegate.type) {
|
||||
val lazyClass = resolveClassByName("lazy")
|
||||
if (isMutable &&
|
||||
lazyClass != null &&
|
||||
resolveInitializerObjClass(initialExpression)?.logicalName == lazyClass.logicalName
|
||||
) {
|
||||
throw ScriptError(initialExpression.pos, "lazy delegate is read-only")
|
||||
}
|
||||
}
|
||||
@@ -9059,6 +9184,7 @@ class Compiler(
|
||||
name,
|
||||
isMutable,
|
||||
visibility,
|
||||
actualExtern,
|
||||
initialExpression,
|
||||
isTransient,
|
||||
declaredType,
|
||||
|
||||
@@ -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.obj.*
|
||||
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.operatorsLyng
|
||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||
import net.sergeych.lynon.ObjLynonClass
|
||||
import net.sergeych.mp_tools.globalDefer
|
||||
@@ -46,6 +48,28 @@ class Script(
|
||||
// private val catchReturn: Boolean = false,
|
||||
) : Statement() {
|
||||
fun statements(): List<Statement> = statements
|
||||
|
||||
/**
|
||||
* Explicitly apply this script's import/module bindings to [scope] without executing the script.
|
||||
* This is intended for embedding scenarios where the host owns scope lifecycle and wants
|
||||
* script-specific imports to be a separate, opt-in preparation step.
|
||||
*/
|
||||
suspend fun importInto(scope: Scope, seedScope: Scope = scope): Scope {
|
||||
prepareScriptScope(scope, seedScope)
|
||||
return scope
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fresh raw module scope and apply this script's import/module bindings to it.
|
||||
* If [seedScope] is provided, its import provider is reused and seed-bound imports resolve from it.
|
||||
*/
|
||||
suspend fun instantiateModule(seedScope: Scope? = null, pos: Pos = this.pos): ModuleScope {
|
||||
val provider = seedScope?.currentImportProvider ?: defaultImportManager
|
||||
val module = provider.newModuleAt(pos)
|
||||
prepareScriptScope(module, seedScope ?: module)
|
||||
return module
|
||||
}
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
scope.pos = pos
|
||||
val execScope = resolveModuleScope(scope) ?: scope
|
||||
@@ -75,6 +99,19 @@ class Script(
|
||||
return ObjVoid
|
||||
}
|
||||
|
||||
private suspend fun prepareScriptScope(scope: Scope, seedScope: Scope) {
|
||||
if (importBindings.isNotEmpty() || importedModules.isNotEmpty()) {
|
||||
seedImportBindings(scope, seedScope)
|
||||
}
|
||||
if (moduleSlotPlan.isNotEmpty()) {
|
||||
scope.applySlotPlan(moduleSlotPlan)
|
||||
for (name in moduleSlotPlan.keys) {
|
||||
val record = scope.objects[name] ?: scope.localBindings[name] ?: continue
|
||||
scope.updateSlotFor(name, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun seedModuleSlots(scope: Scope, seedScope: Scope) {
|
||||
if (importBindings.isEmpty() && importedModules.isEmpty()) return
|
||||
seedImportBindings(scope, seedScope)
|
||||
@@ -505,10 +542,6 @@ class Script(
|
||||
cachedValue
|
||||
}
|
||||
}
|
||||
addFn("lazy") {
|
||||
val builder = requireOnlyArg<Obj>()
|
||||
ObjLazyDelegate(builder, requireScope())
|
||||
}
|
||||
addVoidFn("delay") {
|
||||
val a = args.firstAndOnly()
|
||||
when (a) {
|
||||
@@ -696,6 +729,36 @@ class Script(
|
||||
module.addConst("ChangeRejectionException", ObjChangeRejectionExceptionClass)
|
||||
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") {
|
||||
it.addConstDoc(
|
||||
name = "Buffer",
|
||||
|
||||
@@ -24,6 +24,7 @@ class VarDeclStatement(
|
||||
val name: String,
|
||||
val isMutable: Boolean,
|
||||
val visibility: Visibility,
|
||||
val actualExtern: Boolean,
|
||||
val initializer: Statement?,
|
||||
val isTransient: Boolean,
|
||||
val typeDecl: TypeDecl?,
|
||||
|
||||
@@ -42,6 +42,7 @@ class BytecodeCompiler(
|
||||
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
||||
private val externCallableNames: Set<String> = emptySet(),
|
||||
private val externBindingNames: Set<String> = emptySet(),
|
||||
private val lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
|
||||
) {
|
||||
private val useScopeSlots: Boolean = allowedScopeNames != null || scopeSlotNameSet != null
|
||||
@@ -1088,7 +1089,6 @@ class BytecodeCompiler(
|
||||
|
||||
private fun isDelegateClass(receiverClass: ObjClass): Boolean =
|
||||
receiverClass.className == "Delegate" ||
|
||||
receiverClass.className == "LazyDelegate" ||
|
||||
receiverClass.implementingNames.contains("Delegate")
|
||||
|
||||
private fun operatorMemberName(op: BinOp): String? = when (op) {
|
||||
@@ -4508,7 +4508,7 @@ class BytecodeCompiler(
|
||||
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
|
||||
return CompiledValue(slot, resolved)
|
||||
}
|
||||
if (useScopeSlots && allowedScopeNames?.contains(name) == true) {
|
||||
if (useScopeSlots && isPreparedScopeName(name)) {
|
||||
scopeSlotIndexByName[name]?.let { slot ->
|
||||
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
|
||||
return CompiledValue(slot, resolved)
|
||||
@@ -8073,7 +8073,7 @@ class BytecodeCompiler(
|
||||
slotInitClassByKey[ScopeSlotKey(scopeId, slotIndex)] = cls
|
||||
}
|
||||
}
|
||||
if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId)) {
|
||||
if (allowLocalSlots && slotIndex != null && (stmt.actualExtern || !shouldUseScopeSlotFor(scopeId, stmt.name, isDelegated = false))) {
|
||||
val key = ScopeSlotKey(scopeId, slotIndex)
|
||||
declaredLocalKeys.add(key)
|
||||
if (!localSlotInfoMap.containsKey(key)) {
|
||||
@@ -8100,7 +8100,7 @@ class BytecodeCompiler(
|
||||
val scopeId = stmt.spec.scopeId ?: 0
|
||||
if (slotIndex != null) {
|
||||
val key = ScopeSlotKey(scopeId, slotIndex)
|
||||
if (allowLocalSlots && !shouldUseScopeSlotFor(scopeId)) {
|
||||
if (allowLocalSlots && !shouldUseScopeSlotFor(scopeId, stmt.spec.name, isDelegated = false)) {
|
||||
if (!localSlotInfoMap.containsKey(key)) {
|
||||
localSlotInfoMap[key] = LocalSlotInfo(stmt.spec.name, isMutable = false, isDelegated = false)
|
||||
}
|
||||
@@ -8117,7 +8117,7 @@ class BytecodeCompiler(
|
||||
is DelegatedVarDeclStatement -> {
|
||||
val slotIndex = stmt.slotIndex
|
||||
val scopeId = stmt.scopeId ?: 0
|
||||
if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId)) {
|
||||
if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId, stmt.name, isDelegated = true)) {
|
||||
val key = ScopeSlotKey(scopeId, slotIndex)
|
||||
declaredLocalKeys.add(key)
|
||||
if (!localSlotInfoMap.containsKey(key)) {
|
||||
@@ -8283,15 +8283,26 @@ class BytecodeCompiler(
|
||||
|
||||
private fun isModuleSlot(scopeId: Int, name: String?): Boolean {
|
||||
if (moduleScopeId != null && scopeId != moduleScopeId) return false
|
||||
val scopeNames = allowedScopeNames ?: scopeSlotNameSet
|
||||
if (scopeNames == null || name == null) return false
|
||||
return scopeNames.contains(name)
|
||||
return isPreparedScopeName(name)
|
||||
}
|
||||
|
||||
private fun shouldUseScopeSlotFor(scopeId: Int): Boolean {
|
||||
return useScopeSlots && moduleScopeId != null && scopeId == moduleScopeId
|
||||
}
|
||||
|
||||
private fun shouldUseScopeSlotFor(scopeId: Int, name: String, isDelegated: Boolean): Boolean {
|
||||
if (moduleScopeId == null || scopeId != moduleScopeId) return false
|
||||
if (isDelegated) return false
|
||||
if (externBindingNames.contains(name)) return true
|
||||
return useScopeSlots && isPreparedScopeName(name)
|
||||
}
|
||||
|
||||
private fun isPreparedScopeName(name: String?): Boolean {
|
||||
if (name == null) return false
|
||||
if (scopeSlotNameSet?.contains(name) == true) return true
|
||||
return allowedScopeNames?.contains(name) == true
|
||||
}
|
||||
|
||||
private fun collectLoopVarNames(stmt: Statement) {
|
||||
if (stmt is BytecodeStatement) {
|
||||
collectLoopVarNames(stmt.original)
|
||||
@@ -8400,8 +8411,9 @@ class BytecodeCompiler(
|
||||
captureSlotKeys.add(key)
|
||||
return
|
||||
}
|
||||
val forceScopeSlot = shouldUseScopeSlotFor(scopeId, ref.name, ref.isDelegated)
|
||||
val isModuleSlot = if (ref.isDelegated) false else isModuleSlot(scopeId, ref.name)
|
||||
if (allowLocalSlots && !isModuleSlot) {
|
||||
if (allowLocalSlots && !isModuleSlot && !forceScopeSlot) {
|
||||
if (!localSlotInfoMap.containsKey(key)) {
|
||||
localSlotInfoMap[key] = LocalSlotInfo(ref.name, ref.isMutable, ref.isDelegated)
|
||||
}
|
||||
@@ -8448,8 +8460,9 @@ class BytecodeCompiler(
|
||||
}
|
||||
captureSlotKeys.add(key)
|
||||
} else {
|
||||
val forceScopeSlot = shouldUseScopeSlotFor(scopeId, target.name, target.isDelegated)
|
||||
val isModuleSlot = if (target.isDelegated) false else isModuleSlot(scopeId, target.name)
|
||||
if (allowLocalSlots && !isModuleSlot) {
|
||||
if (allowLocalSlots && !isModuleSlot && !forceScopeSlot) {
|
||||
if (!localSlotInfoMap.containsKey(key)) {
|
||||
localSlotInfoMap[key] = LocalSlotInfo(target.name, target.isMutable, target.isDelegated)
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ class BytecodeStatement private constructor(
|
||||
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
||||
externCallableNames: Set<String> = emptySet(),
|
||||
externBindingNames: Set<String> = emptySet(),
|
||||
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
|
||||
slotTypeDeclByScopeId: Map<Int, Map<Int, TypeDecl>> = emptyMap(),
|
||||
): Statement {
|
||||
@@ -122,6 +123,7 @@ class BytecodeStatement private constructor(
|
||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||
callableReturnTypeByName = callableReturnTypeByName,
|
||||
externCallableNames = externCallableNames,
|
||||
externBindingNames = externBindingNames,
|
||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||
)
|
||||
val compiled = compiler.compileStatement(nameHint, statement)
|
||||
@@ -236,6 +238,7 @@ class BytecodeStatement private constructor(
|
||||
stmt.name,
|
||||
stmt.isMutable,
|
||||
stmt.visibility,
|
||||
stmt.actualExtern,
|
||||
stmt.initializer?.let { unwrapDeep(it) },
|
||||
stmt.isTransient,
|
||||
stmt.typeDecl,
|
||||
|
||||
@@ -2392,8 +2392,13 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() {
|
||||
?: error("DECL_LOCAL expects LocalDecl at $constId")
|
||||
if (slot < frame.fn.scopeSlotCount) {
|
||||
val target = frame.scopeTarget(slot)
|
||||
frame.ensureScopeSlot(target, slot)
|
||||
val value = frame.slotToObj(slot).byValueCopy()
|
||||
val index = frame.ensureScopeSlot(target, slot)
|
||||
val raw = target.getSlotRecord(index).value
|
||||
val value = when (raw) {
|
||||
is FrameSlotRef -> raw.read()
|
||||
is RecordSlotRef -> raw.read()
|
||||
else -> raw
|
||||
}.byValueCopy()
|
||||
target.updateSlotFor(
|
||||
decl.name,
|
||||
ObjRecord(
|
||||
@@ -3930,7 +3935,7 @@ class CmdFrame(
|
||||
} else {
|
||||
val raw = frame.getRawObj(localIndex)
|
||||
if (raw == null && name != null) {
|
||||
val record = scope.get(name)
|
||||
val record = findNamedExistingRecord(scope, name)
|
||||
if (record != null) {
|
||||
val value = record.value
|
||||
return@mapIndexed when (value) {
|
||||
@@ -3939,6 +3944,12 @@ class CmdFrame(
|
||||
else -> ObjRecord(value, isMutable)
|
||||
}
|
||||
}
|
||||
if (hasNamedScopeBinding(scope, name)) {
|
||||
throw ScriptError(
|
||||
ensureScope().pos,
|
||||
"captured binding '$name' is not available in the execution scope; prepare the script imports/module bindings explicitly"
|
||||
)
|
||||
}
|
||||
}
|
||||
when (raw) {
|
||||
is FrameSlotRef -> ObjRecord(raw, isMutable)
|
||||
@@ -3952,22 +3963,27 @@ class CmdFrame(
|
||||
val target = moduleScope
|
||||
val name = captureNames?.getOrNull(index)
|
||||
if (name != null) {
|
||||
target.tryGetLocalRecord(target, name, target.currentClassCtx)?.let { return@mapIndexed it }
|
||||
target.getSlotIndexOf(name)?.let { return@mapIndexed target.getSlotRecord(it) }
|
||||
target.get(name)?.let { return@mapIndexed it }
|
||||
findNamedExistingRecord(target, name)?.let { return@mapIndexed it }
|
||||
// Fallback to current scope in case the module scope isn't in the parent chain
|
||||
// or doesn't carry the imported symbol yet.
|
||||
scope.tryGetLocalRecord(scope, name, scope.currentClassCtx)?.let { return@mapIndexed it }
|
||||
scope.get(name)?.let { return@mapIndexed it }
|
||||
findNamedExistingRecord(scope, name)?.let { return@mapIndexed it }
|
||||
}
|
||||
if (slotId < target.slotCount) {
|
||||
return@mapIndexed target.getSlotRecord(slotId)
|
||||
val existing = target.getSlotRecord(slotId)
|
||||
if (name == null || existing.value !== ObjUnset || hasResolvedNamedScopeBinding(target, name)) {
|
||||
return@mapIndexed existing
|
||||
}
|
||||
}
|
||||
if (name != null) {
|
||||
target.applySlotPlan(mapOf(name to slotId))
|
||||
return@mapIndexed target.getSlotRecord(slotId)
|
||||
throw ScriptError(
|
||||
ensureScope().pos,
|
||||
"module capture '$name' is not available in the execution scope; prepare the script imports/module bindings explicitly"
|
||||
)
|
||||
}
|
||||
error("Missing module capture slot $slotId")
|
||||
throw ScriptError(
|
||||
ensureScope().pos,
|
||||
"missing module capture slot $slotId"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4546,7 +4562,7 @@ class CmdFrame(
|
||||
return getScopeSlotValueAtAddr(addrSlot)
|
||||
}
|
||||
|
||||
fun setAddrObj(addrSlot: Int, value: Obj) {
|
||||
suspend fun setAddrObj(addrSlot: Int, value: Obj) {
|
||||
setScopeSlotValueAtAddr(addrSlot, value)
|
||||
}
|
||||
|
||||
@@ -4554,7 +4570,7 @@ class CmdFrame(
|
||||
return getScopeSlotValueAtAddr(addrSlot).toLong()
|
||||
}
|
||||
|
||||
fun setAddrInt(addrSlot: Int, value: Long) {
|
||||
suspend fun setAddrInt(addrSlot: Int, value: Long) {
|
||||
setScopeSlotValueAtAddr(addrSlot, ObjInt.of(value))
|
||||
}
|
||||
|
||||
@@ -4562,7 +4578,7 @@ class CmdFrame(
|
||||
return getScopeSlotValueAtAddr(addrSlot).toDouble()
|
||||
}
|
||||
|
||||
fun setAddrReal(addrSlot: Int, value: Double) {
|
||||
suspend fun setAddrReal(addrSlot: Int, value: Double) {
|
||||
setScopeSlotValueAtAddr(addrSlot, ObjReal.of(value))
|
||||
}
|
||||
|
||||
@@ -4570,7 +4586,7 @@ class CmdFrame(
|
||||
return getScopeSlotValueAtAddr(addrSlot).toBool()
|
||||
}
|
||||
|
||||
fun setAddrBool(addrSlot: Int, value: Boolean) {
|
||||
suspend fun setAddrBool(addrSlot: Int, value: Boolean) {
|
||||
setScopeSlotValueAtAddr(addrSlot, if (value) ObjTrue else ObjFalse)
|
||||
}
|
||||
|
||||
@@ -4776,12 +4792,13 @@ class CmdFrame(
|
||||
|
||||
private suspend fun getScopeSlotValue(slot: Int): Obj {
|
||||
val target = scopeTarget(slot)
|
||||
val name = fn.scopeSlotNames[slot]
|
||||
val hadNamedBinding = name != null && hasResolvedNamedScopeBinding(target, name)
|
||||
val index = ensureScopeSlot(target, slot)
|
||||
val record = target.getSlotRecord(index)
|
||||
val direct = record.value
|
||||
if (direct is FrameSlotRef) return direct.read()
|
||||
if (direct is RecordSlotRef) return direct.read()
|
||||
val name = fn.scopeSlotNames[slot]
|
||||
if (name != null && record.memberName != null && record.memberName != name) {
|
||||
val resolved = target.get(name)
|
||||
if (resolved != null) {
|
||||
@@ -4802,9 +4819,15 @@ class CmdFrame(
|
||||
return direct
|
||||
}
|
||||
if (name == null) return record.value
|
||||
val resolved = target.get(name) ?: return record.value
|
||||
val resolved = target.get(name)
|
||||
if (resolved == null) {
|
||||
failMissingPreparedModuleBinding(slot, name, hadNamedBinding, record)
|
||||
return record.value
|
||||
}
|
||||
if (resolved.value !== ObjUnset) {
|
||||
target.updateSlotFor(name, resolved)
|
||||
} else {
|
||||
failMissingPreparedModuleBinding(slot, name, hadNamedBinding, resolved)
|
||||
}
|
||||
return resolved.value
|
||||
}
|
||||
@@ -4812,12 +4835,13 @@ class CmdFrame(
|
||||
private suspend fun getScopeSlotValueAtAddr(addrSlot: Int): Obj {
|
||||
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
|
||||
val index = addrIndices[addrSlot]
|
||||
val slotId = addrScopeSlots[addrSlot]
|
||||
val name = fn.scopeSlotNames.getOrNull(slotId)
|
||||
val hadNamedBinding = name != null && hasResolvedNamedScopeBinding(target, name)
|
||||
val record = target.getSlotRecord(index)
|
||||
val direct = record.value
|
||||
if (direct is FrameSlotRef) return direct.read()
|
||||
if (direct is RecordSlotRef) return direct.read()
|
||||
val slotId = addrScopeSlots[addrSlot]
|
||||
val name = fn.scopeSlotNames.getOrNull(slotId)
|
||||
if (name != null && record.memberName != null && record.memberName != name) {
|
||||
val resolved = target.get(name)
|
||||
if (resolved != null) {
|
||||
@@ -4838,16 +4862,29 @@ class CmdFrame(
|
||||
return direct
|
||||
}
|
||||
if (name == null) return record.value
|
||||
val resolved = target.get(name) ?: return record.value
|
||||
val resolved = target.get(name)
|
||||
if (resolved == null) {
|
||||
failMissingPreparedModuleBinding(slotId, name, hadNamedBinding, record)
|
||||
return record.value
|
||||
}
|
||||
if (resolved.value !== ObjUnset) {
|
||||
target.updateSlotFor(name, resolved)
|
||||
} else {
|
||||
failMissingPreparedModuleBinding(slotId, name, hadNamedBinding, resolved)
|
||||
}
|
||||
return resolved.value
|
||||
}
|
||||
|
||||
private fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) {
|
||||
private suspend fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) {
|
||||
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
|
||||
val index = addrIndices[addrSlot]
|
||||
val record = target.getSlotRecord(index)
|
||||
val slotId = addrScopeSlots[addrSlot]
|
||||
val name = fn.scopeSlotNames.getOrNull(slotId)
|
||||
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) {
|
||||
target.assign(record, name, value)
|
||||
return
|
||||
}
|
||||
target.setSlotValue(index, value)
|
||||
}
|
||||
|
||||
@@ -4875,6 +4912,41 @@ class CmdFrame(
|
||||
return index
|
||||
}
|
||||
|
||||
private fun hasNamedScopeBinding(target: Scope, name: String): Boolean {
|
||||
if (target.tryGetLocalRecord(target, name, target.currentClassCtx) != null) return true
|
||||
if (target.getSlotIndexOf(name) != null) return true
|
||||
if (target.get(name) != null) return true
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasResolvedNamedScopeBinding(target: Scope, name: String): Boolean =
|
||||
findNamedExistingRecord(target, name) != null
|
||||
|
||||
private fun findNamedExistingRecord(target: Scope, name: String): ObjRecord? {
|
||||
target.tryGetLocalRecord(target, name, target.currentClassCtx)?.let { return it }
|
||||
target.get(name)?.let { record ->
|
||||
if (record.value !== ObjUnset || record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
|
||||
return record
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun failMissingPreparedModuleBinding(
|
||||
slot: Int,
|
||||
name: String,
|
||||
hadNamedBinding: Boolean,
|
||||
record: ObjRecord
|
||||
) {
|
||||
if (hadNamedBinding) return
|
||||
if (record.value !== ObjUnset) return
|
||||
if (fn.scopeSlotIsModule.getOrNull(slot) != true) return
|
||||
throw ScriptError(
|
||||
ensureScope().pos,
|
||||
"module binding '$name' is not available in the execution scope; prepare the script imports/module bindings explicitly"
|
||||
)
|
||||
}
|
||||
|
||||
private fun ensureLocalMutable(localIndex: Int) {
|
||||
val name = fn.localSlotNames.getOrNull(localIndex) ?: return
|
||||
val isMutable = fn.localSlotMutables.getOrNull(localIndex) ?: true
|
||||
|
||||
@@ -36,7 +36,7 @@ internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) {
|
||||
} else {
|
||||
record.value
|
||||
}
|
||||
if (value is net.sergeych.lyng.FrameSlotRef && value.refersTo(frame.frame, base + i)) {
|
||||
if (value is net.sergeych.lyng.FrameSlotRef && value.refersTo(frame.frame, i)) {
|
||||
continue
|
||||
}
|
||||
frame.setObjUnchecked(base + i, value)
|
||||
|
||||
@@ -25,6 +25,8 @@ import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.serializer
|
||||
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.addFnDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
@@ -175,7 +177,8 @@ open class Obj {
|
||||
if (other === this) return 0
|
||||
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -284,7 +287,8 @@ open class Obj {
|
||||
}
|
||||
if (self !== this) return self.plus(scope, 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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,10 +106,20 @@ open class ObjClass(
|
||||
vararg parents: ObjClass,
|
||||
) : Obj() {
|
||||
|
||||
private fun declaringModulePackageName(scope: Scope?): String? {
|
||||
var current = scope
|
||||
while (current != null) {
|
||||
if (current is ModuleScope) return current.packageName
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var isAnonymous: Boolean = false
|
||||
|
||||
var isAbstract: Boolean = false
|
||||
var isClosed: Boolean = false
|
||||
var logicalPackageNameOverride: String? = null
|
||||
|
||||
// Stable identity and simple structural version for PICs
|
||||
val classId: Long = ClassIdGen.nextId()
|
||||
@@ -169,6 +179,16 @@ open class ObjClass(
|
||||
*/
|
||||
var classScope: Scope? = null
|
||||
|
||||
/**
|
||||
* Stable logical identity for class matching across separately instantiated modules.
|
||||
* Uses declaring module package plus class name when available.
|
||||
*/
|
||||
val logicalPackageName: String?
|
||||
get() = logicalPackageNameOverride ?: declaringModulePackageName(classScope)
|
||||
|
||||
val logicalName: String
|
||||
get() = logicalPackageName?.let { "$it::$className" } ?: className
|
||||
|
||||
/** Direct parents in declaration order (kept deterministic). */
|
||||
val directParents: List<ObjClass> = parents.toList()
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
|
||||
}
|
||||
|
||||
val type = object : ObjClass("Delegate") {}.apply {
|
||||
logicalPackageNameOverride = "lyng.stdlib"
|
||||
addFn("getValue") { raiseError("Delegate.getValue is not implemented") }
|
||||
addFn("setValue") { raiseError("Delegate.setValue is not implemented") }
|
||||
addFn("invoke") { raiseError("Delegate.invoke is not implemented") }
|
||||
|
||||
@@ -364,6 +364,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
onNotFoundResult: (suspend () -> Obj?)?
|
||||
): Obj {
|
||||
val caller = scope.currentClassCtx
|
||||
val methodScope = scope.applyClosure(instanceScope)
|
||||
|
||||
// Fast path for public members when outside any class context
|
||||
if (caller == null) {
|
||||
@@ -373,7 +374,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||
} 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 (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||
} 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 (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||
} 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 (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||
} 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 (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||
} 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 (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||
} 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 effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
|
||||
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)
|
||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||
return rec.value.invoke(
|
||||
instanceScope,
|
||||
methodScope,
|
||||
this,
|
||||
args,
|
||||
decl
|
||||
@@ -586,8 +587,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if (other !is ObjInstance) return -1
|
||||
if (other.objClass != objClass) return -1
|
||||
if (other !is ObjInstance || other.objClass != objClass) {
|
||||
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||
return -1
|
||||
}
|
||||
for (f in comparableVars) {
|
||||
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")
|
||||
|
||||
@@ -19,6 +19,8 @@ package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.InteropOperator
|
||||
import net.sergeych.lyng.OperatorInteropRegistry
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.miniast.addFnDoc
|
||||
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 {
|
||||
if (other !is Numeric) return -2
|
||||
return if (other is ObjInt) {
|
||||
value.compareTo(other.value)
|
||||
} else {
|
||||
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||
if (other !is Numeric) return -2
|
||||
doubleValue.compareTo(other.doubleValue)
|
||||
}
|
||||
}
|
||||
@@ -70,28 +73,39 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
||||
if (other is ObjInt)
|
||||
of(this.value + other.value)
|
||||
else
|
||||
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus)
|
||||
?:
|
||||
ObjReal.of(this.doubleValue + other.toDouble())
|
||||
|
||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt)
|
||||
of(this.value - other.value)
|
||||
else
|
||||
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus)
|
||||
?:
|
||||
ObjReal.of(this.doubleValue - other.toDouble())
|
||||
|
||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||
if (other is ObjInt) {
|
||||
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 =
|
||||
if (other is ObjInt)
|
||||
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 =
|
||||
if (other is ObjInt)
|
||||
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.
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Statement
|
||||
import net.sergeych.lyng.Visibility
|
||||
import net.sergeych.lyng.executeBytecodeWithSeed
|
||||
|
||||
/**
|
||||
* Lazy delegate used by `val x by lazy { ... }`.
|
||||
*/
|
||||
class ObjLazyDelegate(
|
||||
private val builder: Obj,
|
||||
private val capturedScope: Scope,
|
||||
) : Obj() {
|
||||
override val objClass: ObjClass = type
|
||||
|
||||
private var calculated = false
|
||||
private var cachedValue: Obj = ObjVoid
|
||||
|
||||
override suspend fun invokeInstanceMethod(
|
||||
scope: Scope,
|
||||
name: String,
|
||||
args: Arguments,
|
||||
onNotFoundResult: (suspend () -> Obj?)?,
|
||||
): Obj {
|
||||
return when (name) {
|
||||
"getValue" -> {
|
||||
if (!calculated) {
|
||||
val callScope = capturedScope.createChildScope(capturedScope.pos, args = Arguments.EMPTY)
|
||||
cachedValue = if (builder is Statement) {
|
||||
executeBytecodeWithSeed(callScope, builder, "lazy delegate")
|
||||
} else {
|
||||
builder.callOn(callScope)
|
||||
}
|
||||
calculated = true
|
||||
}
|
||||
cachedValue
|
||||
}
|
||||
"setValue" -> scope.raiseIllegalAssignment("lazy delegate is read-only")
|
||||
else -> super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("LazyDelegate").apply {
|
||||
implementingNames.add("Delegate")
|
||||
createField(
|
||||
"getValue",
|
||||
ObjNull,
|
||||
isMutable = false,
|
||||
visibility = Visibility.Public,
|
||||
pos = Pos.builtIn,
|
||||
declaringClass = this,
|
||||
type = ObjRecord.Type.Fun
|
||||
)
|
||||
createField(
|
||||
"setValue",
|
||||
ObjNull,
|
||||
isMutable = false,
|
||||
visibility = Visibility.Public,
|
||||
pos = Pos.builtIn,
|
||||
declaringClass = this,
|
||||
type = ObjRecord.Type.Fun
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.InteropOperator
|
||||
import net.sergeych.lyng.OperatorInteropRegistry
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.miniast.addConstDoc
|
||||
import net.sergeych.lyng.miniast.addFnDoc
|
||||
@@ -40,6 +42,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
override fun byValueCopy(): Obj = this
|
||||
|
||||
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
|
||||
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 =
|
||||
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 =
|
||||
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 =
|
||||
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 =
|
||||
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 =
|
||||
of(this.value % other.toDouble())
|
||||
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod)
|
||||
?: of(this.value % other.toDouble())
|
||||
|
||||
/**
|
||||
* 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,88 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.ScriptError
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.asFacade
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import net.sergeych.lyng.obj.toInt
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CompilerVmReviewRegressionTest {
|
||||
|
||||
@Test
|
||||
fun missingModuleCaptureFailsFastInsteadOfBecomingUnset() = runTest {
|
||||
val manager = ImportManager()
|
||||
manager.addTextPackages(
|
||||
"""
|
||||
package foo
|
||||
|
||||
val answer = 42
|
||||
""".trimIndent()
|
||||
)
|
||||
val script = Compiler.compile(
|
||||
Source(
|
||||
"<missing-module-capture>",
|
||||
"""
|
||||
import foo
|
||||
fun make() = { answer }
|
||||
make()
|
||||
""".trimIndent()
|
||||
),
|
||||
manager
|
||||
)
|
||||
|
||||
val prepared = manager.newModule()
|
||||
script.importInto(prepared)
|
||||
val preparedLambda = script.execute(prepared)
|
||||
assertEquals(42, prepared.asFacade().call(preparedLambda).toInt())
|
||||
|
||||
val rawModule = manager.newModule()
|
||||
val hostScope = Scope(parent = rawModule, thisObj = ObjString("receiver"))
|
||||
|
||||
val lambda = script.execute(hostScope)
|
||||
val ex = assertFailsWith<ScriptError> {
|
||||
hostScope.asFacade().call(lambda)
|
||||
}
|
||||
assertContains(ex.errorMessage, "module binding 'answer'")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun subjectlessWhenReportsScriptError() = runTest {
|
||||
val ex = assertFailsWith<ScriptError> {
|
||||
Compiler.compile(
|
||||
Source(
|
||||
"<when-without-subject>",
|
||||
"""
|
||||
when {
|
||||
true -> 1
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
Script.defaultImportManager
|
||||
)
|
||||
}
|
||||
assertContains(ex.errorMessage, "when without subject")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 net.sergeych.lyng.bridge.bindGlobalVar
|
||||
import net.sergeych.lyng.bridge.globalBinder
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class GlobalPropertyCaptureRegressionTest {
|
||||
@Test
|
||||
fun externGlobalVarAssignmentInsideFunctionShouldCallBoundSetter() = runTest {
|
||||
val scope = Script.newScope()
|
||||
var x = 1.0
|
||||
|
||||
scope.eval(
|
||||
"""
|
||||
extern var X: Real
|
||||
|
||||
fun main() {
|
||||
X = X + 1.0
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
scope.globalBinder().bindGlobalVar(
|
||||
name = "X",
|
||||
get = { x },
|
||||
set = { x = it }
|
||||
)
|
||||
|
||||
scope.eval("main()")
|
||||
|
||||
assertEquals(2.0, x, "bound extern var should stay live inside function bodies")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.obj.toInt
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ScriptImportPreparationTest {
|
||||
|
||||
@Test
|
||||
fun scriptImportIntoExplicitlyPreparesExistingScope() = runTest {
|
||||
val manager = ImportManager()
|
||||
manager.addTextPackages(
|
||||
"""
|
||||
package foo
|
||||
|
||||
val answer = 42
|
||||
""".trimIndent()
|
||||
)
|
||||
val script = Compiler.compile(
|
||||
Source(
|
||||
"<prepare-scope>",
|
||||
"""
|
||||
import foo
|
||||
answer
|
||||
""".trimIndent()
|
||||
),
|
||||
manager
|
||||
)
|
||||
val scope = manager.newModule()
|
||||
|
||||
assertNull(scope["answer"])
|
||||
|
||||
script.importInto(scope)
|
||||
|
||||
val record = assertNotNull(scope["answer"])
|
||||
assertEquals(42, scope.resolve(record, "answer").toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scriptInstantiateModuleUsesSeedScopeImportProvider() = runTest {
|
||||
val manager = ImportManager()
|
||||
manager.addTextPackages(
|
||||
"""
|
||||
package foo
|
||||
|
||||
val answer = 42
|
||||
""".trimIndent()
|
||||
)
|
||||
val script = Compiler.compile(
|
||||
Source(
|
||||
"<instantiate-module>",
|
||||
"""
|
||||
import foo
|
||||
answer
|
||||
""".trimIndent()
|
||||
),
|
||||
manager
|
||||
)
|
||||
val seedScope = manager.newModule()
|
||||
|
||||
val module = script.instantiateModule(seedScope)
|
||||
|
||||
val record = assertNotNull(module["answer"])
|
||||
assertEquals(42, module.resolve(record, "answer").toInt())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.FrameSlotRef
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.bytecode.BytecodeConst
|
||||
import net.sergeych.lyng.bytecode.CmdFrame
|
||||
import net.sergeych.lyng.bytecode.CmdFunction
|
||||
import net.sergeych.lyng.bytecode.CmdVm
|
||||
import net.sergeych.lyng.bytecode.seedFrameLocalsFromScope
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class SeedLocalsRegressionTest {
|
||||
|
||||
@Test
|
||||
fun seedFrameLocalsSkipsSelfReferentialFrameSlotRef() = runTest {
|
||||
val fn = CmdFunction(
|
||||
name = "seed-self-ref",
|
||||
localCount = 1,
|
||||
addrCount = 0,
|
||||
returnLabels = emptySet(),
|
||||
scopeSlotCount = 1,
|
||||
scopeSlotIndices = intArrayOf(0),
|
||||
scopeSlotNames = arrayOf(null),
|
||||
scopeSlotIsModule = booleanArrayOf(false),
|
||||
localSlotNames = arrayOf("x"),
|
||||
localSlotMutables = booleanArrayOf(true),
|
||||
localSlotDelegated = booleanArrayOf(false),
|
||||
localSlotCaptures = booleanArrayOf(false),
|
||||
constants = listOf(BytecodeConst.IntVal(0)),
|
||||
cmds = emptyArray(),
|
||||
posByIp = emptyArray()
|
||||
)
|
||||
val scope = Scope(null, pos = Pos.builtIn)
|
||||
val record = scope.addConst("x", net.sergeych.lyng.obj.ObjInt.of(1))
|
||||
val frame = CmdFrame(CmdVm(), fn, scope, emptyList())
|
||||
record.value = FrameSlotRef(frame.frame, 0)
|
||||
scope.updateSlotFor("x", record)
|
||||
|
||||
seedFrameLocalsFromScope(frame, scope)
|
||||
|
||||
assertNull(frame.frame.getRawObj(0))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,96 @@ class DelegationTest {
|
||||
assertTrue(badThrown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPureLyngLazyPreservesReceiverAndClosure() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval(
|
||||
"""
|
||||
val GLOBAL_NUMBERS = [1,2,3]
|
||||
|
||||
class PureLazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
|
||||
private val creator: ThisRefType.()->T = creatorParam
|
||||
private var value = Unset
|
||||
|
||||
override fun bind(name: String, access, thisRef: ThisRefType): Object = this
|
||||
|
||||
override fun getValue(thisRef: ThisRefType, name: String): T {
|
||||
if (value == Unset)
|
||||
value = creator(thisRef)
|
||||
value as T
|
||||
}
|
||||
}
|
||||
|
||||
fun pureLazy<T,ThisRefType=Object>(creator: ThisRefType.()->T): Delegate<T,ThisRefType> = PureLazy(creator)
|
||||
|
||||
class A {
|
||||
val numbers = [1,2,3]
|
||||
val fromThis: List by pureLazy { this.numbers }
|
||||
val fromScope: List by pureLazy { GLOBAL_NUMBERS }
|
||||
}
|
||||
|
||||
class B {
|
||||
val a: A by pureLazy { A() }
|
||||
val test: List by pureLazy { (a as A).fromThis + [4] }
|
||||
}
|
||||
|
||||
assertEquals([1,2,3], A().fromThis)
|
||||
assertEquals([1,2,3], A().fromScope)
|
||||
assertEquals([1,2,3,4], B().test)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testImportedPureLyngLazyPreservesReceiverAndClosure() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.importManager.addTextPackages(
|
||||
"""
|
||||
package repro.lazy
|
||||
|
||||
import lyng.stdlib
|
||||
|
||||
class PureLazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
|
||||
private val creator: ThisRefType.()->T = creatorParam
|
||||
private var value = Unset
|
||||
|
||||
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
|
||||
if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
|
||||
this
|
||||
}
|
||||
|
||||
override fun getValue(thisRef: ThisRefType, name: String): T {
|
||||
if (value == Unset)
|
||||
value = with(thisRef, creator)
|
||||
value as T
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
scope.eval(
|
||||
"""
|
||||
import repro.lazy
|
||||
|
||||
val GLOBAL_NUMBERS = [1,2,3]
|
||||
|
||||
class A {
|
||||
val numbers = [1,2,3]
|
||||
val fromThis: List by PureLazy { this.numbers }
|
||||
val fromScope: List by PureLazy { GLOBAL_NUMBERS }
|
||||
}
|
||||
|
||||
class B {
|
||||
val a: A by PureLazy { A() }
|
||||
val test: List by PureLazy { (a as A).fromThis + [4] }
|
||||
}
|
||||
|
||||
assertEquals([1,2,3], A().fromThis)
|
||||
assertEquals([1,2,3], A().fromScope)
|
||||
assertEquals([1,2,3,4], B().test)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLazyIsDelegate() = runTest {
|
||||
eval("""
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -421,7 +421,6 @@ fun with<T,R>(self: T, block: T.()->R): R {
|
||||
Can only be used with 'val' properties.
|
||||
*/
|
||||
class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
|
||||
private val creator: ThisRefType.()->T = creatorParam
|
||||
private var value = Unset
|
||||
|
||||
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
|
||||
@@ -431,7 +430,7 @@ class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,T
|
||||
|
||||
override fun getValue(thisRef: ThisRefType, name: String): T {
|
||||
if (value == Unset)
|
||||
value = with(thisRef,creator)
|
||||
value = with(thisRef, creatorParam)
|
||||
value as T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# :lynglib Compiler and VM Review (2026-03-26)
|
||||
|
||||
## Scope
|
||||
- Reviewed `:lynglib` compiler and bytecode VM paths, focusing on compile/execution correctness.
|
||||
- Read core files around `Compiler`, `Script`, `BytecodeCompiler`, `CmdRuntime`, `Scope`, `ModuleScope`, and capture/slot plumbing.
|
||||
- Ran `./gradlew :lynglib:jvmTest` on 2026-03-26: PASS.
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. High: local seeding uses the wrong slot index when checking for self-referential `FrameSlotRef`
|
||||
- Status: fixed in worktree on 2026-03-26; covered by `SeedLocalsRegressionTest`.
|
||||
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt:39`
|
||||
- The self-reference guard compares `FrameSlotRef` against `base + i`, but `FrameSlotRef` stores the underlying `BytecodeFrame` local slot index, not the VM absolute slot id.
|
||||
- The same slot is then written using `setObjUnchecked(base + i, value)` at `SeedLocals.kt:42`, which means a self-reference is not filtered out before being reinserted.
|
||||
- Impact: this can seed a local with a reference to itself, creating recursive `FrameSlotRef` chains. Reads then recurse through `BytecodeFrame.getObj()` / `FrameSlotRef.read()` instead of resolving to a value, which is a realistic path to stack overflow or non-terminating execution in bytecode-only helper execution (`executeBytecodeWithSeed`).
|
||||
- Why this looks real: the equivalent check in `CmdFrame.applyCaptureRecords()` uses the local index directly (`CmdRuntime.kt:3867`), and `Script.seedModuleLocals()` also compares with the local index, not `scopeSlotCount + i` (`Script.kt:109`).
|
||||
- Suggested fix: compare with `i`, not `base + i`, and add a regression test around `executeBytecodeWithSeed` seeding a scope that already contains a `FrameSlotRef` to the same local.
|
||||
|
||||
### 2. Design note: script-specific import/module preparation should stay explicit, not hidden in `execute(scope)`
|
||||
- Status: resolved by API addition, without changing `Script.execute(scope)` semantics.
|
||||
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt:51-57`
|
||||
- `shouldSeedModule` is only true when `execScope` is a `ModuleScope` or `thisObj === ObjVoid`.
|
||||
- If a compiled script is executed against a non-module scope with a real receiver object, `seedModuleSlots()` is skipped entirely, so script imports and explicit module bindings are never installed into `moduleTarget`.
|
||||
- Original concern: top-level bytecode that expects imported names or module globals can see different behavior depending on whether the host prepared the target scope explicitly.
|
||||
- Resolution taken: do **not** change `Script.execute(scope)`, because in this codebase it is expected to run on exactly the provided scope and many embedding flows already rely on explicit scope setup via `Script.newScope()`, `Scope.eval(...)`, `addFn(...)`, `addConst(...)`, and direct import-manager configuration.
|
||||
- New explicit APIs added instead:
|
||||
- `Script.importInto(scope, seedScope = scope)`
|
||||
- `Script.instantiateModule(seedScope = null, pos = script.pos)`
|
||||
- This keeps existing execution behavior stable while giving hosts an opt-in way to apply a script's import/module bindings when they actually want that preparation step.
|
||||
- Coverage added: `ScriptImportPreparationTest`.
|
||||
|
||||
### 3. Medium: missing module captures are silently converted into fresh `Unset` slots
|
||||
- Status: fixed in worktree on 2026-03-27; covered by `CompilerVmReviewRegressionTest`.
|
||||
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt:3950-3970`
|
||||
- In `CmdFrame.buildCaptureRecords()`, the module-capture path first tries several lookups. If the requested `slotId` is missing but the capture has a `name`, it calls `target.applySlotPlan(mapOf(name to slotId))` and immediately returns `target.getSlotRecord(slotId)`.
|
||||
- That record is a newly created placeholder from `Scope.applySlotPlan()` (`Scope.kt:460-471`) and defaults to `ObjUnset`.
|
||||
- Impact: a compiler/runtime disagreement in capture resolution is masked as a normal capture of `Unset`, so the failure moves far away from closure creation and becomes data corruption or an unrelated later exception. This will be difficult to debug when it happens.
|
||||
- Resolution taken: the VM now refuses to treat synthetic placeholder slots as real module/captured bindings. Reads and capture construction fail with a source-positioned `ScriptError` when the execution scope was not prepared with the script's required module/import bindings.
|
||||
|
||||
### 4. Medium: subject-less `when { ... }` still crashes through a raw Kotlin `TODO`
|
||||
- Status: fixed in worktree on 2026-03-27; covered by `CompilerVmReviewRegressionTest`.
|
||||
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:6447-6449`
|
||||
- The unsupported branch uses `TODO("when without object is not yet implemented")`.
|
||||
- Current docs explicitly say subject-less `when` is not implemented, so the language limitation itself is documented. The problem is the failure mode: the compiler throws a raw Kotlin `NotImplementedError` instead of a normal `ScriptError` or a feature diagnostic.
|
||||
- Impact: IDE/embedding callers get an implementation exception rather than a source-positioned language error, which is especially bad across non-JVM targets and for editor tooling.
|
||||
- Resolution taken: the parser now throws a normal `ScriptError` at the `when` position with an explicit "when without subject is not implemented" message.
|
||||
|
||||
## Risks Worth Checking Next
|
||||
|
||||
### 5. Medium risk: module frame growth only retargets records stored in `objects`/`localBindings`
|
||||
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt:39-67`
|
||||
- When `ensureModuleFrame()` replaces the old `BytecodeFrame` with a larger one, it retargets `FrameSlotRef`s only in `objects` and `localBindings`.
|
||||
- Existing closures/capture records that already hold `FrameSlotRef(oldFrame, slot)` are not retargeted here.
|
||||
- Impact: if a module scope survives across recompilation/re-execution with a larger local frame, previously exported closures can keep reading stale values from the old frame instance.
|
||||
- Confidence is lower because this depends on module reuse across differing compiled shapes, but the reference-retargeting logic is clearly incomplete for that scenario.
|
||||
- Suggested check: add a regression covering module re-execution with increased local count and an exported closure captured before the resize.
|
||||
|
||||
## Test Status
|
||||
- `./gradlew :lynglib:jvmTest` passed during this review.
|
||||
- `./gradlew :lynglib:jvmTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTest` passed after the fixes/API additions.
|
||||
- `./gradlew :lynglib:jvmTest --tests CompilerVmReviewRegressionTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTest` passed after fixing findings 3 and 4.
|
||||
- Finding 1 is covered directly; finding 2 is covered by explicit preparation API tests.
|
||||
|
||||
## Suggested Fix Order
|
||||
1. Fix finding 1 first: it is a concrete slot-index bug with likely recursive failure modes. Done.
|
||||
2. Keep `Script.execute(scope)` semantics stable and use explicit preparation APIs where script-owned import/module setup is needed. Done.
|
||||
3. Tighten finding 3 next: fail fast on capture mismatches. Done.
|
||||
4. Replace the raw `TODO` in finding 4 so unsupported syntax produces normal diagnostics. Done.
|
||||
5. Decide whether finding 5 matters for current module-reload workflows; add a regression before changing behavior.
|
||||
@@ -0,0 +1,46 @@
|
||||
WASM использует стековую модель памяти. Lyng стеки не использует. В чем разница?
|
||||
|
||||
Стек это очень древняя структура, была придумана в 1955 (магазинная память Бауэра и Самельсона), и приняла современную форму в 1960, благодаря Барбаре Лисокв, вполседствии лауреата премии Тьюринга, которого, в свою очередь, за выдающийся вклад в развитие IT кастрировали, формально, за гомосексуализм, но в британии, где на трех джентльменов приходилось тогда, и приходится и теперь, четыре гомосексуала, все понимают, что это был просто предлог.
|
||||
|
||||
Стек был прекрасен в 60е годы, когда IBM планировала продавать по компьютеру в год в лучшем случае, а за идеи о микропроцессорах или ядрах можно было устроиться только пациентом психушки. Стек это заранее жестко выделенная область памяти, которая обычно вообще не используется, и по ней в одну сторону растет используемая область, которая хранит локальные переменные, адреса возврата и иногда состояние процессора.
|
||||
|
||||
Она крайне плохо подходит для многозадачности. Представь себе что у тебя есть wasm процесс, со своим стеком, и ему надо переключиться, остановив текущий поток исполнения (например, он ждет ответа от сети). Тогда тебе придется создать _новый стек_ — пока еще пустой, переключиться на него и исполнять там другую задачу. Сколько потоков - столько и стеков. И все они по большей части не используются — там просто запас памяти "на вырост", если исполнение потребует. А оценить заранее адекватно почти невозможно, дают с запасом.
|
||||
|
||||
Далее, васм использует фиксированную память выделенную на процесс. Ее тоже приходится брать "с запасом", так как если по ходу не хватит, процесс вылетит. И она тоже в изрядной части пустая. Ну и назасладочку, васм был задуман однозадачным, и все попытки туда хотя бы треды засунуть, получаются очень кривыми.
|
||||
|
||||
Линг использует фреймы и сопрограммы. Линг вообще треды не использует. Основная команда исполнения программы на линге - сопрограмма (coroutine), ей не нужен стейк, она им не пользуется. Вместо этого каждый вызов в линге, грубо говоря, это вызов сопрограммы. Для него формируется фрейм (не на стеке, а в динамической памяти!), которого гарантировано достаточно для исполнения собственно кода. Он заменяет собой "стековый фрейм", но его размер известен заранее, и его можно распределять и освобождать проще, как обычную динамическую память.
|
||||
|
||||
Далее, линг использует ту же динамическую память, что и его родительская платформа. Если он работает на Java, то использует память со сборкой мусора JVM, на Javascript - память машины JS, в нативных машинах - специальнй менеджер памяти со сборкой мусора, от Kotlin Native (довольно хороший и очень быстро развивающийся). В результате, запустить одновременно десять, или сто тысяч программ на лигне, которые будут исполняться конкурентно, вполне реально. Просто попытка запустить 10к васм-машин на v8 или другом движке скорее всего убьет систему, да и 10 тысяч тредов редко какой сервер приложению даст. Огромный перерасход ресурсов и тормоза.
|
||||
|
||||
Далее. Васм СТЕКОВАЯ машина со стековыми командами, он без стека вообще не может. Посмотрим как он считает примитивное выражение:
|
||||
|
||||
```wasm
|
||||
(module
|
||||
;; Экспортируем функцию, чтобы её можно было вызвать из JS
|
||||
(func (export "calc") (param $a i32) (param $b i32) (param $c i32) (param $d i32) (result i32)
|
||||
;; --- вычисление (a + b) / c * d ---
|
||||
local.get $a ;; помещаем a на стек
|
||||
local.get $b ;; помещаем b на стек
|
||||
i32.add ;; складываем: стек содержит (a+b)
|
||||
|
||||
local.get $c ;; помещаем c
|
||||
i32.div_s ;; знаковое деление: стек содержит ((a+b) / c)
|
||||
|
||||
local.get $d ;; помещаем d
|
||||
i32.mul ;; умножение: стек содержит (((a+b)/c) * d)
|
||||
|
||||
;; результат остаётся на стеке — это возвращаемое значение функции
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
На ВМ Линга нет стека, он трехадресный универсальный ассемблер, если можно так сказать:
|
||||
|
||||
```
|
||||
add_int s1, s2 -> s5 // a + b -> s5
|
||||
div_int s5, s3 -> s5 // s5 -> s5 / c
|
||||
mul_int s5, s4 -> s5 // s5 -> s5 * d
|
||||
return s5
|
||||
```
|
||||
|
||||
Этот код крайне эффективно реализуется на любом реальном процессоре, и хорошо оптимизируется. Исполняется он без всякого стека.
|
||||
@@ -0,0 +1,21 @@
|
||||
Почему не WASM?
|
||||
|
||||
- wasm очень громоздкий и неудобный для разработки, он проектировался очень давно и с другими целями, так что сейчас его безуспешно пытаются "перезаточить" в современные области, но он поддается с трудом. Компилировать в wasm была бы задача по сложности превосходящая весь проект, а результат был бы уныл: мы и так имеем все достоинсва wasm, полезные для Lyng, но не имеем многих его недостатков.
|
||||
|
||||
Виртуалка lyng легкая, и доступна на всех платформах уже сейчас, а вот прикрутить скажем wasm машину к котлину на сервере, чтобы она работала как сопрограммы первого класса вместе с котлинскими, задача сейчас неразрешимая.
|
||||
|
||||
То есть, преимущества wasm что он доступен на многих платформах, для нас неважно - мы и так на тех же платформах имеем полную поддержку.
|
||||
|
||||
- Lyng VM изначально заточена на исполнение именно сопрограмм со сборкой мусора. Эти два ключевых механизма линга практически отсутствуют в wasm, их мучительно прикручивают как расширения, и они довольно слабо поддерживаются.
|
||||
|
||||
- LyngVM заточен на исполнение программ с гибридной ООП-ФП моделью, она хорошо поддерживает множественное наследование, делегацию, работу со сложными списками аргументов (ФП фичи, подстановки, деструктурирование), на васме это очень непросто написать, будет медленно и громоздко.
|
||||
|
||||
- одна из целей Lyng была получить быстрое безопасное и очень мощное скриптовое решение для вклучения в проекты на Java/Kotlin multiplatform. На wasm это в принципе невозможно - его включение это кошмар, он громоздкий. К тому же требует компиляции, а Lyng работает с JIT (исполняет прямо исходник, компилируя его на лету, как JS), и может использоваться как скрипт (wasm не может)
|
||||
|
||||
- типы данных которые мы используем в Lyng в WASM отсутстсвтуют, их бы пришлось добавлять
|
||||
|
||||
В результате если бы мы компилировали Lyng->WASM мы бы получили огромный и медленный код. Для которого потребовалась бы огромная wasm VM. Практического смысла в этом нет.
|
||||
|
||||
Если же ты рассматриваешь идею в единой платформе использовать разные языки, а wasm как промежуточный язых совместимости, то это собственно противоречит идее использовать общую кодовую базу и интерфейсы. Разные языки имеют несовпадающие, несовместимые интерфейсы вызовов, так что приходится писать руками биндинги, которые не добавляют скорости и добавляют ошибки, к тому же, разные языки предоставляют часто вообще несовместимые модели программирования (например прототипы в JS, множественное наследование С++ или Линга и кошмар на улице вязов от руста, в владением ссылками).
|
||||
|
||||
Платформ на васме полно, и там и ловить нечего, и неинтересно. Я же предлагаю вылезти в другой класс. Навеяно реальным опоытом переносов контрактов из старого золота в мольтпей, которое получается на удивление хорошо
|
||||
@@ -27,74 +27,85 @@ import org.jetbrains.compose.web.dom.*
|
||||
fun HomePage() {
|
||||
val samples = remember {
|
||||
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
|
||||
val x: Int = 10
|
||||
val status: String = if (x > 0) "Positive" else "Zero or Negative"
|
||||
|
||||
// Even loops return values!
|
||||
val result = for (i in 1..5) {
|
||||
if (i == 3) break "Found 3!"
|
||||
} else "Not found"
|
||||
|
||||
println("Result: " + result)
|
||||
println(status)
|
||||
println(result)
|
||||
""".trimIndent(),
|
||||
"""
|
||||
// Functional power with generics and collections
|
||||
// Functional collections with strict static types
|
||||
val squares: List<Int> = (1..10)
|
||||
.filter { it % 2 == 0 }
|
||||
.map { it * it }
|
||||
|
||||
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(),
|
||||
"""
|
||||
// Diamond-safe Multiple Inheritance (C3 MRO)
|
||||
@@ -123,18 +134,6 @@ fun HomePage() {
|
||||
println([10, 20, 30].second)
|
||||
""".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
|
||||
class Vector(val x: Real, val y: Real) {
|
||||
fun plus(other: Vector): Vector = Vector(x + other.x, y + other.y)
|
||||
@@ -156,20 +155,13 @@ fun HomePage() {
|
||||
println("User name: " + u.name)
|
||||
""".trimIndent(),
|
||||
"""
|
||||
// Implicit coroutines: parallelism without ceremony
|
||||
import lyng.time
|
||||
// Flexible map literals and shorthands
|
||||
val id = 101
|
||||
val name = "Lyng"
|
||||
val base = { id:, name: }
|
||||
val full = { ...base, status: "active", tags: ["typed", "portable"] }
|
||||
|
||||
val d1 = launch {
|
||||
delay(100.milliseconds)
|
||||
"Task A finished"
|
||||
}
|
||||
val d2 = launch {
|
||||
delay(50.milliseconds)
|
||||
"Task B finished"
|
||||
}
|
||||
|
||||
println(d1.await())
|
||||
println(d2.await())
|
||||
println(full)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
@@ -195,6 +187,8 @@ fun HomePage() {
|
||||
Div({ classes("d-flex", "justify-content-center", "gap-2", "flex-wrap", "mb-4") }) {
|
||||
// Benefits pills
|
||||
listOf(
|
||||
"Decimal arithmetic",
|
||||
"Operator interop",
|
||||
"Strict static typing",
|
||||
"Generics & Type Aliases",
|
||||
"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
|
||||
Div({
|
||||
classes("markdown-body", "mt-0", "slide-container", "position-relative")
|
||||
|
||||
Reference in new issue
Block a user