+added console spport for lyngio

+added console support to lyng/jlyng CLI
+added unicode escapes
+created tetris console sample
This commit is contained in:
sergeych committed 2026-03-19 01:09:32 +03:00
1 parent 1a080bc53e
commit d9d7cafec8
44 files changed
+3842 -28

No files matched your search

@@ -0,0 +1,22 @@
/*
* 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.lyng.bytecode
internal actual fun vmIterDebug(message: String, error: Throwable?) {
// no-op on Android
}
@@ -306,17 +306,41 @@ private class Parser(fromPos: Pos) {
'\'' -> {
val start = pos.toPos()
var value = currentChar
pos.advance()
if (currentChar == '\\') {
value = currentChar
pos.advance()
value = when (value) {
'n' -> '\n'
'r' -> '\r'
't' -> '\t'
'\'', '\\' -> value
else -> throw ScriptError(currentPos, "unsupported escape character: $value")
if (pos.end) throw ScriptError(start, "unterminated character literal")
value = when (currentChar) {
'n' -> {
pos.advance()
'\n'
}
'r' -> {
pos.advance()
'\r'
}
't' -> {
pos.advance()
'\t'
}
'\'' -> {
pos.advance()
'\''
}
'\\' -> {
pos.advance()
'\\'
}
'u' -> loadUnicodeEscape(start)
else -> throw ScriptError(currentPos, "unsupported escape character: $currentChar")
}
} else {
pos.advance()
}
if (currentChar != '\'') throw ScriptError(currentPos, "expected end of character literal: '")
pos.advance()
@@ -494,6 +518,10 @@ private class Parser(fromPos: Pos) {
sb.append('\\'); pos.advance()
}
'u' -> {
sb.append(loadUnicodeEscape(start))
}
else -> {
sb.append('\\').append(currentChar)
pos.advance()
@@ -520,6 +548,23 @@ private class Parser(fromPos: Pos) {
return Token(result, start, Token.Type.STRING)
}
private fun loadUnicodeEscape(start: Pos): Char {
// Called when currentChar points to 'u' right after a backslash.
if (currentChar != 'u') throw ScriptError(currentPos, "expected unicode escape marker: u")
pos.advance() ?: throw ScriptError(start, "unterminated unicode escape")
var code = 0
repeat(4) {
val ch = currentChar
if (ch !in hexDigits) {
throw ScriptError(currentPos, "invalid unicode escape sequence, expected 4 hex digits")
}
code = (code shl 4) + ch.digitToInt(16)
pos.advance()
}
return code.toChar()
}
/**
* Load characters from the set until it reaches EOF or invalid character found.
* stop at EOF on character filtered by [isValidChar].
@@ -3784,6 +3784,7 @@ class CmdFrame(
val exceptionSlot: Int,
val catchIp: Int,
val finallyIp: Int,
val iterDepthAtPush: Int,
var inCatch: Boolean = false
)
internal val tryStack = ArrayDeque<TryHandler>()
@@ -4069,11 +4070,15 @@ class CmdFrame(
return scope
}
fun handleException(t: Throwable): Boolean {
suspend fun handleException(t: Throwable): Boolean {
val handler = tryStack.lastOrNull() ?: return false
vmIterDebug(
"handleException fn=${fn.name} throwable=${t::class.simpleName} message=${t.message} catchIp=${handler.catchIp} finallyIp=${handler.finallyIp} iterDepth=${iterStack.size}"
)
val finallyIp = handler.finallyIp
if (t is ReturnException || t is LoopBreakContinueException) {
if (finallyIp >= 0) {
cancelIteratorsToDepth(handler.iterDepthAtPush, "handleException:returnOrLoop->finally")
pendingThrowable = t
ip = finallyIp
return true
@@ -4082,6 +4087,7 @@ class CmdFrame(
}
if (handler.inCatch) {
if (finallyIp >= 0) {
cancelIteratorsToDepth(handler.iterDepthAtPush, "handleException:inCatch->finally")
pendingThrowable = t
ip = finallyIp
return true
@@ -4091,6 +4097,7 @@ class CmdFrame(
handler.inCatch = true
pendingThrowable = t
if (handler.catchIp >= 0) {
cancelIteratorsToDepth(handler.iterDepthAtPush, "handleException:toCatch")
val caughtObj = when (t) {
is ExecutionError -> t.errorObject
else -> ObjUnknownException(ensureScope(), t.message ?: t.toString())
@@ -4100,6 +4107,7 @@ class CmdFrame(
return true
}
if (finallyIp >= 0) {
cancelIteratorsToDepth(handler.iterDepthAtPush, "handleException:toFinallyNoCatch")
ip = finallyIp
return true
}
@@ -4107,7 +4115,7 @@ class CmdFrame(
}
fun pushTry(exceptionSlot: Int, catchIp: Int, finallyIp: Int) {
tryStack.addLast(TryHandler(exceptionSlot, catchIp, finallyIp))
tryStack.addLast(TryHandler(exceptionSlot, catchIp, finallyIp, iterDepthAtPush = iterStack.size))
}
fun popTry() {
@@ -4164,24 +4172,47 @@ class CmdFrame(
fun pushIterator(iter: Obj) {
iterStack.addLast(iter)
if (iter.objClass.className == "FlowIterator") {
vmIterDebug("pushIterator fn=${fn.name} depth=${iterStack.size} iterClass=${iter.objClass.className}")
}
}
fun popIterator() {
val iter = iterStack.lastOrNull()
if (iter != null && iter.objClass.className == "FlowIterator") {
vmIterDebug("popIterator fn=${fn.name} depth=${iterStack.size} iterClass=${iter.objClass.className}")
}
iterStack.removeLastOrNull()
}
suspend fun cancelTopIterator() {
val iter = iterStack.removeLastOrNull() ?: return
vmIterDebug("cancelTopIterator fn=${fn.name} depthAfter=${iterStack.size} iterClass=${iter.objClass.className}")
iter.invokeInstanceMethod(ensureScope(), "cancelIteration") { ObjVoid }
}
suspend fun cancelIterators() {
while (iterStack.isNotEmpty()) {
val iter = iterStack.removeLast()
vmIterDebug("cancelIterators fn=${fn.name} depthAfter=${iterStack.size} iterClass=${iter.objClass.className}")
iter.invokeInstanceMethod(ensureScope(), "cancelIteration") { ObjVoid }
}
}
private suspend fun cancelIteratorsToDepth(depth: Int, reason: String) {
while (iterStack.size > depth) {
val iter = iterStack.removeLast()
vmIterDebug(
"cancelIteratorsToDepth fn=${fn.name} reason=$reason targetDepth=$depth depthAfter=${iterStack.size} iterClass=${iter.objClass.className}"
)
try {
iter.invokeInstanceMethod(ensureScope(), "cancelIteration") { ObjVoid }
} catch (e: Throwable) {
vmIterDebug("cancelIteratorsToDepth: cancelIteration failed fn=${fn.name} reason=$reason", e)
}
}
}
fun pushSlotPlan(plan: Map<String, Int>) {
if (scope.hasSlotPlanConflict(plan)) {
scopeStack.addLast(scope)
@@ -0,0 +1,20 @@
/*
* 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.lyng.bytecode
internal expect fun vmIterDebug(message: String, error: Throwable? = null)
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -24,7 +24,8 @@ import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.lyng.*
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScriptFlowIsNoMoreCollected
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
@@ -55,7 +56,7 @@ class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
else
// Flow consumer is no longer collecting; signal producer to stop
throw ScriptFlowIsNoMoreCollected()
} catch (x: Exception) {
} catch (x: Throwable) {
// Any failure to send (including closed channel) should gracefully stop the producer.
if (x is CancellationException) {
// Cancellation is a normal control-flow event
@@ -80,7 +81,7 @@ private fun createLyngFlowInput(scope: Scope, producer: Obj): ReceiveChannel<Obj
producer.callOn(builderScope)
} catch (x: ScriptFlowIsNoMoreCollected) {
// premature flow closing, OK
} catch (x: Exception) {
} catch (x: Throwable) {
channel.close(x)
return@globalLaunch
}
@@ -0,0 +1,60 @@
/*
* 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.*
import net.sergeych.lyng.obj.ObjChar
import net.sergeych.lyng.obj.ObjString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class UnicodeEscapeTest {
@Test
fun parserDecodesUnicodeEscapeInStringLiteral() {
val token = parseLyng("\"\\u263A\"".toSource()).first()
assertEquals(Token.Type.STRING, token.type)
assertEquals("☺", token.value)
}
@Test
fun parserDecodesUnicodeEscapeInCharLiteral() {
val token = parseLyng("'\\u263A'".toSource()).first()
assertEquals(Token.Type.CHAR, token.type)
assertEquals("☺", token.value)
}
@Test
fun parserRejectsMalformedUnicodeEscapeInStringLiteral() {
assertFailsWith<ScriptError> {
parseLyng("\"\\u12G4\"".toSource())
}
}
@Test
fun parserRejectsShortUnicodeEscapeInCharLiteral() {
assertFailsWith<ScriptError> {
parseLyng("'\\u12'".toSource())
}
}
@Test
fun evalDecodesUnicodeEscapes() = runTest {
assertEquals(ObjString("☺"), eval("\"\\u263A\""))
assertEquals(ObjChar('☺'), eval("'\\u263A'"))
}
}
@@ -0,0 +1,22 @@
/*
* 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.lyng.bytecode
internal actual fun vmIterDebug(message: String, error: Throwable?) {
// no-op on JS
}
@@ -0,0 +1,46 @@
/*
* 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.lyng.bytecode
import java.io.File
import java.time.Instant
private val vmIterLogFilePath: String =
System.getenv("LYNG_VM_DEBUG_LOG")
?.takeIf { it.isNotBlank() }
?: "/tmp/lyng_vm_iter_debug.log"
private val vmIterLogLock = Any()
internal actual fun vmIterDebug(message: String, error: Throwable?) {
runCatching {
val line = buildString {
append(Instant.now().toString())
append(" [vm-iter] ")
append(message)
append('\n')
if (error != null) {
append(error.stackTraceToString())
append('\n')
}
}
synchronized(vmIterLogLock) {
File(vmIterLogFilePath).appendText(line)
}
}
}
@@ -0,0 +1,22 @@
/*
* 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.lyng.bytecode
internal actual fun vmIterDebug(message: String, error: Throwable?) {
// no-op on Native
}
@@ -0,0 +1,22 @@
/*
* 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.lyng.bytecode
internal actual fun vmIterDebug(message: String, error: Throwable?) {
// no-op on wasmJs
}