Skip to content

Commit 8f2cb5e

Browse files
authored
Give vararg calls a fixed-arity copy on Lua (#1286)
1 parent be57b4e commit 8f2cb5e

3 files changed

Lines changed: 519 additions & 18 deletions

File tree

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

Lines changed: 8 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

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/VarargEliminator.java

Lines changed: 208 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.stream.Collectors;
1414

1515
import static de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum.IS_VARARG;
16+
import static de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum.PRESERVE_NAME;
1617

1718
/**
1819
* Takes a program and eliminates vararg functions, replacing them with
@@ -21,30 +22,178 @@
2122
public class VarargEliminator {
2223

2324
private static final int JASS_MAX_PARAMETERS = ImHelper.JASS_MAX_PARAMETERS;
25+
/**
26+
* Largest number of emitted parameters a fixed-arity copy may have on Lua, counted after tuple
27+
* flattening: one four-field tuple argument is four parameters, so an arity that looks modest in
28+
* source can exceed what the target accepts. Lua caps a function at 200 locals including its
29+
* parameters, and the locals-table fallback cannot spill parameters, so this leaves room for the
30+
* body's own locals. A call above it keeps the original `...` function, which is always still
31+
* present on that target.
32+
*/
33+
public static final int LUA_MAX_SPECIALISED_VARARG_PARAMETERS = 64;
2434
private final ImProg prog;
35+
/**
36+
* On Lua classes are still present when this runs, so a vararg function can also be reached
37+
* through a method dispatch or a function reference. Originals are therefore kept, only direct
38+
* calls are redirected, and unreferenced originals are left to garbage removal.
39+
*/
40+
private final boolean luaTarget;
2541
// original + number of args --> new function
2642
private final Table<ImFunction, Integer, ImFunction> varargFuncs = HashBasedTable.create();
2743

2844
public VarargEliminator(ImProg prog) {
45+
this(prog, false);
46+
}
47+
48+
public VarargEliminator(ImProg prog, boolean luaTarget) {
2949
this.prog = prog;
50+
this.luaTarget = luaTarget;
3051
}
3152

3253
public void run() {
33-
// create new vararg functions
34-
for (ImFunctionCall c : collectVarargCalls()) {
35-
if (c.getFunc().hasFlag(IS_VARARG)) {
36-
generateVarargFunc(c);
54+
// Create new vararg functions. Repeated to a fixpoint: a generated copy can contain a call
55+
// to a vararg function at an arity nothing has needed yet, which is what a recursive vararg
56+
// function calling itself with a different argument count produces.
57+
boolean generated = true;
58+
while (generated) {
59+
generated = false;
60+
for (ImFunctionCall c : collectVarargCalls()) {
61+
if (c.getFunc().hasFlag(IS_VARARG) && shouldSpecialise(c) && !forwardsAVarargParameter(c.getArguments())
62+
&& !varargFuncs.contains(c.getFunc(), c.getArguments().size())) {
63+
generateVarargFunc(c);
64+
generated = true;
65+
}
66+
}
67+
if (luaTarget) {
68+
// The Lua backend already turns a method call with exactly one possible
69+
// implementation into a direct call of that implementation. Doing the same here for
70+
// vararg methods is what lets ArrayList.add and friends get a fixed-arity copy at
71+
// all: on this target the call is still an ImMethodCall when varargs are eliminated.
72+
for (ImMethodCall c : collectMonomorphicVarargMethodCalls()) {
73+
ImFunction implementation = c.getMethod().getImplementation();
74+
List<ImExpr> arguments = receiverAndArguments(c);
75+
if (shouldSpecialise(arguments) && !forwardsAVarargParameter(arguments)
76+
&& !varargFuncs.contains(implementation, arguments.size())) {
77+
generateVarargFunc(implementation, arguments, c);
78+
generated = true;
79+
}
80+
}
3781
}
3882
}
3983

40-
// remove original vararg functions:
41-
prog.getFunctions().removeIf(f -> f.hasFlag(IS_VARARG));
84+
if (!luaTarget) {
85+
// remove original vararg functions:
86+
prog.getFunctions().removeIf(f -> f.hasFlag(IS_VARARG));
87+
}
4288

4389
// rewrite calls to use new functions:
4490
// (need to collect vararg calls again, because first phase can create copies of calls)
4591
for (ImFunctionCall call : collectVarargCalls()) {
46-
redirectCall(call, varargFuncs.get(call.getFunc(), call.getArguments().size()));
92+
ImFunction newFunc = varargFuncs.get(call.getFunc(), call.getArguments().size());
93+
if (newFunc != null && !forwardsAVarargParameter(call.getArguments())) {
94+
redirectCall(call, newFunc);
95+
}
96+
}
97+
if (luaTarget) {
98+
for (ImMethodCall call : collectMonomorphicVarargMethodCalls()) {
99+
ImFunction implementation = call.getMethod().getImplementation();
100+
ImFunction newFunc = varargFuncs.get(implementation, 1 + call.getArguments().size());
101+
if (newFunc != null && !forwardsAVarargParameter(receiverAndArguments(call))) {
102+
redirectMethodCall(call, newFunc);
103+
}
104+
}
105+
}
106+
}
107+
108+
109+
/**
110+
* Whether a call passes a vararg placeholder straight through, which is what the generated
111+
* `new_C` wrapper of a vararg constructor does with its own parameter. The placeholder is a
112+
* single node standing for however many arguments the caller actually passed, so the call's node
113+
* count is not an arity: specialising by it would produce a fixed-arity callee and drop every
114+
* argument after the first.
115+
*
116+
* <p>Only reachable on Lua. The forwarding call lives in the body of a vararg original, and a
117+
* copy has its placeholder expanded into real parameters before anything looks at it again, so
118+
* this matches only originals - which Jass removes and Lua retains.
119+
*
120+
* <p>Both the generation and the rewrite loop consult this. Skipping generation alone would not
121+
* be enough: another call could have produced a copy at the same node count, and the rewrite
122+
* would then redirect the forwarding call to it.
123+
*/
124+
private static boolean forwardsAVarargParameter(List<ImExpr> arguments) {
125+
for (ImExpr argument : arguments) {
126+
if (argument instanceof ImVarAccess access && isVarargPlaceholder(access.getVar())) {
127+
return true;
128+
}
129+
}
130+
return false;
131+
}
132+
133+
/** The trailing parameter of a function still marked vararg, as opposed to a local or a copy's. */
134+
private static boolean isVarargPlaceholder(ImVar variable) {
135+
if (variable.getParent() == null
136+
|| !(variable.getParent().getParent() instanceof ImFunction function)
137+
|| !function.hasFlag(IS_VARARG)) {
138+
return false;
139+
}
140+
List<ImVar> parameters = function.getParameters();
141+
return !parameters.isEmpty() && parameters.get(parameters.size() - 1) == variable;
142+
}
143+
/** A method call which can only ever reach one implementation, and that implementation is vararg. */
144+
private Collection<ImMethodCall> collectMonomorphicVarargMethodCalls() {
145+
final Collection<ImMethodCall> calls = new ArrayList<>();
146+
prog.accept(new ImProg.DefaultVisitor() {
147+
@Override
148+
public void visit(ImMethodCall c) {
149+
super.visit(c);
150+
ImMethod method = c.getMethod();
151+
if (method != null && !method.getIsAbstract() && method.getImplementation() != null
152+
&& method.getSubMethods().isEmpty() && method.getImplementation().hasFlag(IS_VARARG)) {
153+
calls.add(c);
154+
}
155+
}
156+
});
157+
return calls;
158+
}
159+
160+
/** The implementation's argument list: the receiver is its first parameter. */
161+
private static List<ImExpr> receiverAndArguments(ImMethodCall call) {
162+
List<ImExpr> arguments = new ArrayList<>(1 + call.getArguments().size());
163+
arguments.add(call.getReceiver());
164+
arguments.addAll(call.getArguments());
165+
return arguments;
166+
}
167+
168+
private void redirectMethodCall(ImMethodCall call, ImFunction newFunc) {
169+
ImExprs args = JassIm.ImExprs(call.getReceiver().copy());
170+
args.addAll(call.getArguments().removeAll());
171+
call.replaceBy(JassIm.ImFunctionCall(call.getTrace(), newFunc,
172+
JassIm.ImTypeArguments(call.getTypeArguments().removeAll()), args,
173+
call.getTuplesEliminated(), CallType.NORMAL));
174+
}
175+
176+
/** Whether a call gets a fixed-arity copy. Always on Jass; on Lua only within the parameter bound. */
177+
private boolean shouldSpecialise(ImFunctionCall call) {
178+
return shouldSpecialise(call.getArguments());
179+
}
180+
181+
/**
182+
* Counted after tuple flattening, because that is what the emitted parameter list costs: twenty
183+
* four-field tuples are eighty parameters, not twenty.
184+
*/
185+
private boolean shouldSpecialise(List<ImExpr> arguments) {
186+
if (!luaTarget) {
187+
return true;
188+
}
189+
int parameters = 0;
190+
for (ImExpr argument : arguments) {
191+
parameters += ImHelper.flattenedJassArity(argument.attrTyp());
192+
if (parameters > LUA_MAX_SPECIALISED_VARARG_PARAMETERS) {
193+
return false;
194+
}
47195
}
196+
return true;
48197
}
49198

50199
@NotNull
@@ -70,13 +219,17 @@ public void visit(ImFunctionCall c) {
70219
* for the function call.
71220
*/
72221
private void generateVarargFunc(ImFunctionCall sourceCall) {
73-
ImFunction func = sourceCall.getFunc();
74-
int numberOfParams = sourceCall.getArguments().size();
75-
int jassParameterCount = sourceCall.getArguments().stream()
222+
generateVarargFunc(sourceCall.getFunc(), sourceCall.getArguments(), sourceCall);
223+
}
224+
225+
/** {@code arguments} are in the callee's parameter order, so for a method they start with the receiver. */
226+
private void generateVarargFunc(ImFunction func, List<ImExpr> arguments, Element trace) {
227+
int numberOfParams = arguments.size();
228+
int jassParameterCount = arguments.stream()
76229
.mapToInt(argument -> ImHelper.flattenedJassArity(argument.attrTyp()))
77230
.sum();
78-
if (jassParameterCount > JASS_MAX_PARAMETERS) {
79-
throw new CompileError(sourceCall, "Vararg call would generate " + jassParameterCount
231+
if (!luaTarget && jassParameterCount > JASS_MAX_PARAMETERS) {
232+
throw new CompileError(trace, "Vararg call would generate " + jassParameterCount
80233
+ " Jass parameters; the maximum is " + JASS_MAX_PARAMETERS
81234
+ ". Use multiple calls (for example with the cascade operator) or pass a collection instead.");
82235
}
@@ -91,6 +244,31 @@ private void generateVarargFunc(ImFunctionCall sourceCall) {
91244

92245
// Create new function
93246
ImFunction newFunc = ReferenceRewritingCopy.copy(func);
247+
// ReferenceRewritingCopy retargets the function's own references - both call and reference
248+
// nodes - so inside the copy they now name the copy. That is wrong for either kind. A
249+
// recursive call must go back to naming the vararg original, so the rewrite below maps it to
250+
// a copy of its own arity like any other call; a self reference must name the original too,
251+
// because it is invoked at an arity this pass never sees.
252+
newFunc.accept(new Element.DefaultVisitor() {
253+
@Override
254+
public void visit(ImFunctionCall call) {
255+
super.visit(call);
256+
if (call.getFunc() == newFunc) {
257+
call.setFunc(func);
258+
}
259+
}
260+
261+
@Override
262+
public void visit(ImFuncRef ref) {
263+
super.visit(ref);
264+
// Lua only: nothing redirects a reference afterwards, so it keeps naming whatever it
265+
// is set to here, and only this target retains the original. On Jass the original is
266+
// removed below and pointing at it would leave the reference dangling.
267+
if (luaTarget && ref.getFunc() == newFunc) {
268+
ref.setFunc(func);
269+
}
270+
}
271+
});
94272
newFunc.setName(func.getName() + "_" + argumentSize);
95273
// replace vararg with special parameters:
96274
ImVar varargParam = newFunc.getParameters().remove(newFunc.getParameters().size() - 1);
@@ -131,16 +309,23 @@ public void visit(ImVarargLoop imLoop) {
131309
params.addAll(list);
132310

133311
// generate function for this new call
134-
generateVarargFunc(call);
312+
if (shouldSpecialise(call)) {
313+
generateVarargFunc(call);
314+
}
135315
}
136316

137317

138-
// Remove vararg flag
318+
// Drop the vararg flag, and on Lua the name preservation with it. A preserved name is part
319+
// of the map's Warcraft-facing API and belongs to the retained original, which is what
320+
// external code calls at an arity this pass never sees. Since a copy shares the original's
321+
// trace, and LuaTranslator.collectPredefinedNames() resets every preserved function to its
322+
// trace's source name, an inherited flag would emit both under one name.
139323
List<FunctionFlag> list = new ArrayList<>();
140324
for (FunctionFlag flag : newFunc.getFlags()) {
141-
if (flag != IS_VARARG) {
142-
list.add(flag);
325+
if (flag == IS_VARARG || (luaTarget && flag == PRESERVE_NAME)) {
326+
continue;
143327
}
328+
list.add(flag);
144329
}
145330
newFunc.setFlags(list);
146331
// Add new function to prog
@@ -166,7 +351,13 @@ public void visit(ImVarAccess va) {
166351

167352
private void redirectCall(ImFunctionCall call, ImFunction newFunc) {
168353
// Redirect call to new function
169-
ImFunctionCall newCall = JassIm.ImFunctionCall(call.getTrace(), newFunc, JassIm.ImTypeArguments(), JassIm.ImExprs(call.getArguments().removeAll()), call.getTuplesEliminated(), call.getCallType());
354+
// Carry the type arguments over rather than assuming there are none. Jass erases generics
355+
// long before this pass, so an empty list was always right there; on Lua the erasure happens
356+
// elsewhere and this list is empty in practice too, but rebuilding the call should not be
357+
// the step that decides that.
358+
ImFunctionCall newCall = JassIm.ImFunctionCall(call.getTrace(), newFunc,
359+
JassIm.ImTypeArguments(call.getTypeArguments().removeAll()),
360+
JassIm.ImExprs(call.getArguments().removeAll()), call.getTuplesEliminated(), call.getCallType());
170361
call.replaceBy(newCall);
171362
}
172363

0 commit comments

Comments
 (0)