Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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.
* <p>
* 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
Expand All @@ -57,9 +70,10 @@
* <p>
* 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.
* <p>
* Both pipelines run it three times: in front of the compile-time functions (which walk the whole program and analyse
Expand Down Expand Up @@ -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<ImClass> needed = new ReferenceOpenHashSet<>();
private final Set<ImType> types = new ReferenceOpenHashSet<>();
private final ArrayDeque<ImClass> 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<ImClass> partitions = new Partitions<>();
for (ImClass c : prog.getClasses()) {
partitions.add(c);
for (ImClassType superclass : c.getSuperClasses()) {
partitions.union(c, superclass.getClassDef());
}
}
Set<ImClass> 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
Expand Down Expand Up @@ -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<ImFunction> reachable;
private final Set<ImClass> constructedClasses = new ReferenceOpenHashSet<>();
Expand All @@ -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());
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,25 @@ public List<ImVar> 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<ImClass> pinnedClasses() {
if (classManagementVars == null) {
return Collections.emptyList();
}
return imProg.getClasses().stream().filter(classManagementVars::containsKey).toList();
}

private final Map<ImVar, VarsForTupleResult> varsForTupleVar = new Object2ObjectLinkedOpenHashMap<>();

private final boolean isUnitTestMode;
Expand Down
Loading
Loading