lyng/lynglib/src/commonTest/kotlin/TestInheritance.kt

198 lines
6.1 KiB
Kotlin

/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
/*
* Copyright 2025 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*
*/
class TestInheritance {
@Test
fun testInheritanceSpecification() = runTest {
eval("""
// Multiple inheritance specification test (spec only, parser/interpreter TBD)
// Parent A: exposes a val and a var, and a method with a name that collides with Bar.common()
class Foo(val a) {
var tag = "F"
fun runA() {
"ResultA:" + a
}
fun common() {
"CommonA"
}
// this can only be called from Foo (not from subclasses):
private fun privateInFoo() {
}
// this can be called from Foo and any subclass (including MI subclasses):
protected fun protectedInFoo() {
}
}
// Parent B: also exposes a val and a var with the same name to test field inheritance and conflict rules
class Bar(val b) {
var tag = "B"
fun runB() {
"ResultB:" + b
}
fun common() {
"CommonB"
}
}
// With multiple inheritance, base constructors are called in the order of declaration,
// and each ancestor is initialized at most once (diamonds are de-duplicated):
class FooBar(a, b) : Foo(a), Bar(b) {
// Ambiguous method name "common" can be disambiguated:
fun commonFromFoo() {
// explicit qualification by ancestor type:
this@Foo.common()
// or by cast:
(this as Foo).common()
}
fun commonFromBar() {
this@Bar.common()
(this as Bar).common()
}
// Accessing inherited fields (val/var) respects the same resolution rules:
fun tagFromFoo() { this@Foo.tag }
fun tagFromBar() { this@Bar.tag }
}
val fb = FooBar(1, 2)
// Methods with distinct names from different bases work:
assertEquals("ResultA:1", fb.runA())
assertEquals("ResultB:2", fb.runB())
// If we call an ambiguous method unqualified, the first in MRO (leftmost base) is used:
assertEquals("CommonA", fb.common())
// We can call a specific one via explicit qualification or cast:
assertEquals("CommonB", (fb as Bar).common())
assertEquals("CommonA", (fb as Foo).common())
// Or again via explicit casts (wrappers may be validated separately):
assertEquals("CommonB", (fb as Bar).common())
assertEquals("CommonA", (fb as Foo).common())
// Inheriting val/var:
// - Reading an ambiguous var/val selects the first along MRO (Foo.tag initially):
assertEquals("F", fb.tag)
// - Qualified access returns the chosen ancestor’s member:
assertEquals("F", (fb as Foo).tag)
assertEquals("B", (fb as Bar).tag)
// - Writing an ambiguous var writes to the same selected member (first in MRO):
fb.tag = "X"
assertEquals("X", fb.tag) // unqualified resolves to Foo.tag
assertEquals("X", (fb as Foo).tag) // Foo.tag updated
assertEquals("B", (fb as Bar).tag) // Bar.tag unchanged
// - Qualified write via cast updates the specific ancestor’s storage:
(fb as Bar).tag = "Y"
assertEquals("X", (fb as Foo).tag)
assertEquals("Y", (fb as Bar).tag)
// A simple single-inheritance subclass still works:
class Buzz : Bar(3)
val buzz = Buzz()
assertEquals("ResultB:3", buzz.runB())
// Optional cast returns null if cast is not possible; use safe-call with it:
assertEquals("ResultB:3", (buzz as? Bar)?.runB())
assertEquals(null, (buzz as? Foo)?.runA())
// Visibility (spec only):
// - Foo.privateInFoo() is accessible only inside Foo body; even FooBar cannot call it,
// including with this@Foo or casts. Attempting to do so must be a compile-time error.
// - Foo.protectedInFoo() is accessible inside Foo and any subclass bodies (including FooBar),
// but not from unrelated classes/instances.
""".trimIndent())
}
@Test
fun testMITypes() = runTest {
eval("""
import lyng.serialization
class Point(x,y)
class Color(r,g,b)
class ColoredPoint(x, y, r, g, b): Point(x,y), Color(r,g,b)
val cp = ColoredPoint(1,2,30,40,50)
// cp is Color, Point and ColoredPoint:
assert(cp is ColoredPoint)
assert(cp is Point)
assert(cp is Color)
// Color fields must be in ColoredPoint:
assertEquals(30, cp.r)
assertEquals(40, cp.g)
assertEquals(50, cp.b)
// point fields must be available too:
assertEquals(1, cp.x)
assertEquals(2, cp.y)
// if we convert type to color, the fields should be available also:
val color = cp as Color
assert(color is Color)
assertEquals(30, color.r)
assertEquals(40, color.g)
assertEquals(50, color.b)
// converted to Point, cp fields are still available:
val p = cp as Point
assert(p is Point)
assertEquals(1, p.x)
assertEquals(2, p.y)
""")
}
}