+added console spport for lyngio
+added console support to lyng/jlyng CLI +added unicode escapes +created tetris console sample
This commit is contained in:
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
|
||||
}
|
||||
Reference in new issue
Block a user