q-language 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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