package FLAT::FA; use strict; use base 'FLAT'; use Carp; use FLAT::Transition; =head1 NAME FLAT::FA - Base class for regular finite automata =head1 SYNOPSIS A FLAT::FA object is a collection of states and transitions. Each state may be labeled as starting or accepting. Each transition between states is labeled with a transition object. =head1 USAGE FLAT::FA is a superclass that is not intended to be used directly. However, it does provide the following methods: =cut sub new { my $pkg = shift; bless { STATES => [], TRANS => [], ALPHA => {} }, $pkg; } sub get_states { my $self = shift; return 0 .. ($self->num_states - 1); } sub num_states { my $self = shift; return scalar @{ $self->{STATES} }; } sub is_state { my ($self, $state) = @_; exists $self->{STATES}->[$state]; } sub _assert_states { my ($self, @states) = @_; for (@states) { croak "'$_' is not a state" if not $self->is_state($_); } } sub _assert_non_states { my ($self, @states) = @_; for (@states) { croak "There is already a state called '$_'" if $self->is_state($_); } } sub delete_states { my ($self, @states) = @_; $self->_assert_states(@states); for my $s ( sort { $b <=> $a } @states ) { $self->_decr_alphabet($_) for @{ splice @{ $self->{TRANS} }, $s, 1 }; $self->_decr_alphabet( splice @$_, $s, 1 ) for @{ $self->{TRANS} }; splice @{ $self->{STATES} }, $s, 1; } } sub add_states { my ($self, $num) = @_; my $id = $self->num_states; for my $s ( $id .. ($id+$num-1) ) { push @$_, undef for @{ $self->{TRANS} }; push @{ $self->{TRANS} }, [ (undef) x ($s+1) ]; push @{ $self->{STATES} }, { starting => 0, accepting => 0 }; } return wantarray ? ($id .. ($id+$num-1)) : $id+$num-1; } ############## sub is_starting { my ($self, $state) = @_; $self->_assert_states($state); return $self->{STATES}[$state]{starting}; } sub set_starting { my ($self, @states) = @_; $self->_assert_states(@states); $self->{STATES}[$_]{starting} = 1 for @states; } sub unset_starting { my ($self, @states) = @_; $self->_assert_states(@states); $self->{STATES}[$_]{starting} = 0 for @states; } sub get_starting { my $self = shift; return grep { $self->is_starting($_) } $self->get_states; } ############## sub is_accepting { my ($self, $state) = @_; $self->_assert_states($state); return $self->{STATES}[$state]{accepting}; } sub set_accepting { my ($self, @states) = @_; $self->_assert_states(@states); $self->{STATES}[$_]{accepting} = 1 for @states; } sub unset_accepting { my ($self, @states) = @_; $self->_assert_states(@states); $self->{STATES}[$_]{accepting} = 0 for @states; } sub get_accepting { my $self = shift; return grep { $self->is_accepting($_) } $self->get_states; } ############### sub _decr_alphabet { my ($self, $t) = @_; return if not defined $t; for ($t->alphabet) { delete $self->{ALPHA}{$_} if not --$self->{ALPHA}{$_}; } } sub _incr_alphabet { my ($self, $t) = @_; return if not defined $t; $self->{ALPHA}{$_}++ for $t->alphabet; } sub set_transition { my ($self, $state1, $state2, @label) = @_; $self->remove_transition($state1, $state2); @label = grep defined, @label; return if not @label; my $t = $self->{TRANS_CLASS}->new(@label); $self->_incr_alphabet($t); $self->{TRANS}[$state1][$state2] = $t; } sub add_transition { my ($self, $state1, $state2, @label) = @_; @label = grep defined, @label; return if not @label; my $t = $self->get_transition($state1, $state2); $self->_decr_alphabet($t); if (!$t) { $t = $self->{TRANS}[$state1][$state2] = $self->{TRANS_CLASS}->new; } $t->add(@label); $self->_incr_alphabet($t); } sub get_transition { my ($self, $state1, $state2) = @_; $self->_assert_states($state1, $state2); $self->{TRANS}[$state1][$state2]; } sub remove_transition { my ($self, $state1, $state2) = @_; $self->_decr_alphabet( $self->{TRANS}[$state1][$state2] ); $self->{TRANS}[$state1][$state2] = undef; } # given a state and a symbol, it tells you # what the next state(s) are; do get successors # for find the successors for a set of symbols, # use array refs. For example: # @NEXT=$self->successors([@nodes],[@symbols]); sub successors { my ($self, $state, $symb) = @_; my @states = ref $state eq 'ARRAY' ? @$state : ($state); my @symbs = defined $symb ? (ref $symb eq 'ARRAY' ? @$symb : ($symb)) : (); $self->_assert_states(@states); my %succ; for my $s (@states) { $succ{$_}++ for grep { my $t = $self->{TRANS}[$s][$_]; defined $t && (@symbs ? $t->does(@symbs) : 1) } $self->get_states; } return keys %succ; } sub predecessors { my $self = shift; $self->clone->reverse->successors(@_); } # reverse - no change from NFA sub reverse { my $self = $_[0]->clone; $self->_transpose; my @start = $self->get_starting; my @final = $self->get_accepting; $self->unset_accepting( $self->get_states ); $self->unset_starting( $self->get_states ); $self->set_accepting( @start ); $self->set_starting( @final ); $self; } # get an array of all symbols sub alphabet { my $self = shift; grep length, keys %{ $self->{ALPHA} }; } # give an array of symbols, return the symbols that # are in the alphabet #sub is_in_alphabet { # my $self = shift; # my $ #} ############ sub prune { my $self = shift; my @queue = $self->get_starting; my %seen = map { $_ => 1 } @queue; while (@queue) { @queue = grep { ! $seen{$_}++ } $self->successors(\@queue); } my @useless = grep { !$seen{$_} } $self->get_states; $self->delete_states(@useless); return @useless; } ############ use Storable 'dclone'; sub clone { dclone( $_[0] ); } sub _transpose { my $self = shift; my $N = $self->num_states - 1; $self->{TRANS} = [ map { my $row = $_; [ map { $_->[$row] } @{$self->{TRANS}} ] } 0 .. $N ]; } # tests to see if set1 is a subset of set2 sub array_is_subset { my $self = shift; my $set1 = shift; my $set2 = shift; my $ok = 1; my %setcount = (); foreach ($self->array_unique(@{$set1}),$self->array_unique(@{$set2})) { $setcount{$_}++; } foreach ($self->array_unique(@{$set1})) { if ($setcount{$_} != 2) { $ok = 0; last; } } return $ok; } sub array_unique { my $self = shift; my %ret = (); foreach (@_) { $ret{$_}++; } return keys(%ret); } sub array_complement { my $self = shift; my $set1 = shift; my $set2 = shift; my @ret = (); # convert set1 to a hash my %set1hash = map {$_ => 1} @{$set1}; # iterate of set2 and test if $set1 foreach (@{$set2}) { if (!defined $set1hash{$_}) { push(@ret,$_); } } ## Now do the same using $set2 # convert set2 to a hash my %set2hash = map {$_ => 1} @{$set2}; # iterate of set1 and test if $set1 foreach (@{$set1}) { if (!defined $set2hash{$_}) { push(@ret,$_); } } # now @ret contains all items in $set1 not in $set 2 and all # items in $set2 not in $set1 return @ret; } # returns all items that 2 arrays have in common sub array_intersect { my $self = shift; my $set1 = shift; my $set2 = shift; my %setcount = (); my @ret = (); foreach ($self->array_unique(@{$set1})) { $setcount{$_}++; } foreach ($self->array_unique(@{$set2})) { $setcount{$_}++; push(@ret,$_) if ($setcount{$_} > 1); } return @ret; } # given a set of symbols, returns only the valid ones sub get_valid_symbols { my $self = shift; my $symbols = shift; return $self->array_intersect([$self->alphabet()],[@{$symbols}]) } ## add an FA's states & transitions to this FA (as disjoint union) sub _swallow { my ($self, $other) = @_; my $N1 = $self->num_states; my $N2 = $other->num_states; push @$_, (undef) x $N2 for @{ $self->{TRANS} }; push @{ $self->{TRANS} }, [ (undef) x $N1, @{ clone $_ } ] for @{ $other->{TRANS} }; push @{ $self->{STATES} }, @{ clone $other->{STATES} }; $self->{ALPHA}{$_} += $other->{ALPHA}{$_} for keys %{ $other->{ALPHA} }; return map { $_ + $N1 } $other->get_states; } 1; __END__ =head2 Manipulation & Inspection Of States =over =item $fa-Eget_states Returns a list of all the state "names" in $fa. =item $fa-Enum_states Returns the number of states in $fa. =item $fa-Eis_state($state_id) Returns a boolean indicating whether $state_id is a recognized state "name." =item $fa-Edelete_states(@states) Deletes the states given in @states and their corresponding transitions. The remaining states in the FA may be "renamed" (renumbered)! Return value not used. =item $fa-Eadd_states($num) Adds $num states to $fa, and returns a list of the new state "names." =item $fa-Eget_starting =item $fa-Eget_accepting Returns a list of all the states which are labeled as starting/accepting, respectively. =item $fa-Eset_accepting(@states) =item $fa-Eunset_accepting(@states) =item $fa-Eset_starting(@states) =item $fa-Eunset_starting(@states) Sets/unsets a list of states as being labeled starting/accepting, respectively. =item $fa-Eis_starting($state) =item $fa-Eis_accepting($state) Returns a boolean indicating whether $state is labeled as starting/accepting, respectively. =item $fa-Eprune Deletes the states which are not reachable (via zero or more transitions) from starting states. Returns a list of the "names" of states that were deleted. =back =head2 Manipulation & Inspection Of Transitions Each transition between states is a transition object, which knows how to organize several "labels." Think of this as the mechanism by which multiple arrows in the state diagram between the same states are collapsed to a single arrow. This interface is abstracted away into the following public methods: =over =item $fa-Eset_transition($state1, $state2, @labels) Resets the transition between $state1 and $state2 to a transition initialized using data @labels. If @labels is omitted or contains only undefined elements, then the call is equivalent to C. =item $fa-Eadd_transition($state1, $state2, @labels) Adds @labels to the transition between $state1 and $state2. =item $fa-Eget_transition($state1, $state2) Returns the transition object stored between $state1 and $state2, or undef if there is no transition. =item $fa-Eremove_transition($state1, $state2) Removes the transition object between $state1 and $state2. =item $fa-Esuccessors(\@states) =item $fa-Esuccessors($state) =item $fa-Esuccessors(\@states, $label) =item $fa-Esuccessors($state, $label) =item $fa-Esuccessors(\@states, \@labels) =item $fa-Esuccessors($state, \@labels) Given a state/set of states, and one or more labels, returns a list of the states (without duplicates) reachable from the states via a single transition having any of the given labels. If no labels are given, returns the states reachable by any (single) transition. Note that this method makes no distinction for epsilon transitions, these are only special in FLAT::NFA objects. =item $fa-Ealphabet Returns the list of characters (without duplicates) used among all transition labels in the automaton. =back =head2 Conversions To External Formats =over =item $fa-Eas_graphviz Returns a string containing a GraphViz (dot) description of the automaton, suitable for rendering with your favorite GraphViz layout engine. =item $fa-Eas_summary Returns a string containing a plaintext description of the automaton, suitable for debugging purposes. =back =head2 Miscellaneous =over =item $fa-Eclone Returns an identical copy of $fa. =back =head1 AUTHORS & ACKNOWLEDGEMENTS FLAT is written by Mike Rosulek Emike at mikero dot comE and Brett Estrade Eestradb at gmail dot comE. The initial version (FLAT::Legacy) by Brett Estrade was work towards an MS thesis at the University of Southern Mississippi. Please visit the Wiki at http://www.0x743.com/flat =head1 LICENSE This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.