Skip to content

Commit 3445a67

Browse files
authored
Lua code emission updates (#1287)
* Stop the local-player barrier from refusing inlining on control taint (#1284) * Optimize Lua array reads and div/mod emission (#1288) * Optimize Lua array reads and div-mod intrinsics Remove typed primitive array normalization, invert legacy assertions to require raw reads, and keep erased-generic normalization intact. Emit raw div/mod primitives directly as Lua operators without helper definitions. * Track Lua numeric intrinsics by IM identity * Give vararg calls a fixed-arity copy on Lua (#1286) * Inline small Lua helpers regardless of popularity (#1289) * Deduplicate Lua callback adapters (#1290) * Deduplicate Lua callback adapters * Preserve renamed Lua callback targets * Bound Lua inlining by register pressure (#1291)
1 parent c5aa7e4 commit 3445a67

19 files changed

Lines changed: 2704 additions & 329 deletions

File tree

AGENTS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,22 @@ Recent fixes established additional rules for backend work. Follow these for all
226226
requirement: common optimized paths must not retain avoidable compiler-introduced allocation,
227227
dispatch, copying, or bookkeeping overhead.
228228

229+
### Lua performance policy
230+
231+
* **Wurst-emitted constructs are consumed by Wurst code.** Never add runtime coercion, nil guards,
232+
normalisation wrappers or other defensive code to emitted Lua whose justification is that foreign
233+
(non-Wurst) Lua might have mutated an emitted table, array or value. A user who bundles raw Lua that
234+
writes into Wurst-emitted structures owns the result. Typed arrays already carry a metatable that
235+
supplies the typed default; a read of a typed array is a raw table index and nothing else.
236+
* **Leverage Lua-native mechanisms wherever semantics permit.** Prefer a metatable default over a
237+
read-site helper, an operator over a helper call, a fixed-arity function over a `...` pack, and a
238+
direct table over an emulated hashtable. Emulating Jass limitations on Lua needs evidence that the
239+
limitation actually applies there.
240+
* **A compiler-introduced call or allocation on an ordinary typed code path is a defect.** The
241+
optimiser must be able to inline small pure helpers; an analysis barrier that refuses to inline a
242+
function must be justified by what that function does, not by where else it happens to be called.
243+
* The concrete open items and their acceptance criteria are in `LUA_HOT_PATH_SPEC.md`.
244+
229245
### Jass/Lua feature parity
230246

231247
* New language/compiler features must be validated for **both Jass and Lua** backends.

LUA_HOT_PATH_SPEC.md

Lines changed: 448 additions & 0 deletions
Large diffs are not rendered by default.

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,14 @@ public LuaCompilationUnit transformProgToLua() {
897897
timeTaker.endPhase();
898898
}
899899
}
900+
// Same position as on Jass: after stack traces, before lowering and inlining. Calls with a
901+
// static argument count go to fixed-arity copies, so the emitted Lua packs no table and the
902+
// copies can inline; originals stay for dispatch, function references and calls above the bound.
903+
beginPhase(4, "eliminate varargs");
904+
new VarargEliminator(imProg, true).run();
905+
imTranslator.assertProperties();
906+
timeTaker.endPhase();
907+
900908
ImTranslator imTranslator2 = getImTranslator();
901909
ImOptimizer optimizer = new ImOptimizer(timeTaker, imTranslator2);
902910

@@ -938,6 +946,15 @@ public LuaCompilationUnit transformProgToLua() {
938946
timeTaker.endPhase();
939947
}
940948

949+
if (runArgs.isInline() && runArgs.isLocalOptimizations()) {
950+
beginPhase(10, "inline Lua arithmetic helpers within allocated local budget");
951+
int arithmeticHelpersInlined = optimizer.inlineLuaDivModHelpersWithinLocalBudget();
952+
if (arithmeticHelpersInlined > 0) {
953+
optimizer.localOptimizations();
954+
}
955+
timeTaker.endPhase();
956+
}
957+
941958
printDebugImProg("./test-output/lua/im " + stage++ + "_afterlocalopts.im");
942959

943960
boolean garbageChanged = optimizer.removeGarbage();

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ControlFlowGraph.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ private Node getNode(ImStmt s) {
163163
result.stmt = null;
164164
} else if (s instanceof ImVarargLoop) {
165165
result.setName("vararg loop");
166-
result.stmt = null;
167166
}
168167
}
169168
return result;

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalMerger.java

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import de.peeeq.wurstscript.translation.imtranslation.ImHelper;
77
import de.peeeq.wurstscript.translation.imtranslation.ImTranslator;
88
import de.peeeq.wurstscript.types.TypesHelper;
9-
import io.vavr.collection.HashSet;
109
import io.vavr.collection.Set;
1110
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
1211
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
@@ -43,9 +42,10 @@ private void optimizeFunctions(List<ImFunction> functions) {
4342
public String getName() { return "Local variables merged"; }
4443

4544
void optimizeFunc(ImFunction func) {
46-
Map<ImStmt, Set<ImVar>> livenessInfo = calculateLiveness(func);
45+
LivenessAnalysis liveness = analyzeLiveness(func);
46+
Map<ImStmt, Set<ImVar>> livenessInfo = liveness.liveOut;
4747
eliminateDeadCode(livenessInfo);
48-
mergeLocals(livenessInfo, func);
48+
mergeLocals(livenessInfo, liveness.liveAtEntry, func);
4949
}
5050

5151
void optimizeFunc(ImFunction func, LocalPlayerContextAnalyzer analyzer) {
@@ -55,11 +55,23 @@ void optimizeFunc(ImFunction func, LocalPlayerContextAnalyzer analyzer) {
5555

5656
private boolean canMerge(ImType a, ImType b) { return a.equalsType(b); }
5757

58-
private void mergeLocals(Map<ImStmt, Set<ImVar>> livenessInfo, ImFunction func) {
59-
Map<ImVar, Set<ImVar>> interference = calculateInferenceGraph(livenessInfo);
58+
private void mergeLocals(Map<ImStmt, Set<ImVar>> livenessInfo, Set<ImVar> liveAtEntry,
59+
ImFunction func) {
60+
Map<ImVar, java.util.Set<ImVar>> interference =
61+
calculateInterferenceGraph(livenessInfo, liveAtEntry, func);
62+
63+
Map<ImVar, Integer> declarationOrder = new IdentityHashMap<>();
64+
int nextOrder = 0;
65+
for (ImVar parameter : func.getParameters()) {
66+
declarationOrder.put(parameter, nextOrder++);
67+
}
68+
for (ImVar local : func.getLocals()) {
69+
declarationOrder.put(local, nextOrder++);
70+
}
6071

6172
PriorityQueue<ImVar> queue = new PriorityQueue<>(
62-
(x, y) -> interference.get(y).size() - interference.get(x).size()
73+
Comparator.<ImVar>comparingInt(v -> interference.get(v).size()).reversed()
74+
.thenComparingInt(declarationOrder::get)
6375
);
6476
queue.addAll(interference.keySet());
6577

@@ -81,8 +93,8 @@ private void mergeLocals(Map<ImStmt, Set<ImVar>> livenessInfo, ImFunction func)
8193
continue;
8294
}
8395
if (localPlayerContextAnalyzer != null
84-
&& (localPlayerContextAnalyzer.isLocalPlayerDependent(v)
85-
|| localPlayerContextAnalyzer.isLocalPlayerDependent(color))) {
96+
&& localPlayerContextAnalyzer.isLocalPlayerDependent(v)
97+
!= localPlayerContextAnalyzer.isLocalPlayerDependent(color)) {
8698
continue;
8799
}
88100

@@ -158,17 +170,91 @@ private static int removeUnusedLocals(ImFunction f) {
158170
return before - kept.size();
159171
}
160172

161-
private Map<ImVar, Set<ImVar>> calculateInferenceGraph(Map<ImStmt, Set<ImVar>> livenessInfo) {
162-
Map<ImVar, Set<ImVar>> g = new LinkedHashMap<>();
163-
for (Map.Entry<ImStmt, Set<ImVar>> e : livenessInfo.entrySet()) {
164-
Set<ImVar> live = e.getValue();
165-
for (ImVar v1 : live) {
166-
Set<ImVar> set = g.getOrDefault(v1, HashSet.empty());
167-
set = set.addAll(live.filter(v2 -> canMerge(v1.getType(), v2.getType())));
168-
g.put(v1, set);
173+
private Map<ImVar, java.util.Set<ImVar>> calculateInterferenceGraph(
174+
Map<ImStmt, Set<ImVar>> livenessInfo, Set<ImVar> liveAtEntry, ImFunction func) {
175+
Map<ImVar, java.util.Set<ImVar>> graph = new LinkedHashMap<>();
176+
for (ImVar parameter : func.getParameters()) {
177+
graph.put(parameter, new ObjectOpenHashSet<>());
178+
}
179+
for (ImVar local : func.getLocals()) {
180+
graph.put(local, new ObjectOpenHashSet<>());
181+
}
182+
183+
// A definition interferes with every compatible value that remains live after it.
184+
// Building only those edges is equivalent to cliquing every live set, while avoiding
185+
// the old O(statements * liveValues^2) behavior on large inlined functions.
186+
for (Map.Entry<ImStmt, Set<ImVar>> entry : livenessInfo.entrySet()) {
187+
List<ImVar> defined = definedLocals(entry.getKey());
188+
if (defined.isEmpty()) {
189+
continue;
190+
}
191+
for (int i = 0; i < defined.size(); i++) {
192+
ImVar definition = defined.get(i);
193+
java.util.Set<ImVar> neighbors = graph.computeIfAbsent(definition, ignored -> new ObjectOpenHashSet<>());
194+
for (ImVar live : entry.getValue()) {
195+
if (live == definition || !canMerge(definition.getType(), live.getType())) {
196+
continue;
197+
}
198+
neighbors.add(live);
199+
graph.computeIfAbsent(live, ignored -> new ObjectOpenHashSet<>()).add(definition);
200+
}
201+
// Vararg tuple components are assigned at the same loop boundary. They must
202+
// occupy distinct slots even when neither component is live before the loop.
203+
for (int j = i + 1; j < defined.size(); j++) {
204+
ImVar other = defined.get(j);
205+
if (canMerge(definition.getType(), other.getType())) {
206+
neighbors.add(other);
207+
graph.computeIfAbsent(other, ignored -> new ObjectOpenHashSet<>()).add(definition);
208+
}
209+
}
210+
}
211+
}
212+
213+
// A local live at entry is read before every control-flow path has assigned it. Its
214+
// target-default value must remain distinct from every incoming parameter and from the
215+
// other entry-live locals, even if a later assignment eventually defines it.
216+
List<ImVar> entryDefinitions = new ArrayList<>(func.getParameters());
217+
for (ImVar local : func.getLocals()) {
218+
if (liveAtEntry.contains(local)) {
219+
entryDefinitions.add(local);
169220
}
170221
}
171-
return g;
222+
for (int i = 0; i < entryDefinitions.size(); i++) {
223+
ImVar definition = entryDefinitions.get(i);
224+
java.util.Set<ImVar> neighbors = graph.get(definition);
225+
for (int j = i + 1; j < entryDefinitions.size(); j++) {
226+
ImVar other = entryDefinitions.get(j);
227+
if (canMerge(definition.getType(), other.getType())) {
228+
neighbors.add(other);
229+
graph.get(other).add(definition);
230+
}
231+
}
232+
}
233+
return graph;
234+
}
235+
236+
private static List<ImVar> definedLocals(ImStmt stmt) {
237+
if (stmt instanceof ImVarargLoop loop) {
238+
List<ImVar> result = new ArrayList<>(loop.getLoopVars().size());
239+
for (ImVarargLoopVar loopVar : loop.getLoopVars()) {
240+
result.add(loopVar.getVar());
241+
}
242+
return result;
243+
}
244+
if (!(stmt instanceof ImSet set)) {
245+
return Collections.emptyList();
246+
}
247+
ImLExpr left = set.getLeft();
248+
if (left instanceof ImVarAccess access && !access.getVar().isGlobal()) {
249+
return Collections.singletonList(access.getVar());
250+
}
251+
if (left instanceof ImTupleSelection selection) {
252+
ImVar var = TypesHelper.getSimpleAndPureTupleVar(selection);
253+
if (var != null && !var.isGlobal()) {
254+
return Collections.singletonList(var);
255+
}
256+
}
257+
return Collections.emptyList();
172258
}
173259

174260
private void eliminateDeadCode(Map<ImStmt, Set<ImVar>> livenessInfo) {
@@ -250,6 +336,10 @@ private static boolean hasSideEffects(Element e) {
250336
* over the strongly connected components of the control flow graph.
251337
*/
252338
public Map<ImStmt, Set<ImVar>> calculateLiveness(ImFunction func) {
339+
return analyzeLiveness(func).liveOut;
340+
}
341+
342+
private LivenessAnalysis analyzeLiveness(ImFunction func) {
253343
// 1. Build Control Flow Graph
254344
ControlFlowGraph cfg = new ControlFlowGraph(func.getBody());
255345
final List<Node> nodes = cfg.getNodes();
@@ -272,6 +362,17 @@ public Map<ImStmt, Set<ImVar>> calculateLiveness(ImFunction func) {
272362
ImStmt stmt = node.getStmt();
273363
if (stmt == null) continue;
274364

365+
if (stmt instanceof ImVarargLoop loop) {
366+
for (ImVarargLoopVar loopVar : loop.getLoopVars()) {
367+
if (!loopVar.getVar().isGlobal()) {
368+
def[i].add(loopVar.getVar());
369+
}
370+
}
371+
// The loop body has its own CFG nodes. Visiting it here would incorrectly
372+
// classify all body reads as uses at the loop header.
373+
continue;
374+
}
375+
275376
final int ii = i;
276377
stmt.accept(new ImStmt.DefaultVisitor() {
277378
@Override public void visit(ImVarAccess va) {
@@ -376,6 +477,19 @@ protected Collection<Node> getIncidentNodes(Node t) {
376477
result.put(stmt, io.vavr.collection.HashSet.ofAll(out[i]));
377478
}
378479
}
379-
return result;
480+
Set<ImVar> liveAtEntry = N == 0
481+
? io.vavr.collection.HashSet.empty()
482+
: io.vavr.collection.HashSet.ofAll(in[0]);
483+
return new LivenessAnalysis(result, liveAtEntry);
484+
}
485+
486+
private static final class LivenessAnalysis {
487+
private final Map<ImStmt, Set<ImVar>> liveOut;
488+
private final Set<ImVar> liveAtEntry;
489+
490+
private LivenessAnalysis(Map<ImStmt, Set<ImVar>> liveOut, Set<ImVar> liveAtEntry) {
491+
this.liveOut = liveOut;
492+
this.liveAtEntry = liveAtEntry;
493+
}
380494
}
381495
}

0 commit comments

Comments
 (0)