diff --git a/docs/exceptions_handling.md b/docs/exceptions_handling.md index fd07c94..324be34 100644 --- a/docs/exceptions_handling.md +++ b/docs/exceptions_handling.md @@ -131,7 +131,7 @@ _this functionality is not yet released_ | class | notes | |----------------------------|-------------------------------------------------------| | Exception | root of al throwable objects | -| NullPointerException | | +| NullReferenceException | | | AssertionFailedException | | | ClassCastException | | | IndexOutOfBoundsException | | diff --git a/docs/tutorial.md b/docs/tutorial.md index ce880b4..221e978 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -118,6 +118,51 @@ These operators return rvalue, unmodifiable. ## Assignment return r-value! +Naturally, assignment returns its value: + + var x + x = 11 + >>> 11 + +rvalue means you cant assign the result if the assignment + + var x + assertThrows { (x = 11) = 5 } + void + >>> void + +This also prevents chain assignments so use parentheses: + + var x + var y + x = (y = 1) + >>> 1 + +## Nullability + +When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it +one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null, +the operation won't be performed and the result will be null. Here is the difference: + + val ref = null + assertThrows { ref.field } + assertThrows { ref.method() } + assertThrows { ref.array[1] } + assertThrows { ref[1] } + assertThrows { ref() } + + assert( ref?.field == null ) + assert( ref?.method() == null ) + assert( ref?.array?[1] == null ) + assert( ref?[1] == null ) + assert( ref?() == null ) + >>> void + +There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`: + + null ?: "nothing" + >>> "nothing" + ## Math It is rather simple, like everywhere else: diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 8baa1ae..02add43 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "0.6.1-SNAPSHOT" +version = "0.6.-SNAPSHOT" buildscript { repositories { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 55aa8d6..95b7f85 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -121,7 +121,8 @@ class Compiler( operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly } } - Token.Type.DOT -> { + Token.Type.DOT, Token.Type.NULL_COALESCE -> { + var isOptional = t.type == Token.Type.NULL_COALESCE operand?.let { left -> // dotcall: calling method on the operand, if next is ID, "(" var isCall = false @@ -138,17 +139,22 @@ class Compiler( operand = Accessor { context -> context.pos = next.pos val v = left.getter(context).value - ObjRecord( - v.invokeInstanceMethod( - context, - next.value, - args.toArguments(context, false) - ), isMutable = false - ) + if (v == ObjNull && isOptional) + ObjNull.asReadonly + else + ObjRecord( + v.invokeInstanceMethod( + context, + next.value, + args.toArguments(context, false) + ), isMutable = false + ) } } - Token.Type.LBRACE -> { + + Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { + isOptional = nt.type == Token.Type.NULL_COALESCE_BLOCKINVOKE // single lambda arg, like assertTrows { ... } cc.next() isCall = true @@ -159,13 +165,16 @@ class Compiler( operand = Accessor { context -> context.pos = next.pos val v = left.getter(context).value - ObjRecord( - v.invokeInstanceMethod( - context, - next.value, - Arguments(listOf(lambda), true) - ), isMutable = false - ) + if (v == ObjNull && isOptional) + ObjNull.asReadonly + else + ObjRecord( + v.invokeInstanceMethod( + context, + next.value, + Arguments(listOf(lambda), true) + ), isMutable = false + ) } } @@ -174,25 +183,30 @@ class Compiler( } if (!isCall) { operand = Accessor({ context -> - left.getter(context).value.readField(context, next.value) + val x = left.getter(context).value + if (x == ObjNull && isOptional) ObjNull.asReadonly + else x.readField(context, next.value) }) { cc, newValue -> left.getter(cc).value.writeField(cc, next.value, newValue) } } - } ?: throw ScriptError(t.pos, "Expecting expression before dot") + } + + ?: throw ScriptError(t.pos, "Expecting expression before dot") } Token.Type.COLONCOLON -> { operand = parseScopeOperator(operand, cc) } - Token.Type.LPAREN -> { + Token.Type.LPAREN, Token.Type.NULL_COALESCE_INVOKE -> { operand?.let { left -> // this is function call from operand = parseFunctionCall( cc, left, false, + t.type == Token.Type.NULL_COALESCE_INVOKE ) } ?: run { // Expression in parentheses @@ -205,15 +219,18 @@ class Compiler( } } - Token.Type.LBRACKET -> { + Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> { operand?.let { left -> // array access + val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression") cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal") operand = Accessor({ cxt -> val i = (index.execute(cxt) as? ObjInt)?.value?.toInt() ?: cxt.raiseError("index must be integer") - left.getter(cxt).value.getAt(cxt, i).asMutable + val x = left.getter(cxt).value + if( x == ObjNull && isOptional) ObjNull.asReadonly + else x.getAt(cxt, i).asMutable }) { cxt, newValue -> val i = (index.execute(cxt) as? ObjInt)?.value?.toInt() ?: cxt.raiseError("index must be integer") @@ -337,10 +354,15 @@ class Compiler( } } - Token.Type.LBRACE -> { + Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { operand = operand?.let { left -> cc.previous() - parseFunctionCall(cc, left, blockArgument = true) + parseFunctionCall( + cc, + left, + blockArgument = true, + t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE + ) } ?: parseLambdaExpression(cc) } @@ -590,7 +612,12 @@ class Compiler( } - private fun parseFunctionCall(cc: CompilerContext, left: Accessor, blockArgument: Boolean): Accessor { + private fun parseFunctionCall( + cc: CompilerContext, + left: Accessor, + blockArgument: Boolean, + isOptional: Boolean + ): Accessor { // insofar, functions always return lvalue var detectedBlockArgument = blockArgument val args = if (blockArgument) { @@ -607,6 +634,7 @@ class Compiler( return Accessor { context -> val v = left.getter(context) + if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly v.value.callOn( context.copy( context.pos, @@ -1600,6 +1628,9 @@ class Compiler( Operator.simple(Token.Type.NOTIN, lastPrty) { c, a, b -> ObjBool(!b.contains(c, a)) }, Operator.simple(Token.Type.IS, lastPrty) { c, a, b -> ObjBool(a.isInstanceOf(b)) }, Operator.simple(Token.Type.NOTIS, lastPrty) { c, a, b -> ObjBool(!a.isInstanceOf(b)) }, + + Operator.simple(Token.Type.ELVIS, ++lastPrty) { c, a, b -> if( a == ObjNull) b else a }, + // shuttle <=> 6 Operator.simple(Token.Type.SHUTTLE, ++lastPrty) { c, a, b -> ObjInt(a.compareTo(c, b).toLong()) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt index 4cac52f..ff5ea25 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Context.kt @@ -16,7 +16,7 @@ class Context( fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented") @Suppress("unused") - fun raiseNPE(): Nothing = raiseError(ObjNullPointerException(this)) + fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this)) @Suppress("unused") fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index c2fc4c9..16e06f1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -289,6 +289,26 @@ object ObjNull : Obj() { return other is ObjNull || other == null } + override suspend fun readField(context: Context, name: String): ObjRecord { + context.raiseNPE() + } + + override suspend fun invokeInstanceMethod(context: Context, name: String, args: Arguments): Obj { + context.raiseNPE() + } + + override suspend fun getAt(context: Context, index: Int): Obj { + context.raiseNPE() + } + + override suspend fun putAt(context: Context, index: Int, newValue: Obj) { + context.raiseNPE() + } + + override suspend fun callOn(context: Context): Obj { + context.raiseNPE() + } + override fun toString(): String = "null" } @@ -383,7 +403,7 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va context.addConst("Exception", Root) existingErrorClasses["Exception"] = Root for (name in listOf( - "NullPointerException", + "NullReferenceException", "AssertionFailedException", "ClassCastException", "IndexOutOfBoundsException", @@ -400,7 +420,7 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va } } -class ObjNullPointerException(context: Context) : ObjException("NullPointerException", context, "object is null") +class ObjNullReferenceException(context: Context) : ObjException("NullReferenceException", context, "object is null") class ObjAssertionFailedException(context: Context, message: String) : ObjException("AssertionFailedException", context, message) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index 877161a..423a9ff 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -267,6 +267,21 @@ private class Parser(fromPos: Pos) { Token(value.toString(), start, Token.Type.CHAR) } + '?' -> { + when(currentChar.also { pos.advance() }) { + ':' -> Token("??", from, Token.Type.ELVIS) + '?' -> Token("??", from, Token.Type.ELVIS) + '.' -> Token("?.", from, Token.Type.NULL_COALESCE) + '[' -> Token("?(", from, Token.Type.NULL_COALESCE_INDEX) + '(' -> Token("?(", from, Token.Type.NULL_COALESCE_INVOKE) + '{' -> Token("?{", from, Token.Type.NULL_COALESCE_BLOCKINVOKE) + else -> { + pos.back() + Token("?", from, Token.Type.QUESTION) + } + } + } + else -> { // text infix operators: // Labels processing is complicated! diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt index df82c10..aceaee4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt @@ -21,6 +21,11 @@ data class Token(val value: String, val pos: Pos, val type: Type) { ELLIPSIS, DOTDOT, DOTDOTLT, NEWLINE, EOF, + NULL_COALESCE, + ELVIS, + NULL_COALESCE_INDEX, + NULL_COALESCE_INVOKE, + NULL_COALESCE_BLOCKINVOKE, } companion object { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 23e9eed..e44f62a 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -2113,4 +2113,24 @@ class ScriptTest { """.trimIndent() ) } + + @Test + fun testNull1() = runTest { + eval(""" + var s = null + assertThrows { s.length } + assertThrows { s.size() } + + assertEquals( null, s?.size() ) + assertEquals( null, s?.length ) + assertEquals( null, s?.length ?{ "test" } ) + assertEquals( null, s?[1] ) + assertEquals( null, s ?{ "test" } ) + assertEquals( null, s.test ?{ "test" } ) + + s = "xx" + assert(s.lower().size == 2) + assert(s.length == 2) + """.trimIndent()) + } } \ No newline at end of file