From 834228d47556e33b83c763943eabec359e9129b4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 15:10:36 +0200 Subject: [PATCH 1/8] Preserve externally visible names during optimization --- .../de/peeeq/wurstscript/WurstChecker.java | 2 - .../imoptimizer/GlobalsInliner.java | 6 +-- .../translation/imoptimizer/ImCompressor.java | 10 ++-- .../translation/imoptimizer/ImOptimizer.java | 10 ++-- .../imtranslation/ExprTranslation.java | 2 + .../imtranslation/FunctionFlagEnum.java | 5 +- .../imtranslation/ImTranslator.java | 12 ++--- .../lua/translation/RemoveGarbage.java | 4 +- .../validation/NamePreservation.java | 50 +++++++++++++++++++ .../wurstscript/validation/TRVEHelper.java | 7 --- .../validation/WurstValidator.java | 12 ++++- .../wurstscript/tests/OptimizerTests.java | 44 ++++++++++++++++ .../wurstscript/tests/WurstScriptTest.java | 9 ++++ 13 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java delete mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/TRVEHelper.java diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java index c8fe3a40a..27e44e1c1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java @@ -7,7 +7,6 @@ import de.peeeq.wurstscript.attributes.names.DesugarArrayLength; import de.peeeq.wurstscript.gui.WurstGui; import de.peeeq.wurstscript.validation.GlobalCaches; -import de.peeeq.wurstscript.validation.TRVEHelper; import de.peeeq.wurstscript.validation.WurstValidator; import java.util.ArrayList; @@ -36,7 +35,6 @@ public void checkProg(WurstModel root, Collection toCheck) { if (root.isEmpty()) { return; } - TRVEHelper.protectedVariables.clear(); new DesugarArrayLength().run(root); gui.sendProgress("Checking Files"); 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 b65abff35..dd4e99051 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 @@ -6,7 +6,7 @@ import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.utils.Utils; -import de.peeeq.wurstscript.validation.TRVEHelper; +import de.peeeq.wurstscript.validation.NamePreservation; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -47,8 +47,8 @@ public int optimize(ImTranslator trans) { // cannot optimize arrays yet continue; } - if (TRVEHelper.protectedVariables.contains(v.getName())) { - // keep TRVE vars + if (NamePreservation.isPreserved(v)) { + // keep names which are part of the external Warcraft III API continue; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java index 8f9c0f2f4..c2e41e24f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java @@ -5,7 +5,7 @@ import de.peeeq.wurstscript.jassIm.ImVar; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; -import de.peeeq.wurstscript.validation.TRVEHelper; +import de.peeeq.wurstscript.validation.NamePreservation; public class ImCompressor { @@ -27,9 +27,8 @@ public void compressNames() { public void compressGlobals() { for (final ImVar global : prog.getGlobals()) { - if (global.getIsBJ() || TRVEHelper.protectedVariables.contains(global.getName())) { - // do not rename bj constants - // do not rename TRVE vars + if (global.getIsBJ() || NamePreservation.isPreserved(global)) { + // do not rename bj constants or names exposed to Warcraft III continue; } @@ -41,7 +40,8 @@ public void compressGlobals() { public void compressFunctions() { for (ImFunction func : ImHelper.calculateFunctionsOfProg(prog)) { - if (func.isNative() || func.isBj() || func.isCompiletime() || func.isExtern()) { + if (func.isNative() || func.isBj() || func.isCompiletime() || func.isExtern() + || NamePreservation.isPreserved(func)) { // do not rename builtin an bj functions continue; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java index c01c52877..ee63a7e8d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImOptimizer.java @@ -17,7 +17,7 @@ import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.utils.Pair; -import de.peeeq.wurstscript.validation.TRVEHelper; +import de.peeeq.wurstscript.validation.NamePreservation; import java.util.stream.Collectors; @@ -174,13 +174,13 @@ public void visit(ImSet e) { super.visit(e); if (e.getLeft() instanceof ImVarAccess) { ImVarAccess va = (ImVarAccess) e.getLeft(); - if (!readVars.contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { + if (!readVars.contains(va.getVar()) && !NamePreservation.isPreserved(va.getVar())) { List sideEffects = collectSideEffects(e.getRight(), sideEffectAnalyzer); replacements.add(Pair.create(e, sideEffects)); } } else if (e.getLeft() instanceof ImVarArrayAccess) { ImVarArrayAccess va = (ImVarArrayAccess) e.getLeft(); - if (!readVars.contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { + if (!readVars.contains(va.getVar()) && !NamePreservation.isPreserved(va.getVar())) { List exprs = new ArrayList<>(); for (ImExpr index : va.getIndexes()) { exprs.addAll(collectSideEffects(index, sideEffectAnalyzer)); @@ -190,13 +190,13 @@ public void visit(ImSet e) { } } else if (e.getLeft() instanceof ImTupleSelection) { ImVar var = TypesHelper.getTupleVar((ImTupleSelection) e.getLeft()); - if(var != null && !readVars.contains(var) && !TRVEHelper.protectedVariables.contains(var.getName())) { + if(var != null && !readVars.contains(var) && !NamePreservation.isPreserved(var)) { List sideEffects = collectSideEffects(e.getRight(), sideEffectAnalyzer); replacements.add(Pair.create(e, sideEffects)); } } else if(e.getLeft() instanceof ImMemberAccess) { ImMemberAccess va = ((ImMemberAccess) e.getLeft()); - if (!readVars.contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { + if (!readVars.contains(va.getVar()) && !NamePreservation.isPreserved(va.getVar())) { List sideEffects = collectSideEffects(e.getRight(), sideEffectAnalyzer); replacements.add(Pair.create(e, sideEffects)); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index c5e3e2e70..5420899d0 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -14,6 +14,7 @@ import de.peeeq.wurstscript.attributes.names.OtherLink; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.types.*; +import de.peeeq.wurstscript.validation.NamePreservation; import de.peeeq.wurstscript.utils.Utils; import io.vavr.control.Either; import io.vavr.control.Option; @@ -588,6 +589,7 @@ private static ImExpr translateFunctionCall(FunctionCall e, ImTranslator t, ImFu String exFunc = s.getValS(); NameLink func = Utils.getFirst(e.lookupFuncs(exFunc)); ImFunction executedFunc = t.getFuncFor((TranslatedToImFunction) func.getDef()); + NamePreservation.preserve(executedFunc); return ImFunctionCall(e, executedFunc, ImTypeArguments(), JassIm.ImExprs(), true, CallType.EXECUTE); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/FunctionFlagEnum.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/FunctionFlagEnum.java index d619b3252..ee48d0586 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/FunctionFlagEnum.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/FunctionFlagEnum.java @@ -6,6 +6,7 @@ public enum FunctionFlagEnum implements FunctionFlag { IS_TEST, IS_COMPILETIME_NATIVE, IS_EXTERN, - IS_VARARG + IS_VARARG, + PRESERVE_NAME -} \ No newline at end of file +} 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 454436cdc..fe361ec0d 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 @@ -19,7 +19,7 @@ import de.peeeq.wurstscript.types.*; import de.peeeq.wurstscript.utils.Pair; import de.peeeq.wurstscript.utils.Utils; -import de.peeeq.wurstscript.validation.TRVEHelper; +import de.peeeq.wurstscript.validation.NamePreservation; import de.peeeq.wurstscript.validation.WurstValidator; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; @@ -1067,6 +1067,9 @@ public ImFunction getFuncFor(TranslatedToImFunction funcDef) { if (m instanceof Annotation) { Annotation annotation = (Annotation) m; flags.add(new FunctionFlagAnnotation(annotation.getAnnotationType())); + if (NamePreservation.isPreserveAnnotation(annotation.getAnnotationType())) { + flags.add(PRESERVE_NAME); + } } } } @@ -1462,12 +1465,9 @@ private void calculateCallRelationsAndVariables(boolean includeUsedVariables) { final ImFunction conf = getConfFunc(); if (conf != null && conf != main) calculateCallRelations(conf, includeUsedVariables); - // mark protected globals as read - // TRVEHelper.protectedVariables is presumably a HashSet (O(1) contains) + // Mark externally visible globals as read so they survive garbage collection. for (ImVar global : imProg.getGlobals()) { - if (TRVEHelper.protectedVariables.contains(global.getName())) { - readVariables.add(global); - } + if (NamePreservation.isPreserved(global)) readVariables.add(global); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java index e79acb1f1..4feccc344 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java @@ -5,7 +5,7 @@ import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; -import de.peeeq.wurstscript.validation.TRVEHelper; +import de.peeeq.wurstscript.validation.NamePreservation; import java.util.Collection; import java.util.Collections; @@ -122,7 +122,7 @@ public static void removeGarbage(ImProg prog, ImTranslator translator) { Used used = collectUsed(prog, translator); prog.getClasses().removeIf(c -> !used.getClasses().contains(c)); - prog.getGlobals().removeIf(g -> !used.getVars().contains(g) && !TRVEHelper.protectedVariables.contains(g.getName())); + prog.getGlobals().removeIf(g -> !used.getVars().contains(g) && !NamePreservation.isPreserved(g)); prog.getFunctions().removeIf(f -> !used.getFunctions().contains(f)); prog.getMethods().removeIf(m -> !used.getMethods().contains(m)); for (ImMethod m : prog.getMethods()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java new file mode 100644 index 000000000..372c2f4bd --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java @@ -0,0 +1,50 @@ +package de.peeeq.wurstscript.validation; + +import de.peeeq.wurstscript.ast.Annotation; +import de.peeeq.wurstscript.ast.Ast; +import de.peeeq.wurstscript.ast.GlobalVarDef; +import de.peeeq.wurstscript.ast.NameDef; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.jassIm.ImVar; +import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum; + +/** Metadata for names which are part of the Warcraft III-facing API. */ +public final class NamePreservation { + + public static final String ANNOTATION = "@preserveName"; + + private NamePreservation() { + } + + public static boolean isPreserved(ImFunction function) { + return function.hasFlag(FunctionFlagEnum.PRESERVE_NAME); + } + + public static boolean isPreserved(ImVar variable) { + return variable.getTrace() instanceof NameDef + && ((NameDef) variable.getTrace()).hasAnnotation(ANNOTATION); + } + + public static void preserve(ImFunction function) { + if (!isPreserved(function)) { + function.getFlags().add(FunctionFlagEnum.PRESERVE_NAME); + } + } + + /** + * Marks a resolved global used by TriggerRegisterVariableEvent without maintaining a + * name-based side table. The marker remains attached to the AST definition and is copied to + * the corresponding IM variable through its trace. + */ + public static void preserve(GlobalVarDef variable) { + if (!variable.hasAnnotation(ANNOTATION)) { + Annotation marker = Ast.Annotation(variable.getSource(), + Ast.Identifier(variable.getSource(), ANNOTATION.substring(1)), Ast.Arguments()); + variable.getModifiers().add(marker); + } + } + + public static boolean isPreserveAnnotation(String annotation) { + return annotation.equalsIgnoreCase(ANNOTATION); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/TRVEHelper.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/TRVEHelper.java deleted file mode 100644 index ee531dcc5..000000000 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/TRVEHelper.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.peeeq.wurstscript.validation; - -import java.util.HashSet; - -public class TRVEHelper { - public static final HashSet protectedVariables = new HashSet<>(); -} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 5d2f55ba3..1de22c63b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -177,7 +177,7 @@ private void postChecks(Collection toCheck) { for (FunctionCall call : wrapperCalls.get(wrapper)) { if (call.getArgs().size() > 1 && call.getArgs().get(1) instanceof ExprStringVal) { ExprStringVal varName = (ExprStringVal) call.getArgs().get(1); - TRVEHelper.protectedVariables.add(varName.getValS()); + preserveVariableName(call, varName.getValS()); WLogger.info("keep: " + varName.getValS()); } else { call.addError("Map contains TriggerRegisterVariableEvent with non-constant arguments. Can't be optimized."); @@ -3670,7 +3670,7 @@ private void checkBannedFunctions(ExprFunctionCall e) { if (e.getArgs().size() > 1) { if (e.getArgs().get(1) instanceof ExprStringVal) { ExprStringVal varName = (ExprStringVal) e.getArgs().get(1); - TRVEHelper.protectedVariables.add(varName.getValS()); + preserveVariableName(e, varName.getValS()); WLogger.info("keep: " + varName.getValS()); return; } else if (e.getArgs().get(1) instanceof ExprVarAccess) { @@ -3728,6 +3728,14 @@ private void checkBannedFunctions(ExprFunctionCall e) { } } + private void preserveVariableName(Element useSite, String variableName) { + NameLink variable = de.peeeq.wurstscript.attributes.names.NameResolution + .lookupVarNoConfig(useSite, variableName, false); + if (variable != null && variable.getDef() instanceof GlobalVarDef) { + NamePreservation.preserve((GlobalVarDef) variable.getDef()); + } + } + private boolean isViableSwitchtype(Expr expr) { WurstType typ = expr.attrTyp(); if (typ.equalsType(WurstTypeInt.instance(), null) || typ.equalsType(WurstTypeString.instance(), null)) { 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 3e4582bb1..8661102fb 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 @@ -444,6 +444,50 @@ public void test_ConstFoldingCombined() { "endpackage"); } + @Test + public void preserveNameAnnotationExemptsFunctionFromCompression() throws IOException { + test().optimize().lines( + "package test", + " native testSuccess()", + " @preserveName function externallyCalled()", + " testSuccess()", + " function normallyCompressed()", + " testSuccess()", + " init", + " externallyCalled()", + " normallyCompressed()", + "endpackage"); + + String output = Files.toString( + new File("./test-output/OptimizerTests_preserveNameAnnotationExemptsFunctionFromCompression_opt.j"), + Charsets.UTF_8); + assertTrue(output.contains("function externallyCalled"), + "Expected @preserveName function to retain its source name.\n" + output); + assertFalse(output.contains("function normallyCompressed"), + "Expected an unannotated function to remain eligible for compression.\n" + output); + } + + @Test + public void executeFuncPreservesResolvedFunctionNameDuringCompression() throws IOException { + test().optimize().lines( + "package test", + " @extern native ExecuteFunc(string name)", + " native testSuccess()", + " function callback()", + " testSuccess()", + " init", + " ExecuteFunc(\"callback\")", + "endpackage"); + + String output = Files.toString( + new File("./test-output/OptimizerTests_executeFuncPreservesResolvedFunctionNameDuringCompression_opt.j"), + Charsets.UTF_8); + assertTrue(output.contains("function callback"), + "Expected ExecuteFunc target to retain its source name.\n" + output); + assertTrue(output.contains("ExecuteFunc(\"callback\")"), + "Expected ExecuteFunc to receive the preserved source name.\n" + output); + } + @Test public void test_tempVarRemover() throws IOException { test().lines( diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java index 023bac4e4..f0c2fe1cd 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java @@ -113,6 +113,7 @@ class TestConfig { private final List additionalCompilationUnits = new ArrayList<>(); private boolean stopOnFirstError = true; private boolean runCompiletimeFunctions; + private boolean optimize; private boolean testLua = false; private boolean luaOnly = false; private boolean uncheckedDispatch = false; @@ -157,6 +158,11 @@ public TestConfig executeTests(boolean b) { return this; } + TestConfig optimize() { + this.optimize = true; + return this; + } + TestConfig executeProg(boolean b) { this.executeProg = b; return this; @@ -269,6 +275,9 @@ private CompilationResult testScript() { if (runCompiletimeFunctions) { runArgs = runArgs.with("-runcompiletimefunctions"); } + if (optimize) { + runArgs = runArgs.with("-opt"); + } if (legacyJassTypeChecks) { runArgs.setLegacyJassTypeChecks(true); } From 4e1b5e824595d37dead9a90b38b7402cc8df8bfc Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 15:48:16 +0200 Subject: [PATCH 2/8] Keep preserved entry points reachable during optimization --- .../imtojass/ImToJassTranslator.java | 6 +++ .../imtranslation/ImTranslator.java | 8 ++++ .../lua/translation/LuaTranslator.java | 10 +++-- .../lua/translation/RemoveGarbage.java | 3 +- .../validation/NamePreservation.java | 36 +++++++++++++++-- .../validation/WurstValidator.java | 6 +-- .../wurstscript/tests/OptimizerTests.java | 39 +++++++++++++++++++ 7 files changed, 95 insertions(+), 13 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImToJassTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImToJassTranslator.java index 6a280c9cb..9247745d0 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImToJassTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ImToJassTranslator.java @@ -9,6 +9,7 @@ import de.peeeq.wurstscript.translation.imoptimizer.RestrictedCompressedNames; import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; +import de.peeeq.wurstscript.validation.NamePreservation; import de.peeeq.wurstscript.utils.Utils; import org.eclipse.jdt.annotation.Nullable; @@ -50,6 +51,11 @@ public JassProg translate() { translateFunctionTransitive(mainFunc); translateFunctionTransitive(confFunction); + for (ImFunction function : ImHelper.calculateFunctionsOfProg(imProg)) { + if (NamePreservation.isPreserved(function)) { + translateFunctionTransitive(function); + } + } return prog; } 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 fe361ec0d..0e38df8e8 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 @@ -1465,6 +1465,14 @@ private void calculateCallRelationsAndVariables(boolean includeUsedVariables) { final ImFunction conf = getConfFunc(); if (conf != null && conf != main) calculateCallRelations(conf, includeUsedVariables); + // Preserved functions are externally visible entry points even when no Wurst code calls + // them. Keep their bodies and everything they call reachable for both backends. + for (ImFunction function : ImHelper.calculateFunctionsOfProg(imProg)) { + if (NamePreservation.isPreserved(function)) { + calculateCallRelations(function, includeUsedVariables); + } + } + // Mark externally visible globals as read so they survive garbage collection. for (ImVar global : imProg.getGlobals()) { if (NamePreservation.isPreserved(global)) readVariables.add(global); 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 59f149520..ab1740279 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 @@ -14,6 +14,7 @@ import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.utils.Lazy; import de.peeeq.wurstscript.utils.Utils; +import de.peeeq.wurstscript.validation.NamePreservation; import java.util.*; import java.util.stream.Collectors; @@ -156,8 +157,10 @@ private static final class LazyArrayDefault { @Override public LuaVariable initFor(ImVar a) { String name = a.getName(); - if (!a.getIsBJ()) { + if (!a.getIsBJ() && !NamePreservation.isPreserved(a)) { name = uniqueName(name); + } else { + usedNames.add(name); } return LuaAst.LuaVariable(name, LuaAst.LuaNoExpr()); } @@ -168,9 +171,10 @@ public LuaVariable initFor(ImVar a) { @Override public LuaFunction initFor(ImFunction a) { String name = a.getName(); - if (!a.isExtern() && !a.isBj() && !a.isNative() && !isFixedEntryPoint(a)) { + if (!a.isExtern() && !a.isBj() && !a.isNative() + && !isFixedEntryPoint(a) && !NamePreservation.isPreserved(a)) { name = uniqueName(name); - } else if (isFixedEntryPoint(a)) { + } else if (isFixedEntryPoint(a) || NamePreservation.isPreserved(a)) { usedNames.add(name); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java index 4feccc344..c26b41b5e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java @@ -153,7 +153,8 @@ private static Used collectUsed(ImProg prog, ImTranslator translator, Used used = new Used(translator, ignoredInitializers); for (ImFunction f : ImHelper.calculateFunctionsOfProg(prog)) { if (f.getName().equals("main") - || f.getName().equals("config")) { + || f.getName().equals("config") + || NamePreservation.isPreserved(f)) { visitFunction(f, used); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java index 372c2f4bd..e6666a310 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java @@ -1,9 +1,6 @@ package de.peeeq.wurstscript.validation; -import de.peeeq.wurstscript.ast.Annotation; -import de.peeeq.wurstscript.ast.Ast; -import de.peeeq.wurstscript.ast.GlobalVarDef; -import de.peeeq.wurstscript.ast.NameDef; +import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.jassIm.ImFunction; import de.peeeq.wurstscript.jassIm.ImVar; import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum; @@ -44,6 +41,37 @@ public static void preserve(GlobalVarDef variable) { } } + /** + * Finds globals by their emitted runtime name, without consulting lexical name resolution. + * This is needed for native APIs such as TriggerRegisterVariableEvent whose string argument + * refers to the generated global name rather than a source-level variable access. + */ + public static void preserveGlobalWithRuntimeName(WurstModel model, String runtimeName) { + model.accept(new Element.DefaultVisitor() { + @Override + public void visit(GlobalVarDef variable) { + super.visit(variable); + if (runtimeName(variable).equals(runtimeName)) { + preserve(variable); + } + } + }); + } + + private static String runtimeName(GlobalVarDef variable) { + if (variable.getParent() != null && variable.getParent().getParent() instanceof NamedScope scope) { + return runtimeName(scope) + "_" + variable.getName(); + } + return variable.getName(); + } + + private static String runtimeName(NamedScope scope) { + if (scope instanceof ModuleInstanciation instantiation) { + return runtimeName(instantiation.getParent().attrNearestNamedScope()) + "_" + instantiation.getName(); + } + return scope.getName(); + } + public static boolean isPreserveAnnotation(String annotation) { return annotation.equalsIgnoreCase(ANNOTATION); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 1de22c63b..c87856995 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -3729,11 +3729,7 @@ private void checkBannedFunctions(ExprFunctionCall e) { } private void preserveVariableName(Element useSite, String variableName) { - NameLink variable = de.peeeq.wurstscript.attributes.names.NameResolution - .lookupVarNoConfig(useSite, variableName, false); - if (variable != null && variable.getDef() instanceof GlobalVarDef) { - NamePreservation.preserve((GlobalVarDef) variable.getDef()); - } + NamePreservation.preserveGlobalWithRuntimeName(prog, variableName); } private boolean isViableSwitchtype(Expr expr) { 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 8661102fb..61808609d 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 @@ -488,6 +488,45 @@ public void executeFuncPreservesResolvedFunctionNameDuringCompression() throws I "Expected ExecuteFunc to receive the preserved source name.\n" + output); } + @Test + public void preserveNameAnnotationKeepsExternallyCalledFunctionReachable() throws IOException { + test().optimize().lines( + "package test", + " native testSuccess()", + " @preserveName function externallyCalled()", + " testSuccess()", + "endpackage"); + + String output = Files.toString( + new File("./test-output/OptimizerTests_preserveNameAnnotationKeepsExternallyCalledFunctionReachable_opt.j"), + Charsets.UTF_8); + assertTrue(output.contains("function externallyCalled"), + "Expected an externally-called @preserveName function to survive garbage collection.\n" + output); + } + + @Test + public void trvePreservesGlobalDespiteLexicalShadow() throws IOException { + test().optimize().lines( + "type trigger extends handle", + "type event extends handle", + "type limitop extends handle", + "package test", + " int myVar = 0", + " @extern native TriggerRegisterVariableEvent(trigger whichTrigger, string varName, limitop opcode, real limitval) returns event", + " function registerVariableEvent()", + " string myVar = \"local\"", + " TriggerRegisterVariableEvent(null, \"test_myVar\", null, 0.0)", + " init", + " registerVariableEvent()", + "endpackage"); + + String output = Files.toString( + new File("./test-output/OptimizerTests_trvePreservesGlobalDespiteLexicalShadow_opt.j"), + Charsets.UTF_8); + assertTrue(output.contains("integer test_myVar"), + "Expected TRVE to preserve the global despite a local shadow.\n" + output); + } + @Test public void test_tempVarRemover() throws IOException { test().lines( From 7470b45e8cd59674e3a207a3beb34fcedaf8dea7 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 16:00:59 +0200 Subject: [PATCH 3/8] Reserve preserved names during compression --- .../translation/imoptimizer/ImCompressor.java | 26 +++++++++++++++++-- .../lua/translation/LuaTranslator.java | 5 +++- .../wurstscript/tests/OptimizerTests.java | 23 ++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java index c2e41e24f..839d1a89f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImCompressor.java @@ -7,16 +7,30 @@ import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.validation.NamePreservation; +import java.util.HashSet; +import java.util.Set; + public class ImCompressor { private final ImTranslator trans; private final ImProg prog; private final NameGenerator ng; + private final Set preservedNames = new HashSet<>(); public ImCompressor(ImTranslator translator) { this.trans = translator; this.prog = translator.getImProg(); ng = new NameGenerator(); + for (ImVar global : prog.getGlobals()) { + if (NamePreservation.isPreserved(global)) { + preservedNames.add(global.getName()); + } + } + for (ImFunction function : ImHelper.calculateFunctionsOfProg(prog)) { + if (NamePreservation.isPreserved(function)) { + preservedNames.add(function.getName()); + } + } } public void compressNames() { @@ -32,7 +46,7 @@ public void compressGlobals() { continue; } - String replacement = ng.getUniqueToken(); + String replacement = nextCompressedName(); global.setName(replacement); } @@ -50,12 +64,20 @@ public void compressFunctions() { // do not rename main and config functions continue; } - String rname = ng.getUniqueToken(); + String rname = nextCompressedName(); func.setName(rname); } } + private String nextCompressedName() { + String replacement; + do { + replacement = ng.getUniqueToken(); + } while (preservedNames.contains(replacement)); + return replacement; + } + private void compressLocals(ImFunction func) { // TODO compressing locals should not use the global name pool but use a own pool for (ImVar local : func.getParameters()) { 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 ab1740279..533c7a105 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 @@ -492,7 +492,8 @@ private boolean isFixedEntryPoint(ImFunction function) { private void collectPredefinedNames() { for (ImFunction function : prog.getFunctions()) { - if (function.isBj() || function.isExtern() || function.isNative()) { + if (function.isBj() || function.isExtern() || function.isNative() + || NamePreservation.isPreserved(function)) { // Don't rename Wurst-internal stubs (names starting with __wurst_) // since their names are intentionally different from their trace's source name. if (!function.getName().startsWith("__wurst_")) { @@ -506,6 +507,8 @@ private void collectPredefinedNames() { if (global.getIsBJ()) { setNameFromTrace(global); usedNames.add(global.getName()); + } else if (NamePreservation.isPreserved(global)) { + usedNames.add(global.getName()); } } } 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 61808609d..5f9018a0e 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 @@ -504,6 +504,29 @@ public void preserveNameAnnotationKeepsExternallyCalledFunctionReachable() throw "Expected an externally-called @preserveName function to survive garbage collection.\n" + output); } + @Test + public void preservedNamesAreReservedBeforeCompression() throws IOException { + test().optimize().lines( + "package test", + " native testSuccess()", + " function ordinary()", + " testSuccess()", + " @preserveName function w()", + " testSuccess()", + " init", + " ordinary()", + " w()", + "endpackage"); + + String output = Files.toString( + new File("./test-output/OptimizerTests_preservedNamesAreReservedBeforeCompression_opt.j"), + Charsets.UTF_8); + assertTrue(output.contains("function w"), + "Expected the preserved function name to remain available.\n" + output); + assertFalse(output.contains("function w_1"), + "Expected compression to reserve the preserved name.\n" + output); + } + @Test public void trvePreservesGlobalDespiteLexicalShadow() throws IOException { test().optimize().lines( From 5ed61014c3619d902b31a407bf484d2e88a7e3c1 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 16:20:29 +0200 Subject: [PATCH 4/8] Handle preserved names across validation and Lua emission --- .../lua/translation/LuaTranslator.java | 4 +- .../validation/NamePreservation.java | 77 ++++++++++++++++--- .../validation/WurstValidator.java | 13 +++- .../wurstscript/tests/OptimizerTests.java | 37 +++++++++ 4 files changed, 114 insertions(+), 17 deletions(-) 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 533c7a105..aca6dda14 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 @@ -1047,7 +1047,9 @@ private void translateClass(ImClass c) { // translate functions for (ImFunction f : c.getFunctions()) { translateFunc(f); - luaFunc.getFor(f).setName(uniqueName(c.getName() + "_" + f.getName())); + if (!NamePreservation.isPreserved(f)) { + luaFunc.getFor(f).setName(uniqueName(c.getName() + "_" + f.getName())); + } } createClassInitFunction(c, classVar, initMethod); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java index e6666a310..c21eedf7f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java @@ -4,6 +4,15 @@ import de.peeeq.wurstscript.jassIm.ImFunction; import de.peeeq.wurstscript.jassIm.ImVar; import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum; +import de.peeeq.wurstscript.types.WurstType; +import de.peeeq.wurstscript.types.WurstTypeArray; +import de.peeeq.wurstscript.types.WurstTypeTuple; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** Metadata for names which are part of the Warcraft III-facing API. */ public final class NamePreservation { @@ -33,29 +42,73 @@ public static void preserve(ImFunction function) { * name-based side table. The marker remains attached to the AST definition and is copied to * the corresponding IM variable through its trace. */ - public static void preserve(GlobalVarDef variable) { - if (!variable.hasAnnotation(ANNOTATION)) { - Annotation marker = Ast.Annotation(variable.getSource(), - Ast.Identifier(variable.getSource(), ANNOTATION.substring(1)), Ast.Arguments()); - variable.getModifiers().add(marker); + public static @Nullable Annotation preserve(GlobalVarDef variable) { + if (variable.hasAnnotation(ANNOTATION)) { + return null; } + Annotation marker = Ast.Annotation(variable.getSource(), + Ast.Identifier(variable.getSource(), ANNOTATION.substring(1)), Ast.Arguments()); + variable.getModifiers().add(marker); + return marker; } /** - * Finds globals by their emitted runtime name, without consulting lexical name resolution. - * This is needed for native APIs such as TriggerRegisterVariableEvent whose string argument - * refers to the generated global name rather than a source-level variable access. + * Resolves globals by their emitted runtime name, without consulting lexical name resolution. + * The index is scoped to one validation run; the preservation marker itself remains attached to + * the AST definition and is copied to the corresponding IM variables through their trace. */ - public static void preserveGlobalWithRuntimeName(WurstModel model, String runtimeName) { + public static RuntimeNameIndex indexGlobals(WurstModel model) { + RuntimeNameIndex result = new RuntimeNameIndex(); model.accept(new Element.DefaultVisitor() { @Override public void visit(GlobalVarDef variable) { super.visit(variable); - if (runtimeName(variable).equals(runtimeName)) { - preserve(variable); - } + String name = runtimeName(variable); + result.add(name, variable); + addTupleComponentNames(result, name, variable.attrTyp(), variable); } }); + return result; + } + + private static void addTupleComponentNames(RuntimeNameIndex index, String name, WurstType type, + GlobalVarDef variable) { + if (type instanceof WurstTypeArray array) { + type = array.getBaseType(); + } + if (!(type instanceof WurstTypeTuple tuple)) { + return; + } + for (WParameter parameter : tuple.getTupleDef().getParameters()) { + String componentName = name + "_" + parameter.getName(); + index.add(componentName, variable); + addTupleComponentNames(index, componentName, parameter.attrTyp(), variable); + } + } + + public static final class RuntimeNameIndex { + private final Map> globalsByName = new LinkedHashMap<>(); + private final Map syntheticMarkers = new LinkedHashMap<>(); + + private void add(String name, GlobalVarDef variable) { + globalsByName.computeIfAbsent(name, ignored -> new ArrayList<>()).add(variable); + } + + public void preserve(String runtimeName) { + for (GlobalVarDef variable : globalsByName.getOrDefault(runtimeName, List.of())) { + Annotation marker = NamePreservation.preserve(variable); + if (marker != null) { + syntheticMarkers.put(variable, marker); + } + } + } + + public void clearSyntheticMarkers() { + for (Map.Entry entry : syntheticMarkers.entrySet()) { + entry.getKey().getModifiers().remove(entry.getValue()); + } + syntheticMarkers.clear(); + } } private static String runtimeName(GlobalVarDef variable) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index c87856995..12a3eed0a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -62,6 +62,7 @@ private enum Phase { LIGHT, HEAVY } private final Map> classVarInitOrderCache = new HashMap<>(); private final Map guaranteedClassFieldInitCache = new IdentityHashMap<>(); private final Map> moduleFieldCopiesCache = new IdentityHashMap<>(); + private NamePreservation.RuntimeNameIndex runtimeNameIndex; private boolean moduleFieldCopiesIndexed; /** @@ -89,6 +90,10 @@ public void validate(Collection toCheck) { guaranteedClassFieldInitCache.clear(); moduleFieldCopiesCache.clear(); moduleFieldCopiesIndexed = false; + if (runtimeNameIndex != null) { + runtimeNameIndex.clearSyntheticMarkers(); + } + runtimeNameIndex = NamePreservation.indexGlobals(prog); lightValidation(toCheck); @@ -177,7 +182,7 @@ private void postChecks(Collection toCheck) { for (FunctionCall call : wrapperCalls.get(wrapper)) { if (call.getArgs().size() > 1 && call.getArgs().get(1) instanceof ExprStringVal) { ExprStringVal varName = (ExprStringVal) call.getArgs().get(1); - preserveVariableName(call, varName.getValS()); + preserveVariableName(varName.getValS()); WLogger.info("keep: " + varName.getValS()); } else { call.addError("Map contains TriggerRegisterVariableEvent with non-constant arguments. Can't be optimized."); @@ -3670,7 +3675,7 @@ private void checkBannedFunctions(ExprFunctionCall e) { if (e.getArgs().size() > 1) { if (e.getArgs().get(1) instanceof ExprStringVal) { ExprStringVal varName = (ExprStringVal) e.getArgs().get(1); - preserveVariableName(e, varName.getValS()); + preserveVariableName(varName.getValS()); WLogger.info("keep: " + varName.getValS()); return; } else if (e.getArgs().get(1) instanceof ExprVarAccess) { @@ -3728,8 +3733,8 @@ private void checkBannedFunctions(ExprFunctionCall e) { } } - private void preserveVariableName(Element useSite, String variableName) { - NamePreservation.preserveGlobalWithRuntimeName(prog, variableName); + private void preserveVariableName(String variableName) { + runtimeNameIndex.preserve(variableName); } private boolean isViableSwitchtype(Expr expr) { 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 5f9018a0e..30477c9e0 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 @@ -550,6 +550,43 @@ public void trvePreservesGlobalDespiteLexicalShadow() throws IOException { "Expected TRVE to preserve the global despite a local shadow.\n" + output); } + @Test + public void trvePreservesLoweredTupleComponent() throws IOException { + test().optimize().lines( + "type trigger extends handle", + "type event extends handle", + "type limitop extends handle", + "package test", + " tuple pair(real x, real y)", + " pair value = pair(0., 0.)", + " @extern native TriggerRegisterVariableEvent(trigger whichTrigger, string varName, limitop opcode, real limitval) returns event", + " init", + " TriggerRegisterVariableEvent(null, \"test_value_x\", null, 0.0)", + "endpackage"); + + String output = Files.toString( + new File("./test-output/OptimizerTests_trvePreservesLoweredTupleComponent_opt.j"), + Charsets.UTF_8); + assertTrue(output.contains("real test_value_x"), + "Expected TRVE to preserve the lowered tuple component.\n" + output); + } + + @Test + public void preserveNameAnnotationKeepsClassFunctionNameInLua() throws IOException { + test().testLua(true).luaOnly(true).executeProg(false).lines( + "package test", + " class ExternalApi", + " @preserveName function callback()", + " skip", + "endpackage"); + + String output = Files.toString( + new File("./test-output/lua/OptimizerTests_preserveNameAnnotationKeepsClassFunctionNameInLua.lua"), + Charsets.UTF_8); + assertTrue(output.contains("function ExternalApi_callback"), + "Expected a preserved class function to keep its emitted name.\n" + output); + } + @Test public void test_tempVarRemover() throws IOException { test().lines( From d32a923dc5cec7439795249a270491ac2bac661e Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 16:41:18 +0200 Subject: [PATCH 5/8] Reserve preserved Lua class names safely --- .../lua/translation/LuaTranslator.java | 11 +++++- .../validation/NamePreservation.java | 38 ++++++++++--------- .../validation/WurstValidator.java | 1 + .../wurstscript/tests/OptimizerTests.java | 21 ++++++++++ 4 files changed, 53 insertions(+), 18 deletions(-) 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 aca6dda14..4535786d7 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 @@ -331,8 +331,8 @@ protected String uniqueName(String rawName) { } public LuaCompilationUnit translate() { - assertNoDanglingFunctionReferences(prog); collectPredefinedNames(); + assertNoDanglingFunctionReferences(prog); normalizeFieldNames(); @@ -503,6 +503,15 @@ private void collectPredefinedNames() { } } + for (ImClass clazz : prog.getClasses()) { + for (ImFunction function : clazz.getFunctions()) { + if (NamePreservation.isPreserved(function)) { + LuaFunction luaFunction = luaFunc.getFor(function); + usedNames.add(luaFunction.getName()); + } + } + } + for (ImVar global : prog.getGlobals()) { if (global.getIsBJ()) { setNameFromTrace(global); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java index c21eedf7f..368f332f3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java @@ -7,7 +7,6 @@ import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeArray; import de.peeeq.wurstscript.types.WurstTypeTuple; -import org.eclipse.jdt.annotation.Nullable; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -18,6 +17,7 @@ public final class NamePreservation { public static final String ANNOTATION = "@preserveName"; + private static final String SYNTHETIC_MARKER = "__wurst_trve_preserve_name"; private NamePreservation() { } @@ -42,14 +42,14 @@ public static void preserve(ImFunction function) { * name-based side table. The marker remains attached to the AST definition and is copied to * the corresponding IM variable through its trace. */ - public static @Nullable Annotation preserve(GlobalVarDef variable) { + public static void preserve(GlobalVarDef variable) { if (variable.hasAnnotation(ANNOTATION)) { - return null; + return; } Annotation marker = Ast.Annotation(variable.getSource(), - Ast.Identifier(variable.getSource(), ANNOTATION.substring(1)), Ast.Arguments()); + Ast.Identifier(variable.getSource(), ANNOTATION.substring(1)), + Ast.Arguments(Ast.ExprStringVal(variable.getSource(), SYNTHETIC_MARKER))); variable.getModifiers().add(marker); - return marker; } /** @@ -71,6 +71,21 @@ public void visit(GlobalVarDef variable) { return result; } + /** Removes markers synthesized for TRVE during an earlier validation run. */ + public static void clearSyntheticMarkers(WurstModel model) { + model.accept(new Element.DefaultVisitor() { + @Override + public void visit(GlobalVarDef variable) { + super.visit(variable); + variable.getModifiers().removeIf(modifier -> modifier instanceof Annotation annotation + && annotation.getAnnotationType().equalsIgnoreCase(ANNOTATION) + && annotation.getArgs().size() == 1 + && annotation.getArgs().get(0) instanceof ExprStringVal value + && value.getValS().equals(SYNTHETIC_MARKER)); + } + }); + } + private static void addTupleComponentNames(RuntimeNameIndex index, String name, WurstType type, GlobalVarDef variable) { if (type instanceof WurstTypeArray array) { @@ -88,7 +103,6 @@ private static void addTupleComponentNames(RuntimeNameIndex index, String name, public static final class RuntimeNameIndex { private final Map> globalsByName = new LinkedHashMap<>(); - private final Map syntheticMarkers = new LinkedHashMap<>(); private void add(String name, GlobalVarDef variable) { globalsByName.computeIfAbsent(name, ignored -> new ArrayList<>()).add(variable); @@ -96,18 +110,8 @@ private void add(String name, GlobalVarDef variable) { public void preserve(String runtimeName) { for (GlobalVarDef variable : globalsByName.getOrDefault(runtimeName, List.of())) { - Annotation marker = NamePreservation.preserve(variable); - if (marker != null) { - syntheticMarkers.put(variable, marker); - } - } - } - - public void clearSyntheticMarkers() { - for (Map.Entry entry : syntheticMarkers.entrySet()) { - entry.getKey().getModifiers().remove(entry.getValue()); + NamePreservation.preserve(variable); } - syntheticMarkers.clear(); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 12a3eed0a..eb2ef8a91 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -93,6 +93,7 @@ public void validate(Collection toCheck) { if (runtimeNameIndex != null) { runtimeNameIndex.clearSyntheticMarkers(); } + NamePreservation.clearSyntheticMarkers(prog); runtimeNameIndex = NamePreservation.indexGlobals(prog); lightValidation(toCheck); 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 30477c9e0..c6c7e2c2f 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 @@ -587,6 +587,27 @@ public void preserveNameAnnotationKeepsClassFunctionNameInLua() throws IOExcepti "Expected a preserved class function to keep its emitted name.\n" + output); } + @Test + public void preservedClassFunctionReservesItsLuaNameBeforeClassVariables() throws IOException { + test().testLua(true).luaOnly(true).executeProg(false).lines( + "package test", + " class Foo", + " @preserveName function bar()", + " skip", + " class Foo_bar", + " init", + " new Foo_bar", + "endpackage"); + + String output = Files.toString( + new File("./test-output/lua/OptimizerTests_preservedClassFunctionReservesItsLuaNameBeforeClassVariables.lua"), + Charsets.UTF_8); + assertTrue(output.contains("function Foo_bar("), + "Expected the preserved class function to keep its Lua name.\n" + output); + assertTrue(output.contains("Foo_bar1 = ({})"), + "Expected the colliding class variable to be uniqued around the preserved function.\n" + output); + } + @Test public void test_tempVarRemover() throws IOException { test().lines( From d0b45cfad385b4f80bc00d56db7eb0f38e2bf6b0 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 17:01:37 +0200 Subject: [PATCH 6/8] Retain preserved Lua class methods safely --- .../lua/translation/RemoveGarbage.java | 5 ++ .../validation/WurstValidator.java | 81 ++++++++++++++++--- .../wurstscript/tests/OptimizerTests.java | 16 ++++ 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java index c26b41b5e..89fc071e2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java @@ -222,6 +222,11 @@ private static void visitFunction(ImFunction f, Used used) { return; } used.addFunction(f); + if (f.getParent() != null && f.getParent().getParent() instanceof ImClass owner) { + // A preserved static method has no receiver type to retain its owner. Keep the class + // because Lua emits class methods together with their class table. + visitClass(owner, used, false); + } visitType(f.getReturnType(), used); f.accept(new Element.DefaultVisitor() { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index eb2ef8a91..bc2b2a345 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -93,8 +93,11 @@ public void validate(Collection toCheck) { if (runtimeNameIndex != null) { runtimeNameIndex.clearSyntheticMarkers(); } + trveWrapperFuncs.clear(); + wrapperCalls.clear(); NamePreservation.clearSyntheticMarkers(prog); runtimeNameIndex = NamePreservation.indexGlobals(prog); + recomputeTrvePreservation(); lightValidation(toCheck); @@ -3681,19 +3684,11 @@ private void checkBannedFunctions(ExprFunctionCall e) { return; } else if (e.getArgs().get(1) instanceof ExprVarAccess) { // Check if this is a two line hook... thanks Bribe - ExprVarAccess varAccess = (ExprVarAccess) e.getArgs().get(1); - @Nullable FunctionImplementation nearestFunc = e.attrNearestFuncDef(); - WStatements fbody = nearestFunc.getBody(); - if (e.getParent() instanceof StmtReturn && fbody.size() <= 4 && fbody.get(fbody.size() - 2).structuralEquals(e.getParent())) { - WParameters params = nearestFunc.getParameters(); - if (params.size() == 4 && ((TypeExprSimple) params.get(0).getTyp()).getTypeName().equals("trigger") - && ((TypeExprSimple) params.get(1).getTyp()).getTypeName().equals("string") - && ((TypeExprSimple) params.get(2).getTyp()).getTypeName().equals("limitop") - && ((TypeExprSimple) params.get(3).getTyp()).getTypeName().equals("real")) { - trveWrapperFuncs.add(nearestFunc.getName()); - WLogger.info("found wrapper: " + nearestFunc.getName()); - return; - } + String wrapper = trveWrapperName(e); + if (wrapper != null) { + trveWrapperFuncs.add(wrapper); + WLogger.info("found wrapper: " + wrapper); + return; } } } else { @@ -3734,6 +3729,66 @@ private void checkBannedFunctions(ExprFunctionCall e) { } } + private void recomputeTrvePreservation() { + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ExprFunctionCall call) { + super.visit(call); + if (call.getFuncName().equals("TriggerRegisterVariableEvent") && call.getArgs().size() > 1) { + if (call.getArgs().get(1) instanceof ExprStringVal varName) { + preserveVariableName(varName.getValS()); + } else if (call.getArgs().get(1) instanceof ExprVarAccess) { + String wrapper = trveWrapperName(call); + if (wrapper != null) { + trveWrapperFuncs.add(wrapper); + } + } + } + } + + }); + + // Repeat the cheap call pass so calls which precede their wrapper declaration are covered. + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ExprFunctionCall call) { + super.visit(call); + if (trveWrapperFuncs.contains(call.getFuncName()) + && call.getArgs().size() > 1 + && call.getArgs().get(1) instanceof ExprStringVal varName) { + preserveVariableName(varName.getValS()); + } + } + }); + } + + private @Nullable String trveWrapperName(ExprFunctionCall e) { + @Nullable FunctionImplementation nearestFunc = e.attrNearestFuncDef(); + if (nearestFunc == null) { + return null; + } + WStatements fbody = nearestFunc.getBody(); + if (!(e.getParent() instanceof StmtReturn) + || fbody.size() < 2 + || fbody.size() > 4 + || !fbody.get(fbody.size() - 2).structuralEquals(e.getParent())) { + return null; + } + WParameters params = nearestFunc.getParameters(); + if (params.size() != 4 + || !(params.get(0).getTyp() instanceof TypeExprSimple triggerType) + || !(params.get(1).getTyp() instanceof TypeExprSimple stringType) + || !(params.get(2).getTyp() instanceof TypeExprSimple limitopType) + || !(params.get(3).getTyp() instanceof TypeExprSimple realType) + || !triggerType.getTypeName().equals("trigger") + || !stringType.getTypeName().equals("string") + || !limitopType.getTypeName().equals("limitop") + || !realType.getTypeName().equals("real")) { + return null; + } + return nearestFunc.getName(); + } + private void preserveVariableName(String variableName) { runtimeNameIndex.preserve(variableName); } 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 c6c7e2c2f..554ddd194 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 @@ -587,6 +587,22 @@ public void preserveNameAnnotationKeepsClassFunctionNameInLua() throws IOExcepti "Expected a preserved class function to keep its emitted name.\n" + output); } + @Test + public void preserveNameAnnotationKeepsStaticClassFunctionReachableInLua() throws IOException { + test().testLua(true).luaOnly(true).executeProg(false).lines( + "package test", + " class ExternalApi", + " @preserveName static function callback()", + " skip", + "endpackage"); + + String output = Files.toString( + new File("./test-output/lua/OptimizerTests_preserveNameAnnotationKeepsStaticClassFunctionReachableInLua.lua"), + Charsets.UTF_8); + assertTrue(output.contains("function callback("), + "Expected a preserved static class function to keep its emitted name.\n" + output); + } + @Test public void preservedClassFunctionReservesItsLuaNameBeforeClassVariables() throws IOException { test().testLua(true).luaOnly(true).executeProg(false).lines( From fd7d4b40d8c783ba26206cab96459a4ee80b580e Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 17:10:45 +0200 Subject: [PATCH 7/8] Scope name preservation to top-level functions --- .../lua/translation/LuaTranslator.java | 13 +---- .../lua/translation/RemoveGarbage.java | 6 --- .../validation/WurstValidator.java | 3 -- .../wurstscript/tests/OptimizerTests.java | 53 ------------------- 4 files changed, 1 insertion(+), 74 deletions(-) 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 4535786d7..ebe88d940 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 @@ -503,15 +503,6 @@ private void collectPredefinedNames() { } } - for (ImClass clazz : prog.getClasses()) { - for (ImFunction function : clazz.getFunctions()) { - if (NamePreservation.isPreserved(function)) { - LuaFunction luaFunction = luaFunc.getFor(function); - usedNames.add(luaFunction.getName()); - } - } - } - for (ImVar global : prog.getGlobals()) { if (global.getIsBJ()) { setNameFromTrace(global); @@ -1056,9 +1047,7 @@ private void translateClass(ImClass c) { // translate functions for (ImFunction f : c.getFunctions()) { translateFunc(f); - if (!NamePreservation.isPreserved(f)) { - luaFunc.getFor(f).setName(uniqueName(c.getName() + "_" + f.getName())); - } + luaFunc.getFor(f).setName(uniqueName(c.getName() + "_" + f.getName())); } createClassInitFunction(c, classVar, initMethod); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java index 89fc071e2..0781c22ec 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/RemoveGarbage.java @@ -222,12 +222,6 @@ private static void visitFunction(ImFunction f, Used used) { return; } used.addFunction(f); - if (f.getParent() != null && f.getParent().getParent() instanceof ImClass owner) { - // A preserved static method has no receiver type to retain its owner. Keep the class - // because Lua emits class methods together with their class table. - visitClass(owner, used, false); - } - visitType(f.getReturnType(), used); f.accept(new Element.DefaultVisitor() { @Override diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index bc2b2a345..b1109340e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -90,9 +90,6 @@ public void validate(Collection toCheck) { guaranteedClassFieldInitCache.clear(); moduleFieldCopiesCache.clear(); moduleFieldCopiesIndexed = false; - if (runtimeNameIndex != null) { - runtimeNameIndex.clearSyntheticMarkers(); - } trveWrapperFuncs.clear(); wrapperCalls.clear(); NamePreservation.clearSyntheticMarkers(prog); 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 554ddd194..f37b83c95 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 @@ -571,59 +571,6 @@ public void trvePreservesLoweredTupleComponent() throws IOException { "Expected TRVE to preserve the lowered tuple component.\n" + output); } - @Test - public void preserveNameAnnotationKeepsClassFunctionNameInLua() throws IOException { - test().testLua(true).luaOnly(true).executeProg(false).lines( - "package test", - " class ExternalApi", - " @preserveName function callback()", - " skip", - "endpackage"); - - String output = Files.toString( - new File("./test-output/lua/OptimizerTests_preserveNameAnnotationKeepsClassFunctionNameInLua.lua"), - Charsets.UTF_8); - assertTrue(output.contains("function ExternalApi_callback"), - "Expected a preserved class function to keep its emitted name.\n" + output); - } - - @Test - public void preserveNameAnnotationKeepsStaticClassFunctionReachableInLua() throws IOException { - test().testLua(true).luaOnly(true).executeProg(false).lines( - "package test", - " class ExternalApi", - " @preserveName static function callback()", - " skip", - "endpackage"); - - String output = Files.toString( - new File("./test-output/lua/OptimizerTests_preserveNameAnnotationKeepsStaticClassFunctionReachableInLua.lua"), - Charsets.UTF_8); - assertTrue(output.contains("function callback("), - "Expected a preserved static class function to keep its emitted name.\n" + output); - } - - @Test - public void preservedClassFunctionReservesItsLuaNameBeforeClassVariables() throws IOException { - test().testLua(true).luaOnly(true).executeProg(false).lines( - "package test", - " class Foo", - " @preserveName function bar()", - " skip", - " class Foo_bar", - " init", - " new Foo_bar", - "endpackage"); - - String output = Files.toString( - new File("./test-output/lua/OptimizerTests_preservedClassFunctionReservesItsLuaNameBeforeClassVariables.lua"), - Charsets.UTF_8); - assertTrue(output.contains("function Foo_bar("), - "Expected the preserved class function to keep its Lua name.\n" + output); - assertTrue(output.contains("Foo_bar1 = ({})"), - "Expected the colliding class variable to be uniqued around the preserved function.\n" + output); - } - @Test public void test_tempVarRemover() throws IOException { test().lines( From 67aaea39a24137c00da58042bc5decdd0664ba3b Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 17:45:14 +0200 Subject: [PATCH 8/8] Avoid recursive tuple metadata overflow --- .../wurstscript/validation/NamePreservation.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java index 368f332f3..58f1e3649 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java @@ -9,9 +9,12 @@ import de.peeeq.wurstscript.types.WurstTypeTuple; import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** Metadata for names which are part of the Warcraft III-facing API. */ public final class NamePreservation { @@ -65,7 +68,8 @@ public void visit(GlobalVarDef variable) { super.visit(variable); String name = runtimeName(variable); result.add(name, variable); - addTupleComponentNames(result, name, variable.attrTyp(), variable); + addTupleComponentNames(result, name, variable.attrTyp(), variable, + Collections.newSetFromMap(new IdentityHashMap<>())); } }); return result; @@ -87,17 +91,20 @@ public void visit(GlobalVarDef variable) { } private static void addTupleComponentNames(RuntimeNameIndex index, String name, WurstType type, - GlobalVarDef variable) { + GlobalVarDef variable, Set expandedTuples) { if (type instanceof WurstTypeArray array) { type = array.getBaseType(); } if (!(type instanceof WurstTypeTuple tuple)) { return; } + if (!expandedTuples.add(tuple.getTupleDef())) { + return; + } for (WParameter parameter : tuple.getTupleDef().getParameters()) { String componentName = name + "_" + parameter.getName(); index.add(componentName, variable); - addTupleComponentNames(index, componentName, parameter.attrTyp(), variable); + addTupleComponentNames(index, componentName, parameter.attrTyp(), variable, expandedTuples); } }