blockly_interpreter 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +4 -0
  4. data/CHANGELOG.md +12 -0
  5. data/COPYING +8 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +91 -0
  8. data/Guardfile +22 -0
  9. data/README.md +72 -0
  10. data/Rakefile +10 -0
  11. data/app/assets/javascripts/blockly_interpreter/extension_blocks.js +1 -0
  12. data/app/assets/javascripts/blockly_interpreter/extension_blocks/dates.js +16 -0
  13. data/app/assets/javascripts/blockly_interpreter/extension_blocks/debugging.js +17 -0
  14. data/app/assets/javascripts/blockly_interpreter/extension_blocks/lists.js +45 -0
  15. data/app/assets/javascripts/blockly_interpreter/extension_blocks/logic.js +274 -0
  16. data/app/assets/javascripts/blockly_interpreter/extension_blocks/text.js.erb +21 -0
  17. data/bin/_guard-core +16 -0
  18. data/bin/console +14 -0
  19. data/bin/guard +16 -0
  20. data/bin/rake +16 -0
  21. data/bin/setup +7 -0
  22. data/blockly_interpreter.gemspec +33 -0
  23. data/exe/blocklyi +13 -0
  24. data/exe/rublocklyc +31 -0
  25. data/lib/blockly_interpreter.rb +25 -0
  26. data/lib/blockly_interpreter/block.rb +113 -0
  27. data/lib/blockly_interpreter/block_library.rb +14 -0
  28. data/lib/blockly_interpreter/console_interpreter.rb +15 -0
  29. data/lib/blockly_interpreter/core_blocks.rb +55 -0
  30. data/lib/blockly_interpreter/core_blocks/arithmetic_operator_block.rb +24 -0
  31. data/lib/blockly_interpreter/core_blocks/boolean_block.rb +23 -0
  32. data/lib/blockly_interpreter/core_blocks/comparison_operator_block.rb +50 -0
  33. data/lib/blockly_interpreter/core_blocks/for_block.rb +68 -0
  34. data/lib/blockly_interpreter/core_blocks/for_each_block.rb +42 -0
  35. data/lib/blockly_interpreter/core_blocks/get_variable_block.rb +19 -0
  36. data/lib/blockly_interpreter/core_blocks/if_block.rb +105 -0
  37. data/lib/blockly_interpreter/core_blocks/lists_create_empty_block.rb +17 -0
  38. data/lib/blockly_interpreter/core_blocks/lists_create_with_block.rb +48 -0
  39. data/lib/blockly_interpreter/core_blocks/lists_get_index_block.rb +95 -0
  40. data/lib/blockly_interpreter/core_blocks/logic_negate_block.rb +20 -0
  41. data/lib/blockly_interpreter/core_blocks/logical_operator_block.rb +22 -0
  42. data/lib/blockly_interpreter/core_blocks/number_block.rb +23 -0
  43. data/lib/blockly_interpreter/core_blocks/procedure_block.rb +49 -0
  44. data/lib/blockly_interpreter/core_blocks/procedures_call_no_return_block.rb +21 -0
  45. data/lib/blockly_interpreter/core_blocks/procedures_call_return_block.rb +21 -0
  46. data/lib/blockly_interpreter/core_blocks/procedures_def_no_return_block.rb +31 -0
  47. data/lib/blockly_interpreter/core_blocks/procedures_def_return_block.rb +64 -0
  48. data/lib/blockly_interpreter/core_blocks/procedures_if_return_block.rb +45 -0
  49. data/lib/blockly_interpreter/core_blocks/repeat_times_block.rb +33 -0
  50. data/lib/blockly_interpreter/core_blocks/set_variable_block.rb +24 -0
  51. data/lib/blockly_interpreter/core_blocks/text_block.rb +23 -0
  52. data/lib/blockly_interpreter/core_blocks/text_change_case_block.rb +32 -0
  53. data/lib/blockly_interpreter/core_blocks/text_join_block.rb +50 -0
  54. data/lib/blockly_interpreter/dsl.rb +291 -0
  55. data/lib/blockly_interpreter/dsl_generator.rb +147 -0
  56. data/lib/blockly_interpreter/engine.rb +8 -0
  57. data/lib/blockly_interpreter/execution_context.rb +72 -0
  58. data/lib/blockly_interpreter/extension_blocks.rb +25 -0
  59. data/lib/blockly_interpreter/extension_blocks/date_today_block.rb +17 -0
  60. data/lib/blockly_interpreter/extension_blocks/debug_message_block.rb +18 -0
  61. data/lib/blockly_interpreter/extension_blocks/lists_append_block.rb +34 -0
  62. data/lib/blockly_interpreter/extension_blocks/lists_concat_block.rb +37 -0
  63. data/lib/blockly_interpreter/extension_blocks/lists_include_operator_block.rb +52 -0
  64. data/lib/blockly_interpreter/extension_blocks/object_present_block.rb +21 -0
  65. data/lib/blockly_interpreter/extension_blocks/switch_block.rb +107 -0
  66. data/lib/blockly_interpreter/extension_blocks/text_inflect_block.rb +27 -0
  67. data/lib/blockly_interpreter/generic_block_dsl_generator.rb +64 -0
  68. data/lib/blockly_interpreter/interpreter.rb +25 -0
  69. data/lib/blockly_interpreter/parser.rb +117 -0
  70. data/lib/blockly_interpreter/program.rb +51 -0
  71. data/lib/blockly_interpreter/program_cache.rb +19 -0
  72. data/lib/blockly_interpreter/test_helper.rb +98 -0
  73. data/lib/blockly_interpreter/version.rb +3 -0
  74. metadata +272 -0
@@ -0,0 +1,291 @@
1
+ require 'nokogiri'
2
+
3
+ module BlocklyInterpreter::DSL
4
+ class BlockContext
5
+ attr_reader :blocks, :procedure_blocks
6
+
7
+ def self.register_block_class(block_class)
8
+ if block_class.const_defined?(:DSLMethods)
9
+ self.include block_class.const_get(:DSLMethods)
10
+ end
11
+ end
12
+
13
+ def initialize
14
+ @blocks = []
15
+ @procedure_blocks = []
16
+ end
17
+
18
+ def block(block_type, &proc)
19
+ @blocks << BlockBuilder.new(block_type).tap do |builder|
20
+ builder.instance_exec(&proc) if proc
21
+ end
22
+ end
23
+
24
+ def with_comment(comment, pinned = false, &proc)
25
+ instance_exec(&proc)
26
+ @blocks.last.tap { |block| block.set_comment(comment, pinned) }
27
+ end
28
+
29
+ def shadow(&proc)
30
+ instance_exec(&proc)
31
+ @blocks.last.tap { |block| block.shadow! }
32
+ end
33
+
34
+ def with_position(x, y, &proc)
35
+ instance_exec(&proc)
36
+ @blocks.last.tap { |block| block.set_position!(x, y) }
37
+ end
38
+
39
+ def procedures(&proc)
40
+ procedures_context = BlockContext.new.tap do |builder|
41
+ builder.instance_exec(&proc) if proc
42
+ end
43
+
44
+ @procedure_blocks.push(*procedures_context.blocks)
45
+ end
46
+
47
+ def to_xml(doc, block_index = 0)
48
+ return unless blocks.any?
49
+
50
+ block = blocks[block_index]
51
+ Nokogiri::XML::Node.new(block.tag_name, doc).tap do |node|
52
+ blocks[block_index].to_xml(node)
53
+
54
+ if blocks.size > block_index + 1
55
+ next_node = Nokogiri::XML::Node.new('next', doc)
56
+ next_node.add_child to_xml(doc, block_index + 1)
57
+ node.add_child(next_node)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ class BlockBuilder
64
+ attr_reader :block_type, :values, :statements, :fields, :mutation_attrs, :mutation_child_procs, :comment, :comment_pinned, :is_shadow, :x, :y
65
+
66
+ def initialize(block_type)
67
+ @block_type = block_type
68
+ @values = {}
69
+ @statements = {}
70
+ @fields = {}
71
+ @mutation_attrs = {}
72
+ @mutation_child_procs = []
73
+ @comment = nil
74
+ @comment_pinned = nil
75
+ @is_shadow = false
76
+ @x = nil
77
+ @y = nil
78
+ end
79
+
80
+ def to_xml(node)
81
+ node['type'] = block_type
82
+ document = node.document
83
+
84
+ node['x'] = x if x
85
+ node['y'] = y if y
86
+
87
+ node.add_child(mutations_to_xml(document)) if mutation_attrs.any? || mutation_child_procs.any?
88
+ node.add_child(comment_to_xml(document)) if comment.present?
89
+
90
+ values.each do |name, context|
91
+ node.add_child subblock_to_xml('value', name, context, document)
92
+ end
93
+
94
+ statements.each do |name, context|
95
+ node.add_child subblock_to_xml('statement', name, context, document)
96
+ end
97
+
98
+ fields.each do |name, value|
99
+ node.add_child field_to_xml(name, value, document)
100
+ end
101
+ end
102
+
103
+ def subblock_to_xml(tag_name, subblock_name, context, doc)
104
+ Nokogiri::XML::Node.new(tag_name, doc).tap do |node|
105
+ node['name'] = subblock_name
106
+ node.add_child context.to_xml(doc)
107
+ end
108
+ end
109
+
110
+ def mutations_to_xml(document)
111
+ Nokogiri::XML::Node.new('mutation', document).tap do |mutation_node|
112
+ mutation_attrs.each do |name, value|
113
+ mutation_node[name] = value.to_s
114
+ end
115
+
116
+ mutation_child_procs.each do |(tag_name, proc)|
117
+ child = Nokogiri::XML::Node.new(tag_name.to_s, document)
118
+ proc.call(child)
119
+ mutation_node.add_child child
120
+ end
121
+ end
122
+ end
123
+
124
+ def field_to_xml(name, value, document)
125
+ Nokogiri::XML::Node.new('field', document).tap do |field_node|
126
+ field_node['name'] = name
127
+ field_node.add_child Nokogiri::XML::Text.new(value.to_s, document)
128
+ end
129
+ end
130
+
131
+ def comment_to_xml(document)
132
+ Nokogiri::XML::Node.new('comment', document).tap do |comment_node|
133
+ comment_node.content = comment
134
+ comment_node['pinned'] = comment_pinned.inspect unless comment_pinned.nil?
135
+ end
136
+ end
137
+
138
+ def build_subblock(&proc)
139
+ BlockContext.new.tap do |builder|
140
+ builder.instance_exec(&proc)
141
+ end
142
+ end
143
+
144
+ def value(name, &proc)
145
+ @values[name.to_s] = build_subblock(&proc)
146
+ end
147
+
148
+ def statement(name, &proc)
149
+ @statements[name.to_s] = build_subblock(&proc)
150
+ end
151
+
152
+ def field(name, value)
153
+ @fields[name.to_s] = value
154
+ end
155
+
156
+ def mutation_attr(name, value)
157
+ @mutation_attrs[name.to_s] = value
158
+ end
159
+
160
+ def mutation_child(tag_name, &proc)
161
+ @mutation_child_procs << [tag_name, proc]
162
+ end
163
+
164
+ def set_comment(comment, pinned = false)
165
+ @comment = comment
166
+ @comment_pinned = pinned
167
+ end
168
+
169
+ def set_position!(x, y)
170
+ @x = x
171
+ @y = y
172
+ end
173
+
174
+ def tag_name
175
+ if is_shadow
176
+ 'shadow'
177
+ else
178
+ 'block'
179
+ end
180
+ end
181
+
182
+ def shadow!
183
+ @is_shadow = true
184
+ end
185
+ end
186
+
187
+ class BinaryOperationBlockBuilder < BlockBuilder
188
+ def initialize(block_type, op, a = nil, b = nil)
189
+ super block_type
190
+ field :OP, op
191
+
192
+ a(a) unless a.nil?
193
+ b(b) unless b.nil?
194
+ end
195
+
196
+ def cast_static_value_proc(static_value)
197
+ case static_value
198
+ when Numeric then -> { math_number(static_value) }
199
+ when String then -> { text(static_value) }
200
+ when true, false then -> { logic_boolean(static_value) }
201
+ end
202
+ end
203
+
204
+ def value_with_static_option(name, static_value = nil, &proc)
205
+ proc ||= cast_static_value_proc(static_value)
206
+ value name, &proc
207
+ end
208
+
209
+ def a(static_value = nil, &proc)
210
+ value_with_static_option :A, static_value, &proc
211
+ end
212
+
213
+ def b(static_value = nil, &proc)
214
+ value_with_static_option :B, static_value, &proc
215
+ end
216
+ end
217
+
218
+ class BinaryOperationDSLGenerator
219
+ include BlocklyInterpreter::DSLGenerator
220
+
221
+ attr_reader :block, :dsl_method_name
222
+
223
+ def initialize(block, dsl_method_name)
224
+ @block = block
225
+ @dsl_method_name = dsl_method_name
226
+ end
227
+
228
+ def castable_static_value(value)
229
+ case value
230
+ when BlocklyInterpreter::CoreBlocks::NumberBlock then value.fields['NUM'].to_i
231
+ when BlocklyInterpreter::CoreBlocks::BooleanBlock then value.to_bool
232
+ when BlocklyInterpreter::CoreBlocks::TextBlock then value.fields['TEXT']
233
+ end
234
+ end
235
+
236
+ def cast_operands
237
+ @cast_operands ||= begin
238
+ a, b = ['A', 'B'].map { |value_name| block.values[value_name] }
239
+ [a, b].map { |value| castable_static_value value }
240
+ end
241
+ end
242
+
243
+ def value_block(value_name, cast_operand)
244
+ return if cast_operand
245
+ method_call_with_block_or_nothing(value_name.downcase, '', block.values[value_name])
246
+ end
247
+
248
+ def block_contents
249
+ cast_a, cast_b = cast_operands
250
+ [
251
+ value_block('A', cast_a),
252
+ value_block('B', cast_b)
253
+ ].compact
254
+ end
255
+
256
+ def method_args
257
+ args = [block.fields['OP'].to_sym.inspect]
258
+ cast_a, cast_b = cast_operands
259
+
260
+ args << cast_a.inspect if cast_a || cast_b
261
+ args << cast_b.inspect if cast_b
262
+
263
+ args
264
+ end
265
+
266
+ def dsl
267
+ method_call_with_possible_block(dsl_method_name, method_args.join(', '), block_contents)
268
+ end
269
+ end
270
+
271
+ def self.build_xml(&block)
272
+ doc = Nokogiri::XML::Document.new
273
+ root = Nokogiri::XML::Node.new('xml', doc)
274
+ root['xmlns'] = 'http://www.w3.org/1999/xhtml'
275
+ doc.add_child root
276
+
277
+ context = BlockContext.new
278
+ context.instance_exec(&block)
279
+ first_block = context.to_xml(doc)
280
+ root.add_child(first_block) if first_block
281
+
282
+ context.procedure_blocks.each do |proc|
283
+ Nokogiri::XML::Node.new(proc.tag_name, doc).tap do |node|
284
+ proc.to_xml(node)
285
+ root.add_child(node)
286
+ end
287
+ end
288
+
289
+ doc.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::NO_DECLARATION | Nokogiri::XML::Node::SaveOptions::NO_EMPTY_TAGS)
290
+ end
291
+ end
@@ -0,0 +1,147 @@
1
+ module BlocklyInterpreter::DSLGenerator
2
+ # Calls #flatten on an enumerable object recursively. Any flattenable things inside obj will also be flattened.
3
+ def deep_flatten(obj)
4
+ if obj.respond_to?(:flatten)
5
+ obj.flatten.map { |item| deep_flatten(item) }
6
+ else
7
+ obj
8
+ end
9
+ end
10
+
11
+ # Given a piece of code as a string, removes any trailing whitespace from each line.
12
+ def strip_trailing_whitespace(code)
13
+ code.gsub(/[ \t]+$/, '')
14
+ end
15
+
16
+ # Given a piece of code, indents it by `level` spaces.
17
+ #
18
+ # indent can accept code as a string (obviously), but can also accept a BlocklyInterpreter::Block, which will
19
+ # automatically be converted to a DSL string and then indented. It can also accept Arrays consisting of strings,
20
+ # BlocklyInterpreter::Blocks, other arrays, and nil, which will be concatenated and indented.
21
+ def indent(code, level = 2)
22
+ lines = case code
23
+ when String then code.split("\n")
24
+ when BlocklyInterpreter::Block then start_block_to_dsl(code).split("\n")
25
+ when Array then deep_flatten(code).compact.flat_map { |code| code.split("\n") }
26
+ else code
27
+ end
28
+
29
+ lines.map { |line| (" " * level) + line }.join("\n")
30
+ end
31
+
32
+ # Generates a Ruby method call given a method name and an arguments string. If `parens` is true, the arguments
33
+ # will be wrapped in parentheses; otherwise, the parentheses will be omitted.
34
+ def method_call(method_name, args, parens = false)
35
+ code = if parens
36
+ method_name.dup.tap do |ruby|
37
+ ruby << "(#{args})" if args.present?
38
+ end
39
+ else
40
+ [method_name, args].map(&:presence).compact.join(" ")
41
+ end
42
+
43
+ strip_trailing_whitespace(code)
44
+ end
45
+
46
+ # Generates a Ruby method call with a block. If `block_contents` is missing, generates the method call with the
47
+ # block omitted. See `method_call_with_block_or_nothing` for argument descriptions.
48
+ def method_call_with_possible_block(method_name, args, block_contents, *block_arg_names)
49
+ method_call_with_block_or_nothing(method_name, args, block_contents, *block_arg_names) || method_call(method_name, args)
50
+ end
51
+
52
+ # Generates a Ruby method call with a block. If `block_contents` is missing, this method will return nil.
53
+ #
54
+ # Arguments:
55
+ # * `method_name` - the name of the method, as a string
56
+ # * `args` - an arguments string for the method call, or nil if there are no arguments
57
+ # * `block_contents` - an indentable object (i.e. a string, BlocklyInterpreter::Block, or array thereof)
58
+ # * `block_arg_names` - an arguments string for the argument names that will be passed to the block
59
+ #
60
+ # Example:
61
+ #
62
+ # > puts method_call_with_block_or_nothing("each", nil, "puts i + 3", "i")
63
+ # each do |i|
64
+ # puts i + 3
65
+ # end
66
+ def method_call_with_block_or_nothing(method_name, args, block_contents, *block_arg_names)
67
+ return unless block_contents.present?
68
+
69
+ block_args = if block_arg_names.present?
70
+ " |#{block_arg_names.join(", ")}|"
71
+ else
72
+ ""
73
+ end
74
+
75
+ unindented_contents = indent(block_contents, 0)
76
+ code = if unindented_contents.size < 40 && !(unindented_contents =~ /\n/)
77
+ "#{method_call(method_name, args, true)} {#{block_args} #{unindented_contents} }"
78
+ else
79
+ <<-DSL
80
+ #{method_call(method_name, args)} do#{block_args}
81
+ #{indent block_contents}
82
+ end
83
+ DSL
84
+ end
85
+
86
+ strip_trailing_whitespace(code)
87
+ end
88
+
89
+ # Given a hash of keyword arguments to a method and a second hash containing that method's default arguments,
90
+ # returns a hash containing all the `keyword_args` omitting the ones that are already the method's default value
91
+ # for that argument.
92
+ #
93
+ # Example:
94
+ #
95
+ # > keyword_args_without_defaults({ field_type: 'radio', placeholder_text: 'Enter an answer' }, { field_type: 'default', placeholder_text: 'Enter an answer' })
96
+ # -> { field_type: 'radio' }
97
+ def keyword_args_without_defaults(keyword_args, default_args)
98
+ keyword_args.reject { |key, value| default_args[key] == value }
99
+ end
100
+
101
+ # Given a hash of keyword arguments for a method call, returns a string containing those arguments as they should
102
+ # be passed to the method.
103
+ def formatted_keyword_args(keyword_args)
104
+ keyword_args.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
105
+ end
106
+
107
+ # Given a BlocklyInterpreter::Block, returns a DSL string that will generate that block.
108
+ def start_block_to_dsl(block)
109
+ return "" unless block
110
+
111
+ block_dsls = block.each_block(false).map do |block|
112
+ dsl_content = block.to_dsl
113
+
114
+ if block.has_comment?
115
+ dsl_content = method_call_with_possible_block(
116
+ "with_comment",
117
+ [block.comment, block.comment_pinned].compact.map(&:inspect).join(", "),
118
+ dsl_content
119
+ )
120
+ end
121
+
122
+ if block.is_shadow?
123
+ dsl_content = method_call_with_possible_block("shadow", "", dsl_content)
124
+ end
125
+
126
+ if block.has_position?
127
+ dsl_content = method_call_with_possible_block("with_position", [block.x, block.y].map(&:inspect).join(", "), dsl_content)
128
+ end
129
+
130
+ dsl_content
131
+ end
132
+ block_dsls.join("\n")
133
+ end
134
+
135
+ # Given a Time object (or nil), generates a Ruby string representation of that object.
136
+ #
137
+ # Example:
138
+ #
139
+ # > puts timestamp_to_dsl(Time.now)
140
+ # Time.utc(2016, 3, 31, 11, 54, 24, 0, 0)
141
+ def timestamp_to_dsl(time)
142
+ return "nil" unless time
143
+
144
+ args = [time.year, time.month, time.day, time.hour, time.min, time.sec, time.usec].map(&:inspect).join(", ")
145
+ "Time.utc(#{args})"
146
+ end
147
+ end
@@ -0,0 +1,8 @@
1
+ module BlocklyInterpreter
2
+ class Engine < ::Rails::Engine
3
+ engine_name 'blockly_interpreter'
4
+ isolate_namespace BlocklyInterpreter
5
+
6
+ config.autoload_paths += Dir["#{config.root}/lib/**/"]
7
+ end
8
+ end
@@ -0,0 +1,72 @@
1
+ require 'logger'
2
+
3
+ class BlocklyInterpreter::ExecutionContext
4
+ attr_reader :interpreter, :early_return_value, :terminated, :debug_messages
5
+
6
+ def initialize(interpreter)
7
+ @interpreter = interpreter
8
+ @variables = {}
9
+ @early_return_value = nil
10
+ @terminated = false
11
+ @debug_messages = []
12
+ end
13
+
14
+ def get_variable(name)
15
+ @variables[name.to_s.upcase]
16
+ end
17
+
18
+ def set_variable(name, value)
19
+ @variables[name.to_s.upcase] = value
20
+ end
21
+
22
+ def set_variables(hash)
23
+ hash.each do |name, value|
24
+ set_variable(name, value)
25
+ end
26
+ end
27
+
28
+ def merge_state_from(context)
29
+ end
30
+
31
+ def execute(block)
32
+ current_block = block
33
+
34
+ while current_block && !terminated
35
+ current_block.execute_statement(self)
36
+ current_block = current_block.next_block
37
+ end
38
+ end
39
+
40
+ def early_return!(value = nil)
41
+ @terminated = true
42
+ @early_return_value = value
43
+ end
44
+
45
+ def execute_procedure(name, parameters)
46
+ procedure_block = interpreter.program.procedures[name]
47
+
48
+ interpreter.build_execution_context.tap do |proc_context|
49
+ proc_context.set_variables(@variables.merge(procedure_block.arg_hash(parameters)))
50
+ proc_context.execute(procedure_block)
51
+ merge_state_from(proc_context)
52
+ end
53
+ end
54
+
55
+ def value_for_procedure(name, parameters)
56
+ procedure_block = interpreter.program.procedures[name]
57
+
58
+ proc_context = interpreter.build_execution_context
59
+ proc_context.set_variables(@variables.merge(procedure_block.arg_hash(parameters)))
60
+
61
+ procedure_block.value(proc_context).tap do |value|
62
+ merge_state_from(proc_context)
63
+ end
64
+ end
65
+
66
+ def add_debug_message(message)
67
+ if message.present?
68
+ Logger.new(STDERR).debug message
69
+ debug_messages << message
70
+ end
71
+ end
72
+ end