ref #49 basic regex support (but not in when yet)

This commit is contained in:
Sergey Chernov 2025-08-25 12:32:50 +03:00
parent 2743511b62
commit ead2f7168e
11 changed files with 397 additions and 208 deletions

View File

@ -1184,6 +1184,30 @@ See also [math operations](math.md)
The type for the character objects is `Char`.
### String literal escapes
| escape | ASCII value |
|--------|-----------------------|
| \n | 0x10, newline |
| \r | 0x13, carriage return |
| \t | 0x07, tabulation |
| \\ | \ slash character |
| \" | " double quote |
Other `\c` combinations, where c is any char except mentioned above, are left intact, e.g.:
val s = "\a"
assert(s[0] == '\')
assert(s[1] == 'a')
>>> void
same as:
val s = "\\a"
assert(s[0] == '\')
assert(s[1] == 'a')
>>> void
### Char literal escapes
Are the same as in string literals with little difference:
@ -1191,6 +1215,7 @@ Are the same as in string literals with little difference:
| escape | ASCII value |
|--------|-------------------|
| \n | 0x10, newline |
| \r | 0x13, carriage return |
| \t | 0x07, tabulation |
| \\ | \ slash character |
| \' | ' apostrophe |

View File

@ -1376,9 +1376,16 @@ class Compiler(
if (sourceObj is ObjRange && sourceObj.isIntRange) {
loopIntRange(
forContext,
sourceObj.start!!.toInt(),
if (sourceObj.isEndInclusive) sourceObj.end!!.toInt() + 1 else sourceObj.end!!.toInt(),
loopSO, body, elseStatement, label, canBreak
sourceObj.start!!.toLong(),
if (sourceObj.isEndInclusive)
sourceObj.end!!.toLong() + 1
else
sourceObj.end!!.toLong(),
loopSO,
body,
elseStatement,
label,
canBreak
)
} else if (sourceObj.isInstanceOf(ObjIterable)) {
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
@ -1439,7 +1446,7 @@ class Compiler(
}
private suspend fun loopIntRange(
forScope: Scope, start: Int, end: Int, loopVar: ObjRecord,
forScope: Scope, start: Long, end: Long, loopVar: ObjRecord,
body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean
): Obj {
var result: Obj = ObjVoid
@ -1447,7 +1454,7 @@ class Compiler(
loopVar.value = iVar
if (catchBreak) {
for (i in start..<end) {
iVar.value = i.toLong()
iVar.value = i//.toLong()
try {
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
@ -1459,7 +1466,7 @@ class Compiler(
}
}
} else {
for (i in start.toLong()..<end.toLong()) {
for (i in start ..< end) {
iVar.value = i
result = body.execute(forScope)
}

View File

@ -437,7 +437,11 @@ private class Parser(fromPos: Pos) {
'r' -> {sb.append('\r'); pos.advance()}
't' -> {sb.append('\t'); pos.advance()}
'"' -> {sb.append('"'); pos.advance()}
else -> sb.append('\\').append(currentChar)
'\\' -> {sb.append('\\'); pos.advance()}
else -> {
sb.append('\\').append(currentChar)
pos.advance()
}
}
}

View File

@ -136,11 +136,6 @@ open class Scope(
?: thisObj.objClass
.getInstanceMemberOrNull(name)
)
// ?.also {
// if( name == "predicate") {
// println("got predicate $it")
// }
// }
}
fun copy(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =

View File

@ -241,6 +241,8 @@ class Script(
addConst("CompletableDeferred", ObjCompletableDeferred.type)
addConst("Mutex", ObjMutex.type)
addConst("Regex", ObjRegex.type)
addFn("launch") {
val callable = requireOnlyArg<Statement>()
ObjDeferred(globalDefer {

View File

@ -21,15 +21,11 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.bintools.encodeToHex
import net.sergeych.lyng.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
import net.sergeych.mptools.CachedExpression
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts
open class Obj {
@ -500,195 +496,4 @@ data class ObjNamespace(val name: String) : Obj() {
}
}
/**
* note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated
* from the current scope which is treated as exception scope. It is used to restore serialized
* exception with stack trace; the scope of the de-serialized exception is not valid
* for stack unwinding.
*/
open class ObjException(
val exceptionClass: ExceptionClass,
val scope: Scope,
val message: ObjString,
@Suppress("unused") val extraData: Obj = ObjNull,
val useStackTrace: ObjList? = null
) : Obj() {
constructor(name: String, scope: Scope, message: String) : this(
getOrCreateExceptionClass(name),
scope,
ObjString(message)
)
private val cachedStackTrace = CachedExpression(initialValue = useStackTrace)
suspend fun getStackTrace(): ObjList {
return cachedStackTrace.get {
val result = ObjList()
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
var s: Scope? = scope
var lastPos: Pos? = null
while (s != null) {
val pos = s.pos
if (pos != lastPos && !pos.currentLine.isEmpty()) {
result.list += cls.callWithArgs(
scope,
pos.source.objSourceName,
ObjInt(pos.line.toLong()),
ObjInt(pos.column.toLong()),
ObjString(pos.currentLine)
)
}
s = s.parent
lastPos = pos
}
result
}
}
constructor(scope: Scope, message: String) : this(Root, scope, ObjString(message))
fun raise(): Nothing {
throw ExecutionError(this)
}
override val objClass: ObjClass = exceptionClass
override suspend fun toString(scope: Scope,calledFromLyng: Boolean): ObjString {
val at = getStackTrace().list.firstOrNull()?.toString(scope)
?: ObjString("(unknown)")
return ObjString("${objClass.className}: $message at $at")
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAny(scope, exceptionClass.classNameObj)
encoder.encodeAny(scope, message)
encoder.encodeAny(scope, extraData)
encoder.encodeAny(scope, getStackTrace())
}
companion object {
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(scope: Scope): Obj {
val message = scope.args.getOrNull(0)?.toString(scope) ?: ObjString(name)
return ObjException(this, scope, message)
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
return try {
val lyngClass = decoder.decodeAnyAs<ObjString>(scope).value.let {
((scope[it] ?: scope.raiseIllegalArgument("Unknown exception class: $it"))
.value as? ExceptionClass)
?: scope.raiseIllegalArgument("Not an exception class: $it")
}
ObjException(
lyngClass,
scope,
decoder.decodeAnyAs<ObjString>(scope),
decoder.decodeAny(scope),
decoder.decodeAnyAs<ObjList>(scope)
)
} catch (e: ScriptError) {
throw e
} catch (e: Exception) {
e.printStackTrace()
scope.raiseIllegalArgument("Failed to deserialize exception: ${e.message}")
}
}
}
val Root = ExceptionClass("Throwable").apply {
addConst("message", statement {
(thisObj as ObjException).message.toObj()
})
addFn("stackTrace") {
(thisObj as ObjException).getStackTrace()
}
}
private val op = ProtectedOp()
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
@OptIn(ExperimentalContracts::class)
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
return op.withLock {
existingErrorClasses.getOrPut(name) {
ExceptionClass(name, Root)
}
}
}
/**
* Get [ObjClass] for error class by name if exists.
*/
@OptIn(ExperimentalContracts::class)
fun getErrorClass(name: String): ObjClass? = op.withLock {
existingErrorClasses[name]
}
fun addExceptionsToContext(scope: Scope) {
scope.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullReferenceException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"NoSuchElementException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
"AccessException",
"UnknownException",
"NotFoundException"
)) {
scope.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
class ObjAssertionFailedException(scope: Scope, message: String) :
ObjException("AssertionFailedException", scope, message)
class ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", scope, message)
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
ObjException("IllegalArgumentException", scope, message)
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
ObjException("IllegalStateException", scope, message)
@Suppress("unused")
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
ObjException("IllegalArgumentException", scope, message)
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
ObjException("NoSuchElementException", scope, message)
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", scope, message)
class ObjIterationFinishedException(scope: Scope) :
ObjException("IterationEndException", scope, "iteration finished")
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
ObjException("AccessException", scope, message)
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
ObjException("UnknownException", scope, message)
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
ObjException("IllegalOperationException", scope, message)
class ObjNotFoundException(scope: Scope, message: String = "not found") :
ObjException("NotFoundException", scope, message)

View File

@ -0,0 +1,221 @@
/*
* Copyright 2025 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.obj
import net.sergeych.bintools.encodeToHex
import net.sergeych.lyng.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
import net.sergeych.mptools.CachedExpression
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts
/**
* note on [getStackTrace]. If [useStackTrace] is not null, it is used instead. Otherwise, it is calculated
* from the current scope which is treated as exception scope. It is used to restore serialized
* exception with stack trace; the scope of the de-serialized exception is not valid
* for stack unwinding.
*/
open class ObjException(
val exceptionClass: ExceptionClass,
val scope: Scope,
val message: ObjString,
@Suppress("unused") val extraData: Obj = ObjNull,
val useStackTrace: ObjList? = null
) : Obj() {
constructor(name: String, scope: Scope, message: String) : this(
getOrCreateExceptionClass(name),
scope,
ObjString(message)
)
private val cachedStackTrace = CachedExpression(initialValue = useStackTrace)
suspend fun getStackTrace(): ObjList {
return cachedStackTrace.get {
val result = ObjList()
val cls = scope.get("StackTraceEntry")!!.value as ObjClass
var s: Scope? = scope
var lastPos: Pos? = null
while (s != null) {
val pos = s.pos
if (pos != lastPos && !pos.currentLine.isEmpty()) {
result.list += cls.callWithArgs(
scope,
pos.source.objSourceName,
ObjInt(pos.line.toLong()),
ObjInt(pos.column.toLong()),
ObjString(pos.currentLine)
)
}
s = s.parent
lastPos = pos
}
result
}
}
constructor(scope: Scope, message: String) : this(Root, scope, ObjString(message))
fun raise(): Nothing {
throw ExecutionError(this)
}
override val objClass: ObjClass = exceptionClass
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
val at = getStackTrace().list.firstOrNull()?.toString(scope)
?: ObjString("(unknown)")
return ObjString("${objClass.className}: $message at $at")
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeAny(scope, exceptionClass.classNameObj)
encoder.encodeAny(scope, message)
encoder.encodeAny(scope, extraData)
encoder.encodeAny(scope, getStackTrace())
}
companion object {
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(scope: Scope): Obj {
val message = scope.args.getOrNull(0)?.toString(scope) ?: ObjString(name)
return ObjException(this, scope, message)
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
return try {
val lyngClass = decoder.decodeAnyAs<ObjString>(scope).value.let {
((scope[it] ?: scope.raiseIllegalArgument("Unknown exception class: $it"))
.value as? ExceptionClass)
?: scope.raiseIllegalArgument("Not an exception class: $it")
}
ObjException(
lyngClass,
scope,
decoder.decodeAnyAs<ObjString>(scope),
decoder.decodeAny(scope),
decoder.decodeAnyAs<ObjList>(scope)
)
} catch (e: ScriptError) {
throw e
} catch (e: Exception) {
e.printStackTrace()
scope.raiseIllegalArgument("Failed to deserialize exception: ${e.message}")
}
}
}
val Root = ExceptionClass("Throwable").apply {
addConst("message", statement {
(thisObj as ObjException).message.toObj()
})
addFn("stackTrace") {
(thisObj as ObjException).getStackTrace()
}
}
private val op = ProtectedOp()
private val existingErrorClasses = mutableMapOf<String, ExceptionClass>()
@OptIn(ExperimentalContracts::class)
protected fun getOrCreateExceptionClass(name: String): ExceptionClass {
return op.withLock {
existingErrorClasses.getOrPut(name) {
ExceptionClass(name, Root)
}
}
}
/**
* Get [ObjClass] for error class by name if exists.
*/
@OptIn(ExperimentalContracts::class)
fun getErrorClass(name: String): ObjClass? = op.withLock {
existingErrorClasses[name]
}
fun addExceptionsToContext(scope: Scope) {
scope.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullReferenceException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"NoSuchElementException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
"AccessException",
"UnknownException",
"NotFoundException"
)) {
scope.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullReferenceException(scope: Scope) : ObjException("NullReferenceException", scope, "object is null")
class ObjAssertionFailedException(scope: Scope, message: String) :
ObjException("AssertionFailedException", scope, message)
class ObjClassCastException(scope: Scope, message: String) : ObjException("ClassCastException", scope, message)
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", scope, message)
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
ObjException("IllegalArgumentException", scope, message)
class ObjIllegalStateException(scope: Scope, message: String = "illegal state") :
ObjException("IllegalStateException", scope, message)
@Suppress("unused")
class ObjNoSuchElementException(scope: Scope, message: String = "no such element") :
ObjException("IllegalArgumentException", scope, message)
class ObjIllegalAssignmentException(scope: Scope, message: String = "illegal assignment") :
ObjException("NoSuchElementException", scope, message)
class ObjSymbolNotDefinedException(scope: Scope, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", scope, message)
class ObjIterationFinishedException(scope: Scope) :
ObjException("IterationEndException", scope, "iteration finished")
class ObjAccessException(scope: Scope, message: String = "access not allowed error") :
ObjException("AccessException", scope, message)
class ObjUnknownException(scope: Scope, message: String = "access not allowed error") :
ObjException("UnknownException", scope, message)
class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") :
ObjException("IllegalOperationException", scope, message)
class ObjNotFoundException(scope: Scope, message: String = "not found") :
ObjException("NotFoundException", scope, message)

View File

@ -0,0 +1,90 @@
/*
* Copyright 2025 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.obj
import net.sergeych.lyng.Scope
class ObjRegex(val regex: Regex) : Obj() {
override val objClass = type
companion object {
val type by lazy {
object : ObjClass("Regex") {
override suspend fun callOn(scope: Scope): Obj {
println(scope.requireOnlyArg<ObjString>().value)
return ObjRegex(
scope.requireOnlyArg<ObjString>().value.toRegex()
)
}
}.apply {
addFn("matches") {
ObjBool(args.firstAndOnly().toString().matches(thisAs<ObjRegex>().regex))
}
addFn("find") {
val s = requireOnlyArg<ObjString>().value
thisAs<ObjRegex>().regex.find(s)?.let { ObjRegexMatch(it) } ?: ObjNull
}
addFn("findAll") {
val s = requireOnlyArg<ObjString>().value
ObjList(thisAs<ObjRegex>().regex.findAll(s).map { ObjRegexMatch(it) }.toMutableList())
}
}
}
}
}
class ObjRegexMatch(val match: MatchResult) : Obj() {
override val objClass = type
val objGroups: ObjList by lazy {
ObjList(
match.groups.map { it?.let { ObjString(it.value) } ?: ObjNull }.toMutableList()
)
}
val objValue by lazy { ObjString(match.value) }
val objRange: ObjRange by lazy {
val r = match.range
ObjRange(
ObjInt(r.first.toLong()),
ObjInt(r.last.toLong()),
false
)
}
companion object {
val type by lazy {
object : ObjClass("RegexMatch") {
override suspend fun callOn(scope: Scope): Obj {
scope.raiseError("RegexMatch can't be constructed directly")
}
}.apply {
addFn("groups") {
thisAs<ObjRegexMatch>().objGroups
}
addFn("value") {
thisAs<ObjRegexMatch>().objValue
}
addFn("range") {
thisAs<ObjRegexMatch>().objRange
}
}
}
}
}

View File

@ -165,6 +165,8 @@ fun Exception.printStackTrace() {
}
}
fun String.re() { Regex(this) }
""".trimIndent()

View File

@ -3242,7 +3242,45 @@ class ScriptTest {
result.insertAt(-i-1, x)
}
assertEquals( src.sorted(), result )
""".trimIndent())
""".trimIndent()
)
}
// @Test
fun testMinimumOptimization() = runTest {
val x = Scope().eval(
"""
fun naiveCountHappyNumbers() {
var count = 0
for( n1 in 0..9 )
for( n2 in 0..9 )
for( n3 in 0..9 )
for( n4 in 0..9 )
for( n5 in 0..9 )
for( n6 in 0..9 )
if( n1 + n2 + n3 == n4 + n5 + n6 ) count++
count
}
naiveCountHappyNumbers()
""".trimIndent()
).toInt()
assertEquals(55252, x)
}
@Test
fun testRegex1() = runTest {
eval(
"""
assert( ! "123".re.matches("abs123def") )
assert( ".*123.*".re.matches("abs123def") )
// assertEquals( "123", "123".re.find("abs123def")?.value )
// assertEquals( "123", "[0-9]{3}".re.find("abs123def")?.value )
assertEquals( "123", "\d{3}".re.find("abs123def")?.value )
assertEquals( "123", "\\d{3}".re.find("abs123def")?.value )
assertEquals( [1,2,3], "\d".re.findAll("abs123def").map { it.value.toInt() } )
"""
.trimIndent()
)
}
}