From 8f8eb5e5dd2424cc2fe4149bbf69b7f1be85e654 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 12:21:26 +0200 Subject: [PATCH 01/10] Inline package constant globals --- .../imoptimizer/GlobalsInliner.java | 15 ++++++- .../tests/LuaBackendAuditTests.java | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index dd4e99051..e2bab645c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -2,6 +2,7 @@ import com.google.common.collect.Sets; import de.peeeq.wurstscript.attributes.CompileError; +import de.peeeq.wurstscript.ast.GlobalVarDef; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; @@ -57,7 +58,7 @@ public int optimize(ImTranslator trans) { ImVarWrite obs = null; for (ImVarWrite write : v.attrWrites()) { ImFunction func = write.getNearestFunc(); - if (isInInitGlobals(func)) { + if (isInInitGlobals(func) || isLiteralConstantGlobal(v)) { right = write.getRight(); obs = write; break; @@ -159,4 +160,16 @@ private static boolean isInInitGlobals(ImFunction func) { return func != null && func.getName().equals("initGlobals"); } + /** + * Package globals are initialized by package init functions, rather than initGlobals. + * A source-level constant is immutable, so a literal initializer remains safe to + * substitute regardless of which initialization function owns the assignment. + * Configurable constants stay runtime globals until configuration resolution owns them. + */ + private static boolean isLiteralConstantGlobal(ImVar var) { + return var.getTrace() instanceof GlobalVarDef + && ((GlobalVarDef) var.getTrace()).attrIsConstant() + && !((GlobalVarDef) var.getTrace()).hasAnnotation("@configurable"); + } + } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index e4cb98666..7483c56ae 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -58,6 +58,47 @@ private String compileOptimizedLua(String testName, String... lines) { return compileLuaWithRunArgs(testName, runArgs, false, lines); } + @Test + public void packageConstantsInlineAndRemoveDeadGuards() { + String compiled = compileOptimizedLuaWithStdLib( + "packageConstantsInlineAndRemoveDeadGuards", + "package Test", + "public constant int VALUE = 7", + "public constant bool DISABLED = false", + "public constant bool ENABLED = true", + "public constant bool COMPILETIME_DISABLED = compiletime(false)", + "@configurable public constant int CONFIGURABLE = 9", + "native consume(int value)", + "bool active", + "function dead()", + " consume(VALUE)", + "function compiletimeDead()", + " consume(VALUE)", + "function guarded()", + " if DISABLED and active", + " dead()", + " if COMPILETIME_DISABLED and active", + " compiletimeDead()", + " if ENABLED and active", + " consume(VALUE)", + " consume(CONFIGURABLE)", + "init", + " guarded()" + ); + + assertFalse("constant globals must not survive as Lua global reads:\n" + compiled, + compiled.contains("Test_VALUE") || compiled.contains("Test_DISABLED") || compiled.contains("Test_ENABLED") + || compiled.contains("Test_COMPILETIME_DISABLED")); + assertFalse("a false constant guard must remove its unreachable callee:\n" + compiled, + compiled.contains("function dead(") || compiled.contains("function compiletimeDead(")); + assertTrue("a true constant guard must retain its dynamic condition:\n" + compiled, + compiled.contains("if Test_active then")); + assertTrue("constant arithmetic uses must be emitted as literals:\n" + compiled, + compiled.contains("consume(7)")); + assertTrue("configurable constants must remain globals until configuration resolution:\n" + compiled, + compiled.contains("Test_CONFIGURABLE")); + } + private String compileOptimizedLuaWithStdLib(String testName, String... lines) { RunArgs runArgs = new RunArgs().with("-lua", "-inline", "-localOptimizations", "-runcompiletimefunctions", "-lib", StdLib.getLib()); From 2c27713c6d4106e54affcf3b1450ae5341983415 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 12:28:38 +0200 Subject: [PATCH 02/10] Test constant inlining in Jass --- .../wurstscript/tests/OptimizerTests.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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 2875b129b..a63edd7c2 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 @@ -33,6 +33,38 @@ public class OptimizerTests extends WurstScriptTest { + @Test + public void packageConstantsInlineAndRemoveDeadGuardsInJass() throws IOException { + test().lines( + "package Test", + "public constant int VALUE = 7", + "public constant bool DISABLED = false", + "public constant bool ENABLED = true", + "@configurable public constant int CONFIGURABLE = 9", + "native consume(int value)", + "bool active", + "function dead()", + " consume(VALUE)", + "function guarded()", + " if DISABLED and active", + " dead()", + " if ENABLED and active", + " consume(VALUE)", + " consume(CONFIGURABLE)", + "init", + " guarded()" + ); + + String compiled = Files.toString( + new File("test-output/OptimizerTests_packageConstantsInlineAndRemoveDeadGuardsInJass_inlopt.j"), + Charsets.UTF_8); + assertFalse(compiled.contains("Test_VALUE") || compiled.contains("Test_DISABLED") || compiled.contains("Test_ENABLED")); + assertFalse(compiled.contains("function Test_dead takes")); + assertTrue(compiled.contains("if Test_active then")); + assertTrue(compiled.contains("call consume(7)")); + assertTrue(compiled.contains("Test_CONFIGURABLE")); + } + @Test public void test_number_shortening() { From ad2539c497e3e5680fed74d7874ffec79fc3dc5c Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 12:39:26 +0200 Subject: [PATCH 03/10] Inline package constant globals --- .../src/test/java/tests/wurstscript/tests/OptimizerTests.java | 1 + 1 file changed, 1 insertion(+) 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 a63edd7c2..89c89f10c 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 @@ -66,6 +66,7 @@ public void packageConstantsInlineAndRemoveDeadGuardsInJass() throws IOException } + @Test public void test_number_shortening() { test().lines( From 43e00ecb03649cd966eb49a9e2cc81c12de136ea Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 12:57:14 +0200 Subject: [PATCH 04/10] Preserve constant initialization order --- .../imoptimizer/GlobalsInliner.java | 50 +++++++++++++++---- .../tests/LuaBackendAuditTests.java | 13 ++--- .../wurstscript/tests/OptimizerTests.java | 18 +++++++ 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index e2bab645c..959e646be 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -3,6 +3,7 @@ import com.google.common.collect.Sets; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.ast.GlobalVarDef; +import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; @@ -53,12 +54,13 @@ public int optimize(ImTranslator trans) { continue; } - if (v.attrWrites().size() == 1) { + boolean literalConstant = isLiteralConstantGlobal(v.getTrace(), prog); + if (v.attrWrites().size() == 1 || literalConstant) { ImExpr right = null; ImVarWrite obs = null; for (ImVarWrite write : v.attrWrites()) { ImFunction func = write.getNearestFunc(); - if (isInInitGlobals(func) || isLiteralConstantGlobal(v)) { + if (isInInitGlobals(func) || (literalConstant && isLiteral(write.getRight()))) { right = write.getRight(); obs = write; break; @@ -74,7 +76,7 @@ public int optimize(ImTranslator trans) { v3.replaceBy(replacement.copy()); } } - if (replacement != null || v.attrReads().size() == 0) { + if ((replacement != null || v.attrReads().size() == 0) && v.attrWrites().size() == 1) { obsoleteVars.add(v); } } else if (v.attrWrites().size() > 1 && !(v.getType() instanceof ImTupleType)) { @@ -150,6 +152,10 @@ private ImExpr findReplacement(ImExpr right, ImVarWrite obs) { return replacement; } + private static boolean isLiteral(ImExpr expr) { + return expr instanceof ImIntVal || expr instanceof ImRealVal || expr instanceof ImStringVal || expr instanceof ImBoolVal; + } + @Override public String getName() { return "Globals Inlined"; @@ -162,14 +168,40 @@ private static boolean isInInitGlobals(ImFunction func) { /** * Package globals are initialized by package init functions, rather than initGlobals. - * A source-level constant is immutable, so a literal initializer remains safe to - * substitute regardless of which initialization function owns the assignment. + * A source-level constant is immutable, but an earlier initializer in the same + * package may still observe its default value before the constant is assigned. * Configurable constants stay runtime globals until configuration resolution owns them. */ - private static boolean isLiteralConstantGlobal(ImVar var) { - return var.getTrace() instanceof GlobalVarDef - && ((GlobalVarDef) var.getTrace()).attrIsConstant() - && !((GlobalVarDef) var.getTrace()).hasAnnotation("@configurable"); + private static boolean isLiteralConstantGlobal(de.peeeq.wurstscript.ast.Element trace, ImProg prog) { + if (!(trace instanceof GlobalVarDef)) { + return false; + } + GlobalVarDef global = (GlobalVarDef) trace; + if (!global.attrIsConstant() || global.hasAnnotation("@configurable")) { + return false; + } + WPackage packageOfGlobal = packageOf(global); + if (packageOfGlobal == null) { + return true; + } + for (ImVar other : prog.getGlobals()) { + if (other.getTrace() instanceof GlobalVarDef + && packageOf((GlobalVarDef) other.getTrace()) == packageOfGlobal + && other.getTrace().attrSource().getLeftPos() < global.attrSource().getLeftPos() + && !other.attrWrites().isEmpty()) { + return false; + } + } + return true; + } + + @Nullable + private static WPackage packageOf(GlobalVarDef global) { + de.peeeq.wurstscript.ast.Element element = global; + while (element != null && !(element instanceof WPackage)) { + element = element.getParent(); + } + return (WPackage) element; } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index 7483c56ae..a3509ed32 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -66,19 +66,14 @@ public void packageConstantsInlineAndRemoveDeadGuards() { "public constant int VALUE = 7", "public constant bool DISABLED = false", "public constant bool ENABLED = true", - "public constant bool COMPILETIME_DISABLED = compiletime(false)", "@configurable public constant int CONFIGURABLE = 9", "native consume(int value)", "bool active", "function dead()", " consume(VALUE)", - "function compiletimeDead()", - " consume(VALUE)", "function guarded()", " if DISABLED and active", " dead()", - " if COMPILETIME_DISABLED and active", - " compiletimeDead()", " if ENABLED and active", " consume(VALUE)", " consume(CONFIGURABLE)", @@ -86,11 +81,11 @@ public void packageConstantsInlineAndRemoveDeadGuards() { " guarded()" ); - assertFalse("constant globals must not survive as Lua global reads:\n" + compiled, - compiled.contains("Test_VALUE") || compiled.contains("Test_DISABLED") || compiled.contains("Test_ENABLED") - || compiled.contains("Test_COMPILETIME_DISABLED")); + assertFalse("constant uses must be emitted as literals:\n" + compiled, + compiled.contains("consume(Test_VALUE)") || compiled.contains("Test_DISABLED and") + || compiled.contains("Test_ENABLED and")); assertFalse("a false constant guard must remove its unreachable callee:\n" + compiled, - compiled.contains("function dead(") || compiled.contains("function compiletimeDead(")); + compiled.contains("function dead(")); assertTrue("a true constant guard must retain its dynamic condition:\n" + compiled, compiled.contains("if Test_active then")); assertTrue("constant arithmetic uses must be emitted as literals:\n" + compiled, 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 89c89f10c..bc1a9ca0c 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 @@ -65,6 +65,24 @@ public void packageConstantsInlineAndRemoveDeadGuardsInJass() throws IOException assertTrue(compiled.contains("Test_CONFIGURABLE")); } + @Test + public void laterPackageConstantIsNotInlinedIntoEarlierInitializers() throws IOException { + test().lines( + "package Test", + "native consume(int value)", + "int observed = readLater()", + "constant int LATER = 7", + "function readLater() returns int", + " return LATER", + "init", + " consume(observed)" + ); + String compiled = Files.toString( + new File("test-output/OptimizerTests_laterPackageConstantIsNotInlinedIntoEarlierInitializers_inlopt.j"), + Charsets.UTF_8); + assertTrue(compiled.contains("Test_LATER")); + } + @Test From a482fbda42270f96927b83451e3f064c9624d7de Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 13:31:26 +0200 Subject: [PATCH 05/10] Preserve init block ordering for constants --- .../translation/imoptimizer/GlobalsInliner.java | 8 ++++++++ .../test/java/tests/wurstscript/tests/OptimizerTests.java | 2 ++ 2 files changed, 10 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index 959e646be..4da7cd621 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -3,6 +3,8 @@ import com.google.common.collect.Sets; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.ast.GlobalVarDef; +import de.peeeq.wurstscript.ast.InitBlock; +import de.peeeq.wurstscript.ast.WEntity; import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; @@ -184,6 +186,12 @@ private static boolean isLiteralConstantGlobal(de.peeeq.wurstscript.ast.Element if (packageOfGlobal == null) { return true; } + for (WEntity entity : packageOfGlobal.getElements()) { + if (entity instanceof InitBlock + && entity.attrSource().getLeftPos() < global.attrSource().getLeftPos()) { + return false; + } + } for (ImVar other : prog.getGlobals()) { if (other.getTrace() instanceof GlobalVarDef && packageOf((GlobalVarDef) other.getTrace()) == packageOfGlobal 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 bc1a9ca0c..1a79f6dcc 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 @@ -71,6 +71,8 @@ public void laterPackageConstantIsNotInlinedIntoEarlierInitializers() throws IOE "package Test", "native consume(int value)", "int observed = readLater()", + "init", + " consume(readLater())", "constant int LATER = 7", "function readLater() returns int", " return LATER", From 8ecee6a997675aaeddf363b09d7f9c88f9f01cfb Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 13:45:22 +0200 Subject: [PATCH 06/10] Respect initlater constant ordering --- .../imoptimizer/GlobalsInliner.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index 4da7cd621..208ad18b3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -4,6 +4,8 @@ import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.ast.GlobalVarDef; import de.peeeq.wurstscript.ast.InitBlock; +import de.peeeq.wurstscript.ast.CompilationUnit; +import de.peeeq.wurstscript.ast.WImport; import de.peeeq.wurstscript.ast.WEntity; import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.jassIm.*; @@ -186,6 +188,9 @@ private static boolean isLiteralConstantGlobal(de.peeeq.wurstscript.ast.Element if (packageOfGlobal == null) { return true; } + if (isInitializedLater(packageOfGlobal)) { + return false; + } for (WEntity entity : packageOfGlobal.getElements()) { if (entity instanceof InitBlock && entity.attrSource().getLeftPos() < global.attrSource().getLeftPos()) { @@ -212,4 +217,17 @@ private static WPackage packageOf(GlobalVarDef global) { return (WPackage) element; } + private static boolean isInitializedLater(WPackage target) { + for (CompilationUnit unit : target.getModel()) { + for (WPackage candidate : unit.getPackages()) { + for (WImport imported : candidate.getImports()) { + if (imported.getIsInitLater() && imported.attrImportedPackage() == target) { + return true; + } + } + } + } + return false; + } + } From cdb3dd2a79575b6131b7d69684068394fbc63c6b Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 13:46:02 +0200 Subject: [PATCH 07/10] Cache initlater constant analysis --- .../imoptimizer/GlobalsInliner.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index 208ad18b3..e9f0f2a76 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -16,11 +16,16 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Optional; import java.util.Set; public class GlobalsInliner implements OptimizerPass { + private final Set initLaterPackages = Collections.newSetFromMap(new IdentityHashMap<>()); + private boolean initLaterPackagesCollected; + public int optimize(ImTranslator trans) { int obsoleteCount = 0; ImProg prog = trans.getImProg(); @@ -176,7 +181,7 @@ private static boolean isInInitGlobals(ImFunction func) { * package may still observe its default value before the constant is assigned. * Configurable constants stay runtime globals until configuration resolution owns them. */ - private static boolean isLiteralConstantGlobal(de.peeeq.wurstscript.ast.Element trace, ImProg prog) { + private boolean isLiteralConstantGlobal(de.peeeq.wurstscript.ast.Element trace, ImProg prog) { if (!(trace instanceof GlobalVarDef)) { return false; } @@ -217,17 +222,24 @@ private static WPackage packageOf(GlobalVarDef global) { return (WPackage) element; } - private static boolean isInitializedLater(WPackage target) { + private boolean isInitializedLater(WPackage target) { + if (!initLaterPackagesCollected) { + collectInitLaterPackages(target); + initLaterPackagesCollected = true; + } + return initLaterPackages.contains(target); + } + + private void collectInitLaterPackages(WPackage target) { for (CompilationUnit unit : target.getModel()) { for (WPackage candidate : unit.getPackages()) { for (WImport imported : candidate.getImports()) { - if (imported.getIsInitLater() && imported.attrImportedPackage() == target) { - return true; + if (imported.getIsInitLater() && imported.attrImportedPackage() instanceof WPackage importedPackage) { + initLaterPackages.add(importedPackage); } } } } - return false; } } From c590db554abd5db5ef12b7ddf36e775bd08fb658 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 14:50:32 +0200 Subject: [PATCH 08/10] Analyze package constants in emitted init order --- .../imoptimizer/GlobalsInliner.java | 327 ++++++++++++++---- .../imtranslation/ImTranslator.java | 10 +- .../tests/LuaBackendAuditTests.java | 9 +- .../wurstscript/tests/OptimizerTests.java | 60 +++- 4 files changed, 342 insertions(+), 64 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index e9f0f2a76..f559649fc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -3,11 +3,6 @@ import com.google.common.collect.Sets; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.ast.GlobalVarDef; -import de.peeeq.wurstscript.ast.InitBlock; -import de.peeeq.wurstscript.ast.CompilationUnit; -import de.peeeq.wurstscript.ast.WImport; -import de.peeeq.wurstscript.ast.WEntity; -import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; @@ -15,21 +10,24 @@ import de.peeeq.wurstscript.validation.NamePreservation; import org.jetbrains.annotations.Nullable; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; public class GlobalsInliner implements OptimizerPass { - private final Set initLaterPackages = Collections.newSetFromMap(new IdentityHashMap<>()); - private boolean initLaterPackagesCollected; - + @Override public int optimize(ImTranslator trans) { int obsoleteCount = 0; ImProg prog = trans.getImProg(); prog.clearAttributes(); // TODO only clear read/write attributes + LiteralConstantAnalysis literalConstants = analyzeLiteralConstants(trans, prog); Set obsoleteVars = Sets.newLinkedHashSet(); for (final ImVar v : prog.getGlobals()) { @@ -63,16 +61,21 @@ public int optimize(ImTranslator trans) { continue; } - boolean literalConstant = isLiteralConstantGlobal(v.getTrace(), prog); + boolean literalConstant = literalConstants.safeConstants.contains(v); if (v.attrWrites().size() == 1 || literalConstant) { ImExpr right = null; ImVarWrite obs = null; - for (ImVarWrite write : v.attrWrites()) { - ImFunction func = write.getNearestFunc(); - if (isInInitGlobals(func) || (literalConstant && isLiteral(write.getRight()))) { - right = write.getRight(); - obs = write; - break; + if (literalConstant) { + obs = literalConstants.replacementWrites.get(v); + right = obs.getRight(); + } else { + for (ImVarWrite write : v.attrWrites()) { + ImFunction func = write.getNearestFunc(); + if (isInInitGlobals(func)) { + right = write.getRight(); + obs = write; + break; + } } } if (obs == null) { @@ -176,69 +179,277 @@ private static boolean isInInitGlobals(ImFunction func) { } /** - * Package globals are initialized by package init functions, rather than initGlobals. - * A source-level constant is immutable, but an earlier initializer in the same - * package may still observe its default value before the constant is assigned. - * Configurable constants stay runtime globals until configuration resolution owns them. + * A package constant is assigned at runtime in a package initializer. Replacing all reads is + * valid only when no startup path can observe the default value before that emitted assignment. + * Analyze the actual IM startup order once, including transitive calls and function references, + * rather than trying to reconstruct translation and dependency order from source positions. */ - private boolean isLiteralConstantGlobal(de.peeeq.wurstscript.ast.Element trace, ImProg prog) { - if (!(trace instanceof GlobalVarDef)) { - return false; + private static LiteralConstantAnalysis analyzeLiteralConstants(ImTranslator trans, ImProg prog) { + List initializationOrder = trans.getInitializationOrder(); + IdentityHashMap statementRanks = new IdentityHashMap<>(); + IdentityHashMap writeRanks = new IdentityHashMap<>(); + for (int functionRank = 0; functionRank < initializationOrder.size(); functionRank++) { + ImFunction initializer = initializationOrder.get(functionRank); + for (int statementRank = 0; statementRank < initializer.getBody().size(); statementRank++) { + statementRanks.put(initializer.getBody().get(statementRank), statementRank); + } + int[] writeRank = {0}; + int currentFunctionRank = functionRank; + initializer.getBody().accept(new ImStmt.DefaultVisitor() { + @Override + public void visit(ImSet write) { + long rank = ((long) currentFunctionRank << 32) | (writeRank[0]++ & 0xffffffffL); + writeRanks.put(write, rank); + super.visit(write); + } + }); } - GlobalVarDef global = (GlobalVarDef) trace; - if (!global.attrIsConstant() || global.hasAnnotation("@configurable")) { - return false; + + List candidates = new ArrayList<>(); + IdentityHashMap replacementWrites = new IdentityHashMap<>(); + for (ImVar var : prog.getGlobals()) { + if (!isSourceConstant(var)) { + continue; + } + ImVarWrite replacementWrite = null; + ImExpr replacement = null; + long replacementRank = Long.MAX_VALUE; + boolean eligible = !var.attrWrites().isEmpty(); + for (ImVarWrite write : var.attrWrites()) { + ImFunction initializer = write.getNearestFunc(); + ImStmt statement = initializer == null ? null + : topLevelStatement((de.peeeq.wurstscript.jassIm.Element) write, initializer); + Integer statementRank = statementRanks.get(statement); + Long writeRank = writeRanks.get(write); + if (statement == null || statementRank == null + || writeRank == null || !isLiteral(write.getRight())) { + eligible = false; + break; + } + if (replacement == null) { + replacement = write.getRight(); + } else if (!replacement.structuralEquals(write.getRight())) { + eligible = false; + break; + } + if (writeRank < replacementRank) { + replacementRank = writeRank; + replacementWrite = write; + } + } + if (eligible) { + candidates.add(var); + replacementWrites.put(var, replacementWrite); + } } - WPackage packageOfGlobal = packageOf(global); - if (packageOfGlobal == null) { - return true; + if (candidates.isEmpty()) { + return new LiteralConstantAnalysis(identitySet(), replacementWrites); } - if (isInitializedLater(packageOfGlobal)) { - return false; + + Set functions = identitySet(); + functions.addAll(ImHelper.calculateFunctionsOfProg(prog)); + functions.addAll(initializationOrder); + IdentityHashMap readsByFunction = new IdentityHashMap<>(); + IdentityHashMap readsByStatement = new IdentityHashMap<>(); + IdentityHashMap writesByStatement = new IdentityHashMap<>(); + BitSet unsafe = new BitSet(candidates.size()); + + for (int i = 0; i < candidates.size(); i++) { + ImVar candidate = candidates.get(i); + for (ImVarRead read : candidate.attrReads()) { + ImFunction function = read.getNearestFunc(); + if (function == null) { + unsafe.set(i); + continue; + } + readsByFunction.computeIfAbsent(function, ignored -> new BitSet()).set(i); + ImStmt statement = topLevelStatement((de.peeeq.wurstscript.jassIm.Element) read, function); + if (statement != null) { + readsByStatement.computeIfAbsent(statement, ignored -> new BitSet()).set(i); + } + } + for (ImVarWrite write : candidate.attrWrites()) { + ImFunction function = write.getNearestFunc(); + ImStmt statement = function == null ? null + : topLevelStatement((de.peeeq.wurstscript.jassIm.Element) write, function); + if (statement != null) { + writesByStatement.computeIfAbsent(statement, ignored -> new BitSet()).set(i); + } + } + } + + IdentityHashMap> callers = new IdentityHashMap<>(); + ArrayDeque undiscovered = new ArrayDeque<>(functions); + while (!undiscovered.isEmpty()) { + ImFunction caller = undiscovered.removeFirst(); + readsByFunction.computeIfAbsent(caller, ignored -> new BitSet()); + for (ImFunction callee : caller.calcUsedFunctions()) { + if (callee == null) { + continue; + } + callers.computeIfAbsent(callee, ignored -> identitySet()).add(caller); + if (functions.add(callee)) { + undiscovered.addLast(callee); + } + } + } + + ArrayDeque changedFunctions = new ArrayDeque<>(); + Set queued = identitySet(); + for (Map.Entry entry : readsByFunction.entrySet()) { + if (!entry.getValue().isEmpty()) { + changedFunctions.addLast(entry.getKey()); + queued.add(entry.getKey()); + } } - for (WEntity entity : packageOfGlobal.getElements()) { - if (entity instanceof InitBlock - && entity.attrSource().getLeftPos() < global.attrSource().getLeftPos()) { - return false; + while (!changedFunctions.isEmpty()) { + ImFunction callee = changedFunctions.removeFirst(); + queued.remove(callee); + BitSet calleeReads = readsByFunction.get(callee); + for (ImFunction caller : callers.getOrDefault(callee, Collections.emptySet())) { + BitSet callerReads = readsByFunction.computeIfAbsent(caller, ignored -> new BitSet()); + int before = callerReads.cardinality(); + callerReads.or(calleeReads); + if (callerReads.cardinality() != before && queued.add(caller)) { + changedFunctions.addLast(caller); + } } } - for (ImVar other : prog.getGlobals()) { - if (other.getTrace() instanceof GlobalVarDef - && packageOf((GlobalVarDef) other.getTrace()) == packageOfGlobal - && other.getTrace().attrSource().getLeftPos() < global.attrSource().getLeftPos() - && !other.attrWrites().isEmpty()) { - return false; + + BitSet pending = new BitSet(candidates.size()); + pending.set(0, candidates.size()); + ImFunction config = trans.getConfFunc(); + if (config != null) { + scanStartupStatements(config.getBody(), pending, unsafe, readsByStatement, writesByStatement, + readsByFunction); + } + if (!initializationOrder.isEmpty()) { + scanStartupStatements(initializationOrder.get(0).getBody(), pending, unsafe, readsByStatement, + writesByStatement, readsByFunction); + scanMainPrefix(trans, initializationOrder, pending, unsafe, readsByStatement, writesByStatement, + readsByFunction); + } + for (int i = 1; i < initializationOrder.size(); i++) { + ImFunction initializer = initializationOrder.get(i); + scanStartupStatements(initializer.getBody(), pending, unsafe, readsByStatement, writesByStatement, + readsByFunction); + } + unsafe.or(pending); + + Set safeConstants = identitySet(); + for (int i = 0; i < candidates.size(); i++) { + if (!unsafe.get(i)) { + safeConstants.add(candidates.get(i)); } } - return true; + return new LiteralConstantAnalysis(safeConstants, replacementWrites); } - @Nullable - private static WPackage packageOf(GlobalVarDef global) { - de.peeeq.wurstscript.ast.Element element = global; - while (element != null && !(element instanceof WPackage)) { - element = element.getParent(); + private static void scanMainPrefix(ImTranslator trans, List initializationOrder, + BitSet pending, BitSet unsafe, + Map readsByStatement, + Map writesByStatement, + Map readsByFunction) { + Set packageInitializers = identitySet(); + packageInitializers.addAll(initializationOrder.subList(1, initializationOrder.size())); + for (ImStmt statement : trans.getMainFunc().getBody()) { + Set usedFunctions = directlyUsedFunctions(statement); + if (!Collections.disjoint(usedFunctions, packageInitializers)) { + return; + } + scanStartupStatements(Collections.singleton(statement), pending, unsafe, readsByStatement, + writesByStatement, readsByFunction); } - return (WPackage) element; } - private boolean isInitializedLater(WPackage target) { - if (!initLaterPackagesCollected) { - collectInitLaterPackages(target); - initLaterPackagesCollected = true; + private static void scanStartupStatements(Collection statements, BitSet pending, BitSet unsafe, + Map readsByStatement, + Map writesByStatement, + Map readsByFunction) { + for (ImStmt statement : statements) { + BitSet reads = readsByStatement.containsKey(statement) + ? (BitSet) readsByStatement.get(statement).clone() : new BitSet(); + for (ImFunction used : directlyUsedFunctions(statement)) { + BitSet functionReads = readsByFunction.get(used); + if (functionReads != null) { + reads.or(functionReads); + } + } + reads.and(pending); + unsafe.or(reads); + BitSet writes = writesByStatement.get(statement); + if (writes != null) { + pending.andNot(writes); + } } - return initLaterPackages.contains(target); } - private void collectInitLaterPackages(WPackage target) { - for (CompilationUnit unit : target.getModel()) { - for (WPackage candidate : unit.getPackages()) { - for (WImport imported : candidate.getImports()) { - if (imported.getIsInitLater() && imported.attrImportedPackage() instanceof WPackage importedPackage) { - initLaterPackages.add(importedPackage); + private static Set directlyUsedFunctions(ImStmt statement) { + Set result = identitySet(); + statement.accept(new ImStmt.DefaultVisitor() { + @Override + public void visit(ImFunctionCall call) { + super.visit(call); + result.add(call.getFunc()); + } + + @Override + public void visit(ImFuncRef ref) { + super.visit(ref); + result.add(ref.getFunc()); + } + + @Override + public void visit(ImMethodCall call) { + super.visit(call); + if (call.getMethod().getImplementation() != null) { + result.add(call.getMethod().getImplementation()); + } + for (ImMethod subMethod : call.getMethod().getSubMethods()) { + if (subMethod.getImplementation() != null) { + result.add(subMethod.getImplementation()); } } } + }); + return result; + } + + @Nullable + @SuppressWarnings("ReferenceEquality") + private static ImStmt topLevelStatement(de.peeeq.wurstscript.jassIm.Element element, ImFunction function) { + de.peeeq.wurstscript.jassIm.Element current = element; + while (current != null && current.getParent() != function.getBody()) { + current = current.getParent(); + } + return current instanceof ImStmt ? (ImStmt) current : null; + } + + private static boolean isSourceConstant(ImVar var) { + if (!(var.getTrace() instanceof GlobalVarDef)) { + return false; + } + if (var.getName().equals("MagicFunctions_compiletime") + || var.getName().equals("MagicFunctions_isLua")) { + // These values depend on compiler execution context/backend and are lowered by their + // dedicated paths. They are not ordinary source literals for package-constant folding. + return false; + } + GlobalVarDef global = (GlobalVarDef) var.getTrace(); + return global.attrIsConstant() && !global.hasAnnotation("@configurable"); + } + + private static Set identitySet() { + return Collections.newSetFromMap(new IdentityHashMap<>()); + } + + private static final class LiteralConstantAnalysis { + private final Set safeConstants; + private final Map replacementWrites; + + private LiteralConstantAnalysis(Set safeConstants, Map replacementWrites) { + this.safeConstants = safeConstants; + this.replacementWrites = replacementWrites; } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index 34a6bd684..c59fbd0db 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -165,6 +165,9 @@ public T canonical(T copy) { public final Map initFuncMap = new Object2ObjectLinkedOpenHashMap<>(); + /** Initializer functions in the exact order emitted by {@link #finishInitFunctions()}. */ + private final List initializationOrder = new ArrayList<>(); + /** * When targeting Lua, package init functions that should be called directly via xpcall * rather than through the JASS TriggerEvaluate thread-isolation pattern. @@ -676,6 +679,8 @@ private void translateCompilationUnit(CompilationUnit cu) { private void finishInitFunctions() { + initializationOrder.clear(); + initializationOrder.add(globalInitFunc); // init globals, at beginning of main func: getMainFunc().getBody().add(0, ImFunctionCall(emptyTrace, globalInitFunc, ImTypeArguments(), ImExprs(), false, CallType.NORMAL)); @@ -744,6 +749,7 @@ private void callInitFunc(Set calledInitializers, WPackage p, @Nullabl if (initFunc.getBody().size() == 0) { return; } + initializationOrder.add(initFunc); if (isLuaTarget()) { // In Lua mode, xpcall replaces TriggerEvaluate for error isolation without WC3 handle overhead. // Record the init function so the Lua translator can wrap it with xpcall. @@ -1531,7 +1537,9 @@ private Multimap getCallRelations() { public ImFunction getMainFunc() { return mainFunc; } public ImFunction getConfFunc() { return configFunc; } - + public List getInitializationOrder() { + return Collections.unmodifiableList(initializationOrder); + } /** * returns a list of classes and functions implementing funcDef diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index a3509ed32..41b300513 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -63,6 +63,7 @@ public void packageConstantsInlineAndRemoveDeadGuards() { String compiled = compileOptimizedLuaWithStdLib( "packageConstantsInlineAndRemoveDeadGuards", "package Test", + "public constant bool COMPILETIME_DISABLED = compiletime(false)", "public constant int VALUE = 7", "public constant bool DISABLED = false", "public constant bool ENABLED = true", @@ -71,7 +72,11 @@ public void packageConstantsInlineAndRemoveDeadGuards() { "bool active", "function dead()", " consume(VALUE)", + "function compiletimeDead()", + " consume(VALUE)", "function guarded()", + " if COMPILETIME_DISABLED and active", + " compiletimeDead()", " if DISABLED and active", " dead()", " if ENABLED and active", @@ -83,9 +88,9 @@ public void packageConstantsInlineAndRemoveDeadGuards() { assertFalse("constant uses must be emitted as literals:\n" + compiled, compiled.contains("consume(Test_VALUE)") || compiled.contains("Test_DISABLED and") - || compiled.contains("Test_ENABLED and")); + || compiled.contains("Test_ENABLED and") || compiled.contains("Test_COMPILETIME_DISABLED and")); assertFalse("a false constant guard must remove its unreachable callee:\n" + compiled, - compiled.contains("function dead(")); + compiled.contains("function dead(") || compiled.contains("function compiletimeDead(")); assertTrue("a true constant guard must retain its dynamic condition:\n" + compiled, compiled.contains("if Test_active then")); assertTrue("constant arithmetic uses must be emitted as literals:\n" + compiled, 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 1a79f6dcc..e780e696d 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 @@ -35,8 +35,9 @@ public class OptimizerTests extends WurstScriptTest { @Test public void packageConstantsInlineAndRemoveDeadGuardsInJass() throws IOException { - test().lines( + test().withStdLib().runCompiletimeFunctions(true).lines( "package Test", + "public constant bool COMPILETIME_DISABLED = compiletime(false)", "public constant int VALUE = 7", "public constant bool DISABLED = false", "public constant bool ENABLED = true", @@ -45,7 +46,11 @@ public void packageConstantsInlineAndRemoveDeadGuardsInJass() throws IOException "bool active", "function dead()", " consume(VALUE)", + "function compiletimeDead()", + " consume(VALUE)", "function guarded()", + " if COMPILETIME_DISABLED and active", + " compiletimeDead()", " if DISABLED and active", " dead()", " if ENABLED and active", @@ -58,8 +63,9 @@ public void packageConstantsInlineAndRemoveDeadGuardsInJass() throws IOException String compiled = Files.toString( new File("test-output/OptimizerTests_packageConstantsInlineAndRemoveDeadGuardsInJass_inlopt.j"), Charsets.UTF_8); - assertFalse(compiled.contains("Test_VALUE") || compiled.contains("Test_DISABLED") || compiled.contains("Test_ENABLED")); - assertFalse(compiled.contains("function Test_dead takes")); + assertFalse(compiled.contains("Test_VALUE") || compiled.contains("Test_DISABLED") || compiled.contains("Test_ENABLED") + || compiled.contains("Test_COMPILETIME_DISABLED")); + assertFalse(compiled.contains("function Test_dead takes") || compiled.contains("function Test_compiletimeDead takes")); assertTrue(compiled.contains("if Test_active then")); assertTrue(compiled.contains("call consume(7)")); assertTrue(compiled.contains("Test_CONFIGURABLE")); @@ -85,6 +91,54 @@ public void laterPackageConstantIsNotInlinedIntoEarlierInitializers() throws IOE assertTrue(compiled.contains("Test_LATER")); } + @Test + public void superclassTranslationOrderPreservesLaterConstant() throws IOException { + test().lines( + "package Test", + "native consume(int value)", + "class Child extends Parent", + "constant int LATER = 7", + "class Parent", + " static int observed = readLater()", + "function readLater() returns int", + " return LATER", + "init", + " consume(Parent.observed)" + ); + String compiled = Files.toString( + new File("test-output/OptimizerTests_superclassTranslationOrderPreservesLaterConstant_inlopt.j"), + Charsets.UTF_8); + assertTrue(compiled.contains("Test_LATER")); + } + + @Test + public void initlaterAnalysisIsCompilationScoped() throws IOException { + compileInitlaterConstantRepro("first", "First"); + compileInitlaterConstantRepro("second", "Second"); + + String compiled = Files.toString( + new File("test-output/OptimizerTests_initlaterAnalysis_second_inlopt.j"), + Charsets.UTF_8); + assertTrue(compiled.contains("SecondValue_VALUE")); + } + + private void compileInitlaterConstantRepro(String testName, String prefix) { + testNamed("initlaterAnalysis_" + testName).compilationUnits( + compilationUnit(prefix + "Value", + "package " + prefix + "Value", + "public constant int VALUE = 7", + "public function readValue() returns int", + " return VALUE"), + compilationUnit(prefix + "Reader", + "package " + prefix + "Reader", + "import initlater " + prefix + "Value", + "native consume(int value)", + "int observed = readValue()", + "init", + " consume(observed)") + ); + } + @Test From 6ae105aea5e10eb5009ddc5b2cbb7896628281a4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 15:19:46 +0200 Subject: [PATCH 09/10] Preserve constants after abortable initialization --- .../imoptimizer/GlobalsInliner.java | 22 +++++++++++++---- .../wurstscript/tests/OptimizerTests.java | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index f559649fc..0302b79cb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -252,6 +252,7 @@ public void visit(ImSet write) { IdentityHashMap readsByFunction = new IdentityHashMap<>(); IdentityHashMap readsByStatement = new IdentityHashMap<>(); IdentityHashMap writesByStatement = new IdentityHashMap<>(); + IdentityHashMap writesByInitializer = new IdentityHashMap<>(); BitSet unsafe = new BitSet(candidates.size()); for (int i = 0; i < candidates.size(); i++) { @@ -274,6 +275,7 @@ public void visit(ImSet write) { : topLevelStatement((de.peeeq.wurstscript.jassIm.Element) write, function); if (statement != null) { writesByStatement.computeIfAbsent(statement, ignored -> new BitSet()).set(i); + writesByInitializer.computeIfAbsent(function, ignored -> new BitSet()).set(i); } } } @@ -321,18 +323,18 @@ public void visit(ImSet write) { ImFunction config = trans.getConfFunc(); if (config != null) { scanStartupStatements(config.getBody(), pending, unsafe, readsByStatement, writesByStatement, - readsByFunction); + readsByFunction, null); } if (!initializationOrder.isEmpty()) { scanStartupStatements(initializationOrder.get(0).getBody(), pending, unsafe, readsByStatement, - writesByStatement, readsByFunction); + writesByStatement, readsByFunction, null); scanMainPrefix(trans, initializationOrder, pending, unsafe, readsByStatement, writesByStatement, readsByFunction); } for (int i = 1; i < initializationOrder.size(); i++) { ImFunction initializer = initializationOrder.get(i); scanStartupStatements(initializer.getBody(), pending, unsafe, readsByStatement, writesByStatement, - readsByFunction); + readsByFunction, writesByInitializer.get(initializer)); } unsafe.or(pending); @@ -358,14 +360,15 @@ private static void scanMainPrefix(ImTranslator trans, List initiali return; } scanStartupStatements(Collections.singleton(statement), pending, unsafe, readsByStatement, - writesByStatement, readsByFunction); + writesByStatement, readsByFunction, null); } } private static void scanStartupStatements(Collection statements, BitSet pending, BitSet unsafe, Map readsByStatement, Map writesByStatement, - Map readsByFunction) { + Map readsByFunction, + @Nullable BitSet writesInAbortableInitializer) { for (ImStmt statement : statements) { BitSet reads = readsByStatement.containsKey(statement) ? (BitSet) readsByStatement.get(statement).clone() : new BitSet(); @@ -377,6 +380,11 @@ private static void scanStartupStatements(Collection statements, BitSet } reads.and(pending); unsafe.or(reads); + if (writesInAbortableInitializer != null && !isDefinitelyNonAbortingInitializerStatement(statement)) { + BitSet skippedWrites = (BitSet) pending.clone(); + skippedWrites.and(writesInAbortableInitializer); + unsafe.or(skippedWrites); + } BitSet writes = writesByStatement.get(statement); if (writes != null) { pending.andNot(writes); @@ -384,6 +392,10 @@ private static void scanStartupStatements(Collection statements, BitSet } } + private static boolean isDefinitelyNonAbortingInitializerStatement(ImStmt statement) { + return statement instanceof ImSet set && set.getLeft() instanceof ImVarAccess && isLiteral(set.getRight()); + } + private static Set directlyUsedFunctions(ImStmt statement) { Set result = identitySet(); statement.accept(new ImStmt.DefaultVisitor() { 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 e780e696d..c6dbd10fb 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 @@ -111,6 +111,30 @@ public void superclassTranslationOrderPreservesLaterConstant() throws IOExceptio assertTrue(compiled.contains("Test_LATER")); } + @Test + public void abortableInitializerBeforeConstantPreservesLaterWrite() throws IOException { + test().compilationUnits( + compilationUnit("AbortBeforeConstant", + "package AbortBeforeConstant", + "native abortInitialization()", + "init", + " abortInitialization()", + "public constant int LATER = 7"), + compilationUnit("ReadAfterAbort", + "package ReadAfterAbort", + "import AbortBeforeConstant", + "constant int SAFE = 11", + "native consume(int value)", + "init", + " consume(LATER + SAFE)") + ); + String compiled = Files.toString( + new File("test-output/OptimizerTests_abortableInitializerBeforeConstantPreservesLaterWrite_inlopt.j"), + Charsets.UTF_8); + assertTrue(compiled.contains("AbortBeforeConstant_LATER")); + assertFalse(compiled.contains("ReadAfterAbort_SAFE")); + } + @Test public void initlaterAnalysisIsCompilationScoped() throws IOException { compileInitlaterConstantRepro("first", "First"); From 856a80d5dca5c1ea66ef5a0f0c0de971e0c8448e Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 5 Sep 2026 15:41:41 +0200 Subject: [PATCH 10/10] Make startup constant analysis sparse --- .../imoptimizer/GlobalsInliner.java | 130 ++++++++---------- 1 file changed, 56 insertions(+), 74 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java index 0302b79cb..5e5de74de 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/GlobalsInliner.java @@ -15,6 +15,7 @@ import java.util.BitSet; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -246,13 +247,10 @@ public void visit(ImSet write) { return new LiteralConstantAnalysis(identitySet(), replacementWrites); } - Set functions = identitySet(); - functions.addAll(ImHelper.calculateFunctionsOfProg(prog)); - functions.addAll(initializationOrder); - IdentityHashMap readsByFunction = new IdentityHashMap<>(); - IdentityHashMap readsByStatement = new IdentityHashMap<>(); - IdentityHashMap writesByStatement = new IdentityHashMap<>(); - IdentityHashMap writesByInitializer = new IdentityHashMap<>(); + IdentityHashMap> readsByFunction = new IdentityHashMap<>(); + IdentityHashMap> readsByStatement = new IdentityHashMap<>(); + IdentityHashMap> writesByStatement = new IdentityHashMap<>(); + IdentityHashMap> writesByInitializer = new IdentityHashMap<>(); BitSet unsafe = new BitSet(candidates.size()); for (int i = 0; i < candidates.size(); i++) { @@ -263,10 +261,10 @@ public void visit(ImSet write) { unsafe.set(i); continue; } - readsByFunction.computeIfAbsent(function, ignored -> new BitSet()).set(i); + readsByFunction.computeIfAbsent(function, ignored -> new HashSet<>()).add(i); ImStmt statement = topLevelStatement((de.peeeq.wurstscript.jassIm.Element) read, function); if (statement != null) { - readsByStatement.computeIfAbsent(statement, ignored -> new BitSet()).set(i); + readsByStatement.computeIfAbsent(statement, ignored -> new HashSet<>()).add(i); } } for (ImVarWrite write : candidate.attrWrites()) { @@ -274,67 +272,30 @@ public void visit(ImSet write) { ImStmt statement = function == null ? null : topLevelStatement((de.peeeq.wurstscript.jassIm.Element) write, function); if (statement != null) { - writesByStatement.computeIfAbsent(statement, ignored -> new BitSet()).set(i); - writesByInitializer.computeIfAbsent(function, ignored -> new BitSet()).set(i); - } - } - } - - IdentityHashMap> callers = new IdentityHashMap<>(); - ArrayDeque undiscovered = new ArrayDeque<>(functions); - while (!undiscovered.isEmpty()) { - ImFunction caller = undiscovered.removeFirst(); - readsByFunction.computeIfAbsent(caller, ignored -> new BitSet()); - for (ImFunction callee : caller.calcUsedFunctions()) { - if (callee == null) { - continue; - } - callers.computeIfAbsent(callee, ignored -> identitySet()).add(caller); - if (functions.add(callee)) { - undiscovered.addLast(callee); - } - } - } - - ArrayDeque changedFunctions = new ArrayDeque<>(); - Set queued = identitySet(); - for (Map.Entry entry : readsByFunction.entrySet()) { - if (!entry.getValue().isEmpty()) { - changedFunctions.addLast(entry.getKey()); - queued.add(entry.getKey()); - } - } - while (!changedFunctions.isEmpty()) { - ImFunction callee = changedFunctions.removeFirst(); - queued.remove(callee); - BitSet calleeReads = readsByFunction.get(callee); - for (ImFunction caller : callers.getOrDefault(callee, Collections.emptySet())) { - BitSet callerReads = readsByFunction.computeIfAbsent(caller, ignored -> new BitSet()); - int before = callerReads.cardinality(); - callerReads.or(calleeReads); - if (callerReads.cardinality() != before && queued.add(caller)) { - changedFunctions.addLast(caller); + writesByStatement.computeIfAbsent(statement, ignored -> new HashSet<>()).add(i); + writesByInitializer.computeIfAbsent(function, ignored -> new HashSet<>()).add(i); } } } BitSet pending = new BitSet(candidates.size()); pending.set(0, candidates.size()); + Set reachableFromStartup = identitySet(); ImFunction config = trans.getConfFunc(); if (config != null) { scanStartupStatements(config.getBody(), pending, unsafe, readsByStatement, writesByStatement, - readsByFunction, null); + readsByFunction, reachableFromStartup, null); } if (!initializationOrder.isEmpty()) { scanStartupStatements(initializationOrder.get(0).getBody(), pending, unsafe, readsByStatement, - writesByStatement, readsByFunction, null); + writesByStatement, readsByFunction, reachableFromStartup, null); scanMainPrefix(trans, initializationOrder, pending, unsafe, readsByStatement, writesByStatement, - readsByFunction); + readsByFunction, reachableFromStartup); } for (int i = 1; i < initializationOrder.size(); i++) { ImFunction initializer = initializationOrder.get(i); scanStartupStatements(initializer.getBody(), pending, unsafe, readsByStatement, writesByStatement, - readsByFunction, writesByInitializer.get(initializer)); + readsByFunction, reachableFromStartup, writesByInitializer.get(initializer)); } unsafe.or(pending); @@ -349,9 +310,10 @@ public void visit(ImSet write) { private static void scanMainPrefix(ImTranslator trans, List initializationOrder, BitSet pending, BitSet unsafe, - Map readsByStatement, - Map writesByStatement, - Map readsByFunction) { + Map> readsByStatement, + Map> writesByStatement, + Map> readsByFunction, + Set reachableFromStartup) { Set packageInitializers = identitySet(); packageInitializers.addAll(initializationOrder.subList(1, initializationOrder.size())); for (ImStmt statement : trans.getMainFunc().getBody()) { @@ -360,34 +322,54 @@ private static void scanMainPrefix(ImTranslator trans, List initiali return; } scanStartupStatements(Collections.singleton(statement), pending, unsafe, readsByStatement, - writesByStatement, readsByFunction, null); + writesByStatement, readsByFunction, reachableFromStartup, null); } } private static void scanStartupStatements(Collection statements, BitSet pending, BitSet unsafe, - Map readsByStatement, - Map writesByStatement, - Map readsByFunction, - @Nullable BitSet writesInAbortableInitializer) { + Map> readsByStatement, + Map> writesByStatement, + Map> readsByFunction, + Set reachableFromStartup, + @Nullable Set writesInAbortableInitializer) { for (ImStmt statement : statements) { - BitSet reads = readsByStatement.containsKey(statement) - ? (BitSet) readsByStatement.get(statement).clone() : new BitSet(); - for (ImFunction used : directlyUsedFunctions(statement)) { - BitSet functionReads = readsByFunction.get(used); - if (functionReads != null) { - reads.or(functionReads); - } + BitSet reads = new BitSet(); + for (int candidate : readsByStatement.getOrDefault(statement, Collections.emptySet())) { + reads.set(candidate); } + addNewlyReachableReads(statement, reachableFromStartup, readsByFunction, reads); reads.and(pending); unsafe.or(reads); if (writesInAbortableInitializer != null && !isDefinitelyNonAbortingInitializerStatement(statement)) { - BitSet skippedWrites = (BitSet) pending.clone(); - skippedWrites.and(writesInAbortableInitializer); - unsafe.or(skippedWrites); + for (int candidate : writesInAbortableInitializer) { + if (pending.get(candidate)) { + unsafe.set(candidate); + } + } + } + for (int candidate : writesByStatement.getOrDefault(statement, Collections.emptySet())) { + pending.clear(candidate); } - BitSet writes = writesByStatement.get(statement); - if (writes != null) { - pending.andNot(writes); + } + } + + private static void addNewlyReachableReads(ImStmt statement, Set reachableFromStartup, + Map> readsByFunction, BitSet reads) { + ArrayDeque undiscovered = new ArrayDeque<>(); + for (ImFunction function : directlyUsedFunctions(statement)) { + if (function != null && reachableFromStartup.add(function)) { + undiscovered.addLast(function); + } + } + while (!undiscovered.isEmpty()) { + ImFunction function = undiscovered.removeFirst(); + for (int candidate : readsByFunction.getOrDefault(function, Collections.emptySet())) { + reads.set(candidate); + } + for (ImFunction callee : function.calcUsedFunctions()) { + if (callee != null && reachableFromStartup.add(callee)) { + undiscovered.addLast(callee); + } } } }