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 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