Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ private ImProg getProg() {

List<ExprTranslation.TupleFunc> tupleEqualsFuncs = new ArrayList<>();
List<ExprTranslation.TupleFunc> tupleCopyFuncs = new ArrayList<>();
private final Map<ImFunction, LuaFunction> 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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ private List<String> 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());
Expand Down Expand Up @@ -173,6 +182,11 @@ private String compileLuaWithRunArgs(String testName, boolean withStdLib, String

private String compileLuaWithCUs(String testName, boolean withStdLib, List<CU> 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<CU> extraCUs,
RunArgs runArgs, String... lines) {
WurstGui gui = new WurstGuiCliImpl();
WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(null, gui, null, runArgs);
List<CU> inputs = new ArrayList<>();
Expand Down Expand Up @@ -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<String> 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",
Expand Down
Loading