Seed Scope.eval symbols and reenable script tests

This commit is contained in:
Sergey Chernov 2026-01-30 12:56:37 +03:00
parent df48a06311
commit a266df6035
8 changed files with 146 additions and 42 deletions

View File

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

View File

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

View File

@ -43,6 +43,7 @@ class BytecodeCompiler(
private var scopeSlotCount = 0
private var scopeSlotIndices = IntArray(0)
private var scopeSlotNames = emptyArray<String?>()
private var scopeSlotIsModule = BooleanArray(0)
private val scopeSlotMap = LinkedHashMap<ScopeSlotKey, Int>()
private val scopeSlotNameMap = LinkedHashMap<ScopeSlotKey, String>()
private val scopeSlotIndexByName = LinkedHashMap<String, Int>()
@ -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<String?>(localSlotInfoMap.size)

View File

@ -61,20 +61,20 @@ class CmdBuilder {
localCount: Int,
addrCount: Int = 0,
returnLabels: Set<String> = emptySet(),
scopeSlotDepths: IntArray = IntArray(0),
scopeSlotIndices: IntArray = IntArray(0),
scopeSlotNames: Array<String?> = emptyArray(),
scopeSlotIsModule: BooleanArray = BooleanArray(0),
localSlotNames: Array<String?> = 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<Label, Int>()
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()

View File

@ -22,22 +22,20 @@ data class CmdFunction(
val addrCount: Int,
val returnLabels: Set<String>,
val scopeSlotCount: Int,
val scopeSlotDepths: IntArray,
val scopeSlotIndices: IntArray,
val scopeSlotNames: Array<String?>,
val scopeSlotIsModule: BooleanArray,
val localSlotNames: Array<String?>,
val localSlotMutables: BooleanArray,
val localSlotDepths: IntArray,
val constants: List<BytecodeConst>,
val fallbackStatements: List<net.sergeych.lyng.Statement>,
val cmds: Array<Cmd>,
) {
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" }
}

View File

@ -1498,6 +1498,7 @@ class CmdFrame(
var ip: Int = 0
var scope: Scope = scope0
private val moduleScope: Scope = scope0
val methodCallSites: MutableMap<Int, MethodCallSite> = CmdCallSiteCache.methodCallSites(fn)
internal val scopeStack = ArrayDeque<Scope>()
@ -1522,6 +1523,15 @@ class CmdFrame(
fun pushScope(plan: Map<String, Int>, captures: List<String>) {
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

View File

@ -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<ResolvedSymbol>,
val captures: List<CaptureInfo>,
val errors: List<ResolutionError>,
val warnings: List<ResolutionWarning>,
)
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()
}
}

View File

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