From 217787e17a0d54788b5545c3e7eedd0d30bfd76a Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 27 Mar 2026 18:46:53 +0300 Subject: [PATCH] Temporary lazy builtin baseline --- .../kotlin/net/sergeych/lyng/Compiler.kt | 16 +++++- .../kotlin/net/sergeych/lyng/Script.kt | 6 ++- .../net/sergeych/lyng/VarDeclStatement.kt | 1 + .../lyng/bytecode/BytecodeCompiler.kt | 32 ++++++++---- .../lyng/bytecode/BytecodeStatement.kt | 3 ++ .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 26 +++++++--- .../net/sergeych/lyng/obj/ObjLazyDelegate.kt | 22 ++++++-- .../GlobalPropertyCaptureRegressionTest.kt | 52 +++++++++++++++++++ lynglib/stdlib/lyng/root.lyng | 16 +++--- 9 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index eaf3e71..3226175 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -184,6 +184,7 @@ class Compiler( private val encodedPayloadTypeByName: MutableMap = mutableMapOf() private val objectDeclNames: MutableSet = mutableSetOf() private val externCallableNames: MutableSet = mutableSetOf() + private val externBindingNames: MutableSet = mutableSetOf() private val moduleDeclaredNames: MutableSet = mutableSetOf() private var seedingSlotPlan: Boolean = false @@ -192,6 +193,7 @@ class Compiler( if (plan.slots.isEmpty()) return emptyMap() val result = LinkedHashMap(plan.slots.size) for ((name, entry) in plan.slots) { + if (externBindingNames.contains(name)) continue result[name] = ForcedLocalSlotInfo( index = entry.index, isMutable = entry.isMutable, @@ -885,6 +887,7 @@ class Compiler( name = declaredName, isMutable = false, visibility = Visibility.Public, + actualExtern = false, initializer = initStmt, isTransient = false, typeDecl = null, @@ -1751,6 +1754,7 @@ class Compiler( enumEntriesByName = enumEntriesByName, callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByName = callableReturnTypeByName, + externBindingNames = externBindingNames, lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) as BytecodeStatement unwrapped to bytecodeStmt.bytecodeFunction() @@ -1957,6 +1961,9 @@ class Compiler( val scopeIndex = slotPlanStack.indexOfLast { it.id == slotLoc.scopeId } if (functionIndex >= 0 && scopeIndex >= functionIndex) return null val modulePlan = moduleSlotPlan() + if (modulePlan != null && slotLoc.scopeId == modulePlan.id && externBindingNames.contains(name)) { + return null + } if (useScopeSlots && modulePlan != null && slotLoc.scopeId == modulePlan.id) { return null } @@ -2075,6 +2082,7 @@ class Compiler( callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByName = callableReturnTypeByName, externCallableNames = externCallableNames, + externBindingNames = externBindingNames, lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) } @@ -2105,6 +2113,7 @@ class Compiler( callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByName = callableReturnTypeByName, externCallableNames = externCallableNames, + externBindingNames = externBindingNames, lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) } @@ -2160,6 +2169,7 @@ class Compiler( callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByName = callableReturnTypeByName, externCallableNames = externCallableNames, + externBindingNames = externBindingNames, lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) } @@ -2343,6 +2353,7 @@ class Compiler( stmt.name, stmt.isMutable, stmt.visibility, + stmt.actualExtern, init, stmt.isTransient, stmt.typeDecl, @@ -8893,7 +8904,7 @@ class Compiler( val effectiveEqToken = if (isProperty) null else eqToken // Register the local name at compile time so that subsequent identifiers can be emitted as fast locals - if (!isStatic && declaringClassNameCaptured == null) declareLocalName(name, isMutable) + if (!isStatic && declaringClassNameCaptured == null && !actualExtern) declareLocalName(name, isMutable) val declKind = if (codeContexts.lastOrNull() is CodeContext.ClassBody) { SymbolKind.MEMBER } else { @@ -8902,6 +8913,8 @@ class Compiler( resolutionSink?.declareSymbol(name, declKind, isMutable, nameStartPos, isOverride = isOverride) if (declKind == SymbolKind.MEMBER && extTypeName == null) { (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.declaredMembers?.add(name) + } else if (actualExtern) { + externBindingNames.add(name) } val isDelegate = if (isAbstract || actualExtern) { @@ -9059,6 +9072,7 @@ class Compiler( name, isMutable, visibility, + actualExtern, initialExpression, isTransient, declaredType, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index d880117..c4dbec6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -542,7 +542,11 @@ class Script( } addFn("lazy") { val builder = requireOnlyArg() - ObjLazyDelegate(builder, requireScope()) + ObjLazyDelegate(builder, requireScope().snapshotForClosure()) + } + addFn("__builtinLazy") { + val builder = requireOnlyArg() + ObjLazyDelegate(builder, requireScope().snapshotForClosure()) } addVoidFn("delay") { val a = args.firstAndOnly() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt index 26dc457..eeb57e0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/VarDeclStatement.kt @@ -24,6 +24,7 @@ class VarDeclStatement( val name: String, val isMutable: Boolean, val visibility: Visibility, + val actualExtern: Boolean, val initializer: Statement?, val isTransient: Boolean, val typeDecl: TypeDecl?, 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 9358fd2..85ee689 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -42,6 +42,7 @@ class BytecodeCompiler( private val callableReturnTypeByScopeId: Map> = emptyMap(), private val callableReturnTypeByName: Map = emptyMap(), private val externCallableNames: Set = emptySet(), + private val externBindingNames: Set = emptySet(), private val lambdaCaptureEntriesByRef: Map> = emptyMap(), ) { private val useScopeSlots: Boolean = allowedScopeNames != null || scopeSlotNameSet != null @@ -4508,7 +4509,7 @@ class BytecodeCompiler( val resolved = slotTypes[slot] ?: SlotType.UNKNOWN return CompiledValue(slot, resolved) } - if (useScopeSlots && allowedScopeNames?.contains(name) == true) { + if (useScopeSlots && isPreparedScopeName(name)) { scopeSlotIndexByName[name]?.let { slot -> val resolved = slotTypes[slot] ?: SlotType.UNKNOWN return CompiledValue(slot, resolved) @@ -8073,7 +8074,7 @@ class BytecodeCompiler( slotInitClassByKey[ScopeSlotKey(scopeId, slotIndex)] = cls } } - if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId)) { + if (allowLocalSlots && slotIndex != null && (stmt.actualExtern || !shouldUseScopeSlotFor(scopeId, stmt.name, isDelegated = false))) { val key = ScopeSlotKey(scopeId, slotIndex) declaredLocalKeys.add(key) if (!localSlotInfoMap.containsKey(key)) { @@ -8100,7 +8101,7 @@ class BytecodeCompiler( val scopeId = stmt.spec.scopeId ?: 0 if (slotIndex != null) { val key = ScopeSlotKey(scopeId, slotIndex) - if (allowLocalSlots && !shouldUseScopeSlotFor(scopeId)) { + if (allowLocalSlots && !shouldUseScopeSlotFor(scopeId, stmt.spec.name, isDelegated = false)) { if (!localSlotInfoMap.containsKey(key)) { localSlotInfoMap[key] = LocalSlotInfo(stmt.spec.name, isMutable = false, isDelegated = false) } @@ -8117,7 +8118,7 @@ class BytecodeCompiler( is DelegatedVarDeclStatement -> { val slotIndex = stmt.slotIndex val scopeId = stmt.scopeId ?: 0 - if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId)) { + if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId, stmt.name, isDelegated = true)) { val key = ScopeSlotKey(scopeId, slotIndex) declaredLocalKeys.add(key) if (!localSlotInfoMap.containsKey(key)) { @@ -8283,15 +8284,26 @@ class BytecodeCompiler( private fun isModuleSlot(scopeId: Int, name: String?): Boolean { if (moduleScopeId != null && scopeId != moduleScopeId) return false - val scopeNames = allowedScopeNames ?: scopeSlotNameSet - if (scopeNames == null || name == null) return false - return scopeNames.contains(name) + return isPreparedScopeName(name) } private fun shouldUseScopeSlotFor(scopeId: Int): Boolean { return useScopeSlots && moduleScopeId != null && scopeId == moduleScopeId } + private fun shouldUseScopeSlotFor(scopeId: Int, name: String, isDelegated: Boolean): Boolean { + if (moduleScopeId == null || scopeId != moduleScopeId) return false + if (isDelegated) return false + if (externBindingNames.contains(name)) return true + return useScopeSlots && isPreparedScopeName(name) + } + + private fun isPreparedScopeName(name: String?): Boolean { + if (name == null) return false + if (scopeSlotNameSet?.contains(name) == true) return true + return allowedScopeNames?.contains(name) == true + } + private fun collectLoopVarNames(stmt: Statement) { if (stmt is BytecodeStatement) { collectLoopVarNames(stmt.original) @@ -8400,8 +8412,9 @@ class BytecodeCompiler( captureSlotKeys.add(key) return } + val forceScopeSlot = shouldUseScopeSlotFor(scopeId, ref.name, ref.isDelegated) val isModuleSlot = if (ref.isDelegated) false else isModuleSlot(scopeId, ref.name) - if (allowLocalSlots && !isModuleSlot) { + if (allowLocalSlots && !isModuleSlot && !forceScopeSlot) { if (!localSlotInfoMap.containsKey(key)) { localSlotInfoMap[key] = LocalSlotInfo(ref.name, ref.isMutable, ref.isDelegated) } @@ -8448,8 +8461,9 @@ class BytecodeCompiler( } captureSlotKeys.add(key) } else { + val forceScopeSlot = shouldUseScopeSlotFor(scopeId, target.name, target.isDelegated) val isModuleSlot = if (target.isDelegated) false else isModuleSlot(scopeId, target.name) - if (allowLocalSlots && !isModuleSlot) { + if (allowLocalSlots && !isModuleSlot && !forceScopeSlot) { if (!localSlotInfoMap.containsKey(key)) { localSlotInfoMap[key] = LocalSlotInfo(target.name, target.isMutable, target.isDelegated) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index b9454d4..ee71e68 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -87,6 +87,7 @@ class BytecodeStatement private constructor( callableReturnTypeByScopeId: Map> = emptyMap(), callableReturnTypeByName: Map = emptyMap(), externCallableNames: Set = emptySet(), + externBindingNames: Set = emptySet(), lambdaCaptureEntriesByRef: Map> = emptyMap(), slotTypeDeclByScopeId: Map> = emptyMap(), ): Statement { @@ -122,6 +123,7 @@ class BytecodeStatement private constructor( callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByName = callableReturnTypeByName, externCallableNames = externCallableNames, + externBindingNames = externBindingNames, lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) val compiled = compiler.compileStatement(nameHint, statement) @@ -236,6 +238,7 @@ class BytecodeStatement private constructor( stmt.name, stmt.isMutable, stmt.visibility, + stmt.actualExtern, stmt.initializer?.let { unwrapDeep(it) }, stmt.isTransient, stmt.typeDecl, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index cf8ac18..e2916e0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -2392,8 +2392,13 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() { ?: error("DECL_LOCAL expects LocalDecl at $constId") if (slot < frame.fn.scopeSlotCount) { val target = frame.scopeTarget(slot) - frame.ensureScopeSlot(target, slot) - val value = frame.slotToObj(slot).byValueCopy() + val index = frame.ensureScopeSlot(target, slot) + val raw = target.getSlotRecord(index).value + val value = when (raw) { + is FrameSlotRef -> raw.read() + is RecordSlotRef -> raw.read() + else -> raw + }.byValueCopy() target.updateSlotFor( decl.name, ObjRecord( @@ -4557,7 +4562,7 @@ class CmdFrame( return getScopeSlotValueAtAddr(addrSlot) } - fun setAddrObj(addrSlot: Int, value: Obj) { + suspend fun setAddrObj(addrSlot: Int, value: Obj) { setScopeSlotValueAtAddr(addrSlot, value) } @@ -4565,7 +4570,7 @@ class CmdFrame( return getScopeSlotValueAtAddr(addrSlot).toLong() } - fun setAddrInt(addrSlot: Int, value: Long) { + suspend fun setAddrInt(addrSlot: Int, value: Long) { setScopeSlotValueAtAddr(addrSlot, ObjInt.of(value)) } @@ -4573,7 +4578,7 @@ class CmdFrame( return getScopeSlotValueAtAddr(addrSlot).toDouble() } - fun setAddrReal(addrSlot: Int, value: Double) { + suspend fun setAddrReal(addrSlot: Int, value: Double) { setScopeSlotValueAtAddr(addrSlot, ObjReal.of(value)) } @@ -4581,7 +4586,7 @@ class CmdFrame( return getScopeSlotValueAtAddr(addrSlot).toBool() } - fun setAddrBool(addrSlot: Int, value: Boolean) { + suspend fun setAddrBool(addrSlot: Int, value: Boolean) { setScopeSlotValueAtAddr(addrSlot, if (value) ObjTrue else ObjFalse) } @@ -4870,9 +4875,16 @@ class CmdFrame( return resolved.value } - private fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) { + private suspend fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) { val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved") val index = addrIndices[addrSlot] + val record = target.getSlotRecord(index) + val slotId = addrScopeSlots[addrSlot] + val name = fn.scopeSlotNames.getOrNull(slotId) + if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) { + target.assign(record, name, value) + return + } target.setSlotValue(index, value) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt index 5dfc865..b3a6d26 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt @@ -18,9 +18,11 @@ 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 @@ -43,13 +45,25 @@ class ObjLazyDelegate( 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 callScope = capturedScope.createChildScope(capturedScope.pos, args = Arguments.EMPTY) - cachedValue = if (builder is Statement) { - executeBytecodeWithSeed(callScope, builder, "lazy delegate") + 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.callOn(callScope) + builder.invoke(callScope, receiver, Arguments.EMPTY) } calculated = true } diff --git a/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt b/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt new file mode 100644 index 0000000..6d0f911 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/GlobalPropertyCaptureRegressionTest.kt @@ -0,0 +1,52 @@ +/* + * 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 + +import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.bridge.bindGlobalVar +import net.sergeych.lyng.bridge.globalBinder +import kotlin.test.Test +import kotlin.test.assertEquals + +class GlobalPropertyCaptureRegressionTest { + @Test + fun externGlobalVarAssignmentInsideFunctionShouldCallBoundSetter() = runTest { + val scope = Script.newScope() + var x = 1.0 + + scope.eval( + """ + extern var X: Real + + fun main() { + X = X + 1.0 + } + """.trimIndent() + ) + + scope.globalBinder().bindGlobalVar( + name = "X", + get = { x }, + set = { x = it } + ) + + scope.eval("main()") + + assertEquals(2.0, x, "bound extern var should stay live inside function bodies") + } +} diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index b36c97e..2c36584 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -415,24 +415,26 @@ 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 creator: ThisRefType.()->T = creatorParam - private var value = Unset + private val delegate: Delegate = __builtinLazy(creatorParam) as Delegate override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object { - if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'" - this + delegate.bind(name, access, thisRef) } override fun getValue(thisRef: ThisRefType, name: String): T { - if (value == Unset) - value = with(thisRef,creator) - value as T + delegate.getValue(thisRef, name) as T + } + + override fun setValue(thisRef: ThisRefType, name: String, newValue: T): void { + delegate.setValue(thisRef, name, newValue) } }