55import de .peeeq .wurstscript .WLogger ;
66import de .peeeq .wurstscript .ast .ClassDef ;
77import de .peeeq .wurstscript .ast .ConstructorDef ;
8+ import de .peeeq .wurstscript .ast .ExprClosure ;
89import de .peeeq .wurstscript .ast .InterfaceDef ;
910import de .peeeq .wurstscript .ast .PackageOrGlobal ;
1011import de .peeeq .wurstscript .ast .WPackage ;
@@ -188,6 +189,18 @@ public void visit(ImMethodCall call) {
188189 super .visit (call );
189190 collectGenericNewUse (call );
190191 }
192+
193+ @ Override
194+ public void visit (ImAlloc alloc ) {
195+ super .visit (alloc );
196+ collectGenericNewUse (alloc );
197+ }
198+
199+ @ Override
200+ public void visit (ImMemberAccess memberAccess ) {
201+ super .visit (memberAccess );
202+ collectGenericNewUse (memberAccess );
203+ }
191204 });
192205 }
193206
@@ -204,6 +217,18 @@ public void visit(ImMethodCall call) {
204217 super .visit (call );
205218 collectGenericNewUse (call );
206219 }
220+
221+ @ Override
222+ public void visit (ImAlloc alloc ) {
223+ super .visit (alloc );
224+ collectGenericNewUse (alloc );
225+ }
226+
227+ @ Override
228+ public void visit (ImMemberAccess memberAccess ) {
229+ super .visit (memberAccess );
230+ collectGenericNewUse (memberAccess );
231+ }
207232 });
208233 }
209234
@@ -225,6 +250,106 @@ && functionNeedsSpecialization(call.getFunc(), Collections.newSetFromMap(new Ide
225250 }
226251 }
227252
253+ /**
254+ * A construction states an instantiation that no call site has to mention. A closure is the case
255+ * that needs it: its class is built from the enclosing type variables and reached through its
256+ * interface, so the call carries no type arguments at all and only the allocation knows what the
257+ * body dispatches on. Restricted to classes that actually dispatch on a bound, so this stays a
258+ * targeted specialisation rather than general monomorphisation on Lua.
259+ */
260+ private void collectGenericNewUse (ImAlloc alloc ) {
261+ ImClassType clazz = alloc .getClazz ();
262+ if (clazz .getTypeArguments ().isEmpty ()
263+ || typeArgumentsContainTypeVariable (clazz .getTypeArguments ())
264+ || !isConstructionOnlyInstantiation (clazz .getClassDef ())) {
265+ return ;
266+ }
267+ genericsUses .add (new GenericClazzUse (alloc ));
268+ }
269+
270+ /**
271+ * Whether the construction is the only place a class's instantiation is stated.
272+ * <p>
273+ * A class the user writes is used through calls that carry its type arguments, and those already
274+ * specialise what they need onto the erased class. A closure has no such call: it is reached
275+ * through the interface it implements, which is not generic, so the allocation is the only thing
276+ * that knows what the body dispatches on. Widening this beyond that case makes the two
277+ * mechanisms disagree — the object comes from the specialised class while its methods were bound
278+ * to the erased one.
279+ */
280+ private boolean isConstructionOnlyInstantiation (ImClass classDef ) {
281+ return classDef .attrTrace () instanceof ExprClosure closure
282+ && !isInsideAnotherClosure (closure )
283+ && classReachesDispatch (classDef );
284+ }
285+
286+ /**
287+ * A closure written inside another one is left alone.
288+ * <p>
289+ * Its captured environment is reached through a receiver belonging to the enclosing closure,
290+ * which by then has been specialised itself, and specialising the owner again with what is
291+ * left over fails inside the rewrite. Supporting that is a further step; until it is taken,
292+ * saying the bound could not be resolved - which is what happens without any of this - is
293+ * better than an error about generics of the wrong size.
294+ */
295+ private static boolean isInsideAnotherClosure (ExprClosure closure ) {
296+ de .peeeq .wurstscript .ast .Element parent = closure .getParent ();
297+ return parent != null && parent .attrNearestExprClosure () != null ;
298+ }
299+
300+ /**
301+ * Whether anything the class does ends in a dispatch on a bound, including through the
302+ * functions it calls. `classNeedsSpecialization` asks only whether a dispatch sits in the class
303+ * itself, which is the wrong question here: a closure whose body is `() -> helper(x)` has no
304+ * dispatch of its own, and the instantiation it needs is still only known at its construction.
305+ * That question is kept as it is, because widening it would change what gets specialised on
306+ * paths that have nothing to do with closures.
307+ */
308+ private boolean classReachesDispatch (ImClass classDef ) {
309+ for (ImFunction f : classDef .getFunctions ()) {
310+ if (functionNeedsSpecialization (f , Collections .newSetFromMap (new IdentityHashMap <>()),
311+ Collections .newSetFromMap (new IdentityHashMap <>()))) {
312+ return true ;
313+ }
314+ }
315+ for (ImMethod m : classDef .getMethods ()) {
316+ if (m .getImplementation () != null
317+ && functionNeedsSpecialization (m .getImplementation (),
318+ Collections .newSetFromMap (new IdentityHashMap <>()),
319+ Collections .newSetFromMap (new IdentityHashMap <>()))) {
320+ return true ;
321+ }
322+ }
323+ return false ;
324+ }
325+
326+ /**
327+ * A field of a class specialised from a construction has to be reached on the copy. The write
328+ * that captures a closure's environment is the case that needs it: it names the field of the
329+ * generic class, which nothing allocates any more once the construction was redirected.
330+ */
331+ private void collectGenericNewUse (ImMemberAccess memberAccess ) {
332+ ImVar field = memberAccess .getVar ();
333+ if (field .getParent () == null || !(field .getParent ().getParent () instanceof ImClass owningClass )) {
334+ return ;
335+ }
336+ // A class that has already been specialised has nothing left to select, and asking the
337+ // receiver to adapt to it fails outright: the receiver is still typed by the generic class
338+ // the specialised one was copied from, which is not a superclass of it.
339+ if (owningClass .getTypeVariables ().isEmpty () || !isConstructionOnlyInstantiation (owningClass )) {
340+ return ;
341+ }
342+ if (memberAccess .getTypeArguments ().isEmpty ()) {
343+ // The access names a field, not an instantiation; the receiver is what knows which one.
344+ addMemberTypeArguments (memberAccess , owningClass );
345+ }
346+ if (memberAccess .getTypeArguments ().isEmpty ()
347+ || typeArgumentsContainTypeVariable (memberAccess .getTypeArguments ())) {
348+ return ;
349+ }
350+ genericsUses .add (new GenericMemberAccess (memberAccess ));
351+ }
352+
228353 private void collectGenericNewUse (ImMethodCall call ) {
229354 if (specializedCallSites .contains (call )) {
230355 return ;
@@ -1316,12 +1441,56 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) {
13161441 // NEW: Create specialized global variables for this class instantiation
13171442 createSpecializedGlobals (c , generics , typeVars );
13181443
1444+ if (genericNewOnly && isConstructionOnlyInstantiation (c )) {
1445+ attachSpecializedClassMethods (c , newC , generics );
1446+ }
13191447
13201448 onSpecializedClassTriggers .get (c ).forEach (consumer ->
13211449 consumer .accept (generics , newC ));
13221450 return newC ;
13231451 }
13241452
1453+ /**
1454+ * Makes the methods of a class specialised from a construction reachable.
1455+ * <p>
1456+ * A class specialised because a call named its instantiation is reached through that call.
1457+ * One specialised because it was constructed is not: the receiver is held as its interface, so
1458+ * dispatch goes through the root method, whose submethods still list only the generic original.
1459+ * Each copy is bound to the same roots, and the original's implementation is recorded as having
1460+ * a specialisation so the dispatch left behind in it settles instead of reaching the backend.
1461+ */
1462+ private void attachSpecializedClassMethods (ImClass original , ImClass specialized , GenericTypes generics ) {
1463+ List <ImMethod > originalMethods = original .getMethods ();
1464+ List <ImMethod > specializedMethods = specialized .getMethods ();
1465+ if (originalMethods .size () != specializedMethods .size ()) {
1466+ // The copy is structural, so this cannot happen; bail rather than pair the wrong ones.
1467+ return ;
1468+ }
1469+ Map <ImMethod , ImMethod > specializationOf = new IdentityHashMap <>();
1470+ for (int i = 0 ; i < originalMethods .size (); i ++) {
1471+ ImMethod copy = specializedMethods .get (i );
1472+ copy .setMethodClass (JassIm .ImClassType (specialized , JassIm .ImTypeArguments ()));
1473+ specializationOf .put (originalMethods .get (i ), copy );
1474+
1475+ ImFunction implementation = originalMethods .get (i ).getImplementation ();
1476+ ImFunction copyImplementation = copy .getImplementation ();
1477+ if (implementation != null && copyImplementation != null && implementation != copyImplementation
1478+ && specializedFunctions .get (implementation , generics ) == null ) {
1479+ specializedFunctions .put (implementation , generics , copyImplementation );
1480+ }
1481+ }
1482+ for (ImClass c : new ArrayList <>(prog .getClasses ())) {
1483+ for (ImMethod root : c .getMethods ()) {
1484+ for (ImMethod sub : new ArrayList <>(root .getSubMethods ())) {
1485+ ImMethod copy = specializationOf .get (sub );
1486+ if (copy != null && !root .getSubMethods ().contains (copy )) {
1487+ root .getSubMethods ().add (copy );
1488+ }
1489+ }
1490+ }
1491+ }
1492+ }
1493+
13251494 private ImExpr rewriteGenericGlobalsInExpr (ImExpr e , ImClass owningClass , GenericTypes generics ) {
13261495 e .accept (new Element .DefaultVisitor () {
13271496 @ Override public void visit (ImVarAccess va ) {
0 commit comments