From 95053d34e7fdbb3cf5d0bda425049de259da4b20 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 1 Nov 2026 16:55:51 +0100 Subject: [PATCH] fix(resolver): Kotlin extension receivers take standard subtypes; vararg need not be last An extension on Iterable or Collection applies to a List, a Set or a project Iterable, and to nothing else. Past a vararg, position no longer says which parameter an argument lands on, so it no longer excludes. A project class is never an Array. resolver_oo: 7 pass, 0 fail. Signed-off-by: Martin Vogel --- internal/cbm/lsp/oo_core.c | 8 ++++++ internal/cbm/lsp/oo_internal.h | 1 + internal/cbm/lsp/oo_kotlin.c | 47 +++++++++++++++++++++++++++++++--- tests/test_resolver_oo.c | 20 +++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/internal/cbm/lsp/oo_core.c b/internal/cbm/lsp/oo_core.c index 5adef628a..1e8a69adf 100644 --- a/internal/cbm/lsp/oo_core.c +++ b/internal/cbm/lsp/oo_core.c @@ -1176,6 +1176,10 @@ static bool oo_argument_excludes(oo_walk_t *w, const oo_callable_t *callable, return true; if (callable->max_args < 0 && index == callable->param_count - 1) continue; /* Variadic element versus array conversion is not modelled. */ + /* Past a vararg, position no longer says which parameter an argument lands on. */ + if (callable->vararg_position && i >= callable->vararg_position - 1 && + !args[i].function_literal) + continue; if (oo_ref_is_tparam(callable, callable->params[index])) continue; oo_value_t parameter = oo_value_from_ref(w, callable->params[index], &scope, subst); @@ -1189,6 +1193,10 @@ static bool oo_argument_excludes(oo_walk_t *w, const oo_callable_t *callable, return true; continue; } + /* Array is final: a project class is never one. */ + if (parameter.external && !strcmp(oo_leaf(parameter.external), "Array") && argument->type && + oo_hierarchy_complete(w->ix, argument->type)) + return true; if (parameter.fn_ref) { /* A value whose whole hierarchy is known is not a function. */ if (argument->type && oo_hierarchy_complete(w->ix, argument->type)) diff --git a/internal/cbm/lsp/oo_internal.h b/internal/cbm/lsp/oo_internal.h index 974597eef..2c159b93e 100644 --- a/internal/cbm/lsp/oo_internal.h +++ b/internal/cbm/lsp/oo_internal.h @@ -94,6 +94,7 @@ typedef struct oo_callable { const oo_file_t *file; uint32_t flags; int min_args, max_args; /* max_args -1: variadic. */ + int vararg_position; /* Kotlin: 1 + the vararg's index; it need not be last. */ const oo_typeref_t **params; int param_count; const oo_typeref_t *ret; diff --git a/internal/cbm/lsp/oo_kotlin.c b/internal/cbm/lsp/oo_kotlin.c index cf8c7e681..aac2c6b13 100644 --- a/internal/cbm/lsp/oo_kotlin.c +++ b/internal/cbm/lsp/oo_kotlin.c @@ -362,6 +362,8 @@ static void kh_parameters(const kh_t *h, oo_callable_t *callable, TSNode paramet if (refs) refs[total] = kt_typeref(&h->text, type); total++; + if (pending_vararg && !vararg) + callable->vararg_position = total; vararg |= pending_vararg; pending_vararg = false; /* A class parameter keeps its default inside its own node. */ @@ -797,6 +799,41 @@ static bool kt_explicit_external(oo_walk_t *w, const oo_scope_t *scope, const ch return false; } +/* The standard library's collection and text subtyping, for extension receivers. */ +static bool kt_external_conforms(const char *actual, const char *declared) { + static const struct { + const char *actual, *supertypes; + } TABLE[] = { + {"List", " Collection Iterable "}, + {"Set", " Collection Iterable "}, + {"Collection", " Iterable "}, + {"MutableList", " List Collection MutableCollection Iterable MutableIterable "}, + {"MutableSet", " Set Collection MutableCollection Iterable MutableIterable "}, + {"MutableCollection", " Collection Iterable MutableIterable "}, + {"ArrayList", " MutableList List Collection MutableCollection Iterable "}, + {"LinkedHashSet", " MutableSet Set Collection MutableCollection Iterable "}, + {"HashSet", " MutableSet Set Collection MutableCollection Iterable "}, + {"MutableMap", " Map "}, + {"HashMap", " MutableMap Map "}, + {"LinkedHashMap", " MutableMap Map HashMap "}, + {"String", " CharSequence Comparable "}, + {"Int", " Number Comparable "}, + {"Long", " Number Comparable "}, + {"Double", " Number Comparable "}, + {"Float", " Number Comparable "}, + {"Short", " Number Comparable "}, + {"Byte", " Number Comparable "}, + }; + char word[64]; + int written = snprintf(word, sizeof(word), " %s ", declared); + if (written <= 0 || (size_t)written >= sizeof(word)) + return false; + for (size_t i = 0; i < sizeof(TABLE) / sizeof(TABLE[0]); i++) + if (!strcmp(TABLE[i].actual, actual)) + return strstr(TABLE[i].supertypes, word) != NULL; + return false; +} + /* Does an extension's declared receiver accept the value? */ static bool kt_ref_accepts(oo_walk_t *w, const oo_typeref_t *ref, oo_scope_t scope, const oo_value_t *value) { @@ -823,11 +860,15 @@ static bool kt_ref_accepts(oo_walk_t *w, const oo_typeref_t *ref, oo_scope_t sco declared = declared ? declared + 1 : receiver.external; if (!strcmp(declared, "Any")) return value->type || value->external; - if (!value->external) - return false; + /* A project collection reaches Iterable through its declared supertypes. */ + if (!value->external) { + oo_value_t element = + !strcmp(declared, "Iterable") ? oo_element_of(w, value) : oo_value_unknown(); + return oo_value_known(&element); + } const char *actual = strrchr(value->external, '.'); actual = actual ? actual + 1 : value->external; - return !strcmp(declared, actual); + return !strcmp(declared, actual) || kt_external_conforms(actual, declared); } return false; } diff --git a/tests/test_resolver_oo.c b/tests/test_resolver_oo.c index a790bfdf1..754ef4a03 100644 --- a/tests/test_resolver_oo.c +++ b/tests/test_resolver_oo.c @@ -320,6 +320,12 @@ static const OOFile KOTLIN_FILES[] = { "private fun fileOnly(): Int = 5\n" "val topProperty: Store = Store()\n" "val Store.extProp: Store get() = this\n" + "fun Iterable.onIterable(): Int = 1\n" + "fun Collection.onCollection(): Int = 1\n" + "class Rows : Iterable {\n" + " override fun iterator(): Iterator = TODO()\n" + "}\n" + "fun middleVararg(first: Int, vararg rest: Store, block: () -> Unit): Int = 1\n" "fun String.shout(): String = this\n" "fun Int.onlyInt(): Int = this\n"}, {"core/Tx.kt", @@ -378,6 +384,12 @@ static const OOFile KOTLIN_FILES[] = { " fun externalExtension() { text?.shout() }\n" " fun externalWrongReceiver() { text?.onlyInt() }\n" " fun extensionProperty(s: Store) { s.extProp.get() }\n" + " fun listIsIterable(l: List) { l.onIterable() }\n" + " fun projectIsIterable(r: Rows) { r.onIterable() }\n" + " fun setIsCollection(v: Set) { v.onCollection() }\n" + " fun stringIsNoCollection(t: String) { t.onCollection() }\n" + " fun storeIsNoIterable(s: Store) { s.onIterable() }\n" + " fun varargInTheMiddle(s: Store) { middleVararg(1, s, s) { } }\n" " fun wrongExtensionProperty(s: Store) { s.otherProp }\n" " fun objectValue() { val t = object : Store() { fun own() = 1 }; t.get(); t.ext() }\n" " fun objectOwnMember() {\n" @@ -495,6 +507,14 @@ static int oo_kotlin_check(bool parallel) { /* An extension on a value whose type lies outside the project. */ OO_EXPECT("CALLS", "Main.externalExtension", "Store.shout", 1); OO_EXPECT("CALLS", "Main.externalWrongReceiver", "Store.onlyInt", 0); + /* An extension's receiver takes the standard library's subtypes, and only those. */ + OO_EXPECT("CALLS", "Main.listIsIterable", "Store.onIterable", 1); + OO_EXPECT("CALLS", "Main.projectIsIterable", "Store.onIterable", 1); + OO_EXPECT("CALLS", "Main.setIsCollection", "Store.onCollection", 1); + OO_EXPECT("CALLS", "Main.stringIsNoCollection", "Store.onCollection", 0); + OO_EXPECT("CALLS", "Main.storeIsNoIterable", "Store.onIterable", 0); + /* Past a vararg, position does not say which parameter an argument lands on. */ + OO_EXPECT("CALLS", "Main.varargInTheMiddle", "Store.middleVararg", 1); /* Extension properties: only the receiver they are declared on. */ OO_EXPECT("USAGE", "Main.extensionProperty", "Store.extProp", 1); OO_EXPECT("CALLS", "Main.extensionProperty", "Store.Store.get", 1);