# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. module Async # A double linked list. class List def initialize # The list behaves like a list node, so @tail points to the next item (the first one) and head points to the previous item (the last one). This may be slightly confusing but it makes the interface more natural. @head = nil @tail = nil @size = 0 end attr :size attr_accessor :head attr_accessor :tail # Inserts an item at the end of the list. def insert(item) unless @tail @tail = item @head = item # Consistency: item.head = nil item.tail = nil else @head.tail = item item.head = @head # Consistency: item.tail = nil @head = item end @size += 1 return self end def delete(item) if @tail.equal?(item) @tail = @tail.tail else item.head.tail = item.tail end if @head.equal?(item) @head = @head.head else item.tail.head = item.head end item.head = nil item.tail = nil @size -= 1 return self end def each(&block) return to_enum unless block_given? current = self while node = current.tail yield node # If the node has deleted itself or any subsequent node, it will no longer be the next node, so don't use it for continued traversal: if current.tail.equal?(node) current = node end end end def include?(needle) self.each do |item| return true if needle.equal?(item) end return false end def first @tail end def last @head end def empty? @tail.nil? end def nil? @tail.nil? end end private_constant :List class Children < List def initialize super @transient_count = 0 end # Does this node have (direct) transient children? def transients? @transient_count > 0 end def insert(item) if item.transient? @transient_count += 1 end super end def delete(item) if item.transient? @transient_count -= 1 end super end def finished? @size == @transient_count end end # Represents a node in a tree, used for nested {Task} instances. class Node # Create a new node in the tree. # @param parent [Node, nil] This node will attach to the given parent. def initialize(parent = nil, annotation: nil, transient: false) @parent = nil @children = nil @annotation = annotation @object_name = nil @transient = transient @head = nil @tail = nil if parent parent.add_child(self) end end # You should not directly rely on these pointers but instead use `#children`. # List pointers: attr_accessor :head attr_accessor :tail # @attr parent [Node, nil] attr :parent # @attr children [List] Optional list of children. attr :children # A useful identifier for the current node. attr :annotation # Whether there are children? def children? @children != nil && !@children.empty? end # Is this node transient? def transient? @transient end def annotate(annotation) if block_given? previous_annotation = @annotation @annotation = annotation yield @annotation = previous_annotation else @annotation = annotation end end def description @object_name ||= "#{self.class}:0x#{object_id.to_s(16)}#{@transient ? ' transient' : nil}" if @annotation "#{@object_name} #{@annotation}" else @object_name end end def backtrace(*arguments) nil end def to_s "\#<#{description}>" end # Change the parent of this node. # @param parent [Node, nil] the parent to attach to, or nil to detach. # @return [self] def parent=(parent) return if @parent.equal?(parent) if @parent @parent.delete_child(self) @parent = nil end if parent parent.add_child(self) end return self end protected def set_parent parent @parent = parent end protected def add_child child @children ||= Children.new @children.insert(child) child.set_parent(self) end protected def delete_child(child) @children.delete(child) child.set_parent(nil) end # Whether the node can be consumed safely. By default, checks if the # children set is empty. # @return [Boolean] def finished? @children.nil? || @children.finished? end # If the node has a parent, and is {finished?}, then remove this node from # the parent. def consume if parent = @parent and finished? parent.delete_child(self) if @children @children.each do |child| if child.finished? delete_child(child) else # In theory we don't need to do this... because we are throwing away the list. However, if you don't correctly update the list when moving the child to the parent, it foobars the enumeration, and subsequent nodes will be skipped, or in the worst case you might start enumerating the parents nodes. delete_child(child) parent.add_child(child) end end @children = nil end parent.consume end end # Traverse the tree. # @yield [node, level] The node and the level relative to the given root. def traverse(level = 0, &block) yield self, level @children&.each do |child| child.traverse(level + 1, &block) end end def stop @children&.each(&:stop) end def print_hierarchy(out = $stdout, backtrace: true) self.traverse do |node, level| indent = "\t" * level out.puts "#{indent}#{node}" print_backtrace(out, indent, node) if backtrace end end private def print_backtrace(out, indent, node) if backtrace = node.backtrace backtrace.each_with_index do |line, index| out.puts "#{indent}#{index.zero? ? "→ " : " "}#{line}" end end end end end