Skip to content

Commit 396c21d

Browse files
committed
Map recursive vararg calls to their own arity, and bound by flattened parameters
Two defects in the Lua vararg elimination, both found in review. A copy retargets the function's own references, so a recursive call inside the two-argument copy named that copy. Whenever the recursion uses a different argument count that is wrong: a call of f(7) inside the two-parameter clone invoked itself with a parameter missing. Self-calls now go back to naming the vararg original, and copies are generated to a fixpoint, so a recursive call at an arity nothing else needed still gets a copy of its own. The Lua bound counted source arguments, but the emitted parameter list costs flattened ones: twenty four-field tuples are eighty parameters, past what the target accepts and past what the locals-table fallback can spill, since that cannot spill formal parameters. The bound is now counted after tuple flattening and the constant says parameters rather than arity. A third report, a vararg parameter forwarded into a method call, is not reachable: a vararg function may have only the one parameter, so a receiver cannot be a second one, and the argument does not type as the element type. Both halves are pinned by a test, since the forwarding branch is otherwise only reached from calls this pass generated itself.
1 parent e2dde9b commit 396c21d

2 files changed

Lines changed: 138 additions & 24 deletions

File tree

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

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ public class VarargEliminator {
2222

2323
private static final int JASS_MAX_PARAMETERS = ImHelper.JASS_MAX_PARAMETERS;
2424
/**
25-
* Largest vararg arity given a fixed-arity copy on Lua. Lua caps a function at 200 locals
26-
* including parameters; this leaves room for the body's own locals. A call above it keeps the
27-
* original `...` function, which is always still present on that target.
25+
* Largest number of emitted parameters a fixed-arity copy may have on Lua, counted after tuple
26+
* flattening: one four-field tuple argument is four parameters, so an arity that looks modest in
27+
* source can exceed what the target accepts. Lua caps a function at 200 locals including its
28+
* parameters, and the locals-table fallback cannot spill parameters, so this leaves room for the
29+
* body's own locals. A call above it keeps the original `...` function, which is always still
30+
* present on that target.
2831
*/
29-
public static final int LUA_MAX_SPECIALISED_VARARG_ARITY = 64;
32+
public static final int LUA_MAX_SPECIALISED_VARARG_PARAMETERS = 64;
3033
private final ImProg prog;
3134
/**
3235
* On Lua classes are still present when this runs, so a vararg function can also be reached
@@ -47,22 +50,32 @@ public VarargEliminator(ImProg prog, boolean luaTarget) {
4750
}
4851

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

126-
/** Whether a call gets a fixed-arity copy. Always on Jass; on Lua only up to the arity bound. */
139+
/** Whether a call gets a fixed-arity copy. Always on Jass; on Lua only within the parameter bound. */
127140
private boolean shouldSpecialise(ImFunctionCall call) {
128-
return shouldSpecialise(call.getFunc(), call.getArguments().size());
141+
return shouldSpecialise(call.getArguments());
129142
}
130143

131-
private boolean shouldSpecialise(ImFunction func, int totalArguments) {
144+
/**
145+
* Counted after tuple flattening, because that is what the emitted parameter list costs: twenty
146+
* four-field tuples are eighty parameters, not twenty.
147+
*/
148+
private boolean shouldSpecialise(List<ImExpr> arguments) {
132149
if (!luaTarget) {
133150
return true;
134151
}
135-
int varargCount = 1 + totalArguments - func.getParameters().size();
136-
return varargCount <= LUA_MAX_SPECIALISED_VARARG_ARITY;
152+
int parameters = 0;
153+
for (ImExpr argument : arguments) {
154+
parameters += ImHelper.flattenedJassArity(argument.attrTyp());
155+
if (parameters > LUA_MAX_SPECIALISED_VARARG_PARAMETERS) {
156+
return false;
157+
}
158+
}
159+
return true;
137160
}
138161

139162
@NotNull
@@ -184,6 +207,19 @@ private void generateVarargFunc(ImFunction func, List<ImExpr> arguments, Element
184207

185208
// Create new function
186209
ImFunction newFunc = ReferenceRewritingCopy.copy(func);
210+
// The copy retargets the function's own references, so a recursive call inside it now names
211+
// the copy. That is wrong whenever the recursion uses a different argument count: the call
212+
// must go back to naming the vararg original, so the rewrite below maps it to a copy of its
213+
// own arity like any other call.
214+
newFunc.accept(new Element.DefaultVisitor() {
215+
@Override
216+
public void visit(ImFunctionCall call) {
217+
super.visit(call);
218+
if (call.getFunc() == newFunc) {
219+
call.setFunc(func);
220+
}
221+
}
222+
});
187223
newFunc.setName(func.getName() + "_" + argumentSize);
188224
// replace vararg with special parameters:
189225
ImVar varargParam = newFunc.getParameters().remove(newFunc.getParameters().size() - 1);

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2123,6 +2123,84 @@ public void virtuallyDispatchedVarargMethodKeepsThePackedPath() throws IOExcepti
21232123
);
21242124
}
21252125

2126+
/**
2127+
* A vararg function may call itself with a different static arity. Copying the function for one
2128+
* arity must not retarget that inner call to the copy, which has the wrong parameter count; the
2129+
* call has to be mapped to its own arity like every other call.
2130+
*/
2131+
@Test
2132+
public void recursiveVarargCallsAreMappedToTheirOwnArity() throws IOException {
2133+
test().testLua(true).executeProg().lines(
2134+
"package Test",
2135+
"native testSuccess()",
2136+
"function depth(vararg int xs) returns int",
2137+
" var n = 0",
2138+
" for x in xs",
2139+
" n++",
2140+
" if n == 3",
2141+
" return 100 + depth(1, 2)",
2142+
" if n == 2",
2143+
" return 10 + depth(1)",
2144+
" return n",
2145+
"init",
2146+
" if depth(1, 2, 3) == 111 and depth(5) == 1 and depth() == 0",
2147+
" testSuccess()"
2148+
);
2149+
}
2150+
2151+
/**
2152+
* The Lua arity bound is about emitted parameters, which tuple elimination multiplies: twenty
2153+
* four-field tuples are eighty formal parameters. Such a call keeps the packed path rather than
2154+
* emitting a function Lua refuses to load.
2155+
*/
2156+
@Test
2157+
public void wideTupleVarargCallCountsFlattenedParametersAgainstTheLuaBound() throws IOException {
2158+
StringBuilder args = new StringBuilder();
2159+
int n = 20;
2160+
for (int i = 1; i <= n; i++) {
2161+
if (i > 1) {
2162+
args.append(", ");
2163+
}
2164+
args.append("quad(").append(i).append(", 0, 0, 1)");
2165+
}
2166+
test().testLua(true).executeProg().lines(
2167+
"package Test",
2168+
"native testSuccess()",
2169+
"tuple quad(int a, int b, int c, int d)",
2170+
"@noinline function sumFirst(vararg quad qs) returns int",
2171+
" var total = 0",
2172+
" for q in qs",
2173+
" total += q.a + q.d",
2174+
" return total",
2175+
"init",
2176+
" if sumFirst(" + args + ") == " + (n * (n + 1) / 2 + n) + " and sumFirst(quad(1, 2, 3, 4)) == 5",
2177+
" testSuccess()"
2178+
);
2179+
String compiled = compiledLua("wideTupleVarargCallCountsFlattenedParametersAgainstTheLuaBound");
2180+
assertTrue("eighty flattened parameters must keep the packed original:\n" + compiled,
2181+
compiled.contains("table.pack"));
2182+
}
2183+
2184+
/**
2185+
* A vararg parameter cannot be passed on, to a function or to a method. Both halves matter here:
2186+
* a vararg function may have only the one parameter, so a receiver cannot be a second one, and
2187+
* the argument itself does not type as the element type. The eliminator's forwarding branch is
2188+
* therefore reachable only from calls it generated itself, which are always ImFunctionCall.
2189+
*/
2190+
@Test
2191+
public void varargParameterCannotBeForwardedToAMethod() {
2192+
testAssertErrorsLines(false, "Found vararg integer",
2193+
"package Test",
2194+
"class Sink",
2195+
" static Sink instance = null",
2196+
" function consume(vararg int xs)",
2197+
" skip",
2198+
"function relay(vararg int xs)",
2199+
" Sink.instance.consume(xs)"
2200+
);
2201+
}
2202+
2203+
21262204
@Test
21272205
public void localPlayerTaintFlowsThroughVarargLoopValues() {
21282206
String compiled = compileOptimizedLua(

0 commit comments

Comments
 (0)