diff --git a/docs/tutorial.md b/docs/tutorial.md index bf68143..00315d0 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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: diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 514cc16..92448ca 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -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") } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 8eef7e3..e6b6a50 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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, + val block: Statement + ) + + private fun parseTryStatement(cc: CompilerContext): Statement { + val body = parseBlock(cc) + val catches = mutableListOf() + 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() + 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 = diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index 5795be4..4cac52f 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -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) } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index 10a873e..d76a19b 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -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 callMethod( + suspend inline fun callMethod( context: Context, name: String, args: Arguments = Arguments.EMPTY @@ -179,11 +182,10 @@ open class Obj { suspend fun 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() + + + @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) \ No newline at end of file +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) diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index 56ef4d7..4c7f777 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -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() 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() diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjInstance.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjInstance.kt index 80d9140..73c2987 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjInstance.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjInstance.kt @@ -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) diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjRangeIterator.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjRangeIterator.kt index 0afc895..bf2afb6 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjRangeIterator.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjRangeIterator.kt @@ -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 { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 632dccb..26d4900 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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(0) if( !cond.value == true ) - raiseError(ObjAssertionError(this,"Assertion failed")) + raiseError(ObjAssertionFailedException(this,"Assertion failed")) } addVoidFn("assertEquals") { val a = requiredArg(0) val b = requiredArg(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() @@ -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") { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt index 68aab66..14d6b14 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt @@ -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) diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 0dddc31..7893cc3 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -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()) + } } \ No newline at end of file