null assignment operator added

This commit is contained in:
sergeych committed 2026-01-06 11:39:23 +01:00
1 parent 8cd980514b
commit 555c9b94de
11 files changed
+169 -9

No files matched your search

@@ -175,6 +175,7 @@ class Compiler(
// A standalone newline not immediately following a comment resets doc buffer
if (!prevWasComment) clearPendingDoc() else prevWasComment = false
}
else -> {}
}
cc.next()
continue
@@ -3602,6 +3603,9 @@ class Compiler(
Operator(Token.Type.PERCENTASSIGN, lastPriority) { pos, a, b ->
AssignOpRef(BinOp.PERCENT, a, b, pos)
},
Operator(Token.Type.IFNULLASSIGN, lastPriority) { pos, a, b ->
AssignIfNullRef(a, b, pos)
},
// logical 1
Operator(Token.Type.OR, ++lastPriority) { _, a, b ->
foldBinary(BinOp.OR, a, b)?.let { return@Operator ConstRef(it.asReadonly) }
@@ -325,6 +325,7 @@ private class Parser(fromPos: Pos) {
'?' -> {
when (currentChar) {
'=' -> { pos.advance(); Token("?=", from, Token.Type.IFNULLASSIGN) }
':' -> { pos.advance(); Token("?:", from, Token.Type.ELVIS) }
'?' -> { pos.advance(); Token("??", from, Token.Type.ELVIS) }
'.' -> { pos.advance(); Token("?.", from, Token.Type.NULL_COALESCE) }
@@ -34,7 +34,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA,
SEMICOLON, COLON,
PLUS, MINUS, STAR, SLASH, PERCENT,
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN,
ASSIGN, PLUSASSIGN, MINUSASSIGN, STARASSIGN, SLASHASSIGN, PERCENTASSIGN, IFNULLASSIGN,
PLUS2, MINUS2,
IN, NOTIN, IS, NOTIS, BY,
EQ, NEQ, LT, LTE, GT, GTE, REF_EQ, REF_NEQ, MATCH, NOTMATCH,
@@ -82,7 +82,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
// operators and symbolic constructs
Type.PLUS, Type.MINUS, Type.STAR, Type.SLASH, Type.PERCENT,
Type.ASSIGN, Type.PLUSASSIGN, Type.MINUSASSIGN, Type.STARASSIGN, Type.SLASHASSIGN, Type.PERCENTASSIGN,
Type.ASSIGN, Type.PLUSASSIGN, Type.MINUSASSIGN, Type.STARASSIGN, Type.SLASHASSIGN, Type.PERCENTASSIGN, Type.IFNULLASSIGN,
Type.PLUS2, Type.MINUS2,
Type.EQ, Type.NEQ, Type.LT, Type.LTE, Type.GT, Type.GTE, Type.REF_EQ, Type.REF_NEQ, Type.MATCH, Type.NOTMATCH,
Type.DOT, Type.ARROW, Type.EQARROW, Type.QUESTION, Type.COLONCOLON,
@@ -1805,6 +1805,21 @@ class RangeRef(
}
}
/** Assignment if null op: target ?= value */
class AssignIfNullRef(
private val target: ObjRef,
private val value: ObjRef,
private val atPos: Pos,
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val current = target.evalValue(scope)
if (current != ObjNull) return current.asReadonly
val newValue = value.evalValue(scope)
target.setAt(atPos, scope, newValue)
return newValue.asReadonly
}
}
/** Simple assignment: target = value */
class AssignRef(
private val target: ObjRef,
@@ -0,0 +1,101 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class IfNullAssignTest {
@Test
fun testBasicAssignment() = runTest {
eval("""
var x = null
x ?= 42
assertEquals(42, x)
x ?= 100
assertEquals(42, x)
""".trimIndent())
}
@Test
fun testPropertyAssignment() = runTest {
eval("""
class Box(var value)
val b = Box(null)
b.value ?= 10
assertEquals(10, b.value)
b.value ?= 20
assertEquals(10, b.value)
""".trimIndent())
}
@Test
fun testIndexAssignment() = runTest {
eval("""
val a = [null, 1]
a[0] ?= 10
assertEquals(10, a[0])
a[1] ?= 20
assertEquals(1, a[1])
""".trimIndent())
}
@Test
fun testOptionalChaining() = runTest {
eval("""
class Inner(var value)
class Outer(var inner)
var o = null
o?.inner?.value ?= 10 // should do nothing
assertEquals(null, o)
o = Outer(null)
o?.inner?.value ?= 10 // should do nothing because inner is null
assertEquals(null, o.inner)
o.inner = Inner(null)
o?.inner?.value ?= 42
assertEquals(42, o.inner.value)
o?.inner?.value ?= 100
assertEquals(42, o.inner.value)
""".trimIndent())
}
@Test
fun testDoubleEvaluation() = runTest {
eval("""
var count = 0
fun getIdx() {
count = count + 1
return 0
}
val a = [null]
a[getIdx()] ?= 10
// Current behavior: double evaluation happens in ObjRef for compound ops
// getIdx() is called once for checking if null, and once for setting if it was null.
assertEquals(10, a[0])
assertEquals(2, count)
a[getIdx()] ?= 20
// If it's NOT null, it only evaluates once to check the value.
assertEquals(10, a[0])
assertEquals(3, count)
""".trimIndent())
}
@Test
fun testReturnValue() = runTest {
eval("""
var x = null
val r1 = (x ?= 42)
assertEquals(42, r1)
assertEquals(42, x)
val r2 = (x ?= 100)
assertEquals(42, r2)
assertEquals(42, x)
""".trimIndent())
}
}
@@ -4560,4 +4560,16 @@ class ScriptTest {
""".trimIndent())
}
@Test
fun testOptOnNullAssignment() = runTest {
eval("""
var x = null
assertEquals(null, x)
x ?= 1
assertEquals(1, x)
x ?= 2
assertEquals(1, x)
""".trimIndent())
}
}