diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87a64e9f1..43c805ccb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,6 +84,14 @@ jobs: shell: bash run: ./gradlew packageSlimCompilerDist --no-daemon --stacktrace + - name: Prepare bundled Lua runtime (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + if [[ -f src/test/resources/lua53 ]]; then + chmod +x src/test/resources/lua53 + fi + - name: Run tests shell: bash run: ./gradlew test --no-daemon --stacktrace 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 808880eb5..5e34d47e6 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 @@ -12,6 +12,7 @@ public class ExprTranslation { public static final String TYPE_ID = "__typeId__"; public static final String WURST_SUPERTYPES = "__wurst_supertypes"; + private static final String WURST_ABORT_THREAD_SENTINEL = "__wurst_abort_thread"; public static LuaExpr translate(ImAlloc e, LuaTranslator tr) { ImClass c = e.getClazz().getClassDef(); @@ -51,7 +52,7 @@ public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) { LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(tempDots))))) ), // LuaAst.LuaLiteral("function(err) " + errorFuncName(tr) + "(tostring(err)) end") - LuaAst.LuaLiteral("function(err) xpcall(function() " + callErrorFunc(tr, "tostring(err)") + " end, function(err2) BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end") + LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end xpcall(function() " + callErrorFunc(tr, "tostring(err)") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end") // unfortunately BJDebugMsg(debug.traceback()) is not working ) ), @@ -73,6 +74,12 @@ private static String callErrorFunc(LuaTranslator tr, String msg) { public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { LuaFunction f = tr.luaFunc.getFor(e.getFunc()); + if ("I2S".equals(f.getName()) && isIntentionalThreadAbortCall(e)) { + return LuaAst.LuaExprFunctionCallByName("error", LuaAst.LuaExprlist( + LuaAst.LuaExprStringVal(WURST_ABORT_THREAD_SENTINEL), + LuaAst.LuaExprIntVal("0") + )); + } if (f.getName().equals(ImTranslator.$DEBUG_PRINT)) { f.setName("BJDebugMsg"); } else if (f.getName().equals("I2S")) { @@ -81,6 +88,27 @@ public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { return LuaAst.LuaExprFunctionCall(f, tr.translateExprList(e.getArguments())); } + private static boolean isIntentionalThreadAbortCall(ImFunctionCall e) { + if (e.getArguments().size() != 1) { + return false; + } + ImExpr arg = e.getArguments().get(0); + if (!(arg instanceof ImOperatorCall)) { + return false; + } + ImOperatorCall op = (ImOperatorCall) arg; + if (op.getOp() != WurstOperator.DIV_INT) { + return false; + } + if (op.getArguments().size() != 2) { + return false; + } + ImExpr left = op.getArguments().get(0); + ImExpr right = op.getArguments().get(1); + return (left instanceof ImIntVal && ((ImIntVal) left).getValI() == 1) + && (right instanceof ImIntVal && ((ImIntVal) right).getValI() == 0); + } + public static LuaExpr translate(ImInstanceof e, LuaTranslator tr) { return LuaAst.LuaExprFunctionCall(tr.instanceOfFunction, LuaAst.LuaExprlist( 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 64e2577b6..de6a17bab 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 @@ -50,7 +50,8 @@ public class LuaNatives { addNative(Collections.singletonList("S2I"), f -> { f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("return tonumber(x)")); + f.getBody().add(LuaAst.LuaLiteral("local m = string.match(tostring(x), \"^[%+%-]?%d+\")")); + f.getBody().add(LuaAst.LuaLiteral("if m then return tonumber(m) else return 0 end")); }); addNative("Player", f -> { @@ -63,7 +64,11 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("return x.id")); }); - addNative("GetRandomReal", f -> f.getBody().add(LuaAst.LuaLiteral("return math.random"))); + addNative("GetRandomReal", f -> { + f.getParams().add(LuaAst.LuaVariable("l", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("return l + math.random() * (h - l)")); + }); addNative("GetRandomInt", f -> { f.getParams().add(LuaAst.LuaVariable("l", LuaAst.LuaNoExpr())); @@ -90,16 +95,19 @@ public class LuaNatives { addNative("TriggerEvaluate", f -> { f.getParams().add(LuaAst.LuaVariable("t", LuaAst.LuaNoExpr())); f.getBody().add(LuaAst.LuaLiteral("for i,a in ipairs(t.actions) do a() end")); + f.getBody().add(LuaAst.LuaLiteral("return true")); }); addNative("R2I", f -> { f.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("return math.floor(x)")); + f.getBody().add(LuaAst.LuaLiteral("return math.modf(x)")); }); - addNative("InitHashtable", f -> f.getBody().add(LuaAst.LuaLiteral("return {}"))); + addNative(Arrays.asList("InitHashtable", "__wurst_InitHashtable"), f -> f.getBody().add(LuaAst.LuaLiteral("return {}"))); - addNative(Arrays.asList("SaveInteger", "SaveBoolean", "SaveReal", "SaveStr", "SaveBoolean"), f -> { + addNative(Arrays.asList( + "SaveInteger", "SaveBoolean", "SaveReal", "SaveStr", + "__wurst_SaveInteger", "__wurst_SaveBoolean", "__wurst_SaveReal", "__wurst_SaveStr"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); @@ -107,13 +115,63 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("if not h[p] then h[p] = {} end h[p][c] = i")); }); - addNative(Arrays.asList("LoadInteger", "LoadBoolean", "LoadReal", "LoadStr", "LoadBoolean"), f -> { + addNative(Arrays.asList( + "LoadInteger", "LoadBoolean", "LoadReal", "LoadStr", + "__wurst_LoadInteger", "__wurst_LoadBoolean", "__wurst_LoadReal", "__wurst_LoadStr"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); f.getBody().add(LuaAst.LuaLiteral("if not h[p] then return nil end return h[p][c]")); }); + addNative(Arrays.asList( + "HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", + "__wurst_HaveSavedInteger", "__wurst_HaveSavedBoolean", "__wurst_HaveSavedReal", "__wurst_HaveSavedString"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("return h[p] ~= nil and h[p][c] ~= nil")); + }); + + addNative(Arrays.asList("FlushChildHashtable", "__wurst_FlushChildHashtable"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("h[p] = nil")); + }); + + addNative(Arrays.asList("FlushParentHashtable", "__wurst_FlushParentHashtable"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("for k in pairs(h) do h[k] = nil end")); + }); + + addNative(Arrays.asList( + "RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString", + "__wurst_RemoveSavedInteger", "__wurst_RemoveSavedBoolean", "__wurst_RemoveSavedReal", "__wurst_RemoveSavedString"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("if h[p] then h[p][c] = nil end")); + }); + + addNative("typeIdToTypeName", f -> { + f.getParams().add(LuaAst.LuaVariable("typeId", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("return \"\"")); + }); + + addNative("maxTypeId", f -> { + f.getBody().add(LuaAst.LuaLiteral("return 0")); + }); + + addNative("instanceCount", f -> { + f.getParams().add(LuaAst.LuaVariable("typeId", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("return 0")); + }); + + addNative("maxInstanceCount", f -> { + f.getParams().add(LuaAst.LuaVariable("typeId", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("return 0")); + }); + } private static void addNative(String name, Consumer g) { 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 9d5688e6c..78e27590a 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 @@ -21,6 +21,14 @@ import static de.peeeq.wurstscript.translation.lua.translation.ExprTranslation.WURST_SUPERTYPES; public class LuaTranslator { + private static final Set HASHTABLE_NATIVE_NAMES = new HashSet<>(Arrays.asList( + "InitHashtable", + "SaveInteger", "SaveBoolean", "SaveReal", "SaveStr", + "LoadInteger", "LoadBoolean", "LoadReal", "LoadStr", + "HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", + "FlushChildHashtable", "FlushParentHashtable", + "RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString" + )); final ImProg prog; final LuaCompilationUnit luaModel; @@ -74,7 +82,7 @@ public LuaVariable initFor(ImVar a) { @Override public LuaFunction initFor(ImFunction a) { - String name = a.getName(); + String name = remapNativeName(a.getName()); if (!a.isExtern() && !a.isBj() && !a.isNative() && !isFixedEntryPoint(a)) { name = uniqueName(name); } else if (isFixedEntryPoint(a)) { @@ -126,9 +134,9 @@ public LuaMethod initFor(ImClass a) { LuaFunction stringConcatFunction = LuaAst.LuaFunction(uniqueName("stringConcat"), LuaAst.LuaParams(), LuaAst.LuaStatements()); - LuaFunction toIndexFunction = LuaAst.LuaFunction(uniqueName("objectToIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); + LuaFunction toIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectToIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); - LuaFunction fromIndexFunction = LuaAst.LuaFunction(uniqueName("objectFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); + LuaFunction fromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); LuaFunction instanceOfFunction = LuaAst.LuaFunction(uniqueName("isInstanceOf"), LuaAst.LuaParams(), LuaAst.LuaStatements()); @@ -163,6 +171,13 @@ public LuaTranslator(ImProg prog, ImTranslator imTr) { luaModel = LuaAst.LuaCompilationUnit(); } + private String remapNativeName(String name) { + if (HASHTABLE_NATIVE_NAMES.contains(name)) { + return "__wurst_" + name; + } + return name; + } + protected String uniqueName(String name) { int i = 0; String rname = name; @@ -176,10 +191,6 @@ protected String uniqueName(String name) { public LuaCompilationUnit translate() { collectPredefinedNames(); - RemoveGarbage.removeGarbage(prog); - prog.flatten(imTr); - - normalizeMethodNames(); normalizeFieldNames(); 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 d9beca124..895146198 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 @@ -50,10 +50,11 @@ public static void translate(ImSet s, List res, LuaTranslator tr) public static void translate(ImVarargLoop loop, List res, LuaTranslator tr) { LuaVariable loopVar = tr.luaVar.getFor(loop.getLoopVar()); // res.add(loopVar); + String argsName = tr.uniqueName("__args"); LuaVariable i = LuaAst.LuaVariable(tr.uniqueName("i"), LuaAst.LuaExprIntVal("0")); - res.add(LuaAst.LuaLiteral("local __args = table.pack(...)")); - res.add(LuaAst.LuaLiteral("for " + i.getName() + "=1,__args.n do")); - res.add(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(loopVar), LuaAst.LuaExprArrayAccess(LuaAst.LuaLiteral("__args"), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(i))))); + res.add(LuaAst.LuaLiteral("local " + argsName + " = table.pack(...)")); + res.add(LuaAst.LuaLiteral("for " + i.getName() + "=1," + argsName + ".n do")); + res.add(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(loopVar), LuaAst.LuaExprArrayAccess(LuaAst.LuaLiteral(argsName), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(i))))); tr.translateStatements(res, loop.getBody()); res.add(LuaAst.LuaLiteral("end")); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java new file mode 100644 index 000000000..4d0faff0d --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java @@ -0,0 +1,46 @@ +package tests.wurstscript.tests; + +import de.peeeq.wurstscript.luaAst.LuaAst; +import de.peeeq.wurstscript.luaAst.LuaFunction; +import de.peeeq.wurstscript.translation.lua.translation.LuaNatives; +import org.testng.annotations.Test; + +import static org.testng.AssertJUnit.assertTrue; + +public class LuaNativesTests { + + private static String renderNative(String name) { + LuaFunction f = LuaAst.LuaFunction(name, LuaAst.LuaParams(), LuaAst.LuaStatements()); + LuaNatives.get(f); + StringBuilder sb = new StringBuilder(); + f.print(sb, 0); + return sb.toString(); + } + + @Test + public void s2iUsesPrefixIntegerParsing() { + String rendered = renderNative("S2I"); + assertTrue(rendered.contains("string.match(tostring(x), \"^[%+%-]?%d+\")")); + assertTrue(rendered.contains("return tonumber(m)")); + } + + @Test + public void r2iUsesTruncationTowardZero() { + String rendered = renderNative("R2I"); + assertTrue(rendered.contains("return math.modf(x)")); + } + + @Test + public void getRandomRealUsesRangeFormula() { + String rendered = renderNative("GetRandomReal"); + assertTrue(rendered.contains("return l + math.random() * (h - l)")); + } + + @Test + public void triggerEvaluateReturnsBoolInFallback() { + String rendered = renderNative("TriggerEvaluate"); + assertTrue(rendered.contains("for i,a in ipairs(t.actions) do a() end")); + assertTrue(rendered.contains("return true")); + } +} + diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaSyntaxCheckTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaSyntaxCheckTests.java new file mode 100644 index 000000000..60152cb12 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaSyntaxCheckTests.java @@ -0,0 +1,46 @@ +package tests.wurstscript.tests; + +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class LuaSyntaxCheckTests { + + @Test + public void luacSyntaxErrorsAreDetected() throws Exception { + WurstScriptTest helper = new WurstScriptTest(); + String luacExecutable = invokeGetLuacExecutable(helper); + + File invalidLua = File.createTempFile("wurst-invalid-lua-", ".lua"); + invalidLua.deleteOnExit(); + java.nio.file.Files.writeString(invalidLua.toPath(), "function broken(\n", StandardCharsets.UTF_8); + + Method checkLuaSyntax = WurstScriptTest.class.getDeclaredMethod("checkLuaSyntax", String.class, File.class); + checkLuaSyntax.setAccessible(true); + try { + checkLuaSyntax.invoke(helper, luacExecutable, invalidLua); + fail("Expected syntax check to fail for invalid Lua source."); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + assertTrue(cause instanceof IOException, + "Expected IOException but got: " + (cause == null ? "null" : cause.getClass().getName())); + assertTrue(cause.getMessage() != null && cause.getMessage().contains("Lua syntax check failed"), + "Expected syntax check failure message, but got: " + cause.getMessage()); + } + } + + private String invokeGetLuacExecutable(WurstScriptTest helper) throws Exception { + Method getLuacExecutable = WurstScriptTest.class.getDeclaredMethod("getLuacExecutable"); + getLuacExecutable.setAccessible(true); + Object result = getLuacExecutable.invoke(helper); + return (String) result; + } +} + 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 aed5a833a..4949b413e 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 @@ -57,6 +57,12 @@ private void assertFunctionBodyContains(String output, String functionName, Stri assertTrue("Function " + functionName + " was not found.", found); } + private void assertDoesNotContainRegex(String output, String regex) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(output); + assertFalse("Pattern must not occur: " + regex, matcher.find()); + } + @Test public void testStdLib() throws IOException { test().testLua(true).withStdLib().lines( @@ -271,9 +277,224 @@ public void intCasting() throws IOException { assertFunctionBodyContains(compiled, "testEnum", "zeroInt = zeroEnum", true); assertFunctionBodyContains(compiled, "testEnum", "zeroEnum2 = zeroInt", true); // classes are cast with objectToIndex and objectFromIndex in lua - assertFunctionBodyContains(compiled, "testClass", "objectToIndex", true); - assertFunctionBodyContains(compiled, "testClass", "objectFromIndex", true); + assertFunctionBodyContains(compiled, "testClass", "__wurst_objectToIndex", true); + assertFunctionBodyContains(compiled, "testClass", "__wurst_objectFromIndex", true); assertFunctionBodyContains(compiled, "testClass", "cInt = cObj", false); assertFunctionBodyContains(compiled, "testClass", "cObj2 = cInt", false); } + + @Test + public void configEntrypointNotRenamedWhenUserHasConfigFunction() throws IOException { + test().testLua(true).lines( + "package Test", + "function config()", + " skip", + "init", + " config()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_configEntrypointNotRenamedWhenUserHasConfigFunction.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("function config(")); + assertFalse(compiled.contains("function config2(")); + assertTrue(compiled.contains("function config1(")); + assertTrue(compiled.contains("config1()")); + } + + @Test + public void objectIndexFunctionsDoNotCollideWithUserFunctions() throws IOException { + test().testLua(true).lines( + "package Test", + "function objectToIndex(int i) returns int", + " return i + 1", + "function objectFromIndex(int i) returns int", + " return i - 1", + "native takesInt(int i)", + "class C", + "function testClass()", + " let cObj = new C()", + " let cInt = cObj castTo int", + " let cObj2 = cInt castTo C", + " takesInt(objectToIndex(3))", + " takesInt(objectFromIndex(5))", + " if cObj2 == null", + " skip", + "init", + " testClass()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_objectIndexFunctionsDoNotCollideWithUserFunctions.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("function objectToIndex(")); + assertTrue(compiled.contains("function objectFromIndex(")); + assertFunctionBodyContains(compiled, "testClass", "__wurst_objectToIndex", true); + assertFunctionBodyContains(compiled, "testClass", "__wurst_objectFromIndex", true); + } + + @Test + public void oldGenericsCastingDoesNotUseGetHandleId() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "class C", + "native takesInt(int i)", + "native takesC(C c)", + "function testCast()", + " let cObj = new C()", + " let cInt = cObj castTo int", + " let cObj2 = cInt castTo C", + " takesInt(cInt)", + " takesC(cObj2)", + "init", + " testCast()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_oldGenericsCastingDoesNotUseGetHandleId.lua"), Charsets.UTF_8); + assertDoesNotContainRegex(compiled, "\\bGetHandleId\\("); + assertFunctionBodyContains(compiled, "testCast", "__wurst_objectToIndex", true); + assertFunctionBodyContains(compiled, "testCast", "__wurst_objectFromIndex", true); + } + + @Test + public void reflectionNativesStubbedForLua() throws IOException { + test().testLua(true).lines( + "package Test", + "native typeIdToTypeName(int typeId) returns string", + "native maxTypeId() returns int", + "native instanceCount(int typeId) returns int", + "native maxInstanceCount(int typeId) returns int", + "class A", + "init", + " let name = typeIdToTypeName(A.typeId)", + " let m = maxTypeId()", + " let c = instanceCount(A.typeId)", + " let mc = maxInstanceCount(A.typeId)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_reflectionNativesStubbedForLua.lua"), Charsets.UTF_8); + assertFalse(compiled.contains("The native 'typeIdToTypeName' is not implemented.")); + assertFalse(compiled.contains("The native 'maxTypeId' is not implemented.")); + assertFalse(compiled.contains("The native 'instanceCount' is not implemented.")); + assertFalse(compiled.contains("The native 'maxInstanceCount' is not implemented.")); + assertTrue(compiled.contains("typeIdToTypeName = function")); + assertTrue(compiled.contains("maxTypeId = function")); + assertTrue(compiled.contains("instanceCount = function")); + assertTrue(compiled.contains("maxInstanceCount = function")); + } + + @Test + public void reflectionNativeStubBodiesReturnDefaults() throws IOException { + test().testLua(true).lines( + "package Test", + "native typeIdToTypeName(int typeId) returns string", + "native maxTypeId() returns int", + "native instanceCount(int typeId) returns int", + "native maxInstanceCount(int typeId) returns int", + "class A", + "init", + " let _n = typeIdToTypeName(A.typeId)", + " let _m = maxTypeId()", + " let _c = instanceCount(A.typeId)", + " let _mc = maxInstanceCount(A.typeId)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_reflectionNativeStubBodiesReturnDefaults.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("typeIdToTypeName = function")); + assertTrue(compiled.contains("return \"\"")); + assertTrue(compiled.contains("maxTypeId = function")); + assertTrue(compiled.contains("instanceCount = function")); + assertTrue(compiled.contains("maxInstanceCount = function")); + assertTrue(compiled.contains("return 0")); + } + + @Test + public void reflectionNativeStubsAreGuardedByExistingDefinitions() throws IOException { + test().testLua(true).lines( + "package Test", + "native typeIdToTypeName(int typeId) returns string", + "native maxTypeId() returns int", + "native instanceCount(int typeId) returns int", + "native maxInstanceCount(int typeId) returns int", + "class A", + "init", + " let _n = typeIdToTypeName(A.typeId)", + " let _m = maxTypeId()", + " let _c = instanceCount(A.typeId)", + " let _mc = maxInstanceCount(A.typeId)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_reflectionNativeStubsAreGuardedByExistingDefinitions.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("if typeIdToTypeName then")); + assertTrue(compiled.contains("if maxTypeId then")); + assertTrue(compiled.contains("if instanceCount then")); + assertTrue(compiled.contains("if maxInstanceCount then")); + } + + @Test + public void stdLibInitUsesTriggerEvaluateGuardInMain() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibInitUsesTriggerEvaluateGuardInMain.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("if not(TriggerEvaluate(")); + assertTrue(compiled.contains("TriggerClearConditions")); + } + + @Test + public void stdLibDoesNotEmitTimerBjNatives() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibDoesNotEmitTimerBjNatives.lua"), Charsets.UTF_8); + assertDoesNotContainRegex(compiled, "\\bCreateTimerBJ\\("); + assertDoesNotContainRegex(compiled, "\\bStartTimerBJ\\("); + assertDoesNotContainRegex(compiled, "\\bGetLastCreatedTimerBJ\\("); + assertFalse(compiled.contains("bj_lastStartedTimer")); + } + + @Test + public void stdLibDoesNotEmitWar3HashtableNatives() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibDoesNotEmitWar3HashtableNatives.lua"), Charsets.UTF_8); + assertDoesNotContainRegex(compiled, "\\bInitHashtable\\("); + assertDoesNotContainRegex(compiled, "\\bSaveInteger\\("); + assertDoesNotContainRegex(compiled, "\\bSaveBoolean\\("); + assertDoesNotContainRegex(compiled, "\\bSaveReal\\("); + assertDoesNotContainRegex(compiled, "\\bSaveStr\\("); + assertDoesNotContainRegex(compiled, "\\bLoadInteger\\("); + assertDoesNotContainRegex(compiled, "\\bLoadBoolean\\("); + assertDoesNotContainRegex(compiled, "\\bLoadReal\\("); + assertDoesNotContainRegex(compiled, "\\bLoadStr\\("); + assertDoesNotContainRegex(compiled, "\\bFlushChildHashtable\\("); + } + + @Test + public void i2sDivisionByZeroCrashTrapUsesAbortSentinelInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_i2sDivisionByZeroCrashTrapUsesAbortSentinelInLua.lua"), Charsets.UTF_8); + assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*//\\s*0\\s*\\)"); + assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*/\\s*0\\s*\\)"); + assertDoesNotContainRegex(compiled, "I2S\\s*\\(\\s*1\\s*/\\s*0\\s*\\)"); + assertDoesNotContainRegex(compiled, "I2S\\s*\\(\\s*1\\s*//\\s*0\\s*\\)"); + assertTrue(compiled.contains("__wurst_abort_thread")); + assertDoesNotContainRegex(compiled, "error\\s*\\(\\s*\"[^\"]*divide by zero[^\"]*\""); + } + + @Test + public void luaErrorWrapperIgnoresAbortSentinel() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "function f()", + " skip", + "init", + " f()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaErrorWrapperIgnoresAbortSentinel.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("if err == \"__wurst_abort_thread\" then return end")); + assertTrue(compiled.contains("if err2 == \"__wurst_abort_thread\" then return end")); + } + } 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 50c45edb7..911aaffff 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 @@ -24,6 +24,7 @@ import de.peeeq.wurstscript.jassinterpreter.TestSuccessException; import de.peeeq.wurstscript.jassprinter.JassPrinter; import de.peeeq.wurstscript.luaAst.LuaCompilationUnit; +import de.peeeq.wurstscript.luaAst.*; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.translation.imtranslation.RecycleCodeGeneratorQueue; import de.peeeq.wurstscript.utils.Utils; @@ -32,9 +33,12 @@ import org.testng.annotations.BeforeMethod; import java.io.*; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.testng.Assert.fail; @@ -42,6 +46,12 @@ public class WurstScriptTest { public static final String TEST_OUTPUT_PATH = "./test-output/"; + private static volatile String resolvedLuaExecutable; + private static volatile String resolvedLuacExecutable; + private static volatile String extractedLuaWin; + private static volatile String extractedLuaUnix; + private static volatile String extractedLuacWin; + private static volatile String extractedLuacUnix; protected boolean testOptimizer() { return true; @@ -70,6 +80,7 @@ class TestConfig { private boolean stopOnFirstError = true; private boolean runCompiletimeFunctions; private boolean testLua = false; + private boolean luaOnly = false; private boolean uncheckedDispatch = false; TestConfig(String name) { @@ -235,16 +246,18 @@ private CompilationResult testScript() { } - // translate with different options: - testWithoutInliningAndOptimization(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + if (!(testLua && luaOnly)) { + // translate with different options: + testWithoutInliningAndOptimization(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); - testWithLocalOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + testWithLocalOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); - testWithInlining(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + testWithInlining(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); - testWithInliningAndOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + testWithInliningAndOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); - testWithInliningAndOptimizationsAndStacktraces(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + testWithInliningAndOptimizationsAndStacktraces(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + } if (testLua) { // test lua translation @@ -302,6 +315,12 @@ public TestConfig runCompiletimeFunctions(boolean b) { public TestConfig testLua(boolean b) { this.testLua = b; + this.luaOnly = b; + return this; + } + + public TestConfig luaOnly(boolean b) { + this.luaOnly = b; return this; } } @@ -460,6 +479,7 @@ private void translateAndTestLua(String name, boolean executeProg, WurstGui gui, compiler.runCompiletime(new WurstProjectConfigData(), false, false); LuaCompilationUnit luaCode = compiler.transformProgToLua(); + checkLuaRootPurity(luaCode); StringBuilder sb = new StringBuilder(); luaCode.print(sb, 0); @@ -473,11 +493,14 @@ private void translateAndTestLua(String name, boolean executeProg, WurstGui gui, FileUtils.write(luaScript, luaFile); // run with lua -l SimpleStatementTests_testIf1 -e 'main()' + String luacExecutable = getLuacExecutable(); + checkLuaSyntax(luacExecutable, luaFile); if (executeProg) { + String luaExecutable = getLuaExecutable(); String line; String[] args = { - "lua", + luaExecutable, "-l", luaFile.getPath().replace(".lua", ""), "-e", "main()" }; @@ -519,6 +542,333 @@ private void translateAndTestLua(String name, boolean executeProg, WurstGui gui, } + private void checkLuaSyntax(String luacExecutable, File luaFile) throws IOException { + Path outFile = java.nio.file.Files.createTempFile("wurst-luac-", ".out"); + outFile.toFile().deleteOnExit(); + ProcessBuilder pb = new ProcessBuilder( + luacExecutable, + "-o", + outFile.toAbsolutePath().toString(), + luaFile.getPath() + ); + Process p = pb.start(); + StringBuilder stdout = new StringBuilder(); + StringBuilder stderr = new StringBuilder(); + Thread outCollector = collectStreamAsync(p.getInputStream(), stdout); + Thread errCollector = collectStreamAsync(p.getErrorStream(), stderr); + boolean finished; + try { + finished = p.waitFor(20, TimeUnit.SECONDS); + if (!finished) { + p.destroyForcibly(); + throw new IOException("Lua syntax check timed out for " + luaFile.getName()); + } + outCollector.join(); + errCollector.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during Lua syntax check for " + luaFile.getName(), e); + } + if (p.exitValue() != 0 || stderr.length() > 0) { + throw new IOException("Lua syntax check failed for " + luaFile.getName() + " using '" + luacExecutable + "'.\n" + + "exit=" + p.exitValue() + "\n" + + (stderr.length() > 0 ? "stderr:\n" + stderr : "") + + (stdout.length() > 0 ? "\nstdout:\n" + stdout : "")); + } + } + + private Thread collectStreamAsync(InputStream stream, StringBuilder out) { + Thread t = new Thread(() -> { + try (BufferedReader input = new BufferedReader(new InputStreamReader(stream))) { + String line; + while ((line = input.readLine()) != null) { + out.append(line).append("\n"); + } + } catch (IOException ignored) { + } + }); + t.setDaemon(true); + t.start(); + return t; + } + + private String getLuaExecutable() { + if (resolvedLuaExecutable != null) { + return resolvedLuaExecutable; + } + + File bundledLuaWin = new File("src/test/resources/lua53.exe"); + File bundledLuaUnix = new File("src/test/resources/lua53"); + String osName = System.getProperty("os.name", "").toLowerCase(); + boolean isWindows = osName.contains("win"); + + List candidates = new ArrayList<>(); + if (isWindows) { + if (bundledLuaWin.exists()) { + candidates.add(bundledLuaWin.getPath()); + } + String cpWin = getOrExtractBundledLuaFromClasspath("lua53.exe", true, false); + if (cpWin != null) { + candidates.add(cpWin); + } + if (bundledLuaUnix.exists()) { + candidates.add(bundledLuaUnix.getPath()); + } + candidates.add("lua53.exe"); + candidates.add("lua"); + } else { + if (bundledLuaUnix.exists()) { + // best effort in case execute bit was lost by checkout settings + // (e.g. core.filemode false on some environments) + bundledLuaUnix.setExecutable(true); + if (bundledLuaUnix.canExecute()) { + candidates.add(bundledLuaUnix.getPath()); + } + } + String cpUnix = getOrExtractBundledLuaFromClasspath("lua53", false, false); + if (cpUnix != null) { + candidates.add(cpUnix); + } + candidates.add("lua53"); + candidates.add("lua"); + } + + for (String candidate : candidates) { + if (isWorkingLuaExecutable(candidate)) { + resolvedLuaExecutable = candidate; + return candidate; + } + } + + throw new IllegalStateException( + "No working Lua executable found. Tried: " + String.join(", ", candidates) + ); + } + + private String getLuacExecutable() { + if (resolvedLuacExecutable != null) { + return resolvedLuacExecutable; + } + + File bundledLuacWin = new File("src/test/resources/luac53.exe"); + File bundledLuacUnix = new File("src/test/resources/luac53"); + String osName = System.getProperty("os.name", "").toLowerCase(); + boolean isWindows = osName.contains("win"); + + List candidates = new ArrayList<>(); + if (isWindows) { + if (bundledLuacWin.exists()) { + candidates.add(bundledLuacWin.getPath()); + } + // allow legacy typo filename if present + File legacyWin = new File("src/test/resources/luac32.exe"); + if (legacyWin.exists()) { + candidates.add(legacyWin.getPath()); + } + String cpWin = getOrExtractBundledLuaFromClasspath("luac53.exe", true, true); + if (cpWin != null) { + candidates.add(cpWin); + } + String cpLegacyWin = getOrExtractBundledLuaFromClasspath("luac32.exe", true, true); + if (cpLegacyWin != null) { + candidates.add(cpLegacyWin); + } + candidates.add("luac53.exe"); + candidates.add("luac.exe"); + candidates.add("luac"); + } else { + if (bundledLuacUnix.exists()) { + bundledLuacUnix.setExecutable(true); + if (bundledLuacUnix.canExecute()) { + candidates.add(bundledLuacUnix.getPath()); + } + } + String cpUnix = getOrExtractBundledLuaFromClasspath("luac53", false, true); + if (cpUnix != null) { + candidates.add(cpUnix); + } + candidates.add("luac53"); + candidates.add("luac"); + } + + for (String candidate : candidates) { + if (isWorkingLuacExecutable(candidate)) { + resolvedLuacExecutable = candidate; + return candidate; + } + } + + throw new IllegalStateException( + "No working luac executable found. Tried: " + String.join(", ", candidates) + ); + } + + private boolean isWorkingLuaExecutable(String executable) { + ProcessBuilder pb = new ProcessBuilder(executable, "-e", "os.exit(0)"); + Process p; + try { + p = pb.start(); + } catch (IOException e) { + return false; + } + try { + boolean finished = p.waitFor(5, TimeUnit.SECONDS); + if (!finished) { + p.destroyForcibly(); + return false; + } + return p.exitValue() == 0; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + private boolean isWorkingLuacExecutable(String executable) { + ProcessBuilder pb = new ProcessBuilder(executable, "-v"); + Process p; + try { + p = pb.start(); + } catch (IOException e) { + return false; + } + try { + boolean finished = p.waitFor(5, TimeUnit.SECONDS); + if (!finished) { + p.destroyForcibly(); + return false; + } + return p.exitValue() == 0; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + private String getOrExtractBundledLuaFromClasspath(String resourceName, boolean windowsBinary, boolean luacBinary) { + try { + if (windowsBinary && luacBinary && extractedLuacWin != null) { + return extractedLuacWin; + } + if (windowsBinary && !luacBinary && extractedLuaWin != null) { + return extractedLuaWin; + } + if (!windowsBinary && luacBinary && extractedLuacUnix != null) { + return extractedLuacUnix; + } + if (!windowsBinary && !luacBinary && extractedLuaUnix != null) { + return extractedLuaUnix; + } + InputStream in = WurstScriptTest.class.getClassLoader().getResourceAsStream(resourceName); + if (in == null) { + return null; + } + String suffix = windowsBinary ? ".exe" : ""; + Path temp = java.nio.file.Files.createTempFile("wurst-lua53-", suffix); + temp.toFile().deleteOnExit(); + try (InputStream src = in) { + java.nio.file.Files.copy(src, temp, StandardCopyOption.REPLACE_EXISTING); + } + if (!windowsBinary) { + temp.toFile().setExecutable(true); + } + String path = temp.toAbsolutePath().toString(); + if (windowsBinary && luacBinary) { + extractedLuacWin = path; + } else if (windowsBinary) { + extractedLuaWin = path; + } else if (luacBinary) { + extractedLuacUnix = path; + } else { + extractedLuaUnix = path; + } + return path; + } catch (IOException e) { + return null; + } + } + + private void checkLuaRootPurity(LuaCompilationUnit luaCode) { + Set forbiddenRootCalls = Set.of( + "CreateTrigger", "CreateTimer", "CreateUnit", "DestroyTrigger", "TimerStart", + "CreateTimerBJ", "StartTimerBJ", "GetLastCreatedTimerBJ", + "InitHashtable", "SaveInteger", "SaveBoolean", "SaveReal", "SaveStr", + "LoadInteger", "LoadBoolean", "LoadReal", "LoadStr", "FlushChildHashtable" + ); + + for (LuaStatement stmt : luaCode) { + if (stmt instanceof LuaVariable) { + LuaExprOpt initialValue = ((LuaVariable) stmt).getInitialValue(); + if (initialValue instanceof LuaExpr) { + assertNoForbiddenRootCall((LuaExpr) initialValue, forbiddenRootCalls); + } + } else if (stmt instanceof LuaAssignment) { + assertNoForbiddenRootCall(((LuaAssignment) stmt).getRight(), forbiddenRootCalls); + } else if (stmt instanceof LuaExprFunctionCallByName) { + String funcName = ((LuaExprFunctionCallByName) stmt).getFuncName(); + if (forbiddenRootCalls.contains(funcName)) { + throw new Error("Lua root purity violation: forbidden root call to " + funcName); + } + } else if (stmt instanceof LuaExprFunctionCall) { + String funcName = ((LuaExprFunctionCall) stmt).getFunc().getName(); + if (forbiddenRootCalls.contains(funcName)) { + throw new Error("Lua root purity violation: forbidden root call to " + funcName); + } + } + } + } + + private void assertNoForbiddenRootCall(LuaExpr expr, Set forbiddenRootCalls) { + if (expr instanceof LuaExprFunctionCallByName) { + String name = ((LuaExprFunctionCallByName) expr).getFuncName(); + if (forbiddenRootCalls.contains(name)) { + throw new Error("Lua root purity violation: forbidden root call to " + name); + } + for (LuaExpr arg : ((LuaExprFunctionCallByName) expr).getArguments()) { + assertNoForbiddenRootCall(arg, forbiddenRootCalls); + } + } else if (expr instanceof LuaExprFunctionCall) { + String name = ((LuaExprFunctionCall) expr).getFunc().getName(); + if (forbiddenRootCalls.contains(name)) { + throw new Error("Lua root purity violation: forbidden root call to " + name); + } + for (LuaExpr arg : ((LuaExprFunctionCall) expr).getArguments()) { + assertNoForbiddenRootCall(arg, forbiddenRootCalls); + } + } else if (expr instanceof LuaExprFunctionCallE) { + LuaExprFunctionCallE call = (LuaExprFunctionCallE) expr; + assertNoForbiddenRootCall(call.getFuncExpr(), forbiddenRootCalls); + for (LuaExpr arg : call.getArguments()) { + assertNoForbiddenRootCall(arg, forbiddenRootCalls); + } + } else if (expr instanceof LuaExprArrayAccess) { + LuaExprArrayAccess a = (LuaExprArrayAccess) expr; + assertNoForbiddenRootCall(a.getLeft(), forbiddenRootCalls); + for (LuaExpr index : a.getIndexes()) { + assertNoForbiddenRootCall(index, forbiddenRootCalls); + } + } else if (expr instanceof LuaExprBinary) { + LuaExprBinary b = (LuaExprBinary) expr; + assertNoForbiddenRootCall(b.getLeftExpr(), forbiddenRootCalls); + assertNoForbiddenRootCall(b.getRight(), forbiddenRootCalls); + } else if (expr instanceof LuaExprUnary) { + assertNoForbiddenRootCall(((LuaExprUnary) expr).getRight(), forbiddenRootCalls); + } else if (expr instanceof LuaExprFieldAccess) { + assertNoForbiddenRootCall(((LuaExprFieldAccess) expr).getReceiver(), forbiddenRootCalls); + } else if (expr instanceof LuaTableConstructor) { + for (LuaTableField field : ((LuaTableConstructor) expr).getTableFields()) { + if (field instanceof LuaTableNamedField) { + assertNoForbiddenRootCall(((LuaTableNamedField) field).getVal(), forbiddenRootCalls); + } else if (field instanceof LuaTableExprField) { + assertNoForbiddenRootCall(((LuaTableExprField) field).getFieldKey(), forbiddenRootCalls); + assertNoForbiddenRootCall(((LuaTableExprField) field).getVal(), forbiddenRootCalls); + } else if (field instanceof LuaTableSingleField) { + assertNoForbiddenRootCall(((LuaTableSingleField) field).getVal(), forbiddenRootCalls); + } + } + } + } + private void translateAndTest(String name, boolean executeProg, boolean executeTests, WurstGui gui, WurstCompilerJassImpl compiler, WurstModel model, boolean executeProgOnlyAfterTransforms) throws Error { diff --git a/de.peeeq.wurstscript/src/test/resources/luac53 b/de.peeeq.wurstscript/src/test/resources/luac53 new file mode 100644 index 000000000..7e0cf5b2c Binary files /dev/null and b/de.peeeq.wurstscript/src/test/resources/luac53 differ diff --git a/de.peeeq.wurstscript/src/test/resources/luac53.exe b/de.peeeq.wurstscript/src/test/resources/luac53.exe new file mode 100644 index 000000000..12d14a13f Binary files /dev/null and b/de.peeeq.wurstscript/src/test/resources/luac53.exe differ