diff --git a/Wurstpack/wurstscript/grill b/Wurstpack/wurstscript/grill index eaa907174..ebcdebfd9 100644 --- a/Wurstpack/wurstscript/grill +++ b/Wurstpack/wurstscript/grill @@ -23,4 +23,4 @@ if [[ ! -x "$RUNTIME" ]]; then exit 3 fi -exec "$RUNTIME" -Dfile.encoding=UTF-8 -jar "$JAR" "$@" +exec "$RUNTIME" -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED -jar "$JAR" "$@" diff --git a/Wurstpack/wurstscript/grill.cmd b/Wurstpack/wurstscript/grill.cmd index d39767b11..3e6f46c05 100644 --- a/Wurstpack/wurstscript/grill.cmd +++ b/Wurstpack/wurstscript/grill.cmd @@ -31,7 +31,7 @@ if not exist "%JAVA%" ( goto :restore ) -"%JAVA%" -Dfile.encoding=UTF-8 -jar "%GRILL_JAR%" %* +"%JAVA%" -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED -jar "%GRILL_JAR%" %* :restore rem Restore previous code page if we captured it diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java index c52418ec8..96aa66dec 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java @@ -41,8 +41,8 @@ public ILconstTuple createObjectDefinition(ILconstString fileType, ILconstInt ne String objIdString = ObjectHelper.objectIdIntToString(newUnitId.getVal()); boolean isMeleeOverride = newUnitId.getVal() == deriveFrom.getVal(); - if (!isMeleeOverride && dataStore.getObjs().containsKey(ObjId.valueOf(objIdString))) { - globalState.compilationError("Object definition with id " + objIdString + " already exists."); + if (!isMeleeOverride && !globalState.registerCreatedObjectDefinition(fileType.getVal(), objIdString)) { + globalState.compilationError("Object definition with id " + objIdString + " is defined more than once."); } ObjMod.Obj objDef = newDefFromFiletype(dataStore, deriveFrom.getVal(), newUnitId.getVal(), isMeleeOverride); if (!isMeleeOverride) { @@ -59,10 +59,15 @@ private ObjMod.Obj newDefFromFiletype(ObjMod dataStore, in if (isMeleeOverride) { ObjId id = ObjId.valueOf(ObjectHelper.objectIdIntToString(newId)); // same id => modify melee/original definition table + ObjMod.Obj existing = dataStore.getObjs().get(id); + if (existing != null) { + return existing; + } return dataStore.addObj(id, null); } ObjId baseIdS = ObjId.valueOf(ObjectHelper.objectIdIntToString(base)); ObjId newIdS = ObjId.valueOf(ObjectHelper.objectIdIntToString(newId)); + dataStore.removeObj(newIdS); return dataStore.addObj(newIdS, baseIdS); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java index 4441149f9..0bae32ac2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java @@ -36,6 +36,7 @@ public class ProgramStateIO extends ProgramState { private @Nullable final MpqEditor mpqEditor; private final Map> dataStoreMap = Maps.newLinkedHashMap(); private final Map dataStoreHashes = Maps.newLinkedHashMap(); + private final Map> createdObjectDefinitionIds = Maps.newLinkedHashMap(); private int id = 0; private final Map objDefinitions = Maps.newLinkedHashMap(); private PrintStream outStream = System.err; @@ -368,6 +369,11 @@ ObjMod.Obj getObjectDefinition(String key) { return objDefinitions.get(key); } + boolean registerCreatedObjectDefinition(String fileExtension, String objId) { + Set ids = createdObjectDefinitionIds.computeIfAbsent(fileExtension, k -> new LinkedHashSet<>()); + return ids.add(objId); + } + /** * Calculate hash of an object file's contents */ diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java index 73b5de9be..2945229bc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java @@ -96,6 +96,7 @@ public Changes removeCompilationUnit(WFile resource) { toRemove.add(compilationUnit); } } + GlobalCaches.clearLookupCacheFor(toRemove); model2.removeAll(toRemove); } @@ -419,6 +420,7 @@ private void updateModel(CompilationUnit cu, WurstGui gui) { Set oldPackages = providedPackages(c); Set mustUpdate = calculateCUsToUpdate(Collections.singletonList(cu), oldPackages, model2); + GlobalCaches.clearLookupCacheFor(Collections.singletonList(c)); clearCompilationUnits(mustUpdate); // replace old compilationunit with new one: it.set(cu); 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 f456412c6..a0acd43f2 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 @@ -6,6 +6,7 @@ import de.peeeq.wurstscript.attributes.ErrorHandler; 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; @@ -34,6 +35,7 @@ public void checkProg(WurstModel root, Collection toCheck) { if (errorHandler.getErrorCount() > 0) return; attachErrorHandler(root); + clearGlobalCaches(root, toCheck); expandModules(root); @@ -50,6 +52,14 @@ public void checkProg(WurstModel root, Collection toCheck) { validator.validate(toCheck); } + private void clearGlobalCaches(WurstModel root, Collection toCheck) { + if (toCheck == root || toCheck.size() >= root.size()) { + GlobalCaches.clearAll(); + } else { + GlobalCaches.clearLookupCacheFor(toCheck); + } + } + private void attachErrorHandler(WurstModel root) { for (CompilationUnit cu : root) { cu.getCuInfo().setCuErrorHandler(errorHandler); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/Exports.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/Exports.java index 9318ec3d6..4f2a5c36d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/Exports.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/Exports.java @@ -53,7 +53,6 @@ public static ImmutableMultimap exportedTypeNameLinks(WPackage return result.build(); } - private static void addExportedTypeNameLinks(Builder result, WPackage p, Set alreadyImported) { if (alreadyImported.contains(p)) { return; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java index ac8782e8e..9fd9b602b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java @@ -268,7 +268,6 @@ public static ImmutableMultimap calculate(WPackage p) { return result.build(); } - public static ImmutableMultimap calculate(WEntities wEntities) { ImmutableMultimap.Builder result = ImmutableSetMultimap.builder(); for (WEntity e : wEntities) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java index bb6cce60e..ac8380a50 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/GlobalCaches.java @@ -6,8 +6,12 @@ import de.peeeq.wurstscript.intermediatelang.interpreter.LocalState; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.ArrayDeque; import java.util.Arrays; +import java.util.Collection; +import java.util.IdentityHashMap; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicLong; // Expose static fields only if you already have them there; otherwise, just clear via dedicated methods. @@ -146,6 +150,28 @@ public static void clearAll() { HasAnnotation.clearCaches(); } + /** + * Clears lookup entries whose query element belongs to one of the changed roots. + * This keeps editor validation from throwing away hot lookup data for unchanged files. + */ + public static void clearLookupCacheFor(Collection roots) { + if (roots.isEmpty()) { + return; + } + Set affected = java.util.Collections.newSetFromMap(new IdentityHashMap<>()); + ArrayDeque todo = new ArrayDeque<>(roots); + while (!todo.isEmpty()) { + Element e = todo.removeLast(); + if (!affected.add(e)) { + continue; + } + for (int i = 0; i < e.size(); i++) { + todo.add(e.get(i)); + } + } + lookupCache.keySet().removeIf(key -> affected.contains(key.element)); + } + public enum LookupType { FUNC, VAR, TYPE, PACKAGE, MEMBER_FUNC, MEMBER_VAR } 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 7bb45a4f1..d8df0a417 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 @@ -64,11 +64,10 @@ public WurstValidator(WurstModel root) { public void validate(Collection toCheck) { try { - functionCount = countFunctions(); + functionCount = countFunctions(toCheck); visitedFunctions = 0; heavyFunctions.clear(); heavyBlocks.clear(); - GlobalCaches.clearAll(); lightValidation(toCheck); @@ -123,11 +122,6 @@ private void lightValidation(Collection toCheck) { for (CompilationUnit cu : toCheck) { walkTree(cu); } - - // Build CFG once for heavy phase (enables reachability/prev/next attrs) - for (CompilationUnit cu : toCheck) { - computeFlowAttributes(cu); - } } /** Visit only statements under root and run checkReachability where applicable. */ @@ -1174,16 +1168,19 @@ private void checkIntVal(ExprIntVal e) { // check range? ... } - private int countFunctions() { + private int countFunctions(Collection toCheck) { final int[] functionCount = new int[1]; - prog.accept(new WurstModel.DefaultVisitor() { + Element.DefaultVisitor visitor = new Element.DefaultVisitor() { @Override public void visit(FuncDef f) { super.visit(f); functionCount[0]++; } - }); + }; + for (CompilationUnit cu : toCheck) { + cu.accept(visitor); + } return functionCount[0]; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java index 653b2fc45..c8db3168f 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java @@ -1,23 +1,34 @@ package tests.wurstscript.tests; import de.peeeq.wurstio.intermediateLang.interpreter.CompiletimeNatives; +import de.peeeq.wurstio.intermediateLang.interpreter.ProgramStateIO; import de.peeeq.wurstio.objectreader.ObjectHelper; +import de.peeeq.wurstscript.ast.Ast; +import de.peeeq.wurstscript.ast.Element; +import de.peeeq.wurstscript.gui.WurstGuiLogger; +import de.peeeq.wurstscript.intermediatelang.ILconstInt; import de.peeeq.wurstscript.intermediatelang.ILconstString; +import de.peeeq.wurstscript.jassIm.ImProg; +import de.peeeq.wurstscript.jassIm.JassIm; import net.moonlightflower.wc3libs.bin.ObjMod; import net.moonlightflower.wc3libs.bin.app.objMod.W3A; import net.moonlightflower.wc3libs.bin.app.objMod.W3U; import net.moonlightflower.wc3libs.dataTypes.DataType; +import net.moonlightflower.wc3libs.dataTypes.app.War3String; import net.moonlightflower.wc3libs.dataTypes.app.War3Real; import net.moonlightflower.wc3libs.misc.MetaFieldId; import net.moonlightflower.wc3libs.misc.ObjId; import org.testng.annotations.Test; import java.lang.reflect.Method; +import java.util.HashMap; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; public class CompiletimeNativesTest { @@ -104,4 +115,97 @@ public void differentIdsObjectDefinitionUsesCustomTable() throws Exception { assertEquals(w3u.getOrigObjs().size(), 0); assertEquals(w3u.getCustomObjs().size(), 1); } + + @Test + public void differentIdsObjectDefinitionOverwritesExistingMapObject() throws Exception { + CompiletimeNatives natives = new CompiletimeNatives(null, null, false); + W3U w3u = new W3U(); + int hfoo = ObjectHelper.objectIdStringToInt("hfoo"); + int hf01 = ObjectHelper.objectIdStringToInt("hf01"); + W3U.Obj existing = w3u.addObj(ObjId.valueOf("hf01"), ObjId.valueOf("hpea")); + existing.addMod(new ObjMod.Obj.Mod(MetaFieldId.valueOf("unam"), ObjMod.ValType.STRING, War3String.valueOf("old map object"))); + + Method newDefFromFiletype = CompiletimeNatives.class.getDeclaredMethod( + "newDefFromFiletype", + ObjMod.class, + int.class, + int.class, + boolean.class + ); + newDefFromFiletype.setAccessible(true); + + ObjMod.Obj obj = (ObjMod.Obj) newDefFromFiletype.invoke(natives, w3u, hfoo, hf01, false); + + assertEquals(w3u.getCustomObjs().size(), 1, "Existing map object should be replaced instead of duplicated"); + assertEquals(obj.getId().getVal(), "hf01"); + assertEquals(obj.getBaseId().getVal(), "hfoo"); + assertEquals(obj.getMods().size(), 0, "Overwritten map object mods should not leak into the Wurst-created object"); + } + + @Test + public void duplicateCodeObjectDefinitionsReportError() { + WurstGuiLogger gui = new WurstGuiLogger(); + ProgramStateIO state = new ProgramStateIO(Optional.empty(), null, gui, emptyProg(), true); + CompiletimeNatives natives = new CompiletimeNatives(state, null, false); + int hfoo = ObjectHelper.objectIdStringToInt("hfoo"); + int hf01 = ObjectHelper.objectIdStringToInt("hf01"); + + natives.createObjectDefinition(new ILconstString("w3u"), new ILconstInt(hf01), new ILconstInt(hfoo)); + natives.createObjectDefinition(new ILconstString("w3u"), new ILconstInt(hf01), new ILconstInt(hfoo)); + + assertEquals(gui.getErrorCount(), 1); + assertTrue(gui.getErrors().contains("Object definition with id hf01 is defined more than once.")); + } + + @Test + public void createObjectDefinitionDoesNotReportExistingMapObjectAsError() throws Exception { + WurstGuiLogger gui = new WurstGuiLogger(); + ProgramStateIO state = new ProgramStateIO(Optional.empty(), null, gui, emptyProg(), true); + CompiletimeNatives natives = new CompiletimeNatives(state, null, false); + int hfoo = ObjectHelper.objectIdStringToInt("hfoo"); + int hf01 = ObjectHelper.objectIdStringToInt("hf01"); + + Method getDataStore = ProgramStateIO.class.getDeclaredMethod("getDataStore", String.class); + getDataStore.setAccessible(true); + W3U w3u = (W3U) getDataStore.invoke(state, "w3u"); + W3U.Obj existing = w3u.addObj(ObjId.valueOf("hf01"), ObjId.valueOf("hpea")); + existing.addMod(new ObjMod.Obj.Mod(MetaFieldId.valueOf("unam"), ObjMod.ValType.STRING, War3String.valueOf("old map object"))); + + natives.createObjectDefinition(new ILconstString("w3u"), new ILconstInt(hf01), new ILconstInt(hfoo)); + + assertEquals(gui.getErrorCount(), 0); + assertEquals(w3u.getCustomObjs().size(), 1); + ObjMod.Obj obj = w3u.getCustomObjs().get(0); + assertEquals(obj.getId().getVal(), "hf01"); + assertEquals(obj.getBaseId().getVal(), "hfoo"); + assertFalse(obj.getMods().stream().anyMatch(m -> m.getId().getVal().equals("unam"))); + } + + @Test + public void sameIdObjectDefinitionsMergeModsWithoutDuplicateError() throws Exception { + WurstGuiLogger gui = new WurstGuiLogger(); + ProgramStateIO state = new ProgramStateIO(Optional.empty(), null, gui, emptyProg(), true); + CompiletimeNatives natives = new CompiletimeNatives(state, null, false); + int hfoo = ObjectHelper.objectIdStringToInt("hfoo"); + + var first = natives.createObjectDefinition(new ILconstString("w3u"), new ILconstInt(hfoo), new ILconstInt(hfoo)); + natives.ObjectDefinition_setString(first, new ILconstString("unam"), new ILconstString("first")); + var second = natives.createObjectDefinition(new ILconstString("w3u"), new ILconstInt(hfoo), new ILconstInt(hfoo)); + natives.ObjectDefinition_setString(second, new ILconstString("utip"), new ILconstString("second")); + + Method getDataStore = ProgramStateIO.class.getDeclaredMethod("getDataStore", String.class); + getDataStore.setAccessible(true); + W3U w3u = (W3U) getDataStore.invoke(state, "w3u"); + + assertEquals(gui.getErrorCount(), 0); + assertEquals(w3u.getOrigObjs().size(), 1); + ObjMod.Obj obj = w3u.getOrigObjs().get(0); + assertTrue(obj.getMods().stream().anyMatch(m -> m.getId().getVal().equals("unam"))); + assertTrue(obj.getMods().stream().anyMatch(m -> m.getId().getVal().equals("utip"))); + } + + private ImProg emptyProg() { + Element trace = Ast.NoExpr(); + return JassIm.ImProg(trace, JassIm.ImVars(), JassIm.ImFunctions(), JassIm.ImMethods(), JassIm.ImClasses(), JassIm.ImTypeClassFuncs(), new HashMap<>()); + } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java index b8ec87295..671df8987 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java @@ -365,6 +365,67 @@ public void readdingMovedFileWithSameContentUpdatesModel() throws IOException { assertNotNull(manager.getCompilationUnit(fileTest), "file should be present after re-adding same contents"); } + @Test + public void replacingCompilationUnitClearsOldLookupCacheKeys() throws IOException { + GlobalCaches.clearAll(); + File projectFolder = new File("./temp/testProject_replace_clears_lookup_cache/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + WFile fileMain = WFile.create(new File(wurstFolder, "Main.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + writeFile(fileMain, string( + "package Main", + "public function foo()" + )); + writeFile(fileWurst, "package Wurst\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + manager.buildProject(); + CompilationUnit oldCu = manager.getCompilationUnit(fileMain); + assertNotNull(oldCu); + + GlobalCaches.CacheKey oldKey = new GlobalCaches.CacheKey(oldCu, "foo", GlobalCaches.LookupType.FUNC); + GlobalCaches.lookupCache.put(oldKey, oldCu); + + manager.syncCompilationUnitContent(fileMain, string( + "package Main", + "public function bar()" + )); + + assertEquals(GlobalCaches.lookupCache.containsKey(oldKey), false, + "replacing a CU must drop lookup keys that keep the old AST alive"); + } + + @Test + public void removingCompilationUnitClearsOldLookupCacheKeys() throws IOException { + GlobalCaches.clearAll(); + File projectFolder = new File("./temp/testProject_remove_clears_lookup_cache/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + WFile fileMain = WFile.create(new File(wurstFolder, "Main.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + writeFile(fileMain, string( + "package Main", + "public function foo()" + )); + writeFile(fileWurst, "package Wurst\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + manager.buildProject(); + CompilationUnit oldCu = manager.getCompilationUnit(fileMain); + assertNotNull(oldCu); + + GlobalCaches.CacheKey oldKey = new GlobalCaches.CacheKey(oldCu, "foo", GlobalCaches.LookupType.FUNC); + GlobalCaches.lookupCache.put(oldKey, oldCu); + + manager.removeCompilationUnit(fileMain); + + assertEquals(GlobalCaches.lookupCache.containsKey(oldKey), false, + "removing a CU must drop lookup keys that keep the old AST alive"); + } + @Test public void duplicatePackageReportsSingleClearMessage() throws IOException { File projectFolder = new File("./temp/testProject_duplicate_package_message/");