From 1efa96a990feb64a994ddf61d0776d4776bae5d3 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 12 Jan 2026 08:19:52 +0100 Subject: [PATCH] improved type inference in plugin --- .../idea/annotators/LyngExternalAnnotator.kt | 39 +++++++- .../completion/LyngCompletionContributor.kt | 49 +++------- .../idea/docs/LyngDocumentationProvider.kt | 13 +-- .../kotlin/net/sergeych/lyng/Compiler.kt | 5 +- .../net/sergeych/lyng/binding/Binder.kt | 97 ++++++++++++------- .../lyng/miniast/BuiltinDocRegistry.kt | 14 ++- .../lyng/miniast/CompletionEngineLight.kt | 30 +++--- .../sergeych/lyng/miniast/DocLookupUtils.kt | 37 ++++++- .../kotlin/net/sergeych/lynon/packer.kt | 34 +++++-- lynglib/src/commonTest/kotlin/MiniAstTest.kt | 15 +++ .../lyng/miniast/ParamTypeInferenceTest.kt | 55 +++++++++++ site/src/jsMain/kotlin/ReferencePage.kt | 13 +-- 12 files changed, 276 insertions(+), 125 deletions(-) create mode 100644 lynglib/src/commonTest/kotlin/net/sergeych/lyng/miniast/ParamTypeInferenceTest.kt diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt index 4176f0e..8eb98aa 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt @@ -117,9 +117,18 @@ class LyngExternalAnnotator : ExternalAnnotator().forEach { fn -> - if (fn.nameStart.source != source) return@forEach - fn.params.forEach { p -> putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER) } + fun addParams(params: List) { + params.forEach { p -> + if (p.nameStart.source == source) + putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER) + } + } + mini.declarations.forEach { d -> + when (d) { + is MiniFunDecl -> addParams(d.params) + is MiniClassDecl -> d.members.filterIsInstance().forEach { addParams(it.params) } + else -> {} + } } // Type name segments (including generics base & args) @@ -146,8 +155,8 @@ class LyngExternalAnnotator : ExternalAnnotator {} } } - mini.declarations.forEach { d -> - if (d.nameStart.source != source) return@forEach + fun addDeclTypeSegments(d: MiniDecl) { + if (d.nameStart.source != source) return when (d) { is MiniFunDecl -> { addTypeSegments(d.returnType) @@ -161,10 +170,23 @@ class LyngExternalAnnotator : ExternalAnnotator { d.ctorFields.forEach { addTypeSegments(it.type) } d.classFields.forEach { addTypeSegments(it.type) } + for (m in d.members) { + when (m) { + is MiniMemberFunDecl -> { + addTypeSegments(m.returnType) + m.params.forEach { addTypeSegments(it.type) } + } + is MiniMemberValDecl -> { + addTypeSegments(m.type) + } + else -> {} + } + } } is MiniEnumDecl -> {} } } + mini.declarations.forEach { d -> addDeclTypeSegments(d) } ProgressManager.checkCanceled() @@ -212,6 +234,13 @@ class LyngExternalAnnotator : ExternalAnnotator d.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER } + is MiniClassDecl -> { + d.members.forEach { m -> + if (m is MiniMemberFunDecl) { + m.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER } + } + } + } else -> {} } } 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 e57d21a..62925ac 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 @@ -116,7 +116,7 @@ class LyngCompletionContributor : CompletionContributor() { if (memberDotPos != null && engineItems.isEmpty()) { if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Fallback: engine returned 0 in member context; trying local inference") // Build imported modules from text (lenient) + stdlib; avoid heavy MiniAst here - val fromText = extractImportsFromText(text) + val fromText = DocLookupUtils.extractImportsFromText(text) val imported = LinkedHashSet().apply { fromText.forEach { add(it) } add("lyng.stdlib") @@ -176,7 +176,7 @@ class LyngCompletionContributor : CompletionContributor() { // In member context, ensure stdlib extension-like methods (e.g., String.re) are present if (memberDotPos != null) { val existing = engineItems.map { it.name }.toMutableSet() - val fromText = extractImportsFromText(text) + val fromText = DocLookupUtils.extractImportsFromText(text) val imported = LinkedHashSet().apply { fromText.forEach { add(it) } add("lyng.stdlib") @@ -249,7 +249,7 @@ class LyngCompletionContributor : CompletionContributor() { // If in member context and engine items are suspiciously sparse, try to enrich via local inference + offerMembers if (memberDotPos != null && engineItems.size < 3) { if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Engine produced only ${engineItems.size} items in member context — trying enrichment") - val fromText = extractImportsFromText(text) + val fromText = DocLookupUtils.extractImportsFromText(text) val imported = LinkedHashSet().apply { fromText.forEach { add(it) } add("lyng.stdlib") @@ -410,13 +410,18 @@ class LyngCompletionContributor : CompletionContributor() { for (name in keys) { val list = map[name] ?: continue // Choose a representative for display: - // 1) Prefer a method with a known return type - // 2) Else any method - // 3) Else the first variant + // 1) Prefer a method with return type AND parameters + // 2) Prefer a method with parameters + // 3) Prefer a method with return type + // 4) Else any method + // 5) Else the first variant val rep = - list.asSequence() - .filterIsInstance() - .firstOrNull { it.returnType != null } + list.asSequence().filterIsInstance() + .firstOrNull { it.returnType != null && it.params.isNotEmpty() } + ?: list.asSequence().filterIsInstance() + .firstOrNull { it.params.isNotEmpty() } + ?: list.asSequence().filterIsInstance() + .firstOrNull { it.returnType != null } ?: list.firstOrNull { it is MiniMemberFunDecl } ?: list.first() when (rep) { @@ -603,32 +608,10 @@ class LyngCompletionContributor : CompletionContributor() { } } - // Lenient textual import extractor (duplicated from QuickDoc privately) - private 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) - re.findAll(text).forEach { m -> - val raw = m.groupValues.getOrNull(1)?.trim().orEmpty() - if (raw.isNotEmpty()) { - val canon = if (raw.startsWith("lyng.")) raw else "lyng.$raw" - result.add(canon) - } - } - return result.toList() - } private fun typeOf(t: MiniTypeRef?): String { - return when (t) { - null -> "" - is MiniTypeName -> t.segments.lastOrNull()?.name?.let { ": $it" } ?: "" - is MiniGenericType -> { - val base = typeOf(t.base).removePrefix(": ") - val args = t.args.joinToString(",") { typeOf(it).removePrefix(": ") } - ": ${base}<${args}>" - } - is MiniFunctionType -> ": (fn)" - is MiniTypeVar -> ": ${t.name}" - } + val s = DocLookupUtils.typeOf(t) + return if (s.isEmpty()) "" else ": $s" } } } 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 4954cbd..21421be 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 @@ -561,16 +561,9 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return sb.toString() } - private fun typeOf(t: MiniTypeRef?): String = when (t) { - is MiniTypeName -> ": ${t.segments.joinToString(".") { it.name }}${if (t.nullable) "?" else ""}" - is MiniGenericType -> { - val base = typeOf(t.base).removePrefix(": ") - val args = t.args.joinToString(", ") { typeOf(it).removePrefix(": ") } - ": ${base}<${args}>${if (t.nullable) "?" else ""}" - } - is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}" - is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}" - null -> ": Object?" + private fun typeOf(t: MiniTypeRef?): String { + val s = DocLookupUtils.typeOf(t) + return if (s.isEmpty()) (if (t == null) ": Object?" else "") else ": $s" } private fun signatureOf(fn: MiniFunDecl): String { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 87d642e..e23ed08 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -917,12 +917,13 @@ class Compiler( else -> null } + // type information (semantic + mini syntax) + val (typeInfo, miniType) = parseTypeDeclarationWithMini() + var defaultValue: Statement? = null cc.ifNextIs(Token.Type.ASSIGN) { defaultValue = parseExpression() } - // type information (semantic + mini syntax) - val (typeInfo, miniType) = parseTypeDeclarationWithMini() val isEllipsis = cc.skipTokenOfType(Token.Type.ELLIPSIS, isOptional = true) result += ArgsDeclaration.Item( t.value, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/binding/Binder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/binding/Binder.kt index 0cbcd58..cad8679 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/binding/Binder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/binding/Binder.kt @@ -35,7 +35,8 @@ data class Symbol( val kind: SymbolKind, val declStart: Int, val declEnd: Int, - val containerId: Int? + val containerId: Int?, + val type: String? = null ) data class Reference(val symbolId: Int, val start: Int, val end: Int) @@ -97,59 +98,83 @@ object Binder { // First pass (classes only): register classes so we can attach methods/fields for (d in mini.declarations) if (d is MiniClassDecl) { val (s, e) = nameOffsets(d.nameStart, d.name) - val sym = Symbol(nextId++, d.name, SymbolKind.Class, s, e, containerId = null) + val sym = Symbol(nextId++, d.name, SymbolKind.Class, s, e, containerId = null, type = d.name) symbols += sym topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id) // Prefer explicit body range; otherwise use the whole class declaration range val bodyStart = d.bodyRange?.start?.let { source.offsetOf(it) } ?: source.offsetOf(d.range.start) val bodyEnd = d.bodyRange?.end?.let { source.offsetOf(it) } ?: source.offsetOf(d.range.end) - classes += ClassScope(sym.id, bodyStart, bodyEnd, mutableListOf()) + val classScope = ClassScope(sym.id, bodyStart, bodyEnd, mutableListOf()) + classes += classScope // Constructor fields (val/var in primary ctor) for (cf in d.ctorFields) { val fs = source.offsetOf(cf.nameStart) val fe = fs + cf.name.length val kind = if (cf.mutable) SymbolKind.Variable else SymbolKind.Value - val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id) + val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(cf.type)) symbols += fieldSym - classes.last().fields += fieldSym.id + classScope.fields += fieldSym.id } // Class fields (val/var in class body, if any are reported here) for (cf in d.classFields) { val fs = source.offsetOf(cf.nameStart) val fe = fs + cf.name.length val kind = if (cf.mutable) SymbolKind.Variable else SymbolKind.Value - val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id) + val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(cf.type)) symbols += fieldSym - classes.last().fields += fieldSym.id + classScope.fields += fieldSym.id } + // Members (including fields and methods) + for (m in d.members) { + if (m is MiniMemberValDecl) { + val fs = source.offsetOf(m.nameStart) + val fe = fs + m.name.length + val kind = if (m.mutable) SymbolKind.Variable else SymbolKind.Value + val fieldSym = Symbol(nextId++, m.name, kind, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(m.type)) + symbols += fieldSym + classScope.fields += fieldSym.id + } + } + } + + fun registerFun(name: String, nameStart: net.sergeych.lyng.Pos, params: List, returnType: MiniTypeRef?, bodyRange: MiniRange?, isTopLevel: Boolean) { + val (s, e) = nameOffsets(nameStart, name) + val ownerClass = classContaining(s) + val sym = Symbol(nextId++, name, SymbolKind.Function, s, e, containerId = ownerClass?.symId, type = DocLookupUtils.typeOf(returnType)) + symbols += sym + if (isTopLevel) { + topLevelByName.getOrPut(name) { mutableListOf() }.add(sym.id) + } + + // Determine body range if present; otherwise, derive a conservative end at decl range end + val bodyStart = bodyRange?.start?.let { source.offsetOf(it) } ?: e + val bodyEnd = bodyRange?.end?.let { source.offsetOf(it) } ?: e + val fnScope = FnScope(sym.id, bodyStart, bodyEnd, mutableListOf(), ownerClass?.symId) + + // Params + for (p in params) { + val ps = source.offsetOf(p.nameStart) + val pe = ps + p.name.length + val pk = SymbolKind.Parameter + val paramSym = Symbol(nextId++, p.name, pk, ps, pe, containerId = sym.id, type = DocLookupUtils.typeOf(p.type)) + fnScope.locals += paramSym.id + symbols += paramSym + } + functions += fnScope } // Second pass: functions and top-level/class vals/vars for (d in mini.declarations) { when (d) { - is MiniClassDecl -> { /* already processed in first pass */ } - is MiniFunDecl -> { - val (s, e) = nameOffsets(d.nameStart, d.name) - val ownerClass = classContaining(s) - val sym = Symbol(nextId++, d.name, SymbolKind.Function, s, e, containerId = ownerClass?.symId) - symbols += sym - topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id) - - // Determine body range if present; otherwise, derive a conservative end at decl range end - val bodyStart = d.body?.range?.start?.let { source.offsetOf(it) } ?: e - val bodyEnd = d.body?.range?.end?.let { source.offsetOf(it) } ?: e - val fnScope = FnScope(sym.id, bodyStart, bodyEnd, mutableListOf(), ownerClass?.symId) - - // Params - for (p in d.params) { - val ps = source.offsetOf(p.nameStart) - val pe = ps + p.name.length - val pk = SymbolKind.Parameter - val paramSym = Symbol(nextId++, p.name, pk, ps, pe, containerId = sym.id) - fnScope.locals += paramSym.id - symbols += paramSym + is MiniClassDecl -> { + for (m in d.members) { + if (m is MiniMemberFunDecl) { + registerFun(m.name, m.nameStart, m.params, m.returnType, m.body?.range, false) + } } - functions += fnScope + } + is MiniFunDecl -> { + registerFun(d.name, d.nameStart, d.params, d.returnType, d.body?.range, true) } is MiniValDecl -> { val (s, e) = nameOffsets(d.nameStart, d.name) @@ -157,18 +182,18 @@ object Binder { val ownerClass = classContaining(s) if (ownerClass != null) { // class field - val fieldSym = Symbol(nextId++, d.name, kind, s, e, containerId = ownerClass.symId) + val fieldSym = Symbol(nextId++, d.name, kind, s, e, containerId = ownerClass.symId, type = DocLookupUtils.typeOf(d.type)) symbols += fieldSym ownerClass.fields += fieldSym.id } else { - val sym = Symbol(nextId++, d.name, kind, s, e, containerId = null) + val sym = Symbol(nextId++, d.name, kind, s, e, containerId = null, type = DocLookupUtils.typeOf(d.type)) symbols += sym topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id) } } is MiniEnumDecl -> { val (s, e) = nameOffsets(d.nameStart, d.name) - val sym = Symbol(nextId++, d.name, SymbolKind.Enum, s, e, containerId = null) + val sym = Symbol(nextId++, d.name, SymbolKind.Enum, s, e, containerId = null, type = d.name) symbols += sym topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id) } @@ -187,7 +212,7 @@ object Binder { "iterator", "hasNext", "next" ) for (name in stdFns) { - val sym = Symbol(nextId++, name, SymbolKind.Function, 0, name.length, containerId = null) + val sym = Symbol(nextId++, name, SymbolKind.Function, 0, name.length, containerId = null, type = null) symbols += sym topLevelByName.getOrPut(name) { mutableListOf() }.add(sym.id) } @@ -204,7 +229,7 @@ object Binder { if (containerFn != null) { val fnSymId = containerFn.id val kind = if (d.mutable) SymbolKind.Variable else SymbolKind.Value - val localSym = Symbol(nextId++, d.name, kind, s, e, containerId = fnSymId) + val localSym = Symbol(nextId++, d.name, kind, s, e, containerId = fnSymId, type = DocLookupUtils.typeOf(d.type)) symbols += localSym containerFn.locals += localSym.id } @@ -245,11 +270,11 @@ object Binder { .maxByOrNull { it.rangeEnd - it.rangeStart } val kind = if (kw.equals("var", true)) SymbolKind.Variable else SymbolKind.Value if (inFn != null) { - val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id) + val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id, type = null) symbols += localSym inFn.locals += localSym.id } else { - val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = null) + val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = null, type = null) symbols += localSym topLevelByName.getOrPut(localSym.name) { mutableListOf() }.add(localSym.id) } 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 76b5165..00d3ce3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -572,14 +572,20 @@ private fun buildStdlibDocs(): List { 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 = "filterFlow", doc = md("filterFlow", "Filter elements by predicate and return a Flow."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Flow")) + method(name = "filterNotNull", doc = md("filterNotNull", "Filter non-null elements."), returns = type("lyng.List")) method(name = "drop", doc = md("drop", "Skip the first N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable")) - method(name = "first", doc = md("first", "Return the first element or throw if empty.")) - method(name = "last", doc = md("last", "Return the last element or throw if empty.")) + field(name = "first", doc = md("first", "Return the first element or throw if empty.")) + field(name = "last", doc = md("last", "Return the last element or throw if empty.")) + method(name = "findFirst", doc = md("findFirst", "Return the first matching element or throw."), params = listOf(ParamDoc("predicate"))) + method(name = "findFirstOrNull", doc = md("findFirstOrNull", "Return the first matching element or null."), params = listOf(ParamDoc("predicate"))) method(name = "dropLast", doc = md("dropLast", "Drop the last N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable")) method(name = "takeLast", doc = md("takeLast", "Take the last N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.List")) - method(name = "joinToString", doc = md("joinToString", "Join elements into a string with an optional separator and transformer."), params = listOf(ParamDoc("prefix", type("lyng.String")), ParamDoc("transformer")), returns = type("lyng.String")) + method(name = "joinToString", doc = md("joinToString", "Join elements into a string with an optional separator and transformer."), params = listOf(ParamDoc("separator", type("lyng.String")), ParamDoc("transformer")), returns = type("lyng.String")) method(name = "any", doc = md("any", "Return true if any element matches the predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Bool")) method(name = "all", doc = md("all", "Return true if all elements match the predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Bool")) + method(name = "forEach", doc = md("forEach", "Execute `action` for each element."), params = listOf(ParamDoc("action"))) + method(name = "count", doc = md("count", "Count elements matching the predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Int")) method(name = "sum", doc = md("sum", "Sum all elements; returns null for empty collections."), returns = type("lyng.Number", nullable = true)) method(name = "sumOf", doc = md("sumOf", "Sum mapped values of elements; returns null for empty collections."), params = listOf(ParamDoc("f"))) method(name = "minOf", doc = md("minOf", "Minimum of mapped values."), params = listOf(ParamDoc("lambda"))) @@ -628,7 +634,7 @@ private fun buildStdlibDocs(): List { 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")) + method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", params = listOf(ParamDoc("flags", type("lyng.String"))), returns = type("lyng.Regex")) } // StackTraceEntry structure 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 a34a9e3..beacf25 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt @@ -74,6 +74,7 @@ object CompletionEngineLight { val word = DocLookupUtils.wordRangeAt(text, caret) val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret) if (memberDot != null) { + val inferredCls = (DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDot, imported, binding) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini)) // 0) Try chained member call return type inference DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDot, imported, binding)?.let { cls -> offerMembersAdd(out, prefix, imported, cls, mini) @@ -254,11 +255,19 @@ object CompletionEngineLight { fun emitGroup(map: LinkedHashMap>, groupPriority: Double) { for (name in map.keys.sortedBy { it.lowercase() }) { val variants = map[name] ?: continue - // Prefer a method with a known return type; else any method; else first variant + // Choose a representative for display: + // 1) Prefer a method with return type AND parameters + // 2) Prefer a method with parameters + // 3) Prefer a method with return type + // 4) Else any method + // 5) Else the first variant val rep = - variants.asSequence() - .filterIsInstance() - .firstOrNull { it.returnType != null } + variants.asSequence().filterIsInstance() + .firstOrNull { it.returnType != null && it.params.isNotEmpty() } + ?: variants.asSequence().filterIsInstance() + .firstOrNull { it.params.isNotEmpty() } + ?: variants.asSequence().filterIsInstance() + .firstOrNull { it.returnType != null } ?: variants.firstOrNull { it is MiniMemberFunDecl } ?: variants.first() when (rep) { @@ -336,16 +345,9 @@ object CompletionEngineLight { } } - private fun typeOf(t: MiniTypeRef?): String = when (t) { - null -> "" - is MiniTypeName -> t.segments.lastOrNull()?.name?.let { ": $it" } ?: "" - is MiniGenericType -> { - val base = typeOf(t.base).removePrefix(": ") - val args = t.args.joinToString(",") { typeOf(it).removePrefix(": ") } - ": ${base}<${args}>" - } - is MiniFunctionType -> ": (fn)" - is MiniTypeVar -> ": ${t.name}" + private fun typeOf(t: MiniTypeRef?): String { + val s = DocLookupUtils.typeOf(t) + return if (s.isEmpty()) "" else ": $s" } // Note: we intentionally skip "params in scope" in the isolated engine to avoid PSI/offset mapping. 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 89a8fbc..7f253b2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocLookupUtils.kt @@ -78,6 +78,11 @@ object DocLookupUtils { } for (m in members) { + if (m is MiniMemberFunDecl) { + for (p in m.params) { + if (matches(p.nameStart, p.name.length)) return p.name to "Parameter" + } + } if (matches(m.nameStart, m.name.length)) { val kind = when (m) { is MiniMemberFunDecl -> "Function" @@ -113,12 +118,18 @@ object DocLookupUtils { if (d is MiniClassDecl) { for (m in d.members) { + if (m is MiniMemberFunDecl) { + for (p in m.params) { + if (p.name == name && matches(p.nameStart, p.name.length)) return p.type + } + } if (m.name == name && matches(m.nameStart, m.name.length)) { return when (m) { is MiniMemberFunDecl -> m.returnType is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) { inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini) } else null + else -> null } } @@ -268,11 +279,20 @@ object DocLookupUtils { dfs(baseName, visited)?.let { return it } } } - // Check for local extensions in this class or bases + // 1) 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 } + (d is MiniValDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name && d.name == member) + }?.let { return name to it as MiniNamedDecl } + + // 2) built-in extensions from BuiltinDocRegistry + for (mod in importedModules) { + val decls = BuiltinDocRegistry.docsForModule(mod) + decls.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 as MiniNamedDecl } + } return null } @@ -970,6 +990,17 @@ object DocLookupUtils { is MiniTypeVar -> null } + fun typeOf(t: MiniTypeRef?): String = when (t) { + is MiniTypeName -> t.segments.joinToString(".") { it.name } + (if (t.nullable) "?" else "") + is MiniGenericType -> typeOf(t.base) + "<" + t.args.joinToString(", ") { typeOf(it) } + ">" + (if (t.nullable) "?" else "") + is MiniFunctionType -> { + val r = t.receiver?.let { typeOf(it) + "." } ?: "" + r + "(" + t.params.joinToString(", ") { typeOf(it) } + ") -> " + typeOf(t.returnType) + (if (t.nullable) "?" else "") + } + is MiniTypeVar -> t.name + (if (t.nullable) "?" else "") + null -> "" + } + fun findDotLeft(text: String, offset: Int): Int? { var i = (offset - 1).coerceAtLeast(0) while (i >= 0 && text[i].isWhitespace()) i-- diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt index 2aa54ad..bcf1093 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/packer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,7 @@ package net.sergeych.lynon import net.sergeych.lyng.Scope -import net.sergeych.lyng.obj.Obj -import net.sergeych.lyng.obj.ObjBitBuffer -import net.sergeych.lyng.obj.ObjClass -import net.sergeych.lyng.obj.ObjString +import net.sergeych.lyng.obj.* // Most often used types: @@ -53,14 +50,35 @@ object ObjLynonClass : ObjClass("Lynon") { } } +/** + * Encode any object into Lynon format. Note that it has a special + * handling for void values, returning an empty byte array. + * + * This is the default behavior for encoding void values in Lynon format, + * ensuring consistency with decoding behavior. It matches the [lynonDecodeAny] + * behavior for handling void values. + */ @Suppress("unused") suspend fun lynonEncodeAny(scope: Scope, value: Obj): UByteArray = - (ObjLynonClass.encodeAny(scope, value)) - .bitArray.asUByteArray() + if (value == ObjVoid) + ubyteArrayOf() + else + (ObjLynonClass.encodeAny(scope, value)) + .bitArray.asUByteArray() + +/** + * Decode any object from Lynon format. If the input is empty, returns ObjVoid. + * This behavior is designed to handle cases where the input data might be incomplete + * or intentionally left empty, indicating a void or null value and matches + * the [lynonEncodeAny] behavior [ObjVoid]. + */ @Suppress("unused") suspend fun lynonDecodeAny(scope: Scope, encoded: UByteArray): Obj = - ObjLynonClass.decodeAny( + if (encoded.isEmpty()) + ObjVoid + else + ObjLynonClass.decodeAny( scope, ObjBitBuffer( BitArray(encoded, 8) diff --git a/lynglib/src/commonTest/kotlin/MiniAstTest.kt b/lynglib/src/commonTest/kotlin/MiniAstTest.kt index 87fc92d..3f6f431 100644 --- a/lynglib/src/commonTest/kotlin/MiniAstTest.kt +++ b/lynglib/src/commonTest/kotlin/MiniAstTest.kt @@ -458,4 +458,19 @@ class MiniAstTest { val className = DocLookupUtils.simpleClassNameOf(type) assertEquals("List", className) } + + @Test + fun miniAst_captures_fun_with_type_and_default() = runTest { + val code = """ + fun foo(a: Int, b: String = "ok"): Bool { true } + """.trimIndent() + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + val fn = mini.declarations.filterIsInstance().firstOrNull { it.name == "foo" } + assertNotNull(fn) + assertEquals(2, fn.params.size) + assertEquals("a", fn.params[0].name) + assertEquals("b", fn.params[1].name) + } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/miniast/ParamTypeInferenceTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/miniast/ParamTypeInferenceTest.kt new file mode 100644 index 0000000..e4e13e5 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/miniast/ParamTypeInferenceTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng.miniast + +import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.Compiler +import net.sergeych.lyng.binding.Binder +import kotlin.test.Test +import kotlin.test.assertEquals + +class ParamTypeInferenceTest { + + @Test + fun testParameterTypeInference() = runTest { + val code = """ + class A { + fun foo(p: String) { + p. + } + } + + fun bar(q: Int) { + q. + } + """.trimIndent() + + val sink = MiniAstBuilder() + Compiler.compileWithMini(code.trimIndent(), sink) + val mini = sink.build()!! + val binding = Binder.bind(code, mini) + + val dotPosQ = code.indexOf("q.") + 1 + val receiverClassQ = DocLookupUtils.guessReceiverClassViaMini(mini, code, dotPosQ, listOf("lyng.stdlib"), binding) + assertEquals("Int", receiverClassQ, "Should infer type of parameter q in top-level function") + + val dotPosP = code.indexOf("p.") + 1 + val receiverClassP = DocLookupUtils.guessReceiverClassViaMini(mini, code, dotPosP, listOf("lyng.stdlib"), binding) + assertEquals("String", receiverClassP, "Should infer type of parameter p in member function") + } +} diff --git a/site/src/jsMain/kotlin/ReferencePage.kt b/site/src/jsMain/kotlin/ReferencePage.kt index 264157f..fe7d6a6 100644 --- a/site/src/jsMain/kotlin/ReferencePage.kt +++ b/site/src/jsMain/kotlin/ReferencePage.kt @@ -170,16 +170,9 @@ fun ReferencePage() { } // --- helpers (mirror IDE provider minimal renderers) --- -private fun typeOf(t: MiniTypeRef?): String = when (t) { - is MiniTypeName -> ": " + t.segments.joinToString(".") { it.name } + if (t.nullable) "?" else "" - is MiniGenericType -> { - val base = typeOf(t.base).removePrefix(": ") - val args = t.args.joinToString(", ") { typeOf(it).removePrefix(": ") } - ": ${base}<${args}>" + if (t.nullable) "?" else "" - } - is MiniFunctionType -> ": (..) -> .." + if (t.nullable) "?" else "" - is MiniTypeVar -> ": ${t.name}" + if (t.nullable) "?" else "" - null -> "" +private fun typeOf(t: MiniTypeRef?): String { + val s = DocLookupUtils.typeOf(t) + return if (s.isEmpty()) "" else ": $s" } private fun signatureOf(fn: MiniFunDecl): String {