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