added T is nullable support

This commit is contained in:
Sergey Chernov 2026-03-14 20:57:15 +03:00
parent c54d947f1c
commit 37d093817e
9 changed files with 171 additions and 11 deletions

View File

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

View 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))

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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