From 43495f3091d0cba9e0b4baab6c0fb0af6794d301 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 09:27:11 +0200 Subject: [PATCH 01/11] Scalarize tuples in the Lua backend --- .../peeeq/wurstio/WurstCompilerJassImpl.java | 32 ++- .../imtranslation/EliminateGenerics.java | 125 +++++++++- .../imtranslation/EliminateTuples.java | 214 +++++++++++++++++- .../imtranslation/ImTranslator.java | 24 +- .../lua/translation/RemoveGarbage.java | 4 + .../tests/LuaBackendAuditTests.java | 70 +++++- 6 files changed, 444 insertions(+), 25 deletions(-) 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..107088183 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,11 @@ 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); timeTaker.endPhase(); } if (runArgs.isNoDebugMessages()) { @@ -919,6 +918,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 +1001,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..be4fc61c5 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. @@ -112,12 +114,21 @@ 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 + * 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 @@ -133,6 +144,14 @@ public void transformGenericNewOnly() { assertNoReachableGenericNewMarkers(); bindSpecialisedMethodsToTheAllocatedClass(); settleRemainingDispatches(); + if (specializeTupleValueTypes) { + for (Map.Entry entry : + new ArrayList<>(specializedFunctionGenerics.entrySet())) { + if (genericTypesContainTuple(entry.getValue())) { + rewriteGenericGlobals(entry.getKey(), entry.getValue()); + } + } + } } /** @@ -290,6 +309,26 @@ public void visit(ImMemberAccess memberAccess) { super.visit(memberAccess); collectGenericNewUse(memberAccess); } + + @Override + public void visit(ImVarAccess access) { + super.visit(access); + if (specializedContextContainsTuple(access) + && globalToClass.containsKey(access.getVar())) { + recordGenericGlobalUse(access, access.getVar()); + genericsUses.add(new GenericGlobalAccess(access)); + } + } + + @Override + public void visit(ImVarArrayAccess access) { + super.visit(access); + if (specializedContextContainsTuple(access) + && globalToClass.containsKey(access.getVar())) { + recordGenericGlobalUse(access, access.getVar()); + genericsUses.add(new GenericGlobalArrayAccess(access)); + } + } }); } @@ -304,7 +343,8 @@ private void collectGenericNewUse(ImFunctionCall call) { return; } if (!call.getTypeArguments().isEmpty() - && functionNeedsSpecialization(call.getFunc(), Collections.newSetFromMap(new IdentityHashMap<>()))) { + && (shouldSpecializeTupleArguments(call.getTypeArguments()) + || functionNeedsSpecialization(call.getFunc(), Collections.newSetFromMap(new IdentityHashMap<>())))) { if (!typeArgumentsContainTypeVariable(call.getTypeArguments())) { genericsUses.add(new GenericImFunctionCall(call)); } @@ -347,8 +387,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, @@ -393,7 +434,8 @@ private void collectGenericNewUse(ImAlloc alloc) { ImClassType clazz = alloc.getClazz(); if (clazz.getTypeArguments().isEmpty() || typeArgumentsContainTypeVariable(clazz.getTypeArguments()) - || !isConstructionOnlyInstantiation(clazz.getClassDef())) { + || (!shouldSpecializeTupleArguments(clazz.getTypeArguments()) + && !isConstructionOnlyInstantiation(clazz.getClassDef()))) { return; } genericsUses.add(new GenericClazzUse(alloc)); @@ -468,7 +510,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 +521,10 @@ private void collectGenericNewUse(ImMemberAccess memberAccess) { || typeArgumentsContainTypeVariable(memberAccess.getTypeArguments())) { return; } + if (!shouldSpecializeTupleArguments(memberAccess.getTypeArguments()) + && !isConstructionOnlyInstantiation(owningClass)) { + return; + } genericsUses.add(new GenericMemberAccess(memberAccess)); } @@ -487,7 +533,8 @@ private void collectGenericNewUse(ImMethodCall call) { return; } ImMethod method = call.getMethod(); - if (!methodNeedsSpecialization(method, + if (!shouldSpecializeTupleArguments(call.getTypeArguments()) + && !methodNeedsSpecialization(method, Collections.newSetFromMap(new IdentityHashMap<>()), Collections.newSetFromMap(new IdentityHashMap<>()))) { return; @@ -550,6 +597,29 @@ 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 specializedContextContainsTuple(Element element) { + ImFunction function = enclosingFunction(element); + GenericTypes generics = function == null ? null : specializedFunctionGenerics.get(function); + return specializeTupleValueTypes && generics != null && genericTypesContainTuple(generics); + } + private boolean functionNeedsSpecialization(ImFunction function, Set visited) { return functionNeedsSpecialization(function, visited, Collections.newSetFromMap(new IdentityHashMap<>())); @@ -1307,6 +1377,10 @@ private ImFunction specializeFunction(ImFunction f, GenericTypes generics) { rewriteGenerics(newF, generics, typeVars); } + if (genericNewOnly && specializeTupleValueTypes && genericTypesContainTuple(generics)) { + rewriteGenericGlobals(newF, generics); + } + // Fix calls inside this specialized function so they also point to specialized callees if (genericNewOnly) { collectGenericNewUses(newF); @@ -1320,6 +1394,36 @@ private ImFunction specializeFunction(ImFunction f, GenericTypes generics) { return newF; } + private void rewriteGenericGlobals(ImFunction function, GenericTypes generics) { + function.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 owner = globalToClass.get(original); + if (owner == null) { + return original; + } + GenericTypes concrete = normalizeToClassArity(generics, owner, + "specialized function " + function.getName()); + if (concrete == null || concrete.containsTypeVariable()) { + return original; + } + ImVar result = ensureSpecializedGlobal(original, owner, concrete); + return result == null ? original : result; + } + }); + } + /** * creates a specialized version of this method */ @@ -1398,6 +1502,9 @@ private ImFunction specializeClassFunction(ImFunction function, ImClass owningCl newImplementation.getTypeVariables().removeAll(); newImplementation.setName(function.getName() + "_specialized"); rewriteGenerics(newImplementation, generics, typeVariables); + if (specializeTupleValueTypes && genericTypesContainTuple(generics)) { + rewriteGenericGlobals(newImplementation, generics); + } collectGenericNewUses(newImplementation); return newImplementation; } 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..c6689e19f 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,16 +23,84 @@ public class EliminateTuples { public static void eliminateTuplesProg(ImProg imProg, ImTranslator translator) { + 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()) { + for (ImFunction f : functions) { eliminateTuplesFunc(f, translator); } - removeOldGlobals.run(); - translator.assertProperties(AssertProperty.NOTUPLES); + 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) { + LinkedHashSet methods = new LinkedHashSet<>(prog.getMethods()); + for (ImClass c : prog.getClasses()) { + methods.addAll(c.getMethods()); + } + Set children = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ImMethod method : methods) { + children.addAll(method.getSubMethods()); + } + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + for (ImMethod method : methods) { + if (!children.contains(method)) { + shareTupleReturnSlots(method, translator, visited); + } + } + for (ImMethod method : methods) { + shareTupleReturnSlots(method, translator, visited); + } + } + + private static void shareTupleReturnSlots(ImMethod root, ImTranslator translator, Set visited) { + if (visited.contains(root)) { + return; + } + ImFunction implementation = root.getImplementation(); + VarsForTupleResult shared = null; + if (implementation != null && translator.getOriginalReturnValue(implementation) instanceof ImTupleType) { + shared = translator.getTupleTempReturnVarsFor(implementation); + } + shareTupleReturnSlots(root, shared, translator, visited); + } + + private static void shareTupleReturnSlots(ImMethod method, VarsForTupleResult shared, + ImTranslator translator, Set visited) { + if (!visited.add(method)) { + return; + } + if (shared != null && method.getImplementation() != null) { + translator.setTupleTempReturnVarsFor(method.getImplementation(), shared); + } + for (ImMethod subMethod : method.getSubMethods()) { + shareTupleReturnSlots(subMethod, shared, translator, visited); + } } private static void transformFunctionReturnsAndParameters(ImFunction f, ImTranslator translator) { @@ -237,6 +305,25 @@ public void visit(ImVarArrayAccess va) { } } + @Override + public void visit(ImMemberAccess ma) { + super.visit(ma); + if (ma.attrTyp() instanceof ImTupleType) { + ImStmts stmts = JassIm.ImStmts(); + ImExpr receiver = captureOnceIfNeeded(ma.getReceiver(), "tupleReceiver", stmts, f); + 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 +348,53 @@ 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) { + if (!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) @@ -330,7 +461,7 @@ 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) { @@ -421,9 +552,26 @@ private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall replacer.replace(opCall, newExpr); } - 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(); @@ -508,6 +656,56 @@ private static ImStatementExpr inSet(ImSet imSet, ImFunction f) { return ImHelper.statementExprVoid(stmts); } + 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; + } + private static boolean isSimpleLiteral(ImExpr expr) { return expr instanceof ImBoolVal || expr instanceof ImIntVal 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..c0ae63996 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,24 @@ public VarsForTupleResult getVarsForTuple(ImVar v) { return result; } + VarsForTupleResult getVarsForTuple(ImVar v, ImType concreteStorageType) { + VarsForTupleResult result = getVarsForTuple(v); + if (!TypesHelper.typeContainsTuples(v.getType()) + && TypesHelper.typeContainsTuples(concreteStorageType)) { + 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; + } + /** * Creates variables for the given type, eliminating tuple types @@ -1956,7 +1974,7 @@ public VarsForTupleResult case_ImArrayType(ImArrayType at) { @Override public VarsForTupleResult case_ImTypeVarRef(ImTypeVarRef imTypeVarRef) { - throw new RuntimeException("Should be called after eliminating generics."); + return new SingleVarResult(JassIm.ImVar(tr, typeConstructor.apply(imTypeVarRef), name, false)); } @Override @@ -2046,6 +2064,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/RemoveGarbage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java index d08024ccf..5f2be4efe 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java @@ -87,6 +87,10 @@ public static void removeGarbage(ImProg prog, ImTranslator 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 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 dbfd02e61..43e477b63 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; @@ -53,6 +56,71 @@ private String compileLuaWithRunArgs(String testName, RunArgs runArgs, String... return result.toString(); } + @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 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 compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( @@ -814,7 +882,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(")); } From e415c7110a5642644f2263582bf826448eabf9fe Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 09:36:15 +0200 Subject: [PATCH 02/11] Share tuple returns across dispatch roots --- .../imtranslation/EliminateTuples.java | 103 +++++++++++++----- .../tests/LuaBackendAuditTests.java | 30 +++++ 2 files changed, 106 insertions(+), 27 deletions(-) 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 c6689e19f..12a2b8f72 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 @@ -59,47 +59,96 @@ private static List allFunctions(ImProg prog) { /** 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) { - LinkedHashSet methods = new LinkedHashSet<>(prog.getMethods()); + 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()) { - methods.addAll(c.getMethods()); + 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); + } + } } - Set children = Collections.newSetFromMap(new IdentityHashMap<>()); + + Map parents = new IdentityHashMap<>(); + Map methodForImplementation = new IdentityHashMap<>(); for (ImMethod method : methods) { - children.addAll(method.getSubMethods()); + parents.put(method, method); } - Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); for (ImMethod method : methods) { - if (!children.contains(method)) { - shareTupleReturnSlots(method, translator, visited); + 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) { - shareTupleReturnSlots(method, translator, visited); + 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); } - } - private static void shareTupleReturnSlots(ImMethod root, ImTranslator translator, Set visited) { - if (visited.contains(root)) { - return; - } - ImFunction implementation = root.getImplementation(); - VarsForTupleResult shared = null; - if (implementation != null && translator.getOriginalReturnValue(implementation) instanceof ImTupleType) { - shared = translator.getTupleTempReturnVarsFor(implementation); + 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); + } + } } - shareTupleReturnSlots(root, shared, translator, visited); } - private static void shareTupleReturnSlots(ImMethod method, VarsForTupleResult shared, - ImTranslator translator, Set visited) { - if (!visited.add(method)) { - return; + private static ImMethod findMethodRoot(ImMethod method, Map parents) { + ImMethod parent = parents.get(method); + if (parent != method) { + parent = findMethodRoot(parent, parents); + parents.put(method, parent); } - if (shared != null && method.getImplementation() != null) { - translator.setTupleTempReturnVarsFor(method.getImplementation(), shared); - } - for (ImMethod subMethod : method.getSubMethods()) { - shareTupleReturnSlots(subMethod, shared, translator, visited); + 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); } } 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 43e477b63..e3b19b446 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 @@ -121,6 +121,36 @@ public void randomizedTupleValueSemanticsStayScalar() throws IOException { 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 compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( From 1b370435db8f0ab92408ed21ab3cc8f511cb0ae6 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 09:44:12 +0200 Subject: [PATCH 03/11] Capture tuple operands before comparison --- .../imtranslation/EliminateTuples.java | 35 ++++++++++++++----- .../tests/LuaBackendAuditTests.java | 18 ++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) 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 12a2b8f72..923572552 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 @@ -515,7 +515,7 @@ private static void removeTupleExprs(int posHint, Element elem, ImTranslator tra ImExprs exprs = (ImExprs) elem; if (exprs.getParent() instanceof ImOperatorCall) { ImOperatorCall opCall = (ImOperatorCall) exprs.getParent(); - handleTupleInOpCall(replacer, opCall); + handleTupleInOpCall(replacer, opCall, f); return; } else { // in function arguments, other tuples @@ -548,7 +548,7 @@ private static void removeTupleExprs(int posHint, Element elem, ImTranslator tra } - private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall) { + private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall, ImFunction f) { if (opCall.getParent() == null) { throw new RuntimeException("opCall not used: " + opCall); } @@ -556,12 +556,17 @@ private static void handleTupleInOpCall(Replacer replacer, ImOperatorCall opCall ImTupleExpr right = (ImTupleExpr) opCall.getArguments().get(1); WurstOperator op = opCall.getOp(); + ImStmts evaluation = JassIm.ImStmts(); + List leftComponents = captureTupleComponents(left, evaluation, f); + List rightComponents = captureTupleComponents(right, evaluation, f); + 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))); } @@ -598,7 +603,21 @@ 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) { + List components = new ArrayList<>(); + List flat = new ArrayList<>(); + flattenTupleExpr(tuple, evaluation, flat); + for (ImExpr expr : flat) { + expr.setParent(null); + ImVar temp = JassIm.ImVar(expr.attrTrace(), expr.attrTyp(), "tuple_compare", false); + f.getLocals().add(temp); + evaluation.add(JassIm.ImSet(expr.attrTrace(), JassIm.ImVarAccess(temp), expr)); + components.add(JassIm.ImVarAccess(temp)); + } + return components; } private static ImStatementExpr inSet(ImSet imSet, ImTranslator translator, ImFunction f) { 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 e3b19b446..03d911440 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 @@ -151,6 +151,24 @@ public void tupleReturnSlotsAreSharedAcrossMultipleInterfaceRoots() throws IOExc assertFalse(compiled.contains("tupleEquals")); } + @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 compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( From ad994db850ca6e84cb2ede8f57ad4c49d515a5e1 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 09:55:33 +0200 Subject: [PATCH 04/11] Preserve tuple calls for component selection --- .../imtranslation/EliminateTuples.java | 5 ++++- .../tests/LuaBackendAuditTests.java | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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 923572552..215d00502 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 @@ -198,7 +198,10 @@ public void visit(ImTupleSelection ts) { 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); + ImExpr remaining = extractSideEffect(te, stmts); + if (SideEffectAnalyzer.quickcheckHasSideeffects(remaining)) { + stmts.add(remaining); + } } else { // if it is the part we want to return ... result = extractSideEffect(te, stmts); } 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 03d911440..fba0d39fb 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 @@ -169,6 +169,28 @@ public void tupleReturningCallsAreCapturedBeforeComparison() throws IOException assertFalse(compiled.contains("tupleEquals")); } + @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 compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( From c96e3708adf63950672721c1386f9e60d3ce75af Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 10:04:44 +0200 Subject: [PATCH 05/11] Stage tuple return components before publishing --- .../imtranslation/EliminateTuples.java | 25 +++++++++++++------ .../tests/LuaBackendAuditTests.java | 21 ++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) 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 215d00502..2857a02e6 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 @@ -840,23 +840,32 @@ private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, "Cannot return tuple with " + flatExprs.size() + " element(s) from function expecting " + returnVars.size() + " element(s)"); } - // 2) Assign per component, converting nulls to proper defaults of LHS type + // 2) Capture every component before publishing any shared return slot. A later + // component can call this function (or a sibling in its dispatch group) and write + // the same slots, so assigning slots while components are still being evaluated + // would corrupt the outer result. + List staged = new ArrayList<>(returnVars.size()); for (int i = 0; i < returnVars.size(); i++) { ImVar rv = returnVars.get(i); ImExpr rhs = flatExprs.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()); } + ImVar temp = JassIm.ImVar(rhs.attrTrace(), rv.getType(), "tuple_return", false); + f.getLocals().add(temp); + stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(temp), rhs)); + staged.add(temp); + } + + // 3) Publish the complete value only after all potentially re-entrant evaluation. + for (int i = 0; i < returnVars.size(); i++) { + stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(returnVars.get(i)), + JassIm.ImVarAccess(staged.get(i)))); } - // 3) Return the first component temp + // 4) Return the first component slot stmts.add(JassIm.ImReturn(parent.getTrace(), JassIm.ImVarAccess(returnVars.get(0)))); return ImHelper.statementExprVoid(stmts); } 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 fba0d39fb..79a10559c 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 @@ -191,6 +191,27 @@ public void selectingLaterTupleComponentStillInvokesProducer() throws IOExceptio 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 compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( From d67226184bb4c70620cadda3e2bc4a1167ad2843 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 10:16:16 +0200 Subject: [PATCH 06/11] Preserve tuple evaluation order --- .../imtranslation/EliminateTuples.java | 70 +++++++++++++++---- .../tests/LuaBackendAuditTests.java | 53 ++++++++++++++ 2 files changed, 111 insertions(+), 12 deletions(-) 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 2857a02e6..8f1a660e3 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 @@ -203,7 +203,14 @@ public void visit(ImTupleSelection ts) { stmts.add(remaining); } } 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; @@ -218,6 +225,22 @@ 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); + } + interface Step { void apply(ImStmts e, ImTranslator t, ImFunction f); } @@ -319,13 +342,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 @@ -652,7 +670,7 @@ private static ImStatementExpr inSet(ImSet imSet, ImTranslator translator, ImFun // 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 @@ -786,17 +804,45 @@ private static boolean isSimpleLiteral(ImExpr expr) { } /** 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)); } } + /** 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; + } + + private static void captureLvalueIndexes(ImExprs indexes, ImStmts stmts, ImFunction f) { + for (int i = 0; i < indexes.size(); i++) { + ImExpr index = indexes.get(i); + index.setParent(null); + indexes.set(i, captureValue(index, "tuple_lvalue_index", stmts, f)); + } + } + + 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); + } + /** 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); 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 79a10559c..fb1943703 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 @@ -212,6 +212,59 @@ public void tupleReturnStagesComponentsBeforeRecursiveSlotWrites() throws IOExce 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 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 compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( From 982eee608ad7053f533855dbdd663e307f81a216 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 10:21:05 +0200 Subject: [PATCH 07/11] Fuzz tuple lowering against interpreter --- .../tests/LuaBackendAuditTests.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) 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 fb1943703..1ba93b76b 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 @@ -121,6 +121,119 @@ public void randomizedTupleValueSemanticsStayScalar() throws IOException { 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("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("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(6)) { + 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; + } + } + } + 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( From 7904f0b954a0eecf933f8d73193f92f5cd886ec6 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 10:32:12 +0200 Subject: [PATCH 08/11] Preserve failing tuple component evaluation --- .../imtranslation/EliminateTuples.java | 80 +++++++++++++++++-- .../tests/LuaBackendAuditTests.java | 20 +++++ 2 files changed, 92 insertions(+), 8 deletions(-) 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 8f1a660e3..c618b35d1 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,6 +23,7 @@ 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()) { @@ -35,7 +36,7 @@ public static void eliminateTuplesProg(ImProg imProg, ImTranslator translator) { transformFunctionReturnsAndParameters(f, translator); } for (ImFunction f : functions) { - eliminateTuplesFunc(f, translator); + eliminateTuplesFunc(f, translator, discardEvaluation); } removeOldVars.forEach(Runnable::run); assertNoTuples(imProg); @@ -159,18 +160,21 @@ private static void transformFunctionReturnsAndParameters(ImFunction f, ImTransl } - 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)); } - 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 @@ -197,11 +201,11 @@ 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 + // 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); - if (SideEffectAnalyzer.quickcheckHasSideeffects(remaining)) { - stmts.add(remaining); - } + retainDiscardedValue(remaining, stmts, tr, discardEvaluation); } else { // if it is the part we want to return ... ImExpr selected = extractSideEffect(te, stmts); if (i < tupleExpr.getExprs().size() - 1 && !ts.isUsedAsLValue()) { @@ -241,6 +245,66 @@ private static ImExpr captureSelectedValue(ImExpr selected, ImStmts stmts, ImFun 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 Map functionsByType = new LinkedHashMap<>(); + + private DiscardEvaluation(ImProg prog) { + this.prog = prog; + } + + private ImFunctionCall call(ImExpr value) { + ImFunction sink = functionsByType.computeIfAbsent(value.attrTyp().toString(), ignored -> { + ImVar parameter = JassIm.ImVar(value.attrTrace(), value.attrTyp().copy(), "value", false); + ImFunction result = JassIm.ImFunction(value.attrTrace(), + "__wurst_tuple_discard_" + functionsByType.size(), JassIm.ImTypeVars(), + JassIm.ImVars(parameter), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), + Collections.emptyList()); + prog.getFunctions().add(result); + return result; + }); + return JassIm.ImFunctionCall(value.attrTrace(), sink, JassIm.ImTypeArguments(), + JassIm.ImExprs(value), false, CallType.NORMAL); + } + } + interface Step { void apply(ImStmts e, ImTranslator t, ImFunction f); } 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 1ba93b76b..da36c29b8 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 @@ -345,6 +345,26 @@ public void tupleSelectionPreservesLeftToRightEvaluation() throws IOException { 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]*nullable[^\\n]*\\.Box_value\\)") + .matcher(compiled).find()); + assertFalse(compiled.contains("tupleCopy")); + } + @Test public void tupleAssignmentCapturesLvalueBeforeRhs() throws IOException { test().testLua(true).executeProg().lines( From 6c8d2051efc81b986b9b96195f015216344715cb Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 10:43:52 +0200 Subject: [PATCH 09/11] Preserve tuple specialization class identity --- .../imtranslation/EliminateGenerics.java | 37 +++++++++++++- .../lua/translation/LuaTranslator.java | 14 +++++- .../tests/LuaBackendAuditTests.java | 48 +++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) 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 be4fc61c5..401d12721 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 @@ -281,9 +281,43 @@ 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); + } }); } + private void collectGenericNewUse(ImClassRelatedExprWithClass expression) { + ImClassType clazz = expression.getClazz(); + if (clazz.getTypeArguments().isEmpty() + || typeArgumentsContainTypeVariable(clazz.getTypeArguments()) + || !shouldSpecializeTupleArguments(clazz.getTypeArguments())) { + return; + } + genericsUses.add(new GenericClazzUse(expression)); + } + private void collectGenericNewUses(Element element) { element.accept(new Element.DefaultVisitor() { @Override @@ -1796,7 +1830,8 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { // NEW: Create specialized global variables for this class instantiation createSpecializedGlobals(c, generics, typeVars); - if (genericNewOnly && isConstructionOnlyInstantiation(c)) { + if (genericNewOnly && (isConstructionOnlyInstantiation(c) + || (specializeTupleValueTypes && genericTypesContainTuple(generics)))) { attachSpecializedClassMethods(c, newC, generics); } 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 b2c9247ae..151b2253d 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 @@ -892,10 +892,16 @@ private void initClassTables(ImClass c) { )); // set typeid metadata: + ImClass typeIdClass = c; + ImTranslator.Specialisation specialization = imTr.specialisationOf(c); + if (specialization != null && specialization.original() instanceof ImClass original) { + // Targeted Lua specialization preserves the nominal identity of the erased class. + typeIdClass = original; + } deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( LuaAst.LuaExprVarAccess(classVar), ExprTranslation.TYPE_ID), - LuaAst.LuaExprIntVal("" + prog.attrTypeId().get(c)) + LuaAst.LuaExprIntVal("" + prog.attrTypeId().get(typeIdClass)) )); @@ -1310,6 +1316,12 @@ private void collectSuperClasses(LuaTableFields superClasses, ImClass c, Set 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 tupleReturningCallsAreCapturedBeforeComparison() throws IOException { test().testLua(true).executeProg().lines( From 5d8969e507d7193f2a8c8ffdee4effc61b5169ab Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 11:02:34 +0200 Subject: [PATCH 10/11] Preserve specialized generic static bindings --- .../imtranslation/EliminateGenerics.java | 9 ++++++ .../tests/LuaBackendAuditTests.java | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+) 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 401d12721..3233b3ee2 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 @@ -1443,6 +1443,15 @@ public void visit(ImVarArrayAccess access) { } private ImVar specializedGlobal(ImVar original) { + ImTranslator.Specialisation existing = translator.specialisationOf(original); + if (existing != null && translator.genericStaticOwnerOf(original) != null + && !existing.typeArguments().isEmpty()) { + // This access already names a concrete instantiation of the static field. + // Its own binding wins over the type arguments of the function which happens + // to contain it; in particular, specialising touch must not turn an + // access produced by Box into one for Box. + return original; + } ImClass owner = globalToClass.get(original); if (owner == null) { return original; 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 e77cc1d4a..c0a2f63c2 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 @@ -446,6 +446,34 @@ public void tupleAssignmentCapturesLvalueBeforeRhs() throws IOException { 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 compiletimeGenericArrayReplayLeavesAreSplit() { String compiled = compileLuaWithRunArgs( From 066517c8e394c7af92a153b3ac49749637d37397 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 29 Aug 2026 11:10:51 +0200 Subject: [PATCH 11/11] Stage scalarized tuple call arguments --- .../imtranslation/EliminateTuples.java | 37 +++++++++++++++++++ .../tests/LuaBackendAuditTests.java | 26 +++++++++++++ 2 files changed, 63 insertions(+) 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 c618b35d1..48f2521f5 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 @@ -602,6 +602,11 @@ private static void removeTupleExprs(int posHint, Element elem, ImTranslator tra ImOperatorCall opCall = (ImOperatorCall) exprs.getParent(); handleTupleInOpCall(replacer, opCall, f); 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 // just flatten tuples @@ -633,6 +638,38 @@ private static void removeTupleExprs(int posHint, Element elem, ImTranslator tra } + private static ImStatementExpr stageTupleCallArguments(ImExpr call, ImExprs arguments, + ImFunction f) { + ImStmts evaluation = JassIm.ImStmts(); + + // 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); + methodCall.setReceiver(captureValue(receiver, "tuple_argument_receiver", evaluation, f)); + } + + List originalArguments = arguments.removeAll(); + for (ImExpr argument : originalArguments) { + argument.setParent(null); + List components = new ArrayList<>(); + if (argument instanceof ImTupleExpr) { + flattenTupleExpr(argument, evaluation, components); + } else { + components.add(argument); + } + for (ImExpr component : components) { + component.setParent(null); + arguments.add(captureValue(component, "tuple_argument", evaluation, f)); + } + } + + // 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) { if (opCall.getParent() == null) { throw new RuntimeException("opCall not used: " + opCall); 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 c0a2f63c2..07940e604 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 @@ -330,6 +330,32 @@ public void tupleReturningCallsAreCapturedBeforeComparison() throws IOException 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(