fixed double imports bug

added clean import scope Scope.new()
This commit is contained in:
Sergey Chernov 2025-07-10 12:41:10 +03:00
parent 230cb0a067
commit 9771b40c98
7 changed files with 102 additions and 19 deletions

View File

@ -7,10 +7,15 @@ import net.sergeych.lyng.pacman.ImportProvider
*/ */
class Compiler( class Compiler(
val cc: CompilerContext, val cc: CompilerContext,
val importManager: ImportProvider = Script.defaultImportManager, val importManager: ImportProvider,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
settings: Settings = Settings() settings: Settings = Settings()
) { ) {
init {
println("Compiler initialized: $importManager")
}
var packageName: String? = null var packageName: String? = null
class Settings class Settings
@ -1618,8 +1623,8 @@ class Compiler(
companion object { companion object {
suspend fun compile(source: Source, importProvider: ImportProvider = Script.defaultImportManager): Script { suspend fun compile(source: Source,importManager: ImportProvider): Script {
return Compiler(CompilerContext(parseLyng(source)),importProvider).parseScript() return Compiler(CompilerContext(parseLyng(source)),importManager).parseScript()
} }
private var lastPriority = 0 private var lastPriority = 0
@ -1742,7 +1747,7 @@ class Compiler(
allOps.filter { it.priority == l }.associateBy { it.tokenType } allOps.filter { it.priority == l }.associateBy { it.tokenType }
} }
suspend fun compile(code: String): Script = compile(Source("<eval>", code)) suspend fun compile(code: String): Script = compile(Source("<eval>", code), Script.defaultImportManager)
/** /**
* The keywords that stop processing of expression term * The keywords that stop processing of expression term

View File

@ -7,7 +7,7 @@ import net.sergeych.lyng.pacman.ImportProvider
* used in [Compiler]; * used in [Compiler];
*/ */
class ModuleScope( class ModuleScope(
val importProvider: ImportProvider, var importProvider: ImportProvider,
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) {
@ -29,9 +29,17 @@ class ModuleScope(
?.also { symbolsToImport!!.remove(it) } ?.also { symbolsToImport!!.remove(it) }
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached") ?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
} ?: symbol } ?: symbol
if (newName in scope.objects) val existing = scope.objects[newName]
scope.raiseError("symbol $newName already exists, redefinition on import is not allowed") if (existing != null ) {
scope.objects[newName] = record if (existing.importedFrom != record.importedFrom)
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
}
} }
} }
if (!symbolsToImport.isNullOrEmpty()) if (!symbolsToImport.isNullOrEmpty())

View File

@ -15,8 +15,13 @@ import kotlin.contracts.ExperimentalContracts
data class ObjRecord( data class ObjRecord(
var value: Obj, var value: Obj,
val isMutable: Boolean, val isMutable: Boolean,
val visibility: Visibility = Visibility.Public val visibility: Visibility = Visibility.Public,
) var importedFrom: Scope? = null
) {
@Suppress("unused")
fun qualifiedName(name: String): String =
"${importedFrom?.packageName ?: "anonymous"}.$name"
}
/** /**
* When we need read-write access to an object in some abstract storage, we need Accessor, * When we need read-write access to an object in some abstract storage, we need Accessor,

View File

@ -1,15 +1,16 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ImportProvider
/** /**
* Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes. * Scope is where local variables and methods are stored. Scope is also a parent scope for other scopes.
* Each block usually creates a scope. Accessing Lyng closures usually is done via a scope. * Each block usually creates a scope. Accessing Lyng closures usually is done via a scope.
* *
* To create default scope, use default `Scope()` constructor, it will create a scope with a parent * To create default scope, use default `Scope()` constructor, it will create a scope with a parent
* module scope with default [ImportManager], you can access with [importManager] as needed. * module scope with default [ImportManager], you can access with [currentImportProvider] as needed.
* *
* If you want to create [ModuleScope] by hand, try [importManager] and [ImportManager.newModule], * If you want to create [ModuleScope] by hand, try [currentImportProvider] and [ImportManager.newModule],
* or [ImportManager.newModuleAt]. * or [ImportManager.newModuleAt].
* *
* There are special types of scopes: * There are special types of scopes:
@ -141,12 +142,12 @@ open class Scope(
fun addConst(name: String, value: Obj) = addItem(name, false, value) fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj = suspend fun eval(code: String): Obj =
Compiler.compile(code.toSource()).execute(this) Compiler.compile(code.toSource(), currentImportProvider).execute(this)
suspend fun eval(source: Source): Obj = suspend fun eval(source: Source): Obj =
Compiler.compile( Compiler.compile(
source, source,
(this as? ModuleScope)?.importProvider ?: Script.defaultImportManager currentImportProvider
).execute(this) ).execute(this)
fun containsLocal(name: String): Boolean = name in objects fun containsLocal(name: String): Boolean = name in objects
@ -168,10 +169,19 @@ open class Scope(
* @throws IllegalStateException if there is no such manager (if you create some specific scope with no manager, * @throws IllegalStateException if there is no such manager (if you create some specific scope with no manager,
* then you knew what you did) * then you knew what you did)
*/ */
val importManager: ImportManager by lazy { val currentImportProvider: ImportProvider by lazy {
if( this is ModuleScope ) if (this is ModuleScope)
(importProvider as? ImportManager)?.let { return@lazy it } importProvider.getActualProvider()
parent?.importManager ?: throw IllegalStateException("this scope has no manager in the chain") else
parent?.currentImportProvider ?: throw IllegalStateException("this scope has no manager in the chain")
} }
val importManager by lazy { (currentImportProvider as? ImportManager)
?: throw IllegalStateException("this scope has no manager in the chain (provided $currentImportProvider") }
companion object {
fun new(): Scope =
Script.defaultImportManager.copy().newModuleAt(Pos.builtIn)
}
} }

View File

@ -19,6 +19,8 @@ class ImportManager(
securityManager: SecurityManager = SecurityManager.allowAll securityManager: SecurityManager = SecurityManager.allowAll
) : ImportProvider(rootScope, securityManager) { ) : ImportProvider(rootScope, securityManager) {
val packageNames: List<String> get() = imports.keys.toList()
private inner class Entry( private inner class Entry(
val packageName: String, val packageName: String,
val builder: suspend (ModuleScope) -> Unit, val builder: suspend (ModuleScope) -> Unit,
@ -43,6 +45,10 @@ class ImportManager(
override suspend fun createModuleScope(pos: Pos, packageName: String): ModuleScope { override suspend fun createModuleScope(pos: Pos, packageName: String): ModuleScope {
return doImport(packageName, pos) return doImport(packageName, pos)
} }
override fun getActualProvider(): ImportProvider {
return this@ImportManager
}
} }
/** /**
@ -95,6 +101,7 @@ class ImportManager(
*/ */
private suspend fun doImport(packageName: String, pos: Pos): ModuleScope { private suspend fun doImport(packageName: String, pos: Pos): ModuleScope {
val entry = imports[packageName] ?: throw ImportException(pos, "package not found: $packageName") val entry = imports[packageName] ?: throw ImportException(pos, "package not found: $packageName")
println("import enrty found: $packageName")
return entry.getScope(pos) return entry.getScope(pos)
} }
@ -125,4 +132,11 @@ class ImportManager(
} }
} }
fun copy(): ImportManager =
op.withLock {
ImportManager(rootScope, securityManager).apply {
imports.putAll(this@ImportManager.imports)
}
}
} }

View File

@ -14,6 +14,9 @@ abstract class ImportProvider(
val rootScope: Scope, val rootScope: Scope,
val securityManager: SecurityManager = SecurityManager.allowAll val securityManager: SecurityManager = SecurityManager.allowAll
) { ) {
open fun getActualProvider() = this
/** /**
* Find an import and create a scope for it. This method must implement caching so repeated * Find an import and create a scope for it. This method must implement caching so repeated
* imports are not repeatedly loaded and parsed and should be cheap. * imports are not repeatedly loaded and parsed and should be cheap.

View File

@ -2443,7 +2443,7 @@ class ScriptTest {
@Test @Test
fun testDefaultImportManager() = runTest { fun testDefaultImportManager() = runTest {
val scope = Scope() val scope = Scope.new()
assertFails { assertFails {
scope.eval(""" scope.eval("""
import foo import foo
@ -2574,4 +2574,42 @@ class ScriptTest {
) )
} }
@Test
fun testDoubleImports() = runTest {
val s = Scope.new()
println(Script.defaultImportManager.packageNames)
println(s.importManager.packageNames)
s.importManager.addTextPackages("""
package foo
import lyng.time
fun foo() {
println("foo: %s"(Instant()))
}
""".trimIndent())
s.importManager.addTextPackages("""
package bar
import lyng.time
fun bar() {
println("bar: %s"(Instant()))
}
""".trimIndent())
println(s.importManager.packageNames)
s.eval("""
import foo
import bar
foo()
bar()
""".trimIndent())
}
} }