blockly_interpreter 0.2.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 (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