Compare commits

..

4 Commits

10 changed files with 607 additions and 56 deletions

View File

@ -0,0 +1,137 @@
package net.sergeych
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.ObjString
import kotlinx.coroutines.runBlocking
import java.nio.file.Files
import kotlin.io.path.writeText
import kotlin.test.Test
import kotlin.test.assertEquals
class CliLocalModuleImportRegressionJvmTest {
private fun writeTransitiveImportTree(root: java.nio.file.Path) {
val packageDir = Files.createDirectories(root.resolve("package1"))
val nestedDir = Files.createDirectories(packageDir.resolve("nested"))
packageDir.resolve("alpha.lyng").writeText(
"""
package package1.alpha
import lyng.stdlib
import lyng.io.net
class Alpha {
val headers = Map<String, String>()
fun makeTask(port: Int, host: String): Deferred = launch {
host + ":" + port
}
fun netModule() = Net
}
fun alphaValue() = "alpha"
""".trimIndent()
)
packageDir.resolve("beta.lyng").writeText(
"""
package package1.beta
import lyng.stdlib
import package1.alpha
fun betaValue() = alphaValue() + "|beta"
""".trimIndent()
)
nestedDir.resolve("gamma.lyng").writeText(
"""
package package1.nested.gamma
import lyng.io.net
import package1.alpha
import package1.beta
val String.gammaTag get() = this + "|gamma"
fun gammaValue() = betaValue().gammaTag
fun netModule() = Net
""".trimIndent()
)
packageDir.resolve("entry.lyng").writeText(
"""
package package1.entry
import lyng.stdlib
import lyng.io.net
import package1.alpha
import package1.beta
import package1.nested.gamma
fun report() = gammaValue() + "|entry"
""".trimIndent()
)
}
@Test
fun localModuleUsingLaunchAndNetImportsWithoutStdlibRedefinition() = runBlocking {
val root = Files.createTempDirectory("lyng-cli-import-regression")
try {
val mainFile = root.resolve("main.lyng")
writeTransitiveImportTree(root)
mainFile.writeText(
"""
import package1.entry
import package1.beta
import package1.nested.gamma
println(report())
""".trimIndent()
)
executeFile(mainFile.toString(), emptyList())
} finally {
root.toFile().deleteRecursively()
}
}
@Test
fun localModuleImportsAreNoOpsWhenEvaldRepeatedlyOnSameCliContext() = runBlocking {
val root = Files.createTempDirectory("lyng-cli-import-regression-repeat")
try {
val mainFile = root.resolve("main.lyng")
writeTransitiveImportTree(root)
mainFile.writeText("println(\"bootstrap\")")
val session = EvalSession(newCliScope(emptyList(), mainFile.toString()))
try {
repeat(5) { index ->
val result = evalOnCliDispatcher(
session,
Source(
"<repeat-local-import-$index>",
"""
import package1.entry
import package1.nested.gamma
import package1.beta
import package1.alpha
report()
""".trimIndent()
)
) as ObjString
assertEquals(
"alpha|beta|gamma|entry",
result.value
)
}
} finally {
session.cancelAndJoin()
}
} finally {
root.toFile().deleteRecursively()
}
}
}

View File

@ -64,6 +64,73 @@ class CliLocalImportsJvmTest {
return CliResult(outBuf.toString("UTF-8"), errBuf.toString("UTF-8"), exitCode)
}
private fun writeTransitiveImportTree(root: java.nio.file.Path) {
val packageDir = Files.createDirectories(root.resolve("package1"))
val nestedDir = Files.createDirectories(packageDir.resolve("nested"))
Files.writeString(
packageDir.resolve("alpha.lyng"),
"""
package package1.alpha
import lyng.stdlib
import lyng.io.net
class Alpha {
val headers = Map<String, String>()
fun makeTask(port: Int, host: String): Deferred = launch {
host + ":" + port
}
fun netModule() = Net
}
fun alphaValue() = "alpha"
""".trimIndent()
)
Files.writeString(
packageDir.resolve("beta.lyng"),
"""
package package1.beta
import lyng.stdlib
import package1.alpha
fun betaValue() = alphaValue() + "|beta"
""".trimIndent()
)
Files.writeString(
nestedDir.resolve("gamma.lyng"),
"""
package package1.nested.gamma
import lyng.io.net
import package1.alpha
import package1.beta
val String.gammaTag get() = this + "|gamma"
fun gammaValue() = betaValue().gammaTag
fun netModule() = Net
""".trimIndent()
)
Files.writeString(
packageDir.resolve("entry.lyng"),
"""
package package1.entry
import lyng.stdlib
import lyng.io.net
import package1.alpha
import package1.beta
import package1.nested.gamma
fun report() = gammaValue() + "|entry"
""".trimIndent()
)
}
@Test
fun cliDiscoversSiblingAndNestedLocalImportsFromEntryRoot() {
val dir = Files.createTempDirectory("lyng_cli_local_imports_")
@ -134,4 +201,37 @@ class CliLocalImportsJvmTest {
dir.toFile().deleteRecursively()
}
}
@Test
fun cliHandlesOverlappingDirectoryImportsWithTransitiveStdlibAndNetSymbols() {
val dir = Files.createTempDirectory("lyng_cli_local_imports_transitive_")
try {
val mainFile = dir.resolve("main.lyng")
writeTransitiveImportTree(dir)
Files.writeString(
mainFile,
"""
import package1.entry
import package1.beta
import package1.nested.gamma
println(report())
println(gammaValue())
""".trimIndent()
)
val result = runCli(mainFile.toString())
assertTrue(result.err, result.err.isBlank())
assertTrue(
result.out,
result.out.contains("alpha|beta|gamma|entry")
)
assertTrue(
result.out,
result.out.contains("alpha|beta|gamma")
)
} finally {
dir.toFile().deleteRecursively()
}
}
}

View File

@ -3436,12 +3436,12 @@ class Compiler(
val resolvedRecords = ArrayList<ObjRecord>(captureSlots.size)
val resolvedNames = ArrayList<String>(captureSlots.size)
for (capture in captureSlots) {
val rec = closureScope.chainLookupIgnoreClosure(
val rec = resolveStableCaptureRecord(
closureScope,
capture.name,
followClosure = true,
caller = context.currentClassCtx
context.currentClassCtx
) ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
resolvedRecords.add(rec)
resolvedRecords.add(freezeImmutableCaptureRecord(rec))
resolvedNames.add(capture.name)
}
context.captureRecords = resolvedRecords
@ -8766,6 +8766,46 @@ class Compiler(
}
}
private fun freezeImmutableCaptureRecord(record: ObjRecord): ObjRecord {
val value = record.value as Obj?
if (record.isMutable || record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || value is ObjProperty) {
return record
}
return when (value) {
is FrameSlotRef -> value.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
is RecordSlotRef -> value.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
is ScopeSlotRef -> value.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
null -> record
else -> record.copy()
}
}
private fun isTransientCapturePlaceholder(value: Obj?): Boolean {
return when (value) {
null, ObjVoid -> true
is FrameSlotRef -> value.resolvedCaptureValueOrNull().let { it == null || it === ObjVoid }
is RecordSlotRef -> value.resolvedCaptureValueOrNull().let { it == null || it === ObjVoid }
is ScopeSlotRef -> value.resolvedCaptureValueOrNull().let { it == null || it === ObjVoid }
else -> false
}
}
private fun resolveStableCaptureRecord(scope: Scope, name: String, caller: ObjClass?): ObjRecord? {
val direct = scope.chainLookupIgnoreClosure(name, followClosure = true, caller = caller) ?: scope.get(name)
if (direct != null && !isTransientCapturePlaceholder(direct.value as Obj?)) {
return direct
}
var parent = scope.parent
while (parent != null) {
val candidate = parent.chainLookupIgnoreClosure(name, followClosure = true, caller = caller) ?: parent.get(name)
if (candidate != null && !isTransientCapturePlaceholder(candidate.value as Obj?)) {
return candidate
}
parent = parent.parent
}
return direct
}
private suspend fun parseFunctionDeclaration(
visibility: Visibility = Visibility.Public,
isAbstract: Boolean = false,
@ -9147,12 +9187,12 @@ class Compiler(
} else if (captureBase != null && captureNames.isNotEmpty()) {
val resolvedRecords = ArrayList<ObjRecord>(captureNames.size)
for (name in captureNames) {
val rec = captureBase.chainLookupIgnoreClosure(
val rec = resolveStableCaptureRecord(
captureBase,
name,
followClosure = true,
caller = context.currentClassCtx
context.currentClassCtx
) ?: captureBase.raiseSymbolNotFound("symbol $name not found")
resolvedRecords.add(rec)
resolvedRecords.add(freezeImmutableCaptureRecord(rec))
}
context.captureRecords = resolvedRecords
context.captureNames = captureNames
@ -9193,7 +9233,12 @@ class Compiler(
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
context.resolve(record, localName)
} else {
record.value
when (val direct = record.value) {
is FrameSlotRef -> direct.read()
is RecordSlotRef -> direct.read(context, localName)
is ScopeSlotRef -> direct.read()
else -> direct
}
}
frame.frame.setObj(i, value)
}

View File

@ -79,6 +79,13 @@ class FrameSlotRef(
}
}
internal fun resolvedCaptureValueOrNull(): Obj? {
return when (frame.getSlotTypeCode(slot)) {
SlotType.INT.code, SlotType.REAL.code, SlotType.BOOL.code -> read()
else -> peekValue()?.let { read() }
}
}
fun write(value: Obj) {
when (value) {
is ObjInt -> frame.setInt(slot, value.value)
@ -132,6 +139,16 @@ class ScopeSlotRef(
}
}
internal fun resolvedCaptureValueOrNull(): Obj? {
val record = scope.getSlotRecord(slot)
return when (val direct = record.value as Obj?) {
is FrameSlotRef -> direct.resolvedCaptureValueOrNull()
is RecordSlotRef -> direct.resolvedCaptureValueOrNull()
is ScopeSlotRef -> direct.resolvedCaptureValueOrNull()
else -> direct
}
}
fun write(value: Obj) {
scope.setSlotValue(slot, value)
}
@ -198,6 +215,15 @@ class RecordSlotRef(
}
}
internal fun resolvedCaptureValueOrNull(): Obj? {
return when (val direct = record.value as Obj?) {
is FrameSlotRef -> direct.resolvedCaptureValueOrNull()
is RecordSlotRef -> direct.resolvedCaptureValueOrNull()
is ScopeSlotRef -> direct.resolvedCaptureValueOrNull()
else -> direct
}
}
fun write(value: Obj) {
val direct = record.value
if (direct is ScopeSlotRef) {

View File

@ -32,6 +32,7 @@ class ModuleScope(
pos: Pos = Pos.builtIn,
override val packageName: String
) : Scope(importProvider.rootScope, Arguments.EMPTY, pos) {
private fun ObjRecord.importedCopy(source: Scope): ObjRecord = copy(importedFrom = source)
constructor(importProvider: ImportProvider, source: Source) : this(importProvider, source.startPos, source.fileName)
@ -84,7 +85,7 @@ class ModuleScope(
override suspend fun importInto(scope: Scope, symbols: Map<String, String>?) {
val symbolsToImport = symbols?.keys?.toMutableSet()
for ((symbol, record) in this.objects) {
if (record.visibility.isPublic) {
if (record.visibility.isPublic && record.importedFrom == null) {
val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol]
?.also { symbolsToImport!!.remove(symbol) }
@ -94,21 +95,21 @@ class ModuleScope(
if (newName != null) {
val existing = scope.objects[newName]
if (existing != null) {
if (existing.importedFrom != record.importedFrom)
val sameBinding = existing.importedFrom == this
if (!sameBinding)
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
// already imported
} else {
// when importing records, we keep track of its package (not otherwise needed)
if (record.importedFrom == null) record.importedFrom = this
scope.objects[newName] = record
scope.updateSlotFor(newName, record)
val imported = record.importedCopy(this)
scope.objects[newName] = imported
scope.updateSlotFor(newName, imported)
}
}
}
}
for ((cls, map) in this.extensions) {
for ((symbol, record) in map) {
if (record.visibility.isPublic) {
if (record.visibility.isPublic && record.importedFrom == null) {
val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol]
?.also { symbolsToImport!!.remove(symbol) }
@ -116,7 +117,7 @@ class ModuleScope(
} ?: if (symbols == null) symbol else null
if (newName != null) {
scope.addExtension(cls, newName, record)
scope.addExtension(cls, newName, record.importedCopy(this))
}
}
}

View File

@ -181,37 +181,42 @@ class Script(
}
}
private fun importedBindingRecord(record: ObjRecord, source: Scope): ObjRecord =
record.copy(importedFrom = source)
private suspend fun seedImportBindings(scope: Scope, seedScope: Scope) {
val provider = scope.currentImportProvider
val importedModules = LinkedHashSet<ModuleScope>()
for (moduleRef in this.importedModules) {
importedModules.add(provider.prepareImport(moduleRef.pos, moduleRef.name, null))
}
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) {
val sourceScope: Scope
val baseRecord = when (val source = binding.source) {
is ImportBindingSource.Module -> {
val module = provider.prepareImport(source.pos, source.name, null)
importedModules.add(module)
sourceScope = module
module.objects[binding.symbol]?.takeIf { it.visibility.isPublic }
?: scope.raiseSymbolNotFound("symbol ${source.name}.${binding.symbol} not found")
}
ImportBindingSource.Root -> {
sourceScope = provider.rootScope
provider.rootScope.objects[binding.symbol]?.takeIf { it.visibility.isPublic }
?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found")
}
ImportBindingSource.Seed -> {
sourceScope = seedScope
findSeedRecord(seedScope, binding.symbol)
?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found")
}
}
val record = importedBindingRecord(baseRecord, sourceScope)
if (name == "Exception" && record.value !is ObjClass) {
scope.updateSlotFor(name, ObjRecord(ObjException.Root, isMutable = false))
scope.updateSlotFor(name, ObjRecord(ObjException.Root, isMutable = false, importedFrom = sourceScope))
} else {
scope.updateSlotFor(name, record)
}
@ -219,12 +224,15 @@ class Script(
for (module in importedModules) {
for ((cls, map) in module.extensions) {
for ((symbol, record) in map) {
if (record.visibility.isPublic) {
scope.addExtension(cls, symbol, record)
if (record.visibility.isPublic && record.importedFrom == null) {
scope.addExtension(cls, symbol, importedBindingRecord(record, module))
}
}
}
}
if (scope is ModuleScope) {
scope.importedModules = importedModules.toList()
}
}
private fun findSeedRecord(scope: Scope?, name: String): ObjRecord? {

View File

@ -53,7 +53,12 @@ class BytecodeStatement private constructor(
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is net.sergeych.lyng.obj.ObjProperty) {
scope.resolve(record, name)
} else {
record.value
when (val direct = record.value) {
is FrameSlotRef -> direct.read()
is RecordSlotRef -> direct.read(scope, name)
is ScopeSlotRef -> direct.read()
else -> direct
}
}
frame.frame.setObj(i, value)
}

View File

@ -2560,6 +2560,46 @@ private fun captureNamesForStatement(stmt: Statement?): List<String> {
return ordered.toList()
}
private fun freezeImmutableCaptureRecord(record: ObjRecord): ObjRecord {
val value = record.value as Obj?
if (record.isMutable || record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || value is ObjProperty) {
return record
}
return when (value) {
is FrameSlotRef -> value.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
is RecordSlotRef -> value.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
is ScopeSlotRef -> value.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
null -> record
else -> record.copy()
}
}
private fun isTransientCapturePlaceholder(value: Obj?): Boolean {
return when (value) {
null, ObjVoid -> true
is FrameSlotRef -> value.resolvedCaptureValueOrNull().let { it == null || it === ObjVoid }
is RecordSlotRef -> value.resolvedCaptureValueOrNull().let { it == null || it === ObjVoid }
is ScopeSlotRef -> value.resolvedCaptureValueOrNull().let { it == null || it === ObjVoid }
else -> false
}
}
private fun resolveStableCaptureRecord(scope: Scope, name: String): ObjRecord? {
val direct = scope.chainLookupIgnoreClosure(name, followClosure = true) ?: scope.get(name)
if (direct != null && !isTransientCapturePlaceholder(direct.value as Obj?)) {
return direct
}
var parent = scope.parent
while (parent != null) {
val candidate = parent.chainLookupIgnoreClosure(name, followClosure = true) ?: parent.get(name)
if (candidate != null && !isTransientCapturePlaceholder(candidate.value as Obj?)) {
return candidate
}
parent = parent.parent
}
return direct
}
private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List<String>): List<ObjRecord>? {
if (captureNames.isEmpty()) return null
val records = ArrayList<ObjRecord>(captureNames.size)
@ -2575,12 +2615,16 @@ private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List<Stri
}
} else {
val raw = frame.frame.getRawObj(localIndex)
val scoped = frame.scope.chainLookupIgnoreClosure(name, followClosure = true) ?: frame.scope.get(name)
if (scoped != null && scoped.value !== ObjUnset) {
records += scoped
val captureRecord = if (isTransientCapturePlaceholder(raw)) {
resolveStableCaptureRecord(frame.scope.parent ?: frame.scope, name)
} else {
resolveStableCaptureRecord(frame.scope, name)
}
if (captureRecord != null) {
records += freezeImmutableCaptureRecord(captureRecord)
continue
}
records += ObjRecord(FrameSlotRef(frame.frame, localIndex), isMutable)
records += freezeImmutableCaptureRecord(ObjRecord(FrameSlotRef(frame.frame, localIndex), isMutable))
}
continue
}
@ -2588,7 +2632,7 @@ private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List<Stri
if (scopeSlot >= 0) {
val target = frame.scopeTarget(scopeSlot)
val index = frame.fn.scopeSlotIndices[scopeSlot]
records += target.getSlotRecord(index)
records += freezeImmutableCaptureRecord(target.getSlotRecord(index))
continue
}
val scopeCaptures = frame.scope.captureRecords
@ -2605,7 +2649,7 @@ private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List<Stri
}
val scoped = frame.scope.chainLookupIgnoreClosure(name, followClosure = true) ?: frame.scope.get(name)
if (scoped != null) {
records += scoped
records += freezeImmutableCaptureRecord(scoped)
continue
}
frame.ensureScope().raiseSymbolNotFound("capture $name not found")
@ -3377,7 +3421,12 @@ class CmdGetMemberSlot(
resolved.declaringClass
)
} else {
resolved.value
when (val value = resolved.value) {
is FrameSlotRef -> value.read()
is RecordSlotRef -> value.read(frame.ensureScope(), name)
is ScopeSlotRef -> value.read()
else -> value
}
}
}
if (receiver is ObjQualifiedView) {
@ -3784,13 +3833,15 @@ class BytecodeLambdaCallable(
override val pos: Pos,
) : Statement(), BytecodeCallable {
private fun freezeRecord(record: ObjRecord): ObjRecord {
val frozenValue = when (val raw = record.value) {
is net.sergeych.lyng.FrameSlotRef -> raw.read()
is net.sergeych.lyng.RecordSlotRef -> raw.read()
is net.sergeych.lyng.ScopeSlotRef -> raw.read()
else -> raw
if (record.isMutable) return record
val raw = record.value as Obj?
return when (raw) {
is net.sergeych.lyng.FrameSlotRef -> raw.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
is net.sergeych.lyng.RecordSlotRef -> raw.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
is net.sergeych.lyng.ScopeSlotRef -> raw.resolvedCaptureValueOrNull()?.let { record.copy(value = it) } ?: record
null -> record
else -> record.copy()
}
return record.copy(value = frozenValue)
}
private fun resolveCaptureRecords(base: Scope): List<ObjRecord>? {
@ -3808,7 +3859,7 @@ class BytecodeLambdaCallable(
return BytecodeLambdaCallable(
fn = fn,
closureScope = newClosureScope,
captureRecords = resolveCaptureRecords(newClosureScope) ?: captureRecords,
captureRecords = captureRecords ?: resolveCaptureRecords(newClosureScope),
captureNames = captureNames,
paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration,
@ -3923,7 +3974,12 @@ class BytecodeLambdaCallable(
if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
context.resolve(record, name)
} else {
record.value
when (val direct = record.value) {
is FrameSlotRef -> direct.read()
is RecordSlotRef -> direct.read(context, name)
is ScopeSlotRef -> direct.read()
else -> direct
}
}
frame.frame.setObj(i, value)
}
@ -4090,7 +4146,31 @@ class CmdFrame(
frame.setObj(localIndex, record.delegate ?: ObjNull)
} else {
val value = record.value
if (value is FrameSlotRef) {
if (!record.isMutable && value is FrameSlotRef) {
val resolved = value.peekValue()
if (resolved != null) {
if (value.refersTo(frame, localIndex)) continue
frame.setObj(localIndex, value.read())
} else {
frame.setObj(localIndex, value)
}
} else if (!record.isMutable && value is RecordSlotRef) {
val resolved = value.peekValue()
if (resolved != null) {
frame.setObj(localIndex, value.read())
} else {
frame.setObj(localIndex, value)
}
} else if (!record.isMutable && value is ScopeSlotRef) {
val resolved = value.peekValue()
if (resolved != null) {
frame.setObj(localIndex, value.read())
} else {
frame.setObj(localIndex, value)
}
} else if (!record.isMutable) {
frame.setObj(localIndex, value)
} else if (value is FrameSlotRef) {
if (value.refersTo(frame, localIndex)) continue
frame.setObj(localIndex, value)
} else {
@ -4105,7 +4185,30 @@ class CmdFrame(
frame.setObj(idx, record.delegate ?: ObjNull)
} else {
val value = record.value
if (value is FrameSlotRef) {
if (!record.isMutable && value is FrameSlotRef) {
val resolved = value.peekValue()
if (resolved != null) {
frame.setObj(idx, value.read())
} else {
frame.setObj(idx, value)
}
} else if (!record.isMutable && value is RecordSlotRef) {
val resolved = value.peekValue()
if (resolved != null) {
frame.setObj(idx, value.read())
} else {
frame.setObj(idx, value)
}
} else if (!record.isMutable && value is ScopeSlotRef) {
val resolved = value.peekValue()
if (resolved != null) {
frame.setObj(idx, value.read())
} else {
frame.setObj(idx, value)
}
} else if (!record.isMutable) {
frame.setObj(idx, value)
} else if (value is FrameSlotRef) {
frame.setObj(idx, value)
} else {
frame.setObj(idx, RecordSlotRef(record))
@ -5084,6 +5187,7 @@ class CmdFrame(
when (obj) {
is FrameSlotRef -> obj.read()
is RecordSlotRef -> obj.read(scope, localName)
is ScopeSlotRef -> obj.read()
is ObjProperty -> resolvePropertyLikeLocal(localName, obj)
ObjUnset -> resolveUnsetLocal(localName)
else -> obj
@ -5095,6 +5199,7 @@ class CmdFrame(
when (obj) {
is FrameSlotRef -> obj.read()
is RecordSlotRef -> obj.read(scope, localName)
is ScopeSlotRef -> obj.read()
is ObjProperty -> resolvePropertyLikeLocal(localName, obj)
ObjUnset -> resolveUnsetLocal(localName)
else -> obj
@ -5119,7 +5224,24 @@ class CmdFrame(
if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
return scope.resolve(record, localName)
}
return record.value
return when (val value = record.value) {
is FrameSlotRef -> value.read()
is RecordSlotRef -> value.read(scope, localName)
is ScopeSlotRef -> value.read()
else -> value
}
}
private suspend fun readResolvedScopeRecord(target: Scope, name: String, record: ObjRecord): Obj {
val value = record.value
return when {
record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || value is ObjProperty ->
target.resolve(record, name)
value is FrameSlotRef -> value.read()
value is RecordSlotRef -> value.read(target, name)
value is ScopeSlotRef -> value.read()
else -> value
}
}
private suspend fun getScopeSlotValue(slot: Int): Obj {
@ -5134,10 +5256,7 @@ class CmdFrame(
if (name != null && record.memberName != null && record.memberName != name) {
val resolved = target.get(name)
if (resolved != null) {
val resolvedValue = resolved.value
if (resolved.type == ObjRecord.Type.Delegated || resolved.type == ObjRecord.Type.Property || resolvedValue is ObjProperty) {
return target.resolve(resolved, name)
}
val resolvedValue = readResolvedScopeRecord(target, name, resolved)
if (resolvedValue !== ObjUnset) {
target.updateSlotFor(name, resolved)
}
@ -5156,12 +5275,13 @@ class CmdFrame(
failMissingPreparedModuleBinding(slot, name, hadNamedBinding, record)
return record.value
}
if (resolved.value !== ObjUnset) {
val resolvedValue = readResolvedScopeRecord(target, name, resolved)
if (resolvedValue !== ObjUnset) {
target.updateSlotFor(name, resolved)
} else {
failMissingPreparedModuleBinding(slot, name, hadNamedBinding, resolved)
}
return resolved.value
return resolvedValue
}
private suspend fun getScopeSlotValueAtAddr(addrSlot: Int): Obj {
@ -5177,10 +5297,7 @@ class CmdFrame(
if (name != null && record.memberName != null && record.memberName != name) {
val resolved = target.get(name)
if (resolved != null) {
val resolvedValue = resolved.value
if (resolved.type == ObjRecord.Type.Delegated || resolved.type == ObjRecord.Type.Property || resolvedValue is ObjProperty) {
return target.resolve(resolved, name)
}
val resolvedValue = readResolvedScopeRecord(target, name, resolved)
if (resolvedValue !== ObjUnset) {
target.updateSlotFor(name, resolved)
}
@ -5199,12 +5316,13 @@ class CmdFrame(
failMissingPreparedModuleBinding(slotId, name, hadNamedBinding, record)
return record.value
}
if (resolved.value !== ObjUnset) {
val resolvedValue = readResolvedScopeRecord(target, name, resolved)
if (resolvedValue !== ObjUnset) {
target.updateSlotFor(name, resolved)
} else {
failMissingPreparedModuleBinding(slotId, name, hadNamedBinding, resolved)
}
return resolved.value
return resolvedValue
}
private suspend fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) {

View File

@ -34,7 +34,12 @@ internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) {
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is net.sergeych.lyng.obj.ObjProperty) {
scope.resolve(record, name)
} else {
record.value
when (val direct = record.value) {
is net.sergeych.lyng.FrameSlotRef -> direct.resolvedCaptureValueOrNull() ?: direct
is net.sergeych.lyng.RecordSlotRef -> direct.resolvedCaptureValueOrNull() ?: direct
is net.sergeych.lyng.ScopeSlotRef -> direct.resolvedCaptureValueOrNull() ?: direct
else -> direct
}
}
if (value is net.sergeych.lyng.FrameSlotRef && value.refersTo(frame.frame, i)) {
continue

View File

@ -16,17 +16,60 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Script
import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.toInt
import net.sergeych.lyng.pacman.ImportManager
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
class ScriptImportPreparationTest {
private fun nestedImportSources(prefix: String): Array<String> =
arrayOf(
"""
package $prefix.alpha
class Alpha {
val headers = Map<String, String>()
fun tagged(port: Int, host: String): String {
val task: Deferred = launch {
host + ":" + port + ":" + headers.size
}
return task.await()
}
}
fun alphaValue() = Alpha().tagged(7, "alpha")
""".trimIndent(),
"""
package $prefix.beta
import $prefix.alpha
fun betaValue() = alphaValue() + "|" + Alpha().tagged(8, "beta")
""".trimIndent(),
"""
package $prefix.gamma
import $prefix.alpha
import $prefix.beta
val String.gammaTag get() = this + ":gamma"
fun gammaValue() = betaValue() + "|" + "done".gammaTag
""".trimIndent()
)
private fun nestedImportManager(prefix: String = "tree"): ImportManager =
Script.defaultImportManager.copy().apply {
addTextPackages(*nestedImportSources(prefix))
}
@Test
fun scriptImportIntoExplicitlyPreparesExistingScope() = runTest {
val manager = ImportManager()
@ -84,4 +127,67 @@ class ScriptImportPreparationTest {
val record = assertNotNull(module["answer"])
assertEquals(42, module.resolve(record, "answer").toInt())
}
@Test
fun repeatedImportIntoOnSameScopeIsIdempotentForNestedPackageGraph() = runTest {
val manager = nestedImportManager()
val script = Compiler.compile(
Source(
"<repeat-import-into>",
"""
import tree.gamma
import tree.beta
import tree.alpha
gammaValue()
""".trimIndent()
),
manager
)
val scope = manager.newStdScope()
script.importInto(scope)
val importedGammaValue = assertNotNull(scope["gammaValue"])
val importedAlpha = assertNotNull(scope["Alpha"])
repeat(5) {
script.importInto(scope)
}
assertSame(importedGammaValue, scope["gammaValue"])
assertSame(importedAlpha, scope["Alpha"])
assertEquals(
"alpha:7:0|beta:8:0|done:gamma",
(script.execute(scope) as ObjString).value
)
}
@Test
fun repeatedEvalOnSameSessionCanReimportNestedPackageGraph() = runTest {
val prefix = "repeattree"
val manager = nestedImportManager(prefix)
val scope = manager.newModule()
val session = EvalSession(scope)
try {
repeat(5) { index ->
val result = session.eval(
Source(
"<repeat-eval-$index>",
"""
import $prefix.gamma
import $prefix.beta
import $prefix.alpha
gammaValue()
""".trimIndent()
)
) as ObjString
assertEquals("alpha:7:0|beta:8:0|done:gamma", result.value)
}
} finally {
session.cancelAndJoin()
}
}
}