Compare commits
No commits in common. "2414da59a7d1a919f99323a3c8220e34c4ce1e2d" and "caad7d8ab9674c4e64668a7753c2f0ff66de80ab" have entirely different histories.
2414da59a7
...
caad7d8ab9
@ -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) {
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user