From a266df603545b4187a920387f84c0cee2c147383 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 30 Jan 2026 12:56:37 +0300 Subject: [PATCH] Seed Scope.eval symbols and reenable script tests --- .../kotlin/net/sergeych/lyng/Compiler.kt | 12 ++- .../kotlin/net/sergeych/lyng/Scope.kt | 9 ++- .../lyng/bytecode/BytecodeCompiler.kt | 15 +++- .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 15 ++-- .../net/sergeych/lyng/bytecode/CmdFunction.kt | 6 +- .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 36 ++++++--- .../lyng/resolution/CompileTimeResolution.kt | 78 +++++++++++++++++++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 17 +--- 8 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/resolution/CompileTimeResolution.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 710686d..9c2c969 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -328,11 +328,13 @@ class Compiler( val useBytecodeStatements: Boolean = true, val strictSlotRefs: Boolean = false, val allowUnresolvedRefs: Boolean = false, + val seedScope: Scope? = null, ) // Optional sink for mini-AST streaming (null by default, zero overhead when not used) private val miniSink: MiniAstSink? = settings.miniAstSink private val resolutionSink: ResolutionSink? = settings.resolutionSink + private val seedScope: Scope? = settings.seedScope private var resolutionScriptDepth = 0 private val resolutionPredeclared = mutableSetOf() @@ -451,6 +453,7 @@ class Compiler( val atTopLevel = resolutionSink != null && resolutionScriptDepth == 0 if (atTopLevel) { resolutionSink?.enterScope(ScopeKind.MODULE, start, null) + seedScope?.let { seedResolutionFromScope(it, start) } seedResolutionFromScope(importManager.rootScope, start) } resolutionScriptDepth++ @@ -459,6 +462,7 @@ class Compiler( if (needsSlotPlan) { slotPlanStack.add(SlotPlan(mutableMapOf(), 0, nextScopeId++)) declareSlotNameIn(slotPlanStack.last(), "__PACKAGE__", isMutable = false, isDelegated = false) + seedScope?.let { seedSlotPlanFromScope(it) } seedSlotPlanFromScope(importManager.rootScope) predeclareTopLevelSymbols() } @@ -637,6 +641,8 @@ class Compiler( private fun captureLocalRef(name: String, slotLoc: SlotLocation, pos: Pos): LocalSlotRef? { if (capturePlanStack.isEmpty() || slotLoc.depth == 0) return null + val moduleId = moduleSlotPlan()?.id + if (moduleId != null && slotLoc.scopeId == moduleId) return null recordCaptureSlot(name, slotLoc) val plan = capturePlanStack.lastOrNull() ?: return null val entry = plan.slotPlan.slots[name] ?: return null @@ -4547,7 +4553,8 @@ class Compiler( resolutionSink: ResolutionSink? = null, useBytecodeStatements: Boolean = true, strictSlotRefs: Boolean = false, - allowUnresolvedRefs: Boolean = false + allowUnresolvedRefs: Boolean = false, + seedScope: Scope? = null ): Script { return Compiler( CompilerContext(parseLyng(source)), @@ -4557,7 +4564,8 @@ class Compiler( resolutionSink = resolutionSink, useBytecodeStatements = useBytecodeStatements, strictSlotRefs = strictSlotRefs, - allowUnresolvedRefs = allowUnresolvedRefs + allowUnresolvedRefs = allowUnresolvedRefs, + seedScope = seedScope ) ).parseScript() } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index c269a6e..ba2a7a6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -143,6 +143,10 @@ open class Scope( return null } + internal fun resolveCaptureRecord(name: String): ObjRecord? { + return chainLookupIgnoreClosure(name, followClosure = true, caller = currentClassCtx) + } + /** * Perform base Scope.get semantics for this frame without delegating into parent.get * virtual dispatch. This checks: @@ -697,9 +701,10 @@ open class Scope( eval(code.toSource()) suspend fun eval(source: Source): Obj { - return Compiler.compile( + return Compiler.compileWithResolution( source, - currentImportProvider + currentImportProvider, + seedScope = this ).execute(this) } 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 351a266..dd2ac81 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -43,6 +43,7 @@ class BytecodeCompiler( private var scopeSlotCount = 0 private var scopeSlotIndices = IntArray(0) private var scopeSlotNames = emptyArray() + private var scopeSlotIsModule = BooleanArray(0) private val scopeSlotMap = LinkedHashMap() private val scopeSlotNameMap = LinkedHashMap() private val scopeSlotIndexByName = LinkedHashMap() @@ -94,6 +95,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -112,6 +114,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -132,6 +135,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -149,6 +153,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -173,7 +178,7 @@ class BytecodeCompiler( if (!allowLocalSlots) return null if (ref.isDelegated) return null if (ref.name.isEmpty()) return null - if (ref.captureOwnerScopeId == null) { + if (ref.captureOwnerScopeId == null && refScopeId(ref) == 0) { val byName = scopeSlotIndexByName[ref.name] if (byName != null) { val resolved = slotTypes[byName] ?: SlotType.UNKNOWN @@ -1906,6 +1911,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -1922,6 +1928,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -1939,6 +1946,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -1956,6 +1964,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -1972,6 +1981,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -1988,6 +1998,7 @@ class BytecodeCompiler( returnLabels = returnLabels, scopeSlotIndices, scopeSlotNames, + scopeSlotIsModule, localSlotNames, localSlotMutables ) @@ -3034,10 +3045,12 @@ class BytecodeCompiler( scopeSlotCount = scopeSlotMap.size scopeSlotIndices = IntArray(scopeSlotCount) scopeSlotNames = arrayOfNulls(scopeSlotCount) + scopeSlotIsModule = BooleanArray(scopeSlotCount) for ((key, index) in scopeSlotMap) { val name = scopeSlotNameMap[key] scopeSlotIndices[index] = key.slot scopeSlotNames[index] = name + scopeSlotIsModule[index] = key.scopeId == 0 } if (allowLocalSlots && localSlotInfoMap.isNotEmpty()) { val names = ArrayList(localSlotInfoMap.size) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt index 38d286a..88e7f7c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -61,20 +61,20 @@ class CmdBuilder { localCount: Int, addrCount: Int = 0, returnLabels: Set = emptySet(), - scopeSlotDepths: IntArray = IntArray(0), scopeSlotIndices: IntArray = IntArray(0), scopeSlotNames: Array = emptyArray(), + scopeSlotIsModule: BooleanArray = BooleanArray(0), localSlotNames: Array = emptyArray(), - localSlotMutables: BooleanArray = BooleanArray(0), - localSlotDepths: IntArray = IntArray(0) + localSlotMutables: BooleanArray = BooleanArray(0) ): CmdFunction { - val scopeSlotCount = scopeSlotDepths.size - require(scopeSlotIndices.size == scopeSlotCount) { "scope slot mapping size mismatch" } + val scopeSlotCount = scopeSlotIndices.size require(scopeSlotNames.isEmpty() || scopeSlotNames.size == scopeSlotCount) { "scope slot name mapping size mismatch" } + require(scopeSlotIsModule.isEmpty() || scopeSlotIsModule.size == scopeSlotCount) { + "scope slot module mapping size mismatch" + } require(localSlotNames.size == localSlotMutables.size) { "local slot metadata size mismatch" } - require(localSlotNames.size == localSlotDepths.size) { "local slot depth metadata size mismatch" } val labelIps = mutableMapOf() for ((label, idx) in labelPositions) { labelIps[label] = idx @@ -103,12 +103,11 @@ class CmdBuilder { addrCount = addrCount, returnLabels = returnLabels, scopeSlotCount = scopeSlotCount, - scopeSlotDepths = scopeSlotDepths, scopeSlotIndices = scopeSlotIndices, scopeSlotNames = if (scopeSlotNames.isEmpty()) Array(scopeSlotCount) { null } else scopeSlotNames, + scopeSlotIsModule = if (scopeSlotIsModule.isEmpty()) BooleanArray(scopeSlotCount) else scopeSlotIsModule, localSlotNames = localSlotNames, localSlotMutables = localSlotMutables, - localSlotDepths = localSlotDepths, constants = constPool.toList(), fallbackStatements = fallbackStatements.toList(), cmds = cmds.toTypedArray() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt index 2425ced..bd1314d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdFunction.kt @@ -22,22 +22,20 @@ data class CmdFunction( val addrCount: Int, val returnLabels: Set, val scopeSlotCount: Int, - val scopeSlotDepths: IntArray, val scopeSlotIndices: IntArray, val scopeSlotNames: Array, + val scopeSlotIsModule: BooleanArray, val localSlotNames: Array, val localSlotMutables: BooleanArray, - val localSlotDepths: IntArray, val constants: List, val fallbackStatements: List, val cmds: Array, ) { init { - require(scopeSlotDepths.size == scopeSlotCount) { "scopeSlotDepths size mismatch" } require(scopeSlotIndices.size == scopeSlotCount) { "scopeSlotIndices size mismatch" } require(scopeSlotNames.size == scopeSlotCount) { "scopeSlotNames size mismatch" } + require(scopeSlotIsModule.size == scopeSlotCount) { "scopeSlotIsModule size mismatch" } require(localSlotNames.size == localSlotMutables.size) { "localSlot metadata size mismatch" } - require(localSlotNames.size == localSlotDepths.size) { "localSlot depth metadata size mismatch" } require(localSlotNames.size <= localCount) { "localSlotNames exceed localCount" } require(addrCount >= 0) { "addrCount must be non-negative" } } 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 e681fe1..0d38577 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -1498,6 +1498,7 @@ class CmdFrame( var ip: Int = 0 var scope: Scope = scope0 + private val moduleScope: Scope = scope0 val methodCallSites: MutableMap = CmdCallSiteCache.methodCallSites(fn) internal val scopeStack = ArrayDeque() @@ -1522,6 +1523,15 @@ class CmdFrame( fun pushScope(plan: Map, captures: List) { val parentScope = scope + val captureRecords = if (captures.isNotEmpty()) { + captures.map { name -> + val rec = parentScope.resolveCaptureRecord(name) + ?: parentScope.raiseSymbolNotFound("symbol $name not found") + name to rec + } + } else { + emptyList() + } if (captures.isNotEmpty() && fn.localSlotNames.isNotEmpty()) { syncFrameToScope() } @@ -1539,10 +1549,8 @@ class CmdFrame( scope.applySlotPlan(plan) } } - if (captures.isNotEmpty()) { - for (name in captures) { - val rec = parentScope.resolveCaptureRecord(name) - ?: parentScope.raiseSymbolNotFound("symbol ${name} not found") + if (captureRecords.isNotEmpty()) { + for ((name, rec) in captureRecords) { scope.updateSlotFor(name, rec) } } @@ -1630,7 +1638,7 @@ class CmdFrame( fun setObj(slot: Int, value: Obj) { if (slot < fn.scopeSlotCount) { - val target = scope + val target = scopeTarget(slot) val index = ensureScopeSlot(target, slot) target.setSlotValue(index, value) } else { @@ -1657,7 +1665,7 @@ class CmdFrame( fun setInt(slot: Int, value: Long) { if (slot < fn.scopeSlotCount) { - val target = scope + val target = scopeTarget(slot) val index = ensureScopeSlot(target, slot) target.setSlotValue(index, ObjInt.of(value)) } else { @@ -1686,7 +1694,7 @@ class CmdFrame( fun setReal(slot: Int, value: Double) { if (slot < fn.scopeSlotCount) { - val target = scope + val target = scopeTarget(slot) val index = ensureScopeSlot(target, slot) target.setSlotValue(index, ObjReal.of(value)) } else { @@ -1713,7 +1721,7 @@ class CmdFrame( fun setBool(slot: Int, value: Boolean) { if (slot < fn.scopeSlotCount) { - val target = scope + val target = scopeTarget(slot) val index = ensureScopeSlot(target, slot) target.setSlotValue(index, if (value) ObjTrue else ObjFalse) } else { @@ -1726,7 +1734,7 @@ class CmdFrame( } fun resolveScopeSlotAddr(scopeSlot: Int, addrSlot: Int) { - val target = scope + val target = scopeTarget(scopeSlot) val index = ensureScopeSlot(target, scopeSlot) addrScopes[addrSlot] = target addrIndices[addrSlot] = index @@ -1918,6 +1926,14 @@ class CmdFrame( return scope } + private fun scopeTarget(slot: Int): Scope { + return if (slot < fn.scopeSlotCount && fn.scopeSlotIsModule.getOrNull(slot) == true) { + moduleScope + } else { + scope + } + } + private fun localSlotToObj(localIndex: Int): Obj { return when (frame.getSlotTypeCode(localIndex)) { SlotType.INT.code -> ObjInt.of(frame.getInt(localIndex)) @@ -1929,7 +1945,7 @@ class CmdFrame( } private fun getScopeSlotValue(slot: Int): Obj { - val target = scope + val target = scopeTarget(slot) val index = ensureScopeSlot(target, slot) val record = target.getSlotRecord(index) if (record.value !== ObjUnset) return record.value diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/resolution/CompileTimeResolution.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/resolution/CompileTimeResolution.kt new file mode 100644 index 0000000..7da8beb --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/resolution/CompileTimeResolution.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * 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.resolution + +import net.sergeych.lyng.Compiler +import net.sergeych.lyng.Pos +import net.sergeych.lyng.Source +import net.sergeych.lyng.pacman.ImportProvider + +enum class SymbolOrigin { + LOCAL, + OUTER, + MODULE, + MEMBER, + PARAM +} + +data class ResolvedSymbol( + val name: String, + val origin: SymbolOrigin, + val slotIndex: Int, + val pos: Pos, +) + +data class CaptureInfo( + val name: String, + val origin: SymbolOrigin, + val slotIndex: Int, + val isMutable: Boolean, + val pos: Pos, +) + +data class ResolutionError( + val message: String, + val pos: Pos, +) + +data class ResolutionWarning( + val message: String, + val pos: Pos, +) + +data class ResolutionReport( + val moduleName: String, + val symbols: List, + val captures: List, + val errors: List, + val warnings: List, +) + +object CompileTimeResolver { + suspend fun dryRun(source: Source, importProvider: ImportProvider): ResolutionReport { + val collector = ResolutionCollector(source.fileName) + Compiler.compileWithResolution( + source, + importProvider, + resolutionSink = collector, + useBytecodeStatements = false, + allowUnresolvedRefs = true + ) + return collector.buildReport() + } +} diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index c81451e..a2714ef 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -893,11 +893,9 @@ class ScriptTest { } } - @Ignore("incremental enable") @Test fun testWhileBlockIsolation3() = runTest { - eval( - """ + val code = """ var outer = 7 var sum = 0 var cnt1 = 0 @@ -916,7 +914,7 @@ class ScriptTest { } println("sum "+sum) """.trimIndent() - ) + eval(code) } @Ignore("bytecode fallback in labeled break") @@ -969,7 +967,6 @@ class ScriptTest { ) } - @Ignore("incremental enable") @Test fun testIncr() = runTest { val c = Scope() @@ -982,7 +979,6 @@ class ScriptTest { assertEquals(12, c.eval("x").toInt()) } - @Ignore("incremental enable") @Test fun testDecr() = runTest { val c = Scope() @@ -994,7 +990,6 @@ class ScriptTest { assertEquals(5, c.eval("x").toInt()) } - @Ignore("incremental enable") @Test fun testDecrIncr() = runTest { val c = Scope() @@ -1009,7 +1004,6 @@ class ScriptTest { assertEquals(7, c.eval("x + 0").toInt()) } - @Ignore("incremental enable") @Test fun testDecrIncr2() = runTest { val c = Scope() @@ -1028,7 +1022,6 @@ class ScriptTest { .toInt()) } - @Ignore("incremental enable") @Test fun testDecrIncr3() = runTest { val c = Scope() @@ -1041,7 +1034,6 @@ class ScriptTest { assertEquals(11, c.eval("x").toInt()) } - @Ignore("incremental enable") @Test fun testIncrAndDecr() = runTest { val c = Scope() @@ -1077,7 +1069,6 @@ class ScriptTest { eval(src) } - @Ignore("incremental enable") @Test fun testAssign1() = runTest { assertEquals(10, eval("var x = 5; x=10; x").toInt()) @@ -1093,7 +1084,6 @@ class ScriptTest { assertEquals(10, ctx.eval("b").toInt()) } - @Ignore("incremental enable") @Test fun testAssign2() = runTest { val ctx = Scope() @@ -1112,7 +1102,6 @@ class ScriptTest { assertEquals(2, ctx.eval("x %= 5").toInt()) } - @Ignore("incremental enable") @Test fun testVals() = runTest { val cxt = Scope() @@ -1163,7 +1152,6 @@ class ScriptTest { ) } - @Ignore("incremental enable") @Test fun testListLiteral() = runTest { eval( @@ -1214,7 +1202,6 @@ class ScriptTest { ) } - @Ignore("incremental enable") @Test fun testListSize() = runTest { eval(