From 794553d81d1f50aa1310bf79b5d1cb27bae228ff Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 19 Mar 2026 01:30:35 +0300 Subject: [PATCH] Add stdlib Random API and migrate tetris RNG --- docs/ai_stdlib_reference.md | 4 + docs/math.md | 23 +++++ examples/tetris_console.lyng | 7 +- .../kotlin/net/sergeych/lyng/Script.kt | 89 +++++++++++++++++++ lynglib/src/commonTest/kotlin/StdlibTest.kt | 69 ++++++++++++++ lynglib/stdlib/lyng/root.lyng | 13 +++ notes/ai_state.md | 5 +- 7 files changed, 202 insertions(+), 8 deletions(-) diff --git a/docs/ai_stdlib_reference.md b/docs/ai_stdlib_reference.md index a32a3b2..93974aa 100644 --- a/docs/ai_stdlib_reference.md +++ b/docs/ai_stdlib_reference.md @@ -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`, `Iterator`, `Collection`, `Array`, `List`, `ImmutableList`, `Set`, `ImmutableSet`, `Map`, `ImmutableMap`, `MapEntry`, `RingBuffer`. - Host iterator bridge: `KotlinIterator`. +- 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` diff --git a/docs/math.md b/docs/math.md index d8a67a9..760d874 100644 --- a/docs/math.md +++ b/docs/math.md @@ -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 | diff --git a/examples/tetris_console.lyng b/examples/tetris_console.lyng index 13e760a..43224f8 100644 --- a/examples/tetris_console.lyng +++ b/examples/tetris_console.lyng @@ -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( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 0623115..8ee7e6f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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(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(0) + sampleRangeValue(requireScope(), KRandom.Default, range) + } + addFun("seeded") { + val seed = requiredArg(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) diff --git a/lynglib/src/commonTest/kotlin/StdlibTest.kt b/lynglib/src/commonTest/kotlin/StdlibTest.kt index 88063a1..3291371 100644 --- a/lynglib/src/commonTest/kotlin/StdlibTest.kt +++ b/lynglib/src/commonTest/kotlin/StdlibTest.kt @@ -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()) + } } diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 9c5ce87..c58f85d 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -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(range: Range): T +} + +extern object Random { + extern fun nextInt(): Int + extern fun nextFloat(): Real + extern fun next(range: Range): T + extern fun seeded(seed: Int): SeededRandom +} + // Last regex match result, updated by =~ / !~. var $~: Object? = null diff --git a/notes/ai_state.md b/notes/ai_state.md index 90a0048..9e7a38d 100644 --- a/notes/ai_state.md +++ b/notes/ai_state.md @@ -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).