1 /// This is a simplified stack implementation optimized to reuse the nodes it creates. 2 /// 3 /// Fluid uses stacks to iterate through tree structures such as `Rope` or `TextRulerCache`. The ability to quickly 4 /// create new stack items is crucial to their performance, and as such avoiding allocations is a must. For this reason, 5 /// nodes are reused whenever possible, instead of being reclaimed by the GC. 6 module fluid.future.stack; 7 8 version (unittest) 9 version = Fluid_EnableStackStatistics; 10 11 /// Implementation of a stack optimized to reduce allocations. 12 /// 13 /// Removing nodes from the stack using `pop` or `clear` will move them to a global stack of "free" nodes. 14 /// The next time an item is added to the stack, it will reuse one of these previously freed nodes. This recycling 15 /// mechanism makes it possible to significantly reduce the number of allocations as the program continues. 16 /// 17 /// If version `Fluid_MemoryStatistics` is set, `Stack` will count the number of total nodes it has allocated per 18 /// type and thread and expose then through `totalNodeCount`. 19 struct Stack(T) { 20 21 private { 22 23 /// All stack nodes that are not currently in use. 24 static StackNode!T* _trash; 25 26 version (Fluid_EnableStackStatistics) 27 static int _totalNodeCount; 28 29 /// Top of the stack. 30 StackNode!T* _top; 31 32 /// Bottom of the stack, used to quickly transfer all nodes to the stack with `clear`. 33 /// Warning: Bottom isn't cleared on `pop`, so it doesn't have to be null when empty. 34 StackNode!T* _bottom; 35 36 } 37 38 this(T item) { 39 // Stack could support batch operations so this would take a variadic argument 40 push(item); 41 } 42 43 ~this() { 44 clear(); 45 } 46 47 version (Fluid_EnableStackStatistics) { 48 49 /// This special field is only available when version `Fluid_MemoryStatistics` is set. `Stack` will then count 50 /// the number of total nodes it has allocated per type and thread. 51 /// 52 /// This count can be reset with `resetNodeCount`. 53 /// 54 /// Returns: Total number of nodes allocated by the stack. 55 static int totalNodeCount() { 56 return _totalNodeCount; 57 } 58 59 /// Reset `totalNodeCount` back to 0. 60 static void resetNodeCount() { 61 _totalNodeCount = 0; 62 } 63 64 } 65 66 /// Returns: True if the stack is empty. 67 bool empty() const { 68 return _top is null; 69 } 70 71 alias back = top; 72 alias removeBack = pop; 73 74 /// Returns: The item at the top of this stack. 75 ref inout(T) top() inout 76 in (!empty, "Nothing is at the top of the stack when it is empty") 77 do { 78 return _top.item; 79 } 80 81 /// Add an item to the stack. 82 /// Params: 83 /// item = Item to add to the top of the stack. 84 void push(T item) { 85 _top = getNode(_top, item); 86 assert(_top !is _top.next, "A node must not be trashed while still in use"); 87 88 // Mark this as the bottom, if it is so 89 if (_top.next is null) { 90 _bottom = _top; 91 assert(_bottom.next is null); 92 } 93 } 94 95 /// Remove the item at the top of the stack. 96 /// Returns: The item that was removed. 97 T pop() 98 in (!empty, "`pop` cannot operate on an empty stack") 99 do { 100 101 auto node = _top; 102 103 assert(node !is _trash, "Node was already trashed. Was the Stack duplicated?"); 104 105 // Remove the node from the stack 106 _top = node.next; 107 108 // Trash the node 109 node.next = _trash; 110 _trash = node; 111 112 return node.item; 113 114 } 115 116 /// Empty the stack. 117 /// 118 /// Done automatically when the stack leaves the scope. 119 void clear() 120 out (; empty) 121 do { 122 123 import core.memory : GC; 124 125 // Already empty, nothing to do 126 if (empty) return; 127 128 // If triggered from the GC, the objects cannot be trashed, as they may not be recyclable anymore 129 if (GC.inFinalizer) { 130 _top = null; 131 return; 132 } 133 134 // Trash the bottom 135 _bottom.next = _trash; 136 _trash = _top; 137 _top = null; 138 139 } 140 141 /// Params: 142 /// item = Item that will be pushed to the top of the stack. 143 void opOpAssign(string op : "~")(T item) { 144 145 push(item); 146 147 } 148 149 /// Returns: 150 /// A range that allows iterating on the range without removing any items from it. While the range functions, 151 /// items cannot be removed from the stack, or the range may break, possibly crashing the program. 152 StackRange!T opIndex() @system { 153 154 return StackRange!T(_top); 155 156 } 157 158 private StackNode!T* getNode(StackNode!T* next, T item) { 159 160 // Trash is empty, allocate 161 if (_trash is null) { 162 version (Fluid_EnableStackStatistics) { 163 _totalNodeCount++; 164 } 165 auto node = new StackNode!T(next, item); 166 return node; 167 } 168 169 // Take an item from the trash 170 else { 171 auto node = _trash; 172 _trash = node.next; 173 *node = StackNode!T(next, item); 174 return node; 175 } 176 177 } 178 179 } 180 181 /// A StackRange can be used to iterate a stack (starting from top, going to bottom) without modifying it. 182 /// 183 /// The stack cannot be modified while it has any range attached to it. 184 struct StackRange(T) { 185 186 private StackNode!T* node; 187 @disable this(); 188 189 private this(StackNode!T* node) { 190 this.node = node; 191 } 192 193 /// Returns: True if the range has been emptied. 194 bool empty() const @system { 195 return node is null; 196 } 197 198 /// Returns: The item at the top of this range. 199 ref inout(T) front() inout @system 200 in (!empty, "Cannot use `front` of an empty range") 201 do { 202 return node.item; 203 } 204 205 /// Advance to the next item of the range. 206 void popFront() @system 207 in (!empty, "Cannot use `popFront` on an empty range") 208 do { 209 node = node.next; 210 } 211 212 } 213 214 private struct StackNode(T) { 215 216 StackNode* next; 217 T item; 218 219 } 220 221 version (unittest) { 222 223 /// Struct for testing the stack. 224 private struct Test { 225 int value; 226 alias value this; 227 } 228 229 } 230 231 @("Stack functions correctly") 232 unittest { 233 234 Stack!Test stack; 235 stack._trash = null; 236 assert(stack.empty); 237 stack.push(Test(1)); 238 assert(stack.top == 1); 239 assert(stack._top.next is null); 240 stack.push(Test(2)); 241 assert(stack.top == 2); 242 assert(stack._top.next !is null); 243 stack.push(Test(3)); 244 assert(stack.top == 3); 245 assert(stack.pop() == 3); 246 assert(stack.pop() == 2); 247 assert(stack.pop() == 1); 248 assert(stack.empty); 249 250 assert(stack._trash !is null); 251 assert(*stack.getNode(null, Test(1)) == StackNode!Test(null, Test(1))); 252 assert(*stack.getNode(null, Test(2)) == StackNode!Test(null, Test(2))); 253 assert(*stack.getNode(null, Test(3)) == StackNode!Test(null, Test(3))); 254 assert(stack._trash is null); 255 assert(Stack!Test.totalNodeCount == 3); 256 Stack!Test.resetNodeCount(); 257 258 } 259 260 @("Stack recycles nodes") 261 unittest { 262 263 Stack!Test stack; 264 stack._trash = null; 265 stack ~= Test(1); 266 stack ~= Test(2); 267 stack ~= Test(3); 268 assert(stack._trash is null); 269 assert(stack._bottom !is null); 270 assert(Stack!Test.totalNodeCount == 3); 271 272 auto nodes = [stack._top, stack._top.next, stack._top.next.next]; 273 274 auto top = stack._top; 275 276 // Clear to destroy the stack 277 stack.clear(); 278 assert(stack.empty); 279 assert(stack._trash is top); 280 assert(stack._trash == nodes[0]); 281 assert(stack._trash.next == nodes[1]); 282 assert(stack._trash.next.next == nodes[2]); 283 284 stack ~= Test(4); 285 stack ~= Test(5); 286 stack ~= Test(6); 287 assert(Stack!Test.totalNodeCount == 3); 288 assert(stack._trash is null); 289 assert(stack._top == nodes[2]); 290 assert(stack._top.next == nodes[1]); 291 assert(stack._top.next.next == nodes[0]); 292 assert(stack.pop() == 6); 293 assert(stack.pop() == 5); 294 assert(stack.pop() == 4); 295 296 stack._trash = null; 297 assert(Stack!Test.totalNodeCount == 3); 298 Stack!Test.resetNodeCount(); 299 300 } 301 302 @("Multiple stacks can share resources") 303 unittest { 304 305 Stack!Test a; 306 Stack!Test b; 307 308 assert(Stack!Test.totalNodeCount == 0); 309 assert(a._trash is null); 310 assert(b._trash is null); 311 312 a ~= Test(1); 313 a ~= Test(2); 314 b ~= Test(3); 315 b ~= Test(4); 316 317 assert(Stack!Test.totalNodeCount == 4); 318 319 assert(a.pop() == 2); 320 assert(a._trash !is null); 321 322 auto freed = a._trash; 323 324 b ~= Test(5); 325 assert(b._top is freed); 326 assert(a._trash is null); 327 assert(Stack!Test.totalNodeCount == 4); 328 329 { 330 Stack!Test c; 331 c ~= Test(6); 332 c ~= Test(7); 333 c ~= Test(8); 334 c ~= Test(9); 335 } 336 assert(Stack!Test.totalNodeCount == 8); 337 assert(a._trash !is null); 338 339 a.clear(); 340 b.clear(); 341 342 foreach (i; 10..18) { 343 a ~= Test(i); 344 } 345 assert(Stack!Test.totalNodeCount == 8); 346 a ~= Test(18); 347 assert(Stack!Test.totalNodeCount == 9); 348 349 a.clear(); 350 Stack!Test.resetNodeCount(); 351 Stack!Test._trash = null; 352 353 } 354 355 @("Stack can be used with Range API") 356 unittest { 357 358 import std.algorithm; 359 360 assert(Stack!Test.totalNodeCount == 0); 361 assert(Stack!Test._trash is null); 362 363 Stack!Test stack; 364 stack ~= Test(1); 365 stack ~= Test(2); 366 stack ~= Test(3); 367 stack ~= Test(4); 368 stack ~= Test(5); 369 370 assert(Stack!Test.totalNodeCount == 5); 371 assert(stack[].equal([5, 4, 3, 2, 1])); 372 assert(Stack!Test.totalNodeCount == 5); 373 374 assert(stack.pop() == Test(5)); 375 assert(stack.pop() == Test(4)); 376 assert(stack.pop() == Test(3)); 377 assert(stack.pop() == Test(2)); 378 assert(stack.pop() == Test(1)); 379 assert(stack.empty()); 380 381 assert(Stack!Test.totalNodeCount == 5); 382 Stack!Test.resetNodeCount(); 383 Stack!Test._trash = null; 384 385 }