1313import java .util .stream .Collectors ;
1414
1515import 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
2122public 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