From f3afb0fe25b027866bbf192b7f2241d9ead58686 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 22 Oct 2025 10:34:50 +0200 Subject: [PATCH 1/5] Update BugTests.java --- .../tests/wurstscript/tests/BugTests.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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 9fad0ba50..267b5e355 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 @@ -1539,5 +1539,33 @@ public void overloadsHiddenBySubclassName_onlyZeroArgSeen_errorsOnArgs() { ); } + @Test + public void iteratorManualType() { + testAssertOkLinesWithStdLib(true, + "package test", + "import Printing", + "import LinkedListModule", + "class A", + " use LinkedListModule", + " string s", + "init", + " A a1 = new A()", + " a1.s = \"hello\"", + " A a2 = new A()", + " a2.s = \"world\"", + " int itrCount = 0", + " let itr = A.iterator()", + " while itr.hasNext()", + " let a = itr.next()", + " itrCount += 1", + " print(a.s)", + " itr.close()", + " if itrCount == 2", + " testSuccess()", + "endpackage" + + ); + } + } From c468907247da7d4c22faaff02d7d9527749a6409 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 22 Oct 2025 15:31:21 +0200 Subject: [PATCH 2/5] clear all caches before validation, remove subtype cache as currently defunct. --- .../languageserver/ModelManagerImpl.java | 1 - .../attributes/AttrModuleInstanciations.java | 44 +++--- .../AttrPossibleFunctionSignatures.java | 16 +- .../de/peeeq/wurstscript/types/WurstType.java | 1 + .../wurstscript/validation/GlobalCaches.java | 146 ++++++++++++++++-- .../validation/WurstValidator.java | 23 +-- .../tests/wurstscript/tests/BugTests.java | 69 +++++++++ .../tests/LuaTranslationTests.java | 1 + .../wurstscript/tests/RealWorldExamples.java | 4 + .../wurstscript/tests/WurstScriptTest.java | 3 + 10 files changed, 250 insertions(+), 58 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 7f556963b..ee483a77c 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 @@ -629,7 +629,6 @@ private void doTypeCheckPartial(WurstGui gui, List toCheckFilenames, Set< @Override public void reconcile(Changes changes) { - GlobalCaches.clearAll(); WurstModel model2 = model; if (model2 == null) { return; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java index 8ad6a49d7..febd8ae87 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java @@ -9,33 +9,37 @@ public final class AttrModuleInstanciations { private AttrModuleInstanciations() {} public static @Nullable ModuleDef getModuleOrigin(ModuleInstanciation mi) { - // NOTE: For ModuleInstanciation the "name" used for resolution has historically been getName(). - // Keep this to preserve prior behavior. + // A ModuleInstanciation's parent is always ModuleInstanciations (the list), + // whose parent is either a ClassDef or a ModuleDef. + // We must resolve the module name through that owner's scope. + final String name = mi.getName(); - // 1) Normal path: resolve relative to the lexical parent (old behavior) - final Element parent = mi.getParent(); - if (parent != null) { - TypeDef def = parent.lookupType(name, /*showErrors*/ false); - if (def instanceof ModuleDef) { - return (ModuleDef) def; - } - // Attached but not found -> keep the old error - mi.addError("Could not find module origin for " + Utils.printElement(mi)); + Element parent = mi.getParent(); // This is ModuleInstanciations (plural) + if (parent == null) { + // Detached node during incremental compilation - this is transient. + // Don't emit errors; return null and let the next full pass resolve it. + return null; + } + + // Get the actual owner (ClassDef or ModuleDef) + Element owner = parent.getParent(); + if (owner == null) { + // Still detached at the owner level return null; } - // 2) Detached during incremental build: try the nearest attached scope - final WScope scope = mi.attrNearestScope(); - if (scope != null) { - TypeDef def = scope.lookupType(name, /*showErrors*/ false); - if (def instanceof ModuleDef) { - return (ModuleDef) def; - } + // Resolve through the owner's scope + TypeDef def = owner.lookupType(name, /*showErrors*/ false); + if (def instanceof ModuleDef) { + return (ModuleDef) def; + } + + // Only emit error if we're fully attached (not in a transient state) + if (mi.getModel() != null) { + mi.addError("Could not find module origin for " + Utils.printElement(mi)); } - // 3) Still not found and we're detached: this can be a transient state, - // so don't emit an error here. Return null and let callers handle gracefully. return null; } } 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 6b21a85d4..204ca7634 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 @@ -179,11 +179,19 @@ public static ImmutableCollection calculate(ExprMemberMethod if (recv != null) { VariableBinding m = leftType.matchAgainstSupertype( recv, mm, sig.getMapping(), VariablePosition.RIGHT); - if (m == null) { - // Should not happen; lookupMemberFuncs already checked. Skip defensively. - continue; +// if (m == null) { +// // Should not happen; lookupMemberFuncs already checked. Skip defensively. +// continue; +// } +// sig = sig.setTypeArgs(mm, m); + + // IMPORTANT: + // For members injected via `use module`, the receiver can be a synthetic/module `thistype` + // that is not directly comparable here (especially during incremental builds). + // Do NOT drop the candidate if binding fails; keep it and let arg matching rank it later. + if (m != null) { + sig = sig.setTypeArgs(mm, m); } - sig = sig.setTypeArgs(mm, m); } // Apply explicit type args from the call-site (e.g., c.foo(...)) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstType.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstType.java index a40441ca9..070b610a3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstType.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstType.java @@ -5,6 +5,7 @@ import de.peeeq.wurstscript.jassIm.ImExprOpt; import de.peeeq.wurstscript.jassIm.ImType; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; +import de.peeeq.wurstscript.validation.GlobalCaches; import io.vavr.control.Option; import org.eclipse.jdt.annotation.Nullable; 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 42004b1b4..549355ccd 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,20 +1,57 @@ package de.peeeq.wurstscript.validation; import de.peeeq.wurstscript.ast.Element; -import de.peeeq.wurstscript.attributes.names.NameResolution; import de.peeeq.wurstscript.intermediatelang.ILconst; import de.peeeq.wurstscript.intermediatelang.interpreter.LocalState; -import de.peeeq.wurstscript.types.WurstType; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap; -import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; import java.util.Arrays; import java.util.Map; -import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicLong; // Expose static fields only if you already have them there; otherwise, just clear via dedicated methods. public final class GlobalCaches { + + // Statistics tracking + public static class CacheStats { + final AtomicLong hits = new AtomicLong(); + final AtomicLong misses = new AtomicLong(); + final AtomicLong evictions = new AtomicLong(); + final String name; + + CacheStats(String name) { + this.name = name; + } + + void recordHit() { + hits.incrementAndGet(); + } + + void recordMiss() { + misses.incrementAndGet(); + } + + void recordEviction(int count) { + evictions.addAndGet(count); + } + + double hitRate() { + long h = hits.get(); + long m = misses.get(); + long total = h + m; + return total == 0 ? 0.0 : (double) h / total; + } + + @Override + public String toString() { + return String.format("%s: hits=%d, misses=%d, hitRate=%.2f%%, evictions=%d", + name, hits.get(), misses.get(), hitRate() * 100, evictions.get()); + } + } + + private static final CacheStats lookupStats = new CacheStats("LookupCache"); + private static final CacheStats localStateStats = new CacheStats("LocalStateCache"); + // Optimized ArgumentKey that minimizes allocation overhead public static final class ArgumentKey { private final ILconst[] args; @@ -48,21 +85,48 @@ public boolean equals(Object o) { } } - public enum Mode { TEST_ISOLATED, DEV_PERSISTENT } + public enum Mode {TEST_ISOLATED, DEV_PERSISTENT} + private static volatile Mode mode = Mode.DEV_PERSISTENT; - public static void setMode(Mode m) { mode = m; } - public static Mode mode() { return mode; } + public static void setMode(Mode m) { + mode = m; + } - private GlobalCaches() {} + public static Mode mode() { + return mode; + } - public static final Object2ObjectOpenHashMap> LOCAL_STATE_CACHE = new Object2ObjectOpenHashMap<>(); - public static final Reference2ObjectOpenHashMap> SUBTYPE_MEMO = new Reference2ObjectOpenHashMap<>(); + private GlobalCaches() { + } - /** Call this between tests (and after each compile) */ + // Wrapped caches with statistics + public static final Object2ObjectOpenHashMap> LOCAL_STATE_CACHE = + new Object2ObjectOpenHashMap>() { + @Override + public Object2ObjectOpenHashMap get(Object key) { + Object2ObjectOpenHashMap result = super.get(key); + if (result != null) { + localStateStats.recordHit(); + } else { + localStateStats.recordMiss(); + } + return result; + } + + @Override + public void clear() { + localStateStats.recordEviction(size()); + super.clear(); + } + }; + + + /** + * Call this between tests (and after each compile) + */ public static void clearAll() { LOCAL_STATE_CACHE.clear(); - SUBTYPE_MEMO.clear(); lookupCache.clear(); } @@ -95,5 +159,59 @@ public int hashCode() { } } - public static final Map lookupCache = new Object2ObjectOpenHashMap<>(); + public static final Map lookupCache = new Object2ObjectOpenHashMap() { + @Override + public Object get(Object key) { + Object result = super.get(key); + if (result != null) { + lookupStats.recordHit(); + } else { + lookupStats.recordMiss(); + } + return result; + } + + @Override + public Object put(CacheKey key, Object value) { + // Note: put returns old value, null if new entry + Object old = super.put(key, value); + if (old == null) { + // New entry, the miss was already recorded in get() + } + return old; + } + + @Override + public void clear() { + lookupStats.recordEviction(size()); + super.clear(); + } + }; + + // Statistics methods + public static void printStats() { + System.out.println("=== GlobalCaches Statistics ==="); + System.out.println(lookupStats); + System.out.println(localStateStats); + System.out.println("Current sizes: lookup=" + lookupCache.size() + + ", localState=" + LOCAL_STATE_CACHE.size()); + System.out.println("=============================="); + } + + public static void resetStats() { + lookupStats.hits.set(0); + lookupStats.misses.set(0); + lookupStats.evictions.set(0); + localStateStats.hits.set(0); + localStateStats.misses.set(0); + localStateStats.evictions.set(0); + } + + public static CacheStats getLookupStats() { + return lookupStats; + } + + public static CacheStats getLocalStateStats() { + return localStateStats; + } } 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 552733913..170578a37 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 @@ -23,7 +23,6 @@ import java.util.stream.Collectors; import static de.peeeq.wurstscript.attributes.SmallHelpers.superArgs; -import static de.peeeq.wurstscript.validation.GlobalCaches.SUBTYPE_MEMO; /** * this class validates a wurstscript program @@ -65,7 +64,7 @@ public void validate(Collection toCheck) { visitedFunctions = 0; heavyFunctions.clear(); heavyBlocks.clear(); - SUBTYPE_MEMO.clear(); + GlobalCaches.clearAll(); lightValidation(toCheck); @@ -1614,26 +1613,12 @@ private static boolean isStrictSuperclassOf(ClassDef sup, ClassDef sub) { return false; } - private static boolean isSubtypeCached(WurstType actual, WurstType expected, Annotation site) { + private static boolean isSubtypeCached(WurstType actual, WurstType expected, Element site) { + // Fast paths first if (actual == expected) return true; - // quick structural equality before expensive check if (actual.equalsType(expected, site)) return true; - Reference2BooleanOpenHashMap inner = SUBTYPE_MEMO.get(actual); - if (inner != null && inner.containsKey(expected)) { - return inner.getBoolean(expected); - } - - boolean res = actual.isSubtypeOf(expected, site); - - if (inner == null) { - inner = new Reference2BooleanOpenHashMap<>(); - SUBTYPE_MEMO.put(actual, inner); - } - if (!inner.containsKey(expected)) { - inner.put(expected, res); - } - return res; + return actual.isSubtypeOf(expected, site); } private void checkAnnotation(Annotation a) { 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 267b5e355..f7ad3f19c 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 @@ -10,6 +10,8 @@ import java.io.File; import java.io.IOException; +import static de.peeeq.wurstscript.utils.Utils.string; + public class BugTests extends WurstScriptTest { private static final String TEST_DIR = "./testscripts/concept/"; @@ -1567,5 +1569,72 @@ public void iteratorManualType() { ); } + @Test + public void moduleInOtherPackage() { + String pkgLinkedList = string( + "package LinkedListModule", + "public module LinkedListModule", + " static function iterator() returns Iterator", + " return new Iterator()", + " static class Iterator", + " LinkedListModule.thistype current = null", + " function hasNext() returns boolean", + " return false", + " function next() returns LinkedListModule.thistype", + " return current", + " function close()", + " destroy this", + "endpackage" + ); + + // --- Minimal string iterator stub to create competing iterator symbol --- + String pkgStrIter = string( + "package StrIter", + "public function string.iterator() returns StringIterator", + " return new StringIterator(this)", + "public class StringIterator", + " string str", + " construct(string s)", + " this.str = s", + " function hasNext() returns boolean", + " return false", + " function next() returns string", + " return \"\"", + " function close()", + " destroy this", + "endpackage" + ); + + // --- Reproducer using both packages --- + String pkgHello = string( + "package Hello", + "import LinkedListModule", + "import StrIter", + "native print(string s)", + "native testSuccess()", + "", + "class A", + " use LinkedListModule", + "", + "init", + " let itr = A.iterator()", + " while itr.hasNext()", + " let a = itr.next()", + " destroy a", + " itr.close()", + " let s = \"hello\".iterator()", + " while s.hasNext()", + " print(s.next())", + " s.close()", + " testSuccess()", + "endpackage" + ); + testAssertOkLines(true, + pkgLinkedList, + pkgStrIter, + pkgHello + ); + } + } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index e3c6953dd..f9a7d38a9 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -2,6 +2,7 @@ import com.google.common.base.Charsets; import com.google.common.io.Files; +import de.peeeq.wurstscript.validation.GlobalCaches; import org.testng.annotations.Ignore; import org.testng.annotations.Test; diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java index 744f2589e..e43ea41a0 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java @@ -6,6 +6,7 @@ import de.peeeq.wurstscript.RunArgs; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.gui.WurstGuiCliImpl; +import de.peeeq.wurstscript.validation.GlobalCaches; import org.testng.annotations.Ignore; import org.testng.annotations.Test; @@ -78,6 +79,7 @@ public void setNullTests() throws IOException { @Test public void setFrottyBugKnockbackNull() throws IOException { super.testAssertOkFileWithStdLib(new File(TEST_DIR + "knockback.wurst"), false); + GlobalCaches.printStats(); } @Test @@ -155,6 +157,8 @@ public void test_stdlib() { .run() .getModel(); + GlobalCaches.printStats(); + } @Test diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java index 2647f59b6..e49aa0bca 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java @@ -205,10 +205,13 @@ private CompilationResult testScript() { // translate with different options: testWithoutInliningAndOptimization(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + GlobalCaches.printStats(); testWithLocalOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + GlobalCaches.printStats(); testWithInlining(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); + GlobalCaches.printStats(); testWithInliningAndOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); From 01cc39b5630ebf809c4b3589f3a4e88289723312 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 22 Oct 2025 15:31:27 +0200 Subject: [PATCH 3/5] Update ModelManagerTests.java --- .../wurstscript/tests/ModelManagerTests.java | 234 +++++++++++++++++- 1 file changed, 233 insertions(+), 1 deletion(-) 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 5f6eea650..0fec21e52 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 @@ -1,13 +1,22 @@ package tests.wurstscript.tests; import com.google.common.collect.ImmutableSet; +import de.peeeq.wurstio.TimeTaker; +import de.peeeq.wurstio.WurstCompilerJassImpl; import de.peeeq.wurstio.languageserver.BufferManager; import de.peeeq.wurstio.languageserver.ModelManager; import de.peeeq.wurstio.languageserver.ModelManagerImpl; import de.peeeq.wurstio.languageserver.WFile; import de.peeeq.wurstio.utils.FileUtils; +import de.peeeq.wurstscript.RunArgs; import de.peeeq.wurstscript.ast.*; +import de.peeeq.wurstscript.gui.WurstGui; +import de.peeeq.wurstscript.gui.WurstGuiLogger; +import de.peeeq.wurstscript.types.WurstType; +import de.peeeq.wurstscript.types.WurstTypeClass; +import de.peeeq.wurstscript.types.WurstTypeString; import de.peeeq.wurstscript.utils.Utils; +import de.peeeq.wurstscript.validation.GlobalCaches; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.hamcrest.CoreMatchers; @@ -20,6 +29,7 @@ import java.nio.file.Files; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.containsString; @@ -249,7 +259,7 @@ private Map keepErrorsInMap(ModelManagerImpl manager) { results.put(WFile.create(res.getUri()), errors); for (Diagnostic err : res.getDiagnostics()) { - System.out.println(" " + err.getSeverity() + " in " + res.getUri() + ", line " + err.getRange().getStart().getLine() + "\n " + err.getMessage()); + System.out.println(" " + err.getSeverity() + " in " + res.getUri() + ", line " + err.getRange().getStart().getLine() + "\n " + err.getMessage()); } }); return results; @@ -587,5 +597,227 @@ public void keepTypeErrorsWhileEditing() throws IOException { assertThat(errors.get(fileT1), CoreMatchers.containsString("extraneous input '(' expecting NL")); } + @Test + public void runmapLikePipeline_cacheRegression_closerToRunMap() throws Exception { + File projectFolder = new File("./temp/testProject_runmap_like_real/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + // --- Minimal LinkedList-style module --- + String pkgLinkedList = string( + "package LinkedListModule", + "public module LinkedListModule", + " static function iterator() returns Iterator", + " return new Iterator()", + " static class Iterator", + " LinkedListModule.thistype current = null", + " function hasNext() returns boolean", + " return false", + " function next() returns LinkedListModule.thistype", + " return current", + " function close()", + " destroy this" + ); + + // --- Minimal string iterator stub to create competing iterator symbol --- + String pkgStrIter = string( + "package StrIter", + "public function string.iterator() returns StringIterator", + " return new StringIterator(this)", + "public class StringIterator", + " string str", + " construct(string s)", + " this.str = s", + " function hasNext() returns boolean", + " return false", + " function next() returns string", + " return \"\"", + " function close()", + " destroy this" + ); + + // --- Reproducer using both packages --- + String pkgHello = string( + "package Hello", + "import LinkedListModule", + "import StrIter", + "", + "class A", + " use LinkedListModule", + "", + "init", + " let itr = A.iterator()", + " while itr.hasNext()", + " let a = itr.next()", + " destroy a", + " itr.close()", + " let s = \"hello\".iterator()", + " while s.hasNext()", + " BJDebugMsg(s.next())", + " s.close()" + ); + + // A tiny JASS file so purge logic and JASS transform path look more like RunMap’s environment: + String jass = "globals\nendglobals\n// war3map.j placeholder"; + + WFile fileLinkedList = WFile.create(new File(wurstFolder, "LinkedListModule.wurst")); + WFile fileStrIter = WFile.create(new File(wurstFolder, "StrIter.wurst")); + WFile fileHello = WFile.create(new File(wurstFolder, "Hello.wurst")); + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + WFile fileWar3mapJ = WFile.create(new File(wurstFolder, "war3map.j")); + + writeFile(fileLinkedList, pkgLinkedList); + writeFile(fileStrIter, pkgStrIter); + writeFile(fileHello, pkgHello); + writeFile(fileWurst, "package Wurst\n"); + writeFile(fileWar3mapJ, jass); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + Map diags = keepErrorsInMap(manager); + + // Baseline build (matches "no IDE errors") + manager.buildProject(); + assertEquals(diags.get(fileHello), "", "initial build must be clean"); + + // VSCode-like reconcile touch: + ModelManager.Changes changes = manager.syncCompilationUnitContent(fileHello, pkgHello + "\n// touch"); + manager.reconcile(changes); + assertEquals(diags.get(fileHello), "", "reconcile after harmless edit must be clean"); + GlobalCaches.printStats(); + + // --- First RunMap-like compile on SAME model --- + touchWar3mapJ(manager, fileWar3mapJ, " // pass 1"); + runRunmapLikeCompile_Closer(projectFolder, manager); + assertLocalAIsClassA(manager.getCompilationUnit(fileHello)); + + GlobalCaches.printStats(); + + // --- Second RunMap-like compile (caches should not corrupt resolution) --- + touchWar3mapJ(manager, fileWar3mapJ, " // pass 2"); + runRunmapLikeCompile_Closer(projectFolder, manager); + assertLocalAIsClassA(manager.getCompilationUnit(fileHello)); + + GlobalCaches.printStats(); + } + + + /** Simulate RunMap.replaceBaseScriptWithConfig(..): rewrite war3map.j inside the SAME model. */ + private void touchWar3mapJ(ModelManagerImpl manager, WFile war3map, String suffix) throws IOException { + String content = "globals\nendglobals\n// war3map.j placeholder" + suffix + "\n"; + manager.syncCompilationUnitContent(war3map, content); + } + + /** Run a RunMap-like compile on the SAME model: purge imports, check → IM, then JASS transform. */ + private void runRunmapLikeCompile_Closer(File projectFolder, ModelManagerImpl manager) { + WurstGui gui = new WurstGuiLogger(); + TimeTaker time = new TimeTaker.Default(); + WurstCompilerJassImpl compiler = + new WurstCompilerJassImpl(time, projectFolder, gui, null, new RunArgs(java.util.Collections.emptyList())); + + WurstModel model = manager.getModel(); + + // 2) Check program + gui.sendProgress("Check program"); + compiler.checkProg(model); + if (gui.getErrorCount() > 0) { + throw new AssertionError("RunMap-like checkProg reported errors: " + gui.getErrorList()); + } + + // 3) Translate to IM + compiler.translateProgToIm(model); + if (gui.getErrorCount() > 0) { + throw new AssertionError("RunMap-like translateProgToIm reported errors: " + gui.getErrorList()); + } + + // 4) (Optional but closer to reality) Transform to JASS + compiler.transformProgToJass(); + // We don't run PJass here; just exercising additional phases & caches. + } + + /** Same as MapRequest.purgeUnimportedFiles: keep wurst-folder CUs and any imported transitively, plus .j files. */ + 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")) + .collect(java.util.stream.Collectors.toSet()); + + // Recursively add imported packages’ CUs (uses attrImportedPackage like RunMap) + addImports_likeRunMap(keep, keep); + 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 + return p.startsWith(w) + && java.nio.file.Files.exists(p) + && Utils.isWurstFile(file); + } + + private void addImports_likeRunMap(java.util.Set result, java.util.Set toAdd) { + java.util.Set imported = + toAdd.stream() + .flatMap(cu -> cu.getPackages().stream()) + .flatMap(p -> p.getImports().stream()) + .map(WImport::attrImportedPackage) + .filter(java.util.Objects::nonNull) + .map(WPackage::attrCompilationUnit) + .collect(java.util.stream.Collectors.toSet()); + boolean changed = result.addAll(imported); + if (changed) addImports_likeRunMap(result, imported); + } + + /** Assert that 'let a = itr.next()' has type A and that next() returns A. */ + private void assertLocalAIsClassA(CompilationUnit cu) { + final AtomicBoolean checked = new AtomicBoolean(false); + final AtomicBoolean didFindA = new AtomicBoolean(false); + final AtomicBoolean didFindString = new AtomicBoolean(false); + + cu.accept(new Element.DefaultVisitor() { + @Override public void visit(LocalVarDef l) { + if (!"a".equals(l.getNameId().getName())) { super.visit(l); return; } + + VarInitialization init = l.getInitialExpr(); + if (init == null) throw new AssertionError("local 'a' has no initializer"); + + if (init instanceof ExprMemberMethod) { + WurstType t = ((ExprMemberMethod) init).attrTyp(); + if (t instanceof WurstTypeClass) { + String cname = ((WurstTypeClass) t).getClassDef().getName(); + assertEquals(cname, "A", "Expected local 'a' to be of class A"); + } else { + throw new AssertionError("Expected class type for 'a', but got: " + t); + } + } + + checked.set(true); + super.visit(l); + } + + @Override public void visit(ExprMemberMethodDot call) { + if ("next".equals(call.getFuncName())) { + WurstType rt = call.attrTyp(); + if (rt instanceof WurstTypeClass) { + String cname = ((WurstTypeClass) rt).getClassDef().getName(); + assertEquals(cname, "A", "next() must return A"); + didFindA.set(true); + } else if (rt instanceof WurstTypeString) { + String cname = ((WurstTypeString) rt).getName(); + assertEquals(cname, "string", "next() must return string"); + didFindString.set(true); + } else { + throw new AssertionError("next() must return class A or string, but was: " + rt); + } + } + super.visit(call); + } + }); + + if (!checked.get()) throw new AssertionError("Did not find local var 'a' to assert."); + if (!didFindA.get()) throw new AssertionError("Did not find call to next() returning class A."); + if (!didFindString.get()) throw new AssertionError("Did not find call to next() returning string."); + } + + } From d94cbe7841642887814e35697304759fdda8ab2d Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 22 Oct 2025 15:36:37 +0200 Subject: [PATCH 4/5] Update AttrPossibleFunctionSignatures.java --- .../attributes/AttrPossibleFunctionSignatures.java | 5 ----- 1 file changed, 5 deletions(-) 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 204ca7634..725f24a44 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 @@ -179,11 +179,6 @@ public static ImmutableCollection calculate(ExprMemberMethod if (recv != null) { VariableBinding m = leftType.matchAgainstSupertype( recv, mm, sig.getMapping(), VariablePosition.RIGHT); -// if (m == null) { -// // Should not happen; lookupMemberFuncs already checked. Skip defensively. -// continue; -// } -// sig = sig.setTypeArgs(mm, m); // IMPORTANT: // For members injected via `use module`, the receiver can be a synthetic/module `thistype` From 40d781e8e0b129eddbe1022e133215159dea5d80 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 22 Oct 2025 15:37:44 +0200 Subject: [PATCH 5/5] remove prints --- .../test/java/tests/wurstscript/tests/ModelManagerTests.java | 5 ----- .../test/java/tests/wurstscript/tests/RealWorldExamples.java | 3 --- .../test/java/tests/wurstscript/tests/WurstScriptTest.java | 3 --- 3 files changed, 11 deletions(-) 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 0fec21e52..518b68cf3 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 @@ -683,21 +683,16 @@ public void runmapLikePipeline_cacheRegression_closerToRunMap() throws Exception ModelManager.Changes changes = manager.syncCompilationUnitContent(fileHello, pkgHello + "\n// touch"); manager.reconcile(changes); assertEquals(diags.get(fileHello), "", "reconcile after harmless edit must be clean"); - GlobalCaches.printStats(); // --- First RunMap-like compile on SAME model --- touchWar3mapJ(manager, fileWar3mapJ, " // pass 1"); runRunmapLikeCompile_Closer(projectFolder, manager); assertLocalAIsClassA(manager.getCompilationUnit(fileHello)); - GlobalCaches.printStats(); - // --- Second RunMap-like compile (caches should not corrupt resolution) --- touchWar3mapJ(manager, fileWar3mapJ, " // pass 2"); runRunmapLikeCompile_Closer(projectFolder, manager); assertLocalAIsClassA(manager.getCompilationUnit(fileHello)); - - GlobalCaches.printStats(); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java index e43ea41a0..e47fb8a9d 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java @@ -79,7 +79,6 @@ public void setNullTests() throws IOException { @Test public void setFrottyBugKnockbackNull() throws IOException { super.testAssertOkFileWithStdLib(new File(TEST_DIR + "knockback.wurst"), false); - GlobalCaches.printStats(); } @Test @@ -157,8 +156,6 @@ public void test_stdlib() { .run() .getModel(); - GlobalCaches.printStats(); - } @Test diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java index e49aa0bca..2647f59b6 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java @@ -205,13 +205,10 @@ private CompilationResult testScript() { // translate with different options: testWithoutInliningAndOptimization(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); - GlobalCaches.printStats(); testWithLocalOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); - GlobalCaches.printStats(); testWithInlining(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs); - GlobalCaches.printStats(); testWithInliningAndOptimizations(name, executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms, runArgs);