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

View File

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

View File

@ -1089,7 +1089,6 @@ class BytecodeCompiler(
private fun isDelegateClass(receiverClass: ObjClass): Boolean =
receiverClass.className == "Delegate" ||
receiverClass.className == "LazyDelegate" ||
receiverClass.implementingNames.contains("Delegate")
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)
}
@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
fun testLazyIsDelegate() = runTest {
eval("""

View File

@ -415,26 +415,24 @@ fun with<T,R>(self: T, block: T.()->R): R {
block(self)
}
extern fun __builtinLazy(creator: Object): Object
/*
Standard implementation of a lazy-initialized property delegate.
The provided creator lambda is called once on the first access to compute the value.
Can only be used with 'val' properties.
*/
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 {
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 {
delegate.getValue(thisRef, name) as T
}
override fun setValue(thisRef: ThisRefType, name: String, newValue: T): void {
delegate.setValue(thisRef, name, newValue)
if (value == Unset)
value = with(thisRef, creator)
value as T
}
}