From 68d03567ea1d351490b8e4516d6ebeb4f9a70cde Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 2 Oct 2025 21:21:10 +0200 Subject: [PATCH 1/4] array list impl with new lang feature: static generic member --- .editorconfig | 2 +- wurst/data/ArrayList.wurst | 707 ++++++++++++++++++++++++++ wurst/data/ArrayListMemoryTests.wurst | 504 ++++++++++++++++++ wurst/data/ArrayListTests.wurst | 206 ++++++++ 4 files changed, 1418 insertions(+), 1 deletion(-) create mode 100644 wurst/data/ArrayList.wurst create mode 100644 wurst/data/ArrayListMemoryTests.wurst create mode 100644 wurst/data/ArrayListTests.wurst diff --git a/.editorconfig b/.editorconfig index 72b66b056..25c767dfb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ [*.wurst] charset = utf-8 -indent_style = tab +indent_style = space indent_size = 4 end_of_line = lf trim_trailing_whitespace = true diff --git a/wurst/data/ArrayList.wurst b/wurst/data/ArrayList.wurst new file mode 100644 index 000000000..354519e37 --- /dev/null +++ b/wurst/data/ArrayList.wurst @@ -0,0 +1,707 @@ +package ArrayList +import NoWurst +import Integer +import String +import Printing +import Real + +/** + * High-performance array-based list implementation using a static shared store. + * + * WHEN TO USE ArrayList vs LinkedList: + * ==================================== + * + * ArrayList shines in these specific cases: + * 1. Iterating over large lists (hundreds+ elements) - no node indirection overhead + * 2. Adding many elements in tight loops - reduced allocation/deallocation churn + * 3. Random access patterns - O(1) get/set operations + * 4. Memory-sensitive scenarios where node overhead matters + * + * Use LinkedList when you need: + * - Frequent insertions/deletions in middle of list + * - Flexibility without preallocating capacity + * - No risk of costly resize operations + * - Simpler memory management + * + * CRITICAL PERFORMANCE CONSIDERATIONS: + * ==================================== + * + * ⚠️ PRESIZE YOUR LISTS! ⚠️ + * The single most important thing: estimate your maximum size and add headroom. + * Example: new ArrayList(200) if you expect ~150 elements max + * + * Why? If the list resizes, ALL elements are copied to a new memory location, + * negating any performance advantage over LinkedList! + * + * PERFORMANCE COMPARISON TABLE: + * ============================ + * + * Operation | ArrayList | LinkedList | Notes + * -------------------|----------------|----------------|--------------------------- + * Add to end | O(1)* | O(1) | *O(n) on resize! + * Add to start | O(n) | O(1) | ArrayList shifts all elements + * Insert at index | O(n) | O(n) | Both require traversal + * Remove (ordered) | O(n) | O(1) | ArrayList shifts elements + * Remove (unordered) | O(1) | O(1) | Use removeSwap() for ArrayList + * Get by index | O(1) | O(n) | ArrayList major advantage + * Iterate all | Fast | Slower | ArrayList: cache-friendly, no indirection + * Search | O(n) | O(n) | Same, but ArrayList faster due to cache + * Create/Destroy | Medium cost | High cost | ArrayList: 1 alloc, LinkedList: n allocs + * Memory overhead | Low | High | LinkedList: 2 pointers per element + * + * COMMON PITFALLS: + * =============== + * + * 1. NOT PRESIZING + * Bad: let list = new ArrayList() // Will resize multiple times! + * Good: let list = new ArrayList(estimatedMaxSize + headroom) + * + * 2. CREATING MANY TEMPORARY LISTS + * Bad: In a loop: let temp = new ArrayList() ... destroy temp + * Good: Create once, reuse with clear(): temp.clear() + * + * 3. ORDERED REMOVAL IN LOOPS + * Bad: for i in list: list.remove(x) // O(n²) with shifting! + * Good: Use removeIf() or removeSwap() if order doesn't matter + * + * 4. USING FOR-IN WHEN NOT NEEDED + * Bad: for elem in list: doSomething(elem) // Creates iterator + * Good: for i = 0 to list.size() - 1: doSomething(list.get(i)) // No allocation + * + * 5. FREQUENT INSERTIONS AT START + * Bad: Repeatedly using addtoStart() - O(n) every time! + * Good: Use LinkedList, or collect and add in reverse order + * + * BEST PRACTICES: + * ============== + * + * ✓ Estimate capacity: measure actual usage, then add 20-30% headroom + * ✓ Reuse lists: clear() and reuse instead of destroy/create + * ✓ Use removeSwap() when order doesn't matter (O(1) vs O(n)) + * ✓ Prefer index loops over for-in loops for hot paths + * ✓ Profile your code: only use ArrayList where it actually helps! + * + * MEMORY MANAGEMENT: + * ================= + * + * ArrayList uses a shared static array store with section allocation. + * Destroyed lists return their sections to a free pool for reuse. + * The system automatically compacts fragmented free sections. + * + * Warning: The static store has a hard limit (JASS_MAX_ARRAY_SIZE). + * If you create many large lists simultaneously, you may exhaust it. + */ +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 > JASS_MAX_ARRAY_SIZE + // Try to compact free sections + compactFreeList() + + if nextFreeIndex + cap > JASS_MAX_ARRAY_SIZE + // Still not enough, wrap around (dangerous!) + print("ArrayList: WARNING - Memory store exhausted, 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.removeAt(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("") diff --git a/wurst/data/ArrayListMemoryTests.wurst b/wurst/data/ArrayListMemoryTests.wurst new file mode 100644 index 000000000..d31a92390 --- /dev/null +++ b/wurst/data/ArrayListMemoryTests.wurst @@ -0,0 +1,504 @@ +package ArrayListMemoryTests +import ArrayList + +// ============================================================================ +// SIMPLIFIED MEMORY VISUALIZATION +// ============================================================================ + +/** Simple memory visualizer that tracks lists manually */ +class MemoryVisualizer + private static int array starts + private static int array capacities + private static string array names + private static int count = 0 + + static function reset() + count = 0 + + static function track(int start, int capacity, string name) + starts[count] = start + capacities[count] = capacity + names[count] = name + count++ + + static function visualize(int maxIndex) returns string + if maxIndex == 0 + return "[Empty memory]\n" + + // Build allocation map + string array allocationMap + for i = 0 to maxIndex - 1 + allocationMap[i] = "░" // Free space + + // Mark tracked allocations + for idx = 0 to count - 1 + let start = starts[idx] + let cap = capacities[idx] + let name = names[idx] + let marker = name.substring(0, 1) + + for i = start to start + cap - 1 + if i < maxIndex + allocationMap[i] = marker + + var result = "\nMemory Layout [0-" + (maxIndex - 1).toString() + "]:\n" + + // Print in rows of 64 for readability + let rowSize = 64 + var row = 0 + while row * rowSize < maxIndex + let startIdx = row * rowSize + let endIdx = startIdx + rowSize + + result += "[" + // Print row label + if startIdx < 10 + result += "0" + if startIdx < 100 + result += "0" + if startIdx < 1000 + result += "0" + result += startIdx.toString() + "] -> |" + + for i = startIdx to endIdx - 1 + if i < maxIndex + result += allocationMap[i] + else + break + + result += "|\n" + row++ + + result += "\n" + + // Legend + for idx = 0 to count - 1 + result += names[idx].substring(0, 1) + "=" + names[idx] + " " + result += "░=free\n" + result += ArrayList.getGlobalMemoryInfo() + "\n" + + return result + +// Helper to extract start index from memory info string +function extractStart(string memInfo) returns int + // "Start: 123, Capacity: 456, Size: 789" + // Find "Start: " and parse number after it + let startPos = memInfo.indexOf("Start: ") + 7 + var endPos = startPos + let len = memInfo.length() + + while endPos < len + let char = memInfo.substring(endPos, endPos + 1) + if char == "," + break + endPos++ + + let numStr = memInfo.substring(startPos, endPos) + return numStr.toInt() + +function extractCapacity(string memInfo) returns int + let startPos = memInfo.indexOf("Capacity: ") + 10 + var endPos = startPos + let len = memInfo.length() + + while endPos < len + let char = memInfo.substring(endPos, endPos + 1) + if char == "," + break + endPos++ + + let numStr = memInfo.substring(startPos, endPos) + return numStr.toInt() + +function extractNextFree() returns int + let info = ArrayList.getGlobalMemoryInfo() + let startPos = info.indexOf("NextFree: ") + 10 + var endPos = startPos + let len = info.length() + + while endPos < len + let char = info.substring(endPos, endPos + 1) + if char == "," + break + endPos++ + + let numStr = info.substring(startPos, endPos) + return numStr.toInt() + +// ============================================================================ +// MEMORY TESTS +// ============================================================================ + +@Test +function testBasicMemoryAllocation() + print("\n=== TEST: Basic Memory Allocation ===") + + MemoryVisualizer.reset() + + let list1 = new ArrayList(10) + let info1 = list1.getMemoryInfo() + MemoryVisualizer.track(extractStart(info1), extractCapacity(info1), "1") + + let list2 = new ArrayList(20) + let info2 = list2.getMemoryInfo() + MemoryVisualizer.track(extractStart(info2), extractCapacity(info2), "2") + + let list3 = new ArrayList(15) + let info3 = list3.getMemoryInfo() + MemoryVisualizer.track(extractStart(info3), extractCapacity(info3), "3") + + print(MemoryVisualizer.visualize(extractNextFree())) + + info1.contains("Start: 0").assertTrue() + info2.contains("Start: 10").assertTrue() + info3.contains("Start: 30").assertTrue() + + destroy list1 + destroy list2 + destroy list3 + +@Test +function testMemoryReclamation() + print("\n=== TEST: Memory Reclamation ===") + + // Clear any previous state by getting current free index + let initialFree = extractNextFree() + print("Initial NextFree: " + initialFree.toString()) + + MemoryVisualizer.reset() + let list1 = new ArrayList(50) + let list1Start = extractStart(list1.getMemoryInfo()) + let list1Cap = extractCapacity(list1.getMemoryInfo()) + MemoryVisualizer.track(list1Start, list1Cap, "List1") + print("After allocation:") + let nextFree = extractNextFree() + print(MemoryVisualizer.visualize(nextFree)) + print("List1 info: " + list1.getMemoryInfo()) + + destroy list1 + + MemoryVisualizer.reset() + let list2 = new ArrayList(30) + let list2Start = extractStart(list2.getMemoryInfo()) + let list2Cap = extractCapacity(list2.getMemoryInfo()) + MemoryVisualizer.track(list2Start, list2Cap, "List2-reused") + print("After reclamation:") + print(MemoryVisualizer.visualize(nextFree)) + print("List2 info: " + list2.getMemoryInfo()) + + // List2 should reuse list1's memory location + print("Checking: list2Start (" + list2Start.toString() + ") == list1Start (" + list1Start.toString() + ")") + list2Start.assertEquals(list1Start) + + // Capacity should be at least 30, but might be 50 if reusing the full section + (list2Cap >= 30).assertTrue() + + destroy list2 + +@Test +function testMemoryFragmentation() + print("\n=== TEST: Memory Fragmentation ===") + + MemoryVisualizer.reset() + + let list1 = new ArrayList(10) + MemoryVisualizer.track(extractStart(list1.getMemoryInfo()), 10, "A") + + let list2 = new ArrayList(20) + MemoryVisualizer.track(extractStart(list2.getMemoryInfo()), 20, "B") + + let list3 = new ArrayList(15) + MemoryVisualizer.track(extractStart(list3.getMemoryInfo()), 15, "C") + + print("Initial layout:") + print(MemoryVisualizer.visualize(extractNextFree())) + + destroy list2 + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list1.getMemoryInfo()), 10, "A") + MemoryVisualizer.track(extractStart(list3.getMemoryInfo()), 15, "C") + + let list4 = new ArrayList(10) + MemoryVisualizer.track(extractStart(list4.getMemoryInfo()), 10, "D-reused") + + print("After destroying B and creating D:") + print(MemoryVisualizer.visualize(extractNextFree())) + + list4.getMemoryInfo().contains("Start: 10").assertTrue() + + destroy list1 + destroy list3 + destroy list4 + +@Test +function testGrowthMemoryBehavior() + print("\n=== TEST: Growth Memory Behavior ===") + + let list = new ArrayList(4) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 4, "List") + print("Initial (capacity 4):") + print(MemoryVisualizer.visualize(extractNextFree())) + print(list.getMemoryInfo()) + + // Force growth + for i = 1 to 5 + list.add(i) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 8, "List-grown") + print("\nAfter adding 5 elements (grew to 8):") + print(MemoryVisualizer.visualize(extractNextFree())) + print(list.getMemoryInfo()) + + // Force another growth + for i = 6 to 10 + list.add(i) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 16, "List-grown2") + print("\nAfter adding 10 total elements (grew to 16):") + print(MemoryVisualizer.visualize(extractNextFree())) + print(list.getMemoryInfo()) + + list.getMemoryInfo().contains("Size: 10").assertTrue() + + // Verify data integrity + for i = 0 to 9 + list.get(i).assertEquals(i + 1) + + destroy list + +@Test +function testMultipleGrowths() + print("\n=== TEST: Multiple Growths ===") + + let list = new ArrayList(2) + + print("Initial capacity: 2") + print(list.getMemoryInfo()) + + // Track key growth points + var lastSize = 0 + + for i = 1 to 50 + list.add(i) + + // Visualize at powers of 2 + if list.size() == 2 or list.size() == 4 or list.size() == 8 or list.size() == 16 or list.size() == 32 or list.size() == 50 + if list.size() != lastSize + MemoryVisualizer.reset() + let cap = extractCapacity(list.getMemoryInfo()) + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), cap, "List") + print("\nAfter adding " + list.size().toString() + " elements:") + print(MemoryVisualizer.visualize(extractNextFree())) + print(list.getMemoryInfo()) + lastSize = list.size() + + list.size().assertEquals(50) + + // Verify all elements + for i = 0 to 49 + list.get(i).assertEquals(i + 1) + + destroy list + +@Test +function testMemoryCompaction() + print("\n=== TEST: Memory Compaction ===") + + let lists = new ArrayList>() + MemoryVisualizer.reset() + + // Create many small lists + for i = 0 to 10 + let list = new ArrayList(5) + lists.add(list) + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 5, "L" + i.toString()) + + print("Initial layout with 11 lists:") + print(MemoryVisualizer.visualize(extractNextFree())) + + // Destroy every other list + for i = 0 to 5 + destroy lists.get(i * 2) + lists.set(i * 2, null) + + MemoryVisualizer.reset() + for i = 0 to 10 + if lists.get(i) != null + MemoryVisualizer.track(extractStart(lists.get(i).getMemoryInfo()), 5, "L" + i.toString()) + + print("\nAfter destroying every other list:") + print(MemoryVisualizer.visualize(extractNextFree())) + + let globalInfo = ArrayList.getGlobalMemoryInfo() + print("Free sections should be present: " + globalInfo) + + // Clean up + for i = 0 to 10 + if lists.get(i) != null + destroy lists.get(i) + destroy lists + +@Test +function testLargeAllocation() + print("\n=== TEST: Large Allocation ===") + + let list = new ArrayList(1000) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 1000, "LargeList") + print("Allocated 1000 capacity:") + print(MemoryVisualizer.visualize(extractNextFree())) + + for i = 0 to 999 + list.add(i) + + list.size().assertEquals(1000) + list.get(500).assertEquals(500) + list.get(999).assertEquals(999) + + print("\nFilled with 1000 elements:") + print(list.getMemoryInfo()) + + destroy list + +@Test +function testMemoryAfterClear() + print("\n=== TEST: Memory After Clear ===") + + let list = new ArrayList(20) + + for i = 1 to 15 + list.add(i) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 20, "List") + print("Before clear (15 elements):") + print(MemoryVisualizer.visualize(extractNextFree())) + print(list.getMemoryInfo()) + + list.clear() + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 20, "List") + print("\nAfter clear:") + print(MemoryVisualizer.visualize(extractNextFree())) + print(list.getMemoryInfo()) + + list.getMemoryInfo().contains("Size: 0").assertTrue() + + destroy list + +@Test +function testCopyMemoryIndependence() + print("\n=== TEST: Copy Memory Independence ===") + + let list1 = new ArrayList() + list1.add(1, 2, 3, 4, 5) + + let list2 = list1.copy() + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(list1.getMemoryInfo()), extractCapacity(list1.getMemoryInfo()), "Original") + MemoryVisualizer.track(extractStart(list2.getMemoryInfo()), extractCapacity(list2.getMemoryInfo()), "Copy") + print("Original and copy in memory:") + print(MemoryVisualizer.visualize(extractNextFree())) + + // Modify one shouldn't affect the other + list1.set(0, 99) + list2.get(0).assertEquals(1) + list1.get(0).assertEquals(99) + + destroy list1 + destroy list2 + +@Test +function testReusePattern() + print("\n=== TEST: Reuse Pattern (Best Practice) ===") + + let reusableList = new ArrayList(100) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(reusableList.getMemoryInfo()), 100, "Reusable") + + print("Initial allocation:") + print(MemoryVisualizer.visualize(extractNextFree())) + + // Simulate multiple operations with reuse + for iteration = 0 to 3 + for i = 0 to 50 + reusableList.add(i) + + print("\nIteration " + iteration.toString() + " - filled:") + print(reusableList.getMemoryInfo()) + + reusableList.clear() + print("Iteration " + iteration.toString() + " - cleared (memory retained):") + print(reusableList.getMemoryInfo()) + + print("\nFinal state - same memory location throughout:") + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(reusableList.getMemoryInfo()), 100, "Reusable") + print(MemoryVisualizer.visualize(extractNextFree())) + + destroy reusableList + +@Test +function testWorstCaseScenario() + print("\n=== TEST: Worst Case - No Presizing ===") + + let badList = new ArrayList() + + print("Starting with default capacity (16)...") + + var growthCount = 0 + var lastCap = 16 + + for i = 1 to 100 + badList.add(i) + + let currentCap = extractCapacity(badList.getMemoryInfo()) + + if currentCap > lastCap + growthCount++ + lastCap = currentCap + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(badList.getMemoryInfo()), currentCap, "BadList") + print("\nGrowth #" + growthCount.toString() + " occurred at size " + badList.size().toString()) + print(MemoryVisualizer.visualize(extractNextFree())) + print(badList.getMemoryInfo()) + + print("\nTotal growths: " + growthCount.toString()) + print("This is why you should presize your lists!") + + destroy badList + +@Test +function testBestCaseScenario() + print("\n=== TEST: Best Case - Proper Presizing ===") + + let goodList = new ArrayList(100) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(goodList.getMemoryInfo()), 100, "GoodList") + print("Starting with presized capacity (100):") + print(MemoryVisualizer.visualize(extractNextFree())) + print(goodList.getMemoryInfo()) + + for i = 1 to 100 + goodList.add(i) + + MemoryVisualizer.reset() + MemoryVisualizer.track(extractStart(goodList.getMemoryInfo()), 100, "GoodList") + print("\nAfter adding 100 elements - NO GROWTH!") + print(MemoryVisualizer.visualize(extractNextFree())) + print(goodList.getMemoryInfo()) + + goodList.size().assertEquals(100) + + destroy goodList diff --git a/wurst/data/ArrayListTests.wurst b/wurst/data/ArrayListTests.wurst new file mode 100644 index 000000000..a1c742062 --- /dev/null +++ b/wurst/data/ArrayListTests.wurst @@ -0,0 +1,206 @@ +package ArrayListTests +import ArrayList + +@Test +function testForLoop() + let list = new ArrayList() + + list.add(1, 2, 3, 4) + + list.updateAll(i -> i + 1) + + var result = 0 + for i in list + result += i + + result.assertEquals(5 + 4 + 3 + 2) + +@Test +function testAddAll() + let list = new ArrayList() + let list2 = new ArrayList() + + list.add(1, 2) + list2.add(3, 4) + list.addAll(list2) + + list.get(2).assertEquals(3) + list.get(3).assertEquals(4) + + var result = 0 + for i in list + result += i + result.assertEquals(1 + 2 + 3 + 4) + +@Test +function testClosures() + let list = new ArrayList() + list.add(1, 2, 3, 4) + + list.updateAll(i -> i * 2) + list.get(3).assertEquals(4 * 2) + + list.removeIf(i -> i > 4) + list.size().assertEquals(2) + + let realList = list.map(i -> i * 10.) + realList.get(1).assertEquals(40.) + +@Test +function testGenerics() + let list = new ArrayList() + list.add("a", "b", "c") + list.get(1).assertEquals("b") + + let list2 = new ArrayList() + list2.add(1.230, 2.563, 1213143.) + list2.get(0).assertEquals(1.230) + list2.get(1).assertEquals(2.563) + list2.get(2).assertEquals(1213143.) + +@Test +function testSort() + let list = new ArrayList() + for i = 0 to 50 + list.add(GetRandomInt(-100, 100) * 2 + 1) + list.sort() + + for i = 0 to list.size() - 2 + list.get(i).assertLessThanOrEqual(list.get(i + 1)) + +@Test +function testAddAt() + let list = new ArrayList() + for i = 1 to 6 + list.add(i) + list.addAt(7, 4) + string elems = "" + for elem in list + elems += elem.toString() + elems.assertEquals("1234756") + +@Test +function testRemoveWhen() + let list = new ArrayList() + for i = 1 to 6 + list.add(i) + list.removeIf(i -> i < 3) + list.size().assertEquals(4) + list.get(2).assertEquals(5) + +@Test +function testFilter() + var list = new ArrayList() + for i = 1 to 6 + list.add(i) + list = list.filter(i -> i > 3) + list.size().assertEquals(3) + list.get(2).assertEquals(6) + +@Test +function testFoldl() + let list = new ArrayList() + for i = 1 to 6 + list.add(i) + let result = list.foldl(0, (i, q) -> q + i) + result.assertEquals(21) + +@Test +function testSortReal() + let list = new ArrayList() + for i = 6 downto 1 + list.add(i.toReal()) + list.sort() + list.get(0).assertEquals(1.) + list.get(5).assertEquals(6.) + +@Test +function testAsArrayList() + asArrayList(1,2,3,4).foldl(0, (i, q) -> q + i) + .assertEquals(asArrayList(4,3,2,1).foldl(0, (i, q) -> q + i)) + +@Test +function testGetters() + let testlist = new ArrayList() + testlist.add("a") + testlist.add("c") + testlist.add("b") + testlist.getFirst().assertEquals("a") + testlist.get(1).assertEquals("c") + +@Test +function testJoin() + let list = new ArrayList() + ..add("this", "is", "a", "string") + + list.join().assertEquals("thisisastring") + list.joinBy(" ").assertEquals("this is a string") + + destroy list + +@Test +function testQueue() + let list = new ArrayList() + ..enqueue("a") + ..enqueue("b") + ..enqueue("c") + + list.getFirst().assertEquals("a") + list.dequeue().assertEquals("a") + list.getFirst().assertEquals("b") + list.dequeue().assertEquals("b") + list.getFirst().assertEquals("c") + list.dequeue().assertEquals("c") + + destroy list + +@Test +function testStack() + let list = new ArrayList() + ..push("a") + ..push("b") + ..push("c") + + list.peek().assertEquals("c") + list.pop().assertEquals("c") + list.peek().assertEquals("b") + list.pop().assertEquals("b") + list.peek().assertEquals("a") + list.pop().assertEquals("a") + + destroy list + +@Test +function testGrowth() + let list = new ArrayList(2) + for i = 1 to 100 + list.add(i) + list.size().assertEquals(100) + list.get(99).assertEquals(100) + +@Test +function testCopy() + let list = new ArrayList() + list.add(1, 2, 3, 4, 5) + let copy = list.copy() + copy.size().assertEquals(5) + copy.get(2).assertEquals(3) + + copy.set(2, 99) + list.get(2).assertEquals(3) + copy.get(2).assertEquals(99) + +@Test +function testIndexOf() + let list = new ArrayList() + list.add("a", "b", "c") + list.indexOf("b").assertEquals(1) + list.indexOf("x").assertEquals(-1) + +@Test +function testReplace() + let list = new ArrayList() + list.add(1, 2, 3, 4) + list.replace(3, 99).assertTrue() + list.get(2).assertEquals(99) + list.replace(999, 1).assertFalse() From 57a1ed47ac2ef5c30d1e426bc4f2c5bd0613e1af Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 5 Oct 2025 00:15:34 +0200 Subject: [PATCH 2/4] improve list and tests --- wurst/data/ArrayList.wurst | 148 +++++++++-------- wurst/data/ArrayListTests.wurst | 278 +++++++++++++++++++++++++++++++- 2 files changed, 345 insertions(+), 81 deletions(-) diff --git a/wurst/data/ArrayList.wurst b/wurst/data/ArrayList.wurst index 354519e37..4531a49aa 100644 --- a/wurst/data/ArrayList.wurst +++ b/wurst/data/ArrayList.wurst @@ -6,91 +6,87 @@ import Printing import Real /** - * High-performance array-based list implementation using a static shared store. + * 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 vs LinkedList: - * ==================================== + * 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 * - * ArrayList shines in these specific cases: - * 1. Iterating over large lists (hundreds+ elements) - no node indirection overhead - * 2. Adding many elements in tight loops - reduced allocation/deallocation churn - * 3. Random access patterns - O(1) get/set operations - * 4. Memory-sensitive scenarios where node overhead matters + * LinkedList is better for: + * - Insertion anywhere except the end of the list + * - Deletions while retaining order + * - Unknown size requirements + * - No resize performance risk * - * Use LinkedList when you need: - * - Frequent insertions/deletions in middle of list - * - Flexibility without preallocating capacity - * - No risk of costly resize operations - * - Simpler memory management + * TYPE SYSTEM IMPLICATIONS: + * ======================== + * Each ArrayList type gets its own static storage array. + * - ArrayList, ArrayList, ArrayList = 3 separate arrays + * - Each type can hold up to JASS_MAX_ARRAY_SIZE elements total across all instances * - * CRITICAL PERFORMANCE CONSIDERATIONS: - * ==================================== + * Choose wisely based on how many types you have. * - * ⚠️ PRESIZE YOUR LISTS! ⚠️ - * The single most important thing: estimate your maximum size and add headroom. - * Example: new ArrayList(200) if you expect ~150 elements max + * PERFORMANCE RULES: + * ================== * - * Why? If the list resizes, ALL elements are copied to a new memory location, - * negating any performance advantage over LinkedList! + * 1. PRESIZE, DON'T RESIZE + * Bad: new ArrayList() // Might resize multiple times + * Good: new ArrayList(maxSize) // One allocation * - * PERFORMANCE COMPARISON TABLE: - * ============================ + * Why: Resize operations copy ALL elements to new memory. Expensive! * - * Operation | ArrayList | LinkedList | Notes - * -------------------|----------------|----------------|--------------------------- - * Add to end | O(1)* | O(1) | *O(n) on resize! - * Add to start | O(n) | O(1) | ArrayList shifts all elements - * Insert at index | O(n) | O(n) | Both require traversal - * Remove (ordered) | O(n) | O(1) | ArrayList shifts elements - * Remove (unordered) | O(1) | O(1) | Use removeSwap() for ArrayList - * Get by index | O(1) | O(n) | ArrayList major advantage - * Iterate all | Fast | Slower | ArrayList: cache-friendly, no indirection - * Search | O(n) | O(n) | Same, but ArrayList faster due to cache - * Create/Destroy | Medium cost | High cost | ArrayList: 1 alloc, LinkedList: n allocs - * Memory overhead | Low | High | LinkedList: 2 pointers per element + * 2. REUSE, DON'T RECREATE + * Bad: In loop `let temp = new ArrayList() ... destroy temp` + * Good: `let temp = new ArrayList() ... temp.clear()` in loop * - * COMMON PITFALLS: - * =============== + * Why: Allocation/Deallocation is moderately expensive and causes fragmentation * - * 1. NOT PRESIZING - * Bad: let list = new ArrayList() // Will resize multiple times! - * Good: let list = new ArrayList(estimatedMaxSize + headroom) + * 3. ORDERED REMOVAL IS SLOW + * Bad: list.removeAt(i) // O(n) - shifts all elements + * Good: list.removeSwap(i) // O(1) - swaps with last element * - * 2. CREATING MANY TEMPORARY LISTS - * Bad: In a loop: let temp = new ArrayList() ... destroy temp - * Good: Create once, reuse with clear(): temp.clear() + * Only use removeSwap() if order doesn't matter * - * 3. ORDERED REMOVAL IN LOOPS - * Bad: for i in list: list.remove(x) // O(n²) with shifting! - * Good: Use removeIf() or 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 * - * 4. USING FOR-IN WHEN NOT NEEDED - * Bad: for elem in list: doSomething(elem) // Creates iterator - * Good: for i = 0 to list.size() - 1: doSomething(list.get(i)) // No allocation + * Use iterators for convenience, index loops for performance * - * 5. FREQUENT INSERTIONS AT START - * Bad: Repeatedly using addtoStart() - O(n) every time! - * Good: Use LinkedList, or collect and add in reverse order + * 5. AVOID FREQUENT INSERTS AT START + * Bad: list.addtoStart(x) // O(n) every time + * Good: Use LinkedList or add in reverse order * - * BEST PRACTICES: - * ============== - * - * ✓ Estimate capacity: measure actual usage, then add 20-30% headroom - * ✓ Reuse lists: clear() and reuse instead of destroy/create - * ✓ Use removeSwap() when order doesn't matter (O(1) vs O(n)) - * ✓ Prefer index loops over for-in loops for hot paths - * ✓ Profile your code: only use ArrayList where it actually helps! + * 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. * - * ArrayList uses a shared static array store with section allocation. - * Destroyed lists return their sections to a free pool for reuse. - * The system automatically compacts fragmented free sections. + * Fragmentation occurs when lists grow - the old section becomes a gap. + * This is why presizing matters: growth = copy to new location = wasted space. * - * Warning: The static store has a hard limit (JASS_MAX_ARRAY_SIZE). - * If you create many large lists simultaneously, you may exhaust it. - */ + * Hard limit: JASS_MAX_ARRAY_SIZE total slots per type across all instances. + **/ public class ArrayList private static T array store private static int nextFreeIndex = 0 @@ -478,7 +474,7 @@ public class ArrayList destroy f /** Returns the list obtained by applying the given closure to each element */ - function map(MapClosure itr) returns ArrayList + function map(MapClosure itr) returns ArrayList let output = new ArrayList(size) forEach(t -> output.add(itr.run(t))) destroy itr @@ -495,7 +491,7 @@ public class ArrayList return result /** Folds this list into a single value of type Q */ - function foldl(Q startValue, FoldClosure predicate) returns 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) @@ -589,7 +585,7 @@ public class ArrayList // UTILITY FUNCTIONS // ============================================================================ -public function asArrayList(vararg T ts) returns ArrayList +public function asArrayList(vararg T ts) returns ArrayList let al = new ArrayList() for t in ts al.add(t) @@ -600,7 +596,7 @@ public function asArrayList(vararg T ts) returns ArrayList // ============================================================================ /** Iterator for ArrayList - NOTE: Allocates an object, prefer index loops in hot paths! */ -public class ALIterator +public class ALIterator private ArrayList parent private int currentIndex private bool destroyOnClose = true @@ -637,7 +633,7 @@ public class ALIterator if not canRemove or currentIndex < 0 return null - let removed = parent.removeAt(currentIndex) + let removed = parent.removeSwap(currentIndex) currentIndex-- canRemove = false return removed @@ -655,19 +651,19 @@ public class ALIterator // INTERFACES // ============================================================================ -public interface ArrayListPredicate +public interface ArrayListPredicate function isTrueFor(T t) returns boolean -public interface ALItrClosure +public interface ALItrClosure function run(T t) -public interface ArrayListUpdater +public interface ArrayListUpdater function update(T t) returns T -public interface MapClosure +public interface MapClosure function run(T t) returns Q -public interface FoldClosure +public interface FoldClosure function run(T t, Q q) returns Q public interface Comparator diff --git a/wurst/data/ArrayListTests.wurst b/wurst/data/ArrayListTests.wurst index a1c742062..f44082529 100644 --- a/wurst/data/ArrayListTests.wurst +++ b/wurst/data/ArrayListTests.wurst @@ -1,5 +1,28 @@ package ArrayListTests import ArrayList +import StringUtils + +@Test +function testIteratorRemove() + let list = new ArrayList() + + list.add(1, 2, 3, 4) + + var acc = 0 + + let itr = list.iterator() + while itr.hasNext() + let v = itr.next() + acc += v + if v % 2 == 0 + itr.remove() + itr.close() + list.size().assertEquals(2) + list.get(0).assertEquals(1) + list.get(1).assertEquals(3) + acc.assertEquals(10) + + @Test function testForLoop() @@ -28,22 +51,32 @@ function testAddAll() list.get(3).assertEquals(4) var result = 0 - for i in list - result += i + for i = 0 to list.size() - 1 + result += list.get(i) result.assertEquals(1 + 2 + 3 + 4) @Test function testClosures() let list = new ArrayList() list.add(1, 2, 3, 4) + for i = 0 to list.size() - 1 + print(list.get(i)) list.updateAll(i -> i * 2) list.get(3).assertEquals(4 * 2) + for i = 0 to list.size() - 1 + print(list.get(i)) list.removeIf(i -> i > 4) + for i = 0 to list.size() - 1 + print(list.get(i)) list.size().assertEquals(2) - let realList = list.map(i -> i * 10.) + let realList = list.map(i -> begin + return i * 10. + end) + for i = 0 to realList.size() - 1 + print(realList.get(i)) realList.get(1).assertEquals(40.) @Test @@ -61,7 +94,7 @@ function testGenerics() @Test function testSort() let list = new ArrayList() - for i = 0 to 50 + for i = 0 to 500 list.add(GetRandomInt(-100, 100) * 2 + 1) list.sort() @@ -86,7 +119,6 @@ function testRemoveWhen() list.add(i) list.removeIf(i -> i < 3) list.size().assertEquals(4) - list.get(2).assertEquals(5) @Test function testFilter() @@ -204,3 +236,239 @@ function testReplace() list.replace(3, 99).assertTrue() list.get(2).assertEquals(99) list.replace(999, 1).assertFalse() + + +// ----------------------------- +// NESTED ARRAYLISTS +// ----------------------------- + +@Test +function testNestedArrayList_basic() + let outer = new ArrayList>() + let a = asArrayList(1, 2, 3) + let b = asArrayList(4, 5) + outer.add(a) + outer.add(b) + + outer.size().assertEquals(2) + outer.get(0).size().assertEquals(3) + outer.get(1).get(0).assertEquals(4) + +@Test +function testNestedArrayList_mutation() + let outer = new ArrayList>() + outer.add(asArrayList(10, 20)) + outer.add(asArrayList(30)) + + // mutate inner list via reference + outer.get(0).set(1, 99) + outer.get(0).get(1).assertEquals(99) + + // replace inner list object + let c = asArrayList(7, 8, 9) + outer.set(1, c) + outer.get(1).size().assertEquals(3) + outer.get(1).get(2).assertEquals(9) + +@Test +function testNestedArrayList_removeSwapOuter() + let outer = new ArrayList>() + ..add(asArrayList(1)) + ..add(asArrayList(2)) + ..add(asArrayList(3)) + let removed = outer.removeSwap(0) + removed.get(0).assertEquals(1) // we removed the first inner list + outer.size().assertEquals(2) + // order not guaranteed; just check the remaining are valid lists + (outer.get(0).size() >= 1 or outer.get(1).size() >= 1).assertTrue() + +// ----------------------------- +// TUPLES IN ARRAYLISTS +// ----------------------------- + +tuple T2(int x, int y) + +@Test +function testTupleInArrayList_basic() + let list = new ArrayList() + list.add(T2(3, 4)) + list.add(T2(7, 8)) + list.size().assertEquals(2) + list.get(0).x.assertEquals(3) + list.get(1).y.assertEquals(8) + +@Test +function testTupleInArrayList_removeSwap() + let list = new ArrayList() + ..add(T2(1, 2)) + ..add(T2(3, 4)) + ..add(T2(5, 6)) + + let mid = list.removeSwap(1) + mid.x.assertEquals(3) + list.size().assertEquals(2) + // element at 1 is now former last (order not preserved) + (list.get(1).x == 1 or list.get(1).x == 5).assertTrue() + +// ----------------------------- +// NESTED TUPLES IN ARRAYLISTS +// ----------------------------- + +tuple TInner(int a, int b) +tuple TOuter(int head, TInner tail) + +@Test +function testNestedTupleInArrayList_get_set() + let list = new ArrayList() + list.add(TOuter(10, TInner(20, 30))) + list.add(TOuter(40, TInner(50, 60))) + + list.get(0).head.assertEquals(10) + list.get(0).tail.a.assertEquals(20) + list.get(1).tail.b.assertEquals(60) + + list.set(0, TOuter(1, TInner(2, 3))) + list.get(0).tail.b.assertEquals(3) + +@Test +function testNullElements_storeAndIndexOf() + let list = new ArrayList() + list.add("a", null, "b") + list.size().assertEquals(3) + (list.get(1) == null).assertTrue() + // indexOf(null) is unspecified in docs, but == comparison should work: + list.indexOf(null).assertEquals(1) + +// ----------------------------- +// ITERATOR EDGE CASES +// ----------------------------- + +@Test +function testIterator_removeAtStartAndEnd() + let list = asArrayList(1, 2, 3, 4, 5) + let itr = list.iterator() + // remove first (after first next) + itr.next() // 1 + itr.remove() // remove 1 + list.size().assertEquals(4) + + // advance and remove last yielded + while itr.hasNext() + let v = itr.next() + if v == 5 + itr.remove() + itr.close() + list.size().assertEquals(3) + // remaining elements are {?, ?, ?} but 1 and 5 must be gone + (list.indexOf(1) == -1).assertTrue() + (list.indexOf(5) == -1).assertTrue() + +@Test +function testIterator_modifyAfterNext() + let list = asArrayList(10, 20, 30) + let itr = list.iterator() + itr.next() // 10 + itr.modify(11) + itr.next() // 20 + itr.modify(21) + itr.close() + list.get(0).assertEquals(11) + list.get(1).assertEquals(21) + + +// ----------------------------- +// BOUNDARIES & INSERTIONS +// ----------------------------- + +@Test +function testAddtoStart_and_addAtBoundaries() + let list = asArrayList(2, 3) + list.addtoStart(1) + list.get(0).assertEquals(1) + list.addAt(4, list.size()) // insert at end + list.getLast().assertEquals(4) + +@Test +function testRemoveAt_middle_and_shift() + let list = asArrayList(1, 2, 3, 4) + let removed = list.removeAt(1) // remove "2" + removed.assertEquals(2) + list.size().assertEquals(3) + list.get(1).assertEquals(3) // shifted over + +// ----------------------------- +// MAP / FILTER / FOLD with TUPLES +// ----------------------------- + +@Test +function testMapTuple_toInt() + let list = new ArrayList() + ..add(T2(1, 2)) + ..add(T2(3, 4)) + let sums = list.map(t -> t.x + t.y) + sums.size().assertEquals(2) + sums.get(0).assertEquals(3) + sums.get(1).assertEquals(7) + +@Test +function testFilterTuple_nested() + let list = new ArrayList() + ..add(TOuter(1, TInner(2, 0))) + ..add(TOuter(9, TInner(2, 1))) + ..add(TOuter(5, TInner(3, 0))) + let filtered = list.filter(o -> o.head >= 5 and o.tail.a >= 2) + filtered.size().assertEquals(2) + filtered.get(0).head.assertEquals(9) + filtered.get(1).head.assertEquals(5) + +@Test +function testFoldlTuple_collectSumOfLeaves() + let list = new ArrayList() + ..add(TOuter(1, TInner(2, 3))) + ..add(TOuter(4, TInner(5, 6))) + let sum = list.foldl(0, (o, acc) -> acc + o.head + o.tail.a + o.tail.b) + sum.assertEquals(1 + 2 + 3 + 4 + 5 + 6) + +// ----------------------------- +// STATIC STORE SEPARATION / MEMORY HINTS +// ----------------------------- + +@Test +function testStaticStore_separatePerType() + // The two types should not interfere; we check behaviorally. + let ints = new ArrayList() + let reals = new ArrayList() + ints.add(1, 2, 3) + reals.add(1.5, 2.5) + ints.size().assertEquals(3) + reals.size().assertEquals(2) + ints.get(1).assertEquals(2) + reals.get(1).assertEquals(2.5) + +@Test +function testClear_keepsCapacityAndStorage() + let list = new ArrayList(32) + for i = 0 to 15 + list.add(i) + let before = list.getMemoryInfo() + list.clear() + let after = list.getMemoryInfo() + // capacity and start usually stay the same; size is 0 now + (before.split(", ").get(0) == after.split(", ").get(0)).assertTrue() // Start: ... + (before.split(", ").get(1) == after.split(", ").get(1)).assertTrue() // Capacity: ... + list.size().assertEquals(0) + +// ----------------------------- +// QUEUE/STACK + NULL INTERACTIONS +// ----------------------------- + +@Test +function testQueue_nulls() + let q = new ArrayList() + q.enqueue("a") + q.enqueue(null) + q.enqueue("c") + q.dequeue().assertEquals("a") + (q.dequeue() == null).assertTrue() + q.dequeue().assertEquals("c") + From daf276806418bd9773889a32d3d5674b9f5e93d2 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 10 Jun 2026 16:57:06 +0200 Subject: [PATCH 3/4] arraylist updates --- wurst/data/ArrayList.wurst | 199 +++---- wurst/data/ArrayListMemoryTests.wurst | 575 ++++---------------- wurst/data/ArrayListTests.wurst | 750 +++++++++++++++----------- 3 files changed, 633 insertions(+), 891 deletions(-) diff --git a/wurst/data/ArrayList.wurst b/wurst/data/ArrayList.wurst index 4531a49aa..82ed3b3f9 100644 --- a/wurst/data/ArrayList.wurst +++ b/wurst/data/ArrayList.wurst @@ -1,9 +1,10 @@ package ArrayList import NoWurst -import Integer -import String -import Printing -import Real +import ErrorHandling +import Annotations + +constant int INITIAL_CAPACITY = 16 +constant int MAX_FREE_SECTIONS = 256 /** * High-performance array-based list using static shared storage per type. @@ -48,16 +49,17 @@ import Real * 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 + * Bad: list.removeAt(i) // O(n) - shifts all elements + * Good: list.removeAtUnordered(i) // O(1) - swaps with last element * - * Only use removeSwap() if order doesn't matter + * Only use removeAtUnordered() if order doesn't matter * - * 4. ITERATORS ALLOCATE - * Bad: for elem in list: process(elem) // Allocates iterator + * 4. ITERATE BY INDEX * Good: for i = 0 to list.size()-1 // Zero allocation + * Good: list[i] // Indexing operator * - * Use iterators for convenience, index loops for performance + * ArrayList has no iterator by design - index loops keep hot paths + * allocation-free. * * 5. AVOID FREQUENT INSERTS AT START * Bad: list.addtoStart(x) // O(n) every time @@ -71,7 +73,7 @@ import Real * 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 + * removeAtUnordered | 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 @@ -85,14 +87,17 @@ import Real * 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: JASS_MAX_ARRAY_SIZE total slots per type across all instances. + * Hard limit (wc3 native-array target): the shared store is bounded by + * JASS_MAX_ARRAY_SIZE total slots per type across all live instances; exceeding + * it raises an error. On the Lua target the backing store grows dynamically, so + * this cap does not apply - there ArrayList is still valuable mainly for keeping + * element types static instead of relying on typecasting. **/ 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 @@ -100,10 +105,6 @@ public class ArrayList 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() @@ -111,16 +112,18 @@ public class ArrayList /** Creates a new list with specified initial capacity - RECOMMENDED for performance */ construct(int initialCapacity) - allocateStorage(initialCapacity) + allocateStorage(initialCapacity > 0 ? initialCapacity : INITIAL_CAPACITY) /** 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) + addAll(base) + /** Allocates storage section - tries to reuse freed sections first */ private function allocateStorage(int cap) + if cap <= 0 + error("ArrayList: allocateStorage capacity must be > 0") // Try to find a freed section that fits for i = 0 to freeSectionCount - 1 if freeSectionCapacity[i] >= cap @@ -140,9 +143,7 @@ public class ArrayList compactFreeList() if nextFreeIndex + cap > JASS_MAX_ARRAY_SIZE - // Still not enough, wrap around (dangerous!) - print("ArrayList: WARNING - Memory store exhausted, wrapping around!") - nextFreeIndex = 0 + error("ArrayList: Storage limit exceeded for type") startIndex = nextFreeIndex capacity = cap @@ -237,21 +238,13 @@ public class ArrayList capacity = tempCap ondestroy - // Clear references to allow garbage collection + // Clear references 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 // ============================================================================ @@ -271,20 +264,21 @@ public class ArrayList while needed > capacity grow() - for elem in other - store[startIndex + size] = elem + for i = 0 to other.size - 1 + store[startIndex + size] = other.get(i) 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()) + error("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()) + error("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)) */ @@ -298,10 +292,9 @@ public class ArrayList 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 + function removeAtOrdered(int index) returns T if index < 0 or index >= size - print("ArrayList: Index out of bounds: " + index.toString()) + error("ArrayList: Index out of bounds: " + index.toString()) let elem = store[startIndex + index] @@ -312,10 +305,15 @@ public class ArrayList size-- return elem + /** Removes the element at the given index and returns it (O(n) - shifts elements) */ + @Deprecated("This operation shifts elements, consider using #removeAtUnordered if order is not important.") + function removeAt(int index) returns T + return removeAtOrdered(index) + /** Removes the element at the given index by swapping with last element (O(1) - DOES NOT PRESERVE ORDER!) */ - function removeSwap(int index) returns T + function removeAtUnordered(int index) returns T if index < 0 or index >= size - print("ArrayList: Index out of bounds: " + index.toString()) + error("ArrayList: Index out of bounds: " + index.toString()) let elem = store[startIndex + index] @@ -327,10 +325,19 @@ public class ArrayList return elem /** Removes the first occurrence of the element from the list (O(n)) */ + @Deprecated("This operation shifts elements, consider using #removeUnordered if order is not important.") function remove(T elem) returns bool let index = indexOf(elem) if index >= 0 - removeAt(index) + removeAtOrdered(index) + return true + return false + + /** Removes the first occurrence of the element from the list (O(n)) */ + function removeUnordered(T elem) returns bool + let index = indexOf(elem) + if index >= 0 + removeAtUnordered(index) return true return false @@ -342,16 +349,16 @@ public class ArrayList function isEmpty() returns boolean return size == 0 - /** Returns the first element in the list (O(1)) */ + /** Returns the first element in the list, or null if empty (O(1)) */ function getFirst() returns T if size == 0 - print("ArrayList: getFirst on empty list") + return null return store[startIndex] - /** Returns the last element in the list (O(1)) */ + /** Returns the last element in the list, or null if empty (O(1)) */ function getLast() returns T if size == 0 - print("ArrayList: getLast on empty list") + return null return store[startIndex + size - 1] /** Clears all elements from the list (O(1) - reuse this list instead of creating new ones!) */ @@ -394,7 +401,7 @@ public class ArrayList size-- return store[startIndex + size] - /** Returns the lastly added element without removing it */ + /** Returns the lastly added element without removing it, or null if empty */ function peek() returns T return getLast() @@ -410,7 +417,7 @@ public class ArrayList function dequeue() returns T if size == 0 return null - return removeAt(0) + return removeAtOrdered(0) // ============================================================================ // INSERTION OPERATIONS @@ -431,7 +438,7 @@ public class ArrayList /** 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()) + error("ArrayList: Index out of bounds: " + index.toString()) if size >= capacity grow() @@ -447,17 +454,28 @@ public class ArrayList // 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 */ + /** Removes elements that satisfy the predicate (O(n), preserves order). + If order is not important, use #removeUnorderedIf */ function removeIf(ArrayListPredicate predicate) - let itr = iterator() - for elem from itr - if predicate.isTrueFor(elem) - itr.remove() - itr.close() + // Single pass compaction: keep elements that fail the predicate + var writeIndex = 0 + for readIndex = 0 to size - 1 + let elem = store[startIndex + readIndex] + if not predicate.isTrueFor(elem) + store[startIndex + writeIndex] = elem + writeIndex++ + size = writeIndex + destroy predicate + + /** Removes elements that satisfy the predicate (O(n), does NOT preserve order) */ + function removeUnorderedIf(ArrayListPredicate predicate) + var i = 0 + while i < size + if predicate.isTrueFor(store[startIndex + i]) + // swaps the last element into i and shrinks - don't advance i + removeAtUnordered(i) + else + i++ destroy predicate /** Executes the closure for each element */ @@ -591,62 +609,6 @@ public function asArrayList(vararg T ts) returns ArrayList 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 // ============================================================================ @@ -701,3 +663,10 @@ public function ArrayList.joinBy(string separator) returns string /** Joins elements from a string list into one string */ public function ArrayList.join() returns string return this.joinBy("") + + +public function ArrayList.op_index(int index) returns T + return this.get(index) + +public function ArrayList.op_indexAssign(int index, T value) + this.set(index, value) diff --git a/wurst/data/ArrayListMemoryTests.wurst b/wurst/data/ArrayListMemoryTests.wurst index d31a92390..52fa6e4c7 100644 --- a/wurst/data/ArrayListMemoryTests.wurst +++ b/wurst/data/ArrayListMemoryTests.wurst @@ -1,504 +1,139 @@ package ArrayListMemoryTests import ArrayList -// ============================================================================ -// SIMPLIFIED MEMORY VISUALIZATION -// ============================================================================ - -/** Simple memory visualizer that tracks lists manually */ -class MemoryVisualizer - private static int array starts - private static int array capacities - private static string array names - private static int count = 0 - - static function reset() - count = 0 - - static function track(int start, int capacity, string name) - starts[count] = start - capacities[count] = capacity - names[count] = name - count++ - - static function visualize(int maxIndex) returns string - if maxIndex == 0 - return "[Empty memory]\n" - - // Build allocation map - string array allocationMap - for i = 0 to maxIndex - 1 - allocationMap[i] = "░" // Free space - - // Mark tracked allocations - for idx = 0 to count - 1 - let start = starts[idx] - let cap = capacities[idx] - let name = names[idx] - let marker = name.substring(0, 1) - - for i = start to start + cap - 1 - if i < maxIndex - allocationMap[i] = marker - - var result = "\nMemory Layout [0-" + (maxIndex - 1).toString() + "]:\n" - - // Print in rows of 64 for readability - let rowSize = 64 - var row = 0 - while row * rowSize < maxIndex - let startIdx = row * rowSize - let endIdx = startIdx + rowSize - - result += "[" - // Print row label - if startIdx < 10 - result += "0" - if startIdx < 100 - result += "0" - if startIdx < 1000 - result += "0" - result += startIdx.toString() + "] -> |" - - for i = startIdx to endIdx - 1 - if i < maxIndex - result += allocationMap[i] - else - break - - result += "|\n" - row++ - - result += "\n" - - // Legend - for idx = 0 to count - 1 - result += names[idx].substring(0, 1) + "=" + names[idx] + " " - result += "░=free\n" - result += ArrayList.getGlobalMemoryInfo() + "\n" - - return result - -// Helper to extract start index from memory info string -function extractStart(string memInfo) returns int - // "Start: 123, Capacity: 456, Size: 789" - // Find "Start: " and parse number after it - let startPos = memInfo.indexOf("Start: ") + 7 - var endPos = startPos - let len = memInfo.length() - - while endPos < len - let char = memInfo.substring(endPos, endPos + 1) - if char == "," - break - endPos++ - - let numStr = memInfo.substring(startPos, endPos) - return numStr.toInt() - -function extractCapacity(string memInfo) returns int - let startPos = memInfo.indexOf("Capacity: ") + 10 - var endPos = startPos - let len = memInfo.length() - - while endPos < len - let char = memInfo.substring(endPos, endPos + 1) - if char == "," - break - endPos++ - - let numStr = memInfo.substring(startPos, endPos) - return numStr.toInt() - -function extractNextFree() returns int - let info = ArrayList.getGlobalMemoryInfo() - let startPos = info.indexOf("NextFree: ") + 10 - var endPos = startPos - let len = info.length() - - while endPos < len - let char = info.substring(endPos, endPos + 1) - if char == "," - break - endPos++ - - let numStr = info.substring(startPos, endPos) - return numStr.toInt() - -// ============================================================================ -// MEMORY TESTS -// ============================================================================ - -@Test -function testBasicMemoryAllocation() - print("\n=== TEST: Basic Memory Allocation ===") - - MemoryVisualizer.reset() - - let list1 = new ArrayList(10) - let info1 = list1.getMemoryInfo() - MemoryVisualizer.track(extractStart(info1), extractCapacity(info1), "1") - - let list2 = new ArrayList(20) - let info2 = list2.getMemoryInfo() - MemoryVisualizer.track(extractStart(info2), extractCapacity(info2), "2") - - let list3 = new ArrayList(15) - let info3 = list3.getMemoryInfo() - MemoryVisualizer.track(extractStart(info3), extractCapacity(info3), "3") - - print(MemoryVisualizer.visualize(extractNextFree())) - - info1.contains("Start: 0").assertTrue() - info2.contains("Start: 10").assertTrue() - info3.contains("Start: 30").assertTrue() - - destroy list1 - destroy list2 - destroy list3 +// Behavioral tests for ArrayList's section allocator (one shared static store per type). +// They assert observable effects - data integrity and that storage is actually reclaimed +// and reused - rather than internal addresses, so they stay valid across compile targets. @Test -function testMemoryReclamation() - print("\n=== TEST: Memory Reclamation ===") - - // Clear any previous state by getting current free index - let initialFree = extractNextFree() - print("Initial NextFree: " + initialFree.toString()) - - MemoryVisualizer.reset() - let list1 = new ArrayList(50) - let list1Start = extractStart(list1.getMemoryInfo()) - let list1Cap = extractCapacity(list1.getMemoryInfo()) - MemoryVisualizer.track(list1Start, list1Cap, "List1") - print("After allocation:") - let nextFree = extractNextFree() - print(MemoryVisualizer.visualize(nextFree)) - print("List1 info: " + list1.getMemoryInfo()) - - destroy list1 - - MemoryVisualizer.reset() - let list2 = new ArrayList(30) - let list2Start = extractStart(list2.getMemoryInfo()) - let list2Cap = extractCapacity(list2.getMemoryInfo()) - MemoryVisualizer.track(list2Start, list2Cap, "List2-reused") - print("After reclamation:") - print(MemoryVisualizer.visualize(nextFree)) - print("List2 info: " + list2.getMemoryInfo()) - - // List2 should reuse list1's memory location - print("Checking: list2Start (" + list2Start.toString() + ") == list1Start (" + list1Start.toString() + ")") - list2Start.assertEquals(list1Start) - - // Capacity should be at least 30, but might be 50 if reusing the full section - (list2Cap >= 30).assertTrue() - - destroy list2 +function testStorageIsReclaimedOnDestroy() + // The shared store is bounded by JASS_MAX_ARRAY_SIZE per type. If destroyed sections + // were not returned to the free pool, allocating far more total capacity than the store + // can hold would raise "Storage limit exceeded". Looping 300x500 (=150k slots) well past + // the cap without erroring proves reclamation works. + for i = 0 to 299 + let list = new ArrayList(500) + for j = 0 to 9 + list.add(j) + list.get(9).assertEquals(9) + destroy list + // A fresh allocation after the churn must still succeed and behave correctly. + let after = new ArrayList(500) + after.add(7) + after.get(0).assertEquals(7) + destroy after @Test -function testMemoryFragmentation() - print("\n=== TEST: Memory Fragmentation ===") - - MemoryVisualizer.reset() - - let list1 = new ArrayList(10) - MemoryVisualizer.track(extractStart(list1.getMemoryInfo()), 10, "A") - - let list2 = new ArrayList(20) - MemoryVisualizer.track(extractStart(list2.getMemoryInfo()), 20, "B") - - let list3 = new ArrayList(15) - MemoryVisualizer.track(extractStart(list3.getMemoryInfo()), 15, "C") - - print("Initial layout:") - print(MemoryVisualizer.visualize(extractNextFree())) - - destroy list2 - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list1.getMemoryInfo()), 10, "A") - MemoryVisualizer.track(extractStart(list3.getMemoryInfo()), 15, "C") - - let list4 = new ArrayList(10) - MemoryVisualizer.track(extractStart(list4.getMemoryInfo()), 10, "D-reused") - - print("After destroying B and creating D:") - print(MemoryVisualizer.visualize(extractNextFree())) - - list4.getMemoryInfo().contains("Start: 10").assertTrue() - - destroy list1 - destroy list3 - destroy list4 +function testFragmentationReuseKeepsDataIntact() + // Layout A | B | C, then destroy B and allocate D into the gap. + // Reusing B's freed slot must not disturb the surviving lists. + let a = new ArrayList(10) + let b = new ArrayList(20) + let c = new ArrayList(15) + a.add(1, 2, 3) + b.add(100) + c.add(7, 8) + + destroy b + + let d = new ArrayList(10) + d.add(42) + + a.get(0).assertEquals(1) + a.get(2).assertEquals(3) + c.get(0).assertEquals(7) + c.get(1).assertEquals(8) + d.get(0).assertEquals(42) + + destroy a + destroy c + destroy d @Test -function testGrowthMemoryBehavior() - print("\n=== TEST: Growth Memory Behavior ===") - - let list = new ArrayList(4) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 4, "List") - print("Initial (capacity 4):") - print(MemoryVisualizer.visualize(extractNextFree())) - print(list.getMemoryInfo()) - - // Force growth - for i = 1 to 5 - list.add(i) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 8, "List-grown") - print("\nAfter adding 5 elements (grew to 8):") - print(MemoryVisualizer.visualize(extractNextFree())) - print(list.getMemoryInfo()) - - // Force another growth - for i = 6 to 10 +function testGrowthPreservesData() + // Growth copies the whole section to a new location; every element must survive the move. + let list = new ArrayList(2) + for i = 0 to 99 list.add(i) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 16, "List-grown2") - print("\nAfter adding 10 total elements (grew to 16):") - print(MemoryVisualizer.visualize(extractNextFree())) - print(list.getMemoryInfo()) - - list.getMemoryInfo().contains("Size: 10").assertTrue() - - // Verify data integrity - for i = 0 to 9 - list.get(i).assertEquals(i + 1) - + list.size().assertEquals(100) + for i = 0 to 99 + list.get(i).assertEquals(i) destroy list @Test -function testMultipleGrowths() - print("\n=== TEST: Multiple Growths ===") - - let list = new ArrayList(2) - - print("Initial capacity: 2") - print(list.getMemoryInfo()) +function testGrowthDoesNotCorruptNeighbour() + // grow() frees the old section after copying. A neighbouring list of the same type + // must keep its own data regardless of where the grower ends up. + let neighbour = new ArrayList(4) + neighbour.add(11, 22, 33, 44) - // Track key growth points - var lastSize = 0 - - for i = 1 to 50 - list.add(i) - - // Visualize at powers of 2 - if list.size() == 2 or list.size() == 4 or list.size() == 8 or list.size() == 16 or list.size() == 32 or list.size() == 50 - if list.size() != lastSize - MemoryVisualizer.reset() - let cap = extractCapacity(list.getMemoryInfo()) - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), cap, "List") - print("\nAfter adding " + list.size().toString() + " elements:") - print(MemoryVisualizer.visualize(extractNextFree())) - print(list.getMemoryInfo()) - lastSize = list.size() + let grower = new ArrayList(2) + for i = 0 to 49 + grower.add(i) - list.size().assertEquals(50) + neighbour.get(0).assertEquals(11) + neighbour.get(3).assertEquals(44) + grower.get(0).assertEquals(0) + grower.get(49).assertEquals(49) - // Verify all elements - for i = 0 to 49 - list.get(i).assertEquals(i + 1) + destroy neighbour + destroy grower +@Test +function testClearThenRefillReusesStorage() + // clear() keeps the section, so repeated fill/clear cycles must not exhaust the store. + let list = new ArrayList(50) + for round = 0 to 9 + for i = 0 to 49 + list.add(i) + list.size().assertEquals(50) + list.get(49).assertEquals(49) + list.clear() + list.size().assertEquals(0) destroy list @Test -function testMemoryCompaction() - print("\n=== TEST: Memory Compaction ===") - - let lists = new ArrayList>() - MemoryVisualizer.reset() - - // Create many small lists - for i = 0 to 10 - let list = new ArrayList(5) - lists.add(list) - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 5, "L" + i.toString()) +function testCopyIsIndependentInStore() + let original = new ArrayList() + original.add(1, 2, 3, 4, 5) + let clone = original.copy() - print("Initial layout with 11 lists:") - print(MemoryVisualizer.visualize(extractNextFree())) + original.set(0, 99) + clone.get(0).assertEquals(1) + original.get(0).assertEquals(99) - // Destroy every other list - for i = 0 to 5 - destroy lists.get(i * 2) - lists.set(i * 2, null) + // mutating the clone must not bleed back into the original + clone.set(4, -1) + original.get(4).assertEquals(5) - MemoryVisualizer.reset() - for i = 0 to 10 - if lists.get(i) != null - MemoryVisualizer.track(extractStart(lists.get(i).getMemoryInfo()), 5, "L" + i.toString()) - - print("\nAfter destroying every other list:") - print(MemoryVisualizer.visualize(extractNextFree())) - - let globalInfo = ArrayList.getGlobalMemoryInfo() - print("Free sections should be present: " + globalInfo) - - // Clean up - for i = 0 to 10 - if lists.get(i) != null - destroy lists.get(i) - destroy lists + destroy original + destroy clone @Test -function testLargeAllocation() - print("\n=== TEST: Large Allocation ===") - +function testLargeAllocationAndAccess() let list = new ArrayList(1000) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 1000, "LargeList") - print("Allocated 1000 capacity:") - print(MemoryVisualizer.visualize(extractNextFree())) - for i = 0 to 999 list.add(i) - list.size().assertEquals(1000) + list.get(0).assertEquals(0) list.get(500).assertEquals(500) list.get(999).assertEquals(999) - - print("\nFilled with 1000 elements:") - print(list.getMemoryInfo()) - - destroy list - -@Test -function testMemoryAfterClear() - print("\n=== TEST: Memory After Clear ===") - - let list = new ArrayList(20) - - for i = 1 to 15 - list.add(i) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 20, "List") - print("Before clear (15 elements):") - print(MemoryVisualizer.visualize(extractNextFree())) - print(list.getMemoryInfo()) - - list.clear() - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list.getMemoryInfo()), 20, "List") - print("\nAfter clear:") - print(MemoryVisualizer.visualize(extractNextFree())) - print(list.getMemoryInfo()) - - list.getMemoryInfo().contains("Size: 0").assertTrue() - destroy list @Test -function testCopyMemoryIndependence() - print("\n=== TEST: Copy Memory Independence ===") - - let list1 = new ArrayList() - list1.add(1, 2, 3, 4, 5) - - let list2 = list1.copy() - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(list1.getMemoryInfo()), extractCapacity(list1.getMemoryInfo()), "Original") - MemoryVisualizer.track(extractStart(list2.getMemoryInfo()), extractCapacity(list2.getMemoryInfo()), "Copy") - print("Original and copy in memory:") - print(MemoryVisualizer.visualize(extractNextFree())) - - // Modify one shouldn't affect the other - list1.set(0, 99) - list2.get(0).assertEquals(1) - list1.get(0).assertEquals(99) - - destroy list1 - destroy list2 - -@Test -function testReusePattern() - print("\n=== TEST: Reuse Pattern (Best Practice) ===") - - let reusableList = new ArrayList(100) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(reusableList.getMemoryInfo()), 100, "Reusable") - - print("Initial allocation:") - print(MemoryVisualizer.visualize(extractNextFree())) - - // Simulate multiple operations with reuse - for iteration = 0 to 3 - for i = 0 to 50 - reusableList.add(i) - - print("\nIteration " + iteration.toString() + " - filled:") - print(reusableList.getMemoryInfo()) - - reusableList.clear() - print("Iteration " + iteration.toString() + " - cleared (memory retained):") - print(reusableList.getMemoryInfo()) - - print("\nFinal state - same memory location throughout:") - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(reusableList.getMemoryInfo()), 100, "Reusable") - print(MemoryVisualizer.visualize(extractNextFree())) - - destroy reusableList - -@Test -function testWorstCaseScenario() - print("\n=== TEST: Worst Case - No Presizing ===") - - let badList = new ArrayList() - - print("Starting with default capacity (16)...") - - var growthCount = 0 - var lastCap = 16 - - for i = 1 to 100 - badList.add(i) - - let currentCap = extractCapacity(badList.getMemoryInfo()) - - if currentCap > lastCap - growthCount++ - lastCap = currentCap - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(badList.getMemoryInfo()), currentCap, "BadList") - print("\nGrowth #" + growthCount.toString() + " occurred at size " + badList.size().toString()) - print(MemoryVisualizer.visualize(extractNextFree())) - print(badList.getMemoryInfo()) - - print("\nTotal growths: " + growthCount.toString()) - print("This is why you should presize your lists!") - - destroy badList - -@Test -function testBestCaseScenario() - print("\n=== TEST: Best Case - Proper Presizing ===") - - let goodList = new ArrayList(100) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(goodList.getMemoryInfo()), 100, "GoodList") - print("Starting with presized capacity (100):") - print(MemoryVisualizer.visualize(extractNextFree())) - print(goodList.getMemoryInfo()) - - for i = 1 to 100 - goodList.add(i) - - MemoryVisualizer.reset() - MemoryVisualizer.track(extractStart(goodList.getMemoryInfo()), 100, "GoodList") - print("\nAfter adding 100 elements - NO GROWTH!") - print(MemoryVisualizer.visualize(extractNextFree())) - print(goodList.getMemoryInfo()) - - goodList.size().assertEquals(100) - - destroy goodList +function testManyConcurrentListsStayIsolated() + // Many live lists of the same type share one store; each must retain only its own data. + let lists = new ArrayList>() + for i = 0 to 20 + let l = new ArrayList(8) + l.add(i * 1000) + l.add(i * 1000 + 1) + lists.add(l) + + for i = 0 to 20 + lists.get(i).get(0).assertEquals(i * 1000) + lists.get(i).get(1).assertEquals(i * 1000 + 1) + + for i = 0 to 20 + destroy lists.get(i) + destroy lists diff --git a/wurst/data/ArrayListTests.wurst b/wurst/data/ArrayListTests.wurst index f44082529..5413d084f 100644 --- a/wurst/data/ArrayListTests.wurst +++ b/wurst/data/ArrayListTests.wurst @@ -1,42 +1,46 @@ package ArrayListTests import ArrayList -import StringUtils + +// ============================================================================ +// CORE API +// ============================================================================ @Test -function testIteratorRemove() +function testAddGetSet() let list = new ArrayList() - - list.add(1, 2, 3, 4) - - var acc = 0 - - let itr = list.iterator() - while itr.hasNext() - let v = itr.next() - acc += v - if v % 2 == 0 - itr.remove() - itr.close() - list.size().assertEquals(2) - list.get(0).assertEquals(1) - list.get(1).assertEquals(3) - acc.assertEquals(10) - - + list.add(10, 20, 30) + list.size().assertEquals(3) + list.get(0).assertEquals(10) + list.get(2).assertEquals(30) + list.set(1, 99) + list.get(1).assertEquals(99) + destroy list @Test function testForLoop() let list = new ArrayList() - list.add(1, 2, 3, 4) - list.updateAll(i -> i + 1) var result = 0 - for i in list - result += i + for i = 0 to list.size() - 1 + result += list.get(i) - result.assertEquals(5 + 4 + 3 + 2) + result.assertEquals(2 + 3 + 4 + 5) + destroy list + +@Test +function testIndexOperator() + let list = new ArrayList() + list.add(10, 20, 30) + // read via [] + list[0].assertEquals(10) + list[2].assertEquals(30) + // write via [] + list[1] = 99 + list.get(1).assertEquals(99) + list[1].assertEquals(99) + destroy list @Test function testAddAll() @@ -55,30 +59,85 @@ function testAddAll() result += list.get(i) result.assertEquals(1 + 2 + 3 + 4) + destroy list + destroy list2 + @Test function testClosures() let list = new ArrayList() list.add(1, 2, 3, 4) - for i = 0 to list.size() - 1 - print(list.get(i)) - list.updateAll(i -> i * 2) - list.get(3).assertEquals(4 * 2) - for i = 0 to list.size() - 1 - print(list.get(i)) + list.updateAll(i -> i * 2) // [2, 4, 6, 8] + list.get(3).assertEquals(8) - list.removeIf(i -> i > 4) - for i = 0 to list.size() - 1 - print(list.get(i)) + list.removeIf(i -> i > 4) // [2, 4] list.size().assertEquals(2) - let realList = list.map(i -> begin - return i * 10. - end) - for i = 0 to realList.size() - 1 - print(realList.get(i)) + let realList = list.map(i -> i * 10.) realList.get(1).assertEquals(40.) + destroy list + destroy realList + +@Test +function testRemoveIf() + // ordered removal preserves the order of the survivors + let list = new ArrayList() + for i = 1 to 6 + list.add(i) + list.removeIf(i -> i % 2 == 0) // remove evens -> [1, 3, 5] + list.size().assertEquals(3) + list.get(0).assertEquals(1) + list.get(1).assertEquals(3) + list.get(2).assertEquals(5) + destroy list + +@Test +function testRemoveIf_all() + // removing everything must not run out of bounds + let list = asArrayList(1, 2, 3, 4) + list.removeIf(i -> true) + list.size().assertEquals(0) + destroy list + +@Test +function testRemoveUnorderedIf() + let list = new ArrayList() + for i = 1 to 6 + list.add(i) + list.removeUnorderedIf(i -> i > 3) // remove 4, 5, 6 (order not preserved) + list.size().assertEquals(3) + list.has(1).assertTrue() + list.has(2).assertTrue() + list.has(3).assertTrue() + list.has(4).assertFalse() + destroy list + +@Test +function testFilter() + let list = new ArrayList() + for i = 1 to 6 + list.add(i) + let filtered = list.filter(i -> i > 3) + filtered.size().assertEquals(3) + filtered.get(2).assertEquals(6) + destroy list + destroy filtered + +@Test +function testFoldl() + let list = new ArrayList() + for i = 1 to 6 + list.add(i) + list.foldl(0, (i, q) -> q + i).assertEquals(21) + destroy list + +@Test +function testFind() + let list = asArrayList(1, 2, 3, 4) + list.find(i -> i > 2).assertEquals(3) + destroy list + @Test function testGenerics() let list = new ArrayList() @@ -88,86 +147,105 @@ function testGenerics() let list2 = new ArrayList() list2.add(1.230, 2.563, 1213143.) list2.get(0).assertEquals(1.230) - list2.get(1).assertEquals(2.563) list2.get(2).assertEquals(1213143.) + destroy list + destroy list2 + @Test function testSort() let list = new ArrayList() for i = 0 to 500 list.add(GetRandomInt(-100, 100) * 2 + 1) list.sort() - for i = 0 to list.size() - 2 list.get(i).assertLessThanOrEqual(list.get(i + 1)) + destroy list + +@Test +function testSortReal() + let list = new ArrayList() + for i = 6 downto 1 + list.add(i.toReal()) + list.sort() + list.get(0).assertEquals(1.) + list.get(5).assertEquals(6.) + destroy list @Test function testAddAt() let list = new ArrayList() for i = 1 to 6 list.add(i) - list.addAt(7, 4) - string elems = "" - for elem in list - elems += elem.toString() - elems.assertEquals("1234756") + list.addAt(7, 4) // 1 2 3 4 7 5 6 + list.size().assertEquals(7) + list.get(4).assertEquals(7) + list.get(5).assertEquals(5) + destroy list @Test -function testRemoveWhen() - let list = new ArrayList() - for i = 1 to 6 - list.add(i) - list.removeIf(i -> i < 3) - list.size().assertEquals(4) +function testAddtoStart() + let list = asArrayList(2, 3) + list.addtoStart(1) + list.get(0).assertEquals(1) + list.get(1).assertEquals(2) + list.addAt(4, list.size()) // insert at end + list.getLast().assertEquals(4) + destroy list @Test -function testFilter() - var list = new ArrayList() - for i = 1 to 6 - list.add(i) - list = list.filter(i -> i > 3) +function testRemoveAtOrdered() + let list = asArrayList(1, 2, 3, 4) + list.removeAtOrdered(1).assertEquals(2) list.size().assertEquals(3) - list.get(2).assertEquals(6) + list.get(1).assertEquals(3) // shifted over + list.get(2).assertEquals(4) + destroy list @Test -function testFoldl() - let list = new ArrayList() - for i = 1 to 6 - list.add(i) - let result = list.foldl(0, (i, q) -> q + i) - result.assertEquals(21) +function testRemoveAtUnordered() + let list = asArrayList(1, 2, 3, 4) + list.removeAtUnordered(0).assertEquals(1) // last element swapped into 0 + list.size().assertEquals(3) + list.get(0).assertEquals(4) + destroy list @Test -function testSortReal() - let list = new ArrayList() - for i = 6 downto 1 - list.add(i.toReal()) - list.sort() - list.get(0).assertEquals(1.) - list.get(5).assertEquals(6.) +function testRemoveUnordered() + let list = asArrayList(1, 2, 3) + list.removeUnordered(2).assertTrue() + list.size().assertEquals(2) + list.has(2).assertFalse() + list.removeUnordered(99).assertFalse() + destroy list @Test -function testAsArrayList() - asArrayList(1,2,3,4).foldl(0, (i, q) -> q + i) - .assertEquals(asArrayList(4,3,2,1).foldl(0, (i, q) -> q + i)) +function testIndexOfHasReplace() + let list = new ArrayList() + list.add("a", "b", "c") + list.indexOf("b").assertEquals(1) + list.indexOf("x").assertEquals(-1) + list.has("c").assertTrue() + list.has("x").assertFalse() + list.replace("b", "z").assertTrue() + list.get(1).assertEquals("z") + list.replace("nope", "y").assertFalse() + destroy list @Test function testGetters() - let testlist = new ArrayList() - testlist.add("a") - testlist.add("c") - testlist.add("b") - testlist.getFirst().assertEquals("a") - testlist.get(1).assertEquals("c") - -@Test -function testJoin() let list = new ArrayList() - ..add("this", "is", "a", "string") - - list.join().assertEquals("thisisastring") - list.joinBy(" ").assertEquals("this is a string") + list.add("a", "c", "b") + list.getFirst().assertEquals("a") + list.getLast().assertEquals("b") + list.peek().assertEquals("b") + list.get(1).assertEquals("c") + destroy list +@Test +function testGetRandomElement() + let list = asArrayList(5, 5, 5) + list.getRandomElement().assertEquals(5) destroy list @Test @@ -176,14 +254,10 @@ function testQueue() ..enqueue("a") ..enqueue("b") ..enqueue("c") - - list.getFirst().assertEquals("a") list.dequeue().assertEquals("a") - list.getFirst().assertEquals("b") list.dequeue().assertEquals("b") - list.getFirst().assertEquals("c") list.dequeue().assertEquals("c") - + list.isEmpty().assertTrue() destroy list @Test @@ -192,14 +266,10 @@ function testStack() ..push("a") ..push("b") ..push("c") - list.peek().assertEquals("c") list.pop().assertEquals("c") - list.peek().assertEquals("b") list.pop().assertEquals("b") - list.peek().assertEquals("a") list.pop().assertEquals("a") - destroy list @Test @@ -209,266 +279,334 @@ function testGrowth() list.add(i) list.size().assertEquals(100) list.get(99).assertEquals(100) + destroy list @Test function testCopy() let list = new ArrayList() list.add(1, 2, 3, 4, 5) - let copy = list.copy() - copy.size().assertEquals(5) - copy.get(2).assertEquals(3) + let cp = list.copy() + cp.size().assertEquals(5) + cp.get(2).assertEquals(3) - copy.set(2, 99) - list.get(2).assertEquals(3) - copy.get(2).assertEquals(99) + cp.set(2, 99) + list.get(2).assertEquals(3) // copy is independent + cp.get(2).assertEquals(99) + + destroy list + destroy cp @Test -function testIndexOf() - let list = new ArrayList() - list.add("a", "b", "c") - list.indexOf("b").assertEquals(1) - list.indexOf("x").assertEquals(-1) +function testCopyEmpty() + // regression: copying an empty list must not error on a 0-capacity request + let empty = new ArrayList() + let cp = empty.copy() + cp.size().assertEquals(0) + cp.add(1) + cp.get(0).assertEquals(1) + destroy empty + destroy cp @Test -function testReplace() - let list = new ArrayList() - list.add(1, 2, 3, 4) - list.replace(3, 99).assertTrue() - list.get(2).assertEquals(99) - list.replace(999, 1).assertFalse() +function testMapEmpty() + // regression: mapping an empty list must not error on a 0-capacity request + let empty = new ArrayList() + let mapped = empty.map(i -> i.toString()) + mapped.size().assertEquals(0) + destroy empty + destroy mapped + +@Test +function testClearKeepsStorage() + let list = new ArrayList(32) + for i = 0 to 15 + list.add(i) + list.clear() + list.size().assertEquals(0) + // still usable and reuses the same storage (no realloc needed) + list.add(99) + list.get(0).assertEquals(99) + destroy list +@Test +function testJoin() + let list = new ArrayList() + ..add("this", "is", "a", "string") + list.join().assertEquals("thisisastring") + list.joinBy(" ").assertEquals("this is a string") + destroy list -// ----------------------------- -// NESTED ARRAYLISTS -// ----------------------------- +@Test +function testAsArrayList() + asArrayList(1, 2, 3, 4).foldl(0, (i, q) -> q + i) + .assertEquals(asArrayList(4, 3, 2, 1).foldl(0, (i, q) -> q + i)) @Test -function testNestedArrayList_basic() +function testNestedArrayList() let outer = new ArrayList>() - let a = asArrayList(1, 2, 3) - let b = asArrayList(4, 5) - outer.add(a) - outer.add(b) + outer.add(asArrayList(1, 2, 3)) + outer.add(asArrayList(4, 5)) outer.size().assertEquals(2) outer.get(0).size().assertEquals(3) outer.get(1).get(0).assertEquals(4) -@Test -function testNestedArrayList_mutation() - let outer = new ArrayList>() - outer.add(asArrayList(10, 20)) - outer.add(asArrayList(30)) - // mutate inner list via reference outer.get(0).set(1, 99) outer.get(0).get(1).assertEquals(99) - // replace inner list object - let c = asArrayList(7, 8, 9) - outer.set(1, c) - outer.get(1).size().assertEquals(3) - outer.get(1).get(2).assertEquals(9) - @Test -function testNestedArrayList_removeSwapOuter() - let outer = new ArrayList>() - ..add(asArrayList(1)) - ..add(asArrayList(2)) - ..add(asArrayList(3)) - let removed = outer.removeSwap(0) - removed.get(0).assertEquals(1) // we removed the first inner list - outer.size().assertEquals(2) - // order not guaranteed; just check the remaining are valid lists - (outer.get(0).size() >= 1 or outer.get(1).size() >= 1).assertTrue() +function testNullElements() + let list = new ArrayList() + list.add("a", null, "b") + list.size().assertEquals(3) + (list.get(1) == null).assertTrue() + list.indexOf(null).assertEquals(1) + destroy list + +// ============================================================================ +// REGRESSION TESTS (addAll aliasing + tuple payload integrity) +// ============================================================================ + +/** Minimal stand-in for a building shape (only what the search loop uses). */ +class CFBuildingTest + boolean isAntiAir = false + ArrayList upgrades = null + + construct(boolean isAntiAir, ArrayList upgrades) + this.isAntiAir = isAntiAir + this.upgrades = upgrades + + +/** + * Structurally a worklist search, with a hard iteration guard so the test + * fails instead of hanging if toCheck grows without bound. + */ +function iterativeSearchHasAntiAir(ArrayList startUpgrades) returns boolean + let toCheck = new ArrayList() + toCheck.addAll(startUpgrades) + + var result = false + var guard = 0 + + while not toCheck.isEmpty() + guard++ + // If addAll(self) ran away, size would explode and this guard would trip. + if guard > 2000 + testFail("iterativeSearch did not terminate (likely addAll(self) via aliasing upgrades==toCheck)") + + let b = toCheck.removeAtUnordered(toCheck.size() - 1) + if b.isAntiAir + result = true + break + if b.upgrades != null + toCheck.addAll(b.upgrades) -// ----------------------------- -// TUPLES IN ARRAYLISTS -// ----------------------------- + destroy toCheck + return result -tuple T2(int x, int y) @Test -function testTupleInArrayList_basic() - let list = new ArrayList() - list.add(T2(3, 4)) - list.add(T2(7, 8)) - list.size().assertEquals(2) - list.get(0).x.assertEquals(3) - list.get(1).y.assertEquals(8) +function testIterativeSearch_NormalTree_TerminatesAndFindsAA() + // A -> B -> C(AA) + let cUp = new ArrayList() + let c = new CFBuildingTest(true, null) + + let bUp = new ArrayList() + bUp.add(c) + let b = new CFBuildingTest(false, bUp) + + let aUp = new ArrayList() + aUp.add(b) + let a = new CFBuildingTest(false, aUp) + + let start = new ArrayList() + start.add(a) + + iterativeSearchHasAntiAir(start).assertTrue() + + destroy start + destroy aUp + destroy bUp + destroy cUp + destroy a + destroy b + destroy c + @Test -function testTupleInArrayList_removeSwap() - let list = new ArrayList() - ..add(T2(1, 2)) - ..add(T2(3, 4)) - ..add(T2(5, 6)) +function testIterativeSearch_AliasingUpgradesToToCheck_Terminates() + /* + * Recreates the nasty case: + * - pop element b from toCheck + * - b.upgrades points to the SAME list instance as toCheck + * - toCheck.addAll(b.upgrades) becomes toCheck.addAll(toCheck) + * + * addAll captures the source size up front, so this terminates (it may + * duplicate elements once, but it stops). + */ + let start = new ArrayList() - let mid = list.removeSwap(1) - mid.x.assertEquals(3) - list.size().assertEquals(2) - // element at 1 is now former last (order not preserved) - (list.get(1).x == 1 or list.get(1).x == 5).assertTrue() + let aUp = new ArrayList() + let a = new CFBuildingTest(false, aUp) + start.add(a) + + // Inlined search so we can build the alias to 'toCheck' itself. + let toCheck = new ArrayList() + toCheck.addAll(start) + + // Alias: a.upgrades points at the toCheck list itself + a.upgrades = toCheck + + var result = false + var guard = 0 + while not toCheck.isEmpty() + guard++ + if guard > 2000 + testFail("Loop did not terminate: addAll(self) alias reproduced") -// ----------------------------- -// NESTED TUPLES IN ARRAYLISTS -// ----------------------------- + let b = toCheck.removeAtUnordered(toCheck.size() - 1) + if b.isAntiAir + result = true + break + if b.upgrades != null + toCheck.addAll(b.upgrades) + + result.assertFalse() + + destroy toCheck + destroy start + destroy aUp + destroy a + + +/** + * Tuple payloads: + * - one purely primitive + * - one with a reference-type field to catch aliasing/reuse bugs + */ +tuple Pair(int a, int b) + +class Dummy + int id + construct(int id) + this.id = id + +tuple ObjPair(Dummy d, int idx) -tuple TInner(int a, int b) -tuple TOuter(int head, TInner tail) @Test -function testNestedTupleInArrayList_get_set() - let list = new ArrayList() - list.add(TOuter(10, TInner(20, 30))) - list.add(TOuter(40, TInner(50, 60))) +function testTupleInArrayList_PreservesDistinctValues_Primitives() + let l = new ArrayList() + + // push enough to force growth/realloc + for i = 0 to 99 + l.enqueue(Pair(i, i * 10)) - list.get(0).head.assertEquals(10) - list.get(0).tail.a.assertEquals(20) - list.get(1).tail.b.assertEquals(60) + // verify each slot retained its own values + for i = 0 to 99 + let p = l.get(i) + (p.a == i).assertTrue() + (p.b == i * 10).assertTrue() + + destroy l - list.set(0, TOuter(1, TInner(2, 3))) - list.get(0).tail.b.assertEquals(3) @Test -function testNullElements_storeAndIndexOf() - let list = new ArrayList() - list.add("a", null, "b") - list.size().assertEquals(3) - (list.get(1) == null).assertTrue() - // indexOf(null) is unspecified in docs, but == comparison should work: - list.indexOf(null).assertEquals(1) +function testTupleInArrayList_NoImplicitAliasingFromLocalVarReuse() + let l = new ArrayList() + + var t = Pair(1, 10) + l.enqueue(t) + + // reassign local variable; list must keep the old tuple value + t = Pair(2, 20) + l.enqueue(t) + + (l.get(0).a == 1).assertTrue() + (l.get(0).b == 10).assertTrue() + (l.get(1).a == 2).assertTrue() + (l.get(1).b == 20).assertTrue() + + destroy l -// ----------------------------- -// ITERATOR EDGE CASES -// ----------------------------- - -@Test -function testIterator_removeAtStartAndEnd() - let list = asArrayList(1, 2, 3, 4, 5) - let itr = list.iterator() - // remove first (after first next) - itr.next() // 1 - itr.remove() // remove 1 - list.size().assertEquals(4) - - // advance and remove last yielded - while itr.hasNext() - let v = itr.next() - if v == 5 - itr.remove() - itr.close() - list.size().assertEquals(3) - // remaining elements are {?, ?, ?} but 1 and 5 must be gone - (list.indexOf(1) == -1).assertTrue() - (list.indexOf(5) == -1).assertTrue() @Test -function testIterator_modifyAfterNext() - let list = asArrayList(10, 20, 30) - let itr = list.iterator() - itr.next() // 10 - itr.modify(11) - itr.next() // 20 - itr.modify(21) - itr.close() - list.get(0).assertEquals(11) - list.get(1).assertEquals(21) +function testTupleInArrayList_WithReferenceField_PreservesPerElementObject() + let l = new ArrayList() + for i = 0 to 49 + let d = new Dummy(1000 + i) + l.enqueue(ObjPair(d, i)) + + for i = 0 to 49 + let op = l.get(i) + (op.idx == i).assertTrue() + (op.d != null).assertTrue() + (op.d.id == 1000 + i).assertTrue() + + destroy l -// ----------------------------- -// BOUNDARIES & INSERTIONS -// ----------------------------- @Test -function testAddtoStart_and_addAtBoundaries() - let list = asArrayList(2, 3) - list.addtoStart(1) - list.get(0).assertEquals(1) - list.addAt(4, list.size()) // insert at end - list.getLast().assertEquals(4) +function testTwoArrayListsOfSameTupleType_DoNotCrossContaminate() + let a = new ArrayList() + let b = new ArrayList() + + for i = 0 to 49 + a.enqueue(Pair(i, 1000 + i)) + b.enqueue(Pair(i, 2000 + i)) + + for i = 0 to 49 + (a.get(i).b == 1000 + i).assertTrue() + (b.get(i).b == 2000 + i).assertTrue() + + destroy a + destroy b + @Test -function testRemoveAt_middle_and_shift() - let list = asArrayList(1, 2, 3, 4) - let removed = list.removeAt(1) // remove "2" - removed.assertEquals(2) - list.size().assertEquals(3) - list.get(1).assertEquals(3) // shifted over - -// ----------------------------- -// MAP / FILTER / FOLD with TUPLES -// ----------------------------- - -@Test -function testMapTuple_toInt() - let list = new ArrayList() - ..add(T2(1, 2)) - ..add(T2(3, 4)) - let sums = list.map(t -> t.x + t.y) - sums.size().assertEquals(2) - sums.get(0).assertEquals(3) - sums.get(1).assertEquals(7) - -@Test -function testFilterTuple_nested() - let list = new ArrayList() - ..add(TOuter(1, TInner(2, 0))) - ..add(TOuter(9, TInner(2, 1))) - ..add(TOuter(5, TInner(3, 0))) - let filtered = list.filter(o -> o.head >= 5 and o.tail.a >= 2) - filtered.size().assertEquals(2) - filtered.get(0).head.assertEquals(9) - filtered.get(1).head.assertEquals(5) - -@Test -function testFoldlTuple_collectSumOfLeaves() - let list = new ArrayList() - ..add(TOuter(1, TInner(2, 3))) - ..add(TOuter(4, TInner(5, 6))) - let sum = list.foldl(0, (o, acc) -> acc + o.head + o.tail.a + o.tail.b) - sum.assertEquals(1 + 2 + 3 + 4 + 5 + 6) - -// ----------------------------- -// STATIC STORE SEPARATION / MEMORY HINTS -// ----------------------------- - -@Test -function testStaticStore_separatePerType() - // The two types should not interfere; we check behaviorally. - let ints = new ArrayList() - let reals = new ArrayList() - ints.add(1, 2, 3) - reals.add(1.5, 2.5) - ints.size().assertEquals(3) - reals.size().assertEquals(2) - ints.get(1).assertEquals(2) - reals.get(1).assertEquals(2.5) - -@Test -function testClear_keepsCapacityAndStorage() - let list = new ArrayList(32) - for i = 0 to 15 - list.add(i) - let before = list.getMemoryInfo() - list.clear() - let after = list.getMemoryInfo() - // capacity and start usually stay the same; size is 0 now - (before.split(", ").get(0) == after.split(", ").get(0)).assertTrue() // Start: ... - (before.split(", ").get(1) == after.split(", ").get(1)).assertTrue() // Capacity: ... - list.size().assertEquals(0) +function testFindAndSet_OnTupleList() + let l = new ArrayList() + for i = 0 to 20 + l.enqueue(Pair(i, 100 + i)) -// ----------------------------- -// QUEUE/STACK + NULL INTERACTIONS -// ----------------------------- + let p = l.find(x -> x.a == 7) + (p.a == 7).assertTrue() + (p.b == 107).assertTrue() + l.set(7, Pair(7, 7777)) + + (l.get(7).b == 7777).assertTrue() + (l.get(6).b == 106).assertTrue() + (l.get(8).b == 108).assertTrue() + + destroy l + + +/** + * Catches a "repeated slots" failure mode: enqueue distinct payloads, then + * read them back and confirm every distinct value survives. + */ @Test -function testQueue_nulls() - let q = new ArrayList() - q.enqueue("a") - q.enqueue(null) - q.enqueue("c") - q.dequeue().assertEquals("a") - (q.dequeue() == null).assertTrue() - q.dequeue().assertEquals("c") +function testDetectRepeatedSlotsBug() + let l = new ArrayList() + + for i = 0 to 30 + l.enqueue(Pair(i, 10000 + i)) + + let seen = new ArrayList() + for i = 0 to 30 + let aVal = l.get(i).a + var found = false + for j = 0 to seen.size() - 1 + if seen.get(j) == aVal + found = true + if not found + seen.enqueue(aVal) + + // should see all 31 distinct values + seen.size().assertEquals(31) + destroy seen + destroy l From 388b736f792564b7d13bb1bc8a97c9bf4cf99f52 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 10 Jun 2026 18:00:34 +0200 Subject: [PATCH 4/4] use isLua flag --- wurst/data/ArrayList.wurst | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/wurst/data/ArrayList.wurst b/wurst/data/ArrayList.wurst index 82ed3b3f9..ec1e58e3e 100644 --- a/wurst/data/ArrayList.wurst +++ b/wurst/data/ArrayList.wurst @@ -29,7 +29,8 @@ constant int MAX_FREE_SECTIONS = 256 * ======================== * Each ArrayList type gets its own static storage array. * - ArrayList, ArrayList, ArrayList = 3 separate arrays - * - Each type can hold up to JASS_MAX_ARRAY_SIZE elements total across all instances + * - On the Jass target each type holds up to JASS_MAX_ARRAY_SIZE elements total across all + * instances (the Lua target grows dynamically and is not bounded by this) * * Choose wisely based on how many types you have. * @@ -87,11 +88,12 @@ constant int MAX_FREE_SECTIONS = 256 * 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 (wc3 native-array target): the shared store is bounded by - * JASS_MAX_ARRAY_SIZE total slots per type across all live instances; exceeding - * it raises an error. On the Lua target the backing store grows dynamically, so - * this cap does not apply - there ArrayList is still valuable mainly for keeping - * element types static instead of relying on typecasting. + * Hard limit (wc3 / Jass native target): the shared store is a fixed-size array, so it is + * bounded by JASS_MAX_ARRAY_SIZE total slots per type across all live instances; exceeding + * it raises an error. On the Lua target the store is a dynamically growing table, so the + * cap does not apply - allocateStorage branches on the magic isLua constant and skips the + * error. ArrayList's added value on Lua is keeping element types static instead of relying + * on typecasting. **/ public class ArrayList private static T array store @@ -139,10 +141,12 @@ public class ArrayList // No suitable free section, allocate new if nextFreeIndex + cap > JASS_MAX_ARRAY_SIZE - // Try to compact free sections + // Past the native array bound - compact to reclaim trailing freed space first compactFreeList() - if nextFreeIndex + cap > JASS_MAX_ARRAY_SIZE + // The Jass target is a fixed-size array and must error here. On Lua the store + // is a dynamically growing table, so the bound doesn't apply and we let it grow. + if not isLua and nextFreeIndex + cap > JASS_MAX_ARRAY_SIZE error("ArrayList: Storage limit exceeded for type") startIndex = nextFreeIndex