fix #18 basic exceptions handling

This commit is contained in:
Sergey Chernov 2025-06-12 19:17:01 +04:00
parent 95aae0b231
commit 6c71f0a2e6
11 changed files with 280 additions and 35 deletions

View File

@ -753,6 +753,34 @@ We can use labels too:
assert( search(["hello", "world"], 'z') == null)
>>> void
# Exception handling
Very much like in Kotlin. Try block returns its body block result, if no exception was cauht, or the result from the catch block that caught the exception:
var error = "not caught"
var finallyCaught = false
val result = try {
throw IllegalArgumentException()
"invalid"
}
catch(nd: SymbolNotDefinedException) {
error = "bad catch"
}
catch(x: IllegalArgumentException) {
error = "no error"
"OK"
}
finally {
// finally does not affect returned value
"too bad"
}
assertEquals( "no error", error)
assertEquals( "OK", result)
>>> void
It is possible to catch several exceptions in the same block (TBD)
# Self-assignments in expression
There are auto-increments and auto-decrements:

View File

@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "0.4.0-SNAPSHOT"
version = "0.5.0-SNAPSHOT"
buildscript {
repositories {
@ -62,6 +62,7 @@ kotlin {
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.contracts.ExperimentalContracts::class")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
}

View File

@ -717,10 +717,115 @@ class Compiler(
"fn", "fun" -> parseFunctionDeclaration(cc)
"if" -> parseIfStatement(cc)
"class" -> parseClassDeclaration(cc, false)
"struct" -> parseClassDeclaration(cc, true)
"try" -> parseTryStatement(cc)
"throw" -> parseThrowStatement(cc)
else -> null
}
private fun parseThrowStatement(cc: CompilerContext): Statement {
val throwStatement = parseStatement(cc) ?: throw ScriptError(cc.currentPos(), "throw object expected")
return statement {
val errorObject = throwStatement.execute(this)
if( errorObject is ObjException )
raiseError(errorObject)
else raiseError("this is not an exception object: $errorObject")
}
}
private data class CatchBlockData(
val catchVar: Token,
val classNames: List<String>,
val block: Statement
)
private fun parseTryStatement(cc: CompilerContext): Statement {
val body = parseBlock(cc)
val catches = mutableListOf<CatchBlockData>()
cc.skipTokens(Token.Type.NEWLINE)
var t = cc.next()
while( t.value == "catch" ) {
ensureLparen(cc)
t = cc.next()
if( t.type != Token.Type.ID ) throw ScriptError(t.pos, "expected catch variable")
val catchVar = t
cc.skipTokenOfType(Token.Type.COLON)
// load list of exception classes
val exClassNames = mutableListOf<String>()
do {
t = cc.next()
if( t.type != Token.Type.ID )
throw ScriptError(t.pos, "expected exception class name")
exClassNames += t.value
t = cc.next()
when(t.type) {
Token.Type.COMMA -> {
continue
}
Token.Type.RPAREN -> {
break
}
else -> throw ScriptError(t.pos, "syntax error: expected ',' or ')'")
}
} while(true)
val block = parseBlock(cc)
catches += CatchBlockData(catchVar, exClassNames, block)
cc.skipTokens(Token.Type.NEWLINE)
t = cc.next()
}
if( catches.isEmpty() )
throw ScriptError(cc.currentPos(), "try block must have at least one catch clause")
val finallyClause = if( t.value == "finally" ) {
parseBlock(cc)
} else {
cc.previous()
null
}
return statement {
var result: Obj = ObjVoid
try {
// body is a parsed block, it already has separate context
result = body.execute(this)
}
catch (e: Exception) {
// convert to appropriate exception
val objException = when(e) {
is ExecutionError -> e.errorObject
else -> ObjUnknownException(this, e.message ?: e.toString())
}
// let's see if we should catch it:
var isCaught = false
for( cdata in catches ) {
var exceptionObject: ObjException? = null
for (exceptionClassName in cdata.classNames) {
val exObj = ObjException.getErrorClass(exceptionClassName)
?: raiseSymbolNotFound("error clas not exists: $exceptionClassName")
println("exObj: $exObj")
println("objException: ${objException.objClass}")
if( objException.objClass == exObj )
exceptionObject = objException
break
}
if( exceptionObject != null ) {
val catchContext = this.copy(pos = cdata.catchVar.pos)
catchContext.addItem(cdata.catchVar.value, false, objException)
result = cdata.block.execute(catchContext)
isCaught = true
break
}
}
// rethrow if not caught this exception
if( !isCaught )
throw e
}
finally {
// finally clause does not alter result!
finallyClause?.execute(this)
}
result
}
}
private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val constructorArgsDeclaration =

View File

@ -16,27 +16,27 @@ class Context(
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
@Suppress("unused")
fun raiseNPE(): Nothing = raiseError(ObjNullPointerError(this))
fun raiseNPE(): Nothing = raiseError(ObjNullPointerException(this))
@Suppress("unused")
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
raiseError(ObjIndexOutOfBoundsError(this, message))
raiseError(ObjIndexOutOfBoundsException(this, message))
@Suppress("unused")
fun raiseArgumentError(message: String = "Illegal argument error"): Nothing =
raiseError(ObjIllegalArgumentError(this, message))
raiseError(ObjIllegalArgumentException(this, message))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastError(this, msg))
fun raiseClassCastError(msg: String): Nothing = raiseError(ObjClassCastException(this, msg))
@Suppress("unused")
fun raiseSymbolNotFound(name: String): Nothing =
raiseError(ObjSymbolNotDefinedError(this, "symbol is not defined: $name"))
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing {
throw ExecutionError(ObjError(this, message))
throw ExecutionError(ObjException(this, message))
}
fun raiseError(obj: ObjError): Nothing {
fun raiseError(obj: ObjException): Nothing {
throw ExecutionError(obj)
}

View File

@ -4,7 +4,10 @@ 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.synctools.ProtectedOp
import net.sergeych.synctools.withLock
import kotlin.contracts.ExperimentalContracts
/**
* Record to store object with access rules, e.g. [isMutable] and access level [visibility].
@ -63,7 +66,7 @@ open class Obj {
suspend fun invokeInstanceMethod(context: Context, name: String, vararg args: Obj): Obj =
invokeInstanceMethod(context, name, Arguments(args.toList()))
inline suspend fun <reified T : Obj> callMethod(
suspend inline fun <reified T : Obj> callMethod(
context: Context,
name: String,
args: Arguments = Arguments.EMPTY
@ -179,11 +182,10 @@ open class Obj {
suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
suspend open fun readField(context: Context, name: String): ObjRecord {
open suspend fun readField(context: Context, name: String): ObjRecord {
// could be property or class field:
val obj = objClass.getInstanceMemberOrNull(name) ?: context.raiseError("no such field: $name")
val value = obj.value
return when (value) {
return when (val value = obj.value) {
is Statement -> {
ObjRecord(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable)
}
@ -327,21 +329,97 @@ data class ObjNamespace(val name: String) : Obj() {
}
}
open class ObjError(val context: Context, val message: String) : Obj() {
override val asStr: ObjString by lazy { ObjString("Error: $message") }
open class ObjException(exceptionClass: ExceptionClass, val context: Context, val message: String) : Obj() {
constructor(name: String,context: Context, message: String) : this(getOrCreateExceptionClass(name), context, message)
constructor(context: Context, message: String) : this(Root, context, message)
fun raise(): Nothing {
throw ExecutionError(this)
}
override val objClass: ObjClass = exceptionClass
override fun toString(): String {
return "ObjException:${objClass.className}:${context.pos}@${hashCode().encodeToHex()}"
}
companion object {
class ExceptionClass(val name: String,vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun callOn(context: Context): Obj {
return ObjException(this, context, name).apply {
println(">>>> "+this)
}
}
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}"
}
val Root = ExceptionClass("Throwable")
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(context: Context) {
context.addConst("Exception", Root)
existingErrorClasses["Exception"] = Root
for (name in listOf(
"NullPointerException",
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"IllegalArgumentException",
"IllegalAssignmentException",
"SymbolNotDefinedException",
"IterationEndException",
"AccessException",
"UnknownException",
)) {
context.addConst(name, getOrCreateExceptionClass(name))
}
}
}
}
class ObjNullPointerError(context: Context) : ObjError(context, "object is null")
class ObjNullPointerException(context: Context) : ObjException("NullPointerException", context, "object is null")
class ObjAssertionError(context: Context, message: String) : ObjError(context, message)
class ObjClassCastError(context: Context, message: String) : ObjError(context, message)
class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message)
class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message)
class ObjIllegalAssignmentError(context: Context, message: String = "illegal assignment") : ObjError(context, message)
class ObjSymbolNotDefinedError(context: Context, message: String = "symbol is not defined") : ObjError(context, message)
class ObjIterationFinishedError(context: Context) : ObjError(context, "iteration finished")
class ObjAccessError(context: Context, message: String = "access not allowed error") : ObjError(context, message)
class ObjAssertionFailedException(context: Context, message: String) :
ObjException("AssertionFailedException", context, message)
class ObjClassCastException(context: Context, message: String) : ObjException("ClassCastException", context, message)
class ObjIndexOutOfBoundsException(context: Context, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", context, message)
class ObjIllegalArgumentException(context: Context, message: String = "illegal argument") :
ObjException("IllegalArgumentException", context, message)
class ObjIllegalAssignmentException(context: Context, message: String = "illegal assignment") :
ObjException("IllegalAssignmentException", context, message)
class ObjSymbolNotDefinedException(context: Context, message: String = "symbol is not defined") :
ObjException("SymbolNotDefinedException", context, message)
class ObjIterationFinishedException(context: Context) :
ObjException("IterationEndException", context, "iteration finished")
class ObjAccessException(context: Context, message: String = "access not allowed error") :
ObjException("AccessException", context, message)
class ObjUnknownException(context: Context, message: String = "access not allowed error") :
ObjException("UnknownException", context, message)

View File

@ -2,7 +2,7 @@ package net.sergeych.lyng
val ObjClassType by lazy { ObjClass("Class") }
class ObjClass(
open class ObjClass(
val className: String,
vararg val parents: ObjClass,
) : Obj() {
@ -122,7 +122,7 @@ class ObjArrayIterator(val array: Obj) : Obj() {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) {
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
} else raiseError(ObjIterationFinishedError(this))
} else raiseError(ObjIterationFinishedException(this))
}
addFn("hasNext") {
val self = thisAs<ObjArrayIterator>()

View File

@ -9,7 +9,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (it.visibility.isPublic)
it
else
context.raiseError(ObjAccessError(context, "can't access non-public field $name"))
context.raiseError(ObjAccessException(context, "can't access non-public field $name"))
}
?: super.readField(context, name)
}
@ -17,8 +17,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun writeField(context: Context, name: String, newValue: Obj) {
instanceContext[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentError(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentError(context, "can't reassign val $name").raise()
ObjIllegalAssignmentException(context, "can't assign to non-public field $name")
if (!f.isMutable) ObjIllegalAssignmentException(context, "can't reassign val $name").raise()
if (f.value.assign(context, newValue) == null)
f.value = newValue
} ?: super.writeField(context, name, newValue)
@ -29,7 +29,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (it.visibility.isPublic)
it.value.invoke(context, this, args)
else
context.raiseError(ObjAccessError(context, "can't invoke non-public method $name"))
context.raiseError(ObjAccessException(context, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(context, name, args)

View File

@ -33,7 +33,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x)
}
else {
context.raiseError(ObjIterationFinishedError(context))
context.raiseError(ObjIterationFinishedException(context))
}
companion object {

View File

@ -21,6 +21,7 @@ class Script(
companion object {
val defaultContext: Context = Context().apply {
ObjException.addExceptionsToContext(this)
addFn("println") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.asStr.value)
@ -117,14 +118,14 @@ class Script(
addVoidFn("assert") {
val cond = requiredArg<ObjBool>(0)
if( !cond.value == true )
raiseError(ObjAssertionError(this,"Assertion failed"))
raiseError(ObjAssertionFailedException(this,"Assertion failed"))
}
addVoidFn("assertEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionError(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
}
addFn("assertThrows") {
val code = requireOnlyArg<Statement>()
@ -138,7 +139,7 @@ class Script(
catch (e: ScriptError) {
ObjNull
}
result ?: raiseError(ObjAssertionError(this,"Expected exception but nothing was thrown"))
result ?: raiseError(ObjAssertionFailedException(this,"Expected exception but nothing was thrown"))
}
addVoidFn("delay") {

View File

@ -12,4 +12,4 @@ open class ScriptError(val pos: Pos, val errorMessage: String,cause: Throwable?=
cause
)
class ExecutionError(val errorObject: ObjError) : ScriptError(errorObject.context.pos, errorObject.message)
class ExecutionError(val errorObject: ObjException) : ScriptError(errorObject.context.pos, errorObject.message)

View File

@ -1748,4 +1748,36 @@ class ScriptTest {
"""
)
}
@Test
fun testThrowExisting()= runTest {
eval("""
val x = IllegalArgumentException("test")
println("instance class",x::class)
println("instance", x)
println("Exception object",Exception)
println("... and it's class",Exception::class)
assert( x is Exception )
println(x)
var t = 0
var finallyCaught = false
try {
t = 1
throw x
t = 2
}
catch( e: SymbolNotDefinedException ) {
t = 101
}
catch( e: IllegalArgumentException ) {
t = 3
}
finally {
finallyCaught = true
}
assertEquals(3, t)
assert(finallyCaught)
""".trimIndent())
}
}