diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index cdb3ebe3a..961d8ec4c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -561,6 +561,11 @@ public JassProg transformProgToJass() { // translate flattened intermediate lang to jass: beginPhase(14, "translate to jass"); + optimizer.removeGarbage(); + imProg.flatten(imTranslator); + imTranslator.removeEmptyPackageInits(); + optimizer.removeGarbage(); + imProg.flatten(imTranslator); getImTranslator().calculateCallRelationsAndUsedVariables(); ImToJassTranslator translator = new ImToJassTranslator(getImProg(), getImTranslator().getCalledFunctions(), getImTranslator().getMainFunc(), getImTranslator().getConfFunc()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java index 0b2db5ca2..309b0547e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/SideEffectAnalyzer.java @@ -8,8 +8,11 @@ import de.peeeq.wurstscript.translation.imtranslation.ImHelper; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -455,4 +458,77 @@ public boolean hasSideEffects(Element elem) { Set imVars = directlySetVariables(elem); return natives.size() + directFuncs.size() + imVars.size() > 0; } + + /** + * Checks if the given element has observable side effects. + * Pure natives can be configured via the predicate. + */ + public boolean hasObservableSideEffects(Element elem, Predicate isNativeWithoutSideEffects) { + return new ObservableSideEffectChecker(isNativeWithoutSideEffects).hasSideEffects(elem); + } + + private final class ObservableSideEffectChecker { + private final Predicate isNativeWithoutSideEffects; + private final Map cache = new HashMap<>(); + private final Set inProgress = new LinkedHashSet<>(); + + private ObservableSideEffectChecker(Predicate isNativeWithoutSideEffects) { + this.isNativeWithoutSideEffects = isNativeWithoutSideEffects; + } + + private boolean hasSideEffects(Element elem) { + if (!directlySetVariables(elem).isEmpty()) { + return true; + } + for (ImFunction nativeFunc : calledNatives(elem)) { + if (!isNativeWithoutSideEffects.test(nativeFunc)) { + return true; + } + } + for (ImFunction called : calledFunctions(elem)) { + if (functionHasSideEffects(called)) { + return true; + } + } + return false; + } + + private boolean functionHasSideEffects(ImFunction func) { + Boolean cached = cache.get(func); + if (cached != null) { + return cached; + } + if (func.isNative()) { + boolean sideEffect = !isNativeWithoutSideEffects.test(func); + cache.put(func, sideEffect); + return sideEffect; + } + if (!inProgress.add(func)) { + return true; + } + boolean sideEffect = hasGlobalSideEffects(func.getBody()); + inProgress.remove(func); + cache.put(func, sideEffect); + return sideEffect; + } + + private boolean hasGlobalSideEffects(Element elem) { + for (ImVar var : directlySetVariables(elem)) { + if (var.isGlobal()) { + return true; + } + } + for (ImFunction nativeFunc : calledNatives(elem)) { + if (!isNativeWithoutSideEffects.test(nativeFunc)) { + return true; + } + } + for (ImFunction called : calledFunctions(elem)) { + if (functionHasSideEffects(called)) { + return true; + } + } + return false; + } + } } 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 3470c4842..10a06118c 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 @@ -6,6 +6,7 @@ import de.peeeq.wurstscript.intermediatelang.optimizer.BranchMerger; import de.peeeq.wurstscript.intermediatelang.optimizer.ConstantAndCopyPropagation; import de.peeeq.wurstscript.intermediatelang.optimizer.LocalMerger; +import de.peeeq.wurstscript.intermediatelang.optimizer.SideEffectAnalyzer; import de.peeeq.wurstscript.intermediatelang.optimizer.SimpleRewrites; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.ImHelper; @@ -97,6 +98,7 @@ public void removeGarbage() { while (changes && iterations++ < 10) { ImProg prog = trans.imProg(); trans.calculateCallRelationsAndUsedVariables(); + SideEffectAnalyzer sideEffectAnalyzer = new SideEffectAnalyzer(prog); // keep only used variables int globalsBefore = prog.getGlobals().size(); @@ -137,25 +139,30 @@ public void visit(ImSet e) { if (e.getLeft() instanceof ImVarAccess) { ImVarAccess va = (ImVarAccess) e.getLeft(); if (!trans.getReadVariables().contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { - replacements.add(Pair.create(e, Collections.singletonList(e.getRight()))); + List sideEffects = collectSideEffects(e.getRight(), sideEffectAnalyzer); + replacements.add(Pair.create(e, sideEffects)); } } else if (e.getLeft() instanceof ImVarArrayAccess) { ImVarArrayAccess va = (ImVarArrayAccess) e.getLeft(); if (!trans.getReadVariables().contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { - // IMPORTANT: removeAll() clears parent references - List exprs = va.getIndexes().removeAll(); - exprs.add(e.getRight()); + List exprs = new ArrayList<>(); + for (ImExpr index : va.getIndexes()) { + exprs.addAll(collectSideEffects(index, sideEffectAnalyzer)); + } + exprs.addAll(collectSideEffects(e.getRight(), sideEffectAnalyzer)); replacements.add(Pair.create(e, exprs)); } } else if (e.getLeft() instanceof ImTupleSelection) { ImVar var = TypesHelper.getTupleVar((ImTupleSelection) e.getLeft()); if(var != null && !trans.getReadVariables().contains(var) && !TRVEHelper.protectedVariables.contains(var.getName())) { - replacements.add(Pair.create(e, Collections.singletonList(e.getRight()))); + List sideEffects = collectSideEffects(e.getRight(), sideEffectAnalyzer); + replacements.add(Pair.create(e, sideEffects)); } } else if(e.getLeft() instanceof ImMemberAccess) { ImMemberAccess va = ((ImMemberAccess) e.getLeft()); if (!trans.getReadVariables().contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { - replacements.add(Pair.create(e, Collections.singletonList(e.getRight()))); + List sideEffects = collectSideEffects(e.getRight(), sideEffectAnalyzer); + replacements.add(Pair.create(e, sideEffects)); } } } @@ -165,7 +172,9 @@ public void visit(ImSet e) { for (Pair> pair : replacements) { changes = true; ImExpr r; - if (pair.getB().size() == 1) { + if (pair.getB().isEmpty()) { + r = ImHelper.statementExprVoid(JassIm.ImStmts()); + } else if (pair.getB().size() == 1) { r = pair.getB().get(0); // CRITICAL: Clear parent before reusing the node r.setParent(null); @@ -187,4 +196,15 @@ public void visit(ImSet e) { } } } + + private List collectSideEffects(ImExpr expr, SideEffectAnalyzer analyzer) { + if (expr == null) { + return Collections.emptyList(); + } + if (analyzer.hasObservableSideEffects(expr, func -> func.isNative() + && UselessFunctionCallsRemover.isFunctionWithoutSideEffect(func.getName()))) { + return Collections.singletonList(expr); + } + return Collections.emptyList(); + } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java index 0bdf0dd1e..4e328b277 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java @@ -455,6 +455,29 @@ private static ImStatementExpr inSet(ImSet imSet, ImFunction f) { + "\nLHS=" + left + "\nRHS=" + right); } + boolean allLiteral = true; + for (ImExpr r : rhsLeaves) { + if (!isSimpleLiteral(r)) { + allLiteral = false; + break; + } + } + + if (allLiteral) { + for (int i = 0; i < lhsLeaves.size(); i++) { + ImLExpr l = lhsLeaves.get(i); + ImType targetT = l.attrTyp(); + ImExpr r = rhsLeaves.get(i); + if (r instanceof ImNull) { + r = ImHelper.defaultValueForComplexType(targetT); + } + l.setParent(null); + r.setParent(null); + stmts.add(JassIm.ImSet(imSet.getTrace(), l, r)); + } + return ImHelper.statementExprVoid(stmts); + } + // 4) Evaluate RHS leaves first into temps (preserve side-effect order & alias safety) List temps = new ArrayList<>(rhsLeaves.size()); for (int i = 0; i < rhsLeaves.size(); i++) { @@ -485,6 +508,14 @@ private static ImStatementExpr inSet(ImSet imSet, ImFunction f) { return ImHelper.statementExprVoid(stmts); } + private static boolean isSimpleLiteral(ImExpr expr) { + return expr instanceof ImBoolVal + || expr instanceof ImIntVal + || expr instanceof ImRealVal + || expr instanceof ImStringVal + || expr instanceof ImNull; + } + /** Flatten LHS recursively into addressable leaves (ImLExpr), hoisting side-effects */ private static void flattenLhsTuple(ImExpr e, List out, ImStmts sideStmts) { ImExpr x = extractSideEffect(e, sideStmts); 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 66b258c1f..36aa54a8e 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 @@ -183,6 +183,154 @@ public ImProg translateProg() { } } + public void removeEmptyPackageInits() { + Set emptyInitFunctions = new HashSet<>(); + for (ImFunction initFunc : imProg.getFunctions()) { + if (initFunc.getName().startsWith("init_") && isTrivialInitFunction(initFunc)) { + emptyInitFunctions.add(initFunc); + } + } + if (emptyInitFunctions.isEmpty()) { + return; + } + + Map initFuncRefs = collectInitFuncRefs(); + removeInitCallsFromMain(emptyInitFunctions, initFuncRefs); + removeInitFuncRefsFromGlobals(emptyInitFunctions); + imProg.getFunctions().removeIf(emptyInitFunctions::contains); + initFuncMap.values().removeIf(emptyInitFunctions::contains); + } + + private boolean isTrivialInitFunction(ImFunction initFunc) { + if (initFunc.getBody().isEmpty()) { + return true; + } + if (initFunc.getBody().size() != 1) { + return false; + } + ImStmt stmt = initFunc.getBody().get(0); + if (!(stmt instanceof ImReturn)) { + return false; + } + ImExprOpt returnValue = ((ImReturn) stmt).getReturnValue(); + if (returnValue instanceof ImNoExpr) { + return true; + } + return returnValue instanceof ImBoolVal && ((ImBoolVal) returnValue).getValB(); + } + + private void removeInitCallsFromMain(Set emptyInitFunctions, Map initFuncRefs) { + ImFunction main = getMainFunc(); + if (main == null) { + return; + } + + ImFunction native_TriggerAddCondition = getNativeFunc("TriggerAddCondition"); + ImFunction native_Condition = getNativeFunc("Condition"); + ImFunction native_ClearTrigger = getNativeFunc("TriggerClearConditions"); + + ImStmts mainBody = main.getBody(); + for (int i = 0; i < mainBody.size(); i++) { + ImStmt stmt = mainBody.get(i); + if (stmt instanceof ImFunctionCall) { + ImFunctionCall call = (ImFunctionCall) stmt; + if (emptyInitFunctions.contains(call.getFunc())) { + mainBody.remove(i--); + continue; + } + if (native_TriggerAddCondition != null && native_Condition != null + && call.getFunc() == native_TriggerAddCondition + && hasInitCondition(call, native_Condition, emptyInitFunctions, initFuncRefs)) { + if (i + 2 < mainBody.size() + && mainBody.get(i + 1) instanceof ImIf + && isTriggerClear(mainBody.get(i + 2), native_ClearTrigger)) { + mainBody.remove(i + 2); + mainBody.remove(i + 1); + mainBody.remove(i--); + } + } + } + } + } + + private void removeInitFuncRefsFromGlobals(Set emptyInitFunctions) { + ImFunction globalInit = getGlobalInitFunc(); + if (globalInit == null) { + return; + } + ImStmts body = globalInit.getBody(); + for (int i = 0; i < body.size(); i++) { + ImStmt stmt = body.get(i); + if (!(stmt instanceof ImSet)) { + continue; + } + ImExpr right = ((ImSet) stmt).getRight(); + if (right instanceof ImFuncRef && emptyInitFunctions.contains(((ImFuncRef) right).getFunc())) { + body.remove(i--); + } + } + } + + private Map collectInitFuncRefs() { + ImFunction globalInit = getGlobalInitFunc(); + if (globalInit == null) { + return Collections.emptyMap(); + } + Map refs = new HashMap<>(); + ImStmts body = globalInit.getBody(); + for (int i = 0; i < body.size(); i++) { + ImStmt stmt = body.get(i); + if (!(stmt instanceof ImSet)) { + continue; + } + ImSet set = (ImSet) stmt; + if (!(set.getLeft() instanceof ImVarAccess)) { + continue; + } + if (!(set.getRight() instanceof ImFuncRef)) { + continue; + } + refs.put(((ImVarAccess) set.getLeft()).getVar(), ((ImFuncRef) set.getRight()).getFunc()); + } + return refs; + } + + private boolean hasInitCondition(ImFunctionCall call, ImFunction nativeCondition, Set emptyInitFunctions, + Map initFuncRefs) { + if (call.getArguments().size() < 2) { + return false; + } + ImExpr conditionExpr = call.getArguments().get(1); + if (!(conditionExpr instanceof ImFunctionCall)) { + return false; + } + ImFunctionCall conditionCall = (ImFunctionCall) conditionExpr; + if (conditionCall.getFunc() != nativeCondition) { + return false; + } + if (conditionCall.getArguments().size() != 1) { + return false; + } + ImExpr argument = conditionCall.getArguments().get(0); + if (argument instanceof ImFuncRef) { + ImFuncRef funcRef = (ImFuncRef) argument; + return emptyInitFunctions.contains(funcRef.getFunc()); + } + if (argument instanceof ImVarAccess) { + ImVar var = ((ImVarAccess) argument).getVar(); + ImFunction target = initFuncRefs.get(var); + return target != null && emptyInitFunctions.contains(target); + } + return false; + } + + private boolean isTriggerClear(ImStmt stmt, ImFunction nativeClearTrigger) { + if (nativeClearTrigger == null) { + return false; + } + return stmt instanceof ImFunctionCall && ((ImFunctionCall) stmt).getFunc() == nativeClearTrigger; + } + /** * Number all the compiletime functions and expressions, * so that the one with the lowest number can be executed first. diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/PackageTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/PackageTests.java index 2cf426568..17fc65e1e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/PackageTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/PackageTests.java @@ -1,7 +1,13 @@ package tests.wurstscript.tests; +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.testng.Assert; import org.testng.annotations.Test; +import java.io.File; +import java.io.IOException; + public class PackageTests extends WurstScriptTest { @@ -21,6 +27,55 @@ public void test_static_init() { // test case for #68 "endpackage"); } + @Test + public void emptyPackageInitNotEmitted() throws IOException { + test().withStdLib().executeProg(false).lines( + "package LightningEmptyInit", + " public function ping() returns int", + " return 7", + "endpackage", + "package Test", + " import LightningEmptyInit", + " init", + " let value = ping()", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "PackageTests_emptyPackageInitNotEmitted_no_opts.j"); + String compiled = Files.toString(output, Charsets.UTF_8); + Assert.assertFalse(compiled.contains("init_Lightning")); + Assert.assertFalse(compiled.contains("init_LightningEmptyInit")); + } + + @Test + public void tupleTempsNotEmittedForConstInits() throws IOException { + test().withStdLib().executeProg(false).lines( + "package Hello", + "init", + " print(\"hello world\")", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "PackageTests_tupleTempsNotEmittedForConstInits_no_opts.j"); + String compiled = Files.toString(output, Charsets.UTF_8); + Assert.assertFalse(compiled.contains("tuple_temp")); + Assert.assertFalse(compiled.contains("init_Matrices")); + } + + @Test + public void unusedPureAssignmentsAreRemoved() throws IOException { + test().withStdLib().executeProg(false).lines( + "package Hello", + "init", + " print(\"hello world\")", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "PackageTests_unusedPureAssignmentsAreRemoved_no_opts.j"); + String compiled = Files.toString(output, Charsets.UTF_8); + Assert.assertFalse(compiled.contains("call colorA_toColorString")); + } + @Test public void duplicatePackageName() { testAssertErrorsLines(false, "already",