diff --git a/src/typechecker/FunctionManager.cpp b/src/typechecker/FunctionManager.cpp index 84c262693..252a0e2eb 100644 --- a/src/typechecker/FunctionManager.cpp +++ b/src/typechecker/FunctionManager.cpp @@ -2,6 +2,7 @@ #include "FunctionManager.h" +#include #include #include @@ -312,7 +313,8 @@ Function *FunctionManager::match(Scope *matchScope, const std::string &reqName, if (matches.empty()) return nullptr; - // Tie-breaking: if multiple candidates match, narrow them by qualifier specificity (see breakOverloadTie). + // Tie-breaking: if multiple candidates match, narrow them by qualifier specificity and by preferring + // explicitly declared overloads over generic substitutions (see breakOverloadTie). breakOverloadTie(matches, reqArgs); // Check if more than one function matches the requirements @@ -519,7 +521,8 @@ const GenericType *FunctionManager::getGenericTypeOfCandidateByName(const Functi * the argument qualifiers. This resolves the typical copy-vs-move ctor ambiguity where both a `const T&` * (copy) and a `T&` (move) ctor match a non-const lvalue argument - we prefer the non-const-ref candidate * (move) since it requires no constification. When the argument is const, we prefer the const-ref candidate - * (copy) since binding to a non-const ref would require const-loss. + * (copy) since binding to a non-const ref would require const-loss. As a secondary criterion, an explicitly + * declared (non-generic) overload is preferred over a generic substitution that matches equally well. * * Modifies `matches` in place, removing any candidate that scores worse than the best one. A no-op if there * are fewer than two candidates. @@ -567,6 +570,21 @@ void FunctionManager::breakOverloadTie(std::vector &matches, const A if (scoreSpecificity(m) == bestScore) filtered.push_back(m); matches = std::move(filtered); + + // Secondary tie-break: prefer an explicitly declared (non-generic) overload over a generic substitution + // when both match equally well, mirroring C++ overload resolution where a non-template wins over a + // template specialization. This resolves e.g. the copy ctor 'Any.ctor(const Any&)' vs. the value ctor + // 'Any.ctor(const Any&)' ambiguity when copy-constructing from another value of the same type. It is + // applied after the qualifier-specificity narrowing above, so a more specific generic match still wins. + // A generic substitution that loses here and was only inserted for this very match is removed from its + // declaration's manifestation list again, so the IR generator never emits a manifestation that was never + // type-checked (and so we leave no dead code behind). + if (matches.size() > 1 && std::ranges::any_of(matches, [](const Function *m) { return !m->isGenericSubstantiation(); })) { + for (Function *m : matches) + if (m->isGenericSubstantiation() && m->isNewlyInserted) + std::erase(*m->declNode->getFctManifestations(m->name), m); + std::erase_if(matches, [](const Function *m) { return m->isGenericSubstantiation(); }); + } } /** diff --git a/test/test-files/typechecker/functions/error-ambiguous-generic-functions/exception.out b/test/test-files/typechecker/functions/error-ambiguous-generic-functions/exception.out deleted file mode 100644 index 8e9dd10c8..000000000 --- a/test/test-files/typechecker/functions/error-ambiguous-generic-functions/exception.out +++ /dev/null @@ -1,7 +0,0 @@ -[Error|Semantic] ./source.spice:8:5: -Function ambiguity: The function/procedure 'foo' is ambiguous. All of the following match the requested criteria: - p foo(byte*&) - p foo(byte*&) - -8 foo(ptr); - ^^^^^^^^ \ No newline at end of file diff --git a/test/test-files/typechecker/functions/error-ambiguous-generic-functions/source.spice b/test/test-files/typechecker/functions/error-ambiguous-generic-functions/source.spice deleted file mode 100644 index 1b558ad6e..000000000 --- a/test/test-files/typechecker/functions/error-ambiguous-generic-functions/source.spice +++ /dev/null @@ -1,9 +0,0 @@ -type T dyn; - -p foo(byte*& ptr) {} -p foo(T*& ptr) {} - -f main() { - byte* ptr = nil; - foo(ptr); -} \ No newline at end of file diff --git a/test/test-files/typechecker/functions/success-generic-overload-prefers-explicit/exit-code.out b/test/test-files/typechecker/functions/success-generic-overload-prefers-explicit/exit-code.out new file mode 100644 index 000000000..c22708346 --- /dev/null +++ b/test/test-files/typechecker/functions/success-generic-overload-prefers-explicit/exit-code.out @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/test/test-files/typechecker/functions/success-generic-overload-prefers-explicit/source.spice b/test/test-files/typechecker/functions/success-generic-overload-prefers-explicit/source.spice new file mode 100644 index 000000000..d0beceefb --- /dev/null +++ b/test/test-files/typechecker/functions/success-generic-overload-prefers-explicit/source.spice @@ -0,0 +1,15 @@ +type T dyn; + +int Selected = 0; + +p foo(byte*& ptr) { Selected = 1; } +p foo(T*& ptr) { Selected = 2; } + +f main() { + byte* ptr = nil; + // Both the explicit 'foo(byte*&)' and the generic substitution 'foo(byte*&)' + // match. The explicitly declared (non-generic) overload must be preferred, mirroring + // C++ overload resolution where a non-template wins over a template specialization. + foo(ptr); + assert Selected == 1; +}