fix #19 set of null-coalesce operators

This commit is contained in:
sergeych committed 2025-06-13 22:25:18 +04:00
1 parent bd2b6bf06e
commit b961296425
9 files changed
+165 -29

No files matched your search

@@ -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())
}
}