diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index 5043a07f4..105d4520f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -875,10 +875,11 @@ public LuaCompilationUnit transformProgToLua() { ImAttrType.setWurstClassType(null); int stage; boolean specializeTupleValueTypes = containsTupleTypeArgument(); - if (containsGenericNewCall() || containsTypeClassDispatch() || specializeTupleValueTypes) { + EliminateGenerics luaGenerics = new EliminateGenerics(getImTranslator(), getImProg()); + if (containsGenericNewCall() || containsTypeClassDispatch() || specializeTupleValueTypes + || luaGenerics.hasGenericStatics()) { beginPhase(2, "Specialize generics for Lua-only concrete operations"); - new EliminateGenerics(getImTranslator(), getImProg()) - .transformGenericNewOnly(specializeTupleValueTypes); + luaGenerics.transformGenericNewOnly(specializeTupleValueTypes); // Remove phantom erased initialization before optimization can preserve only its side // effect. A specialized static owns its copied initializer unless the erased static is live. RemoveGarbage.removePhantomGenericStaticInitializers(getImProg(), getImTranslator()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index 89563be53..0b04c2f54 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -40,6 +40,10 @@ public class EliminateGenerics { * has them re-derived from its receiver, which would collect and specialise it again forever. */ private final Set specializedCallSites = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set recordedErasedStaticAllocations = + Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set scannedFixedStaticCallees = + Collections.newSetFromMap(new IdentityHashMap<>()); private final Table specializedFunctions = HashBasedTable.create(); /** The class each function was moved out of, for calls which name their target without a receiver. */ private final Map functionOwners = new IdentityHashMap<>(); @@ -59,6 +63,8 @@ private record RuntimeTypeUse(ImClass clazz, GenericTypes generics) { // NEW: Track specialized global variables for generic static fields // Key: (original generic global var, concrete type instantiation) -> specialized var private final Table specializedGlobals = HashBasedTable.create(); + /** Last specialized initializer emitted for each original initializer, preserving discovery order. */ + private final Map specializedInitializerTails = new IdentityHashMap<>(); // NEW: Track which global vars belong to which generic class // This helps us know which globals need specialization @@ -127,10 +133,11 @@ public void transformGenericNewOnly() { public void transformGenericNewOnly(boolean specializeTupleValueTypes) { genericNewOnly = true; this.specializeTupleValueTypes = specializeTupleValueTypes; - if (specializeTupleValueTypes) { + identifyGenericGlobals(); + if (specializeTupleValueTypes || !globalToClass.isEmpty()) { addMemberTypeArguments(); - identifyGenericGlobals(); } + indexGenericGlobalUses(); collectUnspecializedGenericClassMethods(); // Specialising a constructor makes its result type concrete, which is what lets a method // call on that result resolve. Repeat until a pass finds nothing new; collection is @@ -151,6 +158,11 @@ public void transformGenericNewOnly(boolean specializeTupleValueTypes) { settleRemainingDispatches(); } + public boolean hasGenericStatics() { + identifyGenericGlobals(); + return !globalToClass.isEmpty(); + } + /** * Moves a specialisation's methods to the class its objects are actually allocated from. *

@@ -459,6 +471,7 @@ private void collectGenericNewUse(ImFunctionCall call) { } return; } + recordErasedConstructorAllocation(call); if (!call.getTypeArguments().isEmpty() && (shouldSpecializeTupleArguments(call.getTypeArguments()) || needsRuntimeTypeSpecialization(call) @@ -470,7 +483,59 @@ private void collectGenericNewUse(ImFunctionCall call) { } if (call.getTypeArguments().isEmpty()) { collectCallThroughGenericReceiver(call); + } else if (!typeArgumentsContainTypeVariable(call.getTypeArguments()) + && !(call.getFunc().getTrace() instanceof ConstructorDef)) { + // The generic callee remains erased, so its body is skipped by collectGenericNewRoots. + // Fixed concrete allocations inside it still name real per-instantiation statics and + // must be registered without cloning the caller for unrelated type arguments. + recordFixedErasedStaticAllocations(call.getFunc()); + } + } + + private void recordFixedErasedStaticAllocations(ImFunction function) { + if (!scannedFixedStaticCallees.add(function)) { + return; + } + function.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImFunctionCall nestedCall) { + super.visit(nestedCall); + collectGenericNewUse(nestedCall); + } + + @Override + public void visit(ImMethodCall nestedCall) { + super.visit(nestedCall); + collectGenericNewUse(nestedCall); + } + }); + } + + private void recordErasedConstructorAllocation(ImFunctionCall call) { + if (call.getTypeArguments().isEmpty() + || typeArgumentsContainTypeVariable(call.getTypeArguments()) + || !(call.getFunc().getTrace() instanceof ConstructorDef) + || !(call.getFunc().getReturnType() instanceof ImClassType) + || shouldSpecializeTupleArguments(call.getTypeArguments()) + || needsRuntimeTypeSpecialization(call)) { + return; + } + ImClass owner = classOwning(call.getFunc()); + if (owner != null && classOwnsGenericGlobals(owner) + && !functionNeedsSpecialization(call.getFunc(), + Collections.newSetFromMap(new IdentityHashMap<>()))) { + recordErasedStaticInstantiation(call, owner, call.getTypeArguments()); + } + } + + private void recordErasedStaticInstantiation(Element site, ImClass owner, + List typeArguments) { + if (!recordedErasedStaticAllocations.add(site)) { + return; } + GenericTypes generics = new GenericTypes(typeArguments); + translator.recordErasedGenericAllocation(owner, typeArguments); + genericsUses.add(() -> specializeClass(owner, generics)); } /** @@ -565,10 +630,15 @@ private void collectCallThroughGenericReceiver(ImFunctionCall call) { private void collectGenericNewUse(ImAlloc alloc) { ImClassType clazz = alloc.getClazz(); if (clazz.getTypeArguments().isEmpty() - || typeArgumentsContainTypeVariable(clazz.getTypeArguments()) - || (!shouldSpecializeTupleArguments(clazz.getTypeArguments()) - && !needsRuntimeTypeSpecialization(clazz) - && !isConstructionOnlyInstantiation(clazz.getClassDef()))) { + || typeArgumentsContainTypeVariable(clazz.getTypeArguments())) { + return; + } + if (!shouldSpecializeTupleArguments(clazz.getTypeArguments()) + && !needsRuntimeTypeSpecialization(clazz) + && !isConstructionOnlyInstantiation(clazz.getClassDef())) { + if (classOwnsGenericGlobals(clazz.getClassDef())) { + recordErasedStaticInstantiation(alloc, clazz.getClassDef(), clazz.getTypeArguments()); + } return; } genericsUses.add(new GenericClazzUse(alloc)); @@ -675,21 +745,25 @@ private void collectGenericNewUse(ImMethodCall call) { specializedCallSites.add(call); return; } - if (!shouldSpecializeTupleArguments(call.getTypeArguments()) - && !methodNeedsSpecialization(method, - Collections.newSetFromMap(new IdentityHashMap<>()), - Collections.newSetFromMap(new IdentityHashMap<>()))) { - return; - } if (isMissingClassTypeArguments(call, method)) { addMemberTypeArguments(call, method.attrClass()); } if (typeArgumentsContainTypeVariable(call.getTypeArguments())) { - // The receiver's declared type is still generic, which happens when the method is - // called straight on a freshly constructed value. The construction states the - // instantiation, so take the arguments from it. + // A call directly on a fresh generic construction gets its concrete class arguments + // from that construction before deciding between specialization and fixed-body scan. useConstructionTypeArguments(call); } + boolean needsSpecialization = methodNeedsSpecialization(method, + Collections.newSetFromMap(new IdentityHashMap<>()), + Collections.newSetFromMap(new IdentityHashMap<>())); + if (!shouldSpecializeTupleArguments(call.getTypeArguments()) && !needsSpecialization) { + if (!call.getTypeArguments().isEmpty() + && !typeArgumentsContainTypeVariable(call.getTypeArguments()) + && method.getImplementation() != null) { + recordFixedErasedStaticAllocations(method.getImplementation()); + } + return; + } if (!call.getTypeArguments().isEmpty() && !typeArgumentsContainTypeVariable(call.getTypeArguments())) { genericsUses.add(new GenericMethodCall(call)); @@ -764,12 +838,16 @@ private boolean functionNeedsSpecialization(ImFunction function, Set /** * Whether a function must be specialised even on Lua, which otherwise keeps generics erased. *

- * Two operations need the concrete type argument: constructing a value of it, and dispatching - * on a type class bound. Specialising these paths keeps a bounded generic as cheap on Lua as it - * is on Jass, at the cost of one copy per instantiation actually used. + * Concrete type arguments are needed when constructing a value of them, dispatching on a type + * class bound, or constructing a generic class whose static storage is per instantiation. + * Specialising these paths keeps a bounded generic as cheap on Lua as it is on Jass, at the cost + * of one copy per instantiation actually used. */ private boolean functionNeedsSpecialization(ImFunction function, Set visitedFunctions, Set visitedMethods) { + if (needsGlobalSpecialization(function)) { + return true; + } if (!visitedFunctions.add(function)) { return false; } @@ -803,8 +881,14 @@ public void visit(ImAlloc alloc) { @Override public void visit(ImFunctionCall call) { - if (translator.isGenericNewMarker(call.getFunc()) - || functionNeedsSpecialization(call.getFunc(), visitedFunctions, visitedMethods)) { + // Empty arguments may be supplied implicitly by the enclosing generic receiver. + // Only an explicit, already-concrete call is independent of the caller context. + boolean dependsOnCaller = call.getTypeArguments().isEmpty() + || typeArgumentsContainTypeVariable(call.getTypeArguments()); + if (constructsClassOwningGenericGlobals(function, call) + || translator.isGenericNewMarker(call.getFunc()) + || (dependsOnCaller + && functionNeedsSpecialization(call.getFunc(), visitedFunctions, visitedMethods))) { found[0] = true; return; } @@ -813,7 +897,10 @@ public void visit(ImFunctionCall call) { @Override public void visit(ImMethodCall call) { - if (methodNeedsSpecialization(call.getMethod(), visitedFunctions, visitedMethods)) { + boolean dependsOnCaller = call.getTypeArguments().isEmpty() + || typeArgumentsContainTypeVariable(call.getTypeArguments()); + if (dependsOnCaller + && methodNeedsSpecialization(call.getMethod(), visitedFunctions, visitedMethods)) { found[0] = true; return; } @@ -823,6 +910,28 @@ public void visit(ImMethodCall call) { return found[0]; } + /** + * A generic caller containing {@code new Box()} must be revisited after {@code T} becomes + * concrete so each constructed instantiation can register its own static storage. Detect the + * constructor call at the caller boundary; marking the constructor implementation itself would + * unnecessarily redirect ordinary objects away from Lua's erased representation. + */ + private boolean constructsClassOwningGenericGlobals(ImFunction enclosingFunction, + ImFunctionCall call) { + if (!(call.getFunc().getTrace() instanceof ConstructorDef) + || !typeArgumentsContainTypeVariable(call.getTypeArguments())) { + return false; + } + // A lowered constructor wrapper calls the class initializer carrying the same source + // ConstructorDef. That call implements the current allocation; it is not another generic + // allocation hidden inside this function and direct callers register it themselves. + if (enclosingFunction.getTrace() == call.getFunc().getTrace()) { + return false; + } + ImClass owner = classOwning(call.getFunc()); + return owner != null && classOwnsGenericGlobals(owner); + } + private boolean methodNeedsSpecialization(ImMethod method, Set visitedFunctions, Set visitedMethods) { if (!visitedMethods.add(method)) { @@ -988,6 +1097,29 @@ private boolean needsGlobalSpecialization(ImFunction f) { return o != null && !o.isEmpty(); } + private boolean classOwnsGenericGlobals(ImClass clazz) { + return classOwnsGenericGlobals(clazz, + Collections.newSetFromMap(new IdentityHashMap<>())); + } + + private boolean classOwnsGenericGlobals(ImClass clazz, Set visited) { + if (!visited.add(clazz)) { + return false; + } + ImClass canonical = translator.canonical(clazz); + for (ImClass owner : globalToClass.values()) { + if (translator.canonical(owner) == canonical) { + return true; + } + } + for (ImClassType superClass : clazz.getSuperClasses()) { + if (classOwnsGenericGlobals(superClass.getClassDef(), visited)) { + return true; + } + } + return false; + } + private ImFunction enclosingFunction(Element e) { Element cur = e; while (cur != null) { @@ -1005,6 +1137,22 @@ private void recordGenericGlobalUse(Element site, ImVar global) { ownersOf(f).add(owner); } + private void indexGenericGlobalUses() { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImVarAccess access) { + recordGenericGlobalUse(access, access.getVar()); + super.visit(access); + } + + @Override + public void visit(ImVarArrayAccess access) { + recordGenericGlobalUse(access, access.getVar()); + super.visit(access); + } + }); + } + private void dbgMethodsByName(String phase) { Map counts = new HashMap<>(); for (ImMethod m : prog.getMethods()) { @@ -1496,7 +1644,8 @@ private ImFunction specializeFunction(ImFunction f, GenericTypes generics) { rewriteGenerics(newF, generics, typeVars); } - if (genericNewOnly && specializeTupleValueTypes && genericTypesContainTuple(generics)) { + if (genericNewOnly && (needsGlobalSpecialization(f) + || (specializeTupleValueTypes && genericTypesContainTuple(generics)))) { ImClass owner = classOwning(f); if (owner != null && !owner.getTypeVariables().isEmpty()) { GenericTypes ownerGenerics = generics.take(owner.getTypeVariables().size()); @@ -1534,16 +1683,39 @@ public void visit(ImVarArrayAccess access) { private ImVar specializedGlobal(ImVar original) { ImClass globalOwner = globalToClass.get(original); - if (globalOwner == null - || translator.canonical(globalOwner) != translator.canonical(owner)) { + if (globalOwner == null) { + return original; + } + GenericTypes globalGenerics = adaptGenericsToOwner(owner, ownerGenerics, globalOwner); + if (globalGenerics == null) { return original; } - ImVar result = ensureSpecializedGlobal(original, globalOwner, ownerGenerics); + ImVar result = ensureSpecializedGlobal(original, globalOwner, globalGenerics); return result == null ? original : result; } }); } + /** Maps a concrete subclass instantiation onto the type arguments of a static's declaring class. */ + private @Nullable GenericTypes adaptGenericsToOwner(ImClass concreteOwner, + GenericTypes concreteGenerics, + ImClass declaringOwner) { + if (translator.canonical(concreteOwner) == translator.canonical(declaringOwner)) { + return concreteGenerics; + } + ImTypeArguments arguments = JassIm.ImTypeArguments(); + for (ImTypeArgument argument : concreteGenerics.getTypeArguments()) { + arguments.add(argument.copy()); + } + ImClassType adapted = adaptToSuperclass( + JassIm.ImClassType(concreteOwner, arguments), declaringOwner); + if (adapted == null || adapted.getTypeArguments().size() != declaringOwner.getTypeVariables().size() + || typeArgumentsContainTypeVariable(adapted.getTypeArguments())) { + return null; + } + return new GenericTypes(adapted.getTypeArguments()); + } + /** * creates a specialized version of this method */ @@ -1624,7 +1796,8 @@ private ImFunction specializeClassFunction(ImFunction function, ImClass owningCl newImplementation.getTypeVariables().removeAll(); newImplementation.setName(function.getName() + "_specialized"); rewriteGenerics(newImplementation, generics, typeVariables); - if (specializeTupleValueTypes && genericTypesContainTuple(generics)) { + if (needsGlobalSpecialization(function) + || (specializeTupleValueTypes && genericTypesContainTuple(generics))) { GenericTypes ownerGenerics = generics.take(owningClass.getTypeVariables().size()); specializeClass(owningClass, ownerGenerics); rewriteOwnedGenericGlobals(newImplementation, owningClass, ownerGenerics); @@ -1935,7 +2108,8 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { // NEW: Create specialized global variables for this class instantiation createSpecializedGlobals(c, generics, typeVars); - if (specializeTupleValueTypes && genericTypesContainTuple(generics)) { + if (genericNewOnly && (classOwnsGenericGlobals(c) + || (specializeTupleValueTypes && genericTypesContainTuple(generics)))) { rewriteOwnedGenericGlobals(newC, c, generics); } @@ -1998,7 +2172,7 @@ private ImExpr rewriteGenericGlobalsInExpr(ImExpr e, ImClass owningClass, Generi ImClass owner = globalToClass.get(v); if (owner == null) return; - GenericTypes g = normalizeToClassArity(generics, owner, "init-rhs"); + GenericTypes g = adaptGenericsToOwner(owningClass, generics, owner); if (g == null || g.containsTypeVariable()) return; ImVar sg = ensureSpecializedGlobal(v, owner, g); @@ -2011,7 +2185,7 @@ private ImExpr rewriteGenericGlobalsInExpr(ImExpr e, ImClass owningClass, Generi ImClass owner = globalToClass.get(v); if (owner == null) return; - GenericTypes g = normalizeToClassArity(generics, owner, "init-rhs"); + GenericTypes g = adaptGenericsToOwner(owningClass, generics, owner); if (g == null || g.containsTypeVariable()) return; ImVar sg = ensureSpecializedGlobal(v, owner, g); @@ -2115,10 +2289,14 @@ private void createSpecializedGlobals(ImClass originalClass, GenericTypes generi ImLExpr newLeft = specializeLhs.apply(origSet.getLeft()); ImSet specSet = JassIm.ImSet(originalGlobal.attrTrace(), newLeft, rhs); - // schedule insertion right after origSet in its parent ImStmts + // Append after earlier specializations of this initializer. Each invocation of + // createSpecializedGlobals has its own insertion batch; always inserting after + // origSet would therefore reverse specialization discovery/initializer order. + ImStmt insertionPoint = specializedInitializerTails.getOrDefault(origSet, origSet); IdentityHashMap> byStmt = insertsByParent.computeIfAbsent(parentStmts, k -> new IdentityHashMap<>()); - byStmt.computeIfAbsent(origSet, k -> new ArrayList<>(1)).add(specSet); + byStmt.computeIfAbsent(insertionPoint, k -> new ArrayList<>(1)).add(specSet); + specializedInitializerTails.put(origSet, specSet); // keep prog.getGlobalInits consistent, but do NOT reuse the tree-attached node elsewhere specializedInitsForMap.add((ImSet) specSet.copy()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java index 5c54dafb4..53ea41c67 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java @@ -1,6 +1,5 @@ package de.peeeq.wurstscript.translation.imtranslation; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import de.peeeq.wurstscript.jassIm.*; @@ -16,9 +15,6 @@ class GenericTypes { public GenericTypes(List typeArguments) { - for (ImTypeArgument ta : typeArguments) { - Preconditions.checkArgument(!EliminateGenerics.isGenericType(ta.getType()), "Type arguments must not be generic: " + typeArguments); - } this.typeArguments = ImmutableList.copyOf(typeArguments); } @@ -36,7 +32,7 @@ public boolean equals(Object o) { for (int i = 0; i < typeArguments.size(); i++) { ImTypeArgument t1 = typeArguments.get(i); ImTypeArgument t2 = ot.typeArguments.get(i); - if (!t1.getType().equalsType(t2.getType())) { + if (!equalTypeIgnoringBindings(t1.getType(), t2.getType())) { return false; } // Deliberately not comparing the type class binding. It is only a fast path for @@ -50,6 +46,60 @@ public boolean equals(Object o) { return false; } + /** + * Type-class bindings are dispatch metadata, not part of a specialization's structural type. + * Unlike the general IM type equality operation, this comparison therefore ignores bindings + * on every nested class-type argument, not just on the arguments wrapped by this key. + */ + private static boolean equalTypeIgnoringBindings(ImType left, ImType right) { + if (left instanceof ImArrayType) { + return right instanceof ImArrayType + && equalTypeIgnoringBindings(((ImArrayType) left).getEntryType(), + ((ImArrayType) right).getEntryType()); + } + if (left instanceof ImArrayTypeMulti) { + return right instanceof ImArrayTypeMulti + && equalTypeIgnoringBindings(((ImArrayTypeMulti) left).getEntryType(), + ((ImArrayTypeMulti) right).getEntryType()); + } + if (left instanceof ImTupleType) { + if (!(right instanceof ImTupleType)) { + return false; + } + ImTupleType leftTuple = (ImTupleType) left; + ImTupleType rightTuple = (ImTupleType) right; + if (leftTuple.getTypes().size() != rightTuple.getTypes().size()) { + return false; + } + for (int i = 0; i < leftTuple.getTypes().size(); i++) { + if (!equalTypeIgnoringBindings(leftTuple.getTypes().get(i), + rightTuple.getTypes().get(i))) { + return false; + } + } + return true; + } + if (left instanceof ImClassType) { + if (!(right instanceof ImClassType)) { + return false; + } + ImClassType leftClass = (ImClassType) left; + ImClassType rightClass = (ImClassType) right; + if (leftClass.getClassDef() != rightClass.getClassDef() + || leftClass.getTypeArguments().size() != rightClass.getTypeArguments().size()) { + return false; + } + for (int i = 0; i < leftClass.getTypeArguments().size(); i++) { + if (!equalTypeIgnoringBindings(leftClass.getTypeArguments().get(i).getType(), + rightClass.getTypeArguments().get(i).getType())) { + return false; + } + } + return true; + } + return left.equalsType(right); + } + @Override public int hashCode() { int res = 7; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index d5feebe23..454436cdc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -61,6 +61,7 @@ public record Specialisation(Element original, List typeArgument } private final Map specialisations = new IdentityHashMap<>(); + private final Map> erasedGenericAllocations = new IdentityHashMap<>(); /** * @param typeArguments the arguments the copy was made for, empty when a copy carries none of its @@ -99,6 +100,31 @@ public void recordGenericStaticOwner(ImVar global, ImClass owner) { return specialisations.get(copy); } + public void recordErasedGenericAllocation(ImClass clazz, List typeArguments) { + erasedGenericAllocations.computeIfAbsent(canonical(clazz), ignored -> new HashSet<>()) + .add(new GenericTypes(typeArguments)); + } + + public boolean hasErasedAllocationWithoutStaticSpecialization(ImClass clazz, ImVar originalStatic) { + Set allocations = erasedGenericAllocations.get(canonical(clazz)); + if (allocations == null || allocations.isEmpty()) { + return false; + } + Set specializedStatics = new HashSet<>(); + for (Map.Entry entry : specialisations.entrySet()) { + Specialisation specialization = entry.getValue(); + if (specialization.original() == originalStatic) { + specializedStatics.add(new GenericTypes(specialization.typeArguments())); + } + } + for (GenericTypes allocation : allocations) { + if (!specializedStatics.contains(allocation)) { + return true; + } + } + return false; + } + /** * The node {@code copy} was ultimately copied from, or {@code copy} itself. *

diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java index f2951044e..f8b4823cb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java @@ -190,7 +190,9 @@ public static void removePhantomGenericStaticInitializers(ImProg prog, ImTransla changed = false; for (ImVar original : candidates.keySet()) { ImClass owner = translator.genericStaticOwnerOf(original); - if ((used.getVars().contains(original) || used.getInstantiatedClasses().contains(owner)) + boolean erasedInstantiationNeedsOriginal = used.getInstantiatedClasses().contains(owner) + && translator.hasErasedAllocationWithoutStaticSpecialization(owner, original); + if ((used.getVars().contains(original) || erasedInstantiationNeedsOriginal) && liveOriginals.add(original)) { changed = true; } diff --git a/de.peeeq.wurstscript/src/test/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypesTests.java b/de.peeeq.wurstscript/src/test/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypesTests.java new file mode 100644 index 000000000..71a8eca59 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypesTests.java @@ -0,0 +1,64 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.ast.Ast; +import de.peeeq.wurstscript.jassIm.ImClass; +import de.peeeq.wurstscript.jassIm.ImClassType; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.jassIm.ImMethod; +import de.peeeq.wurstscript.jassIm.ImSimpleType; +import de.peeeq.wurstscript.jassIm.ImTypeArgument; +import de.peeeq.wurstscript.jassIm.ImTypeClassFunc; +import de.peeeq.wurstscript.jassIm.JassIm; +import io.vavr.control.Either; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.testng.Assert.assertEquals; + +public class GenericTypesTests { + + @Test + public void nestedTypeClassBindingsDoNotSplitSpecializationKeys() { + ImClass box = genericClass("Box"); + ImClass list = genericClass("List"); + ImSimpleType integer = JassIm.ImSimpleType("integer"); + ImTypeClassFunc requirement = JassIm.ImTypeClassFunc(Ast.NoExpr(), "toIndex", + JassIm.ImTypeVars(), JassIm.ImVars(), integer); + ImFunction instance = JassIm.ImFunction(Ast.NoExpr(), "intToIndex", JassIm.ImTypeVars(), + JassIm.ImVars(), integer, JassIm.ImVars(), JassIm.ImStmts(), List.of()); + + Map> binding = new LinkedHashMap<>(); + binding.put(requirement, Either.right(instance)); + ImClassType unboundBox = JassIm.ImClassType(box, + JassIm.ImTypeArguments(argument(integer, Collections.emptyMap()))); + ImClassType boundBox = JassIm.ImClassType(box, + JassIm.ImTypeArguments(argument(integer, binding))); + + GenericTypes unbound = key(list, unboundBox); + GenericTypes bound = key(list, boundBox); + + assertEquals(bound, unbound, + "type-class dispatch metadata must not change a structural specialization key"); + assertEquals(bound.hashCode(), unbound.hashCode()); + } + + private static GenericTypes key(ImClass list, ImClassType nestedType) { + ImClassType listType = JassIm.ImClassType(list, + JassIm.ImTypeArguments(argument(nestedType, Collections.emptyMap()))); + return new GenericTypes(List.of(argument(listType, Collections.emptyMap()))); + } + + private static ImTypeArgument argument(de.peeeq.wurstscript.jassIm.ImType type, + Map> binding) { + return JassIm.ImTypeArgument(type, binding); + } + + private static ImClass genericClass(String name) { + return JassIm.ImClass(Ast.NoExpr(), name, JassIm.ImTypeVars(JassIm.ImTypeVar("T")), + JassIm.ImVars(), JassIm.ImMethods(), JassIm.ImFunctions(), List.of()); + } +} diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index 7f5d1fef1..18eedcfca 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -870,7 +870,7 @@ public void tupleSpecializedTypedLocalDoesNotRootErasedInitializer() throws IOEx } @Test - public void tupleSpecializedStaticKeepsLiveErasedInitializer() { + public void tupleSpecializedStaticKeepsAllLiveInitializers() { test().testLua(true).executeProg().lines( "package Test", "native testSuccess()", @@ -902,10 +902,12 @@ public void tupleSpecializedStaticKeepsInitializerForConstructedErasedClass() { "class Box", " static int value = bump()", " construct()", + " static function get() returns int", + " return value", "init", " new Box()", " new Box()", - " if bumps == 2", + " if Box.get() == 1 and Box.get() == 2 and bumps == 2", " testSuccess()" ); } @@ -934,6 +936,509 @@ public void tupleSpecializedInterfaceDispatchDoesNotRootErasedStaticInitializer( ); } + @Test + public void nestedConcreteGenericStaticStorageCompilesInLua() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "class List", + " static T array store", + " int size", + " construct()", + " function add(T value)", + " store[size] = value", + " size++", + " function get(int index) returns T", + " return store[index]", + "class Item", + " int value", + " construct(int value)", + " this.value = value", + "init", + " let nested = new List>()", + " let inner = new List()", + " nested.add(inner)", + " inner.add(new Item(7))", + " let pairs = new List()", + " pairs.add(pair(2, 3))", + " if nested.get(0) == inner and inner.get(0).value == 7 and pairs.get(0).x == 2", + " testSuccess()" + ); + + String compiled = compiledLua("nestedConcreteGenericStaticStorageCompilesInLua"); + java.util.regex.Matcher storageDeclarations = java.util.regex.Pattern + .compile("(?m)^(List_store\\S*) = nil$") + .matcher(compiled); + List storageNames = new ArrayList<>(); + while (storageDeclarations.find()) { + storageNames.add(storageDeclarations.group(1)); + } + assertEquals("each concrete List instantiation needs independent static storage", + 3, storageNames.size()); + assertEquals("each structural List specialization must emit one storage slot", + 3L, storageNames.stream().distinct().count()); + } + + @Test + public void genericStaticsAreIndependentWithoutTupleInstantiation() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Slot", + " static T value", + " static function set(T newValue)", + " value = newValue", + " static function get() returns T", + " return value", + "init", + " Slot.set(7)", + " Slot.set(\"ok\")", + " if Slot.get() == 7 and Slot.get() == \"ok\"", + " testSuccess()" + ); + + String compiled = compiledLua("genericStaticsAreIndependentWithoutTupleInstantiation"); + java.util.regex.Matcher storageDeclarations = java.util.regex.Pattern + .compile("(?m)^Slot_value_\\S* = nil$") + .matcher(compiled); + int storages = 0; + while (storageDeclarations.find()) { + storages++; + } + assertEquals("each concrete Slot instantiation needs its own static", + 2, storages); + } + + @Test + public void constructedErasedInstantiationDoesNotDuplicateSpecializedStaticInitializer() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " construct()", + " static function get() returns int", + " return value", + "init", + " new Box()", + " if Box.get() == 1 and bumps == 1", + " testSuccess()" + ); + } + + @Test + public void eachConstructedErasedInstantiationGetsItsOwnStaticInitializer() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " construct()", + " static function get() returns int", + " return value", + "init", + " new Box()", + " new Box()", + " if Box.get() == 1 and Box.get() == 2 and Box.get() == 3 and bumps == 3", + " testSuccess()" + ); + } + + @Test + public void genericFactoryAllocationSpecializesStaticOwningClass() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " construct()", + " static function get() returns int", + " return value", + "function make() returns Box", + " return new Box()", + "function forward() returns Box", + " return make()", + "class Maker", + " construct()", + " function makeBox() returns Box", + " return new Box()", + "init", + " let first = forward()", + " let second = forward()", + " let third = new Maker().makeBox()", + " if first != null and second != null and third != null", + " and Box.get() == 1 and Box.get() == 2", + " and Box.get() == 3 and bumps == 3", + " testSuccess()" + ); + + String compiled = compiledLua("genericFactoryAllocationSpecializesStaticOwningClass"); + assertEquals("the shared constructor must allocate ordinary objects on the erased Lua class", + 1, countOccurrences(compiled, "= Box:create()")); + assertFalse("static specialization must not create specialized object classes", + java.util.regex.Pattern.compile("(?m)^Box_specialized\\S* = \\(\\{\\}\\)$") + .matcher(compiled).find()); + } + + @Test + public void inheritedGenericStaticUsesDeclaringOwnerSpecialization() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "class Base", + " static T value", + "class Child extends Base", + " construct()", + " function set(T newValue)", + " value = newValue", + " function get() returns T", + " return value", + "init", + " let ints = new Child()", + " let strings = new Child()", + " ints.set(7)", + " strings.set(\"ok\")", + " if ints.get() == 7 and strings.get() == \"ok\"", + " testSuccess()" + ); + + String compiled = compiledLua("inheritedGenericStaticUsesDeclaringOwnerSpecialization"); + java.util.regex.Matcher declarations = java.util.regex.Pattern + .compile("(?m)^Base_value_\\S* = nil$").matcher(compiled); + int storages = 0; + while (declarations.find()) { + storages++; + } + assertEquals("each inherited Base static needs independent storage", 2, storages); + } + + @Test + public void constructedSubclassInitializesInheritedGenericStatic() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Base", + " static int value = bump()", + "class Child extends Base", + " construct()", + "init", + " new Child()", + " new Child()", + " if bumps == 2", + " testSuccess()" + ); + } + + @Test + public void fixedConcreteConstructionDoesNotSpecializeUnrelatedGenericCaller() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " construct()", + " static function get() returns int", + " return value", + "function helper() returns Box", + " return new Box()", + "init", + " let first = helper()", + " let second = helper()", + " if first != null and second != null and Box.get() == 2 and bumps == 2", + " testSuccess()" + ); + + String compiled = compiledLua("fixedConcreteConstructionDoesNotSpecializeUnrelatedGenericCaller"); + assertFalse("fixed Box construction must not clone helper", + compiled.contains("helper_specialized")); + } + + @Test + public void inheritedGenericStaticInitializerUsesDeclaringOwnerMapping() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Base", + " static int serial = bump()", + "class Child extends Base", + " static int copied = serial", + " static function get() returns int", + " return copied", + "init", + " if Child.get() == 1 and Child.get() == 2", + " and bumps == 2", + " testSuccess()" + ); + } + + @Test + public void fixedAllocationInsideErasedGenericMethodIsRegistered() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " construct()", + " static function get() returns int", + " return value", + "class Factory", + " construct()", + " function make() returns Box", + " return new Box()", + "init", + " let factory = new Factory()", + " let made = factory.make()", + " let fresh = new Factory().make()", + " if made != null and fresh != null and Box.get() == 2 and bumps == 2", + " testSuccess()" + ); + + String compiled = compiledLua("fixedAllocationInsideErasedGenericMethodIsRegistered"); + assertFalse("fixed generic method body must not be cloned for its class argument", + compiled.contains("Factory_make_specialized")); + } + + @Test + public void fixedStaticCalleeDoesNotSpecializeUnrelatedGenericCaller() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " static function get() returns int", + " return value", + "function helper() returns int", + " return Box.get()", + "init", + " if helper() == 1 and helper() == 1", + " and Box.get() == 2 and bumps == 2", + " testSuccess()" + ); + + String compiled = compiledLua("fixedStaticCalleeDoesNotSpecializeUnrelatedGenericCaller"); + assertFalse("fixed Box static call must not clone helper", + compiled.contains("helper_specialized")); + } + + @Test + public void randomizedNestedGenericTupleClassShapesMatchAllBackends() { + record Shape(String type, String value, int constructions) {} + + Random random = new Random(0x6E3571A9L); + List declarations = new ArrayList<>(); + List shapes = new ArrayList<>(); + for (int i = 0; i < 18; i++) { + int first = random.nextInt(17) + 1; + int second = random.nextInt(17) + 1; + Shape shape = i % 2 == 0 + ? new Shape("int", Integer.toString(first), 0) + : new Shape("pair", "pair(" + first + ", " + second + ")", + 0); + int depth = 2 + random.nextInt(3); + for (int d = 0; d < depth; d++) { + switch ((i + d + random.nextInt(3)) % 3) { + case 0 -> shape = new Shape("Box<" + shape.type() + ">", + "new Box<" + shape.type() + ">(" + shape.value() + ")", + shape.constructions() + 1); + case 1 -> shape = new Shape("Child<" + shape.type() + ">", + "new Child<" + shape.type() + ">(" + shape.value() + ")", + shape.constructions() + 1); + case 2 -> { + String tupleName = "Wrapped" + i + "_" + d; + int tag = random.nextInt(11) + 1; + declarations.add("tuple " + tupleName + "(" + shape.type() + + " value, int tag)"); + shape = new Shape(tupleName, + tupleName + "(" + shape.value() + ", " + tag + ")", + shape.constructions()); + } + } + } + shapes.add(shape); + } + + List source = new ArrayList<>(); + source.add("package Test"); + source.add("native testSuccess()"); + source.add("tuple pair(int x, int y)"); + source.add("int constructions"); + source.add("int writes"); + source.add("interface Marker"); + source.add("class Box implements Marker"); + source.add(" T value"); + source.add(" construct(T value)"); + source.add(" this.value = value"); + source.add(" constructions++"); + source.add("class Child extends Box"); + source.add(" construct(T value)"); + source.add(" super(value)"); + source.add("class Vault"); + source.add(" static T value"); + source.add(" static function set(T newValue)"); + source.add(" value = newValue"); + source.add(" writes++"); + source.addAll(declarations); + source.add("init"); + int expectedConstructions = 0; + for (Shape shape : shapes) { + source.add(" Vault<" + shape.type() + ">.set(" + shape.value() + ")"); + expectedConstructions += shape.constructions(); + } + source.add(" if writes == " + shapes.size() + + " and constructions == " + expectedConstructions); + source.add(" testSuccess()"); + + test().testLua(true).executeProg().lines(source.toArray(new String[0])); + } + + @Test + public void randomizedClassInterfaceModuleDispatchMatchesAllBackends() { + Random random = new Random(0xD15A7C4L); + List source = new ArrayList<>(); + Collections.addAll(source, + "package Test", + "native testSuccess()", + "int destroyed", + "interface Primary", + " function score() returns int", + "interface Secondary", + " function bonus() returns int", + "module Payload", + " int moduleValue", + " function payload() returns int", + " return moduleValue * 3", + " ondestroy", + " destroyed++", + "class Root implements Primary", + " use Payload", + " int base", + " construct(int base)", + " this.base = base", + " moduleValue = base + 1", + " override function score() returns int", + " return base + payload()", + "class Alpha extends Root implements Secondary", + " construct(int base)", + " super(base)", + " override function score() returns int", + " return super.score() + 11", + " override function bonus() returns int", + " return base * 5 + 1", + "class AlphaLeaf extends Alpha", + " construct(int base)", + " super(base)", + " override function score() returns int", + " return super.score() * 2", + " override function bonus() returns int", + " return super.bonus() + 5", + "class Beta extends Root implements Secondary", + " construct(int base)", + " super(base)", + " override function score() returns int", + " return super.score() - 7", + " override function bonus() returns int", + " return base * 7 + 2", + "module ScoreContract", + " abstract function score() returns int", + "module StandaloneScore", + " use ScoreContract", + " use Payload", + " override function score() returns int", + " return moduleValue * 9 + 4", + "class ModuleOnly implements Primary", + " use StandaloneScore", + " construct(int base)", + " moduleValue = base", + "function viaPrimary(Primary value) returns int", + " return value.score()", + "function viaSecondary(Secondary value) returns int", + " return value.bonus()", + "init", + " int checksum = 0"); + + int expected = 0; + int objectCount = 40; + for (int i = 0; i < objectCount; i++) { + int value = random.nextInt(30) + 1; + int kind = random.nextInt(5); + String className; + int score; + Integer bonus = null; + switch (kind) { + case 0 -> { + className = "Root"; + score = 4 * value + 3; + } + case 1 -> { + className = "Alpha"; + score = 4 * value + 14; + bonus = value * 5 + 1; + } + case 2 -> { + className = "AlphaLeaf"; + score = (4 * value + 14) * 2; + bonus = value * 5 + 6; + } + case 3 -> { + className = "Beta"; + score = 4 * value - 4; + bonus = value * 7 + 2; + } + default -> { + className = "ModuleOnly"; + score = value * 9 + 4; + } + } + source.add(" let object" + i + " = new " + className + "(" + value + ")"); + source.add(" Primary primary" + i + " = object" + i); + source.add(" checksum += viaPrimary(primary" + i + ")"); + expected += score; + if (bonus != null) { + source.add(" Secondary secondary" + i + " = object" + i); + source.add(" checksum += viaSecondary(secondary" + i + ")"); + expected += bonus; + } + source.add(" destroy object" + i); + } + source.add(" if checksum == " + expected + " and destroyed == " + objectCount); + source.add(" testSuccess()"); + + test().testLua(true).executeProg().lines(source.toArray(new String[0])); + } + @Test public void tupleSpecializedStaticInitializerCycleDoesNotRootErasedCopy() throws IOException { test().testLua(true).executeProg().lines( @@ -987,7 +1492,9 @@ public void compiletimeGenericArrayReplayLeavesAreSplit() { .matcher(compiled); int persistedAssignments = 0; while (replayBody.find()) { - int assignmentsInFunction = countOccurrences(replayBody.group(1), "Box_store["); + int assignmentsInFunction = (int) java.util.regex.Pattern + .compile("Box_store[^\\[]*\\[") + .matcher(replayBody.group(1)).results().count(); assertTrue("each generic replay leaf must honor the configured split limit:\n" + replayBody.group(), assignmentsInFunction <= 1); persistedAssignments += assignmentsInFunction;