From 7d6c565757e3a7bb65eada4efb3b17888e521b65 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 10 Sep 2026 13:59:24 +0200 Subject: [PATCH 1/2] Lower KeyedTable membership to a Lua table keyed directly by the element. --- .../lua/translation/LuaKeyedTable.java | 96 +++++++++++++++++++ .../lua/translation/LuaTranslator.java | 5 + .../tests/LuaTranslationTests.java | 96 +++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java new file mode 100644 index 000000000..a34e1f6e3 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java @@ -0,0 +1,96 @@ +package de.peeeq.wurstscript.translation.lua.translation; + +import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.ast.WPackage; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.jassIm.ImVar; +import de.peeeq.wurstscript.luaAst.LuaAst; +import de.peeeq.wurstscript.luaAst.LuaFunction; + +/** + * Lua bodies for the {@code KeyedTable} package: a set keyed directly by its element. + * + *

Jass has no hashing, so every keyed structure in the library bottoms out in the hashtable + * natives, which take a (parent, child) pair and are emitted on Lua as a nested table plus a nil + * check. A membership test therefore costs a call and two indexes where Lua needs one, and the + * element has to be squeezed through {@code castTo int} first. That is a Jass limitation carried + * into a runtime which is already a hash table, which AGENTS.md section 7 asks us not to do. + * + *

On Lua each keyed table is its own table and the element is the key, so the four operations + * become a single index each. {@code castTo int} is already the identity on Lua for class types + * (see {@code rewriteTypeCastingCompatFunction}), and handles are identity-cached tables, so the + * value arriving here is a usable key with reference identity either way. + * + *

Iteration is deliberately absent. Enumerating a Lua table needs {@code pairs()}, whose + * order depends on internal hash layout and therefore differs between clients - which desyncs a + * lockstep game. Anything that must be iterated needs a separately maintained insertion-ordered + * array, which is what SparseSet's dense half provides; this primitive is membership only. + * + *

The Jass path is the ordinary Wurst body of these functions and is left alone: correctness + * matters there, performance does not. + */ +final class LuaKeyedTable { + + /** Package whose functions get the native Lua bodies below. */ + private static final String PACKAGE = "KeyedTable"; + + static final String CREATE = "keyedTableCreate"; + static final String ADD = "keyedTableAdd"; + static final String CONTAINS = "keyedTableContains"; + static final String REMOVE = "keyedTableRemove"; + + private LuaKeyedTable() { + } + + /** The {@code KeyedTable} function {@code f} implements, or null if it is not one. */ + static String operationOf(ImFunction f) { + if (!(f.attrTrace() instanceof FuncDef fd)) { + return null; + } + if (!(fd.attrNearestPackage() instanceof WPackage p) || !PACKAGE.equals(p.getName())) { + return null; + } + String name = fd.getName(); + return CREATE.equals(name) || ADD.equals(name) || CONTAINS.equals(name) || REMOVE.equals(name) + ? name + : null; + } + + /** + * Replaces the body of a {@code KeyedTable} function with the Lua-native form. + * + * @return whether a body was written; false leaves the ordinary translation in place, so a + * signature this does not recognise keeps working rather than silently emitting nothing. + */ + static boolean rewrite(ImFunction f, LuaFunction lf, LuaTranslator tr) { + String op = operationOf(f); + if (op == null) { + return false; + } + if (CREATE.equals(op)) { + if (!f.getParameters().isEmpty()) { + return false; + } + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaLiteral("return {}")); + return true; + } + if (f.getParameters().size() != 2) { + return false; + } + String table = luaNameOf(f.getParameters().get(0), tr); + String key = luaNameOf(f.getParameters().get(1), tr); + lf.getBody().clear(); + switch (op) { + case ADD -> lf.getBody().add(LuaAst.LuaLiteral(table + "[" + key + "] = true")); + case REMOVE -> lf.getBody().add(LuaAst.LuaLiteral(table + "[" + key + "] = nil")); + case CONTAINS -> lf.getBody().add(LuaAst.LuaLiteral("return " + table + "[" + key + "] ~= nil")); + default -> throw new IllegalStateException("unhandled KeyedTable operation " + op); + } + return true; + } + + private static String luaNameOf(ImVar param, LuaTranslator tr) { + return tr.luaVar.getFor(param).getName(); + } +} 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 b98676769..89ddcfcd0 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 @@ -812,6 +812,11 @@ private void translateFunc(ImFunction f) { luaModel.add(lf); return; } + // KeyedTable is membership keyed directly by the element on Lua; see LuaKeyedTable. + if (LuaKeyedTable.rewrite(f, lf, this)) { + luaModel.add(lf); + return; + } if (f.hasFlag(FunctionFlagEnum.IS_VARARG)) { 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 3badbd250..d096fefd7 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 @@ -1868,6 +1868,102 @@ public void genericOverrideChainBindsGlobalStateSlotToMostSpecificImplInLua() th assertDoesNotContainRegex(compiled, "GlobalCheckState\\." + dispatchedSlot + "\\s*=\\s*NoOpState_NoOpState_update"); } + /** + * KeyedTable source shared by the tests below: the Jass path built on the library's hashtable + * wrapper, which the Lua backend replaces with a table keyed directly by the element. + */ + private static String[] keyedTableSource(String... usage) { + java.util.List lines = new java.util.ArrayList<>(java.util.Arrays.asList( + "package KeyedTable", + "import Table", + "public function keyedTableCreate() returns int", + " return (new Table()) castTo int", + "public function keyedTableAdd(int tbl, int key)", + " (tbl castTo Table).saveBoolean(key, true)", + "public function keyedTableContains(int tbl, int key) returns boolean", + " return (tbl castTo Table).loadBoolean(key)", + "public function keyedTableRemove(int tbl, int key)", + " (tbl castTo Table).removeBoolean(key)", + "endpackage")); + lines.addAll(java.util.Arrays.asList(usage)); + return lines.toArray(new String[0]); + } + + /** On Lua each keyed table is its own table and membership is a single index. */ + @Test + public void keyedTableLowersToASingleLuaIndex() throws IOException { + test().testLua(true).withStdLib().lines(keyedTableSource( + "package Test", + "import KeyedTable", + "init", + " let t = keyedTableCreate()", + " keyedTableAdd(t, 7)", + " print(keyedTableContains(t, 7).toString())", + "endpackage")); + + String compiled = Files.toString( + new File("test-output/lua/LuaTranslationTests_keyedTableLowersToASingleLuaIndex.lua"), Charsets.UTF_8); + + String add = getFunctionBody(compiled, "keyedTableAdd"); + String contains = getFunctionBody(compiled, "keyedTableContains"); + String create = getFunctionBody(compiled, "keyedTableCreate"); + + assertTrue("add should be a single table store, was: " + add, add.contains("] = true")); + assertTrue("contains should be a single index, was: " + contains, contains.contains("] ~= nil")); + assertTrue("create should allocate a bare table, was: " + create, create.contains("return {}")); + + // The whole point: no hashtable machinery on this path. + assertFalse("add must not go through the hashtable natives: " + add, add.contains("SaveBoolean")); + assertFalse("contains must not go through the hashtable natives: " + contains, contains.contains("LoadBoolean")); + } + + /** + * Membership must mean the same thing on both backends: the Jass path runs the hashtable + * body, the Lua path runs a bare table, and the observable behaviour has to agree. + */ + @Test + public void keyedTableMembershipAgreesOnBothBackends() { + test().testLua(true).executeProg(true).withStdLib().lines(keyedTableSource( + "package Test", + "import KeyedTable", + "init", + " let t = keyedTableCreate()", + " if keyedTableContains(t, 7)", + " testFail(\"empty table reported 7 as present\")", + " keyedTableAdd(t, 7)", + " if not keyedTableContains(t, 7)", + " testFail(\"7 was added but reported absent\")", + " if keyedTableContains(t, 8)", + " testFail(\"8 was never added but reported present\")", + " keyedTableRemove(t, 7)", + " if keyedTableContains(t, 7)", + " testFail(\"7 was removed but reported present\")", + " testSuccess()", + "endpackage")); + } + + /** + * pairs() iteration order differs between clients and desyncs a lockstep game, so no emitted + * Lua may contain it. Cheap to assert and worth keeping regardless of this feature. + */ + @Test + public void keyedTableEmitsNoTableIteration() throws IOException { + test().testLua(true).withStdLib().lines(keyedTableSource( + "package Test", + "import KeyedTable", + "init", + " let t = keyedTableCreate()", + " keyedTableAdd(t, 3)", + " keyedTableRemove(t, 3)", + "endpackage")); + + String compiled = Files.toString( + new File("test-output/lua/LuaTranslationTests_keyedTableEmitsNoTableIteration.lua"), Charsets.UTF_8); + + assertFalse("emitted Lua must never iterate a table with pairs()", compiled.contains("pairs(")); + assertFalse("emitted Lua must never iterate a table with next()", compiled.contains(" next(")); + } + private CU[] genericOverrideReproUnits() { return new CU[]{ compilationUnit("fsmLib.wurst", From 473dc4a3b1ccd45c40f7604d2834d6e73f12ea3c Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 10 Sep 2026 14:32:46 +0200 Subject: [PATCH 2/2] Lower KeyedTable in IM before inlining and match it by declaration rather than name. --- .../imtranslation/LuaKeyedTable.java | 79 +++++++++++++++ .../imtranslation/LuaNativeLowering.java | 19 ++++ .../lua/translation/LuaKeyedTable.java | 96 ------------------- .../lua/translation/LuaNatives.java | 22 +++++ .../lua/translation/LuaTranslator.java | 5 - .../tests/LuaTranslationTests.java | 84 ++++++++++++++-- .../wurstscript/tests/WurstScriptTest.java | 10 ++ 7 files changed, 207 insertions(+), 108 deletions(-) create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java delete mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java new file mode 100644 index 000000000..faa65f1a7 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedTable.java @@ -0,0 +1,79 @@ +package de.peeeq.wurstscript.translation.imtranslation; + +import de.peeeq.wurstscript.CompilerIntrinsics; +import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.jassIm.ImVoid; +import de.peeeq.wurstscript.types.TypesHelper; + +/** + * Recognises the compiler-owned {@code KeyedTable} membership operations. + * + *

Jass has no hashing, so every keyed structure in the library bottoms out in the hashtable + * natives. Those take a (parent, child) pair and are emitted on Lua as a nested table plus a nil + * check, so a membership test costs a call and two indexes on a runtime that is already a hash + * table - a Jass compromise carried into Lua, which AGENTS.md section 7 asks us not to do. On Lua + * each keyed table is its own table and the element is the key, so each operation is one index. + * + *

Matching is by declaration, not by name: a function qualifies only if it is annotated + * {@code @compilerintrinsic} and its IM signature is exactly the one the lowering assumes. + * Name-only matching would silently replace the body of any user function that happened to share + * the name, including one with different types, which section 7 rules out - semantic identity must + * come from the declaration and its structural signature, never from string comparison. + * + *

Iteration is deliberately absent. Enumerating a Lua table needs {@code pairs()}, whose + * order follows internal hash layout and so differs between clients, which desyncs a lockstep + * game. Anything that must be iterated needs a separately maintained insertion-ordered array - + * what SparseSet's dense half provides. This primitive is membership only. + */ +public final class LuaKeyedTable { + + /** Source-level names of the operations. Necessary to identify them, never sufficient. */ + private static final String CREATE = "keyedTableCreate"; + private static final String ADD = "keyedTableAdd"; + private static final String CONTAINS = "keyedTableContains"; + private static final String REMOVE = "keyedTableRemove"; + + /** Stub names whose Lua bodies live in {@code LuaNatives}. */ + public static final String NATIVE_CREATE = "__wurst_keyedTableCreate"; + public static final String NATIVE_ADD = "__wurst_keyedTableAdd"; + public static final String NATIVE_CONTAINS = "__wurst_keyedTableContains"; + public static final String NATIVE_REMOVE = "__wurst_keyedTableRemove"; + + private LuaKeyedTable() { + } + + /** + * The {@code __wurst_} stub {@code f} should be lowered to on Lua, or null if {@code f} is not + * a compiler-owned KeyedTable operation. + */ + public static String nativeStubFor(ImFunction f) { + if (!(f.attrTrace() instanceof FuncDef fd) + || !fd.attrHasAnnotation(CompilerIntrinsics.ANNOTATION)) { + return null; + } + int params = f.getParameters().size(); + return switch (fd.getName()) { + case CREATE -> params == 0 && TypesHelper.isIntType(f.getReturnType()) + ? NATIVE_CREATE : null; + case ADD -> isKeyedPair(f) && f.getReturnType() instanceof ImVoid + ? NATIVE_ADD : null; + case REMOVE -> isKeyedPair(f) && f.getReturnType() instanceof ImVoid + ? NATIVE_REMOVE : null; + case CONTAINS -> isKeyedPair(f) && TypesHelper.isBoolType(f.getReturnType()) + ? NATIVE_CONTAINS : null; + default -> null; + }; + } + + /** + * A (table, key) parameter pair. Both are {@code int} at source level: on Jass everything is an + * integer anyway, and on Lua {@code castTo int} is the identity for class types, so the value + * arriving here is already a usable table key with reference identity. + */ + private static boolean isKeyedPair(ImFunction f) { + return f.getParameters().size() == 2 + && TypesHelper.isIntType(f.getParameters().get(0).getType()) + && TypesHelper.isIntType(f.getParameters().get(1).getType()); + } +} 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 b75bded09..51bfb04ef 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 @@ -146,6 +146,25 @@ public static void transform(ImProg prog, ImTranslator translator) { public void visit(ImFunctionCall call) { super.visit(call); ImFunction f = call.getFunc(); + // KeyedTable membership becomes a table keyed directly by the element. Done here, + // before optimization, rather than at emission: the inliner runs in between, and a + // call inlined before an emission-time rewrite would keep the hashtable body while + // a surviving one got the Lua table - mixing an integer class id with a table index + // for the same value. Replacing the call makes every site agree. + String keyedStub = LuaKeyedTable.nativeStubFor(f); + if (keyedStub != null) { + ImFunction replacement = specialNativeStubs.computeIfAbsent(keyedStub, + name -> createNativeStub(name, f)); + if (!deferredAdditions.contains(replacement)) { + deferredAdditions.add(replacement); + } + call.replaceBy(JassIm.ImFunctionCall( + call.attrTrace(), replacement, + JassIm.ImTypeArguments(), + call.getArguments().copy(), + false, CallType.NORMAL)); + return; + } if (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && isCompatGetHandleIdFunction(f)) { if (shouldRewriteGetHandleId(call)) { ImFunction replacement = specialNativeStubs.computeIfAbsent("__wurst_GetHandleId", diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java deleted file mode 100644 index a34e1f6e3..000000000 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaKeyedTable.java +++ /dev/null @@ -1,96 +0,0 @@ -package de.peeeq.wurstscript.translation.lua.translation; - -import de.peeeq.wurstscript.ast.FuncDef; -import de.peeeq.wurstscript.ast.WPackage; -import de.peeeq.wurstscript.jassIm.ImFunction; -import de.peeeq.wurstscript.jassIm.ImVar; -import de.peeeq.wurstscript.luaAst.LuaAst; -import de.peeeq.wurstscript.luaAst.LuaFunction; - -/** - * Lua bodies for the {@code KeyedTable} package: a set keyed directly by its element. - * - *

Jass has no hashing, so every keyed structure in the library bottoms out in the hashtable - * natives, which take a (parent, child) pair and are emitted on Lua as a nested table plus a nil - * check. A membership test therefore costs a call and two indexes where Lua needs one, and the - * element has to be squeezed through {@code castTo int} first. That is a Jass limitation carried - * into a runtime which is already a hash table, which AGENTS.md section 7 asks us not to do. - * - *

On Lua each keyed table is its own table and the element is the key, so the four operations - * become a single index each. {@code castTo int} is already the identity on Lua for class types - * (see {@code rewriteTypeCastingCompatFunction}), and handles are identity-cached tables, so the - * value arriving here is a usable key with reference identity either way. - * - *

Iteration is deliberately absent. Enumerating a Lua table needs {@code pairs()}, whose - * order depends on internal hash layout and therefore differs between clients - which desyncs a - * lockstep game. Anything that must be iterated needs a separately maintained insertion-ordered - * array, which is what SparseSet's dense half provides; this primitive is membership only. - * - *

The Jass path is the ordinary Wurst body of these functions and is left alone: correctness - * matters there, performance does not. - */ -final class LuaKeyedTable { - - /** Package whose functions get the native Lua bodies below. */ - private static final String PACKAGE = "KeyedTable"; - - static final String CREATE = "keyedTableCreate"; - static final String ADD = "keyedTableAdd"; - static final String CONTAINS = "keyedTableContains"; - static final String REMOVE = "keyedTableRemove"; - - private LuaKeyedTable() { - } - - /** The {@code KeyedTable} function {@code f} implements, or null if it is not one. */ - static String operationOf(ImFunction f) { - if (!(f.attrTrace() instanceof FuncDef fd)) { - return null; - } - if (!(fd.attrNearestPackage() instanceof WPackage p) || !PACKAGE.equals(p.getName())) { - return null; - } - String name = fd.getName(); - return CREATE.equals(name) || ADD.equals(name) || CONTAINS.equals(name) || REMOVE.equals(name) - ? name - : null; - } - - /** - * Replaces the body of a {@code KeyedTable} function with the Lua-native form. - * - * @return whether a body was written; false leaves the ordinary translation in place, so a - * signature this does not recognise keeps working rather than silently emitting nothing. - */ - static boolean rewrite(ImFunction f, LuaFunction lf, LuaTranslator tr) { - String op = operationOf(f); - if (op == null) { - return false; - } - if (CREATE.equals(op)) { - if (!f.getParameters().isEmpty()) { - return false; - } - lf.getBody().clear(); - lf.getBody().add(LuaAst.LuaLiteral("return {}")); - return true; - } - if (f.getParameters().size() != 2) { - return false; - } - String table = luaNameOf(f.getParameters().get(0), tr); - String key = luaNameOf(f.getParameters().get(1), tr); - lf.getBody().clear(); - switch (op) { - case ADD -> lf.getBody().add(LuaAst.LuaLiteral(table + "[" + key + "] = true")); - case REMOVE -> lf.getBody().add(LuaAst.LuaLiteral(table + "[" + key + "] = nil")); - case CONTAINS -> lf.getBody().add(LuaAst.LuaLiteral("return " + table + "[" + key + "] ~= nil")); - default -> throw new IllegalStateException("unhandled KeyedTable operation " + op); - } - return true; - } - - private static String luaNameOf(ImVar param, LuaTranslator tr) { - return tr.luaVar.getFor(param).getName(); - } -} 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 58fe81efc..5e17ada7f 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 @@ -271,6 +271,28 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("__wurst_enumDestructable_override = prev")); }); + // KeyedTable: membership keyed directly by the element, one index per operation. + // Lowered to these stubs by LuaKeyedTable/LuaNativeLowering before the inliner runs, so + // every call site agrees on the representation. No iteration is offered: pairs() order + // differs between clients and desyncs a lockstep game. + addNative("__wurst_keyedTableCreate", f -> + f.getBody().add(LuaAst.LuaLiteral("return {}"))); + addNative("__wurst_keyedTableAdd", f -> { + f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("t[k] = true")); + }); + addNative("__wurst_keyedTableContains", f -> { + f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("return t[k] ~= nil")); + }); + addNative("__wurst_keyedTableRemove", f -> { + f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("k", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("t[k] = nil")); + }); + addNative(Arrays.asList("InitHashtable", "__wurst_InitHashtable"), f -> f.getBody().add(LuaAst.LuaLiteral("return { __wurst_ht_int = {}, __wurst_ht_bool = {}, __wurst_ht_real = {}, __wurst_ht_str = {}, __wurst_ht_handle = {} }"))); 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 89ddcfcd0..b98676769 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 @@ -812,11 +812,6 @@ private void translateFunc(ImFunction f) { luaModel.add(lf); return; } - // KeyedTable is membership keyed directly by the element on Lua; see LuaKeyedTable. - if (LuaKeyedTable.rewrite(f, lf, this)) { - luaModel.add(lf); - return; - } if (f.hasFlag(FunctionFlagEnum.IS_VARARG)) { 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 d096fefd7..1c596b124 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 @@ -1876,13 +1876,13 @@ private static String[] keyedTableSource(String... usage) { java.util.List lines = new java.util.ArrayList<>(java.util.Arrays.asList( "package KeyedTable", "import Table", - "public function keyedTableCreate() returns int", + "@compilerintrinsic public function keyedTableCreate() returns int", " return (new Table()) castTo int", - "public function keyedTableAdd(int tbl, int key)", + "@compilerintrinsic public function keyedTableAdd(int tbl, int key)", " (tbl castTo Table).saveBoolean(key, true)", - "public function keyedTableContains(int tbl, int key) returns boolean", + "@compilerintrinsic public function keyedTableContains(int tbl, int key) returns boolean", " return (tbl castTo Table).loadBoolean(key)", - "public function keyedTableRemove(int tbl, int key)", + "@compilerintrinsic public function keyedTableRemove(int tbl, int key)", " (tbl castTo Table).removeBoolean(key)", "endpackage")); lines.addAll(java.util.Arrays.asList(usage)); @@ -1904,9 +1904,9 @@ public void keyedTableLowersToASingleLuaIndex() throws IOException { String compiled = Files.toString( new File("test-output/lua/LuaTranslationTests_keyedTableLowersToASingleLuaIndex.lua"), Charsets.UTF_8); - String add = getFunctionBody(compiled, "keyedTableAdd"); - String contains = getFunctionBody(compiled, "keyedTableContains"); - String create = getFunctionBody(compiled, "keyedTableCreate"); + String add = getFunctionBody(compiled, "__wurst_keyedTableAdd"); + String contains = getFunctionBody(compiled, "__wurst_keyedTableContains"); + String create = getFunctionBody(compiled, "__wurst_keyedTableCreate"); assertTrue("add should be a single table store, was: " + add, add.contains("] = true")); assertTrue("contains should be a single index, was: " + contains, contains.contains("] ~= nil")); @@ -1942,6 +1942,76 @@ public void keyedTableMembershipAgreesOnBothBackends() { "endpackage")); } + /** + * The lowering happens in IM before the inliner, so inlining cannot leave one call site on the + * hashtable body while another gets the Lua table - which would mix an integer class id with a + * table index for the same value and fail at runtime. + */ + @Test + public void keyedTableMembershipSurvivesInlining() throws IOException { + test().testLua(true).executeProg(true).inline().withStdLib().lines(keyedTableSource( + "package Test", + "import KeyedTable", + "init", + " let t = keyedTableCreate()", + " keyedTableAdd(t, 7)", + " if not keyedTableContains(t, 7)", + " testFail(\"7 was added but reported absent under -inline\")", + " keyedTableRemove(t, 7)", + " if keyedTableContains(t, 7)", + " testFail(\"7 was removed but reported present under -inline\")", + " testSuccess()", + "endpackage")); + + // Whether a given site actually gets inlined depends on the inliner's local-register + // budget, so assert the rule rather than the symptom: every call was replaced in IM, so + // the Table-backed originals are unreachable and collected. If any site had kept the old + // body, that function would still be here. + String compiled = Files.toString( + new File("test-output/lua/LuaTranslationTests_keyedTableMembershipSurvivesInlining.lua"), + Charsets.UTF_8); + assertFalse("no call site may keep the Table-backed keyedTableAdd", + compiled.contains("function keyedTableAdd(")); + assertFalse("no call site may keep the Table-backed keyedTableContains", + compiled.contains("function keyedTableContains(")); + assertTrue("membership must go through the lowered stub", + compiled.contains("__wurst_keyedTableContains")); + } + + /** + * The lowering is opt-in by declaration. A user package that merely shares these names keeps + * its own body, so nothing silently changes meaning under it. + */ + @Test + public void keyedTableWithoutTheAnnotationIsLeftAlone() throws IOException { + test().testLua(true).withStdLib().lines( + "package KeyedTable", + "import Table", + "public function keyedTableCreate() returns int", + " return (new Table()) castTo int", + "public function keyedTableAdd(int tbl, int key)", + " (tbl castTo Table).saveBoolean(key, true)", + "public function keyedTableContains(int tbl, int key) returns boolean", + " return (tbl castTo Table).loadBoolean(key)", + "endpackage", + "package Test", + "import KeyedTable", + "init", + " let t = keyedTableCreate()", + " keyedTableAdd(t, 7)", + " print(keyedTableContains(t, 7).toString())", + "endpackage"); + + String compiled = Files.toString( + new File("test-output/lua/LuaTranslationTests_keyedTableWithoutTheAnnotationIsLeftAlone.lua"), + Charsets.UTF_8); + + assertFalse("an unannotated package must not be lowered to the keyed-table stubs", + compiled.contains("__wurst_keyedTableAdd")); + assertTrue("it should keep its own Table-backed body", + getFunctionBody(compiled, "keyedTableAdd").contains("Table_Table_saveBoolean")); + } + /** * pairs() iteration order differs between clients and desyncs a lockstep game, so no emitted * Lua may contain it. Cheap to assert and worth keeping regardless of this feature. 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 ba2e5fc9f..9e17d5f24 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 @@ -117,6 +117,7 @@ class TestConfig { private boolean stopOnFirstError = true; private boolean runCompiletimeFunctions; private boolean optimize; + private boolean inline; private boolean testLua = false; private boolean luaOnly = false; private boolean uncheckedDispatch = false; @@ -161,6 +162,12 @@ public TestConfig executeTests(boolean b) { return this; } + /** Enables the IM inliner (-inline), which is a separate option from -opt. */ + TestConfig inline() { + this.inline = true; + return this; + } + TestConfig optimize() { this.optimize = true; return this; @@ -281,6 +288,9 @@ private CompilationResult testScript() { if (optimize) { runArgs = runArgs.with("-opt"); } + if (inline) { + runArgs = runArgs.with("-inline"); + } if (legacyJassTypeChecks) { runArgs.setLegacyJassTypeChecks(true); }