module Stamina # # Automaton data-structure. # # == Examples # The following example uses a lot of useful DRY shortcuts, so, if it does not # fit you needs then, read on!): # # # Building an automaton for the regular language a(ba)* # fa = Automaton.new do # add_state(:initial => true) # add_state(:accepting => true) # connect(0,1,'a') # connect(1,0,'b') # end # # # It accepts 'a b a b a', rejects 'a b' as well as '' # puts fa.accepts?('? a b a b a') # prints true # puts fa.accepts?('? a b') # prints false # puts fa.rejects?('?') # prints true # # == Four things you need to know # 1. Automaton, State and Edge classes implement a Markable design pattern, that # is, you can read and write any key/value pair you want on them using the [] # and []= operators. Note that the following keys are used by Stamina itself, # with the obvious semantics (for automata and transducers): # - :initial, :accepting, :error on State; # expected to be _true_ or _false_ (_nil_ and ommitted are considered as false). # Shortcuts for querying and setting these attributes are provided by State. # - :symbol on Edge, with shortcuts as well on Edge. # The convention is to use _nil_ for the epsilon symbol (aka non observable) # on non deterministic automata. # The following keys are reserved for future extensions: # - :output on State and Edge. # - :short_prefix on State. # See also the "About states and edges" subsection of the design choices. # 2. Why using State methods State#step and State#delta ? The Automaton class includes # the Walking module by default, which is much more powerful ! # 3. The constructor of this class executes the argument block (between do # and end) with instance_eval by default. You won't be able to invoke # the methods defined in the scope of your block in such a case. See new # for details. # 4. This class has not been designed with efficiency in mind. If you experiment # performance problems, read the "About Automaton modifications" sub section # of the design choices. # # == Design choices # This section fully details the design choices that has been made for the # implementation of the Automaton data structure used by Stamina. It is provided # because Automaton is one of the core classes of Stamina, that probably all # users (and contributors) will use. Automaton usage is really user-friendly, # so you are normally not required to read this section in the first # place ! Read it only if of interest for you, or if you experiment unexpected # results. # # === One Automaton class only # One class only implements all kinds of automata: deterministic, non-deterministic, # transducers, prefix-tree-acceptors, etc. The Markable design pattern on states and # edges should allow you to make anything you could find useful with this class. # # === Adjacency-list graph # This class implements an automaton using a adjacent-list graph structure. # The automaton has state and edge array lists and exposes them through the # _states_ and _edges_ accessors. In order to let users enjoy the enumerability # of Ruby's arrays while allowing automata to be modified, these arrays are # externaly modifiable. However, users are not expected to modify them! # and future versions of Stamina will certainly remove this ability. # # === Indices exposed # State and Edge indices in these arrays are exposed by this class. Unless stated # explicitely, all methods taking state or edge arguments support indices as well. # Moreover, ith_state, ith_states, ith_edge and ith_edges methods provide powerful # access to states and edges by indices. All these methods are robust to invalid # indices (and raise an IndexError if incorrectly invoked) but do not allow # negative indexing (unlike ruby arrays). # # States and edges know their index in the corresponding array and expose them # through the (read-only) _index_ accessor. These indices are always valid; # without deletion of states or edges in the automaton, they are guaranteed not # to change. Indices saved in your own variables must be considered deprecated # each time you perform a deletion ! That's the only rule to respect if you plan # to use indices. # # Indices exposition may seem a strange choice and could be interpreted as # breaking OOP's best practice. You are not required to use them but, as will # quiclky appear, using them is really powerful and leads to beautiful code! # If you don't remove any state or edge, this class guarantees that indices # are assigned in the same order as invocations of add_state and add_edge (as # well as their plural forms and aliases). # # === About states and edges # Edges know their source and target states, which are exposed through the # _source_ and _target_ (read-only) accessors (also aliased as _from_ and _to_). # States keep their incoming and outgoing edges in arrays, which are accessible # (in fact, a copy) using State#in_edges and State#out_edges. If you use them # for walking the automaton in a somewhat standard way, consider using the Walking # module instead! # # Common attributes of states and edges are installed using the Markable pattern # itself: # - :initial, :accepting and :error on states. These # attributes are expected to be _true_ or _false_ (_nil_ and ommitted are also # supported and both considered as false). # - :symbol on edges. Any object you want as long as it responds to the # <=> operator. Also, the convention is to use _nil_ for the epsilon # symbol (aka non observable) on non deterministic automata. # # In addition, useful shortcuts are available: # - s.initial? is a shortcut for s[:initial] if _s_ is a State # - s.initial! is a shortcut for s[:initial]=true if _s_ is a State # - Similar shortcuts are available for :accepting and :error # - e.symbol is a shortcut for e[:symbol] if _e_ is an Edge # - e.symbol='a' is a shortcut for e[:symbol]='a' if _e_ is an Edge # # Following keys should be considered reserved by Stamina for future extensions: # - :output on State and Edge. # - :short_prefix on State. # # === About Automaton modifications # This class has not been implemented with efficiency in mind. In particular, we expect # the vast majority of Stamina core algorithms considering automata as immutable values. # For this reason, the Automaton class does not handle modifications really efficiently. # # So, if you experiment performance problems, consider what follows: # 1. Why updating an automaton ? Building a fresh one is much more clean and efficient ! # This is particularly true for removals. # 2. If you can create multiples states or edges at once, consider the plural form # of the modification methods: add_n_states and drop_states. Those methods are # optimized for multiple updates. # # == Detailed API class Automaton include Stamina::Markable # # Automaton state. # class State include Stamina::Markable attr_reader :automaton, :index # # Creates a state. # # Arguments: # - automaton: parent automaton of the state. # - index: index of the state in the state list. # - data: user data attached to this state. # def initialize(automaton, index, data) @automaton = automaton @index = index @data = data.dup @out_edges = [] @in_edges = [] @epsilon_closure = nil end ### public read-only section ############################################### public # # Returns true if this state is an initial state, false otherwise. # def initial?() return false unless @data[:initial]; @data[:initial] end # # Sets this state as an initial state. # def initial!() @data[:initial] = true end # # Returns true if this state is an accepting state, false otherwise. # def accepting?() return false unless @data[:accepting]; @data[:accepting] end # # Sets this state as an accepting state. # def accepting!() @data[:accepting] = true end # # Returns true if this state is an error state, false otherwise. # def error?() return false unless @data[:error]; @data[:error] end # # Sets this state as an error state. # def error!() @data[:error] = true end # # Returns true if this state is deterministic, false otherwise. # def deterministic? outs = out_symbols (outs.size==@out_edges.size) and not(outs.include?(nil)) end # Checks if this state is a sink state or not. Sink states are defined as # non accepting states having no outgoing transition or only loop # transitions. def sink? return false if accepting? out_edges.each{|e| return false unless e.target==self} true end # # Returns an array containing all incoming edges of the state. Edges are # sorted if _sorted_ is set to true. If two incoming edges have same symbol # no order is guaranteed between them. # # Returned array may be modified. # def in_edges(sorted=false) sorted ? @in_edges.sort : @in_edges.dup end # # Returns an array containing all outgoing edges of the state. Edges are # sorted if _sorted_ is set to true. If two outgoing edges have same symbol # no order is guaranteed between them. # # Returned array may be modified. # def out_edges(sorted=false) sorted ? @out_edges.sort : @out_edges.dup end # # Returns an array with the different symbols appearing on incoming edges. # Returned array does not contain duplicates. Symbols are sorted in the # array if _sorted_ is set to true. # # Returned array may be modified. # def in_symbols(sorted=false) symbols = @in_edges.collect{|e| e.symbol}.uniq return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols end # # Returns an array with the different symbols appearing on outgoing edges. # Returned array does not contain duplicates. Symbols are sorted in the # array if _sorted_ is set to true. # # Returned array may be modified. # def out_symbols(sorted=false) symbols = @out_edges.collect{|e| e.symbol}.uniq return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols end # # Returns an array with adjacent states (in or out edge). # # Returned array may be modified. # def adjacent_states() (in_adjacent_states+out_adjacent_states).uniq end # # Returns an array with adjacent states along an incoming edge (without # duplicates). # # Returned array may be modified. # def in_adjacent_states() (@in_edges.collect {|e| e.source}).uniq end # # Returns an array with adjacent states along an outgoing edge (whithout # duplicates). # # Returned array may be modified. # def out_adjacent_states() (@out_edges.collect {|e| e.target}).uniq end # # Returns reachable states from this one with an input _symbol_. Returned # array does not contain duplicates and may be modified. This method if not # epsilon symbol aware. # def step(symbol) @out_edges.select{|e| e.symbol==symbol}.collect{|e| e.target} end # # Returns the state reached from this one with an input _symbol_, or nil if # no such state. This method is not epsilon symbol aware. Moreover it is # expected to be used on deterministic states only. If the state is not # deterministic, the method returns one reachable state if such a state # exists; which one is returned must be considered non deterministic. # def dfa_step(symbol) @out_edges.each {|e| return e.target if e.symbol==symbol} nil end # # Computes the epsilon closure of this state. Epsilon closure is the set of # all states reached from this one with a eps* input (sequence of # zero or more epsilon symbols). The current state is always contained in # the epsilon closure. Returns an unsorted array without duplicates; this # array may not be modified. # def epsilon_closure() @epsilon_closure ||= compute_epsilon_closure(Set.new).to_a.freeze end # # Internal implementation of epsilon_closure. _result_ is expected to be # a Set instance, is modified and is the returned value. # def compute_epsilon_closure(result) result << self step(nil).each do |t| t.compute_epsilon_closure(result) unless result.include?(t) end raise if result.nil? return result end # # Computes an array representing the set of states that can be reached from # this state with a given input _symbol_. Returned array does not contain # duplicates and may be modified. No particular ordering of states in the # array is guaranteed. # # This method is epsilon symbol aware (represented with nil) on non # deterministic automata, meaning that it actually computes the set of # reachable states through strings respecting the eps* symbol eps* # regular expression, where eps is the epsilon symbol. # def delta(symbol) if automaton.deterministic? target = dfa_delta(symbol) target.nil? ? [] : [target] else # 1) first compute epsilon closure of self at_epsilon = epsilon_closure # 2) now, look where we can go from there at_espilon_then_symbol = at_epsilon.collect do |s| s.step(symbol) end.flatten.uniq # 3) look where we can go from there using epsilon result = at_espilon_then_symbol.collect do |s| s.epsilon_closure end.flatten.uniq # return result as an array result end end # # Returns the target state that can be reached from this state with _symbol_ # input. Returns nil if no such state exists. # # This method is expected to be used on deterministic automata. Unlike delta, # it returns a State instance (or nil), not an array of states. When used on # non deterministic automata, it returns a state immediately reachable from # this state with _symbol_ input, or nil if no such state exists. This # method is not epsilon aware. # def dfa_delta(symbol) return nil if symbol.nil? edge = @out_edges.find{|e| e.symbol==symbol} edge.nil? ? nil : edge.target end # # Provides comparator of states, based on the index in the automaton state # list. This method returns nil unless _o_ is a State from the same # automaton than self. # def <=>(o) return nil unless State===o return nil unless automaton===o.automaton return index <=> o.index end # Returns a string representation def inspect 's' << @index.to_s end # Returns a string representation def to_s 's' << @index.to_s end ### protected write section ################################################ protected # Changes the index of this state in the state list. This method is only # expected to be used by the automaton itself. def index=(i) @index=i end # # Fired by Loaded when a user data is changed. The message is forwarded to # the automaton. # def state_changed(what, description) @epsilon_closure = nil @automaton.send(:state_changed, what, description) end # Adds an incoming edge to the state. def add_incoming_edge(edge) @epsilon_closure = nil @in_edges << edge end # Adds an outgoing edge to the state. def add_outgoing_edge(edge) @epsilon_closure = nil @out_edges << edge end # Adds an incoming edge to the state. def drop_incoming_edge(edge) @epsilon_closure = nil @in_edges.delete(edge) end # Adds an outgoing edge to the state. def drop_outgoing_edge(edge) @epsilon_closure = nil @out_edges.delete(edge) end protected :compute_epsilon_closure end # # Automaton edge. # class Edge include Stamina::Markable attr_reader :automaton, :index, :from, :to # # Creates an edge. # # Arguments: # - automaton: parent automaton of the edge. # - index: index of the edge in the edge list. # - data: user data attached to this edge. # - from: source state of the edge. # - to: target state of the edge. # def initialize(automaton, index, data, from, to) @automaton, @index = automaton, index @data = data @from, @to = from, to end # Returns edge symbol. def symbol() @data[:symbol] end # Sets edge symbol. def symbol=(symbol) @data[:symbol] = symbol end alias :source :from alias :target :to # # Provides comparator of edges, based on the index in the automaton edge # list. This method returns nil unless _o_ is an Edge from the same # automaton than self. # Once again, this method has nothing to do with equality, it looks at an # index and ID only. # def <=>(o) return nil unless Edge===o return nil unless automaton===o.automaton return index <=> o.index end # Returns a string representation def inspect 'e' << @index.to_s end # Returns a string representation def to_s 'e' << @index.to_s end ### protected write section ################################################ protected # Changes the index of this edge in the edge list. This method is only # expected to be used by the automaton itself. def index=(i) @index=i end # # Fired by Loaded when a user data is changed. The message if forwarded to # the automaton. # def state_changed(what, infos) @automaton.send(:state_changed, what, infos) end end ### Automaton class ########################################################## public # State list and edge list of the automaton attr_reader :states, :edges # # Creates an empty automaton and executes the block passed as argument. The _onself_ # argument dictates the way _block_ is executed: # - when set to false, the block is executed traditionnally (i.e. using yield). # In this case, methods invocations must be performed on the automaton object # passed as block argument. # - when set to _true_ (by default) the block is executed in the context of the # automaton itself (i.e. with instance_eval), allowing call of its methods # without prefixing them by the automaton variable. The automaton still # passes itself as first block argument. Note that in this case, you won't be # able to invoke a method defined in the scope of your block. # # Example: # # The DRY way to do: # Automaton.new do |automaton| # automaton will not be used here, but it is passed # add_state(:initial => true) # add_state(:accepting => true) # connect(0, 1, 'a') # connect(1, 0, 'b') # # # method_in_caller_scope() # commented because not allowed here !! # end # # # The other way: # Automaton.new(false) do |automaton| # automaton MUST be used here # automaton.add_state(:initial => true) # automaton.add_state(:accepting => true) # automaton.connect(0, 1, 'a') # automaton.connect(1, 0, 'b') # # method_in_caller_scope() # allowed in this variant !! # end # def initialize(onself=true, &block) # :yields: automaton @states = [] @edges = [] @initials = nil @alphabet = nil @deterministic = nil # if there's a block, execute it now! if block_given? if onself if RUBY_VERSION >= "1.9.0" instance_exec(self, &block) else instance_eval(&block) end else block.call(self) end end end ### public read-only section ################################################# public # # Returns a symbols comparator taking epsilon symbols into account. Comparator # is provided as Proc instance which is a lambda function. # def symbols_comparator @symbols_comparator ||= Kernel.lambda do |a,b| if a==b then 0 elsif a.nil? then -1 elsif b.nil? then 1 else a <=> b end end end # Returns the number of states def state_count() @states.size end # Returns the number of edges def edge_count() @edges.size end # # Returns the i-th state of the state list. # # Raises: # - ArgumentError unless i is an Integer # - IndexError if i is not in [0..state_count) # def ith_state(i) raise(ArgumentError, "Integer expected, #{i} found.", caller)\ unless Integer === i raise(ArgumentError, "Invalid state index #{i}", caller)\ unless i>=0 and i=0 and i=state_count. # - ArgumentError if _state_ is not a valid state for this automaton. # def in_edges(state, sorted=false) to_state(state).in_edges(sorted) end # # Returns an array with outgoing edges of _state_. Edges are sorted by symbols # if _sorted_ is set to true. If two incoming edges have same symbol, no # order is guaranteed between them. Returned array may be modified. # # If _state_ is an Integer, this method returns the outgoing edges of the # state'th state in the state list. # # Raises: # - IndexError if state is an Integer and state<0 or state>=state_count. # - ArgumentError if state is not a valid state (not a state or not from this # automaton) # def out_edges(state, sorted=false) to_state(state).out_edges(sorted) end # # Returns an array with the different symbols appearing on incoming edges of # _state_. Returned array does not contain duplicates and may be modified; # it is sorted if _sorted_ is set to true. # # If _state_ is an Integer, this method returns the incoming symbols of the # state'th state in the state list. # # Raises: # - IndexError if state is an Integer and state<0 or state>=state_count. # - ArgumentError if _state_ is not a valid state for this automaton. # def in_symbols(state, sorted=false) to_state(state).in_symbols(sorted) end # # Returns an array with the different symbols appearing on outgoing edges of # _state_. Returned array does not contain duplicates and may be modified; # it is sorted if _sorted_ is set to true. # # If _state_ is an Integer, this method returns the outgoing symbols of the # state'th state in the state list. # # Raises: # - IndexError if state is an Integer and state<0 or state>=state_count. # - ArgumentError if state is not a valid state (not a state or not from this # automaton) # def out_symbols(state, sorted=false) to_state(state).out_symbols(sorted) end # # Returns an array with adjacent states (along incoming and outgoing edges) # of _state_. Returned array does not contain duplicates; it may be modified. # # If _state_ is an Integer, this method returns the adjacent states of the # state'th state in the state list. # # Raises: # - IndexError if state is an Integer and state<0 or state>=state_count. # - ArgumentError if state is not a valid state (not a state or not from this # automaton) # def adjacent_states(state) to_state(state).adjacent_states() end # # Returns an array with adjacent states (along incoming edges) of _state_. # Returned array does not contain duplicates; it may be modified. # # If _state_ is an Integer, this method returns the incoming adjacent states # of the state'th state in the state list. # # Raises: # - IndexError if state is an Integer and state<0 or state>=state_count. # - ArgumentError if state is not a valid state (not a state or not from this # automaton) # def in_adjacent_states(state) to_state(state).in_adjacent_states() end # # Returns an array with adjacent states (along outgoing edges) of _state_. # Returned array does not contain duplicates; it may be modified. # # If _state_ is an Integer, this method returns the outgoing adjacent states # of the state'th state in the state list. # # Raises: # - IndexError if state is an Integer and state<0 or state>=state_count. # - ArgumentError if state is not a valid state (not a state or not from this # automaton) # def out_adjacent_states(state) to_state(state).out_adjacent_states() end # # Collects all initial states of this Automaton and returns it. Returned array # does not contain duplicates and may be modified. # # This method is epsilon symbol aware (represented with nil) on # non-deterministic automata, meaning that it actually computes the set of # reachable states from an initial state through strings respecting the # eps* regular expression, where eps is the epsilon symbol. # def initial_states @initials = compute_initial_states if @initials.nil? or @initials.empty? @initials end # # Returns the initial state of the automaton. This method is expected to used # on deterministic automata only. Unlike initial_states, it returns one State # instance instead of an Array. # # When used with a non deterministic automaton, it returns one of the states # tagged as initial. Which one is returned must be considered a non # deterministic choice. This method is not epsilon symbol aware. # def initial_state initial_states[0] end # Internal implementation of initial_states. def compute_initial_states() initials = @states.select {|s| s.initial?} initials.collect{|s| s.epsilon_closure}.flatten.uniq end ### public write section ##################################################### public # # Adds a new state. # # Arguments: # - data: user-data to attach to the state (see Automaton documentation). # # Raises: # - ArgumentError if _data_ is not a valid state data. # def add_state(data={}) data = to_valid_state_data(data) # create new state, add it to state-list state = State.new(self, state_count, data) @states << state # let the automaton know that something has changed state_changed(:state_added, state) # return created state state end alias :create_state :add_state # # Adds _n_ new states in the automaton. Created states are returned as an # ordered array (order of states according to their index in state list). # # _data_ is duplicated for each created state. # def add_n_states(n, data={}) created = [] n.times do |i| created << add_state(data.dup) end created end alias :create_n_states :add_n_states # # Adds a new edge, connecting _from_ and _to_ states of the automaton. # # Arguments: # - from: either a State or a valid state index (Integer). # - to: either a State or a valid state index (Integer). # - data: user data to attach to the created edge (see Automaton documentation). # # Raises: # - IndexError if _from_ is an Integer but not in [0..state_count) # - IndexError if _to_ is an Integer but not in [0..state_count) # - ArgumentError if _from_ is not a valid state for this automaton. # - ArgumentError if _to_ is not a valid state for this automaton. # - ArgumentError if _data_ is not a valid edge data. # def add_edge(from, to, data) from, to, data = to_state(from), to_state(to), to_valid_edge_data(data) # create edge, install it, add it to edge-list edge = Edge.new(self, edge_count, data, from, to) @edges << edge from.send(:add_outgoing_edge, edge) to.send(:add_incoming_edge, edge) # let automaton know that something has changed state_changed(:edge_added, edge) # return created edge edge end alias :create_edge :add_edge alias :connect :add_edge # Adds all states and transitions (as copies) from a different automaton. # Returns the initial state of the added part. In order to ensure that names of # the new states do not clash with names of existing states, state names may have # to be removed from added states; this is the case if _clear_names_ is set to true. # None of the added states are made initial. def add_automaton(what,clear_names=true) map_what_self = {} what.states.each do |state| map_what_self[state]=add_state(state.data) map_what_self[state][:name]=nil if clear_names map_what_self[state][:initial]=false end what.edges.each do |edge| add_edge(map_what_self[edge.from],map_what_self[edge.to],edge.data) end map_what_self[what.initial_state] end # Constructs a replica of this automaton and returns a copy. # This copy can be modified in whatever way without affecting the original # automaton. def dup Automaton.new(false) do |fa| initial = fa.add_automaton(self,false) initial[:initial] = true unless initial.nil? end end # # Drops a state of the automaton, as well as all connected edges to that state. # If _state_ is an integer, the state-th state of the state list is removed. # This method returns the automaton itself. # # Raises: # - IndexError if _edge_ is an Integer but not in [0..edge_count) # - ArgumentError if _edge_ is not a valid edge for this automaton. # def drop_state(state) state = to_state(state) # remove edges first: drop_edges ensures that edge list is coherent drop_edges(*(state.in_edges + state.out_edges).uniq) # remove state now and renumber @states.delete_at(state.index) state.index.upto(state_count-1) do |i| @states[i].send(:index=, i) end state.send(:index=, -1) state_changed(:state_dropped, state) self end alias :delete_state :drop_state # # Drops all states passed as parameter as well as all their connected edges. # Arguments may be state instances, as well as valid state indices. Duplicates # are even supported. This method has no effect on the automaton and raises # an error if some state argument is not valid. # # Raises: # - ArgumentError if one state in _states_ is not a valid state of this # automaton. # def drop_states(*states) # check states first states = states.collect{|s| to_state(s)}.uniq.sort edges = states.collect{|s| (s.in_edges + s.out_edges).uniq}.flatten.uniq.sort # Remove all edges, we do not use drop_edges to avoid spending too much # time reindexing edges. Moreover, we can do it that way because we take # edges in reverse indexing order (has been sorted previously) until edges.empty? edge = edges.pop edge.source.send(:drop_outgoing_edge,edge) edge.target.send(:drop_incoming_edge,edge) @edges.delete_at(edge.index) edge.send(:index=, -1) state_changed(:edge_dropped, edge) end # Remove all states, same kind of hack is used until states.empty? state = states.pop @states.delete_at(state.index) state.send(:index=, -1) state_changed(:state_dropped, state) end # sanitize state and edge lists @states.each_with_index {|s,i| s.send(:index=,i)} @edges.each_with_index {|e,i| e.send(:index=,i)} self end # # Drops an edge in the automaton. If _edge_ is an integer, the edge-th edge # of the edge list is removed. This method returns the automaton itself. # # Raises: # - IndexError if _edge_ is an Integer but not in [0..edge_count) # - ArgumentError if _edge_ is not a valid edge for this automaton. # def drop_edge(edge) edge = to_edge(edge) @edges.delete_at(edge.index) edge.from.send(:drop_outgoing_edge,edge) edge.to.send(:drop_incoming_edge,edge) edge.index.upto(edge_count-1) do |i| @edges[i].send(:index=, i) end edge.send(:index=,-1) state_changed(:edge_dropped, edge) self end alias :delete_edge :drop_edge # # Drops all edges passed as parameters. Arguments may be edge objects, # as well as valid edge indices. Duplicates are even supported. This method # has no effect on the automaton and raises an error if some edge argument # is not valid. # # Raises: # - ArgumentError if one edge in _edges_ is not a valid edge of this automaton. # def drop_edges(*edges) # check edges first edges = edges.collect{|e| to_edge(e)}.uniq # remove all edges edges.each do |e| @edges.delete(e) e.from.send(:drop_outgoing_edge,e) e.to.send(:drop_incoming_edge,e) e.send(:index=, -1) state_changed(:edge_dropped, e) end @edges.each_with_index do |e,i| e.send(:index=,i) end self end alias :delete_edges :drop_edges ### protected section ######################################################## protected # # Converts a _state_ argument to a valid State of this automaton. # There are three ways to refer to a state, by position in the internal # collection of states, using an instance of State and using a name of a # state (represented with a String). # # Raises: # - IndexError if state is an Integer and state<0 or state>=state_count. # - ArgumentError if state is not a valid state (not a state or not from this # automaton) # def to_state(state) case state when State return state if state.automaton==self and state==@states[state.index] raise ArgumentError, "Not a state of this automaton", caller when Integer return ith_state(state) when String result = get_state(state) return result unless result.nil? end raise ArgumentError, "Invalid state argument #{state}", caller end # # Converts an _edge_ argument to a valid Edge of this automaton. # # Raises: # - IndexError if _edge_ is an Integer but not in [0..edge_count) # - ArgumentError if _edge_ is not a valid edge (not a edge or not from this # automaton) # def to_edge(edge) case edge when Edge return edge if edge.automaton==self and edge==@edges[edge.index] raise ArgumentError, "Not an edge of this automaton", caller when Integer return ith_edge(edge) end raise ArgumentError, "Invalid edge argument #{edge}", caller end # # Checks if a given user-data contains enough information to be attached to # a given state. Returns the data if ok. # # Raises: # - ArgumentError if data is not considered a valid state data. # def to_valid_state_data(data) raise(ArgumentError, "User data should be an Hash", caller) unless Hash===data data end # # Checks if a given user-data contains enough information to be attached to # a given edge. Returns the data if ok. # # Raises: # - ArgumentError if data is not considered a valid edge data. # def to_valid_edge_data(data) return {:symbol => data} if data.nil? or data.is_a?(String) raise(ArgumentError, "User data should be an Hash", caller) unless Hash===data raise(ArgumentError, "User data should contain a :symbol attribute.", caller) unless data.has_key?(:symbol) raise(ArgumentError, "Edge :symbol attribute cannot be an array.", caller) if Array===data[:symbol] data end ### public sections with useful utilities #################################### public # Returns true if the automaton is deterministic, false otherwise def deterministic? @deterministic = @states.reject{|s| s.deterministic?}.empty? if @deterministic.nil? @deterministic end ### public & protected sections about alphabet ############################### protected # Deduces the alphabet from the automaton edges. def deduce_alphabet edges.collect{|e| e.symbol}.uniq.compact.sort end public # Returns the alphabet of the automaton. def alphabet @alphabet || deduce_alphabet end # Sets the aphabet of the automaton. _alph_ is expected to be an array without # nil nor duplicated. This method raises an ArgumentError otherwise. Such an # error is also raised if a symbol used on the automaton edges is not included # in _alph_. def alphabet=(alph) raise ArgumentError, "Invalid alphabet" unless alph.uniq.compact.size==alph.size raise ArgumentError, "Invalid alphabet" unless deduce_alphabet.reject{|s| alph.include?(s)}.empty? @alphabet = alph.sort end ### public section about dot utilities ####################################### protected # # Converts a hash of attributes (typically automaton, state or edge attributes) # to a [...] dot string. Braces are part of the output. # def attributes2dot(attrs) buffer = "" attrs.keys.sort{|k1,k2| k1.to_s <=> k2.to_s}.each do |key| buffer << " " unless buffer.empty? value = attrs[key].to_s.gsub('"','\"') buffer << "#{key}=\"#{value}\"" end buffer end public # # Generates a dot output from an automaton. The rewriter block takes # two arguments: the first one is a Markable instance (graph, state or # edge), the second one indicates which kind of element is passed (through # :automaton, :state or :edge symbol). The rewriter is expected to return a # hash-like object providing dot attributes for the element. # # When no rewriter is provided, a default one is used by default, providing # the following behavior: # - on :automaton # # {:rankdir => "LR"} # # - on :state # # {:shape => "doublecircle/circle" (following accepting?), # :style => "filled", # :fillcolor => "green/red/white" (if initial?/error?/else, respectively)} # # - on edge # # {:label => "#{edge.symbol}"} # def to_dot(&rewriter) unless rewriter to_dot do |elm, kind| case kind when :automaton {:rankdir => "LR"} when :state {:shape => (elm.accepting? ? "doublecircle" : "circle"), :style => "filled", :color => "black", :fillcolor => (elm.initial? ? "green" : (elm.error? ? "red" : "white"))} when :edge {:label => elm.symbol.nil? ? '' : elm.symbol.to_s} end end else buffer = "digraph G {\n" attrs = attributes2dot(rewriter.call(self, :automaton)) buffer << " graph [#{attrs}];\n" states.each do |s| attrs = attributes2dot(rewriter.call(s, :state)) buffer << " #{s.index} [#{attrs}];\n" end edges.each do |e| attrs = attributes2dot(rewriter.call(e, :edge)) buffer << " #{e.source.index} -> #{e.target.index} [#{attrs}];\n" end buffer << "}\n" end end ### public section about adl utilities ####################################### public # Prints this automaton in ADL format def to_adl(buffer = "") Stamina::ADL.print_automaton(self, buffer) end ### public section about reordering ########################################## public # Uses a comparator block to reorder the state list. def order_states(&block) raise ArgumentError, "A comparator block must be given" unless block_given? raise ArgumentError, "A comparator block of arity 2 must be given" unless block.arity==2 @states.sort!(&block) @states.each_with_index{|s,i| s.send(:index=, i)} self end ### protected section about changes ########################################## protected # # Fires by write method when an automaton change occurs. # def state_changed(what, infos) @initials = nil @deterministic = nil end protected :compute_initial_states end # class Automaton end # module Stamina require 'stamina/automaton/walking' require 'stamina/automaton/complete' require 'stamina/automaton/strip' require 'stamina/automaton/equivalence' require 'stamina/automaton/minimize' require 'stamina/automaton/metrics'