diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java index 62d4fcb41..d956cc096 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java @@ -5,10 +5,12 @@ import de.peeeq.wurstscript.ast.AstElementWithTypeParameters; import de.peeeq.wurstscript.ast.ExprClosure; import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.jassIm.ImArrayType; import de.peeeq.wurstscript.jassIm.ImArrayTypeMulti; import de.peeeq.wurstscript.jassIm.ImClass; import de.peeeq.wurstscript.jassIm.ImClassType; +import de.peeeq.wurstscript.jassIm.Element; import de.peeeq.wurstscript.jassIm.ImFunction; import de.peeeq.wurstscript.jassIm.ImMethod; import de.peeeq.wurstscript.jassIm.ImProg; @@ -100,7 +102,10 @@ private static void assignDispatchGroupKeys(List allMethods) { if (partition.isEmpty()) { continue; } - String key = methodSortKey(partition.get(0)) + "|" + dispatchSignatureKey(partition.get(0)); + // The sort key names the class only; two packages may each declare one of that name. + ImClass owner = partition.get(0).attrClass(); + String key = methodSortKey(partition.get(0)) + "|" + dispatchSignatureKey(partition.get(0)) + + OWNERS + (owner == null ? "" : packageOf(owner.getTrace())); for (int k = 0; k < partition.size(); k++) { ImMethod method = partition.get(k); method.setLuaDispatchGroupKey(key); @@ -467,6 +472,11 @@ private static String dispatchSignatureKey(ImMethod method) { } sb.append(typeKey(params.get(i).getType())); } + sb.append(OWNERS); + appendOwners(implementation.getReturnType(), sb); + for (int i = 1; i < params.size(); i++) { + appendOwners(params.get(i).getType(), sb); + } return sb.toString(); } @@ -489,6 +499,10 @@ private static String dispatchParameterSignatureKey(ImMethod method) { } sb.append(typeKey(params.get(i).getType())); } + sb.append(OWNERS); + for (int i = 1; i < params.size(); i++) { + appendOwners(params.get(i).getType(), sb); + } return sb.toString(); } @@ -657,6 +671,43 @@ public static String typeKey(ImType type) { return type.toString(); } + /** + * Separates a signature from the packages of what it names. It sorts below every character a type key holds, so + * two signatures which differed before them keep their order, and two which named equal types stay together. + */ + private static final char OWNERS = ''; + + /** + * The packages of the classes and of the owners of the type variables {@code type} names, in order. Two packages may + * each declare a class of the same name, and {@link #typeKey} names a class by its name only: without these, the + * methods of two such classes shared a dispatch group and their signatures compared equal for an alias. + */ + private static void appendOwners(ImType type, StringBuilder sb) { + if (type instanceof ImTypeVarRef ref) { + ImTypeVar typeVar = ref.getTypeVariable(); + Element owner = typeVar.getParent() instanceof ImTypeVars vars ? vars.getParent() : null; + sb.append(owner instanceof ImClass c ? packageOf(c.getTrace()) + : owner instanceof ImFunction f ? packageOf(f.getTrace()) : "").append(';'); + } else if (type instanceof ImClassType classType) { + sb.append(packageOf(classType.getClassDef().getTrace())).append(';'); + for (int i = 0; i < classType.getTypeArguments().size(); i++) { + appendOwners(classType.getTypeArguments().get(i).getType(), sb); + } + } else if (type instanceof ImArrayType arrayType) { + appendOwners(arrayType.getEntryType(), sb); + } else if (type instanceof ImArrayTypeMulti arrayType) { + appendOwners(arrayType.getEntryType(), sb); + } else if (type instanceof ImTupleType tupleType) { + for (int i = 0; i < tupleType.getTypes().size(); i++) { + appendOwners(tupleType.getTypes().get(i), sb); + } + } + } + + private static String packageOf(de.peeeq.wurstscript.ast.Element trace) { + return trace != null && trace.attrNearestPackage() instanceof WPackage p ? p.getName() : ""; + } + private static String typeVarKey(ImTypeVar typeVar) { String owner = "?"; int position = -1; diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java index 583797f62..980e1cdc0 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java @@ -841,6 +841,109 @@ public void sameNamedClassesInSeveralPackagesEmitTheSameLuaInAnyUnitOrder() thro "same-named receivers must bind in an order independent of the compilation units"); } + /** + * Two packages each declare the generic hierarchy Box, First and Second. The key of a dispatch group named its class, + * its method and the types of its signature by name only, so both families of pick had the same key and shared + * one group: the packages of the class and of what the signature names keep them apart, in any order of the + * compilation units. + */ + @Test + public void sameNamedGenericHierarchiesInTwoPackagesKeepTheirOwnDispatchGroups() throws IOException { + assertOwnDispatchGroups("sameNamedOldGenericHierarchies", "T"); + } + + /** The same with new generics, whose signatures name the type variable and so its owner. */ + @Test + public void sameNamedNewGenericHierarchiesInTwoPackagesKeepTheirOwnDispatchGroups() throws IOException { + assertOwnDispatchGroups("sameNamedNewGenericHierarchies", "T:"); + } + + /** + * One family across packages: Box in a shared package, and a class First overriding pick in each of two others. + * Both overrides belong to Box's group and must bind the slot a call through Box uses. + */ + @Test + public void sameNamedOverridesInTwoPackagesShareTheirRootSlot() throws IOException { + assertSharedRootSlot("sameNamedOldGenericOverrides", "T"); + } + + @Test + public void sameNamedNewGenericOverridesInTwoPackagesShareTheirRootSlot() throws IOException { + assertSharedRootSlot("sameNamedNewGenericOverrides", "T:"); + } + + private void assertSharedRootSlot(String name, String typeParameter) throws IOException { + List units = new ArrayList<>(); + units.add(compilationUnit("Common.wurst", + "package Common", + "public abstract class Box<" + typeParameter + ">", + " abstract function pick(T a, T b) returns T", + "public function pickOf(Box box) returns int", + " return box.pick(10, 20)")); + for (String p : List.of("PA", "PB")) { + boolean first = p.equals("PA"); + units.add(compilationUnit(p + ".wurst", + "package " + p, + "import Common", + "public class First<" + typeParameter + "> extends Box", + " override function pick(T a, T b) returns T", + " return " + (first ? "a" : "b"), + "public function run" + p + "() returns int", + " return pickOf(new First())")); + } + units.add(compilationUnit("Main.wurst", + "package Main", + "import PA", + "import PB", + "native testSuccess()", + "init", + " if runPA() == 10 and runPB() == 20", + " testSuccess()")); + String lua = compileAndRunToLua(name, units.toArray(new CU[0])); + List shuffled = List.of(units.get(3), units.get(2), units.get(0), units.get(1)); + assertEquals(compileToLua(name, shuffled), lua, "the same in any order of the compilation units"); + } + + private void assertOwnDispatchGroups(String name, String typeParameter) throws IOException { + List units = new ArrayList<>(); + for (String p : List.of("PA", "PB")) { + boolean first = p.equals("PA"); + units.add(compilationUnit(p + ".wurst", + "package " + p, + "public abstract class Box<" + typeParameter + ">", + " abstract function pick(T a, T b) returns T", + "public class First<" + typeParameter + "> extends Box", + " override function pick(T a, T b) returns T", + " return " + (first ? "a" : "b"), + "public class Second<" + typeParameter + "> extends Box", + " override function pick(T a, T b) returns T", + " return " + (first ? "b" : "a"), + "public function run" + p + "() returns int", + " Box x = new First()", + " Box y = new Second()", + " return x.pick(10, 20) * 100 + y.pick(10, 20)")); + } + units.add(compilationUnit("Main.wurst", + "package Main", + "import PA", + "import PB", + "native testSuccess()", + "init", + " if runPA() == 1020 and runPB() == 2010", + " testSuccess()")); + String lua = compileAndRunToLua(name, units.toArray(new CU[0])); + java.util.Set slots = new java.util.TreeSet<>(); + java.util.regex.Matcher binding = java.util.regex.Pattern.compile("\\n\\t\\w+\\.(\\w+) = First_First_pick\\w*\\n") + .matcher(lua); + while (binding.find()) { + slots.add(binding.group(1)); + } + assertEquals(slots.size(), 2, "each package's First binds pick to a slot of its own:\n" + lua); + List shuffled = List.of(units.get(2), units.get(1), units.get(0)); + assertEquals(compileToLua(name, shuffled), lua, + "the slots are the same in any order of the compilation units"); + } + /** Compiles {@code units} for every backend, runs them, and returns the Lua script written under {@code name}. */ private String compileAndRunToLua(String name, CU... units) throws IOException { testNamed(name).testLua(true).luaOnly(false).executeProg().compilationUnits(units);