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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3aa15b901bc12dcc15430512e8ec06fdaa5bbc98afd222a62892fad5b1fa689
4
- data.tar.gz: 5b4d53652f5a1d0df9c759d6b9226d4780c87cb4258d650295748e8ecd64ab01
3
+ metadata.gz: bf2b58a9924b03dc5e15d6c0fca42ebac8988fcdbf9e0b02f1ab3b1e2365de56
4
+ data.tar.gz: 7117970f25e2e310be68a2269dd77216cae26442d298e73dd1bb580a29752914
5
5
  SHA512:
6
- metadata.gz: 5f3143596b9615ab3a538a2b5256fc43649b11ca1cf6a90fa33a616cee74c09d6ba39e4832ad417352fd15a1162a2698daca1aedcd069af4cf2d48b551dd1245
7
- data.tar.gz: 7f1fd1112549fd7cf19c2a472678fbd5dc58d6d07178d7ce84d0a5d7b96293a3e272b691499624fd23de961aff1df50b742bdaeab640e4d0e4c065fa6771b5d7
6
+ metadata.gz: 9b41ba0c24de3f75fb91fbc02e3b20afc5d2d4a8aac8f43d9ae4237e354fcab2611d48eb885f2bef542d9d9da5bbb5c6686f98c78ea26287e84e9c7fb8242011
7
+ data.tar.gz: 1d84bef861cf31696148031a01a13fd04ef5a76d9bd45733f3a0043976e62a97aa56836b3c7bdcaf54d45bf798df9abe4c59b619ebd406f7d457442340d1ed5c
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ K�ŷ���▬�Vl��t3�x�����_Y6/o��"r�.�Xr��,��"k�Ƿ2�SH�Ո�є��+�u؂����yv��FF ��=r�S�����oE� k�oEd���[����Q°���xz�/���2Хkp�2,�x�&��Mկˋ�l�C�8�����Hnu1(�����7�����Cm��L�`?�5r�T��y�wv�V0� R���DU�
2
+ ���9p鬭BD;7E�Î�4��*�cr~Ş��{���g�@j;ڸ�^˳�Aޒ���i�u�Ie�*z��c��i&�)�U����,� ��D%}/#)H��V��U�=9���
data/History.md CHANGED
@@ -1,4 +1,9 @@
1
- ## v0.0.1 [YYYY-MM-DD] Michael Granger <ged@FaerieMUD.org>
1
+ # Release History for pushdown
2
+
3
+ ---
4
+
5
+
6
+ ## v0.1.0 [2021-08-27] Michael Granger <ged@faeriemud.org>
2
7
 
3
8
  Initial release.
4
9
 
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