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) 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 @Test
fun cliDiscoversSiblingAndNestedLocalImportsFromEntryRoot() { fun cliDiscoversSiblingAndNestedLocalImportsFromEntryRoot() {
val dir = Files.createTempDirectory("lyng_cli_local_imports_") val dir = Files.createTempDirectory("lyng_cli_local_imports_")
@ -134,4 +201,37 @@ class CliLocalImportsJvmTest {
dir.toFile().deleteRecursively() 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 resolvedRecords = ArrayList<ObjRecord>(captureSlots.size)
val resolvedNames = ArrayList<String>(captureSlots.size) val resolvedNames = ArrayList<String>(captureSlots.size)
for (capture in captureSlots) { for (capture in captureSlots) {
val rec = closureScope.chainLookupIgnoreClosure( val rec = resolveStableCaptureRecord(
closureScope,
capture.name, capture.name,
followClosure = true, context.currentClassCtx
caller = context.currentClassCtx
) ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found") ) ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
resolvedRecords.add(rec) resolvedRecords.add(freezeImmutableCaptureRecord(rec))
resolvedNames.add(capture.name) resolvedNames.add(capture.name)
} }
context.captureRecords = resolvedRecords 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( private suspend fun parseFunctionDeclaration(
visibility: Visibility = Visibility.Public, visibility: Visibility = Visibility.Public,
isAbstract: Boolean = false, isAbstract: Boolean = false,
@ -9147,12 +9187,12 @@ class Compiler(
} else if (captureBase != null && captureNames.isNotEmpty()) { } else if (captureBase != null && captureNames.isNotEmpty()) {
val resolvedRecords = ArrayList<ObjRecord>(captureNames.size) val resolvedRecords = ArrayList<ObjRecord>(captureNames.size)
for (name in captureNames) { for (name in captureNames) {
val rec = captureBase.chainLookupIgnoreClosure( val rec = resolveStableCaptureRecord(
captureBase,
name, name,
followClosure = true, context.currentClassCtx
caller = context.currentClassCtx
) ?: captureBase.raiseSymbolNotFound("symbol $name not found") ) ?: captureBase.raiseSymbolNotFound("symbol $name not found")
resolvedRecords.add(rec) resolvedRecords.add(freezeImmutableCaptureRecord(rec))
} }
context.captureRecords = resolvedRecords context.captureRecords = resolvedRecords
context.captureNames = captureNames 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) { val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty) {
context.resolve(record, localName) context.resolve(record, localName)
} else { } 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) 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) { fun write(value: Obj) {
when (value) { when (value) {
is ObjInt -> frame.setInt(slot, value.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) { fun write(value: Obj) {
scope.setSlotValue(slot, value) 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) { fun write(value: Obj) {
val direct = record.value val direct = record.value
if (direct is ScopeSlotRef) { if (direct is ScopeSlotRef) {

View File

@ -32,6 +32,7 @@ class ModuleScope(
pos: Pos = Pos.builtIn, pos: Pos = Pos.builtIn,
override val packageName: String override val packageName: String
) : Scope(importProvider.rootScope, Arguments.EMPTY, pos) { ) : 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) 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>?) { override suspend fun importInto(scope: Scope, symbols: Map<String, String>?) {
val symbolsToImport = symbols?.keys?.toMutableSet() val symbolsToImport = symbols?.keys?.toMutableSet()
for ((symbol, record) in this.objects) { 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> -> val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol] ss[symbol]
?.also { symbolsToImport!!.remove(symbol) } ?.also { symbolsToImport!!.remove(symbol) }
@ -94,21 +95,21 @@ class ModuleScope(
if (newName != null) { if (newName != null) {
val existing = scope.objects[newName] val existing = scope.objects[newName]
if (existing != null) { 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") scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
// already imported // already imported
} else { } else {
// when importing records, we keep track of its package (not otherwise needed) val imported = record.importedCopy(this)
if (record.importedFrom == null) record.importedFrom = this scope.objects[newName] = imported
scope.objects[newName] = record scope.updateSlotFor(newName, imported)
scope.updateSlotFor(newName, record)
} }
} }
} }
} }
for ((cls, map) in this.extensions) { for ((cls, map) in this.extensions) {
for ((symbol, record) in map) { for ((symbol, record) in map) {
if (record.visibility.isPublic) { if (record.visibility.isPublic && record.importedFrom == null) {
val newName = symbols?.let { ss: Map<String, String> -> val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol] ss[symbol]
?.also { symbolsToImport!!.remove(symbol) } ?.also { symbolsToImport!!.remove(symbol) }
@ -116,7 +117,7 @@ class ModuleScope(
} ?: if (symbols == null) symbol else null } ?: if (symbols == null) symbol else null
if (newName != 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) { private suspend fun seedImportBindings(scope: Scope, seedScope: Scope) {
val provider = scope.currentImportProvider val provider = scope.currentImportProvider
val importedModules = LinkedHashSet<ModuleScope>() val importedModules = LinkedHashSet<ModuleScope>()
for (moduleRef in this.importedModules) { for (moduleRef in this.importedModules) {
importedModules.add(provider.prepareImport(moduleRef.pos, moduleRef.name, null)) importedModules.add(provider.prepareImport(moduleRef.pos, moduleRef.name, null))
} }
if (scope is ModuleScope) {
scope.importedModules = importedModules.toList()
}
for (module in importedModules) { for (module in importedModules) {
module.importInto(scope, null) module.importInto(scope, null)
} }
for ((name, binding) in importBindings) { 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 -> { is ImportBindingSource.Module -> {
val module = provider.prepareImport(source.pos, source.name, null) val module = provider.prepareImport(source.pos, source.name, null)
importedModules.add(module) importedModules.add(module)
sourceScope = module
module.objects[binding.symbol]?.takeIf { it.visibility.isPublic } module.objects[binding.symbol]?.takeIf { it.visibility.isPublic }
?: scope.raiseSymbolNotFound("symbol ${source.name}.${binding.symbol} not found") ?: scope.raiseSymbolNotFound("symbol ${source.name}.${binding.symbol} not found")
} }
ImportBindingSource.Root -> { ImportBindingSource.Root -> {
sourceScope = provider.rootScope
provider.rootScope.objects[binding.symbol]?.takeIf { it.visibility.isPublic } provider.rootScope.objects[binding.symbol]?.takeIf { it.visibility.isPublic }
?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found") ?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found")
} }
ImportBindingSource.Seed -> { ImportBindingSource.Seed -> {
sourceScope = seedScope
findSeedRecord(seedScope, binding.symbol) findSeedRecord(seedScope, binding.symbol)
?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found") ?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found")
} }
} }
val record = importedBindingRecord(baseRecord, sourceScope)
if (name == "Exception" && record.value !is ObjClass) { 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 { } else {
scope.updateSlotFor(name, record) scope.updateSlotFor(name, record)
} }
@ -219,12 +224,15 @@ class Script(
for (module in importedModules) { for (module in importedModules) {
for ((cls, map) in module.extensions) { for ((cls, map) in module.extensions) {
for ((symbol, record) in map) { for ((symbol, record) in map) {
if (record.visibility.isPublic) { if (record.visibility.isPublic && record.importedFrom == null) {
scope.addExtension(cls, symbol, record) scope.addExtension(cls, symbol, importedBindingRecord(record, module))
} }
} }
} }
} }
if (scope is ModuleScope) {
scope.importedModules = importedModules.toList()
}
} }
private fun findSeedRecord(scope: Scope?, name: String): ObjRecord? { 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) { 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) scope.resolve(record, name)
} else { } 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) frame.frame.setObj(i, value)
} }

View File

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

View File

@ -16,17 +16,60 @@
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Compiler import net.sergeych.lyng.Compiler
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Script import net.sergeych.lyng.Script
import net.sergeych.lyng.Source import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.toInt import net.sergeych.lyng.obj.toInt
import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportManager
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertSame
class ScriptImportPreparationTest { 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 @Test
fun scriptImportIntoExplicitlyPreparesExistingScope() = runTest { fun scriptImportIntoExplicitlyPreparesExistingScope() = runTest {
val manager = ImportManager() val manager = ImportManager()
@ -84,4 +127,67 @@ class ScriptImportPreparationTest {
val record = assertNotNull(module["answer"]) val record = assertNotNull(module["answer"])
assertEquals(42, module.resolve(record, "answer").toInt()) 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()
}
}
} }