Add stdlib Random API and migrate tetris RNG

This commit is contained in:
Sergey Chernov 2026-03-19 01:30:35 +03:00
parent d9d7cafec8
commit 794553d81d
7 changed files with 202 additions and 8 deletions

View File

@ -20,6 +20,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
- Values: `Unset`, `π`.
- Primitive/class symbols: `Object`, `Int`, `Real`, `Bool`, `Char`, `String`, `Class`, `Callable`.
- Collections/types: `Iterable`, `Iterator`, `Collection`, `Array`, `List`, `ImmutableList`, `Set`, `ImmutableSet`, `Map`, `ImmutableMap`, `MapEntry`, `Range`, `RingBuffer`.
- Random: singleton `Random` and class `SeededRandom`.
- Async types: `Deferred`, `CompletableDeferred`, `Mutex`, `Flow`, `FlowBuilder`.
- Delegation types: `Delegate`, `DelegateContext`.
- Regex types: `Regex`, `RegexMatch`.
@ -30,6 +31,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
- Exceptions/delegation base: `Exception`, `IllegalArgumentException`, `NotImplementedException`, `Delegate`.
- Collections and iterables: `Iterable<T>`, `Iterator<T>`, `Collection<T>`, `Array<T>`, `List<T>`, `ImmutableList<T>`, `Set<T>`, `ImmutableSet<T>`, `Map<K,V>`, `ImmutableMap<K,V>`, `MapEntry<K,V>`, `RingBuffer<T>`.
- Host iterator bridge: `KotlinIterator<T>`.
- Random APIs: `extern object Random`, `extern class SeededRandom`.
### 4.2 High-use extension APIs
- Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`.
@ -48,6 +50,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
- `$~` (last regex match object).
- `TODO(message?)` utility.
- `StackTraceEntry` class.
- `Random.nextInt()`, `Random.nextFloat()`, `Random.next(range)`, `Random.seeded(seed)`.
- `SeededRandom.nextInt()`, `SeededRandom.nextFloat()`, `SeededRandom.next(range)`.
## 5. Additional Built-in Modules (import explicitly)
- `import lyng.observable`

View File

@ -110,6 +110,29 @@ For example:
assert( 5.clamp(0..10) == 5 )
>>> void
## Random values
Lyng stdlib provides a global random singleton and deterministic seeded generators:
| name | meaning |
|--------------------------|---------|
| Random.nextInt() | random `Int` from full platform range |
| Random.nextFloat() | random `Real` in `[0,1)` |
| Random.next(range) | random value from the given finite range |
| Random.seeded(seed) | creates deterministic generator |
| SeededRandom.nextInt() | deterministic random `Int` |
| SeededRandom.nextFloat() | deterministic random `Real` in `[0,1)` |
| SeededRandom.next(range) | deterministic random value from range |
Examples:
val rng = Random.seeded(1234)
assert( rng.next(1..10) in 1..10 )
assert( rng.next('a'..<'f') in 'a'..<'f' )
assert( rng.next(0.0..<1.0) >= 0.0 )
assert( rng.next(0.0..<1.0) < 1.0 )
>>> void
## Scientific constant
| name | meaning |

View File

@ -26,9 +26,6 @@ val DROP_FRAMES_BASE = 15
val DROP_FRAMES_MIN = 3
val FRAME_DELAY_MS = 35
val RESIZE_WAIT_MS = 250
val RNG_A = 1103515245
val RNG_C = 12345
val RNG_M = 2147483647
val ROTATION_KICKS = [0, -1, 1, -2, 2]
val ANSI_ESC = "\u001b["
val ANSI_RESET = ANSI_ESC + "0m"
@ -442,10 +439,8 @@ if (!Console.isSupported()) {
val board: Board = createBoard(boardW, boardH)
var rng = 1337
fun nextPieceId() {
rng = (rng * RNG_A + RNG_C) % RNG_M
(rng % PIECES.size) + 1
Random.next(1..7)
}
val state: GameState = GameState(

View File

@ -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)

View File

@ -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())
}
}

View File

@ -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

View File

@ -12,9 +12,10 @@ Current focus
Key recent changes
- Updated AI helper docs to reflect static typing, type expressions, and compile-time-only name resolution.
- Added stdlib random API: `Random` and deterministic `SeededRandom` with `nextInt`, `nextFloat`, and generic `next(range)`.
Known failing tests
- Not checked in this session.
- None in :lynglib:jvmTest after Random/SeededRandom integration.
Last test run
- Not checked in this session.
- `./gradlew :lynglib:jvmTest` (PASS).