diff --git a/de.peeeq.wurstscript/parserspec/lua.parseq b/de.peeeq.wurstscript/parserspec/lua.parseq
index 7bdbfae19..ef596655f 100644
--- a/de.peeeq.wurstscript/parserspec/lua.parseq
+++ b/de.peeeq.wurstscript/parserspec/lua.parseq
@@ -18,6 +18,7 @@ LuaStatements * LuaStatement
LuaStatement =
LuaDefinition
+ | LuaChunkLocal(LuaDefinition definition) // a definition printed as a local of the main chunk
| LuaAssignment(LuaExpr left, LuaExpr right)
| LuaWhile(LuaExpr cond, LuaStatements body)
| LuaFor(ref LuaVariable loopVar, LuaExpr from, LuaExpr to, LuaExprOpt step, LuaStatements body)
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 e052b0217..79d9cb8f1 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
@@ -1034,6 +1034,9 @@ public LuaCompilationUnit transformProgToLua() {
optimizer.removeGarbage();
imProg.flatten(imTranslator);
timeTaker.endPhase();
+ // After the inlining an allocation is followed by the constructor's writes, so the local optimizations can
+ // drop the defaults those overwrite. The backend no longer writes them, so this runs in every mode.
+ LuaFieldDefaults.materialize(imProg, imTranslator2);
stage = 10;
if (runArgs.isLocalOptimizations()) {
beginPhase(10, "local optimizations");
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java
index 3494f39fc..17765ad76 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/LuaEnsureTypeProvider.java
@@ -66,4 +66,13 @@ public ILconstInt __wurst_rawFloorModInt(ILconstInt a, ILconstInt b) {
public ILconstReal __wurst_rawFmodReal(ILconstReal a, ILconstReal b) {
return ILconstReal.create(a.getVal() % b.getVal());
}
+
+ /** Lua 5.3's float {@code %}: fmod, plus the divisor when {@code fmod * b < 0}. */
+ public ILconstReal __wurst_rawFloorModReal(ILconstReal a, ILconstReal b) {
+ float m = a.getVal() % b.getVal();
+ if (m * b.getVal() < 0) {
+ m += b.getVal();
+ }
+ return ILconstReal.create(m);
+ }
}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/RedundantFieldStores.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/RedundantFieldStores.java
new file mode 100644
index 000000000..767406c9d
--- /dev/null
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/RedundantFieldStores.java
@@ -0,0 +1,154 @@
+package de.peeeq.wurstscript.intermediatelang.optimizer;
+
+import de.peeeq.wurstscript.jassIm.*;
+import de.peeeq.wurstscript.translation.imoptimizer.OptimizerPass;
+import de.peeeq.wurstscript.translation.imtranslation.ImHelper;
+import de.peeeq.wurstscript.translation.imtranslation.ImTranslator;
+import de.peeeq.wurstscript.translation.imtranslation.LuaTraps;
+
+import java.util.*;
+
+/**
+ * Removes a write of a constant to a field of an object allocated in the same statement list, which the list writes
+ * again before anything can read it.
+ *
+ * Between the two writes there must be no read of the field through any object (so an alias cannot see the first
+ * value), no call (a callee can read anything, a native can run Wurst code through an event), nothing which can stop
+ * the thread or leave the list ({@link LuaTraps#mayRaise}, such as a division, a deallocation, arithmetic on a value
+ * which may be nil, or a write through an object which may be null; {@code exitwhen}, {@code return}: the first value
+ * would then be what remains) and no statement with statement lists of its own (whose paths are not followed). The object is told by value: a local copied from another names the same object
+ * until either is assigned again. It must have been allocated in the list, so it is not null and the first write
+ * cannot fail either. An allocation reads no field and runs no Wurst code, so it is not in the way.
+ *
+ * This removes the defaults an allocation writes on Lua where the constructor sets the field
+ * ({@code LuaFieldDefaults}). Fields are only still fields on Lua: on Jass the class elimination has turned them into
+ * arrays before the local optimisations run.
+ */
+public class RedundantFieldStores implements OptimizerPass {
+
+ private ImTranslator trans;
+ private int removed;
+
+ @Override
+ public int optimize(ImTranslator trans) {
+ this.trans = trans;
+ removed = 0;
+ for (ImFunction func : ImHelper.calculateFunctionsOfProg(trans.getImProg())) {
+ if (!func.isNative() && !func.isBj()) {
+ optimizeStatements(func.getBody());
+ }
+ }
+ return removed;
+ }
+
+ @Override
+ public String getName() {
+ return "Redundant field stores removed";
+ }
+
+ /** A field, by its storage, of the object a value number stands for. */
+ private record Slot(ImVar field, int object) {
+ }
+
+ private void optimizeStatements(ImStmts stmts) {
+ Map objectOf = new IdentityHashMap<>();
+ int[] nextObject = {0};
+ Set allocated = new HashSet<>();
+ Map pending = new HashMap<>();
+ Set dead = Collections.newSetFromMap(new IdentityHashMap<>());
+ for (ImStmt s : stmts) {
+ if (hasStatementLists(s)) {
+ // Its paths can assign the locals, so their values are told apart afresh after it.
+ pending.clear();
+ objectOf.clear();
+ allocated.clear();
+ s.accept(new Element.DefaultVisitor() {
+ @Override
+ public void visit(ImStmts nested) {
+ optimizeStatements(nested);
+ }
+ });
+ continue;
+ }
+ if (isBarrier(s)) {
+ pending.clear();
+ }
+ if (s instanceof ImSet set) {
+ forgetReadFields(set.getRight(), pending);
+ if (set.getLeft() instanceof ImMemberAccess target) {
+ forgetReadFields(target.getReceiver(), pending);
+ forgetReadFields(target.getIndexes(), pending);
+ Integer object = target.getIndexes().isEmpty()
+ && target.getReceiver() instanceof ImVarAccess receiver
+ && !receiver.getVar().isGlobal() ? objectOf.get(receiver.getVar()) : null;
+ if (object == null || !allocated.contains(object)) {
+ // A write through an object which may be null raises (a nil table key), like a read of an
+ // array field through one.
+ pending.clear();
+ } else {
+ Slot slot = new Slot(trans.canonical(target.getVar()), object);
+ ImSet earlier = pending.remove(slot);
+ if (earlier != null) {
+ dead.add(earlier);
+ }
+ if (isConstant(set.getRight())) {
+ pending.put(slot, set);
+ }
+ }
+ } else {
+ forgetReadFields(set.getLeft(), pending);
+ if (set.getLeft() instanceof ImVarAccess assigned && !assigned.getVar().isGlobal()) {
+ if (set.getRight() instanceof ImVarAccess copied && !copied.getVar().isGlobal()) {
+ objectOf.put(assigned.getVar(),
+ objectOf.computeIfAbsent(copied.getVar(), v -> nextObject[0]++));
+ } else {
+ int object = nextObject[0]++;
+ objectOf.put(assigned.getVar(), object);
+ if (set.getRight() instanceof ImAlloc) {
+ allocated.add(object);
+ }
+ }
+ }
+ }
+ } else {
+ forgetReadFields(s, pending);
+ }
+ }
+ if (!dead.isEmpty()) {
+ stmts.removeIf(dead::contains);
+ removed += dead.size();
+ }
+ }
+
+ private static boolean hasStatementLists(ImStmt s) {
+ return s instanceof ImIf || s instanceof ImLoop || s instanceof ImVarargLoop;
+ }
+
+ /**
+ * Whether running {@code s} can read any field or leave the list with the first value in place: anything which can
+ * raise ({@link LuaTraps#mayRaise}, a call among them), or an {@code exitwhen} or {@code return}.
+ */
+ private static boolean isBarrier(ImStmt s) {
+ return s instanceof ImExitwhen || s instanceof ImReturn || LuaTraps.mayRaise(s);
+ }
+
+ /** Forgets the pending writes of every field {@code e} reads, through whatever object. */
+ private void forgetReadFields(Element e, Map pending) {
+ if (pending.isEmpty()) {
+ return;
+ }
+ e.accept(new Element.DefaultVisitor() {
+ @Override
+ public void visit(ImMemberAccess access) {
+ super.visit(access);
+ ImVar field = trans.canonical(access.getVar());
+ pending.keySet().removeIf(slot -> slot.field() == field);
+ }
+ });
+ }
+
+ private static boolean isConstant(ImExpr e) {
+ return e instanceof ImIntVal || e instanceof ImRealVal || e instanceof ImBoolVal
+ || e instanceof ImStringVal || e instanceof ImNull;
+ }
+}
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 77bb2815d..d59365963 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
@@ -9,6 +9,7 @@
import de.peeeq.wurstscript.intermediatelang.optimizer.LocalPlayerAwareOptimizerPass;
import de.peeeq.wurstscript.intermediatelang.optimizer.LocalPlayerContextAnalyzer;
import de.peeeq.wurstscript.intermediatelang.optimizer.LocalMerger;
+import de.peeeq.wurstscript.intermediatelang.optimizer.RedundantFieldStores;
import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer;
import de.peeeq.wurstscript.intermediatelang.optimizer.SimpleRewrites;
import de.peeeq.wurstscript.jassIm.*;
@@ -37,6 +38,9 @@ public class ImOptimizer {
localPasses.add(new BranchMerger());
localPasses.add(new ConstantAndCopyPropagation());
localPasses.add(new UselessFunctionCallsRemover());
+ // After the passes which use the local-player analysis: a pass which does not use it discards it, and none
+ // after this one needs it again in the same sweep.
+ localPasses.add(new RedundantFieldStores());
localPasses.add(new GlobalsInliner());
localPasses.add(new DispatchCheckDeduplicator());
localPasses.add(new SimpleRewrites());
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java
index c0b3bdef4..01a23e4a4 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java
@@ -155,8 +155,9 @@ private static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr tran
boolean indexContext) {
ImFunction toIndex = null;
ImFunction fromIndex = null;
+ FuncDef fromIndexFunc = null;
if (actualType instanceof WurstTypeBoundTypeParam wtb) {
- FuncDef fromIndexFunc = wtb.getFromIndex();
+ fromIndexFunc = wtb.getFromIndex();
if (fromIndexFunc != null) {
fromIndex = t.getFuncFor(fromIndexFunc);
}
@@ -178,7 +179,10 @@ private static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr tran
return wrapLua(trace, t, translated, actualType);
} else if (fromIndex != null) {
// System.out.println(" --> fromIndex");
- if(t.isLuaTarget()) {
+ // Lua normalises the erased value to an int first, except for the conversions which
+ // answer the same for every value a slot can hold (nil, 0, an index) without it.
+ if (t.isLuaTarget() && !de.peeeq.wurstscript.translation.lua.translation.ExprTranslation
+ .printsAsObjectFromIndex(fromIndexFunc)) {
translated = ImFunctionCall(trace, t.ensureIntFunc, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL);
}
// no ensure type necessary here, because the fromIndex function is already type safe
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 2e715bb35..6c9ea2276 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
@@ -202,6 +202,7 @@ public T canonical(T copy) {
@Nullable public ImFunction luaRawFmodIntFunc = null;
@Nullable public ImFunction luaRawFmodRealFunc = null;
@Nullable public ImFunction luaRawFloorModIntFunc = null;
+ @Nullable public ImFunction luaRawFloorModRealFunc = null;
@Nullable public ImFunction luaRawConcatFunc = null;
/** {@code x or ""}: a string operand which may be nil, made safe for {@link #luaRawConcatFunc}. */
@Nullable public ImFunction luaRawOrEmptyFunc = null;
@@ -240,7 +241,8 @@ public boolean isTrapFreeLuaIntrinsicCall(ImFunctionCall call) {
|| (divisor instanceof ImRealVal realVal && Double.parseDouble(realVal.getValR()) != 0.0);
return nonZeroDivisor
&& (target == luaRawFloorDivIntFunc || target == luaRawFmodIntFunc
- || target == luaRawFloorModIntFunc || target == luaRawFmodRealFunc);
+ || target == luaRawFloorModIntFunc || target == luaRawFmodRealFunc
+ || target == luaRawFloorModRealFunc);
}
/**
* Whether {@code f} is one of the Lua keyed-map read stubs, which the backend prints as a table
@@ -263,7 +265,8 @@ public List pinnedFunctions() {
List result = new ArrayList<>();
for (ImFunction f : new ImFunction[]{
ensureIntFunc, ensureBoolFunc, ensureRealFunc, ensureStrFunc, stringConcatFunc,
- luaRawFloorDivIntFunc, luaRawFmodIntFunc, luaRawFmodRealFunc, luaRawFloorModIntFunc, luaRawConcatFunc,
+ luaRawFloorDivIntFunc, luaRawFmodIntFunc, luaRawFmodRealFunc, luaRawFloorModIntFunc,
+ luaRawFloorModRealFunc, luaRawConcatFunc,
luaRawOrEmptyFunc, luaRawR2IFunc, luaRawToNumberIntFunc, luaRawToNumberRealFunc, luaRawToIntegerFunc,
luaRawToStringFunc, luaIntDivFunc, luaModIntFunc, luaModRealFunc,
debugPrintFunction, errorFunc, genericNewMarker, globalInitFunc}) {
@@ -465,6 +468,17 @@ private record Bridge(ImMethod bridge, ImMethod inherited) {
private final List bridges = new ArrayList<>();
+ /** Whether {@link LuaFieldDefaults} wrote the field defaults after the allocations, which the Lua backend needs. */
+ private boolean luaFieldDefaultsMaterialized = false;
+
+ public boolean luaFieldDefaultsMaterialized() {
+ return luaFieldDefaultsMaterialized;
+ }
+
+ public void setLuaFieldDefaultsMaterialized() {
+ luaFieldDefaultsMaterialized = 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/LuaFieldDefaults.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaFieldDefaults.java
new file mode 100644
index 000000000..a70c1394a
--- /dev/null
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaFieldDefaults.java
@@ -0,0 +1,211 @@
+package de.peeeq.wurstscript.translation.imtranslation;
+
+import de.peeeq.wurstscript.jassIm.*;
+import de.peeeq.wurstscript.types.TypesHelper;
+
+import java.util.*;
+
+/**
+ * Writes the default of each field of a new object on Lua, as IM statements after its allocation, so that the
+ * optimiser sees the ones the constructor overwrites.
+ *
+ * Lua keeps a field in a table indexed by the object id. A slot nothing wrote reads as nil, where Jass reads 0,
+ * and an id is reused, so a new object's fields are set to their defaults. The backend's allocation used to write
+ * them all, out of the optimiser's sight, so a construction wrote each field the constructor sets twice: a damage
+ * event wrote the 11 fields of DamageInstance 22 times.
+ *
+ * {@link #materialize} runs after the inlining, when an allocation is followed by the constructor's own writes, and
+ * the local optimisations remove a default which is written again before anything can read it
+ * ({@code RedundantFieldStores}). {@link #moveSurvivorsToAllocations} runs before the backend: a default which
+ * survives at some allocation of a class goes back into that class's shared allocation function, and its copies
+ * after the allocations are removed, so a field leaves the allocation only where every allocation overwrote it, and
+ * no construction writes more than before nor is the script bigger. Without the local optimisations every default
+ * goes back; only the allocation function of a class nothing allocates (an abstract base), which nothing calls, is
+ * left without them.
+ *
+ * An array field gets a table of its own per object, which IM cannot express; the backend's allocation always makes
+ * those ({@link #isArrayField}).
+ */
+public final class LuaFieldDefaults {
+
+ private LuaFieldDefaults() {
+ }
+
+ /** Inserts the defaults after every allocation and returns how many allocations there were. */
+ public static int materialize(ImProg prog, ImTranslator tr) {
+ List allocations = new ArrayList<>();
+ prog.accept(new ImProg.DefaultVisitor() {
+ @Override
+ public void visit(ImAlloc alloc) {
+ super.visit(alloc);
+ if (!(alloc.getParent() instanceof ImSet set)
+ || set.getRight() != alloc
+ || !(set.getLeft() instanceof ImVarAccess)
+ || !(set.getParent() instanceof ImStmts)) {
+ throw new IllegalStateException("An allocation of " + alloc.getClazz()
+ + " which is not assigned to a variable as a statement: " + alloc.getParent());
+ }
+ allocations.add(set);
+ }
+ });
+ for (ImSet allocation : allocations) {
+ ImAlloc alloc = (ImAlloc) allocation.getRight();
+ ImVar object = ((ImVarAccess) allocation.getLeft()).getVar();
+ ImStmts stmts = (ImStmts) allocation.getParent();
+ int index = stmts.indexOf(allocation);
+ List defaults = new ArrayList<>();
+ for (ImVar field : fieldsOf(alloc.getClazz().getClassDef(), tr)) {
+ if (isArrayField(field)) {
+ continue;
+ }
+ ImTypeArguments typeArguments = alloc.getClazz().getTypeArguments().copy();
+ defaults.add(JassIm.ImSet(allocation.getTrace(),
+ JassIm.ImMemberAccess(allocation.getTrace(), JassIm.ImVarAccess(object), typeArguments, field,
+ JassIm.ImExprs()),
+ defaultValue(field.getType())));
+ }
+ stmts.addAll(index + 1, defaults);
+ }
+ tr.setLuaFieldDefaultsMaterialized();
+ return allocations.size();
+ }
+
+ /**
+ * Removes the defaults which still follow an allocation and returns, for each class allocated, the fields (by
+ * storage) whose default survived at one of its allocations, which its allocation function writes instead.
+ *
+ * A surviving default is found where {@link #materialize} put it: in the run of writes right after the
+ * allocation, each setting a field of the object to exactly its default. Moving such a write into the
+ * allocation, which runs where the allocation statement is, changes nothing: nothing runs between them,
+ * whoever wrote it. At another allocation of the class, where the optimiser removed that default because the
+ * field is set before anything reads it, the allocation's write is dead, which is what every allocation did
+ * before.
+ */
+ public static Map> moveSurvivorsToAllocations(ImProg prog, ImTranslator tr) {
+ if (!tr.luaFieldDefaultsMaterialized()) {
+ throw new IllegalStateException("The field defaults were not written after the allocations "
+ + "(LuaFieldDefaults.materialize), so the allocations would leave fields nil.");
+ }
+ Map> written = new LinkedHashMap<>();
+ List survivors = new ArrayList<>();
+ prog.accept(new ImProg.DefaultVisitor() {
+ @Override
+ public void visit(ImStmts stmts) {
+ super.visit(stmts);
+ for (int i = 0; i < stmts.size(); i++) {
+ if (!(stmts.get(i) instanceof ImSet set) || !(set.getRight() instanceof ImAlloc alloc)
+ || !(set.getLeft() instanceof ImVarAccess object)) {
+ continue;
+ }
+ ImClass c = alloc.getClazz().getClassDef();
+ Map fields = new IdentityHashMap<>();
+ for (ImVar field : fieldsOf(c, tr)) {
+ if (!isArrayField(field)) {
+ fields.put(tr.canonical(field), field);
+ }
+ }
+ for (int j = i + 1; j < stmts.size(); j++) {
+ ImVar field = defaultWrittenTo(stmts.get(j), object.getVar(), fields, tr);
+ if (field == null) {
+ break;
+ }
+ written.computeIfAbsent(c, k -> Collections.newSetFromMap(new IdentityHashMap<>()))
+ .add(field);
+ survivors.add(stmts.get(j));
+ }
+ }
+ }
+ });
+ for (ImStmt survivor : survivors) {
+ ((ImStmts) survivor.getParent()).remove(survivor);
+ }
+ return written;
+ }
+
+ /** The field (by storage) which {@code s} sets to exactly its default on {@code object}, or null. */
+ private static @org.eclipse.jdt.annotation.Nullable ImVar defaultWrittenTo(ImStmt s, ImVar object,
+ Map fields,
+ ImTranslator tr) {
+ if (!(s instanceof ImSet set) || !(set.getLeft() instanceof ImMemberAccess target)
+ || !target.getIndexes().isEmpty()
+ || !(target.getReceiver() instanceof ImVarAccess receiver) || receiver.getVar() != object) {
+ return null;
+ }
+ ImVar storage = tr.canonical(target.getVar());
+ ImVar field = fields.get(storage);
+ if (field == null || !isDefault(set.getRight(), field.getType())) {
+ return null;
+ }
+ return storage;
+ }
+
+ private static boolean isDefault(ImExpr e, ImType type) {
+ if (type instanceof ImSimpleType simpleType) {
+ if (TypesHelper.isIntType(simpleType)) {
+ return e instanceof ImIntVal v && v.getValI() == 0;
+ } else if (TypesHelper.isBoolType(simpleType)) {
+ return e instanceof ImBoolVal v && !v.getValB();
+ } else if (TypesHelper.isRealType(simpleType)) {
+ if (!(e instanceof ImRealVal v)) {
+ return false;
+ }
+ double value = Double.parseDouble(v.getValR());
+ return value == 0 && Double.doubleToRawLongBits(value) == 0;
+ } else if (TypesHelper.isStringType(simpleType)) {
+ return e instanceof ImStringVal v && v.getValS().isEmpty();
+ }
+ }
+ return e instanceof ImNull;
+ }
+
+ /**
+ * The fields an object of {@code c} has: those of its superclasses, in the order of their names, then its own,
+ * each storage once ({@link ImTranslator#canonical}).
+ */
+ public static List fieldsOf(ImClass c, ImTranslator tr) {
+ List result = new ArrayList<>();
+ collectFields(c, tr, result, Collections.newSetFromMap(new IdentityHashMap<>()),
+ Collections.newSetFromMap(new IdentityHashMap<>()));
+ return result;
+ }
+
+ private static void collectFields(ImClass c, ImTranslator tr, List out, Set visitedClasses,
+ Set visitedFields) {
+ if (!visitedClasses.add(c)) {
+ return;
+ }
+ List superClasses = new ArrayList<>(c.getSuperClasses());
+ superClasses.sort(Comparator.comparing(sc -> sc.getClassDef().getName()));
+ for (ImClassType sc : superClasses) {
+ collectFields(sc.getClassDef(), tr, out, visitedClasses, visitedFields);
+ }
+ for (ImVar field : c.getFields()) {
+ if (visitedFields.add(tr.canonical(field))) {
+ out.add(field);
+ }
+ }
+ }
+
+ /** An array field, whose default is a table of its own per object, which the backend makes. */
+ public static boolean isArrayField(ImVar field) {
+ return field.getType() instanceof ImArrayType || field.getType() instanceof ImArrayTypeMulti;
+ }
+
+ /** The value a field of {@code type} holds before anything writes it, as the Lua backend prints defaults. */
+ private static ImExpr defaultValue(ImType type) {
+ if (type instanceof ImSimpleType simpleType) {
+ if (TypesHelper.isIntType(simpleType)) {
+ return JassIm.ImIntVal(0);
+ } else if (TypesHelper.isBoolType(simpleType)) {
+ return JassIm.ImBoolVal(false);
+ } else if (TypesHelper.isRealType(simpleType)) {
+ return JassIm.ImRealVal("0.");
+ } else if (TypesHelper.isStringType(simpleType)) {
+ return JassIm.ImStringVal("");
+ }
+ } else if (type instanceof ImTupleType) {
+ throw new IllegalStateException("A tuple field after the tuples were eliminated: " + type);
+ }
+ return JassIm.ImNull(type);
+ }
+}
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java
index c044de110..bd4a8d31c 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java
@@ -60,6 +60,9 @@ public final class LuaKeyedMap {
private static final java.util.List READ_STUBS = java.util.List.of(
NATIVE_GET, NATIVE_GET_INT, NATIVE_GET_REAL, NATIVE_GET_BOOL, NATIVE_GET_STR, NATIVE_HAS);
+ /** The stubs which store into a map, printed as the table store they stand for where they are called. */
+ private static final java.util.List WRITE_STUBS = java.util.List.of(NATIVE_PUT, NATIVE_REMOVE);
+
private LuaKeyedMap() {
}
@@ -68,10 +71,19 @@ private LuaKeyedMap() {
* each stub once and a user function that happens to share the name stays an ordinary function.
*/
public static String readStubName(ImTranslator translator, ImFunction f) {
+ return stubName(translator, f, READ_STUBS);
+ }
+
+ /** The name of the put or remove stub {@code f} is, or null; matched like {@link #readStubName}. */
+ public static String writeStubName(ImTranslator translator, ImFunction f) {
+ return stubName(translator, f, WRITE_STUBS);
+ }
+
+ private static String stubName(ImTranslator translator, ImFunction f, java.util.List names) {
if (!f.isNative()) {
return null;
}
- for (String name : READ_STUBS) {
+ for (String name : names) {
if (translator.luaKeyedStubs.get(name) == f) {
return name;
}
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 bb3595cd6..d235f999c 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
@@ -408,15 +408,21 @@ private static ImExpr concatenation(ImOperatorCall call, ImTranslator translator
*/
private static final Set STRING_NATIVES_NEVER_NIL = Set.of("I2S");
- private static boolean neverNil(ImExpr e, ImTranslator translator) {
+ /**
+ * Whether the string {@code e} cannot be nil on Lua. Asked here, before the optimiser, and again
+ * by the backend of the operand a {@code luaRawOrEmptyFunc} guard finally holds: inlining turns a
+ * call such as {@code int.toString()} into the {@code I2S} it returns, which needs no guard.
+ */
+ public static boolean neverNil(ImExpr e, ImTranslator translator) {
if (e instanceof ImStringVal) {
return true;
}
if (e instanceof ImFunctionCall imFunctionCall) {
ImFunction f = imFunctionCall.getFunc();
- // a lowered concatenation always has two strings to join
+ // a lowered concatenation always has two strings to join; tostring answers a string
return f == translator.luaRawConcatFunc
|| f == translator.luaRawOrEmptyFunc
+ || f == translator.luaRawToStringFunc
|| (f.isNative() && STRING_NATIVES_NEVER_NIL.contains(f.getName()));
}
return false;
@@ -612,6 +618,7 @@ private static final class DivModFunctions {
private ImFunction rawFmodInt;
private ImFunction rawFloorModInt;
private ImFunction rawFmodReal;
+ private ImFunction rawFloorModReal;
private ImFunction intDiv;
private ImFunction modInt;
private ImFunction modReal;
@@ -633,18 +640,27 @@ ImFunction intDiv() {
return intDiv;
}
+ /** For an int divisor b > 0, Lua's floored % is exactly the ModuloInteger result. */
ImFunction modInt() {
if (modInt == null) {
- modInt = buildMod("__wurst_modInt", TypesHelper.imInt(), JassIm.ImIntVal(0), rawFmodInt());
+ modInt = buildMod("__wurst_modInt", TypesHelper.imInt(), JassIm.ImIntVal(0),
+ WurstOperator.GREATER, JassIm.ImIntVal(0), rawFmodInt(), rawFloorModInt());
translator.luaModIntFunc = modInt;
created.add(modInt);
}
return modInt;
}
+ /**
+ * Lua 5.3 computes a float % as fmod plus the divisor when {@code fmod * b < 0}, which for a
+ * positive b is the ModuloReal correction (-0.0 stays -0.0 on both), except where that product
+ * underflows to zero: a subnormal remainder and a divisor below 1. So the operator is taken
+ * from b >= 1 on, and never for NaN.
+ */
ImFunction modReal() {
if (modReal == null) {
- modReal = buildMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."), rawFmodReal());
+ modReal = buildMod("__wurst_modReal", TypesHelper.imReal(), JassIm.ImRealVal("0."),
+ WurstOperator.GREATER_EQ, JassIm.ImRealVal("1."), rawFmodReal(), rawFloorModReal());
translator.luaModRealFunc = modReal;
created.add(modReal);
}
@@ -692,6 +708,16 @@ private ImFunction rawFmodReal() {
return rawFmodReal;
}
+ /** Lua's float {@code %}; only correct for a divisor of at least 1, which the caller guarantees. */
+ private ImFunction rawFloorModReal() {
+ if (rawFloorModReal == null) {
+ rawFloorModReal = rawNative("__wurst_rawFloorModReal", TypesHelper.imReal());
+ translator.luaRawFloorModRealFunc = rawFloorModReal;
+ created.add(rawFloorModReal);
+ }
+ return rawFloorModReal;
+ }
+
/** A native leaf with two params and a return, translated as a Lua backend intrinsic. */
private static ImFunction rawNative(String name, ImType numType) {
ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false);
@@ -733,24 +759,35 @@ private static ImFunction buildIntDiv(ImFunction rawFloorDiv) {
}
/**
- * local r = rawFmod(a, b)
- * if r < 0 then r = r + b end
+ * local r
+ * if b [> 0 | >= 1.] then r = rawFloorMod(a, b)
+ * else r = rawFmod(a, b); if r < 0 then r = r + b end end
* return r
- * (Lua's % is floored; Wurst mod follows Blizzard.j's ModuloInteger/ModuloReal:
- * truncated remainder, plus the divisor when the remainder is negative.)
+ * (Wurst mod follows Blizzard.j's ModuloInteger/ModuloReal: truncated remainder, plus the
+ * divisor when the remainder is negative. Lua's % is floored, which gives the same for the
+ * divisors {@code positiveTest} admits, as one VM operation instead of a C call and a branch.
+ * Once inlined where the divisor is a literal, the test folds and only one branch is left.)
*/
- private static ImFunction buildMod(String name, ImType numType, ImExpr zeroLiteral, ImFunction rawFmod) {
+ private static ImFunction buildMod(String name, ImType numType, ImExpr zeroLiteral,
+ WurstOperator positiveTest, ImExpr positiveBound,
+ ImFunction rawFmod, ImFunction rawFloorMod) {
ImVar a = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "a", false);
ImVar b = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "b", false);
ImVar r = JassIm.ImVar(SYNTHETIC_TRACE, numType.copy(), "r", false);
ImStmts body = JassIm.ImStmts(
- JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), call(rawFmod, JassIm.ImVarAccess(a), JassIm.ImVarAccess(b))),
JassIm.ImIf(SYNTHETIC_TRACE,
- JassIm.ImOperatorCall(WurstOperator.LESS, JassIm.ImExprs(JassIm.ImVarAccess(r), zeroLiteral)),
+ JassIm.ImOperatorCall(positiveTest, JassIm.ImExprs(JassIm.ImVarAccess(b), positiveBound)),
JassIm.ImStmts(JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r),
- JassIm.ImOperatorCall(WurstOperator.PLUS, JassIm.ImExprs(JassIm.ImVarAccess(r), JassIm.ImVarAccess(b))))),
- JassIm.ImStmts()
+ call(rawFloorMod, JassIm.ImVarAccess(a), JassIm.ImVarAccess(b)))),
+ JassIm.ImStmts(
+ JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r), call(rawFmod, JassIm.ImVarAccess(a), JassIm.ImVarAccess(b))),
+ JassIm.ImIf(SYNTHETIC_TRACE,
+ JassIm.ImOperatorCall(WurstOperator.LESS, JassIm.ImExprs(JassIm.ImVarAccess(r), zeroLiteral)),
+ JassIm.ImStmts(JassIm.ImSet(SYNTHETIC_TRACE, JassIm.ImVarAccess(r),
+ JassIm.ImOperatorCall(WurstOperator.PLUS, JassIm.ImExprs(JassIm.ImVarAccess(r), JassIm.ImVarAccess(b))))),
+ JassIm.ImStmts()
+ ))
),
JassIm.ImReturn(SYNTHETIC_TRACE, JassIm.ImVarAccess(r))
);
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaTraps.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaTraps.java
new file mode 100644
index 000000000..3458c6e54
--- /dev/null
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaTraps.java
@@ -0,0 +1,88 @@
+package de.peeeq.wurstscript.translation.imtranslation;
+
+import de.peeeq.wurstscript.WurstOperator;
+import de.peeeq.wurstscript.jassIm.*;
+
+/**
+ * What can raise a Lua error when it is evaluated, and so stop the thread at that point.
+ *
+ * A pass which removes or moves an evaluation must not move it past such a point: the code after it would then run
+ * where it did not, or a value would be left which the thread never saw replaced. Beyond what stops the thread on Jass
+ * too ({@link Flatten#mayStopTheThread}), Lua raises where Jass reads a default through an object which is null or
+ * freed: an array field is {@code storage[o][i]}, which indexes nil, an object's type id is read from its class
+ * descriptor, which is nil, and a field read gives nil, on which arithmetic and orderings raise, as does a table
+ * write under a nil key. A nil read can reach an operand through any variable, so whether an operand is nil is not
+ * traced: arithmetic, an ordering and an array write count as raising unless their operands are literals.
+ *
+ * The answer is decided by the kind of each node, and a kind not named here counts as raising until it is shown not
+ * to.
+ */
+public final class LuaTraps {
+
+ private LuaTraps() {
+ }
+
+ /**
+ * Whether evaluating {@code e}, an expression or a statement which is not a nested block, can raise. It cannot
+ * when it consists only of constants, variable reads, field reads without an index, array reads, allocations, type
+ * ids of a class, instanceof tests and tuples, of {@code ==}, {@code !=}, {@code and}, {@code or} and {@code not},
+ * of arithmetic and orderings on literals, and of assignments to a variable or a field. Anything else can: a call,
+ * a deallocation (a double free fails), a division, arithmetic or an ordering on a value which is not a literal (it
+ * may be nil), an array write under an index which is not a literal, a type id read through an object, a cast, an
+ * array field read. A field write through an object which may be null raises too; that is for the caller to tell.
+ */
+ public static boolean mayRaise(Element e) {
+ if (e instanceof ImIntVal || e instanceof ImRealVal || e instanceof ImStringVal || e instanceof ImBoolVal
+ || e instanceof ImFuncRef || e instanceof ImNull || e instanceof ImVarAccess || e instanceof ImAlloc
+ || e instanceof ImTypeIdOfClass) {
+ return false;
+ } else if (e instanceof ImMemberAccess access) {
+ return !access.getIndexes().isEmpty() || mayRaise(access.getReceiver());
+ } else if (e instanceof ImVarArrayAccess access) {
+ return anyMayRaise(access.getIndexes());
+ } else if (e instanceof ImInstanceof instanceOf) {
+ // isInstanceOf answers false for an object without a class descriptor.
+ return mayRaise(instanceOf.getObj());
+ } else if (e instanceof ImTupleExpr tuple) {
+ return anyMayRaise(tuple.getExprs());
+ } else if (e instanceof ImTupleSelection selection) {
+ return mayRaise(selection.getTupleExpr());
+ } else if (e instanceof ImOperatorCall call) {
+ if (Flatten.mayStopTheThread(call)) {
+ return true;
+ }
+ if (!neverRaisesOnNil(call.getOp())) {
+ for (ImExpr operand : call.getArguments()) {
+ if (!(operand instanceof ImConst)) {
+ return true;
+ }
+ }
+ }
+ return anyMayRaise(call.getArguments());
+ } else if (e instanceof ImSet set) {
+ if (set.getLeft() instanceof ImVarArrayAccess target) {
+ for (ImExpr index : target.getIndexes()) {
+ if (!(index instanceof ImConst)) {
+ return true;
+ }
+ }
+ }
+ return mayRaise(set.getLeft()) || mayRaise(set.getRight());
+ }
+ return true;
+ }
+
+ private static boolean anyMayRaise(ImExprs exprs) {
+ for (ImExpr e : exprs) {
+ if (mayRaise(e)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean neverRaisesOnNil(WurstOperator op) {
+ return op == WurstOperator.EQ || op == WurstOperator.NOTEQ || op == WurstOperator.AND
+ || op == WurstOperator.OR || op == WurstOperator.NOT;
+ }
+}
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 802065013..d3ce94254 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
@@ -4,6 +4,7 @@
import de.peeeq.wurstscript.utils.Utils;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
public class LuaPrinter {
@@ -26,6 +27,10 @@ public static void print(LuaCompilationUnit cu, StringBuilder sb, int indent) {
printVariable(luaVariable, sb, indent);
sb.append("\n");
statementBlock = true;
+ } else if (d instanceof LuaChunkLocal local && local.getDefinition() instanceof LuaVariable) {
+ d.print(sb, indent);
+ sb.append("\n");
+ statementBlock = true;
} else if(d instanceof LuaAssignment) {
// these are top level assignments that are not inside functions
d.print(sb, indent);
@@ -53,6 +58,10 @@ public static void print(LuaExprArrayAccess e, StringBuilder sb, int indent) {
}
public static void print(LuaExprBinary e, StringBuilder sb, int indent) {
+ if (e.getOp() instanceof LuaOpConcatString && isConcatenation(e.getLeftExpr())) {
+ printConcatenation(e, sb, indent);
+ return;
+ }
sb.append("(");
if (continuesChain(e, e.getLeftExpr())) {
printChain(e, sb, indent);
@@ -111,9 +120,11 @@ private static boolean continuesChain(LuaExprBinary parent, LuaExpr left) {
* The Lua precedence level of an operator whose left-nested chains print flat, or -1 for one
* which keeps its parentheses. Only the levels (not their order) matter, and they follow the
* Lua 5.3 manual, section 3.4.8: {@code or}; {@code and}; {@code + -}; {@code * / // %}.
- * Not listed, so always parenthesised: {@code ..}, which is right associative (so
- * {@code (a .. b) .. c} is not {@code a .. b .. c}); the comparisons, where a chain reads as
- * a range check but compares a boolean; and any operator added later until its level is decided.
+ * Not listed: {@code ..}, which is right associative, so the text {@code a .. b .. c} parses as
+ * {@code a .. (b .. c)}, not as {@code (a .. b) .. c}; it has its own printing, see
+ * {@link #printConcatenation}. Also not listed, so always parenthesised: the comparisons, where
+ * a chain reads as a range check but compares a boolean; and any operator added later until its
+ * level is decided.
*/
private static int chainLevel(LuaOpBinary op) {
if (op instanceof LuaOpOr) {
@@ -129,6 +140,67 @@ private static int chainLevel(LuaOpBinary op) {
return -1;
}
+ /**
+ * The most operands one flat group of a concatenation holds. A flat {@code a .. b .. c} is one
+ * CONCAT over consecutive registers and the parser nests one level per {@code ..}, so a group
+ * costs as many registers and parser levels as it has operands. Sixteen is more than the parts
+ * of nearly every message, and a chain of up to 4096 parts then holds at most 46 registers for
+ * its operands, which fit beside the 199 locals a function may keep before they are spilled
+ * (Lua has 255).
+ */
+ private static final int CONCATENATION_GROUP = 16;
+
+ private static boolean isConcatenation(LuaExpr e) {
+ return e instanceof LuaExprBinary binary && binary.getOp() instanceof LuaOpConcatString;
+ }
+
+ /**
+ * Prints a left-nested chain of {@code ..} flat: {@code (a .. b .. c)}, not {@code ((a .. b) .. c)}.
+ * The nested text concatenates once per operator and builds every intermediate string; the flat
+ * one is a single CONCAT into one buffer. The text parses to another tree, {@code a .. (b .. c)},
+ * because {@code ..} is right associative, which is why {@link #chainLevel} leaves it out. Here the
+ * value is still the same: the backend only concatenates strings (operands which might be nil are
+ * guarded with {@code or ""}), and joining strings is associative, with no metamethod to observe
+ * the grouping. The operands are still evaluated from left to right.
+ *
+ *
A long chain is split into parenthesised groups of at most {@link #CONCATENATION_GROUP}
+ * operands, and those into groups again, so neither the parser levels (luac stops at 200,
+ * "too many C levels") nor the registers grow with its length.
+ */
+ private static void printConcatenation(LuaExprBinary outermost, StringBuilder sb, int indent) {
+ List operands = new ArrayList<>();
+ LuaExpr current = outermost;
+ while (isConcatenation(current)) {
+ LuaExprBinary binary = (LuaExprBinary) current;
+ operands.add(binary.getRight());
+ current = binary.getLeftExpr();
+ }
+ operands.add(current);
+ Collections.reverse(operands);
+ printConcatenation(operands, 0, operands.size(), sb, indent);
+ }
+
+ private static void printConcatenation(List operands, int from, int to, StringBuilder sb, int indent) {
+ if (to - from == 1) {
+ operands.get(from).print(sb, indent);
+ return;
+ }
+ // Each part holds at most 'span' operands, a power of the group size, and there are at most
+ // CONCATENATION_GROUP parts, so the nesting is logarithmic in the length of the chain.
+ long span = 1;
+ while (span * CONCATENATION_GROUP < to - from) {
+ span *= CONCATENATION_GROUP;
+ }
+ sb.append("(");
+ for (int start = from; start < to; start += (int) span) {
+ if (start > from) {
+ sb.append(" .. ");
+ }
+ printConcatenation(operands, start, (int) Math.min(to, start + span), sb, indent);
+ }
+ sb.append(")");
+ }
+
public static void print(LuaExprBoolVal e, StringBuilder sb, int indent) {
sb.append(e.getValB());
}
@@ -275,6 +347,27 @@ public static void print(LuaExprVarAccess e, StringBuilder sb, int indent) {
public static void print(LuaFunction f, StringBuilder sb, int indent) {
printIndent(sb, indent);
+ printFunction(f, sb, indent);
+ }
+
+ /**
+ * A local of the main chunk. A function printed after it reaches it as an upvalue, one
+ * instruction, instead of looking its name up in _ENV, the table of every global.
+ */
+ public static void print(LuaChunkLocal l, StringBuilder sb, int indent) {
+ LuaDefinition definition = l.getDefinition();
+ if (definition instanceof LuaVariable v) {
+ print(v, sb, indent);
+ } else if (definition instanceof LuaFunction f) {
+ printIndent(sb, indent);
+ sb.append("local ");
+ printFunction(f, sb, indent);
+ } else {
+ throw new IllegalArgumentException("A method is a table field and cannot be a local: " + definition);
+ }
+ }
+
+ private static void printFunction(LuaFunction f, StringBuilder sb, int indent) {
sb.append("function ");
sb.append(f.getName());
sb.append("(");
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java
index 10b712567..3f6865495 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
@@ -1,6 +1,8 @@
package de.peeeq.wurstscript.translation.lua.translation;
import de.peeeq.wurstscript.WurstOperator;
+import de.peeeq.wurstscript.ast.FuncDef;
+import de.peeeq.wurstscript.ast.WPackage;
import de.peeeq.wurstscript.attributes.CompileError;
import de.peeeq.wurstscript.jassIm.*;
import de.peeeq.wurstscript.luaAst.*;
@@ -8,6 +10,7 @@
import de.peeeq.wurstscript.translation.imtranslation.ImTranslator;
import de.peeeq.wurstscript.translation.imtranslation.LuaKeyedMap;
import de.peeeq.wurstscript.translation.imtranslation.LuaMethodCallLowering;
+import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering;
import de.peeeq.wurstscript.types.TypesHelper;
import java.util.Optional;
@@ -91,15 +94,13 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
return classFromIndex(arg, tr);
} else if (tcFunc.equals("objectToIndex")) {
return LuaAst.LuaExprFunctionCall(tr.toIndexFunction, LuaAst.LuaExprlist(arg));
- } else if (tcFunc.equals("objectFromIndex")) {
- return LuaAst.LuaExprFunctionCall(tr.fromIndexFunction, LuaAst.LuaExprlist(arg));
} else if (tcFunc.equals("stringToIndex")) {
return LuaAst.LuaExprFunctionCall(tr.stringToIndexFunction, LuaAst.LuaExprlist(arg));
} else if (tcFunc.equals("stringFromIndex")) {
return LuaAst.LuaExprFunctionCall(tr.stringFromIndexFunction, LuaAst.LuaExprlist(arg));
} else if (LUA_HANDLE_TO_INDEX.contains(tcFunc)) {
return LuaAst.LuaExprFunctionCall(tr.toIndexFunction, LuaAst.LuaExprlist(arg));
- } else if (LUA_HANDLE_FROM_INDEX.contains(tcFunc)) {
+ } else if (isObjectFromIndexName(tcFunc)) {
return LuaAst.LuaExprFunctionCall(tr.fromIndexFunction, LuaAst.LuaExprlist(arg));
}
}
@@ -122,8 +123,14 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
unaryIntrinsic.substring(dot + 1)), argument);
}
if (e.getFunc() == tr.imTr.luaRawOrEmptyFunc && e.getArguments().size() == 1) {
- return LuaAst.LuaExprBinary(e.getArguments().get(0).translateToLua(tr), LuaAst.LuaOpOr(),
- LuaAst.LuaExprStringVal(""));
+ ImExpr operand = e.getArguments().get(0);
+ LuaExpr translated = operand.translateToLua(tr);
+ // The guard was placed before inlining; the operand it finally holds may be one that
+ // cannot be nil (a literal, I2S, a concatenation), which then needs no guard.
+ if (LuaNativeLowering.neverNil(operand, tr.imTr)) {
+ return translated;
+ }
+ return LuaAst.LuaExprBinary(translated, LuaAst.LuaOpOr(), LuaAst.LuaExprStringVal(""));
}
if (e.getFunc() == tr.imTr.luaRawR2IFunc && e.getArguments().size() == 1) {
LuaExpr x = e.getArguments().get(0).translateToLua(tr);
@@ -139,6 +146,12 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
return keyedMapRead(keyedRead, e.getArguments().get(0).translateToLua(tr),
e.getArguments().get(1).translateToLua(tr));
}
+ if (LuaKeyedMap.writeStubName(tr.imTr, e.getFunc()) != null) {
+ // A store returns nothing, so it is always a statement of a flat program; see
+ // StmtTranslation#translateKeyedMapWrite. Its stub has no definition to call.
+ throw new CompileError(e.attrTrace().attrSource(),
+ "Lua backend: " + imFuncName + " used as an expression; a keyed-map store must be a statement.");
+ }
if (isBackendIntrinsic(e.getFunc(), tr)) {
if (e.getArguments().size() != 2) {
throw new CompileError(e.attrTrace().attrSource(),
@@ -149,7 +162,7 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
if (e.getFunc() == tr.imTr.luaRawFloorDivIntFunc) {
return LuaAst.LuaExprBinary(left, LuaAst.LuaOpFloorDiv(), right);
}
- if (e.getFunc() == tr.imTr.luaRawFloorModIntFunc) {
+ if (e.getFunc() == tr.imTr.luaRawFloorModIntFunc || e.getFunc() == tr.imTr.luaRawFloorModRealFunc) {
return LuaAst.LuaExprBinary(left, LuaAst.LuaOpMod(), right);
}
if (e.getFunc() == tr.imTr.luaRawConcatFunc) {
@@ -172,9 +185,26 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) {
return LuaAst.LuaExprFunctionCall(f, tr.translateExprList(e.getArguments()));
}
+ private static boolean isObjectFromIndexName(String typeCastingFunction) {
+ return "objectFromIndex".equals(typeCastingFunction) || LUA_HANDLE_FROM_INDEX.contains(typeCastingFunction);
+ }
+
+ /**
+ * Whether a call of {@code fromIndex}, an old-generics conversion back to a handle, is printed as
+ * {@code __wurst_objectFromIndex}. That helper answers nil for nil, for 0 and for every number it
+ * never handed out, and indexes its table with the number it is given (Lua reads 3.0 as the key 3),
+ * so it needs no int normalisation in front of it: an old-generics slot holds nil or the index the
+ * matching toIndex gave.
+ */
+ public static boolean printsAsObjectFromIndex(FuncDef fromIndex) {
+ return fromIndex.attrNearestPackage() instanceof WPackage p && "TypeCasting".equals(p.getName())
+ && isObjectFromIndexName(fromIndex.getName());
+ }
+
/**
- * Compiler-synthesised natives which the backend prints as an operator instead of a call.
- * Recognised by node identity: an ordinary function of the same name keeps its definition.
+ * Compiler-synthesised natives which the backend prints as an operator or a table store instead
+ * of a call, so they have no definition. Recognised by node identity: an ordinary function of the
+ * same name keeps its definition.
*/
static boolean isBackendIntrinsic(ImFunction function, LuaTranslator tr) {
return unaryIntrinsicName(function, tr) != null
@@ -182,9 +212,11 @@ static boolean isBackendIntrinsic(ImFunction function, LuaTranslator tr) {
|| function == tr.imTr.luaRawFmodIntFunc
|| function == tr.imTr.luaRawFmodRealFunc
|| function == tr.imTr.luaRawFloorModIntFunc
+ || function == tr.imTr.luaRawFloorModRealFunc
|| function == tr.imTr.luaRawConcatFunc
|| function == tr.imTr.luaRawOrEmptyFunc
- || LuaKeyedMap.readStubName(tr.imTr, function) != null;
+ || LuaKeyedMap.readStubName(tr.imTr, function) != null
+ || LuaKeyedMap.writeStubName(tr.imTr, function) != null;
}
/**
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 4f4b9622f..b5c9f046d 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
@@ -309,16 +309,10 @@ public class LuaNatives {
// KeyedMap: one native table per map, keyed by the element itself. Typed reads answer the
// Wurst default for a missing key so the caller needs no nil normalisation. Lowered by
// LuaKeyedMap/LuaNativeLowering before the inliner runs, like KeyedTable. No iteration.
+ // Puts and removes have no body: they are printed as the store where they are called
+ // (StmtTranslation#translateKeyedMapWrite), as the reads are (ExprTranslation#keyedMapRead).
addNative("__wurst_keyedMapCreate", f ->
f.getBody().add(LuaAst.LuaLiteral("return {}")));
- addNative("__wurst_keyedMapPut", f -> {
- f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()));
- f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr()));
- f.getParams().add(LuaAst.LuaVariable("v", LuaAst.LuaNoExpr()));
- // Writing under a nil key is an error in Lua, where reading one is only nil: a null
- // element stores nothing and reads as absent.
- f.getBody().add(LuaAst.LuaLiteral("if k ~= nil then t[k] = v end"));
- });
addNative("__wurst_keyedMapGet", f -> {
f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()));
f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr()));
@@ -362,11 +356,6 @@ public class LuaNatives {
// as it is behind a trigger.
f.getBody().add(LuaAst.LuaLiteral("local i = 1 local c = t[1] while c do c() i = i + 1 c = t[i] end"));
});
- addNative("__wurst_keyedMapRemove", f -> {
- f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()));
- f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr()));
- f.getBody().add(LuaAst.LuaLiteral("if k ~= nil then t[k] = nil end"));
- });
addNative("__wurst_keyedMapDestroy", f -> {
f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr()));
// The owner may still be reachable through an alias after destroy, so clear this table
diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java
index 01c4ba272..f6031fdfd 100644
--- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java
+++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaPolyfillSetup.java
@@ -94,10 +94,14 @@ static void createObjectIndexFunctions(LuaTranslator tr) {
}
}
- /** The sentinel an old-generics int value 0 is stored as (see ExprTranslation.translate(ImCast)). */
+ /**
+ * The sentinel an old-generics int value 0 is stored as (see ExprTranslation.translate(ImCast)),
+ * read by every such cast, so a main-chunk local. It stays {@code math.mininteger} rather than a
+ * literal: the integer width of the game's Lua need not be the test Lua's 64 bits.
+ */
static LuaVariable createOldGenericsZero(LuaTranslator tr) {
LuaVariable zero = LuaAst.LuaVariable("__wurst_oldGenericsZero", LuaAst.LuaLiteral("math.mininteger"));
- tr.luaModel.add(zero);
+ tr.declareChunkLocal(zero);
return zero;
}
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 741a324ba..9725279b4 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
@@ -8,6 +8,7 @@
import de.peeeq.wurstscript.translation.imtranslation.GetAForB;
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.GenericTypes;
import de.peeeq.wurstscript.translation.imtranslation.LuaDispatchPreparation;
import de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering;
@@ -383,6 +384,7 @@ protected String uniqueName(String rawName) {
public LuaCompilationUnit translate() {
collectPredefinedNames();
assertNoDanglingFunctionReferences(prog);
+ fieldDefaultsOnAllocation = LuaFieldDefaults.moveSurvivorsToAllocations(prog, imTr);
normalizeFieldNames();
@@ -454,18 +456,23 @@ LuaFunction callbackAdapterFor(ImFunction target) {
}
LuaFunction targetLua = luaFunc.getFor(target);
- LuaVariable dots = LuaAst.LuaVariable("...", LuaAst.LuaNoExpr());
LuaFunction adapter = LuaAst.LuaFunction(
uniqueName("__wurst_callback_" + targetLua.getName()),
- LuaAst.LuaParams(dots), LuaAst.LuaStatements());
+ LuaAst.LuaParams(), LuaAst.LuaStatements());
callbackAdapters.put(target, adapter);
LuaFunction errorHandler = callbackErrorHandler();
- LuaExprFunctionCallByName xpcall = LuaAst.LuaExprFunctionCallByName("xpcall",
- LuaAst.LuaExprlist(
- LuaAst.LuaExprFuncRef(targetLua),
- LuaAst.LuaExprFuncRef(errorHandler),
- LuaAst.LuaExprVarAccess(dots.copy())));
+ LuaExprlist xpcallArgs = LuaAst.LuaExprlist(
+ LuaAst.LuaExprFuncRef(targetLua),
+ LuaAst.LuaExprFuncRef(errorHandler));
+ // A Wurst code value takes no parameters, so whatever the caller passes is dropped by the
+ // target anyway: such an adapter takes no varargs. Only a target with parameters needs them.
+ if (!targetLua.getParams().isEmpty()) {
+ LuaVariable dots = LuaAst.LuaVariable("...", LuaAst.LuaNoExpr());
+ adapter.getParams().add(dots);
+ xpcallArgs.add(LuaAst.LuaExprVarAccess(dots));
+ }
+ LuaExprFunctionCallByName xpcall = LuaAst.LuaExprFunctionCallByName("xpcall", xpcallArgs);
if (target.getReturnType() instanceof ImVoid) {
adapter.getBody().add(xpcall);
} else {
@@ -748,16 +755,35 @@ private void createInstanceOfFunction() {
LuaPolyfillSetup.createInstanceOfFunction(this);
}
+ /** For each class, the fields whose default its allocation writes ({@link LuaFieldDefaults}). */
+ private Map> fieldDefaultsOnAllocation = Collections.emptyMap();
+
LuaVariable fieldStorage(ImVar field) {
return luaFieldStorage.getFor(imTr.canonical(field));
}
+ /** How many main-chunk locals lead the model; see {@link #declareChunkLocal}. */
+ private int chunkLocalCount;
+
+ /**
+ * Declares a local of the main chunk at the top of the script, before every function, so that
+ * each function reaches it as an upvalue instead of looking it up among the thousands of globals.
+ * Only the runtime state read on hot paths belongs here: the main chunk has room for 200 locals
+ * and a function for 255 upvalues. Declared now: the four object-allocator variables, the
+ * deallocator and, when used, the old-generics zero sentinel.
+ */
+ void declareChunkLocal(LuaDefinition definition) {
+ luaModel.add(chunkLocalCount++, LuaAst.LuaChunkLocal(definition));
+ }
+
private void createObjectManagement() {
- luaModel.add(objectClass);
- localizableStorageTables.add(objectClass);
- luaModel.add(objectFree);
- luaModel.add(objectMax);
- luaModel.add(objectFreeCount);
+ // Every allocation and destroy reads these. The descriptor map is not aliased into function
+ // locals like the field stores (localizeHotStorageTables): Lua indexes an upvalue table in
+ // one instruction (GETTABUP), as it does a local one, so an alias would only add a copy.
+ declareChunkLocal(objectClass);
+ declareChunkLocal(objectFree);
+ declareChunkLocal(objectMax);
+ declareChunkLocal(objectFreeCount);
LuaVariable object = LuaAst.LuaVariable("object", LuaAst.LuaNoExpr());
objectDealloc.getParams().add(object);
@@ -781,7 +807,8 @@ private void createObjectManagement() {
LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectFree),
LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(objectFreeCount))),
LuaAst.LuaExprVarAccess(object)));
- luaModel.add(objectDealloc);
+ // Called by every destroy, so a local function like the state it pushes onto.
+ declareChunkLocal(objectDealloc);
LuaVariable toIndexObject = LuaAst.LuaVariable("object", LuaAst.LuaNoExpr());
classToIndex.getParams().add(toIndexObject);
@@ -1343,14 +1370,13 @@ private void createClassInitFunction(ImClass c, LuaVariable classVar, LuaMethod
LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(objectMax),
LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(objectMax), LuaAst.LuaOpPlus(), LuaAst.LuaExprIntVal("1"))),
LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(newInst), LuaAst.LuaExprVarAccess(objectMax)));
+ // The popped slot is not cleared: a slot above the count is never read before a destroy
+ // writes it again, and it holds an integer id, so it retains nothing. The free stack stays
+ // as large as its peak.
LuaStatements recycled = LuaAst.LuaStatements(
LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(newInst),
LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectFree),
LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(objectFreeCount)))),
- LuaAst.LuaAssignment(
- LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectFree),
- LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(objectFreeCount))),
- LuaAst.LuaExprNull()),
LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(objectFreeCount),
LuaAst.LuaExprBinary(LuaAst.LuaExprVarAccess(objectFreeCount), LuaAst.LuaOpMinus(), LuaAst.LuaExprIntVal("1"))));
body.add(LuaAst.LuaIf(
@@ -1360,38 +1386,18 @@ private void createClassInitFunction(ImClass c, LuaVariable classVar, LuaMethod
LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(objectClass),
LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(newInst))),
LuaAst.LuaExprVarAccess(classVar)));
- for (ImVar field : collectFieldsForAllocation(c)) {
- body.add(LuaAst.LuaAssignment(
- LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(fieldStorage(field)),
- LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(newInst))),
- defaultValue(field.getType())));
- }
- body.add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(newInst)));
- }
-
- private List collectFieldsForAllocation(ImClass c) {
- List result = new ArrayList<>();
- Set visitedClasses = Collections.newSetFromMap(new IdentityHashMap<>());
- Set visitedFields = Collections.newSetFromMap(new IdentityHashMap<>());
- collectFieldsForAllocation(c, result, visitedClasses, visitedFields);
- return result;
- }
-
- private void collectFieldsForAllocation(ImClass c, List out,
- Set visitedClasses, Set visitedFields) {
- if (!visitedClasses.add(c)) {
- return;
- }
- List superClasses = new ArrayList<>(c.getSuperClasses());
- superClasses.sort(Comparator.comparing(sc -> classSortKey(sc.getClassDef())));
- for (ImClassType sc : superClasses) {
- collectFieldsForAllocation(sc.getClassDef(), out, visitedClasses, visitedFields);
- }
- for (ImVar field : c.getFields()) {
- if (visitedFields.add(imTr.canonical(field))) {
- out.add(field);
+ // The defaults the optimiser did not remove at some allocation of this class (LuaFieldDefaults), and the
+ // table an array field needs per object, which only the backend can make.
+ Set survivingDefaults = fieldDefaultsOnAllocation.getOrDefault(c, Collections.emptySet());
+ for (ImVar field : LuaFieldDefaults.fieldsOf(c, imTr)) {
+ if (LuaFieldDefaults.isArrayField(field) || survivingDefaults.contains(imTr.canonical(field))) {
+ body.add(LuaAst.LuaAssignment(
+ LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(fieldStorage(field)),
+ LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(newInst))),
+ defaultValue(field.getType())));
}
}
+ body.add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(newInst)));
}
private void initClassTables(ImClass c) {
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 2aefb571c..f0cde2434 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
@@ -1,8 +1,13 @@
package de.peeeq.wurstscript.translation.lua.translation;
+import de.peeeq.wurstscript.attributes.CompileError;
import de.peeeq.wurstscript.jassIm.*;
import de.peeeq.wurstscript.luaAst.*;
+import de.peeeq.wurstscript.translation.imtranslation.LuaKeyedMap;
+import de.peeeq.wurstscript.translation.imtranslation.LuaTraps;
+import de.peeeq.wurstscript.translation.lua.printing.LuaPrinter;
+import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -18,11 +23,123 @@ public static void translate(ImExpr e, List res, LuaTranslator tr)
emitLuaInitXpcall(call.getFunc(), res, tr);
return;
}
+ String keyedWrite = LuaKeyedMap.writeStubName(tr.imTr, call.getFunc());
+ if (keyedWrite != null) {
+ translateKeyedMapWrite(call, keyedWrite, 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 {
+ /** 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. */
+ READ,
+ /** Anything else, such as a call. */
+ EFFECT
+ }
+
+ private 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 (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.
+ return WriteOperand.EFFECT;
+ }
+ if (e instanceof ImVarAccess access) {
+ return access.getVar().isGlobal() ? WriteOperand.READ : WriteOperand.STABLE;
+ }
+ if (e instanceof ImVarArrayAccess access && readsOnly(access.getIndexes())) {
+ return WriteOperand.READ;
+ }
+ if (e instanceof ImMemberAccess access && writeOperand(access.getReceiver()) != WriteOperand.EFFECT
+ && readsOnly(access.getIndexes())) {
+ return WriteOperand.READ;
+ }
+ return WriteOperand.EFFECT;
+ }
+
+ private static boolean readsOnly(ImExprs exprs) {
+ for (ImExpr e : exprs) {
+ if (writeOperand(e) == WriteOperand.EFFECT) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A keyed-map put or remove as the table store it stands for, {@code t[k] = v} or
+ * {@code t[k] = nil}, instead of a call to a stub with that body. Storing under a nil key is an
+ * error in Lua, where reading one only answers nil, so a key which may be nil is tested first: a
+ * null element stores nothing and reads as absent. Any key but a literal may be nil, an int too
+ * when it is a field read through an object which is null, so only a literal is stored without
+ * the test.
+ *
+ * Each operand is evaluated once and in the order of the call. A plain store does that by
+ * itself. The test reads the key twice and evaluates the table and the value only when the key
+ * is not nil, so there an operand with an effect, and each operand before it which is not
+ * {@link WriteOperand#STABLE}, is first evaluated into a local.
+ */
+ private static void translateKeyedMapWrite(ImFunctionCall call, String stub, List res,
+ LuaTranslator tr) {
+ boolean put = LuaKeyedMap.NATIVE_PUT.equals(stub);
+ ImExprs args = call.getArguments();
+ int arity = put ? 3 : 2;
+ if (args.size() != arity) {
+ throw new CompileError(call.attrTrace().attrSource(),
+ "Lua backend: " + stub + " expects " + arity + " arguments, got " + args.size() + ".");
+ }
+ ImExpr key = args.get(1);
+ // Only a literal: a key of any type can hold nil, read through an object which is null (LuaTraps).
+ boolean keyNeverNil = key instanceof ImIntVal || key instanceof ImRealVal || key instanceof ImStringVal
+ || key instanceof ImBoolVal;
+ List operands = new ArrayList<>();
+ for (ImExpr arg : args) {
+ operands.add(arg.translateToLua(tr));
+ }
+ boolean[] intoLocal = new boolean[arity];
+ if (!keyNeverNil) {
+ int lastEffect = -1;
+ for (int i = 0; i < arity; i++) {
+ if (writeOperand(args.get(i)) == WriteOperand.EFFECT) {
+ lastEffect = i;
+ }
+ }
+ for (int i = 0; i <= lastEffect; i++) {
+ intoLocal[i] = writeOperand(args.get(i)) != WriteOperand.STABLE;
+ }
+ }
+ // A statement must not start with '(' (Lua would join it onto the previous line as a call).
+ intoLocal[0] |= !LuaPrinter.startsWithName(operands.get(0));
+ String[] names = {"__wurst_map", "__wurst_key", "__wurst_value"};
+ for (int i = 0; i < arity; i++) {
+ if (intoLocal[i]) {
+ LuaVariable local = LuaAst.LuaVariable(tr.uniqueName(names[i]), operands.get(i));
+ res.add(local);
+ operands.set(i, LuaAst.LuaExprVarAccess(local));
+ }
+ }
+ LuaExpr value = put ? operands.get(2) : LuaAst.LuaExprNull();
+ LuaExpr keyExpr = operands.get(1);
+ LuaAssignment store = LuaAst.LuaAssignment(
+ LuaAst.LuaExprArrayAccess(operands.get(0), LuaAst.LuaExprlist(keyNeverNil ? keyExpr : keyExpr.copy())),
+ value);
+ if (keyNeverNil) {
+ res.add(store);
+ } else {
+ res.add(LuaAst.LuaIf(LuaAst.LuaExprBinary(keyExpr, LuaAst.LuaOpUnequals(), LuaAst.LuaExprNull()),
+ LuaAst.LuaStatements(store), LuaAst.LuaStatements()));
+ }
+ }
+
private static void emitLuaInitXpcall(ImFunction initFunc, List res, LuaTranslator tr) {
String funcName = tr.luaFunc.getFor(initFunc).getName();
String packageName = tr.imTr.luaInitFunctions.getOrDefault(initFunc, "?");
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 ec02e8aa7..a57f6ae52 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
@@ -5805,7 +5805,9 @@ public void optimizedMovedImHelpersHaveNoDanglingReferences() {
assertTrue("repro must exercise string concat lowering", compiled.contains(" .. "));
assertFalse("the raw concat primitive is an operator, not a call", compiled.contains("__wurst_rawConcat"));
assertTrue("repro must exercise integer div lowering", compiled.contains(" // "));
- assertTrue("repro must exercise integer and real mod lowering", compiled.contains("math.fmod("));
+ // the divisors are literals once inlined, so both lower to Lua's % (see modByADivisorThatBecomesALiteral...)
+ assertTrue("repro must exercise integer and real mod lowering:\n" + compiled,
+ compiled.contains(" % 2)") && compiled.contains(" % 2.)"));
assertFalse("raw numeric primitive calls must not survive Lua emission", compiled.contains("__wurst_rawF"));
}
@@ -5900,6 +5902,173 @@ public void classInstancesAllocateIdsWithoutInstanceTables() throws IOException
assertFalse("create must not attach an instance metatable", compiled.contains("setmetatable(new_inst"));
}
+ /** The globals a test's script reads or writes, as {@code luac -l} lists them: an access through _ENV. */
+ private java.util.Set globalsAccessedBy(String testName) throws IOException, InterruptedException {
+ File luaFile = new File("test-output/lua/LuaBackendAuditTests_" + testName + ".lua");
+ Process luac = new ProcessBuilder(getLuacExecutable(), "-l", "-p", luaFile.getPath())
+ .redirectErrorStream(true).start();
+ String listing = new String(luac.getInputStream().readAllBytes(), Charsets.UTF_8);
+ assertTrue("luac did not finish", luac.waitFor(60, java.util.concurrent.TimeUnit.SECONDS));
+ assertEquals(listing, 0, luac.exitValue());
+ java.util.Set globals = new java.util.TreeSet<>();
+ java.util.regex.Matcher access = java.util.regex.Pattern.compile("; _ENV \"(\\w+)\"").matcher(listing);
+ while (access.find()) {
+ globals.add(access.group(1));
+ }
+ // The entry point is a global, so a listing which names none has changed its format.
+ assertTrue("the listing names no global:\n" + listing, globals.contains("main"));
+ return globals;
+ }
+
+ /**
+ * Every allocation and destroy reads the allocator's state and every destroy calls the
+ * deallocator, so they are locals of the main chunk, declared before any function: a function
+ * reaches them as upvalues, not by a lookup among the thousands of globals of a map.
+ */
+ @Test
+ public void objectRuntimeStateIsMainChunkLocal() throws IOException, InterruptedException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "abstract class Shape",
+ " abstract function area() returns int",
+ "class Square extends Shape",
+ " int side",
+ " construct(int side)",
+ " this.side = side",
+ " override function area() returns int",
+ " return side * side",
+ "init",
+ " Shape first = new Square(2)",
+ " let firstId = first castTo int",
+ " destroy first",
+ " Shape second = new Square(3)",
+ " if second castTo int == firstId and second.area() == 9 and second instanceof Square",
+ " and second.typeId == Square.typeId",
+ " testSuccess()");
+ String compiled = compiledLua("objectRuntimeStateIsMainChunkLocal");
+ assertTrue("the script opens with the allocator state and the deallocator as locals:\n" + compiled,
+ compiled.startsWith("local __wurst_objectClass = ({})\n"
+ + "local __wurst_objectFree = ({})\n"
+ + "local __wurst_objectMax = 0\n"
+ + "local __wurst_objectFreeCount = 0\n"
+ + "\n"
+ + "local function __wurst_deallocObject(object) \n"));
+ java.util.Set globals = globalsAccessedBy("objectRuntimeStateIsMainChunkLocal");
+ for (String local : List.of("__wurst_objectClass", "__wurst_objectFree", "__wurst_objectMax",
+ "__wurst_objectFreeCount", "__wurst_deallocObject")) {
+ assertFalse(local + " is reached as a global: " + globals, globals.contains(local));
+ }
+ }
+
+ /**
+ * Allocation pops a recycled id without clearing its slot: a slot above the count is never read
+ * before a destroy writes it again, and it holds an integer, so clearing it retained nothing.
+ */
+ @Test
+ public void recycledIdIsPoppedWithoutClearingItsSlot() throws IOException {
+ test().testLua(true).luaOnly(false).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "class Node",
+ "init",
+ " let a = new Node()",
+ " let b = new Node()",
+ " let c = new Node()",
+ " let ia = a castTo int",
+ " let ib = b castTo int",
+ " let ic = c castTo int",
+ " destroy a",
+ " destroy b",
+ " destroy c",
+ " let x = new Node() castTo int",
+ " let y = new Node() castTo int",
+ " let z = new Node() castTo int",
+ // The free stack is empty again and still holds the three ids above its count.
+ " let fresh = new Node()",
+ " let freshId = fresh castTo int",
+ " destroy fresh",
+ " let again = new Node() castTo int",
+ " let following = new Node() castTo int",
+ " if x == ic and y == ib and z == ia and freshId == ic + 1 and again == freshId",
+ " and following == freshId + 1",
+ " testSuccess()");
+ String create = topLevelFunctionBodyWithPrefix(
+ compiledLua("recycledIdIsPoppedWithoutClearingItsSlot"), "Node:create");
+ assertTrue("the id comes off the free stack:\n" + create,
+ create.contains("new_inst = __wurst_objectFree[__wurst_objectFreeCount]"));
+ assertFalse("the popped slot is not cleared:\n" + create,
+ create.contains("__wurst_objectFree[__wurst_objectFreeCount] = nil"));
+ }
+
+ /**
+ * Every old-generics cast reads the zero sentinel, so it is a main-chunk local too. It stays
+ * {@code math.mininteger}, never a literal: the game's integers need not have the test Lua's 64 bits.
+ */
+ @Test
+ public void oldGenericsZeroSentinelIsMainChunkLocal() throws IOException, InterruptedException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "int array slots",
+ "class Store",
+ " function put(int key, T value)",
+ " slots[key] = value castTo int",
+ " function get(int key) returns T",
+ " return slots[key] castTo T",
+ "init",
+ " let ints = new Store()",
+ " ints.put(0, 0)",
+ " ints.put(1, 7)",
+ " if ints.get(0) == 0 and ints.get(1) == 7",
+ " testSuccess()");
+ String compiled = compiledLua("oldGenericsZeroSentinelIsMainChunkLocal");
+ String declaration = "\nlocal __wurst_oldGenericsZero = math.mininteger\n";
+ int at = compiled.indexOf(declaration);
+ assertTrue("the sentinel is declared as a local:\n" + compiled, at >= 0);
+ assertTrue("the sentinel is declared before the first function which is not a local:\n" + compiled,
+ at < compiled.indexOf("\nfunction "));
+ assertEquals("the sentinel is assigned once:\n" + compiled,
+ 1, compiled.split("__wurst_oldGenericsZero = ", -1).length - 1);
+ assertTrue("the casts read the sentinel:\n" + compiled,
+ compiled.indexOf("__wurst_oldGenericsZero", at + declaration.length()) >= 0);
+ assertFalse("the sentinel is reached as a global",
+ globalsAccessedBy("oldGenericsZeroSentinelIsMainChunkLocal").contains("__wurst_oldGenericsZero"));
+ }
+
+ /**
+ * A Wurst code value takes no parameters, so the callback adapter of one takes no varargs: the
+ * target would drop anything it forwarded.
+ */
+ @Test
+ public void callbackAdapterOfANullaryTargetTakesNoVarargs() throws IOException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "@extern native pcall(code callback) returns bool",
+ "int calls = 0",
+ "function tick()",
+ " calls++",
+ "function tickAndAccept() returns boolean",
+ " calls++",
+ " return true",
+ "init",
+ " if pcall(function tick) and pcall(function tickAndAccept) and calls == 2",
+ " testSuccess()");
+ String compiled = compiledLua("callbackAdapterOfANullaryTargetTakesNoVarargs");
+ assertTrue("the adapter calls its target with nothing to forward:\n" + compiled,
+ java.util.regex.Pattern.compile(
+ "function __wurst_callback_tick\\w*\\(\\) \\n\\txpcall\\(tick\\w*, __wurst_callback_error\\w*\\)\\n")
+ .matcher(compiled).find());
+ assertTrue("a returning adapter keeps its single result:\n" + compiled,
+ java.util.regex.Pattern.compile(
+ "function __wurst_callback_tickAndAccept\\w*\\(\\) \\n(?:\\t.*\\n)*?"
+ + "\\t_, result = xpcall\\(tickAndAccept\\w*, __wurst_callback_error\\w*\\)\\n")
+ .matcher(compiled).find());
+ assertFalse("no adapter takes varargs:\n" + compiled,
+ java.util.regex.Pattern.compile("function __wurst_callback_\\w+\\(\\.\\.\\.\\)").matcher(compiled).find());
+ }
+
/**
* Reading a never-written array slot must not permanently store an entry
* for it - merely probing a sparse array would otherwise grow it
@@ -6232,12 +6401,13 @@ public void virtualCallsReadTheSlotAtTheCallSite() {
" shapes[1] = new Square()",
" consume(total(2))");
String total = topLevelFunctionBodyWithPrefix(compiled, "total");
- java.util.regex.Matcher alias = java.util.regex.Pattern
- .compile("local (\\w+) = __wurst_objectClass\\R").matcher(total);
- assertTrue("the descriptor table is aliased for the loop:\n" + total, alias.find());
+ // An upvalue table is indexed in one instruction, as a local one is, so an alias would only
+ // add a copy.
+ assertFalse("the descriptor table is an upvalue and needs no alias for the loop:\n" + total,
+ total.contains("= __wurst_objectClass\n"));
assertTrue("the slot is read from the receiver's descriptor at the call site:\n" + total,
- java.util.regex.Pattern.compile(java.util.regex.Pattern.quote(alias.group(1))
- + "\\[\\w+\\[i\\]\\]\\.\\w*area\\w*\\(").matcher(total).find());
+ java.util.regex.Pattern.compile("__wurst_objectClass\\[\\w+\\[i\\]\\]\\.\\w*area\\w*\\(")
+ .matcher(total).find());
assertFalse("no dispatch stub is needed for a table-read receiver:\n" + compiled,
compiled.contains("dispatch_"));
}
@@ -7153,4 +7323,331 @@ public void closureMethodsWithUnderscoresKeepTheirSemanticNames() throws IOExcep
luaFunctionsWithPrefix(lua, "Under_pass"));
assertFalse(lua, lua.contains("passUnder_passUnder"));
}
+
+ /**
+ * A new object's field defaults are IM writes after its allocation, so where the constructor sets a field the
+ * default is gone: the allocation writes no field, and each field is written once where the object is made. (A
+ * value computed between them, which may raise on nil, would keep the defaults after it: OptimizerTests.)
+ */
+ @Test
+ public void aConstructedFieldIsWrittenOnceNotFirstWithItsDefault() throws IOException {
+ test().testLua(true).inline().localOptimizations().executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "class Hit",
+ " int amount",
+ " real factor",
+ " boolean crit",
+ " string label",
+ " Hit next",
+ " construct(int amount, real factor, boolean crit)",
+ " this.amount = amount",
+ " this.factor = factor",
+ " this.crit = crit",
+ " this.label = \"hit\"",
+ " this.next = null",
+ "@noinline function make(int amount) returns Hit",
+ " return new Hit(amount, 1.5, amount > 10)",
+ "init",
+ " let h = make(12)",
+ " if h.amount == 12 and h.factor == 1.5 and h.crit and h.label == \"hit\" and h.next == null",
+ " testSuccess()");
+ String compiled = compiledLua("aConstructedFieldIsWrittenOnceNotFirstWithItsDefault");
+ java.util.regex.Matcher create = java.util.regex.Pattern
+ .compile("function Hit:create\\d*\\(\\) \\n(.*?)\\nend", java.util.regex.Pattern.DOTALL).matcher(compiled);
+ assertTrue("the allocation:\n" + compiled, create.find());
+ assertFalse("the allocation writes no field:\n" + create.group(1), create.group(1).contains("_storage["));
+ for (String field : new String[] {"amount", "factor", "crit", "label", "next"}) {
+ int writes = compiled.split("Hit_" + field + "_storage(_local\\d*)?\\[[^\\]]+\\] = ", -1).length - 1;
+ assertEquals("Hit." + field + " is written once:\n" + compiled, 1, writes);
+ }
+ }
+
+ /**
+ * A field nothing sets before reading it still starts at its default, also on an id which an earlier object
+ * had: here the constructor reads count first, and the earlier object had set every field.
+ */
+ @Test
+ public void aFieldReadBeforeItIsSetStartsAtItsDefaultOnAReusedId() {
+ String[] lines = {
+ "package Test",
+ "native testSuccess()",
+ "class A",
+ " int count",
+ " string name",
+ " A other",
+ " construct()",
+ " count += 1",
+ "init",
+ " let a = new A()",
+ " a.count = 41",
+ " a.name = \"x\"",
+ " a.other = a",
+ " destroy a",
+ " let b = new A()",
+ " if b.count == 1 and b.name == \"\" and b.other == null",
+ " testSuccess()"};
+ test().testLua(true).inline().localOptimizations().executeProg().lines(lines);
+ testNamed("aFieldReadBeforeItIsSetStartsAtItsDefaultOnAReusedId_unoptimised").testLua(true).executeProg()
+ .lines(lines);
+ }
+
+ /**
+ * A call between the allocation and the constructor's write can read the field, so the default before it stays:
+ * the observer sees 0 on an id whose earlier object had set the field to 7.
+ */
+ @Test
+ public void aDefaultACallCanReadBeforeTheConstructorSetsItStays() {
+ test().testLua(true).inline().localOptimizations().executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "int seen = -1",
+ "class A",
+ " int f",
+ " construct()",
+ " observe(this)",
+ " f = 5",
+ "@noinline function observe(A a)",
+ " seen = a.f",
+ "init",
+ " let a = new A()",
+ " a.f = 7",
+ " destroy a",
+ " let b = new A()",
+ " if seen == 0 and b.f == 5",
+ " testSuccess()");
+ }
+
+ /**
+ * A message joined with + is one flat Lua concatenation, a single CONCAT, where the nested text
+ * {@code (("a=" .. x) .. ", b=")} built every intermediate string.
+ */
+ @Test
+ public void stringConcatenationChainPrintsFlat() throws IOException {
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "native I2S(int i) returns string",
+ "function describe(int a, int b) returns string",
+ " return \"a=\" + I2S(a) + \", b=\" + I2S(b) + \".\"",
+ "init",
+ " if describe(1, -2) == \"a=1, b=-2.\"",
+ " testSuccess()");
+ String describe = topLevelFunctionBodyWithPrefix(compiledLua("stringConcatenationChainPrintsFlat"), "describe");
+ assertTrue(describe, describe.contains("(\"a=\" .. tostring(a) .. \", b=\" .. tostring(b) .. \".\")"));
+ assertFalse(describe, describe.contains("(("));
+ }
+
+ /**
+ * Printed nested, a concatenation of 300 parts was 300 parentheses deep, more than luac accepts
+ * ("too many C levels"). Printed flat in groups, it loads and runs.
+ */
+ @Test
+ public void aLongStringConcatenationLoadsAndRuns() throws IOException {
+ int parts = 300;
+ StringBuilder chain = new StringBuilder("\"<\"");
+ StringBuilder expected = new StringBuilder("<");
+ for (int i = 0; i < parts; i++) {
+ chain.append(" + I2S(x + ").append(i).append(")");
+ expected.append(1000 + i);
+ }
+ test().testLua(true).executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "native I2S(int i) returns string",
+ "function longMessage(int x) returns string",
+ " return " + chain,
+ "init",
+ " if longMessage(1000) == \"" + expected + "\"",
+ " testSuccess()");
+ String longMessage = topLevelFunctionBodyWithPrefix(compiledLua("aLongStringConcatenationLoadsAndRuns"),
+ "longMessage");
+ assertEquals(parts, countOccurrences(longMessage, " .. "));
+ }
+
+ /**
+ * A divisor which only becomes a literal once the helper is inlined (here a parameter, in a map
+ * often a constant) is Lua's %, with no fmod and no sign correction: the helper tests the divisor
+ * and the test folds. A real divisor below 1 keeps fmod, and so does a runtime divisor, whose
+ * test runs.
+ */
+ @Test
+ public void modByADivisorThatBecomesALiteralIsTheLuaOperator() {
+ String compiled = compileOptimizedLua("modByADivisorThatBecomesALiteralIsTheLuaOperator",
+ "package Test",
+ "native consume(int value)",
+ "native consumeReal(real value)",
+ "function wrap(int a, int m) returns int",
+ " return a mod m",
+ "function wrapReal(real a, real m) returns real",
+ " return a % m",
+ "@noinline function fixed(int x) returns int",
+ " return wrap(x, 1000003)",
+ "@noinline function fixedReal(real x) returns real",
+ " return wrapReal(x, 360.)",
+ "@noinline function belowOne(real x) returns real",
+ " return wrapReal(x, 0.5)",
+ "@noinline function varying(int x, int y) returns int",
+ " return x mod y",
+ "init",
+ " consume(fixed(5))",
+ " consumeReal(fixedReal(5.))",
+ " consumeReal(belowOne(5.))",
+ " consume(varying(5, 3))");
+ String fixed = topLevelFunctionBodyWithPrefix(compiled, "fixed(");
+ assertTrue(fixed, fixed.contains(" % 1000003)"));
+ assertFalse(fixed, fixed.contains("fmod") || fixed.contains("< 0"));
+ String fixedReal = topLevelFunctionBodyWithPrefix(compiled, "fixedReal");
+ assertTrue(fixedReal, fixedReal.contains(" % 360.)"));
+ assertFalse(fixedReal, fixedReal.contains("fmod") || fixedReal.contains("< 0"));
+ String belowOne = topLevelFunctionBodyWithPrefix(compiled, "belowOne");
+ assertTrue("a real divisor below 1 keeps fmod:\n" + belowOne, belowOne.contains("math.fmod("));
+ assertFalse(belowOne, belowOne.contains(" % "));
+ String varying = topLevelFunctionBodyWithPrefix(compiled, "varying");
+ assertTrue("a runtime divisor is tested:\n" + varying,
+ varying.contains(" % ") && varying.contains("math.fmod("));
+ }
+
+ private static final String[] MOD_SEMANTICS_PROG = {
+ "package Test",
+ "native testSuccess()",
+ "real zero = 0.",
+ "function im(int a, int b) returns int",
+ " return a mod b",
+ "function rm(real a, real b) returns real",
+ " return a % b",
+ "@noinline function v(int x) returns int",
+ " return x",
+ "@noinline function w(real x) returns real",
+ " return x",
+ "function near(real a, real b) returns boolean",
+ " return a - b < 0.0001 and b - a < 0.0001",
+ "init",
+ " let negZero = zero * -1.",
+ " boolean ok = true",
+ // runtime dividends, literal divisors
+ " ok = ok and im(v(7), 3) == 1 and im(v(-7), 3) == 2 and im(v(0), 3) == 0",
+ " ok = ok and im(v(-3), 3) == 0 and im(v(-1), 8) == 7 and im(v(-2147483647), 2) == 1",
+ // runtime divisors, both signs
+ " ok = ok and im(v(7), v(3)) == 1 and im(v(-7), v(3)) == 2 and im(v(7), v(-3)) == 1",
+ " ok = ok and im(v(-7), v(-3)) == -4 and im(v(0), v(-5)) == 0",
+ // reals: a divisor of at least 1, below 1, negative, at runtime
+ " ok = ok and near(rm(w(7.5), 2.), 1.5) and near(rm(w(-7.5), 2.), 0.5) and near(rm(w(0.), 360.), 0.)",
+ " ok = ok and near(rm(w(-370.), 360.), 350.) and near(rm(w(-0.75), 0.5), 0.25)",
+ " ok = ok and near(rm(w(7.5), -2.), 1.5) and near(rm(w(-7.5), -2.), -3.5)",
+ " ok = ok and near(rm(w(-7.5), w(2.)), 0.5) and near(rm(w(-7.5), w(0.5)), 0.)",
+ // -0.0 is a zero, not the divisor
+ " ok = ok and rm(negZero, 360.) == 0. and rm(negZero, 360.) < 1. and rm(negZero, w(360.)) < 1.",
+ " if ok",
+ " testSuccess()"
+ };
+
+ /**
+ * Wurst mod is Blizzard.j's ModuloInteger/ModuloReal: the truncated remainder, plus the divisor
+ * when it is negative. Lua's % agrees for an int divisor above 0 and a real one of at least 1,
+ * which the helper tests at run time and the optimiser folds for a literal. Against Jass and the
+ * interpreter, once with the helper called and once inlined and folded.
+ */
+ @Test
+ public void modAgreesWithJassForEveryDivisorSign() {
+ test().testLua(true).luaOnly(false).executeProg().lines(MOD_SEMANTICS_PROG);
+ }
+
+ @Test
+ public void modAgreesWithJassForEveryDivisorSignWhenFolded() {
+ test().testLua(true).luaOnly(false).inline().localOptimizations().executeProg().lines(MOD_SEMANTICS_PROG);
+ }
+
+ /**
+ * The {@code or ""} guard of a concatenation operand is placed before inlining, where
+ * {@code x.str()} is a call that might answer nil. Inlined it is the {@code tostring} of I2S, and
+ * a parameter may become a literal; neither can be nil, so neither keeps the guard. A variable
+ * which may be nil keeps it.
+ */
+ @Test
+ public void concatenationOperandsWhichCannotBeNilLoseTheirGuardOnceInlined() throws IOException {
+ test().testLua(true).inline().localOptimizations().executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "native I2S(int i) returns string",
+ "function int.str() returns string",
+ " return I2S(this)",
+ "function greet(string name) returns string",
+ " return \"hi \" + name",
+ "@noinline function label(int x) returns string",
+ " return \"x=\" + x.str() + \"!\"",
+ "@noinline function fixedGreeting() returns string",
+ " return greet(\"bob\")",
+ "@noinline function greetAnyone(string name) returns string",
+ " return \"hi \" + name",
+ "init",
+ " if label(-3) == \"x=-3!\" and fixedGreeting() == \"hi bob\" and greetAnyone(\"al\") == \"hi al\"",
+ " testSuccess()");
+ String compiled = compiledLua("concatenationOperandsWhichCannotBeNilLoseTheirGuardOnceInlined");
+ String label = topLevelFunctionBodyWithPrefix(compiled, "label");
+ assertTrue(label, label.contains("(\"x=\" .. tostring(x) .. \"!\")"));
+ String fixedGreeting = topLevelFunctionBodyWithPrefix(compiled, "fixedGreeting");
+ assertTrue(fixedGreeting, fixedGreeting.contains("(\"hi \" .. \"bob\")"));
+ String greetAnyone = topLevelFunctionBodyWithPrefix(compiled, "greetAnyone");
+ assertTrue("a variable keeps its guard:\n" + greetAnyone, greetAnyone.contains("(name or \"\")"));
+ }
+
+ /**
+ * An old-generics value read back as a handle goes through the TypeCasting fromIndex, which Lua
+ * prints as __wurst_objectFromIndex. That maps nil, 0 and an unknown index to nil and indexes its
+ * table with the integer it is given, so an int normalisation in front of it (tonumber,
+ * math.tointeger) answers the same and is not emitted. Jass has no such normalisation.
+ */
+ @Test
+ public void oldGenericHandleReadsNeedNoIntNormalisation() throws IOException {
+ test().withStdLib().testLua(true).executeProg().lines(
+ "package Test",
+ "class Box",
+ " T elem",
+ " construct(T e)",
+ " elem = e",
+ " function get() returns T",
+ " return elem",
+ "class Unset",
+ " T elem",
+ "init",
+ " let t = CreateTimer()",
+ " let other = CreateTimer()",
+ " let full = new Box(t)",
+ " let empty = new Box(null)",
+ " let unset = new Unset()",
+ " if full.get() != t",
+ " testFail(\"the handle did not come back\")",
+ " if full.get() == other",
+ " testFail(\"another handle came back\")",
+ " if empty.get() != null",
+ " testFail(\"null (index 0) did not come back\")",
+ " if unset.elem != null",
+ " testFail(\"an unset slot (nil) did not read as null\")",
+ " testSuccess()");
+ String init = topLevelFunctionBodyWithPrefix(compiledLua("oldGenericHandleReadsNeedNoIntNormalisation"),
+ "init_Test");
+ assertTrue(init, init.contains("__wurst_objectFromIndex(") || init.contains("timerFromIndex("));
+ assertFalse(init, init.contains("math.tointeger") || init.contains("tonumber") || init.contains("__wurst_ensureInt"));
+ }
+
+ /** -0.0 stays -0.0 under Lua's % as under fmod and its correction: 1 / r is minus infinity. */
+ @Test
+ public void modKeepsTheSignOfANegativeZeroOnLua() throws IOException {
+ test().testLua(true).inline().localOptimizations().executeProg().lines(
+ "package Test",
+ "native testSuccess()",
+ "real zero = 0.",
+ "function rm(real a, real b) returns real",
+ " return a % b",
+ "@noinline function w(real x) returns real",
+ " return x",
+ "init",
+ " let negZero = zero * -1.",
+ " if 1. / rm(negZero, 360.) < 0. and 1. / rm(negZero, w(360.)) < 0. and 1. / rm(negZero, w(0.5)) < 0.",
+ " testSuccess()");
+ String compiled = topLevelFunctionBodyWithPrefix(compiledLua("modKeepsTheSignOfANegativeZeroOnLua"),
+ "init_Test");
+ assertTrue(compiled, compiled.contains(" % 360.)"));
+ }
}
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 22779dd60..f1a0668de 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
@@ -183,10 +183,13 @@ public void keyedMapLowersToSingleLuaIndexes() throws IOException {
String compiled = compiled("keyedMapLowersToSingleLuaIndexes");
assertTrue("create allocates a bare table", getFunctionBody(compiled, "__wurst_keyedMapCreate").contains("return {}"));
- assertTrue("put is one store", getFunctionBody(compiled, "__wurst_keyedMapPut").contains("t[k] = v"));
- assertTrue("remove is one store", getFunctionBody(compiled, "__wurst_keyedMapRemove").contains("t[k] = nil"));
String init = getFunctionBody(compiled, "init_Test");
+ // The writes are printed as the store they stand for; a literal key is never nil.
+ assertTrue("put is one store: " + init, init.contains("\tm[7] = 70\n"));
+ assertTrue("remove is one store: " + init, init.endsWith("\tm[7] = nil"));
+ assertFalse("no write stub is called or defined:\n" + compiled,
+ compiled.contains("__wurst_keyedMapPut") || compiled.contains("__wurst_keyedMapRemove"));
// The reads are printed as the index they stand for, so there is no stub to call or define.
assertTrue("an int read is one index that answers 0 for a missing key: " + init,
init.contains("] or 0)"));
@@ -239,8 +242,8 @@ public void keyedMapStaysNativeWithStackTraces() throws IOException {
String compiled = compiled("keyedMapStaysNativeWithStackTraces");
String init = getFunctionBody(compiled, "init_Test");
- assertTrue("the caller must read the table itself and call the put stub directly: " + init,
- init.contains("] or 0)") && init.contains("__wurst_keyedMapPut"));
+ 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"));
}
@@ -337,9 +340,9 @@ public void keyedMapGenericKeyAndValueReachLuaUncast() throws IOException {
"endpackage");
String compiled = compiled("keyedMapGenericKeyAndValueReachLuaUncast");
- assertTrue("generic operations lower to the keyed-map put stub and the table read",
- compiled.contains("__wurst_keyedMapPut") && compiled.contains("] or 0)"));
String init = getFunctionBody(compiled, "init_Test");
+ assertTrue("generic operations lower to the table store and the table read: " + init,
+ init.contains("[u] = 5") && init.contains("] or 0)") && !compiled.contains("__wurst_keyedMapPut"));
assertFalse("the unit must be handed over as itself: " + init,
init.contains("__wurst_objectToIndex") || init.contains("__wurst_classFromIndex")
|| init.contains("__wurst_classToIndex"));
@@ -384,14 +387,14 @@ public void handleBoundWrapperSpecializesToOneLuaTableAccess() throws IOExceptio
// The small specialized wrappers are expanded into the caller, which is all that is left to read.
String lua = compiled("handleBoundWrapperSpecializesToOneLuaTableAccess");
String init = getFunctionBody(lua, "init_Test");
- assertTrue("the caller calls the unchanged put intrinsic: " + init, init.contains("__wurst_keyedMapPut"));
+ assertTrue("the unchanged put intrinsic is one direct Lua table store: " + init,
+ init.contains("[key2] = value6") && init.indexOf("] = ") == init.lastIndexOf("] = "));
assertTrue("and reads the table with the int default: " + init,
init.contains("] or 0)") && !init.contains("__wurst_keyedMapGetInt"));
assertFalse("the native unit is used as the key without handle-id conversion: " + init,
init.contains("GetHandleId") || init.contains("__wurst_objectToIndex")
|| init.contains("__wurst_classToIndex"));
- assertTrue("put is one direct Lua table store",
- getFunctionBody(lua, "__wurst_keyedMapPut").contains("t[k] = v"));
+ assertFalse("put has no stub to call or define", lua.contains("__wurst_keyedMapPut"));
assertTrue("get is one direct Lua table read", init.indexOf("] or 0)") == init.lastIndexOf("] or 0)"));
}
@@ -501,16 +504,17 @@ public void handleBoundGenericValuesKeepTheirNativeLuaRepresentation() throws IO
// The small specialized wrappers are expanded into the caller, which is all that is left to read.
String lua = compiled("handleBoundGenericValuesKeepTheirNativeLuaRepresentation");
String init = getFunctionBody(lua, "init_Test");
- assertTrue("the caller calls the generic put intrinsic: " + init, init.contains("__wurst_keyedMapPut"));
+ assertTrue("the caller stores into the table directly: " + init, init.contains("[u] = 7"));
assertTrue("and reads the table directly: " + init,
init.contains("] or 0)") && !init.contains("__wurst_keyedMapGet"));
assertFalse("generic values and handle keys need no index conversions: " + init,
init.contains("GetHandleId") || init.contains("__wurst_objectToIndex")
|| init.contains("__wurst_classToIndex") || init.contains("__wurst_classFromIndex"));
- String putStub = getFunctionBody(lua, "__wurst_keyedMapPut");
- assertTrue("generic put has exactly one direct table store: " + putStub,
- putStub.contains("t[k] = v") && putStub.indexOf("t[k]") == putStub.lastIndexOf("t[k]"));
- assertFalse("generic get is a table index, with no stub to define", lua.contains("function __wurst_keyedMapGet("));
+ assertTrue("each generic put is one direct table store: " + init,
+ init.contains("KeyedMap_map_storage[this10][key1] = value4")
+ && init.contains("KeyedMap_map_storage[this11][key2] = value5"));
+ assertFalse("generic get and put are table accesses, with no stub to define",
+ lua.contains("function __wurst_keyedMapGet(") || lua.contains("function __wurst_keyedMapPut("));
}
@Test
@@ -1052,9 +1056,9 @@ public void unitKeyedMapKeysTheUnitItselfOnLua() throws IOException {
String compiled = compiled("unitKeyedMapKeysTheUnitItselfOnLua");
String init = getFunctionBody(compiled, "init_Test");
- assertTrue("writes lower to the keyed-map stubs and reads to the table index: " + init,
- init.contains("__wurst_keyedMapPut") && init.contains("] or 0)")
- && init.contains("] ~= nil)") && init.contains("__wurst_keyedMapRemove"));
+ assertTrue("writes lower to nil-guarded table stores and reads to the table index: " + init,
+ 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"));
}
@@ -1103,6 +1107,144 @@ public void handleKeyedMapAgreesOnBothBackends() {
"endpackage");
}
+ /**
+ * A put or remove is printed as the store itself, guarded by a nil test where the key is a
+ * handle. The test reads the key twice and skips the store, so operands with an effect are
+ * evaluated into locals first: each one still runs once, in the order of the call, also when the
+ * key is null, and a map read before an effect is the map from before it. Run against the
+ * interpreter and Jass through the Table bodies.
+ */
+ @Test
+ public void keyedMapWritesEvaluateEachOperandOnceAndInOrder() throws IOException {
+ test().testLua(true).luaOnly(false).executeProg(true).withStdLib().lines(
+ "package KeyedMap",
+ "import Table",
+ "@compilerintrinsic public function keyedMapCreate() returns int",
+ " return (new Table()) castTo int",
+ "@compilerintrinsic public function keyedMapPut(int tbl, timer key, int value)",
+ " (tbl castTo Table).saveInt(GetHandleId(key), value)",
+ "@compilerintrinsic public function keyedMapGetInt(int tbl, timer key) returns int",
+ " return (tbl castTo Table).loadInt(GetHandleId(key))",
+ "@compilerintrinsic public function keyedMapRemove(int tbl, timer key)",
+ " (tbl castTo Table).removeInt(GetHandleId(key))",
+ "endpackage",
+ "package Test",
+ "import KeyedMap",
+ "string trace = \"\"",
+ "int theMap",
+ "int otherMap",
+ "function mapOf(string tag) returns int",
+ " trace += tag",
+ " return theMap",
+ "function keyOf(string tag, timer t) returns timer",
+ " trace += tag",
+ " return t",
+ "function valueOf(string tag, int v) returns int",
+ " trace += tag",
+ " return v",
+ "function switchingMap(timer t) returns timer",
+ " theMap = otherMap",
+ " return t",
+ "init",
+ " theMap = keyedMapCreate()",
+ " otherMap = keyedMapCreate()",
+ " let firstMap = theMap",
+ " let key = CreateTimer()",
+ " timer none = null",
+ " keyedMapPut(mapOf(\"m\"), keyOf(\"k\", key), valueOf(\"v\", 5))",
+ " keyedMapPut(mapOf(\"M\"), keyOf(\"K\", none), valueOf(\"V\", 6))",
+ " if keyedMapGetInt(firstMap, key) != 5",
+ " testFail(\"put did not store\")",
+ " keyedMapRemove(mapOf(\"r\"), keyOf(\"q\", key))",
+ " keyedMapRemove(mapOf(\"R\"), keyOf(\"Q\", none))",
+ " if trace != \"mkvMKVrqRQ\"",
+ " testFail(\"operands evaluated as \" + trace)",
+ " if keyedMapGetInt(firstMap, key) != 0",
+ " testFail(\"remove did not clear\")",
+ " keyedMapPut(theMap, switchingMap(key), 7)",
+ " if keyedMapGetInt(firstMap, key) != 7 or keyedMapGetInt(otherMap, key) != 0",
+ " testFail(\"the map was read after the key's effect\")",
+ " testSuccess()",
+ "endpackage");
+
+ String init = getFunctionBody(compiled("keyedMapWritesEvaluateEachOperandOnceAndInOrder"), "init_Test");
+ assertTrue("operands with effects are evaluated into locals before the guarded store: " + init,
+ init.contains("local __wurst_key = keyOf(\"k\", ") && init.contains("(__wurst_key ~= nil) then"));
+ assertFalse("no write stub is called: " + init,
+ init.contains("__wurst_keyedMapPut") || init.contains("__wurst_keyedMapRemove"));
+ }
+
+ /**
+ * A value which can raise, such as an array field read through an object which may be null, is evaluated before
+ * the nil-key test, as the stub's argument was: with a nil key it still runs.
+ */
+ @Test
+ public void aKeyedMapWriteEvaluatesAValueWhichCanRaiseEvenForANilKey() throws IOException {
+ test().testLua(true).luaOnly(false).executeProg(true).withStdLib().lines(
+ "package KeyedMap",
+ "import Table",
+ "@compilerintrinsic public function keyedMapCreate() returns int",
+ " return (new Table()) castTo int",
+ "@compilerintrinsic public function keyedMapPut(int tbl, timer key, int value)",
+ " (tbl castTo Table).saveInt(GetHandleId(key), value)",
+ "@compilerintrinsic public function keyedMapGetInt(int tbl, timer key) returns int",
+ " return (tbl castTo Table).loadInt(GetHandleId(key))",
+ "endpackage",
+ "package Test",
+ "import KeyedMap",
+ "class Holder",
+ " int array[2] arr",
+ "init",
+ " let theMap = keyedMapCreate()",
+ " let h = new Holder()",
+ " h.arr[0] = 5",
+ " timer none = null",
+ " keyedMapPut(theMap, none, h.arr[0])",
+ " let key = CreateTimer()",
+ " keyedMapPut(theMap, key, h.arr[0])",
+ " if keyedMapGetInt(theMap, key) != 5",
+ " testFail(\"put did not store\")",
+ " testSuccess()",
+ "endpackage");
+
+ String init = getFunctionBody(compiled("aKeyedMapWriteEvaluatesAValueWhichCanRaiseEvenForANilKey"), "init_Test");
+ java.util.regex.Matcher read = java.util.regex.Pattern
+ .compile("local (\\w+) = Holder_arr_storage\\[\\w+\\]\\[0\\]\\s*\\n\\s*if \\(\\w+ ~= nil\\) then")
+ .matcher(init);
+ assertTrue("the array field read is evaluated before the nil-key test: " + init, read.find());
+ }
+
+ /**
+ * An int key read through a null object is nil on Lua, so an int key which is not a literal keeps the nil test:
+ * the store does nothing, as the stub did, instead of raising on a nil table key. (Jass reads the field as 0 and
+ * stores under 0; Lua only.)
+ */
+ @Test
+ public void aKeyedMapWriteUnderAnIntKeyReadThroughANullObjectStoresNothing() {
+ test().testLua(true).executeProg(true).withStdLib().lines(
+ "package KeyedMap",
+ "import Table",
+ "@compilerintrinsic public function keyedMapCreate() returns int",
+ " return (new Table()) castTo int",
+ "@compilerintrinsic public function keyedMapPut(int tbl, int key, int value)",
+ " (tbl castTo Table).saveInt(key, value)",
+ "@compilerintrinsic public function keyedMapGetInt(int tbl, int key) returns int",
+ " return (tbl castTo Table).loadInt(key)",
+ "endpackage",
+ "package Test",
+ "import KeyedMap",
+ "class Holder",
+ " int id",
+ "init",
+ " let theMap = keyedMapCreate()",
+ " Holder none = null",
+ " keyedMapPut(theMap, none.id, 7)",
+ " keyedMapPut(theMap, 3, 4)",
+ " if keyedMapGetInt(theMap, 3) == 4 and keyedMapGetInt(theMap, 0) == 0",
+ " testSuccess()",
+ "endpackage");
+ }
+
@Test
public void keyedMapDestroyClearsLuaStoreWhileAliasRemains() {
test().testLua(true).luaOnly(true).executeProg().withStdLib().lines(
@@ -1203,9 +1345,11 @@ public void intKeyedClassMapLowersToSingleLuaIndexes() throws IOException {
String compiled = compiled("intKeyedClassMapLowersToSingleLuaIndexes");
String init = getFunctionBody(compiled, "init_Test");
- assertTrue("writes lower to the keyed-map stubs and reads to the table index: " + init,
- init.contains("__wurst_keyedMapPut") && init.contains("] ~= nil)")
- && !init.contains("__wurst_keyedMapGet") && init.contains("__wurst_keyedMapRemove"));
+ assertTrue("writes under an int key lower to unguarded table stores and reads to the table index: " + init,
+ init.contains("[1093677104] = this6") && init.contains("m[1093677104] = nil")
+ && init.contains("] ~= nil)") && !init.contains("__wurst_keyedMapPut")
+ && !init.contains("__wurst_keyedMapRemove") && !init.contains("__wurst_keyedMapGet"));
+ assertFalse("an int key is never nil and needs no test: " + init, init.contains("(1093677104 ~= nil)"));
assertFalse("the object must be stored as itself: " + init,
init.contains("__wurst_objectToIndex") || init.contains("__wurst_classToIndex")
|| init.contains("__wurst_classFromIndex"));
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 d0912476d..af7e4e047 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
@@ -388,15 +388,11 @@ public void otherLevelsKeepTheirParentheses() {
}
/**
- * {@code ..} is right associative, so {@code (a .. b) .. c} is not {@code a .. b .. c}; a chain
- * of comparisons reads as a range check but compares a boolean; unary operators print their
- * operand in parentheses. None of them joins a chain.
+ * A chain of comparisons reads as a range check but compares a boolean; unary operators print
+ * their operand in parentheses. Neither joins a chain.
*/
@Test
- public void concatenationComparisonsAndUnaryOperatorsKeepTheirParentheses() {
- assertEquals("((a .. b) .. c)", renderLuaExpr(concat(concat(v("a"), v("b")), v("c"))));
- assertEquals("(a .. (b .. c))", renderLuaExpr(concat(v("a"), concat(v("b"), v("c")))));
- assertEquals("((a + b) .. c)", renderLuaExpr(concat(plus(v("a"), v("b")), v("c"))));
+ public void comparisonsAndUnaryOperatorsKeepTheirParentheses() {
assertEquals("((a < b) == c)", renderLuaExpr(equal(less(v("a"), v("b")), v("c"))));
assertEquals("((a == b) == c)", renderLuaExpr(equal(equal(v("a"), v("b")), v("c"))));
assertEquals("(-(a) - b)", renderLuaExpr(
@@ -420,6 +416,78 @@ public void aVeryLongChainPrintsFlatWithoutRecursion() {
assertTrue(rendered.endsWith(" + t" + (terms - 1) + ")"));
}
+ /**
+ * A left-nested concatenation prints flat, one CONCAT instead of one per operator. The text
+ * parses as {@code a .. (b .. c)} because {@code ..} is right associative, which gives the same
+ * string: the backend only joins strings. A right operand, which already is one CONCAT with its
+ * left neighbours, and another operator as an operand keep their parentheses.
+ */
+ @Test
+ public void leftNestedConcatenationPrintsFlat() {
+ assertEquals("(a .. b .. c)", renderLuaExpr(concat(concat(v("a"), v("b")), v("c"))));
+ assertEquals("(a .. b .. c .. d)",
+ renderLuaExpr(concat(concat(concat(v("a"), v("b")), v("c")), v("d"))));
+ assertEquals("(a .. (b .. c))", renderLuaExpr(concat(v("a"), concat(v("b"), v("c")))));
+ assertEquals("(a .. b .. (c .. d))",
+ renderLuaExpr(concat(concat(v("a"), v("b")), concat(v("c"), v("d")))));
+ assertEquals("((a + b) .. c)", renderLuaExpr(concat(plus(v("a"), v("b")), v("c"))));
+ assertEquals("((a or b) .. c .. (d or e))",
+ renderLuaExpr(concat(concat(or(v("a"), v("b")), v("c")), or(v("d"), v("e")))));
+ }
+
+ /**
+ * A flat concatenation is one parser level per operator and one register per operand, so a long
+ * chain is split into groups of sixteen, and those into groups again: luac stops at 200 levels
+ * and a function has 255 registers.
+ */
+ @Test
+ public void aLongConcatenationIsGroupedWithinLuaLimits() {
+ LuaExpr chain = v("t0");
+ for (int i = 1; i < 20; i++) {
+ chain = concat(chain, v("t" + i));
+ }
+ StringBuilder expected = new StringBuilder("((t0");
+ for (int i = 1; i < 16; i++) {
+ expected.append(" .. t").append(i);
+ }
+ expected.append(") .. (t16 .. t17 .. t18 .. t19))");
+ assertEquals(expected.toString(), renderLuaExpr(chain));
+
+ int terms = 5000;
+ chain = v("t0");
+ for (int i = 1; i < terms; i++) {
+ chain = concat(chain, v("t" + i));
+ }
+ String rendered = renderLuaExpr(chain);
+ assertEquals(terms - 1, countOccurrences(rendered, " .. "));
+ int depth = 0;
+ int maxDepth = 0;
+ int longestRun = 0;
+ int run = 0;
+ for (int i = 0; i < rendered.length(); i++) {
+ char ch = rendered.charAt(i);
+ if (ch == '(') {
+ maxDepth = Math.max(maxDepth, ++depth);
+ run = 0;
+ } else if (ch == ')') {
+ depth--;
+ run = 0;
+ } else if (rendered.startsWith(" .. ", i)) {
+ longestRun = Math.max(longestRun, ++run);
+ }
+ }
+ assertEquals("5000 parts are four levels of groups", 4, maxDepth);
+ assertTrue("no group joins more than sixteen operands: " + longestRun, longestRun < 16);
+ }
+
+ private static int countOccurrences(String haystack, String needle) {
+ int count = 0;
+ for (int at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + needle.length())) {
+ count++;
+ }
+ return count;
+ }
+
@Test
public void testStdLib() throws IOException {
test().testLua(true).withStdLib().lines(
@@ -3654,20 +3722,21 @@ public void hashtableHelpersEmitPerTypeBucketsInLua() throws IOException {
assertFunctionBodyContains(compiled, "__wurst_LoadAbilityHandle", "h.__wurst_ht_handle", true);
}
+ /** A code value takes no parameters, so its adapter forwards none: no varargs at all. */
@Test
- public void luaFunctionRefWrapperForwardsVarargs() throws IOException {
+ public void luaFunctionRefWrapperOfNullaryTargetTakesNoVarargs() throws IOException {
test().testLua(true).withStdLib().lines(
"package Test",
"init",
" let f = CreateForce()",
" ForForce(f, () -> skip)"
);
- String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaFunctionRefWrapperForwardsVarargs.lua"), Charsets.UTF_8);
- assertContainsRegex(compiled, "function\\s+__wurst_callback_[A-Za-z0-9_]+\\(\\.\\.\\.\\)");
+ String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaFunctionRefWrapperOfNullaryTargetTakesNoVarargs.lua"), Charsets.UTF_8);
+ assertContainsRegex(compiled, "function\\s+__wurst_callback_[A-Za-z0-9_]+\\(\\)");
+ assertDoesNotContainRegex(compiled, "function\\s+__wurst_callback_[A-Za-z0-9_]+\\(\\.\\.\\.\\)");
assertFalse(compiled.contains("xpcall(function (...)"));
assertContainsRegex(compiled,
- "xpcall\\([A-Za-z0-9_]+, __wurst_callback_error[A-Za-z0-9_]*, \\.\\.\\.\\)");
- assertTrue(compiled.contains(", ...)"));
+ "xpcall\\([A-Za-z0-9_]+, __wurst_callback_error[A-Za-z0-9_]*\\)");
assertFalse(compiled.contains("local temp = ..."));
assertFalse(compiled.contains("ForForce(f, function (...) \n\t\t\tlocal tempRes"));
}
@@ -3704,7 +3773,7 @@ public void luaFunctionRefsReuseOneAdapterAndPreserveSingleReturn() {
);
List adapters = uniqueMatches(compiled,
- "function\\s+(__wurst_callback_predicate[A-Za-z0-9_]*)\\(\\.\\.\\.\\)", 1);
+ "function\\s+(__wurst_callback_predicate[A-Za-z0-9_]*)\\(\\)", 1);
assertEquals("one adapter must serve every reference to the same function:\n" + compiled,
1, adapters.size());
String adapter = adapters.get(0);
@@ -3979,10 +4048,11 @@ public void subclassAllocationIncludesInheritedFieldsInLua() throws IOException
);
String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_subclassAllocationIncludesInheritedFieldsInLua.lua"), Charsets.UTF_8);
+ // Within Child's own allocation function (Window is never allocated, so its allocation writes nothing).
assertContainsRegex(compiled,
- "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)[\\s\\S]*?Window_anchorTop_storage\\[new_inst\\] = 0");
+ "function\\s+Child:create\\d*\\s*\\(\\)(?:(?!\\nend)[\\s\\S])*?Window_anchorTop_storage\\[new_inst\\] = 0");
assertContainsRegex(compiled,
- "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)[\\s\\S]*?Window_anchorBottom_storage\\[new_inst\\] = 0");
+ "function\\s+Child:create\\d*\\s*\\(\\)(?:(?!\\nend)[\\s\\S])*?Window_anchorBottom_storage\\[new_inst\\] = 0");
}
// ----- GetHandleId remapping -----
diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java
index 93e3c1b58..dd8215c1e 100644
--- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java
+++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java
@@ -2113,7 +2113,7 @@ public T measure(String name, java.util.function.Supplier f) {
new ImOptimizer(timeTaker, translator).localOptimizations();
- assertEquals(timeTaker.measurements, 16,
+ assertEquals(timeTaker.measurements, 18,
"the optimizer should run two fixed sweeps rather than iterating to convergence");
}
@@ -4505,4 +4505,94 @@ public void inlinedReturnLoweringMatchesTheCalledFunction() {
" if failures == 0",
" testSuccess()");
}
+
+ /**
+ * The pass behind the Lua field defaults, on hand-made IM: of {@code o = alloc C; o.f = 0; ...; o.f = 2} the
+ * first write goes only when nothing between the writes can raise. A deallocation can (a double free), and so can
+ * reading an array field or the type id through an object which may be null ({@code storage[p][0]} and the class
+ * descriptor of p index nil), arithmetic on anything but literals (a nil read can reach any variable), and a write
+ * through such an object (a nil table key). An object
+ * which was not allocated in the list may be null itself, so its first write could raise.
+ */
+ @Test
+ public void aFieldWriteGoesOnlyForANewObjectWithNothingBetweenWhichCanRaise() {
+ assertEquals(redundantFieldStoresLeave(true, "nothing"), 2, "o = alloc; o.f = 2");
+ assertEquals(redundantFieldStoresLeave(true, "arithmetic on literals"), 3, "1 + 2 does not raise");
+ assertEquals(redundantFieldStoresLeave(true, "arithmetic on a local"), 4, "x + 1 can add nil: x may hold one");
+ assertEquals(redundantFieldStoresLeave(true, "dealloc"), 4, "a deallocation between keeps the first write");
+ assertEquals(redundantFieldStoresLeave(true, "array field read"), 4, "p.arr[0] can index nil");
+ assertEquals(redundantFieldStoresLeave(true, "arithmetic on a field read"), 4, "p.g + 1 can add nil");
+ assertEquals(redundantFieldStoresLeave(true, "arithmetic on a local read from a field"), 5,
+ "x = p.g; x + 1 can add nil");
+ assertEquals(redundantFieldStoresLeave(true, "arithmetic on a global read from a field"), 5,
+ "G = p.g; G + 1 can add nil");
+ assertEquals(redundantFieldStoresLeave(true, "write through another object"), 4, "p.g = 1 can use a nil key");
+ assertEquals(redundantFieldStoresLeave(true, "type id read through another object"), 4,
+ "p.typeId reads a nil class descriptor");
+ assertEquals(redundantFieldStoresLeave(false, "nothing"), 2, "an object not allocated here keeps it");
+ }
+
+ /** The statements left of {@code [o = alloc C;] o.f = 0; ; o.f = 2} after the pass. */
+ private int redundantFieldStoresLeave(boolean allocate, String middle) {
+ WurstModel model = Ast.WurstModel();
+ ImTranslator translator = new ImTranslator(model, false, new RunArgs());
+ ImVar field = JassIm.ImVar(model, TypesHelper.imInt(), "f", false);
+ ImVar other = JassIm.ImVar(model, TypesHelper.imInt(), "g", false);
+ ImVar array = JassIm.ImVar(model, JassIm.ImArrayType(TypesHelper.imInt()), "arr", false);
+ ImClass c = JassIm.ImClass(model, "C", JassIm.ImTypeVars(), JassIm.ImVars(field, other, array),
+ JassIm.ImMethods(), JassIm.ImFunctions(), new ArrayList<>());
+ translator.getImProg().getClasses().add(c);
+ ImVar o = JassIm.ImVar(model, JassIm.ImClassType(c, JassIm.ImTypeArguments()), "o", false);
+ ImVar p = JassIm.ImVar(model, JassIm.ImClassType(c, JassIm.ImTypeArguments()), "p", false);
+ ImVar x = JassIm.ImVar(model, TypesHelper.imInt(), "x", false);
+ ImStmts body = JassIm.ImStmts();
+ if (allocate) {
+ body.add(JassIm.ImSet(model, JassIm.ImVarAccess(o),
+ JassIm.ImAlloc(model, JassIm.ImClassType(c, JassIm.ImTypeArguments()))));
+ }
+ body.add(JassIm.ImSet(model, member(model, o, field), JassIm.ImIntVal(0)));
+ switch (middle) {
+ case "nothing" -> {
+ }
+ case "arithmetic on literals" -> body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x), plusOne(JassIm.ImIntVal(1))));
+ case "arithmetic on a local" -> body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x), plusOne(JassIm.ImVarAccess(x))));
+ case "arithmetic on a global read from a field" -> {
+ ImVar g = JassIm.ImVar(model, TypesHelper.imInt(), "G", false);
+ translator.getImProg().getGlobals().add(g);
+ body.add(JassIm.ImSet(model, JassIm.ImVarAccess(g), member(model, p, other)));
+ body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x), plusOne(JassIm.ImVarAccess(g))));
+ }
+ case "dealloc" -> body.add(JassIm.ImDealloc(model, JassIm.ImClassType(c, JassIm.ImTypeArguments()),
+ JassIm.ImVarAccess(o)));
+ case "array field read" -> body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x),
+ JassIm.ImMemberAccess(model, JassIm.ImVarAccess(p), JassIm.ImTypeArguments(), array,
+ JassIm.ImExprs(JassIm.ImIntVal(0)))));
+ case "arithmetic on a field read" -> body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x),
+ plusOne(member(model, p, other))));
+ case "arithmetic on a local read from a field" -> {
+ body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x), member(model, p, other)));
+ body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x), plusOne(JassIm.ImVarAccess(x))));
+ }
+ case "write through another object" -> body.add(JassIm.ImSet(model, member(model, p, other),
+ JassIm.ImIntVal(1)));
+ case "type id read through another object" -> body.add(JassIm.ImSet(model, JassIm.ImVarAccess(x),
+ JassIm.ImTypeIdOfObj(JassIm.ImVarAccess(p), JassIm.ImClassType(c, JassIm.ImTypeArguments()))));
+ default -> throw new IllegalArgumentException(middle);
+ }
+ body.add(JassIm.ImSet(model, member(model, o, field), JassIm.ImIntVal(2)));
+ ImFunction f = JassIm.ImFunction(model, "f", JassIm.ImTypeVars(), JassIm.ImVars(p), JassIm.ImVoid(),
+ JassIm.ImVars(o, x), body, Collections.emptyList());
+ translator.getImProg().getFunctions().add(f);
+ new de.peeeq.wurstscript.intermediatelang.optimizer.RedundantFieldStores().optimize(translator);
+ return f.getBody().size();
+ }
+
+ private static ImMemberAccess member(WurstModel model, ImVar receiver, ImVar field) {
+ return JassIm.ImMemberAccess(model, JassIm.ImVarAccess(receiver), JassIm.ImTypeArguments(), field,
+ JassIm.ImExprs());
+ }
+
+ private static ImExpr plusOne(ImExpr e) {
+ return JassIm.ImOperatorCall(de.peeeq.wurstscript.WurstOperator.PLUS, JassIm.ImExprs(e, JassIm.ImIntVal(1)));
+ }
}
diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java
index d7d5e9e7c..dcf6eb97e 100644
--- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java
+++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java
@@ -888,7 +888,7 @@ protected String getLuaExecutable() {
);
}
- private String getLuacExecutable() {
+ protected String getLuacExecutable() {
if (resolvedLuacExecutable != null) {
return resolvedLuacExecutable;
}
@@ -1041,6 +1041,10 @@ private void checkLuaRootPurity(LuaCompilationUnit luaCode) {
);
for (LuaStatement stmt : luaCode) {
+ if (stmt instanceof LuaChunkLocal local) {
+ // A main-chunk local is initialised at the root like a global.
+ stmt = local.getDefinition();
+ }
if (stmt instanceof LuaVariable) {
LuaExprOpt initialValue = ((LuaVariable) stmt).getInitialValue();
if (initialValue instanceof LuaExpr) {