anotther compiler inference bug andlyng.io.console improvements

This commit is contained in:
Sergey Chernov 2026-04-03 11:55:25 +03:00
parent caad7d8ab9
commit fd6d05d568
7 changed files with 389 additions and 130 deletions

View File

@ -80,6 +80,30 @@ class GameState(
var paused = false var paused = false
} }
class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {} class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {}
class InputBuffer {
private val mutex: Mutex = Mutex()
private val items: List<String> = []
fun push(value: String): Void {
mutex.withLock {
if (items.size >= MAX_PENDING_INPUTS) {
items.removeAt(0)
}
items.add(value)
}
}
fun drain(): List<String> {
val out: List<String> = []
mutex.withLock {
while (items.size > 0) {
out.add(items[0])
items.removeAt(0)
}
}
out
}
}
fun clearAndHome() { fun clearAndHome() {
Console.clear() Console.clear()
@ -540,9 +564,8 @@ if (!Console.isSupported()) {
) )
var prevFrameLines: List<String> = [] var prevFrameLines: List<String> = []
val gameMutex: Mutex = Mutex()
var forceRedraw = false var forceRedraw = false
val pendingInputs: List<String> = [] val inputBuffer: InputBuffer = InputBuffer()
val rawModeEnabled = Console.setRawMode(true) val rawModeEnabled = Console.setRawMode(true)
if (!rawModeEnabled) { if (!rawModeEnabled) {
@ -642,13 +665,7 @@ if (!Console.isSupported()) {
continue continue
} }
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
val mm: Mutex = gameMutex inputBuffer.push(mapped)
mm.withLock {
if (pendingInputs.size >= MAX_PENDING_INPUTS) {
pendingInputs.removeAt(0)
}
pendingInputs.add(mapped)
}
} }
} }
} catch (eventErr: Object) { } catch (eventErr: Object) {
@ -745,19 +762,11 @@ if (!Console.isSupported()) {
continue continue
} }
val mm: Mutex = gameMutex val toApply = inputBuffer.drain()
mm.withLock { if (toApply.size > 0) {
if (pendingInputs.size > 0) { for (k in toApply) {
val toApply: List<String> = [] applyKeyInput(state, k)
while (pendingInputs.size > 0) { if (!state.running || state.gameOver) break
val k = pendingInputs[0]
pendingInputs.removeAt(0)
toApply.add(k)
}
for (k in toApply) {
applyKeyInput(state, k)
if (!state.running || state.gameOver) break
}
} }
} }
if (!state.running || state.gameOver) { if (!state.running || state.gameOver) {

View File

@ -17,6 +17,7 @@
package net.sergeych.lyng.io.console package net.sergeych.lyng.io.console
import kotlinx.coroutines.delay
import net.sergeych.lyng.ModuleScope import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeFacade import net.sergeych.lyng.ScopeFacade
@ -58,18 +59,31 @@ fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Bo
if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false
manager.addPackage(CONSOLE_MODULE_NAME) { module -> manager.addPackage(CONSOLE_MODULE_NAME) { module ->
buildConsoleModule(module, policy) buildConsoleModule(module, policy, getSystemConsole())
} }
return true return true
} }
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager) fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) { internal fun createConsoleModule(
policy: ConsoleAccessPolicy,
manager: ImportManager,
console: LyngConsole,
): Boolean {
if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false
manager.addPackage(CONSOLE_MODULE_NAME) { module ->
buildConsoleModule(module, policy, console)
}
return true
}
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy, baseConsole: LyngConsole) {
// Load Lyng declarations for console enums/types first (module-local source of truth). // Load Lyng declarations for console enums/types first (module-local source of truth).
module.eval(Source(CONSOLE_MODULE_NAME, consoleLyng)) module.eval(Source(CONSOLE_MODULE_NAME, consoleLyng))
ConsoleEnums.initialize(module) ConsoleEnums.initialize(module)
val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy) val console: LyngConsole = LyngConsoleSecured(baseConsole, policy)
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {} val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
@ -179,7 +193,7 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
addClassFn("events") { addClassFn("events") {
consoleGuard { consoleGuard {
console.events().toConsoleEventStream() ObjConsoleEventStream { console.events() }
} }
} }
@ -210,12 +224,8 @@ private suspend inline fun ScopeFacade.consoleGuard(crossinline block: suspend (
} }
} }
private fun ConsoleEventSource.toConsoleEventStream(): ObjConsoleEventStream {
return ObjConsoleEventStream(this)
}
private class ObjConsoleEventStream( private class ObjConsoleEventStream(
private val source: ConsoleEventSource, private val sourceFactory: () -> ConsoleEventSource,
) : Obj() { ) : Obj() {
override val objClass: net.sergeych.lyng.obj.ObjClass override val objClass: net.sergeych.lyng.obj.ObjClass
get() = type get() = type
@ -224,35 +234,61 @@ private class ObjConsoleEventStream(
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply { val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply {
addFn("iterator") { addFn("iterator") {
val stream = thisAs<ObjConsoleEventStream>() val stream = thisAs<ObjConsoleEventStream>()
ObjConsoleEventIterator(stream.source) ObjConsoleEventIterator(stream.sourceFactory)
} }
} }
} }
} }
private class ObjConsoleEventIterator( private class ObjConsoleEventIterator(
private val source: ConsoleEventSource, private val sourceFactory: () -> ConsoleEventSource,
) : Obj() { ) : Obj() {
private var cached: Obj? = null private var cached: Obj? = null
private var closed = false private var closed = false
private var source: ConsoleEventSource? = null
override val objClass: net.sergeych.lyng.obj.ObjClass override val objClass: net.sergeych.lyng.obj.ObjClass
get() = type get() = type
private fun ensureSource(): ConsoleEventSource {
val current = source
if (current != null) return current
return sourceFactory().also { source = it }
}
private suspend fun recycleSource(reason: String, error: Throwable? = null) {
if (error != null) {
consoleFlowDebug(reason, error)
} else {
consoleFlowDebug(reason)
}
val current = source
source = null
runCatching { current?.close() }
.onFailure { consoleFlowDebug("console-bridge: failed to close recycled source", it) }
if (!closed) delay(25)
}
private suspend fun ensureCached(): Boolean { private suspend fun ensureCached(): Boolean {
if (closed) return false if (closed) return false
if (cached != null) return true if (cached != null) return true
while (!closed && cached == null) { while (!closed && cached == null) {
val event = try { val currentSource = try {
source.nextEvent() ensureSource()
} catch (e: Throwable) { } catch (e: Throwable) {
// Consumer loops must survive source/read failures: report and keep polling. recycleSource("console-bridge: source creation failed; retrying", e)
consoleFlowDebug("console-bridge: nextEvent failed; dropping failure and continuing", e) continue
}
val event = try {
currentSource.nextEvent()
} catch (e: Throwable) {
// Consumer loops must survive source/read failures: rebuild the source and keep polling.
recycleSource("console-bridge: nextEvent failed; recycling source", e)
continue continue
} }
if (event == null) { if (event == null) {
closeSource() recycleSource("console-bridge: source ended; recreating")
return false continue
} }
cached = try { cached = try {
event.toObjEvent() event.toObjEvent()
@ -268,10 +304,10 @@ private class ObjConsoleEventIterator(
private suspend fun closeSource() { private suspend fun closeSource() {
if (closed) return if (closed) return
closed = true closed = true
// Do not close the underlying console source from VM iterator cancellation. val current = source
// CmdFrame.cancelIterators() may call cancelIteration() while user code is still source = null
// expected to keep processing input (e.g. recover from app-level exceptions). runCatching { current?.close() }
// The source lifecycle is managed by the console runtime. .onFailure { consoleFlowDebug("console-bridge: failed to close iterator source", it) }
} }
suspend fun hasNext(): Boolean = ensureCached() suspend fun hasNext(): Boolean = ensureCached()

View File

@ -36,6 +36,97 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource import kotlin.time.TimeSource
internal object JvmConsoleKeyDecoder {
fun decode(firstCode: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown {
if (firstCode == 27) {
val next = nextCode(25L)
if (next == null || next < 0) {
return key("Escape")
}
if (next == '['.code || next == 'O'.code) {
val sb = StringBuilder()
sb.append(next.toChar())
var i = 0
while (i < 6) {
val c = nextCode(25L) ?: break
if (c < 0) break
sb.append(c.toChar())
if (c.toChar().isLetter() || c == '~'.code) break
i += 1
}
return keyFromAnsiSequence(sb.toString()) ?: key("Escape")
}
val base = decodePlainKey(next)
return ConsoleEvent.KeyDown(
key = base.key,
code = base.code,
ctrl = base.ctrl,
alt = true,
shift = base.shift,
meta = false,
)
}
return decodePlainKey(firstCode)
}
private fun decodePlainKey(code: Int): ConsoleEvent.KeyDown = when (code) {
3 -> key("c", ctrl = true)
9 -> key("Tab")
10, 13 -> key("Enter")
127, 8 -> key("Backspace")
32 -> key(" ")
else -> {
if (code in 1..26) {
val ch = ('a'.code + code - 1).toChar().toString()
key(ch, ctrl = true)
} else {
val ch = code.toChar().toString()
key(ch, shift = ch.length == 1 && ch[0].isLetter() && ch[0].isUpperCase())
}
}
}
private fun keyFromAnsiSequence(seq: String): ConsoleEvent.KeyDown? {
val shift = seq.contains(";2")
val alt = seq.contains(";3")
val ctrl = seq.contains(";5")
val key = when {
seq.endsWith("A") -> "ArrowUp"
seq.endsWith("B") -> "ArrowDown"
seq.endsWith("C") -> "ArrowRight"
seq.endsWith("D") -> "ArrowLeft"
seq.endsWith("H") -> "Home"
seq.endsWith("F") -> "End"
seq.endsWith("~") -> when (seq.substringAfter('[').substringBefore(';').substringBefore('~')) {
"2" -> "Insert"
"3" -> "Delete"
"5" -> "PageUp"
"6" -> "PageDown"
else -> null
}
else -> null
} ?: return null
return key(key, ctrl = ctrl, alt = alt, shift = shift)
}
private fun key(
value: String,
ctrl: Boolean = false,
alt: Boolean = false,
shift: Boolean = false,
): ConsoleEvent.KeyDown {
require(value.isNotEmpty()) { "ConsoleEvent.KeyDown.key must never be empty" }
return ConsoleEvent.KeyDown(
key = value,
code = null,
ctrl = ctrl,
alt = alt,
shift = shift,
meta = false,
)
}
}
/** /**
* JVM console implementation: * JVM console implementation:
* - output/capabilities/input use a single JLine terminal instance * - output/capabilities/input use a single JLine terminal instance
@ -508,40 +599,7 @@ object JvmLyngConsole : LyngConsole {
val code = reader.read(120L) val code = reader.read(120L)
if (code == NonBlockingReader.READ_EXPIRED) return null if (code == NonBlockingReader.READ_EXPIRED) return null
if (code < 0) throw EOFException("non-blocking reader returned EOF") if (code < 0) throw EOFException("non-blocking reader returned EOF")
return decodeKey(code) { timeout -> readNextCode(reader, timeout) } return JvmConsoleKeyDecoder.decode(code) { timeout -> readNextCode(reader, timeout) }
}
private fun decodeKey(code: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown {
if (code == 27) {
val next = nextCode(25L)
if (next == null || next < 0) {
return key("Escape")
}
if (next == '['.code || next == 'O'.code) {
val sb = StringBuilder()
sb.append(next.toChar())
var i = 0
while (i < 6) {
val c = nextCode(25L) ?: break
if (c < 0) break
sb.append(c.toChar())
if (c.toChar().isLetter() || c == '~'.code) break
i += 1
}
return keyFromAnsiSequence(sb.toString()) ?: key("Escape")
}
// Alt+key
val base = decodePlainKey(next)
return ConsoleEvent.KeyDown(
key = base.key,
code = base.code,
ctrl = base.ctrl,
alt = true,
shift = base.shift,
meta = false
)
}
return decodePlainKey(code)
} }
private fun readNextCode(reader: NonBlockingReader, timeoutMs: Long): Int? { private fun readNextCode(reader: NonBlockingReader, timeoutMs: Long): Int? {
@ -550,53 +608,4 @@ object JvmLyngConsole : LyngConsole {
if (c < 0) throw EOFException("non-blocking reader returned EOF while decoding key sequence") if (c < 0) throw EOFException("non-blocking reader returned EOF while decoding key sequence")
return c return c
} }
private fun decodePlainKey(code: Int): ConsoleEvent.KeyDown = when (code) {
3 -> key("c", ctrl = true)
9 -> key("Tab")
10, 13 -> key("Enter")
127, 8 -> key("Backspace")
32 -> key(" ")
else -> {
if (code in 1..26) {
val ch = ('a'.code + code - 1).toChar().toString()
key(ch, ctrl = true)
} else {
val ch = code.toChar().toString()
key(ch, shift = ch.length == 1 && ch[0].isLetter() && ch[0].isUpperCase())
}
}
}
private fun keyFromAnsiSequence(seq: String): ConsoleEvent.KeyDown? = when (seq) {
"[A", "OA" -> key("ArrowUp")
"[B", "OB" -> key("ArrowDown")
"[C", "OC" -> key("ArrowRight")
"[D", "OD" -> key("ArrowLeft")
"[H", "OH" -> key("Home")
"[F", "OF" -> key("End")
"[2~" -> key("Insert")
"[3~" -> key("Delete")
"[5~" -> key("PageUp")
"[6~" -> key("PageDown")
else -> null
}
private fun key(
value: String,
ctrl: Boolean = false,
alt: Boolean = false,
shift: Boolean = false,
): ConsoleEvent.KeyDown {
require(value.isNotEmpty()) { "ConsoleEvent.KeyDown.key must never be empty" }
return ConsoleEvent.KeyDown(
key = value,
code = null,
ctrl = ctrl,
alt = alt,
shift = shift,
meta = false
)
}
} }

View File

@ -22,16 +22,14 @@ import net.sergeych.lyng.ExecutionError
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.ObjBool import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjIllegalOperationException import net.sergeych.lyng.obj.ObjIllegalOperationException
import net.sergeych.lyngio.console.*
import net.sergeych.lyngio.console.security.ConsoleAccessOp import net.sergeych.lyngio.console.security.ConsoleAccessOp
import net.sergeych.lyngio.console.security.ConsoleAccessPolicy import net.sergeych.lyngio.console.security.ConsoleAccessPolicy
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
import net.sergeych.lyngio.fs.security.AccessContext import net.sergeych.lyngio.fs.security.AccessContext
import net.sergeych.lyngio.fs.security.AccessDecision import net.sergeych.lyngio.fs.security.AccessDecision
import net.sergeych.lyngio.fs.security.Decision import net.sergeych.lyngio.fs.security.Decision
import kotlin.test.Test import kotlin.test.*
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertTrue
class LyngConsoleModuleTest { class LyngConsoleModuleTest {
@ -112,4 +110,63 @@ class LyngConsoleModuleTest {
assertIs<ObjIllegalOperationException>(error.errorObject) assertIs<ObjIllegalOperationException>(error.errorObject)
} }
} }
@Test
fun eventsIteratorRecoversAfterSourceFailure() = runBlocking {
val scope = newScope()
var eventsCalls = 0
val console = object : LyngConsole {
override val isSupported: Boolean = true
override suspend fun isTty(): Boolean = true
override suspend fun geometry(): ConsoleGeometry? = null
override suspend fun ansiLevel(): ConsoleAnsiLevel = ConsoleAnsiLevel.NONE
override suspend fun write(text: String) {}
override suspend fun flush() {}
override fun events(): ConsoleEventSource {
eventsCalls += 1
val callNo = eventsCalls
return object : ConsoleEventSource {
private var emitted = false
override suspend fun nextEvent(timeoutMs: Long): ConsoleEvent? {
if (emitted) return null
emitted = true
return when (callNo) {
1 -> throw IllegalStateException("synthetic source failure")
else -> ConsoleEvent.KeyDown(key = "x")
}
}
override suspend fun close() {}
}
}
override suspend fun setRawMode(enabled: Boolean): Boolean = enabled
}
assertTrue(createConsoleModule(PermitAllConsoleAccessPolicy, scope.importManager, console))
val result = scope.eval(
"""
import lyng.io.console
val it = Console.events().iterator()
assert(it.hasNext())
val ev = it.next()
assert(ev is ConsoleKeyEvent)
assert((ev as ConsoleKeyEvent).key == "x")
true
""".trimIndent()
)
assertIs<ObjBool>(result)
assertTrue(result.value)
assertEquals(2, eventsCalls)
}
} }

View File

@ -0,0 +1,59 @@
/*
* 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.
*
*/
package net.sergeych.lyngio.console
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class JvmConsoleKeyDecoderTest {
private fun decode(vararg bytes: Int): ConsoleEvent.KeyDown {
var index = 1
return JvmConsoleKeyDecoder.decode(bytes[0]) { _ ->
if (index >= bytes.size) null else bytes[index++]
}
}
@Test
fun decodesArrowLeft() {
val ev = decode(27, '['.code, 'D'.code)
assertEquals("ArrowLeft", ev.key)
assertFalse(ev.ctrl)
assertFalse(ev.alt)
assertFalse(ev.shift)
}
@Test
fun decodesCtrlArrowRightModifier() {
val ev = decode(27, '['.code, '1'.code, ';'.code, '5'.code, 'C'.code)
assertEquals("ArrowRight", ev.key)
assertTrue(ev.ctrl)
assertFalse(ev.alt)
assertFalse(ev.shift)
}
@Test
fun decodesAltCharacter() {
val ev = decode(27, 'x'.code)
assertEquals("x", ev.key)
assertTrue(ev.alt)
assertFalse(ev.ctrl)
}
}

View File

@ -4674,6 +4674,7 @@ class Compiler(
?: nameObjClass[ref.name] ?: nameObjClass[ref.name]
?: resolveClassByName(ref.name) ?: resolveClassByName(ref.name)
} }
is ImplicitThisMemberRef -> resolveReceiverClassForMember(ref)
is ClassScopeMemberRef -> { is ClassScopeMemberRef -> {
val targetClass = resolveClassByName(ref.ownerClassName()) ?: return null val targetClass = resolveClassByName(ref.ownerClassName()) ?: return null
inferFieldReturnClass(targetClass, ref.name) inferFieldReturnClass(targetClass, ref.name)
@ -4728,6 +4729,15 @@ class Compiler(
is FastLocalVarRef -> nameTypeDecl[ref.name] is FastLocalVarRef -> nameTypeDecl[ref.name]
?: lookupLocalTypeDeclByName(ref.name) ?: lookupLocalTypeDeclByName(ref.name)
?: seedTypeDeclByName(ref.name) ?: seedTypeDeclByName(ref.name)
is ImplicitThisMemberRef -> {
val typeName = ref.preferredThisTypeName() ?: currentImplicitThisTypeName()
val targetClass = typeName?.let { resolveClassByName(it) } ?: return null
targetClass.getInstanceMemberOrNull(ref.name, includeAbstract = true)?.typeDecl?.let { return it }
classFieldTypesByName[targetClass.className]?.get(ref.name)
?.let { return TypeDecl.Simple(it.className, false) }
classMethodReturnTypeDeclByName[targetClass.className]?.get(ref.name)?.let { return it }
null
}
is FieldRef -> { is FieldRef -> {
val targetDecl = resolveReceiverTypeDecl(ref.target) ?: return null val targetDecl = resolveReceiverTypeDecl(ref.target) ?: return null
val targetClass = resolveTypeDeclObjClass(targetDecl) ?: resolveReceiverClassForMember(ref.target) val targetClass = resolveTypeDeclObjClass(targetDecl) ?: resolveReceiverClassForMember(ref.target)
@ -7388,6 +7398,15 @@ class Compiler(
val mutable = param.accessType?.isMutable ?: false val mutable = param.accessType?.isMutable ?: false
declareSlotNameIn(classSlotPlan, param.name, mutable, isDelegated = false) declareSlotNameIn(classSlotPlan, param.name, mutable, isDelegated = false)
} }
constructorArgsDeclaration?.let { ctorDecl ->
val classParamTypeMap = slotTypeByScopeId.getOrPut(classSlotPlan.id) { mutableMapOf() }
val classParamTypeDeclMap = slotTypeDeclByScopeId.getOrPut(classSlotPlan.id) { mutableMapOf() }
for (param in ctorDecl.params) {
val slot = classSlotPlan.slots[param.name]?.index ?: continue
classParamTypeDeclMap[slot] = param.type
resolveTypeDeclObjClass(param.type)?.let { classParamTypeMap[slot] = it }
}
}
val ctorForcedLocalSlots = LinkedHashMap<String, Int>() val ctorForcedLocalSlots = LinkedHashMap<String, Int>()
if (constructorArgsDeclaration != null) { if (constructorArgsDeclaration != null) {
val ctorDecl = constructorArgsDeclaration val ctorDecl = constructorArgsDeclaration
@ -8873,6 +8892,7 @@ class Compiler(
val targetClass = resolveReceiverClassForMember(directRef.target) val targetClass = resolveReceiverClassForMember(directRef.target)
inferFieldReturnClass(targetClass, directRef.name) inferFieldReturnClass(targetClass, directRef.name)
} }
is ImplicitThisMemberRef -> resolveReceiverClassForMember(directRef)
is CallRef -> { is CallRef -> {
val target = directRef.target val target = directRef.target
when { when {

View File

@ -0,0 +1,69 @@
/*
* 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.eval
import kotlin.test.Test
class InferredCtorFieldIntRegressionTest {
@Test
fun inferredIntFieldsFromCtorParamsStayIntInsideTypedCalls() = runTest {
eval(
"""
class GameState(
pieceId0: Int,
nextId0: Int,
next2Id0: Int,
px0: Int,
py0: Int,
) {
var pieceId = pieceId0
var nextId = nextId0
var next2Id = next2Id0
var rot = 0
var px = px0
var py = py0
var score = 0
var totalLines = 0
var level = 1
var running = true
var gameOver = false
var paused = false
}
fun canPlace(board: List<List<Int>>, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool {
true
}
val board: List<List<Int>> = []
val boardW = 10
val boardH = 20
fun applyKeyInput(s: GameState, key: String) {
if (key == "ArrowLeft") {
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px - 1, s.py) == true) s.px--
}
}
val s = GameState(1, 2, 3, 4, 5)
applyKeyInput(s, "ArrowLeft")
assertEquals(3, s.px)
""".trimIndent()
)
}
}