fix #19 set of null-coalesce operators
This commit is contained in:
		
							parent
							
								
									bd2b6bf06e
								
							
						
					
					
						commit
						b961296425
					
				@ -131,7 +131,7 @@ _this functionality is not yet released_
 | 
			
		||||
| class                      | notes                                                 |
 | 
			
		||||
|----------------------------|-------------------------------------------------------|
 | 
			
		||||
| Exception                  | root of al throwable objects                          |
 | 
			
		||||
| NullPointerException       |                                                       |
 | 
			
		||||
| NullReferenceException       |                                                       |
 | 
			
		||||
| AssertionFailedException   |                                                       | 
 | 
			
		||||
| ClassCastException         |                                                       |
 | 
			
		||||
| IndexOutOfBoundsException  |                                                       |
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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 <left>
 | 
			
		||||
                        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())
 | 
			
		||||
 | 
			
		||||
@ -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 =
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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!
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user