added T is nullable support
This commit is contained in:
parent
c54d947f1c
commit
37d093817e
@ -109,6 +109,24 @@ Examples (T = A | B):
|
|||||||
B in T // true
|
B in T // true
|
||||||
T is A | B // true
|
T is A | B // true
|
||||||
|
|
||||||
|
# Nullability checks for types
|
||||||
|
|
||||||
|
Use `is nullable` to check whether a type expression accepts `null`:
|
||||||
|
|
||||||
|
T is nullable
|
||||||
|
T !is nullable
|
||||||
|
|
||||||
|
This works with concrete and generic types:
|
||||||
|
|
||||||
|
fun describe<T>(x: T): String = when (T) {
|
||||||
|
nullable -> "nullable"
|
||||||
|
else -> "non-null"
|
||||||
|
}
|
||||||
|
|
||||||
|
Equivalent legacy form:
|
||||||
|
|
||||||
|
null is T
|
||||||
|
|
||||||
# Practical examples
|
# Practical examples
|
||||||
|
|
||||||
fun acceptInts<T: Int>(xs: List<T>) { }
|
fun acceptInts<T: Int>(xs: List<T>) { }
|
||||||
|
|||||||
10
docs/samples/type_nullability_checks.lyng
Normal file
10
docs/samples/type_nullability_checks.lyng
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
fun describe<T>(x: T): String = when (T) {
|
||||||
|
nullable -> "nullable"
|
||||||
|
else -> "non-null"
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaybeInt = Int?
|
||||||
|
assert(MaybeInt is nullable)
|
||||||
|
assert(!(Int is nullable))
|
||||||
|
assertEquals("nullable", describe<Int?>(null))
|
||||||
|
assertEquals("non-null", describe<Int>(1))
|
||||||
@ -502,6 +502,18 @@ Aliases expand to their underlying type expressions. See `docs/generics.md` for
|
|||||||
|
|
||||||
`Null` is the class of `null`. It is a singleton type and mostly useful for type inference results.
|
`Null` is the class of `null`. It is a singleton type and mostly useful for type inference results.
|
||||||
|
|
||||||
|
For type expressions, you can check nullability directly:
|
||||||
|
|
||||||
|
T is nullable
|
||||||
|
T !is nullable
|
||||||
|
|
||||||
|
This is especially useful in generic code and in `when` over a type parameter:
|
||||||
|
|
||||||
|
fun describe<T>(x: T): String = when (T) {
|
||||||
|
nullable -> "nullable"
|
||||||
|
else -> "non-null"
|
||||||
|
}
|
||||||
|
|
||||||
## Type inference
|
## Type inference
|
||||||
|
|
||||||
The compiler infers types from:
|
The compiler infers types from:
|
||||||
|
|||||||
@ -2247,6 +2247,7 @@ class Compiler(
|
|||||||
is WhenEqualsCondition -> containsDelegatedRefs(cond.expr)
|
is WhenEqualsCondition -> containsDelegatedRefs(cond.expr)
|
||||||
is WhenInCondition -> containsDelegatedRefs(cond.expr)
|
is WhenInCondition -> containsDelegatedRefs(cond.expr)
|
||||||
is WhenIsCondition -> false
|
is WhenIsCondition -> false
|
||||||
|
is WhenNullableCondition -> false
|
||||||
}
|
}
|
||||||
} || containsDelegatedRefs(case.block)
|
} || containsDelegatedRefs(case.block)
|
||||||
} ||
|
} ||
|
||||||
@ -2520,12 +2521,22 @@ class Compiler(
|
|||||||
CastRef(lvalue!!, typeRef, true, opToken.pos)
|
CastRef(lvalue!!, typeRef, true, opToken.pos)
|
||||||
}
|
}
|
||||||
} else if (opToken.type == Token.Type.IS || opToken.type == Token.Type.NOTIS) {
|
} else if (opToken.type == Token.Type.IS || opToken.type == Token.Type.NOTIS) {
|
||||||
val (typeDecl, _) = parseTypeExpressionWithMini()
|
val nullablePredicate = tryConsumeNullablePredicate()
|
||||||
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(typeDecl, opToken.pos)
|
if (nullablePredicate) {
|
||||||
if (opToken.type == Token.Type.IS) {
|
val nullRef = ConstRef(ObjNull.asReadonly)
|
||||||
BinaryOpRef(BinOp.IS, lvalue!!, typeRef)
|
if (opToken.type == Token.Type.IS) {
|
||||||
|
BinaryOpRef(BinOp.IS, nullRef, lvalue!!)
|
||||||
|
} else {
|
||||||
|
BinaryOpRef(BinOp.NOTIS, nullRef, lvalue!!)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
BinaryOpRef(BinOp.NOTIS, lvalue!!, typeRef)
|
val (typeDecl, _) = parseTypeExpressionWithMini()
|
||||||
|
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(typeDecl, opToken.pos)
|
||||||
|
if (opToken.type == Token.Type.IS) {
|
||||||
|
BinaryOpRef(BinOp.IS, lvalue!!, typeRef)
|
||||||
|
} else {
|
||||||
|
BinaryOpRef(BinOp.NOTIS, lvalue!!, typeRef)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val rvalue = parseExpressionLevel(level + 1)
|
val rvalue = parseExpressionLevel(level + 1)
|
||||||
@ -2557,6 +2568,17 @@ class Compiler(
|
|||||||
return lvalue
|
return lvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tryConsumeNullablePredicate(): Boolean {
|
||||||
|
val save = cc.savePos()
|
||||||
|
val t = cc.nextNonWhitespace()
|
||||||
|
return if (t.type == Token.Type.ID && t.value == "nullable") {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
cc.restorePos(save)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun parseTerm(): ObjRef? {
|
private suspend fun parseTerm(): ObjRef? {
|
||||||
var operand: ObjRef? = null
|
var operand: ObjRef? = null
|
||||||
var pendingCallTypeArgs: List<TypeDecl>? = null
|
var pendingCallTypeArgs: List<TypeDecl>? = null
|
||||||
@ -6252,10 +6274,14 @@ class Compiler(
|
|||||||
Token.Type.IS,
|
Token.Type.IS,
|
||||||
Token.Type.NOTIS -> {
|
Token.Type.NOTIS -> {
|
||||||
val negated = t.type == Token.Type.NOTIS
|
val negated = t.type == Token.Type.NOTIS
|
||||||
val (typeDecl, _) = parseTypeExpressionWithMini()
|
if (tryConsumeNullablePredicate()) {
|
||||||
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(typeDecl, t.pos)
|
currentConditions += WhenNullableCondition(negated, t.pos)
|
||||||
val caseType = ExpressionStatement(typeRef, t.pos)
|
} else {
|
||||||
currentConditions += WhenIsCondition(caseType, negated, t.pos)
|
val (typeDecl, _) = parseTypeExpressionWithMini()
|
||||||
|
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(typeDecl, t.pos)
|
||||||
|
val caseType = ExpressionStatement(typeRef, t.pos)
|
||||||
|
currentConditions += WhenIsCondition(caseType, negated, t.pos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.COMMA ->
|
Token.Type.COMMA ->
|
||||||
@ -6268,7 +6294,9 @@ class Compiler(
|
|||||||
break@outer
|
break@outer
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (t.value == "else") {
|
if (t.type == Token.Type.ID && t.value == "nullable") {
|
||||||
|
currentConditions += WhenNullableCondition(negated = false, t.pos)
|
||||||
|
} else if (t.value == "else") {
|
||||||
cc.skipTokens(Token.Type.ARROW)
|
cc.skipTokens(Token.Type.ARROW)
|
||||||
if (elseCase != null) throw ScriptError(
|
if (elseCase != null) throw ScriptError(
|
||||||
cc.currentPos(),
|
cc.currentPos(),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2026 Sergey S. Chernov
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -12,6 +12,7 @@
|
|||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
@ -55,6 +56,15 @@ class WhenIsCondition(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WhenNullableCondition(
|
||||||
|
val negated: Boolean,
|
||||||
|
override val pos: Pos,
|
||||||
|
) : WhenCondition(ExpressionStatement(net.sergeych.lyng.obj.ConstRef(net.sergeych.lyng.obj.ObjVoid.asReadonly), pos), pos) {
|
||||||
|
override suspend fun matches(scope: Scope, value: Obj): Boolean {
|
||||||
|
return bytecodeOnly(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class WhenCase(val conditions: List<WhenCondition>, val block: Statement)
|
data class WhenCase(val conditions: List<WhenCondition>, val block: Statement)
|
||||||
|
|
||||||
class WhenStatement(
|
class WhenStatement(
|
||||||
|
|||||||
@ -4345,6 +4345,22 @@ class BytecodeCompiler(
|
|||||||
CompiledValue(neg, SlotType.BOOL)
|
CompiledValue(neg, SlotType.BOOL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is WhenNullableCondition -> {
|
||||||
|
val nullSlot = allocSlot()
|
||||||
|
builder.emit(Opcode.CONST_NULL, nullSlot)
|
||||||
|
updateSlotType(nullSlot, SlotType.OBJ)
|
||||||
|
val baseDst = allocSlot()
|
||||||
|
builder.emit(Opcode.CHECK_IS, nullSlot, subject.slot, baseDst)
|
||||||
|
updateSlotType(baseDst, SlotType.BOOL)
|
||||||
|
if (!cond.negated) {
|
||||||
|
CompiledValue(baseDst, SlotType.BOOL)
|
||||||
|
} else {
|
||||||
|
val neg = allocSlot()
|
||||||
|
builder.emit(Opcode.NOT_BOOL, baseDst, neg)
|
||||||
|
updateSlotType(neg, SlotType.BOOL)
|
||||||
|
CompiledValue(neg, SlotType.BOOL)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -443,6 +443,7 @@ class BytecodeStatement private constructor(
|
|||||||
is WhenEqualsCondition -> WhenEqualsCondition(unwrapDeep(cond.expr), cond.pos)
|
is WhenEqualsCondition -> WhenEqualsCondition(unwrapDeep(cond.expr), cond.pos)
|
||||||
is WhenInCondition -> WhenInCondition(unwrapDeep(cond.expr), cond.negated, cond.pos)
|
is WhenInCondition -> WhenInCondition(unwrapDeep(cond.expr), cond.negated, cond.pos)
|
||||||
is WhenIsCondition -> WhenIsCondition(unwrapDeep(cond.expr), cond.negated, cond.pos)
|
is WhenIsCondition -> WhenIsCondition(unwrapDeep(cond.expr), cond.negated, cond.pos)
|
||||||
|
is WhenNullableCondition -> WhenNullableCondition(cond.negated, cond.pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -535,6 +535,49 @@ class TypesTest {
|
|||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGenericNullableTypePredicate() = runTest {
|
||||||
|
eval("""
|
||||||
|
fun isTypeNullable<T>(x: T): Bool = T is nullable
|
||||||
|
type MaybeInt = Int?
|
||||||
|
assert(isTypeNullable<Int?>(null))
|
||||||
|
assert(!isTypeNullable<Int>(1))
|
||||||
|
assert(MaybeInt is nullable)
|
||||||
|
assert(!(Int is nullable))
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWhenNullableTypeCase() = runTest {
|
||||||
|
eval("""
|
||||||
|
fun describe<T>(x: T): String = when(T) {
|
||||||
|
nullable -> "nullable"
|
||||||
|
else -> "non-null"
|
||||||
|
}
|
||||||
|
fun describeIs<T>(x: T): String = when(T) {
|
||||||
|
is nullable -> "nullable"
|
||||||
|
else -> "non-null"
|
||||||
|
}
|
||||||
|
assertEquals("nullable", describe<Int?>(null))
|
||||||
|
assertEquals("non-null", describe<Int>(1))
|
||||||
|
assertEquals("nullable", describeIs<Int?>(null))
|
||||||
|
assertEquals("non-null", describeIs<Int>(1))
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// fun testNullableGenericTypes() = runTest {
|
||||||
|
// eval("""
|
||||||
|
// fun t<T>(): String =
|
||||||
|
// when(T) {
|
||||||
|
// is Object -> "%s is Object"(T::class.name)
|
||||||
|
// else -> throw "It should not happen"
|
||||||
|
// }
|
||||||
|
// assert( Int is Object)
|
||||||
|
// assertEquals( t<Int>(), "Class is Object")
|
||||||
|
// """.trimIndent())
|
||||||
|
// }
|
||||||
|
|
||||||
@Test fun testIndexer() = runTest {
|
@Test fun testIndexer() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
class Greeter {
|
class Greeter {
|
||||||
|
|||||||
@ -201,6 +201,28 @@ class WebsiteSamplesTest {
|
|||||||
assertEquals(12L, (result as ObjInt).value)
|
assertEquals(12L, (result as ObjInt).value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNullableTypePredicate() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
val result = scope.eval(
|
||||||
|
"""
|
||||||
|
// Type nullability checks
|
||||||
|
fun describe<T>(x: T): String = when(T) {
|
||||||
|
nullable -> "nullable"
|
||||||
|
else -> "non-null"
|
||||||
|
}
|
||||||
|
type MaybeInt = Int?
|
||||||
|
[describe<Int?>(null), describe<Int>(1), Int is nullable, MaybeInt is nullable]
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
assertTrue(result is ObjList)
|
||||||
|
val list = result.list
|
||||||
|
assertEquals("nullable", (list[0] as ObjString).value)
|
||||||
|
assertEquals("non-null", (list[1] as ObjString).value)
|
||||||
|
assertEquals(false, (list[2] as ObjBool).value)
|
||||||
|
assertEquals(true, (list[3] as ObjBool).value)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEasyOperatorOverloading() = runTest {
|
fun testEasyOperatorOverloading() = runTest {
|
||||||
val scope = Script.newScope()
|
val scope = Script.newScope()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user