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..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 @@ -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()); + } + } + } } /** @@ -262,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 @@ -290,6 +343,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 +377,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 +421,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 +468,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 +544,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 +555,10 @@ private void collectGenericNewUse(ImMemberAccess memberAccess) { || typeArgumentsContainTypeVariable(memberAccess.getTypeArguments())) { return; } + if (!shouldSpecializeTupleArguments(memberAccess.getTypeArguments()) + && !isConstructionOnlyInstantiation(owningClass)) { + return; + } genericsUses.add(new GenericMemberAccess(memberAccess)); } @@ -487,7 +567,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 +631,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 +1411,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 +1428,45 @@ 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) { + 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; + } + 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 +1545,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; } @@ -1689,7 +1839,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/imtranslation/EliminateTuples.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java index f292c7f98..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 @@ -23,16 +23,134 @@ 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) { @@ -42,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 @@ -80,10 +201,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 +229,82 @@ 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 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); } @@ -199,13 +406,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 +439,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 +482,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,12 +595,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); + 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 +638,39 @@ 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(); + + // 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); } @@ -376,12 +678,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))); } @@ -418,12 +725,43 @@ 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 ImStatementExpr inSet(ImSet imSet, ImFunction f) { + 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) { + 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,7 +771,7 @@ 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 @@ -508,6 +846,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 @@ -517,17 +905,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); @@ -571,23 +987,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/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/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 !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..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 @@ -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,450 @@ 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 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( + "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 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 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]*nullable[^\\n]*\\.Box_value\\)") + .matcher(compiled).find()); + 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 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( @@ -814,7 +1261,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(")); }