explain 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use rbx-head@explain --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in explain.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Josep M. Bach
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,69 @@
1
+ # explain
2
+
3
+ Explain explains your Ruby code in natural language. It is intended to be a
4
+ tool for beginners who aren't yet very familiar with programming.
5
+
6
+ It is a work in progress (a bit rough on the edges), so don't be mad. It will
7
+ get better over time ;)
8
+
9
+ (Explain runs only on Rubinius.)
10
+
11
+ ## Installation
12
+
13
+ Install Rubinius if you don't have it yet:
14
+
15
+ $ rvm install rbx-head
16
+ $ rvm use rbx-head
17
+
18
+ Install explain as a gem:
19
+
20
+ $ gem install explain
21
+
22
+ ## Usage
23
+
24
+ Given some example Ruby code, for example this in `examples/person.rb`:
25
+
26
+ ```ruby
27
+ class Person
28
+ def walk(distance)
29
+ @distance += distance
30
+ @hunger += 2
31
+ end
32
+
33
+ def eat(food)
34
+ @hunger -= food.nutritional_value
35
+ end
36
+ end
37
+ ```
38
+
39
+ We execute `explain` from the command line:
40
+
41
+ $ explain examples/person.rb
42
+
43
+ And it will output:
44
+
45
+ Let's describe the general attributes and behavior of any Person.
46
+
47
+ A Person can **walk**, given a specific distance. This is described as
48
+ follows: its distance will be its distance plus what we previously defined as
49
+ `distance`. Finally we return its hunger will be its hunger plus the number
50
+ 2..
51
+
52
+ A Person can **eat**, given a specific food. This is described as follows:
53
+ Finally we return its hunger will be its hunger minus the result of calling
54
+ **nutritional_value** on what we previously defined as `food`..
55
+ And with this we're done describing a Person.
56
+
57
+ ## Contributing
58
+
59
+ 1. Fork it
60
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
61
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
62
+ 4. Push to the branch (`git push origin my-new-feature`)
63
+ 5. Create new Pull Request
64
+
65
+ ## Who's this
66
+
67
+ This was made by [Josep M. Bach (Txus)](http://txustice.me) under the MIT
68
+ license. I'm [@txustice](http://twitter.com/txustice) on twitter (where you
69
+ should probably follow me!).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rbx
2
+
3
+ $: << 'lib'
4
+ require 'explain'
5
+ puts Explain.explain File.read(ARGV.first)
@@ -0,0 +1,10 @@
1
+ class Person
2
+ def walk(distance)
3
+ @distance += distance
4
+ @hunger += 2
5
+ end
6
+
7
+ def eat(food)
8
+ @hunger -= food.nutritional_value
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'explain/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "explain"
8
+ gem.version = Explain::VERSION
9
+ gem.authors = ["Josep M. Bach"]
10
+ gem.email = ["josep.m.bach@gmail.com"]
11
+ gem.description = %q{Explain explains your Ruby code in natural language.}
12
+ gem.summary = %q{Explain explains your Ruby code in natural language.}
13
+ gem.homepage = "https://github.com/txus/explain"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,10 @@
1
+ require "explain/version"
2
+ require "explain/visitor"
3
+
4
+ module Explain
5
+ def self.explain(code)
6
+ visitor = Visitor.new
7
+ visitor.visit(code.to_ast)
8
+ visitor.output
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Explain
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,337 @@
1
+ module Explain
2
+ class Visitor
3
+ def initialize
4
+ @output = []
5
+ end
6
+
7
+ def visit(node, in_sentence=false)
8
+ __send__ node.node_name, node, in_sentence
9
+ end
10
+
11
+ def emit(str)
12
+ @output.push str
13
+ end
14
+
15
+ def output
16
+ @output.join
17
+ end
18
+
19
+ def class(node, in_sentence=false)
20
+ @in_class = node.name.name
21
+ emit "Let's describe the general attributes and behavior of any #{node.name.name}."
22
+ visit node.body
23
+ emit "\nAnd with this we're done describing a #{node.name.name}."
24
+ @in_class = nil
25
+ end
26
+
27
+ def module(node, in_sentence=false)
28
+ @in_class = node.name.name
29
+ emit "Let's describe a collection of behavior that we'll call #{node.name.name}."
30
+ visit node.body
31
+ emit "\nAnd with this we're done describing #{node.name.name}."
32
+ @in_class = nil
33
+ end
34
+
35
+ def empty_body(*)
36
+ # do nothing
37
+ end
38
+
39
+ def class_scope(node, in_sentence=false)
40
+ visit node.body
41
+ end
42
+ alias module_scope class_scope
43
+
44
+ def local_variable_assignment(node, in_sentence=false)
45
+ emit in_sentence ? "w" : "W"
46
+ emit "e define as `#{node.name}` "
47
+ visit(node.value)
48
+ end
49
+
50
+ def local_variable_access(node, in_sentence=false)
51
+ emit "what we previously defined as `#{node.name}`"
52
+ end
53
+
54
+ def instance_variable_assignment(node, in_sentence=false)
55
+ emit in_sentence ? "i" : "I"
56
+ emit "ts #{node.name[1..-1]} will be "
57
+ visit(node.value)
58
+ end
59
+
60
+ def instance_variable_access(node, in_sentence=false)
61
+ emit "its #{node.name[1..-1]}"
62
+ end
63
+
64
+ def fixnum_literal(node, in_sentence=false)
65
+ emit "the number #{node.value}"
66
+ end
67
+
68
+ def float_literal(node, in_sentence=false)
69
+ emit "the number #{node.value}"
70
+ end
71
+
72
+ def string_literal(node, in_sentence=false)
73
+ emit "the word \"#{node.string}\""
74
+ end
75
+
76
+ def symbol_literal(node, in_sentence=false)
77
+ emit "the name `#{node.value}`"
78
+ end
79
+
80
+ def true_literal(node, in_sentence=false)
81
+ emit "the truth"
82
+ end
83
+
84
+ def false_literal(node, in_sentence=false)
85
+ emit "the falsehood"
86
+ end
87
+
88
+ def nil_literal(node, in_sentence=false)
89
+ emit "the nothingness"
90
+ end
91
+
92
+ def empty_array(node, in_sentence=false)
93
+ emit "an empty list"
94
+ end
95
+
96
+ def array_literal(node, in_sentence=false)
97
+ body = node.body
98
+ emit "a list of "
99
+ body.each_with_index do |node, index|
100
+ visit node
101
+ if body.length == index + 2 # last element
102
+ emit " and "
103
+ elsif body.length != index + 1
104
+ emit ", "
105
+ end
106
+ end
107
+ end
108
+
109
+ def hash_literal(node, in_sentence=false)
110
+ return emit "an empty dictionary" if node.array.empty?
111
+
112
+ body = node.array.each_slice(2)
113
+
114
+ emit 'a dictionary where '
115
+ body.each_with_index do |slice, index|
116
+ key, value = slice
117
+
118
+ visit key
119
+ emit " relates to "
120
+ visit value
121
+
122
+ if body.to_a.length == index + 2 # last element
123
+ emit " and "
124
+ elsif body.to_a.length != index + 1
125
+ emit ", "
126
+ end
127
+ end
128
+ end
129
+
130
+ # def range(node, in_sentence=false)
131
+ # end
132
+
133
+ # def range_exclude(node, in_sentence=false)
134
+ # end
135
+
136
+ # def regex_literal(node, in_sentence=false)
137
+ # end
138
+
139
+ def send(node, in_sentence=false)
140
+ if node.receiver.is_a?(Rubinius::AST::Self)
141
+ emit in_sentence ? "the result of calling " : "We "
142
+ emit "**#{node.name}**"
143
+ else
144
+ emit in_sentence ? "the result of calling " : "We call "
145
+ emit "**#{node.name}** on "
146
+ visit node.receiver
147
+ end
148
+ end
149
+
150
+ def send_with_arguments(node, in_sentence=false)
151
+ return if process_binary_operator(node, in_sentence) # 1 * 2, a / 3, true && false
152
+
153
+ if node.receiver.is_a?(Rubinius::AST::Self)
154
+ emit in_sentence ? "the result of calling " : "We "
155
+ emit "**#{node.name}**"
156
+ else
157
+ emit in_sentence ? "the result of calling " : "We call "
158
+ emit "**#{node.name}** on "
159
+ visit node.receiver
160
+ end
161
+
162
+ unless node.arguments.array.empty?
163
+ emit " given "
164
+ visit(node.arguments)
165
+ end
166
+ end
167
+
168
+ def actual_arguments(node, in_sentence=false)
169
+ body = node.array
170
+ body.each_with_index do |node, index|
171
+ visit node
172
+ if body.length == index + 2 # last element
173
+ emit " and "
174
+ elsif body.length != index + 1
175
+ emit ", "
176
+ end
177
+ end
178
+ end
179
+
180
+ # def iter_arguments(node, in_sentence=false)
181
+ # end
182
+
183
+ # def iter(node, in_sentence=false)
184
+ # end
185
+
186
+ def block(node, in_sentence=false)
187
+ body = node.array
188
+ body.each_with_index do |node, index|
189
+ if body.length == index + 1 # last element
190
+ if @in_method
191
+ emit "Finally we return "
192
+ end
193
+ visit node, true
194
+ emit "."
195
+ else
196
+ visit node, in_sentence
197
+ emit '. '
198
+ end
199
+ end
200
+ end
201
+
202
+ # def not(node, in_sentence=false)
203
+ # end
204
+
205
+ # def and(node, in_sentence=false)
206
+ # end
207
+
208
+ # def or(node, in_sentence=false)
209
+ # end
210
+
211
+ # def op_assign_and(node, in_sentence=false)
212
+ # end
213
+
214
+ # def op_assign_or(node, in_sentence=false)
215
+ # end
216
+
217
+ # def toplevel_constant(node, in_sentence=false)
218
+ # end
219
+
220
+ # def constant_access(node, in_sentence=false)
221
+ # end
222
+
223
+ # def scoped_constant(node, in_sentence=false)
224
+ # end
225
+
226
+ def if(node, in_sentence=false)
227
+ body, else_body = node.body, node.else
228
+ keyword = 'if'
229
+
230
+ if node.body.is_a?(Rubinius::AST::NilLiteral) && !node.else.is_a?(Rubinius::AST::NilLiteral)
231
+
232
+ body, else_body = else_body, body
233
+ keyword = 'unless'
234
+ end
235
+
236
+ emit "#{keyword} "
237
+ visit node.condition, true
238
+ emit " is truthy, then "
239
+
240
+ visit body, true
241
+
242
+ if else_body.is_a?(Rubinius::AST::NilLiteral)
243
+ return
244
+ end
245
+
246
+ emit " -- else, "
247
+
248
+ visit else_body, true
249
+ end
250
+
251
+ def while(node, in_sentence=false)
252
+ emit in_sentence ? "w" : "W"
253
+ emit 'hile '
254
+ visit node.condition, true
255
+ emit ", "
256
+ visit node.body, true
257
+ end
258
+
259
+ def until(node, in_sentence=false)
260
+ emit in_sentence ? "u" : "U"
261
+ emit 'ntil '
262
+ visit node.condition, true
263
+ emit ", "
264
+ visit node.body, true
265
+ end
266
+
267
+ def return(node, in_sentence=false)
268
+ emit in_sentence ? "w" : "W"
269
+ emit "e return "
270
+ visit node.value
271
+ end
272
+
273
+ def define(node, in_sentence=false)
274
+ args = enumerate(node.arguments.names)
275
+
276
+ if @in_class
277
+ emit "\n\nA #{@in_class} can **#{node.name}**"
278
+ else
279
+ emit "\n\nLet's define a method: **#{node.name}**"
280
+ end
281
+ if node.arguments.names.empty?
282
+ emit "."
283
+ else
284
+ emit ", given a specific #{args}."
285
+ end
286
+
287
+ unless node.body.array.first.is_a?(Rubinius::AST::NilLiteral) && node.body.array.length == 1
288
+ @in_method = true
289
+ emit " This is described as follows: "
290
+ visit node.body, true
291
+ @in_method = false
292
+ end
293
+ end
294
+
295
+ def method_missing(m, *args, &block)
296
+ emit "(eerr I don't know how to explain `#{m}`)"
297
+ end
298
+
299
+ private
300
+
301
+ def enumerate(ary)
302
+ comma_joinable = ary[0..-2]
303
+ if comma_joinable.empty?
304
+ "#{ary[-1]}"
305
+ else
306
+ [comma_joined.join(', '), ary[-1]].join(' and ')
307
+ end
308
+ end
309
+
310
+ def process_binary_operator(node, in_sentence)
311
+ operators = %w(+ - * / & | << < > >= <= == !=).map(&:to_sym)
312
+ return false unless operators.include?(node.name)
313
+ return false if node.arguments.array.length != 1
314
+
315
+ operand = node.arguments.array[0]
316
+
317
+ unless node.receiver.is_a?(Rubinius::AST::Self)
318
+ visit node.receiver
319
+ end
320
+
321
+ emit case node.name.to_s
322
+ when "+" then " plus "
323
+ when "-" then " minus "
324
+ when "*" then " times "
325
+ when "/" then " divided by "
326
+ when "<" then " is less than "
327
+ when ">" then " is greater than "
328
+ when ">=" then " is greater or equal than "
329
+ when "<=" then " is less or equal than "
330
+ when "==" then " is equal to "
331
+ when "!=" then " is different than "
332
+ end
333
+
334
+ visit operand, true
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,90 @@
1
+ require 'test_helper'
2
+ require 'explain/visitor'
3
+
4
+ module Explain
5
+ describe Visitor do
6
+ let(:code) { "a = 123".to_ast }
7
+ let(:visitor) { Visitor.new }
8
+
9
+ def self.compiles(code, expected)
10
+ it "explains #{code}" do
11
+ visitor.visit(code.to_ast)
12
+ visitor.output.must_equal(expected)
13
+ end
14
+ end
15
+
16
+ # Basic types
17
+ compiles %q{123},
18
+ %q{the number 123}
19
+ compiles %q{12.3},
20
+ %q{the number 12.3}
21
+ compiles %q{:hey},
22
+ %q{the name `hey`}
23
+ compiles %q{"hey"},
24
+ %q{the word "hey"}
25
+
26
+ compiles %q{true},
27
+ %q{the truth}
28
+ compiles %q{false},
29
+ %q{the falsehood}
30
+ compiles %q{nil},
31
+ %q{the nothingness}
32
+
33
+ compiles %q{[]},
34
+ %q{an empty list}
35
+ compiles %q{["hey", 9.9, 123]},
36
+ %q{a list of the word "hey", the number 9.9 and the number 123}
37
+ compiles %q{{}},
38
+ %q{an empty dictionary}
39
+ compiles %q{{:foo => "bar", :baz => 123}},
40
+ %q{a dictionary where the name `foo` relates to the word "bar" and the name `baz` relates to the number 123}
41
+
42
+ # Assignments
43
+ compiles %q{a = 123},
44
+ %q{We define as `a` the number 123}
45
+ compiles %q{name = "John"},
46
+ %q{We define as `name` the word "John"}
47
+
48
+ compiles %q{def ret_name; name = "John"; name; end},
49
+ %Q{\n\nLet's define a method: **ret_name**. This is described as follows: we define as `name` the word "John". Finally we return what we previously defined as `name`.}
50
+ compiles %q{@name},
51
+ %q{its name}
52
+ compiles %q{@name = "John"},
53
+ %q{Its name will be the word "John"}
54
+
55
+ # Classes and modules
56
+ compiles %q{class Person; end},
57
+ %Q{Let's describe the general attributes and behavior of any Person.\nAnd with this we're done describing a Person.}
58
+
59
+ compiles %q{class Person
60
+ def walk(distance)
61
+ @position += distance
62
+ update_hunger 1
63
+ end
64
+ end},
65
+ %Q{Let's describe the general attributes and behavior of any Person.\n\nA Person can **walk**, given a specific distance. This is described as follows: its position will be its position plus what we previously defined as `distance`. Finally we return the result of calling **update_hunger** given the number 1..\nAnd with this we're done describing a Person.}
66
+
67
+ # Control flow
68
+ compiles %q{if true
69
+ false
70
+ else
71
+ :foo
72
+ end},
73
+ %Q{if the truth is truthy, then the falsehood -- else, the name `foo`}
74
+
75
+ compiles %q{number = 0
76
+ while number < 3
77
+ number += 1
78
+ end},
79
+ %Q{We define as `number` the number 0. while what we previously defined as `number` is less than the number 3, we define as `number` what we previously defined as `number` plus the number 1.}
80
+
81
+ compiles %q{number = 0
82
+ until number == 3
83
+ number += 1
84
+ end},
85
+ %Q{We define as `number` the number 0. until what we previously defined as `number` is equal to the number 3, we define as `number` what we previously defined as `number` plus the number 1.}
86
+
87
+ compiles %q{return 3},
88
+ %Q{We return the number 3}
89
+ end
90
+ end
@@ -0,0 +1,2 @@
1
+ require 'minitest/spec'
2
+ require 'minitest/autorun'
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: explain
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Josep M. Bach
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-07 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Explain explains your Ruby code in natural language.
15
+ email:
16
+ - josep.m.bach@gmail.com
17
+ executables:
18
+ - explain
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - .rvmrc
24
+ - Gemfile
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - bin/explain
29
+ - examples/person.rb
30
+ - explain.gemspec
31
+ - lib/explain.rb
32
+ - lib/explain/version.rb
33
+ - lib/explain/visitor.rb
34
+ - test/explain/visitor_test.rb
35
+ - test/test_helper.rb
36
+ homepage: https://github.com/txus/explain
37
+ licenses: []
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ none: false
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ none: false
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 1.8.24
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: Explain explains your Ruby code in natural language.
60
+ test_files:
61
+ - test/explain/visitor_test.rb
62
+ - test/test_helper.rb