diff --git a/.editorconfig b/.editorconfig index 72b66b05..25c767df 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 00000000..ec1e58e3 --- /dev/null +++ b/wurst/data/ArrayList.wurst @@ -0,0 +1,676 @@ +package ArrayList +import NoWurst +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. + * In most cases, a LinkedList is a better choice due to its flexibility. + * This data structure is only recommended for performance-critical code + * and requires careful use to avoid fragmentation. + * + * WHEN TO USE: + * =========== + * ArrayList is faster than LinkedList for: + * - Iterating large lists (1000+ elements) - no node indirection + * - Index based operations - O(1) vs O(n) + * - When only appending elements to the end + * + * LinkedList is better for: + * - Insertion anywhere except the end of the list + * - Deletions while retaining order + * - Unknown size requirements + * - No resize performance risk + * + * TYPE SYSTEM IMPLICATIONS: + * ======================== + * Each ArrayList type gets its own static storage array. + * - ArrayList, ArrayList, ArrayList = 3 separate arrays + * - 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. + * + * PERFORMANCE RULES: + * ================== + * + * 1. PRESIZE, DON'T RESIZE + * Bad: new ArrayList() // Might resize multiple times + * Good: new ArrayList(maxSize) // One allocation + * + * Why: Resize operations copy ALL elements to new memory. Expensive! + * + * 2. REUSE, DON'T RECREATE + * Bad: In loop `let temp = new ArrayList() ... destroy temp` + * Good: `let temp = new ArrayList() ... temp.clear()` in loop + * + * Why: Allocation/Deallocation is moderately expensive and causes fragmentation + * + * 3. ORDERED REMOVAL IS SLOW + * Bad: list.removeAt(i) // O(n) - shifts all elements + * Good: list.removeAtUnordered(i) // O(1) - swaps with last element + * + * Only use removeAtUnordered() if order doesn't matter + * + * 4. ITERATE BY INDEX + * Good: for i = 0 to list.size()-1 // Zero allocation + * Good: list[i] // Indexing operator + * + * 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 + * Good: Use LinkedList or add in reverse order + * + * PERFORMANCE TABLE: + * ================== + * Operation | ArrayList | LinkedList | Notes + * -------------------|-----------|------------|--------------------------- + * add(elem) | O(1)* | O(1) | *O(n) on resize! + * addtoStart(elem) | O(n) | O(1) | Shifts all elements + * get(index) | O(1) | O(n) | Major ArrayList advantage + * removeAt(index) | O(n) | O(1) | Shifts remaining elements + * 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 + * + * MEMORY MANAGEMENT: + * ================== + * ArrayList uses section allocation in a shared static array per type. + * Destroyed lists return sections to a free pool for reuse. + * Free sections are compacted to reduce fragmentation. + * + * Fragmentation occurs when lists grow - the old section becomes a gap. + * This is why presizing matters: growth = copy to new location = wasted space. + * + * Hard limit (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 + private static int nextFreeIndex = 0 + + // Memory management structures + 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 + + /** 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 > 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) + 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 + 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 + // Past the native array bound - compact to reclaim trailing freed space first + compactFreeList() + + // 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 + 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 + for i = 0 to size - 1 + store[startIndex + i] = null + + // Return storage to free pool + freeStorage() + + // ============================================================================ + // 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 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 + 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 + 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)) */ + 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 + + function removeAtOrdered(int index) returns T + if index < 0 or index >= size + error("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 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 removeAtUnordered(int index) returns T + if index < 0 or index >= size + error("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)) */ + @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 + 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 + + /** 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, or null if empty (O(1)) */ + function getFirst() returns T + if size == 0 + return null + return store[startIndex] + + /** Returns the last element in the list, or null if empty (O(1)) */ + function getLast() returns T + if size == 0 + return null + 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, or null if empty */ + 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 removeAtOrdered(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 + error("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 + // ============================================================================ + + /** Removes elements that satisfy the predicate (O(n), preserves order). + If order is not important, use #removeUnorderedIf */ + function removeIf(ArrayListPredicate predicate) + // 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 */ + 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 + +// ============================================================================ +// 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("") + + +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 new file mode 100644 index 00000000..52fa6e4c --- /dev/null +++ b/wurst/data/ArrayListMemoryTests.wurst @@ -0,0 +1,139 @@ +package ArrayListMemoryTests +import ArrayList + +// 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 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 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 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) + list.size().assertEquals(100) + for i = 0 to 99 + list.get(i).assertEquals(i) + destroy list + +@Test +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) + + let grower = new ArrayList(2) + for i = 0 to 49 + grower.add(i) + + neighbour.get(0).assertEquals(11) + neighbour.get(3).assertEquals(44) + grower.get(0).assertEquals(0) + grower.get(49).assertEquals(49) + + 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 testCopyIsIndependentInStore() + let original = new ArrayList() + original.add(1, 2, 3, 4, 5) + let clone = original.copy() + + original.set(0, 99) + clone.get(0).assertEquals(1) + original.get(0).assertEquals(99) + + // mutating the clone must not bleed back into the original + clone.set(4, -1) + original.get(4).assertEquals(5) + + destroy original + destroy clone + +@Test +function testLargeAllocationAndAccess() + let list = new ArrayList(1000) + 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) + destroy list + +@Test +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 new file mode 100644 index 00000000..5413d084 --- /dev/null +++ b/wurst/data/ArrayListTests.wurst @@ -0,0 +1,612 @@ +package ArrayListTests +import ArrayList + +// ============================================================================ +// CORE API +// ============================================================================ + +@Test +function testAddGetSet() + let list = new ArrayList() + 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 = 0 to list.size() - 1 + result += list.get(i) + + 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() + 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 = 0 to list.size() - 1 + 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) + + list.updateAll(i -> i * 2) // [2, 4, 6, 8] + list.get(3).assertEquals(8) + + list.removeIf(i -> i > 4) // [2, 4] + list.size().assertEquals(2) + + 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() + 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(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) // 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 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 testRemoveAtOrdered() + let list = asArrayList(1, 2, 3, 4) + list.removeAtOrdered(1).assertEquals(2) + list.size().assertEquals(3) + list.get(1).assertEquals(3) // shifted over + list.get(2).assertEquals(4) + destroy list + +@Test +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 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 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 list = new ArrayList() + 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 +function testQueue() + let list = new ArrayList() + ..enqueue("a") + ..enqueue("b") + ..enqueue("c") + list.dequeue().assertEquals("a") + list.dequeue().assertEquals("b") + list.dequeue().assertEquals("c") + list.isEmpty().assertTrue() + destroy list + +@Test +function testStack() + let list = new ArrayList() + ..push("a") + ..push("b") + ..push("c") + list.peek().assertEquals("c") + list.pop().assertEquals("c") + list.pop().assertEquals("b") + 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) + destroy list + +@Test +function testCopy() + let list = new ArrayList() + list.add(1, 2, 3, 4, 5) + let cp = list.copy() + cp.size().assertEquals(5) + cp.get(2).assertEquals(3) + + cp.set(2, 99) + list.get(2).assertEquals(3) // copy is independent + cp.get(2).assertEquals(99) + + destroy list + destroy cp + +@Test +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 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 + +@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() + let outer = new ArrayList>() + 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) + + // mutate inner list via reference + outer.get(0).set(1, 99) + outer.get(0).get(1).assertEquals(99) + +@Test +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) + + destroy toCheck + return result + + +@Test +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 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 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") + + 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) + + +@Test +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)) + + // 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 + + +@Test +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 + + +@Test +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 + + +@Test +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 testFindAndSet_OnTupleList() + let l = new ArrayList() + for i = 0 to 20 + l.enqueue(Pair(i, 100 + i)) + + 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 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