diff --git a/lyng-idea/build.gradle.kts b/lyng-idea/build.gradle.kts index c3296f1..820396d 100644 --- a/lyng-idea/build.gradle.kts +++ b/lyng-idea/build.gradle.kts @@ -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 { diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt index 72bd154..22acd83 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt @@ -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) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt index 0178483..dc812c0 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt @@ -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().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().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) } + docs.filterIsInstance().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) } // And classes/enums - docs.filterIsInstance().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) } - docs.filterIsInstance().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) } + docs.filterIsInstance().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) } + docs.filterIsInstance().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 { 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 { diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt index 2cc1114..75aaf81 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt @@ -63,6 +63,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase "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))) } diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt index b8d03f3..3a95380 100644 --- a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ArgBuilderAndroid.kt @@ -23,7 +23,7 @@ actual object ArgBuilderProvider { private val tl = object : ThreadLocal() { override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder() } - actual fun acquire(): ArgsBuilder = tl.get() + actual fun acquire(): ArgsBuilder = tl.get()!! } private class AndroidArgsBuilder : ArgsBuilder { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index dbec7f2..0abd75c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -973,7 +973,10 @@ class Compiler( private fun parseTypeDeclarationWithMini(): Pair { // 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 { // Parse a qualified base name: ID ('.' ID)* val segments = mutableListOf() 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? = null + // Optional generic arguments: '<' Type (',' Type)* '>' + var miniArgs: MutableList? = null + var semArgs: MutableList? = 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() - 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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt index bbc7f12..36d78df 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt @@ -34,18 +34,34 @@ class CompilerContext(val tokens: List) { } 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) { 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) { * @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) { 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 } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt index 29db318..309fac6 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt @@ -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, isNullable: Boolean) : TypeDecl(isNullable) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt index 20a5724..dc5eab5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -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 { ) // 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 { 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 { } // 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 { } // 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")) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt index 23dabab..39d99fd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt @@ -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 { + suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null, binding: BindingSnapshot? = null): List { // 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().sortedBy { it.name.lowercase() } val classes = decls.filterIsInstance().sortedBy { it.name.lowercase() } val enums = decls.filterIsInstance().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) + } } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt index 23d10a7..89a8fbc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt @@ -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? = 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 { + val res = mutableSetOf() + // 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 { val result = LinkedHashSet() 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, className: String, member: String, localMini: MiniScript? = null): Pair? { + fun resolveMemberWithInheritance(importedModules: List, className: String, member: String, localMini: MiniScript? = null): Pair? { val classes = aggregateClasses(importedModules, localMini) - fun dfs(name: String, visited: MutableSet): Pair? { - val cls = classes[name] ?: return null - cls.members.firstOrNull { it.name == member }?.let { return name to it } + fun dfs(name: String, visited: MutableSet): Pair? { 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, member: String, localMini: MiniScript? = null): Pair? { + fun collectExtensionMemberNames(importedModules: List, className: String, localMini: MiniScript? = null): Set { + val classes = aggregateClasses(importedModules, localMini) + val visited = mutableSetOf() + val result = mutableSetOf() + + 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, member: String, localMini: MiniScript? = null): Pair? { 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, 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, 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, mini: MiniScript? = null): String? { + fun guessReceiverClass(text: String, dotPos: Int, imported: List, 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, 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().firstOrNull { it.name == callee } + if (fn != null) return fn.returnType + } + mini?.declarations?.filterIsInstance()?.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() ?: 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, 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, 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() // 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( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt index e14aede..53c65bf 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt @@ -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 = 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() 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().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().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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt index 240f64c..171ba2a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/StdlibDocsBootstrap.kt @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 08144d9..0c1bf1e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -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(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() } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt index d75531a..0671102 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt @@ -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 } } diff --git a/lynglib/src/commonTest/kotlin/MiniAstTest.kt b/lynglib/src/commonTest/kotlin/MiniAstTest.kt index b1462d1..922b1d4 100644 --- a/lynglib/src/commonTest/kotlin/MiniAstTest.kt +++ b/lynglib/src/commonTest/kotlin/MiniAstTest.kt @@ -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 + val x = test(1) + """.trimIndent() + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + val vd = mini.declarations.filterIsInstance().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()?.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 + } + val x = API.getData() + """.trimIndent() + val (_, sink3) = compileWithMini(code3) + val mini3 = sink3.build() + val vd3 = mini3?.declarations?.filterIsInstance()?.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" + 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().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> = {} + """ + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + val vd = mini.declarations.filterIsInstance().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 + val x = test() + """.trimIndent() + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + + val vd = mini.declarations.filterIsInstance().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) + } } diff --git a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt index e66aafa..ee83f27 100644 --- a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt +++ b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt @@ -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 + val x = test(1) + val y = x. + """.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 + } + extern val c: MyClass + val x = c.getList() + val y = x. + """.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. + """.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 + val x = test() + val y = x[0]. + """.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, but got: $ns") + assertTrue(ns.contains("re"), "String member 're' should be suggested after indexing List, but got: $ns") + } + + @Test + fun inferredTypeFromAssignmentWithoutVal() = runBlocking { + val code = """ + extern fun test(): List + x = test() + x. + """.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 + x = test() + x[0]. + """.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 + x = test() + y = x + y. + """.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 = [] + } + O.getList(). + """.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 + test(1). + """.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 + test(). + """.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 = "[]." + 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 + x = test(1) + x. + """.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 + x = test(1) + x[0]. + """.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 + x = test() + x. + """.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 + x = test(1) + x[0] + """.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 + x = test2 + x. + """.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, 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. + """.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]. + """.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 + ns = getNames() + ns. + """.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 + val x = test(1) + x. + """.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 + class X(fld1, fld2) { + var prop + get() { 12 } + set(value) { + val x = test(2) + x. + } + } + """.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 + class X(fld1, fld2) { + var prop + get() { 12 } + set(value) { + val x = test(2) + x[0]. + } + } + """.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. + } + } + """.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") + } }