Fix lazy delegate type resolution across modules

This commit is contained in:
Sergey Chernov 2026-03-27 18:57:27 +03:00
parent 217787e17a
commit 7c059b4741
6 changed files with 103 additions and 122 deletions

View File

@ -8401,7 +8401,7 @@ class Compiler(
is ImplicitThisMethodCallRef -> { is ImplicitThisMethodCallRef -> {
when (directRef.methodName()) { when (directRef.methodName()) {
"iterator" -> ObjIterator "iterator" -> ObjIterator
"lazy" -> ObjLazyDelegate.type "lazy" -> resolveClassByName("lazy") ?: inferMethodCallReturnClass(directRef.methodName())
else -> inferMethodCallReturnClass(directRef.methodName()) else -> inferMethodCallReturnClass(directRef.methodName())
} }
} }
@ -8420,7 +8420,7 @@ class Compiler(
when { when {
target is LocalSlotRef -> { target is LocalSlotRef -> {
when (target.name) { when (target.name) {
"lazy" -> ObjLazyDelegate.type "lazy" -> resolveClassByName("lazy")
"iterator" -> ObjIterator "iterator" -> ObjIterator
"flow" -> ObjFlow.type "flow" -> ObjFlow.type
"launch" -> ObjDeferred.type "launch" -> ObjDeferred.type
@ -8431,7 +8431,7 @@ class Compiler(
} }
target is LocalVarRef -> { target is LocalVarRef -> {
when (target.name) { when (target.name) {
"lazy" -> ObjLazyDelegate.type "lazy" -> resolveClassByName("lazy")
"iterator" -> ObjIterator "iterator" -> ObjIterator
"flow" -> ObjFlow.type "flow" -> ObjFlow.type
"launch" -> ObjDeferred.type "launch" -> ObjDeferred.type
@ -8470,7 +8470,9 @@ class Compiler(
?: unwrapDirectRef(initializer)?.let { inferObjClassFromRef(it) } ?: unwrapDirectRef(initializer)?.let { inferObjClassFromRef(it) }
?: throw ScriptError(initializer.pos, "Delegate type must be known at compile time") ?: throw ScriptError(initializer.pos, "Delegate type must be known at compile time")
if (initClass !== delegateClass && if (initClass !== delegateClass &&
initClass.className != delegateClass.className &&
!initClass.allParentsSet.contains(delegateClass) && !initClass.allParentsSet.contains(delegateClass) &&
!initClass.allParentsSet.any { it.className == delegateClass.className } &&
!initClass.allImplementingNames.contains(delegateClass.className) !initClass.allImplementingNames.contains(delegateClass.className)
) { ) {
throw ScriptError( throw ScriptError(
@ -8980,7 +8982,7 @@ class Compiler(
if (isDelegate && initialExpression != null) { if (isDelegate && initialExpression != null) {
ensureDelegateType(initialExpression) ensureDelegateType(initialExpression)
if (isMutable && resolveInitializerObjClass(initialExpression) == ObjLazyDelegate.type) { if (isMutable && resolveInitializerObjClass(initialExpression)?.className == "lazy") {
throw ScriptError(initialExpression.pos, "lazy delegate is read-only") throw ScriptError(initialExpression.pos, "lazy delegate is read-only")
} }
} }

View File

@ -540,14 +540,6 @@ class Script(
cachedValue cachedValue
} }
} }
addFn("lazy") {
val builder = requireOnlyArg<Obj>()
ObjLazyDelegate(builder, requireScope().snapshotForClosure())
}
addFn("__builtinLazy") {
val builder = requireOnlyArg<Obj>()
ObjLazyDelegate(builder, requireScope().snapshotForClosure())
}
addVoidFn("delay") { addVoidFn("delay") {
val a = args.firstAndOnly() val a = args.firstAndOnly()
when (a) { when (a) {

View File

@ -1089,7 +1089,6 @@ class BytecodeCompiler(
private fun isDelegateClass(receiverClass: ObjClass): Boolean = private fun isDelegateClass(receiverClass: ObjClass): Boolean =
receiverClass.className == "Delegate" || receiverClass.className == "Delegate" ||
receiverClass.className == "LazyDelegate" ||
receiverClass.implementingNames.contains("Delegate") receiverClass.implementingNames.contains("Delegate")
private fun operatorMemberName(op: BinOp): String? = when (op) { private fun operatorMemberName(op: BinOp): String? = when (op) {

View File

@ -1,100 +0,0 @@
/*
* 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.
*
*/
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.BytecodeBodyProvider
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
import net.sergeych.lyng.bytecode.BytecodeStatement
import net.sergeych.lyng.Visibility
import net.sergeych.lyng.executeBytecodeWithSeed
/**
* Lazy delegate used by `val x by lazy { ... }`.
*/
class ObjLazyDelegate(
private val builder: Obj,
private val capturedScope: Scope,
) : Obj() {
override val objClass: ObjClass = type
private var calculated = false
private var cachedValue: Obj = ObjVoid
override suspend fun invokeInstanceMethod(
scope: Scope,
name: String,
args: Arguments,
onNotFoundResult: (suspend () -> Obj?)?,
): Obj {
return when (name) {
"bind" -> {
val access = args.getOrNull(1)?.toString() ?: ""
if (!access.endsWith("Val")) {
scope.raiseIllegalArgument("lazy delegate can only be used with 'val'")
}
this
}
"getValue" -> {
if (!calculated) {
val receiver = args.getOrNull(0) ?: ObjNull
val callScope = scope.createChildScope(
scope.pos,
args = Arguments.EMPTY,
newThisObj = receiver
)
cachedValue = if (builder is BytecodeStatement || builder is BytecodeBodyProvider) {
executeBytecodeWithSeed(callScope, builder as Statement, "lazy delegate")
} else {
builder.invoke(callScope, receiver, Arguments.EMPTY)
}
calculated = true
}
cachedValue
}
"setValue" -> scope.raiseIllegalAssignment("lazy delegate is read-only")
else -> super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
}
companion object {
val type = ObjClass("LazyDelegate").apply {
implementingNames.add("Delegate")
createField(
"getValue",
ObjNull,
isMutable = false,
visibility = Visibility.Public,
pos = Pos.builtIn,
declaringClass = this,
type = ObjRecord.Type.Fun
)
createField(
"setValue",
ObjNull,
isMutable = false,
visibility = Visibility.Public,
pos = Pos.builtIn,
declaringClass = this,
type = ObjRecord.Type.Fun
)
}
}
}

View File

@ -212,6 +212,96 @@ class DelegationTest {
assertTrue(badThrown) assertTrue(badThrown)
} }
@Test
fun testPureLyngLazyPreservesReceiverAndClosure() = runTest {
val scope = Script.newScope()
scope.eval(
"""
val GLOBAL_NUMBERS = [1,2,3]
class PureLazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
private val creator: ThisRefType.()->T = creatorParam
private var value = Unset
override fun bind(name: String, access, thisRef: ThisRefType): Object = this
override fun getValue(thisRef: ThisRefType, name: String): T {
if (value == Unset)
value = creator(thisRef)
value as T
}
}
fun pureLazy<T,ThisRefType=Object>(creator: ThisRefType.()->T): Delegate<T,ThisRefType> = PureLazy(creator)
class A {
val numbers = [1,2,3]
val fromThis: List by pureLazy { this.numbers }
val fromScope: List by pureLazy { GLOBAL_NUMBERS }
}
class B {
val a: A by pureLazy { A() }
val test: List by pureLazy { (a as A).fromThis + [4] }
}
assertEquals([1,2,3], A().fromThis)
assertEquals([1,2,3], A().fromScope)
assertEquals([1,2,3,4], B().test)
""".trimIndent()
)
}
@Test
fun testImportedPureLyngLazyPreservesReceiverAndClosure() = runTest {
val scope = Script.newScope()
scope.importManager.addTextPackages(
"""
package repro.lazy
import lyng.stdlib
class PureLazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
private val creator: ThisRefType.()->T = creatorParam
private var value = Unset
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
this
}
override fun getValue(thisRef: ThisRefType, name: String): T {
if (value == Unset)
value = with(thisRef, creator)
value as T
}
}
""".trimIndent()
)
scope.eval(
"""
import repro.lazy
val GLOBAL_NUMBERS = [1,2,3]
class A {
val numbers = [1,2,3]
val fromThis: List by PureLazy { this.numbers }
val fromScope: List by PureLazy { GLOBAL_NUMBERS }
}
class B {
val a: A by PureLazy { A() }
val test: List by PureLazy { (a as A).fromThis + [4] }
}
assertEquals([1,2,3], A().fromThis)
assertEquals([1,2,3], A().fromScope)
assertEquals([1,2,3,4], B().test)
""".trimIndent()
)
}
@Test @Test
fun testLazyIsDelegate() = runTest { fun testLazyIsDelegate() = runTest {
eval(""" eval("""

View File

@ -415,26 +415,24 @@ fun with<T,R>(self: T, block: T.()->R): R {
block(self) block(self)
} }
extern fun __builtinLazy(creator: Object): Object
/* /*
Standard implementation of a lazy-initialized property delegate. Standard implementation of a lazy-initialized property delegate.
The provided creator lambda is called once on the first access to compute the value. The provided creator lambda is called once on the first access to compute the value.
Can only be used with 'val' properties. Can only be used with 'val' properties.
*/ */
class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> { class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
private val delegate: Delegate<T,ThisRefType> = __builtinLazy(creatorParam) as Delegate<T,ThisRefType> private val creator: ThisRefType.()->T = creatorParam
private var value = Unset
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object { override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
delegate.bind(name, access, thisRef) if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
this
} }
override fun getValue(thisRef: ThisRefType, name: String): T { override fun getValue(thisRef: ThisRefType, name: String): T {
delegate.getValue(thisRef, name) as T if (value == Unset)
} value = with(thisRef, creator)
value as T
override fun setValue(thisRef: ThisRefType, name: String, newValue: T): void {
delegate.setValue(thisRef, name, newValue)
} }
} }