plugin with type declarations, collection types and much better type tracking for autocomplete

This commit is contained in:
Sergey Chernov 2026-01-07 02:55:47 +01:00
parent aba0048a83
commit fe5dded7af
17 changed files with 1195 additions and 182 deletions

View File

@ -45,6 +45,8 @@ dependencies {
// Tests for IntelliJ Platform fixtures rely on JUnit 3/4 API (junit.framework.TestCase)
// Add JUnit 4 which contains the JUnit 3 compatibility classes used by BasePlatformTestCase/UsefulTestCase
testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2")
testImplementation("org.opentest4j:opentest4j:1.3.0")
}
intellij {

View File

@ -102,7 +102,7 @@ class LyngCompletionContributor : CompletionContributor() {
// Delegate computation to the shared engine to keep behavior in sync with tests
val engineItems = try {
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini) }
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini, binding) }
} catch (t: Throwable) {
if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}")
emptyList()
@ -185,33 +185,51 @@ class LyngCompletionContributor : CompletionContributor() {
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
if (!inferredClass.isNullOrBlank()) {
val ext = BuiltinDocRegistry.extensionMemberNamesFor(inferredClass)
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}")
val ext = DocLookupUtils.collectExtensionMemberNames(imported, inferredClass, mini)
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${ext}")
for (name in ext) {
if (existing.contains(name)) continue
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
if (resolved != null) {
when (val member = resolved.second) {
val m = resolved.second
val builder = when (m) {
is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType)
val builder = LookupElementBuilder.create(name)
val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(m.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("(${ '$' }params)", true)
.withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
}
is MiniFunDecl -> {
val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(m.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
emit(builder)
existing.add(name)
}
is MiniMemberValDecl -> {
val builder = LookupElementBuilder.create(name)
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true)
emit(builder)
existing.add(name)
LookupElementBuilder.create(name)
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(m.type), true)
}
is MiniValDecl -> {
LookupElementBuilder.create(name)
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(m.type), true)
}
else -> {
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
}
is MiniInitDecl -> {}
}
emit(builder)
existing.add(name)
} else {
// Fallback: emit simple method name without detailed types
val builder = LookupElementBuilder.create(name)
@ -455,27 +473,44 @@ class LyngCompletionContributor : CompletionContributor() {
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name, mini)
if (resolved != null) {
val member = resolved.second
when (member) {
val builder = when (member) {
is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType)
val builder = LookupElementBuilder.create(name)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("(${params})", true)
.withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
}
is MiniFunDecl -> {
val params = member.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
emit(builder)
already.add(name)
}
is MiniMemberValDecl -> {
val builder = LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Field)
LookupElementBuilder.create(name)
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true)
emit(builder)
already.add(name)
}
is MiniInitDecl -> {}
is MiniValDecl -> {
LookupElementBuilder.create(name)
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true)
}
else -> {
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
}
}
emit(builder)
already.add(name)
} else {
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs
val isProperty = name in setOf("size", "length")
@ -504,29 +539,46 @@ class LyngCompletionContributor : CompletionContributor() {
// Try to resolve full signature via registry first to get params and return type
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
if (resolved != null) {
when (val member = resolved.second) {
val m = resolved.second
val builder = when (m) {
is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name }
val ret = typeOf(member.returnType)
val builder = LookupElementBuilder.create(name)
val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(m.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("(${params})", true)
.withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
}
is MiniFunDecl -> {
val params = m.params.joinToString(", ") { it.name }
val ret = typeOf(m.returnType)
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("($params)", true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
emit(builder)
already.add(name)
continue
}
is MiniMemberValDecl -> {
val builder = LookupElementBuilder.create(name)
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(member.type), true)
emit(builder)
already.add(name)
continue
LookupElementBuilder.create(name)
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(m.type), true)
}
is MiniValDecl -> {
LookupElementBuilder.create(name)
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
.withTypeText(typeOf(m.type), true)
}
else -> {
LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Method)
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
}
is MiniInitDecl -> {}
}
emit(builder)
already.add(name)
continue
}
// Fallback: emit without detailed types if we couldn't resolve
val builder = LookupElementBuilder.create(name)

View File

@ -68,6 +68,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
// 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations)
val mini = LyngAstManager.getMiniAst(file) ?: return null
val miniSource = mini.range.start.source
val imported = DocLookupUtils.canonicalImportedModules(mini, text)
// Try resolve to: function param at position, function/class/val declaration at position
// 1) Use unified declaration detection
@ -78,7 +79,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
if (d.name == name) {
val s: Int = miniSource.offsetOf(d.nameStart)
if (s <= offset && s + d.name.length > offset) {
return renderDeclDoc(d)
return renderDeclDoc(d, text, mini, imported)
}
}
// Handle members if it was a member
@ -105,6 +106,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name,
mutable = cf.mutable,
type = cf.type,
initRange = null,
doc = null,
nameStart = cf.nameStart
)
@ -122,6 +124,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name,
mutable = cf.mutable,
type = cf.type,
initRange = null,
doc = null,
nameStart = cf.nameStart
)
@ -171,7 +174,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
}
}
}
if (dsFound != null) return renderDeclDoc(dsFound)
if (dsFound != null) return renderDeclDoc(dsFound, text, mini, imported)
// Check parameters
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
@ -209,6 +212,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name,
mutable = cf.mutable,
type = cf.type,
initRange = null,
doc = null,
nameStart = cf.nameStart
)
@ -226,6 +230,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
name = cf.name,
mutable = cf.mutable,
type = cf.type,
initRange = null,
doc = null,
nameStart = cf.nameStart
)
@ -308,6 +313,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
}
}
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
@ -318,7 +327,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
// 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
mini.declarations.firstOrNull { it.name == ident }?.let {
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
return renderDeclDoc(it)
return renderDeclDoc(it, text, mini, imported)
}
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
@ -338,13 +347,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
if (arity != null && chosen.params.size != arity && matches.size > 1) {
return renderOverloads(ident, matches)
}
return renderDeclDoc(chosen)
return renderDeclDoc(chosen, text, mini, imported)
}
// Also allow values/consts
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
// And classes/enums
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
docs.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
docs.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
}
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
if (ident == "println" || ident == "print") {
@ -364,6 +373,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
}
}
} else {
@ -383,6 +396,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
}
}
} else {
@ -396,6 +413,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
}
}
}
@ -421,6 +442,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
is MiniInitDecl -> null
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
}
}
}
@ -468,12 +493,16 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return contextElement ?: file.findElementAt(targetOffset)
}
private fun renderDeclDoc(d: MiniDecl): String {
private fun renderDeclDoc(d: MiniDecl, text: String, mini: MiniScript, imported: List<String>): String {
val title = when (d) {
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
is MiniClassDecl -> "class ${d.name}"
is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }"
is MiniValDecl -> if (d.mutable) "var ${d.name}${typeOf(d.type)}" else "val ${d.name}${typeOf(d.type)}"
is MiniValDecl -> {
val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini)
val typeStr = if (t == null) ": Object?" else typeOf(t)
if (d.mutable) "var ${d.name}${typeStr}" else "val ${d.name}${typeStr}"
}
}
// Show full detailed documentation, not just the summary
val raw = d.doc?.raw
@ -506,7 +535,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
}
private fun renderMemberValDoc(className: String, m: MiniMemberValDecl): String {
val ts = typeOf(m.type)
val ts = if (m.type == null) ": Object?" else typeOf(m.type)
val kind = if (m.mutable) "var" else "val"
val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}${kind} $className.${m.name}${ts}"
@ -527,7 +556,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
}
is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}"
is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}"
null -> ""
null -> ": Object?"
}
private fun signatureOf(fn: MiniFunDecl): String {

View File

@ -63,6 +63,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
is MiniMemberFunDecl -> "Function"
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
is MiniInitDecl -> "Initializer"
is MiniFunDecl -> "Function"
is MiniValDecl -> if (member.mutable) "Variable" else "Value"
is MiniClassDecl -> "Class"
is MiniEnumDecl -> "Enum"
}
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
}

View File

@ -23,7 +23,7 @@ actual object ArgBuilderProvider {
private val tl = object : ThreadLocal<AndroidArgsBuilder>() {
override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder()
}
actual fun acquire(): ArgsBuilder = tl.get()
actual fun acquire(): ArgsBuilder = tl.get()!!
}
private class AndroidArgsBuilder : ArgsBuilder {

View File

@ -973,7 +973,10 @@ class Compiler(
private fun parseTypeDeclarationWithMini(): Pair<TypeDecl, MiniTypeRef?> {
// Only parse a type if a ':' follows; otherwise keep current behavior
if (!cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) return Pair(TypeDecl.TypeAny, null)
return parseTypeExpressionWithMini()
}
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
// Parse a qualified base name: ID ('.' ID)*
val segments = mutableListOf<MiniTypeName.Segment>()
var first = true
@ -1009,41 +1012,28 @@ class Compiler(
else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
}
// Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now)
var args: MutableList<MiniTypeRef>? = null
// Optional generic arguments: '<' Type (',' Type)* '>'
var miniArgs: MutableList<MiniTypeRef>? = null
var semArgs: MutableList<TypeDecl>? = null
val afterBasePos = cc.savePos()
if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) {
args = mutableListOf()
miniArgs = mutableListOf()
semArgs = mutableListOf()
do {
// Parse argument as simple or qualified type (single level), with optional nullable '?'
val argSegs = mutableListOf<MiniTypeName.Segment>()
var argFirst = true
val argStart = cc.currentPos()
while (true) {
val idTok = if (argFirst) cc.requireToken(
Token.Type.ID,
"type argument name expected"
) else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument")
argFirst = false
argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
val p = cc.savePos()
val tt = cc.next()
if (tt.type == Token.Type.DOT) continue else {
cc.restorePos(p); break
}
}
val argNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
val argEnd = cc.currentPos()
val argRef = MiniTypeName(MiniRange(argStart, argEnd), argSegs.toList(), nullable = argNullable)
args += argRef
val (argSem, argMini) = parseTypeExpressionWithMini()
miniArgs += argMini
semArgs += argSem
val sep = cc.next()
when (sep.type) {
Token.Type.COMMA -> { /* continue */
}
Token.Type.GT -> break
else -> sep.raiseSyntax("expected ',' or '>' in generic arguments")
if (sep.type == Token.Type.COMMA) {
// continue
} else if (sep.type == Token.Type.GT) {
break
} else if (sep.type == Token.Type.SHR) {
cc.pushPendingGT()
break
} else {
sep.raiseSyntax("expected ',' or '>' in generic arguments")
}
} while (true)
lastEnd = cc.currentPos()
@ -1055,10 +1045,11 @@ class Compiler(
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
val endPos = cc.currentPos()
val miniRef = buildBaseRef(if (args != null) endPos else lastEnd, args, isNullable)
val miniRef = buildBaseRef(if (miniArgs != null) endPos else lastEnd, miniArgs, isNullable)
// Semantic: keep simple for now, just use qualified base name with nullable flag
val qualified = segments.joinToString(".") { it.name }
val sem = TypeDecl.Simple(qualified, isNullable)
val sem = if (semArgs != null) TypeDecl.Generic(qualified, semArgs, isNullable)
else TypeDecl.Simple(qualified, isNullable)
return Pair(sem, miniRef)
}
@ -1474,7 +1465,9 @@ class Compiler(
"init" -> {
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
miniSink?.onEnterFunction(null)
val block = parseBlock()
miniSink?.onExitFunction(cc.currentPos())
lastParsedBlockRange?.let { range ->
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
}
@ -2714,8 +2707,7 @@ class Compiler(
val declDocLocal = pendingDeclDoc
val outerLabel = lastLabel
// Emit MiniFunDecl before body parsing (body range unknown yet)
run {
val node = run {
val params = argsDeclaration.params.map { p ->
MiniParam(
name = p.name,
@ -2737,8 +2729,10 @@ class Compiler(
)
miniSink?.onFunDecl(node)
pendingDeclDoc = null
node
}
miniSink?.onEnterFunction(node)
return inCodeContext(CodeContext.Function(name)) {
cc.labels.add(name)
outerLabel?.let { cc.labels.add(it) }
@ -2941,6 +2935,7 @@ class Compiler(
isExtern = actualExtern
)
miniSink?.onFunDecl(node)
miniSink?.onExitFunction(cc.currentPos())
}
}
@ -3186,9 +3181,11 @@ class Compiler(
while (true) {
val t = cc.skipWsTokens()
if (t.isId("get")) {
val getStart = cc.currentPos()
cc.next() // consume 'get'
cc.requireToken(Token.Type.LPAREN)
cc.requireToken(Token.Type.RPAREN)
miniSink?.onEnterFunction(null)
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens()
parseBlock()
@ -3200,11 +3197,14 @@ class Compiler(
} else {
throw ScriptError(cc.current().pos, "Expected { or = after get()")
}
miniSink?.onExitFunction(cc.currentPos())
} else if (t.isId("set")) {
val setStart = cc.currentPos()
cc.next() // consume 'set'
cc.requireToken(Token.Type.LPAREN)
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
cc.requireToken(Token.Type.RPAREN)
miniSink?.onEnterFunction(null)
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens()
val body = parseBlock()
@ -3226,6 +3226,7 @@ class Compiler(
} else {
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
}
miniSink?.onExitFunction(cc.currentPos())
} else if (t.isId("private") || t.isId("protected")) {
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
val mark = cc.savePos()
@ -3237,6 +3238,7 @@ class Compiler(
cc.next() // consume '('
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
cc.requireToken(Token.Type.RPAREN)
miniSink?.onEnterFunction(null)
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens()
val body = parseBlock()
@ -3261,6 +3263,7 @@ class Compiler(
} else {
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
}
miniSink?.onExitFunction(cc.currentPos())
}
} else {
cc.restorePos(mark)

View File

@ -34,18 +34,34 @@ class CompilerContext(val tokens: List<Token>) {
}
var currentIndex = 0
private var pendingGT = 0
fun hasNext() = currentIndex < tokens.size
fun hasNext() = currentIndex < tokens.size || pendingGT > 0
fun hasPrevious() = currentIndex > 0
fun next() =
if (currentIndex < tokens.size) tokens[currentIndex++]
fun next(): Token {
if (pendingGT > 0) {
pendingGT--
val last = tokens[currentIndex - 1]
return Token(">", last.pos.copy(column = last.pos.column + 1), Token.Type.GT)
}
return if (currentIndex < tokens.size) tokens[currentIndex++]
else Token("", tokens.last().pos, Token.Type.EOF)
}
fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun pushPendingGT() {
pendingGT++
}
fun savePos() = currentIndex
fun previous() = if (pendingGT > 0) {
pendingGT-- // This is wrong, previous should go back.
// But we don't really use previous() in generics parser after splitting.
throw IllegalStateException("previous() not supported after pushPendingGT")
} else if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = (currentIndex shl 2) or (pendingGT and 3)
fun restorePos(pos: Int) {
currentIndex = pos
currentIndex = pos shr 2
pendingGT = pos and 3
}
fun ensureLabelIsValid(pos: Pos, label: String) {
@ -106,12 +122,13 @@ class CompilerContext(val tokens: List<Token>) {
errorMessage: String = "expected ${tokenType.name}",
isOptional: Boolean = false
): Boolean {
val pos = savePos()
val t = next()
return if (t.type != tokenType) {
if (!isOptional) {
throw ScriptError(t.pos, errorMessage)
} else {
previous()
restorePos(pos)
false
}
} else true
@ -122,20 +139,25 @@ class CompilerContext(val tokens: List<Token>) {
* @return true if token was found and skipped
*/
fun skipNextIf(vararg types: Token.Type): Boolean {
val pos = savePos()
val t = next()
return if (t.type in types)
true
else {
previous()
restorePos(pos)
false
}
}
@Suppress("unused")
fun skipTokens(vararg tokenTypes: Token.Type) {
while (next().type in tokenTypes) { /**/
while (hasNext()) {
val pos = savePos()
if (next().type !in tokenTypes) {
restorePos(pos)
break
}
}
previous()
}
fun nextNonWhitespace(): Token {
@ -163,12 +185,13 @@ class CompilerContext(val tokens: List<Token>) {
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
val pos = savePos()
val t = next()
return if (t.type == typeId) {
f(t)
true
} else {
previous()
restorePos(pos)
false
}
}

View File

@ -27,5 +27,6 @@ sealed class TypeDecl(val isNullable:Boolean = false) {
object TypeAny : TypeDecl()
object TypeNullableAny : TypeDecl(true)
class Simple(val name: String,isNullable: Boolean) : TypeDecl(isNullable)
class Simple(val name: String, isNullable: Boolean) : TypeDecl(isNullable)
class Generic(val name: String, val args: List<TypeDecl>, isNullable: Boolean) : TypeDecl(isNullable)
}

View File

@ -236,6 +236,7 @@ class ClassDocsBuilder internal constructor(private val className: String) {
name = name,
mutable = mutable,
type = type?.toMiniTypeRef(),
initRange = null,
doc = md,
nameStart = Pos.builtIn,
isStatic = isStatic,
@ -534,6 +535,9 @@ private fun buildStdlibDocs(): List<MiniDecl> {
)
// Concurrency helpers
mod.classDoc(name = "Deferred", doc = "Represents a value that will be available in the future.", bases = listOf(type("Obj"))) {
method(name = "await", doc = "Suspend until the value is available and return it.")
}
mod.funDoc(
name = "launch",
doc = StdlibInlineDocIndex.topFunDoc("launch") ?: "Launch an asynchronous task and return a `Deferred`.",
@ -551,8 +555,17 @@ private fun buildStdlibDocs(): List<MiniDecl> {
returns = type("lyng.Iterable")
)
// Common types
mod.classDoc(name = "Int", doc = "64-bit signed integer.", bases = listOf(type("Obj")))
mod.classDoc(name = "Real", doc = "64-bit floating point number.", bases = listOf(type("Obj")))
mod.classDoc(name = "Bool", doc = "Boolean value (true or false).", bases = listOf(type("Obj")))
mod.classDoc(name = "Char", doc = "Single character (UTF-16 code unit).", bases = listOf(type("Obj")))
mod.classDoc(name = "Buffer", doc = "Mutable byte array.", bases = listOf(type("Obj")))
mod.classDoc(name = "Regex", doc = "Regular expression.", bases = listOf(type("Obj")))
mod.classDoc(name = "Range", doc = "Arithmetic progression.", bases = listOf(type("Obj")))
// Common Iterable helpers (document top-level extension-like APIs as class members)
mod.classDoc(name = "Iterable", doc = StdlibInlineDocIndex.classDoc("Iterable") ?: "Helper operations for iterable collections.") {
mod.classDoc(name = "Iterable", doc = StdlibInlineDocIndex.classDoc("Iterable") ?: "Helper operations for iterable collections.", bases = listOf(type("Obj"))) {
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterable", name) ?: fallback
method(name = "filter", doc = md("filter", "Filter elements by predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Iterable"))
method(name = "drop", doc = md("drop", "Skip the first N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable"))
@ -591,7 +604,7 @@ private fun buildStdlibDocs(): List<MiniDecl> {
}
// Iterator helpers
mod.classDoc(name = "Iterator", doc = StdlibInlineDocIndex.classDoc("Iterator") ?: "Iterator protocol for sequential access.") {
mod.classDoc(name = "Iterator", doc = StdlibInlineDocIndex.classDoc("Iterator") ?: "Iterator protocol for sequential access.", bases = listOf(type("Obj"))) {
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterator", name) ?: fallback
method(name = "hasNext", doc = md("hasNext", "Whether another element is available."), returns = type("lyng.Bool"))
method(name = "next", doc = md("next", "Return the next element."))
@ -600,22 +613,22 @@ private fun buildStdlibDocs(): List<MiniDecl> {
}
// Exceptions and utilities
mod.classDoc(name = "Exception", doc = StdlibInlineDocIndex.classDoc("Exception") ?: "Exception helpers.") {
mod.classDoc(name = "Exception", doc = StdlibInlineDocIndex.classDoc("Exception") ?: "Exception helpers.", bases = listOf(type("Obj"))) {
method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.")
}
mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.") {
mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.", bases = listOf(type("Obj"))) {
method(name = "name", doc = "Returns the name of this enum constant.", returns = type("lyng.String"))
method(name = "ordinal", doc = "Returns the ordinal of this enum constant.", returns = type("lyng.Int"))
}
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.") {
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.", bases = listOf(type("Obj"))) {
// Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions.
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
}
// StackTraceEntry structure
mod.classDoc(name = "StackTraceEntry", doc = StdlibInlineDocIndex.classDoc("StackTraceEntry") ?: "Represents a single stack trace element.") {
mod.classDoc(name = "StackTraceEntry", doc = StdlibInlineDocIndex.classDoc("StackTraceEntry") ?: "Represents a single stack trace element.", bases = listOf(type("Obj"))) {
// Fields are not present as declarations in root.lyng's class header docs. Keep seeded defaults.
field(name = "sourceName", doc = "Source (file) name.", type = type("lyng.String"))
field(name = "line", doc = "Line number (1-based).", type = type("lyng.Int"))

View File

@ -23,6 +23,7 @@ package net.sergeych.lyng.miniast
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Script
import net.sergeych.lyng.Source
import net.sergeych.lyng.binding.BindingSnapshot
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.pacman.ImportProvider
@ -58,7 +59,7 @@ object CompletionEngineLight {
return completeSuspend(text, idx)
}
suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null): List<CompletionItem> {
suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null, binding: BindingSnapshot? = null): List<CompletionItem> {
// Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup
StdlibDocsBootstrap.ensure()
val prefix = prefixAt(text, caret)
@ -73,6 +74,10 @@ object CompletionEngineLight {
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
if (memberDot != null) {
// 0) Try chained member call return type inference
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDot, imported, binding)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
@ -88,7 +93,7 @@ object CompletionEngineLight {
return out
}
// 1) Receiver inference fallback
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported, binding) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
@ -97,11 +102,16 @@ object CompletionEngineLight {
}
// Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical
if (mini != null) {
offerParamsInScope(out, prefix, mini, text, caret)
offerParamsInScope(out, prefix, mini, text, caret)
val locals = DocLookupUtils.extractLocalsAt(text, caret)
for (name in locals) {
if (name.startsWith(prefix, true)) {
out.add(CompletionItem(name, Kind.Value))
}
}
val decls = mini?.declarations ?: emptyList()
val decls = mini.declarations
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
@ -274,33 +284,38 @@ object CompletionEngineLight {
emitGroup(directMap)
emitGroup(inheritedMap)
// Supplement with stdlib extension members defined in root.lyng (e.g., fun String.re(...))
// Supplement with extension members (both stdlib and local)
run {
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
for (name in ext) {
val extensions = DocLookupUtils.collectExtensionMemberNames(imported, className, mini)
for (name in extensions) {
if (already.contains(name)) continue
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
if (resolved != null) {
when (val member = resolved.second) {
val m = resolved.second
val ci = when (m) {
is MiniMemberFunDecl -> {
val params = member.params.joinToString(", ") { it.name }
val ci = CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(member.returnType))
if (ci.name.startsWith(prefix, true)) out += ci
already.add(name)
val params = m.params.joinToString(", ") { it.name }
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
}
is MiniMemberValDecl -> {
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(member.type))
if (ci.name.startsWith(prefix, true)) out += ci
already.add(name)
is MiniFunDecl -> {
val params = m.params.joinToString(", ") { it.name }
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
}
is MiniInitDecl -> {}
is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type))
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type))
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
}
if (ci.name.startsWith(prefix, true)) {
out += ci
already.add(name)
}
} else {
// Fallback: emit simple method name without detailed types
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
if (ci.name.startsWith(prefix, true)) out += ci
already.add(name)
if (ci.name.startsWith(prefix, true)) {
out += ci
already.add(name)
}
}
}
}

View File

@ -91,14 +91,15 @@ object DocLookupUtils {
return null
}
fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int): MiniTypeRef? {
fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int, text: String? = null, imported: List<String>? = null): MiniTypeRef? {
if (mini == null) return null
val src = mini.range.start.source
fun matches(p: net.sergeych.lyng.Pos, len: Int) = src.offsetOf(p).let { s -> startOffset >= s && startOffset < s + len }
for (d in mini.declarations) {
if (d.name == name && src.offsetOf(d.nameStart) == startOffset) {
if (d.name == name && matches(d.nameStart, d.name.length)) {
return when (d) {
is MiniValDecl -> d.type
is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null
is MiniFunDecl -> d.returnType
else -> null
}
@ -106,25 +107,27 @@ object DocLookupUtils {
if (d is MiniFunDecl) {
for (p in d.params) {
if (p.name == name && src.offsetOf(p.nameStart) == startOffset) return p.type
if (p.name == name && matches(p.nameStart, p.name.length)) return p.type
}
}
if (d is MiniClassDecl) {
for (m in d.members) {
if (m.name == name && src.offsetOf(m.nameStart) == startOffset) {
if (m.name == name && matches(m.nameStart, m.name.length)) {
return when (m) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) {
inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
} else null
else -> null
}
}
}
for (cf in d.ctorFields) {
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
if (cf.name == name && matches(cf.nameStart, cf.name.length)) return cf.type
}
for (cf in d.classFields) {
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
if (cf.name == name && matches(cf.nameStart, cf.name.length)) return cf.type
}
}
}
@ -157,6 +160,21 @@ object DocLookupUtils {
return result.toList()
}
fun extractLocalsAt(text: String, offset: Int): Set<String> {
val res = mutableSetOf<String>()
// 1) find val/var declarations
val re = Regex("(?:^|[\\n;])\\s*(?:val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)")
re.findAll(text).forEach { m ->
if (m.range.first < offset) res.add(m.groupValues[1])
}
// 2) find implicit assignments
val re2 = Regex("(?:^|[\\n;])\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*=[^=]")
re2.findAll(text).forEach { m ->
if (m.range.first < offset) res.add(m.groupValues[1])
}
return res
}
fun extractImportsFromText(text: String): List<String> {
val result = LinkedHashSet<String>()
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
@ -232,24 +250,65 @@ object DocLookupUtils {
for ((name, list) in buckets) {
result[name] = mergeClassDecls(name, list)
}
// Root object alias
if (result.containsKey("Obj") && !result.containsKey("Any")) {
result["Any"] = result["Obj"]!!
}
return result
}
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniNamedDecl>? {
val classes = aggregateClasses(importedModules, localMini)
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
val cls = classes[name] ?: return null
cls.members.firstOrNull { it.name == member }?.let { return name to it }
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniNamedDecl>? {
if (!visited.add(name)) return null
for (baseName in cls.bases) {
dfs(baseName, visited)?.let { return it }
val cls = classes[name]
if (cls != null) {
cls.members.firstOrNull { it.name == member }?.let { return name to it }
for (baseName in cls.bases) {
dfs(baseName, visited)?.let { return it }
}
}
// Check for local extensions in this class or bases
localMini?.declarations?.firstOrNull { d ->
(d is MiniFunDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name && d.name == member) ||
(d is MiniValDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name && d.name == member)
}?.let { return name to it }
return null
}
return dfs(className, mutableSetOf())
}
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
fun collectExtensionMemberNames(importedModules: List<String>, className: String, localMini: MiniScript? = null): Set<String> {
val classes = aggregateClasses(importedModules, localMini)
val visited = mutableSetOf<String>()
val result = mutableSetOf<String>()
fun dfs(name: String) {
if (!visited.add(name)) return
// 1) stdlib extensions from BuiltinDocRegistry
result.addAll(BuiltinDocRegistry.extensionMemberNamesFor(name))
// 2) local extensions from mini
localMini?.declarations?.forEach { d ->
if (d is MiniFunDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name) result.add(d.name)
if (d is MiniValDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name) result.add(d.name)
}
// 3) bases
classes[name]?.bases?.forEach { dfs(it) }
}
dfs(className)
// Hardcoded supplements for common containers if not explicitly in bases
if (className == "List" || className == "Array") {
dfs("Collection")
dfs("Iterable")
}
dfs("Any")
dfs("Obj")
return result
}
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniNamedDecl>? {
val classes = aggregateClasses(importedModules, localMini)
// Preferred order for ambiguous common ops
val preference = listOf("Iterable", "Iterator", "List")
@ -301,6 +360,12 @@ object DocLookupUtils {
if (mini == null) return null
val i = prevNonWs(text, dotPos - 1)
if (i < 0) return null
// Handle indexing x[0]. or literal [1].
if (text[i] == ']') {
return guessReceiverClass(text, dotPos, imported, mini)
}
val wordRange = wordRangeAt(text, i + 1) ?: return null
val ident = text.substring(wordRange.first, wordRange.second)
@ -310,14 +375,14 @@ object DocLookupUtils {
if (ref != null) {
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
if (sym != null) {
val type = findTypeByRange(mini, sym.name, sym.declStart)
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
simpleClassNameOf(type)?.let { return it }
}
} else {
// Check if it's a declaration (e.g. static access to a class)
val sym = binding.symbols.firstOrNull { it.declStart == wordRange.first && it.name == ident }
if (sym != null) {
val type = findTypeByRange(mini, sym.name, sym.declStart)
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
simpleClassNameOf(type)?.let { return it }
// if it's a class/enum, return its name directly
if (sym.kind == net.sergeych.lyng.binding.SymbolKind.Class || sym.kind == net.sergeych.lyng.binding.SymbolKind.Enum) return sym.name
@ -325,13 +390,17 @@ object DocLookupUtils {
}
}
// 1) Global declarations in current file (val/var/fun/class/enum)
val d = mini.declarations.firstOrNull { it.name == ident }
// 1) Declarations in current file (val/var/fun/class/enum), prioritized by proximity
val src = mini.range.start.source
val d = mini.declarations
.filter { it.name == ident && src.offsetOf(it.nameStart) < dotPos }
.maxByOrNull { src.offsetOf(it.nameStart) }
if (d != null) {
return when (d) {
is MiniClassDecl -> d.name
is MiniEnumDecl -> d.name
is MiniValDecl -> simpleClassNameOf(d.type)
is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini))
is MiniFunDecl -> simpleClassNameOf(d.returnType)
}
}
@ -343,6 +412,9 @@ object DocLookupUtils {
}
}
// 2a) Try to find plain assignment in text if not found in declarations: x = test()
inferTypeFromAssignmentInText(ident, text, imported, mini, beforeOffset = dotPos)?.let { return simpleClassNameOf(it) }
// 3) Recursive chaining: Base.ident.
val dotBefore = findDotLeft(text, wordRange.first)
if (dotBefore != null) {
@ -353,7 +425,7 @@ object DocLookupUtils {
if (resolved != null) {
val rt = when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
else -> null
}
return simpleClassNameOf(rt)
@ -368,6 +440,43 @@ object DocLookupUtils {
return null
}
private fun inferTypeFromAssignmentInText(ident: String, text: String, imported: List<String>, mini: MiniScript?, beforeOffset: Int = Int.MAX_VALUE): MiniTypeRef? {
// Heuristic: search for "val ident =" or "ident =" in text
val re = Regex("(?:^|[\\n;])\\s*(?:val|var)?\\s*${ident}\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?\\s*(?:=|by)\\s*([^\\n;]+)")
val match = re.findAll(text)
.filter { it.range.first < beforeOffset }
.lastOrNull() ?: return null
val explicitType = match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
if (explicitType != null) return syntheticTypeRef(explicitType)
val expr = match.groupValues.getOrNull(2)?.let { stripComments(it) } ?: return null
return inferTypeRefFromExpression(expr, imported, mini, contextText = text, beforeOffset = beforeOffset)
}
private fun stripComments(text: String): String {
var result = ""
var i = 0
var inString = false
while (i < text.length) {
val ch = text[i]
if (ch == '"' && (i == 0 || text[i - 1] != '\\')) {
inString = !inString
}
if (!inString && ch == '/' && i + 1 < text.length) {
if (text[i + 1] == '/') break // single line comment
if (text[i + 1] == '*') {
// Skip block comment
i += 2
while (i + 1 < text.length && !(text[i] == '*' && text[i + 1] == '/')) i++
i += 2 // Skip '*/'
continue
}
}
result += ch
i++
}
return result.trim()
}
fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
if (mini == null) return null
var i = prevNonWs(text, dotPos - 1)
@ -420,7 +529,9 @@ object DocLookupUtils {
val rt = when (m) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
is MiniInitDecl -> null
is MiniFunDecl -> m.returnType
is MiniValDecl -> m.type
else -> null
}
simpleClassNameOf(rt)
}
@ -455,13 +566,25 @@ object DocLookupUtils {
return map
}
fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null, beforeOffset: Int = dotPos): String? {
guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
var i = prevNonWs(text, dotPos - 1)
if (i >= 0) {
when (text[i]) {
'"' -> return "String"
']' -> return "List"
']' -> {
// Check if literal or indexing
val matchingOpen = findMatchingOpenBracket(text, i)
if (matchingOpen != null && matchingOpen > 0) {
val beforeOpen = prevNonWs(text, matchingOpen - 1)
if (beforeOpen >= 0 && (isIdentChar(text[beforeOpen]) || text[beforeOpen] == ')' || text[beforeOpen] == ']')) {
// Likely indexing: infer type of full expression
val exprText = text.substring(0, i + 1)
return simpleClassNameOf(inferTypeRefFromExpression(exprText, imported, mini, beforeOffset = beforeOffset))
}
}
return "List"
}
'}' -> return "Dict"
')' -> {
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
@ -564,7 +687,9 @@ object DocLookupUtils {
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type
is MiniInitDecl -> null
is MiniFunDecl -> member.returnType
is MiniValDecl -> member.type
else -> null
}
return simpleClassNameOf(ret)
}
@ -627,11 +752,216 @@ object DocLookupUtils {
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
is MiniMemberValDecl -> member.type
is MiniInitDecl -> null
is MiniFunDecl -> member.returnType
is MiniValDecl -> member.type
else -> null
}
return simpleClassNameOf(ret)
}
fun inferTypeRefFromExpression(text: String, imported: List<String>, mini: MiniScript? = null, contextText: String? = null, beforeOffset: Int = Int.MAX_VALUE): MiniTypeRef? {
val trimmed = stripComments(text)
if (trimmed.isEmpty()) return null
val fullText = contextText ?: text
// 1) Literals
if (trimmed.startsWith("\"")) return syntheticTypeRef("String")
if (trimmed.startsWith("[")) return syntheticTypeRef("List")
if (trimmed.startsWith("{")) return syntheticTypeRef("Dict")
if (trimmed == "true" || trimmed == "false") return syntheticTypeRef("Boolean")
if (trimmed.all { it.isDigit() || it == '.' || it == '_' || it == 'e' || it == 'E' }) {
val hasDigits = trimmed.any { it.isDigit() }
if (hasDigits)
return if (trimmed.contains('.') || trimmed.contains('e', ignoreCase = true)) syntheticTypeRef("Real") else syntheticTypeRef("Int")
}
// 2) Function/Constructor calls or Indexing
if (trimmed.endsWith(")")) {
val openParen = findMatchingOpenParen(trimmed, trimmed.length - 1)
if (openParen != null && openParen > 0) {
var j = openParen - 1
while (j >= 0 && trimmed[j].isWhitespace()) j--
val end = j + 1
while (j >= 0 && isIdentChar(trimmed[j])) j--
val start = j + 1
if (start < end) {
val callee = trimmed.substring(start, end)
// Check if it's a member call (dot before callee)
var k = start - 1
while (k >= 0 && trimmed[k].isWhitespace()) k--
if (k >= 0 && trimmed[k] == '.') {
val prevDot = k
// Recursive: try to infer type of what's before the dot
val receiverText = trimmed.substring(0, prevDot)
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
val receiverClass = simpleClassNameOf(receiverType)
if (receiverClass != null) {
val resolved = resolveMemberWithInheritance(imported, receiverClass, callee, mini)
if (resolved != null) {
return when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
else -> null
}
}
}
} else {
// Top-level call or constructor
val classes = aggregateClasses(imported, mini)
if (classes.containsKey(callee)) return syntheticTypeRef(callee)
for (mod in imported) {
val decls = BuiltinDocRegistry.docsForModule(mod)
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
if (fn != null) return fn.returnType
}
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.firstOrNull { it.name == callee }?.let { return it.returnType }
}
}
}
}
if (trimmed.endsWith("]")) {
val openBracket = findMatchingOpenBracket(trimmed, trimmed.length - 1)
if (openBracket != null && openBracket > 0) {
val receiverText = trimmed.substring(0, openBracket).trim()
if (receiverText.isNotEmpty()) {
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
if (receiverType is MiniGenericType) {
val baseName = simpleClassNameOf(receiverType.base)
if (baseName == "List" && receiverType.args.isNotEmpty()) {
return receiverType.args[0]
}
if (baseName == "Map" && receiverType.args.size >= 2) {
return receiverType.args[1]
}
}
// Fallback for non-generic collections or if base name matches
val baseName = simpleClassNameOf(receiverType)
if (baseName == "List" || baseName == "Array" || baseName == "String") {
if (baseName == "String") return syntheticTypeRef("Char")
return syntheticTypeRef("Any")
}
}
}
}
// 3) Member field or simple identifier at the end
val lastWord = wordRangeAt(trimmed, trimmed.length)
if (lastWord != null && lastWord.second == trimmed.length) {
val ident = trimmed.substring(lastWord.first, lastWord.second)
var k = lastWord.first - 1
while (k >= 0 && trimmed[k].isWhitespace()) k--
if (k >= 0 && trimmed[k] == '.') {
// Member field: receiver.ident
val receiverText = trimmed.substring(0, k).trim()
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
val receiverClass = simpleClassNameOf(receiverType)
if (receiverClass != null) {
val resolved = resolveMemberWithInheritance(imported, receiverClass, ident, mini)
if (resolved != null) {
return when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
else -> null
}
}
}
} else {
// Simple identifier
// 1) Declarations in current file (val/var/fun/class/enum), prioritized by proximity
val src = mini?.range?.start?.source
val d = if (src != null) {
mini.declarations
.filter { it.name == ident && src.offsetOf(it.nameStart) < beforeOffset }
.maxByOrNull { src.offsetOf(it.nameStart) }
} else {
mini?.declarations?.firstOrNull { it.name == ident }
}
if (d != null) {
return when (d) {
is MiniClassDecl -> syntheticTypeRef(d.name)
is MiniEnumDecl -> syntheticTypeRef(d.name)
is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini)
is MiniFunDecl -> d.returnType
}
}
// 2) Parameters in any function
for (fd in mini?.declarations?.filterIsInstance<MiniFunDecl>() ?: emptyList()) {
for (p in fd.params) {
if (p.name == ident) return p.type
}
}
// 3) Try to find plain assignment in text: ident = expr
inferTypeFromAssignmentInText(ident, fullText, imported, mini, beforeOffset = beforeOffset)?.let { return it }
// 4) Check if it's a known class (static access)
val classes = aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return syntheticTypeRef(ident)
}
}
return null
}
private fun findMatchingOpenBracket(text: String, closeBracketPos: Int): Int? {
if (closeBracketPos < 0 || closeBracketPos >= text.length || text[closeBracketPos] != ']') return null
var depth = 0
var i = closeBracketPos - 1
while (i >= 0) {
when (text[i]) {
']' -> depth++
'[' -> if (depth == 0) return i else depth--
}
i--
}
return null
}
private fun findMatchingOpenParen(text: String, closeParenPos: Int): Int? {
if (closeParenPos < 0 || closeParenPos >= text.length || text[closeParenPos] != ')') return null
var depth = 0
var i = closeParenPos - 1
while (i >= 0) {
when (text[i]) {
')' -> depth++
'(' -> if (depth == 0) return i else depth--
}
i--
}
return null
}
private fun syntheticTypeRef(name: String): MiniTypeRef =
MiniTypeName(MiniRange(net.sergeych.lyng.Pos.builtIn, net.sergeych.lyng.Pos.builtIn),
listOf(MiniTypeName.Segment(name, MiniRange(net.sergeych.lyng.Pos.builtIn, net.sergeych.lyng.Pos.builtIn))), false)
fun inferTypeRefForVal(vd: MiniValDecl, text: String, imported: List<String>, mini: MiniScript?): MiniTypeRef? {
return inferTypeRefFromInitRange(vd.initRange, vd.nameStart, text, imported, mini)
}
fun inferTypeRefFromInitRange(initRange: MiniRange?, nameStart: net.sergeych.lyng.Pos, text: String, imported: List<String>, mini: MiniScript?): MiniTypeRef? {
val range = initRange ?: return null
val src = mini?.range?.start?.source ?: return null
val start = src.offsetOf(range.start)
val end = src.offsetOf(range.end)
if (start < 0 || start >= end || end > text.length) return null
var exprText = text.substring(start, end).trim()
if (exprText.startsWith("=")) {
exprText = exprText.substring(1).trim()
}
if (exprText.startsWith("by")) {
exprText = exprText.substring(2).trim()
}
val beforeOffset = src.offsetOf(nameStart)
return inferTypeRefFromExpression(exprText, imported, mini, contextText = text, beforeOffset = beforeOffset)
}
fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
null -> null
is MiniTypeName -> t.segments.lastOrNull()?.name
@ -667,13 +997,13 @@ object DocLookupUtils {
fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl {
val staticMembers = mutableListOf<MiniMemberDecl>()
// entries: List
staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, en.nameStart, isStatic = true))
staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, null, en.nameStart, isStatic = true))
// valueOf(name: String): Enum
staticMembers.add(MiniMemberFunDecl(en.range, "valueOf", listOf(MiniParam("name", null, en.nameStart)), null, null, en.nameStart, isStatic = true))
// Also add each entry as a static member (const)
for (entry in en.entries) {
staticMembers.add(MiniMemberValDecl(en.range, entry, false, MiniTypeName(en.range, listOf(MiniTypeName.Segment(en.name, en.range)), false), null, en.nameStart, isStatic = true))
staticMembers.add(MiniMemberValDecl(en.range, entry, false, MiniTypeName(en.range, listOf(MiniTypeName.Segment(en.name, en.range)), false), null, null, en.nameStart, isStatic = true))
}
return MiniClassDecl(

View File

@ -81,7 +81,7 @@ data class MiniTypeVar(
) : MiniTypeRef
// Script and declarations (lean subset; can be extended later)
sealed interface MiniDecl : MiniNode {
sealed interface MiniNamedDecl : MiniNode {
val name: String
val doc: MiniDoc?
// Start position of the declaration name identifier in source; end can be derived as start + name.length
@ -89,6 +89,8 @@ sealed interface MiniDecl : MiniNode {
val isExtern: Boolean
}
sealed interface MiniDecl : MiniNamedDecl
data class MiniScript(
override val range: MiniRange,
val declarations: MutableList<MiniDecl> = mutableListOf(),
@ -172,12 +174,8 @@ data class MiniIdentifier(
) : MiniNode
// --- Class member declarations (for built-in/registry docs) ---
sealed interface MiniMemberDecl : MiniNode {
val name: String
val doc: MiniDoc?
val nameStart: Pos
sealed interface MiniMemberDecl : MiniNamedDecl {
val isStatic: Boolean
val isExtern: Boolean
}
data class MiniMemberFunDecl(
@ -189,6 +187,7 @@ data class MiniMemberFunDecl(
override val nameStart: Pos,
override val isStatic: Boolean = false,
override val isExtern: Boolean = false,
val body: MiniBlock? = null
) : MiniMemberDecl
data class MiniMemberValDecl(
@ -196,6 +195,7 @@ data class MiniMemberValDecl(
override val name: String,
val mutable: Boolean,
val type: MiniTypeRef?,
val initRange: MiniRange?,
override val doc: MiniDoc?,
override val nameStart: Pos,
override val isStatic: Boolean = false,
@ -222,6 +222,9 @@ interface MiniAstSink {
fun onEnterClass(node: MiniClassDecl) {}
fun onExitClass(end: Pos) {}
fun onEnterFunction(node: MiniFunDecl?) {}
fun onExitFunction(end: Pos) {}
fun onImport(node: MiniImport) {}
fun onFunDecl(node: MiniFunDecl) {}
fun onValDecl(node: MiniValDecl) {}
@ -254,6 +257,7 @@ class MiniAstBuilder : MiniAstSink {
private val classStack = ArrayDeque<MiniClassDecl>()
private var lastDoc: MiniDoc? = null
private var scriptDepth: Int = 0
private var functionDepth: Int = 0
fun build(): MiniScript? = currentScript
@ -291,6 +295,14 @@ class MiniAstBuilder : MiniAstSink {
}
}
override fun onEnterFunction(node: MiniFunDecl?) {
functionDepth++
}
override fun onExitFunction(end: Pos) {
functionDepth--
}
override fun onImport(node: MiniImport) {
currentScript?.imports?.add(node)
}
@ -298,7 +310,7 @@ class MiniAstBuilder : MiniAstSink {
override fun onFunDecl(node: MiniFunDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
val currentClass = classStack.lastOrNull()
if (currentClass != null) {
if (currentClass != null && functionDepth == 0) {
// Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members
val member = MiniMemberFunDecl(
range = attach.range,
@ -308,13 +320,29 @@ class MiniAstBuilder : MiniAstSink {
doc = attach.doc,
nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed
isExtern = attach.isExtern
isExtern = attach.isExtern,
body = attach.body
)
// 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))
// Check if we already have this member (from a previous onFunDecl call for the same function)
val existing = currentClass.members.filterIsInstance<MiniMemberFunDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
if (existing != null) {
val members = currentClass.members.map { if (it === existing) member else it }
classStack.removeLast()
classStack.addLast(currentClass.copy(members = members))
} else {
classStack.removeLast()
classStack.addLast(currentClass.copy(members = currentClass.members + member))
}
} else {
currentScript?.declarations?.add(attach)
// Check if already in declarations to avoid duplication
val existing = currentScript?.declarations?.find { it.name == attach.name && it.nameStart == attach.nameStart }
if (existing != null) {
val idx = currentScript?.declarations?.indexOf(existing) ?: -1
if (idx >= 0) currentScript?.declarations?.set(idx, attach)
} else {
currentScript?.declarations?.add(attach)
}
}
lastDoc = null
}
@ -322,21 +350,36 @@ class MiniAstBuilder : MiniAstSink {
override fun onValDecl(node: MiniValDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
val currentClass = classStack.lastOrNull()
if (currentClass != null) {
if (currentClass != null && functionDepth == 0) {
val member = MiniMemberValDecl(
range = attach.range,
name = attach.name,
mutable = attach.mutable,
type = attach.type,
initRange = attach.initRange,
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))
// Duplicates for vals are rare but possible if Compiler calls it twice
val existing = currentClass.members.filterIsInstance<MiniMemberValDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
if (existing != null) {
val members = currentClass.members.map { if (it === existing) member else it }
classStack.removeLast()
classStack.addLast(currentClass.copy(members = members))
} else {
classStack.removeLast()
classStack.addLast(currentClass.copy(members = currentClass.members + member))
}
} else {
currentScript?.declarations?.add(attach)
val existing = currentScript?.declarations?.find { it.name == attach.name && it.nameStart == attach.nameStart }
if (existing != null) {
val idx = currentScript?.declarations?.indexOf(existing) ?: -1
if (idx >= 0) currentScript?.declarations?.set(idx, attach)
} else {
currentScript?.declarations?.add(attach)
}
}
lastDoc = null
}

View File

@ -4,8 +4,6 @@
*/
package net.sergeych.lyng.miniast
import net.sergeych.lyng.obj.ObjString
object StdlibDocsBootstrap {
// Simple idempotent guard; races are harmless as initializer side-effects are idempotent
private var ensured = false
@ -16,7 +14,25 @@ object StdlibDocsBootstrap {
// Touch core Obj* types whose docs are registered via addFnDoc/addConstDoc
// Accessing .type forces their static initializers to run and register docs.
@Suppress("UNUSED_VARIABLE")
val _string = ObjString.type
val _string = net.sergeych.lyng.obj.ObjString.type
@Suppress("UNUSED_VARIABLE")
val _any = net.sergeych.lyng.obj.Obj.rootObjectType
@Suppress("UNUSED_VARIABLE")
val _list = net.sergeych.lyng.obj.ObjList.type
@Suppress("UNUSED_VARIABLE")
val _map = net.sergeych.lyng.obj.ObjMap.type
@Suppress("UNUSED_VARIABLE")
val _int = net.sergeych.lyng.obj.ObjInt.type
@Suppress("UNUSED_VARIABLE")
val _real = net.sergeych.lyng.obj.ObjReal.type
@Suppress("UNUSED_VARIABLE")
val _bool = net.sergeych.lyng.obj.ObjBool.type
@Suppress("UNUSED_VARIABLE")
val _regex = net.sergeych.lyng.obj.ObjRegex.type
@Suppress("UNUSED_VARIABLE")
val _range = net.sergeych.lyng.obj.ObjRange.type
@Suppress("UNUSED_VARIABLE")
val _buffer = net.sergeych.lyng.obj.ObjBuffer.type
} catch (_: Throwable) {
// Best-effort; absence should not break consumers
} finally {

View File

@ -25,6 +25,9 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.serializer
import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
@ -524,20 +527,46 @@ open class Obj {
companion object {
val rootObjectType = ObjClass("Obj").apply {
addFn("toString", true) {
addFnDoc(
name = "toString",
doc = "Returns a string representation of the object.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.toString(this, true)
}
addFn("inspect", true) {
addFnDoc(
name = "inspect",
doc = "Returns a detailed string representation for debugging.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.inspect(this).toObj()
}
addFn("contains") {
addFnDoc(
name = "contains",
doc = "Returns true if the object contains the given element.",
params = listOf(ParamDoc("element")),
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
ObjBool(thisObj.contains(this, args.firstAndOnly()))
}
// utilities
addFn("let") {
addFnDoc(
name = "let",
doc = "Calls the specified function block with `this` value as its argument and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
}
addFn("apply") {
addFnDoc(
name = "apply",
doc = "Calls the specified function block with `this` value as its receiver and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
val body = args.firstAndOnly()
(thisObj as? ObjInstance)?.let {
body.callOn(ApplyScope(this, it.instanceScope))
@ -546,11 +575,21 @@ open class Obj {
}
thisObj
}
addFn("also") {
addFnDoc(
name = "also",
doc = "Calls the specified function block with `this` value as its argument and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
thisObj
}
addFn("run") {
addFnDoc(
name = "run",
doc = "Calls the specified function block with `this` value as its receiver and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(this)
}
addFn("getAt") {
@ -563,7 +602,12 @@ open class Obj {
thisObj.putAt(this, requiredArg<Obj>(0), newValue)
newValue
}
addFn("toJsonString") {
addFnDoc(
name = "toJsonString",
doc = "Encodes this object to a JSON string.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.toJson(this).toString().toObj()
}
}

View File

@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
@ -178,7 +179,12 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
}
}.apply {
addFn("toInt") {
addFnDoc(
name = "toInt",
doc = "Returns this integer (identity operation).",
returns = net.sergeych.lyng.miniast.type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
thisObj
}
}

View File

@ -21,6 +21,7 @@
package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.*
import kotlin.test.Test
import kotlin.test.assertEquals
@ -274,6 +275,89 @@ class MiniAstTest {
assertEquals("Doc6", e1.doc?.summary)
}
@Test
fun resolve_inferred_member_type() = runTest {
val code = """
object O3 {
val name = "ozone"
}
val x = O3.name
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
val type = DocLookupUtils.findTypeByRange(mini, "x", code.indexOf("val x") + 4, code, emptyList())
assertEquals("String", DocLookupUtils.simpleClassNameOf(type))
}
@Test
fun resolve_inferred_val_type_from_extern_fun() = runTest {
val code = """
extern fun test(a: Int): List<Int>
val x = test(1)
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val inferred = DocLookupUtils.inferTypeRefForVal(vd, code, emptyList(), mini)
assertNotNull(inferred)
assertTrue(inferred is MiniGenericType)
assertEquals("List", (inferred.base as MiniTypeName).segments.last().name)
val code2 = """
extern fun test2(a: Int): String
val y = test2(1)
""".trimIndent()
val (_, sink2) = compileWithMini(code2)
val mini2 = sink2.build()
val vd2 = mini2?.declarations?.filterIsInstance<MiniValDecl>()?.firstOrNull { it.name == "y" }
assertNotNull(vd2)
val inferred2 = DocLookupUtils.inferTypeRefForVal(vd2, code2, emptyList(), mini2)
assertNotNull(inferred2)
assertTrue(inferred2 is MiniTypeName)
assertEquals("String", inferred2.segments.last().name)
val code3 = """
extern object API {
fun getData(): List<String>
}
val x = API.getData()
""".trimIndent()
val (_, sink3) = compileWithMini(code3)
val mini3 = sink3.build()
val vd3 = mini3?.declarations?.filterIsInstance<MiniValDecl>()?.firstOrNull { it.name == "x" }
assertNotNull(vd3)
val inferred3 = DocLookupUtils.inferTypeRefForVal(vd3, code3, emptyList(), mini3)
assertNotNull(inferred3)
assertTrue(inferred3 is MiniGenericType)
assertEquals("List", (inferred3.base as MiniTypeName).segments.last().name)
}
@Test
fun resolve_inferred_val_type_cross_script() = runTest {
val dCode = "extern fun test(a: Int): List<Int>"
val mainCode = "val x = test(1)"
val (_, dSink) = compileWithMini(dCode)
val dMini = dSink.build()!!
val (_, mainSink) = compileWithMini(mainCode)
val mainMini = mainSink.build()!!
// Merge manually
val merged = mainMini.copy(declarations = (mainMini.declarations + dMini.declarations).toMutableList())
val vd = merged.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val inferred = DocLookupUtils.inferTypeRefForVal(vd, mainCode, emptyList(), merged)
assertNotNull(inferred)
assertTrue(inferred is MiniGenericType)
assertEquals("List", (inferred.base as MiniTypeName).segments.last().name)
}
@Test
fun miniAst_captures_user_sample_extern_doc() = runTest {
val code = """
@ -311,4 +395,48 @@ class MiniAstTest {
assertEquals("O3", resolved.first)
assertEquals("doc for name", resolved.second.doc?.summary)
}
@Test
fun miniAst_captures_nested_generics() = runTest {
val code = """
val x: Map<String, List<Int>> = {}
"""
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val ty = vd.type as MiniGenericType
assertEquals("Map", (ty.base as MiniTypeName).segments.last().name)
assertEquals(2, ty.args.size)
val arg1 = ty.args[0] as MiniTypeName
assertEquals("String", arg1.segments.last().name)
val arg2 = ty.args[1] as MiniGenericType
assertEquals("List", (arg2.base as MiniTypeName).segments.last().name)
assertEquals(1, arg2.args.size)
val innerArg = arg2.args[0] as MiniTypeName
assertEquals("Int", innerArg.segments.last().name)
}
@Test
fun inferTypeForValWithInference() = runTest {
val code = """
extern fun test(): List<Int>
val x = test()
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
val imported = listOf("lyng.stdlib")
val src = mini.range.start.source
val type = DocLookupUtils.findTypeByRange(mini, "x", src.offsetOf(vd.nameStart), code, imported)
assertNotNull(type)
val className = DocLookupUtils.simpleClassNameOf(type)
assertEquals("List", className)
}
}

View File

@ -174,4 +174,308 @@ class CompletionEngineLightTest {
val ns = names(items)
assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns")
}
@Test
fun inferredTypeFromFunctionCall() = runBlocking {
val code = """
extern fun test(a: Int): List<Int>
val x = test(1)
val y = x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type, but got: $ns")
}
@Test
fun inferredTypeFromMemberCall() = runBlocking {
val code = """
extern class MyClass {
fun getList(): List<String>
}
extern val c: MyClass
val x = c.getList()
val y = x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type from member call, but got: $ns")
}
@Test
fun inferredTypeFromListLiteral() = runBlocking {
val code = """
val x = [1, 2, 3]
val y = x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type from literal, but got: $ns")
}
@Test
fun inferredTypeAfterIndexing() = runBlocking {
val code = """
extern fun test(): List<String>
val x = test()
val y = x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// Should contain String members, e.g., 'length' or 're'
assertTrue(ns.contains("length"), "String member 'length' should be suggested after indexing List<String>, but got: $ns")
assertTrue(ns.contains("re"), "String member 're' should be suggested after indexing List<String>, but got: $ns")
}
@Test
fun inferredTypeFromAssignmentWithoutVal() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for variable assigned without 'val', but got: $ns")
assertTrue(ns.contains("add"), "List member 'add' should be suggested for variable assigned without 'val', but got: $ns")
}
@Test
fun inferredTypeAfterIndexingWithoutVal() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// String members include 'trim', 'lower', etc.
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for x[0] where x assigned without val, but got: $ns")
assertFalse(ns.contains("add"), "List member 'add' should NOT be suggested for x[0], but got: $ns")
}
@Test
fun transitiveInferenceWithoutVal() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
y = x
y.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for transitive inference, but got: $ns")
}
@Test
fun objectMemberReturnInference() = runBlocking {
val code = """
object O {
fun getList(): List<String> = []
}
O.getList().<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for object member call, but got: $ns")
}
@Test
fun directFunctionCallCompletion() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
test(1).<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for direct function call, but got: $ns")
assertTrue(ns.contains("map"), "Inherited member 'map' should be suggested for List, but got: $ns")
}
@Test
fun completionWithTrailingDotError() = runBlocking {
// This simulates typing mid-expression where the script is technically invalid
val code = """
extern fun test(): List<String>
test().<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested even if script ends with a dot, but got: $ns")
}
@Test
fun listLiteralCompletion() = runBlocking {
val code = "[].<caret>"
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for [], but got: $ns")
assertTrue(ns.contains("map"), "Inherited member 'map' should be suggested for [], but got: $ns")
}
@Test
fun userReportedSample() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
x = test(1)
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for x, but got: $ns")
}
@Test
fun userReportedSampleIndexed() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
x = test(1)
x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "String member 'size' should be suggested for x[0], but got: $ns")
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for x[0], but got: $ns")
}
@Test
fun userReportedSampleImplicitVariable() = runBlocking {
val code = """
extern fun test(): List<String>
x = test()
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for implicit variable x, but got: $ns")
}
@Test
fun userReportedSampleNoDot() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
x = test(1)
x[0]<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("x"), "Implicit variable 'x' should be suggested as global, but got: $ns")
}
@Test
fun userReportedIssue_X_equals_test2() = runBlocking {
val code = """
extern fun test2(): List<String>
x = test2
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// Since test2 is a function, x = test2 (without parens) should probably be the function itself,
// but current DocLookupUtils returns returnType.
// If it returns List<String>, then size should be there.
assertTrue(ns.contains("size"), "List member 'size' should be suggested for x = test2, but got: $ns")
}
@Test
fun anyMembersOnInferred() = runBlocking {
val code = """
x = 42
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("toString"), "Any member 'toString' should be suggested for x=42, but got: $ns")
assertTrue(ns.contains("let"), "Any member 'let' should be suggested for x=42, but got: $ns")
}
@Test
fun charMembersOnIndexedString() = runBlocking {
val code = """
x = "hello"
x[0].<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("code"), "Char member 'code' should be suggested for indexed string x[0], but got: $ns")
assertTrue(ns.contains("toString"), "Any member 'toString' should be suggested for x[0], but got: $ns")
}
@Test
fun extensionMemberOnInferredList() = runBlocking {
val code = """
extern fun getNames(): List<String>
ns = getNames()
ns.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("map"), "Extension member 'map' should be suggested for List, but got: $ns")
assertTrue(ns.contains("filter"), "Extension member 'filter' should be suggested for List, but got: $ns")
}
@Test
fun inferredTypeFromExternFunWithVal() = runBlocking {
val code = """
extern fun test(a: Int): List<Int>
val x = test(1)
x.<caret>
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for val x = test(1), but got: $ns")
}
@Test
fun userReportedNestedSample() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
class X(fld1, fld2) {
var prop
get() { 12 }
set(value) {
val x = test(2)
x.<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "List member 'size' should be suggested for local val x inside set(), but got: $ns")
}
@Test
fun userReportedNestedSampleIndexed() = runBlocking {
val code = """
extern fun test(value: Int): List<String>
class X(fld1, fld2) {
var prop
get() { 12 }
set(value) {
val x = test(2)
x[0].<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("size"), "String member 'size' should be suggested for local x[0] inside set(), but got: $ns")
}
@Test
fun nestedShadowingCompletion() = runBlocking {
val code = """
val x = 42
class X {
fun test() {
val x = "hello"
x.<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
// Should contain String members (like trim)
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for shadowed x, but got: $ns")
}
}