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 50f10fa97..3c1c92871 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 @@ -53,42 +53,7 @@ public static LuaExpr translate(ImDealloc e, LuaTranslator tr) { } public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) { -// return LuaAst.LuaExprFuncRef(tr.luaFunc.getFor(e.getFunc())); -// alternative: use xpcall to get stacktraces (did not work) - boolean returnsValue = !(e.getFunc().getReturnType() instanceof ImVoid); - LuaVariable dots = LuaAst.LuaVariable("...", LuaAst.LuaNoExpr()); - LuaStatements callbackBody = LuaAst.LuaStatements(); - if (returnsValue) { - LuaVariable tempRes = LuaAst.LuaVariable("tempRes", LuaAst.LuaExprNull()); - callbackBody.add(tempRes); - callbackBody.add(LuaAst.LuaExprFunctionCallByName("xpcall", - LuaAst.LuaExprlist( - LuaAst.LuaExprFunctionAbstraction( - LuaAst.LuaParams(dots.copy()), - LuaAst.LuaStatements( - LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(tempRes), - LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(dots.copy()))))) - ), - LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)", "in lua callback error handler") + " 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"), - LuaAst.LuaExprVarAccess(dots.copy()) - ) - )); - callbackBody.add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(tempRes))); - } else { - callbackBody.add(LuaAst.LuaExprFunctionCallByName("xpcall", - LuaAst.LuaExprlist( - LuaAst.LuaExprFunctionAbstraction( - LuaAst.LuaParams(dots.copy()), - LuaAst.LuaStatements( - LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(dots.copy()))) - ) - ), - LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)", "in lua callback error handler") + " 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"), - LuaAst.LuaExprVarAccess(dots.copy()) - ) - )); - } - return LuaAst.LuaExprFunctionAbstraction(LuaAst.LuaParams(dots), callbackBody); + return LuaAst.LuaExprFuncRef(tr.callbackAdapterFor(e.getFunc())); } static String callErrorFunc(LuaTranslator tr, String msg) { 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 a7180712e..29188d783 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 @@ -137,6 +137,8 @@ private ImProg getProg() { List tupleEqualsFuncs = new ArrayList<>(); List tupleCopyFuncs = new ArrayList<>(); + private final Map callbackAdapters = new IdentityHashMap<>(); + private LuaFunction callbackErrorHandler; // Array-default infrastructure (metatables/helper functions) shared across // every array of a given entry type, instead of allocated per array @@ -385,6 +387,68 @@ public LuaCompilationUnit translate() { return luaModel; } + /** + * Function references need an xpcall boundary, but the boundary is a property of the referenced + * function rather than of each expression which names it. Emit one reusable adapter per target + * so evaluating a function reference performs no closure allocation. + */ + LuaFunction callbackAdapterFor(ImFunction target) { + LuaFunction existing = callbackAdapters.get(target); + if (existing != null) { + return existing; + } + + 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()); + callbackAdapters.put(target, adapter); + + LuaFunction errorHandler = callbackErrorHandler(); + LuaExprFunctionCallByName xpcall = LuaAst.LuaExprFunctionCallByName("xpcall", + LuaAst.LuaExprlist( + LuaAst.LuaExprFuncRef(targetLua), + LuaAst.LuaExprFuncRef(errorHandler), + LuaAst.LuaExprVarAccess(dots.copy()))); + if (target.getReturnType() instanceof ImVoid) { + adapter.getBody().add(xpcall); + } else { + // Keep exactly the first callback result. Returning select(2, xpcall(...)) directly + // could leak additional Lua return values into a surrounding argument list. + LuaVariable ignored = LuaAst.LuaVariable("_", LuaAst.LuaNoExpr()); + LuaVariable result = LuaAst.LuaVariable("result", LuaAst.LuaNoExpr()); + adapter.getBody().add(ignored); + adapter.getBody().add(result); + adapter.getBody().add(LuaAst.LuaAssignment( + LuaAst.LuaLiteral("_, result"), xpcall)); + adapter.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(result))); + } + luaModel.add(adapter); + return adapter; + } + + private LuaFunction callbackErrorHandler() { + if (callbackErrorHandler != null) { + return callbackErrorHandler; + } + LuaVariable err = LuaAst.LuaVariable("err", LuaAst.LuaNoExpr()); + callbackErrorHandler = LuaAst.LuaFunction(uniqueName("__wurst_callback_error"), + LuaAst.LuaParams(err), LuaAst.LuaStatements()); + callbackErrorHandler.getBody().add(LuaAst.LuaLiteral( + "if err == \"" + ExprTranslation.WURST_ABORT_THREAD_SENTINEL + "\" then return end")); + callbackErrorHandler.getBody().add(LuaAst.LuaLiteral( + "BJDebugMsg(\"lua callback error: \" .. tostring(err))")); + callbackErrorHandler.getBody().add(LuaAst.LuaLiteral( + "xpcall(function() " + ExprTranslation.callErrorFunc(this, "tostring(err)", + "in lua callback error handler") + + " end, function(err2) if err2 == \"" + ExprTranslation.WURST_ABORT_THREAD_SENTINEL + + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2))" + + " BJDebugMsg(\"while reporting: \" .. tostring(err)) end)")); + luaModel.add(callbackErrorHandler); + return callbackErrorHandler; + } + /** * Rejects calls/references to functions that an earlier optimizer pass * detached from the IM program. Without this invariant the Lua printer 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 1405c74ba..211830688 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 @@ -128,6 +128,15 @@ private List uniqueMatches(String output, String regex, int group) { return result; } + private int countMatches(String output, String regex) { + Matcher matcher = Pattern.compile(regex).matcher(output); + int result = 0; + while (matcher.find()) { + result++; + } + return result; + } + private String singleMatch(String output, String regex, int group) { Matcher matcher = Pattern.compile(regex).matcher(output); assertTrue("Expected pattern to occur: " + regex, matcher.find()); @@ -173,6 +182,11 @@ private String compileLuaWithRunArgs(String testName, boolean withStdLib, String private String compileLuaWithCUs(String testName, boolean withStdLib, List extraCUs, String... lines) { RunArgs runArgs = new RunArgs().with("-lua", "-inline", "-localOptimizations", "-stacktraces"); + return compileLuaWithCUs(testName, withStdLib, extraCUs, runArgs, lines); + } + + private String compileLuaWithCUs(String testName, boolean withStdLib, List extraCUs, + RunArgs runArgs, String... lines) { WurstGui gui = new WurstGuiCliImpl(); WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(null, gui, null, runArgs); List inputs = new ArrayList<>(); @@ -2475,12 +2489,72 @@ public void luaFunctionRefWrapperForwardsVarargs() throws IOException { " ForForce(f, () -> skip)" ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaFunctionRefWrapperForwardsVarargs.lua"), Charsets.UTF_8); - assertTrue(compiled.contains("xpcall(function (...)")); + assertContainsRegex(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(", ...)")); assertFalse(compiled.contains("local temp = ...")); assertFalse(compiled.contains("ForForce(f, function (...) \n\t\t\tlocal tempRes")); } + @Test + public void luaFunctionRefsReuseOneAdapterAndPreserveSingleReturn() { + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaFunctionRefsReuseOneAdapterAndPreserveSingleReturn", + false, + Collections.emptyList(), + new RunArgs().with("-lua", "-inline", "-localOptimizations"), + "type boolexpr extends handle", + "package Test", + "@extern native Condition(code callback) returns boolexpr", + "function predicate() returns boolean", + " return true", + "init", + " let first = Condition(function predicate)", + " let second = Condition(function predicate)" + ); + + List adapters = uniqueMatches(compiled, + "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); + assertEquals("both Condition calls must reference the cached adapter", 2, + countMatches(compiled, "Condition\\(" + Pattern.quote(adapter) + "\\)")); + String adapterBody = getFunctionBody(compiled, adapter); + assertTrue(adapterBody.contains("_, result = xpcall(predicate,")); + assertTrue(adapterBody.contains("return result")); + assertFalse("callback sites must not allocate anonymous wrappers", compiled.contains("Condition(function (")); + } + + @Test + public void luaFunctionRefAdapterTracksLateClassFunctionRename() { + String compiled = compileLuaWithCUs( + "LuaTranslationTests_luaFunctionRefAdapterTracksLateClassFunctionRename", + false, + Collections.emptyList(), + new RunArgs().with("-lua"), + "package Test", + "@extern native consume(code callback)", + "@extern native CallbackOwner_staticCallback()", + "class CallbackOwner", + " function start()", + " consume(function staticCallback)", + " private static function staticCallback()", + " consume(function staticCallback)", + "init", + " CallbackOwner_staticCallback()", + " new CallbackOwner().start()" + ); + + String callbackName = singleMatch(compiled, + "function\\s+(CallbackOwner_[A-Za-z0-9_]*staticCallback[A-Za-z0-9_]*)\\(\\)", 1); + assertTrue("adapter must track the class callback's final name:\n" + compiled, + compiled.contains("xpcall(" + callbackName + ",")); + assertFalse(compiled.contains("xpcall(staticCallback,")); + } + @Test public void luaFunctionRefStacktraceHandlerUsesWurstStackPosition() throws IOException { CU errorHandling = new CU("ErrorHandling.wurst", String.join("\n",