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 |
|
| class | notes |
|
||||||
|----------------------------|-------------------------------------------------------|
|
|----------------------------|-------------------------------------------------------|
|
||||||
| Exception | root of al throwable objects |
|
| Exception | root of al throwable objects |
|
||||||
| NullPointerException | |
|
| NullReferenceException | |
|
||||||
| AssertionFailedException | |
|
| AssertionFailedException | |
|
||||||
| ClassCastException | |
|
| ClassCastException | |
|
||||||
| IndexOutOfBoundsException | |
|
| IndexOutOfBoundsException | |
|
||||||
|
@ -118,6 +118,51 @@ These operators return rvalue, unmodifiable.
|
|||||||
|
|
||||||
## Assignment return r-value!
|
## 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
|
## Math
|
||||||
|
|
||||||
It is rather simple, like everywhere else:
|
It is rather simple, like everywhere else:
|
||||||
|
@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.6.1-SNAPSHOT"
|
version = "0.6.-SNAPSHOT"
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -121,7 +121,8 @@ class Compiler(
|
|||||||
operand = Accessor { op.getter(it).value.logicalNot(it).asReadonly }
|
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 ->
|
operand?.let { left ->
|
||||||
// dotcall: calling method on the operand, if next is ID, "("
|
// dotcall: calling method on the operand, if next is ID, "("
|
||||||
var isCall = false
|
var isCall = false
|
||||||
@ -138,6 +139,9 @@ class Compiler(
|
|||||||
operand = Accessor { context ->
|
operand = Accessor { context ->
|
||||||
context.pos = next.pos
|
context.pos = next.pos
|
||||||
val v = left.getter(context).value
|
val v = left.getter(context).value
|
||||||
|
if (v == ObjNull && isOptional)
|
||||||
|
ObjNull.asReadonly
|
||||||
|
else
|
||||||
ObjRecord(
|
ObjRecord(
|
||||||
v.invokeInstanceMethod(
|
v.invokeInstanceMethod(
|
||||||
context,
|
context,
|
||||||
@ -148,7 +152,9 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.LBRACE -> {
|
|
||||||
|
Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> {
|
||||||
|
isOptional = nt.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
|
||||||
// single lambda arg, like assertTrows { ... }
|
// single lambda arg, like assertTrows { ... }
|
||||||
cc.next()
|
cc.next()
|
||||||
isCall = true
|
isCall = true
|
||||||
@ -159,6 +165,9 @@ class Compiler(
|
|||||||
operand = Accessor { context ->
|
operand = Accessor { context ->
|
||||||
context.pos = next.pos
|
context.pos = next.pos
|
||||||
val v = left.getter(context).value
|
val v = left.getter(context).value
|
||||||
|
if (v == ObjNull && isOptional)
|
||||||
|
ObjNull.asReadonly
|
||||||
|
else
|
||||||
ObjRecord(
|
ObjRecord(
|
||||||
v.invokeInstanceMethod(
|
v.invokeInstanceMethod(
|
||||||
context,
|
context,
|
||||||
@ -174,25 +183,30 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
if (!isCall) {
|
if (!isCall) {
|
||||||
operand = Accessor({ context ->
|
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 ->
|
}) { cc, newValue ->
|
||||||
left.getter(cc).value.writeField(cc, next.value, 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 -> {
|
Token.Type.COLONCOLON -> {
|
||||||
operand = parseScopeOperator(operand, cc)
|
operand = parseScopeOperator(operand, cc)
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.LPAREN -> {
|
Token.Type.LPAREN, Token.Type.NULL_COALESCE_INVOKE -> {
|
||||||
operand?.let { left ->
|
operand?.let { left ->
|
||||||
// this is function call from <left>
|
// this is function call from <left>
|
||||||
operand = parseFunctionCall(
|
operand = parseFunctionCall(
|
||||||
cc,
|
cc,
|
||||||
left,
|
left,
|
||||||
false,
|
false,
|
||||||
|
t.type == Token.Type.NULL_COALESCE_INVOKE
|
||||||
)
|
)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// Expression in parentheses
|
// Expression in parentheses
|
||||||
@ -205,15 +219,18 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.LBRACKET -> {
|
Token.Type.LBRACKET, Token.Type.NULL_COALESCE_INDEX -> {
|
||||||
operand?.let { left ->
|
operand?.let { left ->
|
||||||
// array access
|
// array access
|
||||||
|
val isOptional = t.type == Token.Type.NULL_COALESCE_INDEX
|
||||||
val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression")
|
val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression")
|
||||||
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
||||||
operand = Accessor({ cxt ->
|
operand = Accessor({ cxt ->
|
||||||
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
|
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
|
||||||
?: cxt.raiseError("index must be integer")
|
?: 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 ->
|
}) { cxt, newValue ->
|
||||||
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
|
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
|
||||||
?: cxt.raiseError("index must be integer")
|
?: 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 ->
|
operand = operand?.let { left ->
|
||||||
cc.previous()
|
cc.previous()
|
||||||
parseFunctionCall(cc, left, blockArgument = true)
|
parseFunctionCall(
|
||||||
|
cc,
|
||||||
|
left,
|
||||||
|
blockArgument = true,
|
||||||
|
t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
|
||||||
|
)
|
||||||
} ?: parseLambdaExpression(cc)
|
} ?: 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
|
// insofar, functions always return lvalue
|
||||||
var detectedBlockArgument = blockArgument
|
var detectedBlockArgument = blockArgument
|
||||||
val args = if (blockArgument) {
|
val args = if (blockArgument) {
|
||||||
@ -607,6 +634,7 @@ class Compiler(
|
|||||||
|
|
||||||
return Accessor { context ->
|
return Accessor { context ->
|
||||||
val v = left.getter(context)
|
val v = left.getter(context)
|
||||||
|
if (v.value == ObjNull && isOptional) return@Accessor v.value.asReadonly
|
||||||
v.value.callOn(
|
v.value.callOn(
|
||||||
context.copy(
|
context.copy(
|
||||||
context.pos,
|
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.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.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.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
|
// shuttle <=> 6
|
||||||
Operator.simple(Token.Type.SHUTTLE, ++lastPrty) { c, a, b ->
|
Operator.simple(Token.Type.SHUTTLE, ++lastPrty) { c, a, b ->
|
||||||
ObjInt(a.compareTo(c, b).toLong())
|
ObjInt(a.compareTo(c, b).toLong())
|
||||||
|
@ -16,7 +16,7 @@ class Context(
|
|||||||
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
|
fun raiseNotImplemented(what: String = "operation"): Nothing = raiseError("$what is not implemented")
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun raiseNPE(): Nothing = raiseError(ObjNullPointerException(this))
|
fun raiseNPE(): Nothing = raiseError(ObjNullReferenceException(this))
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
|
fun raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
|
||||||
|
@ -289,6 +289,26 @@ object ObjNull : Obj() {
|
|||||||
return other is ObjNull || other == null
|
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"
|
override fun toString(): String = "null"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,7 +403,7 @@ open class ObjException(exceptionClass: ExceptionClass, val context: Context, va
|
|||||||
context.addConst("Exception", Root)
|
context.addConst("Exception", Root)
|
||||||
existingErrorClasses["Exception"] = Root
|
existingErrorClasses["Exception"] = Root
|
||||||
for (name in listOf(
|
for (name in listOf(
|
||||||
"NullPointerException",
|
"NullReferenceException",
|
||||||
"AssertionFailedException",
|
"AssertionFailedException",
|
||||||
"ClassCastException",
|
"ClassCastException",
|
||||||
"IndexOutOfBoundsException",
|
"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) :
|
class ObjAssertionFailedException(context: Context, message: String) :
|
||||||
ObjException("AssertionFailedException", context, message)
|
ObjException("AssertionFailedException", context, message)
|
||||||
|
@ -267,6 +267,21 @@ private class Parser(fromPos: Pos) {
|
|||||||
Token(value.toString(), start, Token.Type.CHAR)
|
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 -> {
|
else -> {
|
||||||
// text infix operators:
|
// text infix operators:
|
||||||
// Labels processing is complicated!
|
// Labels processing is complicated!
|
||||||
|
@ -21,6 +21,11 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
ELLIPSIS, DOTDOT, DOTDOTLT,
|
ELLIPSIS, DOTDOT, DOTDOTLT,
|
||||||
NEWLINE,
|
NEWLINE,
|
||||||
EOF,
|
EOF,
|
||||||
|
NULL_COALESCE,
|
||||||
|
ELVIS,
|
||||||
|
NULL_COALESCE_INDEX,
|
||||||
|
NULL_COALESCE_INVOKE,
|
||||||
|
NULL_COALESCE_BLOCKINVOKE,
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -2113,4 +2113,24 @@ class ScriptTest {
|
|||||||
""".trimIndent()
|
""".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