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
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
The compiler infers types from:
|
||||
|
||||
@ -2247,6 +2247,7 @@ class Compiler(
|
||||
is WhenEqualsCondition -> containsDelegatedRefs(cond.expr)
|
||||
is WhenInCondition -> containsDelegatedRefs(cond.expr)
|
||||
is WhenIsCondition -> false
|
||||
is WhenNullableCondition -> false
|
||||
}
|
||||
} || containsDelegatedRefs(case.block)
|
||||
} ||
|
||||
@ -2520,12 +2521,22 @@ class Compiler(
|
||||
CastRef(lvalue!!, typeRef, true, opToken.pos)
|
||||
}
|
||||
} else if (opToken.type == Token.Type.IS || opToken.type == Token.Type.NOTIS) {
|
||||
val (typeDecl, _) = parseTypeExpressionWithMini()
|
||||
val typeRef = net.sergeych.lyng.obj.TypeDeclRef(typeDecl, opToken.pos)
|
||||
if (opToken.type == Token.Type.IS) {
|
||||
BinaryOpRef(BinOp.IS, lvalue!!, typeRef)
|
||||
val nullablePredicate = tryConsumeNullablePredicate()
|
||||
if (nullablePredicate) {
|
||||
val nullRef = ConstRef(ObjNull.asReadonly)
|
||||
if (opToken.type == Token.Type.IS) {
|
||||
BinaryOpRef(BinOp.IS, nullRef, lvalue!!)
|
||||
} else {
|
||||
BinaryOpRef(BinOp.NOTIS, nullRef, lvalue!!)
|
||||
}
|
||||
} 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 {
|
||||
val rvalue = parseExpressionLevel(level + 1)
|
||||
@ -2557,6 +2568,17 @@ class Compiler(
|
||||
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? {
|
||||
var operand: ObjRef? = null
|
||||
var pendingCallTypeArgs: List<TypeDecl>? = null
|
||||
@ -6252,10 +6274,14 @@ class Compiler(
|
||||
Token.Type.IS,
|
||||
Token.Type.NOTIS -> {
|
||||
val negated = t.type == Token.Type.NOTIS
|
||||
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)
|
||||
if (tryConsumeNullablePredicate()) {
|
||||
currentConditions += WhenNullableCondition(negated, t.pos)
|
||||
} else {
|
||||
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 ->
|
||||
@ -6268,7 +6294,9 @@ class Compiler(
|
||||
break@outer
|
||||
|
||||
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)
|
||||
if (elseCase != null) throw ScriptError(
|
||||
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");
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
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)
|
||||
|
||||
class WhenStatement(
|
||||
|
||||
@ -4345,6 +4345,22 @@ class BytecodeCompiler(
|
||||
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 WhenInCondition -> WhenInCondition(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())
|
||||
}
|
||||
|
||||
@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 {
|
||||
eval("""
|
||||
class Greeter {
|
||||
|
||||
@ -201,6 +201,28 @@ class WebsiteSamplesTest {
|
||||
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
|
||||
fun testEasyOperatorOverloading() = runTest {
|
||||
val scope = Script.newScope()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user