diff --git a/AGENTS.md b/AGENTS.md index 54284409f..16786cf9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -212,6 +212,20 @@ For config and run-pipeline changes, prefer these focused checks before broader Recent fixes established additional rules for backend work. Follow these for all future changes: +### Compiler phase ownership and lowering invariants + +* Fix malformed or underspecified IR in the phase which creates it. Do not add downstream recovery, + name parsing, or backend-specific guessing for information an earlier phase discarded. +* Semantic identity and specialization keys must use referenced AST/IM nodes plus structural type + arguments, never generated names or string comparison. +* Each lowering phase has one explicit input/output contract. After an abstraction is lowered, + downstream phases consume the lowered representation and must not reconstruct its source meaning. +* Prefer backend-appropriate, state-of-the-art lowering when semantics permit it. Jass limitations may + require compatibility compromises; do not carry those compromises into Lua without evidence. +* Behavioral correctness is the primary requirement. Runtime and allocation performance are the next + requirement: common optimized paths must not retain avoidable compiler-introduced allocation, + dispatch, copying, or bookkeeping overhead. + ### Jass/Lua feature parity * New language/compiler features must be validated for **both Jass and Lua** backends. 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 62a73b79b..5043a07f4 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 @@ -874,12 +874,14 @@ public LuaCompilationUnit transformProgToLua() { ImAttrType.setWurstClassType(null); int stage; - if (containsGenericNewCall() || containsTypeClassDispatch()) { - // Both operations need the concrete type argument, which erasure does not keep. Only - // the paths reaching them are specialised: the full elimination used for Jass is - // followed there by class elimination, and leaves state this backend cannot consume. - beginPhase(2, "Specialize generics for generic construction and type class dispatch"); - new EliminateGenerics(getImTranslator(), getImProg()).transformGenericNewOnly(); + boolean specializeTupleValueTypes = containsTupleTypeArgument(); + if (containsGenericNewCall() || containsTypeClassDispatch() || specializeTupleValueTypes) { + beginPhase(2, "Specialize generics for Lua-only concrete operations"); + new EliminateGenerics(getImTranslator(), getImProg()) + .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()); timeTaker.endPhase(); } if (runArgs.isNoDebugMessages()) { @@ -919,6 +921,12 @@ public LuaCompilationUnit transformProgToLua() { getImProg().flatten(imTranslator2); EliminateLocalTypes.eliminateLocalTypesProg(getImProg(), imTranslator2); + timeTaker.beginPhase("eliminate tuples"); + getImProg().flatten(imTranslator2); + EliminateTuples.eliminateTuplesProg(getImProg(), imTranslator2); + imTranslator2.assertProperties(AssertProperty.NOTUPLES); + timeTaker.endPhase(); + optimizer.removeGarbage(); imProg.flatten(imTranslator); timeTaker.endPhase(); @@ -996,4 +1004,19 @@ public void visit(ImTypeVarDispatch dispatch) { }); return found[0]; } + + /** Tuple type arguments need monomorphisation before tuples can become scalar storage. */ + private boolean containsTupleTypeArgument() { + boolean[] found = {false}; + getImProg().accept(new de.peeeq.wurstscript.jassIm.Element.DefaultVisitor() { + @Override + public void visit(ImTypeArgument argument) { + if (TypesHelper.typeContainsTuples(argument.getType())) { + found[0] = true; + } + super.visit(argument); + } + }); + return found[0]; + } } 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 d080df02b..89563be53 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 @@ -14,6 +14,7 @@ import de.peeeq.wurstscript.translation.imtojass.ImAttrType; import de.peeeq.wurstscript.translation.imtojass.TypeRewriteMatcher; import de.peeeq.wurstscript.translation.lua.translation.RemoveGarbage; +import de.peeeq.wurstscript.types.TypesHelper; import io.vavr.control.Either; import org.eclipse.jdt.annotation.Nullable; import org.jetbrains.annotations.NotNull; @@ -29,6 +30,7 @@ public class EliminateGenerics { private final ImTranslator translator; private final ImProg prog; private boolean genericNewOnly; + private boolean specializeTupleValueTypes; private final Deque genericsUses = new ArrayDeque<>(); /** * Call sites already rewritten to a specialisation. @@ -43,6 +45,10 @@ public class EliminateGenerics { private final Map functionOwners = new IdentityHashMap<>(); private final Table specializedMethods = HashBasedTable.create(); private final Table specializedClasses = HashBasedTable.create(); + /** Concrete generic identities named by runtime instanceof checks. */ + private final Table runtimeTypeSpecializations = HashBasedTable.create(); + private record RuntimeTypeUse(ImClass clazz, GenericTypes generics) { + } private final Multimap> onSpecializedClassTriggers = HashMultimap.create(); // Track concrete generic arguments for specialized functions to simplify later lookups @@ -52,15 +58,12 @@ public class EliminateGenerics { // 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(); - - private static String gKey(GenericTypes g) { - return g.makeName(); - } + private final Table specializedGlobals = HashBasedTable.create(); // NEW: Track which global vars belong to which generic class // This helps us know which globals need specialization - private final Map globalToClass = new HashMap<>(); + /** Generic statics in source/program order; specialization emission must be deterministic. */ + private final Map globalToClass = new LinkedHashMap<>(); // NEW: which functions touch generic globals (identity-based) private final Map> functionToGenericGlobalOwners = new IdentityHashMap<>(); @@ -112,17 +115,30 @@ public void transform() { } /** - * Lua normally erases new generics. Generic construction is the one operation which needs the - * concrete type, so only specialize functions on paths leading to {@code wurstNewInstance}. All other - * generic calls and classes keep the Lua backend's normal erased representation. + * Lua normally erases generics. Generic construction and scalar storage for tuple type arguments, + * bounded dispatch, and parameterized runtime identity are the operations which need the concrete + * type, so only specialize paths leading to those operations. All other generic calls and classes + * keep the Lua backend's erased representation. */ public void transformGenericNewOnly() { + transformGenericNewOnly(false); + } + + public void transformGenericNewOnly(boolean specializeTupleValueTypes) { genericNewOnly = true; + this.specializeTupleValueTypes = specializeTupleValueTypes; + if (specializeTupleValueTypes) { + addMemberTypeArguments(); + identifyGenericGlobals(); + } 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 // idempotent, so this terminates once every reachable site has been rewritten. while (true) { + if (specializeTupleValueTypes) { + collectRuntimeTypeSpecializations(); + } collectGenericNewRoots(); if (genericsUses.isEmpty()) { break; @@ -262,9 +278,149 @@ public void visit(ImMemberAccess memberAccess) { super.visit(memberAccess); collectGenericNewUse(memberAccess); } + + @Override + public void visit(ImDealloc dealloc) { + super.visit(dealloc); + collectGenericNewUse(dealloc); + } + + @Override + public void visit(ImInstanceof instanceOf) { + super.visit(instanceOf); + collectGenericNewUse(instanceOf); + } + + @Override + public void visit(ImTypeIdOfObj typeId) { + super.visit(typeId); + collectGenericNewUse(typeId); + } + + @Override + public void visit(ImTypeIdOfClass typeId) { + super.visit(typeId); + collectGenericNewUse(typeId); + } + }); + } + + /** + * Records parameterized classes whose runtime identity is observed. Lua normally erases + * generics, but an instanceof target must denote the same concrete class as allocations of that + * instantiation; otherwise a tuple-specialized object is also an instance of every erased + * non-tuple instantiation. + */ + private void collectRuntimeTypeSpecializations() { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImInstanceof instanceOf) { + super.visit(instanceOf); + ImClassType clazz = instanceOf.getClazz(); + if (!clazz.getTypeArguments().isEmpty() + && !typeArgumentsContainTypeVariable(clazz.getTypeArguments())) { + GenericTypes generics = new GenericTypes(clazz.getTypeArguments()); + ImClass original = clazz.getClassDef(); + if (runtimeTypeSpecializations.put(original, generics, true) == null) { + rewriteExistingRuntimeTypeSuperEdges(); + } + } + } }); } + private void rewriteExistingRuntimeTypeSuperEdges() { + for (Table.Cell cell + : new ArrayList<>(specializedClasses.cellSet())) { + if (needsRuntimeTypeSpecialization(cell.getRowKey(), cell.getColumnKey(), + new HashSet<>())) { + rewriteRuntimeTypeSuperEdges(cell.getRowKey(), cell.getColumnKey(), cell.getValue()); + } + } + } + + /** Redirects concrete inheritance edges to the same class identity used by instanceof. */ + private void rewriteRuntimeTypeSuperEdges(ImClass original, GenericTypes generics, + ImClass specialized) { + for (ImClass clazz : prog.getClasses()) { + clazz.getSuperClasses().replaceAll(superType -> { + if (superType.getClassDef() != original + || typeArgumentsContainTypeVariable(superType.getTypeArguments()) + || !new GenericTypes(superType.getTypeArguments()).equals(generics)) { + return superType; + } + return JassIm.ImClassType(specialized, JassIm.ImTypeArguments()); + }); + } + } + + private boolean needsRuntimeTypeSpecialization(ImClassType clazz) { + if (typeArgumentsContainTypeVariable(clazz.getTypeArguments())) { + return false; + } + return needsRuntimeTypeSpecialization(clazz.getClassDef(), + new GenericTypes(clazz.getTypeArguments()), + new HashSet<>()); + } + + private boolean needsRuntimeTypeSpecialization(ImClass clazz, GenericTypes generics, + Set visited) { + if (!visited.add(new RuntimeTypeUse(clazz, generics))) { + return false; + } + if (runtimeTypeSpecializations.contains(clazz, generics)) { + return true; + } + if (generics.getTypeArguments().size() != clazz.getTypeVariables().size()) { + return false; + } + for (ImClassType superType : clazz.getSuperClasses()) { + ImClassType concreteSuper = (ImClassType) transformType(superType, generics, + clazz.getTypeVariables()); + if (!typeArgumentsContainTypeVariable(concreteSuper.getTypeArguments()) + && needsRuntimeTypeSpecialization(concreteSuper.getClassDef(), + new GenericTypes(concreteSuper.getTypeArguments()), visited)) { + return true; + } + } + return false; + } + + private boolean needsRuntimeTypeSpecialization(ImClass clazz, + ImTypeArguments typeArguments) { + int classArgumentCount = clazz.getTypeVariables().size(); + if (classArgumentCount == 0 || typeArguments.size() < classArgumentCount) { + return false; + } + List classArguments = new ArrayList<>(classArgumentCount); + for (int i = 0; i < classArgumentCount; i++) { + ImTypeArgument argument = typeArguments.get(i); + if (containsTypeVariable(argument.getType())) { + return false; + } + classArguments.add(argument); + } + return needsRuntimeTypeSpecialization(clazz, new GenericTypes(classArguments), + new HashSet<>()); + } + + private boolean needsRuntimeTypeSpecialization(ImFunctionCall call) { + ImClass owner = classOwning(call.getFunc()); + return owner != null + && needsRuntimeTypeSpecialization(owner, call.getTypeArguments()); + } + + private void collectGenericNewUse(ImClassRelatedExprWithClass expression) { + ImClassType clazz = expression.getClazz(); + if (clazz.getTypeArguments().isEmpty() + || typeArgumentsContainTypeVariable(clazz.getTypeArguments()) + || (!shouldSpecializeTupleArguments(clazz.getTypeArguments()) + && !needsRuntimeTypeSpecialization(clazz))) { + return; + } + genericsUses.add(new GenericClazzUse(expression)); + } + private void collectGenericNewUses(Element element) { element.accept(new Element.DefaultVisitor() { @Override @@ -304,7 +460,9 @@ private void collectGenericNewUse(ImFunctionCall call) { return; } if (!call.getTypeArguments().isEmpty() - && functionNeedsSpecialization(call.getFunc(), Collections.newSetFromMap(new IdentityHashMap<>()))) { + && (shouldSpecializeTupleArguments(call.getTypeArguments()) + || needsRuntimeTypeSpecialization(call) + || functionNeedsSpecialization(call.getFunc(), Collections.newSetFromMap(new IdentityHashMap<>())))) { if (!typeArgumentsContainTypeVariable(call.getTypeArguments())) { genericsUses.add(new GenericImFunctionCall(call)); } @@ -347,8 +505,9 @@ private void collectCallThroughGenericReceiver(ImFunctionCall call) { || typeArgumentsContainTypeVariable(classType.getTypeArguments())) { return; } - if (!functionNeedsSpecialization(call.getFunc(), - Collections.newSetFromMap(new IdentityHashMap<>()))) { + if (!shouldSpecializeTupleArguments(classType.getTypeArguments()) + && !functionNeedsSpecialization(call.getFunc(), + Collections.newSetFromMap(new IdentityHashMap<>()))) { return; } genericsUses.add(new GenericClassFunctionCall(call, owningClass, @@ -365,7 +524,11 @@ private void collectCallThroughGenericReceiver(ImFunctionCall call) { private @Nullable ImClass classOwning(ImFunction function) { if (classByFunction == null) { classByFunction = new IdentityHashMap<>(); + Map classBySource = new IdentityHashMap<>(); for (ImClass imClass : prog.getClasses()) { + if (imClass.getTrace() instanceof ClassDef sourceClass) { + classBySource.put(sourceClass, imClass); + } for (ImFunction f : imClass.getFunctions()) { classByFunction.putIfAbsent(f, imClass); } @@ -376,6 +539,16 @@ private void collectCallThroughGenericReceiver(ImFunctionCall call) { } } } + for (ImFunction f : prog.getFunctions()) { + ClassDef sourceClass = f.attrTrace() == null + ? null : f.attrTrace().attrNearestClassDef(); + if (sourceClass != null) { + ImClass owner = classBySource.get(sourceClass); + if (owner != null) { + classByFunction.putIfAbsent(f, owner); + } + } + } } return classByFunction.get(function); } @@ -393,7 +566,9 @@ private void collectGenericNewUse(ImAlloc alloc) { ImClassType clazz = alloc.getClazz(); if (clazz.getTypeArguments().isEmpty() || typeArgumentsContainTypeVariable(clazz.getTypeArguments()) - || !isConstructionOnlyInstantiation(clazz.getClassDef())) { + || (!shouldSpecializeTupleArguments(clazz.getTypeArguments()) + && !needsRuntimeTypeSpecialization(clazz) + && !isConstructionOnlyInstantiation(clazz.getClassDef()))) { return; } genericsUses.add(new GenericClazzUse(alloc)); @@ -468,7 +643,7 @@ private void collectGenericNewUse(ImMemberAccess memberAccess) { // A class that has already been specialised has nothing left to select, and asking the // receiver to adapt to it fails outright: the receiver is still typed by the generic class // the specialised one was copied from, which is not a superclass of it. - if (owningClass.getTypeVariables().isEmpty() || !isConstructionOnlyInstantiation(owningClass)) { + if (owningClass.getTypeVariables().isEmpty()) { return; } if (memberAccess.getTypeArguments().isEmpty()) { @@ -479,6 +654,10 @@ private void collectGenericNewUse(ImMemberAccess memberAccess) { || typeArgumentsContainTypeVariable(memberAccess.getTypeArguments())) { return; } + if (!shouldSpecializeTupleArguments(memberAccess.getTypeArguments()) + && !isConstructionOnlyInstantiation(owningClass)) { + return; + } genericsUses.add(new GenericMemberAccess(memberAccess)); } @@ -487,7 +666,17 @@ private void collectGenericNewUse(ImMethodCall call) { return; } ImMethod method = call.getMethod(); - if (!methodNeedsSpecialization(method, + ImTranslator.Specialisation existing = translator.specialisationOf(method); + if (existing != null && existing.original() instanceof ImMethod) { + // The owning class created this concrete method. Its type arguments were consumed by + // that structural class specialisation; collecting it again invents a second generic + // boundary with no type variables and loses the phase invariant. + call.getTypeArguments().removeAll(); + specializedCallSites.add(call); + return; + } + if (!shouldSpecializeTupleArguments(call.getTypeArguments()) + && !methodNeedsSpecialization(method, Collections.newSetFromMap(new IdentityHashMap<>()), Collections.newSetFromMap(new IdentityHashMap<>()))) { return; @@ -550,6 +739,23 @@ private boolean typeArgumentsContainTypeVariable(ImTypeArguments typeArguments) return false; } + private boolean typeArgumentsContainTuple(Iterable typeArguments) { + for (ImTypeArgument typeArgument : typeArguments) { + if (TypesHelper.typeContainsTuples(typeArgument.getType())) { + return true; + } + } + return false; + } + + private boolean shouldSpecializeTupleArguments(ImTypeArguments typeArguments) { + return specializeTupleValueTypes && typeArgumentsContainTuple(typeArguments); + } + + private boolean genericTypesContainTuple(GenericTypes generics) { + return typeArgumentsContainTuple(generics.getTypeArguments()); + } + private boolean functionNeedsSpecialization(ImFunction function, Set visited) { return functionNeedsSpecialization(function, visited, Collections.newSetFromMap(new IdentityHashMap<>())); @@ -574,6 +780,15 @@ public void visit(ImTypeVarDispatch dispatch) { found[0] = true; } + @Override + public void visit(ImInstanceof instanceOf) { + if (typeArgumentsContainTypeVariable(instanceOf.getClazz().getTypeArguments())) { + found[0] = true; + return; + } + super.visit(instanceOf); + } + @Override public void visit(ImAlloc alloc) { // Constructing a class whose methods dispatch has to be specialised as well: @@ -1092,11 +1307,15 @@ private void moveFunctionsOutOfClass(ImClass c) { * These are the "static" fields that need specialization */ private void identifyGenericGlobals() { - // Only include "relevant" classes: new-generic or subclass of new-generic. - Map relevantClassMap = buildRelevantClassMap(); + Map genericClassesBySource = new IdentityHashMap<>(); + for (ImClass imClass : prog.getClasses()) { + if (!imClass.getTypeVariables().isEmpty() && imClass.getTrace() instanceof ClassDef sourceClass) { + genericClassesBySource.put(sourceClass, imClass); + } + } for (ImVar global : prog.getGlobals()) { - ImClass owner = resolveOwningClassFromTrace(global, relevantClassMap); + ImClass owner = resolveOwningClassFromTrace(global, genericClassesBySource); if (owner == null) { continue; // not defined inside a class (package/global constant, etc.) } @@ -1109,43 +1328,13 @@ private void identifyGenericGlobals() { } } - /** - * Build a map of class-name -> ImClass, but only for "relevant" classes: - * - the class is new-generic (has typeVariables) - * - OR any of its superclasses is new-generic (transitively) - */ - private Map buildRelevantClassMap() { - Map m = new HashMap<>(); - IdentityHashMap memo = new IdentityHashMap<>(); - - for (ImClass c : prog.getClasses()) { - if (!c.getTypeVariables().isEmpty()) { - m.put(c.getName(), c); - } - } - return m; - } - - /** - * Resolve owning class for a global via trace: - * - if the global's trace source is inside a class, return the matching ImClass (if relevant) - * - otherwise return null - */ - private @Nullable ImClass resolveOwningClassFromTrace(ImVar global, Map relevantClassMap) { + /** Resolve a generic static's owner through its source class identity. */ + private @Nullable ImClass resolveOwningClassFromTrace( + ImVar global, Map genericClassesBySource) { if (global.getTrace() == null) return null; - - // This is the only assumption you may need to adapt if your ImTrace API differs: - de.peeeq.wurstscript.ast.Element srcObj = global.getTrace(); // expected to be a wurst AST Element - if (srcObj == null) return null; - - @Nullable ClassDef classDef = srcObj.attrNearestClassDef(); + @Nullable ClassDef classDef = global.getTrace().attrNearestClassDef(); if (classDef == null) return null; - - // Get the class name from the AST (no global-name parsing). - String className = classDef.getNameId().getName(); - - // Only accept if it is one of the relevant classes (new-generic or inherits new-generic). - return relevantClassMap.get(className); + return genericClassesBySource.get(classDef); } /** @@ -1307,6 +1496,15 @@ private ImFunction specializeFunction(ImFunction f, GenericTypes generics) { rewriteGenerics(newF, generics, typeVars); } + if (genericNewOnly && specializeTupleValueTypes && genericTypesContainTuple(generics)) { + ImClass owner = classOwning(f); + if (owner != null && !owner.getTypeVariables().isEmpty()) { + GenericTypes ownerGenerics = generics.take(owner.getTypeVariables().size()); + specializeClass(owner, ownerGenerics); + rewriteOwnedGenericGlobals(newF, owner, ownerGenerics); + } + } + // Fix calls inside this specialized function so they also point to specialized callees if (genericNewOnly) { collectGenericNewUses(newF); @@ -1320,6 +1518,32 @@ private ImFunction specializeFunction(ImFunction f, GenericTypes generics) { return newF; } + private void rewriteOwnedGenericGlobals(Element copy, ImClass owner, GenericTypes ownerGenerics) { + copy.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImVarAccess access) { + super.visit(access); + access.setVar(specializedGlobal(access.getVar())); + } + + @Override + public void visit(ImVarArrayAccess access) { + super.visit(access); + access.setVar(specializedGlobal(access.getVar())); + } + + private ImVar specializedGlobal(ImVar original) { + ImClass globalOwner = globalToClass.get(original); + if (globalOwner == null + || translator.canonical(globalOwner) != translator.canonical(owner)) { + return original; + } + ImVar result = ensureSpecializedGlobal(original, globalOwner, ownerGenerics); + return result == null ? original : result; + } + }); + } + /** * creates a specialized version of this method */ @@ -1386,7 +1610,9 @@ private ImFunction specializeClassFunction(ImFunction function, ImClass owningCl List typeVariables = new ArrayList<>(owningClass.getTypeVariables()); typeVariables.addAll(function.getTypeVariables()); if (typeVariables.size() != generics.getTypeArguments().size()) { - throw new CompileError(blameFor, "Generics should match class method type variables."); + throw new CompileError(blameFor, "Generics should match class method type variables for " + + function.getName() + ": expected " + typeVariables.size() + " but found " + + generics.getTypeArguments().size() + "."); } ImFunction newImplementation = function.copyWithRefs(); @@ -1398,6 +1624,11 @@ private ImFunction specializeClassFunction(ImFunction function, ImClass owningCl newImplementation.getTypeVariables().removeAll(); newImplementation.setName(function.getName() + "_specialized"); rewriteGenerics(newImplementation, generics, typeVariables); + if (specializeTupleValueTypes && genericTypesContainTuple(generics)) { + GenericTypes ownerGenerics = generics.take(owningClass.getTypeVariables().size()); + specializeClass(owningClass, ownerGenerics); + rewriteOwnedGenericGlobals(newImplementation, owningClass, ownerGenerics); + } collectGenericNewUses(newImplementation); return newImplementation; } @@ -1673,6 +1904,19 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { for (int i = 0; i < c.getFields().size() && i < newC.getFields().size(); i++) { translator.recordSpecialisation(newC.getFields().get(i), c.getFields().get(i), generics.getTypeArguments()); } + for (int i = 0; i < c.getMethods().size() && i < newC.getMethods().size(); i++) { + ImMethod originalMethod = c.getMethods().get(i); + ImMethod copiedMethod = newC.getMethods().get(i); + translator.recordSpecialisation(copiedMethod, originalMethod, generics.getTypeArguments()); + if (originalMethod.getImplementation() != null && copiedMethod.getImplementation() != null) { + translator.recordSpecialisation(copiedMethod.getImplementation(), + originalMethod.getImplementation(), generics.getTypeArguments()); + } + } + for (int i = 0; i < c.getFunctions().size() && i < newC.getFunctions().size(); i++) { + translator.recordSpecialisation(newC.getFunctions().get(i), c.getFunctions().get(i), + generics.getTypeArguments()); + } translator.recordSpecialisation(newC, c, generics.getTypeArguments()); specializedClasses.put(c, generics, newC); prog.getClasses().add(newC); @@ -1685,11 +1929,18 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { List typeVars = c.getTypeVariables(); rewriteGenerics(newC, generics, typeVars); newC.getSuperClasses().replaceAll(this::specializeType); + if (needsRuntimeTypeSpecialization(c, generics, new HashSet<>())) { + rewriteRuntimeTypeSuperEdges(c, generics, newC); + } // NEW: Create specialized global variables for this class instantiation createSpecializedGlobals(c, generics, typeVars); + if (specializeTupleValueTypes && genericTypesContainTuple(generics)) { + rewriteOwnedGenericGlobals(newC, c, generics); + } - if (genericNewOnly && isConstructionOnlyInstantiation(c)) { + if (genericNewOnly && (isConstructionOnlyInstantiation(c) + || (specializeTupleValueTypes && genericTypesContainTuple(generics)))) { attachSpecializedClassMethods(c, newC, generics); } @@ -1771,24 +2022,25 @@ private ImExpr rewriteGenericGlobalsInExpr(ImExpr e, ImClass owningClass, Generi } private void createSpecializedGlobals(ImClass originalClass, GenericTypes generics, List typeVars) { - String key = gKey(generics); - // Collect "insert specialized init right after original init" operations per parent ImStmts // Using identity maps because IM nodes use identity semantics for parent/ownership. Map>> insertsByParent = new IdentityHashMap<>(); + List> newlyCreated = new ArrayList<>(); + // Establish the complete declaration environment before translating any initializer. An + // initializer may refer to a later static of the same class; interleaving declaration and + // initializer lowering would make correctness depend on global iteration order. for (Map.Entry entry : globalToClass.entrySet()) { ImVar originalGlobal = entry.getKey(); ImClass owningClass = entry.getValue(); - // be robust: sometimes class objects differ; name match is good enough here - if (owningClass != originalClass && !owningClass.getName().equals(originalClass.getName())) continue; + if (translator.canonical(owningClass) != translator.canonical(originalClass)) continue; - if (specializedGlobals.contains(originalGlobal, key)) continue; + if (specializedGlobals.contains(originalGlobal, generics)) continue; ImType specializedType = transformType(originalGlobal.getType(), generics, typeVars); - String specializedName = originalGlobal.getName() + "⟪" + key + "⟫"; + String specializedName = originalGlobal.getName() + "⟪" + generics.makeName() + "⟫"; ImVar specializedGlobal = JassIm.ImVar( originalGlobal.getTrace(), specializedType, @@ -1803,8 +2055,15 @@ private void createSpecializedGlobals(ImClass originalClass, GenericTypes generi translator.recordSpecialisation(specializedGlobal, originalGlobal, generics.getTypeArguments()); translator.recordGenericStaticOwner(specializedGlobal, originalClass); translator.recordGenericStaticOwner(originalGlobal, originalClass); - specializedGlobals.put(originalGlobal, key, specializedGlobal); + specializedGlobals.put(originalGlobal, generics, specializedGlobal); dbg("Created specialized global: " + specializedName + " type=" + specializedType); + newlyCreated.add(Map.entry(originalGlobal, specializedGlobal)); + } + + for (Map.Entry specialization : newlyCreated) { + ImVar originalGlobal = specialization.getKey(); + ImVar specializedGlobal = specialization.getValue(); + ImType specializedType = specializedGlobal.getType(); // If original has init(s), create corresponding specialized init(s) and schedule insertion List originalInits = prog.getGlobalInits().get(originalGlobal); @@ -2425,19 +2684,18 @@ private ImVar ensureSpecializedGlobal(ImVar originalGlobal, ImClass owningClass, concreteGenerics = normalizeToClassArity(concreteGenerics, owningClass, "ensureSpecializedGlobal:" + originalGlobal.getName()); if (concreteGenerics == null) return null; - String key = gKey(concreteGenerics); - ImVar sg = specializedGlobals.get(originalGlobal, key); + ImVar sg = specializedGlobals.get(originalGlobal, concreteGenerics); if (sg != null) return sg; // Ensure class specialization exists (this should also call createSpecializedGlobals) specializeClass(owningClass, concreteGenerics); - sg = specializedGlobals.get(originalGlobal, key); + sg = specializedGlobals.get(originalGlobal, concreteGenerics); if (sg != null) return sg; - // Absolute fallback: force-create (in case specializeClass was short-circuited) - createSpecializedGlobals(owningClass, concreteGenerics, owningClass.getTypeVariables()); - return specializedGlobals.get(originalGlobal, key); + throw new CompileError(originalGlobal, + "Generic static specialization was not created for " + originalGlobal.getName() + + " in " + owningClass.getName() + " with " + concreteGenerics + "."); } /** @@ -2508,10 +2766,7 @@ class GenericGlobalArrayAccess implements GenericUse { return r; } - /** - * NEW: Infer generic types from the enclosing function context - * For specialized functions, the name contains the type information - */ + /** Infer class type arguments from the enclosing function's structural specialization context. */ private GenericTypes inferGenericsFromFunction(Element element, ImClass owningClass) { Element current = element; while (current != null) { @@ -2533,10 +2788,10 @@ private GenericTypes inferGenericsFromFunction(Element element, ImClass owningCl ImClassType ct = (ImClassType) rt; ImClass raw = ct.getClassDef(); - boolean matches = - raw.getName().equals(owningClass.getName()) || - raw.getName().startsWith(owningClass.getName() + "⟪") || - raw.isSubclassOf(owningClass); + ImClass canonicalRaw = translator.canonical(raw); + ImClass canonicalOwner = translator.canonical(owningClass); + boolean matches = canonicalRaw == canonicalOwner + || canonicalRaw.isSubclassOf(canonicalOwner); if (matches) { if (!ct.getTypeArguments().isEmpty()) { @@ -2547,9 +2802,12 @@ private GenericTypes inferGenericsFromFunction(Element element, ImClass owningCl return normalizeToClassArity(new GenericTypes(copied), owningClass, "receiverTypeArgs:" + func.getName()); } - GenericTypes fromName = extractGenericsFromClassName(raw.getName()); - if (fromName != null && !fromName.getTypeArguments().isEmpty()) { - return normalizeToClassArity(fromName, owningClass, "className:" + raw.getName()); + ImTranslator.Specialisation classSpecialisation = translator.specialisationOf(raw); + if (classSpecialisation != null + && !classSpecialisation.typeArguments().isEmpty()) { + return normalizeToClassArity( + new GenericTypes(classSpecialisation.typeArguments()), owningClass, + "receiverSpecialisation:" + func.getName()); } } } @@ -2561,89 +2819,6 @@ private GenericTypes inferGenericsFromFunction(Element element, ImClass owningCl } return null; } - - - - /** - * NEW: Extract generic types from a specialized class name like "Box⟪integer⟫" - */ - private GenericTypes extractGenericsFromClassName(String className) { - int start = className.indexOf('⟪'); - int end = className.lastIndexOf('⟫'); - if (start < 0 || end < 0 || end <= start + 1) return null; - - String payload = className.substring(start + 1, end).trim(); - List parts = splitTopLevel(payload); // comma-split with bracket depth - List args = new ArrayList<>(parts.size()); - for (String p : parts) { - ImType t = parseTypeAtom(p.trim()); - args.add(JassIm.ImTypeArgument(t, Collections.emptyMap())); - } - return new GenericTypes(args); - } - - /** split by commas at top level, respecting both ⟪⟫ and ⦅⦆ */ - private List splitTopLevel(String s) { - List res = new ArrayList<>(); - StringBuilder cur = new StringBuilder(); - int depthAngle = 0, depthTuple = 0; - for (int i = 0; i < s.length(); i++) { - char ch = s.charAt(i); - if (ch == '⟪') depthAngle++; - else if (ch == '⟫') depthAngle--; - else if (ch == '⦅') depthTuple++; - else if (ch == '⦆') depthTuple--; - - if (ch == ',' && depthAngle == 0 && depthTuple == 0) { - res.add(cur.toString()); - cur.setLength(0); - continue; - } - cur.append(ch); - } - if (cur.length() > 0) res.add(cur.toString()); - return res; - } - - /** parse simple atoms and tuples like ⦅integer, integer⦆ (can nest) */ - private ImType parseTypeAtom(String s) { - s = s.trim(); - // tuple - if (s.startsWith("⦅") && s.endsWith("⦆")) { - String inner = s.substring(1, s.length() - 1).trim(); - List elems = splitTopLevel(inner); - List tt = new ArrayList<>(); - List names = Lists.newArrayList(); - int i = 1; - for (String e : elems) { - tt.add(parseTypeAtom(e)); - names.add("" + i++); - } - return JassIm.ImTupleType(tt, names); - } - - // common simples - switch (s) { - case "integer": - case "int": return JassIm.ImSimpleType("integer"); - case "real": return JassIm.ImSimpleType("real"); - case "boolean": - case "bool": return JassIm.ImSimpleType("boolean"); - case "string": return JassIm.ImSimpleType("string"); - } - - // class type without visible args here - for (ImClass c : prog.getClasses()) { - if (c.getName().equals(s)) { - return JassIm.ImClassType(c, JassIm.ImTypeArguments()); - } - } - // fallback: simple type with this name - return JassIm.ImSimpleType(s); - } - - - class GenericVar implements GenericUse { private final ImVar mc; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java index f292c7f98..5e2c8f5eb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java @@ -23,37 +23,175 @@ public class EliminateTuples { public static void eliminateTuplesProg(ImProg imProg, ImTranslator translator) { + DiscardEvaluation discardEvaluation = new DiscardEvaluation(imProg); + List removeOldVars = new ArrayList<>(); + removeOldVars.add(transformVars(imProg.getGlobals(), translator)); + for (ImClass c : imProg.getClasses()) { + removeOldVars.add(transformVars(c.getFields(), translator)); + } - Runnable removeOldGlobals = transformVars(imProg.getGlobals(), translator); - for (ImFunction f : imProg.getFunctions()) { + shareTupleReturnSlotsAcrossOverrides(imProg, translator); + List functions = allFunctions(imProg); + for (ImFunction f : functions) { transformFunctionReturnsAndParameters(f, translator); } - for (ImFunction f : imProg.getFunctions()) { - eliminateTuplesFunc(f, translator); + for (ImFunction f : functions) { + eliminateTuplesFunc(f, translator, discardEvaluation); + } + removeOldVars.forEach(Runnable::run); + assertNoTuples(imProg); + } + + private static void assertNoTuples(Element element) { + AssertProperty.NOTUPLES.check(element); + for (int i = 0; i < element.size(); i++) { + assertNoTuples(element.get(i)); + } + } + + private static List allFunctions(ImProg prog) { + LinkedHashSet result = new LinkedHashSet<>(prog.getFunctions()); + for (ImClass c : prog.getClasses()) { + result.addAll(c.getFunctions()); + } + return new ArrayList<>(result); + } + + /** Lua retains virtual methods, so all implementations in a dispatch group must write + * additional tuple return components to the same scalar return slots. */ + private static void shareTupleReturnSlotsAcrossOverrides(ImProg prog, ImTranslator translator) { + List methods = new ArrayList<>(); + Set knownMethods = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ImMethod method : prog.getMethods()) { + if (knownMethods.add(method)) { + methods.add(method); + } + } + for (ImClass c : prog.getClasses()) { + for (ImMethod method : c.getMethods()) { + if (knownMethods.add(method)) { + methods.add(method); + } + } + } + for (int i = 0; i < methods.size(); i++) { + for (ImMethod subMethod : methods.get(i).getSubMethods()) { + if (knownMethods.add(subMethod)) { + methods.add(subMethod); + } + } + } + + Map parents = new IdentityHashMap<>(); + Map methodForImplementation = new IdentityHashMap<>(); + for (ImMethod method : methods) { + parents.put(method, method); + } + for (ImMethod method : methods) { + for (ImMethod subMethod : method.getSubMethods()) { + unionMethods(method, subMethod, parents); + } + ImFunction implementation = method.getImplementation(); + if (implementation != null) { + ImMethod other = methodForImplementation.putIfAbsent(implementation, method); + if (other != null) { + unionMethods(method, other, parents); + } + } + } + + Map> groupsByRoot = new IdentityHashMap<>(); + List> groups = new ArrayList<>(); + for (ImMethod method : methods) { + ImMethod root = findMethodRoot(method, parents); + List group = groupsByRoot.get(root); + if (group == null) { + group = new ArrayList<>(); + groupsByRoot.put(root, group); + groups.add(group); + } + group.add(method); + } + + for (List group : groups) { + VarsForTupleResult shared = null; + for (ImMethod method : group) { + ImFunction implementation = method.getImplementation(); + if (implementation != null + && translator.getOriginalReturnValue(implementation) instanceof ImTupleType) { + shared = translator.getTupleTempReturnVarsFor(implementation); + break; + } + } + if (shared == null) { + continue; + } + for (ImMethod method : group) { + ImFunction implementation = method.getImplementation(); + if (implementation != null + && translator.getOriginalReturnValue(implementation) instanceof ImTupleType) { + translator.setTupleTempReturnVarsFor(implementation, shared); + } + } + } + } + + private static ImMethod findMethodRoot(ImMethod method, Map parents) { + ImMethod parent = parents.get(method); + if (parent != method) { + parent = findMethodRoot(parent, parents); + parents.put(method, parent); + } + return parent; + } + + private static void unionMethods(ImMethod left, ImMethod right, Map parents) { + ImMethod leftRoot = findMethodRoot(left, parents); + ImMethod rightRoot = findMethodRoot(right, parents); + if (leftRoot != rightRoot) { + parents.put(rightRoot, leftRoot); } - removeOldGlobals.run(); - translator.assertProperties(AssertProperty.NOTUPLES); } private static void transformFunctionReturnsAndParameters(ImFunction f, ImTranslator translator) { + preserveVarargParameter(f); transformVars(f.getParameters(), translator).run(); translator.setOriginalReturnValue(f, f.getReturnType()); f.setReturnType(getFirstType(f.getReturnType())); } + /** + * A Lua vararg is represented by one placeholder parameter which the backend renames to `...`. + * Keep that single parameter even when each source element is a tuple; calls are flattened, and + * the vararg loop regroups those scalar values into the loop variable's scalar leaves. + */ + private static void preserveVarargParameter(ImFunction f) { + if (!f.hasFlag(FunctionFlagEnum.IS_VARARG) || f.getParameters().isEmpty()) { + return; + } + ImVar parameter = f.getParameters().getLast(); + if (TypesHelper.typeContainsTuples(parameter.getType())) { + parameter.setType(getFirstType(parameter.getType()).copy()); + } + } + - private static void eliminateTuplesFunc(ImFunction f, final ImTranslator translator) { + private static void eliminateTuplesFunc(ImFunction f, final ImTranslator translator, + DiscardEvaluation discardEvaluation) { transformVars(f.getLocals(), translator).run(); tryStep(f, translator, EliminateTuples::toTupleExpressions); tryStep(f, translator, EliminateTuples::normalizeTuplesInStatementExprs); - tryStep(f, translator, EliminateTuples::removeTupleSelections); + tryStep(f, translator, (stmts, tr, fn) -> + removeTupleSelections(stmts, tr, fn, discardEvaluation)); tryStep(f, translator, EliminateTuples::normalizeTuplesInStatementExprs); - tryStep(f, translator, (stmts, translator1, fn) -> removeTupleExprs(0, stmts, translator1, fn)); + tryStep(f, translator, (stmts, translator1, fn) -> + removeTupleExprs(0, stmts, translator1, fn, discardEvaluation)); } - private static void removeTupleSelections(ImStmts stmts, ImTranslator tr, ImFunction f) { + private static void removeTupleSelections(ImStmts stmts, ImTranslator tr, ImFunction f, + DiscardEvaluation discardEvaluation) { Replacer replacer = new Replacer(); stmts.accept(new Element.DefaultVisitor() { @Override @@ -80,10 +218,20 @@ public void visit(ImTupleSelection ts) { de.peeeq.wurstscript.ast.Element trace = te.attrTrace(); te.setParent(null); if (i != ti) { - // if not the thing we want to return, just keep it in statements for side-effects - extractSideEffect(te, stmts); + // Constructing a tuple evaluates every component. A read can be free of + // side effects and still fail (for example, a member access on null), so + // only values proven trivial to evaluate may disappear here. + ImExpr remaining = extractSideEffect(te, stmts); + retainDiscardedValue(remaining, stmts, tr, discardEvaluation); } else { // if it is the part we want to return ... - result = extractSideEffect(te, stmts); + ImExpr selected = extractSideEffect(te, stmts); + if (i < tupleExpr.getExprs().size() - 1 && !ts.isUsedAsLValue()) { + // Later tuple components still have to run, but the selected value is + // evaluated at its original position in the tuple's left-to-right order. + result = captureSelectedValue(selected, stmts, f); + } else { + result = selected; + } } } assert result != null; @@ -98,6 +246,93 @@ public void visit(ImTupleSelection ts) { }); } + private static ImExpr captureSelectedValue(ImExpr selected, ImStmts stmts, ImFunction f) { + if (selected instanceof ImTupleExpr tuple) { + ImExprs captured = JassIm.ImExprs(); + for (ImExpr component : tuple.getExprs()) { + component.setParent(null); + captured.add(captureSelectedValue(extractSideEffect(component, stmts), stmts, f)); + } + return JassIm.ImTupleExpr(captured); + } + ImVar temp = JassIm.ImVar(selected.attrTrace(), selected.attrTyp(), "tupleSelection", false); + f.getLocals().add(temp); + selected.setParent(null); + stmts.add(JassIm.ImSet(selected.attrTrace(), JassIm.ImVarAccess(temp), selected)); + return JassIm.ImVarAccess(temp); + } + + private static void retainDiscardedValue(ImExpr value, ImStmts stmts, ImTranslator tr, + DiscardEvaluation discardEvaluation) { + if (value instanceof ImTupleExpr tuple) { + for (ImExpr component : tuple.getExprs()) { + component.setParent(null); + retainDiscardedValue(extractSideEffect(component, stmts), stmts, tr, + discardEvaluation); + } + return; + } + if (isTriviallyDiscardable(value)) { + return; + } + if (SideEffectAnalyzer.quickcheckHasSideeffects(value)) { + value.setParent(null); + stmts.add(value); + } else if (tr.isLuaTarget()) { + value.setParent(null); + stmts.add(discardEvaluation.call(value)); + } + } + + private static boolean isTriviallyDiscardable(ImExpr value) { + return value instanceof ImBoolVal + || value instanceof ImIntVal + || value instanceof ImRealVal + || value instanceof ImStringVal + || value instanceof ImNull + || value instanceof ImVarAccess + || value instanceof ImFuncRef; + } + + /** + * Lua must evaluate unused tuple components which can still trap. Passing such a value to a + * tiny non-native sink makes argument evaluation explicit and keeps later optimizers from + * deleting it as an unread local assignment. One sink is shared by every scalar IM type. + */ + private static final class DiscardEvaluation { + private final ImProg prog; + private final List functionsByType = new ArrayList<>(); + + private record DiscardFunction(ImType type, ImFunction function) { + } + + private DiscardEvaluation(ImProg prog) { + this.prog = prog; + } + + private ImFunctionCall call(ImExpr value) { + ImType type = value.attrTyp(); + ImFunction sink = functionsByType.stream() + .filter(entry -> entry.type().equalsType(type)) + .map(DiscardFunction::function) + .findFirst() + .orElseGet(() -> createSink(value, type)); + return JassIm.ImFunctionCall(value.attrTrace(), sink, JassIm.ImTypeArguments(), + JassIm.ImExprs(value), false, CallType.NORMAL); + } + + private ImFunction createSink(ImExpr value, ImType type) { + ImVar parameter = JassIm.ImVar(value.attrTrace(), type.copy(), "value", false); + ImFunction sink = JassIm.ImFunction(value.attrTrace(), + "__wurst_tuple_discard_" + functionsByType.size(), JassIm.ImTypeVars(), + JassIm.ImVars(parameter), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.emptyList()); + functionsByType.add(new DiscardFunction(type.copy(), sink)); + prog.getFunctions().add(sink); + return sink; + } + } + interface Step { void apply(ImStmts e, ImTranslator t, ImFunction f); } @@ -199,13 +434,8 @@ public void visit(ImVarArrayAccess va) { ImExprs indexes = va.getIndexes(); ImExprs indexExprs = JassIm.ImExprs(); ImStmts stmts = JassIm.ImStmts(); - boolean sideEffects = false; - for (ImExpr index : indexes) { - if (SideEffectAnalyzer.quickcheckHasSideeffects(index)) { - sideEffects = true; - break; - } - } + boolean sideEffects = indexes.stream() + .anyMatch(SideEffectAnalyzer::quickcheckHasSideeffects); for (ImExpr ie : indexes) { if (sideEffects) { // use temp variables if there are side effects @@ -237,6 +467,28 @@ public void visit(ImVarArrayAccess va) { } } + @Override + public void visit(ImMemberAccess ma) { + super.visit(ma); + if (ma.attrTyp() instanceof ImTupleType) { + ImStmts stmts = JassIm.ImStmts(); + boolean indexesAreEffectful = ma.getIndexes().stream() + .anyMatch(SideEffectAnalyzer::quickcheckHasSideeffects); + ImExpr receiver = captureOnceIfNeeded(ma.getReceiver(), "tupleReceiver", stmts, + f, indexesAreEffectful); + ImExprs indexes = captureIndexesOnceIfNeeded(ma.getIndexes(), stmts, f); + VarsForTupleResult vars = translator.getVarsForTuple(ma.getVar()); + ImExpr replacement = vars.map( + parts -> JassIm.ImTupleExpr(parts.collect(Collectors.toCollection(JassIm::ImExprs))), + var -> JassIm.ImMemberAccess(ma.getTrace(), receiver.copy(), ma.getTypeArguments().copy(), + var, indexes.copy())); + if (!stmts.isEmpty()) { + replacement = JassIm.ImStatementExpr(stmts, replacement); + } + replacer.replace(ma, replacement); + } + } + @Override public void visit(ImFunctionCall fc) { @@ -261,9 +513,54 @@ public void visit(ImFunctionCall fc) { } } + @Override + public void visit(ImMethodCall mc) { + super.visit(mc); + ImFunction implementation = mc.getMethod().getImplementation(); + if (implementation != null && translator.getOriginalReturnValue(implementation) instanceof ImTupleType) { + Element parent = mc.getParent(); + mc.setParent(null); + VarsForTupleResult returnVars = translator.getTupleTempReturnVarsFor(implementation); + ImVar firstVar = returnVars.allValuesStream().findFirst().get(); + ImExpr newCall = returnVars.map( + parts -> JassIm.ImTupleExpr(parts.collect(Collectors.toCollection(JassIm::ImExprs))), + var -> var == firstVar ? mc.copy() : JassIm.ImVarAccess(var)); + replacer.replaceInParent(parent, mc, newCall); + } + } + }); } + private static ImExpr captureOnceIfNeeded(ImExpr expr, String name, ImStmts stmts, ImFunction f, + boolean forceCapture) { + if (!forceCapture && !SideEffectAnalyzer.quickcheckHasSideeffects(expr)) { + return expr; + } + ImVar temp = JassIm.ImVar(expr.attrTrace(), expr.attrTyp(), name, false); + f.getLocals().add(temp); + expr.setParent(null); + stmts.add(JassIm.ImSet(expr.attrTrace(), JassIm.ImVarAccess(temp), expr)); + return JassIm.ImVarAccess(temp); + } + + private static ImExprs captureIndexesOnceIfNeeded(ImExprs original, ImStmts stmts, ImFunction f) { + boolean capture = original.stream().anyMatch(SideEffectAnalyzer::quickcheckHasSideeffects); + ImExprs result = JassIm.ImExprs(); + for (ImExpr index : original) { + if (capture) { + ImVar temp = JassIm.ImVar(index.attrTrace(), index.attrTyp(), "tupleIndex", false); + f.getLocals().add(temp); + index.setParent(null); + stmts.add(JassIm.ImSet(index.attrTrace(), JassIm.ImVarAccess(temp), index)); + result.add(JassIm.ImVarAccess(temp)); + } else { + result.add(index.copy()); + } + } + return result; + } + /** * Normalize Tuples in statement-expressions (move to first tuple param) @@ -308,13 +605,14 @@ private static ImTupleExpr normalizeStatementExpr(ImStatementExpr se, ImTranslat * - Assignments: Become several assignments * - In Return: Use temp returns */ - private static void removeTupleExprs(int posHint, Element elem, ImTranslator translator, ImFunction f) { + private static void removeTupleExprs(int posHint, Element elem, ImTranslator translator, + ImFunction f, DiscardEvaluation discardEvaluation) { if (elem.getParent() == null) { throw new RuntimeException("elem not used: " + elem); } for (int i = 0; i < elem.size(); i++) { Element child = elem.get(i); - removeTupleExprs(i, child, translator, f); + removeTupleExprs(i, child, translator, f, discardEvaluation); } Replacer replacer = new Replacer(); for (int i = 0; i < elem.size(); i++) { @@ -330,12 +628,17 @@ private static void removeTupleExprs(int posHint, Element elem, ImTranslator tra newElem = inReturn((ImReturn) elem, tupleExpr, translator, f); } else if (elem instanceof ImSet) { ImSet imSet = (ImSet) elem; - newElem = inSet(imSet, f); + newElem = inSet(imSet, translator, f); } else if (elem instanceof ImExprs) { ImExprs exprs = (ImExprs) elem; if (exprs.getParent() instanceof ImOperatorCall) { ImOperatorCall opCall = (ImOperatorCall) exprs.getParent(); - handleTupleInOpCall(replacer, opCall); + handleTupleInOpCall(replacer, opCall, f, discardEvaluation); + return; + } else if (exprs.getParent() instanceof ImFunctionCall + || exprs.getParent() instanceof ImMethodCall) { + ImExpr call = (ImExpr) exprs.getParent(); + replacer.replace(call, stageTupleCallArguments(call, exprs, f)); return; } else { // in function arguments, other tuples @@ -368,7 +671,43 @@ private static void removeTupleExprs(int posHint, Element elem, ImTranslator tra } - private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall) { + private static ImStatementExpr stageTupleCallArguments(ImExpr call, ImExprs arguments, + ImFunction f) { + ImStmts evaluation = JassIm.ImStmts(); + boolean forceOrder = arguments.stream().anyMatch(EliminateTuples::needsOrderedCapture); + if (call instanceof ImMethodCall methodCall) { + forceOrder |= needsOrderedCapture(methodCall.getReceiver()); + } + + // A dynamic receiver is evaluated before the arguments in the source program. Keep it in + // the same ordered prelude as the flattened tuple components. + if (call instanceof ImMethodCall methodCall) { + ImExpr receiver = methodCall.getReceiver(); + receiver.setParent(null); + OrderedBundle receiverBundle = lowerBundle(receiver, f, forceOrder, + "tuple_argument_receiver"); + receiverBundle.appendPreludeTo(evaluation); + if (receiverBundle.values.size() != 1) { + throw new CompileError(call.attrTrace(), "A method receiver cannot be a tuple."); + } + methodCall.setReceiver(receiverBundle.values.getFirst()); + } + + List originalArguments = arguments.removeAll(); + for (ImExpr argument : originalArguments) { + argument.setParent(null); + OrderedBundle argumentBundle = lowerBundle(argument, f, forceOrder, "tuple_argument"); + argumentBundle.appendPreludeTo(evaluation); + arguments.addAll(argumentBundle.values); + } + + // Keep the original node in place until Replacer has found its parent. The detached copy is + // the scalar-only call evaluated after the complete left-to-right argument prelude. + return JassIm.ImStatementExpr(evaluation, (ImExpr) call.copy()); + } + + private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall, ImFunction f, + DiscardEvaluation discardEvaluation) { if (opCall.getParent() == null) { throw new RuntimeException("opCall not used: " + opCall); } @@ -376,12 +715,20 @@ private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall ImTupleExpr right = (ImTupleExpr) opCall.getArguments().get(1); WurstOperator op = opCall.getOp(); + ImStmts evaluation = JassIm.ImStmts(); + boolean forceOrder = needsOrderedCapture(left) || needsOrderedCapture(right); + List leftComponents = captureTupleComponents(left, evaluation, f, forceOrder, + discardEvaluation); + List rightComponents = captureTupleComponents(right, evaluation, f, forceOrder, + discardEvaluation); + if (leftComponents.size() != rightComponents.size()) { + throw new CompileError(opCall.attrTrace(), "Cannot compare tuples with different arity."); + } + List componentComparisons = new ArrayList<>(); - for (int i = 0; i < left.getExprs().size(); i++) { - ImExpr l = left.getExprs().get(i); - ImExpr r = right.getExprs().get(i); - l.setParent(null); - r.setParent(null); + for (int i = 0; i < leftComponents.size(); i++) { + ImExpr l = leftComponents.get(i); + ImExpr r = rightComponents.get(i); componentComparisons.add(JassIm.ImOperatorCall(op, JassIm.ImExprs(l, r))); } @@ -418,12 +765,42 @@ private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall newExpr = (seen ? Optional.of(acc) : Optional.empty()) .get(); } - replacer.replace(opCall, newExpr); + replacer.replace(opCall, JassIm.ImStatementExpr(evaluation, newExpr)); + } + + private static List captureTupleComponents(ImTupleExpr tuple, ImStmts evaluation, + ImFunction f, boolean forceOrder, + DiscardEvaluation discardEvaluation) { + OrderedBundle bundle = lowerBundle(tuple, f, forceOrder, "tuple_compare", true); + bundle.appendPreludeTo(evaluation); + for (int i = 0; i < bundle.values.size(); i++) { + if (bundle.requiresEagerEvaluation.get(i)) { + evaluation.add(discardEvaluation.call(bundle.values.get(i).copy())); + } + } + return bundle.values; } - private static ImStatementExpr inSet(ImSet imSet, ImFunction f) { + private static ImStatementExpr inSet(ImSet imSet, ImTranslator translator, ImFunction f) { + registerConcreteTupleStorage(imSet.getLeft(), imSet.getRight(), translator); + registerConcreteTupleStorage(imSet.getRight(), imSet.getLeft(), translator); + if (!(imSet.getLeft() instanceof ImTupleExpr) && imSet.getRight() instanceof ImTupleExpr) { + ImTupleExpr expanded = expandTupleStorageAccess(imSet.getLeft(), translator); + if (expanded != null) { + imSet.setLeft(expanded); + } + } + if (!(imSet.getRight() instanceof ImTupleExpr) && imSet.getLeft() instanceof ImTupleExpr + && imSet.getRight() instanceof ImLExpr) { + ImTupleExpr expanded = expandTupleStorageAccess((ImLExpr) imSet.getRight(), translator); + if (expanded != null) { + imSet.setRight(expanded); + } + } if (!(imSet.getLeft() instanceof ImTupleExpr && imSet.getRight() instanceof ImTupleExpr)) { - throw new RuntimeException("invalid set statement:\n" + imSet); + throw new RuntimeException("invalid set statement:\n" + imSet + + "\nleft type=" + imSet.getLeft().attrTyp() + + " right type=" + imSet.getRight().attrTyp()); } ImTupleExpr left = (ImTupleExpr) imSet.getLeft(); ImTupleExpr right = (ImTupleExpr) imSet.getRight(); @@ -433,14 +810,14 @@ private static ImStatementExpr inSet(ImSet imSet, ImFunction f) { // 1) Flatten LHS into L-values (recursively), hoisting side-effects List lhsLeaves = new ArrayList<>(); for (ImExpr e : left.getExprs()) { - flattenLhsTuple(e, lhsLeaves, stmts); + flattenLhsTuple(e, lhsLeaves, stmts, f); } - // 2) Flatten RHS into expressions (recursively), expanding null to defaults, hoisting side-effects - List rhsLeaves = new ArrayList<>(); - for (ImExpr e : right.getExprs()) { - flattenRhsTuple(e, rhsLeaves, stmts); - } + // 2) Capture RHS leaves at their original positions. Assignment always forces capture for + // non-immutable leaves: this preserves swaps/aliasing even when the RHS itself is pure. + OrderedBundle rhs = lowerBundle(right, f, true, "tuple_assignment"); + rhs.appendPreludeTo(stmts); + List rhsLeaves = rhs.values; // 3) Pad / normalize RHS arity to match LHS arity (needed for nested tuples + null) for (int i = rhsLeaves.size(); i < lhsLeaves.size(); i++) { @@ -455,103 +832,189 @@ private static ImStatementExpr inSet(ImSet imSet, ImFunction f) { + "\nLHS=" + left + "\nRHS=" + right); } - boolean allLiteral = true; - for (ImExpr r : rhsLeaves) { - if (!isSimpleLiteral(r)) { - allLiteral = false; - break; - } - } - - if (allLiteral) { - for (int i = 0; i < lhsLeaves.size(); i++) { - ImLExpr l = lhsLeaves.get(i); - ImType targetT = l.attrTyp(); - ImExpr r = rhsLeaves.get(i); - if (r instanceof ImNull) { - r = ImHelper.defaultValueForComplexType(targetT); - } - l.setParent(null); - r.setParent(null); - stmts.add(JassIm.ImSet(imSet.getTrace(), l, r)); - } - return ImHelper.statementExprVoid(stmts); - } - - // 4) Evaluate RHS leaves first into temps (preserve side-effect order & alias safety) - List temps = new ArrayList<>(rhsLeaves.size()); - for (int i = 0; i < rhsLeaves.size(); i++) { + // 4) Publish only after every RHS component has been captured. + for (int i = 0; i < lhsLeaves.size(); i++) { ImLExpr l = lhsLeaves.get(i); - ImType targetT = l.attrTyp(); - ImExpr r = rhsLeaves.get(i); - - // if a scalar null slipped through, replace with default of target type + ImExpr r = rhsLeaves.get(i); if (r instanceof ImNull) { - r = ImHelper.defaultValueForComplexType(targetT); + r = ImHelper.defaultValueForComplexType(l.attrTyp()); } - - ImVar t = JassIm.ImVar(r.attrTrace(), targetT, "tuple_temp", false); - f.getLocals().add(t); - - r.setParent(null); - stmts.add(JassIm.ImSet(r.attrTrace(), JassIm.ImVarAccess(t), r)); - temps.add(t); - } - - // 5) Now assign temps into LHS leaves - for (int i = 0; i < lhsLeaves.size(); i++) { - ImLExpr l = lhsLeaves.get(i); l.setParent(null); - stmts.add(JassIm.ImSet(imSet.getTrace(), l, JassIm.ImVarAccess(temps.get(i)))); + r.setParent(null); + stmts.add(JassIm.ImSet(imSet.getTrace(), l, r)); } return ImHelper.statementExprVoid(stmts); } - private static boolean isSimpleLiteral(ImExpr expr) { - return expr instanceof ImBoolVal - || expr instanceof ImIntVal - || expr instanceof ImRealVal - || expr instanceof ImStringVal - || expr instanceof ImNull; + private static void registerConcreteTupleStorage(ImExpr storage, ImExpr value, + ImTranslator translator) { + if (!(value.attrTyp() instanceof ImTupleType tupleType)) { + return; + } + ImVar var; + ImType concreteType; + if (storage instanceof ImVarAccess access) { + var = access.getVar(); + concreteType = tupleType.copy(); + } else if (storage instanceof ImVarArrayAccess access) { + var = access.getVar(); + concreteType = JassIm.ImArrayType(tupleType.copy()); + } else if (storage instanceof ImMemberAccess access) { + var = access.getVar(); + if (var.getType() instanceof ImArrayType) { + concreteType = JassIm.ImArrayType(tupleType.copy()); + } else { + concreteType = tupleType.copy(); + } + } else { + return; + } + translator.getVarsForTuple(var, concreteType); + } + + private static @org.eclipse.jdt.annotation.Nullable ImTupleExpr expandTupleStorageAccess( + ImLExpr left, ImTranslator translator) { + if (left instanceof ImVarAccess access) { + ImExpr expanded = translator.getVarsForTuple(access.getVar()).map( + parts -> JassIm.ImTupleExpr(parts.collect(Collectors.toCollection(JassIm::ImExprs))), + JassIm::ImVarAccess); + return expanded instanceof ImTupleExpr ? (ImTupleExpr) expanded : null; + } + if (left instanceof ImVarArrayAccess access) { + ImExpr expanded = translator.getVarsForTuple(access.getVar()).map( + parts -> JassIm.ImTupleExpr(parts.collect(Collectors.toCollection(JassIm::ImExprs))), + var -> JassIm.ImVarArrayAccess(access.getTrace(), var, access.getIndexes().copy())); + return expanded instanceof ImTupleExpr ? (ImTupleExpr) expanded : null; + } + if (left instanceof ImMemberAccess access) { + ImExpr expanded = translator.getVarsForTuple(access.getVar()).map( + parts -> JassIm.ImTupleExpr(parts.collect(Collectors.toCollection(JassIm::ImExprs))), + var -> JassIm.ImMemberAccess(access.getTrace(), access.getReceiver().copy(), + access.getTypeArguments().copy(), var, access.getIndexes().copy())); + return expanded instanceof ImTupleExpr ? (ImTupleExpr) expanded : null; + } + return null; } /** Flatten LHS recursively into addressable leaves (ImLExpr), hoisting side-effects */ - private static void flattenLhsTuple(ImExpr e, List out, ImStmts sideStmts) { + private static void flattenLhsTuple(ImExpr e, List out, ImStmts sideStmts, ImFunction f) { ImExpr x = extractSideEffect(e, sideStmts); if (x instanceof ImTupleExpr) { for (ImExpr sub : ((ImTupleExpr) x).getExprs()) { - flattenLhsTuple(sub, out, sideStmts); + flattenLhsTuple(sub, out, sideStmts, f); } } else { - out.add((ImLExpr) x); + out.add(captureLvalueAddress((ImLExpr) x, sideStmts, f)); } } - /** Flatten RHS recursively into leaves, expanding null to tuple of defaults, hoisting side-effects */ - private static void flattenRhsTuple(ImExpr e, List out, ImStmts sideStmts) { - ImExpr x = extractSideEffect(e, sideStmts); + /** Capture the address-bearing parts of an lvalue before evaluating the assignment RHS. */ + private static ImLExpr captureLvalueAddress(ImLExpr lvalue, ImStmts stmts, ImFunction f) { + if (lvalue instanceof ImMemberAccess access) { + ImExpr receiver = access.getReceiver(); + receiver.setParent(null); + access.setReceiver(captureValue(receiver, "tuple_lvalue_receiver", stmts, f)); + captureLvalueIndexes(access.getIndexes(), stmts, f); + } else if (lvalue instanceof ImVarArrayAccess access) { + captureLvalueIndexes(access.getIndexes(), stmts, f); + } + return lvalue; + } - // Expand typed nulls for tuple types so arities match - if (x instanceof ImNull) { - ImType t = ((ImNull) x).getType(); - if (t instanceof ImTupleType) { - ImExpr defaults = ImHelper.defaultValueForComplexType(t); // -> ImTupleExpr of defaults - flattenRhsTuple(defaults, out, sideStmts); - return; + private static void captureLvalueIndexes(ImExprs indexes, ImStmts stmts, ImFunction f) { + for (int i = 0; i < indexes.size(); i++) { + ImExpr index = indexes.get(i); + if (isImmutableValue(index)) { + continue; } + index.setParent(null); + indexes.set(i, captureValue(index, "tuple_lvalue_index", stmts, f)); } + } - if (x instanceof ImTupleExpr) { - for (ImExpr sub : ((ImTupleExpr) x).getExprs()) { - flattenRhsTuple(sub, out, sideStmts); + private static ImExpr captureValue(ImExpr value, String name, ImStmts stmts, ImFunction f) { + ImVar temp = JassIm.ImVar(value.attrTrace(), value.attrTyp(), name, false); + f.getLocals().add(temp); + stmts.add(JassIm.ImSet(value.attrTrace(), JassIm.ImVarAccess(temp), value)); + return JassIm.ImVarAccess(temp); + } + + /** + * A tuple value after lowering: statements which must run in source order, followed by its + * identity-free scalar components. When ordering matters, each mutable/effectful component is + * captured as soon as it is reached; a later component can therefore neither change an earlier + * read nor overwrite a shared tuple-return slot. + */ + private static final class OrderedBundle { + private final ImStmts prelude = JassIm.ImStmts(); + private final List values = new ArrayList<>(); + private final List requiresEagerEvaluation = new ArrayList<>(); + + private void appendPreludeTo(ImStmts target) { + for (ImStmt statement : prelude.removeAll()) { + statement.setParent(null); + target.add(statement); + } + } + } + + private static OrderedBundle lowerBundle(ImExpr expression, ImFunction f, boolean forceOrder, + String temporaryName) { + return lowerBundle(expression, f, forceOrder, temporaryName, false); + } + + private static OrderedBundle lowerBundle(ImExpr expression, ImFunction f, boolean forceOrder, + String temporaryName, + boolean capturePotentiallyFailingValues) { + OrderedBundle result = new OrderedBundle(); + lowerBundleInto(expression, f, forceOrder || needsOrderedCapture(expression), temporaryName, + capturePotentiallyFailingValues, result); + return result; + } + + private static void lowerBundleInto(ImExpr expression, ImFunction f, boolean capture, + String temporaryName, boolean capturePotentiallyFailingValues, + OrderedBundle result) { + ImExpr value = extractSideEffect(expression, result.prelude); + if (value instanceof ImNull nullValue && nullValue.getType() instanceof ImTupleType) { + lowerBundleInto(ImHelper.defaultValueForComplexType(nullValue.getType()), f, capture, + temporaryName, capturePotentiallyFailingValues, result); + return; + } + if (value instanceof ImTupleExpr tuple) { + for (ImExpr component : new ArrayList<>(tuple.getExprs())) { + lowerBundleInto(component, f, capture, temporaryName, + capturePotentiallyFailingValues, result); } + return; + } + + value.setParent(null); + boolean requiresEagerEvaluation = capturePotentiallyFailingValues + && !isTriviallyDiscardable(value) + && !SideEffectAnalyzer.quickcheckHasSideeffects(value); + if ((capture || requiresEagerEvaluation) && !isImmutableValue(value)) { + result.values.add(captureValue(value, temporaryName, result.prelude, f)); } else { - out.add(x); + result.values.add(value); } + result.requiresEagerEvaluation.add(requiresEagerEvaluation); } + private static boolean needsOrderedCapture(ImExpr expression) { + return SideEffectAnalyzer.quickcheckHasSideeffects(expression); + } + private static boolean isImmutableValue(ImExpr expression) { + return expression instanceof ImBoolVal + || expression instanceof ImIntVal + || expression instanceof ImRealVal + || expression instanceof ImStringVal + || expression instanceof ImNull + || expression instanceof ImFuncRef + || expression instanceof ImTypeIdOfClass; + } private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, ImTranslator translator, ImFunction f) { @@ -561,64 +1024,36 @@ private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, ImStmts stmts = JassIm.ImStmts(); - // 1) Flatten the RHS tuple expression (preserving side effects) - List flatExprs = new ArrayList<>(); - flattenTupleExpr(tupleExpr, stmts, flatExprs); + // 1) Lower and, where necessary, capture each component at its original evaluation point. + OrderedBundle result = lowerBundle(tupleExpr, f, false, "tuple_return"); + result.appendPreludeTo(stmts); // Sanity: - if (flatExprs.size() != returnVars.size()) { + if (result.values.size() != returnVars.size()) { throw new CompileError(parent.getTrace(), - "Cannot return tuple with " + flatExprs.size() + " element(s) from function expecting " + returnVars.size() + " element(s)"); + "Cannot return tuple with " + result.values.size() + + " element(s) from function expecting " + returnVars.size() + " element(s)"); } - // 2) Assign per component, converting nulls to proper defaults of LHS type + // 2) Publish only after the complete ordered bundle has been evaluated. Effectful bundles + // have already captured mutable reads and shared return slots in their prelude. for (int i = 0; i < returnVars.size(); i++) { ImVar rv = returnVars.get(i); - ImExpr rhs = flatExprs.get(i); + ImExpr rhs = result.values.get(i); rhs.setParent(null); if (rhs instanceof ImNull) { - // Use the *component target type* to build the correct default (0 for ints, - // (0,0) for tuple components if those ever occur, etc) - ImExpr defaultRhs = ImHelper.defaultValueForComplexType(rv.getType()); - stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(rv), defaultRhs)); - } else { - stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(rv), rhs)); + rhs = ImHelper.defaultValueForComplexType(rv.getType()); } + stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(returnVars.get(i)), + rhs)); } - // 3) Return the first component temp + // 3) Return the first component slot stmts.add(JassIm.ImReturn(parent.getTrace(), JassIm.ImVarAccess(returnVars.get(0)))); return ImHelper.statementExprVoid(stmts); } - private static void flattenTupleExpr(ImExpr e, ImStmts intoStmts, List out) { - // Hoist side-effects out of the way first: - ImExpr noSE = extractSideEffect(e, intoStmts); - - // NEW: expand null into a tuple of defaults so arity matches - if (noSE instanceof ImNull) { - ImType t = ((ImNull) noSE).getType(); // already the typed null - if (t instanceof ImTupleType) { - ImExpr defaultTuple = ImHelper.defaultValueForComplexType(t); // -> ImTupleExpr of defaults (recursively) - flattenTupleExpr(defaultTuple, intoStmts, out); - return; - } - } - - if (noSE instanceof ImTupleExpr) { - ImTupleExpr te = (ImTupleExpr) noSE; - for (ImExpr sub : te.getExprs()) { - flattenTupleExpr(sub, intoStmts, out); - } - } else { - out.add(noSE); - } - } - - - - private static Element inTupleSelection(ImTupleSelection ts, ImTupleExpr tupleExpr, ImFunction f) { assert ts.getTupleExpr() == tupleExpr; @@ -685,13 +1120,4 @@ private static ImExpr extractSideEffect(ImExpr e, List into) { } return e; } - - - private static ImExprs accessVars(List tempIndexes) { - return tempIndexes.stream() - .map(JassIm::ImVarAccess) - .collect(Collectors.toCollection(JassIm::ImExprs)); - } - - } 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 248d20691..d5feebe23 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 @@ -1925,6 +1925,33 @@ public VarsForTupleResult getVarsForTuple(ImVar v) { return result; } + /** Scalar leaves assigned for one source-level tuple variable, in source order. */ + public List getTupleScalarVars(ImVar v) { + return getVarsForTuple(v).allValuesStream().toList(); + } + + VarsForTupleResult getVarsForTuple(ImVar v, ImType concreteStorageType) { + if (!TypesHelper.typeContainsTuples(v.getType()) + && TypesHelper.typeContainsTuples(concreteStorageType)) { + VarsForTupleResult result = varsForTupleVar.get(v); + if (result != null) { + return result; + } + result = createVarsForType(v.getName(), concreteStorageType, Function.identity(), v.getTrace()); + varsForTupleVar.put(v, result); + if (v.getParent() instanceof ImVars owner) { + int position = owner.indexOf(v) + 1; + for (ImVar scalar : result.allValues()) { + if (!owner.contains(scalar)) { + owner.add(position++, scalar); + } + } + } + return result; + } + return getVarsForTuple(v); + } + /** * Creates variables for the given type, eliminating tuple types @@ -2046,6 +2073,10 @@ public VarsForTupleResult getTupleTempReturnVarsFor(ImFunction f) { return result; } + void setTupleTempReturnVarsFor(ImFunction f, VarsForTupleResult vars) { + tempReturnVars.put(f, vars); + } + private final Map originalReturnValues = Maps.newLinkedHashMap(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index 29c065da9..b4fc3b2d4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -1062,10 +1062,13 @@ private void initClassTables(ImClass c) { )); // set typeid metadata: + // Targeted Lua specialization changes storage, not nominal identity. Garbage reachability + // retains this canonical metadata dependency before emission. + ImClass typeIdClass = imTr.canonical(c); deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( LuaAst.LuaExprVarAccess(classVar), ExprTranslation.TYPE_ID), - LuaAst.LuaExprIntVal("" + prog.attrTypeId().get(c)) + LuaAst.LuaExprIntVal("" + prog.attrTypeId().get(typeIdClass)) )); @@ -1480,6 +1483,12 @@ private void collectSuperClasses(LuaTableFields superClasses, ImClass c, Set functions = new HashSet<>(); private final Set methods = new HashSet<>(); // methods that will be added once the class is used: private final Multimap waitingMethods = HashMultimap.create(); private final Set classes = new HashSet<>(); + /** Classes whose reachable runtime objects may dispatch virtual methods. */ + private final Set dispatchClasses = new HashSet<>(); + /** Classes which reachable code actually allocates, excluding nominal type-only references. */ + private final Set instantiatedClasses = new HashSet<>(); private final Set vars = new HashSet<>(); + private final Set ignoredInitializers; + + private Used(ImTranslator translator, Set ignoredInitializers) { + this.translator = translator; + this.ignoredInitializers = ignoredInitializers; + } public void addMethod(ImMethod m) { methods.add(m); @@ -32,7 +49,7 @@ public void addMethod(ImMethod m) { public void maybeVisitMethod(ImMethod m) { ImClass c = m.attrClass(); - if (classes.contains(c)) { + if (dispatchClasses.contains(c)) { visitMethod(m, this); } else { waitingMethods.put(c, m); @@ -51,6 +68,10 @@ public Set getClasses() { return classes; } + public Set getInstantiatedClasses() { + return instantiatedClasses; + } + public Set getVars() { return vars; } @@ -63,30 +84,50 @@ public void addVar(ImVar var) { vars.add(var); } - public void addClass(ImClass c) { - classes.add(c); - Collection imMethods = waitingMethods.get(c); - Iterator it = imMethods.iterator(); - while (it.hasNext()) { - ImMethod m = it.next(); - visitMethod(m, this); - it.remove(); + public boolean addClass(ImClass c, boolean dispatchReachable) { + boolean newClass = classes.add(c); + boolean newDispatchClass = dispatchReachable && dispatchClasses.add(c); + if (newClass) { + ImClass nominalClass = translator.canonical(c); + if (nominalClass != c) { + // A targeted specialization has a distinct storage layout but keeps the source + // class's nominal type id and instanceof identity. The canonical class is therefore + // a metadata dependency, not evidence that erased instances can dispatch. + visitClass(nominalClass, this, false); + } + } + if (newDispatchClass) { + Collection imMethods = waitingMethods.get(c); + Iterator it = imMethods.iterator(); + while (it.hasNext()) { + ImMethod m = it.next(); + visitMethod(m, this); + it.remove(); + } } + return newClass || newDispatchClass; } - } - public static void removeGarbage(ImProg prog, ImTranslator translator) { - Used used = new Used(); - for (ImFunction f : ImHelper.calculateFunctionsOfProg(prog)) { - if (f.getName().equals("main") - || f.getName().equals("config")) { - visitFunction(f, used); + public void addInstantiatedClass(ImClass c) { + if (!instantiatedClasses.add(c)) { + return; + } + for (ImClassType superClass : c.getSuperClasses()) { + addInstantiatedClass(superClass.getClassDef()); } } + } + + public static void removeGarbage(ImProg prog, ImTranslator translator) { + Used used = collectUsed(prog, translator); prog.getClasses().removeIf(c -> !used.getClasses().contains(c)); prog.getGlobals().removeIf(g -> !used.getVars().contains(g) && !TRVEHelper.protectedVariables.contains(g.getName())); prog.getFunctions().removeIf(f -> !used.getFunctions().contains(f)); + prog.getMethods().removeIf(m -> !used.getMethods().contains(m)); + for (ImMethod m : prog.getMethods()) { + m.getSubMethods().removeIf(sm -> !used.getMethods().contains(sm)); + } // A field of a specialised class is a copy which nothing refers to, an access made before // specialisation still naming the original's variable. It is live exactly when the field it // was copied from is; dropping it leaves an instance of the specialised class allocated with @@ -103,6 +144,73 @@ public static void removeGarbage(ImProg prog, ImTranslator translator) { } + private static Used collectUsed(ImProg prog, ImTranslator translator) { + return collectUsed(prog, translator, Collections.emptySet()); + } + + private static Used collectUsed(ImProg prog, ImTranslator translator, + Set ignoredInitializers) { + Used used = new Used(translator, ignoredInitializers); + for (ImFunction f : ImHelper.calculateFunctionsOfProg(prog)) { + if (f.getName().equals("main") + || f.getName().equals("config")) { + visitFunction(f, used); + } + } + return used; + } + + public static void removePhantomGenericStaticInitializers(ImProg prog, ImTranslator translator) { + Map> candidates = new LinkedHashMap<>(); + for (ImVar global : prog.getGlobals()) { + ImTranslator.Specialisation specialization = translator.specialisationOf(global); + if (specialization != null && specialization.original() instanceof ImVar original + && translator.genericStaticOwnerOf(original) != null) { + List initializers = prog.getGlobalInits().get(original); + if (initializers != null) { + candidates.putIfAbsent(original, initializers); + } + } + } + + // First ignore every erased initializer, then mark the originals referenced by real roots. + // Re-enable initializers of marked originals until their transitive dependencies are marked. + // This is graph reachability rather than deletion order, so unreachable initializer cycles + // cannot keep themselves alive. + Set liveOriginals = new LinkedHashSet<>(); + boolean changed; + do { + Set ignored = Collections.newSetFromMap(new IdentityHashMap<>()); + for (Map.Entry> candidate : candidates.entrySet()) { + if (!liveOriginals.contains(candidate.getKey())) { + ignored.addAll(candidate.getValue()); + } + } + Used used = collectUsed(prog, translator, ignored); + changed = false; + for (ImVar original : candidates.keySet()) { + ImClass owner = translator.genericStaticOwnerOf(original); + if ((used.getVars().contains(original) || used.getInstantiatedClasses().contains(owner)) + && liveOriginals.add(original)) { + changed = true; + } + } + } while (changed); + + for (Map.Entry> candidate : candidates.entrySet()) { + if (liveOriginals.contains(candidate.getKey())) { + continue; + } + prog.getGlobalInits().remove(candidate.getKey()); + for (ImSet initializer : candidate.getValue()) { + if (!(initializer.getParent() instanceof ImStmts statements)) { + throw new IllegalStateException("Global initializer is not attached to an ImStmts node."); + } + statements.remove(initializer); + } + } + } + private static void visitFunction(ImFunction f, Used used) { if (used.getFunctions().contains(f)) { return; @@ -111,6 +219,13 @@ private static void visitFunction(ImFunction f, Used used) { visitType(f.getReturnType(), used); f.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImSet e) { + if (!used.ignoredInitializers.contains(e)) { + super.visit(e); + } + } + @Override public void visit(ImFunctionCall e) { super.visit(e); @@ -150,31 +265,32 @@ public void visit(ImVarArrayAccess e) { @Override public void visit(ImAlloc e) { super.visit(e); + used.addInstantiatedClass(e.getClazz().getClassDef()); visitClass(e.getClazz().getClassDef(), used); } @Override public void visit(ImDealloc e) { super.visit(e); - visitClass(e.getClazz().getClassDef(), used); + visitClass(e.getClazz().getClassDef(), used, false); } @Override public void visit(ImInstanceof e) { super.visit(e); - visitClass(e.getClazz().getClassDef(), used); + visitClass(e.getClazz().getClassDef(), used, false); } @Override public void visit(ImTypeIdOfObj e) { super.visit(e); - visitClass(e.getClazz().getClassDef(), used); + visitClass(e.getClazz().getClassDef(), used, false); } @Override public void visit(ImTypeIdOfClass e) { super.visit(e); - visitClass(e.getClazz().getClassDef(), used); + visitClass(e.getClazz().getClassDef(), used, false); } @Override @@ -196,7 +312,7 @@ private static void visitMethod(ImMethod m, Used used) { return; } used.addMethod(m); - visitClass(m.getMethodClass().getClassDef(), used); + visitClass(m.getMethodClass().getClassDef(), used, false); if (m.getImplementation() != null) { // abstract methods can have no implementation visitFunction(m.getImplementation(), used); @@ -207,12 +323,15 @@ private static void visitMethod(ImMethod m, Used used) { } private static void visitClass(ImClass c, Used used) { - if (used.getClasses().contains(c)) { + visitClass(c, used, true); + } + + private static void visitClass(ImClass c, Used used, boolean dispatchReachable) { + if (!used.addClass(c, dispatchReachable)) { return; } - used.addClass(c); for (ImClassType superClass : c.getSuperClasses()) { - visitClass(superClass.getClassDef(), used); + visitClass(superClass.getClassDef(), used, dispatchReachable); } } @@ -253,7 +372,7 @@ public void case_ImArrayTypeMulti(ImArrayTypeMulti tt) { @Override public void case_ImClassType(ImClassType tt) { - visitClass(tt.getClassDef(), used); + visitClass(tt.getClassDef(), used, false); for (ImTypeArgument ta : tt.getTypeArguments()) { visitType(ta.getType(), used); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index 6afb1d8e7..cd722af06 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -80,7 +80,7 @@ public static void translate(ImSet s, List res, LuaTranslator tr) public static void translate(ImVarargLoop loop, List res, LuaTranslator tr) { - LuaVariable loopVar = tr.luaVar.getFor(loop.getLoopVar()); + List loopVars = tr.imTr.getTupleScalarVars(loop.getLoopVar()); // The loop is built from real AST nodes (a while loop) instead of literal // 'for ... do' / 'end' lines: the printer stops printing a statement list // after a return/break (Lua forbids trailing statements), which would @@ -90,10 +90,12 @@ public static void translate(ImVarargLoop loop, List res, LuaTrans res.add(args); res.add(i); LuaStatements body = LuaAst.LuaStatements(); - body.add(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(i), - LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(i), LuaAst.LuaOpPlus(), LuaAst.LuaExprIntVal("1")))); - body.add(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(loopVar), - LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(args), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(i))))); + for (ImVar loopVar : loopVars) { + body.add(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(i), + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(i), LuaAst.LuaOpPlus(), LuaAst.LuaExprIntVal("1")))); + body.add(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(tr.luaVar.getFor(loopVar)), + LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(args), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(i))))); + } tr.translateStatements(body, loop.getBody()); res.add(LuaAst.LuaWhile( LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(i), LuaAst.LuaOpLess(), 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 5bf33791a..7f5d1fef1 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 @@ -12,7 +12,10 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.Random; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; @@ -140,6 +143,823 @@ public void legacyGenericHandleCastsUseObjectIndexMap() throws IOException { .matcher(compiled).find()); } + @Test + public void tuplesAreScalarizedWithoutLuaAllocations() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple vec2(real x, real y)", + "tuple segment(vec2 start, vec2 finish)", + "vec2 array points", + "abstract class Producer", + " vec2 offset", + " abstract function produce(real x) returns vec2", + "class Concrete extends Producer", + " override function produce(real x) returns vec2", + " return vec2(x + offset.x, x + offset.y)", + "function shifted(segment s, vec2 delta) returns segment", + " return segment(vec2(s.start.x + delta.x, s.start.y + delta.y),", + " vec2(s.finish.x + delta.x, s.finish.y + delta.y))", + "init", + " Producer producer = new Concrete()", + " producer.offset = vec2(3., 4.)", + " points[2] = producer.produce(5.)", + " let result = shifted(segment(points[2], vec2(10., 20.)), vec2(1., 2.))", + " if points[2] == vec2(8., 9.) and result.start == vec2(9., 11.)", + " and result.finish == vec2(11., 22.)", + " testSuccess()" + ); + + String compiled = compiledLua("tuplesAreScalarizedWithoutLuaAllocations"); + assertFalse("tuple assignment must not allocate through a copy helper", compiled.contains("tupleCopy")); + assertFalse("tuple comparison must be lowered to scalar comparisons", compiled.contains("tupleEquals")); + assertFalse("tuple arrays must be split into scalar arrays", compiled.contains("__wurst_arrIndex(")); + } + + @Test + public void optimizedTupleCommonPathIsOnlyScalarCode() { + String compiled = compileOptimizedLua( + "optimizedTupleCommonPathIsOnlyScalarCode", + "package Test", + "tuple vec2(real x, real y)", + "native consume(real x, real y)", + "@noinline function add(vec2 left, vec2 right) returns vec2", + " return vec2(left.x + right.x, left.y + right.y)", + "init", + " let result = add(vec2(1., 2.), vec2(3., 4.))", + " consume(result.x, result.y)" + ); + + java.util.regex.Matcher add = java.util.regex.Pattern + .compile("function add\\(([^)]*)\\)\\s*\\n(.*?)\\nend", java.util.regex.Pattern.DOTALL) + .matcher(compiled); + assertTrue("optimized tuple function must remain for shape inspection", add.find()); + assertEquals("both vec2 parameters must unfold into four scalar parameters", + 4, add.group(1).split(",").length); + assertFalse("the optimized tuple function body must not allocate a Lua table", + add.group(2).contains("{")); + assertFalse(compiled.contains("tupleCopy")); + assertFalse(compiled.contains("tupleEquals")); + } + + @Test + public void randomizedTupleValueSemanticsStayScalar() throws IOException { + Random random = new Random(0x5CA1A2L); + List source = new ArrayList<>(); + source.add("package Test"); + source.add("native testSuccess()"); + source.add("tuple pair(int x, int y)"); + source.add("init"); + source.add(" int checksum = 0"); + int expected = 0; + for (int i = 0; i < 64; i++) { + int ax = random.nextInt(101) - 50; + int ay = random.nextInt(101) - 50; + int bx = random.nextInt(101) - 50; + int by = random.nextInt(101) - 50; + int resultX = ay + bx; + int resultY = ax - by; + source.add(" pair a" + i + " = pair(" + ax + ", " + ay + ")"); + source.add(" let b" + i + " = pair(" + bx + ", " + by + ")"); + source.add(" a" + i + " = pair(a" + i + ".y + b" + i + ".x, a" + i + ".x - b" + i + ".y)"); + source.add(" checksum += a" + i + ".x * " + (i + 1) + " + a" + i + ".y"); + expected += resultX * (i + 1) + resultY; + } + source.add(" if checksum == " + expected); + source.add(" testSuccess()"); + + test().testLua(true).executeProg().lines(source.toArray(new String[0])); + String compiled = compiledLua("randomizedTupleValueSemanticsStayScalar"); + assertFalse(compiled.contains("tupleCopy")); + assertFalse(compiled.contains("tupleEquals")); + } + + @Test + public void randomizedTupleEvaluationMatchesInterpreterAndLua() throws IOException { + Random random = new Random(0x0D1FF3A7L); + List source = new ArrayList<>(); + source.add("package Test"); + source.add("native testSuccess()"); + source.add("tuple pair(int x, int y)"); + source.add("tuple nested(pair left, pair right)"); + source.add("int trace"); + source.add("int calls"); + source.add("int mutable"); + source.add("class Holder"); + source.add(" pair value"); + source.add("Holder current"); + source.add("Holder replacement"); + source.add("int currentIndex"); + source.add("pair array values"); + source.add("function mark(int value) returns int"); + source.add(" trace = trace * 37 + value"); + source.add(" return value"); + source.add("@noinline function produce(int seed) returns pair"); + source.add(" calls++"); + source.add(" return pair(mark(seed), mark(seed + 1))"); + source.add("@noinline function recursive(int seed) returns pair"); + source.add(" if seed == 0"); + source.add(" return pair(mark(7), recursive(1).x)"); + source.add(" return pair(mark(seed), mark(seed + 10))"); + source.add("function retarget(int x, int y) returns pair"); + source.add(" current = replacement"); + source.add(" currentIndex = 2"); + source.add(" return pair(mark(x), mark(y))"); + source.add("function mutatingPair(int replacementValue, int x, int y) returns pair"); + source.add(" mutable = replacementValue"); + source.add(" return pair(mark(x), mark(y))"); + source.add("function scorePairs(pair first, pair second) returns int"); + source.add(" return first.x * 41 + first.y * 43 + second.x * 47 + second.y * 53"); + source.add("init"); + source.add(" int checksum = 0"); + + int expected = 0; + for (int i = 0; i < 96; i++) { + int a = random.nextInt(9) + 1; + int b = random.nextInt(9) + 1; + int c = random.nextInt(9) + 1; + int d = random.nextInt(9) + 1; + switch (random.nextInt(8)) { + case 0 -> { + boolean selectFirst = random.nextBoolean(); + source.add(" trace = 0"); + source.add(" let selected" + i + " = pair(mark(" + a + "), mark(" + b + "))." + + (selectFirst ? "x" : "y")); + source.add(" checksum += trace + selected" + i + " * 13"); + expected += a * 37 + b + (selectFirst ? a : b) * 13; + } + case 1 -> { + int selection = random.nextInt(4); + String[] paths = {"left.x", "left.y", "right.x", "right.y"}; + int[] values = {a, b, c, d}; + source.add(" trace = 0"); + source.add(" let selected" + i + " = nested(pair(mark(" + a + "), mark(" + b + + ")), pair(mark(" + c + "), mark(" + d + ")))." + paths[selection]); + source.add(" checksum += trace + selected" + i + " * 17"); + expected += (((a * 37 + b) * 37 + c) * 37 + d) + values[selection] * 17; + } + case 2 -> { + source.add(" trace = 0"); + source.add(" calls = 0"); + source.add(" let selected" + i + " = produce(" + a + ").y"); + source.add(" checksum += trace + selected" + i + " * 19 + calls * 23"); + expected += a * 37 + (a + 1) + (a + 1) * 19 + 23; + } + case 3 -> { + source.add(" trace = 0"); + source.add(" calls = 0"); + source.add(" if produce(" + a + ") != produce(" + b + ")"); + source.add(" checksum += " + (a == b ? 29 : 31)); + source.add(" else"); + source.add(" checksum += " + (a == b ? 31 : 29)); + source.add(" checksum += trace + calls * 37"); + expected += 31 + + (((a * 37 + (a + 1)) * 37 + b) * 37 + (b + 1)) + 2 * 37; + } + case 4 -> { + source.add(" let original" + i + " = new Holder()"); + source.add(" replacement = new Holder()"); + source.add(" current = original" + i); + source.add(" trace = 0"); + source.add(" current.value = retarget(" + a + ", " + b + ")"); + source.add(" checksum += original" + i + ".value.x * 41 + original" + i + + ".value.y * 43 + replacement.value.x + trace"); + expected += a * 41 + b * 43 + a * 37 + b; + } + case 5 -> { + source.add(" values[1] = pair(0, 0)"); + source.add(" values[2] = pair(0, 0)"); + source.add(" replacement = new Holder()"); + source.add(" currentIndex = 1"); + source.add(" trace = 0"); + source.add(" values[currentIndex] = retarget(" + a + ", " + b + ")"); + source.add(" checksum += values[1].x * 47 + values[1].y * 53 + values[2].x + trace"); + expected += a * 47 + b * 53 + a * 37 + b; + } + case 6 -> { + source.add(" mutable = " + a); + source.add(" trace = 0"); + source.add(" if pair(mutable, mark(" + a + ")) == mutatingPair(" + + c + ", " + a + ", " + a + ")"); + source.add(" checksum += 71"); + source.add(" else"); + source.add(" checksum += 67"); + source.add(" checksum += trace + mutable * 73"); + expected += 71 + ((a * 37 + a) * 37 + a) + c * 73; + } + case 7 -> { + source.add(" mutable = " + a); + source.add(" trace = 0"); + source.add(" checksum += scorePairs(pair(mutable, mark(" + a + + ")), mutatingPair(" + c + ", " + b + ", " + d + "))"); + source.add(" checksum += trace + mutable * 79"); + expected += a * 41 + a * 43 + b * 47 + d * 53 + + ((a * 37 + b) * 37 + d) + c * 79; + } + } + } + source.add(" trace = 0"); + source.add(" let recursiveResult = recursive(0)"); + source.add(" checksum += recursiveResult.x * 59 + recursiveResult.y * 61 + trace"); + expected += 7 * 59 + 61 + ((7 * 37 + 1) * 37 + 11); + source.add(" if checksum == " + expected); + source.add(" testSuccess()"); + + // executeProg validates the source-level IM interpreter; testLua additionally runs the + // scalarized output in Lua 5.3, making the generated program a deterministic differential test. + test().testLua(true).executeProg().lines(source.toArray(new String[0])); + String compiled = compiledLua("randomizedTupleEvaluationMatchesInterpreterAndLua"); + assertFalse(compiled.contains("tupleCopy")); + assertFalse(compiled.contains("tupleEquals")); + } + + @Test + public void tupleReturnSlotsAreSharedAcrossMultipleInterfaceRoots() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "interface First", + " function value(int seed) returns pair", + "interface Second", + " function value(int seed) returns pair", + "class Both implements First, Second", + " function value(int seed) returns pair", + " return pair(seed, seed + 1)", + "@noinline function fromFirst(First value) returns pair", + " return value.value(10)", + "@noinline function fromSecond(Second value) returns pair", + " return value.value(20)", + "init", + " let both = new Both()", + " let first = fromFirst(both)", + " let second = fromSecond(both)", + " if first == pair(10, 11) and second == pair(20, 21)", + " testSuccess()" + ); + + String compiled = compiledLua("tupleReturnSlotsAreSharedAcrossMultipleInterfaceRoots"); + assertFalse(compiled.contains("tupleCopy")); + assertFalse(compiled.contains("tupleEquals")); + } + + @Test + public void tupleSpecializedClassBindsNongenericInterfaceDispatch() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "interface Producer", + " function produce() returns pair", + "class GenericProducer implements Producer", + " pair stored", + " construct(pair value)", + " stored = value", + " function produce() returns pair", + " return stored", + "init", + " Producer producer = new GenericProducer(pair(4, 5))", + " if producer.produce() == pair(4, 5)", + " testSuccess()" + ); + + String compiled = compiledLua("tupleSpecializedClassBindsNongenericInterfaceDispatch"); + assertTrue(compiled.contains("GenericProducer_specialized")); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleSpecializedClassPreservesRuntimeTypeOperations() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "interface Marker", + "class Box implements Marker", + " T value", + " construct(T initial)", + " value = initial", + "init", + " Marker box = new Box(pair(6, 7))", + " Marker plain = new Box(1)", + " if box instanceof Box and box.typeId == plain.typeId", + " testSuccess()" + ); + + String compiled = compiledLua("tupleSpecializedClassPreservesRuntimeTypeOperations"); + assertTrue(compiled.contains("Box_specialized")); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleSpecializedClassPreservesGenericInstanceofIdentity() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "interface Marker", + "class Box implements Marker", + " T value", + " construct(T initial)", + " value = initial", + " function get() returns T", + " return value", + "function isIntBox(Marker value) returns bool", + " return value instanceof Box", + "class Child extends Box", + " construct(int initial)", + " super(initial)", + "class GenericChild extends Box", + " construct(U initial)", + " super(initial)", + "class GrandChild extends GenericChild", + " construct(int initial)", + " super(initial)", + "init", + " Marker tupleBox = new Box(pair(6, 7))", + " Box intBox = new Box(1)", + " Marker intMarker = intBox", + " Marker child = new Child(2)", + " Marker genericChild = new GenericChild(3)", + " Marker grandChild = new GrandChild(4)", + " if tupleBox instanceof Box", + " and not (tupleBox instanceof Box)", + " and intMarker instanceof Box", + " and not (intMarker instanceof Box)", + " and intBox.get() == 1", + " and intBox.value == 1", + " and isIntBox(intMarker)", + " and not isIntBox(tupleBox)", + " and child instanceof Box", + " and not (child instanceof Box)", + " and genericChild instanceof Box", + " and not (genericChild instanceof Box)", + " and grandChild instanceof Box", + " and not (grandChild instanceof Box)", + " testSuccess()" + ); + } + + @Test + public void tupleSpecializedClassRetainsNominalMetadataWhenItIsTheOnlyReachableForm() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "interface Marker", + "class Box implements Marker", + " T value", + " construct(T initial)", + " value = initial", + "init", + " Marker box = new Box(pair(6, 7))", + " if box != null", + " testSuccess()" + ); + + String compiled = compiledLua( + "tupleSpecializedClassRetainsNominalMetadataWhenItIsTheOnlyReachableForm"); + assertTrue(compiled.contains("Box_specialized")); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleReturningCallsAreCapturedBeforeComparison() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "@noinline function value(int seed) returns pair", + " return pair(0, seed)", + "init", + " if value(1) != value(2) and not (value(1) == value(2))", + " testSuccess()" + ); + + String compiled = compiledLua("tupleReturningCallsAreCapturedBeforeComparison"); + assertFalse(compiled.contains("tupleCopy")); + assertFalse(compiled.contains("tupleEquals")); + } + + @Test + public void tupleReturningCallArgumentsAreStagedInOrder() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int trace", + "@noinline function produce(int seed) returns pair", + " trace = trace * 10 + seed", + " return pair(seed, seed + 10)", + "@noinline function consume(pair first, int middle, pair second) returns bool", + " return first == pair(1, 11) and middle == 7 and second == pair(2, 12)", + "function mark(int value) returns int", + " trace = trace * 10 + value", + " return value", + "init", + " if consume(produce(1), mark(7), produce(2)) and trace == 172", + " testSuccess()" + ); + + String compiled = compiledLua("tupleReturningCallArgumentsAreStagedInOrder"); + assertTrue("tuple arguments must be materialized before the scalar call", + compiled.contains("tuple_argument")); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void selectingLaterTupleComponentStillInvokesProducer() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "class Producer", + " int calls", + " @noinline function produce(int seed) returns pair", + " calls++", + " return pair(seed, seed + calls)", + "init", + " let producer = new Producer()", + " let selected = producer.produce(5).y", + " if selected == 6 and producer.calls == 1", + " testSuccess()" + ); + + String compiled = compiledLua("selectingLaterTupleComponentStillInvokesProducer"); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleReturnStagesComponentsBeforeRecursiveSlotWrites() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "class Producer", + " @noinline function produce(int seed) returns pair", + " if seed == 0", + " return pair(7, produce(1).x)", + " return pair(seed, 99)", + "init", + " let result = new Producer().produce(0)", + " if result == pair(7, 1)", + " testSuccess()" + ); + + String compiled = compiledLua("tupleReturnStagesComponentsBeforeRecursiveSlotWrites"); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleBundleCapturesEarlierReadsBeforeLaterPreludes() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int mutable", + "@noinline function mutate() returns pair", + " mutable = 9", + " return pair(5, 2)", + "@noinline function makeResult() returns pair", + " mutable = 7", + " return pair(mutable, mutate().y)", + "init", + " mutable = 5", + " pair assigned = pair(mutable, mutate().y)", + " let returned = makeResult()", + " mutable = 5", + " let compared = pair(mutable, 2) == mutate()", + " if assigned == pair(5, 2) and returned == pair(7, 2)", + " and compared and mutable == 9", + " testSuccess()" + ); + + String compiled = compiledLua("tupleBundleCapturesEarlierReadsBeforeLaterPreludes"); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleSelectionPreservesLeftToRightEvaluation() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int trace", + "function mark(int value) returns int", + " trace = trace * 10 + value", + " return value", + "init", + " let selected = pair(mark(1), mark(2)).x", + " if selected == 1 and trace == 12", + " testSuccess()" + ); + + String compiled = compiledLua("tupleSelectionPreservesLeftToRightEvaluation"); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void discardedTupleComponentsThatCanFailAreStillEvaluated() { + String compiled = compileLuaWithRunArgs( + "discardedTupleComponentsThatCanFailAreStillEvaluated", + new RunArgs().with("-lua"), + "package Test", + "tuple pair(int x, int y)", + "class Box", + " int value", + "Box nullable", + "init", + " let selected = pair(1, nullable.value).x" + ); + + assertTrue("discarded member access must still be evaluated so null access can fail", + java.util.regex.Pattern.compile( + "__wurst_tuple_discard_\\d+\\([^\\n]*Box_value_storage\\[[^]]*nullable]\\)") + .matcher(compiled).find()); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleComparisonEagerlyEvaluatesPotentiallyFailingComponents() { + String compiled = compileLuaWithRunArgs( + "tupleComparisonEagerlyEvaluatesPotentiallyFailingComponents", + new RunArgs().with("-lua"), + "package Test", + "tuple pair(int x, int y)", + "class Box", + " int value", + "Box nullable", + "init", + " let equal = pair(1, nullable.value) == pair(2, 0)" + ); + + assertTrue("comparison operands must cross the eager-evaluation barrier before and/or", + java.util.regex.Pattern.compile( + "__wurst_tuple_discard_\\d+\\([^\\n]*tuple_compare[^\\n]*\\)") + .matcher(compiled).find()); + assertFalse(compiled.contains("tupleCopy")); + assertFalse(compiled.contains("tupleEquals")); + } + + @Test + public void tupleAssignmentCapturesLvalueBeforeRhs() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "class Holder", + " pair value", + "Holder current", + "Holder replacement", + "int currentIndex = 1", + "pair array values", + "function changeTargets() returns pair", + " current = replacement", + " currentIndex = 2", + " return pair(3, 4)", + "init", + " let original = new Holder()", + " replacement = new Holder()", + " current = original", + " current.value = changeTargets()", + " current = original", + " currentIndex = 1", + " values[currentIndex] = changeTargets()", + " if original.value == pair(3, 4) and replacement.value == pair(0, 0)", + " and values[1] == pair(3, 4) and values[2] == pair(0, 0)", + " testSuccess()" + ); + + String compiled = compiledLua("tupleAssignmentCapturesLvalueBeforeRhs"); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleFieldReadCapturesReceiverBeforeEffectfulIndex() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "class Holder", + " pair array[8] values", + "Holder current", + "Holder replacement", + "function retarget() returns int", + " current = replacement", + " return 1", + "init", + " let original = new Holder()", + " replacement = new Holder()", + " original.values[1] = pair(3, 4)", + " replacement.values[1] = pair(8, 9)", + " current = original", + " let result = current.values[retarget()]", + " if result == pair(3, 4) and current == replacement", + " testSuccess()" + ); + + String compiled = compiledLua("tupleFieldReadCapturesReceiverBeforeEffectfulIndex"); + java.util.regex.Matcher capture = java.util.regex.Pattern.compile( + "(tupleReceiver\\w*) = Test_current\\s+(tupleIndex\\w*) = retarget\\(\\)" + + "\\s+[^\\n]*_values_x_storage\\[\\1]\\[\\2]") + .matcher(compiled); + assertTrue("receiver must be captured before the index retargets it", capture.find()); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleSpecializationPreservesExplicitGenericStaticOwner() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "class Box", + " static int counter", + " static function setCounter(int value)", + " counter = value", + " static function incrementCounter()", + " counter++", + " static function getCounter() returns int", + " return counter", + "function touch(T value)", + " Box.incrementCounter()", + "init", + " Box.setCounter(10)", + " Box.setCounter(100)", + " touch(pair(1, 2))", + " if Box.getCounter() == 11 and Box.getCounter() == 100", + " testSuccess()" + ); + + String compiled = compiledLua("tupleSpecializationPreservesExplicitGenericStaticOwner"); + assertFalse(compiled.contains("tupleCopy")); + } + + @Test + public void tupleSpecializedStaticsEmitDeterministically() { + String[] source = { + "package Test", + "tuple pair(int x, int y)", + "class Box", + " static T first", + " static T second", + " static T third", + " static function set(T a, T b, T c)", + " first = a", + " second = b", + " third = c", + "init", + " Box.set(pair(1, 2), pair(3, 4), pair(5, 6))" + }; + RunArgs runArgs = new RunArgs().with("-lua"); + String first = compileLuaWithRunArgs("tupleSpecializedStaticsEmitDeterministically1", + runArgs, source); + String second = compileLuaWithRunArgs("tupleSpecializedStaticsEmitDeterministically2", + runArgs, source); + assertEquals("tuple-specialized statics must emit byte-identically", first, second); + } + + @Test + public void tupleSpecializedStaticInitializerRunsOnceWithoutErasedInstantiation() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " static function get() returns int", + " return value", + "init", + " if Box.get() == 1 and bumps == 1", + " testSuccess()" + ); + + String compiled = compiledLua( + "tupleSpecializedStaticInitializerRunsOnceWithoutErasedInstantiation"); + assertEquals("only the live tuple instantiation may call the static initializer", + 2, countOccurrences(compiled, "bump()")); // one function declaration plus one call + } + + @Test + public void tupleSpecializedTypedLocalDoesNotRootErasedInitializer() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " construct()", + "init", + " let box = new Box()", + " if bumps == 1", + " testSuccess()" + ); + + String compiled = compiledLua( + "tupleSpecializedTypedLocalDoesNotRootErasedInitializer"); + assertEquals("a tuple-specialized local type must not retain the erased initializer", + 2, countOccurrences(compiled, "bump()")); // one function declaration plus one call + } + + @Test + public void tupleSpecializedStaticKeepsLiveErasedInitializer() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " static function get() returns int", + " return value", + "init", + " if Box.get() == 1 and Box.get() == 2 and bumps == 2", + " testSuccess()" + ); + } + + @Test + public void tupleSpecializedStaticKeepsInitializerForConstructedErasedClass() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int value = bump()", + " construct()", + "init", + " new Box()", + " new Box()", + " if bumps == 2", + " testSuccess()" + ); + } + + @Test + public void tupleSpecializedInterfaceDispatchDoesNotRootErasedStaticInitializer() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "interface Reader", + " function read() returns int", + "class Box implements Reader", + " static int value = bump()", + " construct()", + " function read() returns int", + " return value", + "init", + " Reader reader = new Box()", + " if reader.read() == 1 and bumps == 1", + " testSuccess()" + ); + } + + @Test + public void tupleSpecializedStaticInitializerCycleDoesNotRootErasedCopy() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int bumps", + "function bump() returns int", + " bumps++", + " return bumps", + "class Box", + " static int a = b + bump()", + " static int b = a", + " static function get() returns int", + " return a + b", + "init", + " if Box.get() == 2 and bumps == 1", + " testSuccess()" + ); + + String compiled = compiledLua( + "tupleSpecializedStaticInitializerCycleDoesNotRootErasedCopy"); + assertEquals("an unreachable erased initializer cycle must not execute", + 2, countOccurrences(compiled, "bump()")); // one function declaration plus one call + } + @Test public void compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( @@ -900,7 +1720,7 @@ public void primitiveArrayReadsDoNotMaterializeEntries() throws IOException { assertFalse("primitive array default reads must not write back into the array table", compiled.substring(fnStart, fnEnd).contains("=")); - assertTrue("tuple array defaults must still be lazily materialized per-slot for identity", + assertFalse("tuple arrays are value types and must be split into scalar arrays", compiled.contains("function __wurst_arrIndex(")); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java index 3bb01c67f..1d5861e49 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java @@ -151,6 +151,23 @@ public void varargAllowsMoreThan31ArgumentsInLua() { ); } + @Test + public void tupleVarargPreservesElementGroupingInLua() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "function sumPairs(vararg pair pairs) returns int", + " var result = 0", + " for p in pairs", + " result = result * 100 + p.x * 10 + p.y", + " return result", + "init", + " if sumPairs(pair(1, 2), pair(3, 4), pair(5, 6)) == 123456", + " testSuccess()" + ); + } + @Test public void varargReceiverCountsAsJassParameter() { testAssertErrorsLines(false, "would generate 32 Jass parameters; the maximum is 31",