From 7c059b4741750b139d6b51b8ed121c12c0aaeda1 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 27 Mar 2026 18:57:27 +0300 Subject: [PATCH] Fix lazy delegate type resolution across modules --- .../kotlin/net/sergeych/lyng/Compiler.kt | 10 +- .../kotlin/net/sergeych/lyng/Script.kt | 8 -- .../lyng/bytecode/BytecodeCompiler.kt | 1 - .../net/sergeych/lyng/obj/ObjLazyDelegate.kt | 100 ------------------ .../net/sergeych/lyng/DelegationTest.kt | 90 ++++++++++++++++ lynglib/stdlib/lyng/root.lyng | 16 ++- 6 files changed, 103 insertions(+), 122 deletions(-) delete mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 3226175..faa8fdf 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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") } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index c4dbec6..2a414db 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -540,14 +540,6 @@ class Script( cachedValue } } - addFn("lazy") { - val builder = requireOnlyArg() - ObjLazyDelegate(builder, requireScope().snapshotForClosure()) - } - addFn("__builtinLazy") { - val builder = requireOnlyArg() - ObjLazyDelegate(builder, requireScope().snapshotForClosure()) - } addVoidFn("delay") { val a = args.firstAndOnly() when (a) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 85ee689..2d302da 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt deleted file mode 100644 index b3a6d26..0000000 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt +++ /dev/null @@ -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 - ) - } - } -} diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt index 269a603..f5f1b25 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/DelegationTest.kt @@ -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(creatorParam: ThisRefType.()->T) : Delegate { + 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(creator: ThisRefType.()->T): Delegate = 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(creatorParam: ThisRefType.()->T) : Delegate { + 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(""" diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 2c36584..2bcf039 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -415,26 +415,24 @@ fun with(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(creatorParam: ThisRefType.()->T) : Delegate { - private val delegate: Delegate = __builtinLazy(creatorParam) as Delegate + 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 } }