diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java index 85a2b3fd5..e7ab22780 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java @@ -459,7 +459,7 @@ private void translateClassInitFunc() { ClassDef trace = classDef; ImVar thisVar = JassIm.ImVar(trace, imClassType(), "this", false); classInitFunc = JassIm.ImFunction(classDef, translator.getNameFor(classDef) + "_init", ImTypeVars(), ImVars(thisVar), ImVoid(), ImVars(), ImStmts(), Collections.emptyList()); - imClass.getFunctions().add(classInitFunc); + translator.addClassFunction(imClass, classInitFunc); ImFunction f = classInitFunc; // initialize vars diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index f183752e6..2e715bb35 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -25,6 +25,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import org.eclipse.jdt.annotation.Nullable; @@ -422,6 +423,7 @@ public ImProg translateProg() { for (CompilationUnit cu : wurstProg) { translateCompilationUnit(cu); } + attachFunctionsAwaitingClass(); linkBridgedOverrides(); addBridgeFunctions(); @@ -1032,19 +1034,227 @@ public void visit(ImReturn imReturn) { } private void addFunction(ImFunction f, StructureDef s) { - ImClass c = getClassFor(s.attrNearestClassOrInterface()); - c.getFunctions().add(f); + addClassFunction(getClassFor(s.attrNearestClassOrInterface()), f); } private void addFunction(ImFunction f, TranslatedToImFunction funcDef) { ImClass classForFunc = getClassForFunc(funcDef); if (classForFunc != null) { - classForFunc.getFunctions().add(f); + addClassFunction(classForFunc, f); } else { addFunction(f); } } + /* + * The order of a class's functions. A function is made when something first asks for it, and the backends emit a + * class's functions, and Lua numbers its overloads, in the order of its function list. Packages are translated + * depth first from each compilation unit in turn, imports first, so whatever asks for a function in another + * package comes in an order which can depend on the order of the units: the imports of a package which do not + * import each other (two interfaces asking for the methods which implement them), and the packages of an import + * cycle, are translated in the order of the units. So a class's function waits until the elements of the class's + * package are translated, and the class then takes its functions in the order they get when that package is + * translated first: first those asked for by the packages translated before it in any order of the units (those it + * imports, directly or not, which do not import it back), in the order of their translation from it, then those + * asked for by the package itself, in the order of its definitions. What other packages ask for does not count. + * Where the order of the units made no difference, this is the order the class had before. + */ + + /** The rank of a request from the class's own package, after those of the packages it imports. */ + private static final int OWN_PACKAGE_RANK = Integer.MAX_VALUE - 1; + + /** A function of a class whose package is not translated yet, and the request for it which counts first. */ + private static final class AwaitingFunction { + final ImFunction function; + final ImClass imClass; + final WPackage pack; + int rank = Integer.MAX_VALUE; + int position; + long request; + + AwaitingFunction(ImFunction function, ImClass imClass, WPackage pack) { + this.function = function; + this.imClass = imClass; + this.pack = pack; + } + } + + /** The functions waiting for their class's package, in the order they were made; null after the translation. */ + private @Nullable Map awaitingFunctions = new LinkedHashMap<>(); + private final Map> awaitingByPackage = new IdentityHashMap<>(); + private final Set packagesTranslated = new ReferenceOpenHashSet<>(); + /** The top level definition being translated, and its package; null between packages. */ + private @Nullable WPackage requestingPackage; + private @Nullable WEntity requestingDefinition; + private long requestCount; + private final Map> dependencyRanks = new IdentityHashMap<>(); + private final Map> definitionPositions = new IdentityHashMap<>(); + + /** Adds {@code f} to its class {@code c}, or makes it wait for the class's package; making it asks for it. */ + void addClassFunction(ImClass c, ImFunction f) { + if (awaitingFunctions != null && c.getTrace().attrNearestPackage() instanceof WPackage pack + && !packagesTranslated.contains(pack)) { + AwaitingFunction a = new AwaitingFunction(f, c, pack); + awaitingFunctions.put(f, a); + awaitingByPackage.computeIfAbsent(pack, p -> new ArrayList<>()).add(a); + requested(f); + } else { + c.getFunctions().add(f); + } + } + + /** {@code f} is asked for: if it waits for its class's package, the request is kept when it counts first. */ + private ImFunction requested(ImFunction f) { + if (awaitingFunctions == null || requestingPackage == null || requestingDefinition == null) { + return f; + } + AwaitingFunction a = awaitingFunctions.get(f); + if (a == null) { + return f; + } + int rank; + if (requestingPackage == a.pack) { + rank = OWN_PACKAGE_RANK; + } else { + rank = dependencyRanks(a.pack).getInt(requestingPackage); + if (rank < 0 || dependencyRanks(requestingPackage).containsKey(a.pack)) { + return f; + } + } + int position = definitionPositions(requestingPackage).getInt(requestingDefinition); + if (rank < a.rank || rank == a.rank && position < a.position) { + a.rank = rank; + a.position = position; + a.request = requestCount++; + } + return f; + } + + /** Runs {@code translation}, the translation of the top level definition {@code e} of {@code pack}. */ + void translateDefinition(WPackage pack, WEntity e, Runnable translation) { + WPackage packageBefore = requestingPackage; + WEntity definitionBefore = requestingDefinition; + requestingPackage = pack; + requestingDefinition = e; + try { + translation.run(); + } finally { + requestingPackage = packageBefore; + requestingDefinition = definitionBefore; + } + } + + /** The elements of {@code pack} are translated: its classes take the functions which wait for it. */ + void endPackageElements(WPackage pack) { + packagesTranslated.add(pack); + List functions = awaitingByPackage.remove(pack); + if (functions != null) { + attachInRequestOrder(functions); + } + } + + /** Every unit is translated: a function still waiting belongs to a class outside the translated packages. */ + private void attachFunctionsAwaitingClass() { + attachInRequestOrder(new ArrayList<>(awaitingFunctions.values())); + awaitingFunctions = null; + awaitingByPackage.clear(); + } + + /** + * Gives the classes their functions in the order of the requests which count. The translation of a class asks for + * each of its functions, so none should be left without one; any such follows, in the order of {@link #sortList}. + */ + private void attachInRequestOrder(List functions) { + List requested = new ArrayList<>(); + Map unrequested = new LinkedHashMap<>(); + for (AwaitingFunction a : functions) { + awaitingFunctions.remove(a.function); + if (a.rank == Integer.MAX_VALUE) { + unrequested.put(a.function, a.imClass); + } else { + requested.add(a); + } + } + requested.sort(Comparator.comparingInt(a -> a.rank) + .thenComparingInt(a -> a.position).thenComparingLong(a -> a.request)); + for (AwaitingFunction a : requested) { + a.imClass.getFunctions().add(a.function); + } + List rest = new ArrayList<>(unrequested.keySet()); + sortList(rest); + for (ImFunction f : rest) { + unrequested.get(f).getFunctions().add(f); + } + } + + /** + * The packages whose elements are translated before those of {@code pack} when it is translated first, numbered + * in that order: its imports, depth first, as {@link TLDTranslation} translates them. + */ + private Reference2IntMap dependencyRanks(WPackage pack) { + Reference2IntMap ranks = dependencyRanks.get(pack); + if (ranks == null) { + ranks = new Reference2IntOpenHashMap<>(); + ranks.defaultReturnValue(-1); + rankImports(pack, pack, new ReferenceOpenHashSet<>(), ranks); + dependencyRanks.put(pack, ranks); + } + return ranks; + } + + private static void rankImports(WPackage first, WPackage p, Set visited, Reference2IntMap ranks) { + if (!visited.add(p)) { + return; + } + for (WImport imp : p.getImports()) { + WPackage imported = imp.attrImportedPackage(); + if (imported != null) { + rankImports(first, imported, visited, ranks); + } + } + if (p != first) { + ranks.put(p, ranks.size()); + } + } + + /** + * The top level definitions of {@code pack} numbered in the order {@link TLDTranslation} translates them: that of + * the source, but a class after the superclass of the same package which it translates first. + */ + private Reference2IntMap definitionPositions(WPackage pack) { + Reference2IntMap positions = definitionPositions.get(pack); + if (positions == null) { + positions = new Reference2IntOpenHashMap<>(); + for (WEntity e : pack.getElements()) { + placeDefinition(pack, e, positions); + } + definitionPositions.put(pack, positions); + } + return positions; + } + + private static void placeDefinition(WPackage pack, WEntity e, Reference2IntMap positions) { + if (positions.containsKey(e)) { + return; + } + if (e instanceof ClassDef c && c.attrTypC().extendedClass() != null) { + WEntity superclass = topLevelDefinition(c.attrTypC().extendedClass().getClassDef()); + if (superclass.attrNearestPackage() == pack) { + placeDefinition(pack, superclass, positions); + } + } + positions.put(e, positions.size()); + } + + /** The top level definition of its package which {@code e} is or is part of. */ + static WEntity topLevelDefinition(de.peeeq.wurstscript.ast.Element e) { + de.peeeq.wurstscript.ast.Element d = e; + while (!(d.getParent() instanceof WEntities)) { + d = d.getParent(); + } + return (WEntity) d; + } + private void addFunction(ImFunction f) { imProg.getFunctions().add(f); } @@ -1117,6 +1327,11 @@ public ImExpr getDefaultValueForJassType(ImType type) { public GetAForB destroyFunc = new GetAForB() { + @Override + public ImFunction getFor(StructureDef classDef) { + return requested(super.getFor(classDef)); + } + @Override public ImFunction initFor(StructureDef classDef) { ImVars params = ImVars(JassIm.ImVar(classDef, selfType(classDef), "this", false)); @@ -1129,6 +1344,13 @@ public ImFunction initFor(StructureDef classDef) { public GetAForB destroyMethod = new GetAForB() { + @Override + public ImMethod getFor(StructureDef classDef) { + // asks for the destroy function each time, which may be waiting for its class's package + destroyFunc.getFor(classDef); + return super.getFor(classDef); + } + @Override public ImMethod initFor(StructureDef classDef) { ImFunction impl = destroyFunc.getFor(classDef); @@ -1240,7 +1462,7 @@ public ImTypeVar initFor(TypeParamDef a) { public ImFunction getFuncFor(TranslatedToImFunction funcDef) { if (functionMap.containsKey(funcDef)) { - return functionMap.get(funcDef); + return requested(functionMap.get(funcDef)); } String name = getNameFor(funcDef); List flags = flags(); @@ -1301,7 +1523,7 @@ public ImFunction getFuncFor(TranslatedToImFunction funcDef) { addFunction(f, funcDef); functionMap.put(funcDef, f); - return f; + return requested(f); } private ImClass getClassForFunc(TranslatedToImFunction funcDef) { @@ -2100,7 +2322,7 @@ public ImFunction getConstructFunc(ConstructorDef constr) { addFunction(f, constr); constructorFuncs.put(constr, f); } - return f; + return requested(f); } @@ -2157,7 +2379,7 @@ public ImFunction getConstructNewFunc(ConstructorDef constr) { addFunction(f, constr); constrNewFuncs.put(constr, f); } - return f; + return requested(f); } public ImProg getImProg() { @@ -2841,10 +3063,10 @@ public ImClass getClassFor(ClassOrInterface s) { Map methodForFuncDef = Maps.newLinkedHashMap(); public ImMethod getMethodFor(FuncDef f) { - + // asks for the implementation each time, which may be waiting for its class's package + ImFunction imFunc = getFuncFor(f); ImMethod m = methodForFuncDef.get(f); if (m == null) { - ImFunction imFunc = getFuncFor(f); // IMPORTANT: method name must match implementation function name, // otherwise EliminateClasses dispatch lookup can fail. diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java index e00f3a7e4..f313da159 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java @@ -78,8 +78,9 @@ public static void translate(WPackage pack, ImTranslator translator) { // Process elements for (WEntity e : elements) { translator.lasttranslatedThing = e; - e.imTranslateEntity(translator); + translator.translateDefinition(pack, e, () -> e.imTranslateEntity(translator)); } + translator.endPackageElements(pack); } public static void translate(ClassDef classDef, ImTranslator translator) { @@ -94,7 +95,9 @@ public static void translate(ClassDef classDef, ImTranslator translator) { translate(extendedClass.getClassDef(), translator); } - ClassTranslator.translate(classDef, translator); + // a superclass is translated before its subclass, which may be in another package + translator.translateDefinition((WPackage) classDef.attrNearestPackage(), ImTranslator.topLevelDefinition(classDef), + () -> ClassTranslator.translate(classDef, translator)); translator.setTranslated(classDef); } 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 355a4294c..d7782be7d 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 @@ -813,4 +813,138 @@ public void sameNamedClassesInSeveralPackagesEmitTheSameLuaInAnyUnitOrder() thro "same-named receivers must bind in an order independent 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); + return Files.toString(new File("test-output/lua/DeterministicChecks_" + name + ".lua"), Charsets.UTF_8); + } + + /** + * The translation of an interface asks for the methods implementing it, which makes them before their class is + * translated. A class's overloads share a name, and the Lua backend numbers them in the order of the class's + * functions, so that order must not follow the order in which the interfaces were translated. + */ + @Test + public void overloadsImplementingInterfacesOfOtherPackagesEmitTheSameLuaInAnyUnitOrder() throws IOException { + CU intM = compilationUnit("IntM.wurst", + "package IntM", + "public interface IntM", + " function m(int x) returns int"); + CU strM = compilationUnit("StrM.wurst", + "package StrM", + "public interface StrM", + " function m(string s) returns int"); + CU lib = compilationUnit("Lib.wurst", + "package Lib", + "import IntM", + "import StrM", + "public class Base implements IntM, StrM", + " function m(int x) returns int", + " return x", + " function m(string s) returns int", + " return 7", + "public class C extends Base"); + CU main = compilationUnit("Main.wurst", + "package Main", + "import IntM", + "import StrM", + "import Lib", + "native testSuccess()", + "function viaInt(IntM i) returns int", + " return i.m(5)", + "function viaStr(StrM s) returns int", + " return s.m(\"a\")", + "init", + " if viaInt(new C()) == 5 and viaStr(new C()) == 7", + " testSuccess()"); + String name = "overloadsImplementingInterfacesOfOtherPackages"; + String first = compileAndRunToLua(name, intM, strM, lib, main); + assertEquals(compileAndRunToLua(name, strM, intM, main, lib), first, + "the overloads of a class must be numbered in an order independent of the compilation units"); + } + + /** + * A superclass asks for the overrides in its subclasses, and an interface for the methods implementing it, each + * with the destroy function, when it is translated. Here they are in packages which do not import each other, so + * which of them asks first follows the order of the compilation units, and the Lua backend emits a class's + * functions in the order of the class's function list. + */ + @Test + public void functionsAskedForByUnrelatedPackagesEmitTheSameLuaInAnyUnitOrder() throws IOException { + CU supA = compilationUnit("SupA.wurst", + "package SupA", + "public abstract class Base", + " abstract function b() returns int"); + CU supB = compilationUnit("SupB.wurst", + "package SupB", + "public interface HasC", + " function c() returns int"); + CU sub = compilationUnit("Sub.wurst", + "package Sub", + "import SupA", + "import SupB", + "public class Sub extends Base implements HasC", + " override function c() returns int", + " return 1", + " override function b() returns int", + " return 2"); + CU main = compilationUnit("Main.wurst", + "package Main", + "import SupA", + "import SupB", + "import Sub", + "native testSuccess()", + "init", + " let s = new Sub()", + " Base b = s", + " HasC h = s", + " if b.b() * 10 + h.c() == 21", + " testSuccess()", + " destroy s"); + String name = "functionsAskedForByUnrelatedPackages"; + String first = compileAndRunToLua(name, supA, supB, sub, main); + assertEquals(compileAndRunToLua(name, supB, supA, main, sub), first, + "a class's functions must be emitted in an order independent of the compilation units"); + } + + /** + * Of two packages which import each other, either can be translated first, depending on the order of the + * compilation units, so a call in one asks for a method of a class in the other before or after the class, and a + * subclass in one translates its superclass in the other first or finds it translated. + */ + @Test + public void functionsAskedForAcrossAnImportCycleEmitTheSameLuaInAnyUnitOrder() throws IOException { + CU cycA = compilationUnit("CycA.wurst", + "package CycA", + "import CycB", + "public class C", + " function x() returns int", + " return 1", + " function y() returns int", + " return twice(2)"); + CU cycB = compilationUnit("CycB.wurst", + "package CycB", + "import initlater CycA", + "public function twice(int v) returns int", + " return v * 2", + "public function useY(C c) returns int", + " return c.y()", + "public class D extends C", + " override function y() returns int", + " return 3"); + CU main = compilationUnit("Main.wurst", + "package Main", + "import CycA", + "import CycB", + "native testSuccess()", + "init", + " let c = new C()", + " if c.x() + useY(c) == 5 and useY(new D()) == 3", + " testSuccess()"); + String name = "functionsAskedForAcrossAnImportCycle"; + String first = compileAndRunToLua(name, cycA, cycB, main); + assertEquals(compileAndRunToLua(name, cycB, cycA, main), first, + "a class's functions must be emitted in an order independent of the compilation units"); + } + }