From dd91877e4a96689537833b50fb0194bf99b585f8 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 00:31:30 +0100 Subject: [PATCH 1/8] better runmap file in use handling --- .../languageserver/requests/BuildMap.java | 27 ++++-- .../languageserver/requests/MapRequest.java | 84 +++++++++++++++++-- .../languageserver/requests/RunMap.java | 9 ++ 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java index 6ac3f4304..060b0a087 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java @@ -61,17 +61,28 @@ public Object execute(ModelManager modelManager) throws IOException { // first we copy in same location to ensure validity File buildDir = getBuildDir(); String fileName = projectConfig.getBuildMapData().getFileName(); - Optional targetMap = Optional.of( - new File(buildDir, fileName.isEmpty() ? projectConfig.getProjectName() + ".w3x" : fileName + ".w3x")); - CompilationResult result = compileScript(modelManager, gui, targetMap, projectConfig, buildDir, true); - - injectMapData(gui, targetMap, result); - - Files.copy(getCachedMapFile().toPath(), targetMap.get().toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + File targetMapFile = new File(buildDir, fileName.isEmpty() ? projectConfig.getProjectName() + ".w3x" : fileName + ".w3x"); + targetMapFile = ensureWritableTargetFile( + targetMapFile, + "Build Map", + "The output map file is in use and cannot be replaced.\nClose Warcraft III and click Retry, choose Rename to use a temporary file name, or Cancel.", + "Build canceled because output map target is in use." + ); + CompilationResult result = compileScript(modelManager, gui, Optional.of(targetMapFile), projectConfig, buildDir, true); + + injectMapData(gui, Optional.of(targetMapFile), result); + + targetMapFile = ensureWritableTargetFile( + targetMapFile, + "Build Map", + "The output map file is still in use and cannot be replaced.\nClick Retry, choose Rename to use a temporary file name, or Cancel.", + "Build canceled because output map target is in use." + ); + Files.copy(getCachedMapFile().toPath(), targetMapFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); gui.sendProgress("Finalizing map"); - try (MpqEditor mpq = MpqEditorFactory.getEditor(targetMap)) { + try (MpqEditor mpq = MpqEditorFactory.getEditor(Optional.of(targetMapFile))) { if (mpq != null) { mpq.closeWithCompression(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java index 2d937f3fd..c7be2c0a0 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java @@ -38,9 +38,11 @@ import org.eclipse.lsp4j.services.LanguageClient; import org.jetbrains.annotations.Nullable; +import javax.swing.*; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.channels.FileChannel; import java.nio.channels.NonWritableChannelException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -67,6 +69,7 @@ public abstract class MapRequest extends UserRequest { private static Long lastMapModified = 0L; private static String lastMapPath = ""; + protected String cachedMapFileName = "cached_map.w3x"; public static final String BUILD_CONFIGURED_SCRIPT_NAME = "01_war3mapj_with_config.j.txt"; public static final String BUILD_COMPILED_JASS_NAME = "02_compiled.j.txt"; @@ -376,7 +379,69 @@ protected File getCachedMapFile() { if (!cacheDir.exists()) { UtilsIO.mkdirs(cacheDir); } - return new File(cacheDir, "cached_map.w3x"); + return new File(cacheDir, cachedMapFileName); + } + + protected File ensureWritableTargetFile(File targetFile, String dialogTitle, String lockMessage, + String cancelMessage) { + File currentTarget = targetFile; + while (isLocked(currentTarget)) { + int choice = showTargetLockedDialog(dialogTitle, lockMessage); + if (choice == 0 || choice == JOptionPane.CLOSED_OPTION) { + throw new RequestFailedException(MessageType.Warning, cancelMessage); + } else if (choice == 1) { + continue; + } else if (choice == 2) { + currentTarget = createTemporaryTargetFile(currentTarget); + } + } + return currentTarget; + } + + private int showTargetLockedDialog(String dialogTitle, String message) { + JFrame frame = new JFrame(); + frame.setAlwaysOnTop(true); + Object[] options = { "Cancel", "Retry", "Rename" }; + return JOptionPane.showOptionDialog( + frame, + message, + dialogTitle, + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + options[1] + ); + } + + private File createTemporaryTargetFile(File currentTarget) { + String currentName = currentTarget.getName(); + String baseName = currentName.endsWith(".w3x") + ? currentName.substring(0, currentName.length() - 4) + : currentName; + try { + File parent = currentTarget.getParentFile(); + if (parent == null) { + parent = getBuildDir(); + } + File tempTarget = java.nio.file.Files.createTempFile(parent.toPath(), baseName + "_", ".w3x").toFile(); + tempTarget.deleteOnExit(); + WLogger.info("Using temporary map target due to locked file: " + tempTarget.getAbsolutePath()); + return tempTarget; + } catch (IOException e) { + throw new RequestFailedException(MessageType.Error, "Could not create temporary map target file.", e); + } + } + + private boolean isLocked(File targetMap) { + if (!targetMap.exists()) { + return false; + } + try (FileChannel ignored = FileChannel.open(targetMap.toPath(), StandardOpenOption.WRITE)) { + return false; + } catch (IOException e) { + return true; + } } /** @@ -541,10 +606,7 @@ protected File loadMapScript(Optional mapCopy, ModelManager modelManager, System.out.println("No extract map script enabled - not extracting."); if (scriptFile.exists()) { System.out.println("war3map.j exists at wurst root."); - CompilationUnit compilationUnit = modelManager.getCompilationUnit(WFile.create(scriptFile)); - if (compilationUnit == null) { - modelManager.syncCompilationUnit(WFile.create(scriptFile)); - } + ensureScriptIsSynced(modelManager, scriptFile); return scriptFile; } else { throw new CompileError(new WPos("", new LineOffsets(), 0, 0), @@ -582,15 +644,25 @@ protected File loadMapScript(Optional mapCopy, ModelManager modelManager, WLogger.info("new map, use extracted"); // write mapfile from map to workspace Files.write(extractedScript, scriptFile); + ensureScriptIsSynced(modelManager, scriptFile); } } else { System.out.println("Map not modified, not extracting script"); } - + if (scriptFile.exists()) { + ensureScriptIsSynced(modelManager, scriptFile); + } return scriptFile; } + private static void ensureScriptIsSynced(ModelManager modelManager, File scriptFile) { + CompilationUnit compilationUnit = modelManager.getCompilationUnit(WFile.create(scriptFile)); + if (compilationUnit == null) { + modelManager.syncCompilationUnit(WFile.create(scriptFile)); + } + } + protected CompilationResult applyProjectConfig(WurstGui gui, Optional testMap, File buildDir, WurstProjectConfigData projectConfig, File scriptFile, String outputScriptName) { AtomicReference result = new AtomicReference<>(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java index 25638973e..f36cb5e88 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java @@ -102,6 +102,15 @@ private String compileMap(ModelManager modelManager, WurstGui gui, WurstProjectC mapLastModified = map.get().lastModified(); mapPath = map.get().getAbsolutePath(); } + if (!runArgs.isHotReload()) { + File cacheTarget = ensureWritableTargetFile( + getCachedMapFile(), + "Run Map", + "The cached run map file is in use and cannot be updated.\nClose Warcraft III and click Retry, choose Rename to use a temporary file name, or Cancel.", + "Run canceled because the cached map file is in use." + ); + cachedMapFileName = cacheTarget.getName(); + } Optional testMap = map.map($ -> new File(buildDir, "WurstRunMap.w3x")); CompilationResult result = compileScript(modelManager, gui, testMap, projectConfig, buildDir, false); From a32626dc030e7333a4324de2d31d80934a531a0d Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 00:59:16 +0100 Subject: [PATCH 2/8] better model robustness --- .../languageserver/ModelManagerImpl.java | 48 +++-- .../requests/CodeActionRequest.java | 30 ++- .../languageserver/requests/MapRequest.java | 27 ++- .../attributes/AttrImportedPackage.java | 5 +- .../wurstscript/tests/ModelManagerTests.java | 171 +++++++++++++++++- 5 files changed, 245 insertions(+), 36 deletions(-) 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 ee483a77c..7e8455853 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 @@ -87,25 +87,30 @@ private List getJassdocCUs(Path jassdoc, WurstGui gui) { @Override public Changes removeCompilationUnit(WFile resource) { - parseErrors.remove(resource); WurstModel model2 = model; - if (model2 == null) { - return Changes.empty(); - } - - syncCompilationUnitContent(resource, ""); List toRemove = new ArrayList<>(); - for (CompilationUnit compilationUnit : model2) { - if (wFile(compilationUnit).equals(resource)) { - toRemove.add(compilationUnit); + if (model2 != null) { + for (CompilationUnit compilationUnit : model2) { + if (wFile(compilationUnit).equals(resource)) { + toRemove.add(compilationUnit); + } } + model2.removeAll(toRemove); } - model2.removeAll(toRemove); - return new Changes(toRemove.stream() - .map(this::wFile), + + // Always clear state and diagnostics for removed files. + clearFileState(resource); + reportErrors("remove cu ", resource, Collections.emptyList()); + + toRemove.forEach(compilationunitFile::remove); + + return new Changes( + java.util.Collections.singletonList(resource), toRemove.stream() .flatMap(cu -> cu.getPackages().stream()) - .map(WPackage::getName)); + .map(WPackage::getName) + .collect(Collectors.toList()) + ); } @Override @@ -526,9 +531,14 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents, if (fileHashcodes.containsKey(filename)) { int oldHash = fileHashcodes.get(filename); if (oldHash == contents.hashCode()) { - // no change - WLogger.trace("CU " + filename + " was unchanged."); - return getCompilationUnit(filename); + CompilationUnit existing = getCompilationUnit(filename); + if (existing != null) { + // no change + WLogger.trace("CU " + filename + " was unchanged."); + return existing; + } + // Stale hash cache after remove/move; CU is gone, so reparse. + WLogger.info("CU hash unchanged but model entry missing for " + filename + ", reparsing."); } else { WLogger.info("CU changed. oldHash = " + oldHash + " == " + contents.hashCode()); } @@ -557,6 +567,12 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents, return cu; } + private void clearFileState(WFile file) { + parseErrors.remove(file); + otherErrors.remove(file); + fileHashcodes.remove(file); + } + @Override public CompilationUnit getCompilationUnit(WFile filename) { List matches = getCompilationUnits(Collections.singletonList(filename)); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java index 06b6b5515..984e264f2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java @@ -22,10 +22,14 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import java.io.File; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static de.peeeq.wurstio.languageserver.WurstCommands.WURST_PERFORM_CODE_ACTION; @@ -113,7 +117,7 @@ public List> execute(ModelManager modelManager) { private List> handleMissingName(ModelManager modelManager, NameRef nr) { String funcName = nr.getVarName(); WurstModel model = modelManager.getModel(); - List possibleImports = new ArrayList<>(); + Set possibleImports = new LinkedHashSet<>(); WurstType receiverType = null; if (nr instanceof ExprMember) { ExprMember m = (ExprMember) nr; @@ -136,6 +140,7 @@ private List> handleMissingName(ModelManager modelMa } } } + addDependencyPackageFallback(modelManager, possibleImports, funcName); return makeImportCommands(possibleImports); @@ -157,7 +162,7 @@ private List> handleMissingFunction(ModelManager mod receiverType = m.getLeft().attrTyp(); } WurstModel model = modelManager.getModel(); - List possibleImports = new ArrayList<>(); + Set possibleImports = new LinkedHashSet<>(); for (CompilationUnit cu : model) { withNextPackage: for (WPackage wPackage : cu.getPackages()) { @@ -171,6 +176,7 @@ private List> handleMissingFunction(ModelManager mod } } } + addDependencyPackageFallback(modelManager, possibleImports, funcName); return Utils.concatLists(makeImportCommands(possibleImports), makeCreateFunctionQuickfix(fr)); } @@ -342,7 +348,7 @@ public void indent(StringBuilder sb) { private List> handleMissingType(ModelManager modelManager, String typeName) { WurstModel model = modelManager.getModel(); - List possibleImports = new ArrayList<>(); + Set possibleImports = new LinkedHashSet<>(); for (CompilationUnit cu : model) { for (WPackage wPackage : cu.getPackages()) { if (!wPackage.attrExportedTypeNameLinks().get(typeName).isEmpty()) { @@ -350,6 +356,7 @@ private List> handleMissingType(ModelManager modelMa } } } + addDependencyPackageFallback(modelManager, possibleImports, typeName); return makeImportCommands(possibleImports); } @@ -361,7 +368,7 @@ private List> handleMissingClass(ModelManager modelM // that are not yet imported into the project. WurstModel model = modelManager.getModel(); - List possibleImports = new ArrayList<>(); + Set possibleImports = new LinkedHashSet<>(); for (CompilationUnit cu : model) { withNextPackage: for (WPackage wPackage : cu.getPackages()) { @@ -373,6 +380,7 @@ private List> handleMissingClass(ModelManager modelM } } } + addDependencyPackageFallback(modelManager, possibleImports, typeName); return makeImportCommands(possibleImports); } @@ -380,7 +388,7 @@ private List> handleMissingClass(ModelManager modelM private List> handleMissingModule(ModelManager modelManager, String moduleName) { WurstModel model = modelManager.getModel(); - List possibleImports = new ArrayList<>(); + Set possibleImports = new LinkedHashSet<>(); for (CompilationUnit cu : model) { withNextPackage: for (WPackage wPackage : cu.getPackages()) { @@ -392,13 +400,23 @@ private List> handleMissingModule(ModelManager model } } } + addDependencyPackageFallback(modelManager, possibleImports, moduleName); return makeImportCommands(possibleImports); } - private List> makeImportCommands(List possibleImports) { + private void addDependencyPackageFallback(ModelManager modelManager, Set possibleImports, String unresolvedName) { + for (File dep : modelManager.getDependencyWurstFiles()) { + String libName = Utils.getLibName(dep); + if (libName.equals(unresolvedName)) { + possibleImports.add(libName); + } + } + } + + private List> makeImportCommands(Collection possibleImports) { return possibleImports.stream() .map(this::makeImportCommand) .collect(Collectors.toList()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java index c7be2c0a0..fb6eaa2a7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java @@ -244,21 +244,36 @@ protected File runJassHotCodeReload(File mapScript) throws IOException, Interrup private void purgeUnimportedFiles(WurstModel model) { Set imported = model.stream() - .filter(cu -> isInWurstFolder(cu.getCuInfo().getFile()) || cu.getCuInfo().getFile().endsWith(".j")).collect(Collectors.toSet()); + .filter(cu -> isInProjectWurstFolder(cu.getCuInfo().getFile()) || isProjectWar3MapScript(cu.getCuInfo().getFile())) + .collect(Collectors.toSet()); addImports(imported, imported); model.removeIf(cu -> !imported.contains(cu)); } - private boolean isInWurstFolder(String file) { - Path p = Paths.get(file); - Path w; + private boolean isProjectWar3MapScript(String file) { + if (!file.endsWith("war3map.j")) { + return false; + } + Path p = Paths.get(file).toAbsolutePath().normalize(); + Path projectWurstFolder; + try { + projectWurstFolder = workspaceRoot.getPath().resolve("wurst").toAbsolutePath().normalize(); + } catch (FileNotFoundException e) { + return false; + } + return p.startsWith(projectWurstFolder) && java.nio.file.Files.exists(p); + } + + private boolean isInProjectWurstFolder(String file) { + Path p = Paths.get(file).toAbsolutePath().normalize(); + Path projectWurstFolder; try { - w = workspaceRoot.getPath(); + projectWurstFolder = workspaceRoot.getPath().resolve("wurst").toAbsolutePath().normalize(); } catch (FileNotFoundException e) { return false; } - return p.startsWith(w) + return p.startsWith(projectWurstFolder) && java.nio.file.Files.exists(p) && Utils.isWurstFile(file); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImportedPackage.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImportedPackage.java index 71160bcce..c6a462bc8 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImportedPackage.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImportedPackage.java @@ -42,9 +42,8 @@ public static ImmutableMap getPackages(WurstModel wurstModel) WPackage old = result.put(p.getName(), p); if (old != null) { if (!p.getName().equals("Wurst")) { - // TODO should this really error? - p.addError("Package " + p.getName() + " is already defined in " + Utils.printPos(old.getSource())); - old.addError("Package " + p.getName() + " is already defined in " + Utils.printPos(p.getSource())); + p.addError("Package '" + p.getName() + "' is defined multiple times. " + + "This is currently not supported. First definition: " + Utils.printPos(old.getSource())); } } } 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 518b68cf3..79c9cf268 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 @@ -246,6 +246,86 @@ public void renamePackage() throws IOException { } + @Test + public void deletingFileClearsErrorsFromModel() throws IOException { + File projectFolder = new File("./temp/testProject_delete_clears_errors/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + WFile badFile = WFile.create(new File(wurstFolder, "Bad.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + writeFile(badFile, string( + "package Bad", + "init", + " if (" + )); + writeFile(fileWurst, "package Wurst\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + Map errors = keepErrorsInMap(manager); + manager.buildProject(); + assertThat(errors.get(badFile), CoreMatchers.containsString("extraneous input")); + assertEquals(manager.hasErrors(), true); + + // Delete and remove from model: stale diagnostics/errors must be cleared immediately. + badFile.getFile().delete(); + manager.removeCompilationUnit(badFile); + + assertEquals(manager.hasErrors(), false); + assertEquals(errors.getOrDefault(badFile, ""), ""); + } + + @Test + public void readdingMovedFileWithSameContentUpdatesModel() throws IOException { + File projectFolder = new File("./temp/testProject_move_same_content/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + String packageTest = string( + "package Test", + "public function foo()" + ); + + WFile fileTest = WFile.create(new File(wurstFolder, "Test.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + writeFile(fileTest, packageTest); + writeFile(fileWurst, "package Wurst\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + manager.buildProject(); + assertNotNull(manager.getCompilationUnit(fileTest)); + + // Simulate move/delete then recreate with same contents. + fileTest.getFile().delete(); + manager.removeCompilationUnit(fileTest); + writeFile(fileTest, packageTest); + manager.syncCompilationUnit(fileTest); + + assertNotNull(manager.getCompilationUnit(fileTest), "file should be present after re-adding same contents"); + } + + @Test + public void duplicatePackageReportsSingleClearMessage() throws IOException { + File projectFolder = new File("./temp/testProject_duplicate_package_message/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + WFile a = WFile.create(new File(wurstFolder, "A.wurst")); + WFile b = WFile.create(new File(wurstFolder, "B.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + writeFile(a, "package Dup\n"); + writeFile(b, "package Dup\n"); + writeFile(fileWurst, "package Wurst\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + Map errors = keepErrorsInMap(manager); + manager.buildProject(); + + String allErrors = errors.values().stream().collect(Collectors.joining("\n")); + assertThat(allErrors, CoreMatchers.containsString("Package 'Dup' is defined multiple times.")); + assertThat(allErrors, new IsNot<>(CoreMatchers.containsString("is already defined in"))); + } + @NotNull private Map keepErrorsInMap(ModelManagerImpl manager) { // keep error messages in a map: @@ -730,10 +810,82 @@ private void runRunmapLikeCompile_Closer(File projectFolder, ModelManagerImpl ma } /** Same as MapRequest.purgeUnimportedFiles: keep wurst-folder CUs and any imported transitively, plus .j files. */ + @Test + public void runmapPurge_keepsProjectWar3MapAndPurgesUnimportedDependency() throws Exception { + File projectFolder = new File("./temp/testProject_runmap_purge_dep/"); + File wurstFolder = new File(projectFolder, "wurst"); + File dependencyRoot = new File(projectFolder, "depA"); + File dependencyWurst = new File(dependencyRoot, "wurst"); + newCleanFolder(wurstFolder); + newCleanFolder(dependencyWurst); + + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + WFile fileMain = WFile.create(new File(wurstFolder, "Main.wurst")); + WFile fileProjectWar3Map = WFile.create(new File(wurstFolder, "war3map.j")); + WFile fileDep = WFile.create(new File(dependencyWurst, "UnimportedDep.wurst")); + + writeFile(fileWurst, "package Wurst\n"); + writeFile(fileMain, string( + "package Main", + "init", + " skip" + )); + writeFile(fileProjectWar3Map, "globals\nendglobals\n"); + writeFile(fileDep, string( + "package UnimportedDep", + "init", + " skip" + )); + Files.writeString(new File(projectFolder, "wurst.dependencies").toPath(), dependencyRoot.getAbsolutePath() + "\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + manager.buildProject(); + assertNotNull(manager.getCompilationUnit(fileProjectWar3Map), "project war3map.j should be loaded before purge"); + + manager.syncCompilationUnit(fileDep); + assertNotNull(manager.getCompilationUnit(fileDep), "dependency CU should be loaded after opening/sync"); + purgeUnimportedFiles_likeRunMap(manager.getModel(), manager); + + assertEquals(manager.getCompilationUnit(fileDep), null, "unimported dependency CU must be removed by purge"); + assertNotNull(manager.getCompilationUnit(fileProjectWar3Map), "project war3map.j must be retained by purge"); + } + + @Test + public void runmapPurge_onlyKeepsWar3MapFromProjectWurstFolder() throws Exception { + File projectFolder = new File("./temp/testProject_runmap_purge_war3map_scope/"); + File wurstFolder = new File(projectFolder, "wurst"); + File dependencyRoot = new File(projectFolder, "depB"); + File dependencyWurst = new File(dependencyRoot, "wurst"); + newCleanFolder(wurstFolder); + newCleanFolder(dependencyWurst); + + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + WFile fileMain = WFile.create(new File(wurstFolder, "Main.wurst")); + WFile fileProjectWar3Map = WFile.create(new File(wurstFolder, "war3map.j")); + WFile fileDependencyWar3Map = WFile.create(new File(dependencyWurst, "war3map.j")); + + writeFile(fileWurst, "package Wurst\n"); + writeFile(fileMain, "package Main\n"); + writeFile(fileProjectWar3Map, "globals\nendglobals\n"); + writeFile(fileDependencyWar3Map, "globals\nendglobals\n"); + Files.writeString(new File(projectFolder, "wurst.dependencies").toPath(), dependencyRoot.getAbsolutePath() + "\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + manager.buildProject(); + assertNotNull(manager.getCompilationUnit(fileProjectWar3Map), "project war3map.j should be loaded before purge"); + manager.syncCompilationUnit(fileDependencyWar3Map); + assertNotNull(manager.getCompilationUnit(fileDependencyWar3Map), "dependency war3map.j should be loaded after opening/sync"); + + purgeUnimportedFiles_likeRunMap(manager.getModel(), manager); + + assertNotNull(manager.getCompilationUnit(fileProjectWar3Map), "project war3map.j must be retained"); + assertEquals(manager.getCompilationUnit(fileDependencyWar3Map), null, "dependency war3map.j must not be retained implicitly"); + } + private void purgeUnimportedFiles_likeRunMap(WurstModel model, ModelManagerImpl manager) { - // Seed: files inside project root (wurst folder) OR .j files. java.util.Set keep = model.stream() - .filter(cu -> isInWurstFolder_likeRunMap(cu.getCuInfo().getFile(), manager) || cu.getCuInfo().getFile().endsWith(".j")) + .filter(cu -> isInProjectWurstFolder_likeRunMap(cu.getCuInfo().getFile(), manager) + || isProjectWar3Map_likeRunMap(cu.getCuInfo().getFile(), manager)) .collect(java.util.stream.Collectors.toSet()); // Recursively add imported packages’ CUs (uses attrImportedPackage like RunMap) @@ -741,14 +893,23 @@ private void purgeUnimportedFiles_likeRunMap(WurstModel model, ModelManagerImpl model.removeIf(cu -> !keep.contains(cu)); } - private boolean isInWurstFolder_likeRunMap(String file, ModelManagerImpl manager) { - java.nio.file.Path p = java.nio.file.Paths.get(file); - java.nio.file.Path w = manager.getProjectPath().toPath(); // project root + private boolean isInProjectWurstFolder_likeRunMap(String file, ModelManagerImpl manager) { + java.nio.file.Path p = java.nio.file.Paths.get(file).toAbsolutePath().normalize(); + java.nio.file.Path w = manager.getProjectPath().toPath().resolve("wurst").toAbsolutePath().normalize(); return p.startsWith(w) && java.nio.file.Files.exists(p) && Utils.isWurstFile(file); } + private boolean isProjectWar3Map_likeRunMap(String file, ModelManagerImpl manager) { + if (!file.endsWith("war3map.j")) { + return false; + } + java.nio.file.Path p = java.nio.file.Paths.get(file).toAbsolutePath().normalize(); + java.nio.file.Path w = manager.getProjectPath().toPath().resolve("wurst").toAbsolutePath().normalize(); + return p.startsWith(w) && java.nio.file.Files.exists(p); + } + private void addImports_likeRunMap(java.util.Set result, java.util.Set toAdd) { java.util.Set imported = toAdd.stream() From d8c45e2525b5970eb1d219039e5b3d9bddcce79a Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 01:14:08 +0100 Subject: [PATCH 3/8] fixes --- .../languageserver/requests/MapRequest.java | 19 ++++++++++++++++++- .../wurstscript/tests/ModelManagerTests.java | 13 ++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java index fb6eaa2a7..3811df3c1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java @@ -244,7 +244,9 @@ protected File runJassHotCodeReload(File mapScript) throws IOException, Interrup private void purgeUnimportedFiles(WurstModel model) { Set imported = model.stream() - .filter(cu -> isInProjectWurstFolder(cu.getCuInfo().getFile()) || isProjectWar3MapScript(cu.getCuInfo().getFile())) + .filter(cu -> isInProjectWurstFolder(cu.getCuInfo().getFile()) + || isProjectWar3MapScript(cu.getCuInfo().getFile()) + || isRequiredBuildJass(cu.getCuInfo().getFile())) .collect(Collectors.toSet()); addImports(imported, imported); @@ -278,6 +280,21 @@ private boolean isInProjectWurstFolder(String file) { && Utils.isWurstFile(file); } + private boolean isRequiredBuildJass(String file) { + Path p = Paths.get(file).toAbsolutePath().normalize(); + Path buildDir; + try { + buildDir = workspaceRoot.getPath().resolve("_build").toAbsolutePath().normalize(); + } catch (FileNotFoundException e) { + return false; + } + if (!p.startsWith(buildDir)) { + return false; + } + String name = p.getFileName().toString(); + return name.equals("common.j") || name.equals("blizzard.j"); + } + protected File getBuildDir() { File buildDir; try { 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 79c9cf268..099d8baec 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 @@ -885,7 +885,8 @@ public void runmapPurge_onlyKeepsWar3MapFromProjectWurstFolder() throws Exceptio private void purgeUnimportedFiles_likeRunMap(WurstModel model, ModelManagerImpl manager) { java.util.Set keep = model.stream() .filter(cu -> isInProjectWurstFolder_likeRunMap(cu.getCuInfo().getFile(), manager) - || isProjectWar3Map_likeRunMap(cu.getCuInfo().getFile(), manager)) + || isProjectWar3Map_likeRunMap(cu.getCuInfo().getFile(), manager) + || isRequiredBuildJass_likeRunMap(cu.getCuInfo().getFile(), manager)) .collect(java.util.stream.Collectors.toSet()); // Recursively add imported packages’ CUs (uses attrImportedPackage like RunMap) @@ -910,6 +911,16 @@ private boolean isProjectWar3Map_likeRunMap(String file, ModelManagerImpl manage return p.startsWith(w) && java.nio.file.Files.exists(p); } + private boolean isRequiredBuildJass_likeRunMap(String file, ModelManagerImpl manager) { + java.nio.file.Path p = java.nio.file.Paths.get(file).toAbsolutePath().normalize(); + java.nio.file.Path buildDir = manager.getProjectPath().toPath().resolve("_build").toAbsolutePath().normalize(); + if (!p.startsWith(buildDir) || !java.nio.file.Files.exists(p)) { + return false; + } + String name = p.getFileName().toString(); + return name.equals("common.j") || name.equals("blizzard.j"); + } + private void addImports_likeRunMap(java.util.Set result, java.util.Set toAdd) { java.util.Set imported = toAdd.stream() From 409cd5ae048ba77d12076b92c289942409ae8b98 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 02:19:16 +0100 Subject: [PATCH 4/8] performance improvements --- .../wurstio/CompiletimeFunctionRunner.java | 58 ++++++++++ .../attributes/names/NameResolution.java | 80 ++++++++++++- .../interpreter/ILInterpreter.java | 19 +++- .../imtojass/ImToJassTranslator.java | 62 +++++----- .../imtranslation/CyclicFunctionRemover.java | 107 ++++++++++-------- .../wurstscript/validation/GlobalCaches.java | 14 +++ .../validation/WurstValidator.java | 17 +-- 7 files changed, 265 insertions(+), 92 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index 59f5d2746..45e5a6f97 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -51,6 +51,8 @@ public class CompiletimeFunctionRunner { private final ImTranslator translator; private boolean injectObjects; private final Deque delayedActions = new ArrayDeque<>(); + private final Map compiletimeFunctionNanos = new LinkedHashMap<>(); + private long compiletimeExprNanos = 0L; public ILInterpreter getInterpreter() { return interpreter; @@ -97,21 +99,29 @@ public CompiletimeFunctionRunner( public void run() { try { + long t0 = System.nanoTime(); List> toExecute = new ArrayList<>(); collectCompiletimeExpressions(toExecute); collectCompiletimeFunctions(toExecute); + long tCollected = System.nanoTime(); toExecute.sort(Comparator.comparing(this::getOrderIndex)); + long tSorted = System.nanoTime(); execute(toExecute); + long tExecuted = System.nanoTime(); if (functionFlag == FunctionFlagToRun.CompiletimeFunctions) { interpreter.writebackGlobalState(isInjectObjects()); } + long tWriteback = System.nanoTime(); runDelayedActions(); + long tDelayed = System.nanoTime(); partitionCompiletimeStateInitFunction(); + long tPartitioned = System.nanoTime(); + logCompiletimeTiming(toExecute, t0, tCollected, tSorted, tExecuted, tWriteback, tDelayed, tPartitioned); } catch (InterpreterException e) { Element origin = e.getTrace(); @@ -136,6 +146,48 @@ public void run() { } + private void logCompiletimeTiming(List> toExecute, + long t0, long tCollected, long tSorted, long tExecuted, + long tWriteback, long tDelayed, long tPartitioned) { + int exprCount = 0; + int funcCount = 0; + for (Either e : toExecute) { + if (e.isLeft()) { + exprCount++; + } else { + funcCount++; + } + } + WLogger.info(String.format( + "Compiletime breakdown: total=%dms collect=%dms sort=%dms execute=%dms writeback=%dms delayed=%dms partition=%dms funcs=%d exprs=%d exprEval=%dms", + ms(tPartitioned - t0), + ms(tCollected - t0), + ms(tSorted - tCollected), + ms(tExecuted - tSorted), + ms(tWriteback - tExecuted), + ms(tDelayed - tWriteback), + ms(tPartitioned - tDelayed), + funcCount, + exprCount, + ms(compiletimeExprNanos) + )); + if (!compiletimeFunctionNanos.isEmpty()) { + List> top = compiletimeFunctionNanos.entrySet().stream() + .sorted((a, b) -> Long.compare(b.getValue(), a.getValue())) + .limit(10) + .collect(Collectors.toList()); + StringBuilder sb = new StringBuilder("Top compiletime functions:"); + for (Map.Entry e : top) { + sb.append("\n ").append(e.getKey()).append(": ").append(ms(e.getValue())).append("ms"); + } + WLogger.info(sb.toString()); + } + } + + private static long ms(long nanos) { + return nanos / 1_000_000L; + } + private void partitionCompiletimeStateInitFunction() { if (compiletimeStateInitFunction == null) { return; @@ -220,6 +272,7 @@ public void visit(ImCompiletimeExpr e) { private void executeCompiletimeExpr(ImCompiletimeExpr cte) { + long t0 = System.nanoTime(); try { ProgramState globalState = interpreter.getGlobalState(); globalState.setLastStatement(cte); @@ -261,6 +314,8 @@ private void executeCompiletimeExpr(ImCompiletimeExpr cte) { e.setStacktrace(msg); e.setTrace(cte.attrTrace()); throw e; + } finally { + compiletimeExprNanos += System.nanoTime() - t0; } } @@ -491,6 +546,7 @@ private ImFunction findNative(String funcName, WPos trace) { private void executeCompiletimeFunction(ImFunction f) { if (functionFlag.matches(f)) { + long t0 = System.nanoTime(); try { if (!f.getBody().isEmpty()) { interpreter.getGlobalState().setLastStatement(f.getBody().get(0)); @@ -505,6 +561,8 @@ private void executeCompiletimeFunction(ImFunction f) { } catch (Throwable e) { failTests.put(f, Pair.create(interpreter.getLastStatement(), e.toString())); throw e; + } finally { + compiletimeFunctionNanos.merge(f.getName(), System.nanoTime() - t0, Long::sum); } } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java index 45a666600..21100e84d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java @@ -12,6 +12,9 @@ import java.util.*; public class NameResolution { + private static final String PACKAGE_NAME_LOOKUP_PREFIX = "__pkg_name__"; + private static final String PACKAGE_TYPE_LOOKUP_PREFIX = "__pkg_type__"; + private static String memberFuncCacheName(String name, WurstType receiverType) { return name + "@" @@ -20,6 +23,71 @@ private static String memberFuncCacheName(String name, WurstType receiverType) { + System.identityHashCode(receiverType); } + private static ImmutableCollection scopeNameLinks(WScope scope, String name) { + if (scope instanceof WPackage) { + return packageNameLinks((WPackage) scope, name); + } + return scope.attrNameLinks().get(name); + } + + private static ImmutableCollection scopeTypeLinks(WScope scope, String name) { + if (scope instanceof WPackage) { + return packageTypeLinks((WPackage) scope, name); + } + return scope.attrTypeNameLinks().get(name); + } + + private static ImmutableCollection packageNameLinks(WPackage p, String name) { + GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(p, PACKAGE_NAME_LOOKUP_PREFIX + name, GlobalCaches.LookupType.PACKAGE); + @SuppressWarnings("unchecked") + ImmutableCollection cached = (ImmutableCollection) GlobalCaches.lookupCache.get(key); + if (cached != null) { + return cached; + } + + LinkedHashSet result = new LinkedHashSet<>(); + boolean repl = p.getName().equals("WurstREPL"); + for (WImport imp : p.getImports()) { + if (imp.getPackagename().equals("NoWurst")) { + continue; + } + WPackage importedPackage = imp.attrImportedPackage(); + if (importedPackage == null) { + continue; + } + if (repl) { + result.addAll(importedPackage.getElements().attrNameLinks().get(name)); + result.addAll(importedPackage.attrNameLinks().get(name)); + } else { + result.addAll(importedPackage.attrExportedNameLinks().get(name)); + } + } + ImmutableCollection links = ImmutableList.copyOf(result); + GlobalCaches.lookupCache.put(key, links); + return links; + } + + private static ImmutableCollection packageTypeLinks(WPackage p, String name) { + GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(p, PACKAGE_TYPE_LOOKUP_PREFIX + name, GlobalCaches.LookupType.PACKAGE); + @SuppressWarnings("unchecked") + ImmutableCollection cached = (ImmutableCollection) GlobalCaches.lookupCache.get(key); + if (cached != null) { + return cached; + } + + LinkedHashSet result = new LinkedHashSet<>(); + for (WImport imp : p.getImports()) { + WPackage importedPackage = imp.attrImportedPackage(); + if (importedPackage == null) { + continue; + } + result.addAll(importedPackage.attrExportedTypeNameLinks().get(name)); + } + ImmutableCollection links = ImmutableList.copyOf(result); + GlobalCaches.lookupCache.put(key, links); + return links; + } + public static ImmutableCollection lookupFuncsNoConfig(Element node, String name, boolean showErrors) { if (!showErrors) { GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(node, name, GlobalCaches.LookupType.FUNC); @@ -51,7 +119,7 @@ public static ImmutableCollection lookupFuncsNoConfig(Element node, St Set seen = new HashSet<>(); for (WScope s : scopes) { - Collection links = s.attrNameLinks().get(name); + Collection links = scopeNameLinks(s, name); if (links.isEmpty()) continue; for (DefLink n : links) { @@ -170,7 +238,7 @@ public static ImmutableCollection lookupMemberFuncs(Element node, Wurs } for (WScope s : scopes) { - Collection links = s.attrNameLinks().get(name); + Collection links = scopeNameLinks(s, name); if (links.isEmpty()) continue; for (DefLink n : links) { @@ -245,7 +313,7 @@ public static NameLink lookupVarNoConfig(Element node, String name, boolean show } } - Collection links = s.attrNameLinks().get(name); + Collection links = scopeNameLinks(s, name); if (links.isEmpty()) continue; for (DefLink n : links) { @@ -308,7 +376,7 @@ public static NameLink lookupMemberVar(Element node, WurstType receiverType, Str DefLinkMatch bestMatch = null; for (WScope s : scopes) { - Collection links = s.attrNameLinks().get(name); + Collection links = scopeNameLinks(s, name); if (links.isEmpty()) continue; DefLinkMatch candidate = findBestMemberVarMatch(links, receiverType, node, showErrors); @@ -513,7 +581,7 @@ private static Iterable typeParamsOfReceiverType(WurstType t) { for (WScope s : scopes) { - ImmutableCollection links = s.attrTypeNameLinks().get(name); + ImmutableCollection links = scopeTypeLinks(s, name); if (links.isEmpty()) continue; for (NameLink n : links) { @@ -556,7 +624,7 @@ private static Iterable typeParamsOfReceiverType(WurstType t) { public static PackageLink lookupPackage(Element node, String name, boolean showErrors) { WScope scope = node.attrNearestScope(); while (scope != null) { - for (NameLink n : scope.attrNameLinks().get(name)) { + for (NameLink n : scopeNameLinks(scope, name)) { if (n instanceof PackageLink) { return (PackageLink) n; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java index 1f546a952..b03e2735a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java @@ -28,6 +28,7 @@ import static de.peeeq.wurstscript.translation.imoptimizer.UselessFunctionCallsRemover.isFunctionPure; import static de.peeeq.wurstscript.validation.GlobalCaches.LOCAL_STATE_CACHE; +import static de.peeeq.wurstscript.validation.GlobalCaches.LOCAL_STATE_NOARG_CACHE; public class ILInterpreter implements AbstractInterpreter { private ImProg prog; @@ -294,12 +295,18 @@ private static LocalState runBuiltinFunction(ProgramState globalState, ImFunctio final String fname = f.getName(); final boolean pure = isFunctionPure(fname); + if (pure && args.length == 0) { + LocalState cachedNoArg = LOCAL_STATE_NOARG_CACHE.get(f); + if (cachedNoArg != null) { + return cachedNoArg; + } + } + GlobalCaches.ArgumentKey key = null; + Object2ObjectOpenHashMap perFn = null; if (pure) { key = GlobalCaches.ArgumentKey.forLookup(args); - - final Object2ObjectOpenHashMap perFn = - LOCAL_STATE_CACHE.get(f); + perFn = LOCAL_STATE_CACHE.get(f); if (perFn != null) { final LocalState cached = perFn.get(key); if (cached != null) { @@ -317,9 +324,11 @@ private static LocalState runBuiltinFunction(ProgramState globalState, ImFunctio final LocalState localState = new LocalState(natives.invoke(fname, args)); if (pure) { + if (args.length == 0) { + LOCAL_STATE_NOARG_CACHE.put(f, localState); + return localState; + } // insert into per-function cache with bounded size - Object2ObjectOpenHashMap perFn = - LOCAL_STATE_CACHE.get(f); if (perFn == null) { perFn = new Object2ObjectOpenHashMap<>(16); LOCAL_STATE_CACHE.put(f, perFn); 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 af32c53af..6a280c9cb 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 @@ -58,24 +58,50 @@ public JassProg translate() { * makes names unique in a consistent way */ private void makeNamesUnique(List list) { - List sorted = new ArrayList<>(); - for (T t : list) { - sorted.add(t); - } + List sorted = new ArrayList<>(list); sorted.sort(Comparator.comparing(JassImElementWithName::getName) .thenComparing(v -> v.getTrace().attrSource().getFile()) .thenComparing(v -> v.getTrace().attrSource().getLine()) .thenComparing(v -> v.getTrace().attrSource().getStartColumn())); - for (int i = 0; i < sorted.size(); i++) { - T vi = sorted.get(i); - for (int j = i + 1; j < sorted.size(); j++) { - T vj = sorted.get(j); - if (vi.getName().equals(vj.getName())) { - vj.setName(vi.getName() + "_" + j); + Set used = new HashSet<>(sorted.size() * 2); + Map nextSuffix = new HashMap<>(); + + for (T v : sorted) { + String base = v.getName(); + if (used.add(base)) { + nextSuffix.putIfAbsent(base, 1); + continue; + } + + int suffix = nextSuffix.getOrDefault(base, 1); + String candidate; + do { + candidate = base + "_" + suffix; + suffix++; + } while (used.contains(candidate)); + + v.setName(candidate); + used.add(candidate); + nextSuffix.put(base, suffix); + nextSuffix.putIfAbsent(candidate, 1); + } + } + + private static final Pattern jassValidName = Pattern.compile("[a-zA-Z][a-zA-Z0-9_]*"); + + private static de.peeeq.wurstscript.ast.Element getTrace(@Nullable Element elem) { + while (elem != null) { + if (elem instanceof ElementWithTrace) { + ElementWithTrace ElementWithTrace = (ElementWithTrace) elem; + de.peeeq.wurstscript.ast.Element t = ElementWithTrace.getTrace(); + if (t != null) { + return t; } } + elem = elem.getParent(); } + throw new Error("Could not get trace to original program."); } private void collectGlobalVars() { @@ -130,20 +156,6 @@ private List sorted(Collection collection) { return r; } - private static de.peeeq.wurstscript.ast.Element getTrace(@Nullable Element elem) { - while (elem != null) { - if (elem instanceof ElementWithTrace) { - ElementWithTrace ElementWithTrace = (ElementWithTrace) elem; - de.peeeq.wurstscript.ast.Element t = ElementWithTrace.getTrace(); - if (t != null) { - return t; - } - } - elem = elem.getParent(); - } - throw new Error("Could not get trace to original program."); - } - private void translateFunction(ImFunction imFunc) { if (imFunc.isBj() || imFunc.hasFlag(FunctionFlagEnum.IS_VARARG)) { return; @@ -230,8 +242,6 @@ private String jassifyName(String name) { return name; } - private final Pattern jassValidName = Pattern.compile("[a-zA-Z][a-zA-Z0-9_]*"); - /** * replaces all invalid characters with underscores */ diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java index bcc5ac4ee..b22d1f8bc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java @@ -9,7 +9,6 @@ import de.peeeq.wurstscript.types.WurstTypeInt; import java.util.*; -import java.util.concurrent.atomic.AtomicReference; /** * Removes cyclic functions from a program @@ -32,15 +31,14 @@ public CyclicFunctionRemover(ImTranslator tr, ImProg prog, TimeTaker timeTaker) public void work() { tr.calculateCallRelationsAndUsedVariables(); - AtomicReference>> components = new AtomicReference<>(); - timeTaker.measure("finding cycles", - () -> components.set(graph.findStronglyConnectedComponents(prog.getFunctions())) + List> components = timeTaker.measure("finding cycles", + () -> graph.findStronglyConnectedComponents(prog.getFunctions()) ); timeTaker.measure("removing cycles", () -> removeCycles(components)); } - private void removeCycles(AtomicReference>> componentsRef) { - for (List component : componentsRef.get()) { + private void removeCycles(List> components) { + for (List component : components) { if (component.size() > 1) { // keep list for order; set for O(1) membership Set funcSet = new HashSet<>(component); @@ -96,11 +94,28 @@ private void removeCycle(List funcs, Set funcSet) { for (int i = 0; i < funcs.size(); i++) { funcToIndex.put(funcs.get(i), i); } - replaceCalls(funcSet, funcToIndex, newFunc, oldToNewVar, prog); + Map proxyByOriginal = new HashMap<>(); + // Rewrite only affected roots: + // - merged cycle body (contains moved bodies from all old funcs) + // - callers that directly call any removed function + Set rewriteRoots = new LinkedHashSet<>(); + rewriteRoots.add(newFunc.getBody()); + for (ImFunction caller : new ArrayList<>(tr.getCalledFunctions().keySet())) { + Collection called = tr.getCalledFunctions().get(caller); + for (ImFunction removed : funcSet) { + if (called.contains(removed)) { + rewriteRoots.add(caller.getBody()); + break; + } + } + } + for (Element root : rewriteRoots) { + replaceCalls(funcSet, funcToIndex, newFunc, oldToNewVar, proxyByOriginal, root); + } - for (ImFunction e : Lists.newArrayList(tr.getCalledFunctions().keys())) { - Collection called = tr.getCalledFunctions().get(e); - called.removeAll(funcs); + // Iterate over a snapshot: removing values may remove keys from the live keySet. + for (ImFunction caller : new ArrayList<>(tr.getCalledFunctions().keySet())) { + tr.getCalledFunctions().get(caller).removeAll(funcSet); } // remove the old funcs @@ -126,53 +141,51 @@ private void replaceVars(Element e, Map oldToNewVar) { } - private void replaceCalls(Set funcSet, Map funcToIndex, ImFunction newFunc, Map oldToNewVar, Element e) { - List relevant = new ArrayList<>(); - e.accept(new Element.DefaultVisitor() { - @Override - public void visit(ImFuncRef imFuncRef) { - super.visit(imFuncRef); - relevant.add(imFuncRef); + private void replaceCalls(Set funcSet, Map funcToIndex, ImFunction newFunc, + Map oldToNewVar, Map proxyByOriginal, Element e) { + ArrayDeque stack = new ArrayDeque<>(); + stack.push(e); + while (!stack.isEmpty()) { + Element current = stack.pop(); + if (current instanceof ImFuncRef) { + replaceImFuncRef(funcSet, funcToIndex, newFunc, oldToNewVar, proxyByOriginal, (ImFuncRef) current); + } else if (current instanceof ImFunctionCall) { + replaceImFunctionCall(funcSet, funcToIndex, newFunc, oldToNewVar, (ImFunctionCall) current); } - - @Override - public void visit(ImFunctionCall imFunctionCall) { - super.visit(imFunctionCall); - relevant.add(imFunctionCall); - } - }); - for (Element relevantElem : relevant) { - if (relevantElem instanceof ImFuncRef) { - replaceImFuncRef(funcSet, funcToIndex, newFunc, oldToNewVar, (ImFuncRef) relevantElem); - } else if (relevantElem instanceof ImFunctionCall) { - replaceImFunctionCall(funcSet, funcToIndex, newFunc, oldToNewVar, (ImFunctionCall) relevantElem); + for (int i = current.size() - 1; i >= 0; i--) { + stack.push(current.get(i)); } } } - private void replaceImFuncRef(Set funcSet, Map funcToIndex, ImFunction newFunc, Map oldToNewVar, ImFuncRef e) { + private void replaceImFuncRef(Set funcSet, Map funcToIndex, ImFunction newFunc, + Map oldToNewVar, Map proxyByOriginal, + ImFuncRef e) { ImFuncRef fr = e; ImFunction f = fr.getFunc(); if (funcSet.contains(f)) { + ImFunction proxyFunc = proxyByOriginal.get(f); + if (proxyFunc == null) { + proxyFunc = JassIm.ImFunction(f.attrTrace(), f.getName() + "_proxy", JassIm.ImTypeVars(), f.getParameters().copy(), f.getReturnType().copy(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + prog.getFunctions().add(proxyFunc); + proxyByOriginal.put(f, proxyFunc); + + ImExprs arguments = JassIm.ImExprs(); + for (ImVar p : proxyFunc.getParameters()) { + arguments.add(JassIm.ImVarAccess(p)); + } - ImFunction proxyFunc = JassIm.ImFunction(f.attrTrace(), f.getName() + "_proxy", JassIm.ImTypeVars(), f.getParameters().copy(), f.getReturnType().copy(), JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); - prog.getFunctions().add(proxyFunc); - - ImExprs arguments = JassIm.ImExprs(); - for (ImVar p : proxyFunc.getParameters()) { - arguments.add(JassIm.ImVarAccess(p)); - } - - ImFunctionCall call = JassIm.ImFunctionCall(fr.attrTrace(), f, JassIm.ImTypeArguments(), arguments, true, CallType.NORMAL); + ImFunctionCall call = JassIm.ImFunctionCall(fr.attrTrace(), f, JassIm.ImTypeArguments(), arguments, true, CallType.NORMAL); - if (f.getReturnType() instanceof ImVoid) { - proxyFunc.getBody().add(call); - } else { - proxyFunc.getBody().add(JassIm.ImReturn(proxyFunc.getTrace(), call)); + if (f.getReturnType() instanceof ImVoid) { + proxyFunc.getBody().add(call); + } else { + proxyFunc.getBody().add(JassIm.ImReturn(proxyFunc.getTrace(), call)); + } + // rewrite the proxy call once per function: + replaceCalls(funcSet, funcToIndex, newFunc, oldToNewVar, proxyByOriginal, call); } - // rewrite the proxy call: - replaceCalls(funcSet, funcToIndex, newFunc, oldToNewVar, call); - // change the funcref to use the proxy + // change the funcref to use the shared proxy fr.setFunc(proxyFunc); } } @@ -289,5 +302,3 @@ protected Collection getIncidentNodes(ImFunction f) { } - - 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 549355ccd..1950cfc40 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 @@ -11,6 +11,8 @@ // Expose static fields only if you already have them there; otherwise, just clear via dedicated methods. public final class GlobalCaches { + // Stats are useful for diagnostics but expensive on very hot cache paths. + private static final boolean ENABLE_CACHE_STATS = false; // Statistics tracking public static class CacheStats { @@ -24,14 +26,17 @@ public static class CacheStats { } void recordHit() { + if (!ENABLE_CACHE_STATS) return; hits.incrementAndGet(); } void recordMiss() { + if (!ENABLE_CACHE_STATS) return; misses.incrementAndGet(); } void recordEviction(int count) { + if (!ENABLE_CACHE_STATS) return; evictions.addAndGet(count); } @@ -56,6 +61,7 @@ public String toString() { public static final class ArgumentKey { private final ILconst[] args; private final int hash; + private static final ArgumentKey EMPTY = new ArgumentKey(new ILconst[0]); // Reuse instances when possible via a small pool for common sizes private static final ThreadLocal POOL = @@ -68,6 +74,9 @@ private ArgumentKey(ILconst[] args) { // Factory method that reuses instances for lookup public static ArgumentKey forLookup(ILconst[] args) { + if (args.length == 0) { + return EMPTY; + } return new ArgumentKey(args); } @@ -121,12 +130,17 @@ public void clear() { } }; + // Fast path for pure builtin calls without arguments: avoid ArgumentKey hashing entirely. + public static final Object2ObjectOpenHashMap LOCAL_STATE_NOARG_CACHE = + new Object2ObjectOpenHashMap<>(); + /** * Call this between tests (and after each compile) */ public static void clearAll() { LOCAL_STATE_CACHE.clear(); + LOCAL_STATE_NOARG_CACHE.clear(); lookupCache.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 e7cfa3530..8105bddbe 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 @@ -483,7 +483,7 @@ private void check(Element e) { } if (e instanceof WScope) checkForDuplicateNames((WScope) e); - if (e instanceof WStatement) + if (isHeavy() && e instanceof WStatement) checkReachability((WStatement) e); if (e instanceof WurstModel) checkForDuplicatePackages((WurstModel) e); @@ -1573,6 +1573,12 @@ private void visit(FuncDef func) { } private void checkUninitializedVars(FunctionLike f) { + if (!isHeavy()) { + // Phase-1: collect only. Avoid triggering extra attributes in light validation. + heavyFunctions.add(f); + return; + } + boolean isAbstract = false; if (f instanceof FuncDef) { FuncDef func = (FuncDef) f; @@ -1586,12 +1592,6 @@ private void checkUninitializedVars(FunctionLike f) { } if (isAbstract) return; - if (!isHeavy()) { - // Phase-1: collect, but do not analyze. - heavyFunctions.add(f); - return; - } - // Phase-2: actually run heavy analyses: checkReturn(f); @@ -2735,6 +2735,9 @@ public static String checkOverride(FuncLink func1, FuncLink func2, boolean allow } private void checkForDuplicateNames(WScope scope) { + if (scope instanceof WPackage) { + return; + } ImmutableMultimap links = scope.attrNameLinks(); for (String name : links.keySet()) { ImmutableCollection nameLinks = links.get(name); From daecc289e66b47bf843eb114b98fd28cad6812b1 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 03:16:15 +0100 Subject: [PATCH 5/8] more opts --- de.peeeq.wurstscript/build.gradle | 2 +- .../wurstio/CompiletimeFunctionRunner.java | 2 +- .../peeeq/wurstio/WurstCompilerJassImpl.java | 18 +- .../jassinterpreter/JassInterpreter.java | 6 +- .../providers/ForceProvider.java | 1 - .../providers/GroupProvider.java | 2 - .../languageserver/ConfigProvider.java | 2 +- .../languageserver/ModelManagerImpl.java | 5 +- .../java/de/peeeq/wurstscript/WLogger.java | 29 ++++ .../de/peeeq/wurstscript/WLoggerDefault.java | 20 +++ .../java/de/peeeq/wurstscript/WLoggerI.java | 8 + .../attributes/AttrFunctionSignature.java | 12 +- .../AttrPossibleFunctionSignatures.java | 10 +- .../attributes/names/NameResolution.java | 12 +- .../interpreter/EvaluateExpr.java | 5 - .../interpreter/ILInterpreter.java | 157 ++++++++++++------ .../interpreter/ProgramState.java | 44 +++-- .../parser/antlr/ExtendedWurstLexer.java | 2 +- .../translation/imoptimizer/ImOptimizer.java | 33 ++-- .../imtranslation/CyclicFunctionRemover.java | 2 +- .../imtranslation/EliminateClasses.java | 2 +- .../imtranslation/EliminateGenerics.java | 4 +- .../imtranslation/ExprTranslation.java | 4 +- .../translation/imtranslation/Flatten.java | 146 +++++++++++----- .../imtranslation/ImTranslator.java | 42 +++-- .../wurstscript/types/FunctionSignature.java | 4 +- 26 files changed, 387 insertions(+), 187 deletions(-) diff --git a/de.peeeq.wurstscript/build.gradle b/de.peeeq.wurstscript/build.gradle index 67f3b0f06..627b3a1d1 100644 --- a/de.peeeq.wurstscript/build.gradle +++ b/de.peeeq.wurstscript/build.gradle @@ -106,7 +106,7 @@ dependencies { implementation 'commons-lang:commons-lang:2.6' implementation 'com.github.albfernandez:juniversalchardet:2.4.0' implementation 'com.github.inwc3:jmpq3:29b55f2c32' - implementation 'com.github.inwc3:wc3libs:bab65b961b' + implementation 'com.github.inwc3:wc3libs:cc49c8e63c' implementation('com.github.wurstscript:wurstsetup:393cf5ea39') { exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit' exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit.ssh.apache' diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index 45e5a6f97..09b5e57da 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -551,7 +551,7 @@ private void executeCompiletimeFunction(ImFunction f) { if (!f.getBody().isEmpty()) { interpreter.getGlobalState().setLastStatement(f.getBody().get(0)); } - WLogger.debug("running " + functionFlag + " function " + f.getName()); + WLogger.debug(() -> "running " + functionFlag + " function " + f.getName()); interpreter.runVoidFunc(f, null); successTests.add(f); } catch (TestSuccessException e) { 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..bc5e17221 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 @@ -535,12 +535,14 @@ public JassProg transformProgToJass() { } beginPhase(13, "flatten"); - optimizer.removeGarbage(); + boolean garbageChanged = optimizer.removeGarbage(); imProg.flatten(imTranslator); // Re-run to avoid #883 - optimizer.removeGarbage(); - imProg.flatten(imTranslator); + if (garbageChanged) { + optimizer.removeGarbage(); + imProg.flatten(imTranslator); + } printDebugImProg("./test-output/im " + stage++ + "_afterremoveGarbage1.im"); timeTaker.endPhase(); @@ -561,7 +563,7 @@ public JassProg transformProgToJass() { // translate flattened intermediate lang to jass: beginPhase(14, "translate to jass"); - getImTranslator().calculateCallRelationsAndUsedVariables(); + getImTranslator().calculateCallRelationsAndReadVariables(); ImToJassTranslator translator = new ImToJassTranslator(getImProg(), getImTranslator().getCalledFunctions(), getImTranslator().getMainFunc(), getImTranslator().getConfFunc()); prog = translator.translate(); @@ -952,12 +954,14 @@ public LuaCompilationUnit transformProgToLua() { printDebugImProg("./test-output/lua/im " + stage++ + "_afterlocalopts.im"); - optimizer.removeGarbage(); + boolean garbageChanged = optimizer.removeGarbage(); imProg.flatten(imTranslator); // Re-run to avoid #883 - optimizer.removeGarbage(); - imProg.flatten(imTranslator); + if (garbageChanged) { + optimizer.removeGarbage(); + imProg.flatten(imTranslator); + } printDebugImProg("./test-output/lua/im " + stage++ + "_afterremoveGarbage1.im"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java index 96b6abef0..f0026501c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/JassInterpreter.java @@ -60,7 +60,7 @@ public static ILconst getDefaultValue(String type) { public ILconst executeFunction(String name, ILconst... arguments) { if (trace) { - WLogger.trace(name + "( " + Utils.join(arguments, ", ") + ")"); + WLogger.trace(() -> name + "( " + Utils.join(arguments, ", ") + ")"); } ExecutableJassFunction func = searchFunction(name); @@ -99,13 +99,13 @@ ILconst executeJassFunction(JassFunction func, ILconst... arguments) { this.executeStatements(localVarMap, body); } catch (ReturnException e) { if (trace) { - WLogger.trace("end function " + func.getName() + " returns " + e.getVal()); + WLogger.trace(() -> "end function " + func.getName() + " returns " + e.getVal()); } return e.getVal(); } if (trace) { - WLogger.trace("end function " + func.getName()); + WLogger.trace(() -> "end function " + func.getName()); } return null; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java index b2f53e8e6..b12f78850 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java @@ -44,7 +44,6 @@ public void DestroyForce(IlConstHandle force) { public void ForForce(IlConstHandle force, ILconstFuncRef funcRef) { // TODO take force param into account // For now this simply executes the supplied function. - WLogger.trace("for force call"); interpreter.runFuncRef(funcRef, null); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GroupProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GroupProvider.java index 48e8c6d4d..02020f527 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GroupProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/GroupProvider.java @@ -52,11 +52,9 @@ public ILconst FirstOfGroup(IlConstHandle group) { } public void ForGroup(IlConstHandle group, ILconstFuncRef funcRef) { - WLogger.trace("for group call"); LinkedHashSet groupList = (LinkedHashSet) group.getObj(); groupList.forEach((IlConstHandle u) -> { enumUnitStack.push(u); - WLogger.trace("for group call itr: " + funcRef.getFuncName()); interpreter.runFuncRef(funcRef, null); enumUnitStack.pop(); }); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ConfigProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ConfigProvider.java index b4a9ef810..73920b42d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ConfigProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ConfigProvider.java @@ -143,7 +143,7 @@ private void refreshCacheAsync() { } }) .exceptionally(e -> { - WLogger.trace("Background config refresh failed (this is normal if client is busy): " + e.getMessage()); + WLogger.trace(() -> "Background config refresh failed (this is normal if client is busy): " + e.getMessage()); return null; }); } 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 7e8455853..66f6988bc 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 @@ -371,7 +371,6 @@ private WurstCompilerJassImpl getCompiler(WurstGui gui) { } private void updateModel(CompilationUnit cu, WurstGui gui) { - WLogger.trace("update model with " + cu.getCuInfo().getFile()); parseErrors.put(wFile(cu), new ArrayList<>(gui.getErrorsAndWarnings())); WurstModel model2 = model; @@ -534,7 +533,7 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents, CompilationUnit existing = getCompilationUnit(filename); if (existing != null) { // no change - WLogger.trace("CU " + filename + " was unchanged."); + WLogger.trace(() -> "CU " + filename + " was unchanged."); return existing; } // Stale hash cache after remove/move; CU is gone, so reparse. @@ -544,7 +543,7 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents, } } - WLogger.trace("replace CU " + filename); + WLogger.trace(() -> "replace CU " + filename); WurstGui gui = new WurstGuiLogger(); WurstCompilerJassImpl c = getCompiler(gui); CompilationUnit cu = c.parse(filename.toString(), new StringReader(contents)); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLogger.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLogger.java index 564889eca..1c3d5dcca 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLogger.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLogger.java @@ -14,6 +14,7 @@ import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Iterator; +import java.util.function.Supplier; public abstract class WLogger { @@ -28,6 +29,16 @@ public static void trace(String msg) { instance.trace(msg); } + public static void trace(String format, Object... args) { + instance.trace(format, args); + } + + public static void trace(Supplier msgSupplier) { + if (instance.isTraceEnabled()) { + instance.trace(msgSupplier.get()); + } + } + public static void info(String msg) { instance.info(msg); } @@ -36,6 +47,24 @@ public static void debug(String s) { instance.debug(s); } + public static void debug(String format, Object... args) { + instance.debug(format, args); + } + + public static void debug(Supplier msgSupplier) { + if (instance.isDebugEnabled()) { + instance.debug(msgSupplier.get()); + } + } + + public static boolean isTraceEnabled() { + return instance.isTraceEnabled(); + } + + public static boolean isDebugEnabled() { + return instance.isDebugEnabled(); + } + public static void setLevel(Level level) { instance.setLevel(level); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java index 552f5f440..03a84849a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java @@ -22,11 +22,21 @@ public void trace(String msg) { logger.trace(msg); } + @Override + public void trace(String format, Object... args) { + logger.trace(format, args); + } + @Override public void debug(String s) { logger.debug(s); } + @Override + public void debug(String format, Object... args) { + logger.debug(format, args); + } + /** * (non-Javadoc) * @@ -109,4 +119,14 @@ public void setLevel(Level level) { } } + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerI.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerI.java index ef6c19da4..8e917af7c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerI.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerI.java @@ -7,8 +7,12 @@ public interface WLoggerI { void trace(String msg); + void trace(String format, Object... args); + void debug(String s); + void debug(String format, Object... args); + void info(String msg); void warning(String msg); @@ -22,4 +26,8 @@ public interface WLoggerI { void setLevel(Level level); void warning(String msg, Throwable e); + + boolean isTraceEnabled(); + + boolean isDebugEnabled(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java index 1483049a7..880fd82b7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java @@ -24,12 +24,12 @@ public static FunctionSignature calculate(StmtCall fc) { // ---- DEBUG: what did we pick, and what did we bind? ---- if (fc instanceof FunctionCall) { - WLogger.trace("[IMPLCONV] call=" + name(fc) + " args=" + at); + WLogger.trace(() -> "[IMPLCONV] call=" + name(fc) + " args=" + at); // WLogger.trace("[IMPLCONV] pickedSig=" + sig); - WLogger.trace("[IMPLCONV] mapping=" + sig.getMapping() + WLogger.trace(() -> "[IMPLCONV] mapping=" + sig.getMapping() + " unbound=" + sig.getMapping().printUnboundTypeVars()); if (fc instanceof ExprMemberMethodDot emmd) { - WLogger.trace("[IMPLCONV] receiver=" + emmd.getLeft().attrTyp() + WLogger.trace(() -> "[IMPLCONV] receiver=" + emmd.getLeft().attrTyp() + " raw=" + emmd.getLeft().attrTypRaw() + " member=" + emmd.getFuncName()); } @@ -67,7 +67,7 @@ private static FunctionSignature filterSigs( if (!isInitTrigFunc(location)) { if (location instanceof ExprMemberMethodDot) { ExprMemberMethodDot emmd = (ExprMemberMethodDot) location; - WLogger.trace("[IMPLCONV] receiver typRaw=" + emmd.getLeft().attrTypRaw() + WLogger.trace(() -> "[IMPLCONV] receiver typRaw=" + emmd.getLeft().attrTypRaw() + " typ=" + emmd.getLeft().attrTyp() + " for call ." + emmd.getFuncName()); } @@ -147,12 +147,12 @@ private static List filterByArgumentTypes( List candidates = new ArrayList<>(); for (FunctionSignature sig : sigs) { - WLogger.trace("[IMPLCONV] trySig=" + sig + " argTypes=" + argTypes); + WLogger.trace(() -> "[IMPLCONV] trySig=" + sig + " argTypes=" + argTypes); FunctionSignature matched = sig.matchAgainstArgs(argTypes, location); if (matched != null) { - WLogger.trace("[IMPLCONV] -> matched, mapping=" + matched.getMapping() + WLogger.trace(() -> "[IMPLCONV] -> matched, mapping=" + matched.getMapping() + " unbound=" + matched.getMapping().printUnboundTypeVars()); candidates.add(matched); } else { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java index 47150c140..2d454d0d1 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java @@ -140,13 +140,13 @@ public static ImmutableCollection calculate(ExprMemberMethod final ImmutableCollection raw = mm.lookupMemberFuncs(leftType, name, /*showErrors*/ false); - WLogger.trace("[IMPLCONV] lookupMemberFuncs name=" + name + WLogger.trace(() -> "[IMPLCONV] lookupMemberFuncs name=" + name + " leftType=" + leftType + " leftRaw=" + left.attrTypRaw() + " raw.size=" + raw.size()); for (FuncLink f : raw) { - WLogger.trace("[IMPLCONV] rawLink name=" + f.getName() + WLogger.trace(() -> "[IMPLCONV] rawLink name=" + f.getName() + " def=" + f.getDef() + " recv=" + f.getReceiverType() + " typeParams=" + f.getTypeParams() @@ -188,7 +188,7 @@ public static ImmutableCollection calculate(ExprMemberMethod for (FuncLink f : visible) { FunctionSignature sig = FunctionSignature.fromNameLink(f); - WLogger.trace("[IMPLCONV] fromNameLink -> sig=" + sig + if (WLogger.isTraceEnabled()) WLogger.trace("[IMPLCONV] fromNameLink -> sig=" + sig + " sig.recv=" + sig.getReceiverType() + " sig.map=" + sig.getMapping()); @@ -208,9 +208,9 @@ public static ImmutableCollection calculate(ExprMemberMethod String rs = String.valueOf(recv); boolean synthetic = rs.contains("thistype") || rs.contains("module"); // refine later if you have a proper predicate if (!synthetic) { - WLogger.trace("[IMPLCONV] drop candidate: receiver mismatch left=" + leftType + " recv=" + recv + WLogger.trace(() -> "[IMPLCONV] drop candidate: receiver mismatch left=" + leftType + " recv=" + recv + " def=" + f.getDef() + " linkBinding=" + f.getVariableBinding()); - WLogger.trace("[IMPLCONV] receiver mismatch: leftType=" + leftType + WLogger.trace(() -> "[IMPLCONV] receiver mismatch: leftType=" + leftType + " recv=" + recv + " func=" + f.getName() + " def=" + f.getDef() diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java index 21100e84d..62efcaa69 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java @@ -197,7 +197,7 @@ public static ImmutableCollection lookupMemberFuncs(Element node, Wurs @SuppressWarnings("unchecked") ImmutableCollection cached = (ImmutableCollection) GlobalCaches.lookupCache.get(key); if (cached != null) { - WLogger.trace("[LOOKUPCACHE] HIT MEMBER_FUNC node=" + System.identityHashCode(node) + WLogger.trace(() -> "[LOOKUPCACHE] HIT MEMBER_FUNC node=" + System.identityHashCode(node) + " name=" + name + " recv=" + receiverType + " recvId=" + System.identityHashCode(receiverType) @@ -207,7 +207,7 @@ public static ImmutableCollection lookupMemberFuncs(Element node, Wurs } List result = new ArrayList<>(4); - WLogger.trace("[LMF] addMemberMethods recv=" + receiverType + WLogger.trace(() -> "[LMF] addMemberMethods recv=" + receiverType + " recvId=" + System.identityHashCode(receiverType) + " name=" + name + " node=" + System.identityHashCode(node)); @@ -222,7 +222,7 @@ public static ImmutableCollection lookupMemberFuncs(Element node, Wurs } for (FuncLink f : result) { - WLogger.trace("[LMF] addMemberMethods -> " + f + WLogger.trace(() -> "[LMF] addMemberMethods -> " + f + " recv=" + f.getReceiverType() + " recvId=" + System.identityHashCode(f.getReceiverType()) + " linkVB=" + f.getVariableBinding() @@ -257,7 +257,7 @@ public static ImmutableCollection lookupMemberFuncs(Element node, Wurs if (!showErrors) { GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(node, memberFuncCacheName(name, receiverType), GlobalCaches.LookupType.MEMBER_FUNC); - WLogger.trace("[LOOKUPCACHE] PUT MEMBER_FUNC node=" + System.identityHashCode(node) + WLogger.trace(() -> "[LOOKUPCACHE] PUT MEMBER_FUNC node=" + System.identityHashCode(node) + " name=" + name + " recv=" + receiverType + " recvId=" + System.identityHashCode(receiverType) @@ -393,7 +393,7 @@ public static NameLink lookupMemberVar(Element node, WurstType receiverType, Str if (bestMatch != null) { if (!showErrors) { GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(node, memberFuncCacheName(name, receiverType), GlobalCaches.LookupType.MEMBER_VAR); - WLogger.trace("[LOOKUPCACHE] PUT MEMBER_FUNC node=" + System.identityHashCode(node) + WLogger.trace(() -> "[LOOKUPCACHE] PUT MEMBER_FUNC node=" + System.identityHashCode(node) + " name=" + name + " recv=" + receiverType + " recvId=" + System.identityHashCode(receiverType)); @@ -529,7 +529,7 @@ public static DefLink matchDefLinkReceiver(DefLink n, WurstType receiverType, El VariableBinding mapping = receiverType.matchAgainstSupertype(candRecv, node, seed, VariablePosition.RIGHT); if (mapping == null) return null; - WLogger.trace("[MATCHRECV] def=" + ((n instanceof FuncLink) ? ((FuncLink) n).getDef().getName() : n.getDef().getName()) + WLogger.trace(() -> "[MATCHRECV] def=" + ((n instanceof FuncLink) ? ((FuncLink) n).getDef().getName() : n.getDef().getName()) + " left=" + receiverType + " candRecv=" + candRecv + " linkTypeParams=" + n.getTypeParams() diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 0099e46c3..940aac896 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -132,14 +132,9 @@ public static ILconst eval(ImVarAccess e, ProgramState globalState, LocalState l if (isMagicCompiletimeConstant(var)) { return ILconstBool.instance(globalState.isCompiletime()); } - WLogger.trace("VarAccess global " + var.getName() + "@" + System.identityHashCode(var) - + " type=" + var.getType()); ILconst r = globalState.getVal(var); if (r == null) { List initExpr = globalState.getProg().getGlobalInits().get(var); - WLogger.trace(" -> was null, using globalInits key=" - + var.getName() + "@" + System.identityHashCode(var) - + " initExpr=" + (initExpr == null ? "null" : initExpr.size())); if (initExpr != null) { r = initExpr.get(0).getRight().evaluate(globalState, localState); } else { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java index b03e2735a..a8aa0c3a2 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java @@ -118,64 +118,84 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab receiverObj = (ILconstObject) args[0]; } - Map subst = new HashMap<>(); - + boolean needsTypeSubstitution = false; if (receiverObj != null) { - subst.putAll(receiverObj.getCapturedTypeSubstitutions()); // <-- implement/get this map (empty for normal objects) - } - - // A) Bind class type vars from receiver (for instance methods / funcs with this as first param) - if (receiverObj != null && !f.getParameters().isEmpty()) { - ImType p0t = f.getParameters().get(0).getType(); - if (p0t instanceof ImClassType) { - ImClassType sigThisType = (ImClassType) p0t; // may contain ImTypeVarRefs - ImClass cls = sigThisType.getClassDef(); - ImTypeVars tvars = cls.getTypeVariables(); // e.g. [T951] - ImTypeArguments concreteArgs = receiverObj.getType().getTypeArguments(); // e.g. [integer] - - int n = Math.min(tvars.size(), concreteArgs.size()); - for (int i2 = 0; i2 < n; i2++) { - subst.put(tvars.get(i2), concreteArgs.get(i2).getType()); + if (!receiverObj.getCapturedTypeSubstitutions().isEmpty()) { + needsTypeSubstitution = true; + } else if (!f.getParameters().isEmpty()) { + ImType p0t = f.getParameters().get(0).getType(); + if (p0t instanceof ImClassType) { + needsTypeSubstitution = !((ImClassType) p0t).getClassDef().getTypeVariables().isEmpty(); } } } + if (!needsTypeSubstitution && caller instanceof ImFunctionCall) { + needsTypeSubstitution = !((ImFunctionCall) caller).getTypeArguments().isEmpty(); + } + + Map normalized = Collections.emptyMap(); + if (needsTypeSubstitution) { + Map subst = new HashMap<>(); - // B) Bind type vars from explicit call type arguments. - // IMPORTANT: In IM, type args on a call often correspond to the *owning class* type vars, not f.getTypeVariables(). - if (caller instanceof ImFunctionCall) { - ImFunctionCall fc = (ImFunctionCall) caller; - ImTypeArguments targs = fc.getTypeArguments(); - - // 1) If the function itself is generic, bind those first - ImTypeVars fvars = f.getTypeVariables(); - int n1 = Math.min(fvars.size(), targs.size()); - for (int i2 = 0; i2 < n1; i2++) { - subst.put(fvars.get(i2), targs.get(i2).getType()); + if (receiverObj != null) { + subst.putAll(receiverObj.getCapturedTypeSubstitutions()); } - // 2) If the function is inside a class, also bind the class type vars using the same call type args - Element owner = f.getParent(); - while (owner != null && !(owner instanceof ImClass)) { - owner = owner.getParent(); + // A) Bind class type vars from receiver (for instance methods / funcs with this as first param) + if (receiverObj != null && !f.getParameters().isEmpty()) { + ImType p0t = f.getParameters().get(0).getType(); + if (p0t instanceof ImClassType) { + ImClassType sigThisType = (ImClassType) p0t; + ImClass cls = sigThisType.getClassDef(); + ImTypeVars tvars = cls.getTypeVariables(); + ImTypeArguments concreteArgs = receiverObj.getType().getTypeArguments(); + + int n = Math.min(tvars.size(), concreteArgs.size()); + for (int i2 = 0; i2 < n; i2++) { + subst.put(tvars.get(i2), concreteArgs.get(i2).getType()); + } + } } - if (owner instanceof ImClass) { - ImClass cls = (ImClass) owner; - ImTypeVars cvars = cls.getTypeVariables(); - int n2 = Math.min(cvars.size(), targs.size()); - for (int i2 = 0; i2 < n2; i2++) { - subst.put(cvars.get(i2), targs.get(i2).getType()); + + // B) Bind type vars from explicit call type arguments. + if (caller instanceof ImFunctionCall) { + ImFunctionCall fc = (ImFunctionCall) caller; + ImTypeArguments targs = fc.getTypeArguments(); + + // 1) If the function itself is generic, bind those first + ImTypeVars fvars = f.getTypeVariables(); + int n1 = Math.min(fvars.size(), targs.size()); + for (int i2 = 0; i2 < n1; i2++) { + subst.put(fvars.get(i2), targs.get(i2).getType()); + } + + // 2) If the function is inside a class, also bind class type vars with the same call type args + Element owner = f.getParent(); + while (owner != null && !(owner instanceof ImClass)) { + owner = owner.getParent(); + } + if (owner instanceof ImClass) { + ImClass cls = (ImClass) owner; + ImTypeVars cvars = cls.getTypeVariables(); + int n2 = Math.min(cvars.size(), targs.size()); + for (int i2 = 0; i2 < n2; i2++) { + subst.put(cvars.get(i2), targs.get(i2).getType()); + } } } - } - // C) Normalize RHS through existing frames (so nested substitutions resolve) - Map normalized = new HashMap<>(); - for (Map.Entry e : subst.entrySet()) { - ImType rhs = globalState.resolveType(e.getValue()); - if (rhs instanceof ImTypeVarRef && ((ImTypeVarRef) rhs).getTypeVariable() == e.getKey()) { - continue; // skip self-maps + // C) Normalize RHS through existing frames (so nested substitutions resolve) + if (!subst.isEmpty()) { + Map normalizedTmp = new HashMap<>(); + for (Map.Entry e : subst.entrySet()) { + ImType rhs = globalState.resolveType(e.getValue()); + if (rhs instanceof ImTypeVarRef && ((ImTypeVarRef) rhs).getTypeVariable() == e.getKey()) { + continue; // skip self-maps + } + normalizedTmp.put(e.getKey(), rhs); + } + normalized = normalizedTmp; } - normalized.put(e.getKey(), rhs); } if (caller != null) { @@ -191,9 +211,11 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab f.getBody().runStatements(globalState, localState); } catch (ReturnException e) { ILconst retVal2 = e.getVal(); - WLogger.trace("RETURN " + f.getName() - + " expected=" + f.getReturnType() - + " got=" + retVal + " (" + (retVal == null ? "null" : retVal2.getClass().getSimpleName()) + ")"); + WLogger.trace("RETURN {} expected={} got={} ({})", + f.getName(), + f.getReturnType(), + retVal, + (retVal == null ? "null" : retVal2.getClass().getSimpleName())); retVal = adjustTypeOfConstant(e.getVal(), f.getReturnType()); didReturn = true; } finally { @@ -318,10 +340,46 @@ private static LocalState runBuiltinFunction(ProgramState globalState, ImFunctio // Build error text lazily (only if we actually get exceptions) StringBuilder errors = null; + // Fast path: direct dispatch to previously resolved provider. + NativesProvider cachedProvider = globalState.getCachedNativeProvider(fname); + if (cachedProvider != null) { + try { + final LocalState localState = new LocalState(cachedProvider.invoke(fname, args)); + if (pure) { + if (args.length == 0) { + LOCAL_STATE_NOARG_CACHE.put(f, localState); + return localState; + } + if (perFn == null) { + perFn = new Object2ObjectOpenHashMap<>(16); + LOCAL_STATE_CACHE.put(f, perFn); + } + perFn.put(key, localState); + if (perFn.size() > MAX_CACHE_PER_FUNC) { + final GlobalCaches.ArgumentKey eldest = perFn.keySet().iterator().next(); + perFn.remove(eldest); + } + } + return localState; + } catch (NoSuchNativeException e) { + // Provider mapping changed or function not available in this provider instance anymore. + // Fall through to full scan and refresh cache. + } + } else if (globalState.isKnownMissingNative(fname)) { + // No provider has this function; skip repeated scans/exceptions. + globalState.compilationError("function " + fname + " cannot be used from the Wurst interpreter.\n"); + if (f.getReturnType() instanceof ImVoid) { + return EMPTY_LOCAL_STATE; + } + final ILconst returnValue = f.getReturnType().defaultValue(); + return new LocalState(returnValue); + } + for (NativesProvider natives : globalState.getNativeProviders()) { try { // Invoke native; TODO: cache method handles per name elsewhere. final LocalState localState = new LocalState(natives.invoke(fname, args)); + globalState.cacheNativeProvider(fname, natives); if (pure) { if (args.length == 0) { @@ -352,6 +410,7 @@ private static LocalState runBuiltinFunction(ProgramState globalState, ImFunctio } // If we reach here, none of the providers handled it + globalState.markMissingNative(fname); if (errors == null) errors = new StringBuilder(64); errors.insert(0, "function ").append(fname).append(" cannot be used from the Wurst interpreter.\n"); globalState.compilationError(errors.toString()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java index 8a0d45672..8252cf316 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java @@ -26,6 +26,8 @@ public class ProgramState extends State { protected WurstGui gui; private PrintStream outStream = System.err; private final List nativeProviders = Lists.newArrayList(); + private final Object2ObjectOpenHashMap nativeProviderByFunc = new Object2ObjectOpenHashMap<>(); + private final Set missingNativeFuncs = new HashSet<>(); private ImProg prog; private int objectIdCounter; private final Int2ObjectOpenHashMap indexToObject = new Int2ObjectOpenHashMap<>(); @@ -112,9 +114,9 @@ private void identifyGenericStaticGlobals() { } } - WLogger.trace("[GENSTATIC] owners detected: " + genericStaticOwner.size()); + WLogger.trace(() -> "[GENSTATIC] owners detected: " + genericStaticOwner.size()); for (var e : genericStaticOwner.entrySet()) { - WLogger.trace("[GENSTATIC] " + e.getKey().getName() + " -> " + e.getValue().getName()); + WLogger.trace(() -> "[GENSTATIC] " + e.getKey().getName() + " -> " + e.getValue().getName()); } } @@ -159,12 +161,30 @@ public void setOutStream(PrintStream os) { public void addNativeProvider(NativesProvider np) { np.setOutStream(outStream); nativeProviders.add(np); + nativeProviderByFunc.clear(); + missingNativeFuncs.clear(); } public Iterable getNativeProviders() { return nativeProviders; } + public @Nullable NativesProvider getCachedNativeProvider(String funcName) { + return nativeProviderByFunc.get(funcName); + } + + public boolean isKnownMissingNative(String funcName) { + return missingNativeFuncs.contains(funcName); + } + + public void cacheNativeProvider(String funcName, NativesProvider provider) { + nativeProviderByFunc.put(funcName, provider); + } + + public void markMissingNative(String funcName) { + missingNativeFuncs.add(funcName); + } + public ProgramState setProg(ImProg p) { prog = p; return this; @@ -178,7 +198,6 @@ public ILconstObject allocate(ImClassType clazz, Element trace) { objectIdCounter++; ILconstObject res = new ILconstObject(clazz, objectIdCounter, trace); indexToObject.put(objectIdCounter, res); - WLogger.trace("alloc objId=" + objectIdCounter + " type=" + clazz + " trace=" + trace); return res; } @@ -240,7 +259,7 @@ public void pushStackframeWithTypes(ImFunction f, @Nullable ILconstObject receiv normalized.put(e.getKey(), rhs); } - WLogger.trace("pushStackframe " + f + " with receiver " + receiver + WLogger.trace(() -> "pushStackframe " + f + " with receiver " + receiver + " and args " + Arrays.toString(args) + " and typesubst " + normalized); // new Exception().printStackTrace(System.out); @@ -284,7 +303,7 @@ public void pushStackframeWithTypes(ImFunction f, @Nullable ILconstObject receiv if (owner == null) return null; String ownerInst = ownerInstantiationKeyFromTypeSubst(owner); - WLogger.trace("[GENSTATIC] key for " + v.getName() + WLogger.trace(() -> "[GENSTATIC] key for " + v.getName() + " owner=" + owner.getName() + " ownerInstFromSubst=" + ownerInst + " receiver=" + (stackFrames.peek() == null ? null : stackFrames.peek().receiver)); @@ -294,7 +313,7 @@ public void pushStackframeWithTypes(ImFunction f, @Nullable ILconstObject receiv // fallback: previous receiver-based logic ImClassType inst = currentReceiverInstantiationFor(owner); - WLogger.trace("[GENSTATIC] key for " + v.getName() + WLogger.trace(() -> "[GENSTATIC] key for " + v.getName() + " owner=" + owner.getName() + " ownerInstFromSubst=" + ownerInst + " receiver=" + (stackFrames.peek() == null ? null : stackFrames.peek().receiver)); @@ -462,7 +481,7 @@ public ImType case_ImArrayTypeMulti(ImArrayTypeMulti t) { } public void pushStackframe(ImCompiletimeExpr f, WPos trace) { - WLogger.trace("pushStackframe compiletime expr " + f); + WLogger.trace(() -> "pushStackframe compiletime expr " + f); stackFrames.push(new ILStackFrame(f, trace)); de.peeeq.wurstscript.jassIm.Element stmt = this.lastStatement; if (stmt == null) { @@ -473,7 +492,7 @@ public void pushStackframe(ImCompiletimeExpr f, WPos trace) { public void popStackframe() { // new Exception().printStackTrace(System.out); - WLogger.trace("popStackframe " + (stackFrames.isEmpty() ? "empty" : stackFrames.peek().f)); + WLogger.trace(() -> "popStackframe " + (stackFrames.isEmpty() ? "empty" : stackFrames.peek().f)); if (!stackFrames.isEmpty()) { stackFrames.pop(); } @@ -601,7 +620,7 @@ private static String vid(ImVar v) { public void setVal(ImVar v, ILconst val) { String key = genericStaticKey(v); if (key != null) { - WLogger.trace("[GENSTATIC] set " + key + " = " + val); + WLogger.trace(() -> "[GENSTATIC] set " + key + " = " + val); genericStaticScalarVals.put(key, val); return; } @@ -614,7 +633,7 @@ public void setVal(ImVar v, ILconst val) { if (key != null) { ILconst existing = genericStaticScalarVals.get(key); if (existing != null) { - WLogger.trace("[GENSTATIC] get " + key + " -> (cached) " + existing); + WLogger.trace(() -> "[GENSTATIC] get " + key + " -> (cached) " + existing); return existing; } @@ -623,12 +642,12 @@ public void setVal(ImVar v, ILconst val) { if (inits != null && !inits.isEmpty()) { ILconst initVal = inits.get(inits.size() - 1).getRight().evaluate(this, EMPTY_LOCAL_STATE); genericStaticScalarVals.put(key, initVal); - WLogger.trace("[GENSTATIC] get " + key + " -> (init) " + initVal); + WLogger.trace(() -> "[GENSTATIC] get " + key + " -> (init) " + initVal); return initVal; } // fallback: default semantics (if your interpreter expects “unset = null/0”) - WLogger.trace("[GENSTATIC] get " + key + " -> (unset) null"); + WLogger.trace(() -> "[GENSTATIC] get " + key + " -> (unset) null"); return null; } @@ -699,4 +718,3 @@ public Map snapshotResolvedTypeSubstitutions() { } } - diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/ExtendedWurstLexer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/ExtendedWurstLexer.java index 55e89d0d2..a7fa9f383 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/ExtendedWurstLexer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/ExtendedWurstLexer.java @@ -101,7 +101,7 @@ public Token nextToken() { Token t = nextTokenIntern(); lastToken = t; - if (debug) WLogger.trace(" new token: " + t); + if (debug) WLogger.trace(() -> " new token: " + t); return t; } 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..b47629698 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 @@ -75,8 +75,10 @@ public void localOptimizations() { totalCount.put(pass.getName(), totalCount.getOrDefault(pass.getName(), 0) + count); } - removeGarbage(); - trans.getImProg().flatten(trans); + if (optCount > 0) { + removeGarbage(); + trans.getImProg().flatten(trans); + } finalItr = i; WLogger.info("=== Optimization pass: " + i + " opts: " + optCount + " ==="); @@ -91,23 +93,26 @@ public void doNullsetting() { trans.assertProperties(); } - public void removeGarbage() { + public boolean removeGarbage() { boolean changes = true; + boolean anyChanges = false; int iterations = 0; while (changes && iterations++ < 10) { ImProg prog = trans.imProg(); - trans.calculateCallRelationsAndUsedVariables(); + trans.calculateCallRelationsAndReadVariables(); + final Set readVars = trans.getReadVariables(); + final Set usedFuncs = trans.getUsedFunctions(); // keep only used variables int globalsBefore = prog.getGlobals().size(); - changes = prog.getGlobals().retainAll(trans.getReadVariables()); + changes = prog.getGlobals().retainAll(readVars); int globalsAfter = prog.getGlobals().size(); int globalsRemoved = globalsBefore - globalsAfter; totalGlobalsRemoved += globalsRemoved; // keep only functions reachable from main and config int functionsBefore = prog.getFunctions().size(); - changes |= prog.getFunctions().retainAll(trans.getUsedFunctions()); + changes |= prog.getFunctions().retainAll(usedFuncs); int functionsAfter = prog.getFunctions().size(); int functionsRemoved = functionsBefore - functionsAfter; totalFunctionsRemoved += functionsRemoved; @@ -116,13 +121,13 @@ public void removeGarbage() { Set allFunctions = new HashSet<>(prog.getFunctions()); for (ImClass c : prog.getClasses()) { int classFunctionsBefore = c.getFunctions().size(); - changes |= c.getFunctions().retainAll(trans.getUsedFunctions()); + changes |= c.getFunctions().retainAll(usedFuncs); int classFunctionsAfter = c.getFunctions().size(); totalFunctionsRemoved += classFunctionsBefore - classFunctionsAfter; allFunctions.addAll(c.getFunctions()); int classFieldsBefore = c.getFields().size(); - changes |= c.getFields().retainAll(trans.getReadVariables()); + changes |= c.getFields().retainAll(readVars); int classFieldsAfter = c.getFields().size(); totalGlobalsRemoved += classFieldsBefore - classFieldsAfter; } @@ -136,12 +141,12 @@ public void visit(ImSet e) { super.visit(e); if (e.getLeft() instanceof ImVarAccess) { ImVarAccess va = (ImVarAccess) e.getLeft(); - if (!trans.getReadVariables().contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { + if (!readVars.contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { replacements.add(Pair.create(e, Collections.singletonList(e.getRight()))); } } else if (e.getLeft() instanceof ImVarArrayAccess) { ImVarArrayAccess va = (ImVarArrayAccess) e.getLeft(); - if (!trans.getReadVariables().contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { + if (!readVars.contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { // IMPORTANT: removeAll() clears parent references List exprs = va.getIndexes().removeAll(); exprs.add(e.getRight()); @@ -149,12 +154,12 @@ public void visit(ImSet e) { } } 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())) { + if(var != null && !readVars.contains(var) && !TRVEHelper.protectedVariables.contains(var.getName())) { replacements.add(Pair.create(e, Collections.singletonList(e.getRight()))); } } else if(e.getLeft() instanceof ImMemberAccess) { ImMemberAccess va = ((ImMemberAccess) e.getLeft()); - if (!trans.getReadVariables().contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { + if (!readVars.contains(va.getVar()) && !TRVEHelper.protectedVariables.contains(va.getVar().getName())) { replacements.add(Pair.create(e, Collections.singletonList(e.getRight()))); } } @@ -183,8 +188,10 @@ public void visit(ImSet e) { } // keep only read local variables - changes |= f.getLocals().retainAll(trans.getReadVariables()); + changes |= f.getLocals().retainAll(readVars); } + anyChanges |= changes; } + return anyChanges; } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java index b22d1f8bc..ce98973fc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java @@ -30,7 +30,7 @@ public CyclicFunctionRemover(ImTranslator tr, ImProg prog, TimeTaker timeTaker) } public void work() { - tr.calculateCallRelationsAndUsedVariables(); + tr.calculateCallRelationsAndReadVariables(); List> components = timeTaker.measure("finding cycles", () -> graph.findStronglyConnectedComponents(prog.getFunctions()) ); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateClasses.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateClasses.java index bdb2e797b..455afbf3d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateClasses.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateClasses.java @@ -248,7 +248,7 @@ public void createDispatchFunc(ImClass c, ImMethod m) { prog.getFunctions().add(df); dispatchFuncs.put(m, df); - WLogger.trace("[DISPATCH] register method=" + m.getName() + "@" + System.identityHashCode(m) + WLogger.trace(() -> "[DISPATCH] register method=" + m.getName() + "@" + System.identityHashCode(m) + " impl=" + m.getImplementation().getName() + "@" + System.identityHashCode(m.getImplementation()) + " sig=" + m.toString() + " df=" + df.getName()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index f82f119b0..da6b1615d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -437,7 +437,7 @@ private void identifyGenericGlobals() { // This global belongs to a relevant (new-generic or inheriting) class: globalToClass.put(global, owner); - WLogger.trace("Identified generic static-field global: " + global.getName() + WLogger.trace(() -> "Identified generic static-field global: " + global.getName() + " of type " + global.getType() + " belonging to class " + owner.getName()); } @@ -1753,7 +1753,7 @@ private static String shortTypeArgs(ImTypeArguments tas) { } private void dbg(String msg) { - WLogger.trace("[ELIMGEN] " + msg); + WLogger.trace(() -> "[ELIMGEN] " + msg); } } 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 0b65aeee0..5d4715798 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 @@ -510,12 +510,12 @@ && isCalledOnDynamicRef(e) // logging if (calledFunc instanceof FuncDef) { FuncDef fd = (FuncDef) calledFunc; - WLogger.trace("[DISPATCH] call " + fd.getName() + if (WLogger.isTraceEnabled()) WLogger.trace("[DISPATCH] call " + fd.getName() + " isStatic=" + fd.attrIsStatic() + " dynCtx=" + isCalledOnDynamicRef(e) + " -> dynamicDispatch=" + dynamicDispatch); } else { - WLogger.trace("[DISPATCH] call " + calledFunc.getName() + if (WLogger.isTraceEnabled()) WLogger.trace("[DISPATCH] call " + calledFunc.getName() + " (non-FuncDef)" + " dynCtx=" + isCalledOnDynamicRef(e) + " -> dynamicDispatch=" + dynamicDispatch); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java index 5cbe146e2..df7f8a41d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java @@ -1,8 +1,6 @@ package de.peeeq.wurstscript.translation.imtranslation; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtranslation.purity.Pure; @@ -12,7 +10,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.ForkJoinPool; import static de.peeeq.wurstscript.jassIm.JassIm.*; @@ -196,7 +193,9 @@ public ImExpr expr(int i) { public static class MultiResultL extends MultiResult { public MultiResultL(List stmts, List exprs) { - super(stmts, ImmutableList.copyOf(exprs)); + // Reuse list directly to avoid extra copy on this hot path. + //noinspection unchecked,rawtypes + super(stmts, (List) exprs); } public ImLExpr expr(int i) { @@ -271,7 +270,8 @@ private static void flattenStatementsInto(List result, ImStmts statement public static Result flatten(ImExitwhen s, ImTranslator t, ImFunction f) { Result cond = s.getCondition().flatten(t, f); - List stmts = Lists.newArrayList(cond.stmts); + List stmts = new ArrayList<>(cond.stmts.size() + 1); + stmts.addAll(cond.stmts); stmts.add(ImExitwhen(s.getTrace(), cond.expr)); return new Result(stmts); } @@ -279,7 +279,8 @@ public static Result flatten(ImExitwhen s, ImTranslator t, ImFunction f) { public static Result flatten(ImIf s, ImTranslator t, ImFunction f) { Result cond = s.getCondition().flatten(t, f); - List stmts = Lists.newArrayList(cond.stmts); + List stmts = new ArrayList<>(cond.stmts.size() + 1); + stmts.addAll(cond.stmts); stmts.add( JassIm.ImIf(s.getTrace(), cond.expr, flattenStatements(s.getThenBlock(), t, f), @@ -297,7 +298,8 @@ public static Result flatten(ImReturn s, ImTranslator t, ImFunction f) { if (s.getReturnValue() instanceof ImExpr) { ImExpr ret = (ImExpr) s.getReturnValue(); Result result = ret.flatten(t, f); - List stmts = Lists.newArrayList(result.stmts); + List stmts = new ArrayList<>(result.stmts.size() + 1); + stmts.addAll(result.stmts); stmts.add(ImReturn(s.getTrace(), result.expr)); return new Result(stmts); } else { @@ -310,7 +312,8 @@ public static Result flatten(ImReturn s, ImTranslator t, ImFunction f) { public static Result flatten(ImSet s, ImTranslator t, ImFunction f) { Result l = s.getLeft().flatten(t, f); Result r = s.getRight().flatten(t, f); - List stmts = Lists.newArrayList(l.stmts); + List stmts = new ArrayList<>(l.stmts.size() + r.stmts.size() + 1); + stmts.addAll(l.stmts); stmts.addAll(r.stmts); stmts.add(JassIm.ImSet(s.getTrace(), (ImLExpr) l.expr, r.expr)); return new Result(stmts); @@ -340,7 +343,8 @@ public static Result flatten(ImOperatorCall e, ImTranslator t, ImFunction f) { if (right.stmts.isEmpty()) { return new Result(left.stmts, JassIm.ImOperatorCall(WurstOperator.AND, ImExprs(left.expr, right.expr))); } else { - ArrayList stmts = Lists.newArrayList(left.stmts); + ArrayList stmts = new ArrayList<>(left.stmts.size() + 1); + stmts.addAll(left.stmts); ImVar tempVar = JassIm.ImVar(e.attrTrace(), WurstTypeBool.instance().imTranslateType(t), getAndLeftVarName(), false); f.getLocals().add(tempVar); ImStmts thenBlock = JassIm.ImStmts(); @@ -360,7 +364,8 @@ public static Result flatten(ImOperatorCall e, ImTranslator t, ImFunction f) { if (right.stmts.isEmpty()) { return new Result(left.stmts, JassIm.ImOperatorCall(WurstOperator.OR, ImExprs(left.expr, right.expr))); } else { - ArrayList stmts = Lists.newArrayList(left.stmts); + ArrayList stmts = new ArrayList<>(left.stmts.size() + 1); + stmts.addAll(left.stmts); ImVar tempVar = JassIm.ImVar(trace, WurstTypeBool.instance().imTranslateType(t), getAndLeftVarName(), false); f.getLocals().add(tempVar); // if left is true then result is true @@ -387,7 +392,7 @@ public static Result flatten(ImConst e, ImTranslator t, ImFunction f) { public static Result flatten(ImStatementExpr e, ImTranslator t, ImFunction f) { - List stmts = Lists.newArrayList(); + List stmts = new ArrayList<>(); flattenStatementsInto(stmts, e.getStatements(), t, f); Result r = e.getExpr().flatten(t, f); stmts.addAll(r.stmts); @@ -395,7 +400,7 @@ public static Result flatten(ImStatementExpr e, ImTranslator t, ImFunction f) { } public static ResultL flattenL(ImStatementExpr e, ImTranslator t, ImFunction f) { - List stmts = Lists.newArrayList(); + List stmts = new ArrayList<>(); flattenStatementsInto(stmts, e.getStatements(), t, f); ResultL r = ((ImLExpr) e.getExpr()).flattenL(t, f); stmts.addAll(r.stmts); @@ -463,20 +468,40 @@ public static void flattenFunc(ImFunction f, ImTranslator translator) { } public static void flattenProg(ImProg imProg, ImTranslator translator) { - // Collect all functions - List allFunctions = new ArrayList<>(); - allFunctions.addAll(imProg.getFunctions()); - for (ImClass c : imProg.getClasses()) { - allFunctions.addAll(c.getFunctions()); - } - // Choose execution strategy based on flags and size - if (USE_PARALLEL_EXECUTION && allFunctions.size() >= PARALLEL_THRESHOLD) { - // Use parallel stream directly - no wrapper needed - allFunctions.parallelStream().forEach(f -> f.flatten(translator)); + if (USE_PARALLEL_EXECUTION) { + int total = imProg.getFunctions().size(); + for (ImClass c : imProg.getClasses()) { + total += c.getFunctions().size(); + } + if (total >= PARALLEL_THRESHOLD) { + // Collect once for parallel traversal. + List allFunctions = new ArrayList<>(total); + allFunctions.addAll(imProg.getFunctions()); + for (ImClass c : imProg.getClasses()) { + allFunctions.addAll(c.getFunctions()); + } + allFunctions.parallelStream().forEach(f -> f.flatten(translator)); + } else { + for (ImFunction f : imProg.getFunctions()) { + f.flatten(translator); + } + for (ImClass c : imProg.getClasses()) { + for (ImFunction f : c.getFunctions()) { + f.flatten(translator); + } + } + } } else { - // Sequential processing for small programs or when parallel is disabled - allFunctions.forEach(f -> f.flatten(translator)); + // Sequential processing avoids intermediate list/lambda overhead. + for (ImFunction f : imProg.getFunctions()) { + f.flatten(translator); + } + for (ImClass c : imProg.getClasses()) { + for (ImFunction f : c.getFunctions()) { + f.flatten(translator); + } + } } translator.assertProperties(AssertProperty.FLAT); @@ -493,25 +518,28 @@ private static MultiResult flattenExprs(ImTranslator t, ImFunction f, ImExpr... } private static MultiResult flattenExprs(ImTranslator t, ImFunction f, List exprs) { - // TODO optimize this function to use less temporary variables - List stmts = Lists.newArrayList(); - List newExprs = Lists.newArrayList(); - List results = Lists.newArrayList(); + int n = exprs.size(); + if (n == 0) { + return new MultiResult(Collections.emptyList(), Collections.emptyList()); + } + List stmts = new ArrayList<>(n * 2); + List newExprs = new ArrayList<>(n); + Result[] results = new Result[n]; int withStmts = -1; - for (int i = 0; i < exprs.size(); i++) { - Result r = exprs.get(i).flatten(t, f); - results.add(r); + for (int i = 0; i < n; i++) { + Result r = flattenExprFast(exprs.get(i), t, f); + results[i] = r; if (!r.stmts.isEmpty()) { withStmts = i; } } - for (int i = 0; i < exprs.size(); i++) { + for (int i = 0; i < n; i++) { ImExpr e = exprs.get(i); - Result r = results.get(i); + Result r = results[i]; stmts.addAll(r.stmts); - if (r.expr.attrPurity() instanceof Pure - || i >= withStmts) { + // Check index first to avoid expensive purity attribute lookups on tail args. + if (i >= withStmts || r.expr.attrPurity() instanceof Pure) { newExprs.add(r.expr); } else { ImVar tempVar = JassIm.ImVar(e.attrTrace(), r.expr.attrTyp(), getTempVarName(), false); @@ -524,25 +552,28 @@ private static MultiResult flattenExprs(ImTranslator t, ImFunction f, List exprs) { - // TODO optimize this function to use less temporary variables - List stmts = Lists.newArrayList(); - List newExprs = Lists.newArrayList(); - List results = Lists.newArrayList(); + int n = exprs.size(); + if (n == 0) { + return new MultiResultL(Collections.emptyList(), Collections.emptyList()); + } + List stmts = new ArrayList<>(n * 2); + List newExprs = new ArrayList<>(n); + ResultL[] results = new ResultL[n]; int withStmts = -1; - for (int i = 0; i < exprs.size(); i++) { - ResultL r = exprs.get(i).flattenL(t, f); - results.add(r); + for (int i = 0; i < n; i++) { + ResultL r = flattenExprLFast(exprs.get(i), t, f); + results[i] = r; if (!r.stmts.isEmpty()) { withStmts = i; } } - for (int i = 0; i < exprs.size(); i++) { + for (int i = 0; i < n; i++) { ImExpr e = exprs.get(i); - ResultL r = results.get(i); + ResultL r = results[i]; stmts.addAll(r.stmts); - if (r.expr.attrPurity() instanceof Pure - || i >= withStmts) { + // Check index first to avoid expensive purity attribute lookups on tail args. + if (i >= withStmts || r.expr.attrPurity() instanceof Pure) { newExprs.add(r.getExpr()); } else { ImVar tempVar = JassIm.ImVar(e.attrTrace(), r.expr.attrTyp(), getTempVarName(), false); @@ -554,6 +585,29 @@ private static MultiResultL flattenExprsL(ImTranslator t, ImFunction f, List getCalledFunctions() { if (callRelations == null) { - calculateCallRelationsAndUsedVariables(); + calculateCallRelationsAndReadVariables(); } return callRelations; } @@ -1066,6 +1066,14 @@ public Multimap getCalledFunctions() { public void calculateCallRelationsAndUsedVariables() { + calculateCallRelationsAndVariables(true); + } + + public void calculateCallRelationsAndReadVariables() { + calculateCallRelationsAndVariables(false); + } + + private void calculateCallRelationsAndVariables(boolean includeUsedVariables) { // estimate sizes to reduce rehashing final int funcEstimate = Math.max(16, imProg.getFunctions().size()); final int varEstimate = Math.max(32, imProg.getGlobals().size()); @@ -1073,14 +1081,14 @@ public void calculateCallRelationsAndUsedVariables() { callRelations = com.google.common.collect.LinkedHashMultimap.create(); // keep Guava type externally usedFunctions = new ReferenceOpenHashSet<>(funcEstimate); - usedVariables = new ObjectOpenHashSet<>(varEstimate); + usedVariables = includeUsedVariables ? new ObjectOpenHashSet<>(varEstimate) : null; readVariables = new ObjectOpenHashSet<>(varEstimate); final ImFunction main = getMainFunc(); - if (main != null) calculateCallRelations(main); + if (main != null) calculateCallRelations(main, includeUsedVariables); final ImFunction conf = getConfFunc(); - if (conf != null && conf != main) calculateCallRelations(conf); + if (conf != null && conf != main) calculateCallRelations(conf, includeUsedVariables); // mark protected globals as read // TRVEHelper.protectedVariables is presumably a HashSet (O(1) contains) @@ -1091,7 +1099,7 @@ public void calculateCallRelationsAndUsedVariables() { } } - private void calculateCallRelations(ImFunction rootFunction) { + private void calculateCallRelations(ImFunction rootFunction, boolean includeUsedVariables) { // nothing to do if (rootFunction == null) return; @@ -1110,7 +1118,9 @@ private void calculateCallRelations(ImFunction rootFunction) { } // Only computed once per function thanks to usedFunctions.add() gate - usedVariables.addAll(f.calcUsedVariables()); + if (includeUsedVariables) { + usedVariables.addAll(f.calcUsedVariables()); + } readVariables.addAll(f.calcReadVariables()); final Set called = f.calcUsedFunctions(); @@ -1673,14 +1683,14 @@ public Set getUsedVariables() { public Set getReadVariables() { if (readVariables == null) { - calculateCallRelationsAndUsedVariables(); + calculateCallRelationsAndReadVariables(); } return readVariables; } public Set getUsedFunctions() { if (usedFunctions == null) { - calculateCallRelationsAndUsedVariables(); + calculateCallRelationsAndReadVariables(); } return usedFunctions; } @@ -1704,19 +1714,19 @@ private static boolean isIteratorLike(ClassOrInterface s) { private void addCapturedTypeVarsFromOwningGeneric(ImTypeVars typeVariables, ClassOrInterface s) { if (isIteratorLike(s)) { - WLogger.trace("[GENCAP] addCaptured enter: " + s.getClass().getSimpleName() + WLogger.trace(() -> "[GENCAP] addCaptured enter: " + s.getClass().getSimpleName() + " name=" + ((NamedScope) s).getName() + " parent=" + (s.getParent() == null ? "null" : s.getParent().getClass().getSimpleName())); } if (!(s instanceof ClassDef)) { - if (isIteratorLike(s)) WLogger.trace("[GENCAP] not a ClassDef -> skip"); + if (isIteratorLike(s)) WLogger.trace(() -> "[GENCAP] not a ClassDef -> skip"); return; } ClassDef cd = (ClassDef) s; boolean isStatic = cd.attrIsStatic(); - if (isIteratorLike(s)) WLogger.trace("[GENCAP] isStatic=" + isStatic); + if (isIteratorLike(s)) WLogger.trace(() -> "[GENCAP] isStatic=" + isStatic); if (!isStatic) return; de.peeeq.wurstscript.ast.Element parent = cd.getParent(); @@ -1739,11 +1749,11 @@ private void addCapturedTypeVarsFromOwningGeneric(ImTypeVars typeVariables, Clas owner = o; ownerInfo = "outerInterface=" + o.getName(); } else { - if (isIteratorLike(s)) WLogger.trace("[GENCAP] parent2 not ModuleInstanciation/ClassDef/InterfaceDef -> skip"); + if (isIteratorLike(s)) WLogger.trace(() -> "[GENCAP] parent2 not ModuleInstanciation/ClassDef/InterfaceDef -> skip"); return; } - if (isIteratorLike(s)) WLogger.trace("[GENCAP] " + ownerInfo); + if (isIteratorLike(s) && WLogger.isTraceEnabled()) WLogger.trace("[GENCAP] " + ownerInfo); if (owner == null) return; if (owner == cd) return; @@ -1778,13 +1788,13 @@ private void addCapturedTypeVarsFromOwningGeneric(ImTypeVars typeVariables, Clas override.put(tp, captured); - if (isIteratorLike(s)) WLogger.trace("[GENCAP] created captured owner tvar: " + captured.getName()); + if (isIteratorLike(s) && WLogger.isTraceEnabled()) WLogger.trace("[GENCAP] created captured owner tvar: " + captured.getName()); } // Add to inner class' ImTypeVars if not already present by name if (!hasTypeVarNamed(typeVariables, captured.getName())) { typeVariables.add(captured); - if (isIteratorLike(s)) WLogger.trace("[GENCAP] captured owner tvar added: " + captured.getName()); + if (isIteratorLike(s) && WLogger.isTraceEnabled()) WLogger.trace("[GENCAP] captured owner tvar added: " + captured.getName()); } } } @@ -1837,7 +1847,7 @@ public ImMethod getMethodFor(FuncDef f) { // IMPORTANT: method name must match implementation function name, // otherwise EliminateClasses dispatch lookup can fail. String methodName = imFunc.getName(); - WLogger.trace("[GENCAP] getMethodFor " + elementNameWithPath(f) + " -> methodName=" + methodName); + WLogger.trace(() -> "[GENCAP] getMethodFor " + elementNameWithPath(f) + " -> methodName=" + methodName); m = JassIm.ImMethod(f, selfType(f), methodName, imFunc, Lists.newArrayList(), false); methodForFuncDef.put(f, m); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java index f05ea4dde..62677a02a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/FunctionSignature.java @@ -220,7 +220,7 @@ public WurstType getVarargType() { } public @Nullable FunctionSignature matchAgainstArgs(List argTypes, Element location) { - WLogger.trace("[IMPLCONV] matchAgainstArgs sigId=" + System.identityHashCode(this) + WLogger.trace(() -> "[IMPLCONV] matchAgainstArgs sigId=" + System.identityHashCode(this) + " vbId=" + System.identityHashCode(this.mapping) + " name=" + name + " recv=" + receiverType @@ -237,7 +237,7 @@ public WurstType getVarargType() { mapping = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT); VariableBinding before = mapping; VariableBinding after = at.matchAgainstSupertype(pt, location, mapping, VariablePosition.RIGHT); - WLogger.trace("[IMPLCONV] vb " + System.identityHashCode(before) + WLogger.trace(() -> "[IMPLCONV] vb " + System.identityHashCode(before) + " -> " + (after == null ? "null" : System.identityHashCode(after)) + " sameObj=" + (before == after) + " pt=" + pt + " at=" + at); From 635fbe685091cf45f50b40f7354dcb7917d3ecca Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 11:54:33 +0100 Subject: [PATCH 6/8] correctness fixes --- .../attributes/names/NameResolution.java | 60 ------------------- .../imtranslation/CyclicFunctionRemover.java | 5 ++ 2 files changed, 5 insertions(+), 60 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java index 62efcaa69..810723097 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java @@ -12,9 +12,6 @@ import java.util.*; public class NameResolution { - private static final String PACKAGE_NAME_LOOKUP_PREFIX = "__pkg_name__"; - private static final String PACKAGE_TYPE_LOOKUP_PREFIX = "__pkg_type__"; - private static String memberFuncCacheName(String name, WurstType receiverType) { return name + "@" @@ -24,70 +21,13 @@ private static String memberFuncCacheName(String name, WurstType receiverType) { } private static ImmutableCollection scopeNameLinks(WScope scope, String name) { - if (scope instanceof WPackage) { - return packageNameLinks((WPackage) scope, name); - } return scope.attrNameLinks().get(name); } private static ImmutableCollection scopeTypeLinks(WScope scope, String name) { - if (scope instanceof WPackage) { - return packageTypeLinks((WPackage) scope, name); - } return scope.attrTypeNameLinks().get(name); } - private static ImmutableCollection packageNameLinks(WPackage p, String name) { - GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(p, PACKAGE_NAME_LOOKUP_PREFIX + name, GlobalCaches.LookupType.PACKAGE); - @SuppressWarnings("unchecked") - ImmutableCollection cached = (ImmutableCollection) GlobalCaches.lookupCache.get(key); - if (cached != null) { - return cached; - } - - LinkedHashSet result = new LinkedHashSet<>(); - boolean repl = p.getName().equals("WurstREPL"); - for (WImport imp : p.getImports()) { - if (imp.getPackagename().equals("NoWurst")) { - continue; - } - WPackage importedPackage = imp.attrImportedPackage(); - if (importedPackage == null) { - continue; - } - if (repl) { - result.addAll(importedPackage.getElements().attrNameLinks().get(name)); - result.addAll(importedPackage.attrNameLinks().get(name)); - } else { - result.addAll(importedPackage.attrExportedNameLinks().get(name)); - } - } - ImmutableCollection links = ImmutableList.copyOf(result); - GlobalCaches.lookupCache.put(key, links); - return links; - } - - private static ImmutableCollection packageTypeLinks(WPackage p, String name) { - GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(p, PACKAGE_TYPE_LOOKUP_PREFIX + name, GlobalCaches.LookupType.PACKAGE); - @SuppressWarnings("unchecked") - ImmutableCollection cached = (ImmutableCollection) GlobalCaches.lookupCache.get(key); - if (cached != null) { - return cached; - } - - LinkedHashSet result = new LinkedHashSet<>(); - for (WImport imp : p.getImports()) { - WPackage importedPackage = imp.attrImportedPackage(); - if (importedPackage == null) { - continue; - } - result.addAll(importedPackage.attrExportedTypeNameLinks().get(name)); - } - ImmutableCollection links = ImmutableList.copyOf(result); - GlobalCaches.lookupCache.put(key, links); - return links; - } - public static ImmutableCollection lookupFuncsNoConfig(Element node, String name, boolean showErrors) { if (!showErrors) { GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(node, name, GlobalCaches.LookupType.FUNC); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java index ce98973fc..63bb57491 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java @@ -98,8 +98,13 @@ private void removeCycle(List funcs, Set funcSet) { // Rewrite only affected roots: // - merged cycle body (contains moved bodies from all old funcs) // - callers that directly call any removed function + // - global inits / other program-level roots that may contain ImFuncRef Set rewriteRoots = new LinkedHashSet<>(); rewriteRoots.add(newFunc.getBody()); + rewriteRoots.add(prog); + for (List initStmts : prog.getGlobalInits().values()) { + rewriteRoots.addAll(initStmts); + } for (ImFunction caller : new ArrayList<>(tr.getCalledFunctions().keySet())) { Collection called = tr.getCalledFunctions().get(caller); for (ImFunction removed : funcSet) { From a0cfe10b538fd9ca8334e2400b8fb8f6fedcebbe Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 13:22:45 +0100 Subject: [PATCH 7/8] less log spam --- .../main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java | 2 +- .../de/peeeq/wurstio/languageserver/ModelManagerImpl.java | 2 +- .../de/peeeq/wurstio/languageserver/requests/MapRequest.java | 2 +- de.peeeq.wurstscript/src/main/resources/logback.xml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java index 09b5e57da..4a34b90a0 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompiletimeFunctionRunner.java @@ -551,7 +551,7 @@ private void executeCompiletimeFunction(ImFunction f) { if (!f.getBody().isEmpty()) { interpreter.getGlobalState().setLastStatement(f.getBody().get(0)); } - WLogger.debug(() -> "running " + functionFlag + " function " + f.getName()); + WLogger.trace(() -> "running " + functionFlag + " function " + f.getName()); interpreter.runVoidFunc(f, null); successTests.add(f); } catch (TestSuccessException e) { 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 66f6988bc..948b1dbea 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 @@ -170,7 +170,7 @@ private File[] getFiles(File dir) { } private void processWurstFile(WFile f) { - WLogger.info("processing file " + f); + WLogger.debug("processing file " + f); replaceCompilationUnit(f); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java index 3811df3c1..f4bbf8c47 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java @@ -340,7 +340,7 @@ protected File compileScript(WurstGui gui, ModelManager modelManager, List - + @@ -53,4 +53,4 @@ - \ No newline at end of file + From b3c4a0787732643e85376ffc7798ce2fce6dcbdd Mon Sep 17 00:00:00 2001 From: Frotty Date: Sat, 28 Feb 2026 14:02:21 +0100 Subject: [PATCH 8/8] one cached map for each input map --- .../languageserver/requests/MapRequest.java | 21 +++++++++++++++++-- .../imtranslation/CyclicFunctionRemover.java | 13 ++++++------ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java index f4bbf8c47..4fe1544ff 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java @@ -69,7 +69,7 @@ public abstract class MapRequest extends UserRequest { private static Long lastMapModified = 0L; private static String lastMapPath = ""; - protected String cachedMapFileName = "cached_map.w3x"; + protected String cachedMapFileName = ""; public static final String BUILD_CONFIGURED_SCRIPT_NAME = "01_war3mapj_with_config.j.txt"; public static final String BUILD_COMPILED_JASS_NAME = "02_compiled.j.txt"; @@ -411,7 +411,24 @@ protected File getCachedMapFile() { if (!cacheDir.exists()) { UtilsIO.mkdirs(cacheDir); } - return new File(cacheDir, cachedMapFileName); + return new File(cacheDir, resolveCachedMapFileName()); + } + + private String resolveCachedMapFileName() { + if (!cachedMapFileName.isEmpty()) { + return cachedMapFileName; + } + if (!map.isPresent()) { + return "cached_map.w3x"; + } + File inputMap = map.get(); + String inputName = inputMap.getName(); + int dot = inputName.lastIndexOf('.'); + String baseName = dot > 0 ? inputName.substring(0, dot) : inputName; + // Keep only filesystem-safe characters and avoid collisions for same basename from different folders. + String safeBase = baseName.replaceAll("[^a-zA-Z0-9._-]", "_"); + String pathHash = Integer.toUnsignedString(inputMap.getAbsolutePath().hashCode(), 36); + return safeBase + "_" + pathHash + "_cached.w3x"; } protected File ensureWritableTargetFile(File targetFile, String dialogTitle, String lockMessage, diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java index 63bb57491..a205bc694 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/CyclicFunctionRemover.java @@ -41,7 +41,8 @@ private void removeCycles(List> components) { for (List component : components) { if (component.size() > 1) { // keep list for order; set for O(1) membership - Set funcSet = new HashSet<>(component); + Set funcSet = Collections.newSetFromMap(new IdentityHashMap<>()); + funcSet.addAll(component); removeCycle(component, funcSet); } } @@ -49,7 +50,7 @@ private void removeCycles(List> components) { private void removeCycle(List funcs, Set funcSet) { List newParameters = Lists.newArrayList(); - Map oldToNewVar = Maps.newLinkedHashMap(); + Map oldToNewVar = new IdentityHashMap<>(); calculateNewParameters(funcs, newParameters, oldToNewVar); @@ -90,11 +91,11 @@ private void removeCycle(List funcs, Set funcSet) { stmts = elseBlock; } - Map funcToIndex = new HashMap<>(); + Map funcToIndex = new IdentityHashMap<>(); for (int i = 0; i < funcs.size(); i++) { funcToIndex.put(funcs.get(i), i); } - Map proxyByOriginal = new HashMap<>(); + Map proxyByOriginal = new IdentityHashMap<>(); // Rewrite only affected roots: // - merged cycle body (contains moved bodies from all old funcs) // - callers that directly call any removed function @@ -107,8 +108,8 @@ private void removeCycle(List funcs, Set funcSet) { } for (ImFunction caller : new ArrayList<>(tr.getCalledFunctions().keySet())) { Collection called = tr.getCalledFunctions().get(caller); - for (ImFunction removed : funcSet) { - if (called.contains(removed)) { + for (ImFunction c : called) { + if (funcSet.contains(c)) { rewriteRoots.add(caller.getBody()); break; }