From 01289ee16c5fcac97f70b81387500fe4cc64fc12 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 10 Oct 2026 00:02:44 +0200 Subject: [PATCH] Order a class's functions independently of the compilation unit order A class's function joined its class's list the first time anything asked for it. Packages are translated depth first from each compilation unit in turn, so imports of a package which do not import each other, and the packages of an import cycle, ask in the order of the units: two interfaces in unrelated packages asking for the overloads implementing them swapped Base_Base_m and Base_Base_m1 in the Lua, and a superclass and an interface in unrelated packages swapped the order in which a class's functions are emitted. A class's function now waits until the elements of its class's package are translated. The class then takes its functions in the order they get when that package is translated first: those asked for by the packages it imports and 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. Requests from other packages do not count. A superclass translated first by a subclass in another package counts for its own package. Where the unit order made no difference this is the old order. Castle fight and zombie defense (-lua -inline -localOptimizations) change only in the function order of 6 and 17 classes, each in or below an import cycle; putting those lists back in the old order gives the old Lua byte for byte. --- .../imtranslation/ClassTranslator.java | 2 +- .../imtranslation/ImTranslator.java | 240 +++++++++++++++++- .../imtranslation/TLDTranslation.java | 7 +- .../tests/DeterministicChecks.java | 134 ++++++++++ 4 files changed, 371 insertions(+), 12 deletions(-) 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 10796d301..869aca063 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(); if (mainFunc == null) { @@ -1001,19 +1003,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); } @@ -1086,6 +1296,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)); @@ -1098,6 +1313,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); @@ -1209,7 +1431,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(); @@ -1270,7 +1492,7 @@ public ImFunction getFuncFor(TranslatedToImFunction funcDef) { addFunction(f, funcDef); functionMap.put(funcDef, f); - return f; + return requested(f); } private ImClass getClassForFunc(TranslatedToImFunction funcDef) { @@ -2069,7 +2291,7 @@ public ImFunction getConstructFunc(ConstructorDef constr) { addFunction(f, constr); constructorFuncs.put(constr, f); } - return f; + return requested(f); } @@ -2126,7 +2348,7 @@ public ImFunction getConstructNewFunc(ConstructorDef constr) { addFunction(f, constr); constrNewFuncs.put(constr, f); } - return f; + return requested(f); } public ImProg getImProg() { @@ -2810,10 +3032,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 261bfa17c..d12e41389 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 @@ -756,4 +756,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"); + } + }