From 3757b4a1d6a576840fa3ef022652a22f0483ea09 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 14 Oct 2026 08:44:39 +0200 Subject: [PATCH] Remove unreachable classes in the early tree shaker --- BACKLOG.md | 10 +- .../translation/imoptimizer/TreeShaker.java | 260 +++++++++++++++++- .../imtranslation/ClassTranslator.java | 3 + .../imtranslation/ImTranslator.java | 19 ++ .../tests/DeterministicChecks.java | 37 +++ .../wurstscript/tests/TreeShakerTests.java | 178 ++++++++++++ 6 files changed, 496 insertions(+), 11 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index 4f2f05584..bd5060c2e 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -45,13 +45,9 @@ Notes; finished-work narrative does not. constants to them (not a call, and a division only by a constant: those may have an effect), on both targets, in front of the compile-time functions, before the generics and after them. On castle fight (Lua) that takes the 34,200 functions of the translation to 10,500 and the statements from 258,000 to 110,000 before the generics, and the script - has 92,000. What it leaves is the weight which is neither: 3,512 classes of which 1,611 reach the script (the - specialisation of generics adds 220 more, and the class elimination on Jass builds an allocator, a deallocator and - field arrays for every one), and the fields of the classes. Next, in this order, each its own change with its own - proof: (1) classes which no live code allocates, names in a type, or inherits from, taking care that type ids are - numbered over the classes in the program and that a dead class can be the supertype of a live one; (2) globals - and fields which only assignments with an effect write, which keeps the effect and drops the variable (what - `ImOptimizer.removeGarbage` does late); (3) the work before the IM exists, which no pass over the IM can reach: + has 92,000. Next, in this order, each its own change with its own proof: (1) globals and fields which only + assignments with an effect write, which keeps the effect and drops the variable (what + `ImOptimizer.removeGarbage` does late); (2) the work before the IM exists, which no pass over the IM can reach: about 2.6 s of a warm castle fight build is outside the phases the IM passes cover, and the translation takes 1.3 s to produce 34,000 functions of which 10,500 are used. Translating only function bodies which are reachable saves about 0.17 s of that (measured), because the cost is per class and per method (`getClassesWithImplementation`, diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/TreeShaker.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/TreeShaker.java index 985108677..ae1639c7b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/TreeShaker.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/TreeShaker.java @@ -1,19 +1,28 @@ package de.peeeq.wurstscript.translation.imoptimizer; +import de.peeeq.datastructures.Partitions; import de.peeeq.wurstscript.CompilerIntrinsics; import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.ast.ClassDef; import de.peeeq.wurstscript.ast.ConstructorDef; import de.peeeq.wurstscript.ast.FunctionDefinition; +import de.peeeq.wurstscript.jassIm.Element; +import de.peeeq.wurstscript.jassIm.ImAlloc; +import de.peeeq.wurstscript.jassIm.ImArrayLikeType; import de.peeeq.wurstscript.jassIm.ImBoolVal; +import de.peeeq.wurstscript.jassIm.ImCast; import de.peeeq.wurstscript.jassIm.ImClass; +import de.peeeq.wurstscript.jassIm.ImClassRelatedExprWithClass; import de.peeeq.wurstscript.jassIm.ImClassType; +import de.peeeq.wurstscript.jassIm.ImDealloc; import de.peeeq.wurstscript.jassIm.ImExpr; import de.peeeq.wurstscript.jassIm.ImFuncRef; import de.peeeq.wurstscript.jassIm.ImFunction; import de.peeeq.wurstscript.jassIm.ImFunctionCall; import de.peeeq.wurstscript.jassIm.ImIntVal; +import de.peeeq.wurstscript.jassIm.ImInstanceof; import de.peeeq.wurstscript.jassIm.ImLExpr; +import de.peeeq.wurstscript.jassIm.ImMemberAccess; import de.peeeq.wurstscript.jassIm.ImMethod; import de.peeeq.wurstscript.jassIm.ImMethodCall; import de.peeeq.wurstscript.jassIm.ImNull; @@ -25,7 +34,11 @@ import de.peeeq.wurstscript.jassIm.ImStmts; import de.peeeq.wurstscript.jassIm.ImStringVal; import de.peeeq.wurstscript.jassIm.ImTupleExpr; +import de.peeeq.wurstscript.jassIm.ImTupleType; +import de.peeeq.wurstscript.jassIm.ImType; import de.peeeq.wurstscript.jassIm.ImTypeArgument; +import de.peeeq.wurstscript.jassIm.ImTypeIdOfClass; +import de.peeeq.wurstscript.jassIm.ImTypeIdOfObj; import de.peeeq.wurstscript.jassIm.ImVar; import de.peeeq.wurstscript.jassIm.ImVarAccess; import de.peeeq.wurstscript.jassIm.ImVarArrayAccess; @@ -48,7 +61,7 @@ import java.util.Set; /** - * Drops the functions and the globals which nothing can reach, before the passes which would otherwise process them. + * Drops unreachable functions, globals and classes before the passes which would otherwise process them. *

* The translation of a project produces every function and every global of every package it imports, and most of * them are never used: on castle fight 43,000 functions go into the passes after the specialisation of generics and @@ -57,9 +70,10 @@ *

* This removes functions, the methods whose implementation goes with them, and the globals which no reachable code * mentions, with the assignments to them which have nothing else to do (a constant, a read, an operator on such: not - * a call). It removes no class, no field and no other statement, so it is the part of garbage removal which is safe - * where it is called, as long as no later pass uses a function or a global it did not find in a body: those are the - * ones the translator keeps for such passes ({@link ImTranslator#pinnedFunctions}, {@link ImTranslator#pinnedGlobals}), + * a call). It also removes classes which no remaining function, type or superclass needs, but no field and no other + * statement. It is the part of garbage removal which is safe where it is called, as long as no later pass uses a + * function, global or class it did not find in a body: those are the ones the translator keeps for such passes + * ({@link ImTranslator#pinnedFunctions}, {@link ImTranslator#pinnedGlobals}, {@link ImTranslator#pinnedClasses}), * which count as roots here. *

* Both pipelines run it three times: in front of the compile-time functions (which walk the whole program and analyse @@ -154,9 +168,184 @@ private static int remove(ImTranslator trans, boolean followTypeArguments, boole } removeUnusedMethods(prog, reachability.usedMethods); reachability.removeUnreadGlobals(prog, trans.pinnedGlobals()); + reachability.classes.removeUnreferencedClasses(); return removed; } + /** + * Types are references, not children of the IM tree. Follow them explicitly, including field types and + * superclasses of each live class. A live implementation also needs its owning class even when no code allocates + * it (dispatch, preserved functions and generic specialization can all keep implementations this way). + * Reflection and explicit type-id reads expose the complete numbering, so they keep the class universe. The + * translator remembers that fact when compile-time evaluation subsequently removes the read. + */ + private static final class ClassReachability { + private final ImTranslator trans; + private final ImProg prog; + private final Set needed = new ReferenceOpenHashSet<>(); + private final Set types = new ReferenceOpenHashSet<>(); + private final ArrayDeque pending = new ArrayDeque<>(); + + private ClassReachability(ImTranslator trans, ImProg prog) { + this.trans = trans; + this.prog = prog; + } + + void removeUnreferencedClasses() { + if (trans.mustPreserveClassTypeIds()) { + return; + } + collect(prog.getGlobals()); + collect(prog.getMethods()); + if (trans.mustPreserveClassTypeIds()) { + return; + } + for (ImVar global : prog.getGlobals()) { + for (ImSet initializer : prog.getGlobalInits().getOrDefault(global, Collections.emptyList())) { + collect(initializer); + } + } + for (ImClass c : prog.getClasses()) { + if (!c.getFunctions().isEmpty() || !c.getMethods().isEmpty()) { + add(c); + } + } + for (ImClass c : trans.pinnedClasses()) { + add(c); + } + followClasses(); + if (trans.mustPreserveClassTypeIds()) { + return; + } + if (!trans.isLuaTarget()) { + keepAllocatorConnections(); + } + prog.getClasses().retainAll(needed); + } + + private void add(ImClass c) { + if (needed.add(c)) { + pending.add(c); + // Lua's targeted specializations use the source class for nominal identity and instanceof. + ImClass nominal = trans.canonical(c); + if (nominal != c && nominal.getParent() != null) { + add(nominal); + } + } + } + + private void followClasses() { + while (!pending.isEmpty()) { + ImClass c = pending.removeLast(); + collect(c.getFields()); + for (ImClassType superclass : c.getSuperClasses()) { + type(superclass); + } + } + } + + /** + * Jass shares one allocator across each inheritance component. Even an unallocated class can join two + * live interfaces, so removing it would give their objects separate integer id spaces. Keep the joins in + * components containing live classes; ordinary unused leaves can still go. Lua has one allocator already. + */ + private void keepAllocatorConnections() { + Partitions partitions = new Partitions<>(); + for (ImClass c : prog.getClasses()) { + partitions.add(c); + for (ImClassType superclass : c.getSuperClasses()) { + partitions.union(c, superclass.getClassDef()); + } + } + Set liveComponents = new ReferenceOpenHashSet<>(); + boolean changed; + do { + for (ImClass c : needed) { + liveComponents.add(partitions.getRep(c)); + } + int before = needed.size(); + for (ImClass c : prog.getClasses()) { + if (c.getSuperClasses().size() > 1 && liveComponents.contains(partitions.getRep(c))) { + add(c); + } + } + followClasses(); + changed = before != needed.size(); + } while (changed); + } + + private void type(ImType type) { + if (!types.add(type)) { + return; + } + switch (type) { + case ImClassType c -> { + add(c.getClassDef()); + collect(c.getTypeArguments()); + } + case ImArrayLikeType a -> type(a.getEntryType()); + case ImTupleType tuple -> tuple.getTypes().forEach(this::type); + default -> { } + } + } + + private void collect(Element e) { + if (trans.mustPreserveClassTypeIds()) { + return; + } + collectNode(e); + for (int i = 0; i < e.size(); i++) { + collect(e.get(i)); + } + } + + /** Also called by the function reachability walk, so class collection does not walk the bodies again. */ + private void collectNode(Element e) { + if (trans.mustPreserveClassTypeIds()) { + return; + } + switch (e) { + case ImVar v -> { + type(v.getType()); + // A generic static's type may only mention T. Its class owns that parameter and is needed + // by specialization even when no instance of the class is ever constructed. + ImClass owner = trans.genericStaticOwnerOf(v); + if (owner != null && owner.getParent() != null) { + add(owner); + } + } + case ImFunction f -> { + type(f.getReturnType()); + if (isReflection(f)) { + trans.preserveClassTypeIds(); + } + } + case ImNull n -> type(n.getType()); + case ImTypeArgument argument -> type(argument.getType()); + case ImCast cast -> { + type(cast.getFromType()); + type(cast.getToType()); + } + case ImClassRelatedExprWithClass expr -> { + type(expr.getClazz()); + if (expr instanceof ImTypeIdOfClass || expr instanceof ImTypeIdOfObj) { + trans.preserveClassTypeIds(); + } + } + case ImMethod method -> type(method.getMethodClass()); + case ImMemberAccess member -> type(member.getVar().getType()); + default -> { } + } + } + + private static boolean isReflection(ImFunction f) { + return f.isNative() && switch (f.getName()) { + case "typeIdToTypeName", "maxTypeId", "instanceCount", "maxInstanceCount" -> true; + default -> false; + }; + } + } + /** * A method which nothing calls has lost its implementation unless something else reaches it, and a method * without one is a dangling reference: the class elimination builds a dispatch function over every method of a @@ -188,6 +377,7 @@ private static boolean isCompilerIntrinsic(ImFunction f) { */ private static final class Reachability extends ImFunction.DefaultVisitor { private final ImTranslator trans; + private final ClassReachability classes; private final boolean followTypeArguments; private final Set reachable; private final Set constructedClasses = new ReferenceOpenHashSet<>(); @@ -201,6 +391,7 @@ private static final class Reachability extends ImFunction.DefaultVisitor { private Reachability(ImTranslator trans, ImProg prog, boolean followTypeArguments) { this.trans = trans; + this.classes = new ClassReachability(trans, prog); this.followTypeArguments = followTypeArguments; this.reachable = new ReferenceOpenHashSet<>(Math.max(16, prog.getFunctions().size() / 2)); this.globals = new ReferenceOpenHashSet<>(prog.getGlobals()); @@ -221,6 +412,66 @@ void run() { } } + @Override + public void visit(ImFunction function) { + classes.collectNode(function); + super.visit(function); + } + + @Override + public void visit(ImVar variable) { + classes.collectNode(variable); + super.visit(variable); + } + + @Override + public void visit(ImNull value) { + classes.collectNode(value); + super.visit(value); + } + + @Override + public void visit(ImCast cast) { + classes.collectNode(cast); + super.visit(cast); + } + + @Override + public void visit(ImMemberAccess member) { + classes.collectNode(member); + super.visit(member); + } + + @Override + public void visit(ImAlloc allocation) { + classes.collectNode(allocation); + super.visit(allocation); + } + + @Override + public void visit(ImDealloc deallocation) { + classes.collectNode(deallocation); + super.visit(deallocation); + } + + @Override + public void visit(ImInstanceof instanceOf) { + classes.collectNode(instanceOf); + super.visit(instanceOf); + } + + @Override + public void visit(ImTypeIdOfObj id) { + classes.collectNode(id); + super.visit(id); + } + + @Override + public void visit(ImTypeIdOfClass id) { + classes.collectNode(id); + super.visit(id); + } + /** * The global which {@code set} assigns a value to which can be dropped with the assignment, or null: a * global of the program which is not preserved, assigned as a whole or by a constant index, a value without @@ -400,6 +651,7 @@ public void visit(ImMethodCall e) { @Override public void visit(ImTypeArgument argument) { + classes.collectNode(argument); super.visit(argument); if (followTypeArguments) { followTypeArgument(argument); 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 e7ab22780..ec19d3085 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 @@ -258,6 +258,9 @@ private void translateVar(GlobalVarDef s) { imClass.getFields().add(v); dynamicInits.add(Pair.create(v, s.getInitialExpr())); } else { // static class member + if (!imClass.getTypeVariables().isEmpty()) { + translator.recordGenericStaticOwner(v, imClass); + } translator.addGlobalInitalizer(v, classDef.attrNearestPackage(), s.getInitialExpr()); translator.addGlobal(v); } 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 9059bba4f..77b582d00 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 @@ -303,6 +303,25 @@ public List pinnedGlobals() { return result; } + private boolean classTypeIdsObserved; + + /** A compile-time expression can expose an id and then disappear; later shakes must keep its numbering. */ + public void preserveClassTypeIds() { + classTypeIdsObserved = true; + } + + public boolean mustPreserveClassTypeIds() { + return classTypeIdsObserved; + } + + /** Compile-time object state has already created these classes' allocator variables and initializers. */ + public List pinnedClasses() { + if (classManagementVars == null) { + return Collections.emptyList(); + } + return imProg.getClasses().stream().filter(classManagementVars::containsKey).toList(); + } + private final Map varsForTupleVar = new Object2ObjectLinkedOpenHashMap<>(); private final boolean isUnitTestMode; 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 980e1cdc0..2597acc8a 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 @@ -29,6 +29,43 @@ */ public class DeterministicChecks extends WurstScriptTest { + @Test + public void unreachableClassesAreRemovedDeterministicallyInBothBackends() throws IOException { + CU library = compilationUnit("Classes.wurst", + "package Classes", + "public class Base", + " int value = 42", + "public class Box extends Base", + " T payload", + "public class DeadSibling extends Base", + " int unusedSiblingField", + "public class Unused", + " int unusedClassField"); + CU main = compilationUnit("Main.wurst", + "package Main", + "import Classes", + "native testSuccess()", + "init", + " let b = new Box()", + " b.payload = 7", + " if b.value + b.payload == 49", + " testSuccess()", + " destroy b"); + String name = "unreachableClasses"; + File jass = new File("test-output/DeterministicChecks_" + name + "_no_opts.j"); + File lua = new File("test-output/lua/DeterministicChecks_" + name + ".lua"); + testNamed(name).testLua(true).luaOnly(false).executeProg().compilationUnits(library, main); + String firstJass = Files.toString(jass, Charsets.UTF_8); + String firstLua = Files.toString(lua, Charsets.UTF_8); + for (String dead : List.of("Unused", "DeadSibling", "unusedClassField", "unusedSiblingField")) { + assertTrue(!firstJass.contains(dead), "dead class storage reaches Jass: " + dead); + assertTrue(!firstLua.contains(dead), "dead class storage reaches Lua: " + dead); + } + testNamed(name).testLua(true).luaOnly(false).executeProg().compilationUnits(main, library); + assertEquals(Files.toString(jass, Charsets.UTF_8), firstJass, "class removal must not depend on unit order"); + assertEquals(Files.toString(lua, Charsets.UTF_8), firstLua, "class removal must not depend on unit order"); + } + @Test public void simple() throws IOException { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TreeShakerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TreeShakerTests.java index e05330ecb..5c533fa02 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TreeShakerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TreeShakerTests.java @@ -30,6 +30,180 @@ */ public class TreeShakerTests extends WurstScriptTest { + private static Set classNames(WurstCompilerJassImpl compiler) { + Set names = new TreeSet<>(); + for (ImClass c : compiler.getImProg().getClasses()) { + names.add(c.getName()); + } + return names; + } + + @Test + public void removesClassesNamedOnlyByUnreachableCode() { + for (boolean lua : new boolean[]{false, true}) { + WurstCompilerJassImpl compiler = translate(lua, + "package Test", + "native testSuccess()", + "class Unused", + " int deadField", + "class Base", + " int value = 42", + "class Live extends Base", + "function unused() returns Unused", + " return new Unused()", + "init", + " if new Live().value == 42", + " testSuccess()"); + assertTrue(classNames(compiler).toString(), classNames(compiler).contains("Unused")); + + TreeShaker.removeUnreachableFunctionsBeforeGenerics(compiler.getImTranslator()); + + assertEquals(Set.of("Base", "Live"), classNames(compiler)); + compiler.getImTranslator().assertProperties(); + } + } + + @Test + public void keepsClassesNamedByFieldsAndNestedTypesWithoutAllocatingThem() { + for (boolean lua : new boolean[]{false, true}) { + WurstCompilerJassImpl compiler = translate(lua, + "package Test", + "native testSuccess()", + "class FieldType", + "class ResultType", + "class ParameterType", + "class Holder", + " FieldType field", + "tuple pair(ResultType first, int second)", + "function value(ParameterType p) returns pair", + " return pair(null, 42)", + "class Unused", + "init", + " let h = new Holder()", + " if h.field == null and value(null).second == 42", + " testSuccess()"); + + TreeShaker.removeUnreachableFunctionsBeforeGenerics(compiler.getImTranslator()); + + assertEquals(Set.of("FieldType", "ResultType", "ParameterType", "Holder"), classNames(compiler)); + compiler.getImTranslator().assertProperties(); + } + } + + @Test + public void genericStaticStorageKeepsItsClassWithoutAnInstance() { + String[] program = { + "package Test", + "native testSuccess()", + "class Box", + " static T array values", + " static function put(T value)", + " values[0] = value", + " static function get() returns T", + " return values[0]", + "class Unused", + "init", + " Box.put(42)", + " if Box.get() == 42", + " testSuccess()"}; + for (boolean lua : new boolean[]{false, true}) { + WurstCompilerJassImpl compiler = translate(lua, program); + TreeShaker.removeUnreachableFunctionsBeforeGenerics(compiler.getImTranslator()); + assertEquals(Set.of("Box"), classNames(compiler)); + } + test().testLua(true).luaOnly(false).executeProg().lines(program); + } + + @Test + public void observableTypeIdsKeepTheOriginalClassSet() { + for (boolean lua : new boolean[]{false, true}) { + WurstCompilerJassImpl compiler = translate(lua, + "package Test", + "native testSuccess()", + "class AUnused", + "class BLive", + "init", + " if BLive.typeId == 2", + " testSuccess()"); + TreeShaker.removeUnreachableFunctionsBeforeGenerics(compiler.getImTranslator()); + assertEquals(Set.of("AUnused", "BLive"), classNames(compiler)); + } + // Lua's existing reflection natives are placeholders; reflection execution is a Jass contract. + test().executeProg().lines( + "package Test", + "native testSuccess()", + "native maxTypeId() returns int", + "native typeIdToTypeName(int id) returns string", + "class AUnused", + "class BLive", + "init", + " if maxTypeId() == 2 and typeIdToTypeName(1) == \"Test.AUnused\" and BLive.typeId == 2", + " testSuccess()"); + } + + @Test + public void aTypeIdCapturedAtCompiletimeKeepsItsNumberAfterTheExpressionDisappears() { + for (boolean lua : new boolean[]{false, true}) { + WurstCompilerJassImpl compiler = translateForCompiletimeFunctions(lua, + "package Test", + "class AUnused", + "class BLive", + "function compiletime(int i) returns int", + " return i", + "int captured = compiletime(BLive.typeId)", + "init", + " captured++"); + compiler.runCompiletime(WurstProjectConfigData.empty(), false, false); + TreeShaker.removeUnreachableFunctionsBeforeGenerics(compiler.getImTranslator()); + assertEquals(Set.of("AUnused", "BLive"), classNames(compiler)); + } + } + + @Test + public void unallocatedClassesWhichJoinLiveJassAllocatorsStay() { + String[] program = { + "package Test", + "native testSuccess()", + "interface Left", + "interface Right", + "class First implements Left", + "class Second implements Right", + "class Join implements Left, Right", + "class Unused", + "init", + " let a = new First()", + " let b = new Second()", + " if (a castTo int) != (b castTo int)", + " testSuccess()"}; + WurstCompilerJassImpl compiler = translate(false, program); + TreeShaker.removeUnreachableFunctionsBeforeGenerics(compiler.getImTranslator()); + assertTrue("Join keeps one allocator for Left and Right", classNames(compiler).contains("Join")); + assertFalse(classNames(compiler).toString(), classNames(compiler).contains("Unused")); + test().testLua(true).luaOnly(false).executeProg().lines(program); + } + + @Test + public void unallocatedSiblingsCanGoWithoutDroppingTheLiveSuperclass() { + String[] program = { + "package Test", + "native testSuccess()", + "class Base", + " int value = 42", + "class Live extends Base", + "class DeadSibling extends Base", + "init", + " let a = new Live()", + " Base b = a", + " if b instanceof Live and b.value == 42", + " testSuccess()"}; + for (boolean lua : new boolean[]{false, true}) { + WurstCompilerJassImpl compiler = translate(lua, program); + TreeShaker.removeUnreachableFunctionsBeforeGenerics(compiler.getImTranslator()); + assertEquals(Set.of("Base", "Live"), classNames(compiler)); + } + test().testLua(true).luaOnly(false).executeProg().lines(program); + } + /** The program as translated to IM, where {@link TreeShaker} has not run yet. */ private WurstCompilerJassImpl translate(boolean lua, String... lines) { WurstGuiCliImpl gui = new WurstGuiCliImpl(); @@ -396,6 +570,8 @@ public void beforeTheGenericsAClassGivenAsATypeArgumentKeepsItsConstruction() { Set after = functionNames(compiler); assertTrue("the construction of State stays: " + after, after.contains("new_State")); assertFalse("the construction of a class nothing names goes: " + after, after.contains("new_Unused")); + assertFalse("the unreferenced class goes too", classNames(compiler).contains("Unused")); + assertTrue("the generic construction still needs State", classNames(compiler).contains("State")); } @Test @@ -444,6 +620,8 @@ public void beforeTheGenericsAClassNamedInsideATypeArgumentKeepsItsConstruction( Set after = functionNames(compiler); assertTrue("the construction of State, named inside Loader, stays: " + after, after.contains("new_State")); assertFalse("the construction of a class nothing names goes: " + after, after.contains("new_Unused")); + assertFalse("the unreferenced class goes too", classNames(compiler).contains("Unused")); + assertTrue("nested type arguments still need State", classNames(compiler).contains("State")); } @Test