anotther compiler inference bug andlyng.io.console improvements
This commit is contained in:
parent
caad7d8ab9
commit
fd6d05d568
@ -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) {
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user