q-language 1.0.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.
Files changed (41) hide show
  1. data/.gemtest +0 -0
  2. data/Rakefile +10 -0
  3. data/lib/q-language.rb +9 -0
  4. data/lib/q-language/device.rb +166 -0
  5. data/lib/q-language/environment.rb +358 -0
  6. data/lib/q-language/methods/array.rb +523 -0
  7. data/lib/q-language/methods/block.rb +26 -0
  8. data/lib/q-language/methods/class.rb +24 -0
  9. data/lib/q-language/methods/dynamic.rb +82 -0
  10. data/lib/q-language/methods/false.rb +47 -0
  11. data/lib/q-language/methods/hash.rb +351 -0
  12. data/lib/q-language/methods/implicit.rb +345 -0
  13. data/lib/q-language/methods/module.rb +39 -0
  14. data/lib/q-language/methods/nil.rb +47 -0
  15. data/lib/q-language/methods/number.rb +118 -0
  16. data/lib/q-language/methods/object.rb +155 -0
  17. data/lib/q-language/methods/string.rb +157 -0
  18. data/lib/q-language/methods/time.rb +110 -0
  19. data/lib/q-language/methods/token.rb +72 -0
  20. data/lib/q-language/methods/true.rb +14 -0
  21. data/lib/q-language/node.rb +45 -0
  22. data/lib/q-language/object.rb +125 -0
  23. data/lib/q-language/parser.rb +104 -0
  24. data/lib/q-language/writer.rb +90 -0
  25. data/test/methods/test_array.rb +191 -0
  26. data/test/methods/test_block.rb +66 -0
  27. data/test/methods/test_class.rb +34 -0
  28. data/test/methods/test_dynamic.rb +158 -0
  29. data/test/methods/test_false.rb +60 -0
  30. data/test/methods/test_hash.rb +10 -0
  31. data/test/methods/test_implicit.rb +332 -0
  32. data/test/methods/test_module.rb +55 -0
  33. data/test/methods/test_nil.rb +60 -0
  34. data/test/methods/test_number.rb +10 -0
  35. data/test/methods/test_object.rb +157 -0
  36. data/test/methods/test_string.rb +271 -0
  37. data/test/methods/test_time.rb +181 -0
  38. data/test/methods/test_token.rb +92 -0
  39. data/test/methods/test_true.rb +16 -0
  40. data/test/test.rb +23 -0
  41. metadata +103 -0
File without changes
@@ -0,0 +1,10 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright © 2010-2011 Jesse Sielaff
4
+ #
5
+
6
+ require 'rake/testtask'
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.pattern = 'test/methods/test_*.rb'
10
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright © 2010-2011 Jesse Sielaff
4
+ #
5
+
6
+ require 'number'
7
+
8
+ Dir[File.expand_path("../q-language/*.rb", __FILE__)].sort.each {|f| require f }
9
+ Dir[File.expand_path("../q-language/methods/*.rb", __FILE__)].sort.each {|f| require f }
@@ -0,0 +1,166 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright © 2010-2011 Jesse Sielaff
4
+ #
5
+
6
+ class Q_Device
7
+ def initialize (script)
8
+ @script = script
9
+ parse_script_into_nodes
10
+
11
+ @max_nodes ||= 3000
12
+ @max_method_nodes ||= 3000
13
+
14
+ @max_array_length ||= 100_000
15
+ @max_hash_length ||= 100_000
16
+ @max_string_length ||= 100_000
17
+ end
18
+
19
+ attr_accessor :max_nodes, :max_method_nodes
20
+ attr_accessor :max_array_length, :max_hash_length, :max_string_length
21
+ attr_reader :script
22
+
23
+ # • User method
24
+ # Removes a Node of the given target type from the Q_Device's tree, rewrites
25
+ # the Q script based on the new tree, then returns the removed Node. All Nodes
26
+ # of the given type will be targeted with equal probability. If no target type
27
+ # is given, Node may be of any type. If no Node of the given type is found,
28
+ # returns nil.
29
+ #
30
+ def delete_node (target_type = :all)
31
+ parse_script_into_nodes if @nodes_need_reloading
32
+
33
+ candidate_nodes = nodes(target_type) - [@tree]
34
+ return nil unless target_node = candidate_nodes.sample
35
+
36
+ target_node.block.nodes.delete(target_node)
37
+ nodes.delete(target_node)
38
+ nodes(target_node.node_type).delete(target_node)
39
+
40
+ rewrite_script
41
+
42
+ target_node
43
+ end
44
+
45
+ # Executes the script in a new Q_Environment with the given variables and
46
+ # implicit receivers, then returns the Q_Environment.
47
+ #
48
+ def execute (variables = {}, *implicit)
49
+ parse_script_into_nodes if @nodes_need_reloading
50
+
51
+ object = Class.new.new
52
+ object.class.send(:define_method, :to_q) { QDynamic.new(self) }
53
+
54
+ begin
55
+ options = [@max_nodes, @max_method_nodes, @max_array_length, @max_hash_length, @max_string_length]
56
+ env = Q_Environment.new(variables, *implicit, object, options)
57
+ env.evaluate(@tree)
58
+ rescue Q_Environment::TooManyNodes
59
+ end
60
+
61
+ env
62
+ end
63
+
64
+ # • User method
65
+ # Returns a Node of the given type. All Nodes may be chosen with equal
66
+ # probability. If no node of the given is found, returns nil.
67
+ #
68
+ def get_node (target_type = :all)
69
+ parse_script_into_nodes if @nodes_need_reloading
70
+ nodes(target_type).sample
71
+ end
72
+
73
+ # • User method
74
+ # Inserts the given Node into a random position in a random block in the tree,
75
+ # rewrites the Q script based on the new tree, then returns the Node. All
76
+ # blocks in the tree will be targeted with equal probability.
77
+ #
78
+ def insert_node (new_node)
79
+ parse_script_into_nodes if @nodes_need_reloading
80
+
81
+ if new_node.is_a? String
82
+ new_node = Q_Parser.new(new_node).parse.first
83
+ end
84
+
85
+ nodes_in_target_block = nodes(:block).sample.nodes
86
+ node_position = rand(nodes_in_target_block.length + 1)
87
+
88
+ nodes_in_target_block.insert(node_position, new_node)
89
+ nodes.push(new_node)
90
+ nodes(new_node.node_type).push(new_node)
91
+
92
+ rewrite_script
93
+
94
+ new_node
95
+ end
96
+
97
+ # • User method
98
+ # Executes the given block, rewrites the Q script to reflect any changes to
99
+ # the tree, then returns the result of the block.
100
+ #
101
+ def modify
102
+ parse_script_into_nodes if @nodes_need_reloading
103
+
104
+ result = yield
105
+
106
+ rewrite_script
107
+ @nodes_need_reloading = true
108
+
109
+ result
110
+ end
111
+
112
+ # • User method
113
+ # Returns the Array containing all Nodes of the given type.
114
+ #
115
+ def nodes (type = :all)
116
+ parse_script_into_nodes if @nodes_need_reloading
117
+ instance_variable_get :"@#{type}_nodes"
118
+ end
119
+
120
+ # Parses the Q script, then stores the resulting tree and all its Nodes.
121
+ #
122
+ def parse_script_into_nodes
123
+ @tree, @block_nodes, @literal_nodes, @method_nodes, @variable_nodes = Q_Parser.new(@script).parse
124
+ @all_nodes = @block_nodes + @literal_nodes + @method_nodes + @variable_nodes
125
+ @nodes_need_reloading = false
126
+ end
127
+
128
+ # • User method
129
+ # Replaces a Node of the given target type from the Q_Device's tree with the
130
+ # given Node, rewrites the Q script based on the new tree, then returns the
131
+ # removed Node. All Nodes of the given type will be targeted with equal
132
+ # probability. If no target type is given, removed Node may be of any type. If
133
+ # no target Node of the given type is found, returns nil.
134
+ #
135
+ def replace_node (new_node, target_type = :all)
136
+ parse_script_into_nodes if @nodes_need_reloading
137
+
138
+ candidate_nodes = nodes(target_type) - [@tree]
139
+ return nil unless target_node = candidate_nodes.sample
140
+
141
+ if new_node.is_a? String
142
+ new_node = Q_Parser.new(new_node).parse.first
143
+ end
144
+
145
+ nodes_in_parent_block = target_node.block.nodes
146
+ node_position = nodes_in_parent_block.index(target_node)
147
+
148
+ nodes_in_parent_block[node_position] = new_node
149
+
150
+ nodes.delete(target_node)
151
+ nodes.push(new_node)
152
+
153
+ nodes(target_node.node_type).delete(target_node)
154
+ nodes(new_node.node_type).push(new_node)
155
+
156
+ rewrite_script
157
+
158
+ target_node
159
+ end
160
+
161
+ # Converts the tree into script form, then stores the script.
162
+ #
163
+ def rewrite_script
164
+ @script = @tree.to_script
165
+ end
166
+ end
@@ -0,0 +1,358 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright © 2010-2011 Jesse Sielaff
4
+ #
5
+
6
+ class Q_Environment
7
+ def initialize (variables = {}, *implicit, object, options)
8
+ @successful_methods = Hash.new {|h,k| h[k] = 0 }
9
+ @failed_methods = Hash.new {|h,k| h[k] = 0 }
10
+ @discarded_objects = Hash.new {|h,k| h[k] = 0 }
11
+ @nodes_evaluated = Hash.new {|h,k| h[k] = 0 }
12
+
13
+ @max_nodes = options[0]
14
+ @max_method_nodes = options[1]
15
+
16
+ @max_array_length = options[2]
17
+ @max_hash_length = options[3]
18
+ @max_string_length = options[4]
19
+
20
+ @variables = variables.to_hash
21
+ @implicit = implicit
22
+ @frame_stack = [object]
23
+
24
+ @scope = { queue: [], method_stack: [], unassigned_variables: [], prev: nil }
25
+ @method_results = []
26
+ end
27
+
28
+ TooManyNodes = Class.new(Exception)
29
+
30
+ attr_accessor :max_nodes, :max_method_nodes
31
+ attr_accessor :max_array_length, :max_hash_length, :max_string_length
32
+ attr_reader :successful_methods, :failed_methods, :discarded_objects, :nodes_evaluated
33
+
34
+ # Returns a Proc used for iterating over objects in the queue while looking
35
+ # for QObject method arguments. The returned Proc uses two parameters: an
36
+ # object from the queue, and that object's index in the queue. If the object
37
+ # parameter is an instance of the given QObject class and has not yet been
38
+ # used as the method receiver or as a method argument, the Proc adds the index
39
+ # parameter to the given indices Array and marks the object's index in the
40
+ # queue as used.
41
+ #
42
+ def arg_search_block (q_class, indices)
43
+ proc do |object, i|
44
+ next if (i == @receiver_index) or @arg_indices.include?(i)
45
+
46
+ if object.to_q.is_a?(q_class)
47
+ @arg_indices.push(i)
48
+ indices.push(i)
49
+ true
50
+ end
51
+ end
52
+ end
53
+
54
+ # Stores the queue indices of the arguments required by the given
55
+ # method_args_hash in @arg_indices, then returns true. If the queue does not
56
+ # contain sufficient objects to fulfill the method argument requirements,
57
+ # returns false.
58
+ #
59
+ def args? (method_args_hash)
60
+ left_indices = []
61
+ splat_indices = []
62
+ right_indices = []
63
+ queue = @scope[:queue]
64
+
65
+ return false unless method_args_hash[:reqs_left].all? do |q_class|
66
+ queue.each_with_index.any? &arg_search_block(q_class, left_indices)
67
+ end
68
+
69
+ return false unless method_args_hash[:reqs_right].all? do |q_class|
70
+ queue.each_with_index.reverse_each.any? &arg_search_block(q_class, right_indices)
71
+ end
72
+
73
+ if q_class = method_args_hash[:splat]
74
+ queue.each_with_index &arg_search_block(q_class, splat_indices)
75
+ end
76
+
77
+ @arg_indices = left_indices + splat_indices + right_indices
78
+ true
79
+ end
80
+
81
+ # Returns a Proc object to be used when calling QObject methods with required
82
+ # block arguments. The returned Proc uses two optional parameters: the first
83
+ # is a single argument object (default is nil) that will be associated with
84
+ # the last unassigned variable name appearing before the block in the script;
85
+ # the second is the Q_Environment in which to evaluate the block Node embedded
86
+ # in the Proc object (default is the Q_Environment that created the Proc).
87
+ #
88
+ def block_arg (block_node)
89
+ return unless block_node
90
+
91
+ parameter_name = @scope[:unassigned_variables].pop
92
+
93
+ lambda do |parameter_object = nil, env = self|
94
+ env.instance_eval do
95
+ if has_old_value = @variables.has_key?(parameter_name)
96
+ old_value = @variables[parameter_name]
97
+ end
98
+
99
+ set(parameter_name, parameter_object)
100
+
101
+ b, j = @scope[:break], @scope[:jump]
102
+
103
+ @scope = { queue: [], method_stack: [], unassigned_variables: [], prev: @scope }
104
+
105
+ return_value = evaluate(block_node)
106
+ b = @scope[:break] || b
107
+ j = @scope[:jump] || j
108
+
109
+ @scope = @scope[:prev]
110
+
111
+ @scope[:break] = b
112
+ @scope[:jump] = j
113
+
114
+ has_old_value ? set(parameter_name, old_value) : unset(parameter_name)
115
+
116
+ return_value
117
+ end
118
+ end
119
+ end
120
+
121
+ # Returns true if a break flag is set in the current scope, nil otherwise.
122
+ #
123
+ def break?
124
+ @scope[:break]
125
+ end
126
+
127
+ # Sets a jump flag in the current scope and a break flag in the previous
128
+ # scope.
129
+ #
130
+ def break!
131
+ jump!
132
+ @scope[:prev][:break] = true
133
+ end
134
+
135
+ # Evaluates the Node. For a literal Node, calls queue_push with the literal
136
+ # object. For a method Node, calls method_push with the method name. For a
137
+ # variable node, calls variable_push with the variable name. For a block Node,
138
+ # first tries to run any pending method that would succeed with a block
139
+ # argument; if no method succeeds, adds a new scope level and evaluates each
140
+ # Node within the block in that scope. Returns the result of calling
141
+ # queue_push with either the method result or with the the frontmost object
142
+ # from the nested scope's queue.
143
+ #
144
+ def evaluate (node)
145
+ raise TooManyNodes if @nodes_evaluated[:total] >= @max_nodes
146
+
147
+ @nodes_evaluated[node.node_type] += 1
148
+ @nodes_evaluated[:total] += 1
149
+
150
+ case node.node_type
151
+ when :literal then queue_push(node.value)
152
+ when :method then method_push(node.value)
153
+ when :variable then variable_push(node.value)
154
+ when :block
155
+ if method?(node)
156
+ return queue_push(@method_results.pop)
157
+ end
158
+
159
+ @scope = { queue: [], method_stack: [], unassigned_variables: [], prev: @scope }
160
+
161
+ node.value.each do |node|
162
+ evaluate node
163
+ break if @scope[:jump]
164
+ end
165
+
166
+ return_value = @scope[:queue].shift
167
+
168
+ @scope[:method_stack].each {|name| @failed_methods[name] += 1 }
169
+ @scope[:queue].each {|obj| @discarded_objects[obj.class] += 1 }
170
+ @scope = @scope[:prev]
171
+
172
+ return queue_push(return_value)
173
+ end
174
+ end
175
+
176
+ # Returns the result of calling the given block with the given object as the
177
+ # value of self.
178
+ #
179
+ def frame (object, &block)
180
+ @frame_stack.push(object)
181
+ result = yield
182
+ @frame_stack.pop
183
+ result
184
+ end
185
+
186
+ # • User method
187
+ # Returns the object associated with the given variable name, or nil if there
188
+ # is no such object.
189
+ #
190
+ def get (name)
191
+ @variables[name]
192
+ end
193
+
194
+ # Sets a jump flag in the current scope.
195
+ #
196
+ def jump!
197
+ @scope[:jump] = true
198
+ end
199
+
200
+ # Returns the name of the last variable used in the current scope that was
201
+ # associated with an object, or nil if no such variable exists.
202
+ #
203
+ def last_assigned_variable
204
+ @scope[:last_assigned_variable]
205
+ end
206
+
207
+ # Pushes the result of calling the top method in the pending methods stack
208
+ # using the combination of sufficient receiver and arguments found frontmost
209
+ # in the queue into @method_results, then returns true. If no receiver or
210
+ # insufficient arguments are found in the queue, returns false. If a
211
+ # block_node argument is given, attempts to call QObject methods requiring a
212
+ # block argument.
213
+ #
214
+ def method? (block_node = nil)
215
+ return false unless name = @scope[:method_stack].last
216
+
217
+ q_receiver = nil
218
+ potential_receivers = @scope[:queue] + @implicit + [QImplicit.new(@scope[:queue])]
219
+
220
+ potential_receivers.each_with_index do |potential_receiver, receiver_i|
221
+ @receiver_index = receiver_i
222
+ q_potential_receiver = potential_receiver.to_q
223
+
224
+ next unless q_potential_receiver.respond_to?(name)
225
+
226
+ method_args_hash = q_potential_receiver.method(name).owner::MethodArguments[name]
227
+
228
+ next if method_args_hash[:block] && !block_node
229
+
230
+ @arg_indices = []
231
+
232
+ if args?(method_args_hash)
233
+ q_receiver = q_potential_receiver
234
+ break
235
+ end
236
+ end
237
+
238
+ return false unless q_receiver
239
+
240
+ @method_results << q_send(q_receiver, block_node)
241
+
242
+ true
243
+ end
244
+
245
+ # Pops the top method name from the pending method stack, and increments that
246
+ # method's success count by 1.
247
+ #
248
+ def method_pop
249
+ name = @scope[:method_stack].pop
250
+ @successful_methods[name] += 1
251
+ name
252
+ end
253
+
254
+ # If the given method succeeds, calls queue_push with the result; otherwise,
255
+ # adds the given method name to the top of the pending method stack.
256
+ #
257
+ def method_push (name)
258
+ @scope[:method_stack].push(name)
259
+ queue_push(@method_results.pop) if method?
260
+ end
261
+
262
+ # • User method
263
+ # Returns the outermost frame object for this Q_Environment.
264
+ #
265
+ def object
266
+ @frame_stack.first
267
+ end
268
+
269
+ # Returns the sanitized result of calling the current method with the given
270
+ # q_receiver, the given block_node as its block arg, and the objects in the
271
+ # queue at the indices currently stored in @arg_indices as its arguments.
272
+ #
273
+ def q_send (q_receiver, block_node)
274
+ arg_objects = @arg_indices.map {|i| @scope[:queue][i] }
275
+ indices = @arg_indices + [@receiver_index]
276
+ @scope[:queue].reject!.each_with_index {|x,i| indices.include? i }
277
+
278
+ q_receiver.instance_variable_set(:@__environment__, self)
279
+ sanitize(q_receiver.__send__(method_pop, *arg_objects, &block_arg(block_node)))
280
+ end
281
+
282
+ # Adds the given object to the end of the queue. Associates all unassigned
283
+ # variables with the object, then tests whether any pending methods succeed
284
+ # with the new object in the queue. If so, calls queue_push again with the
285
+ # result of the successful method; otherwise, returns the given object.
286
+ #
287
+ def queue_push (object)
288
+ @scope[:queue] << object
289
+ @scope[:unassigned_variables].each {|v| set(v, object) }.clear
290
+
291
+ method? ? queue_push(@method_results.pop) : object
292
+ end
293
+
294
+ # Returns a new Q_Environment referencing the same variables, object, and
295
+ # implicit receivers as this Q_Environment, but with new runtime statistics,
296
+ # for use in executing methods outside the context of the original Q_Device.
297
+ #
298
+ def replicate
299
+ options = [@max_nodes, @max_method_nodes, @max_array_length, @max_hash_length, @max_string_length]
300
+ Q_Environment.new(@variables, *@implicit, @object, options)
301
+ end
302
+
303
+ # • User method
304
+ # Returns the return value of the outermost block.
305
+ #
306
+ def return_value
307
+ @scope[:queue].first
308
+ end
309
+
310
+ # If the given object is an Array, Hash, or String, returns the object
311
+ # shortened to the maximum length; otherwise, returns the object unchanged.
312
+ #
313
+ def sanitize (obj)
314
+ case obj
315
+ when Array then obj.slice!(0...max_array_length) if obj.length > @max_array_length
316
+ when Hash then obj.pop while obj.length > @max_hash_length
317
+ when String then obj.slice!(0...max_string_length) if obj.length > @max_string_length
318
+ end
319
+
320
+ obj
321
+ end
322
+
323
+ # Returns the value of self for the current frame.
324
+ #
325
+ def self
326
+ @frame_stack.last
327
+ end
328
+
329
+ # • User method
330
+ # Associates the given variable name with the given object, then returns the
331
+ # object.
332
+ #
333
+ def set (name, object)
334
+ return unless name
335
+ @variables[name] = object
336
+ end
337
+
338
+ # Unsets the named variable, then returns the object previously associated
339
+ # with the variable.
340
+ #
341
+ def unset (name)
342
+ @variables.delete(name)
343
+ end
344
+
345
+ # If there is an object already associated with the given variable name, marks
346
+ # that variable name as the last assigned variable, and then calls queue_push
347
+ # with that object. Otherwise, adds the variable name to the list of
348
+ # unassigned variables.
349
+ #
350
+ def variable_push (variable)
351
+ if object = get(variable)
352
+ @scope[:last_assigned_variable] = variable
353
+ queue_push(object)
354
+ else
355
+ @scope[:unassigned_variables].push(variable)
356
+ end
357
+ end
358
+ end