Skip to content

Commit 47c3049

Browse files
committed
Select a type class instance by type identity, not by printed name
The fallback registry keyed implementations on the printed type. A class prints as its simple name, so two classes of the same name in different packages collided: the second registration was dropped and its values silently dispatched through the first's implementation, with no error. Implementations are now held per requirement and matched by structural type equality. The printed form also fed those keys and reaches specialization names, while the binding it prints is a hash map, so identical input could produce different symbols between runs. The names are sorted, and with the keys no longer built from text the printing is once again only for reading.
1 parent 2ec9571 commit 47c3049

3 files changed

Lines changed: 86 additions & 20 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import de.peeeq.wurstscript.jassIm.*;
44

55
import java.io.IOException;
6+
import java.util.ArrayList;
7+
import java.util.Collections;
68
import java.util.List;
79
import java.util.StringJoiner;
810
import java.util.stream.Collectors;
@@ -545,15 +547,16 @@ private static void printTypeArguments(ImTypeArguments typeArguments, int indent
545547
// Show the type class instances travelling with the argument: whether they are
546548
// present is the whole question when a bound fails to dispatch.
547549
if (!ta.getTypeClassBinding().isEmpty()) {
548-
append(sb, "{");
549-
boolean firstBinding = true;
550+
// Sorted: this printing reaches specialization names and so the emitted symbols,
551+
// which have to be identical for identical input. The binding is a hash map, so
552+
// its iteration order is not.
553+
List<String> requirements = new ArrayList<>();
550554
for (ImTypeClassFunc requirement : ta.getTypeClassBinding().keySet()) {
551-
if (!firstBinding) {
552-
append(sb, ", ");
553-
}
554-
append(sb, requirement.getName());
555-
firstBinding = false;
555+
requirements.add(requirement.getName());
556556
}
557+
Collections.sort(requirements);
558+
append(sb, "{");
559+
append(sb, String.join(", ", requirements));
557560
append(sb, "}");
558561
}
559562
first = false;

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,29 +1714,45 @@ public ImTypeClassFunc getTypeClassFunc(FuncDef method) {
17141714
private final Map<FuncDef, ImTypeClassFunc> typeClassFuncs = new LinkedHashMap<>();
17151715

17161716
/**
1717-
* Every type class implementation in the program, keyed by requirement and the type it is for.
1717+
* Every type class implementation in the program, held per requirement against the type it is
1718+
* for.
17181719
* <p>
17191720
* A type argument can carry its instances directly, but that binding lives on the argument
17201721
* position and is lost as soon as a type variable is substituted into a plain type, which is
17211722
* what happens when a generic class type travels through a return type or a receiver. Instance
1722-
* selection is static, so keying on the concrete type is both simpler and robust: the type at a
1723-
* dispatch site is enough to find the implementation, however it got there.
1723+
* selection is static, so recording the type is enough to recover it.
1724+
* <p>
1725+
* Matched by structural type equality rather than by printed name: a class prints as its simple
1726+
* name, so two classes of the same name in different packages would otherwise collide and the
1727+
* second would silently dispatch through the first. The lists hold one entry per instance of a
1728+
* requirement, so scanning them is cheaper than the printing it replaces.
17241729
*/
1725-
private final Map<ImTypeClassFunc, Map<String, ImFunction>> typeClassImpls = new LinkedHashMap<>();
1730+
private final Map<ImTypeClassFunc, List<TypeClassImpl>> typeClassImpls = new LinkedHashMap<>();
17261731

1727-
public void registerTypeClassImpl(ImTypeClassFunc requirement, ImType instanceType, ImFunction impl) {
1728-
typeClassImpls.computeIfAbsent(requirement, r -> new LinkedHashMap<>())
1729-
.putIfAbsent(typeClassKey(instanceType), impl);
1732+
private record TypeClassImpl(ImType instanceType, ImFunction impl) {
17301733
}
17311734

1732-
public @Nullable ImFunction lookupTypeClassImpl(ImTypeClassFunc requirement, ImType instanceType) {
1733-
Map<String, ImFunction> byType = typeClassImpls.get(requirement);
1734-
return byType == null ? null : byType.get(typeClassKey(instanceType));
1735+
public void registerTypeClassImpl(ImTypeClassFunc requirement, ImType instanceType, ImFunction impl) {
1736+
List<TypeClassImpl> impls = typeClassImpls.computeIfAbsent(requirement, r -> new ArrayList<>());
1737+
for (TypeClassImpl existing : impls) {
1738+
if (existing.instanceType().equalsType(instanceType)) {
1739+
return;
1740+
}
1741+
}
1742+
impls.add(new TypeClassImpl(instanceType, impl));
17351743
}
17361744

1737-
/** Structural key for an instance type; concrete types print stably. */
1738-
private static String typeClassKey(ImType type) {
1739-
return type.toString();
1745+
public @Nullable ImFunction lookupTypeClassImpl(ImTypeClassFunc requirement, ImType instanceType) {
1746+
List<TypeClassImpl> impls = typeClassImpls.get(requirement);
1747+
if (impls == null) {
1748+
return null;
1749+
}
1750+
for (TypeClassImpl candidate : impls) {
1751+
if (candidate.instanceType().equalsType(instanceType)) {
1752+
return candidate.impl();
1753+
}
1754+
}
1755+
return null;
17401756
}
17411757

17421758
public ImTypeVar getTypeVar(TypeParamDef tp) {

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,53 @@ public void genericRequirement() {
800800
);
801801
}
802802

803+
/**
804+
* Two classes with the same simple name in different packages, dispatched through a bounded
805+
* generic class so the lookup by type is the path used. Selecting by printed name made the
806+
* second silently dispatch through the first's implementation.
807+
*/
808+
@Test
809+
public void sameSimpleNameThroughRegistryFallback() {
810+
testAssertOkLines(true,
811+
"package Iface",
812+
"public interface Show<T:>",
813+
" function show(T x) returns string",
814+
"public class Renderer<Q: Show>",
815+
" function render(Q x) returns string",
816+
" return Q.show(x)",
817+
"endpackage",
818+
"",
819+
"package First",
820+
"import public Iface",
821+
"public class Item",
822+
"implements Show<Item>",
823+
" function show(Item x) returns string",
824+
" return \"first\"",
825+
"public function firstResult() returns string",
826+
" return new Renderer<Item>().render(new Item())",
827+
"endpackage",
828+
"",
829+
"package Second",
830+
"import public Iface",
831+
"public class Item",
832+
"implements Show<Item>",
833+
" function show(Item x) returns string",
834+
" return \"second\"",
835+
"public function secondResult() returns string",
836+
" return new Renderer<Item>().render(new Item())",
837+
"endpackage",
838+
"",
839+
"package test",
840+
"import First",
841+
"import Second",
842+
"native testSuccess()",
843+
"init",
844+
" if firstResult() == \"first\" and secondResult() == \"second\"",
845+
" testSuccess()",
846+
"endpackage"
847+
);
848+
}
849+
803850
/** A type parameter is not a value, so it may only appear as the receiver of a requirement. */
804851
@Test
805852
public void typeParameterIsNotAValue() {

0 commit comments

Comments
 (0)