Add stdlib Random API and migrate tetris RNG

This commit is contained in:
sergeych committed 2026-03-19 01:30:35 +03:00
1 parent d9d7cafec8
commit 794553d81d
7 files changed
+202 -8

No files matched your search

@@ -20,6 +20,8 @@ package net.sergeych.lyng
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import net.sergeych.lyng.Script.Companion.defaultImportManager
import net.sergeych.lyng.bridge.bind
import net.sergeych.lyng.bridge.bindObject
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.CmdVm
import net.sergeych.lyng.miniast.*
@@ -30,6 +32,7 @@ import net.sergeych.lyng.stdlib_included.rootLyng
import net.sergeych.lynon.ObjLynonClass
import net.sergeych.mp_tools.globalDefer
import kotlin.math.*
import kotlin.random.Random as KRandom
@Suppress("TYPE_INTERSECTION_AS_REIFIED_WARNING")
class Script(
@@ -588,11 +591,97 @@ class Script(
}
}
private fun seededRandomFromThis(scope: Scope, thisObj: Obj): KRandom {
val instance = thisObj as? ObjInstance
?: scope.raiseIllegalState("SeededRandom method requires instance receiver")
val stored = instance.kotlinInstanceData
if (stored is KRandom) return stored
return KRandom.Default.also { instance.kotlinInstanceData = it }
}
private suspend fun sampleRangeValue(scope: Scope, random: KRandom, range: ObjRange): Obj {
if (range.start == null || range.start.isNull || range.end == null || range.end.isNull) {
scope.raiseIllegalArgument("Random.next(range) requires a finite range")
}
val start = range.start
val end = range.end
// Real ranges without explicit step are sampled continuously.
if (!range.hasExplicitStep &&
start is Numeric &&
end is Numeric &&
(start !is ObjInt || end !is ObjInt)
) {
val from = start.doubleValue
val to = end.doubleValue
if (from > to || (!range.isEndInclusive && from == to)) {
scope.raiseIllegalArgument("Random.next(range) got an empty numeric range")
}
if (from == to) return ObjReal(from)
val upperExclusive = if (range.isEndInclusive) to.nextUp() else to
if (upperExclusive <= from) {
scope.raiseIllegalArgument("Random.next(range) got an empty numeric range")
}
return ObjReal(random.nextDouble(from, upperExclusive))
}
// Discrete sampling for stepped ranges and integer/char ranges.
var picked: Obj? = null
var count = 0L
range.enumerate(scope) { value ->
count += 1
if (random.nextLong(count) == 0L) {
picked = value
}
true
}
if (count <= 0L || picked == null) {
scope.raiseIllegalArgument("Random.next(range) got an empty range")
}
return picked
}
val defaultImportManager: ImportManager by lazy {
ImportManager(rootScope, SecurityManager.allowAll).apply {
addPackage("lyng.stdlib") { module ->
module.eval(Source("lyng.stdlib", rootLyng))
ObjKotlinIterator.bindTo(module.requireClass("KotlinIterator"))
val seededRandomClass = module.requireClass("SeededRandom")
module.bind("SeededRandom") {
init { data = KRandom.Default }
addFun("nextInt") {
val rnd = seededRandomFromThis(requireScope(), thisObj)
ObjInt.of(rnd.nextInt().toLong())
}
addFun("nextFloat") {
val rnd = seededRandomFromThis(requireScope(), thisObj)
ObjReal(rnd.nextDouble())
}
addFun("next") {
val rnd = seededRandomFromThis(requireScope(), thisObj)
val range = requiredArg<ObjRange>(0)
sampleRangeValue(requireScope(), rnd, range)
}
}
module.bindObject("Random") {
addFun("nextInt") {
ObjInt.of(KRandom.Default.nextInt().toLong())
}
addFun("nextFloat") {
ObjReal(KRandom.Default.nextDouble())
}
addFun("next") {
val range = requiredArg<ObjRange>(0)
sampleRangeValue(requireScope(), KRandom.Default, range)
}
addFun("seeded") {
val seed = requiredArg<ObjInt>(0).value.toInt()
val instance = call(seededRandomClass) as? ObjInstance
?: requireScope().raiseIllegalState("SeededRandom() did not return an object instance")
instance.kotlinInstanceData = KRandom(seed)
instance
}
}
}
addPackage("lyng.observable") { module ->
module.addConst("Observable", ObjObservable)
@@ -172,4 +172,73 @@ class StdlibTest {
""".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())
}
}