added Multiple Inheritance
This commit is contained in:
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())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user