diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/FrameProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/FrameProvider.java index 06f6a6e51..ed6f775fe 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/FrameProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/FrameProvider.java @@ -35,4 +35,8 @@ public IlConstHandle ConvertOriginFrameType(ILconstInt i) { return new IlConstHandle("frameType", i.getVal()); } + public IlConstHandle BlzGetFrameByName(ILconstString name, ILconstInt createContext) { + return new IlConstHandle("framehandle", new FrameHandle()); + } + } 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 bea2efe8f..38f74a860 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 @@ -168,7 +168,7 @@ public ILconstObject allocate(ImClassType clazz, Element trace) { objectIdCounter++; ILconstObject res = new ILconstObject(clazz, objectIdCounter, trace); indexToObject.put(objectIdCounter, res); - System.out.println("alloc objId=" + objectIdCounter + " type=" + clazz + " trace=" + trace); + WLogger.trace("alloc objId=" + objectIdCounter + " type=" + clazz + " trace=" + trace); return res; } 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 a43de5666..fdb18a317 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 @@ -2,10 +2,13 @@ import com.google.common.collect.*; import de.peeeq.wurstscript.WLogger; +import de.peeeq.wurstscript.ast.PackageOrGlobal; +import de.peeeq.wurstscript.ast.WPackage; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtojass.ImAttrType; import de.peeeq.wurstscript.translation.imtojass.TypeRewriteMatcher; +import org.eclipse.jdt.annotation.Nullable; import org.jetbrains.annotations.NotNull; import java.util.*; @@ -675,6 +678,10 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { private void createSpecializedGlobals(ImClass originalClass, GenericTypes generics, List typeVars) { String key = gKey(generics); + // Collect "insert specialized init right after original init" operations per parent ImStmts + // Using identity maps because IM nodes use identity semantics for parent/ownership. + Map>> insertsByParent = new IdentityHashMap<>(); + for (Map.Entry entry : globalToClass.entrySet()) { ImVar originalGlobal = entry.getKey(); ImClass owningClass = entry.getValue(); @@ -694,17 +701,89 @@ private void createSpecializedGlobals(ImClass originalClass, GenericTypes generi originalGlobal.getIsBJ() ); + // Create + register global + translator.addGlobal(specializedGlobal); + specializedGlobals.put(originalGlobal, key, specializedGlobal); + dbg("Created specialized global: " + specializedName + " type=" + specializedType); + + // If original has init(s), create corresponding specialized init(s) and schedule insertion List originalInits = prog.getGlobalInits().get(originalGlobal); if (originalInits != null && !originalInits.isEmpty()) { - ImExpr initRhs = originalInits.getFirst().getRight().copy(); - initRhs = specializeNullInitializer(initRhs, specializedType); - translator.addGlobalWithInitalizer(specializedGlobal, initRhs); - } else { - translator.addGlobal(specializedGlobal); + + ImSet firstOrig = originalInits.getFirst(); + if (!(firstOrig.getParent() instanceof ImStmts parentStmts)) { + throw new CompileError(originalGlobal, + "Initializer for global " + originalGlobal.getName() + " is not inside ImStmts."); + } + // ensure all original init sets share the same parent statement list + for (ImSet s : originalInits) { + if (s.getParent() != parentStmts) { + throw new CompileError(originalGlobal, + "Initializer statements for global " + originalGlobal.getName() + " are not in the same ImStmts."); + } + } + + // Helper: rebuild LHS as ImLExpr for specialized global + java.util.function.Function specializeLhs = (ImLExpr lhs) -> { + if (lhs instanceof ImVarAccess va) { + if (va.getVar() == originalGlobal) { + return JassIm.ImVarAccess(specializedGlobal); + } + return (ImLExpr) va.copy(); + } + if (lhs instanceof ImVarArrayAccess aa) { + if (aa.getVar() == originalGlobal) { + return JassIm.ImVarArrayAccess( + aa.getTrace(), + specializedGlobal, + aa.getIndexes().copy() + ); + } + return (ImLExpr) aa.copy(); + } + throw new CompileError(originalGlobal, + "Unsupported initializer LHS for global " + originalGlobal.getName() + ": " + lhs.getClass().getSimpleName()); + }; + + List specializedInitsForMap = new ArrayList<>(originalInits.size()); + + // Create specialized init sets and schedule: insert each right after its corresponding original init set + for (ImSet origSet : originalInits) { + ImExpr rhs = origSet.getRight().copy(); + rhs = specializeNullInitializer(rhs, specializedType); + + ImLExpr newLeft = specializeLhs.apply(origSet.getLeft()); + ImSet specSet = JassIm.ImSet(originalGlobal.attrTrace(), newLeft, rhs); + + // schedule insertion right after origSet in its parent ImStmts + IdentityHashMap> byStmt = + insertsByParent.computeIfAbsent(parentStmts, k -> new IdentityHashMap<>()); + byStmt.computeIfAbsent(origSet, k -> new ArrayList<>(1)).add(specSet); + + // keep prog.getGlobalInits consistent, but do NOT reuse the tree-attached node elsewhere + specializedInitsForMap.add((ImSet) specSet.copy()); + } + + prog.getGlobalInits().put(specializedGlobal, specializedInitsForMap); } + } - specializedGlobals.put(originalGlobal, key, specializedGlobal); - dbg("Created specialized global: " + specializedName + " type=" + specializedType); + // Perform insertions after the loop (so indices/state remain stable during collection) + for (Map.Entry>> e : insertsByParent.entrySet()) { + ImStmts parent = e.getKey(); + IdentityHashMap> toInsertAfter = e.getValue(); + + ListIterator it = parent.listIterator(); + while (it.hasNext()) { + ImStmt curr = it.next(); + List ins = toInsertAfter.get(curr); + if (ins != null) { + // add in order, immediately after the original init + for (ImStmt s : ins) { + it.add(s); + } + } + } } } 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 24c02a51e..d7b869f3d 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 @@ -13,7 +13,7 @@ import static de.peeeq.wurstscript.utils.Utils.string; public class BugTests extends WurstScriptTest { - private static final String TEST_DIR = "./testscripts/concept/"; + public static final String TEST_DIR = "./testscripts/concept/"; @Test public void localsInOndestroy() throws IOException { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java index bafd64911..ed0ebf60b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java @@ -3,6 +3,11 @@ import org.testng.annotations.Ignore; import org.testng.annotations.Test; +import java.io.File; +import java.io.IOException; + +import static tests.wurstscript.tests.BugTests.TEST_DIR; + public class GenericsWithTypeclassesTests extends WurstScriptTest { @@ -2013,5 +2018,10 @@ public void genericClassWithStaticMemberArray() { ); } + @Test + public void fullArrayListTest() throws IOException { + testAssertOkFileWithStdLib(new File(TEST_DIR + "arrayList.wurst"), true); + } + } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java index 1c68780d8..8406bfd70 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java @@ -23,7 +23,7 @@ public class StdLib { /** * version to use for the tests */ - private final static String version = "6107c40e64fa646a016e8c446026f2f5cf3f2a1e"; + private final static String version = "e6463189f754b8794e59ba9d4ac1a91977c8aaac"; /** * flag so that initialization in only done once 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 c04a02381..c0c752ca0 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 @@ -399,7 +399,7 @@ protected WurstModel testScript(String name, boolean executeProg, String prog) { private void testWithInliningAndOptimizations(String name, boolean executeProg, boolean executeTests, WurstGui gui, WurstCompilerJassImpl compiler, WurstModel model, boolean executeProgOnlyAfterTransforms, RunArgs runArgs) throws Error { // test with inlining and local optimization - currentTestEnv = "With Inlining and Optimizations"; + setCurrentTestEnv("With Inlining and Optimizations"); compiler.setRunArgs(runArgs.with("-inline", "-localOptimizations")); translateAndTest(name + "_inlopt", executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms); } @@ -407,7 +407,7 @@ private void testWithInliningAndOptimizations(String name, boolean executeProg, private void testWithInliningAndOptimizationsAndStacktraces(String name, boolean executeProg, boolean executeTests, WurstGui gui, WurstCompilerJassImpl compiler, WurstModel model, boolean executeProgOnlyAfterTransforms, RunArgs runArgs) throws Error { // test with inlining and local optimization - currentTestEnv = "With Inlining, Optimizations and Stacktraces"; + setCurrentTestEnv("With Inlining, Optimizations and Stacktraces"); compiler.setRunArgs(runArgs.with("-inline", "-localOptimizations", "-stacktraces")); translateAndTest(name + "_stacktraceinlopt", executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms); } @@ -416,7 +416,7 @@ private void testWithInlining(String name, boolean executeProg, boolean executeT , WurstCompilerJassImpl compiler, WurstModel model, boolean executeProgOnlyAfterTransforms , RunArgs runArgs) throws Error { // test with inlining - currentTestEnv = "With Inlining"; + setCurrentTestEnv("With Inlining"); compiler.setRunArgs(runArgs.with("-inline")); translateAndTest(name + "_inl", executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms); } @@ -424,7 +424,7 @@ private void testWithInlining(String name, boolean executeProg, boolean executeT private void testWithLocalOptimizations(String name, boolean executeProg, boolean executeTests, WurstGui gui, WurstCompilerJassImpl compiler, WurstModel model, boolean executeProgOnlyAfterTransforms, RunArgs runArgs) throws Error { // test with local optimization - currentTestEnv = "With Local Optimizations"; + setCurrentTestEnv("With Local Optimizations"); compiler.setRunArgs(runArgs.with("-localOptimizations")); translateAndTest(name + "_opt", executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms); } @@ -434,7 +434,7 @@ private void testWithoutInliningAndOptimization(String name, boolean executeProg throws Error { compiler.setRunArgs(runArgs); // test without inlining and optimization - currentTestEnv = "No opts"; + setCurrentTestEnv("No opts"); translateAndTest(name + "_no_opts", executeProg, executeTests, gui, compiler, model, executeProgOnlyAfterTransforms); } @@ -526,7 +526,7 @@ private void translateAndTest(String name, boolean executeProg, if (executeProg) { WLogger.info("Executing imProg before jass transformation"); String currentEnv = currentTestEnv; - currentTestEnv = "ImProg before jass transformation"; + setCurrentTestEnv("ImProg before jass transformation"); executeImProg(gui, imProg); currentTestEnv = currentEnv; } @@ -545,7 +545,7 @@ private void translateAndTest(String name, boolean executeProg, if (executeProg) { WLogger.info("Executing imProg after jass transformation"); String currentEnv = currentTestEnv; - currentTestEnv += "-ImProg"; + setCurrentTestEnv(currentTestEnv + "-ImProg"); executeImProg(gui, imProg); currentTestEnv = currentEnv; } @@ -563,7 +563,7 @@ private void translateAndTest(String name, boolean executeProg, if (executeProg) { String currentEnv = currentTestEnv; - currentTestEnv += "-JassProg"; + setCurrentTestEnv(currentTestEnv + "-JassProg"); executeJassProg(prog); currentTestEnv = currentEnv; } @@ -613,7 +613,16 @@ private void runPjass(File outputFile) throws Error { } } - public static String currentTestEnv = ""; + private static String currentTestEnv = ""; + + public static String getCurrentTestEnv() { + return currentTestEnv; + } + + public static void setCurrentTestEnv(String env) { + currentTestEnv = env; + System.out.println("Current test environment: " + currentTestEnv); + } private void executeImProg(WurstGui gui, ImProg imProg) throws TestFailException { try { diff --git a/de.peeeq.wurstscript/testscripts/concept/arrayList.wurst b/de.peeeq.wurstscript/testscripts/concept/arrayList.wurst new file mode 100644 index 000000000..d5fa75b53 --- /dev/null +++ b/de.peeeq.wurstscript/testscripts/concept/arrayList.wurst @@ -0,0 +1,853 @@ +package ArrayList +import NoWurst +import Integer +import String +import Printing +import Real + +native testSuccess() + +constant MAX_ARRAY_SIZE = 8192 + +/** + * High-performance array-based list using static shared storage per type. + * In most cases, a LinkedList is a better choice due to its flexibility. + * This data structure is only recommended for performance-critical code + * and requires careful use to avoid fragmentation. + * + * WHEN TO USE: + * =========== + * ArrayList is faster than LinkedList for: + * - Iterating large lists (1000+ elements) - no node indirection + * - Index based operations - O(1) vs O(n) + * - When only appending elements to the end + * + * LinkedList is better for: + * - Insertion anywhere except the end of the list + * - Deletions while retaining order + * - Unknown size requirements + * - No resize performance risk + * + * TYPE SYSTEM IMPLICATIONS: + * ======================== + * Each ArrayList type gets its own static storage array. + * - ArrayList, ArrayList, ArrayList = 3 separate arrays + * - Each type can hold up to MAX_ARRAY_SIZE elements total across all instances + * + * Choose wisely based on how many types you have. + * + * PERFORMANCE RULES: + * ================== + * + * 1. PRESIZE, DON'T RESIZE + * Bad: new ArrayList() // Might resize multiple times + * Good: new ArrayList(maxSize) // One allocation + * + * Why: Resize operations copy ALL elements to new memory. Expensive! + * + * 2. REUSE, DON'T RECREATE + * Bad: In loop `let temp = new ArrayList() ... destroy temp` + * Good: `let temp = new ArrayList() ... temp.clear()` in loop + * + * Why: Allocation/Deallocation is moderately expensive and causes fragmentation + * + * 3. ORDERED REMOVAL IS SLOW + * Bad: list.removeAt(i) // O(n) - shifts all elements + * Good: list.removeSwap(i) // O(1) - swaps with last element + * + * Only use removeSwap() if order doesn't matter + * + * 4. ITERATORS ALLOCATE + * Bad: for elem in list: process(elem) // Allocates iterator + * Good: for i = 0 to list.size()-1 // Zero allocation + * + * Use iterators for convenience, index loops for performance + * + * 5. AVOID FREQUENT INSERTS AT START + * Bad: list.addtoStart(x) // O(n) every time + * Good: Use LinkedList or add in reverse order + * + * PERFORMANCE TABLE: + * ================== + * Operation | ArrayList | LinkedList | Notes + * -------------------|-----------|------------|--------------------------- + * add(elem) | O(1)* | O(1) | *O(n) on resize! + * addtoStart(elem) | O(n) | O(1) | Shifts all elements + * get(index) | O(1) | O(n) | Major ArrayList advantage + * removeAt(index) | O(n) | O(1) | Shifts remaining elements + * removeSwap(index) | O(1) | N/A | Doesn't preserve order + * Iterate all | Faster | Fast | LinkedList does double the work, but is still fast + * Memory per element | 1 slot | 3 slots | Element + 2 pointers + * Create/destroy | Varies | High | AL might need memory management, LL needs to process Nodes + * + * MEMORY MANAGEMENT: + * ================== + * ArrayList uses section allocation in a shared static array per type. + * Destroyed lists return sections to a free pool for reuse. + * Free sections are compacted to reduce fragmentation. + * + * Fragmentation occurs when lists grow - the old section becomes a gap. + * This is why presizing matters: growth = copy to new location = wasted space. + * + * Hard limit: MAX_ARRAY_SIZE total slots per type across all instances. + **/ +public class ArrayList + private static T array store + private static int nextFreeIndex = 0 + + // Memory management structures + private static constant int MAX_FREE_SECTIONS = 256 + private static int array freeSectionStart + private static int array freeSectionCapacity + private static int freeSectionCount = 0 + + private int startIndex + private int capacity + private int size = 0 + private static constant int INITIAL_CAPACITY = 16 + + static function getNextFreeIndex() returns int + return nextFreeIndex + + /** Creates a new empty list with default capacity (16) */ + construct() + allocateStorage(INITIAL_CAPACITY) + + /** Creates a new list with specified initial capacity - RECOMMENDED for performance */ + construct(int initialCapacity) + allocateStorage(initialCapacity) + + /** Creates a new list by copying all elements from another list */ + construct(thistype base) + allocateStorage(base.size > INITIAL_CAPACITY ? base.size : INITIAL_CAPACITY) + for elem in base + add(elem) + + /** Allocates storage section - tries to reuse freed sections first */ + private function allocateStorage(int cap) + // Try to find a freed section that fits + for i = 0 to freeSectionCount - 1 + if freeSectionCapacity[i] >= cap + startIndex = freeSectionStart[i] + capacity = freeSectionCapacity[i] + + // Remove this section from free list + for j = i to freeSectionCount - 2 + freeSectionStart[j] = freeSectionStart[j + 1] + freeSectionCapacity[j] = freeSectionCapacity[j + 1] + freeSectionCount-- + return + + // No suitable free section, allocate new + if nextFreeIndex + cap > MAX_ARRAY_SIZE + // Try to compact free sections + compactFreeList() + + if nextFreeIndex + cap > MAX_ARRAY_SIZE + // Still not enough, wrap around (dangerous!) + print("ArrayList: WARNING - Memory store exhausted (" + MAX_ARRAY_SIZE.toString() + "), wrapping around!") + nextFreeIndex = 0 + + startIndex = nextFreeIndex + capacity = cap + nextFreeIndex += cap + + /** Compacts the free list by merging adjacent sections */ + private static function compactFreeList() + if freeSectionCount <= 1 + return + + // Sort free sections by start index using insertion sort + for i = 1 to freeSectionCount - 1 + let keyStart = freeSectionStart[i] + let keyCap = freeSectionCapacity[i] + var j = i - 1 + + while j >= 0 and freeSectionStart[j] > keyStart + freeSectionStart[j + 1] = freeSectionStart[j] + freeSectionCapacity[j + 1] = freeSectionCapacity[j] + j-- + + freeSectionStart[j + 1] = keyStart + freeSectionCapacity[j + 1] = keyCap + + // Merge adjacent sections + var writeIdx = 0 + for readIdx = 0 to freeSectionCount - 1 + if writeIdx > 0 and freeSectionStart[writeIdx - 1] + freeSectionCapacity[writeIdx - 1] == freeSectionStart[readIdx] + // Merge with previous + freeSectionCapacity[writeIdx - 1] += freeSectionCapacity[readIdx] + else + // Keep as separate section + if writeIdx != readIdx + freeSectionStart[writeIdx] = freeSectionStart[readIdx] + freeSectionCapacity[writeIdx] = freeSectionCapacity[readIdx] + writeIdx++ + + freeSectionCount = writeIdx + + // Update nextFreeIndex if last section extends to it + if freeSectionCount > 0 + let lastIdx = freeSectionCount - 1 + if freeSectionStart[lastIdx] + freeSectionCapacity[lastIdx] == nextFreeIndex + nextFreeIndex = freeSectionStart[lastIdx] + freeSectionCount-- + + /** Frees this list's storage section for reuse */ + private function freeStorage() + if capacity <= 0 + return + + // Add to free list if there's space + if freeSectionCount < MAX_FREE_SECTIONS + freeSectionStart[freeSectionCount] = startIndex + freeSectionCapacity[freeSectionCount] = capacity + freeSectionCount++ + + // If this was at the end, we can reclaim it immediately + if startIndex + capacity == nextFreeIndex + nextFreeIndex = startIndex + freeSectionCount-- + else + // Free list full, try to compact + compactFreeList() + + // Try again after compaction + if freeSectionCount < MAX_FREE_SECTIONS + freeSectionStart[freeSectionCount] = startIndex + freeSectionCapacity[freeSectionCount] = capacity + freeSectionCount++ + + /** Grows the capacity (doubles it) - EXPENSIVE OPERATION! */ + private function grow() + let newCapacity = capacity * 2 + let oldStart = startIndex + let oldCapacity = capacity + + // Try to allocate new section + allocateStorage(newCapacity) + + // Copy elements to new location + for i = 0 to size - 1 + store[startIndex + i] = store[oldStart + i] + + // Free old section + let tempStart = startIndex + let tempCap = capacity + startIndex = oldStart + capacity = oldCapacity + freeStorage() + startIndex = tempStart + capacity = tempCap + + ondestroy + // Clear references to allow garbage collection + for i = 0 to size - 1 + store[startIndex + i] = null + + // Return storage to free pool + freeStorage() + + /** Debug function to get memory layout info */ + function getMemoryInfo() returns string + return "Start: " + startIndex.toString() + ", Capacity: " + capacity.toString() + ", Size: " + size.toString() + + /** Static function to get global memory state */ + static function getGlobalMemoryInfo() returns string + return "NextFree: " + nextFreeIndex.toString() + ", FreeSections: " + freeSectionCount.toString() + ", Used: " + (nextFreeIndex - freeSectionCount).toString() + + // ============================================================================ + // BASIC OPERATIONS + // ============================================================================ + + /** Adds one or more elements to the end of the list (amortized O(1)) */ + function add(vararg T elems) + for elem in elems + if size >= capacity + grow() + store[startIndex + size] = elem + size++ + + /** Adds all elements from another list */ + function addAll(ArrayList other) + // Optimize: pre-grow if needed + let needed = size + other.size + while needed > capacity + grow() + + for elem in other + store[startIndex + size] = elem + size++ + + /** Returns the element at the specified index (O(1)) */ + function get(int index) returns T + if index < 0 or index >= size + print("ArrayList: Index out of bounds: " + index.toString()) + return store[startIndex + index] + + /** Sets the element at the specified index (O(1)) */ + function set(int index, T elem) + if index < 0 or index >= size + print("ArrayList: Index out of bounds: " + index.toString()) + store[startIndex + index] = elem + + /** Returns the index of the specified element or -1 if it doesn't exist (O(n)) */ + function indexOf(T elem) returns int + for i = 0 to size - 1 + if store[startIndex + i] == elem + return i + return -1 + + /** Returns whether the list contains the specified element (O(n)) */ + function has(T elem) returns boolean + return indexOf(elem) >= 0 + + /** Removes the element at the given index and returns it (O(n) - shifts elements) */ + function removeAt(int index) returns T + if index < 0 or index >= size + print("ArrayList: Index out of bounds: " + index.toString()) + + let elem = store[startIndex + index] + + // Shift elements left + for i = index to size - 2 + store[startIndex + i] = store[startIndex + i + 1] + + size-- + return elem + + /** Removes the element at the given index by swapping with last element (O(1) - DOES NOT PRESERVE ORDER!) */ + function removeSwap(int index) returns T + if index < 0 or index >= size + print("ArrayList: Index out of bounds: " + index.toString()) + + let elem = store[startIndex + index] + + // Replace with last element + size-- + if index < size + store[startIndex + index] = store[startIndex + size] + + return elem + + /** Removes the first occurrence of the element from the list (O(n)) */ + function remove(T elem) returns bool + let index = indexOf(elem) + if index >= 0 + removeAt(index) + return true + return false + + /** Returns the size of the list (O(1)) */ + function size() returns int + return size + + /** Checks whether this list is empty (O(1)) */ + function isEmpty() returns boolean + return size == 0 + + /** Returns the first element in the list (O(1)) */ + function getFirst() returns T + if size == 0 + print("ArrayList: getFirst on empty list") + return store[startIndex] + + /** Returns the last element in the list (O(1)) */ + function getLast() returns T + if size == 0 + print("ArrayList: getLast on empty list") + return store[startIndex + size - 1] + + /** Clears all elements from the list (O(1) - reuse this list instead of creating new ones!) */ + function clear() + size = 0 + + /** Returns a shallow copy of this list */ + function copy() returns ArrayList + let list = new ArrayList(size) + for i = 0 to size - 1 + list.add(store[startIndex + i]) + return list + + /** Replaces the first occurrence of 'whichElement' with 'newElement' */ + function replace(T whichElement, T newElement) returns boolean + let index = indexOf(whichElement) + if index >= 0 + set(index, newElement) + return true + return false + + /** Returns a random element from this list or null if empty */ + function getRandomElement() returns T + if size == 0 + return null + return get(GetRandomInt(0, size - 1)) + + // ============================================================================ + // STACK OPERATIONS (LIFO) + // ============================================================================ + + /** Adds an element to the end of the list (stack push) */ + function push(T elem) + add(elem) + + /** Returns and removes the last added element (LIFO) */ + function pop() returns T + if size == 0 + return null + size-- + return store[startIndex + size] + + /** Returns the lastly added element without removing it */ + function peek() returns T + return getLast() + + // ============================================================================ + // QUEUE OPERATIONS (FIFO) + // ============================================================================ + + /** Adds an element to the end (queue enqueue) */ + function enqueue(T elem) + add(elem) + + /** Returns and removes the first element (FIFO) - WARNING: O(n) operation! */ + function dequeue() returns T + if size == 0 + return null + return removeAt(0) + + // ============================================================================ + // INSERTION OPERATIONS + // ============================================================================ + + /** Adds element at the beginning of the list - WARNING: O(n) operation! */ + function addtoStart(T elem) + if size >= capacity + grow() + + // Shift all elements right + for i = size - 1 downto 0 + store[startIndex + i + 1] = store[startIndex + i] + + store[startIndex] = elem + size++ + + /** Adds the given element at the given index - WARNING: O(n) operation! */ + function addAt(T elem, int index) + if index < 0 or index > size + print("ArrayList: Index out of bounds: " + index.toString()) + + if size >= capacity + grow() + + // Shift elements right + for i = size - 1 downto index + store[startIndex + i + 1] = store[startIndex + i] + + store[startIndex + index] = elem + size++ + + // ============================================================================ + // ITERATOR & FUNCTIONAL OPERATIONS + // ============================================================================ + + /** Get an iterator for this list - NOTE: Creates an object, use index loops in hot paths! */ + function iterator() returns ALIterator + return new ALIterator(this) + + /** Removes elements that satisfy the predicate */ + function removeIf(ArrayListPredicate predicate) + let itr = iterator() + for elem from itr + if predicate.isTrueFor(elem) + itr.remove() + itr.close() + destroy predicate + + /** Executes the closure for each element */ + function forEach(ALItrClosure itr) returns ArrayList + for i = 0 to size - 1 + itr.run(store[startIndex + i]) + destroy itr + return this + + /** Updates all elements */ + function updateAll(ArrayListUpdater f) + for i = 0 to size - 1 + store[startIndex + i] = f.update(store[startIndex + i]) + destroy f + + /** Returns the list obtained by applying the given closure to each element */ + function map(MapClosure itr) returns ArrayList + let output = new ArrayList(size) + forEach(t -> output.add(itr.run(t))) + destroy itr + return output + + /** Returns a new list of elements that satisfy the predicate */ + function filter(ArrayListPredicate predicate) returns ArrayList + let result = new ArrayList() + for i = 0 to size - 1 + let elem = store[startIndex + i] + if predicate.isTrueFor(elem) + result.add(elem) + destroy predicate + return result + + /** Folds this list into a single value of type Q */ + function foldl(Q startValue, FoldClosure predicate) returns Q + var result = startValue + for i = 0 to size - 1 + result = predicate.run(store[startIndex + i], result) + destroy predicate + return result + + /** Returns the first element that satisfies the predicate, or null if none present */ + function find(ArrayListPredicate predicate) returns T + T result = null + for i = 0 to size - 1 + let elem = store[startIndex + i] + if predicate.isTrueFor(elem) + result = elem + break + destroy predicate + return result + + // ============================================================================ + // SORTING & SHUFFLING + // ============================================================================ + + /** Performs a Fisher-Yates shuffle on this list */ + function shuffle() + for i = size - 1 downto 1 + let j = GetRandomInt(0, i) + let tmp = store[startIndex + i] + store[startIndex + i] = store[startIndex + j] + store[startIndex + j] = tmp + + /** Sorts the list using optimized quicksort with median-of-three pivot */ + function sortWith(Comparator comparator) + if comparator != null and size > 1 + quicksort(comparator, 0, size - 1) + + /** Optimized quicksort with median-of-three pivot selection */ + private function quicksort(Comparator comparator, int low, int high) + if low < high + let pivot = medianOfThree(comparator, low, low + (high - low) div 2, high) + let p = partition(comparator, low, high, pivot) + + quicksort(comparator, low, p - 1) + quicksort(comparator, p + 1, high) + + /** Median-of-three pivot selection */ + private function medianOfThree(Comparator comparator, int a, int b, int c) returns int + let va = store[startIndex + a] + let vb = store[startIndex + b] + let vc = store[startIndex + c] + + if comparator.compare(va, vb) < 0 + if comparator.compare(vb, vc) < 0 + return b + else if comparator.compare(va, vc) < 0 + return c + else + return a + else + if comparator.compare(va, vc) < 0 + return a + else if comparator.compare(vb, vc) < 0 + return c + else + return b + + /** Optimized partition with median pivot */ + private function partition(Comparator comparator, int low, int high, int pivotIndex) returns int + let pivotValue = store[startIndex + pivotIndex] + + // Move pivot to end + let temp = store[startIndex + pivotIndex] + store[startIndex + pivotIndex] = store[startIndex + high] + store[startIndex + high] = temp + + var storeIndex = low + + for i = low to high - 1 + if comparator.compare(store[startIndex + i], pivotValue) < 0 + let t = store[startIndex + storeIndex] + store[startIndex + storeIndex] = store[startIndex + i] + store[startIndex + i] = t + storeIndex++ + + // Move pivot to final position + let t2 = store[startIndex + storeIndex] + store[startIndex + storeIndex] = store[startIndex + high] + store[startIndex + high] = t2 + + return storeIndex + +// ============================================================================ +// UTILITY FUNCTIONS +// ============================================================================ + +public function asArrayList(vararg T ts) returns ArrayList + let al = new ArrayList() + for t in ts + al.add(t) + return al + +// ============================================================================ +// ITERATOR +// ============================================================================ + +/** Iterator for ArrayList - NOTE: Allocates an object, prefer index loops in hot paths! */ +public class ALIterator + private ArrayList parent + private int currentIndex + private bool destroyOnClose = true + private bool canRemove = false + + construct(ArrayList parent) + this.parent = parent + reset() + + construct(ArrayList parent, bool destroyOnClose) + this.parent = parent + this.destroyOnClose = destroyOnClose + reset() + + function reset() + currentIndex = -1 + canRemove = false + + function hasNext() returns boolean + return currentIndex + 1 < parent.size() + + function next() returns T + currentIndex++ + canRemove = true + return parent.get(currentIndex) + + function lookahead() returns T + if currentIndex + 1 < parent.size() + return parent.get(currentIndex + 1) + return null + + /** Removes the last element returned by next() */ + function remove() returns T + if not canRemove or currentIndex < 0 + return null + + let removed = parent.removeSwap(currentIndex) + currentIndex-- + canRemove = false + return removed + + /** Modifies the last element returned by next() */ + function modify(T newval) + if canRemove and currentIndex >= 0 + parent.set(currentIndex, newval) + + function close() + if destroyOnClose + destroy this + +// ============================================================================ +// INTERFACES +// ============================================================================ + +public interface ArrayListPredicate + function isTrueFor(T t) returns boolean + +public interface ALItrClosure + function run(T t) + +public interface ArrayListUpdater + function update(T t) returns T + +public interface MapClosure + function run(T t) returns Q + +public interface FoldClosure + function run(T t, Q q) returns Q + +public interface Comparator + function compare(T o1, T o2) returns int + +// ============================================================================ +// SPECIALIZED SORT FUNCTIONS +// ============================================================================ + +constant Comparator intComparator = (i1, i2) -> i1 - i2 +public function ArrayList.sort() + this.sortWith(intComparator) + +constant Comparator realComparator = (r1, r2) -> (r1 - r2).toInt() +public function ArrayList.sort() + this.sortWith(realComparator) + +constant Comparator stringComparator = (s1, s2) -> stringCompare(s1, s2) +public function ArrayList.sort() + this.sortWith(stringComparator) + +// ============================================================================ +// STRING OPERATIONS +// ============================================================================ + +/** Joins elements from a string list into one string using a separator */ +public function ArrayList.joinBy(string separator) returns string + var joined = "" + for i = 0 to this.size() - 1 + if i > 0 + joined += separator + joined += this.get(i) + return joined + +/** Joins elements from a string list into one string */ +public function ArrayList.join() returns string + return this.joinBy("") + +native testFail(string message) + +// Hard fail helper (no prints) +function fail(string msg) + testFail("ArrayList generics test: " + msg) + +function assertTrue(bool cond, string msg) + if not cond + fail(msg) + +function assertEqInt(int a, int b, string msg) + if a != b + fail(msg + " expected=" + b.toString() + " got=" + a.toString()) + +function assertEqReal(real a, real b, string msg) + if a != b + fail(msg + " expected=" + b.toString() + " got=" + a.toString()) + +function assertEqStr(string a, string b, string msg) + if a != b + fail(msg + " expected=\"" + b + "\" got=\"" + a + "\"") + +init + // ========================================================================= + // Goal: catch "shared static bookkeeping across specializations" WITHOUT + // static calls. We do it by interleaving allocations/frees across types and + // verifying that each list's contents remain correct (no overwrites/gaps). + // ========================================================================= + + // 1) Interleaved creation + fill with type-specific sentinels + let iA = new ArrayList(64) + let rA = new ArrayList(64) + let sA = new ArrayList(64) + + for k = 0 to 63 + iA.add(100000 + k) // ints: 100000..100063 + rA.add((200000 + k).toReal()) // reals: 200000..200063 + sA.add("S" + k.toString()) // strings: S0..S63 + + for k = 0 to 63 + assertEqInt(iA.get(k), 100000 + k, "phase1 int mismatch at " + k.toString()) + assertEqReal(rA.get(k), (200000 + k).toReal(), "phase1 real mismatch at " + k.toString()) + assertEqStr(sA.get(k), "S" + k.toString(), "phase1 string mismatch at " + k.toString()) + + // 2) Create/destroy many lists of ONE type to exercise free-section metadata. + // If freeSectionStart/freeSectionCapacity/freeSectionCount are shared across types, + // later allocations of another type can start reusing wrong "free sections" + // and overwrite other type's store. + let churnCount = 40 + + for t = 0 to churnCount - 1 + let tmp = new ArrayList(32) + for k = 0 to 31 + tmp.add(9000 + t * 100 + k) + // quick sanity on tmp + assertEqInt(tmp.get(0), 9000 + t * 100, "tmp int mismatch t=" + t.toString()) + assertEqInt(tmp.get(31), 9000 + t * 100 + 31, "tmp int mismatch end t=" + t.toString()) + destroy tmp + + // After int churn, the real/string lists must remain intact + for k = 0 to 63 + assertEqReal(rA.get(k), (200000 + k).toReal(), "after int churn real corrupted at " + k.toString()) + assertEqStr(sA.get(k), "S" + k.toString(), "after int churn string corrupted at " + k.toString()) + + // 3) Now churn REAL lists, then check int/string still intact + for t = 0 to churnCount - 1 + let tmp = new ArrayList(33) + for k = 0 to 32 + tmp.add((7000 + t * 100 + k).toReal()) + assertEqReal(tmp.get(0), (7000 + t * 100).toReal(), "tmp real mismatch t=" + t.toString()) + assertEqReal(tmp.get(32), (7000 + t * 100 + 32).toReal(), "tmp real mismatch end t=" + t.toString()) + destroy tmp + + for k = 0 to 63 + assertEqInt(iA.get(k), 100000 + k, "after real churn int corrupted at " + k.toString()) + assertEqStr(sA.get(k), "S" + k.toString(), "after real churn string corrupted at " + k.toString()) + + // 4) Now churn STRING lists, then check int/real still intact + for t = 0 to churnCount - 1 + let tmp = new ArrayList(31) + for k = 0 to 30 + tmp.add("T" + t.toString() + "_" + k.toString()) + assertEqStr(tmp.get(0), "T" + t.toString() + "_0", "tmp string mismatch t=" + t.toString()) + assertEqStr(tmp.get(30), "T" + t.toString() + "_30", "tmp string mismatch end t=" + t.toString()) + destroy tmp + + for k = 0 to 63 + assertEqInt(iA.get(k), 100000 + k, "after string churn int corrupted at " + k.toString()) + assertEqReal(rA.get(k), (200000 + k).toReal(), "after string churn real corrupted at " + k.toString()) + + // 5) Mixed-size allocate/free pattern: encourages fragmentation + compaction paths. + // If free-list metadata is shared, one type can “compact” another type’s free list. + let iB = new ArrayList(8) + let iC = new ArrayList(128) + let rB = new ArrayList(9) + let sB = new ArrayList(10) + + for k = 0 to 7 + iB.add(300000 + k) + for k = 0 to 127 + iC.add(400000 + k) + for k = 0 to 8 + rB.add((500000 + k).toReal()) + for k = 0 to 9 + sB.add("X" + k.toString()) + + destroy iB + destroy rB + // allocate again to reuse freed sections + let iD = new ArrayList(7) + let rC = new ArrayList(8) + + for k = 0 to 6 + iD.add(600000 + k) + for k = 0 to 7 + rC.add((700000 + k).toReal()) + + // Verify older long list still correct (would be overwritten if reuse crosses types) + assertEqInt(iC.get(0), 400000, "iC corrupted at 0") + assertEqInt(iC.get(127), 400127, "iC corrupted at end") + + // Verify original main lists still correct + for k = 0 to 63 + assertEqInt(iA.get(k), 100000 + k, "final int corrupted at " + k.toString()) + assertEqReal(rA.get(k), (200000 + k).toReal(), "final real corrupted at " + k.toString()) + assertEqStr(sA.get(k), "S" + k.toString(), "final string corrupted at " + k.toString()) + + // Verify new allocations correct + for k = 0 to 6 + assertEqInt(iD.get(k), 600000 + k, "iD mismatch at " + k.toString()) + for k = 0 to 7 + assertEqReal(rC.get(k), (700000 + k).toReal(), "rC mismatch at " + k.toString()) + for k = 0 to 9 + assertEqStr(sB.get(k), "X" + k.toString(), "sB mismatch at " + k.toString()) + + // Cleanup + destroy iD + destroy rC + destroy iC + destroy sB + destroy iA + destroy rA + destroy sA + + testSuccess() diff --git a/de.peeeq.wurstscript/testscripts/realbugs/module.wurst b/de.peeeq.wurstscript/testscripts/realbugs/module.wurst index 96ce37668..92f4409ce 100644 --- a/de.peeeq.wurstscript/testscripts/realbugs/module.wurst +++ b/de.peeeq.wurstscript/testscripts/realbugs/module.wurst @@ -195,7 +195,7 @@ package Entity function setTarget( vec3 tpos, real speed ) var t = pos.distanceTo2d(tpos) / speed let tangle = pos.angleTo2d(tpos) - let e = getTerrainZ(tpos.x,tpos.y) + let e = getTerrainZ2(tpos.x,tpos.y) if t < 1. t = 1./speed @@ -237,7 +237,7 @@ package Entity override function setPos(vec3 tpos) pos = tpos actor.setPos(tpos.x,tpos.y) - actor.setFlyHeight(tpos.z - getTerrainZ(tpos.x, tpos.y), 0) + actor.setFlyHeight(tpos.z - getTerrainZ2(tpos.x, tpos.y), 0) function setXY(vec3 tpos) pos = tpos @@ -462,16 +462,16 @@ public function getTerrainZ(vec2 v) returns real return GetLocationZ( tempLoc ) public function vec2.withTerrainZ() returns vec3 - return vec3(this.x, this.y, getTerrainZ(this.x, this.y)) + return vec3(this.x, this.y, getTerrainZ2(this.x, this.y)) public function vec2.withTerrainZ(real zoffset) returns vec3 - return vec3(this.x, this.y, getTerrainZ(this.x, this.y) + zoffset) + return vec3(this.x, this.y, getTerrainZ2(this.x, this.y) + zoffset) public function vec3.withTerrainZ() returns vec3 - return vec3(this.x, this.y, getTerrainZ(this.x, this.y)) + return vec3(this.x, this.y, getTerrainZ2(this.x, this.y)) public function vec3.withTerrainZ(real zoffset) returns vec3 - return vec3(this.x, this.y, getTerrainZ(this.x, this.y) + zoffset) + return vec3(this.x, this.y, getTerrainZ2(this.x, this.y) + zoffset) public function isTerrainDeepWater(real x, real y) returns boolean return not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY) and IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY)