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:
parent
44675b976d
commit
fdc044d1e0
@ -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
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -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>()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user