Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,45 @@ public void transformGenericNewOnly() {
}
eliminateRemainingGenericNewCalls();
assertNoReachableGenericNewMarkers();
settleRemainingDispatches();
}

/**
* Deals with the dispatches left after targeted specialization.
* <p>
* A function that has a specialization is dead: every reachable call to it was rewritten to
* that specialization, so a dispatch still sitting in the original can never run. This backend
* keeps generics rather than removing them wholesale, so those originals are still translated,
* and a dispatch would reach a backend with no way to express it. Such a dispatch is replaced by
* the default value of its type.
* <p>
* A dispatch in a function that was never specialized is a different matter: it would run, and
* nothing has supplied the concrete type. That is reported rather than quietly defaulted.
*/
private void settleRemainingDispatches() {
List<ImTypeVarDispatch> remaining = new ArrayList<>();
prog.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImTypeVarDispatch dispatch) {
super.visit(dispatch);
remaining.add(dispatch);
}
});
for (ImTypeVarDispatch dispatch : remaining) {
ImFunction owner = dispatch.getNearestFunc();
if (owner != null && specializedFunctions.containsRow(owner)) {
dispatch.replaceBy(defaultValueFor(dispatch.getTypeClassFunc().getReturnType()));
continue;
}
throw new CompileError(dispatch.attrTrace().attrSource(),
"Type class dispatch of " + dispatch.getTypeClassFunc().getName()
+ " could not be resolved for this target: the concrete type is not available"
+ " where it is used.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip unreachable dispatches before rejecting them

When a bounded generic function or class method is declared but never called, its owner has no row in specializedFunctions, so this throws even though the dispatch is unreachable; settleRemainingDispatches() runs before optimizer.removeGarbage(), which would otherwise delete that function. I reproduced this with an unused function unused<Q: Show>(Q x) returns string during Lua compilation: the parent revision accepts it, while this change reports that the concrete type is unavailable. Determine reachability or remove garbage before rejecting unresolved dispatches so unused generic APIs remain valid on Lua as they are on Jass.

AGENTS.md reference: AGENTS.md:L215-L221

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 13eb5f5, another regression from this PR rather than a pre-existing one. Declaring a bounded generic and never calling it is entirely reasonable, and my settle pass rejected it because it could not distinguish unreachable from unresolvable.

Rather than compute reachability there, I stopped deciding it. The pass now only neutralises dispatches it can prove dead, which is those in a function that does have a specialization, since every reachable call was rewritten to it. Everything else is left to the passes that already decide reachability, and garbage removal deletes the unused generic as before.

A dispatch which survives that far and reaches the Lua backend is now reported there rather than failing as an unimplemented case. That is the point where it is known to be both reachable and unresolvable, so the message is worth something.

Tests: unusedBoundedGenericFunctionLua is your repro; I added unusedBoundedGenericClassLua and unusedBoundedGenericFunction alongside it, since an unused generic class and the Jass path are the neighbouring cases and my last two fixes here were both too narrow.

}
}

private static ImExpr defaultValueFor(ImType type) {
return JassIm.ImNull(type.copy());
}


Expand Down Expand Up @@ -271,7 +310,7 @@ public void visit(ImAlloc alloc) {
// Constructing a class whose methods dispatch has to be specialised as well:
// otherwise the constructor keeps a generic result type, and a method call on that
// result never becomes concrete enough to resolve.
if (classNeedsSpecialization(alloc.getClazz().getClassDef(), visitedFunctions, visitedMethods)) {
if (classNeedsSpecialization(alloc.getClazz().getClassDef())) {
found[0] = true;
return;
}
Expand Down Expand Up @@ -320,44 +359,59 @@ && functionNeedsSpecialization(method.getImplementation(), visitedFunctions, vis
/**
* Whether constructing this class requires the concrete type argument, because one of its own
* or inherited members dispatches on a type class bound.
* <p>
* Deliberately a property of the class alone, not of the path that asked. An earlier version
* threaded the caller's visited set through here and memoised the answer, so a query made while
* one of the class's own functions was already being visited recorded a negative result that
* then stood for every later query.
*/
private boolean classNeedsSpecialization(ImClass classDef, Set<ImFunction> visitedFunctions,
Set<ImMethod> visitedMethods) {
Boolean cached = classNeedsSpecialization.get(classDef);
private boolean classNeedsSpecialization(ImClass classDef) {
Boolean cached = classNeedsSpecializationCache.get(classDef);
if (cached != null) {
// Already answered, or currently being answered: a class reached through its own
// members contributes nothing new to the decision.
return cached;
}
classNeedsSpecialization.put(classDef, false);
boolean result = false;
classNeedsSpecializationCache.put(classDef, false);
boolean result = classDispatchesOnBound(classDef,
Collections.newSetFromMap(new IdentityHashMap<>()));
classNeedsSpecializationCache.put(classDef, result);
return result;
}

private boolean classDispatchesOnBound(ImClass classDef, Set<ImClass> visited) {
if (!visited.add(classDef)) {
return false;
}
for (ImFunction f : classDef.getFunctions()) {
if (functionNeedsSpecialization(f, visitedFunctions, visitedMethods)) {
result = true;
break;
if (containsDispatch(f)) {
return true;
}
}
if (!result) {
for (ImMethod m : classDef.getMethods()) {
if (methodNeedsSpecialization(m, visitedFunctions, visitedMethods)) {
result = true;
break;
}
for (ImMethod m : classDef.getMethods()) {
if (m.getImplementation() != null && containsDispatch(m.getImplementation())) {
return true;
}
}
if (!result) {
for (ImClassType superType : classDef.getSuperClasses()) {
if (classNeedsSpecialization(superType.getClassDef(), visitedFunctions, visitedMethods)) {
result = true;
break;
}
for (ImClassType superType : classDef.getSuperClasses()) {
if (classDispatchesOnBound(superType.getClassDef(), visited)) {
return true;
}
}
classNeedsSpecialization.put(classDef, result);
return result;
return false;
}

/** Whether this function body dispatches on a bound, without following calls out of it. */
private static boolean containsDispatch(ImFunction f) {
boolean[] found = {false};
f.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImTypeVarDispatch dispatch) {
found[0] = true;
}
});
return found[0];
}

private final Map<ImClass, Boolean> classNeedsSpecialization = new IdentityHashMap<>();
private final Map<ImClass, Boolean> classNeedsSpecializationCache = new IdentityHashMap<>();

private void assertNoReachableGenericNewMarkers() {
prog.accept(new Element.DefaultVisitor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.vavr.control.Option;
import org.eclipse.jdt.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

Expand Down Expand Up @@ -98,13 +99,45 @@ public void addMemberMethods(Element node, String name, List<FuncLink> result) {
if (!staticRef) {
return;
}
// Bounds are ordered and an earlier one wins, but only over the same signature: two bounds
// may require the very same operation, and offering both would make every call ambiguous.
// Differently shaped overloads are not in competition, so later bounds still contribute
// them and overload resolution picks between them as usual.
List<FuncLink> supplied = new ArrayList<>();
for (InterfaceDef bound : TypeClassConstraints.boundInterfaces(def)) {
for (FuncDef method : bound.getMethods()) {
if (method.getName().equals(name)) {
result.add(requirementLink(node, bound, method));
if (!method.getName().equals(name)) {
continue;
}
FuncLink candidate = requirementLink(node, bound, method);
if (!alreadySupplied(supplied, candidate, node)) {
supplied.add(candidate);
}
}
}
result.addAll(supplied);
}

/** True when an earlier bound already supplied a requirement of the same shape. */
private static boolean alreadySupplied(List<FuncLink> supplied, FuncLink candidate, Element node) {
for (FuncLink existing : supplied) {
List<WurstType> a = existing.getParameterTypes();
List<WurstType> b = candidate.getParameterTypes();
if (a.size() != b.size()) {
continue;
}
boolean same = true;
for (int i = 0; i < a.size(); i++) {
if (!a.get(i).equalsType(b.get(i), node)) {
same = false;
break;
}
}
if (same) {
return true;
}
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,66 @@ public void sameSimpleNameThroughRegistryFallback() {
);
}

/**
* Two bounds may require the same operation. Bounds are ordered and the earlier one wins,
* rather than every call to the shared operation becoming ambiguous.
*/
@Test
public void duplicateRequirementAcrossBounds() {
testAssertOkLines(true,
"package test",
"native testSuccess()",
"interface First<T:>",
" function show(T x) returns string",
"interface Second<T:>",
" function other(T x) returns int",
" function show(T x) returns string",
"implements First<int>",
" function show(int x) returns string",
" return \"first\"",
"implements Second<int>",
" function other(int x) returns int",
" return 1",
" function show(int x) returns string",
" return \"second\"",
"function render<Q: First and Second>(Q x) returns string",
" return Q.show(x)",
"init",
" if render(1) == \"first\"",
" testSuccess()"
);
}

/**
* Bounds only shadow each other when they require the same shape. A later bound still supplies
* a differently shaped overload, which overload resolution then chooses between.
*/
@Test
public void overloadFromLaterBoundStaysAvailable() {
testAssertOkLines(true,
"package test",
"native testSuccess()",
"interface First<T:>",
" function show(T x) returns string",
"interface Second<T:>",
" function show(T x) returns string",
" function show(int scale, T x) returns string",
"implements First<int>",
" function show(int x) returns string",
" return \"first\"",
"implements Second<int>",
" function show(int x) returns string",
" return \"second\"",
" function show(int scale, int x) returns string",
" return \"scaled\"",
"function render<Q: First and Second>(Q x) returns string",
" return Q.show(x) + Q.show(2, x)",
"init",
" if render(1) == \"firstscaled\"",
" testSuccess()"
);
}

/** A type parameter is not a value, so it may only appear as the receiver of a requirement. */
@Test
public void typeParameterIsNotAValue() {
Expand Down
Loading