Compare commits

..

No commits in common. "2414da59a7d1a919f99323a3c8220e34c4ce1e2d" and "caad7d8ab9674c4e64668a7753c2f0ff66de80ab" have entirely different histories.

10 changed files with 144 additions and 515 deletions

View File

@ -80,30 +80,6 @@ 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()
@ -564,8 +540,9 @@ if (!Console.isSupported()) {
) )
var prevFrameLines: List<String> = [] var prevFrameLines: List<String> = []
val gameMutex: Mutex = Mutex()
var forceRedraw = false var forceRedraw = false
val inputBuffer: InputBuffer = InputBuffer() val pendingInputs: List<String> = []
val rawModeEnabled = Console.setRawMode(true) val rawModeEnabled = Console.setRawMode(true)
if (!rawModeEnabled) { if (!rawModeEnabled) {
@ -665,7 +642,13 @@ 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
inputBuffer.push(mapped) val mm: Mutex = gameMutex
mm.withLock {
if (pendingInputs.size >= MAX_PENDING_INPUTS) {
pendingInputs.removeAt(0)
}
pendingInputs.add(mapped)
}
} }
} }
} catch (eventErr: Object) { } catch (eventErr: Object) {
@ -762,11 +745,19 @@ if (!Console.isSupported()) {
continue continue
} }
val toApply = inputBuffer.drain() val mm: Mutex = gameMutex
if (toApply.size > 0) { mm.withLock {
for (k in toApply) { if (pendingInputs.size > 0) {
applyKeyInput(state, k) val toApply: List<String> = []
if (!state.running || state.gameOver) break while (pendingInputs.size > 0) {
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,7 +17,6 @@
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
@ -59,31 +58,18 @@ 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, getSystemConsole()) buildConsoleModule(module, policy)
} }
return true return true
} }
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager) fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
internal fun createConsoleModule( private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) {
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(baseConsole, policy) val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy)
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {} val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
@ -193,7 +179,7 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
addClassFn("events") { addClassFn("events") {
consoleGuard { consoleGuard {
ObjConsoleEventStream { console.events() } console.events().toConsoleEventStream()
} }
} }
@ -224,8 +210,12 @@ 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 sourceFactory: () -> ConsoleEventSource, private val source: ConsoleEventSource,
) : Obj() { ) : Obj() {
override val objClass: net.sergeych.lyng.obj.ObjClass override val objClass: net.sergeych.lyng.obj.ObjClass
get() = type get() = type
@ -234,61 +224,35 @@ 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.sourceFactory) ObjConsoleEventIterator(stream.source)
} }
} }
} }
} }
private class ObjConsoleEventIterator( private class ObjConsoleEventIterator(
private val sourceFactory: () -> ConsoleEventSource, private val source: 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 currentSource = try {
ensureSource()
} catch (e: Throwable) {
recycleSource("console-bridge: source creation failed; retrying", e)
continue
}
val event = try { val event = try {
currentSource.nextEvent() source.nextEvent()
} catch (e: Throwable) { } catch (e: Throwable) {
// Consumer loops must survive source/read failures: rebuild the source and keep polling. // Consumer loops must survive source/read failures: report and keep polling.
recycleSource("console-bridge: nextEvent failed; recycling source", e) consoleFlowDebug("console-bridge: nextEvent failed; dropping failure and continuing", e)
continue continue
} }
if (event == null) { if (event == null) {
recycleSource("console-bridge: source ended; recreating") closeSource()
continue return false
} }
cached = try { cached = try {
event.toObjEvent() event.toObjEvent()
@ -304,10 +268,10 @@ private class ObjConsoleEventIterator(
private suspend fun closeSource() { private suspend fun closeSource() {
if (closed) return if (closed) return
closed = true closed = true
val current = source // Do not close the underlying console source from VM iterator cancellation.
source = null // CmdFrame.cancelIterators() may call cancelIteration() while user code is still
runCatching { current?.close() } // expected to keep processing input (e.g. recover from app-level exceptions).
.onFailure { consoleFlowDebug("console-bridge: failed to close iterator source", it) } // The source lifecycle is managed by the console runtime.
} }
suspend fun hasNext(): Boolean = ensureCached() suspend fun hasNext(): Boolean = ensureCached()

View File

@ -36,97 +36,6 @@ 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
@ -599,7 +508,40 @@ 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 JvmConsoleKeyDecoder.decode(code) { timeout -> readNextCode(reader, timeout) } return decodeKey(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? {
@ -608,4 +550,53 @@ 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,14 +22,16 @@ 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.* import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertTrue
class LyngConsoleModuleTest { class LyngConsoleModuleTest {
@ -110,63 +112,4 @@ 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

@ -1,59 +0,0 @@
/*
* 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

@ -22,14 +22,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import platform.posix.* import platform.posix.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource import kotlin.time.TimeSource
internal actual fun getNativeSystemConsole(): LyngConsole = LinuxPosixLyngConsole internal actual fun getNativeSystemConsole(): LyngConsole = LinuxPosixLyngConsole
private const val RAW_IDLE_POLL_MS = 10L
private const val NON_RAW_IDLE_POLL_MS = 25L
private const val ESCAPE_FOLLOWUP_TIMEOUT_MS = 25L
internal object LinuxConsoleKeyDecoder { internal object LinuxConsoleKeyDecoder {
fun decode(firstCode: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown { fun decode(firstCode: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown {
if (firstCode == 27) { if (firstCode == 27) {
@ -135,7 +132,6 @@ object LinuxPosixLyngConsole : LyngConsole {
} }
} }
consoleFlowDebug("linux-events: source created")
return object : ConsoleEventSource { return object : ConsoleEventSource {
var closed = false var closed = false
var lastGeometry: ConsoleGeometry? = null var lastGeometry: ConsoleGeometry? = null
@ -151,27 +147,23 @@ object LinuxPosixLyngConsole : LyngConsole {
} }
val rawRequested = stateMutex.withLock { rawModeRequested } val rawRequested = stateMutex.withLock { rawModeRequested }
val pollSliceMs = if (timeoutMs <= 0L) 250L else minOf(250L, timeoutMs)
if (rawRequested) { if (rawRequested) {
val ev = readKeyEventNonBlocking() val ev = readKeyEvent(pollSliceMs)
if (ev != null) return ev if (ev != null) return ev
} else {
delay(25)
} }
val elapsedMs = started.elapsedNow().inWholeMilliseconds if (timeoutMs > 0L && started.elapsedNow() >= timeoutMs.milliseconds) {
if (timeoutMs > 0L && elapsedMs >= timeoutMs) {
return null return null
} }
val remainingMs = if (timeoutMs > 0L) timeoutMs - elapsedMs else Long.MAX_VALUE
val idleMs = if (rawRequested) RAW_IDLE_POLL_MS else NON_RAW_IDLE_POLL_MS
val sleepMs = if (timeoutMs > 0L) minOf(idleMs, remainingMs) else idleMs
if (sleepMs > 0L) delay(sleepMs)
} }
return null return null
} }
override suspend fun close() { override suspend fun close() {
closed = true closed = true
consoleFlowDebug("linux-events: source closed")
} }
} }
} }
@ -191,11 +183,12 @@ object LinuxPosixLyngConsole : LyngConsole {
} }
savedAttrsBlob = saved savedAttrsBlob = saved
configureRawInput(attrs) attrs.c_lflag = attrs.c_lflag and ICANON.convert<UInt>().inv() and ECHO.convert<UInt>().inv()
attrs.c_iflag = attrs.c_iflag and IXON.convert<UInt>().inv() and ISTRIP.convert<UInt>().inv()
attrs.c_oflag = attrs.c_oflag and OPOST.convert<UInt>().inv()
if (tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr) != 0) return@withLock false if (tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr) != 0) return@withLock false
} }
rawModeRequested = true rawModeRequested = true
consoleFlowDebug("linux-events: setRawMode(true): enabled")
true true
} else { } else {
val hadRaw = rawModeRequested val hadRaw = rawModeRequested
@ -210,7 +203,6 @@ object LinuxPosixLyngConsole : LyngConsole {
tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr) tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr)
} }
} }
consoleFlowDebug("linux-events: setRawMode(false): disabled hadRaw=$hadRaw")
hadRaw hadRaw
} }
} }
@ -233,55 +225,19 @@ object LinuxPosixLyngConsole : LyngConsole {
val ready = poll(pfd.ptr, 1.convert(), timeoutMs.toInt()) val ready = poll(pfd.ptr, 1.convert(), timeoutMs.toInt())
if (ready <= 0) return null if (ready <= 0) return null
readReadyByte()
}
private fun readReadyByte(): Int? {
val buf = ByteArray(1) val buf = ByteArray(1)
val count = buf.usePinned { pinned -> val count = buf.usePinned { pinned ->
read(STDIN_FILENO, pinned.addressOf(0), 1.convert()) read(STDIN_FILENO, pinned.addressOf(0), 1.convert())
} }
if (count <= 0) { if (count <= 0) return null
if (count < 0) {
consoleFlowDebug("linux-events: stdin read returned $count errno=$errno")
}
return null
}
val b = buf[0].toInt() val b = buf[0].toInt()
return if (b < 0) b + 256 else b if (b < 0) b + 256 else b
} }
private fun readByteNow(): Int? = memScoped { private fun readKeyEvent(timeoutMs: Long): ConsoleEvent.KeyDown? {
val pfd = alloc<pollfd>() val first = readByte(timeoutMs) ?: return null
pfd.fd = STDIN_FILENO
pfd.events = POLLIN.convert()
pfd.revents = 0
val ready = poll(pfd.ptr, 1.convert(), 0)
if (ready <= 0) return null
readReadyByte()
}
private fun readKeyEventNonBlocking(): ConsoleEvent.KeyDown? {
val first = readByteNow() ?: return null
return LinuxConsoleKeyDecoder.decode(first) { timeout -> return LinuxConsoleKeyDecoder.decode(first) { timeout ->
readByte(minOf(timeout, ESCAPE_FOLLOWUP_TIMEOUT_MS)) readByte(timeout)
} }
} }
} }
@OptIn(ExperimentalForeignApi::class)
internal fun configureRawInput(attrs: termios) {
attrs.c_iflag = attrs.c_iflag and BRKINT.convert<UInt>().inv()
attrs.c_iflag = attrs.c_iflag and ICRNL.convert<UInt>().inv()
attrs.c_iflag = attrs.c_iflag and INPCK.convert<UInt>().inv()
attrs.c_iflag = attrs.c_iflag and ISTRIP.convert<UInt>().inv()
attrs.c_iflag = attrs.c_iflag and IXON.convert<UInt>().inv()
attrs.c_oflag = attrs.c_oflag and OPOST.convert<UInt>().inv()
attrs.c_cflag = attrs.c_cflag or CS8.convert<UInt>()
attrs.c_lflag = attrs.c_lflag and ECHO.convert<UInt>().inv()
attrs.c_lflag = attrs.c_lflag and ICANON.convert<UInt>().inv()
attrs.c_lflag = attrs.c_lflag and IEXTEN.convert<UInt>().inv()
attrs.c_lflag = attrs.c_lflag and ISIG.convert<UInt>().inv()
attrs.c_cc[VMIN] = 0u
attrs.c_cc[VTIME] = 1u
}

View File

@ -17,8 +17,6 @@
package net.sergeych.lyngio.console package net.sergeych.lyngio.console
import kotlinx.cinterop.*
import platform.posix.*
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
@ -66,41 +64,4 @@ class LinuxPosixLyngConsoleTest {
assertEquals("A", ev.key) assertEquals("A", ev.key)
assertTrue(ev.shift) assertTrue(ev.shift)
} }
@OptIn(ExperimentalForeignApi::class)
@Test
fun configuresRawModeReadSemantics() = memScoped {
val attrs = alloc<termios>()
attrs.c_iflag =
BRKINT.convert<UInt>() or
ICRNL.convert<UInt>() or
INPCK.convert<UInt>() or
ISTRIP.convert<UInt>() or
IXON.convert<UInt>()
attrs.c_oflag = OPOST.convert<UInt>()
attrs.c_cflag = 0u
attrs.c_lflag =
ECHO.convert<UInt>() or
ICANON.convert<UInt>() or
IEXTEN.convert<UInt>() or
ISIG.convert<UInt>()
attrs.c_cc[VMIN] = 9u
attrs.c_cc[VTIME] = 9u
configureRawInput(attrs)
assertEquals(0u, attrs.c_iflag and BRKINT.convert<UInt>())
assertEquals(0u, attrs.c_iflag and ICRNL.convert<UInt>())
assertEquals(0u, attrs.c_iflag and INPCK.convert<UInt>())
assertEquals(0u, attrs.c_iflag and ISTRIP.convert<UInt>())
assertEquals(0u, attrs.c_iflag and IXON.convert<UInt>())
assertEquals(0u, attrs.c_oflag and OPOST.convert<UInt>())
assertEquals(CS8.convert<UInt>(), attrs.c_cflag and CS8.convert<UInt>())
assertEquals(0u, attrs.c_lflag and ECHO.convert<UInt>())
assertEquals(0u, attrs.c_lflag and ICANON.convert<UInt>())
assertEquals(0u, attrs.c_lflag and IEXTEN.convert<UInt>())
assertEquals(0u, attrs.c_lflag and ISIG.convert<UInt>())
assertEquals(0u, attrs.c_cc[VMIN].toUInt())
assertEquals(1u, attrs.c_cc[VTIME].toUInt())
}
} }

View File

@ -17,35 +17,6 @@
package net.sergeych.lyngio.console package net.sergeych.lyngio.console
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.toKString
import platform.posix.fclose
import platform.posix.fopen
import platform.posix.fputs
import platform.posix.getenv
@OptIn(ExperimentalForeignApi::class)
private val flowDebugLogFilePath: String
get() = getenv("LYNG_CONSOLE_DEBUG_LOG")?.toKString()?.takeIf { it.isNotBlank() }
?: "/tmp/lyng_console_flow_debug.log"
@OptIn(ExperimentalForeignApi::class)
internal actual fun consoleFlowDebug(message: String, error: Throwable?) { internal actual fun consoleFlowDebug(message: String, error: Throwable?) {
runCatching { // no-op on Native
val line = buildString {
append("[console-flow] ")
append(message)
append('\n')
if (error != null) {
append(error.toString())
append('\n')
}
}
val file = fopen(flowDebugLogFilePath, "ab") ?: return
try {
fputs(line, file)
} finally {
fclose(file)
}
}
} }

View File

@ -4674,7 +4674,6 @@ 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)
@ -4729,15 +4728,6 @@ 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)
@ -7398,15 +7388,6 @@ 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
@ -8892,7 +8873,6 @@ 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

@ -1,69 +0,0 @@
/*
* 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()
)
}
}