/* * 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. * 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.Script import net.sergeych.lyng.eval import net.sergeych.lyng.obj.ObjInstance import net.sergeych.lyng.obj.ObjList import net.sergeych.lyng.toSource import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails class OOTest { @Test fun testClassProps() = runTest { eval( """ import lyng.time class Point(val x, val y) { static val origin = Point(0,0) static var center = null } Point.center = Point.origin assertEquals(Point(0,0), Point.origin) assertEquals(Point(0,0), Point.center) Point.center = Point(1,2) assertEquals(Point(0,0), Point.origin) assertEquals(Point(1,2), Point.center) """.trimIndent() ) } @Test fun testClassMethods() = runTest { eval( """ import lyng.time var pointData = null class Point(val x, val y) { static fun getData() = pointData static fun setData(value) { pointData = value + "!" } } assertEquals(Point(0,0), Point(0,0) ) assertEquals(null, Point.getData() ) Point.setData("foo") assertEquals( "foo!", Point.getData() ) """.trimIndent() ) } @Test fun testDynamicGet() = runTest { eval( """ val accessor: Delegate = dynamic { get { name -> if( name == "foo" ) "bar" else null } } println("-- " + accessor.foo) assertEquals("bar", accessor.foo) assertEquals(null, accessor.bar) """.trimIndent() ) } @Test fun testDelegateSet() = runTest { eval( """ var setValueForBar = null val accessor: Delegate = dynamic { get { name -> when(name) { "foo" -> "bar" "bar" -> setValueForBar else -> null } } set { name, value -> if( name == "bar" ) setValueForBar = value else throw IllegalAssignmentException("Can't assign "+name) } } assertEquals("bar", accessor.foo) assertEquals(null, accessor.bar) accessor.bar = "buzz" assertEquals("buzz", accessor.bar) assertThrows { accessor.bad = "!23" } """.trimIndent() ) } @Test fun testDynamicIndexAccess() = runTest { eval( """ val store = Map() val accessor: Delegate = dynamic { get { name -> store[name] } set { name, value -> store[name] = value } } assertEquals(null, accessor["foo"]) assertEquals(null, accessor.foo) accessor["foo"] = "bar" assertEquals("bar", accessor["foo"]) assertEquals("bar", accessor.foo) """.trimIndent() ) } @Test fun testMultilineConstructor() = runTest { eval( """ class Point( x, y ) assertEquals(Point(1,2), Point(1,2) ) """.trimIndent() ) } @Test fun testDynamicClass() = runTest { eval( """ fun getContract(contractName): Delegate { dynamic { get { name -> println("Call: %s.%s"(contractName,name)) } } } getContract("foo").bar """ ) } @Test fun testDynamicClassReturn2() = runTest { // todo: should work without extra parenthesis // see below eval( """ fun getContract(contractName): Delegate { println("1") dynamic { get { name -> println("innrer %s.%s"(contractName,name)) { args... -> if( name == "bar" ) (args as List).sum() else null } } } } val cc: Delegate = dynamic { get { name -> println("Call cc %s"(name)) getContract(name) } } val x = cc.foo.bar println(x) x(1,2,3) assertEquals(6, x(1,2,3)) // v HERE v assertEquals(15, cc.foo.bar(10,2,3)) """ ) } @Test fun testClassInitialization() = runTest { eval( """ var countInstances = 0 class Point(val x: Int, val y: Int) { println("Class initializer is called 1") var magnitude /* init {} section optionally provide initialization code that is called on each instance creation. it should have the same access to this.* and constructor parameters as any other member function. */ init { countInstances++ magnitude = Math.sqrt(x*x + y*y) } } val p = Point(1, 2) assertEquals(1, countInstances) assertEquals(p, Point(1,2) ) assertEquals(2, countInstances) """.trimIndent() ) } @Test fun testMIInitialization() = runTest { eval( """ var order = [] class A { init { order.add("A") } } class B : A { init { order.add("B") } } class C { init { order.add("C") } } class D : B, C { init { order.add("D") } } D() assertEquals(["A", "B", "C", "D"], order) """ ) } @Test fun testMIDiamondInitialization() = runTest { eval( """ var order = [] class A { init { order.add("A") } } class B : A { init { order.add("B") } } class C : A { init { order.add("C") } } class D : B, C { init { order.add("D") } } D() assertEquals(["A", "B", "C", "D"], order) """ ) } @Test fun testInitBlockInDeserialization() = runTest { eval( """ import lyng.serialization var count = 0 class A { init { count++ } } val a1 = A() val coded = Lynon.encode(a1) val a2 = Lynon.decode(coded) assertEquals(2, count) """ ) } @Test fun testDefaultCompare() = runTest { eval( """ class Point(val x: Int, val y: Int) assertEquals(Point(1,2), Point(1,2) ) assert( Point(1,2) != Point(2,1) ) assert( Point(1,2) == Point(1,2) ) """.trimIndent() ) } @Test fun testConstructorCallsWithNamedParams() = runTest { val scope = Script.newScope() val list = scope.eval( """ import lyng.time class BarRequest( val id, val vaultId, val userAddress, val isDepositRequest, val grossWeight, val fineness, val notes="", val createdAt = Instant.now().truncateToSecond(), val updatedAt = Instant.now().truncateToSecond() ) { // unrelated for comparison static val stateNames = [1, 2, 3] val cell = cached { id } } assertEquals( 5,5.toInt()) val b1 = BarRequest(1, "v1", "u1", true, 1000, 999) val b2 = BarRequest(1, "v1", "u1", true, 1000, 999, createdAt: b1.createdAt, updatedAt: b1.updatedAt) assertEquals(b1, b2) assertEquals( 0, b1 <=> b2) [b1, b2] """.trimIndent() ) as ObjList val b1 = list.list[0] as ObjInstance val b2 = list.list[1] as ObjInstance assertEquals(0, b1.compareTo(scope, b2)) } @Test fun testPropAsExtension() = runTest { val scope = Script.newScope() scope.eval( """ class A(val x) { private val privateVal = 100 val p1 get() = this.x + 1 } assertEquals(2, A(1).p1) fun A.f() = this.x + 5 assertEquals(7, __ext__A__f(A(2))) // The same, we should be able to add member values to a class; // notice it should access to the class public instance members, // somewhat like it is declared in the class body val A.simple get() = this.x + 3 assertEquals(5, __ext_get__A__simple(A(2))) // it should also work with properties: val A.p10 get() = this.x * 10 assertEquals(20, __ext_get__A__p10(A(2))) """.trimIndent() ) // important is that such extensions should not be able to access private members // and thus remove privateness: assertFails { scope.eval("val A.exportPrivateVal = privateVal; __ext_get__A__exportPrivateVal(A(1))") } assertFails { scope.eval("val A.exportPrivateValProp get() = privateVal; __ext_get__A__exportPrivateValProp(A(1))") } } @Test fun testExtensionsAreScopeIsolated() = runTest { val scope1 = Script.newScope() scope1.eval( """ fun String.totalDigits() = // notice using `this`: (this.characters as List).filter{ (it as Char).isDigit() }.size() assertEquals(2, __ext__String__totalDigits("answer is 42")) """ ) val scope2 = Script.newScope() assertFails { // in scope2 we didn't override `totalDigits` extension: scope2.eval("""__ext__String__totalDigits("answer is 42")""".trimIndent()) } } @Test fun testCacheInClass() = runTest { eval( """ class T(salt) { private var c init { println("create cached with "+salt) c = cached { salt + "." } } fun getResult() = c() } val t1 = T("foo") val t2 = T("bar") assertEquals("bar.", t2.getResult()) assertEquals("foo.", t1.getResult()) """.trimIndent() ) } @Test fun testLateInitValsInClasses() = runTest { assertFails { eval( """ class T { val x } """ ) } assertFails { eval("val String.late") } eval( """ // but we can "late-init" them in init block: class OK { val x init { x = "foo" } } val ok = OK() assertEquals("foo", ok.x) // they can't be reassigned: assertThrows(IllegalAssignmentException) { ok.x = "bar" } // To test access before init, we need a trick: class AccessBefore { val x fun readX() = x init { assertEquals(x, Unset) // if we call readX() here, x is Unset. // Just reading it is fine, but using it should throw: assertThrows(UnsetException) { readX() + 1 } x = 42 } } AccessBefore() """.trimIndent() ) } @Test fun testPrivateSet() = runTest { eval( """ class A { var y = 100 private set fun setValue(newValue) { y = newValue } } assertEquals(100, A().y) assertThrows(IllegalAccessException) { A().y = 200 } val a = A() a.setValue(200) assertEquals(200, a.y) class B { var y = 10 protected set } class C : B { fun setBValue(v) { y = v } } val c = C() assertEquals(10, c.y) assertThrows(IllegalAccessException) { c.y = 20 } c.setBValue(30) assertEquals(30, c.y) class D { private var _y = 0 var y get() = _y private set(v) { _y = v } fun setY(v) { y = v } } val d = D() assertEquals(0, d.y) assertThrows(IllegalAccessException) { d.y = 10 } d.setY(20) assertEquals(20, d.y) """ ) } @Test fun testValPrivateSetError() = runTest { assertFails { eval("class E { val x = 1 private set }") } } @Test fun testAbstractClassesAndOverridingProposal() = runTest { val scope = Script.newScope() /* Abstract class is a sort of interface on steroidsL it is a class some members/methods of which are required to be implemented by heirs. Still it is a regular class in all other respects. Just can't be instantiated */ scope.eval( """ // abstract modifier is required. It can have a constructor, or be without it: abstract class A(someParam=1) { // if the method is marked as abstract, it has no body: abstract fun foo(): Int // abstract members have no initializer: abstract fun getBar(): Int } // can't create instance of the abstract class: assertThrows { A() } """.trimIndent() ) // create abstract method with body or val/var with initializer is an error: assertFails { scope.eval("abstract class B { abstract fun foo() = 1 }") } assertFails { scope.eval("abstract class C { abstract val bar = 1 }") } // inheriting an abstract class without implementing all of it abstract members and methods // is not allowed: assertFails { scope.eval("class D : A(1) { override fun foo() = 10 }") } // but it is allowed to inherit in another abstract class: scope.eval("abstract class E : A(1) { override fun foo() = 10 }") // implementing all abstracts let us have regular class: scope.eval( """ class F : E() { override fun getBar() = 11 } assertEquals(10, F().foo()) assertEquals(11, F().getBar()) """.trimIndent() ) // MI-based abstract implementation is deferred. } @Test fun testAbstractAndOverrideEdgeCases() = runTest { val scope = Script.newScope() // 1. abstract private is an error: assertFails { scope.eval("abstract class Err { abstract private fun foo() }") } assertFails { scope.eval("abstract class Err { abstract private val x }") } // 2. private member in parent is not visible for overriding: scope.eval( """ class Base { private fun secret() = 1 fun callSecret() = secret() } class Derived : Base() { // New method name avoids private override ambiguity fun secret2() = 2 } val d = Derived() assertEquals(2, d.secret2()) assertEquals(1, d.callSecret()) """.trimIndent() ) // Using override keyword when there is only a private member in parent is an error: assertFails { scope.eval("class D2 : Base() { override fun secret() = 3 }") } // 3. interface can have state (constructor, fields, init): scope.eval( """ class I(val x) { var y = x * 2 val z init { z = y + 1 } fun foo() = x + y + z } class Impl : I(10) val impl = Impl() assertEquals(10, impl.x) assertEquals(20, impl.y) assertEquals(21, impl.z) assertEquals(51, impl.foo()) """.trimIndent() ) // 4. closed members cannot be overridden: scope.eval( """ class G { closed fun locked() = "locked" closed val permanent = 42 } """.trimIndent() ) assertFails { scope.eval("class H : G() { override fun locked() = \"free\" }") } assertFails { scope.eval("class H : G() { override val permanent = 0 }") } // Even without override keyword, it should fail if it's closed: assertFails { scope.eval("class H : G() { fun locked() = \"free\" }") } // 5. Visibility widening is allowed, narrowing is forbidden: scope.eval( """ class BaseVis { protected fun prot() = 1 } class Widened : BaseVis() { override fun prot() = 2 // Widened to public (default) } assertEquals(2, Widened().prot()) class BasePub { fun pub() = 1 } """.trimIndent() ) // Narrowing: assertFails { scope.eval("class Narrowed : BasePub() { override protected fun pub() = 2 }") } assertFails { scope.eval("class Narrowed : BasePub() { override private fun pub() = 2 }") } } @Test fun testInterfaceImplementationByParts() = runTest { val scope = Script.newScope() scope.eval( """ // Interface with state (id) and abstract requirements interface Character { abstract val id var health var mana abstract fun getName() fun isAlive() = health > 0 fun status() = getName() + " (#" + id + "): " + health + " HP, " + mana + " MP" // name is also abstractly required by the status method, // even if not explicitly marked 'abstract val' here, // it will be looked up in MRO } class Warrior(id0, health0, mana0) : Character { override val id = id0 override var health = health0 override var mana = mana0 override fun getName() = "Hero" } val w = Warrior(1, 100, 50) assertEquals(100, w.health) assertEquals(50, w.mana) assertEquals(1, w.id) assert(w.isAlive()) assertEquals("Hero (#1): 100 HP, 50 MP", w.status()) w.health = 0 assert(!w.isAlive()) """.trimIndent() ) } @Test fun testBasicObjectExpression() = runTest { eval(""" val x = object { val y = 1 } assertEquals(1, x.y) class Base(v) { val value = v fun squares() = value * value } val y = object : Base(2) { override val value = 5 } assertEquals(25, y.squares()) """.trimIndent()) } @Test fun testArgsPriority() = runTest { eval(""" class A(id) { var stored = null // Arguments should have priority on // instance fields fun setStored(id) { stored = id } } val a = A(1) assertEquals(1, a.id) assertEquals(null, a.stored) // Check that arguments of the call have the priority: a.setStored(2) assertEquals(1, a.id) assertEquals(2, a.stored) """.trimIndent()) } /** * Demonstrates that function parameters are shadowed by class methods of the same name * when accessed within a block, but not in a single expression. */ @Test fun testParameterShadowingConflict() = runTest { val scope = Script.newScope() val result = scope.eval(""" class Tester() { fun id() { "method" } // This correctly returns "success" fun checkOk(id) = id // This incorrectly returns the 'id' method (a Callable) instead of "success" fun checkFail(id) { id } } val t = Tester() if (t.checkOk("success") != "success") throw "checkOk failed" t.checkFail("success") """.trimIndent().toSource("repro")) assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block") } @Test fun testOverrideVisibilityRules1() = runTest { val scope = Script.newScope() scope.eval(""" interface Base { abstract protected fun foo() fun bar() { // it must see foo() as it is protected and // is declared here (even as abstract): foo() } } class Derived : Base { protected val suffix = "!" private fun fooPrivateImpl() = "bar" override protected fun foo() { // it should access own private and all protected memberes here: fooPrivateImpl() + suffix } } class Derived2: Base { private var value = 42 private fun fooPrivateImpl() = value override protected fun foo() { fooPrivateImpl() value++ } } val d: Derived = Derived() assertEquals("bar!", d.bar()) val d2: Derived2 = Derived2() assertEquals(42, d2.bar()) assertEquals(43, d2.bar()) """.trimIndent()) scope.createChildScope().eval(""" val d: Derived = Derived() assertEquals("bar!", d.bar()) val d2: Derived2 = Derived2() assertEquals(42, d2.bar()) """.trimIndent()) } @Test fun testOverrideVisibilityRules2() = runTest { val scope = Script.newScope() scope.eval(""" interface Base { abstract fun foo() fun bar() { // it must see foo() as it is protected and // is declared here (even as abstract): foo() } } class Derived : Base { protected val suffix = "!" private fun fooPrivateImpl() = "bar" override fun foo() { // it should access own private and all protected memberes here: fooPrivateImpl() + suffix } } class Derived2: Base { private var value = 42 private fun fooPrivateImpl() = value override fun foo() { fooPrivateImpl() value++ } } val d: Derived = Derived() assertEquals("bar!", (d as Derived).bar()) class Holder { val d2: Derived2 = Derived2() fun callBar() = (d2 as Derived2).bar() } val holder: Holder = Holder() assertEquals(42, (holder as Holder).callBar()) assertEquals(43, (holder as Holder).callBar()) """.trimIndent()) } @Test fun testToStringWithTransients() = runTest { eval(""" class C(val amount,@Transient var transient=0) { val l by lazy { transient + amount } fun lock(): C { if( transient < 10 ) return C(amount).also { it.transient = transient + 10 } else return this } } println(C(1)) val c1: C = C(1).lock() as C val c1b: C = c1.lock() as C val c2: C = c1b.lock() as C println(c1.amount) println(c2.amount) """.trimIndent()) } @Test fun testToStringWithTransient() = runTest { eval(""" class C(val amount,@Transient var transient=0) { val l by lazy { transient + amount } fun lock(): C { if( transient < 10 ) return C(amount).also { it.transient = transient + 10 } else return this } } println(C(1)) val c1: C = C(1).lock() as C val c1b: C = c1.lock() as C val c2: C = c1b.lock() as C println(c1.amount) println(c2.amount) """.trimIndent()) } @Test fun testToJsonString() = runTest { eval(""" class T(a,b,@Transient c=0) assertEquals("{\"a\":\"foo\",\"b\":\"bar\"}",T("foo", "bar").toJsonString()) """.trimIndent()) } @Test fun testAssignToUnqualifiedParams() = runTest { eval(""" class T(x) { fun setx(v) { x = v } fun incr(v) { x += v } } val t = T(1) t.setx(2) assertEquals(2, t.x) t.incr(3) assertEquals(5, t.x) """.trimIndent()) } }