When createField() was called with a pre-assigned methodId, the ID was used but methodIdMap was not updated and nextMethodId was not advanced. This caused assignMethodId() for static methods to reuse slot IDs already occupied by instance methods. In complex.lyng, fromInt/imaginary got IDs 12/14 (same as plus/mul), causing binary operator dispatch via CALL_MEMBER_SLOT to call the wrong function (e.g. a*b would invoke imaginary instead of mul). Fix: after computing effectiveMethodId in createField, always register it in methodIdMap and advance nextMethodId past it so subsequent auto-assignments start from a clean range. Also pre-assigns method IDs for non-static fun/fn declarations during class body pre-scan so forward references resolve correctly in class bodies, and adds ComplexModuleTest coverage for operator slot dispatch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1221 lines
38 KiB
Kotlin
1221 lines
38 KiB
Kotlin
/*
|
|
* 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.EvalSession
|
|
import net.sergeych.lyng.ModuleScope
|
|
import net.sergeych.lyng.Script
|
|
import net.sergeych.lyng.bridge.bindObject
|
|
import net.sergeych.lyng.bridge.data
|
|
import net.sergeych.lyng.eval
|
|
import net.sergeych.lyng.obj.ObjBool
|
|
import net.sergeych.lyng.obj.ObjBuffer
|
|
import net.sergeych.lyng.obj.ObjInt
|
|
import net.sergeych.lyng.obj.ObjInstance
|
|
import net.sergeych.lyng.obj.ObjList
|
|
import net.sergeych.lyng.obj.ObjNull
|
|
import net.sergeych.lyng.obj.ObjString
|
|
import net.sergeych.lyng.obj.ObjVoid
|
|
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 {
|
|
val ms = Script.newScope()
|
|
ms.eval(
|
|
"""
|
|
val accessor: String = dynamic {
|
|
get { name ->
|
|
if( name == "foo" ) "bar" else null
|
|
}
|
|
}
|
|
|
|
println("-- " + accessor.foo)
|
|
assertEquals("bar", accessor.foo)
|
|
assertEquals(null, accessor.bar)
|
|
|
|
""".trimIndent()
|
|
)
|
|
ms.eval("""
|
|
assertEquals("bar", accessor.foo)
|
|
assertEquals(null, accessor.bad)
|
|
""".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 testObjectSingletonSupportsExtensions() = runTest {
|
|
val scope = Script.newScope()
|
|
scope.eval(
|
|
"""
|
|
object X {
|
|
fun base() = "base"
|
|
}
|
|
|
|
fun X.decorate<T>(value: T): String {
|
|
this.base() + ":" + value.toString()
|
|
}
|
|
val X.tag get() = this.base() + ":tag"
|
|
|
|
assertEquals("base", X.base())
|
|
assertEquals("base:42", X.decorate(42))
|
|
assertEquals("base:ok", X.decorate("ok"))
|
|
assertEquals("base:tag", X.tag)
|
|
|
|
// Wrapper names should be generated for singleton-object receivers too.
|
|
assertEquals("base:17", __ext__X__decorate(X, 17))
|
|
assertEquals("base:tag", __ext_get__X__tag(X))
|
|
""".trimIndent()
|
|
)
|
|
}
|
|
|
|
@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())
|
|
}
|
|
@Test
|
|
fun testDynamicToDynamic() = runTest {
|
|
val ms = Script.newScope()
|
|
ms.eval("""
|
|
|
|
class A(prefix) {
|
|
val da = dynamic {
|
|
get { name -> "a:"+prefix+":"+name }
|
|
}
|
|
}
|
|
|
|
val B: A = dynamic {
|
|
get { p -> A(p) }
|
|
}
|
|
assertEquals(A("bar").da.foo, "a:bar:foo")
|
|
assertEquals( B.buzz.da.foo, "a:buzz:foo" )
|
|
|
|
val C = dynamic {
|
|
get { p -> A(p).da }
|
|
}
|
|
|
|
assertEquals(C.buzz.foo, "a:buzz:foo")
|
|
""".trimIndent())
|
|
ms.eval("""
|
|
""")
|
|
}
|
|
@Test
|
|
fun testDynamicToDynamicFun() = runTest {
|
|
val ms = Script.newScope()
|
|
ms.eval("""
|
|
|
|
class A(prefix) {
|
|
val da = dynamic {
|
|
get { name -> { x -> "a:"+prefix+":"+name+"/"+x } }
|
|
}
|
|
}
|
|
|
|
val B: A = dynamic {
|
|
get { p -> A(p) }
|
|
}
|
|
assertEquals(A("bar").da.foo("buzz"), "a:bar:foo/buzz")
|
|
assertEquals( B.buzz.da.foo("42"), "a:buzz:foo/42" )
|
|
|
|
val C = dynamic {
|
|
get { p -> A(p).da }
|
|
}
|
|
|
|
assertEquals(C.buzz.foo("one"), "a:buzz:foo/one")
|
|
""".trimIndent())
|
|
ms.eval("""
|
|
""")
|
|
}
|
|
|
|
@Test
|
|
fun testExtendingObjectWithExternals() = runTest {
|
|
val s = EvalSession()
|
|
s.eval("""
|
|
extern object Storage {
|
|
|
|
extern val spaceUsed: Int
|
|
extern val spaceAvailable: Int
|
|
|
|
/*
|
|
Return packed binary data or null
|
|
*/
|
|
extern fun getPacked(key: String): Buffer?
|
|
|
|
/*
|
|
Upsert packed binary data
|
|
*/
|
|
extern fun putPacked(key: String,value: Buffer)
|
|
|
|
/*
|
|
Delete data.
|
|
@return true if data were actually deleted, false means
|
|
there were no data for the key.
|
|
*/
|
|
extern fun delete(key: String): Bool
|
|
}
|
|
""".trimIndent()
|
|
)
|
|
val scope = s.getScope() as ModuleScope
|
|
scope.bindObject("Storage") {
|
|
init { _ ->
|
|
data = mutableMapOf<String, ObjBuffer>()
|
|
}
|
|
addVal("spaceUsed") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
ObjInt(storage.values.sumOf { it.size }.toLong())
|
|
}
|
|
addVal("spaceAvailable") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val capacity = 1_024
|
|
ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
|
|
}
|
|
addFun("getPacked") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val key = (args.list[0] as ObjString).value
|
|
storage[key] ?: ObjNull
|
|
}
|
|
addFun("putPacked") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val key = (args.list[0] as ObjString).value
|
|
val value = args.list[1] as ObjBuffer
|
|
storage[key] = value
|
|
ObjVoid
|
|
}
|
|
addFun("delete") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val key = (args.list[0] as ObjString).value
|
|
ObjBool(storage.remove(key) != null)
|
|
}
|
|
}
|
|
s.eval("""
|
|
import lyng.serialization
|
|
|
|
// Use names that do not collide with Obj built-ins so extension dispatch is exercised.
|
|
override fun Storage.getAt(key: String): Object? {
|
|
Storage.getPacked(key)?.let {
|
|
Lynon.decode(it.toBitInput())
|
|
}
|
|
}
|
|
|
|
override fun Storage.putAt(key: String, value: Object) {
|
|
Storage.putPacked(key, Lynon.encode(value).toBuffer())
|
|
}
|
|
|
|
assertEquals(0, Storage.spaceUsed)
|
|
assertEquals(1024, Storage.spaceAvailable)
|
|
val missing: String? = Storage["missing"]
|
|
assertEquals(null, missing)
|
|
|
|
Storage["name"] = "alice"
|
|
Storage["count"] = 42
|
|
|
|
val name: String? = Storage["name"]
|
|
val count: Int? = Storage["count"]
|
|
assertEquals("alice", name)
|
|
assertEquals(42, count)
|
|
assert(Storage.spaceUsed > 0)
|
|
assert(Storage.spaceAvailable < 1024)
|
|
|
|
val wrappedName: String? = __ext__Storage__getAt(Storage, "name")
|
|
assertEquals("alice", wrappedName)
|
|
__ext__Storage__putAt(Storage, "flag", true)
|
|
val flag: Bool? = Storage["flag"]
|
|
assertEquals(true, flag)
|
|
|
|
assert(Storage.delete("name"))
|
|
val deletedName: String? = Storage["name"]
|
|
assertEquals(null, deletedName)
|
|
assert(!Storage.delete("name"))
|
|
""".trimIndent())
|
|
}
|
|
|
|
@Test
|
|
fun testExtendingObjectWithExternals2() = runTest {
|
|
val s = EvalSession()
|
|
s.eval("""
|
|
import lyng.serialization
|
|
object Storage {
|
|
extern val spaceUsed: Int
|
|
extern val spaceAvailable: Int
|
|
|
|
/*
|
|
Return packed binary data or null
|
|
*/
|
|
extern fun getPacked(key: String): Buffer?
|
|
|
|
/*
|
|
Upsert packed binary data
|
|
*/
|
|
extern fun putPacked(key: String,value: Buffer)
|
|
|
|
/*
|
|
Delete data.
|
|
@return true if data were actually deleted, false means
|
|
there were no data for the key.
|
|
*/
|
|
extern fun delete(key: String): Bool
|
|
|
|
override fun putAt(key: String,value: Object) {
|
|
putPacked(key, Lynon.encode(value).toBuffer())
|
|
}
|
|
|
|
override fun getAt(key: String): Object? =
|
|
getPacked(key)?.let { Lynon.decode(it.toBitInput()) }
|
|
}
|
|
|
|
""".trimIndent()
|
|
)
|
|
val scope = s.getScope() as ModuleScope
|
|
scope.bindObject("Storage") {
|
|
init { _ ->
|
|
data = mutableMapOf<String, ObjBuffer>()
|
|
}
|
|
addVal("spaceUsed") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
ObjInt(storage.values.sumOf { it.size }.toLong())
|
|
}
|
|
addVal("spaceAvailable") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val capacity = 1_024
|
|
ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
|
|
}
|
|
addFun("getPacked") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val key = (args.list[0] as ObjString).value
|
|
storage[key] ?: ObjNull
|
|
}
|
|
addFun("putPacked") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val key = (args.list[0] as ObjString).value
|
|
val value = args.list[1] as ObjBuffer
|
|
storage[key] = value
|
|
ObjVoid
|
|
}
|
|
addFun("delete") {
|
|
val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
|
|
val key = (args.list[0] as ObjString).value
|
|
ObjBool(storage.remove(key) != null)
|
|
}
|
|
}
|
|
s.eval("""
|
|
assertEquals(0, Storage.spaceUsed)
|
|
assertEquals(1024, Storage.spaceAvailable)
|
|
val missing: String? = Storage["missing"]
|
|
assertEquals(null, missing)
|
|
|
|
Storage["name"] = "alice"
|
|
Storage["count"] = 42
|
|
|
|
val name: String? = Storage["name"]
|
|
val count: Int? = Storage["count"]
|
|
assertEquals("alice", name)
|
|
assertEquals(42, count)
|
|
assert(Storage.spaceUsed > 0)
|
|
assert(Storage.spaceAvailable < 1024)
|
|
|
|
val wrappedName: String? = Storage.getAt("name")
|
|
assertEquals("alice", wrappedName)
|
|
Storage.putAt("flag", true)
|
|
val flag: Bool? = Storage["flag"]
|
|
assertEquals(true, flag)
|
|
|
|
assert(Storage.delete("name"))
|
|
val deletedName: String? = Storage["name"]
|
|
assertEquals(null, deletedName)
|
|
assert(!Storage.delete("name"))
|
|
""".trimIndent())
|
|
}
|
|
|
|
@Test
|
|
fun testForwardSymbolsUsageMustBeAllowed() = runTest {
|
|
eval("""
|
|
class Foo(x) {
|
|
fn fn2() {
|
|
fn1()
|
|
println("fn2")
|
|
}
|
|
fn fn1() {
|
|
println("fn1")
|
|
}
|
|
}
|
|
|
|
val foo = Foo(33)
|
|
foo.fn2()
|
|
""".trimIndent())
|
|
}
|
|
|
|
}
|