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/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/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java index 210b2cb67..b1b6b39d3 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) || !(SideEffectFreeNatives.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 d68a125f2..43ffa5b95 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 @@ -137,6 +137,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"); } @@ -438,7 +443,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(); } @@ -455,7 +460,7 @@ private List collectSideEffects(ImExpr expr, SideEffectAnalyzer analyzer } if (analyzer.hasObservableSideEffects(expr, func -> func.isNative() && (SideEffectFreeNatives.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/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..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; @@ -479,6 +486,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/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/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/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/imtranslation/LuaNativeLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaNativeLowering.java index e8323d15c..b60747f0e 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 d3ce94254..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,18 @@ 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); + } + + public static void print(LuaMultipleAssignment s, StringBuilder sb, int indent) { + s.getTargets().print(sb, indent); sb.append(" = "); s.getRight().print(sb, indent); } @@ -313,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("("); @@ -507,12 +519,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..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,8 +8,10 @@ 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; import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering; import de.peeeq.wurstscript.types.TypesHelper; @@ -152,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(), @@ -629,16 +635,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 +663,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/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 9725279b4..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,8 +9,10 @@ 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; import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering; import de.peeeq.wurstscript.translation.lua.printing.LuaPrinter; import de.peeeq.wurstscript.types.TypesHelper; @@ -187,6 +189,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 @@ -331,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()); @@ -874,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); @@ -895,6 +943,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..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 @@ -3,9 +3,12 @@ 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; import de.peeeq.wurstscript.translation.lua.printing.LuaPrinter; +import de.peeeq.wurstscript.types.TypesHelper; import java.util.ArrayList; import java.util.List; @@ -28,13 +31,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. */ @@ -43,11 +53,23 @@ 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; } + 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 (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. @@ -66,7 +88,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; @@ -187,10 +210,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 +235,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/DeterministicChecks.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/DeterministicChecks.java index d7782be7d..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 @@ -947,4 +975,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..0a688063c --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaHashtableTests.java @@ -0,0 +1,348 @@ +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")); + } + + /** + * 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); + } + + /** + * 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 + * 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/LuaMultipleResultsTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java new file mode 100644 index 000000000..de8c8a53e --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaMultipleResultsTests.java @@ -0,0 +1,370 @@ +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()); + 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 + 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); + } +} 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 f1f3d77fc..f53a8641c 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. */