pushdown 0.1.0.pre.20210714190141 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -0
- data/History.md +6 -1
- data/LICENSE.txt +20 -0
- data/lib/pushdown/automaton.rb +236 -0
- data/lib/pushdown/exceptions.rb +18 -0
- data/lib/pushdown/state.rb +110 -0
- data/lib/pushdown/transition/pop.rb +35 -0
- data/lib/pushdown/transition/push.rb +41 -0
- data/lib/pushdown/transition/replace.rb +43 -0
- data/lib/pushdown/transition/switch.rb +41 -0
- data/lib/pushdown/transition.rb +85 -0
- data/lib/pushdown.rb +5 -5
- data/spec/pushdown/automaton_spec.rb +151 -0
- data/spec/pushdown/state_spec.rb +88 -0
- data/spec/pushdown/transition/pop_spec.rb +67 -0
- data/spec/pushdown/transition/push_spec.rb +53 -0
- data/spec/pushdown/transition/replace_spec.rb +57 -0
- data/spec/pushdown/transition/switch_spec.rb +52 -0
- data/spec/pushdown/transition_spec.rb +91 -0
- data/spec/spec_helper.rb +4 -0
- data.tar.gz.sig +0 -0
- metadata +91 -10
- metadata.gz.sig +0 -0
- data/.simplecov +0 -9
- data/Rakefile +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf2b58a9924b03dc5e15d6c0fca42ebac8988fcdbf9e0b02f1ab3b1e2365de56
|
4
|
+
data.tar.gz: 7117970f25e2e310be68a2269dd77216cae26442d298e73dd1bb580a29752914
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b41ba0c24de3f75fb91fbc02e3b20afc5d2d4a8aac8f43d9ae4237e354fcab2611d48eb885f2bef542d9d9da5bbb5c6686f98c78ea26287e84e9c7fb8242011
|
7
|
+
data.tar.gz: 1d84bef861cf31696148031a01a13fd04ef5a76d9bd45733f3a0043976e62a97aa56836b3c7bdcaf54d45bf798df9abe4c59b619ebd406f7d457442340d1ed5c
|
checksums.yaml.gz.sig
ADDED
data/History.md
CHANGED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2019 Michael Granger
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'loggability'
|
5
|
+
|
6
|
+
require 'pushdown' unless defined?( Pushdown )
|
7
|
+
|
8
|
+
|
9
|
+
# A mixin that adds pushdown-automaton functionality to another module/class.
|
10
|
+
module Pushdown::Automaton
|
11
|
+
extend Loggability
|
12
|
+
|
13
|
+
# Loggability API -- log to the pushdown logger
|
14
|
+
log_to :pushdown
|
15
|
+
|
16
|
+
|
17
|
+
### Extension callback -- add some stuff to extending objects.
|
18
|
+
def self::extended( object )
|
19
|
+
super
|
20
|
+
|
21
|
+
unless object.respond_to?( :log )
|
22
|
+
object.extend( Loggability )
|
23
|
+
object.log_to( :pushdown )
|
24
|
+
end
|
25
|
+
|
26
|
+
object.instance_variable_set( :@pushdown_states, {} )
|
27
|
+
object.singleton_class.attr_reader( :pushdown_states )
|
28
|
+
object.include( Pushdown::Automaton::InstanceMethods )
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
### Generate the pushdown API methods for the pushdown automaton with the given
|
33
|
+
### +name+ and install them in the extending +object+.
|
34
|
+
def self::install_state_methods( name, object )
|
35
|
+
self.log.debug "Installing pushdown methods for %p in %p" % [ name, object ]
|
36
|
+
object.attr_reader( "#{name}_stack" )
|
37
|
+
|
38
|
+
# Relies on the above method having already been declared
|
39
|
+
state_method = self.generate_state_method( name, object )
|
40
|
+
object.define_method( name, &state_method )
|
41
|
+
|
42
|
+
event_method = self.generate_event_method( name, object )
|
43
|
+
object.define_method( "handle_#{name}_event", &event_method )
|
44
|
+
|
45
|
+
update_method = self.generate_update_method( name, object )
|
46
|
+
object.define_method( "update_#{name}", &update_method )
|
47
|
+
|
48
|
+
update_method = self.generate_shadow_update_method( name, object )
|
49
|
+
object.define_method( "shadow_update_#{name}", &update_method )
|
50
|
+
|
51
|
+
initial_state_method = self.generate_initial_state_method( name )
|
52
|
+
object.define_singleton_method( "initial_#{name}", &initial_state_method )
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
### Generate the method used to access the current state object.
|
57
|
+
def self::generate_state_method( name, object )
|
58
|
+
self.log.debug "Generating current state method for %p: %p" % [ object, name ]
|
59
|
+
stack_method = object.instance_method( "#{name}_stack" )
|
60
|
+
meth = lambda { stack_method.bind(self).call&.last }
|
61
|
+
|
62
|
+
return meth
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
### Generate the external event handler method for the pushdown state named +name+
|
67
|
+
### on the specified +object+.
|
68
|
+
def self::generate_event_method( name, object )
|
69
|
+
self.log.debug "Generating event method for %p: handle_%s_event" % [ object, name ]
|
70
|
+
|
71
|
+
stack_method = object.instance_method( "#{name}_stack" )
|
72
|
+
meth = lambda do |event, *args|
|
73
|
+
stack = stack_method.bind( self ).call
|
74
|
+
current_state = stack.last
|
75
|
+
|
76
|
+
result = current_state.on_event( event, *args )
|
77
|
+
return self.handle_pushdown_result( stack, result, name )
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
### Generate the timed update method for the active pushdown state named +name+
|
83
|
+
### on the specified +object+.
|
84
|
+
def self::generate_update_method( name, object )
|
85
|
+
self.log.debug "Generating update method for %p: update_%s" % [ object, name ]
|
86
|
+
|
87
|
+
stack_method = object.instance_method( "#{name}_stack" )
|
88
|
+
meth = lambda do |*args|
|
89
|
+
stack = stack_method.bind( self ).call
|
90
|
+
current_state = stack.last
|
91
|
+
|
92
|
+
result = current_state.update( *args )
|
93
|
+
return self.handle_pushdown_result( stack, result, name )
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
### Generate the timed update method for every pushdown state named +name+
|
99
|
+
### on the specified +object+.
|
100
|
+
def self::generate_shadow_update_method( name, object )
|
101
|
+
self.log.debug "Generating shadow update method for %p: shadow_update_%s" % [ object, name ]
|
102
|
+
|
103
|
+
stack_method = object.instance_method( "#{name}_stack" )
|
104
|
+
meth = lambda do |*args|
|
105
|
+
stack = stack_method.bind( self ).call
|
106
|
+
stack.each do |state|
|
107
|
+
state.shadow_update( *args )
|
108
|
+
end
|
109
|
+
|
110
|
+
# :TODO: Calling/return convention? Could do something like #flat_map the
|
111
|
+
# results? Or map to a hash keyed by state object? Is it useful enough to justify
|
112
|
+
# the object churn of a method that might potentionally be in a hot loop?
|
113
|
+
return nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
### Generate the method that returns the initial state class for a pushdown
|
119
|
+
### state named +name+.
|
120
|
+
def self::generate_initial_state_method( name )
|
121
|
+
self.log.debug "Generating initial state method for %p" % [ name ]
|
122
|
+
return lambda do
|
123
|
+
config = self.pushdown_states[ name ]
|
124
|
+
class_name = config[ :initial_state ]
|
125
|
+
return self.pushdown_state_class( name, class_name )
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# A mixin to add instance methods for setting up pushdown states.
|
131
|
+
module InstanceMethods
|
132
|
+
|
133
|
+
### Overload the initializer to push the initial state object for any pushdown
|
134
|
+
### states.
|
135
|
+
def initialize( * )
|
136
|
+
super if defined?( super )
|
137
|
+
|
138
|
+
self.class.pushdown_states.each do |name, config|
|
139
|
+
self.log.debug "Pushing initial %s" % [ name ]
|
140
|
+
|
141
|
+
state_class = self.class.public_send( "initial_#{name}" ) or
|
142
|
+
raise "unset initial_%s while pushing initial state" % [ name ]
|
143
|
+
|
144
|
+
data = if self.respond_to?( "initial_#{name}_data" )
|
145
|
+
self.public_send( "initial_#{name}_data" )
|
146
|
+
else
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
150
|
+
transition = Pushdown::Transition.create( :push, :initial, state_class, data )
|
151
|
+
stack = transition.apply( [] )
|
152
|
+
self.instance_variable_set( "@#{name}_stack", stack )
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
### The body of the event handler, called by the #handle_{name}_event.
|
158
|
+
def handle_pushdown_result( stack, result, name )
|
159
|
+
if result.is_a?( Symbol )
|
160
|
+
transition_name = result
|
161
|
+
current_state = stack.last
|
162
|
+
self.log.debug "Looking up the %p transition for %p" % [ result, current_state ]
|
163
|
+
transition_type, state_class_name = current_state.class.transitions[ transition_name ]
|
164
|
+
raise "no such transition %p for %s" % [ transition_name, name ] unless transition_type
|
165
|
+
|
166
|
+
result = if state_class_name
|
167
|
+
state_class = self.class.pushdown_state_class( name, state_class_name )
|
168
|
+
Pushdown::Transition.create( transition_type, transition_name, state_class )
|
169
|
+
else
|
170
|
+
Pushdown::Transition.create( transition_type, transition_name )
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
if result.is_a?( Pushdown::Transition )
|
175
|
+
new_stack = result.apply( stack )
|
176
|
+
stack.replace( new_stack )
|
177
|
+
end
|
178
|
+
|
179
|
+
return result
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# :TODO: Methods that call #update, #on_event, etc. and then manage the
|
184
|
+
# application of any transition(s) that are returned.
|
185
|
+
|
186
|
+
end # module InstanceMethods
|
187
|
+
|
188
|
+
|
189
|
+
### Declare a attribute +name+ which is a pushdown state.
|
190
|
+
def pushdown_state( name, initial_state:, states: nil )
|
191
|
+
@pushdown_states[ name ] = { initial_state: initial_state, states: states }
|
192
|
+
|
193
|
+
Pushdown::Automaton.install_state_methods( name, self )
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
### Return the Class object for the class named +class_name+ of the pushdown state
|
198
|
+
### +state_name+.
|
199
|
+
def pushdown_state_class( state_name, class_name )
|
200
|
+
config = self.pushdown_states[ state_name ] or
|
201
|
+
raise "No pushdown state named %p" % [ state_name ]
|
202
|
+
states = config[ :states ]
|
203
|
+
|
204
|
+
case states
|
205
|
+
when NilClass
|
206
|
+
return self.pushdown_inferred_state_class( class_name )
|
207
|
+
when Class
|
208
|
+
return self.pushdown_pluggable_state_class( states, class_name )
|
209
|
+
when Hash
|
210
|
+
return states[ class_name ]
|
211
|
+
else
|
212
|
+
raise "don't know how to derive a state class from %p (%p)" % [ states, states.class ]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
### Return the state class with the name inferred from the given +class_name+.
|
218
|
+
def pushdown_inferred_state_class( class_name )
|
219
|
+
constant_name = class_name.to_s.capitalize.gsub( /_(\p{Alnum})/ ) do |match|
|
220
|
+
match[ 1 ].capitalize
|
221
|
+
end
|
222
|
+
self.log.debug "Inferred state class for %p is: %s" % [ class_name, constant_name ]
|
223
|
+
|
224
|
+
return self.const_get( constant_name )
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
### Derive a state class object named +class_name+ via the (pluggable)
|
229
|
+
### +state_base_class+.
|
230
|
+
def pushdown_pluggable_state_class( state_base_class, class_name )
|
231
|
+
return state_base_class.get_subclass( class_name )
|
232
|
+
end
|
233
|
+
|
234
|
+
end # module Pushdown::Automaton
|
235
|
+
|
236
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown' unless defined?( Pushdown )
|
5
|
+
require 'e2mmap'
|
6
|
+
|
7
|
+
module Pushdown
|
8
|
+
extend Exception2MessageMapper
|
9
|
+
|
10
|
+
|
11
|
+
def_exception :Error, "pushdown error"
|
12
|
+
|
13
|
+
def_exception :StateError, "error in state machine", Pushdown::Error
|
14
|
+
def_exception :TransitionError, "error while transitioning", Pushdown::Error
|
15
|
+
|
16
|
+
|
17
|
+
end # module Pushdown
|
18
|
+
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pluggability'
|
5
|
+
require 'loggability'
|
6
|
+
|
7
|
+
require 'pushdown' unless defined?( Pushdown )
|
8
|
+
|
9
|
+
|
10
|
+
# A componented state object in a Pushdown automaton
|
11
|
+
class Pushdown::State
|
12
|
+
extend Loggability
|
13
|
+
|
14
|
+
# Loggability API -- log to the pushdown logger
|
15
|
+
log_to :pushdown
|
16
|
+
|
17
|
+
# Don't allow instantation of the abstract class
|
18
|
+
private_class_method :new
|
19
|
+
|
20
|
+
##
|
21
|
+
# Allow introspection on declared transitions
|
22
|
+
singleton_class.attr_reader :transitions
|
23
|
+
|
24
|
+
|
25
|
+
### Inheritance callback -- allow subclasses to be instantiated, and add some
|
26
|
+
### class-instance data to them.
|
27
|
+
def self::inherited( subclass )
|
28
|
+
super
|
29
|
+
|
30
|
+
subclass.public_class_method( :new )
|
31
|
+
subclass.instance_variable_set( :@transitions, {} )
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
#
|
36
|
+
# Transition declarations
|
37
|
+
#
|
38
|
+
|
39
|
+
### Register a transition +type+ declaration method.
|
40
|
+
def self::register_transition( type )
|
41
|
+
type = type.to_sym
|
42
|
+
meth = lambda do |transition_name, *args|
|
43
|
+
self.transitions[ transition_name ] = [ type, *args ]
|
44
|
+
end
|
45
|
+
|
46
|
+
method_name = "transition_#{type}"
|
47
|
+
define_singleton_method( method_name, &meth )
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
#
|
52
|
+
# Stack callbacks
|
53
|
+
#
|
54
|
+
|
55
|
+
### Stack callback -- called when the state is added to the stack.
|
56
|
+
def on_start( data=nil )
|
57
|
+
return nil # no-op
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
### Stack callback -- called when the state is removed from the stack.
|
62
|
+
def on_stop( data=nil )
|
63
|
+
return nil # no-op
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
### Stack callback -- called when another state is pushed over this one.
|
68
|
+
def on_pause( data=nil )
|
69
|
+
return nil # no-op
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
### Stack callback -- called when another state is popped off from in front of
|
74
|
+
### this one, making it the current state.
|
75
|
+
def on_resume( data=nil )
|
76
|
+
return nil # no-op
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
#
|
81
|
+
# State callbacks
|
82
|
+
#
|
83
|
+
|
84
|
+
### State callback -- interval callback called when the state is the current one.
|
85
|
+
def update( *data )
|
86
|
+
return nil # no-op
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
### State callback -- interval callback called when the state is on the stack,
|
91
|
+
### even when the state is not the current one.
|
92
|
+
def shadow_update( *data )
|
93
|
+
return nil # no-op
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
#
|
98
|
+
# Introspection/information
|
99
|
+
#
|
100
|
+
|
101
|
+
### Return a description of the State as an engine phrase.
|
102
|
+
def description
|
103
|
+
return "%#x" % [ self.class.object_id ] unless self.class.name
|
104
|
+
return self.class.name.sub( /.*::/, '' ).
|
105
|
+
gsub( /([A-Z])([A-Z])/ ) { "#$1 #$2" }.
|
106
|
+
gsub( /([a-z])([A-Z])/ ) { "#$1 #$2" }.downcase
|
107
|
+
end
|
108
|
+
|
109
|
+
end # class Pushdown::State
|
110
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pushdown/transition' unless defined?( Pushdown::Transition )
|
5
|
+
require 'pushdown/exceptions'
|
6
|
+
|
7
|
+
|
8
|
+
# A push transition -- add an instance of a given State to the top of the state
|
9
|
+
# stack.
|
10
|
+
class Pushdown::Transition::Pop < Pushdown::Transition
|
11
|
+
|
12
|
+
|
13
|
+
######
|
14
|
+
public
|
15
|
+
######
|
16
|
+
|
17
|
+
##
|
18
|
+
# Return the state that was popped
|
19
|
+
attr_reader :popped_state
|
20
|
+
|
21
|
+
|
22
|
+
### Apply the transition to the given +stack+.
|
23
|
+
def apply( stack )
|
24
|
+
raise Pushdown::TransitionError, "can't pop from an empty stack" if stack.empty?
|
25
|
+
raise Pushdown::TransitionError, "can't pop the only state on the stack" if stack.length == 1
|
26
|
+
|
27
|
+
self.log.debug "popping a state"
|
28
|
+
@popped_state = stack.pop
|
29
|
+
self.data = @popped_state.on_stop( self.data )
|
30
|
+
stack.last.on_resume( self.data )
|
31
|
+
|
32
|
+
return stack
|
33
|
+
end
|
34
|
+
|
35
|
+
end # class Pushdown::Transition::Pop
|