pushdown 0.1.0.pre.20210714190141 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|