require 'roby/support' require 'roby/graph' module Roby # This exception is raised when an edge is being added in a DAG, while this # edge would create a cycle. class CycleFoundError < RuntimeError; end # Base support for relations. It is mixed-in objects that are part of # relation networks (like Task and EventGenerator) module DirectedRelationSupport include BGL::Vertex alias :child_object? :child_vertex? alias :parent_object? :parent_vertex? alias :related_object? :related_vertex? alias :each_child_object :each_child_vertex alias :each_parent_object :each_parent_vertex alias :each_relation :each_graph alias :clear_relations :clear_vertex cached_enum("graph", "relations", false) cached_enum("parent_object", "parent_objects", true) cached_enum("child_object", "child_objects", true) # The array of relations this object is part of def relations; enum_relations.to_a end # Computes and returns the set of objects related with this one (parent # or child). If +relation+ is given, enumerate only for this relation, # otherwise enumerate for all relations. If +result+ is given, it is a # ValueSet in which the related objects are added def related_objects(relation = nil, result = nil) result ||= ValueSet.new if relation result.merge(parent_objects(relation).to_value_set) result.merge(child_objects(relation).to_value_set) else each_relation { |rel| related_objects(rel, result) } end result end # Set of all parent objects in +relation+ alias :parent_objects :enum_parent_objects # Set of all child object in +relation+ alias :child_objects :enum_child_objects # Add a new child object in the +relation+ relation. This calls # * #adding_child_object on +self+ and #adding_parent_object on +child+ # just before the relation is added # * #added_child_object on +self+ and #added_parent_object on +child+ # just after def add_child_object(child, relation, info = nil) check_is_relation(relation) relation.add_relation(self, child, info) end # Add a new parent object in the +relation+ relation # * #adding_child_object on +parent+ and #adding_parent_object on # +self+ just before the relation is added # * #added_child_object on +parent+ and #added_child_object on +self+ # just after def add_parent_object(parent, relation, info = nil) parent.add_child_object(self, relation, info) end # Hook called before a new child is added in the +relation+ relation # def adding_child_object(child, relation, info) # child.adding_parent_object(self, relations, info) # super if defined? super # end # Hook called after a new child has been added in the +relation+ relation # def added_child_object(child, relations, info) # child.added_parent_object(self, relation, info) # super if defined? super # end # Hook called after a new parent has been added in the +relation+ relation #def added_parent_object(parent, relation, info); super if defined? super end ## Hook called after a new parent is being added in the +relation+ relation #def adding_parent_object(parent, relation, info); super if defined? super end # Remove the relation between +self+ and +child+. If +relation+ is # given, remove only a relations in this relation kind. def remove_child_object(child, relation = nil) check_is_relation(relation) apply_selection(relation, (relation || enum_relations)) do |relation| relation.remove_relation(self, child) end end # Remove relations where self is a parent. If +relation+ is given, # remove only the relations in this relation graph. def remove_children(relation = nil) apply_selection(relation, (relation || enum_relations)) do |relation| self.each_child_object(relation) do |child| remove_child_object(child, relation) end end end # Remove relations where +self+ is a child. If +relation+ is given, # remove only the relations in this relation graph def remove_parent_object(parent, relation = nil) parent.remove_child_object(self, relation) end # Remove all parents of +self+. If +relation+ is given, remove only the # parents in this relation graph def remove_parents(relation = nil) check_is_relation(relation) apply_selection(relation, (relation || enum_relations)) do |relation| relation.each_parent_object(self) do |parent| remove_parent_object(relation, parent) end end end # Hook called after a parent has been removed # def removing_parent_object(parent, relation); super if defined? super end # Hook called after a child has been removed # def removing_child_object(child, relation) # child.removing_parent_object(self, relation) # super if defined? super # end # Hook called after a parent has been removed # def removed_parent_object(parent, relation); super if defined? super end # Hook called after a child has been removed # def removed_child_object(child, relation) # child.removed_parent_object(self, relation) # super if defined? super # end # Remove all relations that point to or come from +to+ If +to+ is nil, # it removes all relations of +self+ def remove_relations(to = nil, relation = nil) check_is_relation(relation) if to remove_parent_object(to, relation) remove_child_object(to, relation) else apply_selection(relation, (relation || enum_relations)) do |relation| each_parent_object(relation) { |parent| remove_parent_object(parent, relation) } each_child_object(relation) { |child| remove_child_object(child, relation) } end end end # Raises if +type+ does not look like a relation def check_is_relation(type) # :nodoc: if type && !(RelationGraph === type) raise ArgumentError, "#{type} (of class #{type.class}) is not a relation type" end end # If +object+ is given, yields object or returns +object+ (if a block # is given or not). If +object+ is nil, either yields the elements of # +enumerator+ or returns enumerator. def apply_selection(object, enumerator) # :nodoc: if block_given? if object; yield(object) else enumerator.each { |o| yield(o) } end else if object; [object] else; enumerator end end end private :apply_selection end # This class manages the graph defined by an object relation in Roby. # Relation graphs are managed in hierarchies (for instance, in # EventStructure, Precedence is a superset of CausalLink, and CausalLink a # superset of both Forwarding and Signal). In this hierarchy, at each # level, an edge cannot be present in more than one graph. Nonetheless, it # is possible for a parent relation to have an edge which is present in # none of its children. class RelationGraph < BGL::Graph # The relation name attr_reader :name # The relation parent if any attr_accessor :parent # The set of graphs attr_reader :subsets # The graph options as given to RelationSpace#relation attr_reader :options # Creates a relation graph with the given name and options. The # following options are recognized: # +dag+:: # if the graph is a DAG. If true, add_relation will check that # no cycle is created # +subsets+:: # a set of RelationGraph objects that are children of this # one # +distributed+:: # if this relation graph should be seen by remote hosts def initialize(name, options = {}) @name = name @options = options @subsets = ValueSet.new @distribute = options[:distribute] @dag = options[:dag] @weak = options[:weak] if options[:subsets] options[:subsets].each(&method(:superset_of)) end end # True if this relation graph is a DAG attr_predicate :dag # True if this relation should be seen by remote peers attr_predicate :distribute # If this relation is weak. Weak relations can be removed without major # consequences. This is mainly used during plan garbage collection to # break cross-relations cycles (cycles which exist in the graph union # of all the relation graphs). attr_predicate :weak def to_s; name end # True if this relation does not have a parent def root_relation?; !parent end # Add a relation between +from+ and +to+. The relation is added on all # parent relation graphs as well. # # If #dag? is true, it checks that the new relation does not create a # cycle def add_relation(from, to, info = nil) # Get the toplevel DAG in our relation hierarchy. We only test for the # DAG property on this one, as it is the union of all its children top_dag = nil new_relations = [] rel = self while rel top_dag = rel if rel.dag? new_relations << rel rel = rel.parent end if top_dag && !top_dag.linked?(from, to) && top_dag.reachable?(to, from) raise CycleFoundError, "cannot add a #{from} -> #{to} relation since it would create a cycle" end # Now compute the set of relations in which we really have to add a # new relation top_rel = new_relations.last if top_rel.linked?(from, to) if !(old_info = from[to, top_rel]).nil? if old_info != info raise ArgumentError, "trying to change edge information" end end changed_info = [new_relations.pop] while !new_relations.empty? if new_relations.last.linked?(from, to) changed_info << new_relations.pop else break end end for rel in changed_info from[to, rel] = info end end unless new_relations.empty? if from.respond_to?(:adding_child_object) from.adding_child_object(to, new_relations, info) end if to.respond_to?(:adding_parent_object) to.adding_parent_object(from, new_relations, info) end for rel in new_relations rel.__bgl_link(from, to, info) end if from.respond_to?(:added_child_object) from.added_child_object(to, new_relations, info) end if to.respond_to?(:added_parent_object) to.added_parent_object(from, new_relations, info) end end end alias :__bgl_link :link # Reimplemented from BGL::Graph. Unlike this implementation, it is # possible to add an already existing edge if the +info+ parameter # matches. def link(from, to, info) if linked?(from, to) if info != from[to, self] raise ArgumentError, "trying to change edge information" end return end super end # Remove the relation between +from+ and +to+, in this graph and in its # parent graphs as well def remove_relation(from, to) rel = self relations = [] while rel relations << rel rel = rel.parent end if from.respond_to?(:removing_child_object) from.removing_child_object(to, relations) end if to.respond_to?(:removing_parent_object) to.removing_parent_object(from, relations) end for rel in relations rel.unlink(from, to) end if from.respond_to?(:removed_child_object) from.removed_child_object(to, relations) end if to.respond_to?(:removed_parent_object) to.removed_parent_object(from, relations) end end # Returns true if +relation+ is included in this relation (i.e. it is # either the same relation or one of its children) def subset?(relation) self.eql?(relation) || subsets.any? { |subrel| subrel.subset?(relation) } end # Returns +true+ if there is an edge +source+ -> +target+ in this graph # or in one of its parents def linked_in_hierarchy?(source, target) # :nodoc: linked?(source, target) || (parent.linked?(source, target) if parent) end # Declare that +relation+ is a superset of this relation def superset_of(relation) relation.each_edge do |source, target, info| if linked_in_hierarchy?(source, target) raise ArgumentError, "relation and self already share an edge" end end relation.parent = self subsets << relation # Copy the relations of the child into this graph relation.each_edge do |source, target, info| source.add_child_object(target, self, info) end end # The Ruby module that gets included in graph objects attr_accessor :support end # A relation space is a module which handles a list of relations and # applies them to a set of classes. In this context, a relation is both a # Ruby module which gets included in the classes this space is applied on, # and a RelationGraph object which holds the object graphs. # # See the files in roby/relations to see definitions of new relations class RelationSpace < Module # The set of relations included in this relation space attr_reader :relations # The set of klasses on which the relations have been applied attr_reader :applied def initialize @relations = Array.new @applied = Array.new super end # This relation applies on klass. It mainly means that a relation # defined on this RelationSpace will define the relation-access methods # and include its support module (if any) in +klass+. def apply_on(klass) klass.include DirectedRelationSupport each_relation do |graph| klass.include graph.support end applied << klass end # Yields the relations that are defined on this space def each_relation for rel in relations yield(rel) end end # Yields the root relations that are defined on this space def each_root_relation for rel in relations yield(rel) unless rel.parent end end # Returns the set of objects that are reachable from +obj+ through any # of the relations. Note that +b+ will be included in the result if # there is an edge obj => a in one relation and another edge # a => b in another relation # # If +strict+ is true, +obj+ is not included in the returned set def children_of(obj, strict = true, relations = nil) set = compute_children_of([obj].to_value_set, relations || self.relations) set.delete(obj) if strict set end # Internal implementation method for +children_of+ def compute_children_of(current, relations) # :nodoc: old_size = current.size for rel in relations next if (rel.parent && relations.include?(rel.parent)) components = rel.generated_subgraphs(current, false) for c in components current.merge c end end if current.size == old_size return current else return compute_children_of(current, relations) end end # Defines a relation in this relation space. This defines a relation # graph, and various iteration methods on the vertices. If a block is # given, it defines a set of functions which should be defined on the # vertex objects. # # = Options # child_name:: # define a each_#{child_name} method to iterate # on the vertex children. Uses the relation name by default (a Child # relation would define a each_child method) # parent_name:: # define a each_#{parent_name} method to iterate # on the vertex parents. If none is given, no method is defined # subsets:: a list of subgraphs. See RelationGraph#superset_of # noinfo:: # if the relation embeds some additional information. If true, # the child iterator method (each_#{child_name}) will yield (child, # info) instead of only child [false] # graph:: the relation graph class # distribute:: if true, the relation can be seen by remote peers [true] # single_child:: # if the relations accepts only one child per vertex # [false]. If this option is set, defines a #{child_name} # method which returns the only child or nil def relation(relation_name, options = {}, &block) options = validate_options options, :child_name => relation_name.to_s.underscore, :const_name => relation_name, :parent_name => nil, :subsets => ValueSet.new, :noinfo => false, :graph => RelationGraph, :distribute => true, :dag => true, :single_child => false, :weak => false # Check if this relation is already defined. If it is the case, reuse it. # This is needed mostly by the reloading code if const_defined?(options[:const_name]) graph = const_get(options[:const_name]) mod = graph.support else graph = options[:graph].new "#{self.name}::#{options[:const_name]}", options mod = Module.new do singleton_class.class_eval do define_method("__r_#{relation_name}__") { graph } end class_eval "@@__r_#{relation_name}__ = __r_#{relation_name}__" end const_set(options[:const_name], graph) relations << graph end mod.class_eval(&block) if block_given? if parent_enumerator = options[:parent_name] mod.class_eval <<-EOD def each_#{parent_enumerator}(&iterator) self.each_parent_object(@@__r_#{relation_name}__, &iterator) end EOD end if options[:noinfo] mod.class_eval <<-EOD def each_#{options[:child_name]} each_child_object(@@__r_#{relation_name}__) { |child| yield(child) } end def find_#{options[:child_name]} each_child_object(@@__r_#{relation_name}__) do |child| return child if yield(child) end nil end EOD else mod.class_eval <<-EOD def each_#{options[:child_name]} each_child_object(@@__r_#{relation_name}__) do |child| yield(child, self[child, @@__r_#{relation_name}__]) end end def find_#{options[:child_name]} each_child_object(@@__r_#{relation_name}__) do |child| return child if yield(child, self[child, @@__r_#{relation_name}__]) end nil end EOD end mod.class_eval <<-EOD def add_#{options[:child_name]}(to, info = nil) add_child_object(to, @@__r_#{relation_name}__, info) self end def remove_#{options[:child_name]}(to) remove_child_object(to, @@__r_#{relation_name}__) self end EOD if options[:single_child] mod.class_eval <<-EOD def #{options[:child_name]} each_child_object(@@__r_#{relation_name}__) do |child_task| return child_task end nil end EOD end graph.support = mod applied.each { |klass| klass.include mod } graph end end # Creates a new relation space which applies on +klass+. If a block is # given, it is eval'd in the context of the new relation space instance def self.RelationSpace(klass) klass.include DirectedRelationSupport relation_space = RelationSpace.new relation_space.apply_on klass relation_space end # Requires all Roby relation files (all files in roby/relations/) def self.load_all_relations Dir.glob("#{File.dirname(__FILE__)}/relations/*.rb").each do |file| require "roby/relations/#{File.basename(file, '.rb')}" end end end