A mixin that adds pushdown-automaton functionality to another module/class.
Extension callback – add some stuff to extending objects.
# File lib/pushdown/automaton.rb, line 17
def self::extended( object )
super
unless object.respond_to?( :log )
object.extend( Loggability )
object.log_to( :pushdown )
end
object.instance_variable_set( :@pushdown_states, {} )
object.singleton_class.attr_reader( :pushdown_states )
object.include( Pushdown::Automaton::InstanceMethods )
end
Generate the external event handler method for the pushdown state named name
on the specified object
.
# File lib/pushdown/automaton.rb, line 118
def self::generate_event_method( name, object )
self.log.debug "Generating event method for %p: handle_%s_event" % [ object, name ]
stack_method = object.instance_method( "#{name}_stack" )
meth = lambda do |event, *args|
if ( stack = stack_method.bind(self).call )
current_state = stack.last
result = current_state.on_event( event, *args )
return self.handle_pushdown_result( stack, result, name )
else
return nil
end
end
end
Generate the method that returns the initial state class for a pushdown state named name
.
# File lib/pushdown/automaton.rb, line 177
def self::generate_initial_state_method( name )
self.log.debug "Generating initial state method for %p" % [ name ]
return lambda do
config = self.pushdown_states[ name ]
class_name = config[ :initial_state ]
return self.pushdown_state_class( name, class_name )
end
end
Generate the timed update method for every pushdown state named name
on the specified object
.
# File lib/pushdown/automaton.rb, line 156
def self::generate_shadow_update_method( name, object )
self.log.debug "Generating shadow update method for %p: shadow_update_%s" % [ object, name ]
stack_method = object.instance_method( "#{name}_stack" )
meth = lambda do |*args|
if ( stack = stack_method.bind(self).call )
stack.each do |state|
state.shadow_update( *args )
end
end
# :TODO: Calling/return convention? Could do something like #flat_map the
# results? Or map to a hash keyed by state object? Is it useful enough to justify
# the object churn of a method that might potentionally be in a hot loop?
return nil
end
end
Generate the method used to access the current state object.
# File lib/pushdown/automaton.rb, line 107
def self::generate_state_method( name, object )
self.log.debug "Generating current state method for %p: %p" % [ object, name ]
stack_method = object.instance_method( "#{name}_stack" )
meth = lambda { stack_method.bind(self).call&.last }
return meth
end
Generate the timed update method for the active pushdown state named name
on the specified object
.
# File lib/pushdown/automaton.rb, line 137
def self::generate_update_method( name, object )
self.log.debug "Generating update method for %p: update_%s" % [ object, name ]
stack_method = object.instance_method( "#{name}_stack" )
meth = lambda do |*args|
if ( stack = stack_method.bind(self).call )
current_state = stack.last
result = current_state.update( *args )
return self.handle_pushdown_result( stack, result, name )
else
return nil
end
end
end
Generate the pushdown API methods for the pushdown automaton with the given name
and install them in the extending object
.
# File lib/pushdown/automaton.rb, line 84
def self::install_state_methods( name, object )
self.log.debug "Installing pushdown methods for %p in %p" % [ name, object ]
object.attr_reader( "#{name}_stack" )
# Relies on the above method having already been declared
state_method = self.generate_state_method( name, object )
object.define_method( name, &state_method )
event_method = self.generate_event_method( name, object )
object.define_method( "handle_#{name}_event", &event_method )
update_method = self.generate_update_method( name, object )
object.define_method( "update_#{name}", &update_method )
update_method = self.generate_shadow_update_method( name, object )
object.define_method( "shadow_update_#{name}", &update_method )
initial_state_method = self.generate_initial_state_method( name )
object.define_singleton_method( "initial_#{name}", &initial_state_method )
end
Return the state class with the name inferred from the given class_name
.
# File lib/pushdown/automaton.rb, line 216
def pushdown_inferred_state_class( class_name )
constant_name = class_name.to_s.capitalize.gsub( /_(\p{Alnum})/ ) do |match|
match[ 1 ].capitalize
end
self.log.debug "Inferred state class for %p is: %s" % [ class_name, constant_name ]
return self.const_get( constant_name )
rescue NameError => err
raise Pushdown::DeclarationError,
"failed to look up a state class for %p: %s" % [ class_name, err.message ]
end
Derive a state class object named class_name
via the (pluggable) state_base_class
.
# File lib/pushdown/automaton.rb, line 231
def pushdown_pluggable_state_class( state_base_class, class_name )
return state_base_class.get_subclass( class_name )
end
Declare a attribute name
which is a pushdown state.
# File lib/pushdown/automaton.rb, line 188
def pushdown_state( name, initial_state: :initial, states: nil )
@pushdown_states[ name ] = { initial_state: initial_state, states: states }
Pushdown::Automaton.install_state_methods( name, self )
end
Return the Class object for the class named class_name
of the pushdown state state_name
.
# File lib/pushdown/automaton.rb, line 197
def pushdown_state_class( state_name, class_name )
config = self.pushdown_states[ state_name ] or
raise "No pushdown state named %p" % [ state_name ]
states = config[ :states ]
case states
when NilClass
return self.pushdown_inferred_state_class( class_name )
when Class
return self.pushdown_pluggable_state_class( states, class_name )
when Hash
return states[ class_name ]
else
raise "don't know how to derive a state class from %p (%p)" % [ states, states.class ]
end
end