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 c37625a02..bf13da627 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 @@ -946,6 +946,15 @@ public LuaCompilationUnit transformProgToLua() { timeTaker.endPhase(); } + if (runArgs.isInline() && runArgs.isLocalOptimizations()) { + beginPhase(10, "inline Lua arithmetic helpers within allocated local budget"); + int arithmeticHelpersInlined = optimizer.inlineLuaDivModHelpersWithinLocalBudget(); + if (arithmeticHelpersInlined > 0) { + optimizer.localOptimizations(); + } + timeTaker.endPhase(); + } + printDebugImProg("./test-output/lua/im " + stage++ + "_afterlocalopts.im"); boolean garbageChanged = optimizer.removeGarbage(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ControlFlowGraph.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ControlFlowGraph.java index 1fcbd54b1..4ae58a116 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ControlFlowGraph.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ControlFlowGraph.java @@ -163,7 +163,6 @@ private Node getNode(ImStmt s) { result.stmt = null; } else if (s instanceof ImVarargLoop) { result.setName("vararg loop"); - result.stmt = null; } } return result; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java index d53385112..ac48751a6 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java @@ -6,7 +6,6 @@ import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.types.TypesHelper; -import io.vavr.collection.HashSet; import io.vavr.collection.Set; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; @@ -43,9 +42,10 @@ private void optimizeFunctions(List functions) { public String getName() { return "Local variables merged"; } void optimizeFunc(ImFunction func) { - Map> livenessInfo = calculateLiveness(func); + LivenessAnalysis liveness = analyzeLiveness(func); + Map> livenessInfo = liveness.liveOut; eliminateDeadCode(livenessInfo); - mergeLocals(livenessInfo, func); + mergeLocals(livenessInfo, liveness.liveAtEntry, func); } void optimizeFunc(ImFunction func, LocalPlayerContextAnalyzer analyzer) { @@ -55,11 +55,23 @@ void optimizeFunc(ImFunction func, LocalPlayerContextAnalyzer analyzer) { private boolean canMerge(ImType a, ImType b) { return a.equalsType(b); } - private void mergeLocals(Map> livenessInfo, ImFunction func) { - Map> interference = calculateInferenceGraph(livenessInfo); + private void mergeLocals(Map> livenessInfo, Set liveAtEntry, + ImFunction func) { + Map> interference = + calculateInterferenceGraph(livenessInfo, liveAtEntry, func); + + Map declarationOrder = new IdentityHashMap<>(); + int nextOrder = 0; + for (ImVar parameter : func.getParameters()) { + declarationOrder.put(parameter, nextOrder++); + } + for (ImVar local : func.getLocals()) { + declarationOrder.put(local, nextOrder++); + } PriorityQueue queue = new PriorityQueue<>( - (x, y) -> interference.get(y).size() - interference.get(x).size() + Comparator.comparingInt(v -> interference.get(v).size()).reversed() + .thenComparingInt(declarationOrder::get) ); queue.addAll(interference.keySet()); @@ -81,8 +93,8 @@ private void mergeLocals(Map> livenessInfo, ImFunction func) continue; } if (localPlayerContextAnalyzer != null - && (localPlayerContextAnalyzer.isLocalPlayerDependent(v) - || localPlayerContextAnalyzer.isLocalPlayerDependent(color))) { + && localPlayerContextAnalyzer.isLocalPlayerDependent(v) + != localPlayerContextAnalyzer.isLocalPlayerDependent(color)) { continue; } @@ -158,17 +170,91 @@ private static int removeUnusedLocals(ImFunction f) { return before - kept.size(); } - private Map> calculateInferenceGraph(Map> livenessInfo) { - Map> g = new LinkedHashMap<>(); - for (Map.Entry> e : livenessInfo.entrySet()) { - Set live = e.getValue(); - for (ImVar v1 : live) { - Set set = g.getOrDefault(v1, HashSet.empty()); - set = set.addAll(live.filter(v2 -> canMerge(v1.getType(), v2.getType()))); - g.put(v1, set); + private Map> calculateInterferenceGraph( + Map> livenessInfo, Set liveAtEntry, ImFunction func) { + Map> graph = new LinkedHashMap<>(); + for (ImVar parameter : func.getParameters()) { + graph.put(parameter, new ObjectOpenHashSet<>()); + } + for (ImVar local : func.getLocals()) { + graph.put(local, new ObjectOpenHashSet<>()); + } + + // A definition interferes with every compatible value that remains live after it. + // Building only those edges is equivalent to cliquing every live set, while avoiding + // the old O(statements * liveValues^2) behavior on large inlined functions. + for (Map.Entry> entry : livenessInfo.entrySet()) { + List defined = definedLocals(entry.getKey()); + if (defined.isEmpty()) { + continue; + } + for (int i = 0; i < defined.size(); i++) { + ImVar definition = defined.get(i); + java.util.Set neighbors = graph.computeIfAbsent(definition, ignored -> new ObjectOpenHashSet<>()); + for (ImVar live : entry.getValue()) { + if (live == definition || !canMerge(definition.getType(), live.getType())) { + continue; + } + neighbors.add(live); + graph.computeIfAbsent(live, ignored -> new ObjectOpenHashSet<>()).add(definition); + } + // Vararg tuple components are assigned at the same loop boundary. They must + // occupy distinct slots even when neither component is live before the loop. + for (int j = i + 1; j < defined.size(); j++) { + ImVar other = defined.get(j); + if (canMerge(definition.getType(), other.getType())) { + neighbors.add(other); + graph.computeIfAbsent(other, ignored -> new ObjectOpenHashSet<>()).add(definition); + } + } + } + } + + // A local live at entry is read before every control-flow path has assigned it. Its + // target-default value must remain distinct from every incoming parameter and from the + // other entry-live locals, even if a later assignment eventually defines it. + List entryDefinitions = new ArrayList<>(func.getParameters()); + for (ImVar local : func.getLocals()) { + if (liveAtEntry.contains(local)) { + entryDefinitions.add(local); } } - return g; + for (int i = 0; i < entryDefinitions.size(); i++) { + ImVar definition = entryDefinitions.get(i); + java.util.Set neighbors = graph.get(definition); + for (int j = i + 1; j < entryDefinitions.size(); j++) { + ImVar other = entryDefinitions.get(j); + if (canMerge(definition.getType(), other.getType())) { + neighbors.add(other); + graph.get(other).add(definition); + } + } + } + return graph; + } + + private static List definedLocals(ImStmt stmt) { + if (stmt instanceof ImVarargLoop loop) { + List result = new ArrayList<>(loop.getLoopVars().size()); + for (ImVarargLoopVar loopVar : loop.getLoopVars()) { + result.add(loopVar.getVar()); + } + return result; + } + if (!(stmt instanceof ImSet set)) { + return Collections.emptyList(); + } + ImLExpr left = set.getLeft(); + if (left instanceof ImVarAccess access && !access.getVar().isGlobal()) { + return Collections.singletonList(access.getVar()); + } + if (left instanceof ImTupleSelection selection) { + ImVar var = TypesHelper.getSimpleAndPureTupleVar(selection); + if (var != null && !var.isGlobal()) { + return Collections.singletonList(var); + } + } + return Collections.emptyList(); } private void eliminateDeadCode(Map> livenessInfo) { @@ -250,6 +336,10 @@ private static boolean hasSideEffects(Element e) { * over the strongly connected components of the control flow graph. */ public Map> calculateLiveness(ImFunction func) { + return analyzeLiveness(func).liveOut; + } + + private LivenessAnalysis analyzeLiveness(ImFunction func) { // 1. Build Control Flow Graph ControlFlowGraph cfg = new ControlFlowGraph(func.getBody()); final List nodes = cfg.getNodes(); @@ -272,6 +362,17 @@ public Map> calculateLiveness(ImFunction func) { ImStmt stmt = node.getStmt(); if (stmt == null) continue; + if (stmt instanceof ImVarargLoop loop) { + for (ImVarargLoopVar loopVar : loop.getLoopVars()) { + if (!loopVar.getVar().isGlobal()) { + def[i].add(loopVar.getVar()); + } + } + // The loop body has its own CFG nodes. Visiting it here would incorrectly + // classify all body reads as uses at the loop header. + continue; + } + final int ii = i; stmt.accept(new ImStmt.DefaultVisitor() { @Override public void visit(ImVarAccess va) { @@ -376,6 +477,19 @@ protected Collection getIncidentNodes(Node t) { result.put(stmt, io.vavr.collection.HashSet.ofAll(out[i])); } } - return result; + Set liveAtEntry = N == 0 + ? io.vavr.collection.HashSet.empty() + : io.vavr.collection.HashSet.ofAll(in[0]); + return new LivenessAnalysis(result, liveAtEntry); + } + + private static final class LivenessAnalysis { + private final Map> liveOut; + private final Set liveAtEntry; + + private LivenessAnalysis(Map> liveOut, Set liveAtEntry) { + this.liveOut = liveOut; + this.liveAtEntry = liveAtEntry; + } } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java index 5cd539817..8bb0c82d1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java @@ -5,8 +5,10 @@ import com.google.common.collect.Sets; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.intermediatelang.optimizer.LocalPlayerContextAnalyzer; +import de.peeeq.wurstscript.intermediatelang.optimizer.LocalMerger; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.*; +import de.peeeq.wurstscript.translation.imtranslation.purity.Pure; import de.peeeq.wurstscript.types.TypesHelper; import java.util.*; @@ -24,6 +26,10 @@ public class ImInliner { private static final int DEFAULT_ALWAYS_INLINE_SIZE = 20; /** Just above the largest measured ordinary Lua leaf: unit_getAbilityLevel at 63 IM nodes. */ private static final int LUA_ALWAYS_INLINE_SIZE = 64; + /** Leave room below Lua's hard 200-local limit for backend-introduced locals. */ + private static final int LUA_INLINE_REGISTER_BUDGET = 190; + /** Rebuild CFG liveness after expansions large enough to invalidate the incremental estimate. */ + private static final int LUA_LIVENESS_REFRESH_INLINE_SIZE = 256; private static final Set dontInline = Sets.newLinkedHashSet(); private static final boolean LOG_INLINER = Boolean.getBoolean("wurst.inliner.log"); @@ -34,6 +40,8 @@ public class ImInliner { private final Map funcSizes = Maps.newLinkedHashMap(); private final Set done = Sets.newLinkedHashSet(); private final Map containsFuncRefCache = Maps.newLinkedHashMap(); + private final Map luaRegisterBudgets = Maps.newLinkedHashMap(); + private final Map luaRegisterPressure = Maps.newLinkedHashMap(); private final double inlineTreshold = 50; private LocalPlayerContextAnalyzer localPlayerContextAnalyzer; @@ -56,6 +64,43 @@ public void doInlining() { inlineFunctions(); } + /** + * Retry the tiny compiler-owned arithmetic wrappers after local allocation has reduced the + * caller. The late check rebuilds the locality analysis and uses the same allocation classes as + * the local merger, so it cannot push Lua over the hard local-variable limit. + */ + public int inlineLuaDivModHelpersWithinLocalBudget() { + if (!translator.isLuaTarget()) { + return 0; + } + prog.flatten(translator); + localPlayerContextAnalyzer = new LocalPlayerContextAnalyzer(prog); + int changed = 0; + for (ImFunction function : sortedFunctions(ImHelper.calculateFunctionsOfProg(prog))) { + LuaRegisterBudget budget = new LuaRegisterBudget(function); + changed += inlineLuaDivModHelpers(function, function, budget); + } + return changed; + } + + private int inlineLuaDivModHelpers(ImFunction function, Element element, LuaRegisterBudget budget) { + int changed = 0; + for (int i = 0; i < element.size(); i++) { + Element child = element.get(i); + if (child instanceof ImFunctionCall call && isLuaDivModHelper(call.getFunc())) { + ImFunction callee = call.getFunc(); + if (budget.fits(call, callee)) { + budget.recordInline(call, callee); + inlineCall(function, element, i, call); + changed++; + child = element.get(i); + } + } + changed += inlineLuaDivModHelpers(function, child, budget); + } + return changed; + } + private void inlineFunctions() { for (ImFunction f : sortedFunctions(ImHelper.calculateFunctionsOfProg(prog))) { inlineFunctions(f); @@ -84,13 +129,23 @@ private ImFunction inlineFunctions(ImFunction f, Element parent, int parentI, El if (LOG_INLINER) { String msg = "[INLINER] caller=" + f.getName() + " callee=" + called.getName() + " decision=" + (canInline ? "inline" : "keep") + " size=" + getFuncSize(called) + " rating=" + getRating(called) + + (translator.isLuaTarget() && inlinableFunctions.contains(called) + ? " projectedLuaRegisters=" + getLuaRegisterBudget(f).projectedPressure(call, called) + : "") + (canInline ? "" : " reason=" + skipReason(f, call, called)); WLogger.info(msg); System.out.println(msg); } if (canInline) { if (alreadyInlined.getOrDefault(called, 0) < 5) { // check maximum to ensure termination + if (translator.isLuaTarget()) { + getLuaRegisterBudget(f).recordInline(call, called); + } inlineCall(f, parent, parentI, call); + if (translator.isLuaTarget() + && getFuncSize(called) >= LUA_LIVENESS_REFRESH_INLINE_SIZE) { + getLuaRegisterBudget(f).refresh(); + } // translator.removeCallRelation(f, called); // XXX is it safe to remove this call relation? changed[0] = true; int newSize = estimateSize(f); @@ -147,6 +202,10 @@ private String skipReason(ImFunction caller, ImFunctionCall call, ImFunction f) if (rating >= threshold) { return "rating_too_high(" + rating + ">=" + threshold + ")"; } + if (translator.isLuaTarget() && !getLuaRegisterBudget(caller).fits(call, f)) { + return "lua_register_budget(" + getLuaRegisterBudget(caller).projectedPressure(call, f) + + ">" + LUA_INLINE_REGISTER_BUDGET + ")"; + } return "unknown"; } @@ -381,7 +440,280 @@ private boolean shouldInline(ImFunction caller, ImFunctionCall call, ImFunction // WLogger.info(" rating: " + getRating(f)); return inlinableFunctions.contains(f) && getRating(f) < threshold - && !isRecursive(f); + && !isRecursive(f) + && (!translator.isLuaTarget() + || getLuaRegisterBudget(caller).fits(call, f)); + } + + private boolean isLuaDivModHelper(ImFunction function) { + return function == translator.luaIntDivFunc + || function == translator.luaModIntFunc + || function == translator.luaModRealFunc; + } + + private static int backendGeneratedLuaLocals(ImFunction function) { + int[] result = {0}; + function.getBody().accept(new ImStmts.DefaultVisitor() { + @Override + public void visit(ImVarargLoop loop) { + result[0] += 2; // Lua translation introduces __args and __i for each retained loop. + super.visit(loop); + } + }); + return result[0]; + } + + private LuaRegisterBudget getLuaRegisterBudget(ImFunction function) { + return luaRegisterBudgets.computeIfAbsent(function, LuaRegisterBudget::new); + } + + private LuaPressure estimateLuaRegisterPressure(ImFunction function) { + LuaPressure cached = luaRegisterPressure.get(function); + if (cached != null) { + return cached; + } + Map> liveness = new LocalMerger().calculateLiveness(function); + LuaPressure pressure = estimateLuaRegisterPressure(function, liveness); + luaRegisterPressure.put(function, pressure); + return pressure; + } + + private LuaPressure estimateLuaRegisterPressure(ImFunction function, + Map> liveness) { + LuaPressure maximum = pressureOf(function.getParameters()); + for (Map.Entry> entry : liveness.entrySet()) { + java.util.Set active = Collections.newSetFromMap(new IdentityHashMap<>()); + active.addAll(entry.getValue().toJavaSet()); + collectReadLocals(entry.getKey(), active); + maximum.keepMaximums(pressureOf(active)); + } + return maximum; + } + + private static void collectReadLocals(ImStmt statement, java.util.Set result) { + if (statement instanceof ImVarargLoop) { + // The loop body has separate liveness entries. Counting all of its reads at the + // header would make sequential temporaries appear simultaneously live. + return; + } + statement.accept(new ImStmt.DefaultVisitor() { + @Override + public void visit(ImVarAccess access) { + super.visit(access); + if (!access.getVar().isGlobal()) { + result.add(access.getVar()); + } + } + }); + } + + private static int statementExpressionResultSlots(ImStmt statement) { + int[] result = {0}; + statement.accept(new ImStmt.DefaultVisitor() { + @Override + public void visit(ImStatementExpr expression) { + super.visit(expression); + ImType type = expression.getExpr().attrTyp(); + if (!(type instanceof ImVoid)) { + result[0] += ImHelper.flattenedJassArity(type); + } + } + }); + return result[0]; + } + + private static int argumentStagingSlots(ImFunctionCall call) { + int result = 0; + Element current = call; + while (current != null) { + if (current != call && current instanceof ImStmt) { + break; + } + Element parent = current.getParent(); + if (parent instanceof ImExprs expressions) { + int currentIndex = -1; + for (int i = 0; i < expressions.size(); i++) { + if (expressions.get(i) == current) { + currentIndex = i; + break; + } + } + for (int i = 0; i < currentIndex; i++) { + ImExpr earlier = expressions.get(i); + if (!(earlier.attrPurity() instanceof Pure)) { + result += ImHelper.flattenedJassArity(earlier.attrTyp()); + } + } + current = expressions.getParent(); + } else { + current = parent; + } + } + return result; + } + + private LuaPressure pressureOf(Iterable variables) { + LuaPressure result = new LuaPressure(); + for (ImVar variable : variables) { + boolean localPlayerDependent = localPlayerContextAnalyzer != null + && localPlayerContextAnalyzer.isLocalPlayerDependent(variable); + result.add(variable.getType() + "|local=" + localPlayerDependent, + ImHelper.flattenedJassArity(variable.getType())); + } + return result; + } + + private static final class LuaPressure { + private final Map slotsByTypeAndLocality = new LinkedHashMap<>(); + + private LuaPressure copy() { + LuaPressure result = new LuaPressure(); + result.slotsByTypeAndLocality.putAll(slotsByTypeAndLocality); + return result; + } + + private void add(String key, int slots) { + slotsByTypeAndLocality.merge(key, slots, Integer::sum); + } + + private void addConcurrent(LuaPressure other) { + for (Map.Entry entry : other.slotsByTypeAndLocality.entrySet()) { + add(entry.getKey(), entry.getValue()); + } + } + + private void keepMaximums(LuaPressure other) { + for (Map.Entry entry : other.slotsByTypeAndLocality.entrySet()) { + slotsByTypeAndLocality.merge(entry.getKey(), entry.getValue(), Math::max); + } + } + + private int total() { + int result = 0; + for (int slots : slotsByTypeAndLocality.values()) { + result += slots; + } + return result; + } + } + + private final class LuaRegisterBudget { + private final ImFunction function; + private Map> liveness; + private LuaPressure peakPressure; + private int backendLocals; + private int declarationsWithoutAllocation; + + private LuaRegisterBudget(ImFunction function) { + this.function = function; + liveness = new LocalMerger().calculateLiveness(function); + LuaPressure cachedPressure = luaRegisterPressure.get(function); + if (cachedPressure == null) { + cachedPressure = estimateLuaRegisterPressure(function, liveness); + luaRegisterPressure.put(function, cachedPressure); + } + peakPressure = cachedPressure.copy(); + backendLocals = backendGeneratedLuaLocals(function); + declarationsWithoutAllocation = flattenedDeclarationCount(function.getParameters()) + + flattenedDeclarationCount(function.getLocals()) + + backendLocals; + } + + private boolean fits(ImFunctionCall call, ImFunction callee) { + if (!translator.getRunArgs().isLocalOptimizations()) { + return declarationsWithoutAllocation + declarationsAddedByInline(callee) + + argumentStagingSlots(call) + <= LUA_INLINE_REGISTER_BUDGET; + } + return projectedPressure(call, callee) <= LUA_INLINE_REGISTER_BUDGET + - backendLocals - backendGeneratedLuaLocals(callee); + } + + private int declarationsAddedByInline(ImFunction callee) { + return flattenedDeclarationCount(callee.getParameters()) + + flattenedDeclarationCount(callee.getLocals()) + inlineControlLocals(callee) + + backendGeneratedLuaLocals(callee); + } + + private int inlineControlLocals(ImFunction callee) { + return maxOneReturn(callee) + ? 0 + : 1 + (callee.getReturnType() instanceof ImVoid + ? 0 + : ImHelper.flattenedJassArity(callee.getReturnType())); + } + + private int flattenedDeclarationCount(ImVars variables) { + int result = 0; + for (int i = 0; i < variables.size(); i++) { + result += ImHelper.flattenedJassArity(variables.get(i).getType()); + } + return result; + } + + private void recordInline(ImFunctionCall call, ImFunction callee) { + peakPressure.keepMaximums(pressureDuringInline(call, callee)); + backendLocals += backendGeneratedLuaLocals(callee); + declarationsWithoutAllocation += declarationsAddedByInline(callee); + declarationsWithoutAllocation += argumentStagingSlots(call); + // Callers are processed after their callees. Publish the expanded pressure so a + // later caller budgets the body it will actually copy, not the pre-inline callee. + luaRegisterPressure.put(function, peakPressure.copy()); + } + + private void refresh() { + liveness = new LocalMerger().calculateLiveness(function); + peakPressure = estimateLuaRegisterPressure(function, liveness); + backendLocals = backendGeneratedLuaLocals(function); + luaRegisterPressure.put(function, peakPressure.copy()); + } + + private int projectedPressure(ImFunctionCall call, ImFunction callee) { + LuaPressure projected = peakPressure.copy(); + projected.keepMaximums(pressureDuringInline(call, callee)); + return projected.total(); + } + + private LuaPressure pressureDuringInline(ImFunctionCall call, ImFunction callee) { + LuaPressure concurrent = pressureAt(call); + concurrent.addConcurrent(estimateLuaRegisterPressure(callee)); + int earlyReturnLocals = inlineControlLocals(callee); + if (earlyReturnLocals > 0) { + // These synthetic values cannot be classified by the source locality analysis. + concurrent.add("inline-control", earlyReturnLocals); + } + int stagedArguments = argumentStagingSlots(call); + if (stagedArguments > 0) { + concurrent.add("argument-staging", stagedArguments); + } + return concurrent; + } + + private LuaPressure pressureAt(Element element) { + Element current = element; + while (current != null) { + if (current instanceof ImStmt statement) { + io.vavr.collection.Set live = liveness.get(statement); + if (live != null) { + java.util.Set active = Collections.newSetFromMap(new IdentityHashMap<>()); + active.addAll(live.toJavaSet()); + collectReadLocals(statement, active); + LuaPressure pressure = pressureOf(active); + int stagedResults = statementExpressionResultSlots(statement); + if (stagedResults > 0) { + // Flattening stages each already-inlined sibling result until the + // surrounding expression consumes it. The pre-inline liveness map + // cannot contain those future backend temporaries yet. + pressure.add("statement-expression-results", stagedResults); + } + return pressure; + } + } + current = current.getParent(); + } + // Unknown synthetic shape: remain conservative rather than risking a whole-function spill. + return peakPressure.copy(); + } } private boolean isRecursive(ImFunction f) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java index ee63a7e8d..01d4fc2ba 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java @@ -67,10 +67,15 @@ public void doInlining() { removeGarbage(); } + public int inlineLuaDivModHelpersWithinLocalBudget() { + return new ImInliner(trans).inlineLuaDivModHelpersWithinLocalBudget(); + } + private int optCount = 1; public void localOptimizations() { totalCount.clear(); + optCount = 1; removeGarbage(); 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 c0f330026..34a6bd684 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 @@ -195,6 +195,9 @@ public T canonical(T copy) { @Nullable public ImFunction luaRawFloorDivIntFunc = null; @Nullable public ImFunction luaRawFmodIntFunc = null; @Nullable public ImFunction luaRawFmodRealFunc = null; + @Nullable public ImFunction luaIntDivFunc = null; + @Nullable public ImFunction luaModIntFunc = null; + @Nullable public ImFunction luaModRealFunc = null; private final Map varsForTupleVar = new Object2ObjectLinkedOpenHashMap<>(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index 7a733f9eb..b75bded09 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java @@ -377,6 +377,7 @@ List createdFunctions() { ImFunction intDiv() { if (intDiv == null) { intDiv = buildIntDiv(rawFloorDivInt()); + translator.luaIntDivFunc = intDiv; created.add(intDiv); } return intDiv; @@ -385,6 +386,7 @@ ImFunction intDiv() { ImFunction modInt() { if (modInt == null) { modInt = buildMod("__wurst_modInt", TypesHelper.imInt(), JassIm.ImIntVal(0), rawFmodInt()); + translator.luaModIntFunc = modInt; created.add(modInt); } return modInt; @@ -393,6 +395,7 @@ ImFunction modInt() { ImFunction modReal() { if (modReal == null) { modReal = buildMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."), rawFmodReal()); + translator.luaModRealFunc = modReal; created.add(modReal); } return modReal; diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index 211830688..3aeec8bb4 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.testng.AssertJUnit.*; @@ -2163,6 +2165,263 @@ public void inlinerDoesNotForceSpillWhenCallerStaysBelowLimit() throws IOExcepti assertTrue("caller should keep direct call in this shape", callerBody.contains("small(1)")); } + @Test + public void luaInlinerKeepsCallWhenLiveValuesWouldExceedRegisterBudget() { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("native takesInt(int i)"); + lines.add("@inline function helper(int x) returns int"); + for (int i = 0; i < 16; i++) { + lines.add(" let h" + i + " = x + " + i); + } + lines.add(" return " + IntStream.range(0, 16) + .mapToObj(i -> "h" + i) + .collect(Collectors.joining(" + "))); + lines.add("@noinline function caller()"); + for (int i = 0; i < 180; i++) { + lines.add(" let v" + i + " = takesIntAndReturn(" + i + ")"); + } + lines.add(" var sum = helper(1)"); + for (int i = 0; i < 180; i++) { + lines.add(" sum += v" + i); + } + lines.add(" takesInt(sum)"); + lines.add("@noinline function takesIntAndReturn(int x) returns int"); + lines.add(" takesInt(x)"); + lines.add(" return x"); + lines.add("init"); + lines.add(" caller()"); + + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaInlinerKeepsCallWhenLiveValuesWouldExceedRegisterBudget", + false, Collections.emptyList(), + new RunArgs().with("-lua", "-inline"), + lines.toArray(new String[0])); + int callerStart = compiled.indexOf("function caller("); + assertTrue("caller function not found", callerStart >= 0); + int callerEnd = compiled.indexOf("\nend", callerStart); + assertTrue("caller function end not found", callerEnd > callerStart); + String callerBody = compiled.substring(callerStart, callerEnd); + assertTrue("@inline is a strong preference, but must not force Lua register spilling:\n" + callerBody, + callerBody.contains("helper(1)")); + assertFalse("budgeted caller must stay out of the heap locals fallback:\n" + callerBody, + callerBody.contains("__wurst_locals")); + } + + @Test + public void luaInliningWithoutLocalAllocationUsesDeclarationBudget() { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("native takesInt(int i)"); + lines.add("@noinline function caller(int value)"); + for (int i = 0; i < 70; i++) { + lines.add(" takesInt((value + " + i + ") mod 3)"); + } + lines.add("init"); + lines.add(" caller(7)"); + + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaInliningWithoutLocalAllocationUsesDeclarationBudget", + false, Collections.emptyList(), new RunArgs().with("-lua", "-inline"), + lines.toArray(new String[0])); + int callerStart = compiled.indexOf("function caller("); + assertTrue("caller function not found", callerStart >= 0); + int callerEnd = compiled.indexOf("\nend", callerStart); + assertTrue("caller function end not found", callerEnd > callerStart); + String body = compiled.substring(callerStart, callerEnd); + assertFalse("inlining without allocation must not cross into whole-function spill mode:\n" + body, + body.contains("__wurst_locals")); + assertTrue("the exact declaration budget must retain residual helper calls near the limit:\n" + body, + body.contains("__wurst_modInt(")); + } + + @Test + public void luaInliningWithoutLocalAllocationCountsFlattenedTuples() { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("tuple quad(int a, int b, int c, int d)"); + lines.add("native takesInt(int i)"); + lines.add("@inline function tupleHelper(quad a, quad b, quad c, quad d) returns int"); + lines.add(" return a.a + b.a + c.a + d.a"); + String parameters = IntStream.range(0, 47) + .mapToObj(i -> "quad p" + i) + .collect(Collectors.joining(", ")); + lines.add("@noinline function caller(" + parameters + ")"); + lines.add(" takesInt(tupleHelper(p0, p1, p2, p3))"); + lines.add("init"); + lines.add(" let value = quad(1, 2, 3, 4)"); + lines.add(" caller(" + IntStream.range(0, 47) + .mapToObj(i -> "value") + .collect(Collectors.joining(", ")) + ")"); + + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaInliningWithoutLocalAllocationCountsFlattenedTuples", + false, Collections.emptyList(), new RunArgs().with("-lua", "-inline"), + lines.toArray(new String[0])); + int callerStart = compiled.indexOf("function caller("); + assertTrue("caller function not found", callerStart >= 0); + int callerEnd = compiled.indexOf("\nend", callerStart); + assertTrue("caller function end not found", callerEnd > callerStart); + String body = compiled.substring(callerStart, callerEnd); + assertFalse("a caller below Lua's hard limit must stay register-backed:\n" + body, + body.contains("__wurst_locals")); + assertTrue("tuple components must count separately when deciding whether to inline:\n" + body, + body.contains("tupleHelper(")); + } + + @Test + public void luaInlinerReusesRegistersAcrossSequentialInlineSites() { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("native takesInt(int i)"); + lines.add("@inline function helper(int x) returns int"); + lines.add(" let a = x + 1"); + lines.add(" let b = a + 1"); + lines.add(" let c = b + 1"); + lines.add(" return c"); + lines.add("@noinline function caller()"); + lines.add(" var sum = 0"); + for (int i = 0; i < 80; i++) { + lines.add(" sum += helper(" + i + ")"); + } + lines.add(" takesInt(sum)"); + lines.add("init"); + lines.add(" caller()"); + + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaInlinerReusesRegistersAcrossSequentialInlineSites", + false, Collections.emptyList(), + new RunArgs().with("-lua", "-inline", "-localOptimizations"), + lines.toArray(new String[0])); + String callerBody = getFunctionBody(compiled, "caller"); + assertFalse("low-pressure sequential helper calls should still inline:\n" + callerBody, + callerBody.contains("helper(")); + assertFalse("sequential inline temporaries should reuse registers:\n" + callerBody, + callerBody.contains("__wurst_locals")); + } + + @Test + public void luaInlinerBudgetsTheExpandedNestedCallee() { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("native takesInt(int i)"); + lines.add("@noinline function takesIntAndReturn(int x) returns int"); + lines.add(" takesInt(x)"); + lines.add(" return x"); + lines.add("@inline function leaf(int x) returns int"); + for (int i = 0; i < 20; i++) { + lines.add(" let h" + i + " = x + " + i); + } + lines.add(" return " + IntStream.range(0, 20) + .mapToObj(i -> "h" + i) + .collect(Collectors.joining(" + "))); + lines.add("@inline function middle(int x) returns int"); + lines.add(" return leaf(x)"); + lines.add("@noinline function caller()"); + for (int i = 0; i < 175; i++) { + lines.add(" let v" + i + " = takesIntAndReturn(" + i + ")"); + } + lines.add(" var sum = middle(1)"); + for (int i = 0; i < 175; i++) { + lines.add(" sum += v" + i); + } + lines.add(" takesInt(sum)"); + lines.add("init"); + lines.add(" caller()"); + + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaInlinerBudgetsTheExpandedNestedCallee", + false, Collections.emptyList(), + new RunArgs().with("-lua", "-inline"), + lines.toArray(new String[0])); + int callerStart = compiled.indexOf("function caller("); + assertTrue("caller function not found", callerStart >= 0); + int callerEnd = compiled.indexOf("\nend", callerStart); + assertTrue("caller function end not found", callerEnd > callerStart); + String callerBody = compiled.substring(callerStart, callerEnd); + assertTrue("caller must budget the already-expanded middle body:\n" + callerBody, + callerBody.contains("middle(1)")); + assertFalse("nested inline accounting must prevent a whole-function spill:\n" + callerBody, + callerBody.contains("__wurst_locals")); + } + + @Test + public void luaLocalMergerReusesNonOverlappingLocalPlayerDependentSlots() { + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaLocalMergerReusesNonOverlappingLocalPlayerDependentSlots", + false, Collections.emptyList(), + new RunArgs().with("-lua", "-localOptimizations"), + "type player extends handle", + "package Test", + "@extern native GetLocalPlayer() returns player", + "@extern native takesPlayer(player p)", + "@noinline function caller()", + " let first = GetLocalPlayer()", + " takesPlayer(first)", + " let second = GetLocalPlayer()", + " takesPlayer(second)", + "init", + " caller()" + ); + String callerBody = getFunctionBody(compiled, "caller"); + assertEquals("same-locality values with disjoint live ranges should share one Lua register:\n" + callerBody, + 1, countMatches(callerBody, "local\\s+(?:first|second)\\b")); + } + + @Test + public void luaLocalMergerKeepsTupleVarargLoopBindingsDistinct() { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("tuple quad(int a, int b, int c, int d)"); + lines.add("@noinline function sumEdges(vararg quad values) returns int"); + lines.add(" var result = 0"); + lines.add(" for value in values"); + lines.add(" result += value.a + value.d"); + lines.add(" return result"); + lines.add("init"); + String arguments = IntStream.range(0, 33) + .mapToObj(i -> "quad(" + i + ", 0, 0, " + (100 + i) + ")") + .collect(Collectors.joining(", ")); + lines.add(" sumEdges(" + arguments + ")"); + + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaLocalMergerKeepsTupleVarargLoopBindingsDistinct", + false, Collections.emptyList(), + new RunArgs().with("-lua", "-localOptimizations"), + lines.toArray(new String[0])); + String body = getFunctionBody(compiled, "sumEdges"); + assertEquals("simultaneously assigned tuple components must use distinct Lua locals:\n" + body, + 4, countMatches(body, "local\\s+value_[abcd]\\b")); + } + + @Test + public void luaInlinerDoesNotTreatSequentialVarargLoopTempsAsConcurrent() { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("native takesInt(int i)"); + lines.add("@inline function small(int x) returns int"); + lines.add(" return x + 1"); + lines.add("@noinline function process(vararg int values)"); + lines.add(" for value in values"); + for (int i = 0; i < 191; i++) { + lines.add(" let temp" + i + " = value + " + i); + lines.add(" takesInt(temp" + i + ")"); + } + lines.add(" takesInt(small(value))"); + lines.add("init"); + lines.add(" process(" + IntStream.range(0, 33) + .mapToObj(Integer::toString) + .collect(Collectors.joining(", ")) + ")"); + + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaInlinerDoesNotTreatSequentialVarargLoopTempsAsConcurrent", + false, Collections.emptyList(), + new RunArgs().with("-lua", "-inline", "-localOptimizations"), + lines.toArray(new String[0])); + assertFalse("sequential loop temporaries must not consume concurrent register budget:\n" + compiled, + compiled.contains("small(")); + } + @Test public void spilledLocalsKeepNestedBlockInitializationsInLua() throws IOException { List lines = new ArrayList<>(); 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 fc6bb5ade..e0bd59db7 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 @@ -2,6 +2,7 @@ import com.google.common.base.Charsets; import com.google.common.io.Files; +import de.peeeq.wurstio.TimeTaker; import de.peeeq.wurstio.UtilsIO; import de.peeeq.wurstscript.RunArgs; import de.peeeq.wurstscript.ast.Ast; @@ -12,6 +13,8 @@ import de.peeeq.wurstscript.intermediatelang.optimizer.LocalPlayerContextAnalyzer; import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer; import de.peeeq.wurstscript.jassIm.*; +import de.peeeq.wurstscript.translation.imoptimizer.ImInliner; +import de.peeeq.wurstscript.translation.imoptimizer.ImOptimizer; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum; import de.peeeq.wurstscript.types.TypesHelper; @@ -1541,6 +1544,340 @@ public void localMergerLiveness() throws IOException { } } + @Test + public void localMergerKeepsImplicitEntryLocalSeparateFromParameter() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, new RunArgs()); + ImProg prog = translator.getImProg(); + ImVar sinkA = JassIm.ImVar(model, TypesHelper.imInt(), "a", false); + ImVar sinkB = JassIm.ImVar(model, TypesHelper.imInt(), "b", false); + ImFunction sink = JassIm.ImFunction(model, "sink", JassIm.ImTypeVars(), + JassIm.ImVars(sinkA, sinkB), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.emptyList()); + ImVar parameter = JassIm.ImVar(model, TypesHelper.imInt(), "parameter", false); + ImVar implicit = JassIm.ImVar(model, TypesHelper.imInt(), "implicit", false); + ImFunctionCall call = JassIm.ImFunctionCall(model, sink, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImVarAccess(parameter), JassIm.ImVarAccess(implicit)), false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL); + ImSet laterDefinition = JassIm.ImSet(model, JassIm.ImVarAccess(implicit), JassIm.ImIntVal(1)); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), + JassIm.ImVars(parameter), JassIm.ImVoid(), JassIm.ImVars(implicit), + JassIm.ImStmts(call, laterDefinition), Collections.emptyList()); + prog.getFunctions().add(sink); + prog.getFunctions().add(caller); + + new LocalMerger().optimize(translator, new LocalPlayerContextAnalyzer(prog)); + + ImFunctionCall optimizedCall = (ImFunctionCall) caller.getBody().get(0); + ImVar first = ((ImVarAccess) optimizedCall.getArguments().get(0)).getVar(); + ImVar second = ((ImVarAccess) optimizedCall.getArguments().get(1)).getVar(); + assertNotSame(first, second, + "function-entry values must not be assigned the same allocation slot"); + } + + @Test + public void repeatedLocalOptimizationStartsANewIteration() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, new RunArgs()); + ImFunction main = JassIm.ImFunction(model, "main", JassIm.ImTypeVars(), JassIm.ImVars(), + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + ImFunction config = JassIm.ImFunction(model, "config", JassIm.ImTypeVars(), JassIm.ImVars(), + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + translator.getImProg().getFunctions().add(main); + translator.getImProg().getFunctions().add(config); + translator.setMainFunc(main); + translator.setConfigFunc(config); + ImOptimizer optimizer = new ImOptimizer(new TimeTaker.Default(), translator); + + optimizer.localOptimizations(); + main.getLocals().add(JassIm.ImVar(model, TypesHelper.imInt(), "lateUnused", false)); + optimizer.localOptimizations(); + + assertTrue(main.getLocals().isEmpty(), + "a second local-optimization invocation must execute its passes"); + } + + @Test + public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, + new RunArgs().with("-lua", "-localOptimizations")); + ImProg prog = translator.getImProg(); + + ImVar helperA = JassIm.ImVar(model, TypesHelper.imInt(), "a", false); + ImVar helperB = JassIm.ImVar(model, TypesHelper.imInt(), "b", false); + ImFunction helper = JassIm.ImFunction(model, "__wurst_modInt", JassIm.ImTypeVars(), + JassIm.ImVars(helperA, helperB), TypesHelper.imInt(), JassIm.ImVars(), + JassIm.ImStmts(JassIm.ImReturn(model, JassIm.ImVarAccess(helperA))), + Collections.emptyList()); + translator.luaModIntFunc = helper; + + ImVars callerParameters = JassIm.ImVars(); + for (int i = 0; i < 177; i++) { + callerParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "p" + i, false)); + } + ImVar result = JassIm.ImVar(model, TypesHelper.imInt(), "result", false); + ImVars callerLocals = JassIm.ImVars(result); + ImStmts callerBody = JassIm.ImStmts(); + for (int i = 0; i < 6; i++) { + ImVar loopVar = JassIm.ImVar(model, TypesHelper.imInt(), "loop" + i, false); + callerLocals.add(loopVar); + callerBody.add(JassIm.ImVarargLoop(model, JassIm.ImStmts(), + JassIm.ImVarargLoopVars(JassIm.ImVarargLoopVar(loopVar)))); + } + ImFunctionCall call = JassIm.ImFunctionCall(model, helper, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImIntVal(7), JassIm.ImIntVal(3)), false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL); + callerBody.add(JassIm.ImSet(model, JassIm.ImVarAccess(result), call)); + ImVars sinkParameters = JassIm.ImVars(); + ImExprs sinkArguments = JassIm.ImExprs(); + for (int i = 0; i < callerParameters.size(); i++) { + sinkParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "value" + i, false)); + sinkArguments.add(JassIm.ImVarAccess(callerParameters.get(i))); + } + ImFunction sink = JassIm.ImFunction(model, "sink", JassIm.ImTypeVars(), sinkParameters, + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + callerBody.add(JassIm.ImFunctionCall(model, sink, JassIm.ImTypeArguments(), sinkArguments, + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), callerParameters, + JassIm.ImVoid(), callerLocals, callerBody, + Collections.emptyList()); + prog.getFunctions().add(helper); + prog.getFunctions().add(sink); + prog.getFunctions().add(caller); + + assertEquals(0, new ImInliner(translator).inlineLuaDivModHelpersWithinLocalBudget()); + ImSet assignment = (ImSet) caller.getBody().get(6); + assertTrue(assignment.getRight() instanceof ImFunctionCall, + "the late retry must retain the helper when declarations exceed the safe budget"); + } + + @Test + public void luaArithmeticHelperRetryReusesSequentialSlots() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, + new RunArgs().with("-lua", "-localOptimizations")); + ImProg prog = translator.getImProg(); + ImVar helperA = JassIm.ImVar(model, TypesHelper.imInt(), "a", false); + ImVar helperB = JassIm.ImVar(model, TypesHelper.imInt(), "b", false); + ImFunction helper = JassIm.ImFunction(model, "__wurst_modInt", JassIm.ImTypeVars(), + JassIm.ImVars(helperA, helperB), TypesHelper.imInt(), JassIm.ImVars(), + JassIm.ImStmts(JassIm.ImReturn(model, JassIm.ImVarAccess(helperA))), + Collections.emptyList()); + translator.luaModIntFunc = helper; + ImVars parameters = JassIm.ImVars(); + for (int i = 0; i < 187; i++) { + parameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "p" + i, false)); + } + ImVar result = JassIm.ImVar(model, TypesHelper.imInt(), "result", false); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), parameters, + JassIm.ImVoid(), JassIm.ImVars(result), JassIm.ImStmts( + JassIm.ImSet(model, JassIm.ImVarAccess(result), JassIm.ImFunctionCall(model, helper, + JassIm.ImTypeArguments(), JassIm.ImExprs(JassIm.ImIntVal(7), JassIm.ImIntVal(3)), + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)), + JassIm.ImSet(model, JassIm.ImVarAccess(result), JassIm.ImFunctionCall(model, helper, + JassIm.ImTypeArguments(), JassIm.ImExprs(JassIm.ImIntVal(8), JassIm.ImIntVal(3)), + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL))), + Collections.emptyList()); + prog.getFunctions().add(helper); + prog.getFunctions().add(caller); + + assertEquals(new ImInliner(translator).inlineLuaDivModHelpersWithinLocalBudget(), 2, + "sequential helper sites should share the same peak allocation slots"); + } + + @Test + public void luaArithmeticHelperRetryBudgetsOverlappingArgumentResults() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, + new RunArgs().with("-lua", "-localOptimizations")); + ImProg prog = translator.getImProg(); + ImVar helperA = JassIm.ImVar(model, TypesHelper.imInt(), "a", false); + ImVar helperB = JassIm.ImVar(model, TypesHelper.imInt(), "b", false); + ImFunction helper = JassIm.ImFunction(model, "__wurst_modInt", JassIm.ImTypeVars(), + JassIm.ImVars(helperA, helperB), TypesHelper.imInt(), JassIm.ImVars(), + JassIm.ImStmts(JassIm.ImReturn(model, JassIm.ImVarAccess(helperA))), + Collections.emptyList()); + translator.luaModIntFunc = helper; + + ImVars callerParameters = JassIm.ImVars(); + for (int i = 0; i < 187; i++) { + callerParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "p" + i, false)); + } + ImVars fiveParameters = JassIm.ImVars(); + ImExprs overlappingArguments = JassIm.ImExprs(); + for (int i = 0; i < 5; i++) { + fiveParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "arg" + i, false)); + overlappingArguments.add(JassIm.ImFunctionCall(model, helper, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImVarAccess(callerParameters.get(i)), JassIm.ImIntVal(3)), + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)); + } + ImFunction takesFive = JassIm.ImFunction(model, "takesFive", JassIm.ImTypeVars(), fiveParameters, + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + ImVars keepAliveParameters = JassIm.ImVars(); + ImExprs keepAliveArguments = JassIm.ImExprs(); + for (int i = 0; i < callerParameters.size(); i++) { + keepAliveParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "value" + i, false)); + keepAliveArguments.add(JassIm.ImVarAccess(callerParameters.get(i))); + } + ImFunction keepAlive = JassIm.ImFunction(model, "keepAlive", JassIm.ImTypeVars(), + keepAliveParameters, JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.emptyList()); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), callerParameters, + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts( + JassIm.ImFunctionCall(model, takesFive, JassIm.ImTypeArguments(), overlappingArguments, + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL), + JassIm.ImFunctionCall(model, keepAlive, JassIm.ImTypeArguments(), keepAliveArguments, + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)), + Collections.emptyList()); + prog.getFunctions().add(helper); + prog.getFunctions().add(takesFive); + prog.getFunctions().add(keepAlive); + prog.getFunctions().add(caller); + + int changed = new ImInliner(translator).inlineLuaDivModHelpersWithinLocalBudget(); + assertTrue(changed < 5, + "overlapping argument results must stop helper inlining at the register budget"); + int[] remaining = {0}; + caller.getBody().accept(new ImStmts.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + if (call.getFunc() == helper) { + remaining[0]++; + } + } + }); + assertTrue(remaining[0] > 0, "some overlapping helper calls must remain after the budget is reached"); + } + + @Test + public void luaArithmeticHelperRetryBudgetsEarlierImpureArguments() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, + new RunArgs().with("-lua", "-localOptimizations")); + ImProg prog = translator.getImProg(); + ImVar helperA = JassIm.ImVar(model, TypesHelper.imInt(), "a", false); + ImVar helperB = JassIm.ImVar(model, TypesHelper.imInt(), "b", false); + ImFunction helper = JassIm.ImFunction(model, "__wurst_modInt", JassIm.ImTypeVars(), + JassIm.ImVars(helperA, helperB), TypesHelper.imInt(), JassIm.ImVars(), + JassIm.ImStmts(JassIm.ImReturn(model, JassIm.ImVarAccess(helperA))), + Collections.emptyList()); + translator.luaModIntFunc = helper; + ImVar impureParameter = JassIm.ImVar(model, TypesHelper.imInt(), "value", false); + ImFunction impure = JassIm.ImFunction(model, "impure", JassIm.ImTypeVars(), + JassIm.ImVars(impureParameter), TypesHelper.imInt(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.singletonList(FunctionFlagEnum.IS_NATIVE)); + + ImVars callerParameters = JassIm.ImVars(); + for (int i = 0; i < 178; i++) { + callerParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "p" + i, false)); + } + ImVars outerParameters = JassIm.ImVars(); + ImExprs outerArguments = JassIm.ImExprs(); + for (int i = 0; i < 11; i++) { + outerParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "arg" + i, false)); + outerArguments.add(JassIm.ImFunctionCall(model, impure, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImVarAccess(callerParameters.get(i))), false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)); + } + outerParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "last", false)); + outerArguments.add(JassIm.ImFunctionCall(model, helper, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImVarAccess(callerParameters.get(11)), JassIm.ImIntVal(3)), false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)); + ImFunction outer = JassIm.ImFunction(model, "outer", JassIm.ImTypeVars(), outerParameters, + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + ImVars keepAliveParameters = JassIm.ImVars(); + ImExprs keepAliveArguments = JassIm.ImExprs(); + for (int i = 0; i < callerParameters.size(); i++) { + keepAliveParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "keep" + i, false)); + keepAliveArguments.add(JassIm.ImVarAccess(callerParameters.get(i))); + } + ImFunction keepAlive = JassIm.ImFunction(model, "keepAlive", JassIm.ImTypeVars(), + keepAliveParameters, JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.emptyList()); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), callerParameters, + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts( + JassIm.ImFunctionCall(model, outer, JassIm.ImTypeArguments(), outerArguments, false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL), + JassIm.ImFunctionCall(model, keepAlive, JassIm.ImTypeArguments(), keepAliveArguments, false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)), + Collections.emptyList()); + prog.getFunctions().add(helper); + prog.getFunctions().add(impure); + prog.getFunctions().add(outer); + prog.getFunctions().add(keepAlive); + prog.getFunctions().add(caller); + + assertEquals(0, new ImInliner(translator).inlineLuaDivModHelpersWithinLocalBudget()); + } + + @Test + public void luaArithmeticHelperRetryPreservesLocalPlayerAllocationClasses() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, + new RunArgs().with("-lua", "-localOptimizations")); + ImProg prog = translator.getImProg(); + + ImVar helperA = JassIm.ImVar(model, TypesHelper.imInt(), "a", false); + ImVar helperB = JassIm.ImVar(model, TypesHelper.imInt(), "b", false); + ImFunction helper = JassIm.ImFunction(model, "__wurst_modInt", JassIm.ImTypeVars(), + JassIm.ImVars(helperA, helperB), TypesHelper.imInt(), JassIm.ImVars(), + JassIm.ImStmts(JassIm.ImReturn(model, JassIm.ImVarAccess(helperA))), + Collections.emptyList()); + translator.luaModIntFunc = helper; + ImFunction localValue = JassIm.ImFunction(model, "GetLocationZ", JassIm.ImTypeVars(), + JassIm.ImVars(), TypesHelper.imReal(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.singletonList(FunctionFlagEnum.IS_NATIVE)); + + ImVars sinkParameters = JassIm.ImVars(); + for (int i = 0; i < 99; i++) { + sinkParameters.add(JassIm.ImVar(model, TypesHelper.imReal(), "value" + i, false)); + } + ImFunction sink = JassIm.ImFunction(model, "sink", JassIm.ImTypeVars(), sinkParameters, + JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + ImVars callerLocals = JassIm.ImVars(); + ImStmts callerBody = JassIm.ImStmts(); + ImExprs localArguments = JassIm.ImExprs(); + ImExprs synchronizedArguments = JassIm.ImExprs(); + for (int i = 0; i < 99; i++) { + ImVar local = JassIm.ImVar(model, TypesHelper.imReal(), "local" + i, false); + ImVar synchronizedVar = JassIm.ImVar(model, TypesHelper.imReal(), "sync" + i, false); + callerLocals.add(local); + callerLocals.add(synchronizedVar); + callerBody.add(JassIm.ImSet(model, JassIm.ImVarAccess(local), + JassIm.ImFunctionCall(model, localValue, JassIm.ImTypeArguments(), JassIm.ImExprs(), + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL))); + localArguments.add(JassIm.ImVarAccess(local)); + synchronizedArguments.add(JassIm.ImVarAccess(synchronizedVar)); + } + callerBody.add(JassIm.ImFunctionCall(model, sink, JassIm.ImTypeArguments(), localArguments, + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)); + for (int i = 0; i < 99; i++) { + ImVar synchronizedVar = callerLocals.get(i * 2 + 1); + callerBody.add(JassIm.ImSet(model, JassIm.ImVarAccess(synchronizedVar), JassIm.ImRealVal("1."))); + } + callerBody.add(JassIm.ImFunctionCall(model, sink, JassIm.ImTypeArguments(), synchronizedArguments, + false, de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL)); + ImVar result = JassIm.ImVar(model, TypesHelper.imInt(), "result", false); + callerLocals.add(result); + ImFunctionCall helperCall = JassIm.ImFunctionCall(model, helper, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImIntVal(7), JassIm.ImIntVal(3)), false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL); + callerBody.add(JassIm.ImSet(model, JassIm.ImVarAccess(result), helperCall)); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), JassIm.ImVars(), + JassIm.ImVoid(), callerLocals, callerBody, Collections.emptyList()); + prog.getFunctions().add(localValue); + prog.getFunctions().add(helper); + prog.getFunctions().add(sink); + prog.getFunctions().add(caller); + + assertEquals(0, new ImInliner(translator).inlineLuaDivModHelpersWithinLocalBudget()); + assertTrue(((ImSet) caller.getBody().get(caller.getBody().size() - 1)).getRight() + instanceof ImFunctionCall, + "local-player-dependent and synchronized allocation classes must both count toward the budget"); + } + @Test public void testFunctionSplitter() { WurstModel model = Ast.WurstModel();