From 9ca9b043bca0923d8302bc5edb5fd3cf913cb829 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 28 Oct 2025 21:54:46 +0100 Subject: [PATCH 1/3] evict cache selectively --- de.peeeq.wurstscript/.gitattributes | 4 + .../de/peeeq/wurstscript/WurstChecker.java | 6 - .../wurstscript/validation/GlobalCaches.java | 121 +++++++++++++- .../validation/WurstValidator.java | 4 +- .../tests/wurstscript/tests/BugTests.java | 2 +- .../wurstscript/tests/ModelManagerTests.java | 156 +++++++++++++++++- 6 files changed, 279 insertions(+), 14 deletions(-) create mode 100644 de.peeeq.wurstscript/.gitattributes diff --git a/de.peeeq.wurstscript/.gitattributes b/de.peeeq.wurstscript/.gitattributes new file mode 100644 index 000000000..15552e1a4 --- /dev/null +++ b/de.peeeq.wurstscript/.gitattributes @@ -0,0 +1,4 @@ +* text=auto +*.sh text eol=lf +gradlew text eol=lf +*.bat text eol=crlf \ No newline at end of file diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java index f456412c6..443ed7766 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstChecker.java @@ -39,12 +39,6 @@ public void checkProg(WurstModel root, Collection toCheck) { if (errorHandler.getErrorCount() > 0) return; - // compute the flow attributes - for (CompilationUnit cu : toCheck) { - WurstValidator.computeFlowAttributes(cu); - } - - // validate the resource: WurstValidator validator = new WurstValidator(root); validator.validate(toCheck); 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..2357eeca2 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 @@ -1,12 +1,15 @@ package de.peeeq.wurstscript.validation; +import de.peeeq.wurstscript.ast.CompilationUnit; import de.peeeq.wurstscript.ast.Element; +import de.peeeq.wurstscript.ast.WurstModel; +import de.peeeq.wurstscript.attributes.AttrNearest; import de.peeeq.wurstscript.intermediatelang.ILconst; import de.peeeq.wurstscript.intermediatelang.interpreter.LocalState; +import de.peeeq.wurstscript.jassIm.ImFunction; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import java.util.Arrays; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicLong; // Expose static fields only if you already have them there; otherwise, just clear via dedicated methods. @@ -87,7 +90,7 @@ public boolean equals(Object o) { public enum Mode {TEST_ISOLATED, DEV_PERSISTENT} - private static volatile Mode mode = Mode.DEV_PERSISTENT; + public static volatile Mode mode = Mode.DEV_PERSISTENT; public static void setMode(Mode m) { mode = m; @@ -130,6 +133,118 @@ public static void clearAll() { lookupCache.clear(); } + /** + * Evict cache entries that are tied to any of the given compilation units. + * + *

The validator replaces only a subset of units on incremental runs. Instead of + * purging the full global caches (which would force re-computation for every file), we + * walk both caches and drop entries whose owner element/function belongs to one of the + * affected compilation units.

+ */ + public static void invalidateFor(WurstModel model, Collection changedUnits) { + if (mode == Mode.TEST_ISOLATED) { + clearAll(); + return; + } + + Set affected = toIdentitySet(changedUnits); + Set live = model == null ? null : toIdentitySet(model); + + if (affected.isEmpty() && (live == null || live.isEmpty())) { + return; + } + + invalidateLookupCache(affected, live); + invalidateLocalStateCache(affected, live); + } + + private static Set toIdentitySet(Iterable units) { + Set set = Collections.newSetFromMap(new IdentityHashMap<>()); + if (units == null) { + return set; + } + for (CompilationUnit cu : units) { + if (cu != null) { + set.add(cu); + } + } + return set; + } + + private static void invalidateLookupCache(Set affected, Set live) { + if (lookupCache.isEmpty()) { + return; + } + + int evicted = 0; + Iterator> it = lookupCache.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + Element element = entry.getKey().element; + if (element == null) { + continue; + } + + CompilationUnit owner = AttrNearest.nearestCompilationUnit(element); + if (owner == null) { + continue; + } + + boolean shouldEvict = affected.contains(owner) + || (live != null && !live.contains(owner)); + + if (shouldEvict) { + it.remove(); + evicted++; + } + } + + if (evicted > 0) { + lookupStats.recordEviction(evicted); + } + } + + private static void invalidateLocalStateCache(Set affected, Set live) { + if (LOCAL_STATE_CACHE.isEmpty()) { + return; + } + + int evicted = 0; + Iterator>> it = + LOCAL_STATE_CACHE.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + Object key = entry.getKey(); + if (!(key instanceof ImFunction)) { + continue; + } + + ImFunction function = (ImFunction) key; + Element trace = function.attrTrace(); + if (trace == null) { + continue; + } + + CompilationUnit owner = AttrNearest.nearestCompilationUnit(trace); + if (owner == null) { + continue; + } + + boolean shouldEvict = affected.contains(owner) + || (live != null && !live.contains(owner)); + + if (shouldEvict) { + it.remove(); + evicted++; + } + } + + if (evicted > 0) { + localStateStats.recordEviction(evicted); + } + } + + public enum LookupType { FUNC, VAR, TYPE, PACKAGE, MEMBER_FUNC, MEMBER_VAR } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index bf935301a..7550b6b33 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -64,7 +64,7 @@ public void validate(Collection toCheck) { visitedFunctions = 0; heavyFunctions.clear(); heavyBlocks.clear(); - GlobalCaches.clearAll(); + GlobalCaches.invalidateFor(prog, toCheck); lightValidation(toCheck); @@ -601,7 +601,7 @@ private int distanceToOwner(ClassDef start, ClassDef owner) { private void visit(StmtExitwhen exitwhen) { Element parent = exitwhen.getParent(); - while (!(parent instanceof FunctionDefinition)) { + while (parent != null && !(parent instanceof FunctionDefinition)) { if (parent instanceof StmtForEach) { StmtForEach forEach = (StmtForEach) parent; if (forEach.getIn().tryGetNameDef().attrIsVararg()) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java index 97633a923..bd79b7808 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java @@ -71,7 +71,7 @@ public void bug62_codearray() { @Test public void bug61_break() { - testAssertErrorsLines(false, "inside a loop", + testAssertErrorsLines(false, "not allowed outside of loop", "package test", " init", " break", 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..12eafeff6 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 @@ -34,8 +34,7 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.*; public class ModelManagerTests { @@ -265,6 +264,116 @@ private Map keepErrorsInMap(ModelManagerImpl manager) { return results; } + private CacheFixture setupCacheFixture(String projectName) throws IOException { + GlobalCaches.clearAll(); + + File projectFolder = new File("./temp/" + projectName + "/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + String packageA = string( + "package A", + "import B", + "public function a() returns int", + " return b()" + ); + + String packageB = string( + "package B", + "import C", + "public function b() returns int", + " return c()" + ); + + String packageC = string( + "package C", + "public function c() returns int", + " return 1" + ); + + String packageD = string( + "package D", + "public function d() returns int", + " return 2" + ); + + WFile fileA = WFile.create(new File(wurstFolder, "A.wurst")); + WFile fileB = WFile.create(new File(wurstFolder, "B.wurst")); + WFile fileC = WFile.create(new File(wurstFolder, "C.wurst")); + WFile fileD = WFile.create(new File(wurstFolder, "D.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + + writeFile(fileA, packageA); + writeFile(fileB, packageB); + writeFile(fileC, packageC); + writeFile(fileD, packageD); + writeFile(fileWurst, "package Wurst\n"); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + Map results = keepErrorsInMap(manager); + manager.buildProject(); + + CompilationUnit cuA = manager.getCompilationUnit(fileA); + CompilationUnit cuB = manager.getCompilationUnit(fileB); + CompilationUnit cuC = manager.getCompilationUnit(fileC); + CompilationUnit cuD = manager.getCompilationUnit(fileD); + + assertNotNull(cuA); + assertNotNull(cuB); + assertNotNull(cuC); + assertNotNull(cuD); + + WPackage pkgA = cuA.getPackages().get(0); + WPackage pkgB = cuB.getPackages().get(0); + WPackage pkgC = cuC.getPackages().get(0); + WPackage pkgD = cuD.getPackages().get(0); + + GlobalCaches.CacheKey keyA = new GlobalCaches.CacheKey(pkgA, "markerA", GlobalCaches.LookupType.FUNC); + GlobalCaches.CacheKey keyB = new GlobalCaches.CacheKey(pkgB, "markerB", GlobalCaches.LookupType.FUNC); + GlobalCaches.CacheKey keyC = new GlobalCaches.CacheKey(pkgC, "markerC", GlobalCaches.LookupType.FUNC); + GlobalCaches.CacheKey keyD = new GlobalCaches.CacheKey(pkgD, "markerD", GlobalCaches.LookupType.FUNC); + + GlobalCaches.lookupCache.put(keyA, Boolean.TRUE); + GlobalCaches.lookupCache.put(keyB, Boolean.TRUE); + GlobalCaches.lookupCache.put(keyC, Boolean.TRUE); + GlobalCaches.lookupCache.put(keyD, Boolean.TRUE); + + assertTrue(GlobalCaches.lookupCache.containsKey(keyA)); + assertTrue(GlobalCaches.lookupCache.containsKey(keyB)); + assertTrue(GlobalCaches.lookupCache.containsKey(keyC)); + assertTrue(GlobalCaches.lookupCache.containsKey(keyD)); + + return new CacheFixture(manager, results, fileA, fileB, fileC, fileD, keyA, keyB, keyC, keyD); + } + + private static final class CacheFixture { + final ModelManagerImpl manager; + final Map results; + final WFile fileA; + final WFile fileB; + final WFile fileC; + final WFile fileD; + final GlobalCaches.CacheKey keyA; + final GlobalCaches.CacheKey keyB; + final GlobalCaches.CacheKey keyC; + final GlobalCaches.CacheKey keyD; + + CacheFixture(ModelManagerImpl manager, Map results, WFile fileA, WFile fileB, + WFile fileC, WFile fileD, GlobalCaches.CacheKey keyA, GlobalCaches.CacheKey keyB, + GlobalCaches.CacheKey keyC, GlobalCaches.CacheKey keyD) { + this.manager = manager; + this.results = results; + this.fileA = fileA; + this.fileB = fileB; + this.fileC = fileC; + this.fileD = fileD; + this.keyA = keyA; + this.keyB = keyB; + this.keyC = keyC; + this.keyD = keyD; + } + } + private void newCleanFolder(File f) throws IOException { FileUtils.deleteRecursively(f); Files.createDirectories(f.toPath()); @@ -355,6 +464,49 @@ public void visit(ClassDef c) { } + @Test + public void selectiveCacheInvalidationSkipsUnaffectedUnits() throws IOException { + CacheFixture fixture = setupCacheFixture("cacheInvalidationProject1"); + fixture.results.clear(); + + String packageBUpdated = string( + "package B", + "import C", + "public function b() returns int", + " return c() + 1" + ); + + ModelManager.Changes changes = fixture.manager.syncCompilationUnitContent(fixture.fileB, packageBUpdated); + fixture.manager.reconcile(changes); + + assertEquals(fixture.results.keySet(), ImmutableSet.of(fixture.fileA, fixture.fileB)); + assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyA)); + assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyB)); + assertTrue(GlobalCaches.lookupCache.containsKey(fixture.keyC)); + assertTrue(GlobalCaches.lookupCache.containsKey(fixture.keyD)); + } + + @Test + public void selectiveCacheInvalidationCoversTransitiveDependents() throws IOException { + CacheFixture fixture = setupCacheFixture("cacheInvalidationProject2"); + fixture.results.clear(); + + String packageCUpdated = string( + "package C", + "public function c() returns int", + " return 2" + ); + + ModelManager.Changes changes = fixture.manager.syncCompilationUnitContent(fixture.fileC, packageCUpdated); + fixture.manager.reconcile(changes); + + assertEquals(fixture.results.keySet(), ImmutableSet.of(fixture.fileA, fixture.fileB, fixture.fileC)); + assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyA)); + assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyB)); + assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyC)); + assertTrue(GlobalCaches.lookupCache.containsKey(fixture.keyD)); + } + @Test public void changeModuleAbstractMethod() throws IOException { File projectFolder = new File("./temp/testProject2/"); From d66dff106b2400845ca245c7ff18089861c4c6e6 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 11 Mar 2026 18:07:52 +0100 Subject: [PATCH 2/3] fixes --- .../wurstscript/validation/GlobalCaches.java | 40 +++++++++++ .../wurstscript/tests/ModelManagerTests.java | 70 ++++++++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) 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 bcbe5237f..761494f29 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 @@ -170,6 +170,7 @@ public static void invalidateFor(WurstModel model, Collection c invalidateLookupCache(affected, live); invalidateLocalStateCache(affected, live); + invalidateLocalStateNoArgCache(affected, live); } private static Set toIdentitySet(Iterable units) { @@ -258,6 +259,45 @@ private static void invalidateLocalStateCache(Set affected, Set } } + private static void invalidateLocalStateNoArgCache(Set affected, Set live) { + if (LOCAL_STATE_NOARG_CACHE.isEmpty()) { + return; + } + + int evicted = 0; + Iterator> it = LOCAL_STATE_NOARG_CACHE.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + Object key = entry.getKey(); + if (!(key instanceof ImFunction)) { + continue; + } + + ImFunction function = (ImFunction) key; + Element trace = function.attrTrace(); + if (trace == null) { + continue; + } + + CompilationUnit owner = AttrNearest.nearestCompilationUnit(trace); + if (owner == null) { + continue; + } + + boolean shouldEvict = affected.contains(owner) + || (live != null && !live.contains(owner)); + + if (shouldEvict) { + it.remove(); + evicted++; + } + } + + if (evicted > 0) { + localStateStats.recordEviction(evicted); + } + } + public enum LookupType { FUNC, VAR, TYPE, PACKAGE, MEMBER_FUNC, MEMBER_VAR 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 4acbcc286..5d7011308 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 @@ -12,6 +12,9 @@ import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.gui.WurstGui; import de.peeeq.wurstscript.gui.WurstGuiLogger; +import de.peeeq.wurstscript.intermediatelang.interpreter.LocalState; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.jassIm.JassIm; import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeClass; import de.peeeq.wurstscript.types.WurstTypeString; @@ -27,6 +30,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -484,7 +488,7 @@ private CacheFixture setupCacheFixture(String projectName) throws IOException { assertTrue(GlobalCaches.lookupCache.containsKey(keyC)); assertTrue(GlobalCaches.lookupCache.containsKey(keyD)); - return new CacheFixture(manager, results, fileA, fileB, fileC, fileD, keyA, keyB, keyC, keyD); + return new CacheFixture(manager, results, fileA, fileB, fileC, fileD, keyA, keyB, keyC, keyD, pkgA, pkgB, pkgC, pkgD); } private static final class CacheFixture { @@ -498,10 +502,15 @@ private static final class CacheFixture { final GlobalCaches.CacheKey keyB; final GlobalCaches.CacheKey keyC; final GlobalCaches.CacheKey keyD; + final WPackage pkgA; + final WPackage pkgB; + final WPackage pkgC; + final WPackage pkgD; CacheFixture(ModelManagerImpl manager, Map results, WFile fileA, WFile fileB, WFile fileC, WFile fileD, GlobalCaches.CacheKey keyA, GlobalCaches.CacheKey keyB, - GlobalCaches.CacheKey keyC, GlobalCaches.CacheKey keyD) { + GlobalCaches.CacheKey keyC, GlobalCaches.CacheKey keyD, + WPackage pkgA, WPackage pkgB, WPackage pkgC, WPackage pkgD) { this.manager = manager; this.results = results; this.fileA = fileA; @@ -512,9 +521,54 @@ private static final class CacheFixture { this.keyB = keyB; this.keyC = keyC; this.keyD = keyD; + this.pkgA = pkgA; + this.pkgB = pkgB; + this.pkgC = pkgC; + this.pkgD = pkgD; } } + private static final class NoArgCacheFixture { + final ImFunction fnA; + final ImFunction fnB; + final ImFunction fnC; + final ImFunction fnD; + + NoArgCacheFixture(ImFunction fnA, ImFunction fnB, ImFunction fnC, ImFunction fnD) { + this.fnA = fnA; + this.fnB = fnB; + this.fnC = fnC; + this.fnD = fnD; + } + } + + private NoArgCacheFixture seedNoArgLocalStateCache(CacheFixture fixture) { + ImFunction fnA = JassIm.ImFunction( + fixture.pkgA, "cachedA", JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), + JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + ImFunction fnB = JassIm.ImFunction( + fixture.pkgB, "cachedB", JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), + JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + ImFunction fnC = JassIm.ImFunction( + fixture.pkgC, "cachedC", JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), + JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + ImFunction fnD = JassIm.ImFunction( + fixture.pkgD, "cachedD", JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), + JassIm.ImVars(), JassIm.ImStmts(), Collections.emptyList()); + + GlobalCaches.LOCAL_STATE_NOARG_CACHE.put(fnA, new LocalState()); + GlobalCaches.LOCAL_STATE_NOARG_CACHE.put(fnB, new LocalState()); + GlobalCaches.LOCAL_STATE_NOARG_CACHE.put(fnC, new LocalState()); + GlobalCaches.LOCAL_STATE_NOARG_CACHE.put(fnD, new LocalState()); + + assertTrue(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(fnA)); + assertTrue(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(fnB)); + assertTrue(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(fnC)); + assertTrue(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(fnD)); + + return new NoArgCacheFixture(fnA, fnB, fnC, fnD); + } + private void newCleanFolder(File f) throws IOException { FileUtils.deleteRecursively(f); Files.createDirectories(f.toPath()); @@ -608,6 +662,7 @@ public void visit(ClassDef c) { @Test public void selectiveCacheInvalidationSkipsUnaffectedUnits() throws IOException { CacheFixture fixture = setupCacheFixture("cacheInvalidationProject1"); + NoArgCacheFixture noArg = seedNoArgLocalStateCache(fixture); fixture.results.clear(); String packageBUpdated = string( @@ -625,11 +680,17 @@ public void selectiveCacheInvalidationSkipsUnaffectedUnits() throws IOException assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyB)); assertTrue(GlobalCaches.lookupCache.containsKey(fixture.keyC)); assertTrue(GlobalCaches.lookupCache.containsKey(fixture.keyD)); + + assertFalse(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnA)); + assertFalse(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnB)); + assertTrue(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnC)); + assertTrue(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnD)); } @Test public void selectiveCacheInvalidationCoversTransitiveDependents() throws IOException { CacheFixture fixture = setupCacheFixture("cacheInvalidationProject2"); + NoArgCacheFixture noArg = seedNoArgLocalStateCache(fixture); fixture.results.clear(); String packageCUpdated = string( @@ -646,6 +707,11 @@ public void selectiveCacheInvalidationCoversTransitiveDependents() throws IOExce assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyB)); assertFalse(GlobalCaches.lookupCache.containsKey(fixture.keyC)); assertTrue(GlobalCaches.lookupCache.containsKey(fixture.keyD)); + + assertFalse(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnA)); + assertFalse(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnB)); + assertFalse(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnC)); + assertTrue(GlobalCaches.LOCAL_STATE_NOARG_CACHE.containsKey(noArg.fnD)); } @Test From 042adac66d69d7b0887e0775b6de8f837ee32146 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 11 Mar 2026 18:22:02 +0100 Subject: [PATCH 3/3] Update WurstValidator.java --- .../java/de/peeeq/wurstscript/validation/WurstValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b7f25a5a7..08097ade5 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 @@ -624,7 +624,7 @@ private void visit(StmtExitwhen exitwhen) { private void visit(StmtContinue stmtContinue) { Element parent = stmtContinue.getParent(); - while (!(parent instanceof FunctionDefinition)) { + while (parent != null && !(parent instanceof FunctionDefinition)) { if (parent instanceof StmtForEach) { StmtForEach forEach = (StmtForEach) parent; if (forEach.getIn().tryGetNameDef().attrIsVararg()) { @@ -636,7 +636,7 @@ private void visit(StmtContinue stmtContinue) { } parent = parent.getParent(); } - stmtContinue.addError("Continue is not allowed outside of loop statements."); + stmtContinue.addError("Continue statements must be used inside a loop."); } private void checkTupleDef(TupleDef e) {