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.openapi.util.Key
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.Compiler import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Source import net.sergeych.lyng.Source
@ -45,7 +46,11 @@ object LyngAstManager {
val provider = IdeLenientImportProvider.create() val provider = IdeLenientImportProvider.create()
val src = Source(file.name, text) val src = Source(file.name, text)
runBlocking { Compiler.compileWithMini(src, provider, sink) } 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) { } catch (_: Throwable) {
sink.build() sink.build()
} }
@ -59,6 +64,26 @@ object LyngAstManager {
return built 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? { fun getBinding(file: PsiFile): BindingSnapshot? {
val doc = file.viewProvider.document ?: return null val doc = file.viewProvider.document ?: return null
val stamp = doc.modificationStamp val stamp = doc.modificationStamp

View File

@ -42,7 +42,7 @@
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<!-- Language and file type --> <!-- 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 --> <!-- Minimal parser/PSI to fully wire editor services for the language -->
<lang.parserDefinition language="Lyng" implementationClass="net.sergeych.lyng.idea.psi.LyngParserDefinition"/> <lang.parserDefinition language="Lyng" implementationClass="net.sergeych.lyng.idea.psi.LyngParserDefinition"/>

View File

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

View File

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

View File

@ -86,6 +86,7 @@ sealed interface MiniDecl : MiniNode {
val doc: MiniDoc? val doc: MiniDoc?
// Start position of the declaration name identifier in source; end can be derived as start + name.length // Start position of the declaration name identifier in source; end can be derived as start + name.length
val nameStart: Pos val nameStart: Pos
val isExtern: Boolean
} }
data class MiniScript( data class MiniScript(
@ -110,7 +111,8 @@ data class MiniFunDecl(
val body: MiniBlock?, val body: MiniBlock?,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
val receiver: MiniTypeRef? = null val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false
) : MiniDecl ) : MiniDecl
data class MiniValDecl( data class MiniValDecl(
@ -121,7 +123,8 @@ data class MiniValDecl(
val initRange: MiniRange?, val initRange: MiniRange?,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
val receiver: MiniTypeRef? = null val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false
) : MiniDecl ) : MiniDecl
data class MiniClassDecl( data class MiniClassDecl(
@ -134,7 +137,9 @@ data class MiniClassDecl(
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
// Built-in extension: list of member declarations (functions and fields) // 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 ) : MiniDecl
data class MiniEnumDecl( data class MiniEnumDecl(
@ -142,7 +147,8 @@ data class MiniEnumDecl(
override val name: String, override val name: String,
val entries: List<String>, val entries: List<String>,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos override val nameStart: Pos,
override val isExtern: Boolean = false
) : MiniDecl ) : MiniDecl
data class MiniCtorField( data class MiniCtorField(
@ -171,6 +177,7 @@ sealed interface MiniMemberDecl : MiniNode {
val doc: MiniDoc? val doc: MiniDoc?
val nameStart: Pos val nameStart: Pos
val isStatic: Boolean val isStatic: Boolean
val isExtern: Boolean
} }
data class MiniMemberFunDecl( data class MiniMemberFunDecl(
@ -181,6 +188,7 @@ data class MiniMemberFunDecl(
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
override val isStatic: Boolean = false, override val isStatic: Boolean = false,
override val isExtern: Boolean = false,
) : MiniMemberDecl ) : MiniMemberDecl
data class MiniMemberValDecl( data class MiniMemberValDecl(
@ -191,6 +199,7 @@ data class MiniMemberValDecl(
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
override val isStatic: Boolean = false, override val isStatic: Boolean = false,
override val isExtern: Boolean = false,
) : MiniMemberDecl ) : MiniMemberDecl
data class MiniInitDecl( data class MiniInitDecl(
@ -200,6 +209,7 @@ data class MiniInitDecl(
override val name: String get() = "init" override val name: String get() = "init"
override val doc: MiniDoc? get() = null override val doc: MiniDoc? get() = null
override val isStatic: Boolean get() = false 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. // 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 onDocCandidate(doc: MiniDoc) {}
fun onEnterClass(node: MiniClassDecl) {}
fun onExitClass(end: Pos) {}
fun onImport(node: MiniImport) {} fun onImport(node: MiniImport) {}
fun onFunDecl(node: MiniFunDecl) {} fun onFunDecl(node: MiniFunDecl) {}
fun onValDecl(node: MiniValDecl) {} fun onValDecl(node: MiniValDecl) {}
@ -238,6 +251,7 @@ interface MiniTypeTrace {
class MiniAstBuilder : MiniAstSink { class MiniAstBuilder : MiniAstSink {
private var currentScript: MiniScript? = null private var currentScript: MiniScript? = null
private val blocks = ArrayDeque<MiniBlock>() private val blocks = ArrayDeque<MiniBlock>()
private val classStack = ArrayDeque<MiniClassDecl>()
private var lastDoc: MiniDoc? = null private var lastDoc: MiniDoc? = null
private var scriptDepth: Int = 0 private var scriptDepth: Int = 0
@ -262,26 +276,80 @@ class MiniAstBuilder : MiniAstSink {
lastDoc = doc 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) { override fun onImport(node: MiniImport) {
currentScript?.imports?.add(node) currentScript?.imports?.add(node)
} }
override fun onFunDecl(node: MiniFunDecl) { override fun onFunDecl(node: MiniFunDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc) 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 lastDoc = null
} }
override fun onValDecl(node: MiniValDecl) { override fun onValDecl(node: MiniValDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc) 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 lastDoc = null
} }
override fun onClassDecl(node: MiniClassDecl) { override fun onClassDecl(node: MiniClassDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc) // This is the old way, we might want to deprecate it or make it call onEnterClass
currentScript?.declarations?.add(attach) // For now, if we are NOT using enter/exit, keep behavior.
lastDoc = null // 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) { override fun onEnumDecl(node: MiniEnumDecl) {

View File

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