From 274abaaf03339f3f4d17c2cb55ccb9525d9fbdac Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 20 Feb 2026 22:55:45 +0300 Subject: [PATCH] Refactor scope handling to improve slot synchronization, dynamic access, and module frame interactions; bump version to 1.5.1-SNAPSHOT. --- lynglib/build.gradle.kts | 2 +- .../kotlin/net/sergeych/lyng/ClosureScope.kt | 27 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 153 ++++++++-- .../kotlin/net/sergeych/lyng/FrameAccess.kt | 87 +++++- .../kotlin/net/sergeych/lyng/ModuleScope.kt | 21 +- .../kotlin/net/sergeych/lyng/Scope.kt | 52 +++- .../kotlin/net/sergeych/lyng/Script.kt | 54 +++- .../lyng/bytecode/BytecodeCompiler.kt | 37 ++- .../sergeych/lyng/bytecode/BytecodeFrame.kt | 11 + .../lyng/bytecode/BytecodeStatement.kt | 32 +- .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 276 +++++++++++++----- .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 19 +- .../net/sergeych/lyng/obj/ObjDynamic.kt | 5 +- .../kotlin/net/sergeych/lyng/obj/ObjRegex.kt | 18 +- .../sergeych/lyng/pacman/ImportProvider.kt | 3 +- .../commonTest/kotlin/BridgeBindingTest.kt | 46 ++- 16 files changed, 690 insertions(+), 153 deletions(-) diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index daf1234..b5eaecb 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.5.0-SNAPSHOT" +version = "1.5.1-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt index b9d80a5..a4ed956 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt @@ -36,10 +36,19 @@ class BytecodeClosureScope( val desired = preferredThisType?.let { typeName -> callScope.thisVariants.firstOrNull { it.objClass.className == typeName } } - val primaryThis = closureScope.thisObj - val merged = ArrayList(callScope.thisVariants.size + closureScope.thisVariants.size + 1) + val primaryThis = when { + callScope is ApplyScope -> callScope.thisObj + desired != null -> desired + else -> closureScope.thisObj + } + val merged = ArrayList(callScope.thisVariants.size + closureScope.thisVariants.size + 3) desired?.let { merged.add(it) } + merged.add(callScope.thisObj) merged.addAll(callScope.thisVariants) + if (callScope is ApplyScope) { + merged.add(callScope.applied.thisObj) + merged.addAll(callScope.applied.thisVariants) + } merged.addAll(closureScope.thisVariants) setThisVariants(primaryThis, merged) this.currentClassCtx = closureScope.currentClassCtx ?: callScope.currentClassCtx @@ -47,10 +56,20 @@ class BytecodeClosureScope( } class ApplyScope(val callScope: Scope, val applied: Scope) : - Scope(callScope, thisObj = applied.thisObj) { + Scope(applied, callScope.args, callScope.pos, callScope.thisObj) { + + init { + // Merge applied receiver variants with the caller variants so qualified this@Type + // can see both the applied receiver and outer receivers. + val merged = ArrayList(applied.thisVariants.size + callScope.thisVariants.size + 1) + merged.addAll(applied.thisVariants) + merged.addAll(callScope.thisVariants) + setThisVariants(callScope.thisObj, merged) + this.currentClassCtx = applied.currentClassCtx ?: callScope.currentClassCtx + } override fun get(name: String): ObjRecord? { - return applied.get(name) ?: super.get(name) + return applied.get(name) ?: callScope.get(name) } override fun applyClosure(closure: Scope, preferredThisType: String?): Scope { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 904b34a..bf12dc7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -294,6 +294,25 @@ class Compiler( if (record.typeDecl != null) { slotTypeDeclByScopeId.getOrPut(plan.id) { mutableMapOf() }[slotIndex] = record.typeDecl } + val resolved = when (val raw = record.value) { + is FrameSlotRef -> raw.peekValue() ?: raw.read() + is RecordSlotRef -> raw.peekValue() ?: raw.read() + else -> raw + } + when (resolved) { + is ObjClass -> { + slotTypeByScopeId.getOrPut(plan.id) { mutableMapOf() }[slotIndex] = resolved + if (nameObjClass[name] == null) nameObjClass[name] = resolved + } + is ObjInstance -> { + slotTypeByScopeId.getOrPut(plan.id) { mutableMapOf() }[slotIndex] = resolved.objClass + if (nameObjClass[name] == null) nameObjClass[name] = resolved.objClass + } + is ObjDynamic -> { + slotTypeByScopeId.getOrPut(plan.id) { mutableMapOf() }[slotIndex] = resolved.objClass + if (nameObjClass[name] == null) nameObjClass[name] = resolved.objClass + } + } } } @@ -334,10 +353,12 @@ class Compiler( extensionNames.add(actual.value) registerExtensionName(nameToken.value, actual.value) declareSlotNameIn(plan, extensionCallableName(nameToken.value, actual.value), isMutable = false, isDelegated = false) + moduleDeclaredNames.add(extensionCallableName(nameToken.value, actual.value)) } continue } declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false) + moduleDeclaredNames.add(nameToken.value) } "val", "var" -> { val nameToken = nextNonWs() @@ -350,19 +371,23 @@ class Compiler( extensionNames.add(actual.value) registerExtensionName(nameToken.value, actual.value) declareSlotNameIn(plan, extensionPropertyGetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false) + moduleDeclaredNames.add(extensionPropertyGetterName(nameToken.value, actual.value)) if (t.value == "var") { declareSlotNameIn(plan, extensionPropertySetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false) + moduleDeclaredNames.add(extensionPropertySetterName(nameToken.value, actual.value)) } } continue } declareSlotNameIn(plan, nameToken.value, isMutable = t.value == "var", isDelegated = false) + moduleDeclaredNames.add(nameToken.value) } "class", "object" -> { val nameToken = nextNonWs() if (nameToken.type == Token.Type.ID) { declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false) scopeSeedNames.add(nameToken.value) + moduleDeclaredNames.add(nameToken.value) } } "enum" -> { @@ -371,6 +396,7 @@ class Compiler( if (nameToken.type == Token.Type.ID) { declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false) scopeSeedNames.add(nameToken.value) + moduleDeclaredNames.add(nameToken.value) } } } @@ -984,7 +1010,7 @@ class Compiler( resolutionSink?.reference(name, pos) return ref } - val ref = if (capturePlanStack.isEmpty() && moduleLoc.depth > 0) { + val ref = if (!useScopeSlots && capturePlanStack.isEmpty() && moduleLoc.depth > 0) { LocalSlotRef( name, moduleLoc.slot, @@ -1044,7 +1070,7 @@ class Compiler( resolutionSink?.reference(name, pos) return ref } - val ref = if (capturePlanStack.isEmpty() && slot.depth > 0) { + val ref = if (!useScopeSlots && capturePlanStack.isEmpty() && slot.depth > 0) { LocalSlotRef( name, slot.slot, @@ -1182,8 +1208,8 @@ class Compiler( if (!record.visibility.isPublic) continue if (nameObjClass.containsKey(name)) continue val resolved = when (val raw = record.value) { - is FrameSlotRef -> raw.peekValue() - is RecordSlotRef -> raw.peekValue() + is FrameSlotRef -> raw.peekValue() ?: raw.read() + is RecordSlotRef -> raw.peekValue() ?: raw.read() else -> raw } ?: continue when (resolved) { @@ -1250,6 +1276,25 @@ class Compiler( } if (moduleMatches.isEmpty()) return null if (moduleMatches.size > 1) { + val byOrigin = LinkedHashMap>>() + for ((_, pair) in moduleMatches) { + val origin = pair.second.importedFrom?.packageName ?: pair.first.scope.packageName + byOrigin.getOrPut(origin) { mutableListOf() }.add(pair) + } + if (byOrigin.size == 1) { + val origin = byOrigin.keys.first() + val candidates = byOrigin[origin] ?: mutableListOf() + val preferred = candidates.firstOrNull { it.first.scope.packageName == origin } ?: candidates.first() + val binding = ImportBinding(name, ImportBindingSource.Module(origin, preferred.first.pos)) + val value = preferred.second.value + if (!nameObjClass.containsKey(name)) { + when (value) { + is ObjClass -> nameObjClass[name] = value + is ObjInstance -> nameObjClass[name] = value.objClass + } + } + return ImportBindingResolution(binding, preferred.second) + } val moduleNames = moduleMatches.keys.toList() throw ScriptError(pos, "symbol $name is ambiguous between imports: ${moduleNames.joinToString(", ")}") } @@ -1539,16 +1584,14 @@ class Compiler( if (needsSlotPlan) { slotPlanStack.add(SlotPlan(mutableMapOf(), 0, nextScopeId++)) seedScope?.let { scope -> - if (scope !is ModuleScope) { - seedSlotPlanFromSeedScope(scope) - } + seedSlotPlanFromSeedScope(scope) } val plan = slotPlanStack.last() - if (!plan.slots.containsKey("__PACKAGE__")) { - declareSlotNameIn(plan, "__PACKAGE__", isMutable = false, isDelegated = false) + seedScope?.getSlotIndexOf("__PACKAGE__")?.let { slotIndex -> + declareSlotNameAt(plan, "__PACKAGE__", slotIndex, isMutable = false, isDelegated = false) } - if (!plan.slots.containsKey("$~")) { - declareSlotNameIn(plan, "$~", isMutable = true, isDelegated = false) + seedScope?.getSlotIndexOf("$~")?.let { slotIndex -> + declareSlotNameAt(plan, "$~", slotIndex, isMutable = true, isDelegated = false) } seedScope?.let { seedNameObjClassFromScope(it) } seedScope?.let { seedNameTypeDeclFromScope(it) } @@ -1557,6 +1600,7 @@ class Compiler( val stdlib = importManager.prepareImport(start, "lyng.stdlib", null) seedResolutionFromScope(stdlib, start) seedNameObjClassFromScope(stdlib) + seedSlotPlanFromScope(stdlib) importedModules.add(ImportedModule(stdlib, start)) } predeclareTopLevelSymbols() @@ -1660,7 +1704,7 @@ class Compiler( val forcedLocalScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id val allowedScopeNames = if (useScopeSlots) modulePlan.keys else null val scopeSlotNameSet = if (useScopeSlots) scopeSeedNames else null - val moduleScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id + val moduleScopeId = moduleSlotPlan()?.id val isModuleScript = codeContexts.lastOrNull() is CodeContext.Module && resolutionScriptDepth == 1 val wrapScriptBytecode = compileBytecode && isModuleScript val (finalStatements, moduleBytecode) = if (wrapScriptBytecode) { @@ -1678,6 +1722,7 @@ class Compiler( slotTypeByScopeId = slotTypeByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownClassMapForBytecode(), + knownClassNames = knownClassNamesForBytecode(), knownObjectNames = objectDeclNames, classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, @@ -1690,11 +1735,16 @@ class Compiler( statements to null } val moduleRefs = importedModules.map { ImportBindingSource.Module(it.scope.packageName, it.pos) } + val declaredNames = if (importBindings.isEmpty()) { + moduleDeclaredNames.toSet() + } else { + moduleDeclaredNames.subtract(importBindings.keys) + } Script( start, finalStatements, modulePlan, - moduleDeclaredNames.toSet(), + declaredNames, importBindings.toMap(), moduleRefs, moduleBytecode @@ -1741,7 +1791,7 @@ class Compiler( private val rangeParamNamesStack = mutableListOf>() private val extensionNames = mutableSetOf() private val extensionNamesByType = mutableMapOf>() - private val useScopeSlots: Boolean = seedScope != null && seedScope !is ModuleScope + private val useScopeSlots: Boolean = seedScope == null private fun registerExtensionName(typeName: String, memberName: String) { extensionNamesByType.getOrPut(typeName) { mutableSetOf() }.add(memberName) @@ -1884,6 +1934,9 @@ class Compiler( val scopeIndex = slotPlanStack.indexOfLast { it.id == slotLoc.scopeId } if (functionIndex >= 0 && scopeIndex >= functionIndex) return null val modulePlan = moduleSlotPlan() + if (useScopeSlots && modulePlan != null && slotLoc.scopeId == modulePlan.id) { + return null + } if (scopeSeedNames.contains(name)) { val isModuleSlot = modulePlan != null && slotLoc.scopeId == modulePlan.id if (!isModuleSlot || useScopeSlots) return null @@ -1949,6 +2002,23 @@ class Compiler( return result } + private fun knownClassNamesForBytecode(): Set { + val result = LinkedHashSet() + fun addScope(scope: Scope?) { + if (scope == null) return + for ((name, rec) in scope.objects) { + if (rec.value is ObjClass) result.add(name) + } + } + addScope(seedScope) + addScope(importManager.rootScope) + for (module in importedModules) { + addScope(module.scope) + } + result.addAll(compileClassInfos.keys) + return result + } + private fun wrapBytecode(stmt: Statement): Statement { if (codeContexts.lastOrNull() is CodeContext.Module) return stmt if (codeContexts.lastOrNull() is CodeContext.ClassBody) return stmt @@ -1975,6 +2045,7 @@ class Compiler( slotTypeByScopeId = slotTypeByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownClassMapForBytecode(), + knownClassNames = knownClassNamesForBytecode(), knownObjectNames = objectDeclNames, classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, @@ -2004,6 +2075,7 @@ class Compiler( slotTypeByScopeId = slotTypeByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownClassMapForBytecode(), + knownClassNames = knownClassNamesForBytecode(), knownObjectNames = objectDeclNames, classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, @@ -2058,6 +2130,7 @@ class Compiler( slotTypeByScopeId = slotTypeByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownNames, + knownClassNames = knownClassNamesForBytecode(), knownObjectNames = objectDeclNames, classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, @@ -3000,11 +3073,18 @@ class Compiler( val paramSlotPlanSnapshot = slotPlanIndices(paramSlotPlan) val captureSlots = capturePlan.captures.toList() val captureEntries = if (captureSlots.isNotEmpty()) { + val modulePlan = moduleSlotPlan() captureSlots.map { capture -> val owner = capturePlan.captureOwners[capture.name] ?: error("Missing capture owner for ${capture.name}") + val isModuleSlot = modulePlan != null && owner.scopeId == modulePlan.id + val ownerKind = if (isModuleSlot) { + net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.MODULE + } else { + net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.LOCAL + } net.sergeych.lyng.bytecode.LambdaCaptureEntry( - ownerKind = net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.LOCAL, + ownerKind = ownerKind, ownerScopeId = owner.scopeId, ownerSlotId = owner.slot, ownerName = capture.name, @@ -4269,15 +4349,24 @@ class Compiler( is LocalSlotRef -> { val ownerScopeId = ref.captureOwnerScopeId ?: ref.scopeId val ownerSlot = ref.captureOwnerSlot ?: ref.slot - slotTypeByScopeId[ownerScopeId]?.get(ownerSlot) - ?: slotTypeDeclByScopeId[ownerScopeId]?.get(ownerSlot)?.let { resolveTypeDeclObjClass(it) } - ?: nameObjClass[ref.name] + val knownClass = nameObjClass[ref.name] + if (knownClass == ObjDynamic.type) { + knownClass + } else { + slotTypeByScopeId[ownerScopeId]?.get(ownerSlot) + ?: slotTypeDeclByScopeId[ownerScopeId]?.get(ownerSlot)?.let { resolveTypeDeclObjClass(it) } + ?: knownClass + } ?: resolveClassByName(ref.name) } is LocalVarRef -> nameObjClass[ref.name] + ?.takeIf { it == ObjDynamic.type } + ?: nameObjClass[ref.name] ?: nameTypeDecl[ref.name]?.let { resolveTypeDeclObjClass(it) } ?: resolveClassByName(ref.name) is FastLocalVarRef -> nameObjClass[ref.name] + ?.takeIf { it == ObjDynamic.type } + ?: nameObjClass[ref.name] ?: nameTypeDecl[ref.name]?.let { resolveTypeDeclObjClass(it) } ?: resolveClassByName(ref.name) is ClassScopeMemberRef -> { @@ -7485,6 +7574,9 @@ class Compiler( val bytecodeBody = (fnStatements as? BytecodeStatement) ?: context.raiseIllegalState("non-bytecode function body encountered") val bytecodeFn = bytecodeBody.bytecodeFunction() + val declaredNames = bytecodeFn.constants + .mapNotNull { it as? BytecodeConst.LocalDecl } + .mapTo(mutableSetOf()) { it.name } val captureNames = if (captureSlots.isNotEmpty()) { captureSlots.map { it.name } } else { @@ -7534,6 +7626,28 @@ class Compiler( if (extTypeName != null) { context.thisObj = scope.thisObj } + val localNames = frame.fn.localSlotNames + for (i in localNames.indices) { + val localName = localNames[i] ?: continue + if (declaredNames.contains(localName)) continue + val slotType = frame.getLocalSlotTypeCode(i) + if (slotType != SlotType.UNKNOWN.code && slotType != SlotType.OBJ.code) { + continue + } + if (slotType == SlotType.OBJ.code && frame.frame.getRawObj(i) != null) { + continue + } + val record = context.getLocalRecordDirect(localName) + ?: context.parent?.get(localName) + ?: context.get(localName) + ?: continue + val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) { + context.resolve(record, localName) + } else { + record.value + } + frame.frame.setObj(i, value) + } } return try { net.sergeych.lyng.bytecode.CmdVm().execute(bytecodeFn, context, scope.args, binder) @@ -8795,7 +8909,8 @@ class Compiler( companion object { suspend fun compile(source: Source, importManager: ImportProvider): Script { - return Compiler(CompilerContext(parseLyng(source)), importManager).parseScript() + val script = Compiler(CompilerContext(parseLyng(source)), importManager).parseScript() + return script } suspend fun dryRun(source: Source, importManager: ImportProvider): ResolutionReport { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt index 5be9a55..a014155 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FrameAccess.kt @@ -56,6 +56,14 @@ class FrameSlotRef( } } + override suspend fun callOn(scope: Scope): Obj { + val resolved = read() + if (resolved === this) { + scope.raiseNotImplemented("call on unresolved frame slot") + } + return resolved.callOn(scope) + } + internal fun refersTo(frame: FrameAccess, slot: Int): Boolean { return this.frame === frame && this.slot == slot } @@ -81,6 +89,62 @@ class FrameSlotRef( } } +class ScopeSlotRef( + private val scope: Scope, + private val slot: Int, + private val name: String? = null, +) : net.sergeych.lyng.obj.Obj() { + override suspend fun compareTo(scope: Scope, other: Obj): Int { + val resolvedOther = when (other) { + is FrameSlotRef -> other.read() + is RecordSlotRef -> other.read() + is ScopeSlotRef -> other.read() + else -> other + } + return read().compareTo(scope, resolvedOther) + } + + fun read(): Obj { + val record = scope.getSlotRecord(slot) + val direct = record.value + if (direct is FrameSlotRef) return direct.read() + if (direct is RecordSlotRef) return direct.read() + if (direct is ScopeSlotRef) return direct.read() + if (direct !== ObjUnset) { + return direct + } + if (name == null) return record.value + val resolved = scope.get(name) ?: return record.value + if (resolved.value !== ObjUnset) { + scope.updateSlotFor(name, resolved) + } + return resolved.value + } + + internal fun peekValue(): Obj? { + val record = scope.getSlotRecord(slot) + val direct = record.value + return when (direct) { + is FrameSlotRef -> direct.peekValue() + is RecordSlotRef -> direct.peekValue() + is ScopeSlotRef -> direct.peekValue() + else -> direct + } + } + + fun write(value: Obj) { + scope.setSlotValue(slot, value) + } + + override suspend fun callOn(scope: Scope): Obj { + val resolved = read() + if (resolved === this) { + scope.raiseNotImplemented("call on unresolved scope slot") + } + return resolved.callOn(scope) + } +} + class RecordSlotRef( private val record: ObjRecord, ) : net.sergeych.lyng.obj.Obj() { @@ -88,6 +152,7 @@ class RecordSlotRef( val resolvedOther = when (other) { is FrameSlotRef -> other.read() is RecordSlotRef -> other.read() + is ScopeSlotRef -> other.read() else -> other } return read().compareTo(scope, resolvedOther) @@ -95,7 +160,19 @@ class RecordSlotRef( fun read(): Obj { val direct = record.value - return if (direct is FrameSlotRef) direct.read() else direct + return when (direct) { + is FrameSlotRef -> direct.read() + is ScopeSlotRef -> direct.read() + else -> direct + } + } + + override suspend fun callOn(scope: Scope): Obj { + val resolved = read() + if (resolved === this) { + scope.raiseNotImplemented("call on unresolved record slot") + } + return resolved.callOn(scope) } internal fun peekValue(): Obj? { @@ -103,11 +180,17 @@ class RecordSlotRef( return when (direct) { is FrameSlotRef -> direct.peekValue() is RecordSlotRef -> direct.peekValue() + is ScopeSlotRef -> direct.peekValue() else -> direct } } fun write(value: Obj) { - record.value = value + val direct = record.value + if (direct is ScopeSlotRef) { + direct.write(value) + } else { + record.value = value + } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt index 8d18298..e166985 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * 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. @@ -44,11 +44,28 @@ class ModuleScope( internal fun ensureModuleFrame(fn: CmdFunction): BytecodeFrame { val current = moduleFrame - val frame = if (current == null || moduleFrameLocalCount != fn.localCount) { + val frame = if (current == null) { BytecodeFrame(fn.localCount, 0).also { moduleFrame = it moduleFrameLocalCount = fn.localCount } + } else if (fn.localCount > moduleFrameLocalCount) { + val next = BytecodeFrame(fn.localCount, 0) + current.copyTo(next) + moduleFrame = next + moduleFrameLocalCount = fn.localCount + // Retarget frame-based locals to the new frame instance. + val localNames = fn.localSlotNames + for (i in localNames.indices) { + val name = localNames[i] ?: continue + val record = objects[name] ?: localBindings[name] ?: continue + val value = record.value + if (value is FrameSlotRef && value.refersTo(current, i)) { + record.value = FrameSlotRef(next, i) + updateSlotFor(name, record) + } + } + next } else { current } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 78e4590..688019a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -635,10 +635,16 @@ open class Scope( objects[name]?.let { if( !it.isMutable ) raiseIllegalAssignment("symbol is readonly: $name") - it.value = value + when (val current = it.value) { + is FrameSlotRef -> current.write(value) + is RecordSlotRef -> current.write(value) + else -> it.value = value + } // keep local binding index consistent within the frame localBindings[name] = it bumpClassLayoutIfNeeded(name, value, recordType) + updateSlotFor(name, it) + syncModuleFrameSlot(name, value) it } ?: addItem(name, true, value, visibility, writeVisibility, recordType, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride) @@ -705,6 +711,7 @@ open class Scope( slots[idx] = rec } } + syncModuleFrameSlot(name, value) return rec } @@ -752,7 +759,48 @@ open class Scope( // --- removed doc-aware overloads to keep runtime lean --- - fun addConst(name: String, value: Obj) = addItem(name, false, value) + fun addConst(name: String, value: Obj): ObjRecord { + val existing = objects[name] + if (existing != null) { + when (val current = existing.value) { + is FrameSlotRef -> current.write(value) + is RecordSlotRef -> current.write(value) + else -> existing.value = value + } + bumpClassLayoutIfNeeded(name, value, existing.type) + updateSlotFor(name, existing) + syncModuleFrameSlot(name, value) + return existing + } + val slotIndex = getSlotIndexOf(name) + if (slotIndex != null) { + val record = getSlotRecord(slotIndex) + when (val current = record.value) { + is FrameSlotRef -> current.write(value) + is RecordSlotRef -> current.write(value) + else -> record.value = value + } + bumpClassLayoutIfNeeded(name, value, record.type) + updateSlotFor(name, record) + syncModuleFrameSlot(name, value) + return record + } + val record = addItem(name, false, value) + syncModuleFrameSlot(name, value) + return record + } + + private fun syncModuleFrameSlot(name: String, value: Obj) { + val module = this as? ModuleScope ?: return + val frame = module.moduleFrame ?: return + val localNames = module.moduleFrameLocalSlotNames + if (localNames.isEmpty()) return + for (i in localNames.indices) { + if (localNames[i] == name) { + frame.setObj(i, value) + } + } + } suspend fun eval(code: String): Obj = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 999be28..0a3e0ab 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -42,13 +42,12 @@ class Script( // private val catchReturn: Boolean = false, ) : Statement() { fun statements(): List = statements - override suspend fun execute(scope: Scope): Obj { scope.pos = pos val execScope = resolveModuleScope(scope) ?: scope val isModuleScope = execScope is ModuleScope val shouldSeedModule = isModuleScope || execScope.thisObj === ObjVoid - val moduleTarget = execScope + val moduleTarget = (execScope as? ModuleScope) ?: execScope.parent as? ModuleScope ?: execScope if (shouldSeedModule) { seedModuleSlots(moduleTarget, scope) } @@ -56,9 +55,15 @@ class Script( if (execScope is ModuleScope) { execScope.ensureModuleFrame(fn) } - return CmdVm().execute(fn, execScope, scope.args) { frame, _ -> + var execFrame: net.sergeych.lyng.bytecode.CmdFrame? = null + val result = CmdVm().execute(fn, execScope, scope.args) { frame, _ -> + execFrame = frame seedModuleLocals(frame, moduleTarget, scope) } + if (execScope !is ModuleScope) { + execFrame?.let { syncFrameLocalsToScope(it, execScope) } + } + return result } if (statements.isNotEmpty()) { scope.raiseIllegalState("bytecode-only execution is required; missing module bytecode") @@ -69,6 +74,13 @@ class Script( private suspend fun seedModuleSlots(scope: Scope, seedScope: Scope) { if (importBindings.isEmpty() && importedModules.isEmpty()) return seedImportBindings(scope, seedScope) + if (moduleSlotPlan.isNotEmpty()) { + scope.applySlotPlan(moduleSlotPlan) + for (name in moduleSlotPlan.keys) { + val record = scope.objects[name] ?: scope.localBindings[name] ?: continue + scope.updateSlotFor(name, record) + } + } } private suspend fun seedModuleLocals( @@ -87,12 +99,43 @@ class Script( val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) { scope.resolve(record, name) } else { - record.value + val raw = record.value + when (raw) { + is FrameSlotRef -> { + if (raw.refersTo(frame.frame, i)) { + raw.peekValue() ?: continue + } else if (seedScope !is ModuleScope) { + raw + } else { + raw.read() + } + } + is RecordSlotRef -> { + if (seedScope !is ModuleScope) raw else raw.read() + } + else -> raw + } } frame.setObjUnchecked(base + i, value) } } + private fun syncFrameLocalsToScope(frame: net.sergeych.lyng.bytecode.CmdFrame, scope: Scope) { + val localNames = frame.fn.localSlotNames + if (localNames.isEmpty()) return + for (i in localNames.indices) { + val name = localNames[i] ?: continue + val record = scope.getLocalRecordDirect(name) ?: scope.localBindings[name] ?: scope.objects[name] ?: continue + val value = frame.readLocalObj(i) + when (val current = record.value) { + is FrameSlotRef -> current.write(value) + is RecordSlotRef -> current.write(value) + else -> record.value = value + } + scope.updateSlotFor(name, record) + } + } + private suspend fun seedImportBindings(scope: Scope, seedScope: Scope) { val provider = scope.currentImportProvider val importedModules = LinkedHashSet() @@ -102,6 +145,9 @@ class Script( if (scope is ModuleScope) { scope.importedModules = importedModules.toList() } + for (module in importedModules) { + module.importInto(scope, null) + } for ((name, binding) in importBindings) { val record = when (val source = binding.source) { is ImportBindingSource.Module -> { 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 9ed60f0..e671f19 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -35,6 +35,7 @@ class BytecodeCompiler( private val slotTypeByScopeId: Map> = emptyMap(), private val slotTypeDeclByScopeId: Map> = emptyMap(), private val knownNameObjClass: Map = emptyMap(), + private val knownClassNames: Set = emptySet(), private val knownObjectNames: Set = emptySet(), private val classFieldTypesByName: Map> = emptyMap(), private val enumEntriesByName: Map> = emptyMap(), @@ -78,7 +79,6 @@ class BytecodeCompiler( private val stableObjSlots = mutableSetOf() private val nameObjClass = knownNameObjClass.toMutableMap() private val listElementClassBySlot = mutableMapOf() - private val knownClassNames = knownNameObjClass.keys.toSet() private val slotInitClassByKey = mutableMapOf() private val intLoopVarNames = LinkedHashSet() private val valueFnRefs = LinkedHashSet() @@ -522,7 +522,7 @@ class BytecodeCompiler( } } if (resolved == SlotType.UNKNOWN) { - val inferred = slotTypeFromClass(nameObjClass[ref.name]) + val inferred = if (knownClassNames.contains(ref.name)) null else slotTypeFromClass(nameObjClass[ref.name]) if (inferred != null) { updateSlotType(mapped, inferred) resolved = inferred @@ -7695,14 +7695,28 @@ class BytecodeCompiler( for (ref in valueFnRefs) { val entries = lambdaCaptureEntriesByRef[ref] ?: continue for (entry in entries) { - if (entry.ownerKind != CaptureOwnerFrameKind.LOCAL) continue - val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId) - if (!localSlotInfoMap.containsKey(key)) { - localSlotInfoMap[key] = LocalSlotInfo( - entry.ownerName, - entry.ownerIsMutable, - entry.ownerIsDelegated - ) + if (entry.ownerKind == CaptureOwnerFrameKind.LOCAL) { + val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId) + if (!localSlotInfoMap.containsKey(key)) { + localSlotInfoMap[key] = LocalSlotInfo( + entry.ownerName, + entry.ownerIsMutable, + entry.ownerIsDelegated + ) + } + continue + } + if (entry.ownerKind == CaptureOwnerFrameKind.MODULE) { + val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId) + if (!scopeSlotMap.containsKey(key)) { + scopeSlotMap[key] = scopeSlotMap.size + } + if (!scopeSlotNameMap.containsKey(key)) { + scopeSlotNameMap[key] = entry.ownerName + } + if (!scopeSlotMutableMap.containsKey(key)) { + scopeSlotMutableMap[key] = entry.ownerIsMutable + } } } } @@ -8171,7 +8185,8 @@ class BytecodeCompiler( } private fun isModuleSlot(scopeId: Int, name: String?): Boolean { - val scopeNames = scopeSlotNameSet ?: allowedScopeNames + if (moduleScopeId != null && scopeId != moduleScopeId) return false + val scopeNames = allowedScopeNames ?: scopeSlotNameSet if (scopeNames == null || name == null) return false return scopeNames.contains(name) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFrame.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFrame.kt index ecf95d5..96e3889 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFrame.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeFrame.kt @@ -34,6 +34,17 @@ class BytecodeFrame( private val realSlots: DoubleArray = DoubleArray(slotCount) private val boolSlots: BooleanArray = BooleanArray(slotCount) + internal fun copyTo(target: BytecodeFrame) { + val limit = minOf(slotCount, target.slotCount) + for (i in 0 until limit) { + target.slotTypes[i] = slotTypes[i] + target.objSlots[i] = objSlots[i] + target.intSlots[i] = intSlots[i] + target.realSlots[i] = realSlots[i] + target.boolSlots[i] = boolSlots[i] + } + } + fun getSlotType(slot: Int): SlotType = SlotType.values().first { it.code == slotTypes[slot] } override fun getSlotTypeCode(slot: Int): Byte = slotTypes[slot] fun setSlotType(slot: Int, type: SlotType) { 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 e16e534..241e5cc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -20,6 +20,7 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.* import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.obj.ValueFnRef class BytecodeStatement private constructor( @@ -30,7 +31,34 @@ class BytecodeStatement private constructor( override suspend fun execute(scope: Scope): Obj { scope.pos = pos - return CmdVm().execute(function, scope, scope.args) + val declaredNames = function.constants + .mapNotNull { it as? BytecodeConst.LocalDecl } + .mapTo(mutableSetOf()) { it.name } + val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, _ -> + val localNames = frame.fn.localSlotNames + for (i in localNames.indices) { + val name = localNames[i] ?: continue + if (declaredNames.contains(name)) continue + val slotType = frame.getLocalSlotTypeCode(i) + if (slotType != SlotType.UNKNOWN.code && slotType != SlotType.OBJ.code) { + continue + } + if (slotType == SlotType.OBJ.code && frame.frame.getRawObj(i) != null) { + continue + } + val record = scope.getLocalRecordDirect(name) + ?: scope.parent?.get(name) + ?: scope.get(name) + ?: continue + val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) { + scope.resolve(record, name) + } else { + record.value + } + frame.frame.setObj(i, value) + } + } + return CmdVm().execute(function, scope, scope.args, binder) } internal fun bytecodeFunction(): CmdFunction = function @@ -52,6 +80,7 @@ class BytecodeStatement private constructor( globalSlotScopeId: Int? = null, slotTypeByScopeId: Map> = emptyMap(), knownNameObjClass: Map = emptyMap(), + knownClassNames: Set = emptySet(), knownObjectNames: Set = emptySet(), classFieldTypesByName: Map> = emptyMap(), enumEntriesByName: Map> = emptyMap(), @@ -86,6 +115,7 @@ class BytecodeStatement private constructor( slotTypeByScopeId = slotTypeByScopeId, slotTypeDeclByScopeId = slotTypeDeclByScopeId, knownNameObjClass = knownNameObjClass, + knownClassNames = knownClassNames, knownObjectNames = knownObjectNames, classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, 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 bb83ce0..8f071bb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -229,8 +229,29 @@ class CmdLoadThisVariant( val typeConst = frame.fn.constants.getOrNull(typeId) as? BytecodeConst.StringVal ?: error("LOAD_THIS_VARIANT expects StringVal at $typeId") val typeName = typeConst.value - val receiver = frame.ensureScope().thisVariants.firstOrNull { it.isInstanceOf(typeName) } - ?: frame.ensureScope().raiseClassCastError("Cannot cast ${frame.ensureScope().thisObj.objClass.className} to $typeName") + val scope = frame.ensureScope() + if (scope.thisVariants.isEmpty() || scope.thisVariants.firstOrNull() !== scope.thisObj) { + scope.setThisVariants(scope.thisObj, scope.thisVariants) + } + val receiver = scope.thisVariants.firstOrNull { it.isInstanceOf(typeName) } + ?: run { + if (scope.thisObj.isInstanceOf(typeName)) return@run scope.thisObj + val typeClass = scope[typeName]?.value as? net.sergeych.lyng.obj.ObjClass + var s: Scope? = scope + while (s != null) { + val candidate = s.thisObj + if (candidate.isInstanceOf(typeName)) return@run candidate + if (typeClass != null) { + val inst = candidate as? net.sergeych.lyng.obj.ObjInstance + if (inst != null && (inst.objClass === typeClass || inst.objClass.allParentsSet.contains(typeClass))) { + return@run inst + } + } + s = s.parent + } + val variants = scope.thisVariants.joinToString { it.objClass.className } + scope.raiseClassCastError("Cannot cast ${scope.thisObj.objClass.className} to $typeName (variants: $variants)") + } frame.setObj(dst, receiver) return } @@ -2398,12 +2419,10 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() { ) val moduleScope = frame.scope as? ModuleScope if (moduleScope != null) { - moduleScope.updateSlotFor(decl.name, record) moduleScope.objects[decl.name] = record moduleScope.localBindings[decl.name] = record } else if (frame.fn.name == "