Add stdlib Random API and migrate tetris RNG
This commit is contained in:
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())
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,19 @@ extern fun ln(x: Object): Real
|
||||
extern fun pow(x: Object, y: Object): Real
|
||||
extern fun sqrt(x: Object): Real
|
||||
|
||||
class SeededRandom {
|
||||
extern fun nextInt(): Int
|
||||
extern fun nextFloat(): Real
|
||||
extern fun next<T>(range: Range): T
|
||||
}
|
||||
|
||||
extern object Random {
|
||||
extern fun nextInt(): Int
|
||||
extern fun nextFloat(): Real
|
||||
extern fun next<T>(range: Range): T
|
||||
extern fun seeded(seed: Int): SeededRandom
|
||||
}
|
||||
|
||||
// Last regex match result, updated by =~ / !~.
|
||||
var $~: Object? = null
|
||||
|
||||
|
||||
Reference in new issue
Block a user