improved type inference in plugin

This commit is contained in:
Sergey Chernov 2026-01-12 08:19:52 +01:00
parent f6deabaa38
commit 1efa96a990
12 changed files with 276 additions and 125 deletions

View File

@ -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 -> {}
}
}

View File

@ -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,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<MiniMemberFunDecl>()
.firstOrNull { it.returnType != null }
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()
when (rep) {
@ -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"
}
}
}

View File

@ -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 {

View File

@ -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,

View File

@ -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)
}

View File

@ -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

View File

@ -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<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>()
.firstOrNull { it.returnType != null }
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()
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.

View File

@ -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--

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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 {