From f37354f382f01544534ed566acd7776bf79ee920 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 4 Jul 2025 00:53:21 +0300 Subject: [PATCH] fix #36 basic import support --- .../kotlin/net/sergeych/lyng/Compiler.kt | 160 ++++++++++++------ .../net/sergeych/lyng/CompilerContext.kt | 2 +- .../kotlin/net/sergeych/lyng/Importer.kt | 130 ++++++++++++++ .../kotlin/net/sergeych/lyng/Obj.kt | 3 + .../kotlin/net/sergeych/lyng/Scope.kt | 22 +++ .../net/sergeych/lyng/SecurityManager.kt | 22 +++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 33 ++++ 7 files changed, 320 insertions(+), 52 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/Importer.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/SecurityManager.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 90bd5d4..a07964d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -5,24 +5,69 @@ package net.sergeych.lyng */ class Compiler( val cc: CompilerContext, + val pacman: Pacman = Pacman.emptyAllowAll, @Suppress("UNUSED_PARAMETER") settings: Settings = Settings() ) { + var packageName: String? = null class Settings - private fun parseScript(): Script { + private suspend fun parseScript(): Script { val statements = mutableListOf() val start = cc.currentPos() // val returnScope = cc.startReturnScope() - while (parseStatement( braceMeansLambda = true)?.also { + // package level declarations + do { + val t = cc.current() + if (t.type == Token.Type.ID) { + when (t.value) { + "package" -> { + cc.next() + val name = loadQualifiedName() + if (name.isEmpty()) throw ScriptError(cc.currentPos(), "Expecting package name here") + if (packageName != null) throw ScriptError( + cc.currentPos(), + "package name redefined, already set to $packageName" + ) + packageName = name + continue + } + "import" -> { + cc.next() + val pos = cc.currentPos() + val name = loadQualifiedName() + pacman.prepareImport(pos, name, null) + statements += statement { + pacman.performImport(this,name,null) + ObjVoid + } + } + } + } + parseStatement(braceMeansLambda = true)?.also { statements += it - } != null) {/**/ - } + } ?: break + + } while (true) return Script(start, statements)//returnScope.needCatch) } - private fun parseStatement(braceMeansLambda: Boolean = false): Statement? { + fun loadQualifiedName(): String { + val result = StringBuilder() + var t = cc.next() + while (t.type == Token.Type.ID) { + result.append(t.value) + t = cc.next() + if (t.type == Token.Type.DOT) { + result.append('.') + t = cc.next() + } + } + return result.toString() + } + + private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? { while (true) { val t = cc.next() return when (t.type) { @@ -70,15 +115,15 @@ class Compiler( } } - private fun parseExpression(): Statement? { + private suspend fun parseExpression(): Statement? { val pos = cc.currentPos() return parseExpressionLevel()?.let { a -> statement(pos) { a.getter(it).value } } } - private fun parseExpressionLevel(level: Int = 0): Accessor? { + private suspend fun parseExpressionLevel(level: Int = 0): Accessor? { if (level == lastLevel) return parseTerm() - var lvalue: Accessor? = parseExpressionLevel( level + 1) ?: return null + var lvalue: Accessor? = parseExpressionLevel(level + 1) ?: return null while (true) { @@ -89,7 +134,7 @@ class Compiler( break } - val rvalue = parseExpressionLevel( level + 1) + val rvalue = parseExpressionLevel(level + 1) ?: throw ScriptError(opToken.pos, "Expecting expression") lvalue = op.generate(opToken.pos, lvalue!!, rvalue) @@ -97,7 +142,7 @@ class Compiler( return lvalue } - private fun parseTerm(): Accessor? { + private suspend fun parseTerm(): Accessor? { var operand: Accessor? = null while (true) { @@ -178,7 +223,7 @@ class Compiler( if (x == ObjNull && isOptional) ObjNull.asReadonly else x.readField(context, next.value) }) { cxt, newValue -> - left.getter(cxt).value.writeField(cxt, next.value, newValue) + left.getter(cxt).value.writeField(cxt, next.value, newValue) } } } @@ -371,12 +416,15 @@ class Compiler( /** * Parse lambda expression, leading '{' is already consumed */ - private fun parseLambdaExpression(): Accessor { + private suspend fun parseLambdaExpression(): Accessor { // lambda args are different: val startPos = cc.currentPos() val argsDeclaration = parseArgsDeclaration() if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW) - throw ScriptError(startPos, "lambda must have either valid arguments declaration with '->' or no arguments") + throw ScriptError( + startPos, + "lambda must have either valid arguments declaration with '->' or no arguments" + ) val body = parseBlock(skipLeadingBrace = true) @@ -410,7 +458,7 @@ class Compiler( } } - private fun parseArrayLiteral(): List { + private suspend fun parseArrayLiteral(): List { // it should be called after Token.Type.LBRACKET is consumed val entries = mutableListOf() while (true) { @@ -452,7 +500,7 @@ class Compiler( * Parse argument declaration, used in lambda (and later in fn too) * @return declaration or null if there is no valid list of arguments */ - private fun parseArgsDeclaration(isClassDeclaration: Boolean = false): ArgsDeclaration? { + private suspend fun parseArgsDeclaration(isClassDeclaration: Boolean = false): ArgsDeclaration? { val result = mutableListOf() var endTokenType: Token.Type? = null val startPos = cc.savePos() @@ -559,7 +607,7 @@ class Compiler( * Parse arguments list during the call and detect last block argument * _following the parenthesis_ call: `(1,2) { ... }` */ - private fun parseArgs(): Pair, Boolean> { + private suspend fun parseArgs(): Pair, Boolean> { val args = mutableListOf() do { @@ -605,7 +653,7 @@ class Compiler( } - private fun parseFunctionCall( + private suspend fun parseFunctionCall( left: Accessor, blockArgument: Boolean, isOptional: Boolean @@ -716,7 +764,7 @@ class Compiler( * Parse keyword-starting statement. * @return parsed statement or null if, for example. [id] is not among keywords */ - private fun parseKeywordStatement(id: Token): Statement? = when (id.value) { + private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) { "val" -> parseVarDeclaration(false, Visibility.Public) "var" -> parseVarDeclaration(true, Visibility.Public) "while" -> parseWhileStatement() @@ -734,13 +782,13 @@ class Compiler( cc.previous() val isExtern = cc.skipId("extern") when { - cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration( Visibility.Private, isExtern) - cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration( Visibility.Private, isExtern) - cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration( isOpen = true, isExtern = isExtern) - cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration( isOpen = true, isExtern = isExtern) + cc.matchQualifiers("fun", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern) + cc.matchQualifiers("fn", "private") -> parseFunctionDeclaration(Visibility.Private, isExtern) + cc.matchQualifiers("fun", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) + cc.matchQualifiers("fn", "open") -> parseFunctionDeclaration(isOpen = true, isExtern = isExtern) - cc.matchQualifiers("fun") -> parseFunctionDeclaration( isOpen = false, isExtern = isExtern) - cc.matchQualifiers("fn") -> parseFunctionDeclaration( isOpen = false, isExtern = isExtern) + cc.matchQualifiers("fun") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern) + cc.matchQualifiers("fn") -> parseFunctionDeclaration(isOpen = false, isExtern = isExtern) cc.matchQualifiers("val", "private") -> parseVarDeclaration(false, Visibility.Private) cc.matchQualifiers("var", "private") -> parseVarDeclaration(true, Visibility.Private) @@ -756,7 +804,7 @@ class Compiler( data class WhenCase(val condition: Statement, val block: Statement) - private fun parseWhenStatement(): Statement { + private suspend fun parseWhenStatement(): Statement { // has a value, when(value) ? var t = cc.skipWsTokens() return if (t.type == Token.Type.LPAREN) { @@ -822,7 +870,10 @@ class Compiler( "when else block already defined" ) elseCase = - parseStatement() ?: throw ScriptError(cc.currentPos(), "when else block expected") + parseStatement() ?: throw ScriptError( + cc.currentPos(), + "when else block expected" + ) skipParseBody = true } else { cc.previous() @@ -861,7 +912,7 @@ class Compiler( } } - private fun parseThrowStatement(): Statement { + private suspend fun parseThrowStatement(): Statement { val throwStatement = parseStatement() ?: throw ScriptError(cc.currentPos(), "throw object expected") return statement { var errorObject = throwStatement.execute(this) @@ -879,7 +930,7 @@ class Compiler( val block: Statement ) - private fun parseTryStatement(): Statement { + private suspend fun parseTryStatement(): Statement { val body = parseBlock() val catches = mutableListOf() cc.skipTokens(Token.Type.NEWLINE) @@ -983,11 +1034,11 @@ class Compiler( } } - private fun parseClassDeclaration(isStruct: Boolean): Statement { + private suspend fun parseClassDeclaration(isStruct: Boolean): Statement { val nameToken = cc.requireToken(Token.Type.ID) val constructorArgsDeclaration = if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) - parseArgsDeclaration( isClassDeclaration = true) + parseArgsDeclaration(isClassDeclaration = true) else null if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN) @@ -1059,7 +1110,7 @@ class Compiler( return found } - private fun parseForStatement(): Statement { + private suspend fun parseForStatement(): Statement { val label = getLabel()?.also { cc.labels += it } val start = ensureLparen() @@ -1210,7 +1261,7 @@ class Compiler( } @Suppress("UNUSED_VARIABLE") - private fun parseDoWhileStatement(): Statement { + private suspend fun parseDoWhileStatement(): Statement { val label = getLabel()?.also { cc.labels += it } val (breakFound, body) = cc.parseLoop { parseStatement() ?: throw ScriptError(cc.currentPos(), "Bad while statement: expected statement") @@ -1262,7 +1313,7 @@ class Compiler( } } - private fun parseWhileStatement(): Statement { + private suspend fun parseWhileStatement(): Statement { val label = getLabel()?.also { cc.labels += it } val start = ensureLparen() val condition = @@ -1304,7 +1355,7 @@ class Compiler( } } - private fun parseBreakStatement(start: Pos): Statement { + private suspend fun parseBreakStatement(start: Pos): Statement { var t = cc.next() val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) { @@ -1376,7 +1427,7 @@ class Compiler( return t.pos } - private fun parseIfStatement(): Statement { + private suspend fun parseIfStatement(): Statement { val start = ensureLparen() val condition = parseExpression() @@ -1411,7 +1462,7 @@ class Compiler( } } - private fun parseFunctionDeclaration( + private suspend fun parseFunctionDeclaration( visibility: Visibility = Visibility.Public, @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false, isExtern: Boolean = false @@ -1425,11 +1476,11 @@ class Compiler( t = cc.next() // Is extension? - if( t.type == Token.Type.DOT) { - extTypeName = name - t = cc.next() - if( t.type != Token.Type.ID) - throw ScriptError(t.pos, "illegal extension format: expected function name") + if (t.type == Token.Type.DOT) { + extTypeName = name + t = cc.next() + if (t.type != Token.Type.ID) + throw ScriptError(t.pos, "illegal extension format: expected function name") name = t.value t = cc.next() } @@ -1462,7 +1513,7 @@ class Compiler( // load params from caller context argsDeclaration.assignToContext(context, callerContext.args, defaultAccessType = AccessType.Val) - if( extTypeName != null ) { + if (extTypeName != null) { context.thisObj = callerContext.thisObj } fnStatements.execute(context) @@ -1474,12 +1525,12 @@ class Compiler( extTypeName?.let { typeName -> // class extension method val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found") - if( type !is ObjClass ) context.raiseClassCastError("$typeName is not the class instance") - type.addFn( name, isOpen = true) { + if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance") + type.addFn(name, isOpen = true) { fnBody.execute(this) } } - // regular function/method + // regular function/method ?: context.addItem(name, false, fnBody, visibility) // as the function can be called from anywhere, we have // saved the proper context in the closure @@ -1487,7 +1538,7 @@ class Compiler( } } - private fun parseBlock(skipLeadingBrace: Boolean = false): Statement { + private suspend fun parseBlock(skipLeadingBrace: Boolean = false): Statement { val startPos = cc.currentPos() if (!skipLeadingBrace) { val t = cc.next() @@ -1505,7 +1556,7 @@ class Compiler( } } - private fun parseVarDeclaration( + private suspend fun parseVarDeclaration( isMutable: Boolean, visibility: Visibility, @Suppress("UNUSED_PARAMETER") isOpen: Boolean = false @@ -1527,7 +1578,7 @@ class Compiler( } } - val initialExpression = if (setNull) null else parseStatement( true) + val initialExpression = if (setNull) null else parseStatement(true) ?: throw ScriptError(eqToken.pos, "Expected initializer expression") return statement(nameToken.pos) { context -> @@ -1561,8 +1612,15 @@ class Compiler( companion object { - fun compile(source: Source): Script { - return Compiler(CompilerContext(parseLyng(source))).parseScript() + suspend fun compile(source: Source,pacman: Pacman = Pacman.emptyAllowAll): Script { + return Compiler(CompilerContext(parseLyng(source)),pacman).parseScript() + } + + suspend fun compilePackage(source: Source): Pair { + val c = Compiler(CompilerContext(parseLyng(source))) + val script = c.parseScript() + if (c.packageName == null) throw ScriptError(source.startPos, "package not set") + return c.packageName!! to script } private var lastPriority = 0 @@ -1685,7 +1743,7 @@ class Compiler( allOps.filter { it.priority == l }.associateBy { it.tokenType } } - fun compile(code: String): Script = compile(Source("", code)) + suspend fun compile(code: String): Script = compile(Source("", code)) /** * The keywords that stop processing of expression term diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt index 941b449..92c2167 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt @@ -97,7 +97,7 @@ class CompilerContext(val tokens: List) { } - fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean { + inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean { val t = next() return if (t.type == typeId) { f(t) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Importer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Importer.kt new file mode 100644 index 0000000..20fe876 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Importer.kt @@ -0,0 +1,130 @@ +package net.sergeych.lyng + +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.sergeych.mp_tools.globalDefer + +/** + * Package manager. Chained manager, too simple. Override [createModuleScope] to return either + * valid [ModuleScope] or call [parent] - or return null. + */ +abstract class Pacman( + val parent: Pacman? = null, + val rootScope: Scope = parent!!.rootScope, + val securityManager: SecurityManager = parent!!.securityManager +) { + private val opScopes = Mutex() + + private val cachedScopes = mutableMapOf() + + /** + * Create a new module scope if this pacman can import the module, return null otherwise so + * the manager can decide what to do + */ + abstract suspend fun createModuleScope(name: String): ModuleScope? + + suspend fun prepareImport(pos: Pos, name: String, symbols: Map?) { + if (!securityManager.canImportModule(name)) + throw ImportException(pos, "Module $name is not allowed") + symbols?.keys?.forEach { + if (!securityManager.canImportSymbol(name, it)) throw ImportException( + pos, + "Symbol $name.$it is not allowed" + ) + } + // if we can import the module, cache it, or go further + opScopes.withLock { + cachedScopes[name] ?: createModuleScope(name)?.let { cachedScopes[name] = it } + } ?: parent?.prepareImport(pos, name, symbols) ?: throw ImportException(pos, "Module $name is not found") + } + + suspend fun performImport(scope: Scope, name: String, symbols: Map?) { + val module = opScopes.withLock { cachedScopes[name] } + ?: scope.raiseSymbolNotFound("module $name not found") + val symbolsToImport = symbols?.keys?.toMutableSet() + for ((symbol, record) in module.objects) { + if (record.visibility.isPublic) { + println("import $name: $symbol: $record") + val newName = symbols?.let { ss: Map -> + ss[symbol] + ?.also { symbolsToImport!!.remove(it) } + ?: scope.raiseError("internal error: symbol $symbol not found though the module is cached") + } ?: symbol + println("import $name.$symbol as $newName") + if (newName in scope.objects) + scope.raiseError("symbol $newName already exists, redefinition on import is not allowed") + scope.objects[newName] = record + } + } + if (!symbolsToImport.isNullOrEmpty()) + scope.raiseSymbolNotFound("symbols $name.{$symbolsToImport} are.were not found") + } + + companion object { + val emptyAllowAll = object : Pacman(rootScope = Script.defaultScope, securityManager = SecurityManager.allowAll) { + override suspend fun createModuleScope(name: String): ModuleScope? { + return null + } + + } + } + +} + +/** + * Module scope supports importing and contains the [pacman]; it should be the same + * used in [Compiler]; + */ +class ModuleScope( + val pacman: Pacman, + pos: Pos = Pos.builtIn, + val packageName: String +) : Scope(pacman.rootScope, Arguments.EMPTY, pos) { + + constructor(pacman: Pacman,source: Source) : this(pacman, source.startPos, source.fileName) + + override suspend fun checkImport(pos: Pos, name: String, symbols: Map?) { + pacman.prepareImport(pos, name, symbols) + } + + /** + * Import symbols into the scope. It _is called_ after the module is imported by [checkImport]. + * If [checkImport] was not called, the symbols will not be imported with exception as module is not found. + */ + override suspend fun importInto(scope: Scope, name: String, symbols: Map?) { + pacman.performImport(scope, name, symbols) + } + + val packageNameObj by lazy { ObjString(packageName).asReadonly} + + override fun get(name: String): ObjRecord? { + return if( name == "__PACKAGE__") + packageNameObj + else + super.get(name) + } +} + + +class InlineSourcesPacman(pacman: Pacman,val sources: List) : Pacman(pacman) { + + val modules: Deferred>> = globalDefer { + val result = mutableMapOf>() + for (source in sources) { + // retrieve the module name and script for deferred execution: + val (name, script) = Compiler.compilePackage(source) + // scope is created used pacman's root scope: + val scope = ModuleScope(this@InlineSourcesPacman, source.startPos, name) + // we execute scripts in parallel which allow cross-imports to some extent: + result[name] = globalDefer { script.execute(scope); scope } + } + result + } + + override suspend fun createModuleScope(name: String): ModuleScope? = + modules.await()[name]?.await() + +} + + diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index b851bf2..93e5391 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -510,3 +510,6 @@ class ObjAccessException(scope: Scope, message: String = "access not allowed err class ObjUnknownException(scope: Scope, message: String = "access not allowed error") : ObjException("UnknownException", scope, message) + +class ObjIllegalOperationException(scope: Scope, message: String = "Operation is illegal") : + ObjException("IllegalOperationException", scope, message) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 54c734f..3770219 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -1,5 +1,14 @@ package net.sergeych.lyng +/** + * 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. + * + * There are special types of scopes: + * + * - [Script.defaultScope] - root scope for a script, safe one + * - [AppliedScope] - scope used to apply a closure to some thisObj scope + */ open class Scope( val parent: Scope?, val args: Arguments = Arguments.EMPTY, @@ -125,6 +134,19 @@ open class Scope( suspend fun eval(code: String): Obj = Compiler.compile(code.toSource()).execute(this) + suspend fun eval(source: Source): Obj = + Compiler.compile( + source, + (this as? ModuleScope)?.pacman?.also { println("pacman found: $pacman")} ?: Pacman.emptyAllowAll + ).execute(this) + fun containsLocal(name: String): Boolean = name in objects + open suspend fun checkImport(pos: Pos, name: String, symbols: Map? = null) { + throw ImportException(pos, "Import is not allowed here: $name") + } + + open suspend fun importInto(scope: Scope, name: String, symbols: Map? = null) { + scope.raiseError(ObjIllegalOperationException(scope,"Import is not allowed here: import $name")) + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/SecurityManager.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/SecurityManager.kt new file mode 100644 index 0000000..e3e622b --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/SecurityManager.kt @@ -0,0 +1,22 @@ +package net.sergeych.lyng + +interface SecurityManager { + /** + * Check that any symbol from the corresponding module can be imported. If it returns false, + * the module will not be imported and no further processing will be done + */ + fun canImportModule(name: String): Boolean + + /**\ + * if [canImportModule] this method allows fine-grained control over symbols. + */ + fun canImportSymbol(moduleName: String, symbolName: String): Boolean = true + + companion object { + val allowAll: SecurityManager = object : SecurityManager { + override fun canImportModule(name: String): Boolean = true + override fun canImportSymbol(moduleName: String, symbolName: String): Boolean = true + } + } + +} \ No newline at end of file diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index e770d42..a4f0267 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -2332,4 +2332,37 @@ class ScriptTest { """) listOf(1,2,3).associateBy { it * 10 } } + + @Test + fun testImports1() = runTest() { + val foosrc = """ + package lyng.foo + + fun foo() { "foo1" } + """.trimIndent() + val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc))) + assertNotNull(pm.modules.await()["lyng.foo"]) + assertIs(pm.modules.await()["lyng.foo"]!!.await()) + + assertEquals("foo1", pm.modules.await()["lyng.foo"]!!.await().eval("foo()").toString()) + } + + @Test + fun testImports2() = runTest() { + val foosrc = """ + package lyng.foo + + fun foo() { "foo1" } + """.trimIndent() + val pm = InlineSourcesPacman(Pacman.emptyAllowAll, listOf(Source("foosrc", foosrc))) + + val src = """ + import lyng.foo + + foo() + """.trimIndent().toSource("test") + + val scope = ModuleScope(pm, src) + assertEquals("foo1", scope.eval(src).toString()) + } } \ No newline at end of file