diff --git a/BACKLOG.md b/BACKLOG.md index 4f2f05584..7c19328bc 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -59,6 +59,12 @@ Notes; finished-work narrative does not. type check of the dependencies only as far as the compilation needs it is the larger prize (the check is the largest block of a cold build), but the validator also marks things the translation reads (`NamePreservation.preserve`), so it needs its own investigation. + A joint class/function reachability worklist could avoid following every virtual override when only some + receiver classes can exist. It must distinguish a nominal type dependency from an allocation and preserve + generic construction, compile-time objects and externally preserved entry points. An initial castle fight + count found 209 retained method implementations in unallocated classes without direct function references + (5,818 body nodes of 868,171); it does not count their transitive callees or prove all can go. Measure the + total build cost before expanding the analysis: removing class metadata alone did not show a consistent gain. Whole-program analyses which look at every function now see less, and a dead function can no longer make them conservative: `LuaTypedValues` proves more values non-nil without the dead writers (a read of a static which only dead code wrote loses its `__wurst_ensureInt`), `GlobalsInliner` inlines a constant of a package initialiser which @@ -91,15 +97,13 @@ Notes; finished-work narrative does not. keeps: `modification counts: ImFunction` in `jass_im.parseq`). Left: (1) more removals which remove nothing: one which the test suite shows to find nothing becomes an `assertNoGarbage`, as the second removal, the one in `optimize` and the one in front of the empty package initialisers did (the one in - front of the first local optimisation is a candidate); (2) the Jass - pipeline analyses the call relation once more after the last removal (`calculateCallRelationsAndReadVariables` - before `ImToJassTranslator`), which the removal has just done; (3) facts which live longer than one removal, valid + front of the first local optimisation is a candidate); (2) facts which live longer than one removal, valid while `modificationCount()` of the function is the same (it replaces `ImTranslator.functionChanged`, which a pass - can forget to call), so that the first round of a removal does not walk the functions no pass touched; (4) `LocalPlayerContextAnalyzer`, built again for the whole program after each - pass which is not local-player aware (see "Build the local-player analysis fewer times" above); (5) the flatten after the tuples + can forget to call), so that the first round of a removal does not walk the functions no pass touched; (3) `LocalPlayerContextAnalyzer`, built again for the whole program after each + pass which is not local-player aware (see "Build the local-player analysis fewer times" above); (4) the flatten after the tuples are eliminated is the one which does work (about 565 to 800 functions of castle fight): the producers of statement expressions (`EliminateTuples`, `SimpleRewrites`, the inliner) could emit statements as the removal does; - (6) a chain of assignments to unread variables (`v1 = v0; v2 = v1; ...`, nothing reads the last) takes a round for + (5) a chain of assignments to unread variables (`v1 = v0; v2 = v1; ...`, nothing reads the last) takes a round for each link, and a round costs the functions which changed (a walk of each, the rewrite of its statement list, its locals), so the cost grows with the square of the length. Measured in a unit test (the checks of unit-test mode on, one removal): a chain inside one function takes 0.24 s for 1,000 links, 1.5 s for 4,000 and 16.7 s for 16,000; with diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index 33ef04e9f..064f65738 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -595,9 +595,7 @@ public JassProg transformProgToJass() { optimizer.removeGarbage(); timeTaker.endPhase(); imTranslator.assertFlat("before it is translated to Jass, which has no statement expression"); - timeTaker.beginPhase("call relations"); - getImTranslator().calculateCallRelationsAndReadVariables(); - timeTaker.endPhase(); + // The final garbage removal leaves current call relations; nothing modifies the program before emission. timeTaker.beginPhase("print IM as Jass"); ImToJassTranslator translator = new ImToJassTranslator(getImProg(), getImTranslator().getCalledFunctions(), getImTranslator().getMainFunc(), getImTranslator().getConfFunc()); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java index 27d2a9eee..e7d163dcd 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java @@ -4,6 +4,8 @@ import com.google.common.io.Files; import de.peeeq.wurstio.TimeTaker; import de.peeeq.wurstio.UtilsIO; +import de.peeeq.wurstio.WurstCompilerJassImpl; +import de.peeeq.wurstscript.gui.WurstGuiCliImpl; import de.peeeq.wurstscript.RunArgs; import de.peeeq.wurstscript.ast.Ast; import de.peeeq.wurstscript.ast.Element; @@ -37,6 +39,66 @@ public class OptimizerTests extends WurstScriptTest { + /** The backend must consume the final removal's current graph, including callbacks and preserved entry points. */ + @Test + public void jassTranslationReusesTheFinalGarbageRemovalsCallRelations() { + class CapturingTimeTaker extends TimeTaker.Default { + final java.util.Deque phases = new java.util.ArrayDeque<>(); + WurstCompilerJassImpl compiler; + com.google.common.collect.Multimap finalRelations; + + @Override public void beginPhase(String name) { + phases.push(name); + } + + @Override public void endPhase() { + String finished = phases.pop(); + if (finished.equals("remove garbage") && "translate to jass".equals(phases.peek())) { + finalRelations = compiler.getImTranslator().getCalledFunctions(); + } + } + } + for (boolean optimize : new boolean[]{false, true}) { + CapturingTimeTaker timeTaker = new CapturingTimeTaker(); + WurstGuiCliImpl gui = new WurstGuiCliImpl(); + RunArgs args = optimize ? new RunArgs().with("-inline", "-localOptimizations", "-opt") : new RunArgs(); + WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(timeTaker, null, gui, null, args); + timeTaker.compiler = compiler; + String source = String.join("\n", + "package Test", + "native register(code callback)", + "native effect()", + "@noinline function callback()", + " effect()", + "@preserveName function externalEntry()", + " effect()", + "function unused()", + " effect()", + "init", + " register(function callback)"); + WurstModel model = parseFiles(Collections.emptyList(), List.of(new CU("ReuseRelations.wurst", source)), + false, compiler); + compiler.checkProg(model); + assertTrue(gui.getErrorList().isEmpty(), gui.getErrorList().toString()); + compiler.translateProgToIm(model); + compiler.runCompiletime(org.wurstscript.projectconfig.WurstProjectConfigData.empty(), false, false); + compiler.transformProgToJass(); + assertTrue(gui.getErrorList().isEmpty(), gui.getErrorList().toString()); + assertNotNull(timeTaker.finalRelations, "the final removal must leave a call graph"); + assertSame(compiler.getImTranslator().getCalledFunctions(), timeTaker.finalRelations, + "Jass emission must reuse the graph already calculated by the final garbage removal"); + java.util.Set callees = timeTaker.finalRelations.values().stream() + .map(f -> f.getTrace() instanceof de.peeeq.wurstscript.ast.FunctionDefinition definition + ? definition.getName() : f.getName()) + .collect(java.util.stream.Collectors.toSet()); + assertTrue(callees.contains("callback"), callees.toString()); + assertTrue(callees.contains("effect"), callees.toString()); + assertFalse(callees.contains("unused"), callees.toString()); + assertTrue(timeTaker.finalRelations.keySet().stream().anyMatch(f -> f.getName().equals("externalEntry")), + "the graph must retain the externally preserved entry point"); + } + } + /** * A common.j converter changes and reads nothing, so a call whose result nothing uses goes. The list is by name, * and 40 converters of the Reforged natives were once missing from it: an unused native call stayed in the script.