Temporary lazy builtin baseline

This commit is contained in:
Sergey Chernov 2026-03-27 18:46:53 +03:00
parent 9ddc7dbee6
commit 217787e17a
9 changed files with 145 additions and 29 deletions

View File

@ -184,6 +184,7 @@ class Compiler(
private val encodedPayloadTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val objectDeclNames: MutableSet<String> = mutableSetOf()
private val externCallableNames: MutableSet<String> = mutableSetOf()
private val externBindingNames: MutableSet<String> = mutableSetOf()
private val moduleDeclaredNames: MutableSet<String> = mutableSetOf()
private var seedingSlotPlan: Boolean = false
@ -192,6 +193,7 @@ class Compiler(
if (plan.slots.isEmpty()) return emptyMap()
val result = LinkedHashMap<String, ForcedLocalSlotInfo>(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,

View File

@ -542,7 +542,11 @@ class Script(
}
addFn("lazy") {
val builder = requireOnlyArg<Obj>()
ObjLazyDelegate(builder, requireScope())
ObjLazyDelegate(builder, requireScope().snapshotForClosure())
}
addFn("__builtinLazy") {
val builder = requireOnlyArg<Obj>()
ObjLazyDelegate(builder, requireScope().snapshotForClosure())
}
addVoidFn("delay") {
val a = args.firstAndOnly()

View File

@ -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?,

View File

@ -42,6 +42,7 @@ class BytecodeCompiler(
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
private val externCallableNames: Set<String> = emptySet(),
private val externBindingNames: Set<String> = emptySet(),
private val lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = 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)
}

View File

@ -87,6 +87,7 @@ class BytecodeStatement private constructor(
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
externCallableNames: Set<String> = emptySet(),
externBindingNames: Set<String> = emptySet(),
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
slotTypeDeclByScopeId: Map<Int, Map<Int, TypeDecl>> = 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,

View File

@ -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)
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -415,24 +415,26 @@ 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 creator: ThisRefType.()->T = creatorParam
private var value = Unset
private val delegate: Delegate<T,ThisRefType> = __builtinLazy(creatorParam) as Delegate<T,ThisRefType>
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)
}
}