fixed extending extern classes (extension methods)

This commit is contained in:
Sergey Chernov 2026-03-15 05:35:29 +03:00
parent 8e0442670d
commit d2a47d34a3
3 changed files with 288 additions and 130 deletions

View File

@ -1120,7 +1120,16 @@ class Compiler(
if (implicitType != null) { if (implicitType != null) {
resolutionSink?.referenceMember(name, pos, implicitType) resolutionSink?.referenceMember(name, pos, implicitType)
val ids = resolveImplicitThisMemberIds(name, pos, implicitType) val ids = resolveImplicitThisMemberIds(name, pos, implicitType)
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType) val inClassContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody }
val currentImplicitType = currentImplicitThisTypeName()
val preferredType = when {
inClassContext -> implicitType
// Extension receiver aliases (extern class name -> host runtime class) can fail strict
// variant casts; keep current receiver untyped but preserve non-current receivers.
implicitType == currentImplicitType -> null
else -> implicitType
}
return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, preferredType)
} }
if (classCtx != null && classCtx.classScopeMembers.contains(name)) { if (classCtx != null && classCtx.classScopeMembers.contains(name)) {
resolutionSink?.referenceMember(name, pos, classCtx.name) resolutionSink?.referenceMember(name, pos, classCtx.name)
@ -4779,6 +4788,11 @@ class Compiler(
if (payload != null) return payload if (payload != null) return payload
} }
val receiverClass = resolveReceiverClassForMember(ref.receiver) val receiverClass = resolveReceiverClassForMember(ref.receiver)
classMethodReturnClass(receiverClass, ref.name)?.let { return it }
inferFieldReturnClass(receiverClass, ref.name)?.let { return it }
if (receiverClass != null && isClassScopeCallableMember(receiverClass.className, ref.name)) {
resolveClassByName("${receiverClass.className}.${ref.name}")?.let { return it }
}
return inferMethodCallReturnClass(ref.name) return inferMethodCallReturnClass(ref.name)
} }

View File

@ -1067,18 +1067,9 @@ class BytecodeCompiler(
updateSlotType(dst, SlotType.OBJ) updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ) return CompiledValue(dst, SlotType.OBJ)
} }
if (receiverClass == null && memberName == "negate") { if (memberName == "negate" &&
val zeroId = builder.addConst(BytecodeConst.IntVal(0)) (receiverClass == null || isDelegateClass(receiverClass) || receiverClass in setOf(ObjInt.type, ObjReal.type))
val zeroSlot = allocSlot() ) {
builder.emit(Opcode.CONST_INT, zeroId, zeroSlot)
updateSlotType(zeroSlot, SlotType.INT)
val obj = ensureObjSlot(value)
val dst = allocSlot()
builder.emit(Opcode.SUB_OBJ, zeroSlot, obj.slot, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
if (memberName == "negate" && receiverClass in setOf(ObjInt.type, ObjReal.type)) {
val zeroId = builder.addConst(BytecodeConst.IntVal(0)) val zeroId = builder.addConst(BytecodeConst.IntVal(0))
val zeroSlot = allocSlot() val zeroSlot = allocSlot()
builder.emit(Opcode.CONST_INT, zeroId, zeroSlot) builder.emit(Opcode.CONST_INT, zeroId, zeroSlot)
@ -1095,6 +1086,11 @@ class BytecodeCompiler(
) )
} }
private fun isDelegateClass(receiverClass: ObjClass): Boolean =
receiverClass.className == "Delegate" ||
receiverClass.className == "LazyDelegate" ||
receiverClass.implementingNames.contains("Delegate")
private fun operatorMemberName(op: BinOp): String? = when (op) { private fun operatorMemberName(op: BinOp): String? = when (op) {
BinOp.PLUS -> "plus" BinOp.PLUS -> "plus"
BinOp.MINUS -> "minus" BinOp.MINUS -> "minus"
@ -1142,11 +1138,7 @@ class BytecodeCompiler(
): CompiledValue? { ): CompiledValue? {
val memberName = operatorMemberName(op) ?: return null val memberName = operatorMemberName(op) ?: return null
val receiverClass = resolveReceiverClass(leftRef) val receiverClass = resolveReceiverClass(leftRef)
if (receiverClass == null || if (receiverClass == null || isDelegateClass(receiverClass)) {
receiverClass.className == "Delegate" ||
receiverClass.className == "LazyDelegate" ||
receiverClass.implementingNames.contains("Delegate")
) {
val objOpcode = when (op) { val objOpcode = when (op) {
BinOp.PLUS -> Opcode.ADD_OBJ BinOp.PLUS -> Opcode.ADD_OBJ
BinOp.MINUS -> Opcode.SUB_OBJ BinOp.MINUS -> Opcode.SUB_OBJ
@ -4821,9 +4813,26 @@ class BytecodeCompiler(
private data class CallArgs(val base: Int, val count: Int, val planId: Int?) private data class CallArgs(val base: Int, val count: Int, val planId: Int?)
private fun resolveExtensionCallableSlot(receiverClass: ObjClass, memberName: String): CompiledValue? { private fun extensionReceiverTypeNames(receiverClass: ObjClass): Set<String> {
val names = LinkedHashSet<String>()
for (cls in receiverClass.mro) { for (cls in receiverClass.mro) {
val candidate = extensionCallableName(cls.className, memberName) names.add(cls.className)
for ((knownName, knownClass) in nameObjClass) {
if (knownClass !== cls && knownClass.className != cls.className) continue
names.add(knownName)
names.add(knownName.substringAfterLast('.'))
}
}
return names
}
private fun resolveExtensionSlotByReceiverNames(
receiverClass: ObjClass,
memberName: String,
wrapperName: (String, String) -> String
): CompiledValue? {
for (receiverName in extensionReceiverTypeNames(receiverClass)) {
val candidate = wrapperName(receiverName, memberName)
if (allowedScopeNames != null && if (allowedScopeNames != null &&
!allowedScopeNames.contains(candidate) && !allowedScopeNames.contains(candidate) &&
!localSlotIndexByName.containsKey(candidate) !localSlotIndexByName.containsKey(candidate)
@ -4835,32 +4844,39 @@ class BytecodeCompiler(
return null return null
} }
private fun resolveUniqueExtensionWrapperSlot(
memberName: String,
wrapperPrefix: String
): CompiledValue? {
val suffix = "__$memberName"
val candidates = LinkedHashSet<String>()
for (name in localSlotIndexByName.keys) {
if (name.startsWith(wrapperPrefix) && name.endsWith(suffix)) {
candidates.add(name)
}
}
for (name in scopeSlotIndexByName.keys) {
if (name.startsWith(wrapperPrefix) && name.endsWith(suffix)) {
candidates.add(name)
}
}
if (candidates.size != 1) return null
return resolveDirectNameSlot(candidates.first())
}
private fun resolveExtensionCallableSlot(receiverClass: ObjClass, memberName: String): CompiledValue? {
return resolveExtensionSlotByReceiverNames(receiverClass, memberName, ::extensionCallableName)
?: resolveUniqueExtensionWrapperSlot(memberName, "__ext__")
}
private fun resolveExtensionGetterSlot(receiverClass: ObjClass, memberName: String): CompiledValue? { private fun resolveExtensionGetterSlot(receiverClass: ObjClass, memberName: String): CompiledValue? {
for (cls in receiverClass.mro) { return resolveExtensionSlotByReceiverNames(receiverClass, memberName, ::extensionPropertyGetterName)
val candidate = extensionPropertyGetterName(cls.className, memberName) ?: resolveUniqueExtensionWrapperSlot(memberName, "__ext_get__")
if (allowedScopeNames != null &&
!allowedScopeNames.contains(candidate) &&
!localSlotIndexByName.containsKey(candidate)
) {
continue
}
resolveDirectNameSlot(candidate)?.let { return it }
}
return null
} }
private fun resolveExtensionSetterSlot(receiverClass: ObjClass, memberName: String): CompiledValue? { private fun resolveExtensionSetterSlot(receiverClass: ObjClass, memberName: String): CompiledValue? {
for (cls in receiverClass.mro) { return resolveExtensionSlotByReceiverNames(receiverClass, memberName, ::extensionPropertySetterName)
val candidate = extensionPropertySetterName(cls.className, memberName) ?: resolveUniqueExtensionWrapperSlot(memberName, "__ext_set__")
if (allowedScopeNames != null &&
!allowedScopeNames.contains(candidate) &&
!localSlotIndexByName.containsKey(candidate)
) {
continue
}
resolveDirectNameSlot(candidate)?.let { return it }
}
return null
} }
private fun compileCallArgsWithReceiver( private fun compileCallArgsWithReceiver(
@ -7468,7 +7484,7 @@ class BytecodeCompiler(
if (targetClass == null) return null if (targetClass == null) return null
if (targetClass == ObjDynamic.type) return ObjDynamic.type if (targetClass == ObjDynamic.type) return ObjDynamic.type
classFieldTypesByName[targetClass.className]?.get(name)?.let { cls -> classFieldTypesByName[targetClass.className]?.get(name)?.let { cls ->
if (cls.className == "Delegate" || cls.className == "LazyDelegate" || cls.implementingNames.contains("Delegate")) { if (isDelegateClass(cls)) {
return null return null
} }
return cls return cls
@ -7555,8 +7571,8 @@ class BytecodeCompiler(
private fun queueExtensionCallableNames(receiverClass: ObjClass, memberName: String) { private fun queueExtensionCallableNames(receiverClass: ObjClass, memberName: String) {
if (!useScopeSlots && globalSlotInfo.isEmpty()) return if (!useScopeSlots && globalSlotInfo.isEmpty()) return
for (cls in receiverClass.mro) { for (receiverName in extensionReceiverTypeNames(receiverClass)) {
val name = extensionCallableName(cls.className, memberName) val name = extensionCallableName(receiverName, memberName)
if (allowedScopeNames == null || allowedScopeNames.contains(name)) { if (allowedScopeNames == null || allowedScopeNames.contains(name)) {
pendingScopeNameRefs.add(name) pendingScopeNameRefs.add(name)
} }
@ -7565,12 +7581,12 @@ class BytecodeCompiler(
private fun queueExtensionPropertyNames(receiverClass: ObjClass, memberName: String) { private fun queueExtensionPropertyNames(receiverClass: ObjClass, memberName: String) {
if (!useScopeSlots && globalSlotInfo.isEmpty()) return if (!useScopeSlots && globalSlotInfo.isEmpty()) return
for (cls in receiverClass.mro) { for (receiverName in extensionReceiverTypeNames(receiverClass)) {
val getter = extensionPropertyGetterName(cls.className, memberName) val getter = extensionPropertyGetterName(receiverName, memberName)
if (allowedScopeNames == null || allowedScopeNames.contains(getter)) { if (allowedScopeNames == null || allowedScopeNames.contains(getter)) {
pendingScopeNameRefs.add(getter) pendingScopeNameRefs.add(getter)
} }
val setter = extensionPropertySetterName(cls.className, memberName) val setter = extensionPropertySetterName(receiverName, memberName)
if (allowedScopeNames == null || allowedScopeNames.contains(setter)) { if (allowedScopeNames == null || allowedScopeNames.contains(setter)) {
pendingScopeNameRefs.add(setter) pendingScopeNameRefs.add(setter)
} }

View File

@ -16,9 +16,10 @@
*/ */
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Script import net.sergeych.lyng.*
import net.sergeych.lyng.ScriptError import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.eval import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjNull
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -27,33 +28,42 @@ class TypesTest {
@Test @Test
fun testTypeCollection1() = runTest { fun testTypeCollection1() = runTest {
eval(""" eval(
"""
class Point(x: Real, y: Real) class Point(x: Real, y: Real)
assert(Point(1,2).x == 1) assert(Point(1,2).x == 1)
assert(Point(1,2).y == 2) assert(Point(1,2).y == 2)
assert(Point(1,2) is Point) assert(Point(1,2) is Point)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testTypeCollection2() = runTest { fun testTypeCollection2() = runTest {
eval(""" eval(
"""
fun fn1(x: Real, y: Real): Real { x + y } fun fn1(x: Real, y: Real): Real { x + y }
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testTypeCollection3() = runTest { fun testTypeCollection3() = runTest {
eval(""" eval(
"""
class Test(a: Int) { class Test(a: Int) {
fun fn1(x: Real, y: Real): Real { x + y } fun fn1(x: Real, y: Real): Real { x + y }
} }
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testExternDeclarations() = runTest { fun testExternDeclarations() = runTest {
eval(""" eval(
"""
extern fun foo1(a: String): Void extern fun foo1(a: String): Void
assertThrows { foo1("1") } assertThrows { foo1("1") }
class Test(a: Int) { class Test(a: Int) {
@ -69,22 +79,26 @@ class TypesTest {
} }
// println("4") // println("4")
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testUserClassCompareTo() = runTest { fun testUserClassCompareTo() = runTest {
eval(""" eval(
"""
class Point(val a,b) class Point(val a,b)
assertEquals(Point(0,1), Point(0,1) ) assertEquals(Point(0,1), Point(0,1) )
assertNotEquals(Point(0,1), Point(1,1) ) assertNotEquals(Point(0,1), Point(1,1) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testUserClassCompareTo2() = runTest { fun testUserClassCompareTo2() = runTest {
eval(""" eval(
"""
class Point(val a,b) { class Point(val a,b) {
var c = 0 var c = 0
} }
@ -98,12 +112,14 @@ class TypesTest {
assertEquals(p1, p2) assertEquals(p1, p2)
assertNotEquals(Point(0,1), Point(1,1) ) assertNotEquals(Point(0,1), Point(1,1) )
assertNotEquals(Point(0,1), p3) assertNotEquals(Point(0,1), p3)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testNumericInference() = runTest { fun testNumericInference() = runTest {
eval(""" eval(
"""
val x = 1 val x = 1
var y = 2.0 var y = 2.0
assert( x is Int ) assert( x is Int )
@ -111,11 +127,14 @@ class TypesTest {
assert( x + y is Real ) assert( x + y is Real )
assert( abs(x+y) is Real ) assert( abs(x+y) is Real )
assert( abs(x/y) is Real ) assert( abs(x/y) is Real )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testNumericInferenceBug1() = runTest { fun testNumericInferenceBug1() = runTest {
eval(""" eval(
"""
fun findSumLimit(f) { fun findSumLimit(f) {
var sum = 0.0 var sum = 0.0
for( n in 1..100 ) { for( n in 1..100 ) {
@ -142,12 +161,14 @@ class TypesTest {
val limit = findSumLimit { n -> 1.0/n/n } val limit = findSumLimit { n -> 1.0/n/n }
assert( limit != null ) assert( limit != null )
println("Result: "+limit) println("Result: "+limit)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testNullableHints() = runTest { fun testNullableHints() = runTest {
eval(""" eval(
"""
// nullable, without type os Object? // nullable, without type os Object?
class N(x=null) class N(x=null)
assertEquals(null, N().x) assertEquals(null, N().x)
@ -167,7 +188,8 @@ class TypesTest {
@Test @Test
fun testIsUnionIntersection() = runTest { fun testIsUnionIntersection() = runTest {
eval(""" eval(
"""
class A class A
class B class B
class C: A, B class C: A, B
@ -179,7 +201,8 @@ class TypesTest {
val v = 1 val v = 1
assert( v is Int | String | Real ) assert( v is Int | String | Real )
assert( !(v is String | Bool) ) assert( !(v is String | Bool) )
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
@ -221,51 +244,64 @@ class TypesTest {
@Test @Test
fun testListLiteralInferenceForBounds() = runTest { fun testListLiteralInferenceForBounds() = runTest {
eval(""" eval(
"""
fun acceptInts<T: Int>(xs: List<T>) { } fun acceptInts<T: Int>(xs: List<T>) { }
acceptInts([1, 2, 3]) acceptInts([1, 2, 3])
val base = [1, 2] val base = [1, 2]
acceptInts([...base, 3]) acceptInts([...base, 3])
""".trimIndent()) """.trimIndent()
eval(""" )
eval(
"""
fun acceptReals<T: Real>(xs: List<T>) { } fun acceptReals<T: Real>(xs: List<T>) { }
acceptReals([1.0, 2.0, 3.0]) acceptReals([1.0, 2.0, 3.0])
val base = [1.0, 2.0] val base = [1.0, 2.0]
acceptReals([...base, 3.0]) acceptReals([...base, 3.0])
""".trimIndent()) """.trimIndent()
)
assertFailsWith<net.sergeych.lyng.ScriptError> { assertFailsWith<net.sergeych.lyng.ScriptError> {
eval(""" eval(
"""
fun acceptInts<T: Int>(xs: List<T>) { } fun acceptInts<T: Int>(xs: List<T>) { }
acceptInts([1, "a"]) acceptInts([1, "a"])
""".trimIndent()) """.trimIndent()
)
} }
assertFailsWith<net.sergeych.lyng.ScriptError> { assertFailsWith<net.sergeych.lyng.ScriptError> {
eval(""" eval(
"""
fun acceptReals<T: Real>(xs: List<T>) { } fun acceptReals<T: Real>(xs: List<T>) { }
acceptReals([1.0, "a"]) acceptReals([1.0, "a"])
""".trimIndent()) """.trimIndent()
)
} }
} }
@Test @Test
fun testMapLiteralInferenceForBounds() = runTest { fun testMapLiteralInferenceForBounds() = runTest {
eval(""" eval(
"""
fun acceptMap<T: Int>(m: Map<String, T>) { } fun acceptMap<T: Int>(m: Map<String, T>) { }
acceptMap({ "a": 1, "b": 2 }) acceptMap({ "a": 1, "b": 2 })
val base = { "a": 1 } val base = { "a": 1 }
acceptMap({ ...base, "b": 3 }) acceptMap({ ...base, "b": 3 })
""".trimIndent()) """.trimIndent()
)
assertFailsWith<net.sergeych.lyng.ScriptError> { assertFailsWith<net.sergeych.lyng.ScriptError> {
eval(""" eval(
"""
fun acceptMap<T: Int>(m: Map<String, T>) { } fun acceptMap<T: Int>(m: Map<String, T>) { }
acceptMap({ "a": 1, "b": "x" }) acceptMap({ "a": 1, "b": "x" })
""".trimIndent()) """.trimIndent()
)
} }
} }
@Test @Test
fun testUnionTypeLists() = runTest { fun testUnionTypeLists() = runTest {
eval(""" eval(
"""
fun fMixed<T>(list: List<T>) { fun fMixed<T>(list: List<T>) {
println(list) println(list)
@ -282,12 +318,14 @@ class TypesTest {
} }
fMixed([1, "two", true]) fMixed([1, "two", true])
fInts([1,2,3]) fInts([1,2,3])
""") """
)
} }
@Test @Test
fun testTypeAliases() = runTest { fun testTypeAliases() = runTest {
eval(""" eval(
"""
type Num = Int | Real type Num = Int | Real
type AB = A & B type AB = A & B
class A class A
@ -308,12 +346,14 @@ class TypesTest {
type IntList<T: Int> = List<T> type IntList<T: Int> = List<T>
fun accept<T: Int>(xs: IntList<T>) { } fun accept<T: Int>(xs: IntList<T>) { }
accept([1,2,3]) accept([1,2,3])
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testMultipleReceivers() = runTest { fun testMultipleReceivers() = runTest {
eval(""" eval(
"""
class R1(shared,r1="r1") class R1(shared,r1="r1")
class R2(shared,r2="r2") class R2(shared,r2="r2")
@ -340,69 +380,85 @@ class TypesTest {
assertEquals("r1", r1) assertEquals("r1", r1)
} }
} }
""") """
)
} }
@Test @Test
fun testLambdaTypes1() = runTest { fun testLambdaTypes1() = runTest {
val scope = Script.newScope() val scope = Script.newScope()
// declare: ok // declare: ok
scope.eval(""" scope.eval(
"""
var l1: (Int,String)->String var l1: (Int,String)->String
""".trimIndent()) """.trimIndent()
)
// this should be Lyng compile time exception // this should be Lyng compile time exception
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
scope.eval(""" scope.eval(
"""
fun test() { fun test() {
// compiler should detect that l1 us called with arguments that does not match // compiler should detect that l1 us called with arguments that does not match
// declare type (Int,String)->String: // declare type (Int,String)->String:
l1() l1()
} }
""".trimIndent()) """.trimIndent()
)
} }
} }
@Test @Test
fun testLambdaTypesEllipsis() = runTest { fun testLambdaTypesEllipsis() = runTest {
val scope = Script.newScope() val scope = Script.newScope()
scope.eval(""" scope.eval(
"""
var l2: (Int,Object...,String)->Real var l2: (Int,Object...,String)->Real
var l4: (Int,String...,String)->Real var l4: (Int,String...,String)->Real
var l3: (...)->Int var l3: (...)->Int
""".trimIndent()) """.trimIndent()
)
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
scope.eval(""" scope.eval(
"""
fun testTooFew() { fun testTooFew() {
l2(1) l2(1)
} }
""".trimIndent()) """.trimIndent()
)
} }
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
scope.eval(""" scope.eval(
"""
fun testWrongHead() { fun testWrongHead() {
l2("x", "y") l2("x", "y")
} }
""".trimIndent()) """.trimIndent()
)
} }
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
scope.eval(""" scope.eval(
"""
fun testWrongEllipsis() { fun testWrongEllipsis() {
l4(1, 2, "x") l4(1, 2, "x")
} }
""".trimIndent()) """.trimIndent()
)
} }
scope.eval(""" scope.eval(
"""
fun testOk1() { l2(1, "x") } fun testOk1() { l2(1, "x") }
fun testOk2() { l2(1, 2, 3, "x") } fun testOk2() { l2(1, 2, 3, "x") }
fun testOk3() { l3() } fun testOk3() { l3() }
fun testOk4() { l3(1, true, "x") } fun testOk4() { l3(1, true, "x") }
fun testOk5() { l4(1, "a", "b", "x") } fun testOk5() { l4(1, "a", "b", "x") }
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testSetTyped() = runTest { fun testSetTyped() = runTest {
eval(""" eval(
"""
var s = Set<String>() var s = Set<String>()
val typed: Set<String> = s val typed: Set<String> = s
assertEquals(Set(), typed) assertEquals(Set(), typed)
@ -413,12 +469,14 @@ class TypesTest {
assertEquals(Set(), s) assertEquals(Set(), s)
s += ["foo", "bar"] s += ["foo", "bar"]
assertEquals(Set("foo", "bar"), s) assertEquals(Set("foo", "bar"), s)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testListTyped() = runTest { fun testListTyped() = runTest {
eval(""" eval(
"""
var l = List<String>() var l = List<String>()
val typed: List<String> = l val typed: List<String> = l
assertEquals(List(), typed) assertEquals(List(), typed)
@ -430,13 +488,15 @@ class TypesTest {
l += ["foo", "bar"] l += ["foo", "bar"]
assertEquals(List("foo", "bar"), l) assertEquals(List("foo", "bar"), l)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testAliasesInGenerics1() = runTest { fun testAliasesInGenerics1() = runTest {
val scope = Script.newScope() val scope = Script.newScope()
scope.eval(""" scope.eval(
"""
type IntList<T: Int> = List<T> type IntList<T: Int> = List<T>
type IntMap<K,V> = Map<K,V> type IntMap<K,V> = Map<K,V>
type IntSet<T: Int> = Set<T> type IntSet<T: Int> = Set<T>
@ -459,13 +519,16 @@ class TypesTest {
assertEquals(Set("tag1", "tag2", Buffer("tag3")), x.tags) assertEquals(Set("tag1", "tag2", Buffer("tag3")), x.tags)
x.tags += Buffer("tag4") x.tags += Buffer("tag4")
assertEquals(Set("tag1", "tag2", Buffer("tag3"), Buffer("tag4")), x.tags) assertEquals(Set("tag1", "tag2", Buffer("tag3"), Buffer("tag4")), x.tags)
""") """
scope.eval(""" )
scope.eval(
"""
assert(x is X) assert(x is X)
x.tags += "42" x.tags += "42"
assertEquals(Set("tag1", "tag2", Buffer("tag3"), Buffer("tag4"), "42"), x.tags) assertEquals(Set("tag1", "tag2", Buffer("tag3"), Buffer("tag4"), "42"), x.tags)
""".trimIndent()) """.trimIndent()
)
// now this must fail becaise element type does not match the declared: // now this must fail becaise element type does not match the declared:
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
scope.eval( scope.eval(
@ -479,7 +542,8 @@ class TypesTest {
@Test @Test
fun testAliasesInGenericsList1() = runTest { fun testAliasesInGenericsList1() = runTest {
val scope = Script.newScope() val scope = Script.newScope()
scope.eval(""" scope.eval(
"""
import lyng.buffer import lyng.buffer
type Tag = String | Buffer type Tag = String | Buffer
@ -495,12 +559,15 @@ class TypesTest {
assertEquals(List("tag1", "tag2", Buffer("tag3")), x.tags) assertEquals(List("tag1", "tag2", Buffer("tag3")), x.tags)
x.tags += ["tag4", Buffer("tag5")] x.tags += ["tag4", Buffer("tag5")]
assertEquals(List("tag1", "tag2", Buffer("tag3"), "tag4", Buffer("tag5")), x.tags) assertEquals(List("tag1", "tag2", Buffer("tag3"), "tag4", Buffer("tag5")), x.tags)
""") """
scope.eval(""" )
scope.eval(
"""
assert(x is X) assert(x is X)
x.tags += "42" x.tags += "42"
assertEquals(List("tag1", "tag2", Buffer("tag3"), "tag4", Buffer("tag5"), "42"), x.tags) assertEquals(List("tag1", "tag2", Buffer("tag3"), "tag4", Buffer("tag5"), "42"), x.tags)
""".trimIndent()) """.trimIndent()
)
assertFailsWith<ScriptError> { assertFailsWith<ScriptError> {
scope.eval( scope.eval(
""" """
@ -512,18 +579,21 @@ class TypesTest {
@Test @Test
fun testClassName() = runTest { fun testClassName() = runTest {
eval(""" eval(
"""
class X { class X {
var x = 1 var x = 1
} }
assert( X::class is Class) assert( X::class is Class)
assertEquals("Class", X::class.name) assertEquals("Class", X::class.name)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testGenericTypes() = runTest { fun testGenericTypes() = runTest {
eval(""" eval(
"""
fun t<T>(): String = fun t<T>(): String =
when(T) { when(T) {
null -> "%s is Null"(T::class.name) null -> "%s is Null"(T::class.name)
@ -532,24 +602,28 @@ class TypesTest {
} }
assert( Int is Object) assert( Int is Object)
assertEquals( t<Int>(), "Class is Object") assertEquals( t<Int>(), "Class is Object")
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testGenericNullableTypePredicate() = runTest { fun testGenericNullableTypePredicate() = runTest {
eval(""" eval(
"""
fun isTypeNullable<T>(x: T): Bool = T is nullable fun isTypeNullable<T>(x: T): Bool = T is nullable
type MaybeInt = Int? type MaybeInt = Int?
assert(isTypeNullable<Int?>(null)) assert(isTypeNullable<Int?>(null))
assert(!isTypeNullable<Int>(1)) assert(!isTypeNullable<Int>(1))
assert(MaybeInt is nullable) assert(MaybeInt is nullable)
assert(!(Int is nullable)) assert(!(Int is nullable))
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testWhenNullableTypeCase() = runTest { fun testWhenNullableTypeCase() = runTest {
eval(""" eval(
"""
fun describe<T>(x: T): String = when(T) { fun describe<T>(x: T): String = when(T) {
nullable -> "nullable" nullable -> "nullable"
else -> "non-null" else -> "non-null"
@ -562,11 +636,14 @@ class TypesTest {
assertEquals("non-null", describe<Int>(1)) assertEquals("non-null", describe<Int>(1))
assertEquals("nullable", describeIs<Int?>(null)) assertEquals("nullable", describeIs<Int?>(null))
assertEquals("non-null", describeIs<Int>(1)) assertEquals("non-null", describeIs<Int>(1))
""".trimIndent()) """.trimIndent()
)
} }
@Test fun testIndexer() = runTest { @Test
eval(""" fun testIndexer() = runTest {
eval(
"""
class Greeter { class Greeter {
override fun getAt(name) = "Hello, %s!"(name) override fun getAt(name) = "Hello, %s!"(name)
} }
@ -581,11 +658,14 @@ class TypesTest {
assertEquals("How do you do, Bob?",Polite["Bob"]) assertEquals("How do you do, Bob?",Polite["Bob"])
""".trimIndent()) """.trimIndent()
)
} }
@Test fun testIndexer2() = runTest { @Test
eval(""" fun testIndexer2() = runTest {
eval(
"""
class Foo(bar) class Foo(bar)
class Greeter { class Greeter {
@ -613,17 +693,65 @@ class TypesTest {
assertEquals("How do you do, Bob?",g2v.bar) assertEquals("How do you do, Bob?",g2v.bar)
assertEquals("How do you do, Bob?",Greeter2()["Bob"].bar) assertEquals("How do you do, Bob?",Greeter2()["Bob"].bar)
""".trimIndent()) """.trimIndent()
)
} }
@Test @Test
fun testExternGenerics() = runTest { fun testExternGenerics() = runTest {
eval(""" eval(
"""
extern fun f<T>(x: T): T extern fun f<T>(x: T): T
extern class Cell<T> { extern class Cell<T> {
var value: T var value: T
} }
""") """
)
}
class ObjFoo : Obj() {
override val objClass = klass
var value: Obj = ObjNull
companion object {
val klass = object : ObjClass("ObjFoo") {
override suspend fun callOn(scope: Scope): Obj {
return ObjFoo()
}
}.apply {
addProperty("bar", {
check(thisObj is ObjFoo)
thisAs<ObjFoo>().value
}, {
thisAs<ObjFoo>().value = args.firstAndOnly()
})
}
}
}
@Test
fun testExtensionToExternClasses() = runTest {
val scope = Script.newScope()
scope.addConst("Foo", ObjFoo.klass)
scope.eval(
"""
extern class Foo {
var bar
}
fun Foo.foobar() = "foo" + bar
val f = Foo()
f.bar = 42
assert( f is Foo )
assertEquals(42, f.bar)
assertEquals("foo42", f.foobar())
""".trimIndent()
)
} }
@Test @Test