From 43e39daa72e9ec514a10513434bd4ae6288ef286 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 10 Oct 2026 12:54:35 +0200 Subject: [PATCH 1/6] Lua: return a tuple as multiple results instead of through return globals A function returning a tuple passed every component but the first through a global of the function on Lua too: a global write per component in the function, a global read per component after each call. Lua has multiple results, so the function now returns its scalar components together (return x, y) and a call takes them into locals of its own (a, b = f()). EliminateTuples makes the shape on Lua (LuaMultipleResults states and checks it): the function's return type is the flat tuple of its components, each return returns a tuple expression of them, and a call is a statement or the value of a results local, read only by component selections. To the optimizer a results local is an ordinary local which a call writes. The backend prints one Lua local per component, a multiple assignment for the call and a return of the values. The global of a component which no call read was garbage, value and all. The garbage removal now drops such a result from every function of its group (the implementations a call can dispatch between, and the functions whose calls share a results local), keeping what the dropped value did in its place. --- de.peeeq.wurstscript/parserspec/lua.parseq | 2 + .../peeeq/wurstio/WurstCompilerJassImpl.java | 2 +- .../translation/imoptimizer/ImOptimizer.java | 7 +- .../imoptimizer/LuaUnreadResults.java | 325 ++++++++++++++++ .../imtranslation/EliminateTuples.java | 101 ++++- .../imtranslation/ImTranslator.java | 11 + .../imtranslation/LuaMultipleResults.java | 230 +++++++++++ .../translation/lua/printing/LuaPrinter.java | 13 +- .../lua/translation/ExprTranslation.java | 20 +- .../lua/translation/LuaTranslator.java | 24 ++ .../lua/translation/StmtTranslation.java | 21 +- .../tests/LuaMultipleResultsTests.java | 368 ++++++++++++++++++ 12 files changed, 1101 insertions(+), 23 deletions(-) create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/LuaUnreadResults.java create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaMultipleResults.java create mode 100644 de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java diff --git a/de.peeeq.wurstscript/parserspec/lua.parseq b/de.peeeq.wurstscript/parserspec/lua.parseq index ef596655f..a9844f5b2 100644 --- a/de.peeeq.wurstscript/parserspec/lua.parseq +++ b/de.peeeq.wurstscript/parserspec/lua.parseq @@ -20,11 +20,13 @@ LuaStatement = LuaDefinition | LuaChunkLocal(LuaDefinition definition) // a definition printed as a local of the main chunk | LuaAssignment(LuaExpr left, LuaExpr right) + | LuaMultipleAssignment(LuaExprlist targets, LuaExpr right) // the results of a call, one per target | LuaWhile(LuaExpr cond, LuaStatements body) | LuaFor(ref LuaVariable loopVar, LuaExpr from, LuaExpr to, LuaExprOpt step, LuaStatements body) | LuaIf(LuaExpr cond, LuaStatements thenStmts, LuaStatements elseStmts) | LuaBreak() | LuaReturn(LuaExprOpt retVal) + | LuaReturnValues(LuaExprlist values) // several results | LuaExpr LuaExprOpt = LuaExpr | LuaNoExpr() 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 79d9cb8f1..f0e35c4b4 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 @@ -1027,8 +1027,8 @@ public LuaCompilationUnit transformProgToLua() { timeTaker.beginPhase("eliminate tuples"); getImProg().flatten(imTranslator2); + // The tuples left are the multiple results of calls (LuaMultipleResults), whose shape the elimination checks. EliminateTuples.eliminateTuplesProg(getImProg(), imTranslator2); - imTranslator2.assertProperties(AssertProperty.NOTUPLES); timeTaker.endPhase(); optimizer.removeGarbage(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java index d59365963..4c485859d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java @@ -138,6 +138,11 @@ public void doNullsetting() { public boolean removeGarbage() { boolean wasFlat = trans.isUnitTestMode() && trans.isFlat(); boolean changed = removeGarbageWithFacts(); + // A Lua result which no call reads is not returned, and what computed it is then garbage of its own. + while (trans.luaMultipleResultsLowered() && LuaUnreadResults.drop(this, trans)) { + changed = true; + removeGarbageWithFacts(); + } if (wasFlat && !trans.isFlat()) { throw new AssertionError("The garbage removal made a flat program not flat"); } @@ -451,7 +456,7 @@ private void collectTargetEffects(ImExpr target, SideEffectAnalyzer analyzer, Li } } - private List collectSideEffects(ImExpr expr, SideEffectAnalyzer analyzer) { + List collectSideEffects(ImExpr expr, SideEffectAnalyzer analyzer) { if (expr == null) { return Collections.emptyList(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/LuaUnreadResults.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/LuaUnreadResults.java new file mode 100644 index 000000000..575bc6023 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/LuaUnreadResults.java @@ -0,0 +1,325 @@ +package de.peeeq.wurstscript.translation.imoptimizer; + +import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer; +import de.peeeq.wurstscript.jassIm.*; +import de.peeeq.wurstscript.translation.imtranslation.ImHelper; +import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; +import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; +import de.peeeq.wurstscript.validation.NamePreservation; + +import java.util.*; + +/** + * Drops a multiple result ({@link LuaMultipleResults}) which no call reads. On Jass such a component goes through a + * global of its function, which the garbage removal drops, value and all, when nothing reads it; on Lua it is one of the + * values the function returns, which only this removes. + *

+ * The functions which have to agree on their results form a group: the implementations of the methods one call can + * dispatch between, and the functions whose calls write the same results local (LocalMerger can give two of them one + * variable). A component goes when no results local of the group reads it: every function of the group stops returning + * it, and the selections of the later components move down. What a dropped value does besides being returned stays, at + * its place in the order of evaluation; what it read becomes garbage for the removal which follows. + */ +final class LuaUnreadResults { + + private final ImOptimizer optimizer; + private final ImTranslator trans; + /** Union-find over the functions which return multiple results, the methods and the results locals. */ + private final Map parent = new IdentityHashMap<>(); + /** For each results local, the components it reads. */ + private final Map reads = new LinkedHashMap<>(); + /** The results locals used other than by a selection or as the target of a call (none, by the shape). */ + private final Set escaping = Collections.newSetFromMap(new IdentityHashMap<>()); + + private LuaUnreadResults(ImOptimizer optimizer, ImTranslator trans) { + this.optimizer = optimizer; + this.trans = trans; + } + + /** @return whether a result was dropped */ + static boolean drop(ImOptimizer optimizer, ImTranslator trans) { + return new LuaUnreadResults(optimizer, trans).drop(); + } + + private boolean drop() { + ImProg prog = trans.getImProg(); + Set functions = ImHelper.calculateFunctionsOfProg(prog); + List producers = new ArrayList<>(); + for (ImFunction f : functions) { + if (f.getReturnType() instanceof ImTupleType) { + producers.add(f); + } + } + if (producers.isEmpty()) { + return false; + } + for (ImMethod method : allMethods(prog)) { + for (ImMethod subMethod : method.getSubMethods()) { + union(method, subMethod); + } + if (method.getImplementation() != null) { + union(method, method.getImplementation()); + } + } + for (ImFunction f : functions) { + collectResultsLocals(f); + } + + Map> producersByGroup = new LinkedHashMap<>(); + for (ImFunction producer : producers) { + producersByGroup.computeIfAbsent(find(producer), g -> new ArrayList<>()).add(producer); + } + Map> localsByGroup = new LinkedHashMap<>(); + for (ImVar local : reads.keySet()) { + localsByGroup.computeIfAbsent(find(local), g -> new ArrayList<>()).add(local); + } + + boolean changed = false; + SideEffectAnalyzer analyzer = new SideEffectAnalyzer(prog); + for (Map.Entry> group : producersByGroup.entrySet()) { + List members = group.getValue(); + List locals = localsByGroup.getOrDefault(group.getKey(), Collections.emptyList()); + int arity = ((ImTupleType) members.get(0).getReturnType()).getTypes().size(); + BitSet read = new BitSet(); + boolean keepAll = false; + for (ImFunction member : members) { + keepAll |= NamePreservation.isPreserved(member) + || ((ImTupleType) member.getReturnType()).getTypes().size() != arity; + } + for (ImVar local : locals) { + keepAll |= escaping.contains(local); + read.or(reads.get(local)); + } + if (keepAll || read.cardinality() == arity) { + continue; + } + dropFrom(members, locals, read, analyzer); + changed = true; + } + return changed; + } + + /** The methods of the program and of its classes, and their sub-methods, however deep. */ + private static List allMethods(ImProg prog) { + Set known = Collections.newSetFromMap(new IdentityHashMap<>()); + List result = new ArrayList<>(); + for (ImMethod method : prog.getMethods()) { + if (known.add(method)) { + result.add(method); + } + } + for (ImClass c : prog.getClasses()) { + for (ImMethod method : c.getMethods()) { + if (known.add(method)) { + result.add(method); + } + } + } + for (int i = 0; i < result.size(); i++) { + for (ImMethod subMethod : result.get(i).getSubMethods()) { + if (known.add(subMethod)) { + result.add(subMethod); + } + } + } + return result; + } + + private void collectResultsLocals(ImFunction f) { + boolean any = false; + for (ImVar local : f.getLocals()) { + if (LuaMultipleResults.isResultsLocal(local)) { + reads.put(local, new BitSet()); + any = true; + } + } + if (!any) { + return; + } + f.getBody().accept(new Element.DefaultVisitor() { + @Override + public void visit(ImSet set) { + super.visit(set); + if (set.getLeft() instanceof ImVarAccess target && reads.containsKey(target.getVar())) { + ImExpr value = LuaMultipleResults.valueOf(set.getRight()); + if (value instanceof ImFunctionCall call) { + union(target.getVar(), call.getFunc()); + } else if (value instanceof ImMethodCall call) { + union(target.getVar(), call.getMethod()); + } else { + escaping.add(target.getVar()); + } + } + } + + @Override + public void visit(ImVarAccess access) { + super.visit(access); + ImVar v = access.getVar(); + if (!reads.containsKey(v)) { + return; + } + if (access.getParent() instanceof ImTupleSelection selection) { + reads.get(v).set(selection.getTupleIndex()); + } else if (!(access.getParent() instanceof ImSet set && set.getLeft() == access)) { + escaping.add(v); + } + } + }); + } + + private void dropFrom(List members, List locals, BitSet read, SideEffectAnalyzer analyzer) { + ImTupleType results = (ImTupleType) members.get(0).getReturnType(); + int arity = results.getTypes().size(); + // the new place of each kept component + int[] newIndex = new int[arity]; + List keptTypes = new ArrayList<>(); + List keptNames = new ArrayList<>(); + for (int k = 0; k < arity; k++) { + newIndex[k] = keptTypes.size(); + if (read.get(k)) { + keptTypes.add(results.getTypes().get(k).copy()); + keptNames.add(results.getNames().get(k)); + } + } + ImType newType = keptTypes.size() >= 2 ? JassIm.ImTupleType(keptTypes, keptNames) + : keptTypes.size() == 1 ? keptTypes.get(0) : JassIm.ImVoid(); + + for (ImFunction member : members) { + List returns = new ArrayList<>(); + member.getBody().accept(new Element.DefaultVisitor() { + @Override + public void visit(ImReturn ret) { + super.visit(ret); + returns.add(ret); + } + }); + for (ImReturn ret : returns) { + dropFromReturn(member, ret, read, analyzer); + } + member.setReturnType(newType.copy()); + trans.functionChanged(member); + } + + for (ImVar local : locals) { + ImFunction owner = (ImFunction) local.getParent().getParent(); + List accesses = new ArrayList<>(); + owner.getBody().accept(new Element.DefaultVisitor() { + @Override + public void visit(ImVarAccess access) { + super.visit(access); + if (access.getVar() == local) { + accesses.add(access); + } + } + }); + for (ImVarAccess access : accesses) { + if (access.getParent() instanceof ImTupleSelection selection) { + if (keptTypes.size() >= 2) { + selection.setTupleIndex(newIndex[selection.getTupleIndex()]); + } else { + access.setParent(null); + selection.replaceBy(access); + } + } else if (keptTypes.isEmpty() && access.getParent() instanceof ImSet set) { + // nothing is read: the call stays for what it does (with the statements in front of it) + ImExpr call = set.getRight(); + call.setParent(null); + set.replaceBy(call); + } + } + if (keptTypes.isEmpty()) { + owner.getLocals().remove(local); + } else { + local.setType(newType.copy()); + } + trans.functionChanged(owner); + } + } + + /** + * Drops the unread components from what {@code ret} returns. A dropped component which does something (a call, a + * division which may stop the thread) keeps doing it where it was evaluated: before it go the statements of what it + * does, and the kept components before it which a call could change (or which could fail) are first saved in + * locals, so they are still evaluated before it. + */ + private void dropFromReturn(ImFunction f, ImReturn ret, BitSet read, SideEffectAnalyzer analyzer) { + if (!(ret.getReturnValue() instanceof ImTupleExpr tuple) || !(ret.getParent() instanceof ImStmts statements)) { + throw new IllegalStateException("Lua multiple results: a return which is not of a tuple expression in a " + + "statement list: " + ret + "\nin " + f); + } + List components = new ArrayList<>(tuple.getExprs()); + List> effects = new ArrayList<>(); + int lastEffect = -1; + for (int k = 0; k < components.size(); k++) { + List componentEffects = read.get(k) + ? Collections.emptyList() + : optimizer.collectSideEffects(components.get(k), analyzer); + effects.add(componentEffects); + if (!componentEffects.isEmpty()) { + lastEffect = k; + } + } + List before = new ArrayList<>(); + for (int k = 0; k <= lastEffect; k++) { + ImExpr component = components.get(k); + if (read.get(k) && !isStable(component)) { + ImVar saved = JassIm.ImVar(component.attrTrace(), component.attrTyp(), "tuple_result", false); + f.getLocals().add(saved); + component.setParent(null); + before.add(JassIm.ImSet(ret.getTrace(), JassIm.ImVarAccess(saved), component)); + components.set(k, JassIm.ImVarAccess(saved)); + } + for (ImExpr effect : effects.get(k)) { + effect.setParent(null); + effect.flatten(trans, f).intoStatements(before, trans, f); + } + } + ImExprs kept = JassIm.ImExprs(); + for (int k = 0; k < components.size(); k++) { + if (read.get(k)) { + ImExpr component = components.get(k); + component.setParent(null); + kept.add(component); + } + } + ret.setReturnValue(kept.size() >= 2 ? JassIm.ImTupleExpr(kept) + : kept.size() == 1 ? kept.remove(0) : JassIm.ImNoExpr()); + if (!before.isEmpty()) { + int position = 0; + while (statements.get(position) != ret) { + position++; + } + statements.addAll(position, before); + } + } + + /** A value no call evaluated before it can change, and which cannot fail: a literal, a local, a call's result. */ + private static boolean isStable(ImExpr e) { + return e instanceof ImConst + || e instanceof ImVarAccess access && !access.getVar().isGlobal() + || e instanceof ImTupleSelection selection && selection.getTupleExpr() instanceof ImVarAccess access + && LuaMultipleResults.isResultsLocal(access.getVar()); + } + + private Object find(Object x) { + Object p = parent.get(x); + if (p == null) { + parent.put(x, x); + return x; + } + if (p != x) { + p = find(p); + parent.put(x, p); + } + return p; + } + + private void union(Object a, Object b) { + Object rootA = find(a); + Object rootB = find(b); + if (rootA != rootB) { + parent.put(rootB, rootA); + } + } +} 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 ce708b82a..f108111d5 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 @@ -30,7 +30,9 @@ public static void eliminateTuplesProg(ImProg imProg, ImTranslator translator) { removeOldVars.add(transformVars(c.getFields(), translator)); } - shareTupleReturnSlotsAcrossOverrides(imProg, translator); + if (!translator.isLuaTarget()) { + shareTupleReturnSlotsAcrossOverrides(imProg, translator); + } List functions = allFunctions(imProg); for (ImFunction f : functions) { transformFunctionReturnsAndParameters(f, translator); @@ -39,7 +41,12 @@ public static void eliminateTuplesProg(ImProg imProg, ImTranslator translator) { eliminateTuplesFunc(f, translator, discardEvaluation); } removeOldVars.forEach(Runnable::run); - assertNoTuples(imProg); + if (translator.isLuaTarget()) { + LuaMultipleResults.assertShape(imProg); + translator.setLuaMultipleResultsLowered(); + } else { + assertNoTuples(imProg); + } } private static void assertNoTuples(Element element) { @@ -157,7 +164,8 @@ private static void transformFunctionReturnsAndParameters(ImFunction f, ImTransl preserveVarargParameter(f); transformVars(f.getParameters(), translator).run(); translator.setOriginalReturnValue(f, f.getReturnType()); - f.setReturnType(getFirstType(f.getReturnType())); + ImTupleType results = translator.isLuaTarget() ? LuaMultipleResults.resultsType(f.getReturnType()) : null; + f.setReturnType(results != null ? results : getFirstType(f.getReturnType())); } /** @@ -198,6 +206,10 @@ private static void removeTupleSelections(ImStmts stmts, ImTranslator tr, ImFunc public void visit(ImTupleSelection ts) { super.visit(ts); + if (isResultRead(ts)) { + // a component of a call's results (Lua), read as it is + return; + } if (!(ts.getTupleExpr() instanceof ImTupleExpr)) { throw new CompileError(ts.attrTrace().attrSource(), "Wrong tuple selection: " + ts); } @@ -304,7 +316,8 @@ private static boolean isSafelyDiscardable(ImExpr value) { || value instanceof ImStringVal || value instanceof ImNull || value instanceof ImVarAccess - || value instanceof ImFuncRef) { + || value instanceof ImFuncRef + || isResultRead(value)) { return true; } if (value instanceof ImOperatorCall operatorCall @@ -457,7 +470,7 @@ public void visit(ImNull n) { @Override public void visit(ImVarAccess va) { - if (va.attrTyp() instanceof ImTupleType) { + if (va.attrTyp() instanceof ImTupleType && !LuaMultipleResults.isResultsLocal(va.getVar())) { ImVar v = va.getVar(); VarsForTupleResult vars = translator.getVarsForTuple(v); ImExpr expr = vars.map( @@ -550,6 +563,10 @@ public void visit(ImMemberAccess ma) { public void visit(ImFunctionCall fc) { super.visit(fc); if (translator.getOriginalReturnValue(fc.getFunc()) instanceof ImTupleType) { + if (translator.isLuaTarget()) { + takeResults(fc, fc.getFunc()); + return; + } Element parent = fc.getParent(); fc.setParent(null); @@ -574,6 +591,10 @@ public void visit(ImMethodCall mc) { super.visit(mc); ImFunction implementation = mc.getMethod().getImplementation(); if (implementation != null && translator.getOriginalReturnValue(implementation) instanceof ImTupleType) { + if (translator.isLuaTarget()) { + takeResults(mc, implementation); + return; + } Element parent = mc.getParent(); mc.setParent(null); VarsForTupleResult returnVars = translator.getTupleTempReturnVarsFor(implementation); @@ -585,6 +606,31 @@ public void visit(ImMethodCall mc) { } } + /** + * On Lua a call takes the results of {@code called} into a results local of its own and stands for the + * tuple of their selections ({@link LuaMultipleResults}). A call whose results nothing uses stays a + * statement, and one of a function returning a single component is that component. + */ + private void takeResults(ImExpr call, ImFunction called) { + if (call.getParent() instanceof ImStmts) { + return; + } + ImType source = translator.getOriginalReturnValue(called); + Element parent = call.getParent(); + call.setParent(null); + ImExpr replacement; + if (called.getReturnType() instanceof ImTupleType results) { + ImVar local = JassIm.ImVar(call.attrTrace(), results.copy(), called.getName() + "_result", false); + f.getLocals().add(local); + replacement = JassIm.ImStatementExpr( + JassIm.ImStmts(JassIm.ImSet(call.attrTrace(), JassIm.ImVarAccess(local), (ImExpr) call.copy())), + LuaMultipleResults.shape(source, i -> JassIm.ImTupleSelection(JassIm.ImVarAccess(local), i))); + } else { + replacement = LuaMultipleResults.shape(source, i -> (ImExpr) call.copy()); + } + replacer.replaceInParent(parent, call, replacement); + } + }); } @@ -606,7 +652,9 @@ public void visit(ImMethodCall mc) { ImExpr expanded; ImStmts prelude = JassIm.ImStmts(); - if (storage instanceof ImVarAccess access && access.attrTyp() instanceof ImTupleType) { + if (storage instanceof ImVarAccess access && LuaMultipleResults.isResultsLocal(access.getVar())) { + return null; + } else if (storage instanceof ImVarAccess access && access.attrTyp() instanceof ImTupleType) { VarsForTupleResult selected = selectTupleComponent( translator.getVarsForTuple(access.getVar()), componentPath); if (selected == null) { @@ -1142,11 +1190,23 @@ private static boolean isImmutableValue(ImExpr expression) { || expression instanceof ImStringVal || expression instanceof ImNull || expression instanceof ImFuncRef - || expression instanceof ImTypeIdOfClass; + || expression instanceof ImTypeIdOfClass + // only the call writes its results local, and it runs before any of the components is read + || isResultRead(expression); + } + + /** A component of the results of a call (Lua): {@code t.k} of a results local t. */ + private static boolean isResultRead(ImExpr expression) { + return expression instanceof ImTupleSelection selection + && selection.getTupleExpr() instanceof ImVarAccess access + && LuaMultipleResults.isResultsLocal(access.getVar()); } private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, ImTranslator translator, ImFunction f) { + if (translator.isLuaTarget()) { + return inReturnOfResults(parent, tupleExpr, f); + } // flat list of return temps, already created by translator: List returnVars = translator.getTupleTempReturnVarsFor(f) .allValuesStream().collect(Collectors.toList()); @@ -1183,6 +1243,33 @@ private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, return ImHelper.statementExprVoid(stmts); } + /** + * On Lua the components are the multiple results of the return ({@link LuaMultipleResults}), which Lua evaluates + * in order as the bundle has them; a single component is returned as it is. + */ + private static ImStatementExpr inReturnOfResults(ImReturn parent, ImTupleExpr tupleExpr, ImFunction f) { + ImStmts stmts = JassIm.ImStmts(); + OrderedBundle result = lowerBundle(tupleExpr, f, false, "tuple_return"); + result.appendPreludeTo(stmts); + List types = f.getReturnType() instanceof ImTupleType results + ? results.getTypes() + : Collections.singletonList(f.getReturnType()); + if (result.values.size() != types.size()) { + throw new CompileError(parent.getTrace(), + "Cannot return tuple with " + result.values.size() + + " element(s) from function expecting " + types.size() + " element(s)"); + } + ImExprs values = JassIm.ImExprs(); + for (int i = 0; i < types.size(); i++) { + ImExpr value = result.values.get(i); + value.setParent(null); + values.add(value instanceof ImNull ? ImHelper.defaultValueForComplexType(types.get(i)) : value); + } + ImExpr returned = values.size() == 1 ? values.remove(0) : JassIm.ImTupleExpr(values); + stmts.add(JassIm.ImReturn(parent.getTrace(), returned)); + return ImHelper.statementExprVoid(stmts); + } + private static Element inTupleSelection(ImTupleSelection ts, ImTupleExpr tupleExpr, ImFunction f) { assert ts.getTupleExpr() == tupleExpr; 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 6c9ea2276..b169663ef 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 @@ -479,6 +479,17 @@ public void setLuaFieldDefaultsMaterialized() { luaFieldDefaultsMaterialized = true; } + /** Whether {@link EliminateTuples} gave the Lua program multiple results ({@link LuaMultipleResults}). */ + private boolean luaMultipleResultsLowered = false; + + public boolean luaMultipleResultsLowered() { + return luaMultipleResultsLowered; + } + + public void setLuaMultipleResultsLowered() { + luaMultipleResultsLowered = true; + } + /** * Gives {@code bridge} the overrides of {@code inherited} which are below its class as sub-methods, once every * class is translated: the sub-methods of {@code inherited} come from the translation of its class, which may come diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaMultipleResults.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaMultipleResults.java new file mode 100644 index 000000000..e4621a2cd --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaMultipleResults.java @@ -0,0 +1,230 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.jassIm.*; +import de.peeeq.wurstscript.types.TypesHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntFunction; + +/** + * On Lua a function whose source return type is a tuple returns its scalar components as Lua multiple results + * ({@code return x, y}), and a call takes them into locals of its own ({@code local a, b = f()}). Jass has no + * multiple results, so there each component but the first goes through a global of the function + * ({@link ImTranslator#getTupleTempReturnVarsFor}): a global write in the function and a global read after the call + * for each, which Lua has no need for. + *

+ * {@link EliminateTuples} makes this shape, and it holds from there to the backend: + *

    + *
  • A function returning two or more scalar components (a nested tuple counts its leaves) has the flat tuple of + * those as its return type, and each of its returns returns a tuple expression of them. A function returning one + * component returns it as an ordinary value.
  • + *
  • A call of such a function, or of a method implemented by one, is a statement, or the right side of an + * assignment to a results local: a local of the flat tuple type, which only such assignments write.
  • + *
  • A results local is read only by a selection of one of its components ({@code t.k}).
  • + *
  • There is no other tuple: no tuple variable, tuple expression or selection anywhere else.
  • + *
+ * To the optimizer a results local is a local like any other: a call writes it and a selection reads it, so it takes + * part in the analyses unchanged (LocalMerger may give two results locals of the same type one variable). A result + * which no call reads is dropped by the garbage removal, as the global which carried it on Jass is. + */ +public final class LuaMultipleResults { + + private LuaMultipleResults() { + } + + /** + * The return type of a function with the source return type {@code type} on Lua: the flat tuple of its scalar + * components when it has two or more, otherwise null (the function returns an ordinary value). + */ + public static ImTupleType resultsType(ImType type) { + if (!(type instanceof ImTupleType)) { + return null; + } + List types = new ArrayList<>(); + List names = new ArrayList<>(); + collectLeaves(type, "", types, names); + return types.size() < 2 ? null : JassIm.ImTupleType(types, names); + } + + private static void collectLeaves(ImType type, String prefix, List types, List names) { + if (type instanceof ImTupleType tuple) { + for (int i = 0; i < tuple.getTypes().size(); i++) { + String name = tuple.getNames().get(i); + collectLeaves(tuple.getTypes().get(i), prefix.isEmpty() ? name : prefix + "_" + name, types, names); + } + } else { + types.add(type.copy()); + names.add(prefix); + } + } + + /** + * A value of the source type {@code type} built from its scalar components: a tuple expression of the same + * nesting whose leaves, numbered in order from 0, are {@code leaf} of their number. + */ + public static ImExpr shape(ImType type, IntFunction leaf) { + return shape(type, leaf, new int[1]); + } + + private static ImExpr shape(ImType type, IntFunction leaf, int[] next) { + if (type instanceof ImTupleType tuple) { + ImExprs parts = JassIm.ImExprs(); + for (ImType part : tuple.getTypes()) { + parts.add(shape(part, leaf, next)); + } + return JassIm.ImTupleExpr(parts); + } + return leaf.apply(next[0]++); + } + + /** Whether {@code v} is a results local: a local of a function whose type is a tuple. */ + public static boolean isResultsLocal(ImVar v) { + return v.getType() instanceof ImTupleType + && v.getParent() instanceof ImVars vars + && vars.getParent() instanceof ImFunction f + && f.getLocals() == vars; + } + + /** Whether {@code e} is a call whose results are multiple results. */ + public static boolean isMultipleResultsCall(Element e) { + return e instanceof ImFunctionCall functionCall && functionCall.getFunc().getReturnType() instanceof ImTupleType + || e instanceof ImMethodCall methodCall && methodCall.getMethod().getImplementation() != null + && methodCall.getMethod().getImplementation().getReturnType() instanceof ImTupleType; + } + + /** + * What {@code e} evaluates to, without the statement expressions around it: until the flatten which follows the + * tuple elimination, the call a results local is assigned can be behind the statements which prepare its + * arguments. + */ + public static ImExpr valueOf(ImExpr e) { + while (e instanceof ImStatementExpr statementExpr) { + e = statementExpr.getExpr(); + } + return e; + } + + /** The return type of the function a multiple-results call calls. */ + public static ImTupleType resultsTypeOf(ImExpr call) { + ImFunction f = call instanceof ImMethodCall methodCall + ? methodCall.getMethod().getImplementation() + : ((ImFunctionCall) call).getFunc(); + return (ImTupleType) f.getReturnType(); + } + + /** Throws when {@code prog} is not of the shape described above. */ + public static void assertShape(ImProg prog) { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImVar v) { + super.visit(v); + if (TypesHelper.typeContainsTuples(v.getType())) { + if (!isResultsLocal(v)) { + fail(v, "a tuple variable which is not a local"); + } + for (ImType component : ((ImTupleType) v.getType()).getTypes()) { + if (TypesHelper.typeContainsTuples(component) || component instanceof ImArrayType + || component instanceof ImArrayTypeMulti) { + fail(v, "a results local which is not a flat tuple of scalars"); + } + } + } + } + + @Override + public void visit(ImFunction f) { + super.visit(f); + if (f.getReturnType() instanceof ImTupleType tuple) { + for (ImType component : tuple.getTypes()) { + if (TypesHelper.typeContainsTuples(component)) { + fail(f, "multiple results which are not flat"); + } + } + } + } + + @Override + public void visit(ImTupleExpr e) { + super.visit(e); + if (!(e.getParent() instanceof ImReturn ret) + || !(ret.getNearestFunc().getReturnType() instanceof ImTupleType tuple) + || tuple.getTypes().size() != e.getExprs().size()) { + fail(e, "a tuple expression which is not the multiple results of a return"); + } + } + + @Override + public void visit(ImReturn ret) { + super.visit(ret); + if (ret.getNearestFunc().getReturnType() instanceof ImTupleType + && !(ret.getReturnValue() instanceof ImTupleExpr)) { + fail(ret, "a return of multiple results which does not return a tuple expression"); + } + } + + @Override + public void visit(ImTupleSelection e) { + super.visit(e); + if (!(e.getTupleExpr() instanceof ImVarAccess access) || !isResultsLocal(access.getVar()) + || e.getTupleIndex() < 0 + || e.getTupleIndex() >= ((ImTupleType) access.getVar().getType()).getTypes().size() + || e.isUsedAsLValue()) { + fail(e, "a tuple selection which is not a read of a results local"); + } + } + + @Override + public void visit(ImVarAccess e) { + super.visit(e); + if (!isResultsLocal(e.getVar())) { + return; + } + if (e.getParent() instanceof ImTupleSelection) { + return; + } + if (!(e.getParent() instanceof ImSet set) || set.getLeft() != e + || !isMultipleResultsCall(valueOf(set.getRight())) + || !resultsTypeOf(valueOf(set.getRight())).equalsType(e.getVar().getType())) { + fail(e, "a results local used other than as the target of a call's results or by a selection"); + } + } + + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + checkCallPosition(call); + } + + @Override + public void visit(ImMethodCall call) { + super.visit(call); + checkCallPosition(call); + } + + private void checkCallPosition(ImExpr call) { + if (!isMultipleResultsCall(call)) { + return; + } + Element value = call; + while (value.getParent() instanceof ImStatementExpr statementExpr && statementExpr.getExpr() == value) { + value = statementExpr; + } + if (!(value.getParent() instanceof ImStmts) + && !(value.getParent() instanceof ImSet set && set.getRight() == value + && set.getLeft() instanceof ImVarAccess target && isResultsLocal(target.getVar()))) { + fail(call, "a call of multiple results which is neither a statement nor assigned to a results local"); + } + } + }); + } + + private static void fail(Element e, String what) { + Element owner = e; + while (owner != null && !(owner instanceof ImFunction)) { + owner = owner.getParent(); + } + throw new IllegalStateException("Lua multiple results: " + what + ": " + e + + (owner == null ? "" : "\nin " + owner)); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java index d3ce94254..4116da60c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java @@ -15,6 +15,12 @@ public static void print(LuaAssignment s, StringBuilder sb, int indent) { s.getRight().print(sb, indent); } + public static void print(LuaMultipleAssignment s, StringBuilder sb, int indent) { + s.getTargets().print(sb, indent); + sb.append(" = "); + s.getRight().print(sb, indent); + } + public static void print(LuaBreak s, StringBuilder sb, int indent) { sb.append("break"); } @@ -507,12 +513,17 @@ public static void print(LuaReturn s, StringBuilder sb, int indent) { s.getRetVal().print(sb, indent); } + public static void print(LuaReturnValues s, StringBuilder sb, int indent) { + sb.append("return "); + s.getValues().print(sb, indent); + } + public static void print(LuaStatements stmts, StringBuilder sb, int indent) { for (LuaStatement s : stmts) { printIndent(sb, indent); s.print(sb, indent); sb.append("\n"); - if (s instanceof LuaReturn || s instanceof LuaBreak) { + if (s instanceof LuaReturn || s instanceof LuaReturnValues || s instanceof LuaBreak) { // there can be no statement after return or break ... break; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 3f6865495..0b22e7c27 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -10,6 +10,7 @@ import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.translation.imtranslation.LuaKeyedMap; import de.peeeq.wurstscript.translation.imtranslation.LuaMethodCallLowering; +import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering; import de.peeeq.wurstscript.types.TypesHelper; @@ -629,16 +630,19 @@ public static LuaExpr translate(ImStringVal e, LuaTranslator tr) { return LuaAst.LuaExprStringVal(e.getValS()); } + /** Only the results of a return are a tuple (StmtTranslation): see {@link LuaMultipleResults}. */ public static LuaExpr translate(ImTupleExpr e, LuaTranslator tr) { - LuaTableFields tableFields = LuaAst.LuaTableFields(); - for (ImExpr te : e.getExprs()) { - tableFields.add(LuaAst.LuaTableSingleField(te.translateToLua(tr))); - } - return LuaAst.LuaTableConstructor(tableFields); + throw new CompileError(e.attrTrace().attrSource(), + "Lua backend: a tuple which is not the results of a return: " + e); } + /** A component of a call's results is the local it was received in. */ public static LuaExpr translate(ImTupleSelection e, LuaTranslator tr) { - return LuaAst.LuaExprArrayAccess(e.getTupleExpr().translateToLua(tr), LuaAst.LuaExprlist(LuaAst.LuaExprIntVal("" + (1 + e.getTupleIndex())))); + if (e.getTupleExpr() instanceof ImVarAccess access && LuaMultipleResults.isResultsLocal(access.getVar())) { + return LuaAst.LuaExprVarAccess(tr.resultVars(access.getVar()).get(e.getTupleIndex())); + } + throw new CompileError(e.attrTrace().attrSource(), + "Lua backend: a tuple selection which is not of a call's results: " + e); } public static LuaExpr translate(ImTypeIdOfClass e, LuaTranslator tr) { @@ -654,6 +658,10 @@ public static LuaExpr translate(ImTypeIdOfObj e, LuaTranslator tr) { } public static LuaExpr translate(ImVarAccess e, LuaTranslator tr) { + if (LuaMultipleResults.isResultsLocal(e.getVar())) { + throw new CompileError(e.attrTrace().attrSource(), + "Lua backend: the results of a call read other than by a component: " + e); + } return LuaAst.LuaExprVarAccess(tr.luaVar.getFor(e.getVar())); } 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 9725279b4..af826f654 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 @@ -11,6 +11,7 @@ import de.peeeq.wurstscript.translation.imtranslation.LuaFieldDefaults; import de.peeeq.wurstscript.translation.imtranslation.GenericTypes; import de.peeeq.wurstscript.translation.imtranslation.LuaDispatchPreparation; +import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering; import de.peeeq.wurstscript.translation.lua.printing.LuaPrinter; import de.peeeq.wurstscript.types.TypesHelper; @@ -187,6 +188,20 @@ public LuaVariable initFor(ImVar a) { } }; + /** The Lua locals of each results local, one per component ({@link LuaMultipleResults}). Only looked up. */ + private final Map> luaResultVars = new IdentityHashMap<>(); + + List resultVars(ImVar local) { + return luaResultVars.computeIfAbsent(local, v -> { + ImTupleType type = (ImTupleType) v.getType(); + List vars = new ArrayList<>(); + for (String component : type.getNames()) { + vars.add(LuaAst.LuaVariable(uniqueName(v.getName() + "_" + component), LuaAst.LuaNoExpr())); + } + return vars; + }); + } + GetAForB luaFunc = new GetAForB() { @Override @@ -895,6 +910,15 @@ private void translateFunc(ImFunction f) { // translate local variables for (ImVar local : f.getLocals()) { + if (LuaMultipleResults.isResultsLocal(local)) { + List types = ((ImTupleType) local.getType()).getTypes(); + List components = resultVars(local); + for (int i = 0; i < types.size(); i++) { + components.get(i).setInitialValue(defaultValue(types.get(i))); + lf.getBody().add(components.get(i)); + } + continue; + } LuaVariable luaLocal = luaVar.getFor(local); luaLocal.setInitialValue(defaultValue(local.getType())); lf.getBody().add(luaLocal); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index f0cde2434..521825065 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -4,6 +4,7 @@ import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.luaAst.*; import de.peeeq.wurstscript.translation.imtranslation.LuaKeyedMap; +import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; import de.peeeq.wurstscript.translation.imtranslation.LuaTraps; import de.peeeq.wurstscript.translation.lua.printing.LuaPrinter; @@ -187,10 +188,23 @@ public static void translate(ImIf s, List res, LuaTranslator tr) { } public static void translate(ImReturn s, List res, LuaTranslator tr) { + if (s.getReturnValue() instanceof ImTupleExpr results) { + res.add(LuaAst.LuaReturnValues(tr.translateExprList(results.getExprs()))); + return; + } res.add(LuaAst.LuaReturn(tr.translateOptional(s.getReturnValue()))); } public static void translate(ImSet s, List res, LuaTranslator tr) { + if (s.getLeft() instanceof ImVarAccess target && LuaMultipleResults.isResultsLocal(target.getVar())) { + // the results of a call, one local for each + LuaExprlist targets = LuaAst.LuaExprlist(); + for (LuaVariable component : tr.resultVars(target.getVar())) { + targets.add(LuaAst.LuaExprVarAccess(component)); + } + res.add(LuaAst.LuaMultipleAssignment(targets, s.getRight().translateToLua(tr))); + return; + } LuaExpr left; if (s.getLeft() instanceof ImVarArrayAccess) { // Assignment LHS must stay a writable table access, never an ensured r-value wrapper. @@ -199,13 +213,6 @@ public static void translate(ImSet s, List res, LuaTranslator tr) left = s.getLeft().translateToLua(tr); } LuaExpr right = s.getRight().translateToLua(tr); - if (s.getRight().attrTyp() instanceof ImTupleType) { - ImTupleType tt = (ImTupleType) s.getRight().attrTyp(); - // tuples must be copied, unless they are literals - if(!(right instanceof LuaTableConstructor)) { - right = LuaAst.LuaExprFunctionCall(ExprTranslation.getTupleCopyFunc(tt, tr), LuaAst.LuaExprlist(right)); - } - } res.add(LuaAst.LuaAssignment(left, right)); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java new file mode 100644 index 000000000..7a064ef0c --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java @@ -0,0 +1,368 @@ +package tests.wurstscript.tests; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import de.peeeq.wurstio.WurstCompilerJassImpl; +import de.peeeq.wurstscript.RunArgs; +import de.peeeq.wurstscript.ast.WurstModel; +import de.peeeq.wurstscript.gui.WurstGuiCliImpl; +import de.peeeq.wurstscript.luaAst.LuaCompilationUnit; +import org.testng.annotations.Test; +import org.wurstscript.projectconfig.WurstProjectConfigData; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +/** + * A function returning a tuple returns its components as Lua multiple results, and a call takes them into locals of + * its own ({@code local a, b = f()}), instead of passing every component but the first through a global of the + * function. Each test runs the emitted Lua; most also check its shape. + */ +public class LuaMultipleResultsTests extends WurstScriptTest { + + private String compiledLua(String testName) throws IOException { + return Files.toString(new File("test-output/lua/LuaMultipleResultsTests_" + testName + ".lua"), Charsets.UTF_8); + } + + private String compileLua(RunArgs runArgs, String... lines) { + WurstGuiCliImpl gui = new WurstGuiCliImpl(); + WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(null, gui, null, runArgs); + WurstModel model = parseFiles(Collections.emptyList(), + Collections.singletonList(new CU("test.wurst", String.join("\n", lines))), false, compiler); + assertTrue("unexpected errors: " + gui.getErrorList(), gui.getErrorList().isEmpty()); + compiler.checkProg(model); + assertTrue("unexpected errors: " + gui.getErrorList(), gui.getErrorList().isEmpty()); + compiler.translateProgToIm(model); + compiler.runCompiletime(WurstProjectConfigData.empty(), false, false); + LuaCompilationUnit luaCode = compiler.transformProgToLua(); + StringBuilder result = new StringBuilder(); + luaCode.print(result, 0); + return result.toString(); + } + + /** The body of the Lua function {@code name}. */ + private static String functionBody(String lua, String name) { + Matcher m = Pattern.compile("(?m)^function " + Pattern.quote(name) + "\\(([^)]*)\\)\\s*\\n(.*?)\\nend$", + Pattern.DOTALL).matcher(lua); + assertTrue("no function " + name + " in\n" + lua, m.find()); + return m.group(2); + } + + private static void assertNoReturnGlobals(String lua) { + assertFalse("a tuple component must not travel through a return global:\n" + lua, + Pattern.compile("\\w+_return_\\w+").matcher(lua).find()); + } + + @Test + public void aTupleIsReturnedAsMultipleResults() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "@noinline function makePair(int a, int b) returns pair", + " return pair(a + 1, b + 2)", + "init", + " let p = makePair(4, 10)", + " if p.x == 5 and p.y == 12", + " testSuccess()" + ); + String lua = compiledLua("aTupleIsReturnedAsMultipleResults"); + assertNoReturnGlobals(lua); + assertTrue("the components are returned together:\n" + lua, + Pattern.compile("(?m)^\\s*return \\(?\\w+ \\+ 1\\)?, \\(?\\w+ \\+ 2\\)?\\s*$").matcher(functionBody(lua, "makePair")).find()); + assertTrue("a call takes both results:\n" + lua, + Pattern.compile("\\w+, \\w+ = makePair\\(").matcher(lua).find()); + } + + @Test + public void nestedTuplesAreReturnedAsTheirScalarComponents() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple vec(real x, real y)", + "tuple seg(vec a, vec b)", + "@noinline function makeSeg(real s) returns seg", + " return seg(vec(s, s + 1.), vec(s + 2., s + 3.))", + "init", + " let g = makeSeg(10.)", + " if g.a.x == 10. and g.a.y == 11. and g.b.x == 12. and g.b.y == 13. and g.b == vec(12., 13.)", + " testSuccess()" + ); + String lua = compiledLua("nestedTuplesAreReturnedAsTheirScalarComponents"); + assertNoReturnGlobals(lua); + assertTrue("a call takes all four results:\n" + lua, + Pattern.compile("\\w+, \\w+, \\w+, \\w+ = makeSeg\\(").matcher(lua).find()); + } + + @Test + public void aMethodReturnsMultipleResultsThroughDispatch() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "abstract class Shape", + " abstract function size(int scale) returns pair", + "class Square extends Shape", + " override function size(int scale) returns pair", + " return pair(scale, scale)", + "class Strip extends Shape", + " override function size(int scale) returns pair", + " return pair(scale * 3, 1)", + "@noinline function area(Shape s) returns int", + " let p = s.size(2)", + " return p.x * p.y", + "init", + " if area(new Square()) == 4 and area(new Strip()) == 6", + " testSuccess()" + ); + String lua = compiledLua("aMethodReturnsMultipleResultsThroughDispatch"); + assertNoReturnGlobals(lua); + assertTrue("a dispatched call takes both results:\n" + lua, + Pattern.compile("\\w+, \\w+ = __wurst_objectClass\\[[^\\]]+\\]\\.\\w+\\(").matcher(lua).find()); + } + + @Test + public void aClosureReturnsMultipleResults() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "interface Maker", + " function make(int x) returns pair", + "@noinline function useMaker(Maker m) returns int", + " let p = m.make(5)", + " return p.x + p.y * 10", + "init", + " int offset = 2", + " Maker m = x -> pair(x + offset, x)", + " if useMaker(m) == 57", + " testSuccess()" + ); + assertNoReturnGlobals(compiledLua("aClosureReturnsMultipleResults")); + } + + @Test + public void resultsPassThroughReturnsAndRecursion() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "@noinline function fib(int n) returns pair", + " if n == 0", + " return pair(0, 1)", + " let p = fib(n - 1)", + " return pair(p.y, p.x + p.y)", + "@noinline function forward(int n) returns pair", + " return fib(n)", + "@noinline function swapped(int n) returns pair", + " let p = forward(n)", + " return pair(p.y, p.x)", + "init", + " let a = forward(10)", + " let b = swapped(10)", + " if a == pair(55, 89) and b == pair(89, 55)", + " testSuccess()" + ); + assertNoReturnGlobals(compiledLua("resultsPassThroughReturnsAndRecursion")); + } + + @Test + public void callsWhoseResultsAreUnusedStillRunInOrder() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int trace = 0", + "@noinline function advance(int v) returns pair", + " trace = trace * 10 + v", + " return pair(v, trace)", + "@noinline function both(pair a, pair b) returns int", + " return a.x * 1000 + a.y * 100 + b.x * 10 + b.y", + "init", + " advance(1)", + " let second = advance(2).y", + " let r = both(advance(3), advance(4))", + " if second == 12 and r == 3 * 1000 + 123 * 100 + 4 * 10 + 1234 and trace == 1234", + " testSuccess()" + ); + assertNoReturnGlobals(compiledLua("callsWhoseResultsAreUnusedStillRunInOrder")); + } + + @Test + public void aResultNoCallerReadsIsNotReturned() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple triple(int x, int y, int z)", + "@noinline function make(int a) returns triple", + " return triple(a + 1, a * 7, a + 3)", + "@noinline function relay(int a) returns triple", + " return make(a)", + "init", + " let t = relay(4)", + " if t.x == 5 and t.z == 7", + " testSuccess()" + ); + String lua = compiledLua("aResultNoCallerReadsIsNotReturned"); + assertNoReturnGlobals(lua); + assertFalse("the component nobody reads is not computed:\n" + lua, lua.contains("* 7")); + assertTrue("make returns the two read components:\n" + lua, + Pattern.compile("(?m)^\\s*return \\(?\\w+ \\+ 1\\)?, \\(?\\w+ \\+ 3\\)?\\s*$").matcher(functionBody(lua, "make")).find()); + } + + @Test + public void aResultWithAnEffectWhichNoCallerReadsStillRunsOnce() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "int calls = 0", + "int trace = 0", + "@noinline function count() returns int", + " calls++", + " trace = trace * 10 + 2", + " return calls", + "@noinline function first() returns int", + " trace = trace * 10 + 1", + " return 5", + "@noinline function make() returns pair", + " return pair(first(), count())", + "init", + " let x = make().x", + " if x == 5 and calls == 1 and trace == 12", + " testSuccess()" + ); + assertNoReturnGlobals(compiledLua("aResultWithAnEffectWhichNoCallerReadsStillRunsOnce")); + } + + @Test + public void aResultReadThroughOneOverrideIsReturnedByEveryOverride() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple pair(int x, int y)", + "abstract class Base", + " abstract function get() returns pair", + "class A extends Base", + " override function get() returns pair", + " return pair(1, 2)", + "class B extends Base", + " override function get() returns pair", + " return pair(3, 4)", + "@noinline function onlyX(A a) returns int", + " return a.get().x", + "@noinline function bothThroughBase(Base b) returns int", + " let p = b.get()", + " return p.x * 10 + p.y", + "init", + " if onlyX(new A()) == 1 and bothThroughBase(new A()) == 12 and bothThroughBase(new B()) == 34", + " testSuccess()" + ); + assertNoReturnGlobals(compiledLua("aResultReadThroughOneOverrideIsReturnedByEveryOverride")); + } + + @Test + public void aTupleOfOneComponentIsAnOrdinaryReturn() throws IOException { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "tuple single(int v)", + "@noinline function wrap(int a) returns single", + " return single(a * 2)", + "init", + " if wrap(21).v == 42", + " testSuccess()" + ); + String lua = compiledLua("aTupleOfOneComponentIsAnOrdinaryReturn"); + assertNoReturnGlobals(lua); + assertTrue(lua, Pattern.compile("(?m)^\\s*return \\(?\\w+ \\* 2\\)?\\s*$").matcher(functionBody(lua, "wrap")).find()); + } + + @Test + public void resultsAreStoredInTheLocalsTableOfAFunctionOverTheLocalLimit() throws IOException { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("native testSuccess()"); + lines.add("tuple pair(int x, int y)"); + lines.add("@noinline function makePair(int a) returns pair"); + lines.add(" return pair(a, a + 1)"); + lines.add("@noinline function big(int seed) returns int"); + StringBuilder sum = new StringBuilder("p.x + p.y"); + for (int i = 0; i < 210; i++) { + lines.add(" int v" + i + " = seed + " + i); + sum.append(" + v").append(i); + } + lines.add(" let p = makePair(seed)"); + lines.add(" return " + sum); + lines.add("init"); + // 2 * 1 + 1 + 210 * 1 + (0 + ... + 209) + lines.add(" if big(1) == " + (1 + 2 + 210 + 209 * 210 / 2)); + lines.add(" testSuccess()"); + test().testLua(true).executeProg().lines(lines.toArray(new String[0])); + String lua = compiledLua("resultsAreStoredInTheLocalsTableOfAFunctionOverTheLocalLimit"); + assertNoReturnGlobals(lua); + assertTrue("both results go to slots of the locals table:\n" + functionBody(lua, "big"), + Pattern.compile("__wurst_locals\\d*\\[\\d+\\], __wurst_locals\\d*\\[\\d+\\] = makePair\\(") + .matcher(functionBody(lua, "big")).find()); + } + + @Test + public void optimizedCallsTakeTheResultsIntoLocals() { + String lua = compileLua(new RunArgs().with("-lua", "-inline", "-localOptimizations"), + "package Test", + "tuple vec2(real x, real y)", + "native consume(real x, real y)", + "@noinline function add(vec2 left, vec2 right) returns vec2", + " return vec2(left.x + right.x, left.y + right.y)", + "init", + " let result = add(vec2(1., 2.), vec2(3., 4.))", + " consume(result.x, result.y)"); + assertNoReturnGlobals(lua); + String add = functionBody(lua, "add"); + assertEquals("add returns once, both components:\n" + add, 1, add.split("\\breturn\\b", -1).length - 1); + assertTrue(add, Pattern.compile("(?m)^\\s*return [^,\\n]+, [^,\\n]+$").matcher(add).find()); + assertTrue("the call takes both results:\n" + lua, + Pattern.compile("(\\w+), (\\w+) = add\\(").matcher(lua).find()); + } + + @Test + public void emissionIsDeterministic() { + String[] source = { + "package Test", + "tuple pair(int x, int y)", + "tuple triple(int x, int y, int z)", + "native consume(int x)", + "abstract class Base", + " abstract function get(int s) returns triple", + "class A extends Base", + " override function get(int s) returns triple", + " return triple(s, s + 1, s + 2)", + "class B extends Base", + " override function get(int s) returns triple", + " return triple(s * 2, s * 3, s * 4)", + "@noinline function make(int a) returns pair", + " return pair(a, a + 1)", + "init", + " Base b = new A()", + " let t = b.get(3)", + " let p = make(t.x)", + " consume(p.y + t.z)", + " b = new B()", + " consume(b.get(4).y)" + }; + RunArgs args = new RunArgs().with("-lua", "-inline", "-localOptimizations"); + String first = compileLua(args, source); + String second = compileLua(args, source); + assertEquals(first, second); + assertNoReturnGlobals(first); + } +} From fa46dccceaa16f83b0ca045291cc6727d16959fe Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 10 Oct 2026 12:54:49 +0200 Subject: [PATCH 2/6] Copy propagation: a component of a call's results is a copy let p = f() becomes p_x = t.0; p_y = t.1 after the call, and the copies stayed: copy propagation knew only a variable or a constant as a value. A component of a results local is one too, until another call writes the local, so the reads of p_x read t.0 and the copy is garbage. --- .../optimizer/ConstantAndCopyPropagation.java | 27 ++++++++++++++++--- .../tests/LuaMultipleResultsTests.java | 6 +++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java index 7343029fa..3c574adfb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java @@ -6,6 +6,7 @@ import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; +import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; import de.peeeq.wurstscript.types.TypesHelper; import io.vavr.Tuple2; import io.vavr.collection.HashMap; @@ -37,26 +38,36 @@ public String getName() { static class Value { final @Nullable ImVar copyVar; + /** With a copyVar which holds a call's results (LuaMultipleResults): the component copied, otherwise -1. */ + final int component; final @Nullable ImConst constantValue; final @Nullable ImTupleExpr constantTuple; public Value(ImVar copyVar) { - this.copyVar = copyVar; + this(copyVar, -1); + } + + /** A copy of component {@code component} of the results local {@code results}. */ + public Value(ImVar results, int component) { + this.copyVar = results; + this.component = component; this.constantValue = null; this.constantTuple = null; - if(copyVar.isGlobal()) { + if(results.isGlobal()) { throw new IllegalArgumentException("copyVar must not be a Global."); } } public Value(ImConst constantValue) { this.copyVar = null; + this.component = -1; this.constantValue = constantValue; this.constantTuple = null; } public Value(ImTupleExpr tupleExpr) { this.copyVar = null; + this.component = -1; this.constantValue = null; this.constantTuple = tupleExpr; @@ -93,7 +104,7 @@ public boolean equals(@Nullable Object obj) { public boolean equalValue(Value other) { if (copyVar != null && other.copyVar != null) { - return copyVar == other.copyVar; + return copyVar == other.copyVar && component == other.component; } else if (constantValue != null && other.constantValue != null) { return constantValue.equalValue(other.constantValue); } else if (constantTuple != null && other.constantTuple != null) { @@ -117,7 +128,7 @@ public boolean equalValue(Value other) { @Override public String toString() { if (copyVar != null) { - return "copy of " + copyVar; + return "copy of " + copyVar + (component < 0 ? "" : "." + component); } else if (constantValue != null) { return "constant " + constantValue; } else { @@ -180,6 +191,9 @@ public void visit(ImVarAccess va) { if (val.constantValue != null) { va.replaceBy(val.constantValue.copy()); totalPropagated++; + } else if (val.component >= 0) { + va.replaceBy(JassIm.ImTupleSelection(JassIm.ImVarAccess(val.copyVar), val.component)); + totalPropagated++; } else if (val.copyVar != null) { ImVar old = va.getVar(); ImVar target = val.copyVar; @@ -328,6 +342,11 @@ private void analyzeComponent(List scc, Map knowledge) { } } else if(right instanceof ImTupleExpr) { newValue = Value.tryValue(right); + } else if (right instanceof ImTupleSelection selection + && selection.getTupleExpr() instanceof ImVarAccess results + && LuaMultipleResults.isResultsLocal(results.getVar())) { + // a component of a call's results, which only another call writes + newValue = new Value(results.getVar(), selection.getTupleIndex()); } if (newValue == null) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java index 7a064ef0c..de8c8a53e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java @@ -330,8 +330,10 @@ public void optimizedCallsTakeTheResultsIntoLocals() { String add = functionBody(lua, "add"); assertEquals("add returns once, both components:\n" + add, 1, add.split("\\breturn\\b", -1).length - 1); assertTrue(add, Pattern.compile("(?m)^\\s*return [^,\\n]+, [^,\\n]+$").matcher(add).find()); - assertTrue("the call takes both results:\n" + lua, - Pattern.compile("(\\w+), (\\w+) = add\\(").matcher(lua).find()); + Matcher call = Pattern.compile("(\\w+), (\\w+) = add\\(").matcher(lua); + assertTrue("the call takes both results:\n" + lua, call.find()); + assertTrue("the results are used where they were received, not copied first:\n" + lua, + lua.contains("consume(" + call.group(1) + ", " + call.group(2) + ")")); } @Test From 854b02e6011ada8cb1409236bc65ea41fc9a3afd Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 10 Oct 2026 13:12:07 +0200 Subject: [PATCH 3/6] Lua: print the hashtable natives where they are called Each emulated hashtable native was a call of a __wurst_ helper: a global lookup and a call on every load and save. The backend now prints the table accesses the helper consists of: a load is (h.T[p] or __wurst_htEmpty)[c] with the default the helper answered, a save (h.T[p] or __wurst_htNewChild(h.T, p))[c] = v, a remove tests the child first, the flushes reset the subtables. An operation keeps the call of its helper where an operand has an effect, since printed in place an operand is read again or not at all; the helper is then defined, and only then. LuaNativeLowering keeps the stubs in ImTranslator.luaHashtableStubs, so the optimizer and the backend match them by identity (LuaHashtable). Loads and HaveSaved tests count as reads (isLuaTableRead), so an unused one is dropped, as the Jass natives already were. The helper bodies lose the tests for a missing subtable, which InitHashtable always creates. The printer puts a ';' before an assignment whose target starts with '(', which Lua would read as the arguments of a call ending the previous line. --- .../translation/imoptimizer/ImInliner.java | 2 +- .../translation/imoptimizer/ImOptimizer.java | 2 +- .../UselessFunctionCallsRemover.java | 2 +- .../imtranslation/ImTranslator.java | 15 +- .../imtranslation/LuaHashtable.java | 116 +++++++ .../imtranslation/LuaNativeLowering.java | 44 +-- .../translation/lua/printing/LuaPrinter.java | 10 +- .../lua/translation/ExprTranslation.java | 5 + .../lua/translation/LuaAssertions.java | 10 +- .../translation/LuaHashtableTranslation.java | 177 +++++++++++ .../lua/translation/LuaNatives.java | 246 ++++----------- .../lua/translation/LuaTranslator.java | 33 ++ .../lua/translation/StmtTranslation.java | 17 +- .../tests/DeterministicChecks.java | 37 +++ .../tests/LuaBackendAuditTests.java | 3 +- .../wurstscript/tests/LuaHashtableTests.java | 298 ++++++++++++++++++ .../wurstscript/tests/LuaKeyedMapTests.java | 10 +- .../wurstscript/tests/LuaNativesTests.java | 15 + .../tests/LuaTranslationTests.java | 76 +++-- 19 files changed, 859 insertions(+), 259 deletions(-) create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java create mode 100644 de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java index 4fdf2612e..c6ce0dbc9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java @@ -435,7 +435,7 @@ private boolean isEffectFree(Element e) { ImFunction target = call.getFunc(); if (!target.isNative() || localPlayerAnalysis().isLocalPlayerSource(target) || !(UselessFunctionCallsRemover.isFunctionWithoutSideEffect(target.getName()) - || translator.isLuaKeyedMapRead(target))) { + || translator.isLuaTableRead(target))) { return false; } } else if (e instanceof ImExpr) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java index 4c485859d..56f5043f2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java @@ -473,7 +473,7 @@ List collectSideEffects(ImExpr expr, SideEffectAnalyzer analyzer) { } if (analyzer.hasObservableSideEffects(expr, func -> func.isNative() && (UselessFunctionCallsRemover.isFunctionWithoutSideEffect(func.getName()) - || trans.isLuaKeyedMapRead(func)))) { + || trans.isLuaTableRead(func)))) { return Collections.singletonList(expr); } return Collections.emptyList(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/UselessFunctionCallsRemover.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/UselessFunctionCallsRemover.java index 15cf95193..9f27db962 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/UselessFunctionCallsRemover.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/UselessFunctionCallsRemover.java @@ -110,7 +110,7 @@ private void optimizeElement(Element e) { private boolean isNativeWithoutSideEffect(ImFunction func) { return func.isNative() && !localPlayerContextAnalyzer.isLocalPlayerSource(func) - && (isFunctionWithoutSideEffect(func.getName()) || translator.isLuaKeyedMapRead(func)); + && (isFunctionWithoutSideEffect(func.getName()) || translator.isLuaTableRead(func)); } /** 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 b169663ef..e5f548366 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 @@ -219,6 +219,12 @@ public T canonical(T copy) { * call was lowered to instead of a name. */ public final Map luaKeyedStubs = new LinkedHashMap<>(); + /** + * The Lua hashtable native stubs, by stub name ({@link LuaHashtable#stubName}). {@link LuaNativeLowering#transform} + * creates each one once, at the calls of its native, so later passes can match a stub by identity. Not pinned: + * the stubs are created after the last tree shake, and one which nothing calls any more needs no definition. + */ + public final Map luaHashtableStubs = new LinkedHashMap<>(); /** * A call to one of the Lua backend's operator intrinsics which cannot fail at runtime: a @@ -245,11 +251,12 @@ public boolean isTrapFreeLuaIntrinsicCall(ImFunctionCall call) { || target == luaRawFloorModRealFunc); } /** - * Whether {@code f} is one of the Lua keyed-map read stubs, which the backend prints as a table - * index. A call whose result nothing uses may be dropped, as for the Jass natives which only read. + * Whether {@code f} is one of the Lua stubs which only read a table: a keyed-map read, or a hashtable + * load or HaveSaved test. The backend prints them as table indexes. A call whose result nothing uses + * may be dropped, as for the Jass natives which only read. */ - public boolean isLuaKeyedMapRead(ImFunction f) { - return LuaKeyedMap.readStubName(this, f) != null; + public boolean isLuaTableRead(ImFunction f) { + return LuaKeyedMap.readStubName(this, f) != null || LuaHashtable.isRead(this, f); } @Nullable public ImFunction luaIntDivFunc = null; @Nullable public ImFunction luaModIntFunc = null; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java new file mode 100644 index 000000000..fc22f423d --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaHashtable.java @@ -0,0 +1,116 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.jassIm.ImFunction; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The Jass hashtable natives on Lua. A hashtable is a table with one subtable per kind of value, as Jass keeps the + * kinds apart under the same keys: {@code { __wurst_ht_int = {}, __wurst_ht_bool = {}, ... }}. A subtable maps a + * parent key to a child table, which maps a child key to the value. Only InitHashtable creates one (a compile-time + * hashtable is emitted as a call of it and its saves) and FlushParentHashtable gives it five new subtables, so every + * subtable is always there. + * + *

{@link LuaNativeLowering} points every call of one of these natives at a stub {@code __wurst_}, made once + * per native and kept in {@link ImTranslator#luaHashtableStubs}. The optimizer and the backend recognise the stub by + * identity: a user function of the same name is an ordinary function. + */ +public final class LuaHashtable { + + /** What an operation does, and the number of its arguments. */ + public enum Kind { + INIT(0), LOAD(3), HAVE(3), SAVE(4), REMOVE(3), FLUSH_CHILD(2), FLUSH_PARENT(1); + + public final int arity; + + Kind(int arity) { + this.arity = arity; + } + } + + /** The kinds of value, each kept in its own subtable. */ + public enum Values { + INT("__wurst_ht_int"), BOOL("__wurst_ht_bool"), REAL("__wurst_ht_real"), STR("__wurst_ht_str"), + HANDLE("__wurst_ht_handle"); + + /** The field of the hashtable which holds the subtable. */ + public final String field; + + Values(String field) { + this.field = field; + } + } + + /** An operation on the values of one kind, or on all of them ({@code values} is null). */ + public record Op(Kind kind, @Nullable Values values) { + } + + /** The handle types with a Save and a Load native of their own; agent has only a Save. */ + private static final List HANDLE_TYPES = List.of( + "Player", "Widget", "Destructable", "Item", "Unit", "Ability", "Timer", "Trigger", "TriggerCondition", + "TriggerAction", "TriggerEvent", "Force", "Group", "Location", "Rect", "BooleanExpr", "Sound", "Effect", + "UnitPool", "ItemPool", "Quest", "QuestItem", "DefeatCondition", "TimerDialog", "Leaderboard", "Multiboard", + "MultiboardItem", "Trackable", "Dialog", "Button", "TextTag", "Lightning", "Image", "Ubersplat", "Region", + "FogState", "FogModifier", "Hashtable", "Frame"); + + /** The operations by stub name. Looked up, never iterated. */ + private static final Map OPS = new HashMap<>(); + + static { + put("InitHashtable", Kind.INIT, null); + put("FlushChildHashtable", Kind.FLUSH_CHILD, null); + put("FlushParentHashtable", Kind.FLUSH_PARENT, null); + family(Values.INT, "SaveInteger", "LoadInteger", "HaveSavedInteger", "RemoveSavedInteger"); + family(Values.BOOL, "SaveBoolean", "LoadBoolean", "HaveSavedBoolean", "RemoveSavedBoolean"); + family(Values.REAL, "SaveReal", "LoadReal", "HaveSavedReal", "RemoveSavedReal"); + family(Values.STR, "SaveStr", "LoadStr", "HaveSavedString", "RemoveSavedString"); + put("HaveSavedHandle", Kind.HAVE, Values.HANDLE); + put("RemoveSavedHandle", Kind.REMOVE, Values.HANDLE); + put("SaveAgentHandle", Kind.SAVE, Values.HANDLE); + for (String type : HANDLE_TYPES) { + put("Save" + type + "Handle", Kind.SAVE, Values.HANDLE); + put("Load" + type + "Handle", Kind.LOAD, Values.HANDLE); + } + } + + private static void family(Values values, String save, String load, String have, String remove) { + put(save, Kind.SAVE, values); + put(load, Kind.LOAD, values); + put(have, Kind.HAVE, values); + put(remove, Kind.REMOVE, values); + } + + private static void put(String nativeName, Kind kind, @Nullable Values values) { + OPS.put(stubName(nativeName), new Op(kind, values)); + } + + private LuaHashtable() { + } + + /** Whether the Jass native {@code name} is one of the hashtable natives. */ + public static boolean isNative(String name) { + return OPS.containsKey(stubName(name)); + } + + /** The name of the stub the calls of the native {@code nativeName} are lowered to. */ + public static String stubName(String nativeName) { + return "__wurst_" + nativeName; + } + + /** The operation {@code f} is the stub of, or null when it is no hashtable stub. */ + public static @Nullable Op op(ImTranslator translator, ImFunction f) { + if (!f.isNative() || translator.luaHashtableStubs.get(f.getName()) != f) { + return null; + } + return OPS.get(f.getName()); + } + + /** Whether {@code f} is the stub of a load or a HaveSaved test, which only read the hashtable. */ + public static boolean isRead(ImTranslator translator, ImFunction f) { + Op op = op(translator, f); + return op != null && (op.kind() == Kind.LOAD || op.kind() == Kind.HAVE); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index d235f999c..bfb98bde9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java @@ -18,7 +18,9 @@ *

  • Hashtable natives ({@code SaveInteger}, {@code LoadBoolean}, …) and * context-callback natives ({@code ForForce}, {@code ForGroup}, …) – * replaced 1:1 by their {@code __wurst_} prefixed equivalents, whose Lua - * implementations are provided by {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}.
  • + * implementations are provided by {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}. + * The backend prints a hashtable operation where it is called instead, as the table accesses + * it stands for, and calls its helper only where it cannot ({@link LuaHashtable}). *
  • All other BJ calls with at least one handle-typed parameter – wrapped * by a generated IM function that first checks each required handle param for * {@code null} and returns the type-appropriate default (0 / 0.0 / false / "" / nil), @@ -40,35 +42,6 @@ */ public final class LuaNativeLowering { - /** Hashtable native names that need to be remapped to {@code __wurst_} equivalents. */ - private static final Set HASHTABLE_NATIVE_NAMES = new HashSet<>(Arrays.asList( - "InitHashtable", - "SaveInteger", "SaveBoolean", "SaveReal", "SaveStr", - "LoadInteger", "LoadBoolean", "LoadReal", "LoadStr", - "HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", "HaveSavedHandle", - "FlushChildHashtable", "FlushParentHashtable", - "RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString", "RemoveSavedHandle", - // Handle-typed save/load variants - "SavePlayerHandle", "SaveWidgetHandle", "SaveDestructableHandle", "SaveItemHandle", "SaveUnitHandle", - "SaveAbilityHandle", "SaveTimerHandle", "SaveTriggerHandle", "SaveTriggerConditionHandle", - "SaveTriggerActionHandle", "SaveTriggerEventHandle", "SaveForceHandle", "SaveGroupHandle", - "SaveLocationHandle", "SaveRectHandle", "SaveBooleanExprHandle", "SaveSoundHandle", "SaveEffectHandle", - "SaveUnitPoolHandle", "SaveItemPoolHandle", "SaveQuestHandle", "SaveQuestItemHandle", - "SaveDefeatConditionHandle", "SaveTimerDialogHandle", "SaveLeaderboardHandle", "SaveMultiboardHandle", - "SaveMultiboardItemHandle", "SaveTrackableHandle", "SaveDialogHandle", "SaveButtonHandle", - "SaveTextTagHandle", "SaveLightningHandle", "SaveImageHandle", "SaveUbersplatHandle", "SaveRegionHandle", - "SaveFogStateHandle", "SaveFogModifierHandle", "SaveAgentHandle", "SaveHashtableHandle", "SaveFrameHandle", - "LoadPlayerHandle", "LoadWidgetHandle", "LoadDestructableHandle", "LoadItemHandle", "LoadUnitHandle", - "LoadAbilityHandle", "LoadTimerHandle", "LoadTriggerHandle", "LoadTriggerConditionHandle", - "LoadTriggerActionHandle", "LoadTriggerEventHandle", "LoadForceHandle", "LoadGroupHandle", - "LoadLocationHandle", "LoadRectHandle", "LoadBooleanExprHandle", "LoadSoundHandle", "LoadEffectHandle", - "LoadUnitPoolHandle", "LoadItemPoolHandle", "LoadQuestHandle", "LoadQuestItemHandle", - "LoadDefeatConditionHandle", "LoadTimerDialogHandle", "LoadLeaderboardHandle", "LoadMultiboardHandle", - "LoadMultiboardItemHandle", "LoadTrackableHandle", "LoadDialogHandle", "LoadButtonHandle", - "LoadTextTagHandle", "LoadLightningHandle", "LoadImageHandle", "LoadUbersplatHandle", "LoadRegionHandle", - "LoadFogStateHandle", "LoadFogModifierHandle", "LoadHashtableHandle", "LoadFrameHandle" - )); - /** Context-callback natives that need to be remapped to {@code __wurst_} equivalents. */ private static final Set CONTEXT_CALLBACK_NATIVE_NAMES = new HashSet<>(Arrays.asList( "ForForce", "GetEnumPlayer", @@ -281,7 +254,10 @@ public void visit(ImFunctionCall call) { ImFunction r = computeReplacement(f); if (r != null) { replacements.put(f, r); - deferredAdditions.add(r); + // A hashtable stub of an earlier run is already in the program, unless it was removed. + if (r.getParent() == null) { + deferredAdditions.add(r); + } } else { noReplacement.add(f); } @@ -299,8 +275,10 @@ public void visit(ImFunctionCall call) { private ImFunction computeReplacement(ImFunction bj) { String name = bj.getName(); - if (HASHTABLE_NATIVE_NAMES.contains(name)) { - return createNativeStub("__wurst_" + name, bj); + if (LuaHashtable.isNative(name)) { + // Kept by the translator, so the optimizer and the backend match the stub by identity. + return translator.luaHashtableStubs.computeIfAbsent(LuaHashtable.stubName(name), + stubName -> createNativeStub(stubName, bj)); } else if (CONTEXT_CALLBACK_NATIVE_NAMES.contains(name)) { return createNativeStub("__wurst_" + name, bj); } else if (hasHandleParam(bj)) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java index 4116da60c..302f03f20 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/printing/LuaPrinter.java @@ -10,7 +10,12 @@ public class LuaPrinter { public static void print(LuaAssignment s, StringBuilder sb, int indent) { - s.getLeft().print(sb, indent); + // Lua joins a statement which starts with '(' onto the previous line as a call; ';' ends that line. + LuaExpr target = s.getLeft(); + if (target instanceof LuaLiteral literal ? literal.getLuaCode().startsWith("(") : !startsWithName(target)) { + sb.append(";"); + } + target.print(sb, indent); sb.append(" = "); s.getRight().print(sb, indent); } @@ -319,7 +324,8 @@ public static void print(LuaExprMethodCall e, StringBuilder sb, int indent) { } private static void printPostfixReceiver(LuaExpr receiver, StringBuilder sb, int indent) { - if (isPrefixExpression(receiver)) { + // A binary operation prints in parentheses of its own. + if (isPrefixExpression(receiver) || receiver instanceof LuaExprBinary) { receiver.print(sb, indent); } else { sb.append("("); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 0b22e7c27..93274b74f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -8,6 +8,7 @@ import de.peeeq.wurstscript.luaAst.*; import de.peeeq.wurstscript.translation.imtranslation.EliminateLocalTypes; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; +import de.peeeq.wurstscript.translation.imtranslation.LuaHashtable; import de.peeeq.wurstscript.translation.imtranslation.LuaKeyedMap; import de.peeeq.wurstscript.translation.imtranslation.LuaMethodCallLowering; import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; @@ -153,6 +154,10 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { throw new CompileError(e.attrTrace().attrSource(), "Lua backend: " + imFuncName + " used as an expression; a keyed-map store must be a statement."); } + LuaHashtable.Op hashtableOp = LuaHashtable.op(tr.imTr, e.getFunc()); + if (hashtableOp != null) { + return LuaHashtableTranslation.expression(e, hashtableOp, tr); + } if (isBackendIntrinsic(e.getFunc(), tr)) { if (e.getArguments().size() != 2) { throw new CompileError(e.attrTrace().attrSource(), diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java index f76f19bf9..7bcd4d049 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaAssertions.java @@ -151,18 +151,24 @@ public static void assertNoLeakedGetHandleIdCalls(String luaCode) { * Asserts that {@code luaCode} contains no raw call to any of the Jass hashtable * natives (SaveInteger, LoadBoolean, …) that should have been rewritten to their * {@code __wurst_} prefixed counterparts, and that every {@code __wurst_} hashtable - * helper that is called is also defined in the output. + * helper that is called is also defined in the output: a stub's helper is defined only + * where a call could not be printed in place, and the child constructor of an in-place + * save only where one is. */ public static void assertNoLeakedHashtableNativeCalls(String luaCode) { List leaked = new ArrayList<>(); List missingHelpers = new ArrayList<>(); Set calledFunctionNames = collectCalledFunctionNames(luaCode); Set definedFunctionNames = collectDefinedFunctionNames(luaCode); + List helperNames = new ArrayList<>(); for (String nativeName : LuaTranslator.allHashtableNativeNames()) { if (calledFunctionNames.contains(nativeName)) { leaked.add(nativeName); } - String helperName = "__wurst_" + nativeName; + helperNames.add("__wurst_" + nativeName); + } + helperNames.add(LuaHashtableTranslation.NEW_CHILD); + for (String helperName : helperNames) { boolean helperCalled = calledFunctionNames.contains(helperName); boolean helperDefined = definedFunctionNames.contains(helperName); if (helperCalled && !helperDefined) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java new file mode 100644 index 000000000..b9ec02d1d --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaHashtableTranslation.java @@ -0,0 +1,177 @@ +package de.peeeq.wurstscript.translation.lua.translation; + +import de.peeeq.wurstscript.attributes.CompileError; +import de.peeeq.wurstscript.jassIm.ImExpr; +import de.peeeq.wurstscript.jassIm.ImExprs; +import de.peeeq.wurstscript.jassIm.ImFunctionCall; +import de.peeeq.wurstscript.luaAst.*; +import de.peeeq.wurstscript.translation.imtranslation.LuaHashtable; + +import java.util.ArrayList; +import java.util.List; + +/** + * The hashtable natives printed where they are called, as the table accesses their helpers consist of + * ({@link LuaHashtable} has the representation). A helper call costs a global lookup and a call on every + * execution, and maps call these in their hottest code. With {@code h, p, c, v} the arguments and {@code T} + * the subtable of the value kind: + *
      + *
    • InitHashtable is the table constructor;
    • + *
    • a load is {@code (h.T[p] or __wurst_htEmpty)[c]}, then the default the helper answers: {@code or 0}, + * {@code or 0.0}, {@code == true}, and nothing for a string or a handle, which are nil when absent;
    • + *
    • a HaveSaved test is the same read {@code ~= nil};
    • + *
    • a save is {@code (h.T[p] or __wurst_htNewChild(h.T, p))[c] = v};
    • + *
    • a remove is {@code if h.T[p] then h.T[p][c] = nil end}. Not through the empty child: in Lua 5.3 + * {@code t[k] = nil} under an absent key inserts a dead key, and nothing may write the shared table;
    • + *
    • FlushChildHashtable is {@code h.T[p] = nil} and FlushParentHashtable {@code h.T = {}}, for each T.
    • + *
    + * A missing key, a nil hashtable and a nil key on a store raise or read nil exactly as in the helpers. + * + *

    A call evaluates each argument once and in order, before the body. Printed in place an operand is read + * once more on some paths (a save which creates the child, a remove) or not at all (the child key of a remove + * whose child is absent), and the later operands are read after part of the body ran, which only touches the + * compiler-owned tables. That is the same only when no operand has an effect, so an operation is printed in + * place only when each operand is a literal, a variable or a read ({@link StmtTranslation#readsOnly}), and is + * otherwise the call of its helper, which is then defined ({@link LuaTranslator#hashtableHelper}). + */ +final class LuaHashtableTranslation { + + /** The main-chunk locals the in-place operations use, each declared when first used. */ + static final String EMPTY = "__wurst_htEmpty"; + static final String NEW_CHILD = "__wurst_htNewChild"; + + private LuaHashtableTranslation() { + } + + /** An operation in an expression: InitHashtable, a load or a test in place, anything else as the call. */ + static LuaExpr expression(ImFunctionCall call, LuaHashtable.Op op, LuaTranslator tr) { + ImExprs args = checkArity(call, op); + if (op.kind() == LuaHashtable.Kind.INIT) { + return newHashtable(); + } + boolean read = op.kind() == LuaHashtable.Kind.LOAD || op.kind() == LuaHashtable.Kind.HAVE; + if (!read || !StmtTranslation.readsOnly(args)) { + return LuaAst.LuaExprFunctionCall(tr.hashtableHelper(call.getFunc()), tr.translateExprList(args)); + } + List operands = translate(args, tr); + LuaExpr child = LuaAst.LuaExprBinary(entry(operands.get(0), operands.get(1), op.values()), LuaAst.LuaOpOr(), + LuaAst.LuaExprVarAccess(tr.hashtableEmpty())); + LuaExpr value = LuaAst.LuaExprArrayAccess(child, LuaAst.LuaExprlist(operands.get(2))); + if (op.kind() == LuaHashtable.Kind.HAVE) { + return LuaAst.LuaExprBinary(value, LuaAst.LuaOpUnequals(), LuaAst.LuaExprNull()); + } + return switch (op.values()) { + case INT -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpOr(), LuaAst.LuaExprIntVal("0")); + case REAL -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpOr(), LuaAst.LuaExprRealVal("0.0")); + case BOOL -> LuaAst.LuaExprBinary(value, LuaAst.LuaOpEquals(), LuaAst.LuaExprBoolVal(true)); + case STR, HANDLE -> value; + }; + } + + /** + * A store, a remove or a flush in place as statements, when no operand has an effect. Returns false for + * anything else, which is then translated as an expression. + */ + static boolean statement(ImFunctionCall call, LuaHashtable.Op op, List res, LuaTranslator tr) { + LuaHashtable.Kind kind = op.kind(); + if (kind == LuaHashtable.Kind.INIT || kind == LuaHashtable.Kind.LOAD || kind == LuaHashtable.Kind.HAVE) { + return false; + } + ImExprs args = checkArity(call, op); + if (!StmtTranslation.readsOnly(args)) { + return false; + } + List operands = translate(args, tr); + LuaExpr h = operands.get(0); + switch (kind) { + case SAVE -> { + LuaExpr p = operands.get(1); + LuaExpr created = LuaAst.LuaExprFunctionCall(tr.hashtableNewChild(), + LuaAst.LuaExprlist(subtable(h.copy(), op.values()), p.copy())); + LuaExpr child = LuaAst.LuaExprBinary(entry(h, p, op.values()), LuaAst.LuaOpOr(), created); + res.add(LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess(child, LuaAst.LuaExprlist(operands.get(2))), + operands.get(3))); + } + case REMOVE -> { + LuaExpr p = operands.get(1); + LuaAssignment remove = LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess( + entry(h.copy(), p.copy(), op.values()), LuaAst.LuaExprlist(operands.get(2))), LuaAst.LuaExprNull()); + res.add(LuaAst.LuaIf(entry(h, p, op.values()), LuaAst.LuaStatements(remove), LuaAst.LuaStatements())); + } + case FLUSH_CHILD -> { + for (LuaHashtable.Values values : LuaHashtable.Values.values()) { + res.add(LuaAst.LuaAssignment(entry(h.copy(), operands.get(1).copy(), values), LuaAst.LuaExprNull())); + } + } + case FLUSH_PARENT -> { + for (LuaHashtable.Values values : LuaHashtable.Values.values()) { + res.add(LuaAst.LuaAssignment(subtable(h.copy(), values), emptyTable())); + } + } + default -> throw new IllegalStateException("not a statement: " + op); + } + return true; + } + + /** {@code { __wurst_ht_int = {}, ... }}: the representation InitHashtable creates. */ + private static LuaExpr newHashtable() { + LuaTableFields fields = LuaAst.LuaTableFields(); + for (LuaHashtable.Values values : LuaHashtable.Values.values()) { + fields.add(LuaAst.LuaTableNamedField(values.field, emptyTable())); + } + return LuaAst.LuaTableConstructor(fields); + } + + /** The shared child of every absent parent key, which nothing writes. */ + static LuaVariable createEmpty(LuaTranslator tr) { + LuaVariable empty = LuaAst.LuaVariable(EMPTY, emptyTable()); + tr.declareChunkLocal(empty); + return empty; + } + + /** {@code (t, p)}: stores a new child under the parent key and answers it. */ + static LuaFunction createNewChild(LuaTranslator tr) { + LuaVariable table = LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()); + LuaVariable parentKey = LuaAst.LuaVariable("p", LuaAst.LuaNoExpr()); + LuaVariable child = LuaAst.LuaVariable("x", emptyTable()); + LuaFunction newChild = LuaAst.LuaFunction(NEW_CHILD, LuaAst.LuaParams(table, parentKey), + LuaAst.LuaStatements( + child, + LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(table), + LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(parentKey))), LuaAst.LuaExprVarAccess(child)), + LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(child)))); + tr.declareChunkLocal(newChild); + return newChild; + } + + /** {@code h.T}. */ + private static LuaExpr subtable(LuaExpr h, LuaHashtable.Values values) { + return LuaAst.LuaExprFieldAccess(h, values.field); + } + + /** {@code h.T[p]}: the child table under a parent key, nil when there is none. */ + private static LuaExpr entry(LuaExpr h, LuaExpr p, LuaHashtable.Values values) { + return LuaAst.LuaExprArrayAccess(subtable(h, values), LuaAst.LuaExprlist(p)); + } + + private static LuaExpr emptyTable() { + return LuaAst.LuaTableConstructor(LuaAst.LuaTableFields()); + } + + private static List translate(ImExprs args, LuaTranslator tr) { + List operands = new ArrayList<>(); + for (ImExpr arg : args) { + operands.add(arg.translateToLua(tr)); + } + return operands; + } + + private static ImExprs checkArity(ImFunctionCall call, LuaHashtable.Op op) { + ImExprs args = call.getArguments(); + if (args.size() != op.kind().arity) { + throw new CompileError(call.attrTrace().attrSource(), "Lua backend: " + call.getFunc().getName() + + " expects " + op.kind().arity + " arguments, got " + args.size() + "."); + } + return args; + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java index b5c9f046d..7a793649e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java @@ -363,152 +363,49 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("for k in pairs(t) do t[k] = nil end")); }); + // The hashtable natives; LuaHashtable has the representation. The backend prints a call of a __wurst_ stub + // where it is (LuaHashtableTranslation), so these bodies run only where it cannot, and for a hashtable native + // declared without the BJ flag. InitHashtable creates every subtable and nothing removes one. addNative(Arrays.asList("InitHashtable", "__wurst_InitHashtable"), f -> f.getBody().add(LuaAst.LuaLiteral("return { __wurst_ht_int = {}, __wurst_ht_bool = {}, __wurst_ht_real = {}, __wurst_ht_str = {}, __wurst_ht_handle = {} }"))); - addNative(Arrays.asList("SaveInteger", "__wurst_SaveInteger"), f -> { + addHashtableSave(withWurstPrefix("SaveInteger"), "__wurst_ht_int"); + addHashtableSave(withWurstPrefix("SaveBoolean"), "__wurst_ht_bool"); + addHashtableSave(withWurstPrefix("SaveReal"), "__wurst_ht_real"); + addHashtableSave(withWurstPrefix("SaveStr"), "__wurst_ht_str"); + addHashtableSave(withWurstPrefix(HASHTABLE_HANDLE_SAVE_NAMES), "__wurst_ht_handle"); + + addHashtableLoad(withWurstPrefix("LoadInteger"), "__wurst_ht_int", "0", "x[c] or 0"); + // true only for a stored true, as LoadBoolean answers false when nothing is stored + addHashtableLoad(withWurstPrefix("LoadBoolean"), "__wurst_ht_bool", "false", "x[c] == true"); + addHashtableLoad(withWurstPrefix("LoadReal"), "__wurst_ht_real", "0.0", "x[c] or 0.0"); + // nil when absent, as the string and handle loads have always answered + addHashtableLoad(withWurstPrefix("LoadStr"), "__wurst_ht_str", "nil", "x[c]"); + addHashtableLoad(withWurstPrefix(HASHTABLE_HANDLE_LOAD_NAMES), "__wurst_ht_handle", "nil", "x[c]"); + + addHashtableEntryOperation(withWurstPrefix("HaveSavedInteger"), "__wurst_ht_int", "return x ~= nil and x[c] ~= nil"); + addHashtableEntryOperation(withWurstPrefix("HaveSavedBoolean"), "__wurst_ht_bool", "return x ~= nil and x[c] ~= nil"); + addHashtableEntryOperation(withWurstPrefix("HaveSavedReal"), "__wurst_ht_real", "return x ~= nil and x[c] ~= nil"); + addHashtableEntryOperation(withWurstPrefix("HaveSavedString"), "__wurst_ht_str", "return x ~= nil and x[c] ~= nil"); + addHashtableEntryOperation(withWurstPrefix("HaveSavedHandle"), "__wurst_ht_handle", "return x ~= nil and x[c] ~= nil"); + + addHashtableEntryOperation(withWurstPrefix("RemoveSavedInteger"), "__wurst_ht_int", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedBoolean"), "__wurst_ht_bool", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedReal"), "__wurst_ht_real", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedString"), "__wurst_ht_str", "if x then x[c] = nil end"); + addHashtableEntryOperation(withWurstPrefix("RemoveSavedHandle"), "__wurst_ht_handle", "if x then x[c] = nil end"); + + addNative(withWurstPrefix("FlushChildHashtable"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_int = t end")); - f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); - }); - addNative(Arrays.asList("SaveBoolean", "__wurst_SaveBoolean"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_bool = t end")); - f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); - }); - addNative(Arrays.asList("SaveReal", "__wurst_SaveReal"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_real = t end")); - f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); - }); - addNative(Arrays.asList("SaveStr", "__wurst_SaveStr"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_str = t end")); - f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); - }); - addNative(withWurstPrefix(HASHTABLE_HANDLE_SAVE_NAMES), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_handle = t end")); - f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); - }); - - addNative(Arrays.asList("LoadInteger", "__wurst_LoadInteger"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return 0 end")); - f.getBody().add(LuaAst.LuaLiteral("local v = t[p][c]")); - f.getBody().add(LuaAst.LuaLiteral("if v == nil then return 0 end")); - f.getBody().add(LuaAst.LuaLiteral("return v")); - }); - - addNative(Arrays.asList("LoadBoolean", "__wurst_LoadBoolean"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return false end")); - f.getBody().add(LuaAst.LuaLiteral("local v = t[p][c]")); - f.getBody().add(LuaAst.LuaLiteral("if v == nil then return false end")); - f.getBody().add(LuaAst.LuaLiteral("return v")); - }); - - addNative(Arrays.asList("LoadReal", "__wurst_LoadReal"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return 0.0 end")); - f.getBody().add(LuaAst.LuaLiteral("local v = t[p][c]")); - f.getBody().add(LuaAst.LuaLiteral("if v == nil then return 0.0 end")); - f.getBody().add(LuaAst.LuaLiteral("return v")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_int[p] = nil")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_bool[p] = nil")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_real[p] = nil")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_str[p] = nil")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_handle[p] = nil")); }); - addNative(Arrays.asList("LoadStr", "__wurst_LoadStr"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return nil end")); - f.getBody().add(LuaAst.LuaLiteral("return t[p][c]")); - }); - addNative(withWurstPrefix(HASHTABLE_HANDLE_LOAD_NAMES), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); - f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return nil end")); - f.getBody().add(LuaAst.LuaLiteral("return t[p][c]")); - }); - - addNative(Arrays.asList("HaveSavedInteger", "__wurst_HaveSavedInteger"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); - f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); - }); - addNative(Arrays.asList("HaveSavedBoolean", "__wurst_HaveSavedBoolean"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); - f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); - }); - addNative(Arrays.asList("HaveSavedReal", "__wurst_HaveSavedReal"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); - f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); - }); - addNative(Arrays.asList("HaveSavedString", "__wurst_HaveSavedString"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); - f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); - }); - addNative(Arrays.asList("HaveSavedHandle", "__wurst_HaveSavedHandle"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); - f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); - }); - - addNative(Arrays.asList("FlushChildHashtable", "__wurst_FlushChildHashtable"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_int then h.__wurst_ht_int[p] = nil end")); - f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_bool then h.__wurst_ht_bool[p] = nil end")); - f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_real then h.__wurst_ht_real[p] = nil end")); - f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_str then h.__wurst_ht_str[p] = nil end")); - f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_handle then h.__wurst_ht_handle[p] = nil end")); - }); - - addNative(Arrays.asList("FlushParentHashtable", "__wurst_FlushParentHashtable"), f -> { + addNative(withWurstPrefix("FlushParentHashtable"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_int = {}")); f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_bool = {}")); @@ -517,42 +414,6 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_handle = {}")); }); - addNative(Arrays.asList("RemoveSavedInteger", "__wurst_RemoveSavedInteger"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); - f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); - }); - addNative(Arrays.asList("RemoveSavedBoolean", "__wurst_RemoveSavedBoolean"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); - f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); - }); - addNative(Arrays.asList("RemoveSavedReal", "__wurst_RemoveSavedReal"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); - f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); - }); - addNative(Arrays.asList("RemoveSavedString", "__wurst_RemoveSavedString"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); - f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); - }); - addNative(Arrays.asList("RemoveSavedHandle", "__wurst_RemoveSavedHandle"), f -> { - f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); - f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); - }); - addNative("__wurst_GetHandleId", f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getBody().add(LuaAst.LuaLiteral("return __wurst_objectToIndex(h)")); @@ -592,7 +453,40 @@ private static void addNative(Iterable names, Consumer g) { } } - private static Iterable withWurstPrefix(String[] names) { + /** Stores {@code i} under the child key, creating the child table under the parent key first if needed. */ + private static void addHashtableSave(Iterable names, String subtable) { + addNative(names, f -> { + addParams(f, "h", "p", "c", "i"); + f.getBody().add(LuaAst.LuaLiteral("local t = h." + subtable)); + f.getBody().add(LuaAst.LuaLiteral("local x = t[p]")); + f.getBody().add(LuaAst.LuaLiteral("if x == nil then x = {} t[p] = x end")); + f.getBody().add(LuaAst.LuaLiteral("x[c] = i")); + }); + } + + /** {@code absent} is the answer when there is no child table, {@code value} the answer read from it. */ + private static void addHashtableLoad(Iterable names, String subtable, String absent, String value) { + addHashtableEntryOperation(names, subtable, "if x == nil then return " + absent + " end", "return " + value); + } + + /** Reads the child table {@code x} under the parent key, then runs {@code body} on it. */ + private static void addHashtableEntryOperation(Iterable names, String subtable, String... body) { + addNative(names, f -> { + addParams(f, "h", "p", "c"); + f.getBody().add(LuaAst.LuaLiteral("local x = h." + subtable + "[p]")); + for (String line : body) { + f.getBody().add(LuaAst.LuaLiteral(line)); + } + }); + } + + private static void addParams(LuaFunction f, String... names) { + for (String name : names) { + f.getParams().add(LuaAst.LuaVariable(name, LuaAst.LuaNoExpr())); + } + } + + private static Iterable withWurstPrefix(String... names) { java.util.List result = new java.util.ArrayList<>(); for (String name : names) { result.add(name); 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 af826f654..e74a7208a 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 @@ -9,6 +9,7 @@ import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.translation.imtranslation.LuaFieldDefaults; +import de.peeeq.wurstscript.translation.imtranslation.LuaHashtable; import de.peeeq.wurstscript.translation.imtranslation.GenericTypes; import de.peeeq.wurstscript.translation.imtranslation.LuaDispatchPreparation; import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; @@ -346,6 +347,34 @@ LuaPolyfillSetup.OldGenericsHelpers oldGenericsHelpers() { return oldGenericsHelpers.get(); } + /** The hashtable stubs whose helper is defined. Membership only, never iterated. */ + private final Set definedHashtableHelpers = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Lazy hashtableEmpty = Lazy.create(() -> LuaHashtableTranslation.createEmpty(this)); + private final Lazy hashtableNewChild = Lazy.create(() -> LuaHashtableTranslation.createNewChild(this)); + + /** + * The helper a call of a hashtable stub calls where the operation cannot be printed in place + * ({@link LuaHashtableTranslation}), defined with the first such call: a stub which every call prints + * in place has no definition. + */ + LuaFunction hashtableHelper(ImFunction stub) { + LuaFunction helper = luaFunc.getFor(stub); + if (definedHashtableHelpers.add(stub)) { + LuaNatives.get(helper); + luaModel.add(helper); + } + return helper; + } + + /** Main-chunk locals of the in-place hashtable operations, declared on first use. */ + LuaVariable hashtableEmpty() { + return hashtableEmpty.get(); + } + + LuaFunction hashtableNewChild() { + return hashtableNewChild.get(); + } + LuaFunction fromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); LuaFunction stringToIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_stringToIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); LuaFunction stringFromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_stringFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); @@ -889,6 +918,10 @@ private void translateFunc(ImFunction f) { if (f.isNative() && ExprTranslation.isBackendIntrinsic(f, this)) { return; } + if (LuaHashtable.op(imTr, f) != null) { + // Printed where it is called; a call which cannot be printed so defines it (hashtableHelper). + return; + } LuaFunction lf = luaFunc.getFor(f); if (f.isNative()) { LuaNatives.get(lf); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index 521825065..0f791aa57 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -3,6 +3,7 @@ import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.luaAst.*; +import de.peeeq.wurstscript.translation.imtranslation.LuaHashtable; import de.peeeq.wurstscript.translation.imtranslation.LuaKeyedMap; import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; import de.peeeq.wurstscript.translation.imtranslation.LuaTraps; @@ -29,13 +30,20 @@ public static void translate(ImExpr e, List res, LuaTranslator tr) translateKeyedMapWrite(call, keyedWrite, res, tr); return; } + LuaHashtable.Op hashtableOp = LuaHashtable.op(tr.imTr, call.getFunc()); + if (hashtableOp != null && LuaHashtableTranslation.statement(call, hashtableOp, res, tr)) { + return; + } } LuaExpr expr = e.translateToLua(tr); res.add(expr); } - /** How an operand of a keyed-map store may be moved: see {@link #translateKeyedMapWrite}. */ - private enum WriteOperand { + /** + * How an operand of a store printed in place of its stub may be moved: see {@link #translateKeyedMapWrite} + * and {@link LuaHashtableTranslation}. + */ + enum WriteOperand { /** A literal or a local: the same value wherever it is read, and reading it does nothing. */ STABLE, /** A global or a table read: reading it does nothing, but an effect before it can change it. */ @@ -44,7 +52,7 @@ private enum WriteOperand { EFFECT } - private static WriteOperand writeOperand(ImExpr e) { + static WriteOperand writeOperand(ImExpr e) { if (e instanceof ImIntVal || e instanceof ImRealVal || e instanceof ImStringVal || e instanceof ImBoolVal || e instanceof ImNull) { return WriteOperand.STABLE; @@ -67,7 +75,8 @@ && readsOnly(access.getIndexes())) { return WriteOperand.EFFECT; } - private static boolean readsOnly(ImExprs exprs) { + /** Whether no operand is an {@link WriteOperand#EFFECT}: each one may be read again, or later, or not at all. */ + static boolean readsOnly(ImExprs exprs) { for (ImExpr e : exprs) { if (writeOperand(e) == WriteOperand.EFFECT) { return false; diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java index d7782be7d..5110c3938 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java @@ -947,4 +947,41 @@ public void functionsAskedForAcrossAnImportCycleEmitTheSameLuaInAnyUnitOrder() t "a class's functions must be emitted in an order independent of the compilation units"); } + /** + * The Lua backend declares the locals of the hashtable operations it prints in place, and defines the helper of + * one it cannot, where it first needs them. Here the packages which first need each do not import each other. + */ + @Test + public void hashtableHelpersNeededByUnrelatedPackagesEmitTheSameLuaInAnyUnitOrder() throws IOException { + CU first = compilationUnit("HtA.wurst", + "package HtA", + "@noinline function keyA(int k) returns int", + " return k", + "public function useA(hashtable h) returns int", + " SaveInteger(h, 1, 1, 7)", + " return LoadInteger(h, keyA(1), 1)"); + CU second = compilationUnit("HtB.wurst", + "package HtB", + "@noinline function keyB(int k) returns int", + " return k", + "public function useB(hashtable h) returns int", + " SaveReal(h, keyB(2), 1, 8.)", + " return R2I(LoadReal(h, 2, 1))"); + CU main = compilationUnit("Main.wurst", + "package Main", + "import HtA", + "import HtB", + "init", + " let h = InitHashtable()", + " if useA(h) + useB(h) == 15", + " testSuccess()"); + String name = "hashtableHelpersNeededByUnrelatedPackages"; + testNamed(name).testLua(true).withStdLib().executeProg().compilationUnits(first, second, main); + File output = new File("test-output/lua/DeterministicChecks_" + name + ".lua"); + String forward = Files.toString(output, Charsets.UTF_8); + testNamed(name).testLua(true).withStdLib().executeProg().compilationUnits(second, first, main); + assertEquals(Files.toString(output, Charsets.UTF_8), forward, + "the hashtable locals and helpers must be emitted in an order independent of the compilation units"); + } + } 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 a57f6ae52..3d28b23b1 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 @@ -1101,8 +1101,9 @@ private void checkOldGenericHashMapKeys(boolean inline, boolean localOptimizatio assertTrue("unit keys must still enter the object registry:\n" + unitProbe, unitProbe.contains("__wurst_objectToIndex")); if (inline) { + // the helper's call, or the read it stands for when every operand may be read in place assertTrue("the regression must exercise an inlined hashtable probe:\n" + intProbe, - intProbe.contains("__wurst_HaveSavedInteger")); + intProbe.contains("__wurst_HaveSavedInteger") || intProbe.contains(".__wurst_ht_int[")); assertTrue("an inlined integer key must retain the zero sentinel:\n" + intProbe, intProbe.contains("== 0") && intProbe.contains("__wurst_oldGenericsZero")); assertTrue("an inlined handle index must retain the zero sentinel:\n" + unitProbe, diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java new file mode 100644 index 000000000..4e6dd2307 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java @@ -0,0 +1,298 @@ +package tests.wurstscript.tests; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import de.peeeq.wurstscript.luaAst.LuaAst; +import de.peeeq.wurstscript.luaAst.LuaStatement; +import de.peeeq.wurstscript.luaAst.LuaVariable; +import de.peeeq.wurstscript.translation.lua.translation.LuaTranslator; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; +import static org.testng.AssertJUnit.fail; + +/** + * The Jass hashtable natives on Lua: each operation is printed where it is called, as the table + * accesses its helper consists of, and calls the helper only where an operand has an effect. + */ +public class LuaHashtableTests extends WurstScriptTest { + + private String compiled(String testName) throws IOException { + return Files.toString(new File("test-output/lua/LuaHashtableTests_" + testName + ".lua"), Charsets.UTF_8); + } + + private String functionBody(String output, String functionName) { + // Up to the closing 'end' at column 0; nested blocks are indented. + Matcher matcher = Pattern.compile("function\\s*" + functionName + "\\s*\\([^\\n]*\\n(.*?)\\nend", + Pattern.DOTALL).matcher(output); + if (!matcher.find()) { + fail("Function " + functionName + " was not found."); + } + return matcher.group(1); + } + + private static void assertMatches(String what, String regex, String text) { + assertTrue(what + ":\n" + text, Pattern.compile(regex).matcher(text).find()); + } + + /** Every operation family, in a program which runs; the shape of each one is asserted on the output. */ + @Test + public void hashtableOperationsArePrintedWhereTheyAreCalled() throws IOException { + test().testLua(true).withStdLib().inline().localOptimizations().executeProg().lines( + "package Test", + "init", + " let h = InitHashtable()", + " SaveInteger(h, 1, 2, 3)", + " SaveTimerHandle(h, 1, 2, null)", + " if LoadInteger(h, 1, 2) == 3 and HaveSavedInteger(h, 1, 2) and LoadTimerHandle(h, 1, 2) == null" + + " and \"x\" + LoadStr(h, 1, 2) == \"x\"", + " RemoveSavedInteger(h, 1, 2)", + " FlushChildHashtable(h, 1)", + " FlushParentHashtable(h)", + " testSuccess()"); + + String compiled = compiled("hashtableOperationsArePrintedWhereTheyAreCalled"); + LuaTranslator.assertNoLeakedHashtableNativeCalls(compiled); + assertTrue("the shared empty child is a main-chunk local:\n" + compiled, + compiled.contains("local __wurst_htEmpty = ({})\n")); + assertTrue("the child constructor is a main-chunk local:\n" + compiled, + compiled.contains("local function __wurst_htNewChild(t, p) \n")); + + String init = functionBody(compiled, "init_Test"); + assertFalse("no hashtable helper is called:\n" + init, + Pattern.compile("__wurst_(InitHashtable|Save|Load|HaveSaved|RemoveSaved|Flush)\\w*\\(").matcher(init).find()); + // The library calls some with an operand which has an effect (String hashes its characters), but not these. + for (String helper : new String[]{"InitHashtable", "LoadInteger", "HaveSavedInteger", "LoadStr", "LoadTimerHandle", + "SaveTimerHandle", "RemoveSavedInteger", "FlushChildHashtable", "FlushParentHashtable"}) { + assertFalse("a helper which nothing calls is not defined: " + helper, + compiled.contains("function __wurst_" + helper + "(")); + } + assertMatches("InitHashtable is the table constructor", + "= \\(\\{__wurst_ht_int=\\(\\{\\}\\), __wurst_ht_bool=\\(\\{\\}\\), __wurst_ht_real=\\(\\{\\}\\), " + + "__wurst_ht_str=\\(\\{\\}\\), __wurst_ht_handle=\\(\\{\\}\\), \\}\\)", init); + assertMatches("a save creates the child only when there is none", + "\n\\s*;\\((\\w+)\\.__wurst_ht_int\\[1\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_int, 1\\)\\)\\[2\\] = 3\n", + init); + assertMatches("a handle save goes to the handle subtable", + ";\\((\\w+)\\.__wurst_ht_handle\\[1\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_handle, 1\\)\\)\\[2\\] = nil", + init); + assertMatches("an int load reads through the empty child and answers 0", + "\\(\\(\\w+\\.__wurst_ht_int\\[1\\] or __wurst_htEmpty\\)\\[2\\] or 0\\)", init); + assertMatches("a test is the same read", + "\\(\\(\\w+\\.__wurst_ht_int\\[1\\] or __wurst_htEmpty\\)\\[2\\] ~= nil\\)", init); + assertMatches("a handle load answers nil when absent, as the helper did", + "\\(\\(\\w+\\.__wurst_ht_handle\\[1\\] or __wurst_htEmpty\\)\\[2\\] == nil\\)", init); + assertMatches("so does a string load, which the concatenation guards", + "\\(\\w+\\.__wurst_ht_str\\[1\\] or __wurst_htEmpty\\)\\[2\\] or \"\"\\)", init); + assertMatches("a remove never writes the shared empty child", + "if (\\w+)\\.__wurst_ht_int\\[1\\] then\\s+\\1\\.__wurst_ht_int\\[1\\]\\[2\\] = nil\\s+end", init); + for (String subtable : new String[]{"int", "bool", "real", "str", "handle"}) { + assertMatches("FlushChildHashtable drops the child of every kind", + "\\w+\\.__wurst_ht_" + subtable + "\\[1\\] = nil", init); + assertMatches("FlushParentHashtable replaces every subtable", + "\\w+\\.__wurst_ht_" + subtable + " = \\(\\{\\}\\)", init); + } + assertFalse("a flush tests no subtable, every one is always there:\n" + init, + Pattern.compile("if \\w+\\.__wurst_ht_\\w+ then").matcher(init).find()); + } + + private static final String[] BEHAVIOUR = { + "package Test", + "let ct = compiletime(InitHashtable())", + "@compiletime function fill()", + " SaveInteger(ct, 1, 2, 12)", + " SaveStr(ct, 1, 3, \"ct\")", + " SaveBoolean(ct, 4, 5, true)", + " SaveReal(ct, 6, 7, 1.5)", + "function check(boolean ok, string what)", + " if not ok", + " testFail(what)", + "init", + " let h = InitHashtable()", + " check(LoadInteger(h, 1, 1) == 0 and LoadReal(h, 1, 1) == 0. and not LoadBoolean(h, 1, 1)" + + " and \"x\" + LoadStr(h, 1, 1) == \"x\" and LoadTimerHandle(h, 1, 1) == null, \"an absent parent key reads defaults\")", + " check(not HaveSavedInteger(h, 1, 1) and not HaveSavedReal(h, 1, 1) and not HaveSavedBoolean(h, 1, 1)" + + " and not HaveSavedString(h, 1, 1) and not HaveSavedHandle(h, 1, 1), \"an absent parent key holds nothing\")", + " SaveInteger(h, 1, 1, 11)", + " check(LoadInteger(h, 1, 1) == 11 and HaveSavedInteger(h, 1, 1), \"int\")", + " check(LoadInteger(h, 1, 2) == 0 and not HaveSavedInteger(h, 1, 2), \"an absent child key reads 0\")", + " check(LoadReal(h, 1, 1) == 0. and not HaveSavedReal(h, 1, 1), \"each kind has its own entries\")", + " SaveBoolean(h, 1, 1, false)", + " check(HaveSavedBoolean(h, 1, 1) and not LoadBoolean(h, 1, 1), \"a stored false\")", + " SaveBoolean(h, 1, 2, true)", + " check(LoadBoolean(h, 1, 2), \"a stored true\")", + " SaveReal(h, 1, 1, 2.5)", + " check(LoadReal(h, 1, 1) == 2.5 and HaveSavedReal(h, 1, 1), \"real\")", + " SaveStr(h, 1, 1, \"s\")", + " check(LoadStr(h, 1, 1) == \"s\" and HaveSavedString(h, 1, 1), \"string\")", + " let t = CreateTimer()", + " SaveTimerHandle(h, 1, 1, t)", + " check(LoadTimerHandle(h, 1, 1) == t and HaveSavedHandle(h, 1, 1), \"handle\")", + " RemoveSavedInteger(h, 1, 1)", + " check(not HaveSavedInteger(h, 1, 1) and LoadInteger(h, 1, 1) == 0 and LoadReal(h, 1, 1) == 2.5," + + " \"a remove takes only its kind\")", + " RemoveSavedInteger(h, 9, 1)", + " check(LoadInteger(h, 9, 1) == 0 and not HaveSavedInteger(h, 9, 1), \"a remove under an absent parent key\")", + " RemoveSavedBoolean(h, 1, 1)", + " RemoveSavedReal(h, 1, 1)", + " RemoveSavedString(h, 1, 1)", + " RemoveSavedHandle(h, 1, 1)", + " check(not HaveSavedBoolean(h, 1, 1) and not HaveSavedReal(h, 1, 1) and not HaveSavedString(h, 1, 1)" + + " and not HaveSavedHandle(h, 1, 1) and LoadBoolean(h, 1, 2), \"removes of every kind\")", + " SaveInteger(h, 2, 1, 21)", + " SaveStr(h, 2, 2, \"t\")", + " FlushChildHashtable(h, 2)", + " check(not HaveSavedInteger(h, 2, 1) and not HaveSavedString(h, 2, 2) and LoadBoolean(h, 1, 2)," + + " \"a flushed child is gone, the others stay\")", + " SaveInteger(h, 2, 1, 22)", + " check(LoadInteger(h, 2, 1) == 22, \"a flushed child takes a save\")", + " FlushParentHashtable(h)", + " check(not HaveSavedBoolean(h, 1, 2) and not HaveSavedInteger(h, 2, 1), \"a flushed hashtable is empty\")", + " SaveInteger(h, 1, 1, 31)", + " check(LoadInteger(h, 1, 1) == 31, \"a flushed hashtable takes a save\")", + " check(LoadInteger(ct, 1, 2) == 12 and LoadStr(ct, 1, 3) == \"ct\" and LoadBoolean(ct, 4, 5)" + + " and LoadReal(ct, 6, 7) == 1.5 and not HaveSavedInteger(ct, 1, 3), \"a compile-time hashtable\")", + " testSuccess()" + }; + + /** The same answers on Jass, in the interpreter and on Lua, where the operations are printed in place. */ + @Test + public void hashtableOperationsBehaveAsTheNatives() { + test().testLua(true).luaOnly(false).withStdLib().executeProg().executeProgOnlyAfterTransforms().lines(BEHAVIOUR); + } + + /** As shipped: inlined, so the operands are the caller's literals and locals, and optimised. */ + @Test + public void hashtableOperationsBehaveAsTheNativesWhenOptimized() { + test().testLua(true).withStdLib().inline().localOptimizations().executeProg().lines(BEHAVIOUR); + } + + /** + * An operand with an effect has to be evaluated once and in the order of the call, which the operation + * printed in place does not do (a save reads its parent key again to create the child, a remove skips its + * child key under an absent parent key). Such a call stays the call of its helper, which is then defined; + * one with plain operands in the same program is still printed in place. + */ + @Test + public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException { + test().testLua(true).luaOnly(false).withStdLib().inline().localOptimizations().executeProg().lines( + "package Test", + "string trace = \"\"", + "@noinline function key(string tag, int k) returns int", + " trace += tag", + " return k", + "init", + " let h = InitHashtable()", + " SaveInteger(h, key(\"p\", 1), key(\"c\", 2), key(\"v\", 3))", + " SaveInteger(h, 1, 4, key(\"w\", 5))", + " SaveInteger(h, 1, 6, 7)", + " if LoadInteger(h, key(\"P\", 1), key(\"C\", 2)) != 3 or not HaveSavedInteger(h, 1, key(\"H\", 4))", + " testFail(\"stored through the helper\")", + " RemoveSavedInteger(h, key(\"r\", 9), key(\"s\", 2))", + " RemoveSavedInteger(h, key(\"R\", 1), key(\"S\", 2))", + " FlushChildHashtable(h, key(\"f\", 1))", + " if trace != \"pcvwPCHrsRSf\"", + " testFail(\"operands evaluated as \" + trace)", + " if HaveSavedInteger(h, 1, 2) or HaveSavedInteger(h, 1, 4) or LoadInteger(h, 9, 2) != 0", + " testFail(\"removed and flushed\")", + " testSuccess()"); + + String compiled = compiled("anOperandWithAnEffectKeepsTheCallOfTheHelper"); + LuaTranslator.assertNoLeakedHashtableNativeCalls(compiled); + String init = functionBody(compiled, "init_Test"); + for (String helper : new String[]{"SaveInteger", "LoadInteger", "HaveSavedInteger", "RemoveSavedInteger", + "FlushChildHashtable"}) { + assertTrue("an operation with an effect in an operand calls its helper: " + helper + "\n" + init, + init.contains("__wurst_" + helper + "(")); + assertTrue("a helper which is called is defined: " + helper + "\n" + compiled, + compiled.contains("function __wurst_" + helper + "(")); + } + assertMatches("a save with plain operands is printed in place", + ";\\((\\w+)\\.__wurst_ht_int\\[1\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_int, 1\\)\\)\\[6\\] = 7", init); + assertMatches("a load with plain operands is printed in place", + "\\(\\w+\\.__wurst_ht_int\\[9\\] or __wurst_htEmpty\\)\\[2\\] or 0\\)", init); + assertFalse("a helper nothing calls is not defined:\n" + compiled, + compiled.contains("function __wurst_LoadReal(") || compiled.contains("function __wurst_InitHashtable(")); + assertFalse("the helper no longer tests for a missing subtable:\n" + compiled, + compiled.contains("if t == nil")); + } + + /** + * Lua reads a statement which starts with '(' as the arguments of a call ending the previous statement, so an + * assignment whose target starts with '(' is printed after a ';'. Here the save follows an assignment which ends + * in a parenthesised expression; the harness compiles the script with luac. + */ + @Test + public void aStoreStartingWithAParenthesisIsSeparatedFromTheStatementBefore() throws IOException { + test().testLua(true).withStdLib().executeProg().lines( + "package Test", + "init", + " let h = InitHashtable()", + " var x = 5", + " x = x + 1", + " SaveInteger(h, 1, 2, x)", + " if LoadInteger(h, 1, 2) == 6", + " testSuccess()"); + + String init = functionBody(compiled("aStoreStartingWithAParenthesisIsSeparatedFromTheStatementBefore"), + "init_Test"); + assertMatches("the save follows a statement ending in a parenthesis, after a ';'", + "= \\(\\w+ \\+ 1\\)\n\\s*;\\(", init); + } + + @Test + public void anAssignmentIsPrintedAfterASemicolonOnlyWhenItsTargetStartsWithAParenthesis() { + LuaVariable a = LuaAst.LuaVariable("a", LuaAst.LuaNoExpr()); + LuaVariable b = LuaAst.LuaVariable("b", LuaAst.LuaNoExpr()); + assertEquals(";(a or b)[1] = 2", print(LuaAst.LuaAssignment(LuaAst.LuaExprArrayAccess( + LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(a), LuaAst.LuaOpOr(), LuaAst.LuaExprVarAccess(b)), + LuaAst.LuaExprlist(LuaAst.LuaExprIntVal("1"))), LuaAst.LuaExprIntVal("2")))); + assertEquals(";(nil).f = 2", print(LuaAst.LuaAssignment( + LuaAst.LuaExprFieldAccess(LuaAst.LuaExprNull(), "f"), LuaAst.LuaExprIntVal("2")))); + assertEquals("a[1].f = 2", print(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess(LuaAst.LuaExprArrayAccess( + LuaAst.LuaExprVarAccess(a), LuaAst.LuaExprlist(LuaAst.LuaExprIntVal("1"))), "f"), LuaAst.LuaExprIntVal("2")))); + assertEquals("InitHashtable = 2", print(LuaAst.LuaAssignment( + LuaAst.LuaLiteral("InitHashtable"), LuaAst.LuaExprIntVal("2")))); + } + + private static String print(LuaStatement statement) { + StringBuilder sb = new StringBuilder(); + statement.print(sb, 0); + return sb.toString(); + } + + /** + * A load and a HaveSaved test only read, as the Jass natives they stand for do, so the optimizer drops one whose + * result nothing uses. Before, it could not tell: the stub has another name than the native. + */ + @Test + public void anUnusedLoadIsDropped() throws IOException { + test().testLua(true).withStdLib().inline().localOptimizations().executeProg().lines( + "package Test", + "@noinline function discard(hashtable h, int p)", + " let ignoredInt = LoadInteger(h, p, 1)", + " let ignoredString = LoadStr(h, p, 2)", + " let ignoredPresence = HaveSavedInteger(h, p, 1)", + "init", + " let h = InitHashtable()", + " SaveInteger(h, 1, 1, 5)", + " discard(h, 1)", + " if LoadInteger(h, 1, 1) == 5", + " testSuccess()"); + + String compiled = compiled("anUnusedLoadIsDropped"); + int start = compiled.indexOf("function discard("); + assertTrue("expected function discard:\n" + compiled, start >= 0); + String discard = compiled.substring(start, compiled.indexOf("\nend", start)); + assertFalse("the unused reads are dropped: " + discard, + discard.contains("__wurst_ht") || discard.contains("wurstExpr") || discard.contains("__wurst_Load") + || discard.contains("__wurst_HaveSaved")); + } +} diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java index f1a0668de..d6eb032f0 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java @@ -199,7 +199,8 @@ public void keyedMapLowersToSingleLuaIndexes() throws IOException { assertFalse("no read stub is defined", compiled.contains("function __wurst_keyedMapGetInt") || compiled.contains("function __wurst_keyedMapHas")); assertFalse("the caller must not reach the hashtable natives: " + init, - init.contains("SaveInteger") || init.contains("LoadInteger") || init.contains("HaveSavedInteger")); + init.contains("SaveInteger") || init.contains("LoadInteger") || init.contains("HaveSavedInteger") + || init.contains("__wurst_ht_")); assertFalse("an int read needs no nil normalisation: " + init, init.contains("__wurst_ensureInt") || init.contains("__wurst_rawTo")); } @@ -245,7 +246,7 @@ public void keyedMapStaysNativeWithStackTraces() throws IOException { assertTrue("the caller must read and store into the table itself: " + init, init.contains("] or 0)") && init.contains("m[7] = 70")); assertFalse("the caller must not reach the hashtable natives: " + init, - init.contains("SaveInteger") || init.contains("LoadInteger")); + init.contains("SaveInteger") || init.contains("LoadInteger") || init.contains("__wurst_ht_")); } /** @@ -302,7 +303,7 @@ public void keyedMapDestroyClearsLuaStore() throws IOException { assertTrue("the clear stub must empty the existing table in place: " + destroy, destroy.contains("for k in pairs(t) do t[k] = nil end")); assertFalse("the Table machinery must not reach Lua: " + init, - init.contains("FlushChildHashtable") || init.contains("Table_destroy")); + init.contains("FlushChildHashtable") || init.contains("__wurst_ht_") || init.contains("Table_destroy")); } /** @@ -1060,7 +1061,8 @@ public void unitKeyedMapKeysTheUnitItselfOnLua() throws IOException { init.contains("if (u ~= nil) then\n\t\tm[u] = 5\n\tend") && init.contains("] or 0)") && init.contains("] ~= nil)") && init.contains("if (u ~= nil) then\n\t\tm[u] = nil\n\tend")); assertFalse("the unit must not go through a handle id or an index map: " + init, - init.contains("GetHandleId") || init.contains("__wurst_objectToIndex") || init.contains("SaveInteger")); + init.contains("GetHandleId") || init.contains("__wurst_objectToIndex") || init.contains("SaveInteger") + || init.contains("__wurst_ht_")); } /** diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java index 7c36f8198..3e99750cc 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java @@ -65,6 +65,7 @@ public void initHashtableCreatesPerTypeBuckets() { assertTrue(rendered.contains("__wurst_ht_handle")); } + /** The helpers a call with an effect in an operand still calls; InitHashtable creates every subtable. */ @Test public void hashtableSavesUseTypeSpecificBuckets() { String saveInt = renderNative("__wurst_SaveInteger"); @@ -73,6 +74,8 @@ public void hashtableSavesUseTypeSpecificBuckets() { assertTrue(saveInt.contains("h.__wurst_ht_int")); assertTrue(saveReal.contains("h.__wurst_ht_real")); assertTrue(saveHandle.contains("h.__wurst_ht_handle")); + assertTrue(saveInt, saveInt.contains("if x == nil then x = {} t[p] = x end")); + assertFalse("no subtable is ever missing: " + saveInt, saveInt.contains("t == nil")); } @Test @@ -80,8 +83,20 @@ public void hashtableLoadsUseTypeSpecificBuckets() { String loadInt = renderNative("__wurst_LoadInteger"); String loadStr = renderNative("__wurst_LoadStr"); String loadHandle = renderNative("__wurst_LoadAbilityHandle"); + String loadBool = renderNative("__wurst_LoadBoolean"); assertTrue(loadInt.contains("h.__wurst_ht_int")); assertTrue(loadStr.contains("h.__wurst_ht_str")); assertTrue(loadHandle.contains("h.__wurst_ht_handle")); + assertTrue(loadInt, loadInt.contains("return x[c] or 0")); + assertTrue("a missing string is nil: " + loadStr, loadStr.contains("return nil") && loadStr.contains("return x[c]\n")); + assertTrue(loadBool, loadBool.contains("return x[c] == true")); + assertFalse("no subtable is ever missing: " + loadInt, loadInt.contains("t == nil")); + } + + @Test + public void hashtableFlushesTestNoSubtable() { + String flushChild = renderNative("__wurst_FlushChildHashtable"); + assertTrue(flushChild, flushChild.contains("h.__wurst_ht_handle[p] = nil")); + assertFalse(flushChild, flushChild.contains("if ")); } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index af7e4e047..0a775a75e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -498,8 +498,12 @@ public void testStdLib() throws IOException { ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_testStdLib.lua"), Charsets.UTF_8); assertTrue(compiled.contains("MagicFunctions_compiletime")); - assertTrue(compiled.contains("function __wurst_InitHashtable(")); - assertTrue(compiled.contains("function __wurst_SaveInteger(")); + // The library's hashtables are created and filled where it uses them (LuaHashtableTranslation). + assertTrue(compiled.contains(" = ({__wurst_ht_int=({}), __wurst_ht_bool=({}), __wurst_ht_real=({}), " + + "__wurst_ht_str=({}), __wurst_ht_handle=({}), })")); + assertTrue(compiled.contains("local function __wurst_htNewChild(")); + assertFalse(compiled.contains("function __wurst_InitHashtable(")); + LuaAssertions.assertNoLeakedHashtableNativeCalls(compiled); } @Test @@ -2677,7 +2681,7 @@ public void keyedTableStaysNativeWithStackTraces() throws IOException { // caller: Table itself is compiled in and legitimately uses the hashtable natives. String init = getFunctionBody(compiled, "init_Test"); assertFalse("the caller must not reach the hashtable natives: " + init, - init.contains("SaveBoolean") || init.contains("LoadBoolean")); + init.contains("SaveBoolean") || init.contains("LoadBoolean") || init.contains("__wurst_ht_")); assertTrue("the caller must call the stubs directly: " + init, init.contains("__wurst_keyedTableContains")); } @@ -2721,7 +2725,7 @@ public void keyedTableDestroyCostsNothingOnLua() throws IOException { // The Jass body must not survive: it would destroy a Table that does not exist here. assertFalse("the Table machinery must not reach Lua: " + init, - init.contains("FlushChildHashtable") || init.contains("Table_destroy")); + init.contains("FlushChildHashtable") || init.contains("__wurst_ht_") || init.contains("Table_destroy")); } /** @@ -3637,9 +3641,11 @@ public void stdLibDoesNotEmitWar3HashtableNatives() throws IOException { assertDoesNotContainRegex(compiled, "\\bRemoveSavedHandle\\("); assertDoesNotContainRegex(compiled, "\\bSaveAbilityHandle\\("); assertDoesNotContainRegex(compiled, "\\bLoadAbilityHandle\\("); - // With IM-level remapping, helpers are only emitted when called. - // SaveInteger is used by stdlib init code, so its helper must be present. - assertContainsRegex(compiled, "\\bfunction\\s+__wurst_SaveInteger\\s*\\("); + // The stdlib init code saves into hashtables. A save is printed in place, through the child + // constructor, and the helper of one which cannot be is defined where it is called. + assertContainsRegex(compiled, "\\blocal function __wurst_htNewChild\\s*\\("); + assertContainsRegex(compiled, "\n\\s*;\\(\\w+\\.__wurst_ht_int\\["); + LuaAssertions.assertNoLeakedHashtableNativeCalls(compiled); } @Test @@ -3654,7 +3660,9 @@ public void hashtableHandleExtensionsUseWurstLuaHelpers() throws IOException { ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleExtensionsUseWurstLuaHelpers.lua"), Charsets.UTF_8); assertDoesNotContainRegex(compiled, "\\bHaveSavedHandle\\("); - assertContainsRegex(compiled, "\\b__wurst_HaveSavedHandle\\("); + // Printed as the read it stands for, so no helper is called. + assertContainsRegex(compiled, "\\(\\w+\\.__wurst_ht_handle\\[\\w+\\] or __wurst_htEmpty\\)\\[\\w+\\] ~= nil\\)"); + assertDoesNotContainRegex(compiled, "\\b__wurst_HaveSavedHandle\\("); } @Test @@ -3669,9 +3677,12 @@ public void hashtableNativesAreReplacedByWurstHelpers() throws IOException { // Runtime guard overrides are removed; compile-time checks are authoritative. assertFalse(compiled.contains("Wurst experimental Lua assertion guards")); assertFalse(compiled.contains("__wurst_guard_ok")); - // __wurst_ helper definitions must still be present. - assertContainsRegex(compiled, "\\bfunction\\s+__wurst_InitHashtable\\s*\\("); - assertContainsRegex(compiled, "\\bfunction\\s+__wurst_SaveInteger\\s*\\("); + // Printed where they are called; a helper which a call still needs must be defined. + assertContainsRegex(compiled, "= \\(\\{__wurst_ht_int=\\(\\{\\}\\), "); + assertContainsRegex(compiled, + ";\\((\\w+)\\.__wurst_ht_int\\[(\\w+)\\] or __wurst_htNewChild\\(\\1\\.__wurst_ht_int, \\2\\)\\)\\[\\w+\\] = \\w+"); + assertDoesNotContainRegex(compiled, "\\bfunction\\s+__wurst_InitHashtable\\s*\\("); + LuaAssertions.assertNoLeakedHashtableNativeCalls(compiled); } @Test @@ -3682,13 +3693,17 @@ public void hashtableHandleLoadSaveUseWurstLuaHelpers() throws IOException { "init", " let h = InitHashtable()", " h.saveAbilityHandle(1, 2, null)", - " let a = h.loadAbilityHandle(1, 2)" + // used, so the load is not dropped as a read whose result nothing uses + " if h.loadAbilityHandle(1, 2) == null", + " print(\"absent\")" ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleLoadSaveUseWurstLuaHelpers.lua"), Charsets.UTF_8); assertDoesNotContainRegex(compiled, "\\bSaveAbilityHandle\\("); assertDoesNotContainRegex(compiled, "\\bLoadAbilityHandle\\("); - assertContainsRegex(compiled, "\\b__wurst_SaveAbilityHandle\\("); - assertContainsRegex(compiled, "\\b__wurst_LoadAbilityHandle\\("); + // Printed in place on the handle subtable, so no helper is called. + assertContainsRegex(compiled, "__wurst_htNewChild\\(\\w+\\.__wurst_ht_handle, \\w+\\)"); + assertContainsRegex(compiled, "\\(\\w+\\.__wurst_ht_handle\\[\\w+\\] or __wurst_htEmpty\\)\\[\\w+\\]"); + assertDoesNotContainRegex(compiled, "\\b__wurst_(Save|Load)AbilityHandle\\("); } @Test @@ -3702,24 +3717,25 @@ public void hashtableHelpersEmitPerTypeBucketsInLua() throws IOException { " h.saveReal(1, 2, 3.5)", " h.saveString(1, 2, \"x\")", " h.saveAbilityHandle(1, 2, null)", - " let i = h.loadInt(1, 2)", - " let r = h.loadReal(1, 2)", - " let s = h.loadString(1, 2)", - " let a = h.loadAbilityHandle(1, 2)" + // used, so the loads are not dropped as reads whose result nothing uses + " if h.loadInt(1, 2) == 7 and h.loadReal(1, 2) == 3.5 and h.loadString(1, 2) == \"x\"" + + " and h.loadAbilityHandle(1, 2) == null", + " print(\"loaded\")" ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHelpersEmitPerTypeBucketsInLua.lua"), Charsets.UTF_8); - assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_int", true); - assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_real", true); - assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_str", true); - assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_handle", true); - assertFunctionBodyContains(compiled, "__wurst_SaveInteger", "h.__wurst_ht_int", true); - assertFunctionBodyContains(compiled, "__wurst_SaveReal", "h.__wurst_ht_real", true); - assertFunctionBodyContains(compiled, "__wurst_SaveStr", "h.__wurst_ht_str", true); - assertFunctionBodyContains(compiled, "__wurst_SaveAbilityHandle", "h.__wurst_ht_handle", true); - assertFunctionBodyContains(compiled, "__wurst_LoadInteger", "h.__wurst_ht_int", true); - assertFunctionBodyContains(compiled, "__wurst_LoadReal", "h.__wurst_ht_real", true); - assertFunctionBodyContains(compiled, "__wurst_LoadStr", "h.__wurst_ht_str", true); - assertFunctionBodyContains(compiled, "__wurst_LoadAbilityHandle", "h.__wurst_ht_handle", true); + // The operations are printed where they are called, the stdlib wrappers here. + String init = getFunctionBody(compiled, "init_Test"); + for (String subtable : new String[]{"int", "bool", "real", "str", "handle"}) { + assertTrue("InitHashtable creates every subtable: " + init, init.contains("__wurst_ht_" + subtable + "=({})")); + } + assertFunctionBodyContains(compiled, "hashtable_saveInt", ".__wurst_ht_int[", true); + assertFunctionBodyContains(compiled, "hashtable_saveReal", ".__wurst_ht_real[", true); + assertFunctionBodyContains(compiled, "hashtable_saveString", ".__wurst_ht_str[", true); + assertFunctionBodyContains(compiled, "hashtable_saveAbilityHandle", ".__wurst_ht_handle[", true); + assertFunctionBodyContains(compiled, "hashtable_loadInt", ".__wurst_ht_int[", true); + assertFunctionBodyContains(compiled, "hashtable_loadReal", ".__wurst_ht_real[", true); + assertFunctionBodyContains(compiled, "hashtable_loadString", ".__wurst_ht_str[", true); + assertFunctionBodyContains(compiled, "hashtable_loadAbilityHandle", ".__wurst_ht_handle[", true); } /** A code value takes no parameters, so its adapter forwards none: no varargs at all. */ From 264a3460d3c53dde0dcc97f48952fb302c15f129 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 10 Oct 2026 13:32:29 +0200 Subject: [PATCH 4/6] Lua: an instance id cast to int may be read in place Table keys its hashtable by this castTo int. Inlined, that cast is an operand of the hashtable native, and the store operand classification counted every cast as one which may raise, so every inlined Table load, test and remove kept the call of its helper. On Lua the cast is (x or 0), and a cast from one class to another is x itself: neither raises nor changes anything, so such a cast is classified as its operand is. --- .../lua/translation/StmtTranslation.java | 7 +++++ .../wurstscript/tests/LuaHashtableTests.java | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index 0f791aa57..6d0ea1b2b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -8,6 +8,7 @@ import de.peeeq.wurstscript.translation.imtranslation.LuaMultipleResults; import de.peeeq.wurstscript.translation.imtranslation.LuaTraps; import de.peeeq.wurstscript.translation.lua.printing.LuaPrinter; +import de.peeeq.wurstscript.types.TypesHelper; import java.util.ArrayList; import java.util.List; @@ -57,6 +58,12 @@ static WriteOperand writeOperand(ImExpr e) { || e instanceof ImBoolVal || e instanceof ImNull) { return WriteOperand.STABLE; } + if (e instanceof ImCast cast && cast.getFromType() instanceof ImClassType + && (TypesHelper.isIntType(cast.getToType()) || cast.getToType() instanceof ImClassType)) { + // An instance id as an int is (x or 0) and as another class x itself (ExprTranslation): neither + // raises nor changes anything. Table keys its hashtable by this castTo int. + return writeOperand(cast.getExpr()); + } if (LuaTraps.mayRaise(e)) { // It must be evaluated even when the key is nil, as the stub's argument was: an array field read // through a null object raises. diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java index 4e6dd2307..3cff2a309 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java @@ -224,6 +224,34 @@ public void anOperandWithAnEffectKeepsTheCallOfTheHelper() throws IOException { compiled.contains("if t == nil")); } + /** + * The library's Table keys its hashtable by the instance, {@code this castTo int}, which is {@code (t or 0)} on + * Lua: once inlined, that cast is an operand of the native, and it can neither raise nor change anything, so the + * operation is still printed in place. + */ + @Test + public void aTableOperationIsPrintedInPlace() throws IOException { + test().testLua(true).withStdLib().inline().localOptimizations().executeProg().lines( + "package Test", + "import Table", + "init", + " let t = new Table()", + " t.saveInt(5, 7)", + " if t.loadInt(5) == 7 and t.hasInt(5) and not t.hasInt(6)", + " t.removeInt(5)", + " if not t.hasInt(5)", + " testSuccess()"); + + String init = functionBody(compiled("aTableOperationIsPrintedInPlace"), "init_Test"); + assertFalse("no hashtable helper is called:\n" + init, + Pattern.compile("__wurst_(Save|Load|HaveSaved|RemoveSaved)\\w*\\(").matcher(init).find()); + assertMatches("the save is printed in place", + ";\\(\\w+\\.__wurst_ht_int\\[[^\\]]+\\] or __wurst_htNewChild\\(\\w+\\.__wurst_ht_int, [^\\n]+\\)\\)\\[5\\] = 7", + init); + assertMatches("the load is printed in place under the instance id", + "\\(Table_ht\\.__wurst_ht_int\\[\\(\\w+ or 0\\)\\] or __wurst_htEmpty\\)\\[5\\] or 0\\)", init); + } + /** * Lua reads a statement which starts with '(' as the arguments of a call ending the previous statement, so an * assignment whose target starts with '(' is printed after a ';'. Here the save follows an assignment which ends From b5b48e8eb696a447af04b0d1f297ef833d967682 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 10 Oct 2026 14:02:17 +0200 Subject: [PATCH 5/6] Lua dispatch: key a type variable by its owner, not its identity hash The methods of an override family are grouped into dispatch slots by the key of their signature, which was the printed type. A type variable prints with an identity hash (ImPrinter), so the parameter T of State and the T of NoOpState had different keys in most runs and the same key in some, and the FSM's slot was named after State in one compilation and after NoOpState in the next (DeterministicChecks failed now and then). A type variable is now its owner, its position there and its name. --- .../imtranslation/LuaDispatchPreparation.java | 55 ++++++++++++++++++- .../tests/DeterministicChecks.java | 28 ++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java index f6b58dada..62d4fcb41 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaDispatchPreparation.java @@ -5,6 +5,8 @@ import de.peeeq.wurstscript.ast.AstElementWithTypeParameters; import de.peeeq.wurstscript.ast.ExprClosure; import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.jassIm.ImArrayType; +import de.peeeq.wurstscript.jassIm.ImArrayTypeMulti; import de.peeeq.wurstscript.jassIm.ImClass; import de.peeeq.wurstscript.jassIm.ImClassType; import de.peeeq.wurstscript.jassIm.ImFunction; @@ -12,7 +14,10 @@ import de.peeeq.wurstscript.jassIm.ImProg; import de.peeeq.wurstscript.jassIm.ImVar; import de.peeeq.wurstscript.jassIm.ImType; +import de.peeeq.wurstscript.jassIm.ImTupleType; +import de.peeeq.wurstscript.jassIm.ImTypeVar; import de.peeeq.wurstscript.jassIm.ImTypeVarRef; +import de.peeeq.wurstscript.jassIm.ImTypeVars; import de.peeeq.wurstscript.jassIm.ImVars; import de.peeeq.wurstscript.translation.lua.translation.LuaIdentifiers; @@ -616,8 +621,54 @@ private static String closureRuntimeDispatchKey(ImMethod method) { return "" + Math.max(0, implementation.getParameters().size() - 1); } - private static String typeKey(ImType type) { - return type == null ? "" : type.toString(); + /** + * The type as a key which every compilation of the program agrees on. Printed, a type variable carries an identity + * hash (ImPrinter), so two variables named T could share a key in one run and not in the next, and the methods of + * a group then fell into one slot or two depending on the run. A type variable is its owner, its position there + * and its name instead. + */ + public static String typeKey(ImType type) { + if (type == null) { + return ""; + } else if (type instanceof ImTypeVarRef ref) { + return typeVarKey(ref.getTypeVariable()); + } else if (type instanceof ImClassType classType) { + StringBuilder sb = new StringBuilder(classType.getClassDef().getName()); + if (!classType.getTypeArguments().isEmpty()) { + sb.append("<"); + for (int i = 0; i < classType.getTypeArguments().size(); i++) { + sb.append(i > 0 ? "," : "").append(typeKey(classType.getTypeArguments().get(i).getType())); + } + sb.append(">"); + } + return sb.toString(); + } else if (type instanceof ImArrayType arrayType) { + return typeKey(arrayType.getEntryType()) + "[]"; + } else if (type instanceof ImArrayTypeMulti arrayType) { + return typeKey(arrayType.getEntryType()) + arrayType.getArraySize(); + } else if (type instanceof ImTupleType tupleType) { + StringBuilder sb = new StringBuilder("("); + for (int i = 0; i < tupleType.getTypes().size(); i++) { + sb.append(i > 0 ? "," : "").append(typeKey(tupleType.getTypes().get(i))); + } + return sb.append(")").toString(); + } + // a simple type, void, any: no identity in the printed form + return type.toString(); + } + + private static String typeVarKey(ImTypeVar typeVar) { + String owner = "?"; + int position = -1; + if (typeVar.getParent() instanceof ImTypeVars vars) { + position = vars.indexOf(typeVar); + if (vars.getParent() instanceof ImClass c) { + owner = c.getName(); + } else if (vars.getParent() instanceof ImFunction f) { + owner = f.getName(); + } + } + return owner + "." + position + "." + typeVar.getName(); } private static String methodSortKey(ImMethod method) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java index 5110c3938..583797f62 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java @@ -684,6 +684,34 @@ private String compileToLua(String name, List units) throws IOException { return Files.toString(new File("test-output/lua/DeterministicChecks_" + name + ".lua"), Charsets.UTF_8); } + /** + * The Lua dispatch groups the methods of an override family into slots by the key of their signature. A type + * variable used to print with an identity hash there, so the parameter T of State and the T of NoOpState shared a + * key in some runs and not in others, and the FSM below got its slot named after either class: two copies of a + * program must give a type variable the same key, and two variables of different owners different ones. + */ + @Test + public void dispatchSignatureKeysDoNotDependOnIdentityHashes() { + String[] first = typeVarKeys(); + String[] second = typeVarKeys(); + assertEquals(second, first); + assertTrue(!first[0].equals(first[1]), first[0] + " / " + first[1]); + } + + private static String[] typeVarKeys() { + String[] keys = new String[2]; + String[] owners = {"State", "NoOpState"}; + for (int i = 0; i < 2; i++) { + de.peeeq.wurstscript.jassIm.ImTypeVar t = de.peeeq.wurstscript.jassIm.JassIm.ImTypeVar("T"); + de.peeeq.wurstscript.jassIm.JassIm.ImClass(de.peeeq.wurstscript.ast.Ast.NoExpr(), owners[i], de.peeeq.wurstscript.jassIm.JassIm.ImTypeVars(t), + de.peeeq.wurstscript.jassIm.JassIm.ImVars(), de.peeeq.wurstscript.jassIm.JassIm.ImMethods(), + de.peeeq.wurstscript.jassIm.JassIm.ImFunctions(), new ArrayList<>()); + keys[i] = de.peeeq.wurstscript.translation.imtranslation.LuaDispatchPreparation.typeKey( + de.peeeq.wurstscript.jassIm.JassIm.ImTypeVarRef(t)); + } + return keys; + } + /** * The FSM of AGENTS.md section 8 with each sibling state in a package of its own: every sibling binds the root * slot FSM.update calls to its own update, never to NoOpState's, and the script is the same with the From 61a93f613853409140e8c903e08665e6d46a9f99 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 10 Oct 2026 14:59:13 +0200 Subject: [PATCH 6/6] Lua: an old-generics value of a variable cast to int may be read in place HashList and the old HashMap key their hashtable by elem castTo int. On Lua that cast of a variable is ((x == 0) and zero) or (x or 0), which only reads, but the store operand classification counted it as an effect, so once the loads counted as pure and the inliner passed the cast in directly, every such load, test and store in zombie defense stayed a helper call (more than before this branch). --- .../lua/translation/StmtTranslation.java | 6 +++++ .../wurstscript/tests/LuaHashtableTests.java | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index 6d0ea1b2b..a3bf1762a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -64,6 +64,12 @@ static WriteOperand writeOperand(ImExpr e) { // raises nor changes anything. Table keys its hashtable by this castTo int. return writeOperand(cast.getExpr()); } + if (e instanceof ImCast cast && cast.getFromType() instanceof ImAnyType && TypesHelper.isIntType(cast.getToType()) + && cast.getExpr() instanceof ImVarAccess) { + // An old-generics value of a variable as an int is ((x == 0) and zero) or (x or 0) (ExprTranslation): + // reads only. HashList and the old HashMap key their hashtable by elem castTo int. + return writeOperand(cast.getExpr()); + } if (LuaTraps.mayRaise(e)) { // It must be evaluated even when the key is nil, as the stub's argument was: an array field read // through a null object raises. diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java index 3cff2a309..0a688063c 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java @@ -252,6 +252,28 @@ public void aTableOperationIsPrintedInPlace() throws IOException { "\\(Table_ht\\.__wurst_ht_int\\[\\(\\w+ or 0\\)\\] or __wurst_htEmpty\\)\\[5\\] or 0\\)", init); } + /** + * HashList keys its hashtable by elem castTo int, an old-generics value as an int, which reads its variable and + * nothing else. Counted as an operand with an effect, it kept every inlined count and add a helper call. + */ + @Test + public void anOldGenericsKeyIsPrintedInPlace() throws IOException { + test().testLua(true).withStdLib().inline().localOptimizations().executeProg().lines( + "package Test", + "import HashList", + "init", + " let l = new HashList()", + " l.add(3)", + " l.add(3)", + " if l.has(3) and not l.has(4) and l.size() == 2", + " testSuccess()"); + + String lua = compiled("anOldGenericsKeyIsPrintedInPlace"); + String init = functionBody(lua, "init_Test"); + assertFalse("no hashtable helper is called:\n" + init, + Pattern.compile("__wurst_(Save|Load|HaveSaved|RemoveSaved)\\w*\\(").matcher(init).find()); + } + /** * Lua reads a statement which starts with '(' as the arguments of a call ending the previous statement, so an * assignment whose target starts with '(' is printed after a ';'. Here the save follows an assignment which ends