null assignment operator added
This commit is contained in:
parent
8cd980514b
commit
555c9b94de
5
.gitignore
vendored
5
.gitignore
vendored
@ -21,3 +21,8 @@ xcuserdata
|
||||
debug.log
|
||||
/build.log
|
||||
/test.md
|
||||
/build_output.txt
|
||||
/build_output_full.txt
|
||||
/check_output.txt
|
||||
/compile_jvm_output.txt
|
||||
/compile_metadata_output.txt
|
||||
|
||||
@ -195,6 +195,7 @@ Ready features:
|
||||
- [x] object expressions `object: List { ... }`
|
||||
- [x] late-init vals in classes
|
||||
- [x] properties with getters and setters
|
||||
- [x] assign-if-null operator `?=`
|
||||
|
||||
All of this is documented in the [language site](https://lynglang.com) and locally [docs/language.md](docs/tutorial.md). the current nightly builds published on the site and in the private maven repository.
|
||||
|
||||
|
||||
@ -188,14 +188,13 @@ Note that assignment operator returns rvalue, it can't be assigned.
|
||||
## Modifying arithmetics
|
||||
|
||||
There is a set of assigning operations: `+=`, `-=`, `*=`, `/=` and even `%=`.
|
||||
There is also a special null-aware assignment operator `?=`: it performs the assignment only if the lvalue is `null`.
|
||||
|
||||
var x = 5
|
||||
assert( 25 == (x*=5) )
|
||||
assert( 25 == x)
|
||||
assert( 24 == (x-=1) )
|
||||
assert( 12 == (x/=2) )
|
||||
x
|
||||
>>> 12
|
||||
var x = null
|
||||
x ?= 10
|
||||
assertEquals(10, x)
|
||||
x ?= 20
|
||||
assertEquals(10, x)
|
||||
|
||||
Notice the parentheses here: the assignment has low priority!
|
||||
|
||||
@ -248,6 +247,13 @@ There is also "elvis operator", null-coalesce infix operator '?:' that returns r
|
||||
null ?: "nothing"
|
||||
>>> "nothing"
|
||||
|
||||
There is also a null-aware assignment operator `?=`, which assigns a value only if the target is `null`:
|
||||
|
||||
var config = null
|
||||
config ?= { port: 8080 }
|
||||
config ?= { port: 9000 } // no-op, config is already not null
|
||||
assertEquals(8080, config.port)
|
||||
|
||||
## Utility functions
|
||||
|
||||
The following functions simplify nullable values processing and
|
||||
|
||||
@ -139,6 +139,21 @@ var name by Observable("initial") { n, old, new ->
|
||||
|
||||
The system features a unified interface (`getValue`, `setValue`, `invoke`) and a `bind` hook for initialization-time validation and configuration. See the [Delegation Guide](delegation.md) for more.
|
||||
|
||||
### Assign-if-null Operator (`?=`)
|
||||
The new `?=` operator provides a concise way to assign a value only if the target is `null`. It is especially useful for setting default values or lazy initialization.
|
||||
|
||||
```lyng
|
||||
var x = null
|
||||
x ?= 42 // x is now 42
|
||||
x ?= 100 // x remains 42 (not null)
|
||||
|
||||
// Works with properties and index access
|
||||
config.port ?= 8080
|
||||
settings["theme"] ?= "dark"
|
||||
```
|
||||
|
||||
The operator returns the final value of the receiver (the original value if it was not `null`, or the new value if the assignment occurred).
|
||||
|
||||
## Tooling and Infrastructure
|
||||
|
||||
### CLI: Formatting Command
|
||||
|
||||
@ -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,
|
||||
|
||||
101
lynglib/src/commonTest/kotlin/IfNullAssignTest.kt
Normal file
101
lynglib/src/commonTest/kotlin/IfNullAssignTest.kt
Normal file
@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user