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..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 @@ -5,18 +5,32 @@ 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; + +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() { @@ -27,13 +41,12 @@ 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; } - String replacement = ng.getUniqueToken(); + String replacement = nextCompressedName(); global.setName(replacement); } @@ -41,7 +54,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; } @@ -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/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/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/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..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 @@ -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,13 +1465,18 @@ 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) - for (ImVar global : imProg.getGlobals()) { - if (TRVEHelper.protectedVariables.contains(global.getName())) { - readVariables.add(global); + // 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); + } } private void calculateCallRelations(ImFunction rootFunction, boolean includeUsedVariables) { 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..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 @@ -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); } @@ -327,8 +331,8 @@ protected String uniqueName(String rawName) { } public LuaCompilationUnit translate() { - assertNoDanglingFunctionReferences(prog); collectPredefinedNames(); + assertNoDanglingFunctionReferences(prog); normalizeFieldNames(); @@ -488,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_")) { @@ -502,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/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..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 @@ -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()) { @@ -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); } } @@ -221,7 +222,6 @@ private static void visitFunction(ImFunction f, Used used) { return; } used.addFunction(f); - visitType(f.getReturnType(), used); f.accept(new Element.DefaultVisitor() { @Override 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..58f1e3649 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/NamePreservation.java @@ -0,0 +1,142 @@ +package de.peeeq.wurstscript.validation; + +import de.peeeq.wurstscript.ast.*; +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 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 { + + public static final String ANNOTATION = "@preserveName"; + private static final String SYNTHETIC_MARKER = "__wurst_trve_preserve_name"; + + 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)) { + return; + } + Annotation marker = Ast.Annotation(variable.getSource(), + Ast.Identifier(variable.getSource(), ANNOTATION.substring(1)), + Ast.Arguments(Ast.ExprStringVal(variable.getSource(), SYNTHETIC_MARKER))); + variable.getModifiers().add(marker); + } + + /** + * 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 RuntimeNameIndex indexGlobals(WurstModel model) { + RuntimeNameIndex result = new RuntimeNameIndex(); + model.accept(new Element.DefaultVisitor() { + @Override + public void visit(GlobalVarDef variable) { + super.visit(variable); + String name = runtimeName(variable); + result.add(name, variable); + addTupleComponentNames(result, name, variable.attrTyp(), variable, + Collections.newSetFromMap(new IdentityHashMap<>())); + } + }); + 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, 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, expandedTuples); + } + } + + public static final class RuntimeNameIndex { + private final Map> globalsByName = 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())) { + NamePreservation.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/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..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 @@ -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,11 @@ public void validate(Collection toCheck) { guaranteedClassFieldInitCache.clear(); moduleFieldCopiesCache.clear(); moduleFieldCopiesIndexed = false; + trveWrapperFuncs.clear(); + wrapperCalls.clear(); + NamePreservation.clearSyntheticMarkers(prog); + runtimeNameIndex = NamePreservation.indexGlobals(prog); + recomputeTrvePreservation(); lightValidation(toCheck); @@ -177,7 +183,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(varName.getValS()); WLogger.info("keep: " + varName.getValS()); } else { call.addError("Map contains TriggerRegisterVariableEvent with non-constant arguments. Can't be optimized."); @@ -3670,24 +3676,16 @@ 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(varName.getValS()); WLogger.info("keep: " + varName.getValS()); 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 { @@ -3728,6 +3726,70 @@ 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); + } + 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..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 @@ -444,6 +444,133 @@ 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 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 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( + "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 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 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); }