diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java index 53d2b64eb..42154dc54 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/ConstantAndCopyPropagation.java @@ -309,31 +309,19 @@ private void analyzeComponent(List scc, Map knowledge) { } else { Value newValue = null; - // Try constant folding first - ImExpr foldedExpr = tryConstantFold(right, newOut); - if (foldedExpr != null && foldedExpr != right) { - // We successfully folded to a constant - newValue = Value.tryValue(foldedExpr); - if (newValue != null) { - // Replace the RHS with the folded constant in the AST - right.replaceBy(foldedExpr); - } - } - - // If no folding happened, try regular value propagation - if (newValue == null) { - if (right instanceof ImConst) { - newValue = Value.tryValue(right); - } else if (right instanceof ImVarAccess) { - ImVar varRight = ((ImVarAccess) right).getVar(); - if(newOut.containsKey(varRight)) { - newValue = newOut.get(varRight).getOrNull(); - } else { - newValue = Value.tryValue(right); - } - } else if(right instanceof ImTupleExpr) { + // Constant folding is intentionally centralized in SimpleRewrites. + // This pass performs propagation only to keep fold semantics in one place. + if (right instanceof ImConst) { + newValue = Value.tryValue(right); + } else if (right instanceof ImVarAccess) { + ImVar varRight = ((ImVarAccess) right).getVar(); + if(newOut.containsKey(varRight)) { + newValue = newOut.get(varRight).getOrNull(); + } else { newValue = Value.tryValue(right); } + } else if(right instanceof ImTupleExpr) { + newValue = Value.tryValue(right); } if (newValue == null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java index 424c951b2..06798af24 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java @@ -116,9 +116,6 @@ private String skipReason(ImFunction caller, ImFunctionCall call, ImFunction f) if (call.getCallType() == CallType.EXECUTE) { return "execute_call"; } - if (translator.isLuaTarget() && !maxOneReturn(f)) { - return "lua_multi_return_inline_disabled"; - } if (translator.isLuaTarget() && containsFuncRef(f)) { return "lua_callback_funcref_barrier"; } @@ -241,14 +238,14 @@ public void visit(ImFunctionCall called) { private ImStmts rewriteForEarlyReturns(ImStmts body, ImVar doneVar, ImVar retVar) { ImStmts rewritten = JassIm.ImStmts(); for (ImStmt s : body) { - ImStmt transformed = rewriteStmtForEarlyReturn(s, doneVar, retVar); + ImStmts transformed = rewriteStmtForEarlyReturn(s, doneVar, retVar); ImExpr notDone = JassIm.ImOperatorCall(de.peeeq.wurstscript.WurstOperator.NOT, JassIm.ImExprs(JassIm.ImVarAccess(doneVar))); - rewritten.add(JassIm.ImIf(s.attrTrace(), notDone, JassIm.ImStmts(transformed), JassIm.ImStmts())); + rewritten.add(JassIm.ImIf(s.attrTrace(), notDone, transformed, JassIm.ImStmts())); } return rewritten; } - private ImStmt rewriteStmtForEarlyReturn(ImStmt s, ImVar doneVar, ImVar retVar) { + private ImStmts rewriteStmtForEarlyReturn(ImStmt s, ImVar doneVar, ImVar retVar) { if (s instanceof ImReturn) { ImReturn r = (ImReturn) s; ImStmts b = JassIm.ImStmts(); @@ -258,27 +255,27 @@ private ImStmt rewriteStmtForEarlyReturn(ImStmt s, ImVar doneVar, ImVar retVar) b.add(JassIm.ImSet(r.getTrace(), JassIm.ImVarAccess(retVar), rv)); } b.add(JassIm.ImSet(r.getTrace(), JassIm.ImVarAccess(doneVar), JassIm.ImBoolVal(true))); - return ImHelper.statementExprVoid(b); + return b; } else if (s instanceof ImIf) { ImIf imIf = (ImIf) s; ImStmts thenBlock = rewriteForEarlyReturns(imIf.getThenBlock().copy(), doneVar, retVar); ImStmts elseBlock = rewriteForEarlyReturns(imIf.getElseBlock().copy(), doneVar, retVar); - return JassIm.ImIf(imIf.getTrace(), imIf.getCondition().copy(), thenBlock, elseBlock); + return JassIm.ImStmts(JassIm.ImIf(imIf.getTrace(), imIf.getCondition().copy(), thenBlock, elseBlock)); } else if (s instanceof ImLoop) { ImLoop l = (ImLoop) s; ImStmts loopBody = JassIm.ImStmts(); loopBody.add(JassIm.ImExitwhen(l.getTrace(), JassIm.ImVarAccess(doneVar))); loopBody.addAll(rewriteForEarlyReturns(l.getBody().copy(), doneVar, retVar).removeAll()); - return JassIm.ImLoop(l.getTrace(), loopBody); + return JassIm.ImStmts(JassIm.ImLoop(l.getTrace(), loopBody)); } else if (s instanceof ImVarargLoop) { ImVarargLoop l = (ImVarargLoop) s; ImStmts loopBody = JassIm.ImStmts(); loopBody.add(JassIm.ImExitwhen(l.getTrace(), JassIm.ImVarAccess(doneVar))); loopBody.addAll(rewriteForEarlyReturns(l.getBody().copy(), doneVar, retVar).removeAll()); - return JassIm.ImVarargLoop(l.getTrace(), loopBody, l.getLoopVar()); + return JassIm.ImStmts(JassIm.ImVarargLoop(l.getTrace(), loopBody, l.getLoopVar())); } // Keep tree ownership valid when rewrapping statements into new blocks. - return s.copy(); + return JassIm.ImStmts(s.copy()); } private void rateInlinableFunctions() { @@ -338,11 +335,6 @@ private boolean shouldInline(ImFunction caller, ImFunctionCall call, ImFunction if (f.isNative() || call.getCallType() == CallType.EXECUTE) { return false; } - if (translator.isLuaTarget() && !maxOneReturn(f)) { - // Conservative safety: Lua inliner multi-return rewriting is not yet fully robust - // across all lowered patterns. Keep call semantics intact for now. - return false; - } if (translator.isLuaTarget() && containsFuncRef(f)) { // Functions that build callback refs are lowered with Lua-specific wrappers/xpcall. // Keeping them as standalone calls avoids callback context/vararg scope breakage. 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 af1d31642..1995353f1 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 @@ -101,6 +101,7 @@ public class LuaTranslator { final ImProg prog; final LuaCompilationUnit luaModel; + private final LuaStatements deferredMainInit = LuaAst.LuaStatements(); private final Set usedNames = new HashSet<>(Arrays.asList( // reserved function names "print", "tostring", "error", @@ -180,7 +181,7 @@ public LuaMethod initFor(ImMethod a) { GetAForB luaClassVar = new GetAForB() { @Override public LuaVariable initFor(ImClass a) { - return LuaAst.LuaVariable(uniqueName(a.getName()), LuaAst.LuaNoExpr()); + return LuaAst.LuaVariable(uniqueName(a.getName()), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())); } }; @@ -321,9 +322,10 @@ public LuaCompilationUnit translate() { initClassTables(c); } + emitExperimentalHashtableLeakGuards(); + prependDeferredMainInitToMain(); cleanStatements(); enforceLuaLocalLimits(); - emitExperimentalHashtableLeakGuards(); return luaModel; } @@ -353,12 +355,39 @@ private void ensureWurstContextCallbackHelpers() { } private void emitExperimentalHashtableLeakGuards() { - luaModel.add(LuaAst.LuaLiteral("-- Wurst experimental Lua assertion guards: raw WC3 hashtable natives must not be called.")); + deferMainInit(LuaAst.LuaLiteral("-- Wurst experimental Lua assertion guards: raw WC3 hashtable natives must not be called.")); + deferMainInit(LuaAst.LuaLiteral("do")); + deferMainInit(LuaAst.LuaLiteral(" local __wurst_guard_ok = pcall(function()")); for (String nativeName : allHashtableNativeNames()) { - luaModel.add(LuaAst.LuaLiteral("if " + nativeName + " ~= nil then " + nativeName + deferMainInit(LuaAst.LuaLiteral(" if " + nativeName + " ~= nil then " + nativeName + " = function(...) error(\"Wurst Lua assertion failed: unexpected call to native " + nativeName + ". Expected __wurst_" + nativeName + ".\") end end")); } + deferMainInit(LuaAst.LuaLiteral(" end)")); + deferMainInit(LuaAst.LuaLiteral(" if not __wurst_guard_ok then")); + deferMainInit(LuaAst.LuaLiteral(" -- Some Lua runtimes lock native globals. Compile-time leak checks stay authoritative.")); + deferMainInit(LuaAst.LuaLiteral(" end")); + deferMainInit(LuaAst.LuaLiteral("end")); + } + + private void deferMainInit(LuaStatement statement) { + deferredMainInit.add(statement); + } + + private void prependDeferredMainInitToMain() { + if (deferredMainInit.isEmpty()) { + return; + } + ImFunction mainIm = imTr.getMainFunc(); + if (mainIm == null) { + return; + } + LuaFunction mainLua = luaFunc.getFor(mainIm); + LuaStatements mainBody = mainLua.getBody(); + for (int i = deferredMainInit.size() - 1; i >= 0; i--) { + LuaStatement stmt = deferredMainInit.remove(i); + mainBody.add(0, stmt); + } } public static void assertNoLeakedHashtableNativeCalls(String luaCode) { @@ -534,15 +563,17 @@ private void createInstanceOfFunction() { private void createObjectIndexFunctions() { String vName = "__wurst_objectIndexMap"; - LuaVariable v = LuaAst.LuaVariable(vName, LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( - LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0")) - ))); + LuaVariable v = LuaAst.LuaVariable(vName, LuaAst.LuaExprNull()); luaModel.add(v); - - LuaVariable im = LuaAst.LuaVariable("__wurst_number_wrapper_map", LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( + deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(v), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0")) - ))); + )))); + + LuaVariable im = LuaAst.LuaVariable("__wurst_number_wrapper_map", LuaAst.LuaExprNull()); luaModel.add(im); + deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(im), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( + LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0")) + )))); { String[] code = { @@ -597,12 +628,13 @@ private void createObjectIndexFunctions() { } private void createStringIndexFunctions() { - LuaVariable map = LuaAst.LuaVariable("__wurst_string_index_map", LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( + LuaVariable map = LuaAst.LuaVariable("__wurst_string_index_map", LuaAst.LuaExprNull()); + luaModel.add(map); + deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(map), LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0")), LuaAst.LuaTableNamedField("byString", LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())), LuaAst.LuaTableNamedField("byIndex", LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())) - ))); - luaModel.add(map); + )))); { String[] code = { @@ -995,8 +1027,6 @@ private void translateClass(ImClass c) { luaModel.add(initMethod); - classVar.setInitialValue(emptyTable()); - // translate functions for (ImFunction f : c.getFunctions()) { translateFunc(f); @@ -1038,14 +1068,14 @@ private void initClassTables(ImClass c) { // set supertype metadata: LuaTableFields superClasses = LuaAst.LuaTableFields(); collectSuperClasses(superClasses, c, new HashSet<>()); - luaModel.add(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( + deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( LuaAst.LuaExprVarAccess(classVar), WURST_SUPERTYPES), LuaAst.LuaTableConstructor(superClasses) )); // set typeid metadata: - luaModel.add(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( + deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( LuaAst.LuaExprVarAccess(classVar), ExprTranslation.TYPE_ID), LuaAst.LuaExprIntVal("" + prog.attrTypeId().get(c)) @@ -1100,7 +1130,7 @@ private void createMethods(ImClass c, LuaVariable classVar) { if (impl == null || impl.getImplementation() == null) { continue; } - luaModel.add(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( + deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprFieldAccess( LuaAst.LuaExprVarAccess(classVar), e.getKey()), LuaAst.LuaExprFuncRef(luaFunc.getFor(impl.getImplementation())) @@ -1343,8 +1373,9 @@ private void translateGlobal(ImVar v) { return; } LuaVariable lv = luaVar.getFor(v); - lv.setInitialValue(defaultValue(v.getType())); + lv.setInitialValue(LuaAst.LuaExprNull()); luaModel.add(lv); + deferMainInit(LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(lv), defaultValue(v.getType()))); } private LuaExpr defaultValue(ImType type) { 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 a61ceb8b3..c1a64b2ff 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 @@ -79,6 +79,14 @@ private void assertContainsRegex(String output, String regex) { assertTrue("Pattern must occur: " + regex, matcher.find()); } + private void assertOccursBefore(String output, String first, String second) { + int firstPos = output.indexOf(first); + int secondPos = output.indexOf(second); + assertTrue("Expected to find: " + first, firstPos >= 0); + assertTrue("Expected to find: " + second, secondPos >= 0); + assertTrue("Expected '" + first + "' before '" + second + "'.", firstPos < secondPos); + } + private String compileLuaWithRunArgs(String testName, boolean withStdLib, String... lines) { RunArgs runArgs = new RunArgs().with("-lua", "-inline", "-localOptimizations", "-stacktraces"); WurstGui gui = new WurstGuiCliImpl(); @@ -162,7 +170,6 @@ public void stacktraceStringsNotInjectedIntoNumericComparisons() { ); assertContainsRegex(compiled, "\"when calling contains"); assertDoesNotContainRegex(compiled, "\"when calling contains[^\"]*\"\\s*[<>]=?"); - assertContainsRegex(compiled, "stepsAlongY\\("); assertDoesNotContainRegex(compiled, "(?s)if not\\((\\w+)\\) then.*?stepsAlongY\\([^\\n]*,\\s*\\1\\s*,"); } @@ -450,6 +457,97 @@ public void mainAndConfigNamesFixed() throws IOException { assertFalse(compiled.contains("function config2(")); } + @Test + public void luaHeavyBootstrapStateIsSeededFromMain() throws IOException { + test().testLua(true).lines( + "package Test", + "class C", + "function id(int x) returns int", + " return x", + "init", + " let c = new C()", + " if c == null", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaHeavyBootstrapStateIsSeededFromMain.lua"), Charsets.UTF_8); + int mainPos = compiled.indexOf("function main("); + assertTrue(mainPos >= 0); + int configPos = compiled.indexOf("function config(", mainPos); + String mainSection = compiled.substring(mainPos, configPos > mainPos ? configPos : compiled.length()); + String beforeMain = compiled.substring(0, mainPos); + assertFalse(beforeMain.contains("__wurst_objectIndexMap = ({")); + assertFalse(beforeMain.contains("__wurst_string_index_map = ({")); + assertFalse(beforeMain.contains("Wurst experimental Lua assertion guards")); + assertTrue(mainSection.contains("__wurst_objectIndexMap = ({")); + assertTrue(mainSection.contains("__wurst_string_index_map = ({")); + assertTrue(mainSection.contains("Wurst experimental Lua assertion guards")); + } + + @Test + public void luaClassDispatchTablesAreInitializedInsideMain() throws IOException { + test().testLua(true).lines( + "package Test", + "class A", + " function f() returns int", + " return 1", + "init", + " let a = new A()", + " if a.f() > 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaClassDispatchTablesAreInitializedInsideMain.lua"), Charsets.UTF_8); + int mainPos = compiled.indexOf("function main("); + assertTrue(mainPos >= 0); + int configPos = compiled.indexOf("function config(", mainPos); + String mainSection = compiled.substring(mainPos, configPos > mainPos ? configPos : compiled.length()); + String beforeMain = compiled.substring(0, mainPos); + assertFalse(beforeMain.contains("A.__wurst_supertypes =")); + assertFalse(beforeMain.contains("A.__typeId__ =")); + assertTrue(mainSection.contains("A.__wurst_supertypes =")); + assertTrue(mainSection.contains("A.__typeId__ =")); + assertTrue(mainSection.contains("A.A_f =")); + } + + @Test + public void luaDeferredBootstrapRunsBeforeInitGlobalsInMain() throws IOException { + test().testLua(true).lines( + "package Test", + "class C", + " function f() returns int", + " return 1", + "init", + " let c = new C()", + " if c.f() > 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaDeferredBootstrapRunsBeforeInitGlobalsInMain.lua"), Charsets.UTF_8); + int mainPos = compiled.indexOf("function main("); + assertTrue(mainPos >= 0); + int configPos = compiled.indexOf("function config(", mainPos); + String mainSection = compiled.substring(mainPos, configPos > mainPos ? configPos : compiled.length()); + + assertOccursBefore(mainSection, "__wurst_objectIndexMap = ({", "initGlobals()"); + assertOccursBefore(mainSection, "__wurst_string_index_map = ({", "initGlobals()"); + assertOccursBefore(mainSection, "C.__wurst_supertypes =", "initGlobals()"); + assertOccursBefore(mainSection, "C.__typeId__ =", "initGlobals()"); + assertOccursBefore(mainSection, "C.C_f =", "initGlobals()"); + assertOccursBefore(mainSection, "Wurst experimental Lua assertion guards", "initGlobals()"); + } + + @Test + public void luaClassTablesExistBeforeCreateMethodDefinitions() throws IOException { + test().testLua(true).lines( + "package Test", + "class A", + "init", + " let a = new A()", + " if a == null", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaClassTablesExistBeforeCreateMethodDefinitions.lua"), Charsets.UTF_8); + assertOccursBefore(compiled, "A = ({})", "function A:create("); + } + @Test public void intCasting() throws IOException { @@ -980,31 +1078,51 @@ public void spilledLocalsDeclareTableBeforeFirstUseInLua() throws IOException { } @Test - public void luaInlinerDoesNotInlineFunctionsWithMultipleReturns() throws IOException { - test().testLua(true).lines( + public void luaInlinerInlinesFunctionsWithMultipleReturns() { + String compiled = compileLuaWithRunArgs( + "LuaTranslationTests_luaInlinerInlinesFunctionsWithMultipleReturns", + false, "package Test", "native takesInt(int i)", + "native randomBool() returns boolean", "function choose(boolean b, int x) returns int", " if b", - " return x + 1", + " return x + 1", " return x + 2", "function caller()", - " let v = choose(true, 40)", + " let v = choose(randomBool(), 40)", " takesInt(v)", "init", " caller()" ); - String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaInlinerDoesNotInlineFunctionsWithMultipleReturns.lua"), Charsets.UTF_8); - int start = compiled.indexOf("function caller("); - assertTrue("caller function not found in generated lua output", start >= 0); - int end = compiled.indexOf("\nend", start); - assertTrue("caller function end not found in generated lua output", end > start); - String callerBody = compiled.substring(start, end); + assertDoesNotContainRegex(compiled, "\\bchoose\\s*\\(\\s*randomBool\\s*\\(\\s*\\)\\s*,\\s*40\\s*\\)"); + assertContainsRegex(compiled, "inlineDone"); + assertContainsRegex(compiled, "inlineRet"); + } + + @Test + public void luaInlinerMultiReturnRewriteIsExplicitWithInlAndLocalOpts() { + String compiled = compileLuaWithRunArgs( + "LuaTranslationTests_luaInlinerMultiReturnRewriteIsExplicitWithInlAndLocalOpts", + false, + "package Test", + "native takesInt(int i)", + "native randomBool() returns boolean", + "function choose(boolean b, int x) returns int", + " if b", + " return x + 1", + " return x + 2", + "function caller()", + " let v = choose(randomBool(), 40)", + " takesInt(v)", + "init", + " caller()" + ); - assertTrue("caller should keep a direct call to choose for Lua target", callerBody.contains("choose(true, 40)")); - assertFalse("caller should not contain multi-return inline control vars", callerBody.contains("inlineDone")); - assertFalse("caller should not contain multi-return inline return temp vars", callerBody.contains("inlineRet")); + assertDoesNotContainRegex(compiled, "\\bchoose\\s*\\(\\s*randomBool\\s*\\(\\s*\\)\\s*,\\s*40\\s*\\)"); + assertContainsRegex(compiled, "inlineDone"); + assertContainsRegex(compiled, "inlineRet"); } @Test @@ -1146,6 +1264,21 @@ public void hashtableHandleExtensionsUseWurstLuaHelpers() throws IOException { assertTrue(compiled.contains("Wurst experimental Lua assertion guards")); } + @Test + public void hashtableNativeOverrideGuardsAreRuntimeSafe() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " let h = InitHashtable()", + " h.saveInt(1, 2, 7)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableNativeOverrideGuardsAreRuntimeSafe.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("Wurst experimental Lua assertion guards")); + assertContainsRegex(compiled, "\\blocal\\s+__wurst_guard_ok\\s*=\\s*pcall\\s*\\(\\s*function\\s*\\("); + assertContainsRegex(compiled, "\\bInitHashtable\\s*=\\s*function\\s*\\("); + assertContainsRegex(compiled, "\\bSaveInteger\\s*=\\s*function\\s*\\("); + } + @Test public void hashtableHandleLoadSaveUseWurstLuaHelpers() throws IOException { test().testLua(true).withStdLib().lines( diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java index 9f121c9dc..f00f4f765 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java @@ -1111,6 +1111,56 @@ public void inlinerLocationLocalsAreInitializedBeforeUse() throws IOException { assertTrue(useIdx > initIdx, "Expected inlineRet to be initialized before use."); } + @Test + public void inlinerMultiReturnKeepsPostReturnSideEffectsUnreachableUnderInlopt() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "native testFail(string s)", + "@inline function pickPositive(int x) returns int", + " if x > 0", + " return x", + " testFail(\"post-return path executed\")", + " return 0 - x", + "init", + " let y = pickPositive(7)", + " if y == 7", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void inlinerMultiReturnRewriteIsExplicitInInlAndInloptOutput() throws IOException { + testAssertOkLinesWithStdLib(true, + "package test", + "@inline function maybeAbs(int x) returns int", + " if x > 0", + " return x", + " return 0 - x", + "init", + " let y = maybeAbs(GetRandomInt(-5, 5))", + " if y >= 0", + " testSuccess()", + "endpackage" + ); + + String inl = Files.toString(new File("test-output/OptimizerTests_inlinerMultiReturnRewriteIsExplicitInInlAndInloptOutput_inl.j"), Charsets.UTF_8); + String inlopt = Files.toString(new File("test-output/OptimizerTests_inlinerMultiReturnRewriteIsExplicitInInlAndInloptOutput_inlopt.j"), Charsets.UTF_8); + + for (String generated : java.util.List.of(inl, inlopt)) { + assertFalse(generated.contains("call maybeAbs("), "Expected maybeAbs() to be fully inlined."); + assertTrue(generated.contains("set inlineDone"), "Expected explicit inlineDone writes."); + assertTrue(generated.contains("set inlineRet"), "Expected explicit inlineRet writes."); + assertTrue(generated.contains("set inlineDone = false") + || generated.contains("local boolean inlineDone = false"), + "Expected explicit inlineDone initialization."); + assertTrue(generated.contains("set inlineDone = true"), "Expected explicit rewritten return marking."); + assertTrue(generated.matches("(?s).*if\\s+not\\s+inlineDone.*"), + "Expected explicit post-return gating in generated code."); + } + } + @Test public void moveTowardsBug() { // see #737 @@ -1223,6 +1273,30 @@ public void inlinerIntRealsConstantFolding() { ); } + @Test + public void precisionSensitiveRealFoldUsesSingleFoldingPath() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "native testFail(string s)", + "@extern native R2I(real r) returns int", + "@extern native I2S(int i) returns string", + "", + "@inline function risky(real a, real b) returns int", + " real d = a - b", + " return R2I(d)", + "", + "init", + " // On WC3 real semantics these literals collapse to same float, so (a - b) should be 0.", + " let x = risky(16777217., 16777216.)", + " if x == 0", + " testSuccess()", + " else", + " testFail(\"precision fold regression: \" + I2S(x))", + "endpackage" + ); + } + @Test public void multiArrayNoInline() { testAssertOkLines(true,