Added support for extern declarations and enhanced .lyng.d merging

- Implemented `extern` support for functions, classes, objects, enums, and properties in the `MiniAST`.
- Updated `MiniAST` to include `isExtern` field for applicable nodes.
- Enabled merging of `.lyng.d` declaration files into main `.lyng` scripts.
- Adjusted tests to validate `extern` behavior and documentation handling.
- Minor fixes to parser logic for improved robustness.
This commit is contained in:
Sergey Chernov 2026-01-06 17:04:56 +01:00
parent 44675b976d
commit fdc044d1e0
8 changed files with 310 additions and 95 deletions

View File

@ -19,6 +19,7 @@ package net.sergeych.lyng.idea.util
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Source
@ -45,7 +46,11 @@ object LyngAstManager {
val provider = IdeLenientImportProvider.create()
val src = Source(file.name, text)
runBlocking { Compiler.compileWithMini(src, provider, sink) }
sink.build()
val script = sink.build()
if (script != null && !file.name.endsWith(".lyng.d")) {
mergeDeclarationFiles(file, script)
}
script
} catch (_: Throwable) {
sink.build()
}
@ -59,6 +64,26 @@ object LyngAstManager {
return built
}
private fun mergeDeclarationFiles(file: PsiFile, mainScript: MiniScript) {
val psiManager = PsiManager.getInstance(file.project)
var current = file.virtualFile?.parent
val seen = mutableSetOf<String>()
while (current != null) {
for (child in current.children) {
if (child.name.endsWith(".lyng.d") && child != file.virtualFile && seen.add(child.path)) {
val psiD = psiManager.findFile(child) ?: continue
val scriptD = getMiniAst(psiD)
if (scriptD != null) {
mainScript.declarations.addAll(scriptD.declarations)
mainScript.imports.addAll(scriptD.imports)
}
}
}
current = current.parent
}
}
fun getBinding(file: PsiFile): BindingSnapshot? {
val doc = file.viewProvider.document ?: return null
val stamp = doc.modificationStamp

View File

@ -42,7 +42,7 @@
<extensions defaultExtensionNs="com.intellij">
<!-- Language and file type -->
<fileType implementationClass="net.sergeych.lyng.idea.LyngFileType" name="Lyng" extensions="lyng" fieldName="INSTANCE" language="Lyng"/>
<fileType implementationClass="net.sergeych.lyng.idea.LyngFileType" name="Lyng" extensions="lyng;lyng.d" fieldName="INSTANCE" language="Lyng"/>
<!-- Minimal parser/PSI to fully wire editor services for the language -->
<lang.parserDefinition language="Lyng" implementationClass="net.sergeych.lyng.idea.psi.LyngParserDefinition"/>

View File

@ -20,7 +20,7 @@ package net.sergeych.lyng
sealed class CodeContext {
class Module(@Suppress("unused") val packageName: String?): CodeContext()
class Function(val name: String): CodeContext()
class ClassBody(val name: String): CodeContext() {
class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() {
val pendingInitializations = mutableMapOf<String, Pos>()
}
}

View File

@ -1327,7 +1327,7 @@ class Compiler(
"private", "protected", "static", "abstract", "closed", "override", "extern", "open" -> {
modifiers.add(currentToken.value)
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.ID) {
if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) {
currentToken = cc.next()
} else {
break
@ -1367,32 +1367,50 @@ class Compiler(
throw ScriptError(currentToken.pos, "modifier abstract at top level is only allowed for classes")
return when (currentToken.value) {
"val" -> parseVarDeclaration(false, visibility, isAbstract, isClosed, isOverride, isStatic)
"var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic)
"val" -> parseVarDeclaration(false, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern)
"var" -> parseVarDeclaration(true, visibility, isAbstract, isClosed, isOverride, isStatic, isExtern)
"fun", "fn" -> parseFunctionDeclaration(visibility, isAbstract, isClosed, isOverride, isExtern, isStatic)
"class" -> {
if (isStatic || isClosed || isOverride || isExtern)
throw ScriptError(currentToken.pos, "unsupported modifiers for class: ${modifiers.joinToString(" ")}")
parseClassDeclaration(isAbstract)
if (isStatic || isClosed || isOverride)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for class: ${modifiers.joinToString(" ")}"
)
parseClassDeclaration(isAbstract, isExtern)
}
"object" -> {
if (isStatic || isClosed || isOverride || isExtern || isAbstract)
throw ScriptError(currentToken.pos, "unsupported modifiers for object: ${modifiers.joinToString(" ")}")
parseObjectDeclaration()
if (isStatic || isClosed || isOverride || isAbstract)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for object: ${modifiers.joinToString(" ")}"
)
parseObjectDeclaration(isExtern)
}
"interface" -> {
if (isStatic || isClosed || isOverride || isExtern || isAbstract)
if (isStatic || isClosed || isOverride || isAbstract)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for interface: ${modifiers.joinToString(" ")}"
)
// interface is synonym for abstract class
parseClassDeclaration(isAbstract = true)
parseClassDeclaration(isAbstract = true, isExtern = isExtern)
}
else -> throw ScriptError(currentToken.pos, "expected declaration after modifiers, found ${currentToken.value}")
"enum" -> {
if (isStatic || isClosed || isOverride || isAbstract)
throw ScriptError(
currentToken.pos,
"unsupported modifiers for enum: ${modifiers.joinToString(" ")}"
)
parseEnumDeclaration(isExtern)
}
else -> throw ScriptError(
currentToken.pos,
"expected declaration after modifiers, found ${currentToken.value}"
)
}
}
@ -1401,7 +1419,7 @@ class Compiler(
* @return parsed statement or null if, for example. [id] is not among keywords
*/
private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) {
"abstract", "closed", "override", "extern", "private", "protected", "static" -> {
"abstract", "closed", "override", "extern", "private", "protected", "static", "open" -> {
parseDeclarationWithModifiers(id)
}
@ -1835,7 +1853,7 @@ class Compiler(
}
}
private fun parseEnumDeclaration(): Statement {
private fun parseEnumDeclaration(isExtern: Boolean = false): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
@ -1873,7 +1891,8 @@ class Compiler(
name = nameToken.value,
entries = names,
doc = doc,
nameStart = nameToken.pos
nameStart = nameToken.pos,
isExtern = isExtern
)
)
@ -1884,7 +1903,7 @@ class Compiler(
}
}
private suspend fun parseObjectDeclaration(): Statement {
private suspend fun parseObjectDeclaration(isExtern: Boolean = false): Statement {
val next = cc.peekNextNonWhitespace()
val nameToken = if (next.type == Token.Type.ID) cc.requireToken(Token.Type.ID) else null
@ -1916,10 +1935,24 @@ class Compiler(
// Robust body detection
var classBodyRange: MiniRange? = null
val bodyInit: Statement? = inCodeContext(CodeContext.ClassBody(className)) {
val bodyInit: Statement? = inCodeContext(CodeContext.ClassBody(className, isExtern = isExtern)) {
val saved = cc.savePos()
val nextBody = cc.nextNonWhitespace()
if (nextBody.type == Token.Type.LBRACE) {
// Emit MiniClassDecl before body parsing to track members via enter/exit
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = className,
bases = baseSpecs.map { it.name },
bodyRange = null,
doc = doc,
nameStart = nameToken?.pos ?: startPos,
isObject = true,
isExtern = isExtern
)
miniSink?.onEnterClass(node)
}
val bodyStart = nextBody.pos
val st = withLocalNames(emptySet()) {
parseScript()
@ -1927,8 +1960,23 @@ class Compiler(
val rbTok = cc.next()
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in object body")
classBodyRange = MiniRange(bodyStart, rbTok.pos)
miniSink?.onExitClass(rbTok.pos)
st
} else {
// No body, but still emit the class
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = className,
bases = baseSpecs.map { it.name },
bodyRange = null,
doc = doc,
nameStart = nameToken?.pos ?: startPos,
isObject = true,
isExtern = isExtern
)
miniSink?.onClassDecl(node)
}
cc.restorePos(saved)
null
}
@ -1965,13 +2013,13 @@ class Compiler(
}
}
private suspend fun parseClassDeclaration(isAbstract: Boolean = false): Statement {
private suspend fun parseClassDeclaration(isAbstract: Boolean = false, isExtern: Boolean = false): Statement {
val nameToken = cc.requireToken(Token.Type.ID)
val startPos = pendingDeclStart ?: nameToken.pos
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
pendingDeclStart = null
return inCodeContext(CodeContext.ClassBody(nameToken.value)) {
return inCodeContext(CodeContext.ClassBody(nameToken.value, isExtern = isExtern)) {
val constructorArgsDeclaration =
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
parseArgsDeclaration(isClassDeclaration = true)
@ -2009,28 +2057,7 @@ class Compiler(
val bodyInit: Statement? = run {
val saved = cc.savePos()
val next = cc.nextNonWhitespace()
if (next.type == Token.Type.LBRACE) {
// parse body
val bodyStart = next.pos
val st = withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) {
parseScript()
}
val rbTok = cc.next()
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in class body")
classBodyRange = MiniRange(bodyStart, rbTok.pos)
st
} else {
// restore if no body starts here
cc.restorePos(saved)
null
}
}
// Emit MiniClassDecl with collected base names; bodyRange is omitted for now
run {
val declRange = MiniRange(startPos, cc.currentPos())
val bases = baseSpecs.map { it.name }
// Collect constructor fields declared in primary constructor
val ctorFields = mutableListOf<MiniCtorField>()
constructorArgsDeclaration?.let { ad ->
for (p in ad.params) {
@ -2044,16 +2071,51 @@ class Compiler(
)
}
}
val node = MiniClassDecl(
range = declRange,
name = nameToken.value,
bases = bases,
bodyRange = classBodyRange,
ctorFields = ctorFields,
doc = doc,
nameStart = nameToken.pos
)
miniSink?.onClassDecl(node)
if (next.type == Token.Type.LBRACE) {
// Emit MiniClassDecl before body parsing to track members via enter/exit
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = nameToken.value,
bases = baseSpecs.map { it.name },
bodyRange = null,
ctorFields = ctorFields,
doc = doc,
nameStart = nameToken.pos,
isExtern = isExtern
)
miniSink?.onEnterClass(node)
}
// parse body
val bodyStart = next.pos
val st = withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) {
parseScript()
}
val rbTok = cc.next()
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in class body")
classBodyRange = MiniRange(bodyStart, rbTok.pos)
miniSink?.onExitClass(rbTok.pos)
st
} else {
// No body, but still emit the class
run {
val node = MiniClassDecl(
range = MiniRange(startPos, cc.currentPos()),
name = nameToken.value,
bases = baseSpecs.map { it.name },
bodyRange = null,
ctorFields = ctorFields,
doc = doc,
nameStart = nameToken.pos,
isExtern = isExtern
)
miniSink?.onClassDecl(node)
}
// restore if no body starts here
cc.restorePos(saved)
null
}
}
val initScope = popInitScope()
@ -2593,6 +2655,7 @@ class Compiler(
isExtern: Boolean = false,
isStatic: Boolean = false,
): Statement {
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
var t = cc.next()
val start = t.pos
var extTypeName: String? = null
@ -2669,7 +2732,8 @@ class Compiler(
body = null,
doc = declDocLocal,
nameStart = nameStartPos,
receiver = receiverMini
receiver = receiverMini,
isExtern = actualExtern
)
miniSink?.onFunDecl(node)
pendingDeclDoc = null
@ -2684,7 +2748,7 @@ class Compiler(
// Parse function body while tracking declared locals to compute precise capacity hints
currentLocalDeclCount
localDeclCountStack.add(0)
val fnStatements = if (isExtern)
val fnStatements = if (actualExtern)
statement { raiseError("extern function not provided: $name") }
else if (isAbstract || isDelegated) {
null
@ -2906,8 +2970,10 @@ class Compiler(
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
isStatic: Boolean = false
isStatic: Boolean = false,
isExtern: Boolean = false
): Statement {
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
val nextToken = cc.next()
val start = nextToken.pos
@ -2929,7 +2995,8 @@ class Compiler(
type = null,
initRange = null,
doc = pendingDeclDoc,
nameStart = namePos
nameStart = namePos,
isExtern = actualExtern
)
miniSink?.onValDecl(node)
}
@ -3018,10 +3085,10 @@ class Compiler(
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
if (!isStatic) declareLocalName(name)
val isDelegate = if (isAbstract) {
val isDelegate = if (isAbstract || actualExtern) {
if (!isProperty && (eqToken.type == Token.Type.ASSIGN || eqToken.type == Token.Type.BY))
throw ScriptError(eqToken.pos, "abstract variable $name cannot have an initializer or delegate")
// Abstract variables don't have initializers
throw ScriptError(eqToken.pos, "${if (isAbstract) "abstract" else "extern"} variable $name cannot have an initializer or delegate")
// Abstract or extern variables don't have initializers
cc.restorePos(markBeforeEq)
cc.skipWsTokens()
setNull = true
@ -3063,7 +3130,8 @@ class Compiler(
initRange = initR,
doc = pendingDeclDoc,
nameStart = nameStartPos,
receiver = receiverMini
receiver = receiverMini,
isExtern = actualExtern
)
miniSink?.onValDecl(node)
pendingDeclDoc = null

View File

@ -151,10 +151,11 @@ class CompilerContext(val tokens: List<Token>) {
* @return next non-whitespace token without extracting it from tokens list
*/
fun peekNextNonWhitespace(): Token {
val saved = savePos()
while (true) {
val t = next()
if (t.type !in wstokens) {
previous()
restorePos(saved)
return t
}
}

View File

@ -86,6 +86,7 @@ sealed interface MiniDecl : MiniNode {
val doc: MiniDoc?
// Start position of the declaration name identifier in source; end can be derived as start + name.length
val nameStart: Pos
val isExtern: Boolean
}
data class MiniScript(
@ -110,7 +111,8 @@ data class MiniFunDecl(
val body: MiniBlock?,
override val doc: MiniDoc?,
override val nameStart: Pos,
val receiver: MiniTypeRef? = null
val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false
) : MiniDecl
data class MiniValDecl(
@ -121,7 +123,8 @@ data class MiniValDecl(
val initRange: MiniRange?,
override val doc: MiniDoc?,
override val nameStart: Pos,
val receiver: MiniTypeRef? = null
val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false
) : MiniDecl
data class MiniClassDecl(
@ -134,7 +137,9 @@ data class MiniClassDecl(
override val doc: MiniDoc?,
override val nameStart: Pos,
// Built-in extension: list of member declarations (functions and fields)
val members: List<MiniMemberDecl> = emptyList()
val members: List<MiniMemberDecl> = emptyList(),
override val isExtern: Boolean = false,
val isObject: Boolean = false
) : MiniDecl
data class MiniEnumDecl(
@ -142,7 +147,8 @@ data class MiniEnumDecl(
override val name: String,
val entries: List<String>,
override val doc: MiniDoc?,
override val nameStart: Pos
override val nameStart: Pos,
override val isExtern: Boolean = false
) : MiniDecl
data class MiniCtorField(
@ -171,6 +177,7 @@ sealed interface MiniMemberDecl : MiniNode {
val doc: MiniDoc?
val nameStart: Pos
val isStatic: Boolean
val isExtern: Boolean
}
data class MiniMemberFunDecl(
@ -181,6 +188,7 @@ data class MiniMemberFunDecl(
override val doc: MiniDoc?,
override val nameStart: Pos,
override val isStatic: Boolean = false,
override val isExtern: Boolean = false,
) : MiniMemberDecl
data class MiniMemberValDecl(
@ -191,6 +199,7 @@ data class MiniMemberValDecl(
override val doc: MiniDoc?,
override val nameStart: Pos,
override val isStatic: Boolean = false,
override val isExtern: Boolean = false,
) : MiniMemberDecl
data class MiniInitDecl(
@ -200,6 +209,7 @@ data class MiniInitDecl(
override val name: String get() = "init"
override val doc: MiniDoc? get() = null
override val isStatic: Boolean get() = false
override val isExtern: Boolean get() = false
}
// Streaming sink to collect mini-AST during parsing. Implementations may assemble a tree or process events.
@ -209,6 +219,9 @@ interface MiniAstSink {
fun onDocCandidate(doc: MiniDoc) {}
fun onEnterClass(node: MiniClassDecl) {}
fun onExitClass(end: Pos) {}
fun onImport(node: MiniImport) {}
fun onFunDecl(node: MiniFunDecl) {}
fun onValDecl(node: MiniValDecl) {}
@ -238,6 +251,7 @@ interface MiniTypeTrace {
class MiniAstBuilder : MiniAstSink {
private var currentScript: MiniScript? = null
private val blocks = ArrayDeque<MiniBlock>()
private val classStack = ArrayDeque<MiniClassDecl>()
private var lastDoc: MiniDoc? = null
private var scriptDepth: Int = 0
@ -262,26 +276,80 @@ class MiniAstBuilder : MiniAstSink {
lastDoc = doc
}
override fun onEnterClass(node: MiniClassDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
classStack.addLast(attach)
lastDoc = null
}
override fun onExitClass(end: Pos) {
val finished = classStack.removeLastOrNull()
if (finished != null) {
val updated = finished.copy(range = MiniRange(finished.range.start, end))
// Always add to top-level for now to ensure visibility in light engine
currentScript?.declarations?.add(updated)
}
}
override fun onImport(node: MiniImport) {
currentScript?.imports?.add(node)
}
override fun onFunDecl(node: MiniFunDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
currentScript?.declarations?.add(attach)
val currentClass = classStack.lastOrNull()
if (currentClass != null) {
// Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members
val member = MiniMemberFunDecl(
range = attach.range,
name = attach.name,
params = attach.params,
returnType = attach.returnType,
doc = attach.doc,
nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed
isExtern = attach.isExtern
)
// Need to update the class in the stack since it's immutable-ish (data class)
classStack.removeLast()
classStack.addLast(currentClass.copy(members = currentClass.members + member))
} else {
currentScript?.declarations?.add(attach)
}
lastDoc = null
}
override fun onValDecl(node: MiniValDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
currentScript?.declarations?.add(attach)
val currentClass = classStack.lastOrNull()
if (currentClass != null) {
val member = MiniMemberValDecl(
range = attach.range,
name = attach.name,
mutable = attach.mutable,
type = attach.type,
doc = attach.doc,
nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed
isExtern = attach.isExtern
)
classStack.removeLast()
classStack.addLast(currentClass.copy(members = currentClass.members + member))
} else {
currentScript?.declarations?.add(attach)
}
lastDoc = null
}
override fun onClassDecl(node: MiniClassDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
currentScript?.declarations?.add(attach)
lastDoc = null
// This is the old way, we might want to deprecate it or make it call onEnterClass
// For now, if we are NOT using enter/exit, keep behavior.
// But Compiler.kt will be updated to use enter/exit.
if (classStack.isEmpty()) {
val attach = node.copy(doc = node.doc ?: lastDoc)
currentScript?.declarations?.add(attach)
lastDoc = null
}
}
override fun onEnumDecl(node: MiniEnumDecl) {

View File

@ -222,4 +222,55 @@ class MiniAstTest {
assertTrue(names.contains("V1"), "Should contain V1")
assertTrue(names.contains("V2"), "Should contain V2")
}
@Test
fun miniAst_captures_extern_docs() = runTest {
val code = """
// Doc1
extern fun f1()
// Doc2
extern class C1 {
// Doc3
fun m1()
}
// Doc4
extern object O1 {
// Doc5
val v1: String
}
// Doc6
extern enum E1 {
V1, V2
}
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val f1 = mini.declarations.filterIsInstance<MiniFunDecl>().firstOrNull { it.name == "f1" }
assertNotNull(f1)
assertEquals("Doc1", f1.doc?.summary)
val c1 = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "C1" }
assertNotNull(c1)
assertEquals("Doc2", c1.doc?.summary)
val m1 = c1.members.filterIsInstance<MiniMemberFunDecl>().firstOrNull { it.name == "m1" }
assertNotNull(m1)
assertEquals("Doc3", m1.doc?.summary)
val o1 = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "O1" }
assertNotNull(o1)
assertTrue(o1.isObject)
assertEquals("Doc4", o1.doc?.summary)
val v1 = o1.members.filterIsInstance<MiniMemberValDecl>().firstOrNull { it.name == "v1" }
assertNotNull(v1)
assertEquals("Doc5", v1.doc?.summary)
val e1 = mini.declarations.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == "E1" }
assertNotNull(e1)
assertEquals("Doc6", e1.doc?.summary)
}
}

View File

@ -3043,32 +3043,34 @@ class ScriptTest {
}
@Test
fun testMapWithNonStringKeys() = runTest {
fun testExternDeclarations() = runTest {
eval(
"""
val map = Map( 1 => "one", 2 => "two" )
assertEquals( "one", map[1] )
assertEquals( "two", map[2] )
assertEquals( null, map[3] )
map[3] = "three"
assertEquals( "three", map[3] )
map += (4 => "four")
assertEquals( "four", map[4] )
extern fun hostFunction(a: Int, b: String): String
extern class HostClass(name: String) {
fun doSomething(): Int
val status: String
}
extern object HostObject {
fun getInstance(): HostClass
}
extern enum HostEnum {
VALUE1, VALUE2
}
// Test toMap()
val map2 = [1 => "a", 2 => "b"].toMap()
assertEquals("a", map2[1])
assertEquals("b", map2[2])
// Test Map constructor with mixed entries and arrays
val map3 = Map( 1 => "a", [2, "b"] )
assertEquals("a", map3[1])
assertEquals("b", map3[2])
// Test plus
val map4 = map3 + (3 => "c")
assertEquals("c", map4[3])
assertEquals("a", map4[1])
// These should not throw errors during compilation
// and should be registered in the scope (though they won't have implementations here)
// In this test environment, they might fail at runtime if called, but we just check compilation.
""".trimIndent()
)
}
@Test
fun testExternExtension() = runTest {
eval(
"""
extern fun String.pretty(): String
// Compiles without error
""".trimIndent()
)
}