lyng/lynglib/src/commonTest/kotlin/StdlibTest.kt

440 lines
12 KiB
Kotlin

/*
* 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.
*
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Script
import net.sergeych.lyng.eval
import net.sergeych.lyng.evalNamed
import net.sergeych.lyng.obj.ObjException
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.getLyngExceptionMessage
import net.sergeych.lyng.obj.getLyngExceptionStackTrace
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class StdlibTest {
@Test
fun testIterableFilter() = runTest {
eval("""
assertEquals([2,4,6,8], (1..8).filter{ println("call2"); it % 2 == 0 }.toList() )
println("-------------------")
assertEquals([1,3,5,7], (1..8).filter{ println("call1"); it % 2 == 1 }.toList() )
""".trimIndent())
}
@Test
fun testFirstLast() = runTest {
eval("""
assertEquals(1, (1..8).first )
assertEquals(8, (1..8).last )
""".trimIndent())
}
@Test
fun testTake() = runTest {
eval("""
val r = 1..8
assertEquals([1,2,3], r.take(3).toList() )
assertEquals([7,8], r.takeLast(2).toList() )
""".trimIndent())
}
@Test
fun testAnyAndAll() = runTest {
eval("""
assert( [1,2,3].any { it > 2 } )
assert( ![1,2,3].any { it > 4 } )
assert( [1,2,3].all { it <= 3 } )
""".trimIndent())
}
@Test
fun testRingBuffer() = runTest {
eval("""
val r = RingBuffer(3)
assert( r is RingBuffer )
assertEquals(0, r.size)
assertEquals(3, r.capacity)
r += 10
assertEquals(1, r.size)
assertEquals(10, r.first)
r += 20
assertEquals(2, r.size)
assertEquals( [10, 20], r.toList() )
r += 30
assertEquals(3, r.size)
assertEquals( [10, 20, 30], r.toList() )
r += 40
assertEquals(3, r.size)
assertEquals( [20, 30, 40], r.toList() )
""".trimIndent())
}
@Test
fun testDrop() = runTest {
eval("""
assertEquals([7,8], (1..8).drop(6).toList() )
assertEquals([1,2], (1..8).dropLast(6).toList() )
""".trimIndent())
}
@Test
fun testFlattenAndFilter() = runTest {
eval("""
assertEquals([1,2,3,4,5,6], [1,3,5].map { [it, it+1] }.flatten() )
assertEquals([1,3,5], [null,1,null, 3,5].filterNotNull().toList())
""")
}
@Test
fun testFlatMap() = runTest {
eval("""
assertEquals([1,2,3,4,5,6], [1,3,5].flatMap { [it,it+1] }.toList() )
""")
}
@Test
fun testCount() = runTest {
eval("""
assertEquals(5, (1..10).toList().count { it % 2 == 1 } )
""")
}
@Test
fun testWith() = runTest {
eval("""
class Person(val name, var age)
val p = Person("Alice", 30)
val result = with(p) {
assertEquals("Alice", name)
assertEquals(30, age)
age = 31
"done"
}
assertEquals("done", result)
assertEquals(31, p.age)
""".trimIndent())
}
@Test
fun testFilter1() = runTest {
// range should be iterable if it is intrange
eval("""
val data = 1..5 // or [1, 2, 3, 4, 5]
assert( data is Iterable<Int> )
fun test() {
data.filter { it % 2 == 0 }.map { it * it }
}
test()
""".trimIndent())
}
@Test
fun testFilter2() = runTest {
eval("""
val data = [1, 2, 3, 4, 5]
assert( data is Iterable<Int> )
fun test() {
data.filter { it % 2 == 0 }.map { it * it }
}
test()
"""
)
}
@Test
fun testFilter3() = runTest {
eval("""
type Numeric = Int | Real
fun process<T: Numeric>(items: List<T>): List<T> {
items.filter { it > 0 }.map { it * it }
}
val data = [-2, -1, 0, 1, 2]
println("Processed: " + process(data))
""".trimIndent())
}
@Test
fun testRandomSeededDeterministic() = runTest {
eval("""
val a = Random.seeded(123456)
val b = Random.seeded(123456)
for( i in 1..20 ) {
assertEquals(a.nextInt(), b.nextInt())
assertEquals(a.nextFloat(), b.nextFloat())
assertEquals(a.next(1..100), b.next(1..100))
}
""".trimIndent())
}
@Test
fun testRandomNextFloatBounds() = runTest {
eval("""
for( i in 1..400 ) {
val x = Random.nextFloat()
assert(x >= 0.0)
assert(x < 1.0)
}
""".trimIndent())
}
@Test
fun testRandomNextRangeVariants() = runTest {
eval("""
val rnd = Random.seeded(77)
for( i in 1..300 ) {
val x = rnd.next(10..<20)
assert(x in 10..<20)
}
val allowed = [0, 3, 6, 9]
for( i in 1..300 ) {
val x = rnd.next(0..9 step 3)
assert(x in allowed)
}
for( i in 1..300 ) {
val ch = rnd.next('a'..<'f')
assert(ch in 'a'..<'f')
}
for( i in 1..300 ) {
val rf = rnd.next(1.5..<4.5)
assert(rf >= 1.5)
assert(rf < 4.5)
}
val qAllowed = [0.0, 0.25, 0.5, 0.75, 1.0]
for( i in 1..300 ) {
val q = rnd.next(0.0..1.0 step 0.25)
assert(q in qAllowed)
}
""".trimIndent())
}
@Test
fun testRandomRejectsOpenRange() = runTest {
eval("""
assertThrows(IllegalArgumentException) { Random.next(1..) }
assertThrows(IllegalArgumentException) { Random.next(..10) }
assertThrows(IllegalArgumentException) { Random.next(..) }
""".trimIndent())
}
@Test
fun testInference2() = runTest {
eval(
$$"""
val a = 10
val b = 3.0
val c = floor(a / b)
//assert(c is Real)
c.toInt()
""".trimIndent()
)
}
@Test
fun testStdlibGlobalFunctionInference() = runTest {
eval(
$$"""
val absInt = abs(-5)
assert(absInt is Int)
absInt.toInt()
val absReal = abs(-5.5)
assert(absReal is Real)
absReal.isNaN()
val floorInt = floor(7)
assert(floorInt is Int)
floorInt.toInt()
val floorReal = floor(7.9)
assert(floorReal is Real)
floorReal.toInt()
val ceilInt = ceil(7)
assert(ceilInt is Int)
ceilInt.toInt()
val ceilReal = ceil(7.1)
assert(ceilReal is Real)
ceilReal.toInt()
val roundInt = round(7)
assert(roundInt is Int)
roundInt.toInt()
val roundReal = round(7.4)
assert(roundReal is Real)
roundReal.toInt()
val sinValue = sin(1)
sinValue.isInfinite()
assert(sinValue is Real)
val cosValue = cos(1)
cosValue.isNaN()
assert(cosValue is Real)
val tanValue = tan(1)
tanValue.toInt()
assert(tanValue is Real)
val asinValue = asin(0.5)
asinValue.toInt()
assert(asinValue is Real)
val acosValue = acos(0.5)
acosValue.toInt()
assert(acosValue is Real)
val atanValue = atan(1)
atanValue.toInt()
assert(atanValue is Real)
val sinhValue = sinh(1)
sinhValue.isInfinite()
assert(sinhValue is Real)
val coshValue = cosh(1)
coshValue.isNaN()
assert(coshValue is Real)
val tanhValue = tanh(1)
tanhValue.toInt()
assert(tanhValue is Real)
val asinhValue = asinh(1)
asinhValue.toInt()
assert(asinhValue is Real)
val acoshValue = acosh(2)
acoshValue.toInt()
assert(acoshValue is Real)
val atanhValue = atanh(0.5)
atanhValue.toInt()
assert(atanhValue is Real)
val expValue = exp(1)
expValue.isInfinite()
assert(expValue is Real)
val lnValue = ln(2)
lnValue.isNaN()
assert(lnValue is Real)
val log10Value = log10(100)
log10Value.toInt()
assert(log10Value is Real)
val log2Value = log2(8)
log2Value.toInt()
assert(log2Value is Real)
val powValue = pow(2, 8)
powValue.isInfinite()
assert(powValue is Real)
val sqrtValue = sqrt(9)
sqrtValue.isNaN()
assert(sqrtValue is Real)
val clampedInt = clamp(20, 0..10)
assert(clampedInt is Int)
clampedInt.toInt()
val clampedReal = clamp(2.5, 0.0..10.0)
assert(clampedReal is Real)
clampedReal.toInt()
""".trimIndent()
)
}
@Test
fun testErrorCatching() = runTest {
val error = evalNamed("testErrorCatching", """
val src = [1,2,3]
val d = launch {
try {
for( i in 0..3 ) src[i]
} catch(e) {
e
}
}
d.await()
""".trimIndent()
)
val scope = when (error) {
is ObjException -> error.scope
is ObjInstance -> error.instanceScope
else -> Script.newScope()
}
val trace = error.getLyngExceptionStackTrace(scope)
val renderedTrace = trace.list.map { it.toString(scope).value }
assertEquals("Index 3 out of bounds for length 3", error.getLyngExceptionMessage(scope))
assertTrue(trace.list.size >= 2, "expected at least await and coroutine frames, got ${trace.list.size}")
assertTrue(
renderedTrace.all { it.contains("testErrorCatching:") },
"unexpected trace entries: $renderedTrace"
)
assertTrue(
renderedTrace.any { it.contains("launch") || it.contains("src[i]") },
"trace should include the coroutine body: $renderedTrace"
)
assertTrue(
renderedTrace.any { it.contains("d.await()") },
"trace should include await site: $renderedTrace"
)
}
@Test
fun testCatchToIt() = runTest {
eval("""
var x = 0
try {
throw "msg1"
x = 1
}
catch {
assert(it.message == "msg1")
x = 2
}
assertEquals(2, x)
""".trimIndent())
}
}