improved type inference in plugin
This commit is contained in:
parent
f6deabaa38
commit
1efa96a990
@ -117,9 +117,18 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
}
|
||||
|
||||
// Parameters
|
||||
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
|
||||
if (fn.nameStart.source != source) return@forEach
|
||||
fn.params.forEach { p -> putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER) }
|
||||
fun addParams(params: List<MiniParam>) {
|
||||
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<MiniMemberFunDecl>().forEach { addParams(it.params) }
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// Type name segments (including generics base & args)
|
||||
@ -146,8 +155,8 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
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<LyngExternalAnnotator.Input, Lyn
|
||||
is MiniClassDecl -> {
|
||||
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<LyngExternalAnnotator.Input, Lyn
|
||||
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
||||
|
||||
is MiniFunDecl -> 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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>().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<String>().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<String>().apply {
|
||||
fromText.forEach { add(it) }
|
||||
add("lyng.stdlib")
|
||||
@ -410,12 +410,17 @@ 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<MiniMemberFunDecl>()
|
||||
list.asSequence().filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.returnType != null && it.params.isNotEmpty() }
|
||||
?: list.asSequence().filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.params.isNotEmpty() }
|
||||
?: list.asSequence().filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.returnType != null }
|
||||
?: list.firstOrNull { it is MiniMemberFunDecl }
|
||||
?: list.first()
|
||||
@ -603,32 +608,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
}
|
||||
|
||||
// Lenient textual import extractor (duplicated from QuickDoc privately)
|
||||
private fun extractImportsFromText(text: String): List<String> {
|
||||
val result = LinkedHashSet<String>()
|
||||
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<MiniParam>, 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)
|
||||
}
|
||||
|
||||
@ -572,14 +572,20 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
||||
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<MiniDecl> {
|
||||
|
||||
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
|
||||
|
||||
@ -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,10 +255,18 @@ object CompletionEngineLight {
|
||||
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>, 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<MiniMemberFunDecl>()
|
||||
variants.asSequence().filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.returnType != null && it.params.isNotEmpty() }
|
||||
?: variants.asSequence().filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.params.isNotEmpty() }
|
||||
?: variants.asSequence().filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.returnType != null }
|
||||
?: variants.firstOrNull { it is MiniMemberFunDecl }
|
||||
?: variants.first()
|
||||
@ -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.
|
||||
|
||||
@ -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 }
|
||||
}?.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--
|
||||
|
||||
@ -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,13 +50,34 @@ 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 =
|
||||
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 =
|
||||
if (encoded.isEmpty())
|
||||
ObjVoid
|
||||
else
|
||||
ObjLynonClass.decodeAny(
|
||||
scope,
|
||||
ObjBitBuffer(
|
||||
|
||||
@ -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<MiniFunDecl>().firstOrNull { it.name == "foo" }
|
||||
assertNotNull(fn)
|
||||
assertEquals(2, fn.params.size)
|
||||
assertEquals("a", fn.params[0].name)
|
||||
assertEquals("b", fn.params[1].name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user