added Multiple Inheritance

This commit is contained in:
sergeych committed 2025-11-17 00:28:07 +01:00
1 parent 882df67909
commit aeeec2d417
33 files changed
+3302 -130

No files matched your search

@@ -0,0 +1,68 @@
/*
* 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.
*
*/
/*
* C3 MRO tests for Multiple Inheritance
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class MIC3MroTest {
@Test
fun diamondConstructorRunsSharedAncestorOnce() = runTest {
eval(
"""
var topInit = 0
class Top() {
// increment module-scope counter in instance initializer; runs once per Top-subobject
var tick = (topInit = topInit + 1)
}
class Left() : Top()
class Right() : Top()
class Bottom() : Left(), Right()
val b = Bottom()
assertEquals(1, topInit)
""".trimIndent()
)
}
@Test
fun methodResolutionFollowsC3() = runTest {
// For the classic diamond D(B,C), B and C derive from A; C3 should result in D -> B -> C -> A
eval(
"""
class A() { fun common() { "A" } }
class B() : A() { fun common() { "B" } }
class C() : A() { fun common() { "C" } }
class D() : B(), C()
val d = D()
// Unqualified must pick B.common() according to C3 when direct bases are (B, C)
assertEquals("B", d.common())
// Qualified disambiguation via casts still works
assertEquals("C", (d as C).common())
assertEquals("A", (d as A).common())
""".trimIndent()
)
}
}
@@ -0,0 +1,105 @@
/*
* 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.
*
*/
/*
* Diagnostics tests for Multiple Inheritance (MI)
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
import kotlin.test.assertFails
import kotlin.test.assertTrue
class MIDiagnosticsTest {
@Test
fun missingMemberIncludesLinearizationAndHint() = runTest {
val ex = assertFails {
eval(
"""
class Foo(val a) { fun runA() { "ResultA:" + a } }
class Bar(val b) { fun runB() { "ResultB:" + b } }
class FooBar(a,b) : Foo(a), Bar(b) { }
val fb = FooBar(1,2)
fb.qux()
""".trimIndent()
)
}
val msg = ex.message ?: ""
assertTrue(msg.contains("no such member: qux"), "must mention missing member name")
assertTrue(msg.contains("FooBar"), "must mention receiver class name")
assertTrue(msg.contains("Considered order:"), "must include linearization header")
assertTrue(msg.contains("FooBar") && msg.contains("Foo") && msg.contains("Bar"), "must list classes in order")
assertTrue(msg.contains("this@") || msg.contains("(obj as"), "must suggest qualification or cast")
}
@Test
fun missingFieldIncludesLinearization() = runTest {
val ex = assertFails {
eval(
"""
class Foo(val a) { var tag = "F" }
class Bar(val b) { var tag = "B" }
class FooBar(a,b) : Foo(a), Bar(b) { }
val fb = FooBar(1,2)
fb.unknownField
""".trimIndent()
)
}
val msg = ex.message ?: ""
assertTrue(msg.contains("no such field: unknownField"))
assertTrue(msg.contains("FooBar"))
assertTrue(msg.contains("Considered order:"))
}
@Test
fun invalidQualifiedThisReportsAncestorError() = runTest {
assertFails {
eval(
"""
class Foo() { fun f() { "F" } }
class Bar() { fun g() { "G" } }
class Baz() : Foo() {
fun bad() { this@Bar.g() }
}
val b = Baz()
b.bad()
""".trimIndent()
)
}
}
@Test
fun castFailureMentionsActualAndTargetTypes() = runTest {
val ex = assertFails {
eval(
"""
class Foo() { }
class Bar() { }
val b = Bar()
(b as Foo)
""".trimIndent()
)
}
val msg = ex.message ?: ""
// message like: Cannot cast Bar to Foo (be tolerant across targets)
val lower = msg.lowercase()
assertTrue(lower.contains("cast"), "message should mention cast")
assertTrue(msg.contains("Bar") || msg.contains("Foo"), "should mention at least one of the types")
}
}
@@ -0,0 +1,98 @@
/*
* 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
class MIQualifiedDispatchTest {
@Test
fun testQualifiedMethodResolution() = runTest {
eval(
"""
class Foo(val a) {
fun common() { "A" }
fun runA() { "ResultA:" + a }
}
class Bar(val b) {
fun common() { "B" }
fun runB() { "ResultB:" + b }
}
class FooBar(a,b) : Foo(a), Bar(b) { }
val fb = FooBar(1,2)
// unqualified picks leftmost base
assertEquals("A", fb.common())
// cast-based disambiguation
assertEquals("B", (fb as Bar).common())
assertEquals("A", (fb as Foo).common())
// Note: wrappers using this@Type inside FooBar body will be validated later
// when class-body method registration is finalized.
""".trimIndent()
)
}
@Test
fun testQualifiedFieldReadWrite() = runTest {
eval(
"""
class Foo(val a) { var tag = "F" }
class Bar(val b) { var tag = "B" }
class FooBar(a,b) : Foo(a), Bar(b) { }
val fb = FooBar(1,2)
// unqualified resolves to leftmost base
assertEquals("F", fb.tag)
// qualified reads via casts
assertEquals("F", (fb as Foo).tag)
assertEquals("B", (fb as Bar).tag)
// unqualified write updates leftmost base
fb.tag = "X"
assertEquals("X", fb.tag)
assertEquals("X", (fb as Foo).tag)
assertEquals("B", (fb as Bar).tag)
// qualified write via cast targets Bar
(fb as Bar).tag = "Y"
assertEquals("X", (fb as Foo).tag)
assertEquals("Y", (fb as Bar).tag)
""".trimIndent()
)
}
@Test
fun testCastsAndSafeCall() = runTest {
eval(
"""
class Foo(val a) { fun runA() { "ResultA:" + a } }
class Bar(val b) { fun runB() { "ResultB:" + b } }
class Buzz : Bar(3)
val buzz = Buzz()
assertEquals("ResultB:3", buzz.runB())
assertEquals("ResultB:3", (buzz as? Bar)?.runB())
assertEquals(null, (buzz as? Foo)?.runA())
""".trimIndent()
)
}
}
@@ -0,0 +1,75 @@
/*
* 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
import kotlin.test.assertFails
class MIVisibilityTest {
@Test
fun privateMethodNotVisibleInSubclass() = runTest {
val code = """
class Foo() {
private fun secret() { "S" }
}
class Bar() : Foo() {
fun trySecret() { this@Foo.secret() }
}
val b = Bar()
// calling a wrapper that tries to access Foo.secret must fail
b.trySecret()
""".trimIndent()
assertFails { eval(code) }
}
@Test
fun protectedMethodNotVisibleFromUnrelatedEvenViaCast() = runTest {
val code = """
class Foo() { protected fun prot() { "P" } }
class Bar() : Foo() { }
val b = Bar()
// Unrelated context tries to call through cast — should fail
(b as Foo).prot()
""".trimIndent()
assertFails { eval(code) }
}
@Test
fun privateFieldNotVisibleInSubclass() = runTest {
val code = """
class Foo() { private val x = "X" }
class Bar() : Foo() { fun getX() { this@Foo.x } }
val b = Bar()
b.getX()
""".trimIndent()
assertFails { eval(code) }
}
@Test
fun protectedFieldNotVisibleFromUnrelatedEvenViaCast() = runTest {
// Not allowed from unrelated, even via cast
val code = """
class Foo() { protected val y = "Y" }
class Bar() : Foo() { }
val b = Bar()
(b as Foo).y
""".trimIndent()
assertFails { eval(code) }
}
}
@@ -0,0 +1,55 @@
/*
* 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.
*
*/
/*
* Focused regression test for local variable visibility across suspension inside withLock { }
*/
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
class ParallelLocalScopeTest {
@Test
fun localsSurviveSuspensionInsideWithLock() = runTest {
// Minimal reproduction of the pattern used in ScriptTest.testParallels2
eval(
"""
class AtomicCounter {
private val m = Mutex()
private var counter = 0
fun increment() {
m.withLock {
val a = counter
delay(1)
counter = a + 1
}
}
fun getCounter() { counter }
}
val ac = AtomicCounter()
// Single-threaded increments should work if locals survive after delay
for (i in 1..3) ac.increment()
assertEquals(3, ac.getCounter())
""".trimIndent()
)
}
}
@@ -0,0 +1,154 @@
/*
* 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())
}
}