From aa4891ad9c25ad34b6be496ef0cefd52bb8cc32f Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 10:04:02 +0200 Subject: [PATCH 1/9] Bound Lua inlining by register pressure --- .../optimizer/LocalMerger.java | 71 ++++-- .../translation/imoptimizer/ImInliner.java | 202 +++++++++++++++++- .../imtranslation/ImTranslator.java | 3 + .../imtranslation/LuaNativeLowering.java | 3 + .../tests/LuaTranslationTests.java | 144 +++++++++++++ 5 files changed, 408 insertions(+), 15 deletions(-) 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..7d3d43f63 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; @@ -56,10 +55,20 @@ 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); + Map> interference = calculateInterferenceGraph(livenessInfo, 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 +90,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 +167,51 @@ 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, 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()) { + ImVar defined = definedLocal(entry.getKey()); + if (defined == null) { + continue; + } + java.util.Set neighbors = graph.computeIfAbsent(defined, ignored -> new ObjectOpenHashSet<>()); + for (ImVar live : entry.getValue()) { + if (live == defined || !canMerge(defined.getType(), live.getType())) { + continue; + } + neighbors.add(live); + graph.computeIfAbsent(live, ignored -> new ObjectOpenHashSet<>()).add(defined); + } + } + return graph; + } + + private static ImVar definedLocal(ImStmt stmt) { + if (!(stmt instanceof ImSet set)) { + return null; + } + ImLExpr left = set.getLeft(); + if (left instanceof ImVarAccess access && !access.getVar().isGlobal()) { + return access.getVar(); + } + if (left instanceof ImTupleSelection selection) { + ImVar var = TypesHelper.getSimpleAndPureTupleVar(selection); + if (var != null && !var.isGlobal()) { + return var; } } - return g; + return null; } private void eliminateDeadCode(Map> livenessInfo) { 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..8dd9c9e37 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,6 +5,7 @@ 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.types.TypesHelper; @@ -24,6 +25,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 +39,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; @@ -84,13 +91,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 +164,14 @@ 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)) { + if (isLuaDivModHelper(f) + && getLuaRegisterBudget(caller).fitsAggregate(call, f, 199)) { + return "unknown"; + } + return "lua_register_budget(" + getLuaRegisterBudget(caller).projectedPressure(call, f) + + ">" + LUA_INLINE_REGISTER_BUDGET + ")"; + } return "unknown"; } @@ -381,7 +406,182 @@ 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) + || (isLuaDivModHelper(f) + && getLuaRegisterBudget(caller).fitsAggregate(call, f, 199))); + } + + private boolean isLuaDivModHelper(ImFunction function) { + return function == translator.luaIntDivFunc + || function == translator.luaModIntFunc + || function == translator.luaModRealFunc; + } + + 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) { + statement.accept(new ImStmt.DefaultVisitor() { + @Override + public void visit(ImVarAccess access) { + super.visit(access); + if (!access.getVar().isGlobal()) { + result.add(access.getVar()); + } + } + }); + } + + 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 int aggregateLiveSlots; + + private LuaPressure copy() { + LuaPressure result = new LuaPressure(); + result.slotsByTypeAndLocality.putAll(slotsByTypeAndLocality); + result.aggregateLiveSlots = aggregateLiveSlots; + return result; + } + + private void add(String key, int slots) { + slotsByTypeAndLocality.merge(key, slots, Integer::sum); + aggregateLiveSlots += slots; + } + + 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); + } + aggregateLiveSlots = Math.max(aggregateLiveSlots, other.aggregateLiveSlots); + } + + 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 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(); + } + + private boolean fits(ImFunctionCall call, ImFunction callee) { + return projectedPressure(call, callee) <= LUA_INLINE_REGISTER_BUDGET; + } + + private boolean fitsAggregate(ImFunctionCall call, ImFunction callee, int limit) { + LuaPressure projected = peakPressure.copy(); + projected.keepMaximums(pressureDuringInline(call, callee)); + return projected.aggregateLiveSlots <= limit; + } + + private void recordInline(ImFunctionCall call, ImFunction callee) { + peakPressure.keepMaximums(pressureDuringInline(call, callee)); + // 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); + 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 = maxOneReturn(callee) + ? 0 + : 1 + (callee.getReturnType() instanceof ImVoid ? 0 : 1); + if (earlyReturnLocals > 0) { + // These synthetic values cannot be classified by the source locality analysis. + concurrent.add("inline-control", earlyReturnLocals); + } + 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); + return pressureOf(active); + } + } + 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/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..83f608277 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,148 @@ 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 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 spilledLocalsKeepNestedBlockInitializationsInLua() throws IOException { List lines = new ArrayList<>(); From d3b79272c399666f7a05b784908d661361e8df59 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 10:37:01 +0200 Subject: [PATCH 2/9] Fix Lua register budget review findings --- .../peeeq/wurstio/WurstCompilerJassImpl.java | 9 +++ .../optimizer/ControlFlowGraph.java | 1 - .../optimizer/LocalMerger.java | 56 +++++++++++++----- .../translation/imoptimizer/ImInliner.java | 58 +++++++++++++------ .../translation/imoptimizer/ImOptimizer.java | 4 ++ .../tests/LuaTranslationTests.java | 26 +++++++++ .../wurstscript/tests/OptimizerTests.java | 36 ++++++++++++ 7 files changed, 159 insertions(+), 31 deletions(-) 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..08c63765e 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()) { + beginPhase(10, "inline Lua arithmetic helpers within allocated local budget"); + int arithmeticHelpersInlined = optimizer.inlineLuaDivModHelpersWithinLocalBudget(); + if (arithmeticHelpersInlined > 0 && runArgs.isLocalOptimizations()) { + 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 7d3d43f63..b4195bdd8 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 @@ -181,37 +181,56 @@ private Map> calculateInterferenceGraph( // 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()) { - ImVar defined = definedLocal(entry.getKey()); - if (defined == null) { + List defined = definedLocals(entry.getKey()); + if (defined.isEmpty()) { continue; } - java.util.Set neighbors = graph.computeIfAbsent(defined, ignored -> new ObjectOpenHashSet<>()); - for (ImVar live : entry.getValue()) { - if (live == defined || !canMerge(defined.getType(), live.getType())) { - 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); + } } - neighbors.add(live); - graph.computeIfAbsent(live, ignored -> new ObjectOpenHashSet<>()).add(defined); } } return graph; } - private static ImVar definedLocal(ImStmt stmt) { + 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 null; + return Collections.emptyList(); } ImLExpr left = set.getLeft(); if (left instanceof ImVarAccess access && !access.getVar().isGlobal()) { - return access.getVar(); + return Collections.singletonList(access.getVar()); } if (left instanceof ImTupleSelection selection) { ImVar var = TypesHelper.getSimpleAndPureTupleVar(selection); if (var != null && !var.isGlobal()) { - return var; + return Collections.singletonList(var); } } - return null; + return Collections.emptyList(); } private void eliminateDeadCode(Map> livenessInfo) { @@ -315,6 +334,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) { 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 8dd9c9e37..98fdda006 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 @@ -63,6 +63,46 @@ public void doInlining() { inlineFunctions(); } + /** + * Retry the tiny compiler-owned arithmetic wrappers after local allocation has reduced the + * caller. Unlike the main inliner's pressure estimate, this late check uses the caller's actual + * function-scope declaration count, so it cannot push Lua over the hard local-variable limit. + */ + public int inlineLuaDivModHelpersWithinLocalBudget() { + if (!translator.isLuaTarget()) { + return 0; + } + prog.flatten(translator); + int changed = 0; + for (ImFunction function : sortedFunctions(ImHelper.calculateFunctionsOfProg(prog))) { + int[] declarations = {function.getParameters().size() + function.getLocals().size()}; + changed += inlineLuaDivModHelpers(function, function, declarations); + } + return changed; + } + + private int inlineLuaDivModHelpers(ImFunction function, Element element, int[] declarations) { + 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(); + int controlLocals = maxOneReturn(callee) + ? 0 + : 1 + (callee.getReturnType() instanceof ImVoid ? 0 : 1); + int addedDeclarations = callee.getParameters().size() + callee.getLocals().size() + controlLocals; + if (declarations[0] + addedDeclarations <= LUA_INLINE_REGISTER_BUDGET) { + inlineCall(function, element, i, call); + declarations[0] += addedDeclarations; + changed++; + child = element.get(i); + } + } + changed += inlineLuaDivModHelpers(function, child, declarations); + } + return changed; + } + private void inlineFunctions() { for (ImFunction f : sortedFunctions(ImHelper.calculateFunctionsOfProg(prog))) { inlineFunctions(f); @@ -165,10 +205,6 @@ private String skipReason(ImFunction caller, ImFunctionCall call, ImFunction f) return "rating_too_high(" + rating + ">=" + threshold + ")"; } if (translator.isLuaTarget() && !getLuaRegisterBudget(caller).fits(call, f)) { - if (isLuaDivModHelper(f) - && getLuaRegisterBudget(caller).fitsAggregate(call, f, 199)) { - return "unknown"; - } return "lua_register_budget(" + getLuaRegisterBudget(caller).projectedPressure(call, f) + ">" + LUA_INLINE_REGISTER_BUDGET + ")"; } @@ -408,9 +444,7 @@ private boolean shouldInline(ImFunction caller, ImFunctionCall call, ImFunction && getRating(f) < threshold && !isRecursive(f) && (!translator.isLuaTarget() - || getLuaRegisterBudget(caller).fits(call, f) - || (isLuaDivModHelper(f) - && getLuaRegisterBudget(caller).fitsAggregate(call, f, 199))); + || getLuaRegisterBudget(caller).fits(call, f)); } private boolean isLuaDivModHelper(ImFunction function) { @@ -471,18 +505,15 @@ private LuaPressure pressureOf(Iterable variables) { private static final class LuaPressure { private final Map slotsByTypeAndLocality = new LinkedHashMap<>(); - private int aggregateLiveSlots; private LuaPressure copy() { LuaPressure result = new LuaPressure(); result.slotsByTypeAndLocality.putAll(slotsByTypeAndLocality); - result.aggregateLiveSlots = aggregateLiveSlots; return result; } private void add(String key, int slots) { slotsByTypeAndLocality.merge(key, slots, Integer::sum); - aggregateLiveSlots += slots; } private void addConcurrent(LuaPressure other) { @@ -495,7 +526,6 @@ private void keepMaximums(LuaPressure other) { for (Map.Entry entry : other.slotsByTypeAndLocality.entrySet()) { slotsByTypeAndLocality.merge(entry.getKey(), entry.getValue(), Math::max); } - aggregateLiveSlots = Math.max(aggregateLiveSlots, other.aggregateLiveSlots); } private int total() { @@ -527,12 +557,6 @@ private boolean fits(ImFunctionCall call, ImFunction callee) { return projectedPressure(call, callee) <= LUA_INLINE_REGISTER_BUDGET; } - private boolean fitsAggregate(ImFunctionCall call, ImFunction callee, int limit) { - LuaPressure projected = peakPressure.copy(); - projected.keepMaximums(pressureDuringInline(call, callee)); - return projected.aggregateLiveSlots <= limit; - } - private void recordInline(ImFunctionCall call, ImFunction callee) { peakPressure.keepMaximums(pressureDuringInline(call, callee)); // Callers are processed after their callees. Publish the expanded pressure so a 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..9fd4c96dc 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,6 +67,10 @@ public void doInlining() { removeGarbage(); } + public int inlineLuaDivModHelpersWithinLocalBudget() { + return new ImInliner(trans).inlineLuaDivModHelpersWithinLocalBudget(); + } + private int optCount = 1; public void localOptimizations() { 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 83f608277..43eeed87f 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 @@ -2307,6 +2307,32 @@ public void luaLocalMergerReusesNonOverlappingLocalPlayerDependentSlots() { 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 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..e373f631b 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 @@ -12,6 +12,7 @@ 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.imtranslation.ImTranslator; import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum; import de.peeeq.wurstscript.types.TypesHelper; @@ -1541,6 +1542,41 @@ public void localMergerLiveness() throws IOException { } } + @Test + public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { + WurstModel model = Ast.WurstModel(); + ImTranslator translator = new ImTranslator(model, false, new RunArgs().with("-lua")); + 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 < 188; i++) { + callerParameters.add(JassIm.ImVar(model, TypesHelper.imInt(), "p" + i, false)); + } + ImVar result = JassIm.ImVar(model, TypesHelper.imInt(), "result", false); + ImFunctionCall call = JassIm.ImFunctionCall(model, helper, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImIntVal(7), JassIm.ImIntVal(3)), false, + de.peeeq.wurstscript.translation.imtranslation.CallType.NORMAL); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), callerParameters, + JassIm.ImVoid(), JassIm.ImVars(result), + JassIm.ImStmts(JassIm.ImSet(model, JassIm.ImVarAccess(result), call)), + Collections.emptyList()); + prog.getFunctions().add(helper); + prog.getFunctions().add(caller); + + assertEquals(0, new ImInliner(translator).inlineLuaDivModHelpersWithinLocalBudget()); + ImSet assignment = (ImSet) caller.getBody().get(0); + assertTrue(assignment.getRight() instanceof ImFunctionCall, + "the late retry must retain the helper when declarations exceed the safe budget"); + } + @Test public void testFunctionSplitter() { WurstModel model = Ast.WurstModel(); From f895c2abc4c283b9f47b6a686863806d8fd36019 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 10:46:37 +0200 Subject: [PATCH 3/9] Fix vararg loop pressure accounting --- .../translation/imoptimizer/ImInliner.java | 5 ++++ .../tests/LuaTranslationTests.java | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+) 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 98fdda006..eb19d39ee 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 @@ -481,6 +481,11 @@ private LuaPressure estimateLuaRegisterPressure(ImFunction function, } 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) { 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 43eeed87f..420ae373e 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 @@ -2333,6 +2333,33 @@ public void luaLocalMergerKeepsTupleVarargLoopBindingsDistinct() { 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"), + 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<>(); From 52eff763689aaffe6009c1f012dbf50b5e92cf0e Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 11:00:20 +0200 Subject: [PATCH 4/9] Account for Lua function-entry locals --- .../optimizer/LocalMerger.java | 24 ++++++++++ .../translation/imoptimizer/ImInliner.java | 22 ++++++++- .../wurstscript/tests/OptimizerTests.java | 46 +++++++++++++++++-- 3 files changed, 86 insertions(+), 6 deletions(-) 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 b4195bdd8..61e861921 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 @@ -177,6 +177,8 @@ private Map> calculateInterferenceGraph( graph.put(local, new ObjectOpenHashSet<>()); } + java.util.Set explicitlyDefined = Collections.newSetFromMap(new IdentityHashMap<>()); + // 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. @@ -185,6 +187,7 @@ private Map> calculateInterferenceGraph( if (defined.isEmpty()) { continue; } + explicitlyDefined.addAll(defined); for (int i = 0; i < defined.size(); i++) { ImVar definition = defined.get(i); java.util.Set neighbors = graph.computeIfAbsent(definition, ignored -> new ObjectOpenHashSet<>()); @@ -206,6 +209,27 @@ private Map> calculateInterferenceGraph( } } } + + // Parameters and locals with no explicit assignment receive their values at function + // entry. Model that simultaneous definition so a warning-only read of an uninitialized + // local cannot be colored onto a parameter (or another implicit entry value). + List entryDefinitions = new ArrayList<>(func.getParameters()); + for (ImVar local : func.getLocals()) { + if (!explicitlyDefined.contains(local)) { + entryDefinitions.add(local); + } + } + 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; } 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 eb19d39ee..f5a958024 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 @@ -75,7 +75,8 @@ public int inlineLuaDivModHelpersWithinLocalBudget() { prog.flatten(translator); int changed = 0; for (ImFunction function : sortedFunctions(ImHelper.calculateFunctionsOfProg(prog))) { - int[] declarations = {function.getParameters().size() + function.getLocals().size()}; + int[] declarations = {function.getParameters().size() + function.getLocals().size() + + backendGeneratedLuaLocals(function)}; changed += inlineLuaDivModHelpers(function, function, declarations); } return changed; @@ -453,6 +454,18 @@ private boolean isLuaDivModHelper(ImFunction function) { || 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); } @@ -546,6 +559,7 @@ private final class LuaRegisterBudget { private final ImFunction function; private Map> liveness; private LuaPressure peakPressure; + private int backendLocals; private LuaRegisterBudget(ImFunction function) { this.function = function; @@ -556,14 +570,17 @@ private LuaRegisterBudget(ImFunction function) { luaRegisterPressure.put(function, cachedPressure); } peakPressure = cachedPressure.copy(); + backendLocals = backendGeneratedLuaLocals(function); } private boolean fits(ImFunctionCall call, ImFunction callee) { - return projectedPressure(call, callee) <= LUA_INLINE_REGISTER_BUDGET; + return projectedPressure(call, callee) <= LUA_INLINE_REGISTER_BUDGET + - backendLocals - backendGeneratedLuaLocals(callee); } private void recordInline(ImFunctionCall call, ImFunction callee) { peakPressure.keepMaximums(pressureDuringInline(call, callee)); + backendLocals += backendGeneratedLuaLocals(callee); // 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()); @@ -572,6 +589,7 @@ private void recordInline(ImFunctionCall call, ImFunction callee) { private void refresh() { liveness = new LocalMerger().calculateLiveness(function); peakPressure = estimateLuaRegisterPressure(function, liveness); + backendLocals = backendGeneratedLuaLocals(function); luaRegisterPressure.put(function, peakPressure.copy()); } 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 e373f631b..f9d9890b1 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 @@ -1542,6 +1542,36 @@ 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); + ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), + JassIm.ImVars(parameter), JassIm.ImVoid(), JassIm.ImVars(implicit), + JassIm.ImStmts(call), 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 luaArithmeticHelperRetryRespectsFunctionLocalBudget() { WurstModel model = Ast.WurstModel(); @@ -1557,22 +1587,30 @@ public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { translator.luaModIntFunc = helper; ImVars callerParameters = JassIm.ImVars(); - for (int i = 0; i < 188; i++) { + for (int i = 0; i < 170; 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)); ImFunction caller = JassIm.ImFunction(model, "caller", JassIm.ImTypeVars(), callerParameters, - JassIm.ImVoid(), JassIm.ImVars(result), - JassIm.ImStmts(JassIm.ImSet(model, JassIm.ImVarAccess(result), call)), + JassIm.ImVoid(), callerLocals, callerBody, Collections.emptyList()); prog.getFunctions().add(helper); prog.getFunctions().add(caller); assertEquals(0, new ImInliner(translator).inlineLuaDivModHelpersWithinLocalBudget()); - ImSet assignment = (ImSet) caller.getBody().get(0); + 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"); } From 45c8ef047f5540be28954f4ccaf9dc48cc962b94 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 11:24:19 +0200 Subject: [PATCH 5/9] Use allocated pressure for Lua helper retries --- .../optimizer/LocalMerger.java | 43 +++++++---- .../translation/imoptimizer/ImInliner.java | 17 ++--- .../translation/imoptimizer/ImOptimizer.java | 1 + .../wurstscript/tests/OptimizerTests.java | 73 ++++++++++++++++++- 4 files changed, 108 insertions(+), 26 deletions(-) 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 61e861921..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 @@ -42,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) { @@ -54,8 +55,10 @@ 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 = calculateInterferenceGraph(livenessInfo, func); + private void mergeLocals(Map> livenessInfo, Set liveAtEntry, + ImFunction func) { + Map> interference = + calculateInterferenceGraph(livenessInfo, liveAtEntry, func); Map declarationOrder = new IdentityHashMap<>(); int nextOrder = 0; @@ -168,7 +171,7 @@ private static int removeUnusedLocals(ImFunction f) { } private Map> calculateInterferenceGraph( - Map> livenessInfo, ImFunction func) { + Map> livenessInfo, Set liveAtEntry, ImFunction func) { Map> graph = new LinkedHashMap<>(); for (ImVar parameter : func.getParameters()) { graph.put(parameter, new ObjectOpenHashSet<>()); @@ -177,8 +180,6 @@ private Map> calculateInterferenceGraph( graph.put(local, new ObjectOpenHashSet<>()); } - java.util.Set explicitlyDefined = Collections.newSetFromMap(new IdentityHashMap<>()); - // 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. @@ -187,7 +188,6 @@ private Map> calculateInterferenceGraph( if (defined.isEmpty()) { continue; } - explicitlyDefined.addAll(defined); for (int i = 0; i < defined.size(); i++) { ImVar definition = defined.get(i); java.util.Set neighbors = graph.computeIfAbsent(definition, ignored -> new ObjectOpenHashSet<>()); @@ -210,12 +210,12 @@ private Map> calculateInterferenceGraph( } } - // Parameters and locals with no explicit assignment receive their values at function - // entry. Model that simultaneous definition so a warning-only read of an uninitialized - // local cannot be colored onto a parameter (or another implicit entry value). + // 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 (!explicitlyDefined.contains(local)) { + if (liveAtEntry.contains(local)) { entryDefinitions.add(local); } } @@ -336,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(); @@ -473,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 f5a958024..e548f83d4 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 @@ -75,31 +75,26 @@ public int inlineLuaDivModHelpersWithinLocalBudget() { prog.flatten(translator); int changed = 0; for (ImFunction function : sortedFunctions(ImHelper.calculateFunctionsOfProg(prog))) { - int[] declarations = {function.getParameters().size() + function.getLocals().size() - + backendGeneratedLuaLocals(function)}; - changed += inlineLuaDivModHelpers(function, function, declarations); + LuaRegisterBudget budget = new LuaRegisterBudget(function); + changed += inlineLuaDivModHelpers(function, function, budget); } return changed; } - private int inlineLuaDivModHelpers(ImFunction function, Element element, int[] declarations) { + 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(); - int controlLocals = maxOneReturn(callee) - ? 0 - : 1 + (callee.getReturnType() instanceof ImVoid ? 0 : 1); - int addedDeclarations = callee.getParameters().size() + callee.getLocals().size() + controlLocals; - if (declarations[0] + addedDeclarations <= LUA_INLINE_REGISTER_BUDGET) { + if (budget.fits(call, callee)) { + budget.recordInline(call, callee); inlineCall(function, element, i, call); - declarations[0] += addedDeclarations; changed++; child = element.get(i); } } - changed += inlineLuaDivModHelpers(function, child, declarations); + changed += inlineLuaDivModHelpers(function, child, budget); } return changed; } 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 9fd4c96dc..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 @@ -75,6 +75,7 @@ public int inlineLuaDivModHelpersWithinLocalBudget() { public void localOptimizations() { totalCount.clear(); + optCount = 1; removeGarbage(); 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 f9d9890b1..6c4f7b88e 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; @@ -13,6 +14,7 @@ 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; @@ -1557,9 +1559,10 @@ public void localMergerKeepsImplicitEntryLocalSeparateFromParameter() { 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), Collections.emptyList()); + JassIm.ImStmts(call, laterDefinition), Collections.emptyList()); prog.getFunctions().add(sink); prog.getFunctions().add(caller); @@ -1572,6 +1575,28 @@ public void localMergerKeepsImplicitEntryLocalSeparateFromParameter() { "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(); @@ -1587,7 +1612,7 @@ public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { translator.luaModIntFunc = helper; ImVars callerParameters = JassIm.ImVars(); - for (int i = 0; i < 170; i++) { + 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); @@ -1603,10 +1628,21 @@ public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { 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()); @@ -1615,6 +1651,39 @@ public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { "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")); + 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 testFunctionSplitter() { WurstModel model = Ast.WurstModel(); From a0d38394ccc60cdd3c6734a1b52ef6693d43ab3e Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 11:37:13 +0200 Subject: [PATCH 6/9] Bound Lua inlining without local allocation --- .../peeeq/wurstio/WurstCompilerJassImpl.java | 4 +-- .../translation/imoptimizer/ImInliner.java | 16 ++++++++++ .../tests/LuaTranslationTests.java | 30 ++++++++++++++++++- .../wurstscript/tests/OptimizerTests.java | 6 ++-- 4 files changed, 51 insertions(+), 5 deletions(-) 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 08c63765e..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,10 +946,10 @@ public LuaCompilationUnit transformProgToLua() { timeTaker.endPhase(); } - if (runArgs.isInline()) { + if (runArgs.isInline() && runArgs.isLocalOptimizations()) { beginPhase(10, "inline Lua arithmetic helpers within allocated local budget"); int arithmeticHelpersInlined = optimizer.inlineLuaDivModHelpersWithinLocalBudget(); - if (arithmeticHelpersInlined > 0 && runArgs.isLocalOptimizations()) { + if (arithmeticHelpersInlined > 0) { optimizer.localOptimizations(); } timeTaker.endPhase(); 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 e548f83d4..34d75e75a 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 @@ -555,6 +555,7 @@ private final class LuaRegisterBudget { private Map> liveness; private LuaPressure peakPressure; private int backendLocals; + private int declarationsWithoutAllocation; private LuaRegisterBudget(ImFunction function) { this.function = function; @@ -566,16 +567,31 @@ private LuaRegisterBudget(ImFunction function) { } peakPressure = cachedPressure.copy(); backendLocals = backendGeneratedLuaLocals(function); + declarationsWithoutAllocation = function.getParameters().size() + function.getLocals().size() + + backendLocals; } private boolean fits(ImFunctionCall call, ImFunction callee) { + if (!translator.getRunArgs().isLocalOptimizations()) { + return declarationsWithoutAllocation + declarationsAddedByInline(callee) + <= LUA_INLINE_REGISTER_BUDGET; + } return projectedPressure(call, callee) <= LUA_INLINE_REGISTER_BUDGET - backendLocals - backendGeneratedLuaLocals(callee); } + private int declarationsAddedByInline(ImFunction callee) { + int controlLocals = maxOneReturn(callee) + ? 0 + : 1 + (callee.getReturnType() instanceof ImVoid ? 0 : 1); + return callee.getParameters().size() + callee.getLocals().size() + controlLocals + + backendGeneratedLuaLocals(callee); + } + private void recordInline(ImFunctionCall call, ImFunction callee) { peakPressure.keepMaximums(pressureDuringInline(call, callee)); backendLocals += backendGeneratedLuaLocals(callee); + declarationsWithoutAllocation += declarationsAddedByInline(callee); // 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()); 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 420ae373e..1f61e7e7f 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 @@ -2208,6 +2208,33 @@ public void luaInlinerKeepsCallWhenLiveValuesWouldExceedRegisterBudget() { 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 luaInlinerReusesRegistersAcrossSequentialInlineSites() { List lines = new ArrayList<>(); @@ -2354,7 +2381,8 @@ public void luaInlinerDoesNotTreatSequentialVarargLoopTempsAsConcurrent() { String compiled = compileLuaWithCUs( "LuaTranslationTests_luaInlinerDoesNotTreatSequentialVarargLoopTempsAsConcurrent", - false, Collections.emptyList(), new RunArgs().with("-lua", "-inline"), + 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(")); 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 6c4f7b88e..96879ea41 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 @@ -1600,7 +1600,8 @@ public void repeatedLocalOptimizationStartsANewIteration() { @Test public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { WurstModel model = Ast.WurstModel(); - ImTranslator translator = new ImTranslator(model, false, new RunArgs().with("-lua")); + ImTranslator translator = new ImTranslator(model, false, + new RunArgs().with("-lua", "-localOptimizations")); ImProg prog = translator.getImProg(); ImVar helperA = JassIm.ImVar(model, TypesHelper.imInt(), "a", false); @@ -1654,7 +1655,8 @@ public void luaArithmeticHelperRetryRespectsFunctionLocalBudget() { @Test public void luaArithmeticHelperRetryReusesSequentialSlots() { WurstModel model = Ast.WurstModel(); - ImTranslator translator = new ImTranslator(model, false, new RunArgs().with("-lua")); + 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); From 98f62cff3e4e8a9d2b7a71a50056e48d7b3644bb Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 11:52:35 +0200 Subject: [PATCH 7/9] Account for Lua allocation classes in inlining --- .../translation/imoptimizer/ImInliner.java | 34 +++++++--- .../tests/LuaTranslationTests.java | 34 ++++++++++ .../wurstscript/tests/OptimizerTests.java | 66 +++++++++++++++++++ 3 files changed, 124 insertions(+), 10 deletions(-) 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 34d75e75a..dead0088f 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 @@ -65,14 +65,15 @@ public void doInlining() { /** * Retry the tiny compiler-owned arithmetic wrappers after local allocation has reduced the - * caller. Unlike the main inliner's pressure estimate, this late check uses the caller's actual - * function-scope declaration count, so it cannot push Lua over the hard local-variable limit. + * 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); @@ -567,7 +568,8 @@ private LuaRegisterBudget(ImFunction function) { } peakPressure = cachedPressure.copy(); backendLocals = backendGeneratedLuaLocals(function); - declarationsWithoutAllocation = function.getParameters().size() + function.getLocals().size() + declarationsWithoutAllocation = flattenedDeclarationCount(function.getParameters()) + + flattenedDeclarationCount(function.getLocals()) + backendLocals; } @@ -581,13 +583,27 @@ private boolean fits(ImFunctionCall call, ImFunction callee) { } private int declarationsAddedByInline(ImFunction callee) { - int controlLocals = maxOneReturn(callee) - ? 0 - : 1 + (callee.getReturnType() instanceof ImVoid ? 0 : 1); - return callee.getParameters().size() + callee.getLocals().size() + controlLocals + 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); @@ -613,9 +629,7 @@ private int projectedPressure(ImFunctionCall call, ImFunction callee) { private LuaPressure pressureDuringInline(ImFunctionCall call, ImFunction callee) { LuaPressure concurrent = pressureAt(call); concurrent.addConcurrent(estimateLuaRegisterPressure(callee)); - int earlyReturnLocals = maxOneReturn(callee) - ? 0 - : 1 + (callee.getReturnType() instanceof ImVoid ? 0 : 1); + int earlyReturnLocals = inlineControlLocals(callee); if (earlyReturnLocals > 0) { // These synthetic values cannot be classified by the source locality analysis. concurrent.add("inline-control", earlyReturnLocals); 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 1f61e7e7f..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 @@ -2235,6 +2235,40 @@ public void luaInliningWithoutLocalAllocationUsesDeclarationBudget() { 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<>(); 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 96879ea41..41536bdfe 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 @@ -1686,6 +1686,72 @@ public void luaArithmeticHelperRetryReusesSequentialSlots() { "sequential helper sites should share the same peak allocation slots"); } + @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(); From 709d4fec45d96b8b9433033eed71a0bf2e72d3d4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 12:11:39 +0200 Subject: [PATCH 8/9] Budget overlapping Lua inline results --- .../translation/imoptimizer/ImInliner.java | 25 ++++++- .../wurstscript/tests/OptimizerTests.java | 65 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) 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 dead0088f..42694e72b 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 @@ -506,6 +506,21 @@ public void visit(ImVarAccess access) { }); } + 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 LuaPressure pressureOf(Iterable variables) { LuaPressure result = new LuaPressure(); for (ImVar variable : variables) { @@ -646,7 +661,15 @@ private LuaPressure pressureAt(Element element) { java.util.Set active = Collections.newSetFromMap(new IdentityHashMap<>()); active.addAll(live.toJavaSet()); collectReadLocals(statement, active); - return pressureOf(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(); 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 41536bdfe..0759e4c96 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 @@ -1686,6 +1686,71 @@ public void luaArithmeticHelperRetryReusesSequentialSlots() { "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 luaArithmeticHelperRetryPreservesLocalPlayerAllocationClasses() { WurstModel model = Ast.WurstModel(); From b2d81455a58bb1511966a0db667dd4b9d1a4df74 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 4 Sep 2026 12:23:39 +0200 Subject: [PATCH 9/9] Budget Lua argument staging locals --- .../translation/imoptimizer/ImInliner.java | 37 +++++++++++ .../wurstscript/tests/OptimizerTests.java | 61 +++++++++++++++++++ 2 files changed, 98 insertions(+) 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 42694e72b..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 @@ -8,6 +8,7 @@ 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.*; @@ -521,6 +522,36 @@ public void visit(ImStatementExpr expression) { 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) { @@ -591,6 +622,7 @@ private LuaRegisterBudget(ImFunction function) { 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 @@ -623,6 +655,7 @@ 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()); @@ -649,6 +682,10 @@ private LuaPressure pressureDuringInline(ImFunctionCall call, ImFunction callee) // 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; } 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 0759e4c96..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 @@ -1751,6 +1751,67 @@ public void visit(ImFunctionCall call) { 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();